Added Game_World_Player and dying/respawning.

This commit is contained in:
2025-12-18 15:59:14 +01:00
parent 0074ecd57b
commit 2a999a12bf
10 changed files with 294 additions and 183 deletions

View File

@@ -14,7 +14,6 @@
typedef struct { typedef struct {
uint16_t player_id; uint16_t player_id;
SQueue input_queue; SQueue input_queue;
Simulation_Game_Input prev_input; // TODO: SS - Remove this and just peek the queue?
} Locally_Controlled_Player; } Locally_Controlled_Player;
typedef struct { typedef struct {

View File

@@ -92,21 +92,15 @@ static void state_tick(Presentation_State *state) {
.left = IsKeyPressed(KEY_LEFT) || IsKeyPressed(KEY_A) .left = IsKeyPressed(KEY_LEFT) || IsKeyPressed(KEY_A)
}; };
Locally_Controlled_Player *player = &instance->locally_controlled_players[0]; if(!simulation_input_empty(input)) {
if(!simulation_input_equal(input, player->prev_input)) {
game_instance_push_local_input(instance, 0, input); game_instance_push_local_input(instance, 0, input);
player->prev_input = input;
} }
} }
else if(instance->amount_of_local_players > 1) { else if(instance->amount_of_local_players > 1) {
for(uint8_t i = 0; i < instance->amount_of_local_players; i++) { for(uint8_t i = 0; i < instance->amount_of_local_players; i++) {
Simulation_Game_Input input = gather_input_for_local_player(i); Simulation_Game_Input input = gather_input_for_local_player(i);
Locally_Controlled_Player *player = &instance->locally_controlled_players[i]; if(!simulation_input_empty(input)) {
if(!simulation_input_equal(input, player->prev_input)) {
game_instance_push_local_input(instance, i, input); game_instance_push_local_input(instance, i, input);
player->prev_input = input;
} }
} }
} }
@@ -121,11 +115,8 @@ static void state_tick(Presentation_State *state) {
for(uint16_t i = 0; i < instance->amount_of_local_players; i++) { for(uint16_t i = 0; i < instance->amount_of_local_players; i++) {
// Pop input from local-player's input_queue and set the player's input to it, if we have one. // Pop input from local-player's input_queue and set the player's input to it, if we have one.
Simulation_Game_Input input = {0}; Simulation_Game_Input input = {0};
if(game_instance_pop_local_input(instance, i, &input)) { game_instance_pop_local_input(instance, i, &input);
// TODO: SS - We should probably check if this input is invalid for the snake. (pressing "go right" when going left, for example.) game_session_set_player_input(session, i, input);
// If it is invalid, the next input in the queue should be tried.
game_session_set_player_input(session, i, input);
}
} }
game_session_tick(session); game_session_tick(session);
@@ -237,8 +228,8 @@ static void state_render(Presentation_State *state) {
Color tint = (Color) { 255, 255, 255, 255 }; Color tint = (Color) { 255, 255, 255, 255 };
// Set the snake's tint based on player-index. // Set the snake's tint based on player-index.
for(uint16_t i = 0; i < sim_world->max_players; i++) { for(uint16_t i = 0; i < world->max_players; i++) {
Simulation_Player *player = &sim_world->players[i]; Game_World_Player *player = &world->players[i];
if(!player->active) { if(!player->active) {
continue; continue;
} }

View File

@@ -120,7 +120,7 @@ static void state_render(Presentation_State *state) {
} }
} }
if (GuiButton((Rectangle){ 64, 64 + (BUTTON_HEIGHT * 1), 128, BUTTON_HEIGHT }, "Cancel")) { if (GuiButton((Rectangle){ 64, 64 + (BUTTON_HEIGHT * 1), 128, BUTTON_HEIGHT }, "Cancel")) {
ctx->mode = ctx->session_settings.max_players == 1 ? Main_Menu_Mode_Home : Main_Menu_Mode_Multiplayer; ctx->mode = Main_Menu_Mode_Home;
} }
break; break;

View File

@@ -42,8 +42,8 @@ void game_session_init_default_settings(bool online, Game_Session_Settings *out_
out_settings->seed = 1337; // TODO: SS - Randomize. out_settings->seed = 1337; // TODO: SS - Randomize.
out_settings->online = online; out_settings->online = online;
out_settings->tickrate = 10.0; out_settings->tickrate = 10.0;
out_settings->level_width = 32; out_settings->level_width = 64;
out_settings->level_height = 32; out_settings->level_height = 48;
out_settings->max_players = online ? 8 : 2; out_settings->max_players = online ? 8 : 2;
} }

View File

@@ -7,17 +7,23 @@
#define MIN_LEVEL_WIDTH 8 // TODO: SS - Move these out of here. #define MIN_LEVEL_WIDTH 8 // TODO: SS - Move these out of here.
#define MIN_LEVEL_HEIGHT 8 #define MIN_LEVEL_HEIGHT 8
Game_World *game_world_create(uint32_t seed, uint8_t amount_of_food_to_spawn_initially, uint16_t level_width, uint16_t level_height) { Game_World *game_world_create(uint32_t seed, uint16_t max_players, uint16_t level_width, uint16_t level_height) {
assert(level_width >= MIN_LEVEL_WIDTH); // We should probably have failed earlier. assert(level_width >= MIN_LEVEL_WIDTH); // We should probably have failed earlier.
assert(level_height >= MIN_LEVEL_HEIGHT); assert(level_height >= MIN_LEVEL_HEIGHT);
Game_World *w = (Game_World *)calloc(1, sizeof(Game_World)); Game_World *w = (Game_World *)calloc(1, sizeof(Game_World));
assert(w != NULL); assert(w != NULL);
w->seed = seed; w->seed = seed;
random_init(&w->random_generator, w->seed); random_init(&w->random_generator, w->seed);
w->max_players = max_players;
w->players = (Game_World_Player *)calloc(w->max_players, sizeof(Game_World_Player));
assert(w->players != NULL);
w->max_entities = level_width * level_height; w->max_entities = level_width * level_height;
w->entities = (Entity *)calloc(w->max_entities, sizeof(Entity)); w->entities = (Entity *)calloc(w->max_entities, sizeof(Entity));
assert(w->entities != NULL);
// Set up entity-id queue. // Set up entity-id queue.
assert(squeue_init(&w->entity_id_queue, w->max_entities, sizeof(Entity_ID))); assert(squeue_init(&w->entity_id_queue, w->max_entities, sizeof(Entity_ID)));
@@ -28,8 +34,8 @@ Game_World *game_world_create(uint32_t seed, uint8_t amount_of_food_to_spawn_ini
grid_initialize(&w->grid, level_width, level_height); grid_initialize(&w->grid, level_width, level_height);
// Spawn initial food(s). // Spawn initial food(s). One per player.
for(uint16_t i = 0; i < amount_of_food_to_spawn_initially; i++) { for(uint16_t i = 0; i < w->max_players; i++) {
Entity_ID entity_food; Entity_ID entity_food;
uint16_t x = 0; uint16_t x = 0;
@@ -51,8 +57,15 @@ void game_world_destroy(Game_World *world) {
assert(world != NULL); assert(world != NULL);
world->seed = 0; world->seed = 0;
world->max_players = 0;
free(world->players);
world->players = NULL;
world->max_entities = 0; world->max_entities = 0;
free(world->entities); free(world->entities);
world->entities = NULL;
grid_dispose(&world->grid); grid_dispose(&world->grid);
squeue_free(&world->entity_id_queue); squeue_free(&world->entity_id_queue);
@@ -122,6 +135,57 @@ void game_world_destroy_entity(Game_World *world, Entity_ID entity_id, bool dest
} }
} }
static void kill_and_respawn_player(Game_World *world, Entity *entity) {
assert(world != NULL);
assert(entity != NULL);
// Loop over the players to find the owner of this entity.
Game_World_Player *owner_player = NULL;
for(uint16_t i = 0; i < world->max_players; i++) {
Game_World_Player *player = &world->players[i];
if(!player->active) {
continue;
}
if(player->entity_id == entity->id) {
owner_player = player;
break;
}
}
assert(owner_player != NULL);
game_world_destroy_entity(world, entity->id, true);
owner_player->entity_id = INVALID_ENTITY_ID;
{ // Respawn.
Entity_ID entity_snake_id;
uint16_t spawn_x = 0;
uint16_t spawn_y = 0;
assert(game_world_find_position_to_spawn(world, &spawn_x, &spawn_y));
assert(game_world_create_entity(
world,
Entity_Type_Snake_Head,
spawn_x, spawn_y,
&entity_snake_id
));
owner_player->entity_id = entity_snake_id;
Entity *player_entity = game_world_try_get_entity_by_id(
world,
owner_player->entity_id
);
// Set initial move-direction for player-entities.
player_entity->move_direction = game_world_choose_initial_move_direction_based_on_coords(
world,
spawn_x, spawn_y
);
}
}
void game_world_tick(Game_World *world) { void game_world_tick(Game_World *world) {
// Game-logic! :) // Game-logic! :)
@@ -133,7 +197,144 @@ void game_world_tick(Game_World *world) {
switch(entity->type) { switch(entity->type) {
case Entity_Type_Snake_Head: { case Entity_Type_Snake_Head: {
{ // Move entity
int16_t dx = 0;
int16_t dy = 0;
switch(entity->move_direction) {
case Entity_Movement_Direction_None: {
break;
}
case Entity_Movement_Direction_Up: {
dy = -1;
break;
}
case Entity_Movement_Direction_Down: {
dy = 1;
break;
}
case Entity_Movement_Direction_Right: {
dx = 1;
break;
}
case Entity_Movement_Direction_Left: {
dx = -1;
break;
}
}
if(dx != 0 || dy != 0) {
// Try moving.
// Figure out what cell we're on and what cell we want to go to.
Grid_Cell *current_cell = grid_get_cell(&world->grid, entity->x, entity->y);
assert(current_cell != NULL);
Grid_Cell *target_cell = grid_get_cell(&world->grid, entity->x + dx, entity->y + dy);
if(target_cell == NULL) {
// Target cell does not exist.
// TODO: SS - Check the session's settings.
// Maybe each session could decide whether the snake should "loop-around" or not.
// If the snake shouldn't loop around, die. For now, we say that it's not okay to loop around.
bool should_loop_around = false;
if(should_loop_around) {
// TODO: SS - Implement looping around.
}
else {
kill_and_respawn_player(world, entity);
}
}
else {
bool should_move_to_target_cell = false;
bool snake_ate = false;
if(target_cell->entity != NULL) {
// Target cell is occupied.
// Check what type of entity it is and determine what should happen.
switch(target_cell->entity->type) {
case Entity_Type_Snake_Head: {
kill_and_respawn_player(world, entity);
break;
}
case Entity_Type_Snake_Body: {
kill_and_respawn_player(world, entity);
break;
}
case Entity_Type_Food: {
// Eat!
assert(grid_try_remove_entity_from_cell(target_cell));
snake_ate = true;
should_move_to_target_cell = true;
break;
}
}
}
else {
// Target cell is unoccupied and free to enter.
should_move_to_target_cell = true;
}
if(should_move_to_target_cell) {
{ // Move snake recursively.
Entity *e = entity;
Grid_Cell *t = target_cell;
while(e != NULL) {
Grid_Cell *a = grid_get_cell(&world->grid, e->x, e->y);
assert(a != NULL);
grid_move_entity_from_cell_to_cell(a, t);
e = game_world_try_get_entity_by_id(world, e->child);
t = a;
}
}
if(snake_ate) {
Entity *e = entity;
uint16_t child_index = 0;
while(e->child != INVALID_ENTITY_ID) {
e = game_world_try_get_entity_by_id(world, e->child);
child_index += 1;
}
Entity_ID child_entity = INVALID_ENTITY_ID;
assert(game_world_create_entity(
world,
Entity_Type_Snake_Body,
e->prev_x,
e->prev_y,
&child_entity
));
assert(e->child == INVALID_ENTITY_ID);
e->child = child_entity;
{ // Spawn food.
uint16_t food_spawn_x = 0;
uint16_t food_spawn_y = 0;
if(!game_world_find_position_to_spawn(world, &food_spawn_x, &food_spawn_y)) {
printf("Failed to find a position to spawn food.\n");
}
else {
Entity_ID entity_food;
assert(game_world_create_entity(
world,
Entity_Type_Food,
food_spawn_x, food_spawn_y,
&entity_food
));
}
}
}
}
}
}
}
break; break;
} }
case Entity_Type_Snake_Body: { case Entity_Type_Snake_Body: {
@@ -150,149 +351,41 @@ void game_world_tick(Game_World *world) {
} }
} }
{ // Move entity
int16_t dx = 0;
int16_t dy = 0;
switch(entity->move_direction) {
case Entity_Movement_Direction_None: {
break;
}
case Entity_Movement_Direction_Up: {
dy = -1;
break;
}
case Entity_Movement_Direction_Down: {
dy = 1;
break;
}
case Entity_Movement_Direction_Right: {
dx = 1;
break;
}
case Entity_Movement_Direction_Left: {
dx = -1;
break;
}
}
if(dx != 0 || dy != 0) {
// Try moving.
// Figure out what cell we're on and what cell we want to go to.
Grid_Cell *current_cell = grid_get_cell(&world->grid, entity->x, entity->y);
assert(current_cell != NULL);
Grid_Cell *target_cell = grid_get_cell(&world->grid, entity->x + dx, entity->y + dy);
// { // Debug-logging.
// printf("Current cell = (x: %u, y: %u, entity: %p).\n", current_cell->x, current_cell->y, current_cell->entity);
// if(target_cell != NULL) {
// printf("Target cell = (x: %u, y: %u, entity: %p).\n", target_cell->x, target_cell->y, target_cell->entity);
// }
// else {
// printf("Target cell = NULL!\n");
// }
// }
if(target_cell == NULL) {
// Target cell does not exist.
// TODO: SS - Check the session's settings.
// Maybe each session could decide whether the snake should "loop-around" or not.
// If the snake shouldn't loop around, die.
}
else {
bool should_move_to_target_cell = false;
bool snake_ate = false;
if(target_cell->entity != NULL) {
// Target cell is occupied.
// Check what type of entity it is and determine what should happen.
switch(target_cell->entity->type) {
case Entity_Type_Snake_Head: {
// TODO: SS - Die.
break;
}
case Entity_Type_Snake_Body: {
// TODO: SS - Die.
break;
}
case Entity_Type_Food: {
// Eat!
assert(grid_try_remove_entity_from_cell(target_cell));
snake_ate = true;
should_move_to_target_cell = true;
break;
}
}
}
else {
// Target cell is unoccupied and free to enter.
should_move_to_target_cell = true;
}
if(should_move_to_target_cell) {
{ // Move snake recursively.
Entity *e = entity;
Grid_Cell *t = target_cell;
while(e != NULL) {
Grid_Cell *a = grid_get_cell(&world->grid, e->x, e->y);
assert(a != NULL);
grid_move_entity_from_cell_to_cell(a, t);
e = game_world_try_get_entity_by_id(world, e->child);
t = a;
}
}
if(snake_ate) {
Entity *e = entity;
uint16_t child_index = 0;
while(e->child != INVALID_ENTITY_ID) {
e = game_world_try_get_entity_by_id(world, e->child);
child_index += 1;
}
Entity_ID child_entity = INVALID_ENTITY_ID;
assert(game_world_create_entity(
world,
Entity_Type_Snake_Body,
e->prev_x,
e->prev_y,
&child_entity
));
assert(e->child == INVALID_ENTITY_ID);
e->child = child_entity;
{ // Spawn food.
uint16_t food_spawn_x = 0;
uint16_t food_spawn_y = 0;
if(!game_world_find_position_to_spawn(world, &food_spawn_x, &food_spawn_y)) {
printf("Failed to find a position to spawn food.\n");
}
else {
Entity_ID entity_food;
assert(game_world_create_entity(
world,
Entity_Type_Food,
food_spawn_x, food_spawn_y,
&entity_food
));
}
}
}
}
}
}
}
} }
} }
bool game_world_add_player(Game_World *world, Game_World_Player **out_gw_player) {
assert(world != NULL);
assert(out_gw_player != NULL);
for(uint16_t i = 0; i < world->max_players; i++) {
Game_World_Player *player = &world->players[i];
if(player->active) {
continue;
}
// Found free player.
player->active = true;
*out_gw_player = player;
return true;
}
return false;
}
bool game_world_remove_player(Game_World *world, Game_World_Player *player) {
assert(world != NULL);
assert(player != NULL);
game_world_destroy_entity(world, player->entity_id, true);
memset(player, 0, sizeof(Game_World_Player));
return true;
}
Entity *game_world_try_get_entity_by_id(Game_World *world, Entity_ID id) { Entity *game_world_try_get_entity_by_id(Game_World *world, Entity_ID id) {
assert(world != NULL); assert(world != NULL);

View File

@@ -6,10 +6,14 @@
#include "grid.h" #include "grid.h"
#include "shared/squeue.h" #include "shared/squeue.h"
#include "shared/random.h" #include "shared/random.h"
#include "game_world_player.h"
typedef struct { typedef struct {
uint32_t seed; uint32_t seed;
Random_Generator random_generator; Random_Generator random_generator;
Game_World_Player *players;
uint16_t max_players;
SQueue entity_id_queue; SQueue entity_id_queue;
Entity *entities; Entity *entities;
@@ -19,7 +23,7 @@ typedef struct {
} Game_World; } Game_World;
Game_World *game_world_create(uint32_t seed, uint8_t amount_of_food_to_spawn_initially, uint16_t level_width, uint16_t level_height); Game_World *game_world_create(uint32_t seed, uint16_t max_players, uint16_t level_width, uint16_t level_height);
void game_world_destroy(Game_World *world); void game_world_destroy(Game_World *world);
bool game_world_create_entity(Game_World *world, Entity_Type type, uint16_t x, uint16_t y, Entity_ID *out_entity_id); bool game_world_create_entity(Game_World *world, Entity_Type type, uint16_t x, uint16_t y, Entity_ID *out_entity_id);
@@ -27,7 +31,8 @@ void game_world_destroy_entity(Game_World *world, Entity_ID entity_id, bool dest
void game_world_tick(Game_World *world); void game_world_tick(Game_World *world);
// TODO: SS - "void game_world_spawn_player(Game_World *world, ..)" bool game_world_add_player(Game_World *world, Game_World_Player **out_gw_player);
bool game_world_remove_player(Game_World *world, Game_World_Player *player);
Entity *game_world_try_get_entity_by_id(Game_World *world, Entity_ID id); Entity *game_world_try_get_entity_by_id(Game_World *world, Entity_ID id);

View File

@@ -0,0 +1,10 @@
#ifndef GAME_WORLD_PLAYER_H
#define GAME_WORLD_PLAYER_H
typedef struct {
bool active;
Entity_ID entity_id; // The entity that this player is controlling. Can be INVALID_ENTITY_ID.
} Game_World_Player;
#endif

View File

@@ -6,11 +6,11 @@ typedef struct {
bool left; bool left;
} Simulation_Game_Input; } Simulation_Game_Input;
static inline bool simulation_input_equal(Simulation_Game_Input a, Simulation_Game_Input b) { static inline bool simulation_input_empty(Simulation_Game_Input input) {
if(a.right != b.right) { if(input.right) {
return false; return false;
} }
if(a.left != b.left) { if(input.left) {
return false; return false;
} }

View File

@@ -6,8 +6,10 @@
typedef struct { typedef struct {
bool active; bool active;
Entity_ID entity_id; Game_World_Player *game_world_player;
Simulation_Game_Input input; Simulation_Game_Input input;
// score, name etc. // score, name etc.
} Simulation_Player; } Simulation_Player;

View File

@@ -14,8 +14,7 @@ Simulation_World simulation_create_world(uint32_t seed, bool online, uint8_t max
w.online = online; w.online = online;
uint16_t amount_of_food_to_spawn_initially = max_players; // NOTE: SS - One per player. Might change and/or be customizable. w.game_world = game_world_create(seed, max_players, width, height);
w.game_world = game_world_create(seed, amount_of_food_to_spawn_initially, width, height);
assert(w.game_world != NULL); assert(w.game_world != NULL);
w.match_state = Simulation_Match_State_Waiting_To_Start; w.match_state = Simulation_Match_State_Waiting_To_Start;
@@ -50,6 +49,9 @@ void simulation_world_tick(Simulation_World *simulation_world) {
// printf("TICK: %lu.\n", simulation_world->tick); // printf("TICK: %lu.\n", simulation_world->tick);
Game_World *gw = simulation_world->game_world;
assert(gw != NULL);
for(uint16_t i = 0; i < simulation_world->command_count; i++) { for(uint16_t i = 0; i < simulation_world->command_count; i++) {
Simulation_Command *cmd = &simulation_world->commands[i]; Simulation_Command *cmd = &simulation_world->commands[i];
// printf("Command type: %i, player id: %i.\n", cmd->type, cmd->player_id); // printf("Command type: %i, player id: %i.\n", cmd->type, cmd->player_id);
@@ -63,9 +65,10 @@ void simulation_world_tick(Simulation_World *simulation_world) {
assert(!player->active); assert(!player->active);
player->active = true; player->active = true;
Game_World *gw = simulation_world->game_world; assert(game_world_add_player(gw, &player->game_world_player));
assert(gw != NULL);
Game_World_Player *gwp = player->game_world_player;
uint16_t spawn_x = 0; uint16_t spawn_x = 0;
uint16_t spawn_y = 0; uint16_t spawn_y = 0;
assert(game_world_find_position_to_spawn(gw, &spawn_x, &spawn_y)); assert(game_world_find_position_to_spawn(gw, &spawn_x, &spawn_y));
@@ -74,10 +77,10 @@ void simulation_world_tick(Simulation_World *simulation_world) {
gw, gw,
Entity_Type_Snake_Head, Entity_Type_Snake_Head,
spawn_x, spawn_y, spawn_x, spawn_y,
&player->entity_id &gwp->entity_id
)); ));
Entity *player_entity = game_world_try_get_entity_by_id(gw, player->entity_id); Entity *player_entity = game_world_try_get_entity_by_id(gw, gwp->entity_id);
// Set initial move-direction for player-entities. // Set initial move-direction for player-entities.
player_entity->move_direction = game_world_choose_initial_move_direction_based_on_coords( player_entity->move_direction = game_world_choose_initial_move_direction_based_on_coords(
@@ -92,8 +95,7 @@ void simulation_world_tick(Simulation_World *simulation_world) {
assert(player->active); assert(player->active);
player->active = false; player->active = false;
game_world_destroy_entity(simulation_world->game_world, player->entity_id, true); assert(game_world_remove_player(gw, player->game_world_player));
break; break;
} }
case Simulation_Command_Type_Set_Player_Input: { case Simulation_Command_Type_Set_Player_Input: {
@@ -136,8 +138,14 @@ void simulation_world_tick(Simulation_World *simulation_world) {
// Loop over all players in the simulation. // Loop over all players in the simulation.
for(uint16_t i = 0; i < simulation_world->max_players; i++) { for(uint16_t i = 0; i < simulation_world->max_players; i++) {
Simulation_Player *player = &simulation_world->players[i]; Simulation_Player *sim_player = &simulation_world->players[i];
if(!player->active) { if(!sim_player->active) {
continue;
}
Game_World_Player *gw_player = sim_player->game_world_player;
assert(gw_player != NULL);
if(!gw_player->active) {
continue; continue;
} }
@@ -148,14 +156,17 @@ void simulation_world_tick(Simulation_World *simulation_world) {
// player->input.right, player->input.left // player->input.right, player->input.left
// ); // );
Entity *player_entity = game_world_try_get_entity_by_id(gw, player->entity_id); Entity *player_entity = game_world_try_get_entity_by_id(gw, gw_player->entity_id);
assert(player_entity != NULL); if(player_entity == NULL) {
// No entity to control. No entity to apply input on.
continue;
}
// Snakes can't go backwards. They have to turn. // Snakes can't go backwards. They have to turn.
// We only have input 'right' and 'up'. // We only have input 'right' and 'up'.
Simulation_Game_Input input = player->input; Simulation_Game_Input input = sim_player->input;
// Turn relative to the player's current move_direction. // Turn relative to the player's current move_direction.
switch(player_entity->move_direction) { switch(player_entity->move_direction) {
case Entity_Movement_Direction_None: { case Entity_Movement_Direction_None: {