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.
(Top)
1 Clean Up
1.1 Introduce handmade_platform.h
1.2 Platform Layer: Clear the Window
1.3 Inline the Rounding Functions
2 Player Collision with the Tilemap
2.1 Setup
2.2 Calculate Player Position
2.3 Validate Player Position
2.4 Initialize Player Position
3 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
4 Multiple Tile Maps
4.1 Create an Array of Tile Maps
4.2 Introduce Worlds
4.3 Connecting the Tile Maps
5 Recap
6 Navigation
handmade_platform.h
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
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);
}
#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
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);
}
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
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;
};
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
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>
/*
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"
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:
typedef struct struct_name{ /* ... */ } struct_name;
In C++, the compiler adds typedef
and alias automatically, allowing programmers only to write struct struct_name { /* ... */ };
.
extern "C"
block so the C++ compiler doesn't mangle the names.#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
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);
}
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);
}
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.
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);
}
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.
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 s32RoundReal32ToInt32(f32 Number)
{
s32 Result = (s32)(Number + 0.5f);
return (Result);
}
inline u32RoundReal32ToUInt32(f32 Number)
{
u32 Result = (u32)(Number + 0.5f);
return (Result);
}
That's about it for the clean-up we need today; let's get to the new stuff!
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;
}
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;
}
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;
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.
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;
// ...
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.
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);
}
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;
// ...
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:
const
variable
#define
macro
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 9f32 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)
{
// ...
}
}
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;
}
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);}
As usual, this is nowhere near the final code, but this should be enough to get us going.
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;
}
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.
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;
}
// ...
}
Let's look better at IsTileMapPointEmpty
. First of all, we probably want to rename a few variables to make the function more generic:
internal b32IsTileMapPointEmpty(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);}
tile_map
StructureIf 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;
};
The use case inside IsTileMapPointEmpty
would be something along these lines.
internal b32IsTileMapPointEmpty(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);
}
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);
}
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 9u32 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;
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;
}
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;
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.
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 9u32 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}
};
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;
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);
}
}
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)
{
}
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 9u32 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];
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];
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;
};
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;
We should now compile with no changes to the game behavior.
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 b32IsWorldPointEmpty(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);}
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);
}
Once again, we're compiling with no visible changes to the game behavior.
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.
Previous: Day 28. Drawing a Tile Map
Up Next: Day 30. Moving Between Tile Maps