Creating a simple 3D game engine in XNA/Printable version
This is the print version of Creating a simple 3D game engine in XNA You won't see this message or any elements not part of the book's content when you print or preview this page. |
The current, editable version of this book is available in Wikibooks, the open-content textbooks collection, at
https://en.wikibooks.org/wiki/Creating_a_simple_3D_game_engine_in_XNA
Introduction
In learning XNA 3, then 4 myself, as I searched the internet, and read many books and I noticed there were no really good tutorials out there to really get you started with an engine. There is an old saying, you can give a man a fish, feed him for a day, or teach him to fish and feed him for a lifetime I mention this, because every book I've read, and tutor I've seen does just that, gives you a fish. I intend to teach you how to fish. As a hobbyist programmer myself, I write this for other hobbyist programmers looking to get some game creation going on. We are going to create a clone of the old classic arcade game, Asteroids by Atari in 1979, only we use 3D models for our graphics. Using the engine we create together with the 3D models and sounds I supply, it should only take a few hours a day to complete this tutor. Then it would only take hours to make a simple game of your own using this engine.
I assume you have a basic or general working knowledge of C#, Java, or C++ at least. What I mean by that is that you know what polymorphic methods are, and other OOP practices so I don't talk over your head. This is meant as an introduction to XNA 4 and real game programming using a game engine, not for learning general programming.
I start with a tutorial on how to make a simple 3D game engine, then proceed to use that engine to make the classic game of Asteroids. I only made one change to the original and that is when the player is hit, their ship does not explode, instead I take down their hits available points, the player starts with three. That was the one thing that frustrated me, starting after getting hit, and bam a rock hits the player ship again! As would often happen it would put the ship out, right in front of a rock. It would only do a check to make sure a rock was not at the spot.
Chapter One
XNA 4 Engine Chapter One I assume you already have XNA 4 ready for use, if not go here to see what you need for it. I have the resources you need for this tutorial, unless you already have made your own space rocks, and spaceships as well as the shots. You can download the zip file of all the models here. At the end of each chapter I will have the project done so far, for you to download without assists. The assists will be in a separate zip file at the bottom of the page that I will explain where to unzip when the time comes. You can see the final working game in this video. What it will look like when you finish the first part of my series of five chapters that will become a book on Amazon. We are going to get our project created, and planned out. Organization is extremely important. Unless you like banging your head on the table, you would agree. First create a new XNA 4 Windows Game project, name it Asteroids as in figure 1.
After you hit ‘OK’ you should see as it is in figure 2.
Very good, now we organize the project folders. First add the folders Entities, then Engine to the project as you see in figure 3.
Then add Models and Textures folder to the Content part of the project. You should have the two folders as well as you see in figure 4.
The project folders should be set up as they look in figure 5.
We will be only working in the Engine folder for now. Even though we will not be using the other folders until later chapters, I like planning ahead; it has worked out for me well so far.
Now we will get the game1 class ready. First, let us make it so the game window is of a reasonable size. By now everyone should have a screen that will handle this, I know that you must have a video card that is not too old, or old tech, or XNA 4 wont compile unless you use the Reach limited API, more about that in later chapters. My engine will work in either, the Reach mode mainly turns on some limiters.
You should still have Game1.cs open; in the Constructor add these lines from listing 1.1:
Listing 1.1 game1 edit
Window.Title = "Asteroids 3D in XNA 4"; graphics.PreferredBackBufferWidth = 1024; graphics.PreferredBackBufferHeight = 600; graphics.ApplyChanges();
Now remove this line in listing 1.2:
LIsting 1.2 game1
graphics = new GraphicsDeviceManager(this);
You should have this now in figure 6.
This makes it so the first thing that happens is it adjusts the screen size that anyone who wants to play the game, will be able to see it good enough to enjoy it. 1024 by 600 is just under WSVGA in height. That makes room for the title in the window for anyone with at least SVGA, we will assume that would be anyone with a PC capable of running a game made using XNA. The default VGA 600 by 480 is just too small for our purposes. After setting the back buffers for both the height and width of the window, you must apply the changes for it to take effect. That is a built in method of the game glass that does as it is named. When you set the properties, all that does is change those variables in that class. So in order to have those take affect you must have them all applied, even if you only change a few. This game, by the end will be resolution independent by the time it is done. This is just for now. In a later chapter we will see how to look to see what the computer it is running on is able to handle, and use that to set the default screen size. It is now time to get started on the engine class. First we need to create a new class. Call it Services; add it inside the engine folder as follows in figure 7 by right clicking on the Engine folder select Add, then click on the ‘class…’ and you will see what is in figure 8.
You should now have as you see it in figure 9.
Now we need to add another new class that the Services class will use, the Camera class as follows. We need to add this next because we can’t build the Services class that uses it without having it there first. So we will start with the Camera class then work on the Services class next. It should look as it does in figure 10. Then after you hit ‘OK’ you should see as it is in figure 11.
First we need to add some using statements I also like to use regions for them as follows in listing 2.1:
Listing 2.1 Camera.cs addition
#region Using using System; using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Content; using Microsoft.Xna.Framework.GamerServices; using Microsoft.Xna.Framework.Graphics; #endregion
I removed the System.Collections.Generic, System.Linq and System.Text namespaces because they are not used in this class. We need to make this a public class so put public in front of class Camera as shown in the screen shot. To make it so you can copy the engine to any project, we are going to make the namespace Engine, so change it to how it is in listing 2.1.1 Listing 2.1.1 Camera.cs edit namespace
namespace Engine
The next thing I will have you do is add all the variables, or as I like to call them fields. I also like to use regions, so I’ll also have you add those in as well. Add class level fields as follows in listing 2.2: Listing 2.2 Camera.cs addition
#region Fields private Matrix cameraRotation; #endregion
This is a sudden introduction to the Matrix class, part of the XNA library, I’m not even sure how it works; to me it is like a kind of magic. However you don’t need to know how it works, in order to use it. Just like you don’t need to know how a car works in order to use that. Go here to read up more on the magic of the Matrix.
Next I’ll have you add the properties that the class will need as follows in listing 2.3:
Listing 2.3 Camera.cs addition
#region Properties public Matrix View { get; set; } public Matrix Projection { get; protected set; } public Vector3 Target { get; set; } #endregion
You will notice that the View and Projection are of type Matrix, and the target is a Vector3. This is very important. The camera works kind of like a projector, as if what it sees is being projected onto your screen. I’ll explain more on that when we get back to this class.
Just one more thing we need to add, to get it ready in listing 2.4.
Listing 2.4 Camera.cs addition
#region Constructor #endregion
This is what you should have so far see listing 2.5 and figure 12.
Listing 2.5 Camera.cs so far
#region Using using System; using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Content; using Microsoft.Xna.Framework.GamerServices; using Microsoft.Xna.Framework.Graphics; #endregion namespace Engine { public class Camera : PositionedObject { #region Fields private Matrix cameraRotation; #endregion #region Properties public Matrix View { get; set; } public Matrix Projection { get; protected set; } public Vector3 Target { get; set; } #endregion #region Constructor #endregion } }
Before we can go any further, we need to create another class, the Positioned Object class. Because the camera is also going to use this class, that keeps track of where everything is, and will be. This class though small, is very powerful. It uses the game timer to keep track of time, so it can be applied to movement. This keeps things moving smoothly no matter how fast the computer is that the game runs on. I cannot stress this enough, never ever use game loops to affect how much something moves. Even though that was used back in the 80s when most computers were the same speed, it was a bad practice. Later games could not be played on newer computers because they ran too fast. Not only that, it made the game movement dependent on how fast each frame is being processed. You like things smooth I’m sure, like I do. So we create the Positioned Object class, called PositionedObject.cs in the engine folder. I’ll assume by now you know how to make a new class. First we tackle the Using name spaces as follows listing 3.1:
Listing 3.1 PositionedObject.cs addition
#region Using using System; using Microsoft.Xna.Framework; #endregion
Now we need to make it a public class, just like we did for Camera, add public in front of class. Most classes will be public for our game as well as add DrawableGameComponent to the end after the colon, this is the inherited class. The reason we use that class is because the game objects that will inherit this class are drawn, the draw method does not need to be in this class. If you do it like this, every object that inherits this class in the game will automatically get there update called, and draw called every frame. Now some argue “Then you don’t have any control of the order they get called” That assumption is not correct. You can indeed control the order, not only that, but you have more control then you otherwise would. There is a property for update order, and draw order that can be changed at any time. By default the order is by the order they are instanced. Also if you don’t want the Draw method called every frame, use the Visible property, if you don’t want the Update method called every frame use the Enabled property. It is just that easy. Just because you don’t know how to use something does not mean it cannot be done. There is a reason this is built into the library, and one should use all the tools at their disposal. I intend to correct that fallacy. By the time we are done you will be kissing those Component classes, if you are at all like me.
We need to change the namespace on this class too, just like it is in listing 3.1.1
Listing 3.1.1 PositionedObject.cs edit namespace
namespace Engine
The class line should now look like this in listing 3.2:
Listing 3.2 PositionedObject.cs addition
public abstract class PositionedObject : DrawableGameComponent
This makes it so a class can only be inherited, and not instanced. That should be done on all base classes as a best practice.
Now we add the fields as you see in listing 3.2.5
Listing 3.2.5 PositionedObject.cs addition
#region Fields private float frameTime; // Doing these as fields is almost twice as fast as if they were properties. // Also, sense XYZ are fields they do not get data binned as a property. public Vector3 Position; public Vector3 Acceleration; public Vector3 Velocity; public Vector3 RotationInRadians; public Vector3 ScalePercent; public Vector3 RotationVelocity; public Vector3 RotationAcceleration; #endregion
Now, you may be wondering why we don’t use properties for all of these, well it turns out using properties is slower than fields. It is the same reason the Vector3 does not use properties. This class is run every frame, so speed is extremely important, every millisecond counts when you realize that every object on screen will be using this class.
Now I want you to add the constructor as follows in listing 3.3:
Listing 3.3 PositionedObject.cs addition
#region Constructor /// <summary> /// This gets the Positioned Object ready for use, initializing all the fields. /// </summary> /// <param name="game">The game class</param> public PositionedObject(Game game) : base(game) { } #endregion
Here we only need to pass the game class to the component class.
Next is the method that does all the work. Notice the override, if you know about what this does already; move along, if not read on. There is already a method by the same name that is a virtual method that we override. When that method gets called, it goes up the line to this class getting called automatically by the component class; we then go back down the line to the base to call it down the line. We are going to stop here, and come back later. The update method uses a method that we need to make in the Services class. This is how it should look now as seen in listing 3.5.
Listing 3.5 PositionedObject.cs so far
#region Using using System; using Microsoft.Xna.Framework; #endregion namespace Engine { public class PositionedObject : DrawableGameComponent { #region Fields private float frameTime; // Doing these as fields is almost twice as fast as if they were properties. // Also, sense XYZ are fields they do not get data binned as a property. public Vector3 Position; public Vector3 Acceleration; public Vector3 Velocity; public Vector3 RotationInRadians; public Vector3 ScalePercent; public Vector3 RotationVelocity; public Vector3 RotationAcceleration; #endregion #region Constructor /// <summary> /// This gets the Positioned Object ready for use, initializing all the fields. /// </summary> /// <param name="game">The game class</param> public PositionedObject(Game game) : base(game) { game.Components.Add(this); } #endregion #region Public Methods #endregion } }
Notice in figure 13 I still have all the classes open. We will be working on them in the future, so you may want to leave them open too so you can just tab back and forth between them.
Now we move back to the Services class to work on that. You may find yourself doing this allot, moving from class to class, as they are interdependent on each other, and you should wait to add parts that you have planned to add until they are created to add them. Now I will have you first change the class line, this is a special class, known as a singleton class. To make sure it cannot be called or initialized. We will use the sealed keyword, change that line as follows in listing 4.1:
Listing 4.1 Services.cs edit public sealed class Services We need to change the namespace in this class too, just like it is in listing 4.1.1 Listing 4.1.1 Services.cs edit namespace namespace Engine Next we add the fields as follows in listing 4.2: Listing 4.2 Services.cs addition
#region Fields private static Services instance = null; private static GraphicsDevice graphics; private static Random randomNumber; #endregion
We are going to make sure this class can only have one existence in life, so we have the Services instance line. This is later checked to make sure it can only have one instance. There can be only one! Here we have the services that our class will give access to,l the camera, graphics, and random number generator. The graphics is well, the graphics, everything you see. Here we have a random number generator, you must make sure to only have one of these, what better way to do that then to have it in this class?
Now I’m going to have you add all the properties, and there are a few. Add them as follows in listing 4.3:
Listing 4.3 Services.cs addition
#region Properties /// <summary> /// This is used to get the Services Instance /// Instead of using the mInstance this will do the check to see if the Instance is valid /// where ever you use it. It is also private so it will only get used inside the engine services. /// </summary> private static Services Instance { get { //Make sure the Instance is valid if (instance != null) { return instance; } throw new InvalidOperationException("The Engine Services have not been started!"); } } public static Camera Camera { get { return camera; } } public static GraphicsDevice Graphics { get { return graphics; } } public static Random RandomNumber { get { return randomNumber; } } /// <summary> /// Returns elapsed seconds, in milliseconds. /// </summary> /// <returns>double</returns> /// <summary> /// Returns the window size in pixels, of the height. /// </summary> /// <returns>int</returns> public static int WindowHeight { get { return graphics.ScissorRectangle.Height; } } /// <summary> /// Returns the window size in pixels, of the width. /// </summary> /// <returns>int</returns> public static int WindowWidth { get { return graphics.ScissorRectangle.Width; } } #endregion
I hope you don’t find this overwhelming, to have you add all of those at once. There is allot going on here, and we will use these at a later time. Remember, plan ahead! As you can see the first one is the instance checker, I have it explained in the summary. Next are access properties for the camera, graphics, game time, and random number generator. Then we have properties to access the Window height and width. Notice the Camera has a private set accessor that is because we pass in the reference of the camera from the Game class on initialization, as you will see when we get to that method. That is the only time we want to allow access to set the camera reference for the main camera.
Next we add the constructor as follows in listing 4.4:
Listing 4.4 Services.cs addition
#region Constructor /// <summary> /// This is the constructor for the Services /// You will note that it is private that means that only the Services can create itself. /// </summary> private Services(Game game) { } #endregion
You will notice that it is private; this makes it so you cannot instance it outside itself. Why do that, you ask? This is how you do a singleton class. Remember, there can be only one. This is all we are going to work on this class for now. We need to go back to the camera class again before we go any further.
This is what you should have so far in listing 4.5:
Listing 4.5 Services.cs so far
#region Using using System; using System.Collections.Generic; using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Graphics; using Microsoft.Xna.Framework.Content; #endregion namespace Engine { public sealed class Services { #region Fields private static Services instance = null; private static GraphicsDevice graphics; private static Random randomNumber; #endregion #region Properties /// <summary> /// This is used to get the Services Instance /// Instead of using the mInstance this will do the check to see if the Instance is valid /// where ever you use it. It is also private so it will only get used inside the engine services. /// </summary> private static Services Instance { get { //Make sure the Instance is valid if (instance != null) { return instance; } throw new InvalidOperationException("The Engine Services have not been started!"); } } public static Camera Camera { get; private set; } public static GraphicsDevice Graphics { get { return graphics; } } public static Random RandomNumber { get { return randomNumber; } } /// <summary> /// Returns elapsed seconds, in milliseconds. /// </summary> /// <returns>double</returns> /// <summary> /// Returns the window size in pixels, of the height. /// </summary> /// <returns>int</returns> public static int WindowHeight { get { return graphics.ScissorRectangle.Height; } } /// <summary> /// Returns the window size in pixels, of the width. /// </summary> /// <returns>int</returns> public static int WindowWidth { get { return graphics.ScissorRectangle.Width; } } #endregion #region Constructor /// <summary> /// This is the constructor for the Services /// You will note that it is private that means that only the Services can only create itself. /// </summary> private Services(Game game) { } #endregion #region Public Methods #endregion } }
Alright, you knew I was going to do this to you, back to the Camera class! I hope by the end of this chapter you will be able to see why I keep going back and forth like this. You may have to do what I do, and drink lots of coffee. With the Camera class ready for editing, the first thing we are going to add the class it will inherit. So change the class line as follows in 5.1:
Listing 5.1 Camera.cs edit
public class Camera : PositionedObject
We need to change the namespace in this class as well, just like it is in listing 5.1.1
Listing 5.1.1 Camera.cs edit namespace
namespace Engine
Now we can add the constructor, and this one does allot so read carefully as follows in listing 5.2:
Listing 5.2 Camera.cs edit
#region Constructor public Camera(Game game, Vector3 position, Vector3 target, Vector3 rotation, bool Orthographic, float near, float far) : base(game) { Position = position; RotationInRadians = rotation; Target = target; if (Orthographic) { Projection = Matrix.CreateOrthographic(Game.Window.ClientBounds.Width, Game.Window.ClientBounds.Height, near, far); } else { Projection = Matrix.CreatePerspectiveFieldOfView(MathHelper.PiOver4, (float)Game.Window.ClientBounds.Width / (float)Game.Window.ClientBounds.Height, near, far); } } #endregion
The first line is very important, you see it calls the game components collection. That is how the Game Component keeps track of what class to do the update on, and when. They are by default updated in order that they are added. So every class that is of the Positioned Object or Game Component needs to be added like that, or it will not be updated. It is the only thing you need to watch out for. I’ll make it easy, when we get there, I’ll have you make a generic enemy class that all but the player will inherit, it will have that line in it, so you will not need to remember to put it there every time. Only the player will need it along with any class that is not an object that is standalone class.
Now you can see how the Matrix is so magical. Now that we have the positioned object class we can inherit from that, so we can use it to keep track of movement and its correct position. Because of how the camera works, we must translate the location into something it understands. We use methods for just that, built into the library! We also have a switch in here just in case we want to use this for an orthographic camera. That would be for a menu, or HUD, or 2D game using sprites. This is a 3D engine so the default camera is not orthographic. This you will see when we get back to the Services class. The rest is just passed into the Position and Rotation, and we added the Target Vector3 earlier. When you instance another camera, you will tell it what to do then, that is all passed in, as you can see. Also, you see the near and far float type fields, that tells it how far and how close to the camera to render objects. All of that is calculated in the matrix, as you see there. Now we move on to the public methods, first is the Initialize method as follows in listing 5.3:
Listing 5.3 Camera.cs edit
#region Public Methods /// <summary> /// Allows the game component to perform any initialization it needs to before starting /// to run. This is where it can query for any required services and load content. /// </summary> public override void Initialize() { base.Initialize(); cameraRotation = Matrix.Identity; } #endregion
The Matrix.Identity property is a blank but not null Matrix, it is like making a new int of 0 such as int i = 0; so you see, it does not do much, but it is handy. You remember the rest I hope from the same method we did earlier in another class. Next we have the Update method, add as follows inside the same region as seen in listing 5.4:
Listing 5.4 Camera.cs edit
/// <summary> /// Allows the game component to update itself via the GameComponent. /// </summary> /// <param name="gameTime">Provides a snapshot of timing values.</param> public override void Update(GameTime gameTime) { base.Update(gameTime); // This rotates the free camera. cameraRotation = Matrix.CreateFromAxisAngle(cameraRotation.Forward, RotationInRadians.Z) * Matrix.CreateFromAxisAngle(cameraRotation.Right, RotationInRadians.X) * Matrix.CreateFromAxisAngle(cameraRotation.Up, RotationInRadians.Y); // Make sure the camera is always pointing forward. Target = Position + cameraRotation.Forward; View = Matrix.CreateLookAt(Position, Target, cameraRotation.Up); }
I hope you are ready to get your fingers dirty, because you are looking at where the work gets done for the camera. This gets called on every frame, but this all must be done on every one of them. The Matrix has every method you will ever need for 3D space. In order to rotate something in 3D space, the Matrix is used to figure out how you want it done. When you multiply the X, Y and Z of the rotation, in that order, it makes it so. Notice the cameraRotation.Forward, Right, and Up, that is so it knows how you want that calculated. Each one corresponds to how we want to have it show up, what way is up, to the right, and facing forward relative to the camera that projects onto the screen. Rotating objects in 3D space could be very complicated, so XNA makes it as simple as possible. When we get back to the Positioned Object class you will see how we make it so this method is called every frame automatically. Next we add the Draw method, this will be the first time you see this method. Remember, as a great book says, “Don’t Panic”. You will add this inside the same region, just after Update as follows in listing 5.5:
Listing 5.5 Camera.cs edit
public void Draw(BasicEffect effect) { effect.View = View; effect.Projection = Projection; }
Now you will notice there is no override. That is because this is the inherited class does not have a Draw method. If you remember, the Positioned Object class uses the Draw Component. That means that the Draw method will be called every frame automatically. That completes our Camera class. One class down, two to go.
This is the finished Camera class as seen in listing 5.6:
Listing 5.6 Camera.cs so far
#region Using using System; using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Content; using Microsoft.Xna.Framework.GamerServices; using Microsoft.Xna.Framework.Graphics; #endregion namespace Engine { public class Camera : PositionedObject { #region Fields private Matrix cameraRotation; #endregion #region Properties public Matrix View { get; set; } public Matrix Projection { get; protected set; } public Vector3 Target { get; set; } #endregion #region Constructor public Camera(Game game, Vector3 position, Vector3 target, Vector3 rotation, bool Orthographic, float near, float far) : base(game) { Position = position; RotationInRadians = rotation; Target = target; if (Orthographic) { Projection = Matrix.CreateOrthographic(Game.Window.ClientBounds.Width, Game.Window.ClientBounds.Height, near, far); } else { Projection = Matrix.CreatePerspectiveFieldOfView(MathHelper.PiOver4, (float)Game.Window.ClientBounds.Width / (float)Game.Window.ClientBounds.Height, near, far); } } #endregion #region Public Methods /// <summary> /// Allows the game component to perform any initialization it needs to before starting /// to run. This is where it can query for any required services and load content. /// </summary> public override void Initialize() { base.Initialize(); cameraRotation = Matrix.Identity; } /// <summary> /// Allows the game component to update itself via the GameComponent. /// </summary> /// <param name="gameTime">Provides a snapshot of timing values.</param> public override void Update(GameTime gameTime) { base.Update(gameTime); // This rotates the free camera. cameraRotation = Matrix.CreateFromAxisAngle(cameraRotation.Forward, RotationInRadians.Z) * Matrix.CreateFromAxisAngle(cameraRotation.Right, RotationInRadians.X) * Matrix.CreateFromAxisAngle(cameraRotation.Up, RotationInRadians.Y); // Make sure the camera is always pointing forward. Target = Position + cameraRotation.Forward; View = Matrix.CreateLookAt(Position, Target, cameraRotation.Up); } public void Draw(BasicEffect effect) { effect.View = View; effect.Projection = Projection; } #endregion } }
I think it is time we finish up the Positioned Object class as well, we only need to add the last two methods, the update method. So you now need to bring the PositionedObject.cs file class back to the front. Inside the public region, just below the Initialize class, add the Update class as follows in listing 6.1:
Listing 6.1 PositionedObject.cs edit
/// <summary> /// Allows the game component to be updated. /// </summary> /// <param name="gameTime">Provides a snapshot of timing values.</param> public override void Update(GameTime gameTime) { base.Update(gameTime); frameTime = (float)gameTime.ElapsedGameTime.Seconds; Velocity += Acceleration * frameTime; Position += Velocity * frameTime; RotationVelocity += RotationAcceleration * frameTime; RotationInRadians += RotationVelocity * frameTime; }
The reason we use the frameTime float like this, is so there is only one outside call here, instead of four. Then we use the elapsed game time in total seconds to calculate the velocity and from acceleration multiplied by the seconds. That is used to calculate the current position by adding the velocity multiplied by the seconds. The same applies for the RotationVelocity and RotationAcceleration. See, this does all the work, and it will do it for every game object we have on drawing on the screen. We don’t use the Services access to the same thing because that would be another outside class call that makes another outside call, and use more CPU then just one. That adds up, when everything on the game screen uses this method.
That finishes up our Positioned Object class. Here is the finished class in listing 6.3:
Listing 6.3 PositionedObject.cs so far
#region Using using System; using Microsoft.Xna.Framework; #endregion namespace Engine { public class PositionedObject : DrawableGameComponent { #region Fields private float frameTime; // Doing these as fields is almost twice as fast as if they were properties. // Also, sense XYZ are fields they do not get data binned as a property. public Vector3 Position; public Vector3 Acceleration; public Vector3 Velocity; public Vector3 RotationInRadians; public Vector3 ScalePercent; public Vector3 RotationVelocity; public Vector3 RotationAcceleration; #endregion #region Constructor /// <summary> /// This gets the Positioned Object ready for use, initializing all the fields. /// </summary> /// <param name="game">The game class</param> public PositionedObject(Game game) : base(game) { game.Components.Add(this); } #endregion #region Public Methods /// <summary> /// Allows the game component to be updated. /// </summary> /// <param name="gameTime">Provides a snapshot of timing values.</param> public override void Update(GameTime gameTime) { base.Update(gameTime); frameTime = (float)gameTime.ElapsedGameTime.Seconds; Velocity += Acceleration * frameTime; Position += Velocity * frameTime; RotationVelocity += RotationAcceleration * frameTime; RotationInRadians += RotationVelocity * frameTime; } #endregion } }
You only have one more class to get done before we move on to chapter two! You should feel proud of yourself to have made it so far. You are so close now to having a game engine! Bring up the Services class, and add the Initialize class inside the public methods region as follows in listing 7.1:
Listing 7.1 Services.cs edit
/// <summary> /// This is used to start up Panther Engine Services. /// It makes sure that it has not already been started if it has been it will throw and exception /// to let the user know. /// /// You pass in the game class so you can get information needed. /// </summary> /// <param name="game">Reference to the game class.</param> /// <param name="graphicsDevice">Reference to the graphic device.</param> /// <param name="Camera">For passing the reference of the camera when instanced.</param> public static void Initialize(Game game, GraphicsDevice graphicsDevice, Camera camera) { //First make sure there is not already an instance started if (instance == null) { //Create the Engine Services instance = new Services(game); //Reference the camera to the property. Camera = camera; graphics = graphicsDevice; randomNumber = new Random(); return; } throw new Exception("The Engine Services have already been started."); }
This method, like the rest of the methods is a static method too. We can access them from any class in our game this way. We will be adding the call for this in the game class, where we pass in the numbers we want. I made the Z default of 20, which is a good size to start with. The Z, is the plane that goes in and out of the screen, in our setup. That is the standard way of doing it in video games. The X is the up and down of the screen and the Y of course is the left and right. The Zero is dead center. The camera needs to be back a ways so we can see what is going on with the models we place in our world. That finishes up our Services class, for now. There are extra methods I will have you add in later chapters. They are helper methods such as one to figure the angle from two points in space. I’ll explain them as I add them. Here is the finished class in listing 7.2:
Listing 7.2 Services.cs so far
#region Using using System; using System.Collections.Generic; using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Graphics; using Microsoft.Xna.Framework.Content; #endregion namespace Engine { public sealed class Services { #region Fields private static Services instance = null; private static GraphicsDevice graphics; private static Random randomNumber; #endregion #region Properties /// <summary> /// This is used to get the Services Instance /// Instead of using the mInstance this will do the check to see if the Instance is valid /// where ever you use it. It is also private so it will only get used inside the engine services. /// </summary> private static Services Instance { get { //Make sure the Instance is valid if (instance != null) { return instance; } throw new InvalidOperationException("The Engine Services have not been started!"); } } public static Camera Camera { get; private set; } public static GraphicsDevice Graphics { get { return graphics; } } public static Random RandomNumber { get { return randomNumber; } } /// <summary> /// Returns the window size in pixels, of the height. /// </summary> /// <returns>int</returns> public static int WindowHeight { get { return graphics.ScissorRectangle.Height; } } /// <summary> /// Returns the window size in pixels, of the width. /// </summary> /// <returns>int</returns> public static int WindowWidth { get { return graphics.ScissorRectangle.Width; } } #endregion #region Constructor /// <summary> /// This is the constructor for the Services /// You will note that it is private that means that only the Services can only create itself. /// </summary> private Services(Game game) { } #endregion #region Public Methods /// <summary> /// This is used to start up Panther Engine Services. /// It makes sure that it has not already been started if it has been it will throw and exception /// to let the user know. /// /// You pass in the game class so you can get information needed. /// </summary> /// <param name="game">Reference to the game class.</param> /// <param name="graphicsDevice">Reference to the graphic device.</param> /// <param name="Camera">For passing the reference of the camera when instanced.</param> public static void Initialize(Game game, GraphicsDevice graphicsDevice, Camera camera) { //First make sure there is not already an instance started if (instance == null) { //Create the Engine Services instance = new Services(game); //Reference the camera to the property. Camera = camera; graphics = graphicsDevice; randomNumber = new Random(); return; } throw new Exception("The Engine Services have already been started."); } #endregion } }
Open up the game1 class again, I will have you get it up to speed. First find the Game1 constructor, and at the bottom add the line from listing 8.1.1. Then find the Initialize method, and add this line in listing 8.1.2: Listing 8.1.1 Game1.cs edit
Camera = new Engine.Camera(this, new Vector3(0, 0, 275), Vector3.Forward, Vector3.Zero, false, 200, 325);
Listing 8.1.2 Game1.cs edit
Engine.Services.Initialize(this, graphics.GraphicsDevice, Camera);
This gets things started, setting the camera with a near plane of 200, and far plane of 325. That means that every object that is between 200 and 325 units from the camera that the camera is pointing at, will be drawn. We are going to edit the Update method; here is what it should look like now in listing 8.2. I do this for all my games, so that I can just hit the Esc key to exit the game. In fact I change the default game class so that I don't have to add that part, I don't know why it did not come that way in the first place. Sense the user will always have a keyboard, but may not have a 360-game controller.
Listing 8.2 Game1.cs edit
/// <summary> /// Allows the game to run logic such as updating the world, /// checking for collisions, gathering input, and playing audio. /// </summary> /// <param name="gameTime">Provides a snapshot of timing values.</param> protected override void Update(GameTime gameTime) { // Allows the game to exit if (GamePad.GetState(PlayerIndex.One).Buttons.Back == ButtonState.Pressed || Keyboard.GetState().IsKeyDown(Keys.Escape)) this.Exit(); base.Update(gameTime); }
I have you change the first line that exits the game. I've added keyboard input for the escape key. Scroll back up to the top, in the constructor class add this line in listing 8.3:
Listing 8.3 Game1.cs edit
graphics = new GraphicsDeviceManager(this);
That means we are done with chapter one. Here is what the game1 class should look like now in listing 8.4.
Listing 8.4 Game1.cs so far
#region Using using System; using System.Collections.Generic; using System.Linq; using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Audio; using Microsoft.Xna.Framework.Content; using Microsoft.Xna.Framework.GamerServices; using Microsoft.Xna.Framework.Graphics; using Microsoft.Xna.Framework.Input; using Microsoft.Xna.Framework.Media; #endregion namespace Asteroids { /// <summary> /// This is the main type for your game /// </summary> public class Game1 : Microsoft.Xna.Framework.Game { private GraphicsDeviceManager graphics; private Engine.Camera Camera; public Game1() { graphics = new GraphicsDeviceManager(this); Window.Title = "Asteroids 3D in XNA 4 Chapter One"; graphics.PreferredBackBufferWidth = 1024; graphics.PreferredBackBufferHeight = 600; graphics.ApplyChanges(); Content.RootDirectory = "Content"; // Here we instance the camera, setting its position, target, rotation, whether it is orthographic, // then finally the near and far plane distances from the camera. Camera = new Engine.Camera(this, new Vector3(0, 0, 275), Vector3.Forward, Vector3.Zero, false, 200, 325); } /// <summary> /// 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. /// </summary> protected override void Initialize() { Engine.Services.Initialize(this, graphics.GraphicsDevice, Camera); base.Initialize(); } /// <summary> /// LoadContent will be called once per game and is the place to load /// all of your content. /// </summary> protected override void LoadContent() { } /// <summary> /// UnloadContent will be called once per game and is the place to unload /// all content. /// </summary> protected override void UnloadContent() { // TODO: Unload any non ContentManager content here } /// <summary> /// Allows the game to run logic such as updating the world, /// checking for collisions, gathering input, and playing audio. /// </summary> /// <param name="gameTime">Provides a snapshot of timing values.</param> protected override void Update(GameTime gameTime) { // Allows the game to exit if (GamePad.GetState(PlayerIndex.One).Buttons.Back == ButtonState.Pressed || Keyboard.GetState().IsKeyDown(Keys.Escape)) this.Exit(); base.Update(gameTime); } /// <summary> /// This is called when the game should draw itself. /// </summary> /// <param name="gameTime">Provides a snapshot of timing values.</param> protected override void Draw(GameTime gameTime) { GraphicsDevice.Clear(new Color(5, 0, 10)); base.Draw(gameTime); } } }
You should be able to click run, and see a blank midnight purple screen as in figure 14, that should mean it is working. We don't have anything to show yet, but if it did not cause an error, you most likely got it right. I did not add regions to the game1 class except the using, it takes up so much space, and you don't need to see it. If you make your game right, you will not be doing much in the game class. I had allot of fun creating this tutor so far, I hope you enjoyed following along with me as I create it. You should have a zip file with this document containing the entire project. Chapter One Project File If you are having any issues, please unzip the included project zip file into your visual studio folder, or wherever you want it in your documents. You will find the project completed so far that you can open, and examine.
Thank you, and Game On! To continue Chapter Two