Compare commits

..

16 Commits

Author SHA1 Message Date
0748b75d66 Not finished. Need to commit. 2025-12-23 15:10:14 +01:00
2b381f7151 Merge branch 'main' of https://git.desond.com/sam/snejk 2025-12-18 16:04:19 +01:00
cd507c544d Added Game_World_Player and dying/respawning. 2025-12-18 16:04:04 +01:00
2a999a12bf Added Game_World_Player and dying/respawning. 2025-12-18 15:59:14 +01:00
0074ecd57b Changed player-movement. From UP, DOWN, LEFT, RIGHT to LEFT, RIGHT. Turn relative to your snake's head. 2025-12-18 14:09:04 +01:00
5e6b042921 Spawn a new food when one was eaten. Initially, there's one food per player spawned. 2025-12-18 13:50:27 +01:00
49732d8b27 Moved game-logic to game_world_tick(..) 2025-12-18 13:33:08 +01:00
c1f43fdc61 Try to get a good position to spawn at. Also gets a good initial move-direction based on where you spawn. 2025-12-18 12:57:06 +01:00
199afff820 Added 'destroy_children' parameter to game_world_destroy_entity(). 2025-12-18 12:10:25 +01:00
4e807afa3c Multiple local inputs. You can now play with your 3 friends locally, fighting over the same keyboard. 2025-12-16 17:29:02 +01:00
d0d1a470b9 Merge branch 'main' of https://git.desond.com/sam/snejk 2025-12-16 13:00:15 +01:00
fb885c0fc3 More player-tints. Escape to quit when in main-menu. 2025-12-16 12:59:59 +01:00
76e5b3b282 Properly shut down the networking API. 2025-12-16 12:43:47 +01:00
167c8a3ce3 Change snake's tint based on player-id. 2025-12-16 12:43:23 +01:00
sam
d0727955d5 Update README.md 2025-12-16 11:40:33 +00:00
9dc13e3feb Fixed mistake that crashes the game if you go to play singleplayer through the menu. 2025-12-16 12:30:14 +01:00
22 changed files with 837 additions and 328 deletions

View File

@@ -19,9 +19,7 @@ LIB_DIR := $(abspath libs/$(PLATFORM))
CFLAGS += -I$(LIB_DIR)
LDFLAGS += -L$(LIB_DIR)
ifeq ($(PLATFORM), linux)
LIBS += -lraylib -lm -lraygui
endif
LIBS += -lraylib -lm -lraygui
SRC := $(shell find -L $(SRC_DIR) -type f -name '*.c')

View File

@@ -1,5 +1,5 @@
# snejk
My take on the classic game "Snake". Can be played solo or with friends online.
My take on the classic game "Snake". Can be played solo or with friends locally/online.
So far, only Linux is supported.

View File

@@ -2,20 +2,27 @@
#include <assert.h>
#define INPUT_QUEUE_CAPACITY 8
void game_instance_init(Game_Instance *instance) {
instance->local_player_id = 0xFFFF;
// TODO: SS - Init input-queue.
instance->amount_of_local_players = 0;
instance->simulation_accumulator = 0.0f; // TODO: SS - Should probably be moved to the Ingame context.
instance->game_session = NULL;
memset(&instance->game_networking, 0, sizeof(Game_Networking));
squeue_init(&instance->local_input_queue, 4, sizeof(Simulation_Game_Input));
for(uint8_t i = 0; i < MAX_LOCAL_PLAYERS; i++) {
Locally_Controlled_Player *locally_controlled_player = &instance->locally_controlled_players[i];
squeue_init(
&locally_controlled_player->input_queue,
MAX_INPUTS_IN_QUEUE_PER_LOCAL_PLAYER,
sizeof(Simulation_Game_Input)
);
}
}
void game_instance_dispose(Game_Instance *instance) {
squeue_free(&instance->local_input_queue);
for(uint8_t i = 0; i < MAX_LOCAL_PLAYERS; i++) {
Locally_Controlled_Player *locally_controlled_player = &instance->locally_controlled_players[i];
squeue_free(&locally_controlled_player->input_queue);
}
}
bool game_instance_host_session(Game_Instance *instance, const Game_Session_Settings settings) {
@@ -33,7 +40,7 @@ bool game_instance_host_session(Game_Instance *instance, const Game_Session_Sett
return false;
}
instance->game_session = (Game_Session *)calloc(1, sizeof(Game_Session)); // LEAK
instance->game_session = (Game_Session *)calloc(1, sizeof(Game_Session));
if(settings.online) {
if(instance->game_networking.api != NULL) {
@@ -46,19 +53,31 @@ bool game_instance_host_session(Game_Instance *instance, const Game_Session_Sett
uint16_t host_player_id = 0xFFFF;
if(!networking_try_host(&instance->game_networking, instance->game_session, settings, &host_player_id)) {
printf("Failed to host session.\n");
free(instance->game_session);
instance->game_session = NULL;
return false;
}
instance->local_player_id = host_player_id;
game_instance_clear_local_players(instance);
assert(game_instance_setup_local_player(instance, host_player_id));
}
else {
if(settings.max_players > MAX_LOCAL_PLAYERS) {
printf("Failed. Max amount of players (when playing locally) is %u. Max amount of players in the settings is %u.\n", MAX_LOCAL_PLAYERS, settings.max_players);
free(instance->game_session);
instance->game_session = NULL;
return false;
}
game_session_init(instance->game_session, settings);
// When playing locally/offline, create the player(s) here.
game_instance_clear_local_players(instance);
for(uint32_t i = 0; i < settings.max_players; i++) {
// When playing locally/offline, create the player(s) here.
assert(game_session_create_player(instance->game_session, &instance->local_player_id)); // TODO: SS - Fix me! Always assigning the newest player to be the active 'local_player_id' which isn't correct.
uint16_t player_id = 0xFFFF;
assert(game_session_create_player(instance->game_session, &player_id));
assert(game_instance_setup_local_player(instance, player_id));
}
printf("Created %u local players.\n", settings.max_players);
@@ -82,10 +101,11 @@ bool game_instance_join_session(Game_Instance *instance, const char *session_id)
return false;
}
instance->game_session = (Game_Session *)calloc(1, sizeof(Game_Session)); // LEAK
instance->game_session = (Game_Session *)calloc(1, sizeof(Game_Session));
if(!networking_try_join(&instance->game_networking, instance->game_session, session_id)) {
free(instance->game_session);
instance->game_session = NULL;
return false;
}
@@ -114,13 +134,44 @@ void game_instance_stop_session(Game_Instance *instance) {
instance->game_session = NULL;
}
bool game_instance_push_local_input(Game_Instance *instance, Simulation_Game_Input input) {
return squeue_push(&instance->local_input_queue, (void *)&input);
void game_instance_clear_local_players(Game_Instance *instance) {
instance->amount_of_local_players = 0;
}
bool game_instance_pop_local_input(Game_Instance *instance, Simulation_Game_Input *out_input) {
bool game_instance_setup_local_player(Game_Instance *instance, uint16_t player_id) {
if(instance->amount_of_local_players >= MAX_LOCAL_PLAYERS) {
return false;
}
Locally_Controlled_Player *locally_controlled_player = &instance->locally_controlled_players[instance->amount_of_local_players];
assert(locally_controlled_player != NULL);
instance->amount_of_local_players += 1;
locally_controlled_player->player_id = player_id;
squeue_clear(&locally_controlled_player->input_queue);
return true;
}
bool game_instance_push_local_input(Game_Instance *instance, uint8_t local_player_index, Simulation_Game_Input input) {
assert(local_player_index < MAX_LOCAL_PLAYERS);
assert(local_player_index < instance->amount_of_local_players);
Locally_Controlled_Player *locally_controlled_player = &instance->locally_controlled_players[local_player_index];
assert(locally_controlled_player != NULL);
return squeue_push(&locally_controlled_player->input_queue, (void *)&input);
}
bool game_instance_pop_local_input(Game_Instance *instance, uint8_t local_player_index, Simulation_Game_Input *out_input) {
assert(local_player_index < MAX_LOCAL_PLAYERS);
assert(local_player_index < instance->amount_of_local_players);
Locally_Controlled_Player *locally_controlled_player = &instance->locally_controlled_players[local_player_index];
assert(locally_controlled_player != NULL);
Simulation_Game_Input input = {0};
if(!squeue_pop(&instance->local_input_queue, (void *)&input)) {
if(!squeue_pop(&locally_controlled_player->input_queue, (void *)&input)) {
return false;
}

View File

@@ -8,9 +8,17 @@
#include "shared/squeue.h"
#define MAX_LOCAL_PLAYERS 4
#define MAX_INPUTS_IN_QUEUE_PER_LOCAL_PLAYER 4
typedef struct {
uint16_t local_player_id; // TODO: SS - We need to modify this to support local multiplayer.
SQueue local_input_queue;
uint16_t player_id;
SQueue input_queue;
} Locally_Controlled_Player;
typedef struct {
Locally_Controlled_Player locally_controlled_players[MAX_LOCAL_PLAYERS];
uint8_t amount_of_local_players;
double simulation_accumulator;
@@ -23,10 +31,12 @@ void game_instance_dispose(Game_Instance *instance);
bool game_instance_host_session(Game_Instance *instance, const Game_Session_Settings settings);
bool game_instance_join_session(Game_Instance *instance, const char *session_id);
void game_instance_stop_session(Game_Instance *instance);
bool game_instance_push_local_input(Game_Instance *instance, Simulation_Game_Input input);
bool game_instance_pop_local_input(Game_Instance *instance, Simulation_Game_Input *out_input);
void game_instance_clear_local_players(Game_Instance *instance);
bool game_instance_setup_local_player(Game_Instance *instance, uint16_t player_id);
bool game_instance_push_local_input(Game_Instance *instance, uint8_t local_player_index, Simulation_Game_Input input);
bool game_instance_pop_local_input(Game_Instance *instance, uint8_t local_player_index, Simulation_Game_Input *out_input);
#endif

View File

@@ -26,7 +26,7 @@ static void state_enter(Presentation_State *state) {
.offset = (Vector2) { 0, 0 },
.target = (Vector2) { 0, 0 },
.rotation = 0.0f,
.zoom = 3.0f
.zoom = 2.0f
};
// TODO: SS - Maybe put the textures in an array and index them using an enum?
@@ -43,6 +43,39 @@ static void state_enter(Presentation_State *state) {
assert(IsTextureValid(ctx->texture_snake_body));
}
static Simulation_Game_Input gather_input_for_local_player(uint8_t local_player_index) {
switch(local_player_index) {
case 0: {
return (Simulation_Game_Input) {
.right = IsKeyPressed(KEY_D),
.left = IsKeyPressed(KEY_A)
};
}
case 1: {
return (Simulation_Game_Input) {
.right = IsKeyPressed(KEY_RIGHT),
.left = IsKeyPressed(KEY_LEFT)
};
}
case 2: {
return (Simulation_Game_Input) {
.right = IsKeyPressed(KEY_L),
.left = IsKeyPressed(KEY_J)
};
}
case 3: {
return (Simulation_Game_Input) {
.right = IsKeyPressed(KEY_H),
.left = IsKeyPressed(KEY_F)
};
}
default: {
assert(false);
return (Simulation_Game_Input) {0};
}
}
}
static void state_tick(Presentation_State *state) {
Presentation_State_Ingame_Context *ctx = (Presentation_State_Ingame_Context *)state->context;
(void)ctx;
@@ -53,16 +86,23 @@ static void state_tick(Presentation_State *state) {
assert(session != NULL);
{ // Push local input to queue.
Simulation_Game_Input input = (Simulation_Game_Input) { // NOTE: SS - This needs to be slightly modified to support N local players.
.up = IsKeyDown(KEY_UP) || IsKeyDown(KEY_W),
.down = IsKeyDown(KEY_DOWN) || IsKeyDown(KEY_S),
.right = IsKeyDown(KEY_RIGHT) || IsKeyDown(KEY_D),
.left = IsKeyDown(KEY_LEFT) || IsKeyDown(KEY_A)
};
if(instance->amount_of_local_players == 1) { // If playing alone (either singleplayer or online-multiplayer), more inputs are allowed.
Simulation_Game_Input input = (Simulation_Game_Input) {
.right = IsKeyPressed(KEY_RIGHT) || IsKeyPressed(KEY_D),
.left = IsKeyPressed(KEY_LEFT) || IsKeyPressed(KEY_A)
};
if(!simulation_input_equal(input, ctx->prev_local_input)) {
game_instance_push_local_input(instance, input);
ctx->prev_local_input = input;
if(!simulation_input_empty(input)) {
game_instance_push_local_input(instance, 0, input);
}
}
else if(instance->amount_of_local_players > 1) {
for(uint8_t i = 0; i < instance->amount_of_local_players; i++) {
Simulation_Game_Input input = gather_input_for_local_player(i);
if(!simulation_input_empty(input)) {
game_instance_push_local_input(instance, i, input);
}
}
}
}
@@ -72,24 +112,26 @@ static void state_tick(Presentation_State *state) {
const double sim_dt = 1.0f / session->settings.tickrate;
while (ctx->simulation_accumulator >= sim_dt) {
// Pop input from instance's local_input_queue and set the local player's input to it, if we have one.
Simulation_Game_Input input = {0};
if(game_instance_pop_local_input(instance, &input)) {
// TODO: SS - We should probably check if this input is invalid for the snake. (pressing "go right" when going left, for example.)
// If it is invalid, the next input in the queue should be tried.
game_session_set_player_input(session, instance->local_player_id, input);
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.
Simulation_Game_Input input = {0};
game_instance_pop_local_input(instance, i, &input);
game_session_set_player_input(session, i, input);
}
game_session_tick(session);
ctx->simulation_accumulator -= sim_dt;
}
// TODO: SS - I believe that this is where we should send game-messages to the clients and the host.
{ // TEMP: SS
if(IsKeyPressed(KEY_TAB)) {
ctx->debug_draw_session_details = !ctx->debug_draw_session_details;
}
if(IsKeyPressed(KEY_ESCAPE)) {
// TODO: SS - Show pause-menu instead.
presentation_state_machine_go_to(&presentation_state_main_menu);
}
}
@@ -99,6 +141,38 @@ uint32_t floor_texture_variant(uint32_t x, uint32_t y, uint32_t seed, uint32_t n
return (wang_hash(x * 73856093u ^ y * 19349663u ^ seed)) % num_variants;
}
Color get_tint_for_player_id(uint16_t player_id) { // TODO: SS - Could get values from 'the active color-palette'.
switch(player_id) {
case 0: {
return (Color) { 255, 135, 102, 255 };
}
case 1: {
return (Color) { 230, 204, 138, 255 };
}
case 3: {
return (Color) { 240, 236, 226, 255 };
}
case 4: {
return (Color) { 77, 106, 148, 255 };
}
case 5: {
return (Color) { 36, 43, 74, 255 };
}
case 6: {
return (Color) { 153, 92, 149, 255 };
}
case 7: {
return (Color) { 245, 79, 79, 255 };
}
case 8: {
return (Color) { 53, 121, 133, 255 };
}
default: {
return (Color) { 82, 33, 110, 255 };
}
}
}
#define SHADOW_ALPHA 64
static void state_render(Presentation_State *state) {
@@ -151,7 +225,21 @@ static void state_render(Presentation_State *state) {
switch(entity->type) {
case Entity_Type_Snake_Head: {
Color tint = (Color) { 255, 135, 102, 255 }; // TODO: SS - Get tint based on player ID.
Color tint = (Color) { 255, 255, 255, 255 };
// Set the snake's tint based on player-index.
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) {
tint = get_tint_for_player_id(i); // NOTE: SS - It's more like player-index. This needs to change if they're not sorted (but they are at the moment and I don't really plan on switching that up.)
break;
}
}
Vector2 origin = (Vector2) { GRID_CELL_SIZE/2, GRID_CELL_SIZE/2 };
Entity *e = entity;
@@ -276,6 +364,9 @@ static void state_render(Presentation_State *state) {
}
}
// TODO: SS - Let the main-camera follow the entity you're controlling?
// TODO: SS - Switch on 'sim_world->match_state'.
if(ctx->debug_draw_session_details) {
@@ -313,7 +404,7 @@ static void state_render(Presentation_State *state) {
networking->is_host ? "true" : "false",
networking->session_id,
networking->local_client_id,
instance->local_player_id,
instance->locally_controlled_players[0].player_id,
game_session_get_amount_of_active_players(session), sim_world->max_players
);
}

View File

@@ -10,7 +10,6 @@
typedef struct {
Game_Instance *game_instance;
double simulation_accumulator;
Simulation_Game_Input prev_local_input;
Camera2D main_camera;

View File

@@ -14,37 +14,48 @@ static void state_enter(Presentation_State *state) {
(void)ctx;
ctx->mode = Main_Menu_Mode_Home;
SetExitKey(KEY_ESCAPE);
}
static void state_tick(Presentation_State *state) {
Presentation_State_Main_Menu_Context *ctx = (Presentation_State_Main_Menu_Context *)state->context;
{ // DEBUG
if(IsKeyPressed(KEY_P)) {
printf("Shortcut to singleplayer.\n");
game_session_init_default_settings(false, &ctx->session_settings);
ctx->session_settings.max_players = 1;
switch(ctx->mode) {
case Main_Menu_Mode_Home: {
{ // DEBUG
if(IsKeyPressed(KEY_P)) {
printf("Shortcut to singleplayer.\n");
game_session_init_default_settings(false, &ctx->session_settings);
ctx->session_settings.max_players = 1;
if(game_instance_host_session(ctx->game_instance, ctx->session_settings)) {
ctx->ingame_ctx->game_instance = ctx->game_instance;
presentation_state_machine_go_to(&presentation_state_ingame);
}
else {
printf("Failed to play.\n");
if(game_instance_host_session(ctx->game_instance, ctx->session_settings)) {
ctx->ingame_ctx->game_instance = ctx->game_instance;
presentation_state_machine_go_to(&presentation_state_ingame);
}
else {
printf("Failed to play.\n");
}
}
else if(IsKeyPressed(KEY_L)) {
printf("Shortcut to local multiplayer (2 players).\n");
game_session_init_default_settings(false, &ctx->session_settings);
ctx->session_settings.max_players = 2;
if(game_instance_host_session(ctx->game_instance, ctx->session_settings)) {
ctx->ingame_ctx->game_instance = ctx->game_instance;
presentation_state_machine_go_to(&presentation_state_ingame);
}
else {
printf("Failed to play.\n");
}
}
}
break;
}
else if(IsKeyPressed(KEY_L)) {
printf("Shortcut to local multiplayer (2 players).\n");
game_session_init_default_settings(false, &ctx->session_settings);
ctx->session_settings.max_players = 2;
if(game_instance_host_session(ctx->game_instance, ctx->session_settings)) {
ctx->ingame_ctx->game_instance = ctx->game_instance;
presentation_state_machine_go_to(&presentation_state_ingame);
}
else {
printf("Failed to play.\n");
}
default: {
break;
}
}
}
@@ -75,8 +86,6 @@ static void state_render(Presentation_State *state) {
case Main_Menu_Mode_Singleplayer: {
ctx->mode = Main_Menu_Mode_Game_Setup;
game_session_init_default_settings(false, &ctx->session_settings);
ctx->ingame_ctx->game_instance = ctx->game_instance;
presentation_state_machine_go_to(&presentation_state_ingame);
break;
}
@@ -89,12 +98,7 @@ static void state_render(Presentation_State *state) {
ctx->mode = Main_Menu_Mode_Multiplayer_Join;
memset(&ctx->session_id_input, 0, sizeof(ctx->session_id_input));
ctx->session_id_input[0] = 'A';
ctx->session_id_input[1] = 'B';
ctx->session_id_input[2] = 'C';
ctx->session_id_input[3] = 'D';
ctx->session_id_input[4] = 'E';
ctx->session_id_input[5] = 'F';
// snprintf(&ctx->session_id_input[0], sizeof(ctx->session_id_input) + 1, "ABCDEF");
}
if (GuiButton((Rectangle){ 64, 64 + (BUTTON_HEIGHT * 2), 128, BUTTON_HEIGHT }, "Back to Main Menu")) {
ctx->mode = Main_Menu_Mode_Home;
@@ -125,7 +129,7 @@ static void state_render(Presentation_State *state) {
}
}
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;
@@ -136,7 +140,7 @@ static void state_render(Presentation_State *state) {
int pressed_button_index = GuiTextInputBox(
(Rectangle) { 64, 64, 256, 128 }, // Bounds
"Session-ID", // Title
NULL, // Message
"6 characters expected. (ABCDEF)", // Message
"Join", // Buttons
&ctx->session_id_input[0], // Text
sizeof(ctx->session_id_input) + 1, // Text max size
@@ -170,6 +174,8 @@ static void state_render(Presentation_State *state) {
static void state_exit(Presentation_State *state) {
(void)state;
printf("Exited main menu\n");
SetExitKey(KEY_NULL);
}
Presentation_State presentation_state_main_menu;

View File

@@ -43,7 +43,7 @@ void game_session_init_default_settings(bool online, Game_Session_Settings *out_
out_settings->online = online;
out_settings->tickrate = 10.0;
out_settings->level_width = 64;
out_settings->level_height = 32;
out_settings->level_height = 48;
out_settings->max_players = online ? 8 : 2;
}

View File

@@ -392,7 +392,7 @@ int mp_api_join(MultiplayerApi *api,
return joinAccepted ? MP_API_OK : MP_API_ERR_REJECTED;
}
int mp_api_game(MultiplayerApi *api, json_t *data, const char* destination) {
int mp_api_game(MultiplayerApi *api, json_t *data, const char* target_client_id) {
if (!api || !data) return MP_API_ERR_ARGUMENT;
if (api->sockfd < 0 || !api->session_id) return MP_API_ERR_STATE;
@@ -403,8 +403,8 @@ int mp_api_game(MultiplayerApi *api, json_t *data, const char* destination) {
json_object_set_new(root, "session", json_string(api->session_id));
json_object_set_new(root, "cmd", json_string("game"));
if(destination)
json_object_set_new(root, "destination", json_string(destination));
if(target_client_id)
json_object_set_new(root, "target_client_id", json_string(target_client_id));
json_t *data_copy;
if (json_is_object(data)) {
@@ -612,7 +612,7 @@ static void process_line(MultiplayerApi *api, const char *line) {
}
if (strcmp(cmd, "joined") != 0 &&
strcmp(cmd, "leaved") != 0 &&
strcmp(cmd, "left") != 0 &&
strcmp(cmd, "game") != 0) {
json_decref(root);
return;

View File

@@ -67,7 +67,7 @@ int mp_api_join(MultiplayerApi *api,
json_t **out_data);
/* Skickar ett "game"meddelande med godtycklig JSONdata till sessionen. */
int mp_api_game(MultiplayerApi *api, json_t *data, const char* destination);
int mp_api_game(MultiplayerApi *api, json_t *data, const char* target_client_id);
/* Registrerar en lyssnare för inkommande events.
Returnerar ett positivt listenerID, eller 1 vid fel. */

View File

@@ -17,7 +17,12 @@ static void listen_callback(
(void)data;
printf("#%li :: Event: '%s' from '%s'.\n", messsage_id, event, client_id);
printf("%s * #%li :: Event: '%s' from '%s'.\n",
networking->is_host ? "Host" : "Client",
messsage_id,
event,
client_id
);
assert(networking != NULL);
@@ -35,8 +40,17 @@ static void listen_callback(
// TODO: SS - Send a 'game' message to the joinee with information about the session?
printf("Added client-id '%s' (player-index: %u) to the map.\n", client_id, player_index);
{ // TEMP: SS - Testing to see if the joinee gets a message.
json_t *gameData = json_object();
json_object_set_new(gameData, "temp", json_integer(4));
mp_api_game(networking->api, gameData, NULL);
json_decref(gameData);
}
}
else if(strcmp(event, "leaved") == 0) {
else if(strcmp(event, "left") == 0) {
// TODO: SS - Index the hashmap using 'client_id' as the key and attempt to get the
// player-index for that client. Run 'game_session_destroy_player()'.
@@ -82,11 +96,22 @@ static inline bool setup_api(Game_Networking *networking) {
return true;
}
static inline bool shutdown_api(Game_Networking *networking) {
if(networking->api == NULL) {
return false;
}
mp_api_destroy(networking->api);
networking->api = NULL;
return true;
}
static bool start_listening(Game_Networking *networking) {
assert(networking != NULL);
assert(networking->api != NULL);
int listener_id = mp_api_listen(networking->api, listen_callback, (void *)networking); // TODO: SS - Change context.
int listener_id = mp_api_listen(networking->api, listen_callback, (void *)networking);
if(listener_id < 0) {
return false;
}
@@ -118,19 +143,35 @@ bool networking_try_host(Game_Networking *networking, Game_Session *session, Gam
return false;
}
json_t *payload = json_object();
json_object_set_new(payload, "test", json_integer(settings.max_players));
json_t *data = json_object();
json_object_set_new(data, "name", json_string("My session")); // TODO: SS - Use 'settings.name'.
json_object_set_new(data, "private", json_boolean(false)); // TODO: SS - Use 'settings.private' or '!settings.public'.
json_object_set_new(data, "maxClients", json_integer(settings.max_players));
json_object_set_new(data, "payload", payload);
char *session_id = NULL;
char *local_client_id = NULL;
json_t *response_data = NULL;
int host_result = mp_api_host(
networking->api,
NULL, // TODO: SS - Send data to server that contains the game-session's settings.
data,
&session_id,
&local_client_id,
&response_data
);
if(host_result != MP_API_OK) {
printf("Failed to host; Got result: %i.\n", host_result);
// TODO: SS - Shutdown API.
shutdown_api(networking);
if(response_data != NULL) {
char *response = json_dumps(response_data, 0);
printf("Response: '%s'.\n", response);
free(response);
}
return false;
}
@@ -180,9 +221,10 @@ bool networking_try_join(Game_Networking *networking, Game_Session *session, con
printf("Trying to join session using code: '%s' ...\n", session_id);
char *result_session_id;
char *local_client_id;
json_t *received_json;
char *result_session_id = NULL;
char *local_client_id = NULL;
json_t *received_json = NULL;
int join_result = mp_api_join(
networking->api, // API.
session_id, // Session-code.
@@ -192,37 +234,70 @@ bool networking_try_join(Game_Networking *networking, Game_Session *session, con
&received_json // Received JSON-data.
);
if(join_result != MP_API_OK) {
// TODO: SS - Shutdown API.
shutdown_api(networking);
printf("Failed to join; Got result: %i.\n", join_result);
return false;
}
assert(strcmp(result_session_id, session_id) == 0);
printf("Joined session '%s', local_client_id is '%s'.\n", result_session_id, local_client_id);
networking->is_host = true;
networking->is_host = false;
networking->game_session = session;
networking->session_id = result_session_id;
networking->local_client_id = local_client_id;
assert(start_listening(networking));
map_init(&networking->client_id_to_player_index);
// TODO: SS - Decide how to network this..
// Should we just receive the session's settings as well as a long list of commands of what has happened throughout the match (so far) - at least one per tick?
// Or, should this client receive the current state of the session?
if(received_json != NULL) {
// TODO: SS - Read this json because it might contain valuable information.
json_decref(received_json);
}
// TODO: SS - Stop listening.
assert(start_listening(networking));
// TODO: SS - Set up session based on what we got in the json.
// game_session_init(
// session,
// settings
// );
Game_Session_Settings settings;
memset(&settings, 0, sizeof(Game_Session_Settings));
settings.online = true;
{ // TEMP: SS - Get values from json.
settings.seed = 1337; // TODO: SS - Get from json.
settings.level_width = 64; // TODO: SS - Get from json.
settings.level_height = 48; // TODO: SS - Get from json.
settings.tickrate = 10.0f; // TODO: SS - Get from json.
settings.max_players = 8; // TODO: SS - Get from json.
}
if(received_json != NULL) {
// TODO: SS - Read this json. It should contain valuable information about the session.
printf("received_json is NOT NULL.\n");
char *recv_str = json_dumps(received_json, 0);
printf("Received JSON: '%s'.\n", recv_str);
printf("trying to get maxClients.\n");
json_t* max_clients_json = json_object_get(received_json, "maxClients");
if(max_clients_json != NULL) {
printf("found maxClients.\n");
if(json_is_integer(max_clients_json)) {
printf("maxClients is integer.\n");
long max_clients = json_integer_value(max_clients_json);
printf("Max clients is: '%lu'.\n", max_clients);
}
else {
printf("maxClients is NOT an integer.\n");
}
}
json_decref(received_json);
}
else {
printf("received_json is NULL.\n");
}
game_session_init(
session,
settings
);
return true;
}
@@ -242,8 +317,7 @@ bool networking_stop(Game_Networking *networking) {
assert(stop_listening(networking));
networking->listener_id = 0;
mp_api_destroy(networking->api);
networking->api = NULL;
shutdown_api(networking);
networking->game_session = NULL;
networking->is_host = false;

View File

@@ -25,7 +25,7 @@ typedef enum {
typedef struct {
bool active;
// TODO: SS - Maybe add an ID here.
Entity_ID id;
Entity_Type type;

View File

@@ -7,17 +7,23 @@
#define MIN_LEVEL_WIDTH 8 // TODO: SS - Move these out of here.
#define MIN_LEVEL_HEIGHT 8
Game_World *game_world_create(uint32_t seed, 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_height >= MIN_LEVEL_HEIGHT);
Game_World *w = (Game_World *)calloc(1, sizeof(Game_World));
assert(w != NULL);
w->seed = 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->entities = (Entity *)calloc(w->max_entities, sizeof(Entity));
assert(w->entities != NULL);
// Set up entity-id queue.
assert(squeue_init(&w->entity_id_queue, w->max_entities, sizeof(Entity_ID)));
@@ -28,17 +34,20 @@ Game_World *game_world_create(uint32_t seed, uint16_t level_width, uint16_t leve
grid_initialize(&w->grid, level_width, level_height);
{ // TEMP: SS - Testing ..
for(uint16_t i = 0; i < 8; i++) {
Entity_ID entity_food;
assert(game_world_create_entity(
w,
Entity_Type_Food,
(uint16_t)random_u32_range(&w->random_generator, 0, level_width),
(uint16_t)random_u32_range(&w->random_generator, 0, level_height),
&entity_food
));
}
// Spawn initial food(s). One per player.
for(uint16_t i = 0; i < w->max_players; i++) {
Entity_ID entity_food;
uint16_t x = 0;
uint16_t y = 0;
assert(game_world_find_position_to_spawn(w, &x, &y));
assert(game_world_create_entity(
w,
Entity_Type_Food,
x, y,
&entity_food
));
}
return w;
@@ -48,8 +57,15 @@ void game_world_destroy(Game_World *world) {
assert(world != NULL);
world->seed = 0;
world->max_players = 0;
free(world->players);
world->players = NULL;
world->max_entities = 0;
free(world->entities);
world->entities = NULL;
grid_dispose(&world->grid);
squeue_free(&world->entity_id_queue);
@@ -78,6 +94,7 @@ bool game_world_create_entity(Game_World *world, Entity_Type type, uint16_t x, u
world->entities[id] = (Entity) {
.active = true,
.id = id,
.type = type,
.child = INVALID_ENTITY_ID
};
@@ -90,23 +107,285 @@ bool game_world_create_entity(Game_World *world, Entity_Type type, uint16_t x, u
return true;
}
void game_world_destroy_entity(Game_World *world, Entity_ID entity_id) {
void game_world_destroy_entity(Game_World *world, Entity_ID entity_id, bool destroy_children) {
assert(world != NULL);
assert(entity_id != INVALID_ENTITY_ID);
Entity *entity = game_world_try_get_entity_by_id(
world,
entity_id
);
assert(entity != NULL);
entity->active = false;
Entity_ID id_to_remove = entity_id;
Grid_Cell *cell = grid_get_cell(&world->grid, entity->x, entity->y);
assert(cell != NULL);
assert(grid_try_remove_entity_from_cell(cell));
while(id_to_remove != INVALID_ENTITY_ID) {
Entity *e = game_world_try_get_entity_by_id(
world,
id_to_remove
);
assert(e != NULL);
squeue_push(&world->entity_id_queue, (void *)&entity_id);
Grid_Cell *cell = grid_get_cell(&world->grid, e->x, e->y);
assert(cell != NULL);
assert(grid_try_remove_entity_from_cell(cell));
squeue_push(&world->entity_id_queue, (void *)&e->id);
id_to_remove = e->child;
memset(e, 0, sizeof(Entity));
if(!destroy_children) {
break;
}
}
}
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) {
// Game-logic! :)
for(uint16_t i = 0; i < world->max_entities; i++) {
Entity *entity = &world->entities[i];
if(!entity->active) {
continue;
}
switch(entity->type) {
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;
}
case Entity_Type_Snake_Body: {
// Skip.
break;
}
case Entity_Type_Food: {
// Skip (for now).
break;
}
default: {
printf("Unhandled entity-type %i.\n", entity->type);
break;
}
}
}
}
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) {
assert(world != NULL);
@@ -116,3 +395,62 @@ Entity *game_world_try_get_entity_by_id(Game_World *world, Entity_ID id) {
return &world->entities[id];
}
bool game_world_find_position_to_spawn(Game_World *world, uint16_t *out_x, uint16_t *out_y) {
assert(world != NULL);
assert(out_x != NULL);
assert(out_y != NULL);
uint32_t attempts = world->grid.width * world->grid.height;
while(attempts > 0) {
uint16_t potential_x = random_u32_range(&world->random_generator, 0, (uint32_t) world->grid.width - 1);
uint16_t potential_y = random_u32_range(&world->random_generator, 0, (uint32_t) world->grid.height - 1);
Grid_Cell *cell = grid_get_cell(&world->grid, potential_x, potential_y);
assert(cell != NULL);
// Check if cell is free.
if(cell->entity == NULL) {
*out_x = potential_x;
*out_y = potential_y;
return true;
}
if(attempts > 0) {
attempts -= 1;
}
else {
break;
}
}
*out_x = 0;
*out_y = 0;
return false;
}
Entity_Movement_Direction game_world_choose_initial_move_direction_based_on_coords(Game_World *world, uint16_t x, uint16_t y) {
assert(world != NULL);
Grid *grid = &world->grid;
// TODO: SS - Consider potential obstacles in the grid (lava, walls etc) if we ever have them.
if (x == 0) {
return Entity_Movement_Direction_Right;
}
if (x == grid->width - 1) {
return Entity_Movement_Direction_Left;
}
if (y == 0) {
return Entity_Movement_Direction_Down;
}
if (y == grid->height - 1) {
return Entity_Movement_Direction_Up;
}
return Entity_Movement_Direction_Up;
}

View File

@@ -6,11 +6,15 @@
#include "grid.h"
#include "shared/squeue.h"
#include "shared/random.h"
#include "game_world_player.h"
typedef struct {
uint32_t seed;
Random_Generator random_generator;
Game_World_Player *players;
uint16_t max_players;
SQueue entity_id_queue;
Entity *entities;
uint16_t max_entities;
@@ -19,14 +23,20 @@ typedef struct {
} Game_World;
Game_World *game_world_create(uint32_t seed, 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);
bool game_world_create_entity(Game_World *world, Entity_Type type, uint16_t x, uint16_t y, Entity_ID *out_entity_id);
void game_world_destroy_entity(Game_World *world, Entity_ID entity_id);
void game_world_destroy_entity(Game_World *world, Entity_ID entity_id, bool destroy_children);
// TODO: SS - "void game_world_spawn_player(Game_World *world, ..)"
void game_world_tick(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);
bool game_world_find_position_to_spawn(Game_World *world, uint16_t *out_x, uint16_t *out_y);
Entity_Movement_Direction game_world_choose_initial_move_direction_based_on_coords(Game_World *world, uint16_t x, uint16_t y);
#endif

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

@@ -55,3 +55,8 @@ bool squeue_peek(const SQueue *q, void *out) {
memcpy(out, src, q->element_size);
return true;
}
void squeue_clear(SQueue *q) {
void *t = NULL;
while(squeue_pop(q, t)) {}
}

View File

@@ -24,4 +24,6 @@ bool squeue_push(SQueue *q, const void *elem);
bool squeue_pop(SQueue *q, void *out);
bool squeue_peek(const SQueue *q, void *out);
void squeue_clear(SQueue *q);
#endif

View File

@@ -2,24 +2,15 @@
#define SIM_GAME_INPUT_H
typedef struct {
// Movement can probably just be one byte. For a traditional snake-game, only one directional-input is OK.
bool up;
bool down;
bool right;
bool left;
} Simulation_Game_Input;
static inline bool simulation_input_equal(Simulation_Game_Input a, Simulation_Game_Input b) {
if(a.up != b.up) {
static inline bool simulation_input_empty(Simulation_Game_Input input) {
if(input.right) {
return false;
}
if(a.down != b.down) {
return false;
}
if(a.right != b.right) {
return false;
}
if(a.left != b.left) {
if(input.left) {
return false;
}

View File

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

View File

@@ -14,7 +14,7 @@ Simulation_World simulation_create_world(uint32_t seed, bool online, uint8_t max
w.online = online;
w.game_world = game_world_create(seed, width, height);
w.game_world = game_world_create(seed, max_players, width, height);
assert(w.game_world != NULL);
w.match_state = Simulation_Match_State_Waiting_To_Start;
@@ -49,6 +49,9 @@ void simulation_world_tick(Simulation_World *simulation_world) {
// 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++) {
Simulation_Command *cmd = &simulation_world->commands[i];
// printf("Command type: %i, player id: %i.\n", cmd->type, cmd->player_id);
@@ -62,18 +65,28 @@ void simulation_world_tick(Simulation_World *simulation_world) {
assert(!player->active);
player->active = true;
assert(game_world_add_player(gw, &player->game_world_player));
Game_World_Player *gwp = player->game_world_player;
uint16_t spawn_x = 0;
uint16_t spawn_y = 0;
assert(game_world_find_position_to_spawn(gw, &spawn_x, &spawn_y));
assert(game_world_create_entity(
simulation_world->game_world,
gw,
Entity_Type_Snake_Head,
i+6, 15, // NOTE: SS - Hardcoded spawn-position.
&player->entity_id
spawn_x, spawn_y,
&gwp->entity_id
));
Entity *player_entity = game_world_try_get_entity_by_id(simulation_world->game_world, player->entity_id);
Entity *player_entity = game_world_try_get_entity_by_id(gw, gwp->entity_id);
// Set initial move-direction for player-entities.
// The initial direction should probably take the spawn-position in to account.
player_entity->move_direction = Entity_Movement_Direction_Up;
player_entity->move_direction = game_world_choose_initial_move_direction_based_on_coords(
gw,
spawn_x, spawn_y
);
break;
}
@@ -82,8 +95,7 @@ void simulation_world_tick(Simulation_World *simulation_world) {
assert(player->active);
player->active = false;
game_world_destroy_entity(simulation_world->game_world, player->entity_id);
assert(game_world_remove_player(gw, player->game_world_player));
break;
}
case Simulation_Command_Type_Set_Player_Input: {
@@ -126,44 +138,75 @@ void simulation_world_tick(Simulation_World *simulation_world) {
// Loop over all players in the simulation.
for(uint16_t i = 0; i < simulation_world->max_players; i++) {
Simulation_Player *player = &simulation_world->players[i];
if(!player->active) {
Simulation_Player *sim_player = &simulation_world->players[i];
if(!sim_player->active) {
continue;
}
// printf("* Input for player %i - up: %i, down: %i, right: %i, left: %i.\n",
Game_World_Player *gw_player = sim_player->game_world_player;
assert(gw_player != NULL);
if(!gw_player->active) {
continue;
}
// NOTE: SS - Not sure if this should be moved to game-world.. This is input that comes from the simulation.
// printf("* Input for player %i - right: %i, left: %i.\n",
// i,
// player->input.up, player->input.down, 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);
assert(player_entity != NULL);
Entity *player_entity = game_world_try_get_entity_by_id(gw, gw_player->entity_id);
if(player_entity == NULL) {
// No entity to control. No entity to apply input on.
continue;
}
// Snakes can't go backwards. They have to turn.
// We only have input 'right' and 'up'.
Simulation_Game_Input input = sim_player->input;
// Turn relative to the player's current move_direction.
switch(player_entity->move_direction) {
case Entity_Movement_Direction_None: {
break;
}
case Entity_Movement_Direction_Up:
case Entity_Movement_Direction_Down: // Snakes can only go right/left if it's currently moving up/down.
{
if(player->input.right) {
case Entity_Movement_Direction_Up: {
if(input.right) {
player_entity->move_direction = Entity_Movement_Direction_Right;
}
else if(player->input.left) {
else if(input.left) {
player_entity->move_direction = Entity_Movement_Direction_Left;
}
break;
}
case Entity_Movement_Direction_Right:
case Entity_Movement_Direction_Left: // Snakes can only go up/down if it's currently moving right/left.
{
if(player->input.up) {
case Entity_Movement_Direction_Down: {
if(input.right) {
player_entity->move_direction = Entity_Movement_Direction_Left;
}
else if(input.left) {
player_entity->move_direction = Entity_Movement_Direction_Right;
}
break;
}
case Entity_Movement_Direction_Right: {
if(input.right) {
player_entity->move_direction = Entity_Movement_Direction_Down;
}
else if(input.left) {
player_entity->move_direction = Entity_Movement_Direction_Up;
}
else if(player->input.down) {
break;
}
case Entity_Movement_Direction_Left: {
if(input.right) {
player_entity->move_direction = Entity_Movement_Direction_Up;
}
else if(input.left) {
player_entity->move_direction = Entity_Movement_Direction_Down;
}
@@ -172,156 +215,7 @@ void simulation_world_tick(Simulation_World *simulation_world) {
}
}
// Game-logic! :)
for(uint16_t i = 0; i < gw->max_entities; i++) {
Entity *entity = &gw->entities[i];
if(!entity->active) {
continue;
}
switch(entity->type) {
case Entity_Type_Snake_Head: {
break;
}
case Entity_Type_Snake_Body: {
// Skip.
break;
}
case Entity_Type_Food: {
// Skip (for now).
break;
}
default: {
printf("Unhandled entity-type %i.\n", entity->type);
break;
}
}
{ // 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(&gw->grid, entity->x, entity->y);
assert(current_cell != NULL);
Grid_Cell *target_cell = grid_get_cell(&gw->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_should_grow = 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_should_grow = 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(&gw->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(gw, e->child);
t = a;
}
}
if(snake_should_grow) {
Entity *e = entity;
uint16_t child_index = 0;
while(e->child != INVALID_ENTITY_ID) {
e = game_world_try_get_entity_by_id(gw, e->child);
child_index += 1;
}
Entity_ID child_entity = INVALID_ENTITY_ID;
assert(game_world_create_entity(
gw,
Entity_Type_Snake_Body,
e->prev_x,
e->prev_y,
&child_entity
));
assert(e->child == INVALID_ENTITY_ID);
e->child = child_entity;
}
}
}
}
}
}
game_world_tick(gw);
}
break;

View File

@@ -46,9 +46,9 @@ int main() {
presentation_state_machine.current->tick(presentation_state_machine.current);
}
if(IsKeyPressed(KEY_F)) {
ToggleFullscreen();
}
// if(IsKeyPressed(KEY_F)) { // TODO: SS - Add this back, but not 'F'.
// ToggleFullscreen();
// }
// Render.
BeginDrawing();

28
src/todo.md Normal file
View File

@@ -0,0 +1,28 @@
# TODO
- [x] find_good_position_to_spawn()
- A lot of gameplay-functionality depends on this.
- [x] Set initial move-direction based on where you spawn.
- Can be improved and take more things in to account.
- Right now it's only considering the size of of the grid, but what if we want to have obstacles in the grid?
- We also don't take if two players spawn one cell from each other in to consideration.
- Add player-death.
- [x] Went out of bounds.
- [x] Hit other snake's head.
- [x] Hit yours/other snake's body.
- Spawn players at a random position.
- [x] When initially joining.
- [x] When dead and should respawn.
- [x] Spawn a new food when one was eaten.
- [x] When a player disconnects, all children should recursively be destroyed too. Not just the head.
- [ ] Send networked input.
- [ ] Set up session when joining the host.
- [x] Change player-movement?
- Only inputs needed are RIGHT and LEFT. Turn relative to your head.
- Right now we're moving relative to world-up.
- This would fix issues with invalid inputs, for example when the player inputs RIGHT when going LEFT.
- Make this an option later?
- [ ] Customize the session.
- [ ] Replays.
- [ ] Sound effects + music.
- [ ] Options menu.
- [ ] Scoreboard.