The Allegro Wiki is migrating to github at https://github.com/liballeg/allegro_wiki/wiki

Create/Chapter 4

From Allegro Wiki
Jump to: navigation, search
Create logo.png

This article is a part of the Create series, originally by Richard Phipps. The article content may have been edited from its original form.

Chapter 3 | Chapter 5

A moving player

Now we have a level we can put a player character in there and move him around. This article gets a bit more complicated as we have a bit of programming to do. First of all we'll consider the players: Our game will feature a maximum of four players, each which can be a different character. Here are the graphics sets for each character and his/her weapon shots:

Create player0.png Create player1.png Create player2.png Create player3.png

We will now add two new source files: player.c & player.h to handle all the player related code. In player.h we will add two new defines so we can easily change the amount of players or character gfx to choose from if we need to:

#define MAX_PLAYERS             4
#define MAX_PLAYERS_GFX         4


We can now use a struct like this:

// This stores a list of character gfx, so players could easily select a different
// character gfx.
struct player_gfx
{
 BITMAP *sprite;
} player_gfx[MAX_PLAYERS_GFX];

To load all the player graphics into. By keeping the sprite bitmap separate from the player struct we can allow players to pick different characters if they want to.

We can now load all the player graphics into our struct, aborting if we have an error:

void load_player_gfx(void)
{
 int ply;
 char name[120];
 
 // Load all the possible character gfx.
 for (ply = 0 ; ply < MAX_PLAYERS_GFX ; ply++)
 {
  uszprintf(name, sizeof(name), "Gfx/player%d.bmp", ply);
  player_gfx[ply].sprite = load_bitmap(name, NULL);
  
  if (!player_gfx[ply].sprite)
  {
   record_error("Can't load sprite for player %d from : %s", ply, name);   
  }   
 }  
}

So, we now have our gfx data loaded, but how do we go about using this to make a controllable player? Wel,l we first need to make another struct for our player details. In this we can store each of the player's positions, scores, directions of movement, etc.. For the player's position we are going to store their x and y co-ordinates in turns of pixels rather than the x and y of the tile they occupy. I realise this sounds strange, but there is a good reason for it...

When I first started testing the player movement I was using a tile co-ordinate system, but this caused some problems. Either the player movement was too sensitive or not sensitive enough and it felt awkward and not good enough for our purposes. I saw from a test of the original game that pressing right once didn't automatically move you right, but instead scrolled the screen right a little. If you moved right enough then your player moved into the new tile. I realised that this indicated that the players co-ordinates were pixel based and that the scrolling was following the pixel co-ordinates rather than the tile based position. These pixel co-ordinates seemed to provide a greater feeling of control and also the scrolling provided a little visual feedback into how much more you needed to hold down a key to move. This was rather ironic considering the theme of visual interaction I descirbed in previous articles!

So, I implemented the pixel based co-ordinates for players and it does indeed work much better. Now we will modify our earlier position_camera_on_start function so it stores the Start tile position and we will also change the name of the function to find_level_start to make it more accurate for what the function does. For now we will deal with the code for making one player move around the map, we can modify the code to deal with multiple players once this is done. So let's place the player by first storing the start tile location:

    // Store the Start position.
    level_info.start_x = x;
    level_info.start_y = y;

Ok, we have the Start position stored and we can use this to position the players:

 // Position player 0 one tile above the Start Tile.
 SET(player, 0).x = (level_info.start_x * 32) + 16;
 SET(player, 0).y = ((level_info.start_y - 1) * 32) + 16;

Now we need to deal with having different control systems for each player and so first we will make a struct to hold the control buttons for each player:

// Used to store the control settings for each players.
struct controls
{
 int up, down, left, right;
 int fire;
 int first_aid_pack;
 int smart_bomb;
 int control_type;
} controls[MAX_PLAYERS];

And then the code to set these for player 0 initially:

// Set default controls for players.
void setup_players_controls(void)
{
 SET(controls, 0).left = KEY_LEFT;
 SET(controls, 0).right = KEY_RIGHT;
 SET(controls, 0).up = KEY_UP; 
 SET(controls, 0).down = KEY_DOWN;    
}

Now we need a routine to test for these controls and use them to set the players movement direction:

void do_player_controls(int ply)
{
 if (key[controls[ply].left]) SET(player, ply).dx = -4;
 if (key[controls[ply].right]) SET(player, ply).dx = 4;
 if (key[controls[ply].up]) SET(player, ply).dy = -4;
 if (key[controls[ply].down]) SET(player, ply).dy = 4;      
}

We will deal with the other control buttons in the future, but right now we are looking to get a moving player as quickly as possible. Looking at the dx and dy values of 4 or -4 you can see that the player will take 8 frames to move from one tile to another. Now we have to use these dx and dy values to move. The first part of the function is:

// This doesn't neccessarily move a player onto a new TILE, but instead alters their
// pixel based x and y values.
void move_player(int ply)
{
 // Move player using dx and dy.
 if ((player[ply].x + player[ply].dx) / 32 == player[ply].x &&
 (player[ply].y + player[ply].dy) / 32 == player[ply].y)
 {
  // If player moves he will still occupy the same tile.
  SET(player, ply).x = player[ply].x + player[ply].dx;
  SET(player, ply).y = player[ply].y + player[ply].dy;    
  return;
 }

This adds the dx and dy values to our players x and y co-ordinates, but only if the player is not going to move onto a new tile once this has been done. Notice the use of the divisions by 32 to convert the pixel co-ordinates to tile positions. The second part of the function deals with moving the player onto a new tile:

 // Erase player tile, move player and reset player's dx and dy.
 make_tile(player[ply].x / 32, player[ply].y / 32, FLOOR);   
 
 SET(player, ply).x = player[ply].x + player[ply].dx;
 SET(player, ply).y = player[ply].y + player[ply].dy;
  
 SET(player, ply).dx = 0;
 SET(player, ply).dy = 0;
 
 // Draw player in new position.
 draw_player(ply);
}

This part uses a new function, make_tile which both changes a tile on the Map, but also draws the new tile to the Level bitmap. We use this here to change the old player tile to a Floor tile. We also adjust the co-ordinates and call our draw_player function, which both draws the player and marks the player on the map with our Player tile:

void draw_player(int ply)
{
 // Draw player at tile player occupies.
 blit(player_gfx[player[ply].gfx].sprite, level, 32, 32, 
 (player[ply].x / 32) * 32, (player[ply].y / 32) * 32, 32, 32);
 
 // Mark as player tile
 putpixel(map, player[ply].x / 32, player[ply].y / 32, PLAYER);     
}

Ok, so now if we call these functions in the right way in our inner loop we will have a moving player! For this we will need to use a timer system so that the game works at a constant speed and also scrolls smoothly. We will request a refresh rate of 60hz (60 updates per second) which is actually pretty common for our 640 x 480 screenmode. However it is still not always possible to switch to it, but we will attempt it before first setting our screenmode:

 // Now we setup a screen mode.
 set_color_depth(8);
 request_refresh_rate(60);
 write_to_log("Setting up a 640 x 480 x 8 bit graphics mode.");
 
 if (set_gfx_mode(GFX_AUTODETECT_FULLSCREEN, 640, 480, 0, 0) != 0)
 {
  write_to_log("Unable to set a graphic mode\n%s\n", allegro_error);
  record_error("Can't open a screen!");
 }

Now we will use our timer running at 120 times per second. The reason for this is that if the screen does update 60 times per second than this divides perfectly into 120. However if the refresh rate is 70, 80 or 85 then we can still draw those frames whereas with a timer running at 60 times a second we wouldn't be able to and it would appear less smooth. We'll remove the old mouse based scrolling and use this routine:

// Scroll camera moves the camera to keep up with player movement.
void scroll_camera(void)
{
 int x, y;
 
 // Find centre point for camera to centre on. The +16 is to focus on the middle of a tile.
 // (Currently we'll just focus on player 0, this will change in the future.
 x = (player[0].x) - (screen->w / 2) + 16;
 y = (player[0].y) - (screen->h / 2) + 16;
 
 if (x < camera.x) camera.x = camera.x - 4;
 if (x > camera.x) camera.x = camera.x + 4;
 if (y < camera.y) camera.y = camera.y - 4;
 if (y > camera.y) camera.y = camera.y + 4;
}

And here is our new ingame loop:

 // Setup players and draw them initially.
 setup_players_in_level();
 draw_player(0);
 
 // Start our high-resolution timer.
 start_timer();

 // Loop until we hit Escape.
 // This is our main ingame loop and uses our timer to happen at the same speed
 // on different PC's.
 do
 {
  // If one frame (of a 120 frame second) has elapsed, do our inner-loop code.
  if (check_timer(120) > 0)
  {
   // Use controls to set dx and dy for player, then move player.
   do_player_controls(0);
   move_player(0);
  
   // Move the camera accordingly, while keeping it within the level.
   scroll_camera();
   clip_camera_within_level();
   
   // Draw the level to our drawing buffer and then show to the screen when needed.
   blit(level, find_drawing_buffer(), camera.x, camera.y, 0, 0, screen->w, screen->h); 
   do_screen_update();
   
   // Reset timer, so next check works ok.
   reset_timer();
  }
  // Wait until we press Escape.
 } while (!key[KEY_ESC]);

Now the player can move around the level, and the camera scrolls to follow him (very smoothly if your screen updates at 60 times a second!)

Finally we'll add some code to stop the player moving over walls, by calling a function that checks the new tile the player would occupy if he moved. We add this to the move_player function:

 // If player moves he will be in a NEW tile, so check if he can move there..
 if (can_player_move_here(ply) == NO)
 {
  // Player can't move, so reset dx and dy and return.
  SET(player, ply).dx = 0;
  SET(player, ply).dy = 0;
  return; 
 }

And here's the can_player_move_here() function:

// This functions returns YES or NO for if the player can walk occupy the new tile
// if their dx and dy are added to the player's x and y.
int can_player_move_here(int ply)
{
 int tile;
 
 // Find out what tile type the player will move onto.
 tile = getpixel(map, (player[ply].x + player[ply].dx) / 32, 
 (player[ply].y + player[ply].dy) / 32);
 
 // If we cannot walk here return NO;
 if (tile == WALL) return NO;
 
 // Tile is walkable.
 return YES;    
}

Well, this was a bit complicated, but all this new code pays off as the player can now move around the level. In the next article we can build on this code to make it even more playable. Here's the latest screenshot:

Create free dungeons screenshot a4.png

And here are the files for this article:

Free Dungeons files for Article 4