Day 29. Basic Tile Map Collision Checking

Day 29. Basic Tile Map Collision Checking
Video Length (including Q&A): 1h39

Welcome to “Handmade Hero Notes”, the book where we follow the footsteps of Handmade Hero in making the complete game from scratch, with no external libraries. If you'd like to follow along, preorder the game on handmadehero.org, and you will receive access to the GitHub repository, containing complete source code (tagged day-by-day) as well as a variety of other useful resources.

“Always write the usage code first” is our mantra. We don't want to write a game engine until we have the game. While it might sound paradoxical, it simply means using whatever tools we can develop quickly to build the structure up. We're just drawing rectangles on the screen, pretending they will be something beautiful later down the line. But, in doing so, we'll understand how exactly we want our engine to function so that when we build those engine components, we won't be wasting time on things we don't need or that are clunky to use in the context we meant to use them.

Today, we'll implement basic collision with the walls we're drawing on the tile map. Additionally, we'll start our work to transition between rooms.

Day 28 Day 30

(Top)
Clean Up
  1.1  Introduce handmade_platform.h
  1.2  Platform Layer: Clear the Window
  1.3  Inline the Rounding Functions
Player Collision with the Tilemap
  2.1  Setup
  2.2  Calculate Player Position
  2.3  Validate Player Position
  2.4  Initialize Player Position
Revise the Tile Map Code
  3.1  Introduce IsTileMapPointEmpty
  3.2  Introduce tile_map Structure
  3.3  Accessing the Tile Map Cells
  3.4  Redefine our Tile Map
  3.5  Add Player Width to Collision
Multiple Tile Maps
  4.1  Create an Array of Tile Maps
  4.2  Introduce Worlds
  4.3  Connecting the Tile Maps
Recap
Navigation

   

Clean Up

   

Introduce handmade_platform.h

Note

Parts of this subsection use the refactoring made on stream during Day 39.

Now that we have started to use handmade.h to store actual game code (even if exploratory), we want to extract the platform headers to a separate file. Let's create a new file that we'll call handmade_platform.h:

#if !defined(HANDMADE_PLATFORM_H) #define HANDMADE_PLATFORM_H #endif
 Listing 1: [handmade_platform.h] Introducing handmade_platform.h.

First, we will move all the various type definitions and useful macros we use in both the game and the platform layers.

/*
NOTE(casey):

HANDMADE_INTERNAL:
0 - Build for public release
1 - Build for developer only

HANDMADE_SLOW:
0 - No slow code allowed!
1 - Slow code welcome.
*/
// TODO(casey): Implement sine ourselves #include <math.h> #include <stdint.h> typedef int8_t s8; typedef int16_t s16; typedef int32_t s32; typedef int64_t s64; typedef s32 b32; typedef uint8_t u8; typedef uint16_t u16; typedef uint32_t u32; typedef uint64_t u64; typedef float f32; typedef double f64; #define internal static #define local_persist static #define global_variable static #define Pi32 3.14159265359f #if HANDMADE_SLOW #define Assert(Expression) if (!(Expression)) { *(int *)0 = 0; } #else #define Assert(Expression) #endif #define Kilobytes(Value) ((Value) * 1024LL) #define Megabytes(Value) (Kilobytes(Value) * 1024LL) #define Gigabytes(Value) (Megabytes(Value) * 1024LL) #define Terabytes(Value) (Gigabytes(Value) * 1024LL) #define ArrayCount(Array) (sizeof(Array) / sizeof((Array)[0])) inline u32 SafeTruncateUInt64(u64 Value) { Assert(Value <= 0xFFFFFFFF); u32 Result = (u32)Value; return (Result); }
 Listing 2: [handmade.h]
#if !defined(HANDMADE_PLATFORM_H)
#include <stdint.h> typedef int8_t s8; typedef int16_t s16; typedef int32_t s32; typedef int64_t s64; typedef s32 b32; typedef uint8_t u8; typedef uint16_t u16; typedef uint32_t u32; typedef uint64_t u64; typedef float f32; typedef double f64; #define internal static #define local_persist static #define global_variable static #define Pi32 3.14159265359f #if HANDMADE_SLOW #define Assert(Expression) if (!(Expression)) { *(int *)0 = 0; } #else #define Assert(Expression) #endif #define Kilobytes(Value) ((Value) * 1024LL) #define Megabytes(Value) (Kilobytes(Value) * 1024LL) #define Gigabytes(Value) (Megabytes(Value) * 1024LL) #define Terabytes(Value) (Gigabytes(Value) * 1024LL) #define ArrayCount(Array) (sizeof(Array) / sizeof((Array)[0])) inline u32 SafeTruncateUInt64(u64 Value) { Assert(Value <= 0xFFFFFFFF); u32 Result = (u32)Value; return (Result); }
#define HANDMADE_PLATFORM_H #endif
 Listing 3: [handmade_platform.h] Moving type definitions.

Next, we'll move the meat of the platform layer: main structures to capture game input, output buffer, and the debug functions we used for reading and writing files.

struct thread_context { int Placeholder; }; // NOTE(casey): Services that the platform layer provides to the game. #if HANDMADE_INTERNAL struct debug_read_file_result { u32 ContentsSize; void *Contents; }; #define DEBUG_PLATFORM_FREE_FILE_MEMORY(name) void name (thread_context *Thread, void *Memory) typedef DEBUG_PLATFORM_FREE_FILE_MEMORY(debug_platform_free_file_memory); #define DEBUG_PLATFORM_READ_ENTIRE_FILE(name) debug_read_file_result name (thread_context *Thread, char *Filename) typedef DEBUG_PLATFORM_READ_ENTIRE_FILE(debug_platform_read_entire_file); #define DEBUG_PLATFORM_WRITE_ENTIRE_FILE(name) b32 name (thread_context *Thread, char *Filename, u32 MemorySize, void *Memory) typedef DEBUG_PLATFORM_WRITE_ENTIRE_FILE(debug_platform_write_entire_file); #endif // NOTE(casey): Services that the game provides to the platform layer. struct game_offscreen_buffer { void *Memory; int Width; int Height; int Pitch; int BytesPerPixel; }; struct game_sound_output_buffer { int SampleCount; int SamplesPerSecond; s16* Samples; }; struct game_button_state { s32 HalfTransitionCount; b32 EndedDown; }; struct game_controller_input { b32 IsConnected; b32 IsAnalog; f32 StickAverageX; f32 StickAverageY; union { game_button_state Buttons[12]; struct { game_button_state MoveUp; game_button_state MoveDown; game_button_state MoveLeft; game_button_state MoveRight; game_button_state ActionUp; game_button_state ActionDown; game_button_state ActionLeft; game_button_state ActionRight; game_button_state LeftShoulder; game_button_state RightShoulder; game_button_state Back; game_button_state Start; // game_button_state Terminator; }; }; }; struct game_input { game_button_state MouseButtons[5]; s32 MouseX, MouseY, MouseZ; f32 dtForFrame; game_controller_input Controllers[5]; };
inline game_controller_input *GetController(game_input *Input, int ControllerIndex) { Assert(ControllerIndex < ArrayCount (Input->Controllers)); game_controller_input *Result = &Input->Controllers[ControllerIndex]; return (Result); }
 Listing 4: [handmade.h]
typedef float f32;
typedef double f64;
struct thread_context { int Placeholder; }; // NOTE(casey): Services that the platform layer provides to the game. #if HANDMADE_INTERNAL struct debug_read_file_result { u32 ContentsSize; void *Contents; }; #define DEBUG_PLATFORM_FREE_FILE_MEMORY(name) void name (thread_context *Thread, void *Memory) typedef DEBUG_PLATFORM_FREE_FILE_MEMORY(debug_platform_free_file_memory); #define DEBUG_PLATFORM_READ_ENTIRE_FILE(name) debug_read_file_result name (thread_context *Thread, char *Filename) typedef DEBUG_PLATFORM_READ_ENTIRE_FILE(debug_platform_read_entire_file); #define DEBUG_PLATFORM_WRITE_ENTIRE_FILE(name) b32 name (thread_context *Thread, char *Filename, u32 MemorySize, void *Memory) typedef DEBUG_PLATFORM_WRITE_ENTIRE_FILE(debug_platform_write_entire_file); #endif // NOTE(casey): Services that the game provides to the platform layer. struct game_offscreen_buffer { void *Memory; int Width; int Height; int Pitch; int BytesPerPixel; }; struct game_sound_output_buffer { int SampleCount; int SamplesPerSecond; s16* Samples; }; struct game_button_state { s32 HalfTransitionCount; b32 EndedDown; }; struct game_controller_input { b32 IsConnected; b32 IsAnalog; f32 StickAverageX; f32 StickAverageY; union { game_button_state Buttons[12]; struct { game_button_state MoveUp; game_button_state MoveDown; game_button_state MoveLeft; game_button_state MoveRight; game_button_state ActionUp; game_button_state ActionDown; game_button_state ActionLeft; game_button_state ActionRight; game_button_state LeftShoulder; game_button_state RightShoulder; game_button_state Back; game_button_state Start; // game_button_state Terminator; }; }; }; struct game_input { game_button_state MouseButtons[5]; s32 MouseX, MouseY, MouseZ; f32 dtForFrame; game_controller_input Controllers[5]; };
#define HANDMADE_PLATFORM_H #endif
 Listing 5: [handmade_platform.h] Moving the service structures.

The last piece is to move the remaining functions, structure, and platform functions declarations:

inline game_controller_input *GetController(game_input *Input, int ControllerIndex) { Assert(ControllerIndex < ArrayCount (Input->Controllers)); game_controller_input *Result = &Input->Controllers[ControllerIndex]; return (Result); } struct game_memory { u64 PermanentStorageSize; void *PermanentStorage; u64 TransientStorageSize; void *TransientStorage; b32 IsInitialized; debug_platform_free_file_memory *DEBUGPlatformFreeFileMemory; debug_platform_read_entire_file *DEBUGPlatformReadEntireFile; debug_platform_write_entire_file *DEBUGPlatformWriteEntireFile; }; #define GAME_UPDATE_AND_RENDER(name) void name(thread_context *Thread, game_memory *Memory, game_input *Input, game_offscreen_buffer* Buffer) typedef GAME_UPDATE_AND_RENDER(game_update_and_render); // NOTE(casey): At the moment, this has to be a very fast function, it cannot be // more than a millisecond or so. // TODO(casey): Reduce the pressure on this function's performance by measuring it // or asking about it, etc. #define GAME_GET_SOUND_SAMPLES(name) void name(thread_context *Thread, game_memory *Memory, game_sound_output_buffer *SoundBuffer) typedef GAME_GET_SOUND_SAMPLES(game_get_sound_samples);
// // // struct game_state { f32 PlayerX; f32 PlayerY; };
 Listing 6: [handmade.h]
struct game_input
{
    game_button_state MouseButtons[5];
    s32 MouseX, MouseY, MouseZ;
    
    f32 dtForFrame;
    game_controller_input Controllers[5];
};
struct game_memory { u64 PermanentStorageSize; void *PermanentStorage; u64 TransientStorageSize; void *TransientStorage; b32 IsInitialized; debug_platform_free_file_memory *DEBUGPlatformFreeFileMemory; debug_platform_read_entire_file *DEBUGPlatformReadEntireFile; debug_platform_write_entire_file *DEBUGPlatformWriteEntireFile; }; #define GAME_UPDATE_AND_RENDER(name) void name(thread_context *Thread, game_memory *Memory, game_input *Input, game_offscreen_buffer* Buffer) typedef GAME_UPDATE_AND_RENDER(game_update_and_render); // NOTE(casey): At the moment, this has to be a very fast function, it cannot be // more than a millisecond or so. // TODO(casey): Reduce the pressure on this function's performance by measuring it // or asking about it, etc. #define GAME_GET_SOUND_SAMPLES(name) void name(thread_context *Thread, game_memory *Memory, game_sound_output_buffer *SoundBuffer) typedef GAME_GET_SOUND_SAMPLES(game_get_sound_samples); inline game_controller_input *GetController(game_input *Input, int ControllerIndex) { Assert(ControllerIndex < ArrayCount (Input->Controllers)); game_controller_input *Result = &Input->Controllers[ControllerIndex]; return (Result); }
#define HANDMADE_PLATFORM_H #endif
 Listing 7: [handmade_platform.h] Moving the DLL export function declarations.

Everything is ready to go, and we can now use our newly-created handmade_platform.h file in both the Windows platform and the game layers:

#include "handmade_platform.h"
#include <windows.h> #include <stdio.h>
 Listing 8: [win32_handmade.cpp]
/* NOTE(casey): HANDMADE_INTERNAL: 0 - Build for public release 1 - Build for developer only HANDMADE_SLOW: 0 - No slow code allowed! 1 - Slow code welcome. */
#include "handmade_platform.h"
 Listing 9: [handmade.h] Adding references to handmade_platform.h

The game code is now officially disconnected from our platform layer! The only thing linking the two is a small header file.

While we're at it, we want to future-proof our platform header to be compilable in C. It will help when we write the platform layer in a language other than C++.

To do so, we need to do two things:

#if !defined(HANDMADE_PLATFORM_H)
#ifdef __cplusplus extern "C" { #endif
#include <stdint.h> // ...
typedef struct thread_context
{ int Placeholder;
} thread_context;
// ...
typedef struct debug_read_file_result
{ u32 ContentsSize; void *Contents;
} debug_read_file_result;
// ...
typedef struct game_offscreen_buffer
{ // ...
} game_offscreen_buffer;
typedef struct game_sound_output_buffer
{ // ...
} game_sound_output_buffer;
typedef struct game_button_state
{ // ...
} game_button_state;
typedef struct game_controller_input
{ // ...
} game_controller_input;
typedef struct game_input
{ game_button_state MouseButtons[5]; s32 MouseX, MouseY, MouseZ; f32 dtForFrame; game_controller_input Controllers[5];
} game_input;
typedef struct game_memory
{ u64 PermanentStorageSize; void *PermanentStorage; u64 TransientStorageSize; void *TransientStorage; b32 IsInitialized; debug_platform_free_file_memory *DEBUGPlatformFreeFileMemory; debug_platform_read_entire_file *DEBUGPlatformReadEntireFile; debug_platform_write_entire_file *DEBUGPlatformWriteEntireFile;
} game_memory;
// ...
#ifdef __cplusplus } #endif
#define HANDMADE_PLATFORM_H #endif
 Listing 10: [handmade_platform.h] Making the platform headers C-compatible.
   

Platform Layer: Clear the Window

As we stand now, we're using a fixed part of our window, which we pretend to be our screen. Our window can be bigger or smaller than that area. If it's bigger, we don't touch the areas outside of that rectangle in any way. Let's fix that and clear the unused window areas to black.

Additionally, we can offset the game window from the window border to know what we're drawing without having the operating system's decorations get in our way.

The code outputting our buffer in the window is inside win32_handmade.cpp`:

internal void
Win32DisplayBufferInWindow(win32_offscreen_buffer *Buffer,
                           HDC DeviceContext, int WindowWidth, int WindowHeight)
{
    // NOTE(casey): For prototyping purposes, we're going to always blit
    // 1-to-1 pixels to make sure we don't introduce artifacts with
    // stretching while we are learning to code the renderer!
    StretchDIBits(DeviceContext,
                  0, 0, Buffer->Width, Buffer->Height,
                  0, 0, Buffer->Width, Buffer->Height,
                  Buffer->Memory,
                  &Buffer->Info,
                  DIB_RGB_COLORS, SRCCOPY);
}
 Listing 11: [win32_handmade.cpp] Win32DisplayBufferInWindow.

A naive method of approaching this problem would be to clear the whole window to black first and then render our window. The function responsible for this is PatBlt, which we briefly used on Day 2 to have our window going. It's a straightforward function that takes the top-left corner, width, and height and performs the operation of choice. In our case, we want to clear the screen to black so we should pick BLACKNESS:

internal void
Win32DisplayBufferInWindow(win32_offscreen_buffer *Buffer,
                           HDC DeviceContext, int WindowWidth, int WindowHeight)
{
int OffsetX = 10; int OffsetY = 10; PatBlt(DeviceContext, 0, 0, WindowWidth, WindowHeight, BLACKNESS);
// NOTE(casey): For prototyping purposes, we're going to always blit // 1-to-1 pixels to make sure we don't introduce artifacts with // stretching while we are learning to code the renderer! StretchDIBits(DeviceContext,
OffsetX, OffsetY, Buffer->Width, Buffer->Height,
0, 0, Buffer->Width, Buffer->Height, Buffer->Memory, &Buffer->Info, DIB_RGB_COLORS, SRCCOPY); }
 Listing 12: [win32_handmade.cpp] A naive approach to clear the screen.

Unfortunately, this method doesn't work, as Windows can page flip between the PatBlt and the StretchDIBits calls, making the whole screen black for one frame.

 Figure 1: Too much Blackness.

What we want to do instead is, instead of having one big rectangle, have four smaller rectangles surrounding our buffer:

internal void
Win32DisplayBufferInWindow(win32_offscreen_buffer *Buffer,
                           HDC DeviceContext, int WindowWidth, int WindowHeight)
{
    int OffsetX = 10;
    int OffsetY = 10;
PatBlt(DeviceContext, 0, 0, WindowWidth, WindowHeight, BLACKNESS);
PatBlt(DeviceContext, 0, 0, WindowWidth, OffsetY, BLACKNESS); // top PatBlt(DeviceContext, 0, OffsetY + Buffer->Height, WindowWidth, WindowHeight, BLACKNESS); // bottom PatBlt(DeviceContext, 0, 0, OffsetX, WindowHeight, BLACKNESS); // left PatBlt(DeviceContext, OffsetX + Buffer->Width, 0, WindowWidth, WindowHeight, BLACKNESS); // right
// NOTE(casey): For prototyping purposes, we're going to always blit // 1-to-1 pixels to make sure we don't introduce artifacts with // stretching while we are learning to code the renderer! StretchDIBits(DeviceContext, OffsetX, OffsetY, Buffer->Width, Buffer->Height, 0, 0, Buffer->Width, Buffer->Height, Buffer->Memory, &Buffer->Info, DIB_RGB_COLORS, SRCCOPY); }
 Listing 13: [win32_handmade.cpp] A better way to clear the screen.

This solution will give us nice black edges surrounding our screen. You might notice that we aren't subtracting the buffer width and height when we draw the bottom and right rectangles - that's not really necessary; Windows will do it for us.

   

Inline the Rounding Functions

When we introduced RoundReal32ToInt32 and RoundReal32ToUInt32, we moved fast and made the functions static without too much thought. However, these are tiny functions, and there is no value to keep them as such. Instead, we want to inline them. This way, the compiler will expand the function directly on the line where it's called instead of making a separate call to a different chunk of code.

The performance gain in this instance is marginal, but let's not be wasteful where we can avoid it.

inline s32
RoundReal32ToInt32(f32 Number) { s32 Result = (s32)(Number + 0.5f); return (Result); }
inline u32
RoundReal32ToUInt32(f32 Number) { u32 Result = (u32)(Number + 0.5f); return (Result); }
 Listing 14: [handmade.cpp] Inlining the rounding functions.

That's about it for the clean-up we need today; let's get to the new stuff!

   

Player Collision with the Tilemap

   

Setup

Our player character moves when we process the user input:

for (int ControllerIndex = 0;
     ControllerIndex < ArrayCount(Input->Controllers);
     ++ControllerIndex)
{
    // ...
    GameState->PlayerX += Input->dtForFrame * dPlayerX;
    GameState->PlayerY += Input->dtForFrame * dPlayerY;
}
 Listing 15: [handmade.cpp > GameUpdateAndRender] Player movement.

If we want to implement collisions, we want to verify the destination position is valid; if it's not (say, it's a wall tile), we will reject the user's input. It would look something like this:

f32 NewPlayerX = GameState->PlayerX + Input->dtForFrame * dPlayerX; f32 NewPlayerY = GameState->PlayerY + Input->dtForFrame * dPlayerY;
b32 IsValid = false; // Do some checks to make sure the new position is valid if (IsValid) { GameState->PlayerX = NewPlayerX; GameState->PlayerY = NewPlayerY; }
 Listing 16: [handmade.cpp > GameUpdateAndRender] Validating player input before accepting it.

We will be doing our checks against the tilemap we defined last time, so we need to move that block before the input processing:

f32 UpperLeftX = -30; f32 UpperLeftY = 0; f32 TileWidth = 60; f32 TileHeight = 60; u32 TileMap[9][17] = { {1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 1, 1, 1, 1, 1, 1, 1}, {1, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 1}, {1, 1, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 1, 0, 1}, {1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1}, {0, 0, 0, 0, 0, 1, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0}, {1, 1, 0, 0, 0, 1, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 1}, {1, 0, 0, 0, 0, 1, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1}, {1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 1}, {1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 1, 1, 1, 1, 1, 1, 1} };
game_state *GameState = (game_state*)Memory->PermanentStorage; if(!Memory->IsInitialized) { Memory->IsInitialized = true; } for (int ControllerIndex = 0; //...) { // ... f32 NewPlayerX = GameState->PlayerX + Input->dtForFrame * dPlayerX; f32 NewPlayerY = GameState->PlayerY + Input->dtForFrame * dPlayerY; b32 IsValid = false; if (IsValid) { GameState->PlayerX = NewPlayerX; GameState->PlayerY = NewPlayerY; } }
u32 TileMap[9][17] = { {1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 1, 1, 1, 1, 1, 1, 1}, {1, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 1}, {1, 1, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 1, 0, 1}, {1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1}, {0, 0, 0, 0, 0, 1, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0}, {1, 1, 0, 0, 0, 1, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 1}, {1, 0, 0, 0, 0, 1, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1}, {1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 1}, {1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 1, 1, 1, 1, 1, 1, 1} };
DrawRectangle(Buffer, 0.0f, 0.0f, (f32)Buffer->Width, (f32)Buffer->Height, 1.0f, 0.0f, 1.0f);
f32 UpperLeftX = -30; f32 UpperLeftY = 0; f32 TileWidth = 60; f32 TileHeight = 60;
 Listing 17: [handmade.cpp > GameUpdateAndRender] Moving the tile map definition.
   

Calculate Player Position

What exactly do we want to do, though?

Every tile occupies multiple pixels in our window: we set up our tile width and height to be 60 pixels, starting from a specific UpperLeftX and Y. Furthermore, we define the player's position in the same pixel space as the tiles. Therefore, we want to calculate the player's tile position first. Then, we look up our tile map, and if the position happens to be the wall, we discard it.

To calculate the relative player's position, we want to get the player's position and subtract the starting X and Y (tile map origin) from it.

TileMapOrigin(UpperLeftX,UpperLeftY)012345012Player-34

 Figure 2: Calculating player position on the tile map.

f32 NewPlayerX = GameState->PlayerX + Input->dtForFrame * dPlayerX;
f32 NewPlayerY = GameState->PlayerY + Input->dtForFrame * dPlayerY;
s32 PlayerTileX = NewPlayerX - UpperLeftX; s32 PlayerTileY = NewPlayerY - UpperLeftY;
b32 IsValid = false; // ...
 Listing 18: [handmade.cpp > GameUpdateAndRender] Player tile position.

We can now calculate which tile the player occupies with a simple divide. We should divide our relative X position by the tile width and our relative Y position by the tile height.

RelativePlayerXPositionTileWidth0123450TileHeight1RelativePlayerYPosition2Player(3,3)34

 Figure 3: Calculating player tile index.

This operation will give us a fractional value, which we should discard to reach the tile index. Rounding won't work here; we only want to round to the lower value. Luckily, that's precisely what C's casting does.

To be explicit, let's define a new function that does the truncation for us:

inline u32
RoundReal32ToUInt32(f32 Number)
{
    u32 Result = (u32)(Number + 0.5f);
    return (Result);
}
inline s32 TruncateReal32ToInt32(f32 Number) { s32 Result = (s32)Number; return (Result); }
 Listing 19: [handmade.cpp] Introducing TruncateReal32ToInt32.

Let's do the division that we talked about:

f32 NewPlayerX = GameState->PlayerX + Input->dtForFrame * dPlayerX;
f32 NewPlayerY = GameState->PlayerY + Input->dtForFrame * dPlayerY;
s32 PlayerTileX = TruncateReal32ToInt32((NewPlayerX - UpperLeftX) / TileWidth); s32 PlayerTileY = TruncateReal32ToInt32((NewPlayerY - UpperLeftY) / TileHeight);
b32 IsValid = false; // ...
 Listing 20: [handmade.cpp > GameUpdateAndRender] Player tile index.
   

Validate Player Position

We can now start thinking about validating the player's position. However, before we check whether the player is inside the wall, we should verify they are on the map at all! To do that, we need to confirm that the TileX and TileY values are larger (or equal) than zero but smaller than the overall tilemap size.

We don't currently store this size; we pass 9 and 17 directly to the array. We could check against raw (also known as “magic”) numbers again, but we'd have to pay attention to changing the values if we decide to make our tilemap bigger or smaller in the future. Unfortunately, we can't simply store the values in a variable like usual. In C/C++, the array size must be known at compile time, so we have three options here:

The latter two have no real difference; it's a style preference. You can use whichever you prefer. In this instance, let's go for a #define:

#define TILE_MAP_COUNT_X 17 #define TILE_MAP_COUNT_Y 9
f32 UpperLeftX = -30; f32 UpperLeftY = 0; f32 TileWidth = 60; f32 TileHeight = 60;
u32 TileMap[TILE_MAP_COUNT_Y][TILE_MAP_COUNT_X] = /* ... */ ;
// ... // Tilemap drawing DrawRectangle(Buffer, 0.0f, 0.0f, (f32)Buffer->Width, (f32)Buffer->Height, 1.0f, 0.0f, 1.0f); for(int Row = 0;
Row < TILE_MAP_COUNT_Y;
++Row) { for(int Column = 0;
Column < TILE_MAP_COUNT_X;
++Column) { // ... } }
 Listing 21: [handmade.cpp > GameUpdateAndRender] Storing the tilemap size.

We can use these values to verify that the player position is within the tilemap bounds:

s32 PlayerTileX = TruncateReal32ToInt32((NewPlayerX - UpperLeftX) / TileWidth);
s32 PlayerTileY = TruncateReal32ToInt32((NewPlayerY - UpperLeftY) / TileHeight);

b32 IsValid = false;
if ((PlayerTileX >= 0) && (PlayerTileX < TILE_MAP_COUNT_X) && (PlayerTileY >= 0) && (PlayerTileY < TILE_MAP_COUNT_Y)) { // Check for IsValid here }
if (IsValid) { GameState->PlayerX = NewPlayerX; GameState->PlayerY = NewPlayerY; }
 Listing 22: [handmade.cpp > GameUpdateAndRender] Making sure we're within the tilemap bounds.

Finally, we can see if the tile we want to go to is occupied (in our case, by a wall). We can use the player tile coordinates to access a specific value inside the tile map. Then, we should check if the value we get is 0 since we use 1 to put a wall on the screen.

if ((PlayerTileX >= 0) && (PlayerTileX < TILE_MAP_COUNT_X) && 
    (PlayerTileY >= 0) && (PlayerTileY < TILE_MAP_COUNT_Y))
{
u32 TileMapValue = TileMap[PlayerTileY][PlayerTileX]; IsValid = (TileMapValue == 0);
}
 Listing 23: [handmade.cpp > GameUpdateAndRender] Validating the tile.

As usual, this is nowhere near the final code, but this should be enough to get us going.

   

Initialize Player Position

Since we don't do any of the initialization ourselves, the game spawns the player at position (0, 0) of our screen buffer. The new collision checks we put in place keep him stuck so he can never move away from its corner. Let's give him a spot on the map during the initialization step:

game_state *GameState = (game_state*)Memory->PermanentStorage;
if(!Memory->IsInitialized)
{
GameState->PlayerX = 150; GameState->PlayerY = 150;
Memory->IsInitialized = true; }
 Listing 24: [handmade.cpp > GameUpdateAndRender] Initializing player position.
   

Revise the Tile Map Code

The player can now collide with the walls, but only with its center point. While we're OK with them eclipsing the scenery with its top part, we definitely want to consider player width.

We'll start from there, but this will lead us to rework our existing code significantly so that everything falls into place.

   

Introduce IsTileMapPointEmpty

A simple way to do wide collision is to check whether the player's rectangle's bottom right and left points are invading a wall tile, same as we did with the center.

Of course, we could just copy and paste the same code twice, but extracting the code we just wrote in a new function is more straightforward and less error-prone. Let's call this new function IsTileMapPointEmpty:

internal b32 IsTileMapPointEmpty(f32 NewPlayerX, f32 NewPlayerY) { b32 IsValid = false; s32 PlayerTileX = TruncateReal32ToInt32((NewPlayerX - UpperLeftX) / TileWidth); s32 PlayerTileY = TruncateReal32ToInt32((NewPlayerY - UpperLeftY) / TileHeight); if ((PlayerTileX >= 0) && (PlayerTileX < TILE_MAP_COUNT_X) && (PlayerTileY >= 0) && (PlayerTileY < TILE_MAP_COUNT_Y)) { u32 TileMapValue = TileMap[PlayerTileY][PlayerTileX]; IsValid = (TileMapValue == 0); } return (IsValid); }
#if defined __cplusplus extern "C" #endif GAME_UPDATE_AND_RENDER(GameUpdateAndRender) { // ... f32 NewPlayerX = GameState->PlayerX + Input->dtForFrame * dPlayerX; f32 NewPlayerY = GameState->PlayerY + Input->dtForFrame * dPlayerY;
s32 PlayerTileX = TruncateReal32ToInt32((NewPlayerX - UpperLeftX) / TileWidth); s32 PlayerTileY = TruncateReal32ToInt32((NewPlayerY - UpperLeftY) / TileHeight); b32 IsValid = false; if ((PlayerTileX >= 0) && (PlayerTileX < TILE_MAP_COUNT_X) && (PlayerTileY >= 0) && (PlayerTileY < TILE_MAP_COUNT_Y)) { u32 TileMapValue = TileMap[PlayerTileY][PlayerTileX]; IsValid = (TileMapValue == 0); }
if (IsTileMapPointEmpty(NewPlayerX, NewPlayerY))
{ GameState->PlayerX = NewPlayerX; GameState->PlayerY = NewPlayerY; } // ... }
 Listing 25: [handmade.cpp] Introducing IsTileMapPointEmpty.

Let's look better at IsTileMapPointEmpty. First of all, we probably want to rename a few variables to make the function more generic:

internal b32
IsTileMapPointEmpty(f32 TestX, f32 TestY)
{
b32 Empty = false;
s32 TileX = TruncateReal32ToInt32((TestX - UpperLeftX) / TileWidth); s32 TileY = TruncateReal32ToInt32((TestY - UpperLeftY) / TileHeight);
if ((TileX >= 0) && (TileX < TILE_MAP_COUNT_X) && (TileY >= 0) && (TileY < TILE_MAP_COUNT_Y))
{
u32 TileMapValue = TileMap[TileY][TileX]; Empty = (TileMapValue == 0);
}
return (Empty);
}
 Listing 26: [handmade.cpp] Renaming variables.
   

Introduce tile_map Structure

If we try to compile, you will notice that we're missing a bunch of additional data:

'UpperLeftX': undeclared identifier
'UpperLeftY': undeclared identifier
'TileWidth': undeclared identifier
'TileHeight': undeclared identifier
'TILE_MAP_COUNT_X': undeclared identifier
'TILE_MAP_COUNT_Y': undeclared identifier
'TileMap': undeclared identifier

Of course, we could pass all this data to the function directly, but it will get messy quickly. However, it's an indication of things that go together; we can, therefore, start grouping data into a struct that we can pass along instead.

It's a very convenient practice - let structures emerge from the code instead of thinking about them beforehand. Even if, in this case, you might have guessed what such a structure might contain, it's always easier to let it emerge on its own.

So, let's introduce a struct with information that allows us to do the math on a tile map:

struct tile_map { s32 CountX; s32 CountY; f32 UpperLeftX; f32 UpperLeftY; f32 TileWidth; f32 TileHeight; u32 *Tiles; };
struct game_state { f32 PlayerX; f32 PlayerY; };
 Listing 27: [handmade.h] Introducing tile_map.

The use case inside IsTileMapPointEmpty would be something along these lines.

internal b32
IsTileMapPointEmpty(tile_map *TileMap, f32 TestX, f32 TestY)
{ b32 Empty = false;
s32 TileX = TruncateReal32ToInt32((TestX - TileMap->UpperLeftX) / TileMap->TileWidth); s32 TileY = TruncateReal32ToInt32((TestY - TileMap->UpperLeftY) / TileMap->TileHeight);
if ((TileX >= 0) && (TileX < TileMap->CountX) && (TileY >= 0) && (TileY < TileMap->CountY))
{
u32 TileMapValue = TileMap->Tiles[TileY][TileX];
Empty = (TileMapValue == 0); } return (Empty); }
 Listing 28: [handmade.cpp] Using tile_map in IsTileMapPointEmpty.
   

Accessing the Tile Map Cells

We still can't compile, though. This solution highlights an important consideration: in C/C++, you cannot pass or store arrays; they are always converted to pointers once they exit their scope. To solve this, we should change our tile map array from two-dimensional to one-dimensional. For the time being, this doesn't affect the tile map definiton; we can still define it as two-dimensional. We have already arranged the tilemap to store rows first and columns second, setting ourselves up for success.

The approach to accessing the tile would be the same as we did for the pixels in our buffer: Multiply the desired Y by width and add the desired X position. We probably want to write a utility function to retrieve the cells so that we don't have to think about it all the time. We'll call this function GetTileValueUnchecked since we'll assume that we already did all the checks to ensure that the X and Y coordinates are within the tilemap bounds.

inline u32 GetTileValueUnchecked(tile_map *TileMap, s32 TileX, s32 TileY) { u32 TileMapValue = TileMap->Tiles[TileY * TileMap->CountX + TileX]; return (TileMapValue); }
internal b32 IsTileMapPointEmpty(tile_map *TileMap, f32 TestX, f32 TestY) { b32 Empty = false; s32 TileX = TruncateReal32ToInt32((TestX - TileMap->UpperLeftX) / TileMap->TileWidth); s32 TileY = TruncateReal32ToInt32((TestY - TileMap->UpperLeftY) / TileMap->TileHeight); if ((TileX >= 0) && (TileX < TileMap->CountX) && (TileY >= 0) && (TileY < TileMap->CountY)) {
u32 TileMapValue = GetTileValueUnchecked(TileMap, TileX, TileY);
Empty = (TileMapValue == 0); } return (Empty); }
 Listing 29: [handmade.cpp] Introducing GetTileValueUnchecked.
   

Redefine our Tile Map

Having done all that, let's go back into GameUpdateAndRender to create and pass the tile map in a struct:

#define TILE_MAP_COUNT_X 17
#define TILE_MAP_COUNT_Y 9
u32 Tiles[TILE_MAP_COUNT_Y][TILE_MAP_COUNT_X] =
{ {1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 1, 1, 1, 1, 1, 1, 1}, {1, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 1}, {1, 1, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 1, 0, 1}, {1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1}, {0, 0, 0, 0, 0, 1, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0}, {1, 1, 0, 0, 0, 1, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 1}, {1, 0, 0, 0, 0, 1, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1}, {1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 1}, {1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 1, 1, 1, 1, 1, 1, 1} };
f32 UpperLeftX = -30; f32 UpperLeftY = 0; f32 TileWidth = 60; f32 TileHeight = 60;
tile_map TileMap; TileMap.CountX = TILE_MAP_COUNT_X; TileMap.CountY = TILE_MAP_COUNT_Y; TileMap.UpperLeftX = -30; TileMap.UpperLeftY = 0; TileMap.TileWidth = 60; TileMap.TileHeight = 60; TileMap.Tiles = (u32 *)Tiles;
// ... for (int ControllerIndex = 0; /* ... */ ) { // ... f32 NewPlayerX = GameState->PlayerX + Input->dtForFrame * dPlayerX; f32 NewPlayerY = GameState->PlayerY + Input->dtForFrame * dPlayerY;
if (IsTileMapPointEmpty(&TileMap, NewPlayerX, NewPlayerY))
{ GameState->PlayerX = NewPlayerX; GameState->PlayerY = NewPlayerY; } } for(int Row = 0; Row < TILE_MAP_COUNT_Y; ++Row) { for(int Column = 0; Column < TILE_MAP_COUNT_X; ++Column) {
u32 TileID = GetTileValueUnchecked(&TileMap, Column, Row);
f32 Gray = 0.5f; if (TileID == 1) { Gray = 1.0f; }
f32 MinX = TileMap.UpperLeftX + ((f32)Column) * TileMap.TileWidth; f32 MinY = TileMap.UpperLeftY + ((f32)Row) * TileMap.TileHeight; f32 MaxX = MinX + TileMap.TileWidth; f32 MaxY = MinY + TileMap.TileHeight;
DrawRectangle(Buffer, MinX, MinY, MaxX, MaxY, Gray, Gray, Gray); } } f32 PlayerR = 1.0f; f32 PlayerG = 1.0f; f32 PlayerB = 0.0f;
f32 PlayerWidth = 0.75f * TileMap.TileWidth; f32 PlayerHeight = TileMap.TileHeight;
f32 PlayerLeft = GameState->PlayerX - (0.5f * PlayerWidth); f32 PlayerTop = GameState->PlayerY - PlayerHeight;
 Listing 30: [handmade.cpp > GameUpdateAndRender] Defining the tile map as a struct.
   

Add Player Width to Collision

We're now compiling! We haven't done anything new, though, since our code is doing the same thing it was before. It's easy to fix since we can call IsTileMapPointEmpty on multiple points and only accept the user's input if all of them are true:

f32 NewPlayerX = GameState->PlayerX + Input->dtForFrame * dPlayerX;
f32 NewPlayerY = GameState->PlayerY + Input->dtForFrame * dPlayerY;
if (IsTileMapPointEmpty(&TileMap, NewPlayerX, NewPlayerY) &&
IsTileMapPointEmpty(&TileMap, NewPlayerX - 0.5f * PlayerWidth, NewPlayerY) && IsTileMapPointEmpty(&TileMap, NewPlayerX + 0.5f * PlayerWidth, NewPlayerY))
{ GameState->PlayerX = NewPlayerX; GameState->PlayerY = NewPlayerY; }
 Listing 31: [handmade.cpp > GameUpdateAndRender] Adding two more points to collision check.

We define the player width and height below, so we should move them towards the beginning. This action, again, should tingle your coding sense to refactor down the line eventually.

tile_map TileMap;
TileMap.CountX = TILE_MAP_COUNT_X;
TileMap.CountY = TILE_MAP_COUNT_Y;

TileMap.UpperLeftX = -30;
TileMap.UpperLeftY = 0;
TileMap.TileWidth = 60;
TileMap.TileHeight = 60;

TileMap.Tiles = (u32 *)Tiles;
f32 PlayerWidth = 0.75f * TileMap.TileWidth; f32 PlayerHeight = TileMap.TileHeight;
game_state *GameState = (game_state*)Memory->PermanentStorage; // ... f32 PlayerR = 1.0f; f32 PlayerG = 1.0f; f32 PlayerB = 0.0f;
f32 PlayerWidth = 0.75f * TileMap.TileWidth; f32 PlayerHeight = TileMap.TileHeight;
f32 PlayerLeft = GameState->PlayerX - (0.5f * PlayerWidth); f32 PlayerTop = GameState->PlayerY - PlayerHeight;
 Listing 32: [handmade.cpp > GameUpdateAndRender] Extracting the player width and height from the drawing code.

Finally, we are colliding as intended! The game feels awful; we're getting stuck once inside walls, but we're getting somewhere! Slow and steady, we're moving towards the design that we want. Concrete baby steps that give you an idea of whether your design moves in a good or wrong direction.

   

Multiple Tile Maps

   

Create an Array of Tile Maps

We now have a player moving around our tilemap. When the player enters a door, we want them to transition into a new tilemap. Let's think about what has to happen for that to occur.

The first thing we'll need is, obviously, new tilemaps. Luckily, in our latest refactor, we had already set ourselves up for success. We have our tile_map structure, so it's simply a matter of filling it out for a new tilemap.

#define TILE_MAP_COUNT_X 17
#define TILE_MAP_COUNT_Y 9
u32 Tiles0[TILE_MAP_COUNT_Y][TILE_MAP_COUNT_X] =
{
{1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1},
{1, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 1}, {1, 1, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 1, 0, 1}, {1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1},
{1, 0, 0, 0, 0, 1, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1},
{1, 1, 0, 0, 0, 1, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 1}, {1, 0, 0, 0, 0, 1, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1}, {1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 1}, {1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 1, 1, 1, 1, 1, 1, 1} };
u32 Tiles1[TILE_MAP_COUNT_Y][TILE_MAP_COUNT_X] = { {1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 1, 1, 1, 1, 1, 1, 1}, {1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1}, {1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1}, {1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1}, {1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1}, {1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1}, {1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1}, {1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1}, {1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1} };
 Listing 33: [handmade.cpp > GameUpdateAndRender] We also want to close all the doors that don't connect to the other tilemap.

We will create an array of two tile maps to store these tilemaps.

tile_map TileMaps[2];
TileMaps[0].CountX = TILE_MAP_COUNT_X; TileMaps[0].CountY = TILE_MAP_COUNT_Y; TileMaps[0].UpperLeftX = -30; TileMaps[0].UpperLeftY = 0; TileMaps[0].TileWidth = 60; TileMaps[0].TileHeight = 60; TileMaps[0].Tiles = (u32 *)Tiles0;
TileMaps[1] = TileMaps[0]; TileMaps[1].Tiles = (u32 *)Tiles1;
 Listing 34: [handmade.cpp > GameUpdateAndRender] Note that to create TileMaps[1] we largely clone `TileMaps[0].

Finally, we will create a pointer TileMap that the rest of GameUpdateAndRender will use.

TileMaps[1] = TileMaps[0];
TileMaps[1].Tiles = (u32 *)Tiles1;
tile_map *TileMap = &TileMaps[0];
f32 PlayerWidth = 0.75f * TileMap->TileWidth; f32 PlayerHeight = TileMap->TileHeight;
game_state *GameState = (game_state*)Memory->PermanentStorage; // ... for (int ControllerIndex = 0; /* ... */ ) { // ... f32 NewPlayerX = GameState->PlayerX + Input->dtForFrame * dPlayerX; f32 NewPlayerY = GameState->PlayerY + Input->dtForFrame * dPlayerY;
if (IsTileMapPointEmpty(TileMap, NewPlayerX, NewPlayerY) && IsTileMapPointEmpty(TileMap, NewPlayerX - 0.5f * PlayerWidth, NewPlayerY) && IsTileMapPointEmpty(TileMap, NewPlayerX + 0.5f * PlayerWidth, NewPlayerY))
{ GameState->PlayerX = NewPlayerX; GameState->PlayerY = NewPlayerY; } } DrawRectangle(Buffer, 0.0f, 0.0f, (f32)Buffer->Width, (f32)Buffer->Height, 1.0f, 0.0f, 1.0f); for(int Row = 0; /* ... */ ) { for(int Column = 0; /* ... */ ) {
u32 TileID = GetTileValueUnchecked(TileMap, Column, Row);
f32 Gray = 0.5f; if (TileID == 1) { Gray = 1.0f; }
f32 MinX = TileMap->UpperLeftX + ((f32)Column) * TileMap->TileWidth; f32 MinY = TileMap->UpperLeftY + ((f32)Row) * TileMap->TileHeight; f32 MaxX = MinX + TileMap->TileWidth; f32 MaxY = MinY + TileMap->TileHeight;
DrawRectangle(Buffer, MinX, MinY, MaxX, MaxY, Gray, Gray, Gray); } }
 Listing 35: [handmade.cpp > GameUpdateAndRender] Replacing a tile_map with a pointer to a tile_map.
   

Introduce Worlds

We want to think about the continuity of tile maps in the world. It's almost as if it were one big tilemap. But for different reasons we don't want to make it one big tilemap. For instance, we'd be wasting space if some tilemaps were empty.

Eventually, we want to have the flexibility of having extremely sparse tilemaps. At some point, we might add a third dimension to our world, and we'll be able to layer tilemaps over tilemaps vertically. Maybe some points would have extremely deep or extremely tall stacks. We don't want the whole world to explode in size only because of very few outliers.

So, when we step through a door, we would ask, “Is it OK for the player to move outside? What's out there?”. In practice, going off the edge of one tilemap would bring us onto another.

Currently, all functions, like IsTileMapPointEmpty, are geared towards thinking of one tilemap at a time. Instead, we want something that can think about more than one tilemap. It would be some higher-level structure that oversees the whole world's tilemaps. Something like this:

internal b32
IsTileMapPointEmpty(tile_map *TileMap, f32 TestX, f32 TestY)
{
    // ...
}
internal b32 IsWorldPointEmpty(world *World, f32 TestX, f32 TestY) { }
 Listing 36: [handmade.cpp] Thinking about worlds.

In IsTileMapPointEmpty, we assume there's nothing beyond the edges of the tilemap. That's why we only evaluate points that we know lie within the tilemap. What if we projected the edges towards the tilemap we would be in and query its world point?

What we're thinking about is a higher coordinate system. Like tilemaps to pixels, we'd have worlds to tilemaps.

To play around with this idea better, let's create a 2×2 grid of tilemaps, extending the amount of tilemaps to 4.

#define TILE_MAP_COUNT_X 17
#define TILE_MAP_COUNT_Y 9
u32 Tiles00[TILE_MAP_COUNT_Y][TILE_MAP_COUNT_X] =
{ {1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1}, {1, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 1}, {1, 1, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 1, 0, 1}, {1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1},
{1, 0, 0, 0, 0, 1, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0},
{1, 1, 0, 0, 0, 1, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 1}, {1, 0, 0, 0, 0, 1, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1}, {1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 1}, {1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 1, 1, 1, 1, 1, 1, 1}, };
u32 Tiles01[TILE_MAP_COUNT_Y][TILE_MAP_COUNT_X] =
{ {1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 1, 1, 1, 1, 1, 1, 1}, {1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1}, {1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1}, {1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1},
{1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0},
{1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1}, {1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1}, {1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1}, {1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1}, };
u32 Tiles10[TILE_MAP_COUNT_Y][TILE_MAP_COUNT_X] = { {1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1}, {1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1}, {1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1}, {1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1}, {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1}, {1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1}, {1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1}, {1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1}, {1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 1, 1, 1, 1, 1, 1, 1}, }; u32 Tiles11[TILE_MAP_COUNT_Y][TILE_MAP_COUNT_X] = { {1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 1, 1, 1, 1, 1, 1, 1}, {1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1}, {1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1}, {1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1}, {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1}, {1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1}, {1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1}, {1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1}, {1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1}, };
tile_map TileMaps[2][2];
 Listing 37: [handmade.cpp] Adding two more tile maps and doors to reach them.

Let's also define the related tile_map structures:

tile_map TileMaps[2][2];
TileMaps[0][0].CountX = TILE_MAP_COUNT_X; TileMaps[0][0].CountY = TILE_MAP_COUNT_Y; TileMaps[0][0].UpperLeftX = -30; TileMaps[0][0].UpperLeftY = 0; TileMaps[0][0].TileWidth = 60; TileMaps[0][0].TileHeight = 60; TileMaps[0][0].Tiles = (uint32 *)Tiles00; TileMaps[0][1] = TileMaps[0][0]; TileMaps[0][1].Tiles = (uint32 *)Tiles01; TileMaps[1][0] = TileMaps[0][0]; TileMaps[1][0].Tiles = (uint32 *)Tiles10; TileMaps[1][1] = TileMaps[0][0]; TileMaps[1][1].Tiles = (uint32 *)Tiles11; tile_map *TileMap = &TileMaps[0][0];
 Listing 38: [handmade.cpp] Using the 2D array of tilemaps.

Finally, we can imagine the world struct to be something that will contain information about the tilemaps. We'll expand on this idea further next time. For the time being, let's simply store the tile_map array we just defined, as well as its X and Y size. However, we will want to implement sparseness in the future.

struct tile_map
{
    // ... 
};
struct world { // TODO(casey): Beginner's sparseness s32 TileMapCountX; s32 TileMapCountY; tile_map *TileMaps; };
 Listing 39: [handmade.h] Introducing the world struct.

Let's define it in GameUpdateAndRender:

tile_map TileMaps[2][2];
// ...

tile_map *TileMap = &TileMaps[0][0];
world World; World.TileMapCountX = 2; World.TileMapCountY = 2; World.TileMaps = (tile_map *)TileMaps;
f32 PlayerWidth = 0.75f * TileMap->TileWidth; f32 PlayerHeight = TileMap->TileHeight;
 Listing 40: [handmade.cpp > GameUpdateAndRender] Defining our World.

We should now compile with no changes to the game behavior.

   

Connecting the Tile Maps

Let's come back to the function we quickly stubbed before, IsWorldPointEmpty. We probably want to get the player's X and Y position in pixels, so we'll additionally need to add another layer to receive the player's X and Y position in tilemaps. From it, we will get a tilemap and, if it's valid, check if the player's X and Y position on that tilemap is valid, either:

internal b32
IsWorldPointEmpty(world *World, s32 TileMapX, s32 TileMapY, f32 TestX, f32 TestY)
{
b32 Empty = false; tile_map *TileMap = GetTileMap(World, TileMapX, TileMapY); if(TileMap) { Empty = IsTileMapPointEmpty(TileMap, TestX, TestY); } return (Empty);
}
 Listing 41: [handmade.cpp] Drafting IsWorldPointEmpty.

Here, we can see a need for a GetTileMap function similar to GetTileValueUnchecked. The only difference is that we will need to check that the tile map exists before attempting to access it:

inline tile_map * GetTileMap(world *World, s32 TileMapX, s32 TileMapY) { tile_map *TileMap = 0; if ((TileMapX >= 0) && (TileMapX < World->TileMapCountX) && (TileMapY >= 0) && (TileMapY < World->TileMapCountY)) { TileMap = &World->TileMaps[TileMapY * World->TileMapCountX + TileMapX]; } return (TileMap); }
inline u32 GetTileValueUnchecked(tile_map *TileMap, s32 TileX, s32 TileY) { u32 TileMapValue = TileMap->Tiles[TileY * TileMap->CountX + TileX]; return (TileMapValue); }
 Listing 42: [handmade.cpp] Introducing GetTileMap.

Once again, we're compiling with no visible changes to the game behavior.

   

Recap

We'll wrap up on a bit of a cliffhanger today. Today, we completed our first basic collision system and started our journey toward multiple tilemaps. There are still some things that we keep around inside the tile_map struct, like UpperLeft coordinates or tile dimensions; it doesn't really make sense, and we'll need to look deeper into it next time.

   

Navigation

Previous: Day 28. Drawing a Tile Map

Up Next: Day 30. Moving Between Tile Maps

Back to Index

Glossary

MSDN

PatBlt

Page Flip

Other

casting

formatted by Markdeep 1.13