Tài liệu hạn chế xem trước, để xem đầy đủ mời bạn chọn Tải xuống
1
/ 30 trang
THÔNG TIN TÀI LIỆU
Thông tin cơ bản
Định dạng
Số trang
30
Dung lượng
391,04 KB
Nội dung
258 CHAPTER 9 ■ SCRIPTING, AI, AND DEPTH (AND DEATH) If you don’t want to flip back but need a really quick refresher, it goes like this: commands are declared and run in Script and parsed in ScriptLine. First, let’s declare our new commands in our enumeration: PlaySound, Ethereal, Solid, Speed, HP, DeathCheck, IfDyingGoto, KillMe, AI We need to parse the new script commands in ScriptLine: case "ethereal": command = Commands.Ethereal; break; case "solid": command = Commands.Solid; break; case "speed": command = Commands.Speed; iParam = Convert.ToInt32(split[1]); break; case "hp": command = Commands.HP; iParam = Convert.ToInt32(split[1]); break; case "deathcheck": command = Commands.DeathCheck; break; case "ifdyinggoto": command = Commands.IfDyingGoto; iParam = Convert.ToInt32(split[1]); break; case "killme": command = Commands.KillMe; break; case "ai": command = Commands.AI; sParam = split[1]; break; Back in Script, we can run the new character script commands. We’ll implement AI next. CHAPTER 9 ■ SCRIPTING, AI, AND DEPTH (AND DEATH) 259 case Commands.Ethereal: character.Ethereal = true; break; case Commands.Solid: character.Ethereal = false; break; case Commands.Speed: character.Speed = (float)line.IParam; break; case Commands.HP: character.HP = character.MHP = line.IParam; break; case Commands.DeathCheck: if (character.HP < 0) { character.KillMe(); } break; case Commands.IfDyingGoto: if (character.HP < 0) { character.SetFrame(line.IParam); done = true; } break; case Commands.KillMe: character.KillMe(); break; case Commands.AI: switch (line.SParam) { case "zombie": character.Ai = new Zombie(); break; default: character.Ai = new Zombie(); break; } break; Adding AI We’re calling it AI for artificial intelligence, but make no mistake—there will be absolutely nothing intelligent about our AI class. We’re basically going to define a list of simple behaviors (chase and attack, evade, stand still, and so on) in a base AI class, and then create monster- specific classes that will decide which behaviors to use and when. 260 CHAPTER 9 ■ SCRIPTING, AI, AND DEPTH (AND DEATH) Making artificial intelligence that looks and feels real is what is important. It doesn’t matter how we do it, as long as the player believes that the zombies act like real zombies. As an inde- pendent game developer, you should start to realize that the quick and hackish way is often enough, and that you do not need a strong core set of AI algorithms just to make a small game. We’ll call the current behavior a “job,” holding the value in the job field for a duration of jobFrame. We’ll keep track of who we’re chasing or fleeing with the targ field—this will allow us to have friendly nonplayable characters (NPCs) in an all-out side-scrolling zombie war, should it come down to it. public class AI { public const int JOB_IDLE = 0; public const int JOB_MELEE_CHASE = 1; public const int JOB_SHOOT_CHASE = 2; public const int JOB_AVOID = 3; protected int job = JOB_IDLE; protected int targ = -1; protected float jobFrame = 0f; protected Character me; In our Update() function, we’ll take the array of characters, ID of the character we’re controlling, and map (it will be nice to know our surroundings, but we won’t be implementing that just yet). We start off by setting all of our character’s keys to false, and then decrement our jobFrame and call DoJob() to . . . well . . . do our job. public virtual void Update(Character[] c, int Id, Map map) { me = c[Id]; me.KeyLeft = false; me.KeyRight = false; me.KeyUp = false; me.KeyDown = false; me.KeyAttack = false; me.KeySecondary = false; me.KeyJump = false; jobFrame -= Game1.FrameTime; DoJob(c, Id); } In DoJob(), we do some case-by-case behavior. protected void DoJob(Character[] c, int Id) CHAPTER 9 ■ SCRIPTING, AI, AND DEPTH (AND DEATH) 261 { switch (job) { case JOB_IDLE: //do nothing! break; For all sorts of chasing and avoiding, we make sure we have a valid (greater than –1) target. If we don’t, we call FindTarg() and get a new one. We also use ChaseTarg() and FaceTarg(), which return false if they’re still working at getting our character within range and facing the correct direction. case JOB_MELEE_CHASE: if (targ > -1) { if (!ChaseTarg(c, 50f)) { if (!FaceTarg(c)) { me. KeyAttack = true; } } } else targ = FindTarg(c); break; case JOB_AVOID: if (targ > -1) { AvoidTarg(c, 500f); } else targ = FindTarg(c); break; case JOB_SHOOT_CHASE: if (targ > -1) { if (!ChaseTarg(c, 150f)) { if (!FaceTarg(c)) { me.KeySecondary = true; } } } 262 CHAPTER 9 ■ SCRIPTING, AI, AND DEPTH (AND DEATH) else targ = FindTarg(c); break; } In this neat little clause at the end, we determine if the character is just running left or right (not attacking). If this is the case, we check to see if there are any friends in the way. If there are, we stop moving. This way, a chasing zombie next to an idle zombie will not keep walking into the guy, which would look kind of silly. if (!me.KeyAttack && !me.KeySecondary) { if (me.KeyLeft) { if (FriendInWay(c, Id, CharDir.Left)) me.KeyLeft = false; } if (me.KeyRight) { if (FriendInWay(c, Id, CharDir.Right)) me.KeyRight = false; } } } All of our helper functions are up next. Basically, they do a lot of spatial comparisons; the code should really speak for itself. protected int FindTarg(Character[] c) { int closest = -1; float d = 0f; for (int i = 0; i < c.Length; i++) { if (i != me.Id) { if (c[i] != null) { if (c[i].Team != me.Team) { float newD = (me.Location – c[i].Location).Length(); if (closest == -1 || newD < d) CHAPTER 9 ■ SCRIPTING, AI, AND DEPTH (AND DEATH) 263 { d = newD; closest = i; } } } } } return closest; } private bool FriendInWay(Character[] c, int Id, CharDir face) { for (int i = 0; i < c.Length; i++) { if (i != Id && c[i] != null) { if (me.Team == c[i].Team) { if (me.Location.Y > c[i].Location.Y - 100f && me.Location.Y < c[i].Location.Y + 10f) { if (face == CharDir.Right) { if (c[i].Location.X > me.Location.X && c[i].Location.X < me.Location.X + 70f) return true; } else { if (c[i].Location.X < me.Location.X && c[i].Location.X > me.Location.X - 70f) return true; } } } } } return false; } ChaseTarg(), AvoidTarg(), and FaceTarg() all return true if the character is in the wrong position, meaning the character is still attempting to chase, avoid, or face its target. When we call these methods, we end up doing what we need to be doing when in the correct position (typically attacking) if everything returns false. We thought this way was intuitive, but if you would prefer to word it differently, go for it! 264 CHAPTER 9 ■ SCRIPTING, AI, AND DEPTH (AND DEATH) protected bool ChaseTarg(Character[] c, float distance) { if (me.Location.X > c[targ].Location.X + distance) { me.KeyLeft = true; return true; } else if (me.Location.X < c[targ].Location.X - distance) { me.KeyRight = true; return true; } return false; } protected bool AvoidTarg(Character[] c, float distance) { if (me.Location.X < c[targ].Location.X + distance) { me.KeyRight = true; return true; } else if (me.Location.X > c[targ].Location.X - distance) { me.KeyLeft = true; return true; } return false; } protected bool FaceTarg(Character[] c) { if (me.Location.X > c[targ].Location.X && me.face == CharDir.Right) { me.KeyLeft = true; return true; } else if (me.Location.X < c[targ].Location.X && me.face == CharDir.Left) { me.KeyRight = true; return true; } return false; } } CHAPTER 9 ■ SCRIPTING, AI, AND DEPTH (AND DEATH) 265 That does it for our AI class for now. We can easily add new behaviors as we create new and more complex monsters—for instance, a boss character that throws axes when its prey is at a certain distance. Our Zombie class, which will extend the AI base class, is much simpler: class Zombie : AI { public override void Update(Character[] c, int Id, Map map) { me = c[Id]; if (jobFrame < 0f) { float r = Rand.GetRandomFloat(0f, 1f); if (r < 0.6f) { job = JOB_MELEE_CHASE; jobFrame = Rand.GetRandomFloat(2f, 4f); targ = FindTarg(c); } else if (r < 0.8f) { job = JOB_AVOID; jobFrame = Rand.GetRandomFloat(1f, 2f); targ = FindTarg(c); } else { job = JOB_IDLE; jobFrame = Rand.GetRandomFloat(.5f, 1f); } } base.Update(c, ID, map); } } The zombie will chase our character, avoid our character, or stand still. This is not exactly groundbreaking behavior, but then again, it’s just a zombie. Dealing Damage We need to add some functionality to HitManager.CheckHit(). We now have ethereal charac- ters that we can’t hit, as well as dying characters that we can’t hit either. In our big series of conditions for checking hit collisions, let’s add another if clause to test for both: for (int i = 0; i < c.Length; i++) { if (i != p.Owner) 266 CHAPTER 9 ■ SCRIPTING, AI, AND DEPTH (AND DEATH) { if (c[i] != null) { if (c[i].DyingFrame < 0f && !c[i].Ethereal) { if (c[i].InHitBounds(p.Location)) Thus far, our HitManager.CheckHit() method doesn’t actually cause any damage—it just checks for successful hits, creates blood splashes, and sets animations. Let’s create a field called hVal that will determine our hit damage. We’ll give hVal a value based on what type of hit it is and then deduct the final damage at the end. If we want to add difficulty levels later, we can scale hVal based on those, too. Also, we’re adding a case for TRIG_ZOMBIE_HIT, our newest hit type. float hVal = 1f; if (typeof(Bullet).Equals(p.GetType())) { if (!r) { hVal *= 4f; } } if (typeof(Hit).Equals(p.GetType())) { switch (p.GetFlag()) { case Character.TRIG_ZOMBIE_HIT: hVal *= 5f; pMan.MakeBloodSplash(p.Location, new Vector2(50f * tX, 100f)); break; case Character.TRIG_WRENCH_DIAG_DOWN: hVal *= 5f; case Character.TRIG_WRENCH_UPPERCUT: hVal *= 15f; } } } c[i].HP -= (int)hVal; CHAPTER 9 ■ SCRIPTING, AI, AND DEPTH (AND DEATH) 267 if (c[i].HP < 0) { if (c[i].AnimName == "hit") c[i].SetAnim("diehit"); } At the end, if our animation had been set to hit, we set it to diehit if the character should be dead. If our character doesn’t have a diehit animation, we’ll just end up using the regular hit animation. On that note, we also need to employ our dieland animation. We set our enemy animation to hitland in the Character.Land() method. Let’s have it set to dieland if the character should be dead: case "jhit": case "jmid": case "jfall": SetAnim("hitland"); if (HP < 0) SetAnim("dieland"); break; In our dieland and diehit animations, we use the killme command, which calls the KillMe() method: public void KillMe() { if (DyingFrame < 0f) { DyingFrame = 0f; } } When we want to add some character building and depth, we could add a few lines in KillMe() to create coins, health, and so on, as necessary. For now, we can just leave it at setting DyingFrame to 0, which signals that our character is dead. Lastly, let’s kill off our characters from Game1.Update(). After updating our characters, we’ll check their dying status—if dyingFrame is greater than 1, we kill them. character[i].Update(map, pManager, character); if (character[i].dyingFrame > 1f) { character[i] = null; } That should do it. We’ve created some blood-related triggers; created zombie death animations; created and implemented new script commands; and added health, death, and AI to characters. Let’s run it. We can kill our zombies now! Our zombie head splatter is shown in Figure 9-5. . 3 addbucket zombie 20 0 100 addbucket zombie 300 100 addbucket zombie 400 100 addbucket zombie 500 100 addbucket zombie 600 100 addbucket zombie 700 100 addbucket zombie 800 100 tag waitb wait 5 ifnotbucketgoto. waitz1 makebucket 3 addbucket zombie 300 100 addbucket zombie 400 100 addbucket zombie 500 100 addbucket zombie 600 100 addbucket zombie 700 100 tag waitb wait 5 ifnotbucketgoto waitb setglobalflag roomclear tag. DEATH) 27 7 In Figure 9-6, we’re using the following script: tag init fog wait 100 monster zombie 100 100 z1 wait 100 monster zombie 20 0 100 z2 tag waitz wait 5 iffalsegoto z1 waitz iffalsegoto z2