Part Four - Player Ship



In this installment we will add the player's star fighter to our game. For our purposes, we will use a ship that is 72x16 pixels. We will create a four-frame sprite sheet for our player's ship. We'll use two frames to show the ship facing left and right, and in the other two frames we will add a "thrust" image to the ship's engines to show that the player is actively moving in a direction.



Save this image to your Content/Textures folder and add it to your project.

As before, we will create a new class for our player to keep things together and organized. Add a new class to your project via Solution Explorer called Player.cs. We will need to add a couple of references at the top of our class file:

using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;


As for declarations, we are going to start out simple. We'll expand on this later when we add more features to our game in later installments but for now, we will need the following:

        AnimatedSprite asSprite;
        int iX = 604;
        int iY = 260;
        int iFacing = 0;
        bool bThrusting = false;
        int iScrollRate = 0;
        int iShipAccelerationRate = 1;
        int iShipVerticalMoveRate = 3;
        float fSpeedChangeCount = 0.0f;
        float fSpeedChangeDelay = 0.1f;
        float fVerticalChangeCount = 0.0f;
        float fVerticalChangeDelay = 0.01f;


  • AnimatedSprite will, of course, be used to house the image above. We will be using the bAnimating feature of our animated sprite class to prevent it from animating automatically so that we can control which frame of the ship "animation" is displayed.

  • iX and iY determine the location of the ship on the screen. In the game we will be producing for this tutorial series, the ship will never leave the center of the screen (horizontally) but will move freely vertically.

  • iFacing determines which direction the player is currently facing. 0=Right, 1=Left.

  • bThrusting is set to true when the player is actively moving in a direction (as opposed to coasting in that direction).

The next few variables are all related to how our ship/screen moves. We'll get into more detail below when we add the ship to the screen and handle movement.

  • iScrollRate determines the speed and direction that the ship is actually moving (this is not related to the Facing, as it is possible to be moving one direction while facing the other). Positive values indicate rightward movement, negative values leftwards. The magnitude of the number determines the number of pixels per update frame that the screen will scroll.

  • iShipAccelerationRate sets how fast iScrollRate can change. A value of 1 means that every time the speed changes it changes by 1.

  • iShipVerticalMovementRate is the number of pixels the ship moves vertically when the player presses up or down on the gamepad/keyboard.

  • fSpeedChangeCount and fSpeedChangeDelay determine how rapidly iShipAccelerationRate can be applied. fSpeedChangeCount accumulates the time since the last speed change. When it is greater than fSpeedChangeDelay, the speed is allowed to change and will be reset to 0 if it does.

  • fVerticalChangeCount and fVerticalChangeDelay work in the same way that fSpeedChangeCounnt/Delay work except that they serve to limit how quickly the player can move vertically. The value we have set here (0.01 seconds) is very, very low so we are basically just using this to keep the ship moving at a consistant speed on fast computers.

Lets include a few properties so we can access these variables from outside the class:
        public int X
        {
            get { return iX; }
            set { iX = value; }
        }
 
        public int Y
        {
            get { return iY; }
            set { iY = value; }
        }
 
        public int Facing
        {
            get { return iFacing; }
            set { iFacing = value; }
        }
 
        public bool Thrusting
        {
            get { return bThrusting; }
            set { bThrusting = value; }
        }
 
        public int ScrollRate
        {
            get { return iScrollRate; }
            set { iScrollRate = value; }
        }
 
        public int AccelerationRate
        {
            get { return iShipAccelerationRate; }
            set { iShipAccelerationRate = value; }
        }
 
        public int VerticalMovementRate
        {
            get { return iShipVerticalMoveRate; }
            set { iShipVerticalMoveRate = value; }
        }
 
        public float SpeedChangeCount
        {
            get { return fSpeedChangeCount; }
            set { fSpeedChangeCount = value; }
        }
 
        public float SpeedChangeDelay
        {
            get { return fSpeedChangeDelay; }
            set { fSpeedChangeDelay = value; }
        }
 
        public float VerticalChangeCount
        {
            get { return fVerticalChangeCount; }
            set { fVerticalChangeCount = value; }
        }
 
        public float VerticalChangeDelay
        {
            get { return fVerticalChangeDelay; }
            set { fVerticalChangeDelay = value; }
        }

This is a big chunk of code, but there is nothing out of the ordinary here, as these are all simple get/set property pairs for the variables we declared above.

Now lets add one last property that will be used later when we start detecting collissions between objects in our game:
        public Rectangle BoundingBox
        {
            get { return new Rectangle(iX, iY, 72, 16); }
        }

The BoundingBox property simply returns a new rectangle based on the position and size of our ship. We will be adding similar properties to other objects in our game, some of them more complex than this to account for objects position within the "world map".

As for our constructor, it is similarly simple:
        public Player(Texture2D texture)
        {
            asSprite = new AnimatedSprite(texture, 0, 0, 72, 16, 4);
            asSprite.IsAnimating = false;
        }

In this case, all we are doing is passing the texture along to create our AnimatedSprite. We set the frame size to 72x16 and tell the AnimatedSprite that it has 4 frames. Then we set IsAnimating to false, which will prevent the sprite from updating frames on its own.

Our Draw code will be even simpler:
        public void Draw(SpriteBatch sb)
        {
            asSprite.Draw(sb, iX, iY, false);
        }

Here we just pass the SpriteBatch object on to the AnimatedSprite's Draw method. We include the X and Y position of the sprite. The final parameter (false) tells the AnimatedSprite not to add SpriteBatch.Begin and SpriteBatch.End calls of it's own.

Finally, we will need to update animation frames based on the values of iFacing and bThrusting. Lets create an Update method:
        public void Update(GameTime gametime)
        {
            if (iFacing==0)
            {
                if (bThrusting)
                    asSprite.Frame=1;
                else
                    asSprite.Frame=0;
            } else {
                if (bThrusting)
                    asSprite.Frame=3;
                else
                    asSprite.Frame=2;
            }
        }

By checking the combination of iFacing and bThrusting we determine what to set the sprite's Frame value to (0=Right, 1=Right with Thrust, 2=Left, 3=Left with Thrust).

In order to add our ship to the game, we will need to start thinking about how our ship will move in the game. The simple way would be to have a pixel speed that is added or subtracted to the ship's position whenever a movement key is pressed, however we want a more "complex" movement system, so we will do something a bit different.

First, we will need to add a few declarations to the top of our game1.cs class:
        Player player;
        public int iPlayAreaTop = 30;
        public int iPlayAreaBottom = 630;
        int iMaxHorizontalSpeed = 8;
        float fBoardUpdateDelay = 0f;
        float fBoardUpdateInterval = 0.01f;

The "player" object houses our instance of the Player class. The two public ints (iPlayAreaTop and iPlayAreaBottom) are public because we will be using them in our Enemy class later.

fBoardUpdateDelay and fBoardUpdateInterval will be used to control how fast the screen can scroll overall (if we rely simply on calls to Update we can potentially get inconsistant speeds).

We need to initialize our Player object, so in the game's LoadContent method, add the following:
     player = new Player(Content.Load<Texture2D>(@"Textures\PlayerShip"));

Next, lets add the code to draw our player sprite to our Draw method. Add the following right after the call to draw our background:

                player.Draw(spriteBatch);

Now we should see our ship sprite on the screen if we run our project, but we can't move it.

We will add two functions to check for movement. We are using two because vertical movement is handled differently from horizontal movement. The player can move vertically with "immediate response", meaning if you are holding down the "up" movement control when the update routine runs, your ship moves 3 pixels up (iShipVerticalMovementRate).

Horizontal movemnt works differently in that we have a current speed that the ship is moving. We can modify this speed by holding down a directional control. When we let up on the directional control, we continue to move in the direction we were last moving in until we alter the speed and direction again.

Here is our routine for horizontal movement:
        protected void CheckHorizontalMovementKeys(KeyboardState ksKeys,
                                           GamePadState gsPad)
        {
            bool bResetTimer = false;
 
            player.Thrusting = false;
            if ((ksKeys.IsKeyDown(Keys.Right)) ||
                (gsPad.ThumbSticks.Left.X > 0))
            {
                if (player.ScrollRate < iMaxHorizontalSpeed)
                {
                    player.ScrollRate += player.AccelerationRate;
                    if (player.ScrollRate > iMaxHorizontalSpeed)
                        player.ScrollRate = iMaxHorizontalSpeed;
                    bResetTimer = true;
                }
                player.Thrusting = true;
                player.Facing = 0;
            }
 
            if ((ksKeys.IsKeyDown(Keys.Left)) ||
                (gsPad.ThumbSticks.Left.X < 0))
            {
                if (player.ScrollRate > -iMaxHorizontalSpeed)
                {
                    player.ScrollRate -= player.AccelerationRate;
                    if (player.ScrollRate < -iMaxHorizontalSpeed)
                        player.ScrollRate = -iMaxHorizontalSpeed;
                    bResetTimer = true;
                }
                player.Thrusting = true;
                player.Facing = 1;
            }
 
            if (bResetTimer)
                player.SpeedChangeCount = 0.0f;
        }

We will pass this routine both a KeyboardState and a GamePadState, so at this point our "game" is playable with either controller. We are treating the Gamepad's left thumbstick as a simple digital control here instead of using it's analog (0.0 to 1.0) value. The player is either pressing in a direction or not pressing in a direction. For our purposes how far the stick is moved isn't important.

When we detect horizontal movement, we alter the player.ScrollRate value by the playerAccelerationRate, limiting it to a magnitude of 8 (iMaxHorizontalSpeed)in either direction (so player.ScrollRate can range from -8 to +8)

Additionally, if either the left or right movement control is active, we set the "Facing" value for our player's ship, and turn on the "Thrusting" boolean.

Finally, if we do alter player.ScrollRate, we reset player.SpeedChangeCount to restart the delay timer before player.ScrollRate can be changed again. As for vertical movement, it is much more straightforward:
        protected void CheckVerticalMovementKeys(KeyboardState ksKeys,
                                         GamePadState gsPad)
        {
 
            bool bResetTimer = false;
            
            if ((ksKeys.IsKeyDown(Keys.Up)) ||
                (gsPad.ThumbSticks.Left.Y > 0))
            {
                if (player.Y > iPlayAreaTop)
                {
                    player.Y -= player.VerticalMovementRate;
                    bResetTimer = true;
                }
            }
 
            if ((ksKeys.IsKeyDown(Keys.Down)) ||
                (gsPad.ThumbSticks.Left.Y < 0))
            {
                if (player.Y < iPlayAreaBottom)
                {
                    player.Y += player.VerticalMovementRate;
                    bResetTimer = true;
                }
            }
 
            if (bResetTimer)
                player.VerticalChangeCount = 0f;
        }

Just like our horizontal movement checks, we determine if a vertical movement key has been pressed and reset the player.VerticalChangeCount variable if appropriate. We add (or subtract) player.VerticalMovementRate to the player.Y position.

We will add another new function that we will expand upon later to update the "game board" during each update cycle:
        public void UpdateBoard()
        {
            background.BackgroundOffset += player.ScrollRate;
            background.ParallaxOffset += player.ScrollRate * 2;
        }

Since our background class takes care of looping around on it's own, that's all we need to do right now to update our game board.

Finally, we need to modify our existing Update method to take our new input functions into account. First we need to remove the code we added when putting the Background object in (that checks for the Left and Right keys being pressed). For clarity, here is the entire update method as it should look now:
       protected override void Update(GameTime gameTime)
        {
            // Allows the game to exit
            if (GamePad.GetState(PlayerIndex.One).Buttons.Back == ButtonState.Pressed)
                this.Exit();
 
            player.SpeedChangeCount += (float)gameTime.ElapsedGameTime.TotalSeconds;
            if (player.SpeedChangeCount > player.SpeedChangeDelay)
            {
                CheckHorizontalMovementKeys(Keyboard.GetState(),
                                            GamePad.GetState(PlayerIndex.One));
            }
 
            player.VerticalChangeCount += (float)gameTime.ElapsedGameTime.TotalSeconds;
            if (player.VerticalChangeCount > player.VerticalChangeDelay)
            {
                CheckVerticalMovementKeys(Keyboard.GetState(),
                                          GamePad.GetState(PlayerIndex.One));
            }
            player.Update(gameTime);
 
            fBoardUpdateDelay += (float)gameTime.ElapsedGameTime.TotalSeconds;
            if (fBoardUpdateDelay > fBoardUpdateInterval)
            {
                fBoardUpdateDelay = 0f;
                UpdateBoard();
            }
 
            // TODO: Add your update logic here
            Explosion.Update(gameTime);
            base.Update(gameTime);
        }

After checking all of our movement keys, we update the player object, and then check our elapsed time to determine if enough game time has passed to update the playfield. If so, we update the board and reset fBoardUpdateDelay back to 0.

If you run your game now, you should be able to use either the keyboard or the game pad to move your player ship around the game map. You will notice a few things:
  • Whenever a horizontal movement control is depressed, the ship displays a fiery thrust graphic.
  • The ship does not stop moving horizontally when you release the movement control.
  • If you accelerate to full speed in one direction and then reverse directions briefly, the ship will turn around while your momentum continues to carry you in the original direction.
  • Vertical movement does not exhibit the same acceleration characteristics. You can freely move up and down without "coasting".
  • Thanks to iPlayAreaTop and iPlayAreaBottom, the player's ship can't move off the top or bottom of the screen.

Try experimenting with the fSpeedChangeDelay variable to alter the handling characteristics of the ship. In a future installment when we add Power Ups, we will include one that reduces this value to make the ship more responsive when changing directions and speed.

That's it for this installment. We are well on our way to having a functional game! In the next installment we will look at giving the player the ability to fire bullets.

(Continued in Part 5...)


































 

 
 
Site Contents Copyright 2006 Full Revolution, Inc. All rights reserved.
This site is in no way affiliated with Microsoft or any other company.
All logos and trademarks are copyright their respective companies.
RSS FEED