Building XNA 2.0 Games- P11

Chia sẻ: Cong Thanh | Ngày: | Loại File: PDF | Số trang:30

0
44
lượt xem
8
download

Building XNA 2.0 Games- P11

Mô tả tài liệu
  Download Vui lòng tải xuống để xem tài liệu đầy đủ

Building XNA 2.0 Games- P11: I would like to acknowledge John Sedlak, who saved this book from certain doom, as well as all of the great guys in the XNA community and Microsoft XNA team, who helped me with all of my stupid programming questions. (That is actually the term used—“stupid programming question”—and it is a question that one should not have to ask if one has been approached to write a book about the subject.)

Chủ đề:
Lưu

Nội dung Text: Building XNA 2.0 Games- P11

  1. 288 CHAPTER 9 ■ SCRIPTING, AI, AND DEPTH (AND DEATH) ), rotation + frame / 4f, new Vector2(32.0f, 32.0f), size, SpriteEffects.None, 1.0f); } } Our map scripting system is now in place. We just need to add our MapFlags class. public class MapFlags { String[] flags; public MapFlags(int size) { flags = new String[size]; for (int i = 0; i < flags.Length; i++) flags[i] = ""; } public bool GetFlag(String flag) { for (int i = 0; i < flags.Length; i++) { if (flags[i] == flag) return true; } return false; } public void SetFlag(String flag) { if (GetFlag(flag)) return; for (int i = 0; i < flags.Length; i++) { if (flags[i] == "") { flags[i] = flag; return; } } } } There’s one last bit of cleanup to do: remove the for loop in our Game1 initialize routine that spawns our ten zombies. We’ll be spawning all of our zombies from map scripts from here on out.
  2. CHAPTER 9 ■ SCRIPTING, AI, AND DEPTH (AND DEATH) 289 With our map in place using its brand-new script, run ZombieSmashers. You’ll see some- thing like Figure 9-7. Figure 9-7. Final product of this chapter Conclusion We’ve covered some fairly varied ground in this chapter. The main goal was to introduce map scripting, but we ended up adding quite a bit of functionality to our characters: blood sprays, initialization and death, and rudimentary AI. We know that we say this every chapter, but we’re a lot closer to having a fleshed-out game. To recap, we accomplished the following: • Added all sorts of blood sprays and spurts to the character editor • Created zombie attack, decap, and bloodsplode animations • Added initialization scripts • Implemented the new blood effects in our game • Implemented character initialization and death • Added AI • Defined a map scripting language • Added script-editing functionality to the map editor • Introduced map level and global level flags • Added fog
  3. 290 CHAPTER 9 ■ SCRIPTING, AI, AND DEPTH (AND DEATH) In the next chapter, we’ll introduce player death, map transitions, a HUD, and game menus. As we near the completion of our game, it is important to keep our head in the game and not get distracted by the want or need to change things that are done. One of the biggest prob- lems that plagues independent developers is their constant desire to fix old code, no matter how well it works. The best thing to do at this point is finish the game first, and then go back.
  4. CHAPTER 10 ■■■ Menus, a HUD, and Deployment At Last, the Coveted Xbox 360 Deployment W e’ll warn you right off the bat: this is going to be another odds-and-ends chapter. The initial design was to produce all of the interface-related items we would need: a health bar, score, map transitions, and a main menu. However, since these tie in with the constraints we’ll be dealing with on the Xbox 360, and because thus far we haven’t touched the thing, it seemed like a perfect time to introduce the concept of deployment. If you don’t have an Xbox 360 and/or an XNA Creators Club membership, you can bliss- fully skip the deployment section of this chapter. But, honestly, if you’ve gotten this far in the book and still don’t have any intention of playing around with an Xbox 360, you’ve got some issues. Granted, they might be good issues (thriftiness?), but they are issues nonetheless. Here’s the rundown of what we’ll be doing in this chapter: • Add a player HUD to display the health and score. We’ll need to implement scoring and map transitions, and add scripting functionality to support map transitions. • Add a menu system. • Implement player death. We’ll need to have a menu system in place to deal with this. • Dive into Xbox 360 deployment. There will be a bit of “we need to do A, but to get there we need to have B and C in place,” so bear with us. Adding a HUD Our HUD will consist of a row of five hearts for our health and a score, as shown in Figure 10-1. The hearts will just be a visual representation of our integer health value, not some sort of atomic unit of health. For instance, if you have 82/100 HP, you’ll have 4.1 hearts. We just thought hearts would look cute. 291
  5. 292 CHAPTER 10 ■ MENUS, A HUD, AND DEPLOYMENT Figure 10-1. Health and score display Creating the HUD Class The HUD class will deal with all things related to the interface: updating the health display and fading transitions, and drawing the health, score, and transitions. Just like everything else so far, we’ll be calling the Update() and Draw() functions from Game1. class HUD { SpriteBatch sprite; Texture2D spritesTex; Texture2D nullTex; Character[] character; Map map; We’ll be using a new object, scoredraw, to draw numbers. “But wait,” you may be saying, “why not just use a text class and ToString() to draw text?” The long and the short of it is that ToString() generates a ton of garbage when used every frame; this kills performance on the Xbox 360. We’ll use a more efficient algorithm that you may have seen in a Computer Science 101 class: ScoreDraw scoreDraw; We will talk more about ScoreDraw in the next section. We’ll use the field heartFrame to let our hearts sort of waver in a classic comic, cutesy fashion. We’re using the fHP field (think floating health points) for a sort of catch-up health bar. When we take damage or get health, we want our health bar to smoothly transition from the previous value to the current value. When we call HUD.Update(), we’ll have the floating bar try to get to
  6. CHAPTER 10 ■ MENUS, A HUD, AND DEPLOYMENT 293 where the real bar is. Then when we Draw(), we’ll draw more prominently at the floating posi- tion than the real position. This technique is infinitely more professional-looking than just drawing the current health value. float heartFrame; float fHP; For the constructor, we’ll just send it all of the objects it will need: the ever-present SpriteBatch, some textures, and the Character array and Map. public HUD(SpriteBatch _sprite, Texture2D _spritesTex, Texture2D _nullTex, Character[] _character, Map _map) { sprite = _sprite; spritesTex = _spritesTex; character = _character; map = _map; nullTex = _nullTex; scoreDraw = new ScoreDraw(sprite, spritesTex); } As promised, the Update() function increments our heartFrame and tries to get fHP to match with our goal HP. public void Update() { heartFrame += Game1.FrameTime; if (heartFrame > 6.28f) heartFrame -= 6.28f; if (character[0].HP > fHP) { fHP += Game1.FrameTime * 15f; if (fHP > character[0].HP) fHP = character[0].HP; } if (character[0].HP < fHP) { fHP -= Game1.FrameTime * 15f; if (fHP < character[0].HP) fHP = character[0].HP; } } Our Draw() method will first draw the score, then some black background hearts (our floating health hearts), and finally our real health hearts. We’re using the same sprites texture we previously used it for smoke, flame, and muzzle flashes, but we’ll add some hearts and numbers to it. The new image is shown in Figure 10-2.
  7. 294 CHAPTER 10 ■ MENUS, A HUD, AND DEPLOYMENT Figure 10-2. Updated sprites texture The hearts all start at 0, 192 and are 32 ✕ 32. public void Draw() { sprite.Begin(SpriteBlendMode.AlphaBlend); scoreDraw.Draw(Game1.Score, new Vector2(50f, 78f), Color.White, Justification.Left); Likening our health meter to a progress bar, we’ll call our floating health value fProg and our health value prog. Each value will be between 0 and 5, since we’re using 5 hearts. When we iterate through our five hearts, we can figure out if we’re using a full heart, no heart, or a portion of a heart by the difference between the currently drawn heart and our progress value. float fProg = fHP / character[0].MHP; float prog = character[0].HP / character[0].MHP; fProg *= 5f; prog *= 5f; for (int i = 0; i < 5; i++) { We’ll be using the r float to determine how much to rotate our hearts. The value is a func- tion of our heartFrame value and the current heart index. This way, the hearts don’t all rotate in sync. Since it’s a cosine function, the hearts bob one way or another.
  8. CHAPTER 10 ■ MENUS, A HUD, AND DEPLOYMENT 295 float r = (float)Math.Cos((double)heartFrame * 2.0 + (double)i) * 0.1f; First, we draw the dark background hearts: sprite.Draw(spritesTex, new Vector2( 66f + (float)i * 32f, 66f), new Rectangle(i * 32, 192, 32, 32), new Color(new Vector4(0.5f, 0f, 0f, .25f)), r, new Vector2(16f, 16f), 1.25f, SpriteEffects.None, 1f); Next, we compute how much of a heart is shown, by getting the difference between the progress value and the current heart index, and draw the floating health heart: float ta = fProg - (float)i; if (ta > 1f) ta = 1f; if (ta > 0f) { sprite.Draw(spritesTex, new Vector2( 66f + (float)i * 32f, 66f), new Rectangle(i * 32, 192, (int)(32f * ta), 32), new Color(new Vector4(1f, 0f, 0f, .75f)), r, new Vector2(16f, 16f), 1.25f, SpriteEffects.None, 1f); } Finally, compute another heart sliver width and draw the real health heart: ta = prog - (float)i; if (ta > 1f) ta = 1f; if (ta > 0f) { sprite.Draw(spritesTex, new Vector2( 66f + (float)i * 32f, 66f), new Rectangle(i * 32, 192, (int)(32f * ta), 32), new Color(new Vector4(.9f, 0f, 0f, 1f)), r, new Vector2(16f, 16f), 1.25f, SpriteEffects.None, 1f); } } Lastly, we’re implementing some fade-to-black functionality. We’ll have transition values in the map soon, for entering and exiting segments, as well as for when we first start a game. Based on where the map is transition-wise, we’ll draw our nullTex over the entire screen with an appropriate alpha value.
  9. 296 CHAPTER 10 ■ MENUS, A HUD, AND DEPLOYMENT float a = map.GetTransVal(); if (a > 0f) { sprite.Draw(nullTex, new Rectangle(0, 0, (int)Game1.ScreenSize.X, (int)Game1.ScreenSize.Y), new Color( new Vector4(0f, 0f, 0f, a))); } sprite.End(); } } Drawing the Score We referred to a ScoreDraw class earlier. Let’s define it here: public enum Justification { Left = 0, Right } class ScoreDraw { SpriteBatch spriteBatch; Texture2D spritesTex; public ScoreDraw(SpriteBatch _spriteBatch, Texture2D _spritesTex) { spriteBatch = _spriteBatch; spritesTex = _spritesTex; } For our drawing, we’re just applying a simple loop to our original score. We calculate modulus 10, draw the modulus 10 value, divide by 10, shift our position left, and repeat until nothing remains. For instance, if we give it the number 123, we’ll do this: • Compute 123 mod 10 = 3 • Draw 3, shift left a bit • Divide 123 by 10 = 12 • Compute 12 mod 10 = 2 • Draw 2, shift left a bit • Divide 12 by 10 = 1 • Compute 1 mod 10 = 1
  10. CHAPTER 10 ■ MENUS, A HUD, AND DEPLOYMENT 297 • Draw 1, shift left a bit • Divide 1 by 10 = 0 • Fini ! Computer science professors love to use this problem as an introduction to modulus arithmetic. public void Draw(long score, Vector2 loc, Color color, Justification justify) { int place = 0; The obnoxiously ugly part is in left-justified text. Drawing and shifting left as necessary is fine, but if we can’t draw and shift right, we would get a reverse score. Instead, we apply our divide-by-10 loop to the score to determine the entire string width, shift our draw position right by that much, and proceed as normal. if (justify == Justification.Left) { loc.X -= 17f; long s = score; if (s == 0) loc.X += 17f; else while (s > 0) { s /= 10; loc.X += 17f; } } The numbers use the same sprites texture. They start at 0, 224, and their dimensions are 16 ✕ 32. while (true) { long digit = score % 10; score = score / 10; spriteBatch.Draw(spritesTex, loc + new Vector2((float)place * -17f, 0f), new Rectangle((int)digit * 16, 224, 16, 32), color); place++; if (score
  11. 298 CHAPTER 10 ■ MENUS, A HUD, AND DEPLOYMENT And that does it for our HUD. We have updating and drawing functionality for health, score, and map transitions. That leaves quite a bit of implementation to do. Let’s start with map transitions! Creating Map Transitions As we described in Chapter 4, our game world will be made up of map segments. When the player walks all the way to the left or right on a map, the previous or subsequent segment will load. An example of a segmented map is shown in Figure 10-3. For the hero in segment 1 to get to the house in segment 4, three map transitions are necessary. Figure 10-3. Four map segments Designating Segment Transitions To designate segment transitions, we’ll add some scripting to our functionality to define exits and entrances. We’ll use the terminology as follows: Exit: This is the destination map we’ll transition to upon hitting a boundary. For instance, if leftexit is map1, we’ll transition to map map1 if our player hits the map segment’s left boundary. If rightexit is "", there is no right exit. Entrance: This is the vector for our player when we transition to a new map. For example, if we’ve just transitioned to map1 by exiting map2 to the left, we’ll place our player at the vector defined as rightentrance. At this point, we’ll be using only leftexit, rightexit, leftentrance, rightentrance, and initentrance. We’ll use initentrance to define an entrance vector for starting a new game. New Script Commands We need to add the new script commands to our MapScript and MapScriptLine classes. First, let’s define them in our MapCommands enumeration: enum MapCommands { Fog = 0, ..., SetLeftExit, SetLeftEntrance,
  12. CHAPTER 10 ■ MENUS, A HUD, AND DEPLOYMENT 299 SetRightExit, SetRightEntrance, SetIntroEntrance } We’ll parse them in the MapScriptLine constructor. The expected syntax is as follows: setleftexit path setrightexit path setleftentrance x y setrightentrance x y setleftexit x y It’s the usual string-to-constant model, with added attention to the vectors we’re using for entrance locations. case "setleftexit": Command = MapCommands.SetLeftExit; break; case "setleftentrance": Command = MapCommands.SetLeftEntrance; VParam = new Vector2(Convert.ToSingle(SParam[1]), Convert.ToSingle(SParam[2])); break; case "setrightexit": Command = MapCommands.SetRightExit; break; case "setrightentrance": Command = MapCommands.SetRightEntrance; VParam = new Vector2(Convert.ToSingle(SParam[1]), Convert.ToSingle(SParam[2])); break; case "setintroentrance": Command = MapCommands.SetIntroEntrance; VParam = new Vector2(Convert.ToSingle(SParam[1]), Convert.ToSingle(SParam[2])); break; Back in MapScript, we’ll run the new commands. Here are our exits: case MapCommands.SetLeftExit: map.TransitionDestination[(int)TransitionDirection.Left] = Lines[curLine].SParam[1]; break; case MapCommands.SetRightExit: map.TransitionDestination[(int)TransitionDirection.Right] = Lines[curLine].SParam[1]; break;
  13. 300 CHAPTER 10 ■ MENUS, A HUD, AND DEPLOYMENT In our Map class, we’ll use an array of enumeration TransitionDirection to hold transition destinations. We’re also referring to fields that we’ll add to Map. We’ll get to the Map updates in the next section. Let’s move on to our entrances: case MapCommands.SetLeftEntrance: if (map.TransDir == TransitionDirection.Right) { c[0].Location = Lines[curLine].VParam; c[0].Face = CharDir.Right; c[0].SetAnim("fly"); c[0].State = CharState.Air; c[0].Trajectory = new Vector2(200f, 0f); map.TransDir = TransitionDirection.None; } break; case MapCommands.SetRightEntrance: if (map.TransDir == TransitionDirection.Left) { c[0].Location = Lines[curLine].VParam; c[0].Face = CharDir.Left; c[0].SetAnim("fly"); c[0].State = CharState.Air; c[0].Trajectory = new Vector2(-200f, 0f); map.TransDir = TransitionDirection.None; } break; case MapCommands.SetIntroEntrance: if (map.TransDir == TransitionDirection.Intro) { c[0].Location = Lines[curLine].VParam; c[0].Face = CharDir.Right; c[0].SetAnim("fly"); c[0].State = CharState.Air; c[0].Trajectory = new Vector2(0f, 0f); map.TransDir = TransitionDirection.None; } break; When we set the entrances, we’re not actually setting anything. We’re checking to see what type of transition has just occurred leading up to this latest map initialization. If the transition matches whichever one we’re checking for, we’ll set the player to the appropriate location, complete with the proper animation, airborne state, and so on.
  14. CHAPTER 10 ■ MENUS, A HUD, AND DEPLOYMENT 301 Map Transition Enum and Fields Now, a bit out of order, we’ll define some of the fields we just talked about at the class level of Map. But first, let’s define our enumeration: public enum TransitionDirection : int { None = -1, Left = 0, Right = 1, Intro = 2 } The fields transInFrame and transOutFrame will be used to operate the map transitioning: public float transInFrame = 0f; public float transOutFrame = 0f; public string[] transitionDestination = { "", "", "" }; public TransitionDirection TransDir; When we first trigger a transition, transOutFrame will be set to 1, and as transOutFrame decreases to 0, the screen will fade to black. When transOutFrame hits 0, we’ll load the new map (which will in turn set the player to the right entrance location), and set transInFrame to 1. As transInFrame decreases to 0, the screen will fade in from black. Remember the GetTransVal() function we use in HUD to black out the screen? Here it is: public float GetTransVal() { if (transInFrame > 0f) { return transInFrame; } if (transOutFrame > 0f) { return 1 - transOutFrame; } return 0f; } We’re just returning a value between 0f and 1f, based on transInFrame and transOutFrame. Checking for Transitions Now we’ll define a function to check if a transition should be triggered. We’ll check for transi- tions based on player index 0’s location. If it’s at the left boundary, we’ll transition left; if it’s at the right boundary, we’ll transition right. We have also started to correct a mistake that was made in a previous chapter. As we add more and more code that is dependent on the size of the map, we want to move that value into a private variable or two. Here, we define two variables: xSize and ySize, both with a value of 20.
  15. 302 CHAPTER 10 ■ MENUS, A HUD, AND DEPLOYMENT public void CheckTransitions(Character[] c) { if (transOutFrame xSize * 64f - 32f && c[0].Trajectory.X > 0f) { if (transitionDestination[(int)TransitionDirection.Right] != "") { transOutFrame = 1f; TransDir = TransitionDirection.Right; } } if (c[0].Location.X < 64f + 16f && c[0].Trajectory.X < 0f) { if (transitionDestination[(int)TransitionDirection.Left] != "") { transOutFrame = 1f; TransDir = TransitionDirection.Left; } } } } In Map.Update(), we have some extra work to do. We’ll need to call CheckTransitions(), of course, and update our transition frames, switching maps if necessary. public void Update(ParticleManager pMan, Character[] c) { CheckTransitions(c); if (transOutFrame > 0f) { transOutFrame -= Game1.FrameTime * 3f; if (transOutFrame
  16. CHAPTER 10 ■ MENUS, A HUD, AND DEPLOYMENT 303 } } if (transInFrame > 0f) { transInFrame -= Game1.FrameTime * 3f; } if (mapScript.IsReading) mapScript.DoScript(c); Uh-oh, looks like we snuck another new method in there. We added Reset() to our ParticleManager class. It just iterates through all particles, setting each to null. Now we have the map transition functionality in place. However, at this point in develop- ment, there’s only one map! Adding a Map Let’s make a new map, and link the two maps together. We created a new one called start, as shown in Figure 10-4. Figure 10-4. The new start map
  17. 304 CHAPTER 10 ■ MENUS, A HUD, AND DEPLOYMENT The initialization script for our start map is as follows: tag init fog 750 setrightexit map setrightentrance 1184 724 setintroentrance 318 134 stop We’re giving the map a right exit, which is map—the first map we created. So, this new start map is basically our starting point. The player can then navigate to the right to get to the map with the zombies. Now we need to add some transition scripting to map, as shown in Figure 10-5. Figure 10-5. Our original map with transitions We’ll add a bit to the initialization script for this one: tag init fog 506 setleftexit start setleftentrance 90 408 ...
  18. CHAPTER 10 ■ MENUS, A HUD, AND DEPLOYMENT 305 So, start has map defined as the right exit, and map has start defined as the left exit. Tedious? We got used to it. There are most likely better ways to do it, such as by incorporating adjoining map data into the map definition, rather than the script. However, for a quick solution, this works fairly well. It handled the more than 100 map segments in The Dishwasher: Dead Samurai game easily enough. Remember that setintroentrance command? We’re getting to that main menu! Adding Menus We’ll create a big fat Menu class to deal with all things menu-related. Our class will have menu functionality in levels—a main level, options level, quit-are-you-sure? level, and so on—where we can be on one level or transitioning from one level to another. We also want to be able to use our Menu class as a pause menu and a you-are-dead menu. Hitting start while in a game will bring up the menu in pause mode; dying will bring up the menu in dead mode. Each mode will carry certain restrictions. For instance, hitting Start in the main menu is functionally the same as pressing A, but hitting Start in the pause menu will return the player to the game, because Start is meant to work as a pause toggle. Designing the Menu We want our menu to look excellent. Figure 10-6 shows a rough sketch of the look we have in mind. Figure 10-6. Main renu, rough draft
  19. 306 CHAPTER 10 ■ MENUS, A HUD, AND DEPLOYMENT Our menu options are at left, slightly staggered. Our hero is at right in a prominent, wrench- wielding pose. In the foreground, we have a zombie hand underfoot. We’ll use a series of layers to do this. Our hero will be one image, as shown in Figure 10-7. Figure 10-7. Our hero poses in pose.png The black foreground will pan in a slightly more exaggerated way than the hero pose, giving an illusion of depth (it’s that crazy parallax again!) Also, we can draw some fog between our hero and the foreground. Figure 10-8 shows the foreground image. Figure 10-8. Pose foreground in posefore.png On the left side of the menu will be some buttons. While it would be more robust to use a text-drawing class for these buttons, it’s a lot easier (and perfectly acceptable) to use images. We put together a sprite sheet, shown in Figure 10-9.
  20. CHAPTER 10 ■ MENUS, A HUD, AND DEPLOYMENT 307 Figure 10-9. Game options (more to come) in options.png We’ll set it up to have a layer of smoothly animated fog, a hero, the buttons, another layer of fog, and the foreground, with everything slowly panning left and right in parallax. Figure 10-10 shows what we’re going for. Figure 10-10. The main menu final product Let’s get to creating the class.
Đồng bộ tài khoản