Chapter 4（1）： Game Components游戏组件
Chapter 4: Game Components游戏组件
This chapter talks about the concept behind the Game class and the game components you can add to it. To get your graphic engine up and running in the next chapter you still need some new helper classes before starting with 3D concepts. The BaseGame class is used to implement more features and to include all the other classes you have written so far. It is derived from the Game class to take advantage of all the existing XNA features. In the same way our main test class TestGame is derived from BaseGame to help you execute static unit tests in your game. Then you will add the TextureFont class to your Helpers namespace to allow you to draw text on the screen, which is not possible out of the box in XNA. Finally, you also add some of the existing functionality from the previous chapters such as input, controller handling, and sound output into special classes to make it much easier to write a new game. Instead of just making some general assumptions, this chapter takes the game you are going to develop later in this chapter as a prime example.
In contrast to the previous chapter you are not going to write any helper classes first, but instead you are going to write the unit tests and the game class first and then add all the game components you need to your project. In the last few projects the problems were fairly simple and once you resolved them there was no need to go through them again. For the game you are going to develop in this chapter many improvements can be made and you will see this becomes even more true the bigger the game projects become. Refactoring is still the most important thing you have to remember when working over existing code and improving your game. Sometimes you will even see the code used in the unit tests ending up somewhere else in the final game code.
As an example game a simple Tetris clone is used. It will feature a big play field with colored blocks falling down, support for keyboard and gamepad input, a next block field showing you what comes next, and a little scoreboard containing the current level, score, highscore, and lines you destroyed. If you are a Tetris fan like me and like to play it every once in a while this game is great fun. Tetris is one of the most popular puzzle arcade games ever. It was invented by the Russian Alexey Pazhitnov in 1985 and became very popular ever since Nintendo released it on the Game Boy system in 1989.
我们使用Tetris的克隆版来作为游戏示例。这个游戏的特点是有一个很大的场地，期中有不同颜色的砖块不断下落，游戏支持键盘和手柄输入，另外一个区域显示下一个要落下的砖块信息，还有一个小的分数板，里面有当前等级、分数、最高分和你消掉的行数。如果你像我一样喜欢玩Tetris，你会再写这个游戏的时候感到更有趣。Tetris曾经是最流行的一款益智游戏。是俄罗斯的Alexey Pazhitnov在1985年发明的，然后由任天堂1989年在Game Boy上发布。
The Game Class
You already used the class in the previous chapters, but other than starting the game by calling the Run method from the Program class and your unit tests and using the Initialize, Update, and Draw methods, those chapters did not talk about the underlying design. Well, you don’t really need to know anything else if you are just creating a few simple games, but as games become bigger and have more features you might want to think about the class overview and class design of your game.
The Game class itself is used to hold the graphics device in the GraphicsDeviceManager instance and the content manager in the content field. From the Program class you just have to create an instance of your game class and call the Run method to get everything started. Unlike in the old days with Managed DirectX or OpenGL you don’t have to manage your own window, create your game loop, handle Windows messages, and so on. XNA does all that for you, and because it is handled in such a way it is even possible to run your game on the Xbox 360 platform where no window classes or Windows events are available.
It is still possible to access the window of the game through the Window property of the game class. It can be used to set the window’s title, specify if the user is allowed to resize the window, get the under lying Windows handle for interop calls, and so on. All these methods do nothing on the Xbox 360 platform. There is no window, there is no window title, and it can certainly not be resized. As you already saw in the previous game, you used the Window.Title property to set some simple text to the title for showing the current level and score to the user. The reason you did that is because there is no font support in XNA; to render text on the screen you have to create your own bitmap font and then render every letter yourself. In the next games and even for the Tetris game you will need that feature, and therefore the TextureFont class is introduced in a few minutes.
Additionally, it is worth mentioning that it is possible to set the preferred resolution in the game class constructor by setting the graphics properties like in the following example, which tries to use the 1024×768 resolution in fullscreen mode:
graphics.PreferredBackBufferWidth = 1024;
graphics.PreferredBackBufferHeight = 768;
graphics.IsFullScreen = true;
There is no guarantee that the game will actually run in that mode; for example, setting 1600×1200 on a system that just supports up to 1024×768 will only use the maximum available resolution.
You already know that and Draw are called every frame, but how do you incorporate new game components without overloading the game class itself? It’s time to look at the game classes and components overview of the Tetris clone game (see Figure 4-1).
The first thing you will notice is that you have now three game classes instead of just one like in the previous game examples. The reason for that is to make the main game class shorter and simpler. The BaseGame class holds the graphics manager with the graphics device, the content manager, and it stores the current width and height values of the current resolution you use in the game. The Update and Draw methods also handle the new Input, Sound, and TextureFont classes to avoid having to update them in the main game class. Then the TetrisGame class is used to load all the graphics from the content pipeline and initialize all sprites and game components, which you learn about in a second.
Finally, the TestGame class derives itself from the TetrisGame class to have access to all the textures, sprites, and game components, and it is only used in debug mode to start unit tests. The functionality of the TestGame class is very similar to the previous chapters, but this time it is organized in a nice way and separate from your main game class. The TetrisGame class uses several unit tests to make sure each part of the game works as you have planned.
The TetrisGame class also holds all game components in the Components property derived from the game class. You can add any class that is derived from the GameComponent class to this list and it will automatically be called when your game is started and when it is updated. It will not be called when you draw your game because the GameComponent does not have a Draw method. You can, however, implement your own drawing methods or just use the DrawableGameComponent class, which does have a Draw method. XNA does not have a direct Draw support for the game components; you have to call it yourself to make sure all components are called in the correct order. Because of this and for several other reasons (forcing you to use this model with almost no advantages, makes unit tests harder, your own game classes might be more effective or specific, and so on), you will not use many game components later in this book. It is generally a nice idea, but you can live without it because you have to create game components yourself anyway and you have to call Draw for them yourself too. Just for the Update method, it does not make much sense to use them.
As I mentioned in Chapter 1 the basic idea is to have users collaborate and share their game components to allow others to use parts of their game engine. For example, a frame counter component or even a fullblown 3D landscape rendering engine could be implemented as a game component, but just because someone does not use a game component does not mean it is harder to copy over. For example, if you have a complicated game component like a landscape rendering module, it will probably involve some other classes too and use its own rendering engine, which might not work out of the box in your engine if you just copy one file over. Either way, plugging in external code often requires quite a bit of refactoring until it is usable in your own game engine. In beta 1 of the XNA Framework a graphical designer was available in XNA Game Studio Express for the game components and you could easily drag and drop components into your game class or even into other game components to add features to your game without writing a single line of code. Because this feature was very complicated, buggy, and did not work on the Xbox 360, it was abandoned in the beta 2 release of the XNA Framework.
It is not a sure thing that game components will not be used, and maybe it does not matter to most programmers that the designer is missing and you have to call the Draw methods yourself. Then a lot of game components might be available and it would be useful to know all the basics about them. In the case of the Tetris game a few components come to mind:
§ The grid itself with all the colored blocks and the current falling block
§ The scoreboard with the current level, score, highscore, and number of lines you destroyed
§ The next block type box for the game
§ More simple things like a frame counter, handling the input, and so on
I decided to just implement the Tetris grid and the next block feature as game components; all the code is just way too simple for implementing several new classes just for them. If you will reuse the scoreboard, for example, you could always put it in a game component, but I cannot think of any other game I would like to write that uses that scoreboard.
Take a closer look at the Game class and the components that were added to it (see Figure 4-2).
The gray arrows indicate that these methods are called automatically through the fact that TetrisGrid and NextBlock were added to the Components list of the Game class. In TetrisGame.Draw the Draw method of TetrisGrid is called, which again calls the NextBlock.Draw method. TetrisGame itself holds just an instance of TetrisGrid. The NextBlock instance is only used inside of the TetrisGrid class.
You can see that using the game components for these three classes forced you to think about the calling order and it made your game more organized just by the fact that you did not put everything into one big class. This is a good thing and, though you can do all this by yourself if you are an experienced programmer, it might be a good idea for beginners to anticipate the idea of the game components in XNA.
More Helper Classes更多的辅助类
Didn’t talk enough about helper classes in the last chapter? Yes we did. The two new classes you are going to use for the Tetris game are not discussed here in great detail and they are just stripped-down versions of the real classes you use later in this book. But they are still useful and help you to make the programming process of your game easier.
You already learned about the missing font support in XNA and you know that using bitmap fonts is the only option to display text in XNA (apart from using some custom 3D font rendering maybe). In the first games of this book you just used some sprites to display fixed text in the game or menu. This approach was very easy, but for your scoreboard you certainly need a dynamic font allowing you to write down any text and numbers the way you need it in the game.
Let’s go a little bit ahead and take a look at the TestScoreboard unit test in the TetrisGame class, which renders the background box for the scoreboard and then writes down all the text lines to display the current level, score, highscore, and number of lines you destroyed in the current game:
// Draw background box
(512 + 240) - 15, 40 - 10, 290 - 30, 190));
// Show current level, score, etc.
TextureFont.WriteText(512 + 240, 50, "Level: ");
TextureFont.WriteText(512 + 420, 50, (level + 1).ToString());
TextureFont.WriteText(512 + 240, 90, "Score: ");
TextureFont.WriteText(512 + 420, 90, score.ToString());
TextureFont.WriteText(512 + 240, 130, "Lines: ");
TextureFont.WriteText(512 + 420, 130, lines.ToString());
TextureFont.WriteText(512 + 240, 170, "Highscore: ");
TextureFont.WriteText(512 + 420, 170, highscore.ToString());
You might notice that you are now using the TestGame class to start your unit test. For this test you use a couple of variables (level, score, and so on), which are replaced by the real values in the game code. In the render loop you first draw the background box and display it immediately to avoid display errors with sprites you draw later. Then you write down four lines of text with help of the WriteText method in the new TextureFont class at the specified screen positions. You actually call WriteText eight times to properly align all the numbers at the right side of your background box, which looks much nicer than just writing down everything in four lines.
After writing this unit test you will get a compiler error telling you that the TextureFont class does not exist yet. After creating a dummy class with a dummy WriteText method you will be able to compile and start the test. It will just show the background box, which is drawn in the upper-right part of the screen with help of the SpriteHelper class you learned about in the last chapter.
Before you even think about implementing the TextureFont class you will need the actual bitmap texture with the font in it to render text on the screen. Without a texture you are just doing theoretical work, and unit testing is about practical testing of game functionality. You will need a texture like the one in Figure 4-3 to display all the letters, numbers, and signs. You can even use more Unicode letters in bigger textures or use multiple textures to achieve that, but that would go too far for this chapter. Please check out the websites I provided at the top of the TextureFont class comment in the source code to learn more about this advanced topic.
Take a look at the implementation of the TextureFont class (see Figure 4-4). Calling the TextureFont class is very easy; you just have to call the WriteText method like in the unit test shown earlier. But the internal code is not very easy. The class stores rectangles for each letter of the GameFont.png texture, which is then used in WriteAll to render text by drawing each letter one by one to the screen. The class also contains the font texture, which is GameFont.png, a sprite batch to help render the font sprites on the screen, and several helper variables to determine the height of the font. For checking how much width a text will consume on the screen you can use the GetTextWidth method.
The internal FontToRender class holds all the text you want to render each frame, which is very similar to the process the SpriteHelper class uses to render all sprites on the screen at the end of each frame. In the same way SpriteHelper.DrawAll is called by BaseGame, TextureFont.WriteAll is also called and flushes everything on the screen and clears all lists. To learn more about the TextureFont class, check out the source code and run the unit tests or try stepping through the WriteAll method.
Another new class that is used in the Tetris game is the Input class, which encapsulates all the input handling, checking, and updating you did in the previous chapters yourself. Chapter 10 talks about the Input class in greater detail and some nice classes that really need all the features from the Input class (see Figure 4-5).
As you can see, the Input class has quite a lot of properties and a few helper methods to access the keyboard, the gamepad, and the mouse data. It will be updated every frame with the help of the static Update method, which is called directly from the BaseGame class. For this game you are mainly going to use the key press and gamepad press methods like GamePadAJustPressed or KeyboardSpaceJustPressed. Similar to the RandomHelper class it is not hard to figure out how this class works, and you already implemented much of the functionally in the previous chapter. For more details and uses you can check out Chapter 10.
Well, you already had sound in the first game in Chapter 2 and you also used it in Chapter 3 for the Breakout game. To keep things simple and to allow you to add more sound functionally later without having to change any of the game classes, the sound management is now moved to the Sound class. Take a quick look at the class (see Figure 4-6). It looks very simple in this version, but in Chapter 9, which also talks about XACT in greater detail, you will extend the Sound class quite a bit and make it ready for your great racing game at the end of this book.
As you can see, all the sound variables are now in this class and the Game class no longer contains any sound variables. The Sound constructor is static and will be called automatically when you play a sound for the first time with the Play method. The Update method is called automatically from the BaseGame class.
The Sounds enum values and the TestPlayClickSound unit test depend on the actual content in your current game. These values will change for every game you are going to write from here on, but it is very easy to change the Sounds enum values. You might ask why you don’t just play the sounds with help of the cue names stored in XACT. Well, many errors could occur by just mistyping a sound cue and it is hard to track all changes in case you remove, rename, or change a sound cue. The Sounds enum also makes it very easy to quickly add a sound effect and see which ones are available through IntelliSense.
The Tetris game uses the following sounds:
§ BlockMove for moving the block left, right, or down. It is a very silent sound effect.当方块上下左右移动的时候播放。这是一个非常安静的声音效果。。
§ BlockRotate is used when you rotate the current block and it sounds very “whooshy.”当你翻转方块的时候播放，这个声音听起来非常的“whooshy”。
§ BlockFalldown is used when the current block reaches the ground and finally lands.当方块下落到底部的时候播放这个声音。
§ LineKill is played every time you manage to kill a line in the game.当你在游戏中消除了一行的时候播放。
§ Fight is played at the start of each game to motivate the player.游戏开始的时候播放。
§ Victory is used when the player reaches the next level and contains an applause sound.当玩家通过这一关到达下一关的时候播放，其中包含了一段喝彩声。
§ Lose is an old school dying sound and is played when the player loses the game.当玩家输掉这一关的时候播放。