Tutorial 2 - Creating a Level

Abstract

This tutorial will create a level out of brown boxes for the red block to navigate.

Step 1 - Constants and Variables

Our goal here is similar to before: we want a player that moves around but is restricted in various ways.
Again, we're going to start by defining game constants. The first three: player size, speed & color, are the same as tutorial 1.
I'm also going to add a variable for block color.
CU32 PLAYER_SPEED = 400;
CU32 PLAYER_SIZE = 32;
CCOL PLAYER_COLOR = nsColor::RED;

CCOL BLOCK_COLOR = nsColor::BROWN;
Instead of a single position variable, we are going to define the player with the cPhysicsBox class, which will give the player a position, velocity, size, and mass, as well as letting us collide it against the level more easily.
We also need a way to define walls or obstacles which we can use to build our level. I could define a cPhysicsBox for each individual wall, but that would be annoying to keep track of, so instead I'm going to define an array of cPhysicsBoxes.
struct cGame : cProgram
{
   GAME_CLASS_BASE;

   cPhysicsBox player;

   DSParray levelBlocks;

   F64 topBoundary, bottomBoundary, leftBoundary, rightBoundary;

};
"levelBlocks" is currently a collection of zero, one, or many cPhysicsBoxes. We can create as many of these blocks as we like, and still manage them as a single entity.
The outer boundaries remain the same as tutorial 1.

Step 2 - Setting up the Player

The player used to be a single vector, with a ".x" an ".y" member. Now the player is a cPhysicsBox, which has a ".position", ".size" and ".velocity" vector - each of which contain their own respective ".x" and ".y" members. The cPhysicsBox also has a ".mass" but we aren't going to care about that for the sake of this tutorial.
The player will still start in the screen center, and we're going to store the PLAYER_SIZE constant as both the x and y size, instead of implying the size as a hard-coded value in the draw function like last time.
The velocity defaults to (0,0) which is fine.
void cGame::open()
{
   player.position = GRAPHICS.screen_center();
   player.size.set( PLAYER_SIZE, PLAYER_SIZE );

   leftBoundary = 0;
   topBoundary = 0;
   rightBoundary = GRAPHICS.screenWidth;
   bottomBoundary = GRAPHICS.screenHeight;
};
The boundaries are the same as before.

Step 3 - Setting up the Level

levelBlocks doesn't by default have anything in it. We need to add walls to the list in order to use them in our game, and we do that by saying "levelBlocks.store( *** );"
As far as what we are telling levelBlocks to store, we need to create a new cPhysicsBox. When creating a new cPhysicsBox we can specify it's position and size directly. The full line of code for defining a wall in the level will appear as such: "levelBlocks.store( new cPhysicsBox( POSITION, SIZE ) );" where POSITION and SIZE are both vectors.
We can specify a vector directly ("V2D( #, # )") or with a mathematical formula, such as "GRAPHICS.screen_center() + V2D( #, # )" in order to tell a wall to be a certain position relative to the center of the screen.
I'm going to create 3 boxes, which will form a horseshoe shape over the center of the screen (and in turn, the player's starting position)
void cGame::open()
{
   player.position = GRAPHICS.screen_center();
   player.size.set( PLAYER_SIZE, PLAYER_SIZE );

   leftBoundary = 0;
   topBoundary = 0;
   rightBoundary = GRAPHICS.screenWidth;
   bottomBoundary = GRAPHICS.screenHeight;

   levelBlocks.store( new cPhysicsBox( GRAPHICS.screen_center() + V2D( 0, -100 ), V2D( 160, 40 ) ) );
   levelBlocks.store( new cPhysicsBox( GRAPHICS.screen_center() + V2D( -100, 0 ), V2D( 40, 240 ) ) );
   levelBlocks.store( new cPhysicsBox( GRAPHICS.screen_center() + V2D( 100, 0 ), V2D( 40, 240 ) ) );
};

Step 4 - Drawing the Player

Drawing the player is going to be roughly the same as last time, except we need to call the draw function using the member variables of the player's cPhysicsBox struct:
void cGame::draw()
{
   GRAPHICS.draw_box( I2D_cv( player.position ), I2D_cv( player.size ), 0, PLAYER_COLOR );

   GRAPHICS.draw_text( I2D( GRAPHICS.screenWidth/2, 8 ), 16, 12, "Tutorial 2 - Creating a Level" );
};

Step 5 - Drawing the Level

Drawing the level is a little more complicated.
After we create the level in the open() function, we never actually want to deal with the individual blocks directly again.
Instead, we want to create code which will apply the same basic action for each block in the level, however few or many that may be.
I created a macro to facilitate cycling through DSParray's, called "PARRAY_FOR_EACH". We will create an iterator "i" which will step through all the cPhysicsBoxes in "levelBlocks".
inside the loop, I'm going to convert the iterator into a more useable cPhysicsBox reference called "block".
At this point, we can draw each block in the same basic manor as the player.
void cGame::draw()
{
   PARRAY_FOR_EACH( i, cPhysicsBox, levelBlocks )
   {
      cPhysicsBox &block = *(*i);
      GRAPHICS.draw_box( I2D_cv( block.position ), I2D_cv( block.size ), 0, BLOCK_COLOR );
   }

   GRAPHICS.draw_box( I2D_cv( player.position ), I2D_cv( player.size ), 0, PLAYER_COLOR );

   GRAPHICS.draw_text( I2D( GRAPHICS.screenWidth/2, 8 ), 16, 12, "Tutorial 2 - Creating a Level" );
};

Step 6 - Changing the Player's Speed

Last time I moved the player's position directly.
In order to create more smooth and clean collisions, it's useful to know how fast the player is going as well as where it is.
I'm going to start by figuring out the exact directon the player is moving, then multiplying that by the speed.

To start with, I'm going to set the player's velocity to (0,0). Then I'm going to use the arrow keys, or the wasd keys to estimate which way the player is going.
In order to make sure that holding both up and right doesn't cause the player to move more quickly than holding just right or up, I need to normalize the player's velocity before multiplying it by the speed.
Basically:
  1. Figure out how the player wants to move
  2. Reduce it to a direction with no length
  3. Multiply by the move speed
void cGame::update( CF64 TimeStep )
{
   if( INPUT.digital_press( nsInput::K_ESC ) ) quit=true;

   player.velocity.set(0,0);

   if( INPUT.digital_down( K_UP ) || INPUT.digital_down( K_W ) ) player.velocity.y -= 1;
   if( INPUT.digital_down( K_DOWN ) || INPUT.digital_down( K_S ) ) player.velocity.y += 1;
   if( INPUT.digital_down( K_LEFT ) || INPUT.digital_down( K_A ) ) player.velocity.x -= 1;
   if( INPUT.digital_down( K_RIGHT ) || INPUT.digital_down( K_D ) ) player.velocity.x += 1;

   player.velocity = player.velocity.get_unit() * PLAYER_SPEED;
}

Step 7 - Moving the Player

Moving the player consists of two conceptual steps:
  1. Move the player
  2. Make sure the player hasn't moved where it shouldn't
Because the TimeStep might change depending on the speed of the computer or the general lag in the game, its not really safe to move with just "PLAYER_SPEED * TimeStep" like we did in tutorial 1. In order to make it safe, we need to garauntee the timestep is always the same length.
I will do that with the CONSTANT_STEP_LOOP macro. I can tell the macro to aim for a specific timeStep (i.e. 1/200th of a second) and it will loop enough times to fullfill that goal. I.E., if TimeStep is 1/200'th of a second, it will loop once, if TimeStep is 1/10th of a second, it will loop 20 times, if TimeStep is 1 second it will loop 200 times.
Inside this loop, we need to move the player, then collide with the level.
I'll start with just moving the player.
void cGame::update( CF64 TimeStep )
{
...

   player.velocity = player.velocity.get_unit() * PLAYER_SPEED;

   CONSTANT_STEP_LOOP( TimeStep, FixedTimeStep, 200 )
   {
      player.position += player.velocity * FixedTimeStep;
   }

   player.position.x = BIND( player.position.x, leftBoundary, rightBoundary );
   player.position.y = BIND( player.position.y, topBoundary, bottomBoundary );
}
With the BINDing at the end, this function will act from the player's perspective the same as the movement in the last tutorial.
However, all the fancy setup does matter when we start colliding against the brown walls

Step 8 - Hitting the Level

Similarly to when we drew the walls in the level, we want to iterate over every block in levelBlocks every time the player moves to make sure we haven't hit one.
The details of the collision are handled with my "PHYSICS.collide_staticBox" function, which will take the memory address of a cPhysicsBox and keep it from passing through an abstract box implied by a position and size.
CONSTANT_STEP_LOOP( TimeStep, FixedTimeStep, 200 )
{
   player.position += player.velocity * FixedTimeStep;

   PARRAY_FOR_EACH( i, cPhysicsBox, levelBlocks )
   {
      cPhysicsBox &block = *(*i);

      PHYSICS.collide_staticBox( &player, block.position, block.size );
   }
}
You can try adding more blocks to the level in open. The rest of the code should adapt to it.