Essential XNA Game Studio 2.0 Programming ®
Jim Perry and Allen Sherrod
Wordware Publishing, Inc.
Library of Congre...
82 downloads
1148 Views
2MB Size
Report
This content was uploaded by our users and we assume good faith they have the permission to share this book. If you own the copyright to this book and it is wrongfully on our website, we offer a simple DMCA procedure to remove your content from our site. Start by pressing the button below!
Report copyright / DMCA form
Essential XNA Game Studio 2.0 Programming ®
Jim Perry and Allen Sherrod
Wordware Publishing, Inc.
Library of Congress Cataloging-in-Publication Data Perry, Jim, 1966Essential XNA Game Studio 2.0 programming / by Jim Perry and Allen Sherrod. p. cm. Includes bibliographical references and index. ISBN-13: 978-1-59822-055-1 ISBN-10: 1-59822-055-1 (pbk. : alk. paper) 1. Computer games--Programming. 2. Microsoft XNA (Computer file) I. Sherrod, Allen. II. Title. QA76.76.C672P4775 2008 794.8'1526--dc22 2008019300 CIP
© 2008, Wordware Publishing, Inc. All Rights Reserved 1100 Summit Avenue, Suite 102 Plano, Texas 75074 No part of this book may be reproduced in any form or by any means without permission in writing from Wordware Publishing, Inc. Printed in the United States of America ISBN-13: 978-1-59822-055-1 ISBN-10: 1-59822-055-1 10 9 8 7 6 5 4 3 2 1 0806 XNA is a registered trademark of Microsoft Corporation in the United States and other countries. Other brand names and product names mentioned in this book are trademarks or service marks of their respective companies. Any omission or misuse (of any kind) of service marks or trademarks should not be regarded as intent to infringe on the property of others. The publisher recognizes and respects all marks used by companies, manufacturers, and developers as a means to distinguish their products. This book is sold as is, without warranty of any kind, either express or implied, respecting the contents of this book and any disks or programs that may accompany it, including but not limited to implied warranties for the book’s quality, performance, merchantability, or fitness for any particular purpose. Neither Wordware Publishing, Inc. nor its dealers or distributors shall be liable to the purchaser or any other person or entity with respect to any liability, loss, or damage caused or alleged to have been caused directly or indirectly by this book.
All inquiries for volume purchases of this book should be addressed to Wordware Publishing, Inc., at the above address. Telephone inquiries may be made by calling: (972) 423-0090
Contents Introduction . . . . . . . . . . . . . . . . . . . . . . . . . . . . . xi Chapter 1
XNA Framework and Game Studio. . . . . . . . . . . . 1
What Is XNA? . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1 What Is the XNA Framework? . . . . . . . . . . . . . . . . . . . 2 Game Assembly . . . . . . . . . . . . . . . . . . . . . . . . . 2 Game Class . . . . . . . . . . . . . . . . . . . . . . . . . . 2 GameComponent/DrawableGameComponent Class . . . . 5 GameServiceContainer Class . . . . . . . . . . . . . . . . 5 GameTime Class . . . . . . . . . . . . . . . . . . . . . . . 6 GameWindow Class . . . . . . . . . . . . . . . . . . . . . . 6 GraphicsDeviceManager . . . . . . . . . . . . . . . . . . . 6 Framework Namespace Interfaces . . . . . . . . . . . . . . 9 Pipeline Assembly . . . . . . . . . . . . . . . . . . . . . . . 10 Framework Assembly . . . . . . . . . . . . . . . . . . . . . 11 What Is XNA Game Studio? . . . . . . . . . . . . . . . . . . . . 17 Installing XNA Game Studio . . . . . . . . . . . . . . . . . . . 18 Project Types. . . . . . . . . . . . . . . . . . . . . . . . . . . . 19 Windows Game . . . . . . . . . . . . . . . . . . . . . . . . . 19 Windows Game Library . . . . . . . . . . . . . . . . . . . . 23 Xbox 360 Game . . . . . . . . . . . . . . . . . . . . . . . . . 23 Xbox 360 Game Library . . . . . . . . . . . . . . . . . . . . 23 Starter Kits . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 23 Spacewar . . . . . . . . . . . . . . . . . . . . . . . . . . . . 24 Marblets . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 24 Racing Game . . . . . . . . . . . . . . . . . . . . . . . . . . 25 Ship Game . . . . . . . . . . . . . . . . . . . . . . . . . . . . 25 Content Pipeline Overview . . . . . . . . . . . . . . . . . . . . 26 Summary . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 28 Chapter 2
Game Development Overview . . . . . . . . . . . . . 29
Parts of a Game . . . . Graphics . . . . . . . Input Device(s) . . . Networking . . . . . AI . . . . . . . . . . GUI/User Interface . Physics System . . .
. . . . . . .
. . . . . . .
. . . . . . .
. . . . . . .
. . . . . . .
. . . . . . .
. . . . . . .
. . . . . . .
. . . . . . .
. . . . . . .
. . . . . . .
. . . . . . .
. . . . . . .
. . . . . . .
. . . . . . .
. . . . . . .
. . . . . . .
. . . . . . .
. . . . . . .
. . . . . . .
. . . . . . .
. . . . . . .
29 30 31 32 34 35 35 iii
Contents
Sound/Music . . . . . . . . . Game States . . . . . . . . . . . The Game Loop — XNA Style . Calculating Frame Rate . . . . . Game Components. . . . . . . . Game Services . . . . . . . . . . Creating Your First Application . Chapter 3
. . . . . . .
. . . . . . .
. . . . . . .
. . . . . . .
. . . . . . .
. . . . . . .
. . . . . . .
. . . . . . .
. . . . . . .
. . . . . . .
. . . . . . .
. . . . . . .
. . . . . . .
. . . . . . .
. . . . . . .
. . . . . . .
. . . . . . .
36 37 41 42 43 46 51
2D Graphics . . . . . . . . . . . . . . . . . . . . . . . 63
Texture2D . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 64 SpriteBatch . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 66 Rotation . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 69 Scaling . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 71 Flipping . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 72 Transparency . . . . . . . . . . . . . . . . . . . . . . . . . . 73 Coloring . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 76 Tiling. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 79 Animating Sprites . . . . . . . . . . . . . . . . . . . . . . . . . 84 Multiple Batch Rendering . . . . . . . . . . . . . . . . . . . . . 86 Text Drawing . . . . . . . . . . . . . . . . . . . . . . . . . . . . 88 Rendering with Multiple Viewports . . . . . . . . . . . . . . . 92 2D Shooter Game Version 1. . . . . . . . . . . . . . . . . . . . 96 Player and Enemy Sprites . . . . . . . . . . . . . . . . . . . 97 Showing the Score, Frame Rate, and Countdown . . . . . . 105 Summary . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 110 Chapter 4
Input . . . . . . . . . . . . . . . . . . . . . . . . . . 111
Gamepad, Keyboard, and Mouse Classes . . Gamepad . . . . . . . . . . . . . . . . . . Keyboard . . . . . . . . . . . . . . . . . . Mouse . . . . . . . . . . . . . . . . . . . State Classes . . . . . . . . . . . GamePadState . . . . . . . . . . . . . . . KeyboardState . . . . . . . . . . . . . . . MouseState . . . . . . . . . . . . . . . . Controlling Sprite Movement . . . . . . . . Collision Detection . . . . . . . . . . . . . . Rectangle Collision Detection . . . . . . Per-pixel Collision Detection . . . . . . . Transformed Sprite Collision Detection . Creating a Configurable Control System . . 2D Shooter Game Version 2 . . . . . . . . . Running and Gunning . . . . . . . . . . . Setting Up User-Configurable Controls. . . iv
. . . . . . . . . . . . . . . . .
. . . . . . . . . . . . . . . . .
. . . . . . . . . . . . . . . . .
. . . . . . . . . . . . . . . . .
. . . . . . . . . . . . . . . . .
. . . . . . . . . . . . . . . . .
. . . . . . . . . . . . . . . . .
. . . . . . . . . . . . . . . . .
. . . . . . . . . . . . . . . . .
. . . . . . . . . . . . . . . . .
111 112 114 114 114 115 116 117 118 118 119 125 127 133 142 156 182
Contents
Chapter 5
Sound . . . . . . . . . . . . . . . . . . . . . . . . . . 189
XACT and Sound Creation . Playing 2D Sound . . . . . . Playing 3D Sound . . . . . . 2D Shooter Game Version 3 Adding Music and Sound Summary . . . . . . . . . . Chapter 6
. . . . . .
. . . . . .
. . . . . .
. . . . . .
. . . . . .
. . . . . .
. . . . . .
. . . . . .
. . . . . .
. . . . . .
. . . . . .
. . . . . .
. . . . . .
. . . . . .
. . . . . .
. . . . . .
. . . . . .
191 194 198 200 200 202
. . . .
. . . .
. . . .
. . . .
. . . .
. . . .
204 206 209 213
Basic Effects. . . . . . . . . . . . . . . . . . . . . . . 223
BasicEffect Class Methods. . . . . . Colors, Textures, and Alpha . . . . . Implementing Textured Models . Fog in XNA . . . . . . . . . . . . . . Implementing Fog . . . . . . . . . Lighting . . . . . . . . . . . . . . . . Emissive and Ambient Light . . . Diffuse Light . . . . . . . . . . . . Specular Light . . . . . . . . . . . Implementing Lighting . . . . . . Additional Basic Effect Information . Summary . . . . . . . . . . . . . . . Chapter 8
. . . . . .
I/O . . . . . . . . . . . . . . . . . . . . . . . . . . . 203
The StorageDevice and StorageContainer Classes Standard I/O Files and Operations. . . . . . . . . . Data Serialization . . . . . . . . . . . . . . . . . . . 2D Shooter Game Version 4 . . . . . . . . . . . . . Chapter 7
. . . . . .
. . . . . . . . . . . .
. . . . . . . . . . . .
. . . . . . . . . . . .
. . . . . . . . . . . .
. . . . . . . . . . . .
. . . . . . . . . . . .
. . . . . . . . . . . .
. . . . . . . . . . . .
. . . . . . . . . . . .
. . . . . . . . . . . .
. . . . . . . . . . . .
. . . . . . . . . . . .
. . . . . . . . . . . .
. . . . . . . . . . . .
224 227 228 232 233 237 239 241 242 243 246 248
3D Math Basics . . . . . . . . . . . . . . . . . . . . . 249
Vectors . . . . . . . . . . . . . . . . . . . . . . Vector Arithmetic . . . . . . . . . . . . . . . Clamping with Vectors . . . . . . . . . . . . The Cross Product . . . . . . . . . . . . . . Distance between Vectors . . . . . . . . . . The Dot Product. . . . . . . . . . . . . . . . The Length of a Vector . . . . . . . . . . . . Interpolating Vectors . . . . . . . . . . . . . Finding the Minimum and Maximum Vector Negating Vectors . . . . . . . . . . . . . . . Normalizing Vectors. . . . . . . . . . . . . . Reflecting Vectors . . . . . . . . . . . . . . . Smooth Interpolation . . . . . . . . . . . . . Vector/Matrix Transformation . . . . . . . . Additional Vector Operations . . . . . . . . .
. . . . . . . . . . . . . . .
. . . . . . . . . . . . . . .
. . . . . . . . . . . . . . .
. . . . . . . . . . . . . . .
. . . . . . . . . . . . . . .
. . . . . . . . . . . . . . .
. . . . . . . . . . . . . . .
. . . . . . . . . . . . . . .
249 251 254 255 256 256 257 257 258 259 260 260 261 262 263 v
Contents
Matrices . . . . . . . . . . . . . . . . . . . . . . . Matrix Operations . . . . . . . . . . . . . . . . Quaternion Rotations. . . . . . . . . . . . . . . . Quaternion Fields, Properties, and Functions . Rays . . . . . . . . . . . . . . . . . . . . . . . . . Ray Properties and Operations . . . . . . . . . Planes . . . . . . . . . . . . . . . . . . . . . . . . Plane Properties and Operations . . . . . . . . Bounding Geometry . . . . . . . . . . . . . . . . Bounding Spheres, Boxes, and Frustums . . . Summary . . . . . . . . . . . . . . . . . . . . . . Chapter 9
. . . . . . . . . . .
. . . . . . . . . . .
. . . . . . . . . . .
. . . . . . . . . . .
. . . . . . . . . . .
. . . . . . . . . . .
. . . . . . . . . . .
267 270 277 278 280 282 283 283 285 285 292
3D Graphics . . . . . . . . . . . . . . . . . . . . . . 293
Primitives . . . . . . . . . . . . . . . . . . . Points. . . . . . . . . . . . . . . . . . . . Lines . . . . . . . . . . . . . . . . . . . . Triangles . . . . . . . . . . . . . . . . . . Indexed Geometry . . . . . . . . . . . . Models and Meshes . . . . . . . . . . . . . XSI and XNA. . . . . . . . . . . . . . . . Loading and Rendering Models . . . . . 3D Models Implementation. . . . . . . . Virtual Cameras . . . . . . . . . . . . . . . 3D Camera Implementation . . . . . . . Animations . . . . . . . . . . . . . . . . . . Straight-Path Animation Implementation Skeleton Animation . . . . . . . . . . . . 3D Shooter Game Version 1 . . . . . . . . . Game and Menu Screens . . . . . . . . . The Player and Camera . . . . . . . . . . Loading the Level and the Game. . . . . Summary . . . . . . . . . . . . . . . . . . .
. . . . . . . . . . . . . . . . . . .
. . . . . . . . . . . . . . . . . . .
. . . . . . . . . . . . . . . . . . .
. . . . . . . . . . . . . . . . . . .
. . . . . . . . . . . . . . . . . . .
. . . . . . . . . . . . . . . . . . .
. . . . . . . . . . . . . . . . . . .
. . . . . . . . . . . . . . . . . . .
. . . . . . . . . . . . . . . . . . .
. . . . . . . . . . . . . . . . . . .
293 294 294 295 297 299 300 302 303 305 307 314 314 322 329 332 333 341 355
Chapter 10 Importers/Converters . . . . . . . . . . . . . . . . . 357
Standard Importers and Content Processors . . . . Autodesk FBX (.fbx) Importer . . . . . . . . . . Effect (.fx) Importer and Processor . . . . . . . Model Processor. . . . . . . . . . . . . . . . . . No Processing Required . . . . . . . . . . . . . Sprite Font Descriptor Importer and Processors Texture Importer and Processor . . . . . . . . . X File Importer . . . . . . . . . . . . . . . . . . XACT Importer and Processor . . . . . . . . . . XML Importer . . . . . . . . . . . . . . . . . . . vi
. . . . . . . . . .
. . . . . . . . . .
. . . . . . . . . .
. . . . . . . . . .
. . . . . . . . . .
. . . . . . . . . .
357 358 358 359 360 360 360 361 361 362
Contents
Extending the Standard Content Processing . . . . . . . . A Simple Example of Extending a Standard Processor . Writing a Custom Importer and Processor . . . . . . . . . Creating the Importer’s Project . . . . . . . . . . . . . Storing Imported and Compiled Data . . . . . . . . . . Creating the Importer. . . . . . . . . . . . . . . . . . . Creating the Processor . . . . . . . . . . . . . . . . . . Creating the Content Writer . . . . . . . . . . . . . . . Creating the Content Reader . . . . . . . . . . . . . . . Testing the Custom Importer in an Application . . . . . 3D Shooter Game Version 2 . . . . . . . . . . . . . . . . . Game Enemies . . . . . . . . . . . . . . . . . . . . . . Version 2 of the Game. . . . . . . . . . . . . . . . . . . Adding Custom Content . . . . . . . . . . . . . . . . . Summary . . . . . . . . . . . . . . . . . . . . . . . . . . .
. . . . . . . . . . . . . . .
. . . . . . . . . . . . . . .
362 362 366 368 370 370 371 373 374 375 377 377 382 394 395
Chapter 11 Shaders and Custom Effects . . . . . . . . . . . . . . 397
Shader Models . . . . . . . . . . . . . . . . . . . . . . . . . Shader Model 1.0 . . . . . . . . . . . . . . . . . . . . . . Shader Model 2.0 . . . . . . . . . . . . . . . . . . . . . . Shader Model 3.0 . . . . . . . . . . . . . . . . . . . . . . Shader Model 4.0 . . . . . . . . . . . . . . . . . . . . . . Vertex and Pixel Shaders . . . . . . . . . . . . . . . . . . . Vertex Shaders . . . . . . . . . . . . . . . . . . . . . . . Pixel Shaders . . . . . . . . . . . . . . . . . . . . . . . . Geometry Shaders . . . . . . . . . . . . . . . . . . . . . An Overview of the High-Level Shading Language (HLSL) Data Types and Swizzling . . . . . . . . . . . . . . . . . . Semantics . . . . . . . . . . . . . . . . . . . . . . . . . . Techniques and Passes . . . . . . . . . . . . . . . . . . . Simple Shader Demo . . . . . . . . . . . . . . . . . . . . . . Summary . . . . . . . . . . . . . . . . . . . . . . . . . . . .
. . . . . . . . . . . . . . .
399 399 400 401 402 402 403 405 407 408 409 412 413 414 421
Chapter 12 Game Distribution, the Creators Club, and the
Xbox 360 . . . . . . . . . . . . . . . . . . . . . . . . 423
Sharing Your Games . . . . . . . . . . . . . . . . . Joining the Creators Club . . . . . . . . . . . . . . Setting Up Your PC and Xbox 360 for XNA Games Programming Considerations for the Xbox 360 . . Safe Regions . . . . . . . . . . . . . . . . . . . . Aspect Ratio . . . . . . . . . . . . . . . . . . . . Graphics, Input, Audio, and Storage Differences between Platforms . . . . . . . . . . . . . . . . Graphics. . . . . . . . . . . . . . . . . . . . .
. . . . . .
. . . . . .
. . . . . .
. . . . . .
. . . . . .
. . . . . .
423 424 425 428 429 430
. . . . . . 430 . . . . . . 430 vii
Contents
Input. . . . . . . . . . . . . . . . . . . . . . Audio . . . . . . . . . . . . . . . . . . . . . Storage . . . . . . . . . . . . . . . . . . . . Setting Up Code for Cross-Platform Sharing . . . Debugging. . . . . . . . . . . . . . . . . . . . . . XNA Framework Remote Performance Monitor .
. . . . . .
. . . . . .
. . . . . .
. . . . . .
. . . . . .
. . . . . .
. . . . . .
431 432 432 433 434 434
Chapter 13 Creating a Difficulty System . . . . . . . . . . . . . . 437
Types of Difficulty Systems . . . . . Determining Our Difficulty Settings Determining Levels of Difficulty . . Storing Difficulty Settings . . . . . . Implementing the Difficulty System Testing the Difficulty System . . . . Moving On… . . . . . . . . . . . . .
. . . . . . .
. . . . . . .
. . . . . . .
. . . . . . .
. . . . . . .
. . . . . . .
. . . . . . .
. . . . . . .
. . . . . . .
. . . . . . .
. . . . . . .
. . . . . . .
. . . . . . .
. . . . . . .
437 438 440 440 443 448 449
Chapter 14 Networking . . . . . . . . . . . . . . . . . . . . . . . 451
Networking Models . . . . . . . . . . . . . . . Peer-to-Peer . . . . . . . . . . . . . . . . . . Client/Server . . . . . . . . . . . . . . . . . Networking Topologies. . . . . . . . . . . . . . Bus Topology . . . . . . . . . . . . . . . . . Star Topology . . . . . . . . . . . . . . . . . Ring Topology . . . . . . . . . . . . . . . . . Mesh Topology . . . . . . . . . . . . . . . . Hybrid Topology . . . . . . . . . . . . . . . . Preparing the Game for Networking . . . . . . Gamer Services . . . . . . . . . . . . . . . . Implementing Networking in Ghost Arena 3D . Changes to the Game Screens . . . . . . . . Changes to the Player and Ghost Classes . . Creating a Networking Session . . . . . . . Hosting and Joining Games . . . . . . . . . . The Game Lobby . . . . . . . . . . . . . . . Adding Networking to the Game . . . . . . . . New Variables and Objects . . . . . . . . . . Updated Initialization Functions . . . . . . . Updated Game Over. . . . . . . . . . . . . . Updated Draw and Update Functions . . . . Updated Firing . . . . . . . . . . . . . . . . . Future Improvements . . . . . . . . . . . . . . Summary . . . . . . . . . . . . . . . . . . . . .
viii
. . . . . . . . . . . . . . . . . . . . . . . . .
. . . . . . . . . . . . . . . . . . . . . . . . .
. . . . . . . . . . . . . . . . . . . . . . . . .
. . . . . . . . . . . . . . . . . . . . . . . . .
. . . . . . . . . . . . . . . . . . . . . . . . .
. . . . . . . . . . . . . . . . . . . . . . . . .
. . . . . . . . . . . . . . . . . . . . . . . . .
. . . . . . . . . . . . . . . . . . . . . . . . .
452 452 454 455 456 456 456 456 457 457 458 464 465 470 473 476 478 488 488 489 493 495 505 506 508
Contents
Chapter 15 Creating an Achievement System . . . . . . . . . . . 509
Our Achievements . . . . The Achievement Class . Capturing Achievements . Sharing Achievements . . Summary . . . . . . . . . Appendix
. . . . .
. . . . .
. . . . .
. . . . .
. . . . .
. . . . .
. . . . .
. . . . .
. . . . .
. . . . .
. . . . .
. . . . .
. . . . .
. . . . .
. . . . .
. . . . .
. . . . .
. . . . .
. . . . .
. . . . .
510 511 520 526 526
Additional Resources . . . . . . . . . . . . . . . . . . 527
Index . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 530
ix
About the authors: Jim Perry is a Microsoft XNA MVP with more than 15 years of development experience. He worked in the gaming industry for three of those years, contributing to five games during that period. He currently works during the day for a company doing .NET Windows and Web apps development and at night spends most of his time in game development related activities. Allen Sherrod has written five books on various game development topics. He has also written for Game Developer magazine and Gamasutra.com and has contributed to Game Programming Gems 6. Currently Allen is the owner of Re-Inventing Games & Graphics, Inc., which he plans to use to enter the independent game development scene. About the reviewer: Michael Morton is a hobby game developer of over 16 years with experience in a gamut of graphics technologies and implementations. He is the administrator of the popular XNA web site Ziggyware.com and was recently awarded the Microsoft XNA MVP for his dedication to consistently helping the community learn XNA and DirectX.
Introduction In the early days of game development, things were a lot less complex. Games didn’t require as many different areas of expertise (physics, advanced audio, 3D graphics, etc.) nor were they at the level of complexity they are today. Something as big and complex as World of Warcraft probably wasn’t even close to being on the radar of game developers. As technology advanced, so did the games that were created. This advancement led games to become increasingly complex in order to remain cutting-edge, thus requiring a lot of man-hours and a lot of talented individuals who were experts in their fields. There was a time when games could be developed by one person or a small team, but as games advanced so did the requirements for the various areas that make up a game (e.g., graphics, physics, artificial intelligence, art, and so forth). Because of this it is no longer practical, efficient, or effective for one person to be a professional in every area that makes up a modern-day commercial game. Game development studios these days can employ in excess of several hundred people, which includes programmers, artists, designers, testers, and management. When the consumer market started to embrace graphics hardware as a standard feature, developers had to manually write code for the various devices that they wanted to support or, in the case of supporting machines without graphics hardware, had to write software routines that were executed on the user’s CPU. There was no single middleman between the code and hardware. But then in September of 1995 Microsoft released the first version of what would later be called DirectX, the Windows Game SDK. Game development using DirectX frees programmers from the worry of ensuring their code works with all the different hardware gamers could be using. Unfortunately, the increase in the power of hardware also means gamers expect more and more features in games, meaning more time is necessary to develop and test a game. With the release of XNA Game Studio, Microsoft has brought console game development to anyone with an Xbox 360, an idea, some talent, and some time. The ability to run and debug a game on xi
Introduction
a retail console is a godsend to the indie and homebrew community. Not only that, it means that any gamer who wishes can download and run these games with, currently, just a small monetary investment. This ability to do console development is in addition to the increase in ease of doing game development on Windows-based PCs. The potential for both sides — developer and gamer — is huge, and Microsoft is doing its best to ensure that potential is reached. The current tools available are just the beginning of what Microsoft has envisioned as the “YouTube of gaming.”
Who Should Read This Book? Anyone who is interested in game programming for the PC and/or the Xbox 360 will benefit greatly from this book. You should have a good understanding of the C# language in order to take full advantage of the material in this book. If you’re comfortable with VB.NET, C++, or Java, you should be able to quickly pick up the nuances of C#. If you have no experience with any of the languages mentioned, you might want to pick up a good book on C# programming before diving into this book. Even if you’re not an experienced programmer, plenty of game development techniques will be discussed that will help increase your knowledge of the field. Experienced game developers who haven’t yet looked at XNA Game Studio will also gain from this book since it progresses step by step through the pieces of the XNA Framework needed to create a game. For those with some experience in game development, you will find several sections that will serve more as a review than an introduction. For those new to game development, you’ll find all the information needed to get started in this field. While the games produced during the course of this book won’t be AAA status, the information gained can be used to create such games, and usually with a significant reduction in development and testing time.
xii
Introduction
Conventions The C# language will be used exclusively throughout this book. The coding conventions for the .NET Framework will be followed with only minor changes due to the styles of the authors. These conventions can currently be found at http://msdn2.microsoft.com/ en-us/library/ms229042(VS.80).aspx. It’s recommended that, if you haven’t already done so, you adopt a set of coding conventions and stick to them. It makes your code easier to read and follow, especially if you’re looking at code that you had put aside for a while and gone back to. It will also make it easier for people you work with to follow your code.
System Requirements The following is required to run XNA Game Studio: n
Microsoft Windows XP SP2 or Windows Vista (Windows Server 2003 and older operating systems are not currently supported)
n
At least the DirectX 9.0c runtime (the SDK is not needed)
n
A video card that supports at least Shader Model 1.1 (2.0 support is recommended as many samples and starter kits require it)
n
Visual C# Express 2005 or Visual Studio 2005
xiii
Introduction
Ghost Arena During the course of this book we’ll be creating 2D and 3D Windows and 3D Xbox 360 versions of a shooter game called Ghost Arena. This game will be a fairly straightforward, simple top-down (for the 2D version) shooter that will incorporate the information learned in each chapter into a complete game. The purpose of the book’s game projects is to give you a handson approach to learning not only XNA, but game development as well. At the end of this book you will have the knowledge and experience to further your education by moving to the next, more advanced level.
Installing and Using the Code All code for the book can be downloaded from http://www.wordware.com/files/xna0551. You’ll find links there for other relevant downloads and information as well. Extract the downloaded file(s) onto your hard drive. After the code has been placed on the hard drive of your PC, you can simply navigate to the appropriate folder for a chapter and open the solution file in XNA Game Studio. Installing XNA Game Studio is covered in Chapter 1.
Support for This Book Support for the material covered in this book, including updates and errata, can be obtained from the publisher’s web site at http://www.wordware.com/files/xna0551. Any questions or comments about the book can be emailed to . The authors welcome feedback and will reply when possible.
xiv
Chapter 1
XNA Framework and Game Studio What Is XNA? If you’ve been on any of the game development message boards or IM systems, you’ll likely have seen many things referred to as “XNA.” It seems Microsoft hasn’t done a great job of defining exactly what XNA is. If you look at the XNA Developer Center site (http://msdn2.microsoft.com/en-us/xna/default.aspx), you’ll find Microsoft has provided this definition of XNA: Microsoft XNA is composed of industry-leading software, services, resources, and communities focused on enabling game developers to be successful on Microsoft gaming platforms. Because of this, Michael Klucher, a member of the XNA Game Studio team, made a blog post to attempt to clear this up (http://klucher.com/archive/2007/03/24/so-wait-xna-isn-t-a-product.aspx). XNA isn’t just one particular product, like XNA Game Studio. It encompasses all of the game development technology Microsoft offers. XNA as a brand is still relatively young, so expecting a lot of pieces to be available right at the start is a bit premature. Software development is a complicated process, and when you’re talking about something as big as XNA it’s going to take some time to see results. Given the size of the XNA Game Studio team and the time 1
2
Chapter 1
it took to get the first version out to waiting developers (about six months), it’s impressive that there’s as much functionality available as there is. What most people think of as XNA — XNA Game Studio — is a small but significant portion of XNA. It, along with the XNA Framework, is what we will concentrate our attention on during the course of this book. This brings us to the next question:
What Is the XNA Framework? The XNA Framework is a set of managed DLLs that contain a large set of class libraries that are mostly specific to helping with the creation of games. It’s made up of three main DLLs — one that handles game-related functionality, one for the content pipeline, and one that handles areas such as audio, graphics, input, storage, etc. Let’s take a look at the DLLs and the namespaces and objects they contain.
Game Assembly The Game assembly contains the classes and interfaces that you’ll use to get as close to the core of your game code as you can. Every game will contain an instance of the Game class, which in turn uses or can contain some of the other classes in the assembly.
Figure 1-1
Game Class The Game class is the first class you’ll become acquainted with since it’s created for you when you use the Windows Game or Xbox 360 Game templates, and it’s the starting point for everything you’ll do. While you could put all of your game code in this class, it would
XNA Framework and Game Studio
3
make for one monstrous chunk of code. We’ll use the class as a container for holding classes that will make managing our game a bit easier. This class has a lot of properties and methods that you’ll need to become acquainted with, as shown in Figure 1-2. The Components property is a collection of GameComponent objects. We’ll discuss the GameComponent class and how you go about creating your own game components in a lot more detail later on. One main point that we’ll mention right now is that the objects in this list have their Update and Draw methods constantly called automatically. You don’t have to worry about writing any code to do this; you just write the code inside those methods. The plumbing code, the stuff most developers don’t like writing and shouldn’t have to, is handled for you. The Content member is a ContentManager object that in version 1.0 was created in a game1.cs Figure 1-2 file that was generated for you when you used the templates to create a game project. With the move to 2.0, it’s been moved into the Game class, meaning as you pass around a reference to your Game object you get the ContentManager as well so you no longer have to create your own in any game-specific classes you write. The Services member works in conjunction with the Components property. It’s a collection of interfaces that allow you to access the GameComponents using the corresponding service. Here is one common example: IGraphicsDeviceManager gdm = (IGraphicsDeviceService)game.Services .GetService(typeof(IGraphicsDeviceService)); GraphicsDevice gd = gdm.GraphicsDevice;
4
Chapter 1
You’ll probably often want to get to the GraphicsDevice object of the Game object from other classes. GameComponent objects get passed the Game object in their constructor. Using that object you can easily get the GraphicsDevice by going through the IGraphicsDeviceService interface in the Services list of the Game object. We’ll do some work with Services in later chapters. IsActive tells you if the game is currently the active application. You probably don’t want the code in the Update method of the game or any components to be executed if the player has minimized or tabbed to another application. IsMouseVisible allows you to check or set whether the mouse cursor is visible. You’ll use this if you want to render your own custom mouse cursor, turning off the built-in cursor. Many RPGs and FPSs do this, using graphics such as a hand or crosshair. IsFixedTimeStep gives you the ability to change how the Update method is called. By default this is set to true, which calls the Update method 60 times per second. Setting the property to false tells the Framework to call the Update method as quickly as possible. Doing this, however, may cause an issue with your game logic since this setting does not affect the drawing to the screen. That’s controlled by a separate property, the SynchronizeWithVerticalRetrace property of the GraphicsDeviceManager. If your Update method is being called 100 times a second and the screen is only being redrawn 60 to 70 times per second, you could end up with things like sprites jumping around instead of smoothly moving since the position could be updated several times before the screen is redrawn. Your update code would have to take this possibility into account. Throughout this book we’ll be leaving the property set to its default of true. The TargetElapsedTime property is used in conjunction with IsFixedTimeStep. If IsFixedTimeStep is set to true, the TargetElapsedTime property controls how often Update is called. By default this property is set to 60. If you’re targeting high-end systems where you know the players could be running with higher refresh rates, you can change this to have your Update and Draw methods called back to back with no time wasted doing nothing, which is the case if Update and Draw take less than the default 1/60 of a second. The Window property allows you to get the GameWindow object for the window that is automatically created for you. This is useful if
XNA Framework and Game Studio
5
you want to allow the user to resize the window and capture the resize event, change the title of the window, and get the handle to the window for making API calls. When you create a project using one of the templates, several skeleton methods for the Game object are created for you — Initialize, LoadContent, UnloadContent, Update, and Draw. There are a handful of other methods: n
Run — If you create your project using one of the templates, you’ll notice a program.cs file that contains your Main procedure. A new instance of the Game object is created in this procedure and its Run method is called. This starts up the game loop built into the Framework.
n
Exit — You’ll see a call to this in the Update method of the Game object. If the player presses the Back button on the controller, this is trapped and the Exit method is called on the Game object, shutting down the game loop and ending the game.
n
OnActivated and OnDeactivated — These are called when the game window gains and loses focus respectively. Although you can check the IsActive property of the game in the Update method, you may want to handle some things outside of the Update. You can override the method to do so in these procedures.
GameComponent/DrawableGameComponent Class The DrawableGameComponent and GameComponent classes help us manage our game. The GameComponent class implements a couple of interfaces that allow it to be hooked into the Game class and be given the chance to automatically update itself many times per second. The DrawableGameComponent class inherits from GameComponent and implements an interface that allows it to draw itself in the same way GameComponent gets updated. We’ll look at exactly how this happens in a bit.
GameServiceContainer Class The GameServiceContainer class holds instances of Game Services. The class has only three methods — AddService, GetService,
6
Chapter 1
and RemoveService. The Services property of the Game class is an instance of this class. We’ll dig into Game Services a bit in the next chapter. Suffice it for now to say that Game Services allow parts of a game to communicate with each other in an object-oriented manner.
GameTime Class The GameTime class gets used in the Update and Draw methods of GameComponent. It tells you how much time (both game and real time) has passed since the last time the method it is passed to was called and how long the game has been running. Usually the ElapsedGame/RealTime members of this class will have a value that is fairly consistent each time the Update and Draw methods are called, but if you have some clunky code taking too much time, you’ll notice it by checking this class.
GameWindow Class The GameWindow class is kind of hidden behind the scenes. You can get at it if you need to as the Game class has a GameWindow member, but usually you won’t need to. Some people have managed to get their game running in controls, such as panels that are placed on a regular form, and in ways not expected, such as creating level editors using the game framework. The GameWindow object has helped them do this.
GraphicsDeviceManager You’ll examine the GraphicsDeviceManager class instance in the Game class to check whether the game is running full-screen or in a window. You’ll also use it to get the size of the game screen and allow resizing the game window. You can use this class to get at the properties of the GraphicsDevice class to set up multiple viewports to allow split-screen play and retrieve and set the current pixel and vertex shaders, among other tasks. There’s a lot of functionality built into the class as you can see in Figure 1-3. The GraphicsDevice property is probably the main property of this class that you’ll use. You saw how to get to this property a few pages ago when we talked about the Game class.
XNA Framework and Game Studio
Figure 1-3
IsFullScreen allows you to set or retrieve whether your game is displayed full-screen or windowed. Simply set the value of the property to true in the constructor of your game and you’re done. Alternatively, you can call the ToggleFullScreen method to switch between display modes. The MinimumPixelShaderProfile and MinimumVertexShaderProfile properties allow you to specify the lowest version that the player’s graphics card must support for pixel shader and vertex shader models. You’ll only need to worry about these if you use functionality of a model not found in Shader Model 1.1, the minimum required to run Game Studio games. The Preferred… properties allow you to change some default settings for your game. If you want your game to default to a resolution other than 800 × 600, for example, you would change the PreferredBackBufferHeight/Width properties. We’ll default our game to 1024 × 768, so you’ll see two lines in the constructor of the Game class instance that does this. The other properties allow
7
8
Chapter 1
you to increase or decrease graphics quality so that you can allow your game to run at a setting suited to the machine on which it’s running. Prior to starting your game you can check the three Valid… properties to ensure, to some extent, that the hardware supports your game. Unless you’re using some feature or function out of the ordinary, the basic requirements for running an XNA game should mean the hardware is up to the task; otherwise the game won’t even start up. The class exposes a half-dozen events related to changes in the graphics device. For example, if you wanted to execute code when the device is reset (such as when the resolution or color depth is changed), you would do something like the following: Listing 1-1 public class Game1 : Microsoft.Xna.Framework.Game { GraphicsDeviceManager graphics; public Game1() { graphics = new GraphicsDeviceManager(this); graphics.GraphicsDevice.DeviceReset += new EventHandler(OnDeviceReset); graphics.PreferredBackBufferWidth = 1024; graphics.PreferredBackBufferHeight = 768; } protected override void Draw(GameTime gameTime) { graphics.GraphicsDevice.Clear(Color.Black); // The real drawing happens inside the screen manager component. base.Draw(gameTime); } public void OnDeviceReset(object sender, EventArgs e) { //code here } }
XNA Framework and Game Studio
9
Framework Namespace Interfaces The interfaces in the namespace allow you to create your own custom classes with some or all of the functionality of the GameComponent… classes or the GraphicsDeviceManager class. While most of the time the classes provided will work just fine, it’s nice to have the flexibility to do it yourself.
IDrawable The IDrawable interface contains one method, two properties, and two events. The Draw method is called automatically if the implementer is a GameComponent added to the Game object’s Components collection. The DrawOrder property specifies when the implementer is drawn in the list of other IDrawable objects. The Visible property determines if the Draw method should be called. The events are DrawOrderChanged and VisibleChanged. If you change the value of the DrawOrder and Visible properties you’ll probably want to add logic to the code in your Draw method to determine what, if anything, should be drawn and ensure that the order in which objects are drawn is correct. If you want to create game components such as HUDs or GUI elements, a console window such as the ones you’ll find in many FPS games today, or even game elements such as a GameLevel component that draws the current level for your 2D dungeon crawl game, you would have your class implement this interface so that you don’t have to worry about calling its drawing functionality. The Game object will automatically call the Draw method for all components in its GameComponents list that implement this interface.
IGameComponent Strangely enough, the IGameComponent interface only defines an Initialize method. Implementing this interface, however, allows you to add the implementing object to the Game object’s Components collection. This Components property is a GameComponentCollection object, which is declared as: public sealed class GameComponentCollection : System.Collections.ObjectModel.Collection
10
Chapter 1
The part means only objects that implement the IGameComponent interface can be added to the collection. Later on in this book we’ll create an input system that inherits from the GameComponent class, which implements this interface. Invariably any component that implements this interface will also implement the IUpdateable interface, which contains an Update method that the game will call automatically each time through the game loop that runs during the game. The interface also contains an Enabled property, which allows you to have the Update method not be called, and an UpdateOrder property, which lets you specify the order in which the components in the GameComponents list have their Update method called.
IGraphicsDeviceManager The IGraphicsDeviceManager interface defines three methods — BeginDraw, CreateDevice, and EndDraw. The GraphicsDeviceManager that gets added as a member of the Game class instance implements this interface. It’s unlikely that you’ll ever need to create a class that implements this interface. The whole point of the XNA Framework is to take care of a lot of the startup code that you would normally have to write and make it easier to interact with the hardware. The GraphicsDeviceManager is one of the classes that does this.
Pipeline Assembly The Pipeline namespace provides the functionality for creating and using your own content pipeline. Figure 1-4 It’s actually more than just the one DLL shown in Figure 1-4. The other four DLLs are importers for the different kinds of content that you can use out of the box — shaders, FBX and X model files, and textures (which can be one of a half-dozen different formats). We’ll be looking at the content pipeline in a lot of detail later on, as it’s one of the biggest
XNA Framework and Game Studio
11
pieces that Microsoft has provided for making game development easier, but for now we’ll just say that it helps with the management of game assets such as graphics and sounds and makes loading and using them in your code much easier. Out of the box the content pipeline handles enough types of assets that you probably won’t need to create your own, but if you work with an application that uses a specific file type that isn’t handled by the basic content pipeline, a little work will let Game Studio Express handle it just like the basic types.
Framework Assembly Most of the namespaces are fairly self-explanatory, but we’ll go over a couple that aren’t. The Microsoft.Xna.Framework namespace contains classes and enums related to math and geometry functionality. More than Figure 1-5 anything else, you’ll probably be using the Vector2, Vector3, Rectangle, and Point classes if you’re doing 2D and the Matrix class if you’re doing 3D. If you need collision detection, you’ll probably use one of the Bounding classes to implement it. The MathHelper class in this namespace provides some helpful functions such as the Clamp, Min, Max, ToDegrees, and ToRadians methods. It also contains some constants that you’ll probably end up using regardless of whether you’re doing 2D or 3D work — Pi and several values that use Pi. We’ll use the classes in the Microsoft.Xna.Framework.Audio namespaces in Chapter 5 when we add sound and music capability to our game. The Microsoft.Xna.Framework.Content namespace gives you the ContentManager class, which you’ll use to load your game assets. An instance of this class is automatically created for you in the Game class if you use one of the game templates that comes with Game Studio. We’ll explore the classes in this namespace further in Chapter 10.
12
Chapter 1
The Microsoft.Xna.Framework.Design namespace contains a dozen classes for converting objects and data types to other objects and data types. We won’t be using the functionality in this namespace much, if at all, but go ahead and take a look at the classes since you never know when you might find something useful for your projects. The Microsoft.Xna.Framework.GamerServices namespace is new in Game Studio 2.0. It provides access to the Guide interface on the Xbox 360 (and to Live for Windows), gamer information such as a person’s profile, privileges that a gamer can perform in a game, game default properties, and more. Here’s the complete list of items in the namespace:
Figure 1-6
Let’s take a look at some of the more important pieces you’ll be using. The Gamer class contains Gamertag and a custom Tag object for the gamer. You can also get a list of Gamer objects for all the players on the local system using SignedInGamers. The Begin/EndGetProfile methods are asynchronous calls to get the profile for the specified gamer.
XNA Framework and Game Studio
13
Figure 1-7
Profile information is contained in a GamerProfile object:
Figure 1-8
This allows you to display information about the player in your game, which gives other players an idea of the type of gamer he or she is without having to look it up by gamertag. The SignedInGame class is a bit different from the regular Game class. Its members deal mainly with online-related data:
Figure 1-9
GameDefaults is a set of data that can be used for almost every game, although some are specific to certain types of games, such as first-person shooters or racing games. The dozen pieces of data in the GameDefaults class are:
14
Chapter 1
Figure 1-10
The Privileges member lets a person know what types of interaction the gamer can have with the system or game:
Figure 1-11
If your game, for example, has expansion packs or the ability to include content made by other players, you’ll want to check these properties before allowing the player to do anything. If the player doesn’t have any credit card information on file, it wouldn’t make much sense to show him content that can be purchased. The Guide class allows you to, not surprisingly, access the Guide user interface and other functionality. This means you now have the ability to allow players to enter text using the Begin/EndShowKeyboardInput methods. You can also do things like show the sign-in interface, messages, friends, gamer cards, and more. There’s a lot of functionality that most Xbox 360 gamers take for granted wrapped up in this class. Here’s the complete list of methods and properties:
XNA Framework and Game Studio
15
Figure 1-12
Most of the methods are asynchronous, meaning you start the process of showing the user interface component and specify a method to be called once the player is done interacting with the user interface. We’ll go over exactly how this is done in some detail in Chapter 6. By far the biggest in the assembly, the Microsoft.Xna.Framework.Graphics namespace will quickly become your friend as you explore the world of XNA game development. As graphics are the main focus of game development, it’s unavoidable that you’ll be using many of the classes and enums you’ll find here. The namespace contains over 150 objects ranging from 2D and 3D to classes that allow you to work with effects to structures that allow you to investigate the capabilities of the graphics device on a machine. You’ll also find the SpriteBatch class here for rendering 2D graphics, several Model… classes for working with 3D models, the VertexShader and PixelShader classes for working with shader code, several Texture classes, the SpriteFont class for rendering text, and the Viewport class, which allows you to do things like create split-screen games. Probably the second most important part of the assembly, the Microsoft.Xna.Framework.Input namespace is just what you think it is — a collection of classes and enums for working with input devices. A generic GamePad class and the typical Mouse and Keyboard classes are housed here. The big change in Game Studio for the typical .NET developer is that these devices are not used in an event-driven fashion. Since games are constructed differently from normal business apps, the method of dealing with input devices
16
Chapter 1
changes. You check the state of the device to see what controls on it are being used and handle things accordingly. It is possible to construct one or more classes to implement an event-driven system, however. We’ll do something like this in Chapter 4. The Microsoft.Xna.Framework.Storage class gives you access to the devices on which to read and write data on both the PC and Xbox 360. These can be hard drives, memory units, or USB devices such as thumb drives and portable hard drives. While you can use the System.IO namespace on the PC to access these devices, you don’t have access to all of these same classes on the 360, so if you want to use the same code on both platforms you’ll need to use the classes in this namespace. We’ll cover how to use these classes in some detail in Chapter 6. The newest addition to the Framework, the Microsoft.Xna.Framework.Net namespace contains all the functionality you’ll need to create online or system link multiplayer games. It’s a lot of functionality and a lot of power that you get for your games.
Figure 1-13
XNA Framework and Game Studio
17
We’ll go over making a multiplayer version of our game in Chapter 14. Fortunately, the XNA Team has made it less difficult to do this than if you were using even regular .NET functionality. There’s still a lot of things you have to take into consideration and you’ll need to do a lot of testing for various situations, but the XNA community is rejoicing at the inclusion of this in XNA 2.0.
What Is XNA Game Studio? XNA Game Studio is a modified version of Visual C# 2005 that allows you to create XNA games for both Windows and Xbox 360. The first beta, released in August 2006, did not include some of the features now available such as the content pipeline and functionality for easily creating 3D games and for developing Xbox projects. It did include a starter kit for creating a Spacewar game. There were some great 2D games developed using this beta version. Following another beta version, the 1.0 version was released in December 2006. Almost immediately more great games started popping up. A lot of additional content has already been released for use with Game Studio — sample games, starter kits (discussed later in this chapter), and tutorials. GarageGames has also released the first version of their Torque X software, which integrates with Game Studio, and other companies are preparing software for use with Game Studio. A 1.0 Refresh version was released in April 2007, adding a lot of new functionality as well as the ability to create a package to share your creations with other Creators Club members. Previously you had to distribute your code as well as your assets in order for others to be able to run your game. Version 2.0 was released December 13, 2007, after almost a month of open beta. In addition to a lot of fixes and improvements, it added one of the most desired pieces of functionality — the ability to create networked games that can be played over the Internet via the Live system on both the Xbox 360 and Windows PCs. In the week following its release, many of the samples and other content were updated for the new version, and much more has probably been released in the time between the writing of this book and you reading it.
18
Chapter 1
At this time Game Studio only officially supports the C# language, either in C# Express or Visual Studio 2005 (although you lose access to some of the project templates in Visual Studio 2005). While the XNA Framework DLLs are standard managed code and can be used by any .NET language and IDE that supports managed code, there are features in Game Studio that make it worth using and giving up the features of the other languages and IDEs. Among the Game Studio features are the templates and starter kits for getting a game up and running with virtually no code needed by the developer, the Game Component and Sprite Font templates, and the built-in content pipeline. All of these features combine to make Game Studio an extremely powerful game development environment.
Installing XNA Game Studio There are several steps necessary for getting up and running with Game Studio, depending on the operating system you’re using. You’ll need to be running Windows XP SP2 or Windows Vista. These are the only supported operating systems. First, you’ll have to download and install Visual C# Express 2005 or have Visual Studio 2005. You’ll then need to download and install the service pack for C# Express or Visual Studio. If you’re using Windows Vista, you’ll also have to download and install the Visual Studio 2005 Service Pack 1 Update for Windows Vista. Once you’ve installed and registered C# Express you can download and install Game Studio. You can find more information and videos on the XNA Creators Club Online web site at http://creators.xna.com. If you’re moving to XNA 2.0 from an earlier version, you’ll also probably want to download and install the Project Upgrade Wizard. While you can create Xbox 360 games using Game Studio at no cost, if you wish to deploy and play them on the Xbox 360 (or play XNA games from other people), you’ll need to purchase a Creators Club membership. Steps for doing this and setting up your PC and Xbox 360 will be covered along with Xbox 360 development in Chapter 12.
XNA Framework and Game Studio
19
Project Types After installing XNA Game Studio, you’ll notice several new project templates have been added when you bring up the dialog to create a new project. These templates help you get your game up and running quickly by adding the skeleton code that you always need to write — your Main procedure, creating an instance of the Game object and calling its Run method, providing a class that inherits from the Game class and adding the implementation of the half-dozen methods that you’ll fill in with your game logic, etc.
Windows Game The Windows Game template is the base template for doing game development with Game Studio. It creates a very basic skeleton for you. Once you’ve opened a project based on this template, you’ll see two code files — program.cs and game1.cs. The program.cs file is the starting point of your game. All it does is create an instance of the Game class and call its Run method: Listing 1-2 using System; namespace WindowsGame1 { static class Program { /// /// The main entry point for the application. /// static void Main(string[] args) { using (Game1 game = new Game1()) { game.Run(); } } } }
The game1.cs file contains a class based on the Game class in the XNA Framework:
20
Chapter 1
Listing 1-3 using using using using using using using using using using
System; System.Collections.Generic; Microsoft.Xna.Framework; Microsoft.Xna.Framework.Audio; Microsoft.Xna.Framework.Content; Microsoft.Xna.Framework.GamerServices; Microsoft.Xna.Framework.Graphics; Microsoft.Xna.Framework.Input; Microsoft.Xna.Framework.Net; Microsoft.Xna.Framework.Storage;
namespace WindowsGame1 { /// /// This is the main type for your game /// public class Game1 : Microsoft.Xna.Framework.Game { GraphicsDeviceManager graphics; SpriteBatch spriteBatch; public Game1() { graphics = new GraphicsDeviceManager(this); Content.RootDirectory = “Content”; } /// /// Allows the game to perform any initialization it needs to before /// starting to run. This is where it can query for any required /// services and load any non-graphic related content. Calling /// base.Initialize will enumerate through any components and /// initialize them as well. /// protected override void Initialize() { // TODO: Add your initialization logic here base.Initialize(); } /// /// LoadContent will be called once per game and is the place to load /// all of your content. /// protected override void LoadContent()
XNA Framework and Game Studio
{ // Create a new SpriteBatch, which can be used to draw textures. spriteBatch = new SpriteBatch(GraphicsDevice); // TODO: use this.Content to load your game content here } /// /// UnloadContent will be called once per game and is the place to /// unload all content. /// protected override void UnloadContent() { // TODO: Unload any non ContentManager content here } /// /// Allows the game to run logic such as updating the world, /// checking for collisions, gathering input, and playing audio. /// /// Provides a snapshot of timing /// values. protected override void Update(GameTime gameTime) { // Allows the game to exit if (GamePad.GetState(PlayerIndex.One).Buttons.Back == ButtonState.Pressed) this.Exit(); // TODO: Add your update logic here base.Update(gameTime); } /// /// This is called when the game should draw itself. /// /// Provides a snapshot of timing /// values. protected override void Draw(GameTime gameTime) { graphics.GraphicsDevice.Clear(Color.CornflowerBlue); // TODO: Add your drawing code here base.Draw(gameTime); }
21
22
Chapter 1
} }
All of the functions in this class are called automatically by the XNA Framework. The constructor is called when the instance of the class is created. The Initialize method is called after the object is created. The LoadContent method is also called when the object is created, after the Initialize method. The UnloadContent method is called when your game ends and allows you to clean up after yourself. After the game is initialized and starts running, the Update and Draw methods are repeatedly called. Depending on whether or not the game is set to run at the refresh rate of your monitor (which defaults to true), the two methods will get called either at the refresh rate (for example, a refresh rate of 75 Hz will cause the methods to be called 75 times per second, or approximately every 133 milliseconds) or as quickly as possible. The SynchronizeWithVerticalRetrace property of the graphics device specifies which timing is used. You should usually be fine with leaving the property set to true. The result of using this template isn’t pretty, but it does for you what would take dozens or more of lines of code in C++:
Figure 1-14
Once you have this skeleton you can fill it in with your gameplayspecific code and not worry about any of the behind-the-scenes stuff that most game developers have to deal with such as handling and restoring a lost graphics device (for instance, the user changes the
XNA Framework and Game Studio
23
resolution during the game or drags the window from one monitor to another in a multiple monitor setup).
Windows Game Library The Windows Game Library template allows you to create a library of reusable objects and code. If you find yourself writing the same code over and over to do things that most games will require, it might be wise to package it up in a library instead of copying files from project to project. You can use a game library to store components like a frame rate counter, input system, or even a rendering engine if you’re ambitious (and knowledgeable) enough. Code reuse is something that you would do well to practice. You’ll save time in coding and debugging. Multiply this time by the number of games you create and it starts to add up. Time is money, after all.
Xbox 360 Game The Xbox 360 Game template is virtually identical to the Windows Game template, but targeted at the 360.
Xbox 360 Game Library The Xbox 360 Game Library template is virtually identical to the Windows Game Library template, but targeted at the 360.
Starter Kits The XNA Game Studio team has provided several starter kits for use with XNA Game Studio. Each starter kit contains a working version of a specific type of game. This game can be expanded upon or changed as needed to allow you to quickly create your game without having to write a lot of code.
24
Chapter 1
Spacewar The Spacewar starter kit was released with the first beta of Game Studio. Developed by Andy Dunn of ZBuffer Games, this starter kit gives you a ready-to-run two-player game complete with multiple weapons and ships.
Figure 1-15
Marblets The Marblets starter kit is similar to the Bejeweled type games that became popular back around 2000. PopCap Games is one of the most well-known developers of these types of games.
Figure 1-16
XNA Framework and Game Studio
25
Racing Game Developed by Ben Nitschke of exDream entertainment, the Racing Game starter kit was released in April 2006. It comes with three different tracks for three levels of difficulty. Because this game makes extensive use of shaders and implements such features as motion blur, you’ll need a fairly beefy graphics card to run it. It would be well worth the cost of upgrading to be able to dig into the code for this game and tweak it to change the way the game works.
Figure 1-17
Ship Game The Ship Game starter kit is the newest one to be released. Developed by Fabio Policarpo, the game is very reminiscent of the old Descent games (which I personally loved). Players fly around in a series of tunnels and rooms where there is really no up or down. The kit makes full use of the advanced effects available in Game Studio. While the multiplayer component is only split-screen at the moment, I expect someone to soon modify it to support LAN or Internet play.
26
Chapter 1
Figure 1-18
Content Pipeline Overview The content pipeline is one of the pieces of technology that makes game development so easy when using Game Studio. Say you want to display a splash screen when your game starts. Normally, you’d have to write a bunch of code to open the file and load it into an in-memory object. With the content pipeline, this gets reduced to one line of code: Texture2D splashScreen = content.Load(“splashscreen”);
You’re then ready to draw the graphic. That one line of code is pretty deceiving. What’s happened behind the scenes is probably better explained by a picture (you know, since it’s worth a thousand words and all):
XNA Framework and Game Studio
27
Figure 1-19
As you can see, there’s five parts of the content pipeline — the importer, the DOM (basically the repository for the content in the project), the processor, the compiler, and the manager. Each type of file has its own importer, but similar types of content are grouped together for the processor, as you can see when you look at the Content Importer and Content Processor properties of a piece of content that you include in Figure 1-20 a project, as shown in Figure 1-20. Note that some types of content have more than one option for processing, such as a texture. One of the nice things about the content pipeline is that you can use a meaningful name to load the assets instead of a possibly arbitrary file name. You don’t need to remember that the mesh file for the rocket launcher is “weapon4.x”; you could call it “rocket launcher.” By default, the name of the piece of content is the text to the left of the “.”, but you can set it to whatever you’d like (preferably something descriptive, like with the rocket launcher) in the property window in the Game Studio IDE. To load a different type of asset, say an audio file, you just change the type in the Load method and the name of the piece of content.
28
Chapter 1
Summary This has been a very quick, mile-high view of Game Studio and the XNA Framework, along with some of the more important pieces of it. Hopefully you’re ready to start writing code and creating a game. If not, we’ll wait. When you’re ready, head on over to the next chapter and get ready to learn about some specifics of game development and take a look at the framework on which we’ll be building our game.
Chapter 2
Game Development Overview Parts of a Game If you look at the credits for most games these days you’ll see anywhere from several to several dozen names in the programming area. All those people don’t work on every area of a game; many specialize in just one. The recently released game BioShock, for instance, had a person dedicated just to making the water look and act realistic. Since the game took place mostly underwater, with water visible in many of the locations of the game, it would make sense to ensure that it didn’t distract the player and break the immersion of the game by not looking and acting the way players would expect it to. Games today are such huge projects and technology advances so rapidly that it would be difficult for someone to keep abreast of the latest changes in every area. Even if you spent all your time studying and working with all the various game technologies out there, it would be difficult, if not impossible, to gain and maintain the knowledge necessary to use them to their full capabilities. Let’s take a quick look at some of the areas that you’ll see in action in most games today.
29
30
Chapter 2
Graphics Debatably the most important part of the game, graphics are definitely something you’ll spend a lot of time writing code for (unless you’re using an existing engine), even for the simplest game since it’s what immediately grabs the attention of players. Great graphics won’t ensure a great game, but poor graphics will immediately decrease the rating of your game in the eyes of most players, and game magazine and web site editors who can sometimes make or break the sales of games with their reviews. The first thing you’ll need to decide on for the graphics system for your game is whether to go 2D, 3D, or even somewhere in between (commonly referred to as 2.5D). If you decide on 2D, you still have some choices to make, such as: n
What kind of view you should use — top-down, side-scrolling, isometric, etc. While the view might be influenced by the type of game you’re working on, if it’s not, this is a decision that could greatly influence how the community receives your game. For example, puzzle games usually don’t use an isometric view, although there are exceptions to every rule, while an RPG could use any of the views mentioned.
n
How the art will be created — drawn pixel-by-pixel? Do you create 3D models for the sprites and render views of them out to 2D image files and use 2D backgrounds? Do you use some other method? Again, the type of game might influence this and could even make it a non-issue.
n
How colors will be handled. If you’re doing a multiplayer shooter, do you have the same character with different colored equipment for each player? If so, do you create images with all the different colors or do you do the coloring with code?
If you decide on 3D, the choices don’t get much easier: n
If the camera isn’t fixed (meaning the player can zoom in and out on his view of the game world), how do you handle level-ofdetail? If you’re not familiar with the term, level-of-detail partially refers to decreasing the complexity of a 3D object as it moves away from the camera. It is also about decreasing the quality of textures for these objects, a process known as mipmapping. Think about when you look at objects that are far
Game Development Overview
31
away and how you can’t really make out details of the objects. Since you can’t make out details, there’s no sense in rendering them. By not doing so, you gain some performance benefit. n
What format do you use for your models? This will probably be somewhat dictated by whoever is doing your modeling. If that person or team is using a package for which XNA Game Studio doesn’t have built-in support, do you write an importer/converter for the content pipeline for that format, or do you try to find a plug-in for that package to get the model to a format that is handled?
n
How much, if any, use of shaders do you want to have? Obviously, if you’re just getting started in game development the answer will probably be “None.” Once you’ve gotten some experience under your belt, however, you’ll probably have to ask yourself if you want to go to the trouble of learning HLSL. Since more and more games are shader driven, it would be a good idea to learn HLSL, even if you’re just doing a 2D game. If you’re going to try to make some money doing game development, any edge you can get in this highly competitive industry will help. If you’re doing it simply for fun or as a hobby, it might not be an issue. We’ll cover shaders a bit later on, but obviously an exhaustive study of the language is beyond the scope of this book.
If you’re just getting into game programming and you want to do some graphics programming — no matter if it’s 2D or 3D — you’ll want to make sure you have a good grasp of some areas of mathematics, mainly matrix arithmetic, geometry, and trigonometry. While many graphics APIs will hide most of the gory details of working with more advanced math, it’s still a good idea to make sure you know what’s going on behind the scenes.
Input Device(s) While input is a critical piece, it’s probably one of the easiest areas for most games. The mouse, keyboard, and controller have set things they can do and you won’t find many surprises when working with them. The only issue is if you’re planning on your game being released under both PC and Xbox 360. That means a kind of balancing act in giving the player the option of using the available devices
32
Chapter 2
on a specific platform as well as ensuring all the devices available for each platform work the best for that platform. If your game is more than just a point and click, you should consider giving the player the option of changing the control configuration and saving it. What you think might be the best configuration might not suit everyone. Your game will be much better received if you give the player the option of setting up the controls to their liking. We’ll be going over one way to do this in Chapters 4 and 6.
Networking If you’re creating a multiplayer game, one of the biggest hurdles you’ll face is getting your network code to be as efficient as possible. Players won’t tolerate for very long things like lag, waiting for connections when the game starts, delays in finding games, etc. They’ll more than likely uninstall your game and play one that handles these things better. For Xbox 360 games some of the burden is handled for you. You start with the existing Live infrastructure and build on top of it. The network functionality in XNA Game Studio 2.0 makes developing multiplayer games much easier than if you were using plain C++ or even the classes provided in the .NET Framework. Some things you’ll normally have to think about in developing your network code are: n
How does the game find other games? Many games are set up to communicate with a central server that stores information about games that are currently being played and games where players are looking to create or join a game. The game will display an interface to provide you with the information you need to find a game. This information will probably include things like the ping to the server (regardless of whether that server is a player acting as the host or a dedicated server), the number of players currently in the game and the maximum number of players for that server, the type of game (deathmatch, capture the flag, etc.), the name of the current level, and the name of the server (so you can keep track of good servers that you might frequently want to join).
Game Development Overview
33
n
Do you want to allow the player to just jump into a game without having to pick from a list? Some games have taken to providing this feature, including the well-known Halo. Some games will allow you to specify criteria to determine the game that’s selected — the type of game, number of players, etc.
n
If your game uses a player-based host as the server, how will you handle cases where the host gets disconnected? In some cases, it might be necessary to end the game; in others, one of the other players could become the host.
n
What information will have to be sent between the players and server? Will that information be able to fit inside a standard packet? For those who haven’t done any multiplayer programming, think of a packet as a letter full of data describing the current game that needs to be sent to players.
n
How often will packets be sent? This may be dependent on the type of game. Obviously you don’t need to send a packet every frame for a turn-based game.
n
What protocol do you want to use? TCP/IP and UDP are the two options here. There are several differences between the two that you’ll have to consider when making your decision.
Table 2-1 TCP
UDP
Connection-oriented service; a session is established between hosts.
Connectionless service; no session is established between hosts.
TCP guarantees delivery through the use of acknowledgments and sequenced delivery of data.
UDP does not guarantee or acknowledge delivery, or sequence data.
Programs that use TCP are provided assurance of reliable data transport.
Programs that use UDP are responsible for providing any reliability needed to transport data.
TCP is slower, has higher overhead requirements, and only supports point-to-point communication.
UDP is fast, has low overhead requirements, and can support point-to-point and point-tomultipoint communication.
34
Chapter 2
Some types of games usually use one protocol over the other. Shooters and faster paced games usually go with UDP since the speed is more important than the guarantee of delivery. If a packet is lost, the game will just compensate the next time it receives one. While this can cause problems, these can be overcome by having the game manually require the delivery of some packets (say one where a player is killed). Slower paced games such as RPGs or turn-based games don’t always need the speed that UDP provides so they can use TCP and gain the benefit of guaranteed delivery of information, which means the programmer doesn’t have to handle or worry about it.
AI Unless you’re doing a casual game or a multiplayer-only game that supports only real people, you’ll more than likely have to spend some time creating an AI system. The complexity depends on several factors — the type of game, the knowledge of the person doing the AI programming, and the time you have to create the system. Good AI in some types of games (RTS games, for example) is extraordinarily difficult to do without cheating. Giving the AI player in an RTS game full view of the map and allowing it to make decisions on this information is going to be frowned upon by players when they figure out that’s what’s happening. And they will figure it out, have no doubt. Players these days will pull your game apart piece by piece to see what makes it tick and how they can beat it. There have been several books written on AI game programming. If you’re putting together a game that requires decent AI, I would suggest either picking one of them up or spending some time researching it on the Internet. If your game is going to feature both human opponents and AI and you have to choose which one to do first, I would suggest working on the code to allow human opponents to play before trying to implement AI opponents. Human intelligence is going to be more challenging for a player than AI, making the game more fun for most types of players. You should, ideally, design your game in such a way that the code doesn’t know if the opponent is AI or human, so that it’s easier to add the AI opponents when you’re ready to do so. If you can design it so that the player doesn’t know if an opponent is human or AI, even better.
Game Development Overview
35
Since AI is not included in the XNA Framework, we won’t be spending much time on it for our game. The AI will literally have a one-track mind — head toward the player and try to kill him. If you’re feeling really adventurous, once you’ve got a good handle on using the XNA Framework you can try to make the AI smarter by giving it the ability to dodge shots, work as a team to box in the player, etc. Perhaps you’ll be one of those rare people who enjoys AI programming; the industry is always going to need people who specialize in that area. If you do decide to look into AI programming, you’ll find some links to resources in the appendix.
GUI/User Interface You can’t really play most games without having an interface, although some games are trying to minimize them to give more of a feeling of immersion. Try playing an FPS without the crosshair for aiming. It would be pretty difficult, wouldn’t it? An MMORPG without an interface would be basically impossible. How would you change game options or load and save games without dialogs? You could probably do it all using the keyboard, but it would be extremely difficult and I wouldn’t recommend it. Designing a GUI takes more artistry than programming, but you can put together a basic interface even if you’re artistically challenged. Menus and things like lists and options can be created using plain text, as well as displaying information to the player like the number of rounds of ammo left in the weapon, the stats of the character, score, time remaining, etc. “Programmer art” can be used for most other pieces — buttons and icons, for example. Ideally you’d find someone to replace these with quality work before releasing it to the community. Don’t let the lack of graphics for a GUI hold up development. Drop something in that works and use it until you have to replace it.
Physics System Physics systems are becoming more popular with the big games. From racing games like Project Gotham where the cars operate like you expect them; to shooters like Crysis where bullets can destroy objects and explosives have a blast radius and affect the terrain; to even RPGs like Oblivion where the world feels alive partly because
36
Chapter 2
of the way you can interact with it, the ability for the game world to operate and feel like the real world is currently the big thing (besides graphics). We’ve even gotten to the point where we’re seeing hardware support for physics, namely the Ageia physics system cards.
Sound/Music Think of any big game you’ve played recently. Now think of it without any sound or music. Imagine Halo without either one. It would totally change the game, wouldn’t it? Sound and music are usually not noticed when they’re done well. Their absence will be noticed, however, as well as if they don’t sound like what the player thinks they should. Imagine if a character were firing a rocket launcher and the sound that was triggered was that of a bubble popping. It would break the suspension of disbelief, wouldn’t it? You just assume that sounds will be there and they’ll sound like what we’re used to them sounding like because that’s how the world works for those of us who hear. n
n
n
When the game first starts up, it still has to render the first screen that you see and, even then, there could be something going on behind it. Half-Life 2 is a good example. When the main menu is showing, you’ll see a limited view of the game world running behind it, almost as if you were playing the game. One of the realizations that you’ll come to at some point is that a game is really just data of many different kinds and that data has to come from somewhere and be handled somehow. Graphics files, both 2D and 3D; music and sound effects files; information about objects in your game and how they behave (physics behavior, AI behavior patterns, etc.); text files for things like dialogs or player information — they all have to work together to make the game the experience it should be. How do you do this though? Do you load it all when the game starts so it’s always available? That’s not really an option for most games these days, since the amount of all that data can be upward of a couple of gigabytes. You need a way to know when to load specific data and what needs to be rendered to the screen every frame. This is where the idea of game states comes in.
Game Development Overview
37
Game States If you know what’s going on in the game, you can usually figure out what kind of processing needs to be done. When you’re at the main menu, you’re not going to be rendering the game world. If the player pauses the game, you might still want to render the game world, maybe in black and white, but you probably don’t want the AI to think or anything else to happen that furthers the gameplay. While the game is being played you’ll probably want a finer grain of game state, possibly depending on the type of game. A game like chess could possibly have states of player’s turn and AI’s turn. A card game could have states such as dealing the cards, betting, playing cards, and resolving the winner of the hand. Here’s what your code might look like for implementing a very basic game state system: Listing 2-1 public enum GameState { MainMenu, Options, PlayingGame } public enum InGameState { None, TalkingToPerson, BuyingStuff, Fighting, Sleeping } public class Game1 : Microsoft.Xna.Framework.Game { GraphicsDeviceManager graphics; InGameState _curInGameState; GameState _curState; public Game1() {
38
Chapter 2
graphics = new GraphicsDeviceManager(this); _curState = GameState.MainMenu; _curInGameState = InGameState.None; } protected override void Update(GameTime gameTime) { switch (_curState) { case GameState.MainMenu: //if player selected Start Game if (mnuNewGame.Selected == true) _curState = GameState.PlayingGame; else if (mnuOptions.Selected == true) _curState = GameState.Options; else if (mnuQuit.Selected == true) this.Exit(); if (GamePad.GetState(PlayerIndex.One).Buttons.Back == ButtonState.Pressed) this.Exit(); break; case GameState.Options: //render options dialog if (mnuOptionsBack.Selected == true || GamePad.GetState(PlayerIndex.One).Buttons.Back == ButtonState.Pressed) _curState = GameState.MainMenu; else { //check for other options dialog controls selected } break; case GameState.PlayingGame: if (GamePad.GetState(PlayerIndex.One).Buttons.Back == ButtonState.Pressed) { _curState = GameState.MainMenu;
Game Development Overview
continue; } //do game stuff switch (_curInGameState) { case InGameState.BuyingStuff: break; case InGameState.Fighting: break; case InGameState.None: break; case InGameState.Sleeping: break; case InGameState.TalkingToPerson: break; } break; }
base.Update(gameTime); } protected override void Draw(GameTime gameTime) { graphics.GraphicsDevice.Clear(Color.CornflowerBlue); switch (_curState) { case GameState.MainMenu: //render menu
39
40
Chapter 2
break; case GameState.Options: //render options dialog break; case GameState.PlayingGame: //render game screen break; } base.Draw(gameTime); } }
Note that this code isn’t meant to be compiled as is; it’s just an example. If you try to compile it, you’ll get some errors. Here’s what’s going on — the GameState enum tells us that we have three possible states in which the game could exist. The player can be looking at the main menu, looking at the options screen, or playing the game. Initially the variable holding the value of the current state is set in the constructor to the main menu. Looking down at the Update method we see how the different states are handled. If the player is looking at the main menu, we do some checks to see if he’s selected one of the menu items. If he has, we set the variable holding the current game state as appropriate so that when the Draw method is called the proper thing will be drawn. If the options menu is being displayed, we check to see if the menu item for going back to the main menu has been selected or the Back key was pressed and, if so, we set the state back to the value for the main menu. What happens in any other case in the options screen depends on what it allows. If the player is playing the game, a lot of things could be happening. From the options given it looks like this might be an RPG (talking to people, buying stuff, etc.) so there are a lot of states. There would obviously be a lot to this section, much of it probably wrapped up in classes.
Game Development Overview
41
The same kind of thing happens in the Draw method. We know what has to be rendered based on the variable holding the current game state. As we’ll see later on in this chapter, we’ll handle this by having a manager for the different types of screens that can be displayed, the main menu, playing the game, and pausing the game. This allows you to easily expand the game if you want to implement new features. Want to add the ability to save your game? Add a save game screen and have the manager display it when necessary. The same would go for options, dialogs, things like inventory screens, HUDs, etc. Even within the screens we could have substates that we use if the game is fairly complex.
The Game Loop — XNA Style A game, like every other application, is constantly doing something. Whether it’s rendering the game to the screen, processing network packets for a multiplayer game, or having the AI decide what to do based on the current game conditions, there’s always something going on. All of this happens within what is called the game loop, like so: Listing 2-2 while (gameIsRunning) { DoInput(); DoNetworkMessages(); //if multiplayer, although this could also, and probably should, be in a separate thread UpdateWorld(); Render(); }
Prior to XNA and other game frameworks, developers usually had to implement this game loop themselves as well as make sure that things such as the player changing the resolution or plugging in a controller were caught and handled. XNA makes it a bit easier for budding game developers. All the normal initialization code is done in the Framework so you never need to worry about it (although you can change some of the things that are set by default —
42
Chapter 2
backbuffer size, for example). It also sets up the loop for you, hiding it in the guts of the Framework. The input, update, and render procedures all get called automatically; you just fill in the update and render methods. We saw the typical skeleton for an XNA game in the previous chapter that gets created in the program.cs and game1.cs files. If you do anything more complicated than, say, tic-tac-toe, you’re probably going to have a lot of components to handle the things in the non-XNA game loop above such as input, networking, rendering, etc. You’re not going to do it all in the Game object. It would get much too disorganized very quickly. You’ll definitely want to take advantage of the systems that have been built into the XNA Framework such as game components and services. The object-oriented nature of the Framework should make it much easier to organize your code into self-contained objects that you can reuse in all your games.
Calculating Frame Rate One of the biggest problems you’ll have to worry about is making sure your game runs smoothly, especially on older systems. While this isn’t a problem with most 2D games, there’s usually a lot more happening in a 3D game and all those things need horsepower to handle properly. You can’t guarantee that those playing your game will be running top-end hardware or even medium level. One way of determining how well your game will run is to check the frame rate of the game and make sure it’s high enough. The frame rate is basically how many times per second your game loop runs, assuming you draw to the screen every frame. You can also look at it as how many times per second you redraw the screen. Anything from 30 to 40 or more frames per second will produce a smoothly running game. It’s possible for a game to run smoothly down to about 20 frames per second, but it depends on the type of game and what’s happening during gameplay. Here’s one example of calculating the frame rate in an XNA game:
Game Development Overview
43
Listing 2-3 private int _framerate = 0; private int _counter = 0; private float _elapsedTime = 0.0f; public override void Update(GameTime gameTime) { _elapsedTime += gameTime.ElapsedGameTime.Milliseconds; if (_elapsedTime >= 1000.0f) { _elapsedTime -= 1000.0f; _framerate = _counter; _counter = 0; } }
XNA does some of the work for us. The Update method is called automatically for us every time through the loop. It also keeps track of the time that’s elapsed since the last time the Update method was called. We simply add that time to a variable until the value of that variable is equal to or greater than 1000. Pretty easy, huh? In Chapter 3 we’ll create and implement a class that does the calculation and draws it on-screen. This class can be dropped into any existing XNA Game Studio project and hooked in to the Components collection of the game. All you have to do is tell it where to draw the information.
Game Components GameComponents are classes that implement at least one interface that specifies the basic functionality that the class must have in order to be added to a Game object’s Components list. The IGameComponent interface must be implemented, and it’s usual to implement the IUpdateable interface as well. You can also implement a third interface, IDisposable, which isn’t a strictly XNA Framework interface. If you’re unfamiliar with interfaces, think of them as contracts. By implementing an interface, the class agrees to provide the functionality for the abstract methods in the
44
Chapter 2
interface. You have to do this in order for the XNA Framework to be able to interact with the component, such as automatically calling the Update and Draw methods. Let’s take a quick look at two of the interfaces from which a GameComponent inherits that are unique to the XNA Framework: Listing 2-4 public interface IGameComponent { // Summary: // Called when the component should be initialized. This method can be // used for tasks like querying for services the component needs and // setting up non-graphics resources. void Initialize(); } public interface IUpdateable { // Summary: // Indicates whether the game component’s IUpdateable.Update // method should be called in Game.Update. // // Returns: // true if IUpdateable.Update should be called; false otherwise. bool Enabled { get; } // // Summary: // Indicates when the game component should be updated relative to // other game components. Lower values are updated first. // // Returns: // When the game component should be updated. int UpdateOrder { get; } // Summary: // Raised when event EventHandler // // Summary: // Raised when event EventHandler
the IUpdateable.Enabled property changes. EnabledChanged;
the IUpdateable.UpdateOrder property changes. UpdateOrderChanged;
// Summary: // Called when the game component should be updated. // // Parameters:
Game Development Overview
45
// gameTime: // Snapshot of the game’s timing state. void Update(GameTime gameTime); }
The IGameComponent interface tells the class that it only needs to implement an Initialize method. IUpdateable brings the Update method and some associated members to the component. Once you’ve implemented the required methods, you can add the component to the Game object’s GameComponents list and have it automatically call the component’s Update method or you can use it any other way you want. We’ll be using game components both ways in our game. The XNA Framework provides one other interface that your components can implement, the IDrawable interface, which allows the component to draw itself to the screen. Here’s the interface: Listing 2-5 public interface IDrawable { // Summary: // The order in which to draw this object relative to other // objects. Objects with a lower value are drawn first. // // Returns: // Order in which to draw this object relative to other objects. int DrawOrder { get; } // // Summary: // Indicates whether IDrawable.Draw should be called in Game.Draw // for this game component. // // Returns: // true if IDrawable.Draw should be called; false otherwise. bool Visible { get; } // Summary: // Raised when event EventHandler // // Summary: // Raised when event EventHandler // Summary:
the IDrawable.DrawOrder property changes. DrawOrderChanged;
the IDrawable.Visible property changes. VisibleChanged;
46
Chapter 2
// Draws the IDrawable. // // Parameters: // gameTime: // Snapshot of the game’s timing state. void Draw(GameTime gameTime); }
Implementing this interface and overriding the Draw method allow the game to call the component so the component can render whatever it needs to render. If you want your components to be able to be used by other code in the game, you’ll probably want to provide a service for the component.
Game Services Game services are a mechanism that allows objects to interact with each other without requiring the holding of a reference. They do this through a mediator, usually the Services collection member of the Game object if you’re using one of the templates that comes with XNA Game Studio. A service is basically just an interface that allows code to query it and get to the members or events of the object that implements the interface. To use a service along with a game component, you first need to create the service. Say we’re creating a GameLevel component and we want a character object to be able to perform some basic activities in that level — search a room, see what exits are available, move from room to room, etc. These activities are the things we’ll provide through a GameLevelService interface, like this: Listing 2-6 interface IGameLevelService { LevelRoom GetExitRoom(LevelRoom room, Direction direction); List GetRoomItems(LevelRoom room); List GetRoomExits(LevelRoom room); }
We then create our GameLevel component:
Game Development Overview
Listing 2-7 public class GameLevel : Microsoft.Xna.Framework.GameComponent, IGameLevelService { private List _rooms; public GameLevel(Game game) : base(game) { } public void Load() { //load the room data here } public override void Initialize() { base.Initialize(); } public override void Update(GameTime gameTime) { base.Update(gameTime); } public LevelRoom GetExitRoom (LevelRoom room, Direction direction) { return _rooms[_rooms.IndexOf(room)].GetExitRoom(direction); } public List GetRoomItems (LevelRoom room) { return _rooms[_rooms.IndexOf(room)].RoomItems (); } public List GetRoomExits(LevelRoom room) { return _rooms[_rooms.IndexOf(room)].Exits(); } }
The class inherits from GameComponent and implements the IGameLevelService interface. A game level is made up of a list of LevelRoom objects:
47
48
Chapter 2
Listing 2-8 public class LevelRoom { private List _items; private List _exits; private List _rooms; public List RoomItems() { return _items; } public LevelRoom GetExitRoom(Direction direction) { return _rooms[(int)direction]; } public List Exits() { return _exits; } }
A room may contain some items, which is reflected by the _items list. For our purposes, this is a simple class with a Name string member. The room has one or more exits, which is a list of Direction enum values: Listing 2-9 public enum Direction { North, South, East, West }
Each of those exits links to a room, which we keep in the _rooms member. We now have our game level and the service we’ll use with it. In our Game object we create an instance of the component, add it to our GameComponents list, create an instance of the service, and add it to the Services list while tying it to the component:
Game Development Overview
Listing 2-10 public Game1() { graphics = new GraphicsDeviceManager(this); Content.RootDirectory = “Content”; GameLevel gameLevel = new GameLevel(this); gameLevel.Load(); this.Components.Add(gameLevel); this.Services.AddService(typeof(IGameLevelService), gameLevel); }
Finally, we create an Entity class that represents the player and gives him the ability to access the game level: Listing 2-11 public class Entity: GameComponent { private LevelRoom _room; private List _items; public Entity(Game game) : base(game) { } public void Search() { IGameLevelService gls = (IGameLevelService)this.Game.Services .GetService(typeof(IGameLevelService)); _items.AddRange(gls.SearchRoom(_room)); } public void Move(Direction direction) { IGameLevelService gls = (IGameLevelService)this.Game.Services .GetService(typeof(IGameLevelService)); _room = gls.Move(_room, direction); }
49
50
Chapter 2
public List GetExits() { IGameLevelService gls = (IGameLevelService)this.Game.Services .GetService(typeof(IGameLevelService)); return gls.GetRoomExits(_room); } }
When the entity wants to access the game level, we use the GetService method of the Services list to get the service for the GameLevel object. We then use that service to access the properties and methods of the GameLevel object that has been exposed to the service. A visual representation of the relationships between components and services might make things a bit easier to understand. The following figure shows the example we’ve created:
Figure 2-1
Hopefully this brief look at game components and services has given you a little bit of an understanding of how they work and the power that they contain. We’ll be using a couple of game component objects, mostly drawable components, in our game and a service for our input system so our game code can get to the input devices to determine what the player is doing.
Game Development Overview
51
Creating Your First Application Let’s take a look at the skeleton upon which our game will be built. We’re going to use the Game State Management sample that the XNA Team released to the community. You can find it on the XNA Creators Club Online site (http://creators.xna.com) or in the download file for this chapter. Unzip the file to a folder on your machine and open the Solution file in the GameStateManagementSample folder using XNA Game Studio. Next, open the Object Browser, drill down into the project’s namespace, and click on the GameScreen class as shown in Figure 2-2.
Figure 2-2
52
Chapter 2
The GameScreen class is the base class for all the other screens. It’s a fairly straightforward, nicely commented class. The inclusion of the transition effect to fade the screens in and out is a very nice feature that will help the game look a little more professional than if they were to just pop in and out. You can easily turn this feature off by setting the TransitionOnTime and TransitionOffTime properties. Note the two methods of making a screen go away. The ExitScreen method will make the screen transition out normally. If you look at the comments for that method, however, you’ll notice that you can make a screen go away without the transition by calling the RemoveScreen method of the ScreenManager class (which we’ll look at shortly). During a game you might not want screens to fade in and out. The RemoveScreen method gives you the ability to do this. The LoadingScreen is only displayed right before the GameplayScreen. If your game loads quickly enough you could easily remove this screen. Take a look at the OnSelectEntry method in the MainMenuScreen class: Listing 2-12 protected override void OnSelectEntry(int entryIndex) { switch (entryIndex) { case 0: // Play the game. LoadingScreen.Load(ScreenManager, LoadGameplayScreen, true); break; case 1: // Go to the options screen. ScreenManager.AddScreen(new OptionsMenuScreen()); break; case 2: // Exit the sample. OnCancel(); break; } }
Game Development Overview
53
The code in the 0 case displays the LoadingScreen. If you have no need for a loading screen you could replace the line with the following: foreach (GameScreen screen in screenManager.GetScreens()) screen.ExitScreen(); ScreenManager.AddScreen(new GameplayScreen());
You probably won’t have a ton of screen objects that need to unload, so the game should start right up. If you need to do any heavy-duty work before you start the game, such as loading world data, you will want to leave this loading screen in place. The MessageBoxScreen works just like the MessageBox class in the .NET Framework. It’s a pop-up screen that asks the user if they’re sure they want to perform whatever action occurred right before the screen was displayed. The BackgroundScreen is just what it says, a screen that is always present no matter what other screens might be displayed. We won’t be using this screen, but you can run the project before we make any changes to see what it does. The GameplayScreen is where we’ll be concentrating most of our efforts. Instead of putting all of our code in the Game class as you’ll see a lot of samples do, we need a place that will only start playing the game when we tell it to and will allow us to quit playing the game without actually exiting the application. Take a look at the code that’s there now just to give you an idea of how you’ll be setting up your own games. Those are the screens that inherit from GameScreen. The rest inherit from MenuScreen (which itself inherits from GameScreen), which offers some additional functionality: Listing 2-13 abstract class MenuScreen : GameScreen { List menuEntries = new List(); int selectedEntry = 0; /// /// /// ///
Gets the list of menu entry strings, so derived classes can add or change the menu contents.
54
Chapter 2
protected IList MenuEntries { get { return menuEntries; } } /// /// Constructor. /// public MenuScreen() { TransitionOnTime = TimeSpan.FromSeconds(0.5); TransitionOffTime = TimeSpan.FromSeconds(0.5); } /// /// Responds to user input, changing the selected entry and accepting /// or cancelling the menu. /// public override void HandleInput(InputState input) { // Move to the previous menu entry? if (input.MenuUp) { selectedEntry--; if (selectedEntry < 0) selectedEntry = menuEntries.Count - 1; } // Move to the next menu entry? if (input.MenuDown) { selectedEntry++; if (selectedEntry >= menuEntries.Count) selectedEntry = 0; } // Accept or cancel the menu? if (input.MenuSelect) { OnSelectEntry(selectedEntry); } else if (input.MenuCancel) { OnCancel(); }
Game Development Overview
} /// /// Notifies derived classes that a menu entry has been chosen. /// protected abstract void OnSelectEntry(int entryIndex); /// /// Notifies derived classes that the menu has been cancelled. /// protected abstract void OnCancel(); /// /// Draws the menu. /// public override void Draw(GameTime gameTime) { Vector2 position = new Vector2(100, 150); // Make the menu slide into place during transitions, using a // power curve to make things look more interesting (this makes // the movement slow down as it nears the end). float transitionOffset = (float)Math.Pow(TransitionPosition, 2); if (ScreenState == ScreenState.TransitionOn) position.X -= transitionOffset * 256; else position.X += transitionOffset * 512; // Draw each menu entry in turn. ScreenManager.SpriteBatch.Begin(); for (int i = 0; i < menuEntries.Count; i++) { Color color; float scale; if (IsActive && (i == selectedEntry)) { // The selected entry is yellow, and has an animating // size. double time = gameTime.TotalGameTime.TotalSeconds; float pulsate = (float)Math.Sin(time * 6) + 1; color = Color.Yellow; scale = 1 + pulsate * 0.05f;
55
56
Chapter 2
} else { // Other entries are white. color = Color.White; scale = 1; } // Modify the alpha to fade text out during transitions. color = new Color(color.R, color.G, color.B, TransitionAlpha); // Draw text, centered on the middle of each line. Vector2 origin = new Vector2(0, ScreenManager.Font.LineSpacing / 2); ScreenManager.SpriteBatch.DrawString(ScreenManager.Font, menuEntries[i], position, color, 0, origin, scale, SpriteEffects.None, 0); position.Y += ScreenManager.Font.LineSpacing; } ScreenManager.SpriteBatch.End(); } }
The class displays a simple list of strings that act as menu items. The current item is drawn in yellow and will pulse, changing size. What happens when you select that item is up to you. Usually you would take the player to another screen, but you can override the way the class works and do whatever you want. This is what’s been done with the OptionsMenuScreen: Listing 2-14 class OptionsMenuScreen : MenuScreen { enum Ungulate { BactrianCamel, Dromedary, Llama, }
Game Development Overview
57
static Ungulate currentUngulate = Ungulate.Dromedary; static string[] languages = { “C#”, “French”, “Deoxyribonucleic acid” }; static int currentLanguage = 0; static bool frobnicate = true; static int elf = 23; public OptionsMenuScreen() { MenuEntries.Add(string.Empty); MenuEntries.Add(string.Empty); MenuEntries.Add(string.Empty); MenuEntries.Add(string.Empty); MenuEntries.Add(“Back”); } public override void Update(GameTime gameTime, bool otherScreenHasFocus, bool coveredByOtherScreen) { base.Update(gameTime, otherScreenHasFocus, coveredByOtherScreen); MenuEntries[0] MenuEntries[1] MenuEntries[2] MenuEntries[3]
= = = =
“Preferred ungulate: ” + currentUngulate.ToString(); “Language: ” + languages[currentLanguage]; “Frobnicate: ” + (frobnicate ? “on” : “off”); “elf: ” + elf.ToString();
} protected override void OnSelectEntry(int entryIndex) { switch (entryIndex) { case 0: // Change the “preferred ungulate” setting. currentUngulate++; if (currentUngulate > Ungulate.Llama) currentUngulate = 0; break; case 1: // Change the “language” setting. currentLanguage = (currentLanguage + 1) % languages.Length; break;
58
Chapter 2
case 2: // Change the “frobnicate” setting. frobnicate = !frobnicate; break; case 3: // Change the “elf” setting. elf++; break; case 4: // Go back to the main menu. ExitScreen(); break; } } protected override void OnCancel() { ExitScreen(); } }
This class gives you an example of one way of overriding the behavior of the menu items. The first item loops through the items in an enum, the second loops through a string array, the third toggles a Boolean variable between true and false, and the fourth increments an integer variable. We’re going to take advantage of this functionality in a couple of chapters when we give the player the ability to configure the controls for our game. We’ll eventually save that custom configuration so it carries over between sessions of the game. In order to transition between all these screens, a ScreenManager class is used. The ScreenManager inherits from DrawableGameComponent and is added to the game’s Components list when the game starts: Listing 2-15 public class Game1 : Microsoft.Xna.Framework.Game { GraphicsDeviceManager graphics; ScreenManager screenManager; public Game1() {
Game Development Overview
59
graphics = new GraphicsDeviceManager(this); screenManager = new ScreenManager(this); Components.Add(screenManager); // Activate the first screens. screenManager.AddScreen(new BackgroundScreen()); screenManager.AddScreen(new MainMenuScreen()); } }
The sample starts off with a background and main menu screen already in the list. From there, screens are added to and removed from the list as you navigate the screens. The ScreenManager class doesn’t have a lot in it for what it does. There are only a handful of properties: Listing 2-16 List screens = new List(); List screensToUpdate = new List(); InputState input = new InputState(); IGraphicsDeviceService graphicsDeviceService; ContentManager content; SpriteBatch spriteBatch; SpriteFont font; Texture2D blankTexture; bool traceEnabled;
The screensToUpdate list is needed because you might have some screens in the list that add or remove other screens in the course of its updating. In this case, trying to loop through a list where one item has been removed would cause an error. You can see this happen in the Update method of the class: Listing 2-17 public override void Update(GameTime gameTime) { // Read the keyboard and gamepad. input.Update(); // Make a copy of the master screen list, to avoid confusion if
60
Chapter 2
// the process of updating one screen adds or removes others. screensToUpdate.Clear(); foreach (GameScreen screen in screens) screensToUpdate.Add(screen); bool otherScreenHasFocus = !Game.IsActive; bool coveredByOtherScreen = false; // Loop as long as there are screens waiting to be updated. while (screensToUpdate.Count > 0) { // Pop the topmost screen off the waiting list. GameScreen screen = screensToUpdate[screensToUpdate.Count - 1]; screensToUpdate.RemoveAt(screensToUpdate.Count - 1); // Update the screen. screen.Update(gameTime, otherScreenHasFocus, coveredByOtherScreen); if (screen.ScreenState == ScreenState.TransitionOn || screen.ScreenState == ScreenState.Active) { // If this is the first active screen we came across, // give it a chance to handle input. if (!otherScreenHasFocus) { screen.HandleInput(input); otherScreenHasFocus = true; } // If this is an active non-popup, inform any subsequent // screens that they are covered by it. if (!screen.IsPopup) coveredByOtherScreen = true; } } // Print debug trace? if (traceEnabled) TraceScreens(); }
Game Development Overview
61
The InputState class is fairly simple, only really offering some helper functions for determining if the game is paused, handling menu item navigation, and determining if a particular key has just been pressed. It does this by keeping two variables for the state of the keyboard and gamepad: Listing 2-18 public KeyboardState CurrentKeyboardState; public GamePadState CurrentGamePadState; public KeyboardState LastKeyboardState; public GamePadState LastGamePadState;
A simple comparison between objects in the current and previous states can tell you what’s currently happening with the input devices: Listing 2-19 public bool PauseGame { get { return IsNewKeyPress(Keys.Escape) || (CurrentGamePadState.Buttons.Back == ButtonState.Pressed && LastGamePadState.Buttons.Back == ButtonState.Released) || (CurrentGamePadState.Buttons.Start == ButtonState.Pressed && LastGamePadState.Buttons.Start == ButtonState.Released); } } bool IsNewKeyPress(Keys key) { return (CurrentKeyboardState.IsKeyDown(key) && LastKeyboardState.IsKeyUp(key)); }
You’ll probably have noticed that the class doesn’t handle the mouse, and looking at the Update method of the class you’ll also see that it only handles one gamepad:
62
Chapter 2
Listing 2-20 public void Update() { LastKeyboardState = CurrentKeyboardState; LastGamePadState = CurrentGamePadState; CurrentKeyboardState = Keyboard.GetState(); CurrentGamePadState = GamePad.GetState(PlayerIndex.One); }
This isn’t an ideal situation. Unless you’re planning on creating only Xbox 360 games, you’ll need to offer the player mouse support. You’ll learn how to do this in Chapter 4, but for now we’ll work with what the sample offers. That’s basically a quick look at the sample that we’ll use as the skeleton for our game. You’ll probably want to run the sample and see how it operates. Depending on your comfort level, you could even try tweaking the sample to change how some things work. For example, you could try displaying the menu items one by one, or having them come in from the bottom of the screen or alternate coming in from the left and right. The code is pretty flexible so you can do some things to make your game uniquely your own.
Chapter 3
2D Graphics Not many programmers jumping into game programming will start off creating a clone of the latest 3D game that’s out there. In fact, if you go onto almost any game programming message board and ask what game you should create for your first project, I’d be willing to bet that no one would suggest a 3D game. You’ll probably get suggestions like Tetris, Breakout, and other clones of existing 2D games. There are a couple of good reasons for this. Creating a game with a known rule set and gameplay will be much easier than trying to come up with your own idea for a game and implementing it on top of learning the ins and outs of game programming. The less you have to deal with for your first projects, the better. Why should you have to put any thought into creating rules and coming up with gameplay elements for a learning project? It would be a much better use of your time to simply write a clone of an existing game. The second reason is that most people find 2D game programming easier to learn than 3D. That third dimension adds a lot of complexity to a game that most programmers just getting into game development would find a significant obstacle to overcome quickly and easily. It’s a much better idea to learn a little at a time and make the move to 3D once you’re comfortable with how game programming is done. Our first steps into the world of 2D game development will consist mainly of learning about the pieces of the XNA Framework that you’ll be working with and seeing them in action in small projects that demonstrate a function of each piece.
63
64
Chapter 3
Texture2D One of the basic objects you’ll be working with for all of your 2D work is instances of the Texture2D class. This class simply describes a 2D array of texels using code. Texel is short for texture element. It’s the smallest element in a texture, similar to a pixel (or picture element) in an image. Depending on how the object that the texture is mapped onto is rendered there may be one or more pixels per texel or one or more texels per pixel. For almost every project we’ll be working with in 2D you needn’t worry about the differences. The XNA Framework hides most of the gory details, taking care of the little things to let you concentrate more on the big things. Let’s take a quick look at the most basic usage of a Texture2D object. Start a new Windows Game project in XNA Game Studio. Give it a name and click OK. Next, using your favorite graphics program (Paint is perfectly fine here, as it is in most of the work we’ll be doing for now) create an image that is 64 pixels square. Draw a star on it, so that it looks something like this:
Figure 3-1
Save it as a .png file called star.png (you can save it as a .bmp, but the file size will be larger so it’s easier to work with the .png format) and either drag it onto the project in the Solution Explorer or add it using Project | Add Existing Item (or by right-clicking on the project in the Solution Explorer and selecting Add | Existing Item). Find the file for the image you just created and click OK to add it to the project.
2D Graphics
65
Take a look at the file’s properties by right-clicking on it and selecting Properties. You’ll see the following:
Figure 3-2
XNA Game Studio automatically recognizes the file when you add it as one that it knows how to handle using the content pipeline. We’ll be talking about the content pipeline and some of these properties in more detail later, but for now what we’re concerned with is the first property, Asset Name. This is how we’ll refer to the image in our code. Let’s see how that works now. Open the Game1.cs file and add the following just before the constructor: private Texture2D _starTexture;
then add the following inside the LoadContent method: _starTexture = content.Load("star");
Here we see how we use the Asset Name property of the image. We pass it as a parameter to the Load method of the ContentManager object that was created for us when we started the project. We also tell it that the asset we’re loading is a Texture2D asset. We’re now ready to render the image onto the screen. This leads us to the next piece of the 2D puzzle, the SpriteBatch object.
66
Chapter 3
SpriteBatch The SpriteBatch class is the middleman between our graphic assets and the screen. The name SpriteBatch comes from the fact that it’s much more efficient to draw things together that are related or use the same render settings rather than draw one object, change render settings, draw another object, change render settings back to the first set, draw another object, etc. By drawing all the objects with the same render settings at the same time, or batching them, you’ll be able to draw more in the same, or possibly less, time than if you drew things haphazardly, changing render settings constantly. The SpriteBatch class is fairly small compared to some of the other graphics-oriented classes in the XNA Framework. It has only one property that you’ll really need to deal with, the GraphicsDevice property, which you set when you create the object by passing a reference to the constructor. It has two members for drawing content to the screen, both of which are overloaded. The Draw method is what we’ll be using here. The other method, DrawString, does just what it says, renders text to the screen. The other two methods that you’ll use are the Begin and End methods for specifying when drawing is beginning and ending. The Begin method takes a parameter that allows you to specify some effects that will be applied to everything you draw until you call the End method. The basic Draw method has six overloads that allow you to do things like draw only part of a texture, scale it, rotate it, apply effects, and sort by depth (yes, even in 2D you still have a form of 3D). The basic Draw method takes three parameters — the texture you’re drawing, a rectangle to specify where to draw it on the screen, and a color that allows you to apply tinting to the image that you’re drawing. This is the one we’ll use for now just to get the texture rendered as quickly and easily as possible. The Windows Game template in Game Studio automatically creates and initializes a SpriteBatch object for us: SpriteBatch spriteBatch; spriteBatch = new SpriteBatch(GraphicsDevice);
2D Graphics
67
Finally we’ll draw our texture in the Draw method: spriteBatch.Begin(); spriteBatch.Draw(_starTexture, new Rectangle(100, 100, 64, 64), Color.White); spriteBatch.End();
Simple, huh? Go ahead and compile and run the game and you’ll see the following:
Figure 3-3
Hmm, that’s not quite what we had in mind. The idea was to draw just the star, not the background as well. What happened? Let’s adjust the Color parameter. Comment out the Draw method line and replace it with: spriteBatch.Draw(_starTexture, new Rectangle(100, 100, 64, 64), Color.CornflowerBlue);
68
Chapter 3
When we run it now, it looks great, but if we decide we don’t like the CornflowerBlue background and change it to Black instead, we end up seeing that we’ve simply hidden the problem:
Figure 3-4
What we really need is a way to have SpriteBatch draw the background of the image transparently. Fortunately, there’s an easy way to do this. We can modify the image file to take advantage of the built-in background masking that SpriteBatch has. To do this we need to use Magenta as the background of the image. Open the file and fill the white areas with the color Magenta (the RGB values are 255, 0, 255 if for some strange reason your paint program doesn’t define it by default). Save it as a new image, newstar.png, and add it to the project. Create another Texture2D object named _newStarTexture, and load the new image into it. Comment out the call to the SpriteBatch’s Draw method and add a new one to draw the newstar asset. Compile and run, and you’ll see the expected result:
2D Graphics
69
Figure 3-5
Perfect. It’s a little boring with it just sitting there though, isn’t it? Let’s see what we can do to make it a bit more interesting. Perhaps if we spin it around in place…
Rotation Rotating a sprite is just a little more complicated than drawing one normally. There’s only three things that have to be taken into account — the angle of rotation, the origin of the rotation (the point on the sprite around which the rotation occurs), and any effects that you might want to add to the rotation. Rotation in the XNA Framework is not measured by degrees as you would normally do; instead, it’s measured in radians. This means we’ll have to do a conversion, since we don’t think in radians. There are 0.0174532925 radians in one degree. It might be good to create a constant for this: public const float DEGREES_TO_RADIANS = 0.0174532925f;
We’ll also need a variable to hold the current angle of the image: private int _angle;
70
Chapter 3
Set this to 0 in the constructor. How will this get updated? It looks like the Update method might do the trick. The XNA Framework calls this as quickly as it can so we can use this to update our angle. Add the following after the if statement that’s already there: _angle += 1;
Now we just have to change our Draw method’s parameters to tell it to rotate the image: float rot = _angle * DEGREES_TO_RADIANS; Vector2 origin = new Vector2(32, 32); _sb.Draw(_newStarTexture, new Rectangle(100, 100, 64, 64), null, Color.White, rot, origin, SpriteEffects.None, 0.0f);
We see that we’re using the constant we defined earlier to create a value that the Draw method knows how to use. The other variable we create is the point around which the rotation will take place. This is a center pixel in our image. Later on you can try changing this value to see what effect it has on the rotation. Set the two values to 0, for instance. You can also change the _angle variable to increase the speed of the rotation. Our call to the Draw method has changed a bit. The third parameter of null is the source rectangle. Passing null means we want to draw the entire texture. The fifth and sixth parameters are the variables we set earlier. The next is an enum that specifies what effects we want to apply to the drawn image. In the “Flipping” section of this chapter, we’ll see what happens when we change this value. The last parameter is used to specify at what depth we want the image drawn. This allows us to have semi-3D rendering. Since we’re only drawing one sprite we can just set this to 0. Run the project and you’ll see the star spinning around its center point. You’ve added one more trick to your 2D arsenal. Feel free to set the parameters to different values and note what happens. This can be used for all kinds of things in games — magical effects, animation, protective shields for sci-fi type games; the list is as large as your imagination. Wouldn’t it be cool if we could change the size while the image is rotating?
2D Graphics
71
Scaling Scaling an image is a simple matter of changing one of the parameters in the Draw method, this time the destination rectangle. We’ll need a couple of additional variables and a couple of lines of code to implement some simple scaling effects. Add these two variables: private float _scale; private int _scaleDir;
Initialize them in the constructor: _scale = 1.0f; _scaleDir = 1;
Update the _scale variable in the Update method: _scale += (0.01f * _scaleDir); if (_scale > MAX_SIZE) { _scaleDir *= -1; _scale = MAX_SIZE; } else if (_scale < MIN_SIZE) { _scaleDir *= -1; _scale = MIN_SIZE; }
All that’s left is to draw it. Comment out the existing line and replace it with the following: spriteBatch.Draw(_newStarTexture, new Rectangle(100, 100, (int)(64*_scale), (int)(64*_scale)), null, Color.White, rot, origin, SpriteEffects.None, 0.0f);
Pretty simple, huh? That’s how much of the XNA Framework operates. Feel free to play with the _scale variable to change the speed of the scaling.
72
Chapter 3
Flipping Like scaling, flipping a sprite is just a matter of specifying a value for a parameter in the overloaded Draw method that allows us to do rotation. You can flip either vertically or horizontally by setting one of the SpriteEffects enum values: SpriteEffects.None SpriteEffects.FlipHorizontally SpriteEffects.FlipVertically
Open up the TwoD Flipping project in the Chapter 3 folder. You can also start from scratch. Create a new Windows Game project and modify the image from the previous sample like so:
Figure 3-6
Here’s the code you’ll need to add: Listing 3-1 public const float DEGREES_TO_RADIANS = 0.0174532925f; private Texture2D _starTexture; private int _angle; private SpriteBatch _sb; public Game1() { graphics = new GraphicsDeviceManager(this); _angle = 0; } protected override void LoadContent() { _starTexture = content.Load("newstar"); }
2D Graphics
73
protected override void Draw(GameTime gameTime) { graphics.GraphicsDevice.Clear(Color.Black); spriteBatch.Begin(); //rotating star float rot = _angle * DEGREES_TO_RADIANS; Vector2 origin = new Vector2(32, 32); spriteBatch.Draw(_starTexture, new Rectangle(100, 100, 64, 64), null, Color.White, rot, origin, SpriteEffects.None, 0.0f); spriteBatch.Draw(_starTexture, new Rectangle(100, 200, 64, 64), null, Color.White, rot, origin, SpriteEffects.FlipHorizontally, 0.0f); spriteBatch.Draw(_starTexture, new Rectangle(100, 300, 64, 64), null, Color.White, rot, origin, SpriteEffects.FlipVertically, 0.0f); spriteBatch.End(); base.Draw(gameTime); }
Take note of the three new calls to the Draw method. We draw the image normally first, flipped horizontally next, and flipped vertically last. As with the rotating sample, you can change the values for the origin to modify where the flipping originates.
Transparency As we’ve seen, the SpriteBatch class will automatically implement transparency for you if you use Magenta as the background color. That’s fine if you always want a portion of a sprite to be transparent, but what if you have a situation where you sometimes want to display part of a sprite with a portion of it showing and sometimes with it not showing? In that case you need to use a technique called masking to make a portion of a sprite transparent. Say we’re drawing a level background where a piece of information is hidden by a house that’s drawn on top of the background:
74
Chapter 3
Figure 3-7
At some point during the game you want the player to be able to blow a hole in the house to reveal that information. What we do is create a texture where the alpha channel is set up in such a way that it makes our hole:
Figure 3-8
2D Graphics
75
The texture on the right is set so that the black pixels will be transparent and the white will not. These two textures are combined using the DirectX Texture Tool that is part of the DirectX SDK and saved as a .dds file:
Figure 3-9
Since Game Studio supports this texture format, we can simply add the file to our project and load it as we normally do. We can then draw the texture with or without alpha blending to hide or show the information: Listing 3-2 protected override void Draw(GameTime gameTime) { graphics.GraphicsDevice.Clear(Color.CornflowerBlue); spriteBatch.Begin(_showInfo == true ? SpriteBlendMode.AlphaBlend : SpriteBlendMode.None); spriteBatch.Draw(_level, _levelRect, Color.White); spriteBatch.Draw(_house, _houseRect, Color.White); spriteBatch.End(); base.Draw(gameTime); }
76
Chapter 3
The _showInfo variable is set in the Update method: Listing 3-3 protected override void Update(GameTime gameTime) { // Allows the game to exit if (GamePad.GetState(PlayerIndex.One).Buttons.Back == ButtonState.Pressed) this.Exit(); if (GamePad.GetState(PlayerIndex.One).Buttons.A == ButtonState.Pressed || Keyboard.GetState().IsKeyDown(Keys.A)) _showInfo = true; else _showInfo = false; base.Update(gameTime); }
When you press the A button, you’ll see the following:
Figure 3-10
Coloring Let’s kick our little sample up a notch and draw a bunch of different stars. Just to make it interesting, we’ll draw each one in a different color. We’ll only need to change a little code to do this. You can either make the changes to the rotating sample or start a new project and copy the code from that sample.
2D Graphics
77
First off, we’ll need a couple of new members: private Random _rnd; private List _locations; private List _colors;
We’re going to generate random locations and colors, and these are the three things we’ll need to do so. Initialize them in the constructor: _rnd = new Random(); _locations = new List(); _colors = new List();
Drop in these two functions: Listing 3-4 private Rectangle GetRandomLocation() { return new Rectangle(_rnd.Next(0, graphics.PreferredBackBufferWidth), _rnd.Next(0, graphics.PreferredBackBufferHeight), 64, 64); } private Color GetRandomColor() { return new Color((byte)_rnd.Next(0, 256), (byte)_rnd.Next(0, 256), (byte)_rnd.Next(0, 256), (byte)_rnd.Next(0, 256)); }
The GetRandomLocation function uses the same basic method to create a rectangle as it did in the Draw call of the SpriteBatch from the sample to rotate the star. We just change the first two parameters to generate the random location. The GetRandomColor function generates a random value for the red, green, blue, and alpha components of our color. The alpha component of the color specifies the transparency of the color. You’ll notice once you run the completed project that some stars can be seen through at some level. The alpha component of the color controls this. The lower the number, the more you can see through it. This value and the color behind the sprite are blended together by default in the call to our Draw method. Next, fill in the _locations and _colors lists in the Initialize method:
78
Chapter 3
for (int i = 1; i < 201; i++) { _locations.Add(GetRandomLocation()); _colors.Add(GetRandomColor()); }
Our Draw method changes a bit: for (int i = 0; i 0) height = (int)(graphics.GraphicsDevice.Viewport.Height / 32) + 1; else height = (int)(graphics.GraphicsDevice.Viewport.Height / 32); if (graphics.GraphicsDevice.Viewport.Width % 32 > 0) width = (int)(graphics.GraphicsDevice.Viewport.Width / 32) + 1; else width = (int)(graphics.GraphicsDevice.Viewport.Width / 32); _map = new int[width, height];
Since we’re basing our map size on the size of the screen, we can’t do this in the constructor as the GraphicsDevice isn’t valid at that point. That’s what the Initialize method is for. We also do a check to see if the tile size will divide evenly into the screen height and width. If it doesn’t, we make our map a little bigger in that direction so that we won’t have any unused space. Once we’ve figured out how big our map should be we create an empty two-dimensional array of integers. Why don’t we fill it here in the Initialize method? Our code doesn’t know how many tiles we have since the _tiles object hasn’t been loaded with the image at this point. The LoadGraphicsContent method is where we have to do this: Listing 3-6 if (loadAllContent) { _tiles = content.Load("tiles"); _sb = new SpriteBatch(graphics.GraphicsDevice); } for (int x = 0; x MAX_SIZE) { _scaleDir *= -1; _scale = MAX_SIZE; } else if (_scale < MIN_SIZE) { _scaleDir *= -1; _scale = MIN_SIZE; }
And then in the Draw method change the code to the following: Listing 3-19 float rot = _angle * DEGREES_TO_RADIANS; Vector2 origin = new Vector2(32, 32); _sb.Begin(); _sb.DrawString(_font, "This is the title", new Vector2(150, 10), Color.White); _sb.DrawString(_fun, "Rotating", new Vector2(150, 150), Color.White, rot, new Vector2(0,0), 1.0f, SpriteEffects.None, 0.0f); _sb.DrawString(_fun, "Scaling", new Vector2(150, 300), Color.White, 0.0f, new Vector2(0,0), _scale, SpriteEffects.None, 0.0f); _sb.End();
That’s a bit cooler, but what if we want to rotate and scale from the middle of the text? There has to be some way to do that, right? Of course there is. The SpriteFont object is a class and, as such, has some functionality to help you use it. One of the methods of the
2D Graphics
91
class is MeasureString, which is exactly what we need. We can change the code in the Draw method as follows: Listing 3-20 float rot = _angle * DEGREES_TO_RADIANS; Vector2 origin = new Vector2(32, 32); Vector2 rotating = _fun.MeasureString("Rotating"); Vector2 scaling = _fun.MeasureString("Scaling"); _sb.Begin(); _sb.DrawString(_font, "This is the title", new Vector2(150, 10), Color.White); //_sb.DrawString(_fun, "Rotating", new Vector2(150, 150), Color.White, rot, new Vector2(0,0), 1.0f, SpriteEffects.None, 0.0f); //_sb.DrawString(_fun, "Scaling", new Vector2(150, 300), Color.White, 0.0f, new Vector2(0,0), _scale, SpriteEffects.None, 0.0f); _sb.DrawString(_fun, "Rotating", new Vector2(150, 150), Color.White, rot, new Vector2(rotating.X / 2, rotating.Y / 2), 1.0f, SpriteEffects.None, 0.0f); _sb.DrawString(_fun, "Scaling", new Vector2(150, 300), Color.White, 0.0f, new Vector2(scaling.X / 2, scaling.Y / 2), _scale, SpriteEffects.None, 0.0f); _sb.End();
In a full game, you would probably have a lot of different text that you would store in a resource file of some kind and read it into variables and then use the variables in the MeasureString and Draw methods. Remember, it’s usually not a good idea to hard-code data. For these quick samples though, any other method is not worth the effort and would just distract from the subject at hand. There are a lot of other effects that you can apply to your text — shadows, glowing borders, materials, etc. We could probably do an entire chapter on text in fact. However, there are already a lot of resources on the web in this area. I’ve seen at least one excellent library to create bitmaps of rasterized character sets with these types of effects. Check the appendix for links to some of these resources.
92
Chapter 3
Rendering with Multiple Viewports If you’ve ever played multiplayer Halo or a similar game on the same screen, you’ve seen how the game can render two different views at the same time. This is done using viewports. You define an area of the screen as the current area to do your rendering and everything is rendered as if that area is the full screen. Say you want to make a four-player split-screen game. You could do something like this: Listing 3-21 //declarations private Texture2D _sprite; private SpriteBatch _sb; private private private private
Texture2D Texture2D Texture2D Texture2D
_black; _white; _red; _green;
private Rectangle[] _areas; //load _sprite = content.Load("sprite"); _sb = new SpriteBatch(graphics.GraphicsDevice); _black _white _red = _green
= content.Load("black"); = content.Load("white"); content.Load("red"); = content.Load("green");
_areas = new Rectangle[4]; _areas[0] = new Rectangle(0, 0, graphics.PreferredBackBufferWidth / 2, graphics.PreferredBackBufferHeight / 2); _areas[1] = new Rectangle(graphics.PreferredBackBufferWidth / 2, 0, graphics.PreferredBackBufferWidth / 2, graphics.PreferredBackBufferHeight / 2); _areas[2] = new Rectangle(0, graphics.PreferredBackBufferHeight / 2, graphics.PreferredBackBufferWidth / 2, graphics.PreferredBackBufferHeight / 2);
2D Graphics
93
_areas[3] = new Rectangle(graphics.PreferredBackBufferWidth / 2, graphics.PreferredBackBufferHeight / 2, graphics.PreferredBackBufferWidth / 2, graphics.PreferredBackBufferHeight / 2); //draw protected override void Draw(GameTime gameTime) { _sb.Begin(); int i; _sb.Draw(_white, _areas[0], Color.White); _sb.Draw(_black, _areas[1], Color.White); _sb.Draw(_green, _areas[2], Color.White); _sb.Draw(_red, _areas[3], Color.White); for (i = 0; i 100
0
No
Yes
Yes
1
Mouse
5
3
No
Yes
No
1
Let’s take a closer look at the three devices.
Gamepad The three methods you’ll be using to control or read input from the gamepad device are: n
GetCapabilities
n
GetState
n
SetVibration
The GetCapabilities method returns a GamePadCapabilities structure that tells you what functionality the gamepad has. This is useful since the GamePad class can be used for a variety of devices. Figure 4-1 shows the members of the structure. The types of devices can be found in the GamePadType enum value of the structure. The following values are possible: n
ArcadeStick
n
DancePad
n
DrumKit
n
FlightStick
Figure 4-1
Input
n
GamePad
n
Guitar
n
Unknown
n
Wheel
113
All of the other members are pretty much self-explanatory. The IsConnected member is kind of redundant since most of the time you’ll probably be using the GetState method of the device and checking the IsConnected member of the structure returned by that method. The SetVibration method is set up as follows: public static bool SetVibration ( PlayerIndex playerIndex, float leftMotor, float rightMotor )
The playerIndex parameter is an enum with four members — One, Two, Three, and Four. The leftMotor and rightMotor float values can be between 0.0 and 1.0. To see this in action, start a new project and drop the following in the Update method: Listing 4-1 float xStick = GamePad.GetState(PlayerIndex.One). ThumbSticks.Left.X; float xStick2 = GamePad.GetState(PlayerIndex.One). ThumbSticks.Right.X; xStick=Math.Abs(xStick); xStick2 = Math.Abs(xStick2); GamePad.SetVibration(PlayerIndex.One, xStick, xStick2);
Run the project and move the two thumbsticks along the X axis. The left stick controls the left motor and the right stick controls the right motor. You can feel the difference between the two motors; the left is a slower, harder vibration, while the right is a faster, softer one.
114
Chapter 4
Keyboard The keyboard is probably, in some ways, the easiest of the three devices to deal with, as there are no analog controls to worry about; a key is either down or up. The only tricky part is dealing with the special keys — Ctrl, Alt, and Shift in conjunction with the regular keys. The keyboard has only one method that you’ll really need to worry about, the GetState method, which we’ll look at a little later.
Mouse In addition to the GetState method, the Mouse class has the SetPosition method and the WindowHandle property. The SetPosition method is useful if for some reason you want to take control away from the player and position the mouse yourself. This could be useful for something like a tutorial where you show the player where to move the mouse or indicate an object that should be clicked on. The WindowHandle property allows you to specify the window handle for which mouse data will be captured. You’ll rarely need to do this for a normal game, but it may be useful if you’re creating tools like a level editor where you have multiple windows open at once and need to specify a certain window for which to get mouse data.
State Classes In order to get the input from a device you have to poll the device. That means capturing a snapshot of the device and the value of all of its controls every frame. You do this using the GetState method for the device you wish to look at. Each device returns a structure with a similar name — the name of the device with “State” tacked on. Each State class has different members, however. Let’s take a look at them.
Input
115
GamePadState The GamePadState structure for the GamePad class has the following members:
Figure 4-2
The Buttons member is a structure that contains members for each of the controls on a gamepad — A, B, Back, LeftShoulder, LeftStick, RightShoulder, RightStick, Start, X, and Y. Each of these members is a ButtonState enum value that has two possibilities, Pressed and Released. The DPad member is another structure set up exactly the same; it has four ButtonState type members in a structure for each of the four directions in the D-pad. The IsConnected member is self-explanatory. If for some reason the gamepad were disconnected, you wouldn’t want to try to capture data and you would probably even want to pause the game and display a message asking the player to reconnect the controller. PacketNumber is an integer value that you can use to determine if the state of the device has changed, rather than checking the controls themselves. So you could do something like: Listing 4-2 if (GamePad.GetState(PlayerIndex.One). PacketNumber != _curPacketNum) { //check controls //save the new packet number _curPacketNum = GamePad.GetState(PlayerIndex.One). PacketNumber; }
116
Chapter 4
You would initialize the _curPacketNum member to –1 so the first time the Update method is called you’d check the controls. More than likely nothing will be pressed, but you’ll have to go through the motions to get a good value in the _curPacketNum member. The ThumbSticks member is a GamePadThumbSticks structure with two properties, Left and Right. Each is a Vector2 data type where the X and Y members will be a value between –1.0 and 1.0. –1.0 means the stick is all the way to the left or bottom, and 1.0 means it’s all the way to the right or top. We’ve seen the thumbsticks at work a little in the example to set the vibration for the controller. One major difference in the GetState method for the gamepad is the optional parameter that can be passed for the dead zone. The Triggers property is a structure with two members, Left and Right. Both are float data types with values that can be between 0.0 and 1.0. This gives you some flexibility in using the triggers for things like actions that have a different effect depending on the strength they’re performed with — say a bow that can be drawn part of the way. The IsButtonUp and IsButtonDown methods are simple helper functions that make it a bit easier to check a button instead of using the Buttons property. These methods are new to the 2.0 XNA Framework, so if you’ve been using a previous version you might want to take a look at using these methods rather than the Buttons property. Either way will get the job done, however.
KeyboardState The KeyboardState structure looks like the following:
Figure 4-3
Input
117
Of these, you’ll be using the GetPressedKeys, IsKeyDown, and IsKeyUp methods, and the this property. The three methods do what you’d expect. The GetPressedKeys method returns an array of keys that are currently being pressed. The IsKeyDown and IsKeyUp methods take a Key parameter and return a Boolean that lets you know if the key is currently being pressed. The this member allows you to get the state of any key on the keyboard, like so: Console.WriteLine("The space key is " + (kstate[Keys.Space] == KeyState.Down ? "down" : "up"));
MouseState The MouseState structure looks like this:
Figure 4-4
As you can see from the members of the structure, it handles a bit more than your basic mouse. The MiddleButton property on a two-button mouse will map to the clicking of the scroll wheel if the mouse has one.
118
Chapter 4
Controlling Sprite Movement Okay, so now that we’ve got the basics of how the devices work under our belts, we can turn our attention to getting a sprite on the screen to move. After all, a game wouldn’t be much fun if you couldn’t control the action. You might as well just watch a movie in that case. These are the basic steps in controlling sprites: n
Create the variables you’ll need for tracking sprite movement. At the minimum this would probably be a Vector2 or individual X and Y coordinate variables.
n
In the Update method, check to see if the controls for movement are being used. For Xbox 360 controllers, this is usually the thumbsticks and/or D-pad.
n
If the controls are being used, update the variables for sprite movement.
n
Render the sprite in the correct location using the variables.
Collision Detection Eventually your character or some other entity is going to run into something on the screen that he shouldn’t be allowed to go through. In order to make the entity behave correctly, you’re going to have to implement some kind of collision detection. You basically have two types to choose from — rectangle collision detection and per-pixel collision detection.
Input
119
Rectangle Collision Detection By far one of the easiest methods, rectangle collision detection isn’t going to be completely accurate unless your sprites are perfect rectangles:
Figure 4-5
Figure 4-6
Since rectangular sprites are pretty rare in most games, you’ll only want to use this method of collision detection if you can live with some inaccuracy and it won’t be noticed much by the player. For example, suppose the entities in your game are circles. Rectangular collision detection is going to give you some invalid collisions:
Figure 4-7
Figure 4-8
Unless the action in the game is very fast, your players will notice this at some point and wonder why the game is broken. Let’s create a quick sample that’ll show us how rectangular collision works in practice. Start up a new Windows Game project. We’ll
120
Chapter 4
need four shapes saved in four .png files, each 64 × 64 pixels, so create two squares and two circles like these:
Figure 4-9
Name them bluecircle.png, bluesquare.png, whitecircle.png, and whitesquare.png. (You could call them whatever you want but the following code expects these names.) Note that these images are included in the Chapter 4 file. Add the shapes to your project and add the following declarations: Listing 4-3 private SpriteBatch _sb; private Texture2D[] _sprites; private Rectangle[] _spriteRects; private SpriteFont _font; private int _curSprite; private bool _buttonDown = false; private float _x, _y;
The _sprites and _spriteRects arrays will allow us to do our collision detection with a couple of loops. The _font object will be used to display any collisions that might have been detected. The _curSprite variable will keep track of which sprite we’re moving around the screen. The _buttonDown variable will be used to allow us to switch between a square and circular sprite so we can see the difference in collision detection between the two types of shapes. The _x and _y variables will be used to keep track of where we’re moving the current sprite.
Input
121
Next, add the following to the constructor: Listing 4-4 _spriteRects = new Rectangle[4]; _spriteRects[0] _spriteRects[1] _spriteRects[2] _spriteRects[3]
= = = =
new new new new
Rectangle(100, Rectangle(500, Rectangle(500, Rectangle(100,
100, 100, 500, 500,
64, 64, 64, 64,
64); 64); 64); 64);
_curSprite = 0; _x = 100.0f; _y = 100.0f; _sprites = new Texture2D[4];
We create an array of four sprites and set their initial locations. We then set the initial sprite we’ll be moving around to be the upperleft sprite and set our variables to track the sprite movement to this same location. We create our array of sprites, which will be filled next. Now add the following to the LoadGraphicsContent method: Listing 4-5 if (_sb == null) _sb = new SpriteBatch(graphics.GraphicsDevice); _sprites[0] _sprites[1] _sprites[2] _sprites[3]
= = = =
content.Load("whitecircle"); content.Load("whitesquare"); content.Load("bluecircle"); content.Load("bluesquare");
_font = content.Load("text");
Nothing extraordinary here. We use white and blue shapes simply to show which shapes can be selected. We’ll enable switching between the white circle and square next. Now we’ll update the necessary objects by replacing the existing code in the Update method with the following:
122
Chapter 4
Listing 4-6 GamePadState state = GamePad.GetState(PlayerIndex.One); // Allows the game to exit if (state.Buttons.Back == ButtonState.Pressed) this.Exit(); if (state.Buttons.A == ButtonState.Pressed) { if (!_buttonDown) _buttonDown = true; } else { if (_buttonDown) { _buttonDown = false; _curSprite = Convert.ToInt32((!Convert.ToBoolean(_curSprite))); _x = _spriteRects[_curSprite].X; _y = _spriteRects[_curSprite].Y; } } if (state.ThumbSticks.Left.X != 0) { _x += (state.ThumbSticks.Left.X * 2.0f); } if (state.ThumbSticks.Left.Y != 0) { //need to do -= here so up on the thumbstick moves the sprite up _y -= (state.ThumbSticks.Left.Y * 2.0f); } _spriteRects[_curSprite] = new Rectangle( (int)_x, (int)_y, 64, 64); base.Update(gameTime);
After the check to see if the project should exit, we do a check to see if we need to switch between shapes. If we are switching, we set the necessary variables to that of the new shape that’s been selected. We then update the movement of the current shape. You could increase the speed of movement by changing the value that’s
Input
123
multiplied to the left thumbstick values. Doing so, however, means that the check for a collision will not be as accurate as it could be since the sprite will already have been updated before doing the check. After updating the x and y values, we update the rectangle for the current sprite so that it’s drawn in the correct location. Now we’ll draw our sprites by adding the following to the Draw method after the first line that’s already there to clear the GraphicsDevice: Listing 4-7 _sb.Begin(); _sb.DrawString(_font, "Press A to switch between circle and square sprites", new Vector2(10, 10), Color.White); //check for collisions _sb.DrawString(_font, CheckForCollisions(), new Vector2(10, 40), Color.White); for (int i = 0; i 0.0f) { switch (_moveDirection) { case Direction.East: _location.X += _speed; break; case Direction.North: _location.Y -= _speed; break;
Input
case Direction.NorthEast: _location.Y -= _speed; _location.X += _speed; break; case Direction.NorthWest: _location.Y -= _speed; _location.X -= _speed; break; case Direction.South: _location.Y += _speed; break; case Direction.SouthEast: _location.Y += _speed; _location.X += _speed; break; case Direction.SouthWest: _location.Y += _speed; _location.X -= _speed; break; case Direction.West: _location.X -= _speed; break; } _location = Vector2.Clamp(_location, vecMin, vecMax); } _bulletManager.Update(); if (_type == EntityType.Ghost) { _frameUpdate += gameTime.ElapsedGameTime.Milliseconds; if (_frameUpdate >= 500.0f)
159
160
Chapter 4
{ //increment frame _curFrame++; if (_curFrame > _numFrames - 1) _curFrame = 0; _frameUpdate -= 500.0f; } } }
The first thing we do in the Update method is set the directions if the entity is a ghost. We do this using the player’s location, which we’ve passed to the method. The GetDirectionFromVectors function does this for us: Listing 4-30 public static Direction GetDirectionFromVectors(Vector2 vecFrom, Vector2 vecTo) { float x = vecTo.X - vecFrom.X; float y = vecTo.Y - vecFrom.Y; float angle = (float)(Math.Atan2(y, x) * 57.2957795); Direction dir; if (angle < 0) { if (angle > -23) dir = Direction.East; else if (angle >= -67) dir = Direction.NorthEast; else if (angle >= -112) dir = Direction.North; else if (angle >= -157) dir = Direction.NorthWest; else dir = Direction.West; } else { if (angle < 23) dir = Direction.East; else if (angle 0; i--) { if (_entities[0].CheckBulletCollision(_entities[i])) { _score++; _entities.Remove(_entities[i]); if (_entities.Count == 1) break; } } } }
The ScreenManager is always going to be the first item in the game’s Components collection, so we just grab it so we can check to see if the game is active. If it is, we start updating. We check to see if it’s time to spawn another ghost and, if so, call the function to do so. The time between spawns is another item that will be adjustable once we implement difficulty settings. After calling the Update method for each entity, we loop through the entity list backward and do collision detections between the player and ghosts and between ghosts and any bullets that are alive. If either returns true, we remove the ghost from the list. If the bullet caused the removal, we increment the player’s score. There is new code for the Draw method only if the entity being drawn is the player. If so, we need to draw his bullets, so we add the following after drawing the player: entity.DrawBullets(_sb);
We also need to update the line of code that calculates the rotation of the entity: rot = (int)entity.ShootDirection * Game1.DEGREES_TO_RADIANS;
166
Chapter 4
We’ll be moving the constant to the Game class since we’ll need it elsewhere. It doesn’t make sense to declare it in more than one place. The only other thing that we need to do to the EntityManager is add a couple of new functions and methods and replace the SetPlayerDirection method. A couple of small helper functions first: Listing 4-36 public void SetPlayerDirection(Direction dir) { _entities[0].ShootDirection = dir; } public void StopPlayer() { _entities[0].Speed = 0.0f; } public Direction GetPlayerDirection() { return _entities[0].ShootDirection; } public int GetPlayerHealth() { return _entities[0].Health; } public bool IsPlayerDead() { return _entities[0].State == EntityState.Dead; } public void MovePlayer(Direction dir) { _entities[0].MoveDirection = dir; _entities[0].Speed = 2.0f; } public void PlayerFire() { _entities[0].SpawnBullet(); } private void SpawnGhost() {
Input
167
int num = _rnd.Next(0, 10); _entities.Add(new Entity(EntityType.Ghost, _ghostSpawnPoints[num], _ghostSpawnDirections[num], 4, _ghostTexture.Width, this.Game)); }
Note that the player’s speed is hard-coded. If you want to add a power-up or something that modifies the player’s speed, you’ll have to change the MovePlayer method to handle your changes. Three big functions and we’re done with the EntityManager. The first is to check for collisions between entities: Listing 4-37 public bool CheckEntityCollision(Entity entity) { Color[] ghostData; Color[] playerData; Rectangle playerRect; Rectangle ghostRect; Matrix matrix1, matrix2; ghostData = new Color[_ghostTexture.Width * _ghostTexture.Height]; _ghostTexture.GetData(0, new Rectangle(entity.CurFrameX, 0, 32, 32), ghostData, 0, 32 * 32); matrix1 = Matrix.CreateTranslation(new Vector3(-_ghostOrigin, 0.0f)) * Matrix.CreateRotationZ( GetRotationFromDirection(entity.ShootDirection)) * Matrix.CreateTranslation(new Vector3(entity.Location.X, entity.Location.Y, 0.0f)); ghostRect = new Rectangle((int)entity.Location.X, (int)entity.Location.Y, 32, 32); playerData = new Color[_playerTexture.Width * _playerTexture.Height]; _playerTexture.GetData(0, new Rectangle(_entities[0].CurFrameX, 0, 64, 64), playerData, 0, 64 * 64); matrix2 = Matrix.CreateTranslation(new Vector3(-_playerOrigin, 0.0f)) * Matrix.CreateRotationZ(GetRotationFromDirection( _entities[0].ShootDirection)) * Matrix.CreateTranslation( new Vector3(_entities[0].Location.X, _entities[0].Location.Y, 0.0f));
168
Chapter 4
playerRect = new Rectangle((int)_entities[0].Location.X, (int)_entities[0].Location.Y, 64, 64); //check for collision using rects first since per-pixel is costly if (ghostRect.Intersects(playerRect)) { if (Intersect(matrix1, matrix2, 32, 64, 32, 64, ghostData, playerData)) return true; } return false; }
We’ve seen this code before. There’s nothing really different here except that we’re dealing with the player and ghost textures, so our sizes need to be adjusted accordingly. The next addition is a helper function to convert a Direction value to radians: Listing 4-38 private float GetRotationFromDirection(Direction dir) { float ret = 0.0f; switch (dir) { case Direction.North: ret = -1.570796f; break; case Direction.NorthEast: ret = -0.785398f; break; case Direction.East: ret = 0.0f; break; case Direction.SouthEast: ret = 0.785398f;
Input
169
break; case Direction.South: ret = 1.570796f; break; case Direction.SouthWest: ret = 2.356194f; break; case Direction.West: ret = 3.141593f; break; case Direction.NorthWest: ret = -2.356194f; break; } return ret; }
Since we have both positive and negative values, here a simple multiplication won’t do the trick. Another option here would be to create an array of float values and use the Direction value as the index into the array. Last up is the Intersect function that we’ve seen before: Listing 4-39 private bool Intersect(Matrix matrix1, Matrix matrix2, int spriteWidth1, int spriteWidth2, int spriteHeight1, int spriteHeight2, Color[] data1, Color[] data2) { Matrix transform = matrix1 * Matrix.Invert(matrix2); Vector2 rowX = Vector2.TransformNormal(Vector2.UnitX, transform); Vector2 rowY = Vector2.TransformNormal(Vector2.UnitY, transform); Vector2 yPos = Vector2.Transform(Vector2.Zero, transform); for (int i = 0; i < spriteHeight1; i++) {
170
Chapter 4
Vector2 pos = yPos; for (int j = 0; j < spriteWidth1; j++) { int i2 = (int)Math.Round(pos.X); int j2 = (int)Math.Round(pos.Y); if (0 0) ScreenManager.FadeBackBufferToBlack(255 - TransitionAlpha); } }
347
348
Chapter 9
The next function in the GameplayScreen class is the function used to draw the heads-up display called DrawHUD(). This function uses the screen manager’s sprite batch to draw the black border on top of the screen, the current level, the current score, and the remaining game time. The DrawHUD() function can be seen in Listing 9-22. The HUD is rendered after the game world so that it is drawn on top of the screen. Because the frame rate is a game component that was set during the LoadContent() function, its information will be automatically rendered and we don’t need to specify anything for it in the DrawHUD() function. Listing 9-22 class GameplayScreen : GameScreen { public void DrawHUD() { ScreenManager.SpriteBatch.Begin(SpriteBlendMode.AlphaBlend, SpriteSortMode.Immediate, SaveStateMode.SaveState); int w = ScreenManager.GraphicsDevice.Viewport.Width; int h = ScreenManager.GraphicsDevice.Viewport.Height; Rectangle rect = new Rectangle(0, 0, w, (int)(h * 0.10f)); string displayString; ScreenManager.SpriteBatch.Draw(hudBorder, rect, Color.Black); displayString = "score: " + gameScore; gameFont.MeasureString(displayString); ScreenManager.SpriteBatch.DrawString(gameFont, displayString, scoreHudPos, Color.White); displayString = "Level: " + gameLevel; ScreenManager.SpriteBatch.DrawString(gameFont, displayString, levelHudPos, Color.White); displayString = gameTimeRemaining >= 60 ? ((int)(gameTimeRemaining / 60)).ToString() : ""; ScreenManager.SpriteBatch.DrawString(gameFont, "Time Remaining: " + displayString + ":" + ((int)(gameTimeRemaining % 60)).ToString("00"), timerHudPos, Color.White);
3D Graphics
349
ScreenManager.SpriteBatch.End(); } }
The last functions in the GameplayScreen class are the HandleInput() function and the callback functions for the input action map. The HandleInput() function is the same function that was supplied with the GameplayScreen class from the Game State Management sample and therefore is unaltered. The action map functions include Move(), MoveForward(), MoveBackward(), MoveLeft(), MoveRight(), Fire(), and Rotate(). The Move() function is used by the left joystick of the Xbox 360 controller to move the camera forward and backward, while the Rotate() function is used to rotate the camera using the right joystick. The remaining movement functions are used to move the camera in a direction using the Xbox 360’s directional pad. The Fire() function is not implemented in this chapter since there are no enemies in this chapter to shoot at. In each of the movement functions, a collision detection test occurs whenever the camera moves to ensure that the player stays within the game world. The remaining functions from the GameplayScreen class can be seen in Listing 9-23. Listing 9-23 class GameplayScreen : GameScreen { public override void HandleInput(InputState input) { if(input == null) throw new ArgumentNullException("input"); if(input.PauseGame) { // If they pressed pause, bring up the pause menu screen. ScreenManager.AddScreen(new PauseMenuScreen()); } }
private void Move(object value, GameTime gameTime) { // Move using the left joystick. if(IsActive == false)
350
Chapter 9
return; Vector2 delta = (Vector2)value; Vector3 oldPosition = player1Cam.Position; Vector3 oldLookAtPosition = player1Cam.LookAt; // Move left if negative; else right if positive. if(delta.X < 0.0f) player1Cam.MoveLeftXZ(delta.X * –1 * camMoveSpeed); else if(delta.X > 0.0f) player1Cam.MoveRightXZ(delta.X * camMoveSpeed); // Move backward if negative; else forward if positive. if(delta.Y < 0.0f) player1Cam.MoveBackwardXZ(delta.Y * –1 * camMoveSpeed); else if(delta.Y > 0.0f) player1Cam.MoveForwardXZ(delta.Y * camMoveSpeed); player1.SetCollisionCenter(player1Cam.Position); if(!gameWorldCollisionBox.Intersects(player1.CollisionSphere)) { // On collision reset to last valid position. player1Cam.Position = oldPosition; player1Cam.LookAt = oldLookAtPosition; } }
private void MoveForward(object value, GameTime gameTime) { if(IsActive == false) return; if(value != null) { if((ButtonState)value == ButtonState.Pressed) { Vector3 oldPosition = player1Cam.Position; Vector3 oldLookAtPosition = player1Cam.LookAt; player1Cam.MoveForwardXZ(camMoveSpeed); player1.SetCollisionCenter(player1Cam.Position); if(!gameWorldCollisionBox.Intersects(player1.CollisionSphere)) {
3D Graphics
351
// On collision reset to last valid position. player1Cam.Position = oldPosition; player1Cam.LookAt = oldLookAtPosition; } } } }
private void MoveBackward(object value, GameTime gameTime) { if(IsActive == false) return; if(value != null) { if((ButtonState)value == ButtonState.Pressed) { Vector3 oldPosition = player1Cam.Position; Vector3 oldLookAtPosition = player1Cam.LookAt; player1Cam.MoveBackwardXZ(camMoveSpeed); if(!gameWorldCollisionBox.Intersects(player1.CollisionSphere)) { // On collision reset to last valid position. player1Cam.Position = oldPosition; player1Cam.LookAt = oldLookAtPosition; } } } }
private void MoveLeft(object value, GameTime gameTime) { if(IsActive == false) return; if(value != null) { if((ButtonState)value == ButtonState.Pressed) { Vector3 oldPosition = player1Cam.Position; Vector3 oldLookAtPosition = player1Cam.LookAt;
352
Chapter 9
player1Cam.MoveLeftXZ(camMoveSpeed); if(!gameWorldCollisionBox.Intersects(player1.CollisionSphere)) { // On collision reset to last valid position. player1Cam.Position = oldPosition; player1Cam.LookAt = oldLookAtPosition; } } } }
private void MoveRight(object value, GameTime gameTime) { if(IsActive == false) return; if(value != null) { if((ButtonState)value == ButtonState.Pressed) { Vector3 oldPosition = player1Cam.Position; Vector3 oldLookAtPosition = player1Cam.LookAt; player1Cam.MoveRightXZ(camMoveSpeed); if(!gameWorldCollisionBox.Intersects(player1.CollisionSphere)) { // On collision reset to last valid position. player1Cam.Position = oldPosition; player1Cam.LookAt = oldLookAtPosition; } } } }
private void Rotate(object value, GameTime gameTime) { if(IsActive == false) return; if(value != null) {
3D Graphics
353
Vector2 delta = (Vector2)value; delta *= camTurnSpeed; player1Cam.RotateCamera(delta); } }
private void Fire(object value, GameTime gameTime) { if(IsActive == false) return; // Nothing to shoot yet. } }
The last bit of code to examine is the game’s main source file. In the main source we have a screen manager, input system, and XACT audio objects that are part of the game’s class. These objects are created, loaded, and initialized in the game’s constructor. The screen manager and input system for the action map are added to the game’s game component list. Once added we attach two screens to the game screen system with the first being the menu’s background and the second being the main menu itself. In the game’s Update() function we must update the XACT audio engine. Other than that, the rest of the game’s source file is the default generated by XNA when the project was created. The game’s main source file can be seen in Listing 9-24. In the game’s constructor the graphics device has multisampling enabled (which is used to reduce aliasing artifacts), a resolution of 1024 × 768, and is set to sync with the vertical retrace. Vertical retrace sync, or v-sync for short, essentially means to wait until the frame change is complete before drawing the next frame to the screen. The frame changes in CRT monitors can be in sync with the vertical blanking interval. Without this it is possible to have two different frames being displayed in different regions of the screen. This can cause page tearing artifacts that can appear drastic and are very unwelcome. Using v-sync tells the graphics hardware to wait until the last frame was fully displayed before displaying the next frame. V-sync is common when games have limited but consistent frames per second (e.g., 30 FPS, 60 FPS, etc.).
354
Chapter 9
Listing 9-24 public class GhostArena3D : Microsoft.Xna.Framework.Game { GraphicsDeviceManager graphics; ScreenManager screenManager; InputSystem input; private AudioEngine audio; private WaveBank wavebank; public SoundBank soundbank;
public GhostArena3D() { graphics = new GraphicsDeviceManager(this); Content.RootDirectory = "Content"; graphics.PreferMultiSampling = true; graphics.PreferredBackBufferWidth = 1024; graphics.PreferredBackBufferHeight = 768; graphics.SynchronizeWithVerticalRetrace = true; input = new InputSystem(this); input.Enabled = false; input.AddAction("Move", Control.LeftStick, ActionType.Move, null); input.AddAction("Move Forward", Control.DPadUp, ActionType.MoveForward, null); input.AddAction("Move Backward", Control.DPadDown, ActionType.MoveBackward, null); input.AddAction("Move Left", Control.DPadLeft, ActionType.MoveLeft, null); input.AddAction("Move Right", Control.DPadRight, ActionType.MoveRight, null); input.AddAction("Fire", Control.RightTrigger, ActionType.Fire, null); input.AddAction("Rotate", Control.RightStick, ActionType.Rotate, null); Components.Add(input);
3D Graphics
355
audio = new AudioEngine("Content/Sounds/GhostArenaGameAudio.xgs"); wavebank = new WaveBank(audio, "Content/Sounds/Wave Bank.xwb"); soundbank = new SoundBank(audio, "Content/Sounds/Sound Bank.xsb"); this.Services.AddService(typeof(IInputSystem), input); screenManager = new ScreenManager(this, graphics); Components.Add(screenManager); screenManager.AddScreen(new BackgroundScreen()); screenManager.AddScreen(new MainMenuScreen()); }
protected override void Update(GameTime gameTime) { audio.Update(); base.Update(gameTime); }
protected override void Draw(GameTime gameTime) { graphics.GraphicsDevice.Clear(Color.Black); base.Draw(gameTime); } }
Summary Creating a 3D video game can be a fun and exciting project to take on as a hobby game developer. Making any type of game can be fun, but what sets 3D games apart from the pack is that the worlds, the storytelling techniques, and the visual output associated with them can reach greater levels than can be reached in a 2D game. Creating a 3D game requires many more assets, more mathematical knowledge, and often much longer development periods, but once the game is done, it can be a very satisfying feeling. This is especially true if this is your first game.
356
Chapter 9
In this chapter we covered the basics of 3D geometry and mathematics, discussed XSI briefly, and started the 3D gaming project. We also covered the creation of a first-person camera and simple animations using line paths and skeleton hierarchies that can be exported as part of an X or FBX model. In the next chapter, we will expand upon this knowledge to learn how to create our own custom importers and processors for XNA’s content pipeline.
Chapter 10
Importers/Converters XNA has a powerful content pipeline that enables many different file formats to be efficiently loaded, compiled, and processed by the XNA Framework. The content pipeline that ships with XNA has built-in importers and processors that are available as a standard feature within XNA. Programmers are able to use these tools to easily load and process common types of assets that are used in many different video games and tools, while at the same time allowing these tools to be expanded upon to suit any individual game developer’s needs. The purpose of this chapter is to review XNA’s standard importers and content processors, to learn how to create custom tools that can be used to process any data a game developer needs to use in a game, and to find out how to make use of these custom tools in an actual XNA application. At the end of this chapter, the information provided throughout the following sections will be applied to the book’s game project to further enhance your knowledge on the subject.
Standard Importers and Content Processors Throughout this book we have been using different standard content importers and processors to load 3D geometry, textures, shaders, and so forth. Before we can discuss how to create your own custom tools, it is important to understand what XNA has available as built-in features of the Framework. Most of these importers
357
358
Chapter 10
and processors are used to work with geometry, shaders, textures, and other files such as XML (Extensible Markup Language).
Autodesk FBX (.fbx) Importer Autodesk’s FBX technology is a set of tools, plug-ins, and file formats for different assets that can exist in a film or gaming project. One area of the FBX technology is the FBX 3D file format. This file format is a standard format that is supported by many different tools including Maya, 3ds Max, and VIZ. The FBX format can be used to store the following information in a file: n
Geometry (meshes and NURBS)
n
Deformers, which largely deal with animations
n
Materials and shaders
n
Animations and constraints
n
Cameras
n
Lights
n
Embedded files, which means storing another file’s contents inside of an FBX file (e.g., an audio file, texture image, video, and so forth)
XNA can import geometry from FBX files into the XNA Framework by using the standard content importer known as the FbxImporter.
Effect (.fx) Importer and Processor DirectX effect files can also be used in the XNA Framework. DirectX effect files have the extension .fx and can be used to represent high-level shading effects and shaders. The effect files in XNA are imported using the EffectImporter class, which returns an EffectContent object. Shaders in Direct3D and XNA use Microsoft’s HLSL (High-Level Shading Language). The effect files are processed by the XNA Framework using the EffectProcessor class, which takes as input the EffectContent object loaded by the EffectImporter. The EffectProcessor compiles the effect for the target platform the project is being compiled on. The process of importing, compiling, exporting, and reading content files is performed by XNA and the details are hidden in the tool’s
Importers/Converters
359
implementation. To use this tool, we simply include the FX file in the project and XNA will recognize its type and what tools to use on it. High-level shading languages will be explored in more detail in Chapter 11. In the meantime we’ll focus on the BasicEffect class that was discussed in detail in Chapter 7.
Model Processor Models are processed and prepared for XNA by using the model processor called ModelProcessor. The ModelProcessor takes as input the NodeContent object that is the output for various content importers such as the FBX (FbxImporter) and X (XImporter) importers. The ModelProcessor can take parameters to allow properties to be applied to the geometry and data that is being processed. These optional parameters include the following items: n
Color key color
n
Color key enabled flag
n
Generate mipmaps flag
n
Generate tangent frames flag
n
Resize texture to a power of two flag
n
Scale as a floating-point value
n
Swap winding order flag (which can be used to render geometry inside out)
n
The texture format to use for the model’s textures
n
X axis rotation as a floating-point value (in degrees)
n
Y axis rotation as a floating-point value (in degrees)
n
Z axis rotation as a floating-point value (in degrees)
The color key is a color value that is specified for color key transparency, which is a color value used to mark the transparent pixels of an image without the use of an alpha channel in the original image. Setting the various parameters of the ModelProcessor is optional and can be used to further manipulate the geometry being processed by the XNA Framework. You can set the properties of the
360
Chapter 10
ModelProcessor for a specific asset by clicking on the asset in the Solution Explorer, which brings up its properties in the Properties window of Visual Studio. In the Properties window you can expand the Content Processor for the model and set each of these individual parameters as desired.
No Processing Required The No Processing Required processor (called PassThroughProcessor) performs no processing on the asset. This can be useful for assets that are already processed either by an external tool that creates XNA-compatible files or previously by XNA. Since the files are XNA ready, no further processing would be required on them. This processor is useful for those types of files or for files that you wish to skip processing for.
Sprite Font Descriptor Importer and Processors Sprites can be imported and processed by files and used in XNA. A sprite font descriptor file, with the extension .spritefont, can be imported using the FontDescriptorImporter class, which returns a FontDescription object. Once imported, the data can be processed using the FontDescriptionProcessor, which converts the descriptor to a font that can be used in XNA. In addition to using a font description file, you can use textures and have those converted into fonts. This is done by specifying a texture to use the FontTextureProcessor, which also outputs a font that can be used in XNA. You can also create your own fonts by using Microsoft’s Bitmap Font Maker Utility that is offered on the XNA Creators Club Online web site as a free download.
Texture Importer and Processor The texture importer class (TextureImporter) is used to import a texture file into the XNA Framework. Once imported, the data is processed by the TextureProcessor class and a texture object is created. The texture file formats that are supported by XNA include the following:
Importers/Converters
n
BMP
n
DDS
n
DIB
n
HDR
n
JPG
n
PFM
n
PNG
n
PPM
n
TGA
361
Additional parameters can be specified for the TextureProcessor, including the following: n
Color key color
n
Color key enabled flag
n
Generate mipmaps flag
n
Resize to power of two flag
n
Texture format
X File Importer Model geometry specified in a DirectX X model file (.x) can be imported into the XNA Framework. The geometry in an X file is assumed to use a left-handed coordinate system and is imported using the XImporter class. The output of this importer is a NodeContent object, which is the input for the ModelProcessor that was discussed earlier.
XACT Importer and Processor Game audio files can be imported into an XNA Game Studio project using Microsoft’s Cross-Platform Audio Creation Tool (XACT). Because the XNA Framework doesn’t have an importer or processor class for audio files, XACT is used to create the necessary files and import game audio specified in the XACT project file format (XAP). These files can be opened and manipulated by XACT, which is included in both the XNA Game Studio and DirectX SDK install
362
Chapter 10
packages. When compiled, XACT generates the audio files that are ultimately used by the game at run time.
XML Importer XML can be used to modify the values of an object by specifying these values in an XML file, loading those values at run time, then replacing the values of an object with those found in the XML file. Regardless of how they are used, XML files can be loaded by XNA by using the XMLImporter class. There is no processor for XML files, and they can be imported into XNA without further processing.
Extending the Standard Content Processing XNA Game Studio allows for programmers to extend the standard content processors that ship with the development toolkit to allow for custom modifications on assets compiled for run-time use. This is an important feature because the creation of a content importer, processor, writer, and reader is not a trivial task if you only want to slightly modify a resource’s contents using the standard tools. In this section we will look at creating a very simple extension to one of the content processors. The example we’ll create will use the ModelProcessor as an example of how to alter the input geometry during the content’s processing stage. The source code for this demo can be found in the Chapter 10 folder called ExtendingContentPipeline.
A Simple Example of Extending a Standard Processor The ExtendingContentPipeline demo is the same sample from the Camera demo in Chapter 9 with the addition of the extended content processor. This extended processor will demonstrate how to create the processor to keep its readability clear and easy to follow. This demo will take the incoming geometry that is being processed and scale it by a factor of 0.2 (i.e., 20 percent or one-fifth the original
Importers/Converters
363
size). Because the XNA 2.0 standard model processor allows for you to set a scale property, this example isn’t the most useful, but it is a very simple way to learn how to create your own extended processor if you are doing it for the first time. If you would like to follow along with the creation of this processor, you can read through this section or else you can open the sample from the downloadable files and read along with that. The first step is to create a project and use the source code from the Camera demo in Chapter 9 as a way to test the processor you are about to create. The next step is to create a new project, which can be separate or added to the solution, using the project type Content Pipeline Extension Library 2.0. In XNA Game Studio 2.0, you can use this project type for your content pipeline code that can then be used in an Xbox 360 or Windows PC game. When you create the Content Pipeline Extension Library project, you are given a default file with a processor ready for you to fill out. If you look at this folder, you’ll notice a property called DisplayName, the class itself, and the overriding of the Process() function. This can be seen in Listing 10-1. Listing 10-1 using TInput = System.String; using TOutput = System.String;
[ContentProcessor(DisplayName = "ContentProcessor1")] public class ContentProcessor1 : ContentProcessor { public override TOutput Process(TInput input, ContentProcessorContext context) { throw new NotImplementedException(); } }
The using statements that define TInput and TOutput are to be replaced with the input and output your processor will work with. In the case of the Model Processor that we will be expanding upon, this will be NodeContent for the input and ModelContent for the output. Since TInput and TOutput are just placeholders from the generated code, you can delete those two using statements for this simple example.
364
Chapter 10
The DisplayName property of the content processor is the name that is displayed in the drop-down list when you choose what processor type you would like to use on an asset. By default when you add a resource that XNA supports to an XNA project, it automatically chooses the importer and processor so that you don’t have to do it manually. You could change the processor type to another, which we’ll do in just a minute. Since we are extending the ModelProcessor class, we can use inheritance to specify that our custom processor will extend that class. We’ll call this class ExtendedModelProcessor (display name Extended Model Processor); it can be seen in Listing 10-2. Listing 10-2 [ContentProcessor(DisplayName = "Extended Model Processor")] public class ExtendedModelProcessor : ModelProcessor { public override ModelContent Process(NodeContent input, ContentProcessorContext context) { MeshHelper.TransformScene(input, Matrix.CreateScale(0.2f)); return base.Process(input, context); } }
The output from the Process() function of the ModelProcessor class is ModelContent, so we must change the return type from TOutput to ModelContent. Also, the input for the ModelProcessor class is NodeContent, which you will specify instead of TInput in the first parameter variable. The goal of this simple example was to scale the geometry being processed by one-fifth. This can be done with the XNA MeshHelper class by calling the function TransformScene(), which takes as parameters a NodeContent object that stores the geometry and a matrix that is to be applied to that geometry. If the matrix used is a scaling matrix, which can be created by calling the Matrix function CreateScale(), then the transformation applied to the geometry will scale the model’s meshes. Once scaled we call the base class (ModelProcessor) function Process(), which will continue processing the information normally.
Importers/Converters
365
To test the new model processor, add a project reference to the Content project of the main demo (see Figure 10-1). Select the asset’s file and change its processor from Model Processor to our newly created Extended Model Processor (see Figure 10-2). If you build and execute the code, you should see the model from the Camera demo displayed at 20 percent of its original size. This demo is one of the simplest ways of creating your own content processor in XNA. Figure 10-3 shows a screenshot from the Camera demo on the left and a screenshot from this demo on the right for comparison.
Figure 10-1: Adding a reference to Content.
Figure 10-2: Changing the file’s processor type.
Figure 10-3: A screenshot of the Camera demo (left) and this demo (right).
366
Chapter 10
Writing a Custom Importer and Processor It is extremely useful for XNA developers to be able to create their own content processors. This allows developers to create their own code so that they can work with any asset their games use and require within XNA and the content pipeline. These assets don’t have to be just textures and geometry; they can be any file with any purpose and information inside of it. Writing code that can be used to process and load files that are not initially supported by XNA requires a number of steps. These include creating the following classes if you are not extending a standard routine as seen earlier in this chapter for the ExtendingContentPipeline demo: n
ContentImporter
n
ContentProcessor
n
ContentWriter
n
ContentReader
When extending the content pipeline earlier in this chapter, we created a processor that extended the ModelProcessor class. Because that processor extended one of XNA’s standard routines it was not necessary to write an importer, writer, or reader since those used by XNA with the ModelProcessor class to load X and FBX models were perfectly valid. Since we are now writing code to load a custom file format into XNA, we’ll have to write classes beyond just the processor. An importer is a class that is used to load the file’s contents into memory. This can be as easy as reading the entire file into memory and passing that information to the next stage, which is the processor. A processor in XNA will process information and make it ready for the writing stage. This was seen in the earlier demo where a processor was used to scale the incoming geometry. This step is used if the information from the importing stage needs special calculations or modifications, which was seen in this chapter’s previous demo where the incoming geometric information for a model was scaled during the processing stage.
Importers/Converters
367
The content writer in XNA is used to write the information from the processor to a file. The writer will create an XNB file, which is the file format XNA uses for the run-time game. During the writing stage you can write the information out however you like, which we’ll see an example of later in this chapter. The XNB file has information XNA uses to efficiently load the file in addition to the information you specify it to write. Therefore, a writer is not just a way of copying information from a source file to another, which would essentially be just copying the file and changing its extension. The final stage is the reader. The reader is used by the game application to read the file that was written during the writing stage. The reader is usually used to read the information into an object, such as the Model class for storing model geometry when using the X/FBX model content classes, for example. Generally speaking, you can see that the importer reads the original file, the processor performs any calculations that need to occur to prepare the data for the content writing stage (as seen in our processor that scaled the geometry of a model) or simply passes the information straight to the writer, and the writer saves the information as an XNB file that XNA can load at run time. The game’s source code has a reader class that loads the XNB file into an object that is used by the game. The importer, processor, and writer are all executed during compilation, while the reader is used by the game during run time. Everything except for the reader is placed in its own project and referenced in the game’s project. Later in this chapter, we will create a demo that uses a custom importer, writer, processor, and reader. This demo will build upon the AnimationPath demo from Chapter 9. It will allow for the animation path information to be specified in a text file, processed by XNA during compilation, and loaded by the application during run time, all using the content pipeline. In the AnimationPath demo the line paths were defined by starting and ending positions and a route was defined as a list of those paths. For our demo in this chapter we will keep things simple so that it is easy to follow what is going on by creating a file format that specifies the 3D position of the starting and ending positions for each path in a text file. The file will have six sets of values for each path, where the first three are the starting position and the last three are the ending position. The total number of these pairs in the
368
Chapter 10
file will determine how many paths make up the entire animation. An example file can be seen in the following where the starting positions of 20, 15, and 10 and the ending positions of 50, 15, and 78 define one path: 20, 15, 10, 50, 15, 78
Since we are using a text file, you can use any text editor, such as Notepad or even Visual Studio, to create animation files for this demo. To ensure that valid files are being used we must ensure that there is at least one path defined. This means that the total number count in the file has to be a multiple of six in order to specify complete paths and that at least one path is defined. We’ll see how this validation can be performed by the custom processor later on in this chapter.
Creating the Importer’s Project The first step in creating code for custom content classes is to create a Content Pipeline Extension Library project. Once the project is created, delete the default file (ContentProcessor1.cs) it generated and add new files to the project by choosing Project from the menu and selecting Add New Item for each class we will need. To begin, create new files for the content importer, processor, and writer and give each one a meaningful name. When the files are created, the classes will be named after the files you specified. In the AnimationPathContent sample demo for this chapter, we’ve used the names AnimationPathImporter.cs for the importer, AnimationPathProcessor.cs for the processor, and AnimationPathWriter for the writer. Listing 10-3 shows the default XNA-generated code for the importer, Listing 10-4 contains the processor code, and Listing 10-5 contains the writer code. Listing 10-3 [ContentImporter(".abc", DisplayName = "ABC Importer", DefaultProcessor = "AbcProcessor")] public class AnimationPathImporter : ContentImporter { public override TImport Import(string filename, ContentImporterContext context) { // TODO: read the specified file into an instance
Importers/Converters
369
// of the imported type. throw new NotImplementedException(); } }
Listing 10-4 [ContentProcessor(DisplayName = "Test.AnimationPathProcessor")] public class AnimationPathProcessor : ContentProcessor { public override TOutput Process(TInput input, ContentProcessorContext context) { // TODO: process the input object, and return the modified data. throw new NotImplementedException(); } }
Listing 10-5 [ContentTypeWriter] public class AnimationPathWriter : ContentTypeWriter { protected override void Write(ContentWriter output, TWrite value) { // TODO: write the specified value to the output ContentWriter. throw new NotImplementedException(); } public override string GetRuntimeReader(TargetPlatform targetPlatform) { // TODO: change this to the name of your ContentTypeReader // class which will be used to load this data. return "MyNamespace.MyContentReader, MyGameAssembly"; } }
In this demo we’ll be using the code from the AnimationPath demo in Chapter 9, so create a new project for the main demo and add a reference under Content for the Content Pipeline Extension Library project we’ve created. At this point you should be able to build the application project but not the content extension project. This is because we must fill in the specifics of each of the classes in the library or else Visual Studio will display exceptions.
370
Chapter 10
Storing Imported and Compiled Data The content processor uses a class that stores the data it will create and send to the writer. This is usually done when the processor takes the information from the importer and compiles it into a different form for the content writer. In this demo an array of vector positions is sent to the content writer. Although we could have specified the output to be directly Vector3[], it is common to use a class to store more complex information in one object. For this demo, the class is called AnimationCompiledData; it represents the data after the processor has performed its job. This class stores a list of Vector3 objects. In the text file loaded by the importer, each number is part of a position. This information is used to build the Vector3 array that is written to the file by the writer. The AnimationCompiledData class can be seen in Listing 10-6. Listing 10-6 using CompiledType = Vector3; public class AnimationCompiledData { CompiledType[] data; public AnimationCompiledData(CompiledType[] compiledData) { data = compiledData; } public CompiledType[] Data { get { return data; } } }
Creating the Importer The content importer is responsible for loading the text file that stores the animation information and prepares it for the content processor. By definition the importer is only responsible for importing, at the very least, the contents of the file. Anything beyond that is up to you. If necessary or convenient, you can create a class that stores and organizes the imported information so that it is easier to work
Importers/Converters
371
with in the processor. In this demo, the file is simply a list of positions, and therefore all of the work of parsing that information to create the Vector3 list can be done in the processor. For this demo it is enough to read all of the information from the text file and return it as the processor’s input. The content importer expects the file extension for the animation path asset to be .apf (which stands for animation path file) and it returns a string that represents the file’s contents. The content importer’s class can be seen in Listing 10-7. The only requirements are that the output you specify for the importer matches what the processor expects; for this demo that happens to be a string containing the file’s contents. Listing 10-7 using TImport = System.String; [ContentImporter(".apf", DisplayName = "Animation Path Importer", DefaultProcessor = "AnimationPathProcessor")] public class AnimationPathImporter : ContentImporter { public override TImport Import(string filename, ContentImporterContext context) { string data = System.IO.File.ReadAllText(filename, System.Text.Encoding.ASCII); return data; } }
Creating the Processor The content processor expects as input a string from the content importer and returns an object that is an instance of the AnimationCompiledData class seen in Listing 10-6. Inside the Process() function the processor takes the string that represents the file and parses the information into an array of Vector3 objects. This is done by first determining how many paths are in the file and looping through the file and saving each set of three numbers as one Vector3 position. To determine how many paths are in the file, the Split() function of the String class is used to split a file into an array using a delimiter. A delimiter is a character that marks the end of a token. This
372
Chapter 10
can be, for example, white space, a new-line character, the end of the file, and so forth. Essentially, anything can be a delimiter; which characters you choose depends on how you’ve designed your file format. In this demo the comma character is used as a delimiter to separate tokens. Tokens are the words between delimiters. For example, you could consider the words of this book as separate tokens, while the punctuation marks and spaces between the words are delimiters. When calling the Split() function, we are going through the string that represents the file’s contents and splitting up the words into tokens (i.e., an array of strings where each array element is a separated token). In the animation path file, each token is a number. Therefore, each position is made up of three numbers, so the total number of tokens divided by three gives us the total positions. As long as there are at least two positions that make up one path, the file is valid. If the file does not have a valid path or if the total number of positions is not a multiple of two, an exception will be thrown. This exception is displayed in Visual Studio when you try to compile the project. This is very useful because any errors in the files you are trying to compile can be displayed for you in a convenient manner. The content processor for the demo can be seen in Listing 10-8. The array is set to an AnimationCompiledData object that is returned by the processor. Listing 10-8 using TInput = System.String; using TOutput = AnimationPathContent.AnimationCompiledData; [ContentProcessor(DisplayName = "AnimationPathContent.AnimationPathProcessor")] public class AnimationPathProcessor : ContentProcessor { public override TOutput Process(TInput input, ContentProcessorContext context) { char[] delimiters = { ',', ' ' }; string[] tokens = input.Split(delimiters); int totalPaths = (tokens.Length / 3); int tokIndex = 0; if(((totalPaths % 2) != 0) || totalPaths < 1)
Importers/Converters
373
{ throw new Exception("Animation path length is invalid"); } Vector3[] pathList = new Vector3[totalPaths]; for(int i = 0; i < totalPaths; i++) { pathList[i] = new Vector3( Convert.ToSingle(tokens[tokIndex++]), Convert.ToSingle(tokens[tokIndex++]), Convert.ToSingle(tokens[tokIndex++])); } return new AnimationCompiledData(pathList); } }
Creating the Content Writer The content writer takes the output from the processor as its input along with a ContentWriter object. The ContentWriter parameter object is used to write the information out to the file using the Write() function. For this demo, the function is used to write out the total number of vectors that make up the Vector3 list and the list itself. The content reader will need to read this information in the same format. The content writer can be seen in Listing 10-9. Listing 10-9 using TWrite = AnimationPathContent.AnimationCompiledData; [ContentTypeWriter] public class AnimationPathWriter : ContentTypeWriter { protected override void Write(ContentWriter output, TWrite value) { output.Write(value.Data.Length); for(int i = 0; i < value.Data.Length; i++) output.Write(value.Data[i]); } public override string GetRuntimeType(TargetPlatform targetPlatform) { return typeof(Vector3[]).AssemblyQualifiedName;
374
Chapter 10
}
public override string GetRuntimeReader(TargetPlatform targetPlatform) { return "CustomContent.AnimationPathReader, CustomContent, Version=1.0.0.0, Culture=neutral"; } }
Creating the Content Reader The last class is the content reader. This class is not part of the content extension project but is instead part of the demo application’s project. To create the content reader you can add a new file to the project of the content reader type. Inside this reader will be a class that extends the ContentTypeReader class, which is a template class that takes the type of object it returns. The function that is to be overridden, the Read() function, takes as a parameter a ContentReader object and the type of object that is being read in (which is used to read for an existing instance). The ContentReader is used to read information from a file much like the ContentWriter object in the writer was used to write information to the file stream. For this demo it is convenient to create a reader that loads the entire animation path information directly into the AnimationRoute class that was created in the animation route demo. The reader for this demo specifies the ContentTypeReader’s type to be AnimationRoute, which is the type the content reader returns. The Read() function simply loads the total number of vectors from the file, then loops to read each vector two at a time. Every time each vector pair is read it is added to the AnimationRoute object that will be returned by the reader. The content reader is called when the Content.Load() function is called by the application. The returned object from the Content.Load() function is the object returned by the reader. Therefore, when we read our animation path file its contents can be loaded directly into an AnimationRoute object and is ready to use without any further setup. In the original demo we had to manually add the various paths to the AnimationRoute object, but for this demo we can assign that responsibility to the content reader. We could have read the information into a Vector3 array since that is what is stored
Importers/Converters
375
in the file, but then we would have to further add the vectors to the AnimationRoute object manually, leaving us with work that we can avoid by placing it within the content reader. The content reader for the demo can be seen in Listing 10-10. Listing 10-10 public class AnimationPathReader : ContentTypeReader { protected override AnimationRoute Read(ContentReader input, AnimationRoute existingInstance) { int size = input.ReadInt32(); AnimationRoute torusAnimation = new AnimationRoute(); for(int i = 0; i < size / 2; i++) { torusAnimation.AddPath(input.ReadVector3(), input.ReadVector3()); } return torusAnimation; } }
Testing the Custom Importer in an Application The CustomContent demo’s main source file is the same as the AnimationPath demo from Chapter 9 except that it loads the animation file using the content pipeline in the LoadContent() function rather than requiring you to specify the paths in the source code. Outside of this minor change in the LoadContent() function, the remainder of the source code in the CustomContent demo is the same as in the AnimationPath demo. For convenience, the important areas from the CustomContent demo’s main source file can be seen in Listing 10-11. Listing 10-11 public class Game1 : Microsoft.Xna.Framework.Game { GraphicsDeviceManager graphics; SpriteBatch spriteBatch; Model torus;
376
Chapter 10
Matrix[] torusFinalBones; Vector3 torusPos = Vector3.Zero; Vector3 cameraPos = new Vector3(0, 0, 50); Vector3 lookAtPos = Vector3.Zero; Vector3 upDir = Vector3.UnitY; AnimationRoute torusAnimation; public Game1() { graphics = new GraphicsDeviceManager(this); Content.RootDirectory = "Content"; }
protected override void Initialize() { base.Initialize(); }
protected override void LoadContent() { spriteBatch = new SpriteBatch(GraphicsDevice); torus = Content.Load("Models\\TorusRing"); torusFinalBones = new Matrix[torus.Bones.Count]; torusAnimation = Content.Load("Animation\\square"); }
protected override void UnloadContent() { }
protected override void Update(GameTime gameTime) { ... }
protected override void Draw(GameTime gameTime)
Importers/Converters
377
{ ... } }
3D Shooter Game Version 2 The purpose of the second version of the 3D game project is to make use of the content pipeline by processing custom data and to add gameplay elements. For the content pipeline we will create a custom importer and processor that can process a file containing spawn locations for the game enemies. The additional code and features added to this chapter’s game project include: n
Loading and rendering game enemies
n
Spawning enemies at predefined locations in the game world
n
Moving enemies toward the player in an effort to “attack” the player
n
Receiving damage from the enemies
n
Attacking enemies and increasing the player’s score
n
Adding additional elements to the heads-up display
n
Making use of the high score system from the 2D game
n
Loading spawn location information from a file
Game Enemies The game’s enemies are simple ghost models. To keep the code simple and easy to understand from a beginner’s standpoint, the enemies are essentially textured spheres. This makes collision detection, attacking, and being attacked easier to grasp for beginners and very straightforward in a 3D environment. The enemies in this game all share the same model geometry because loading the same geometry multiple times is a waste of resources and memory. Therefore, in the gameplay screen there is only one Model instance that all the ghosts use. The ghosts are individually defined using the Ghost class that was created for this game. Inside the Ghost class is a Boolean flag indicating whether
378
Chapter 10
the ghost is alive or dead, the ghost’s original looking direction, the ghost’s current position, a set of transformation matrices, and a bounding sphere. The flag representing the dead or alive status of the ghost is used to kill a ghost when it is attacked and as an indicator that a ghost is ready to be respawned. In this game, the game’s Update() function will attempt to spawn a ghost based on a specific time interval. Once it is ready to spawn a ghost, it looks for one that is not alive, assigns it a random spawn position, and then sets the ghost as alive. We’ll discuss the spawn locations later in this section. The ghost position is its translation into the game world. The direction of the ghost is the ghost’s starting direction that the 3D model is looking toward. During the game’s update, the ghost will be rotated so it is looking at the player. In order to do that, which we’ll see later on in this section, we’ll need the vector direction of the ghost and the vector direction to the player, which is calculated in the game’s Update() function. The matrices in the Ghost class are used to compute the transformation of the ghost whenever it moves or rotates. This matrix is then used when rendering the ghost to the screen. The collision bounding sphere is used solely to test if the ghost has touched the player. If so, the ghost has damaged the player, the player loses health, and the ghost is marked dead so that it can respawn. The Ghost class can be seen in Listing 10-12. The Ghost class has properties for setting and retrieving the class member variables and it has functions for moving and rotating a ghost. The movement function simply moves the ghost’s position in the direction that is specified in the first parameter of the function. When the ghost moves, we must update the center of the collision sphere to match the position of the ghost for accurate collision detection, and we must also update the transformation matrices so that the ghost is rendered in its true position. All of this occurs in the class’s Move() function. The function used to rotate the ghost is used to orient the ghost so that it is looking at a target position. This target position in the game will be the player’s location. To rotate an object so that it is pointing to a target position we will need the position to look at, the position of the ghost, the direction the ghost is currently looking, and the direction vector from the ghost’s position to the player’s position. The direction vector from the ghost and the player is used,
Importers/Converters
379
along with the ghost’s viewing direction, to determine the axis of rotation, which is found by obtaining the cross product of the two normalized vectors. The arc cosine (Math.Acos() in XNA) of the dot product between the direction the ghost is facing and the vector from the ghost to the player will give us the angle we need to rotate by. With the axis to rotate around and the angle of rotation, we can turn the ghost from its current looking direction to look at the player and create a matrix out of it using Matrix.CreateFromAxisAngle(). This rotation matrix is stored in the Matrix member variable called orientation and is combined with the ghost’s translation matrix to create its final transformation matrix. The final transformation matrix is used as the model matrix shader parameter while rendering. Listing 10-12 public class Ghost { bool isDead; Vector3 direction; Vector3 position; Matrix orientation, translation, finalTransformation; BoundingSphere collisionSphere;
public Vector3 Direction { get { return direction; } set { direction = value; } }
public Vector3 Position { get { return position; } set { position = value;
380
Chapter 10
translation = Matrix.CreateTranslation(position); finalTransformation = orientation * translation; collisionSphere.Center = position; } }
public Matrix Transformation { get { return finalTransformation; } }
public bool Alive { get { return isDead; } set { isDead = value; } }
public BoundingSphere CollisionSphere { get { return collisionSphere; } set { collisionSphere = value; } }
public Ghost(Vector3 pos, Vector3 dir, float collisionRadius) { position = pos; direction = dir; translation = Matrix.CreateTranslation(position); orientation = Matrix.Identity; finalTransformation = orientation * translation; collisionSphere = new BoundingSphere(position, collisionRadius); isDead = false; }
public void Move(Vector3 direction, float amount) { position += direction * amount; translation = Matrix.CreateTranslation(position);
Importers/Converters
381
finalTransformation = orientation * translation; collisionSphere.Center = position; }
public void OrientationToTarget(Vector3 target) { Vector3 vecToTarget = Vector3.Normalize(target – position); Vector3 axisToRotate = Vector3.Cross(direction, vecToTarget); double angle = Math.Acos((double)Vector3.Dot(direction, vecToTarget)); // Avoid divide by zero. Setting to Unit Y is suitable for this // game application. This case would occur if the cross product // of the vectors equal 0. if(axisToRotate == Vector3.Zero) axisToRotate = Vector3.UnitY; else axisToRotate.Normalize(); orientation = Matrix.CreateFromAxisAngle(axisToRotate, (float)angle); finalTransformation = orientation * translation; } }
A spawn location is defined by the Spawn class. Currently the Spawn class stores the Vector3 location where the ghost will appear. The game will store a list of these Spawn objects to define all spawn locations within the game world. The Spawn class can be seen in Listing 10-13. Listing 10-13 class Spawn { Vector3 location;
public Vector3 SpawnPosition { get { return location; } set { location = value; } }
public Spawn(Vector3 spawnPosition)
382
Chapter 10
{ location = spawnPosition; } }
Version 2 of the Game The remainder of the new code for this game project can be found in the GameplayScreen class where many functions have had new code added. The new variables in the GameplayScreen class include gameplay variables, enemy ghost-related variables, and variables for a new HUD element, the crosshair. The gameplay variables include the previous game project’s current level, score, and remaining time. Variables to limit the shooting rate of fire, and indicate ghost and respawn locations have been added. The sessionTime variable is used to store a constant representing the total amount of time the player is given in the level. The gameTimeRemaining variable is reset to this constant after every level and it decreases in the Update() function as the level progresses. The rate of fire is defined by the constant timeBetweenShots. When the game is updated, gameShotTime is decreased. Once it is below 0, a shot is allowed to fire and gameShotTime is reset to timeBetweenShots. The respawn time works the same way with gameRespawnTime and timeBetweenRespawn. The list of spawn locations is stored in spawnLocations. The ghost-related variables include ghostModel for the geometry, ghostWorldTransforms for ghostModel’s absolute transformations, ghostSpeed for the ghost’s movement speed, ghostSpeedDelta for the amount that is added to the ghost’s speed after each level to increase it and make the game more difficult, ghostDamage for the amount of damage the ghost can inflict on the player, and pointsPerGhost for the amount of points the player receives for killing a ghost. The remaining new variables include musicCue, which is an XACT audio cue for playing background music, crossHairHudRect, which represents the location and size of the crosshair sprite, and crossHairDecal, which is the crosshair texture that will be rendered to the screen. The variables of the GameplayScreen class can be seen in Listing 10-14.
Importers/Converters
Listing 10-14 class GameplayScreen : GameScreen { ContentManager content = null; SpriteFont gameFont;
Model gameWorld = null; Matrix[] gameWorldTransforms; BoundingBox gameWorldCollisionBox;
int gameScore; int gameLevel; float gameTimeRemaining; const float sessionTime = 180.0f; float gameShotTime; const float timeBetweenShots = 0.2f; float gameRespawnTime; const float timeBetweenRespawn = 3.0f; List spawnLocations = null; bool gameSessionSaved = false;
Cue musicCue = null;
float ghostSpeed; float ghostSpeedDelta; int ghostDamage; const int pointsPerGhost = 10; Model ghostModel = null; Matrix[] ghostWorldTransforms; List ghostEnemies = null;
Player player1; Camera player1Cam; float camMoveSpeed, camTurnSpeed;
383
384
Chapter 10
Rectangle hudRect; Rectangle crossHairHudRect; Texture2D hudBorder = null; Texture2D crossHairDecal = null; FrameRateCounter hudFrameRate = null; Vector2 Vector2 Vector2 Vector2 Vector2
fpsHudPos; scoreHudPos; levelHudPos; timerHudPos; healthHudPos;
public GameplayScreen() { TransitionOnTime = TimeSpan.FromSeconds(1.0); TransitionOffTime = TimeSpan.FromSeconds(0.5); } }
The LoadContent() function has been rewritten so that it calls other functions to initialize the various variables for the game. This allows for smaller functions that are easier to follow rather than one large complex function. At the end of the LoadContent() function the background music is played, which is the same music played by the menu. The smaller functions include functions to initialize the gameplay variables, HUD-related objects, the game player, and enemies. The InitializeGamePlayVariables() function sets up everything dealing with the gameplay such as the starting level, starting time, spawn locations (loaded by the content pipeline, which we’ll discuss later in this chapter), and ghost speed and damage. The InitializeHUD() function sets up everything dealing with the HUD for the game. The InitializeGameActors() function (player and enemies) is used to set up the player’s class, camera, ghostModel object, and Ghost objects. The maximum number of ghosts allowed at one time is four. The LoadContent() and each initialize function can be seen in Listing 10-15. The UnloadContent() function, which can also be seen in Listing 10-15, has code added to it to stop the background music when the game is unloading. If this is not done, the music will continue as the
Importers/Converters
menu starts a new instance of the background music, causing the two sounds to overlap. Listing 10-15 class GameplayScreen : GameScreen { public override void LoadContent() { if(content == null) content = new ContentManager(ScreenManager.Game.Services, "Content"); // Initialize game variables. InitializeGamePlayVariables(); // Pre-calculate the HUD element positions. InitializeHUD(); // Game content to load. InitializeGameWorld(); // Create the players and enemies. InitializeGameActors(); // Set up input action mapping. IInputSystem input = ((IInputSystem)ScreenManager.Game.Services.GetService( typeof(IInputSystem))); input.SetActionFunction("Move", Move); input.SetActionFunction("Move Forward", MoveForward); input.SetActionFunction("Move Backward", MoveBackward); input.SetActionFunction("Move Left", MoveLeft); input.SetActionFunction("Move Right", MoveRight); input.SetActionFunction("Fire", Fire); input.SetActionFunction("Rotate", Rotate); input.Enable(); musicCue = ((GhostArena3D)ScreenManager.Game).soundbank.GetCue( "title"); musicCue.Play(); ScreenManager.Game.ResetElapsedTime(); }
public void InitializeGamePlayVariables()
385
386
Chapter 10
{ gameScore = 0; gameLevel = 1; gameTimeRemaining = sessionTime; gameRespawnTime = timeBetweenRespawn; gameShotTime = 0.0f; gameSessionSaved = false; ghostSpeed = 80.0f; ghostSpeedDelta = 15.0f; ghostDamage = 10; spawnLocations = content.Load("Levels/Level1/Level1Spawns"); }
public void InitializeHUD() { hudBorder = content.Load("Images/black"); crossHairDecal = content.Load("Images/crosshair"); int w = ScreenManager.GraphicsDevice.Viewport.Width; int h = ScreenManager.GraphicsDevice.Viewport.Height; hudRect = new Rectangle(0, 0, w, (int)(h * 0.10f)); fpsHudPos = new Vector2((int)(w * 0.05f), (int)(h * 0.01f)); scoreHudPos = new Vector2(w – (int)(w * 0.2f), (int)(h * 0.04f)); levelHudPos = new Vector2((int)(w * 0.05f), (int)(h * 0.04f)); timerHudPos = new Vector2((int)(w * 0.4f), (int)(h * 0.04f)); healthHudPos = new Vector2(w – (int)(w * 0.2f), (int)(h * 0.01f)); crossHairHudRect = new Rectangle((int)((w / 2) – crossHairDecal.Width / 2), (int)((h / 2) – crossHairDecal.Height / 2), crossHairDecal.Width, crossHairDecal.Height); hudFrameRate = new FrameRateCounter(ScreenManager.Game, fpsHudPos); ScreenManager.Game.Components.Add(hudFrameRate); }
public void InitializeGameWorld() { gameFont = content.Load("Fonts/data"); gameWorld = content.Load("Levels/Level1/Level1");
Importers/Converters
387
gameWorldTransforms = new Matrix[gameWorld.Bones.Count]; // Calculate the bounding box of the game world. Vector3 min = new Vector3(–600, 0, –600); Vector3 max = new Vector3(600, 216, 600); gameWorldCollisionBox = new BoundingBox(min, max); }
public void InitializeGameActors() { player1 = new Player(); player1.Health = 100; player1Cam = new Camera(new Vector3(0, 75, 0), new Vector3(0, 75, –1), Vector3.UnitY); player1.SetCollisionCenter(player1Cam.Position); ghostModel = content.Load("Models/ghost"); ghostWorldTransforms = new Matrix[ghostModel.Bones.Count]; float radius = ghostModel.Meshes[0].BoundingSphere.Radius; ghostEnemies = new List(); ghostEnemies.Add(new Ghost(Vector3.Zero, ghostEnemies.Add(new Ghost(Vector3.Zero, ghostEnemies.Add(new Ghost(Vector3.Zero, ghostEnemies.Add(new Ghost(Vector3.Zero,
–Vector3.UnitZ, –Vector3.UnitZ, –Vector3.UnitZ, –Vector3.UnitZ,
radius)); radius)); radius)); radius));
}
public override void UnloadContent() { if(musicCue != null) musicCue.Stop(AudioStopOptions.Immediate); ScreenManager.Game.Components.Remove(hudFrameRate); content.Unload(); } }
We added a line of code to the OnLevelChange() function from the last game project that increases the speed of the ghost by the constant speed amount every time the level changes. On the other end of the spectrum, there is the new function OnGameOver() that is called when the player has died. The OnGameOver() function will
388
Chapter 10
display a message box that alerts players that they have died and displays their score. When a player accepts the game over screen, the GameOverMessageBoxAccepted() function is called to load the main menu and save to the high score list using the HighScoreList class from the 2D game. These functions can be seen in Listing 10-16. Spawn locations are randomly obtained by calling the new function GetRandomSpawnLocation(). This function uses the Random class to get a random number between 0 and the total number of spawn locations. This random value is used as an array index that returns the random spawn location from the spawn list. This function can also be seen in Listing 10-16. The last of the gameplay functions is the Update() function. This function adds code to decrease time from the shot and ghost respawn timers. The function also adds code to test if it is time to spawn a new ghost, to test if the player has no health remaining, and to move the existing ghost toward the player. If it is time to spawn a new ghost, the code will loop through the ghost list to find one that is dead, mark it as alive, and give it a random spawn position using the GetRandomSpawnLocation() function. Since the list is small (four ghosts at most) there is no performance hit by linearly looping through it to search for a dead ghost. If the player has no health, the OnGameOver() function is called to end the game. Any ghosts that are alive are moved in the direction of the player. Additionally, if any ghost’s bounding sphere collides with the player’s bounding sphere, the player receives damage and the ghost is marked as dead so that it is no longer rendered and so the Update() function can eventually respawn the ghost. The Update() function can be seen in Listing 10-16, along with the other gameplay functions we mentioned in this section. Listing 10-16 class GameplayScreen : GameScreen { public void OnLevelChange() { gameTimeRemaining = sessionTime; gameLevel++; ghostSpeed += ghostSpeedDelta;
Importers/Converters
389
}
public void OnGameOver() { String message = "Game Over. Final score: " + gameScore; MessageBoxScreen messageBox = new MessageBoxScreen(message); messageBox.Accepted += GameOverMessageBoxAccepted; ScreenManager.AddScreen(messageBox); if(gameSessionSaved == false) { HighScoreList scores = new HighScoreList(); scores.AddScore(new HighScore( DateTime.Now.ToShortDateString(), gameLevel, gameScore)); scores.Save(); gameSessionSaved = true; } }
void GameOverMessageBoxAccepted(object sender, EventArgs e) { LoadingScreen.Load(ScreenManager, true, new BackgroundScreen(), new MainMenuScreen()); }
public Vector3 GetRandomSpawnLocation(int seed) { Random random = new Random(seed); int spawnLocationIndex = random.Next(0, spawnLocations.Count - 1); return spawnLocations[spawnLocationIndex].SpawnPosition; }
public override void Update(GameTime gameTime, bool otherScreenHasFocus, bool coveredByOtherScreen) { base.Update(gameTime, otherScreenHasFocus, coveredByOtherScreen);
390
Chapter 10
hudFrameRate.Update(gameTime); float time = (float)gameTime.ElapsedGameTime.TotalSeconds; camMoveSpeed = 75.0f * time; camTurnSpeed = 3.0f * time; if(IsActive) { gameTimeRemaining –= time; gameRespawnTime –= time; gameShotTime –= time; // Handle running out of time. if(gameTimeRemaining