PlanetX Part 3
This is Part 3 of the series PlanetX. This part is probably the most interesting part of this series. At the end of this tutorial, we will be able to launch bullets and watch them orbit the planet!
Download the source code and the content here.
The first step is creating the Bullet class. The class will appear as follows:
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; using Microsoft.Xna.Framework.Net; using Microsoft.Xna.Framework.Storage; namespace PlanetX { class Bullet { #region Member Variables private float m_Rotation; private Vector2 m_Position; private Vector2 m_Velocity; private Vector2 m_Acceleration; private Boolean m_IsActive = false; private float DistanceFromPlanet; //Distance from planet's center private Vector2 DirectionVector; //Vector from bullet to planet center #endregion #region Constants float GravityConstant = 40f; float BulletVelocityDamping = 0.999f; float PlanetaryMass = 2000000; #endregion #region Properties public float Rotation { get { return m_Rotation; } set { m_Rotation = value; } } public Vector2 Position { get { return m_Position; } set { m_Position = value; } } public Vector2 Velocity { get { return m_Velocity; } set { m_Velocity = value; } } public Vector2 Acceleration { get { return m_Acceleration; } } public Boolean IsActive { get { return m_IsActive; } set { m_IsActive = value; } } #endregion #region Methods public void Update(GameTime gameTime, Vector2 ScreenCenter) { DistanceFromPlanet = Vector2.Distance(m_Position, ScreenCenter); //Find distance from planet center DirectionVector = Vector2.Normalize(ScreenCenter - m_Position); //Calculate the direction vector of gravity //Acceleration = Direction Vector * {(G * Mass of Planet) / (Distance)^2} .......... Newton's Law of Gravitation //NOTE: Mass of Projectile is considered to be unity m_Acceleration = Vector2.Multiply(DirectionVector, (float)(GravityConstant * PlanetaryMass / Math.Pow(DistanceFromPlanet, 2))); m_Velocity += Vector2.Multiply(m_Acceleration, (float)gameTime.ElapsedGameTime.TotalSeconds); //Calculate Velocity m_Velocity *= BulletVelocityDamping; //Apply friction to bullet m_Rotation = ((float)(Math.Atan2(m_Velocity.Y, m_Velocity.X))) + (float)(Math.PI / 2); //Find the orientation of the bullet m_Position += m_Velocity * (float)gameTime.ElapsedGameTime.TotalSeconds; //Update the position if (DistanceFromPlanet > (float)(ScreenCenter.X * 3)) m_IsActive = false; } public Boolean IsColliding(float PlanetRadius) { if (DistanceFromPlanet < (PlanetRadius + 10)) { m_IsActive = false; return true; } else return false; } #endregion } }
Let’s review the class one block at a time.
#region Member Variables private float m_Rotation; private Vector2 m_Position; private Vector2 m_Velocity; private Vector2 m_Acceleration; private Boolean m_IsActive = false; private float DistanceFromPlanet; //Distance from planet's center private Vector2 DirectionVector; //Vector from bullet to planet center #endregion
Here, we create member variables to store the bullet’s Rotation, Position, Velocity and Acceleration. We also create a Boolean which indicates the state of the Bullet, i.e. whether it is active or not. The DistanceFromPlanet and DirectionVector variables just make the physics calculations easier.
#region Constants float GravityConstant = 40f; float BulletVelocityDamping = 0.999f; float PlanetaryMass = 2000000; #endregion
These are some physics constants. BulletVelocityDamping is the air-friction coefficient. The others’ names are self-explanatory.
#region Properties public float Rotation { get { return m_Rotation; } set { m_Rotation = value; } } public Vector2 Position { get { return m_Position; } set { m_Position = value; } } public Vector2 Velocity { get { return m_Velocity; } set { m_Velocity = value; } } public Vector2 Acceleration { get { return m_Acceleration; } } public Boolean IsActive { get { return m_IsActive; } set { m_IsActive = value; } } #endregion
We are exposing the member variables as properties. Now we look at the Update method:
public void Update(GameTime gameTime, Vector2 ScreenCenter) { DistanceFromPlanet = Vector2.Distance(m_Position, ScreenCenter); //Find distance from planet center DirectionVector = Vector2.Normalize(ScreenCenter - m_Position); //Calculate the direction vector of gravity //Acceleration = Direction Vector * {(G * Mass of Planet) / (Distance)^2} .......... Newton's Law of Gravitation //NOTE: Mass of Projectile is considered to be unity m_Acceleration = Vector2.Multiply(DirectionVector, (float)(GravityConstant * PlanetaryMass / Math.Pow(DistanceFromPlanet, 2))); m_Velocity += Vector2.Multiply(m_Acceleration, (float)gameTime.ElapsedGameTime.TotalSeconds); //Calculate Velocity m_Velocity *= BulletVelocityDamping; //Apply friction to bullet m_Rotation = ((float)(Math.Atan2(m_Velocity.Y, m_Velocity.X))) + (float)(Math.PI / 2); //Find the orientation of the bullet m_Position += m_Velocity * (float)gameTime.ElapsedGameTime.TotalSeconds; //Update the position if (DistanceFromPlanet > (float)(ScreenCenter.X * 3)) m_IsActive = false; }
The physics calculations are performed first. The comments explain the calculations. Next, if the Bullet strays too far from the planet, its IsActive property is made False. Let’s look at the IsColliding function:
public Boolean IsColliding(float PlanetRadius) { if (DistanceFromPlanet < (PlanetRadius + 10)) { m_IsActive = false; return true; } else return false; }
This function checks whether the Bullet is colliding with the planet and returns a Boolean accordingly. 10 pixels are added to the Planet Radius to make the explosions look more realistic. Try turning this value to zero after the next tutorial and observe what happens. Now we make some changes to the main Game1 class. The whole class looks like this:
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; using Microsoft.Xna.Framework.Net; using Microsoft.Xna.Framework.Storage; namespace PlanetX { /// <summary> /// This is the main type for your game /// </summary> public class Game1 : Microsoft.Xna.Framework.Game { #region Variables GraphicsDeviceManager graphics; SpriteBatch spriteBatch; //Content Texture2D tx2Back; Texture2D tx2Player; Texture2D tx2Turret; Texture2D tx2Cursor; Texture2D tx2Bullet; SpriteFont Font; //Game Classes Player Player1; Bullet[] Bullets; //Input ButtonState PreviousFireKey; //Counters int CurrentBullet = 0; int ActiveBullets = 0; #endregion #region Constants float PlanetRadius = 260; float TurretElevation = 4; //Elevation of the turret over the player's origin float InitialBulletVelocity = 600; int MaxBullets = 1000; Vector2 ScreenCenter; #endregion public Game1() { graphics = new GraphicsDeviceManager(this); Content.RootDirectory = "Content"; Components.Add(new GeneralExtensions.FrameRateCounter(this)); } /// <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() { InitGraphicsMode(800, 600, false); //Screen Size ScreenCenter = new Vector2(graphics.GraphicsDevice.Viewport.Width / 2, graphics.GraphicsDevice.Viewport.Height / 2); Player1 = new Player(); Bullets = new Bullet[MaxBullets]; for (int i = 0; i < MaxBullets; i++) { Bullets[i] = new Bullet(); } 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() { // Create a new SpriteBatch, which can be used to draw textures. spriteBatch = new SpriteBatch(GraphicsDevice); //Load Textures tx2Back = Content.Load<Texture2D>("Graphics/PlanetX"); tx2Player = Content.Load<Texture2D>("Graphics/Player"); tx2Turret = Content.Load<Texture2D>("Graphics/Turret"); tx2Cursor = Content.Load<Texture2D>("Graphics/Cursor"); tx2Bullet = Content.Load<Texture2D>("Graphics/Bullet"); //Load font Font = Content.Load<SpriteFont>("Font"); } /// <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) { HandleInput(); MovePlayer(); UpdateBullets(gameTime); //TestBulletCollisions(); 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(Color.CornflowerBlue); spriteBatch.Begin(); //Draw Background spriteBatch.Draw(tx2Back, new Vector2(0, 0), Color.White); //Draw Explosions //Draw Bullets foreach (Bullet myBullet in Bullets) { if (myBullet.IsActive == true) { spriteBatch.Draw(tx2Bullet, myBullet.Position, null, Color.White, myBullet.Rotation, new Vector2(tx2Bullet.Width / 2, tx2Bullet.Height), 1, SpriteEffects.None, 0); } } //Draw Tank spriteBatch.Draw(tx2Player, Player1.Position, null, Color.White, Player1.Rotation - (float)(Math.PI / 2), new Vector2(tx2Player.Width / 2, tx2Player.Height / 2), 1, SpriteEffects.None, 0); //Draw Turret spriteBatch.Draw(tx2Turret, new Vector2(ScreenCenter.X - (float)((PlanetRadius + tx2Player.Height / 2 + TurretElevation) * Math.Cos(Player1.Rotation)), ScreenCenter.Y - (float)((PlanetRadius + tx2Player.Height / 2 + TurretElevation) * Math.Sin(Player1.Rotation))) , null, Color.White, Player1.AbsoluteTurretRotation + (float)(Math.PI / 2), new Vector2(tx2Turret.Width / 2, tx2Turret.Height), 1, SpriteEffects.None, 0); //Draw Cursor spriteBatch.Draw(tx2Cursor, new Vector2(Mouse.GetState().X, Mouse.GetState().Y), Color.White); //Draw HUD spriteBatch.End(); base.Draw(gameTime); } //////////////////////////////////////////////////////////////////////////////////////////////////////////////////// #region Update Functions private void HandleInput() { //Keyboard Input if (Keyboard.GetState().IsKeyDown(Keys.Left) == true) Player1.Rotation = Player1.Rotation - 0.01f; if (Keyboard.GetState().IsKeyDown(Keys.Right) == true) Player1.Rotation = Player1.Rotation + 0.01f; //if (Mouse.GetState().LeftButton == ButtonState.Pressed && PreviousFireKey != ButtonState.Pressed) if (Mouse.GetState().LeftButton == ButtonState.Pressed) { LaunchBullet(); } PreviousFireKey = Mouse.GetState().LeftButton; } private void MovePlayer() { //Player Rotation Player1.Rotation = (float)(Player1.Rotation % (Math.PI * 2)); if (Player1.Rotation < 0) Player1.Rotation = (float)(2 * Math.PI + Player1.Rotation); //Player Position Player1.Position = new Vector2(ScreenCenter.X - (float)((PlanetRadius + tx2Player.Height / 2) * Math.Cos(Player1.Rotation)), ScreenCenter.Y - (float)((PlanetRadius + tx2Player.Height / 2) * Math.Sin(Player1.Rotation))); //Turret Rotation Player1.AbsoluteTurretRotation = (float)(Math.Atan2((new Vector2(Mouse.GetState().X, Mouse.GetState().Y) - Player1.Position).Y, (new Vector2(Mouse.GetState().X, Mouse.GetState().Y) - Player1.Position).X)); } private void UpdateBullets(GameTime gameTime) { //Reset Counters ActiveBullets = 0; //Update foreach (Bullet myBullet in Bullets) { if (myBullet.IsActive == true) { myBullet.Update(gameTime, ScreenCenter); ActiveBullets++; } } } private void TestBulletCollisions() { foreach (Bullet myBullet in Bullets) { if (myBullet.IsActive == true) { if (myBullet.IsColliding(PlanetRadius) == true) { //Explode!! } } } } private void LaunchBullet() { Bullets[CurrentBullet].IsActive = true; Bullets[CurrentBullet].Position = new Vector2((ScreenCenter.X) - (float)((PlanetRadius + tx2Player.Height / 2 + TurretElevation) * Math.Cos(Player1.Rotation)), (ScreenCenter.Y) - (float)((PlanetRadius + tx2Player.Height / 2 + TurretElevation) * Math.Sin(Player1.Rotation))); Bullets[CurrentBullet].Velocity = new Vector2((float)(InitialBulletVelocity * Math.Cos(Player1.AbsoluteTurretRotation)), (float)(InitialBulletVelocity * Math.Sin(Player1.AbsoluteTurretRotation))); Bullets[CurrentBullet].Rotation = Player1.AbsoluteTurretRotation; if (CurrentBullet == MaxBullets - 1) { //Roll back to first bullet CurrentBullet = 0; } else { CurrentBullet++; } } #endregion #region Miscellaneous Functions /// <summary> /// Attempt to set the display mode to the desired resolution. Iterates through the display /// capabilities of the default graphics adapter to determine if the graphics adapter supports the /// requested resolution. If so, the resolution is set and the function returns true. If not, /// no change is made and the function returns false. /// </summary> /// <param name="iWidth">Desired screen width.</param> /// <param name="iHeight">Desired screen height.</param> /// <param name="bFullScreen">True if you wish to go to Full Screen, false for Windowed Mode.</param> private bool InitGraphicsMode(int iWidth, int iHeight, bool bFullScreen) { // If we aren't using a full screen mode, the height and width of the window can // be set to anything equal to or smaller than the actual screen size. if (bFullScreen == false) { if ((iWidth <= GraphicsAdapter.DefaultAdapter.CurrentDisplayMode.Width) && (iHeight <= GraphicsAdapter.DefaultAdapter.CurrentDisplayMode.Height)) { graphics.PreferredBackBufferWidth = iWidth; graphics.PreferredBackBufferHeight = iHeight; graphics.IsFullScreen = bFullScreen; graphics.ApplyChanges(); return true; } } else { // If we are using full screen mode, we should check to make sure that the display // adapter can handle the video mode we are trying to set. To do this, we will // iterate thorugh the display modes supported by the adapter and check them against // the mode we want to set. foreach (DisplayMode dm in GraphicsAdapter.DefaultAdapter.SupportedDisplayModes) { // Check the width and height of each mode against the passed values if ((dm.Width == iWidth) && (dm.Height == iHeight)) { // The mode is supported, so set the buffer formats, apply changes and return graphics.PreferredBackBufferWidth = iWidth; graphics.PreferredBackBufferHeight = iHeight; graphics.IsFullScreen = bFullScreen; graphics.ApplyChanges(); return true; } } } return false; } #endregion } }
Let’s look at the changes one by one:
Texture2D tx2Bullet;We add a bullet texture.
tx2Bullet = Content.Load<Texture2D>("Graphics/Bullet");
In the LoadContent() method, we load the bullet texture.
Bullet[] Bullets;
We declare an array of our Bullet class.
//Counters int CurrentBullet = 0; int ActiveBullets = 0;
We declare the counters here.
Bullets = new Bullet[MaxBullets]; for (int i = 0; i < MaxBullets; i++) { Bullets[i] = new Bullet(); }
Here, we initialize the array and all of its elements.
UpdateBullets(gameTime); //TestBulletCollisions();
We add calls to methods to update Bullets and check for collisions. We won’t call the IsColliding method in this tutorial. We create the methods as follows:
private void UpdateBullets(GameTime gameTime) { //Reset Counters ActiveBullets = 0; //Update foreach (Bullet myBullet in Bullets) { if (myBullet.IsActive == true) { myBullet.Update(gameTime, ScreenCenter); ActiveBullets++; } } }
We count active Bullets for ourselves (as developers). We will improve the HUD (Heads Up Display) in the next Tutorial. We call the Bullet.Update() method for every active bullet.
private void TestBulletCollisions() { foreach (Bullet myBullet in Bullets) { if (myBullet.IsActive == true) { if (myBullet.IsColliding(PlanetRadius) == true) { //Explode!! } } } }
We call the IsColliding() function for every active Bullet and set the Bullet to explode if it returns True. The bullet will just disappear on impact in this tutorial. We will write the explosion code in the next one.
Next, we write the LaunchBullet() method as follows:
private void LaunchBullet() { Bullets[CurrentBullet].IsActive = true; Bullets[CurrentBullet].Position = new Vector2((ScreenCenter.X) - (float)((PlanetRadius + tx2Player.Height / 2 + TurretElevation) * Math.Cos(Player1.Rotation)), (ScreenCenter.Y) - (float)((PlanetRadius + tx2Player.Height / 2 + TurretElevation) * Math.Sin(Player1.Rotation))); Bullets[CurrentBullet].Velocity = new Vector2((float)(InitialBulletVelocity * Math.Cos(Player1.AbsoluteTurretRotation)), (float)(InitialBulletVelocity * Math.Sin(Player1.AbsoluteTurretRotation))); Bullets[CurrentBullet].Rotation = Player1.AbsoluteTurretRotation; if (CurrentBullet == MaxBullets - 1) { //Roll back to first bullet CurrentBullet = 0; } else { CurrentBullet++; } }
What we do in this method is we keep “launching” a Bullet from the Bullets array, and then setting the “pointer” variable, CurrentBullet, to the next one in the array. If we reach the end of the array, we roll back to the first Bullet. The launching mechanism is fairly straightforward. We set the position of the bullet, using the formulae: x=r.cos(theta) and y=r.sin(theta) as discussed in the previous tutorial. The velocity (v) is resolved into its components using the formulae:
vx=r.cos(theta)
vy=r.sin(theta)
Finally, Bullet rotation is set to turret rotation.
To enable bullet launching, we uncomment the following line in the HandleInput() method:
if (Mouse.GetState().LeftButton == ButtonState.Pressed) { LaunchBullet(); }
Lastly, we add some code to the Draw() method:
//Draw Bullets foreach (Bullet myBullet in Bullets) { if (myBullet.IsActive == true) { spriteBatch.Draw(tx2Bullet, myBullet.Position, null, Color.White, myBullet.Rotation, new Vector2(tx2Bullet.Width / 2, tx2Bullet.Height), 1, SpriteEffects.None, 0); } }
Here we draw every active bullet. Done!!! Just run this and you should get something that looks like this:
We will implement collisions in the next and final tutorial of this series.

