#include "state_ingame.h" #include #include #include #include #include "third_party/raylib.h" #include "third_party/raygui.h" #include "session/game_session.h" #include "shared/wang_hash.h" #define GRID_CELL_SIZE 8 #define ENTITY_PRESENTATION_Y_OFFSET -2 static void state_enter(Presentation_State *state) { Presentation_State_Ingame_Context *ctx = (Presentation_State_Ingame_Context *)state->context; (void)ctx; printf("Entered ingame.\n"); printf("Setting up context ...\n"); ctx->main_camera = (Camera2D) { .offset = (Vector2) { 0, 0 }, .target = (Vector2) { 0, 0 }, .rotation = 0.0f, .zoom = 2.0f }; // TODO: SS - Maybe put the textures in an array and index them using an enum? // These should probably be loaded at start-up instead of here. ctx->texture_shadow_basic = LoadTexture("game/assets/sprites/spr_shadow_basic.png"); assert(IsTextureValid(ctx->texture_shadow_basic)); ctx->texture_grass = LoadTexture("game/assets/sprites/spr_floor_grass.png"); assert(IsTextureValid(ctx->texture_grass)); ctx->texture_apple = LoadTexture("game/assets/sprites/spr_food_apple.png"); assert(IsTextureValid(ctx->texture_apple)); ctx->texture_snake_head = LoadTexture("game/assets/sprites/spr_snake_head.png"); assert(IsTextureValid(ctx->texture_snake_head)); ctx->texture_snake_body = LoadTexture("game/assets/sprites/spr_snake_body.png"); 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) { .up = IsKeyDown(KEY_W), .down = IsKeyDown(KEY_S), .right = IsKeyDown(KEY_D), .left = IsKeyDown(KEY_A) }; } case 1: { return (Simulation_Game_Input) { .up = IsKeyDown(KEY_UP), .down = IsKeyDown(KEY_DOWN), .right = IsKeyDown(KEY_RIGHT), .left = IsKeyDown(KEY_LEFT) }; } case 2: { return (Simulation_Game_Input) { .up = IsKeyDown(KEY_I), .down = IsKeyDown(KEY_K), .right = IsKeyDown(KEY_L), .left = IsKeyDown(KEY_J) }; } case 3: { return (Simulation_Game_Input) { .up = IsKeyDown(KEY_T), .down = IsKeyDown(KEY_G), .right = IsKeyDown(KEY_H), .left = IsKeyDown(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; Game_Instance *instance = ctx->game_instance; assert(instance != NULL); Game_Session *session = instance->game_session; assert(session != NULL); { // Push local input to queue. 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) { .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) }; Locally_Controlled_Player *player = &instance->locally_controlled_players[0]; if(!simulation_input_equal(input, player->prev_input)) { game_instance_push_local_input(instance, 0, input); player->prev_input = 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); Locally_Controlled_Player *player = &instance->locally_controlled_players[i]; if(!simulation_input_equal(input, player->prev_input)) { game_instance_push_local_input(instance, i, input); player->prev_input = input; } } } } const double delta_time = GetFrameTime(); ctx->simulation_accumulator += delta_time; const double sim_dt = 1.0f / session->settings.tickrate; while (ctx->simulation_accumulator >= sim_dt) { 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}; if(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.) // 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); 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); } } } uint32_t floor_texture_variant(uint32_t x, uint32_t y, uint32_t seed, uint32_t num_variants) { 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) { Presentation_State_Ingame_Context *ctx = (Presentation_State_Ingame_Context *)state->context; (void)ctx; ClearBackground((Color) { 240, 236, 226, 255 }); Game_Instance *instance = ctx->game_instance; Game_Session *session = instance->game_session; Simulation_World *sim_world = &session->simulation_world; assert(sim_world != NULL); Game_World *world = sim_world->game_world; assert(world != NULL); Grid *grid = &world->grid; uint32_t grid_total_size = grid->width * grid->height; BeginMode2D(ctx->main_camera); { // Render the level. for(uint32_t i = 0; i < grid_total_size; i++) { uint32_t x = i % grid->width; uint32_t y = i / grid->width; uint32_t pres_x = x * GRID_CELL_SIZE; uint32_t pres_y = y * GRID_CELL_SIZE; uint32_t random_floor_index = floor_texture_variant(x, y, world->seed, 4); DrawTextureRec( ctx->texture_grass, (Rectangle) { GRID_CELL_SIZE * random_floor_index, 0, GRID_CELL_SIZE, GRID_CELL_SIZE }, (Vector2) { pres_x, pres_y }, WHITE ); DrawRectangleLines(pres_x, pres_y, GRID_CELL_SIZE, GRID_CELL_SIZE, (Color) { 0, 0, 0, 2 }); // TODO: SS - Let the user customize the alpha locally. } for(uint32_t i = 0; i < grid_total_size; i++) { uint32_t x = i % grid->width; uint32_t y = i / grid->width; uint32_t pres_x = x * GRID_CELL_SIZE; uint32_t pres_y = y * GRID_CELL_SIZE; Grid_Cell *cell = &grid->cells[i]; if(cell->entity != NULL) { Entity *entity = cell->entity; switch(entity->type) { case Entity_Type_Snake_Head: { Color tint = (Color) { 255, 255, 255, 255 }; // Set the snake's tint based on player-index. for(uint16_t i = 0; i < sim_world->max_players; i++) { Simulation_Player *player = &sim_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; uint16_t i = 0; while(e != NULL) { // Loop over all the body-parts of the snake, including the head. float sin_frequency = 0.0f; float sin_amplitude = 0.0f; uint32_t pres_x = e->x * GRID_CELL_SIZE; uint32_t pres_y = e->y * GRID_CELL_SIZE; Texture2D *texture = NULL; if(e->type == Entity_Type_Snake_Head) { texture = &ctx->texture_snake_head; } else { texture = &ctx->texture_snake_body; sin_frequency = 4.0f; sin_amplitude = 1.0f; } // Draw shadow. DrawTextureEx( ctx->texture_shadow_basic, (Vector2) { pres_x, pres_y }, 0.0f, 1.0f, (Color) { 0, 0, 0, SHADOW_ALPHA } ); float rotation = 0.0f; switch(e->move_direction){ case Entity_Movement_Direction_None: { rotation = 0.0f; break; } case Entity_Movement_Direction_Up: { rotation = -90.0f; break; } case Entity_Movement_Direction_Down: { rotation = 90.0f; break; } case Entity_Movement_Direction_Right: { rotation = 0.0f; break; } case Entity_Movement_Direction_Left: { rotation = 180.0f; break; } } DrawTexturePro( *texture, (Rectangle) { // Source. 0, 0, GRID_CELL_SIZE, GRID_CELL_SIZE }, (Rectangle) { // Destination. pres_x + origin.x, pres_y + origin.y + ENTITY_PRESENTATION_Y_OFFSET - (sin((GetTime() + (float)(i) / 4.0) * sin_frequency)) * sin_amplitude, GRID_CELL_SIZE, GRID_CELL_SIZE }, origin, // Origin. rotation, // Rotation. tint // Tint. ); e = game_world_try_get_entity_by_id(world, e->child); i += 1; } // TODO: SS - Don't draw player-name if playing by yourself. // TODO: SS - Don't draw your own player-name, only others. { // Draw player-name. const char *player_name = "player"; // NOTE: SS - Hardcoded. const uint32_t font_size = 8; int text_width = MeasureText(player_name, font_size); DrawText(player_name, pres_x - (float)text_width/2.0f + 4, pres_y - 16, font_size, (Color) { 255, 255, 255, 128 }); } break; } case Entity_Type_Snake_Body: { break; } case Entity_Type_Food: { uint32_t flash = ((sin(GetTime() * 8) + 1)/2) * 32; Color tint = (Color) { 255-flash, 255-flash, 255-flash, 255 }; Vector2 origin = (Vector2) { GRID_CELL_SIZE/2, GRID_CELL_SIZE/2 }; // Draw shadow. DrawTextureEx( ctx->texture_shadow_basic, (Vector2) { pres_x, pres_y }, 0.0f, 1.0f, (Color) { 0, 0, 0, SHADOW_ALPHA } ); DrawTexturePro( ctx->texture_apple, (Rectangle) { // Source. 0, 0, GRID_CELL_SIZE, GRID_CELL_SIZE }, (Rectangle) { // Destination. pres_x + origin.x, pres_y + origin.y + ENTITY_PRESENTATION_Y_OFFSET - ((sin((GetTime() + (x + y)) * 12) + 1)/2) * 1, GRID_CELL_SIZE, GRID_CELL_SIZE }, origin, // Origin. 0, // Rotation. tint // Tint. ); break; } } } } // 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) { char buf[2048]; // Add generic info about the game to the buffer. uint32_t i = snprintf(&buf[0], sizeof(buf), "Match-state: %i\n" "Tick: %lu\n" "\n" "Seed: %u\n" "Level: %ux%u\n" "Tickrate: %.2f\n" , sim_world->match_state, sim_world->tick, sim_world->game_world->seed, sim_world->game_world->grid.width, sim_world->game_world->grid.height, session->settings.tickrate ); // Now, write info specific to whether you're online or offline. if(session->settings.online) { Game_Networking *networking = &instance->game_networking; snprintf(&buf[i], sizeof(buf), "\n" "[Online]\n" "Host: %s\n" "Session-id: %s\n" "Client-id: %s\n" "\n" "Local player: %u\n" "Player count: %u / %u\n" , networking->is_host ? "true" : "false", networking->session_id, networking->local_client_id, instance->locally_controlled_players[0].player_id, game_session_get_amount_of_active_players(session), sim_world->max_players ); } // Finally, draw the buffer to the screen (with nice shadowing!). DrawText(buf, 17, 17, 8, BLACK); DrawText(buf, 16, 16, 8, WHITE); } } EndMode2D(); } static void state_exit(Presentation_State *state) { Presentation_State_Ingame_Context *ctx = (Presentation_State_Ingame_Context *)state->context; (void)ctx; printf("Exiting ingame\n"); UnloadTexture(ctx->texture_shadow_basic); UnloadTexture(ctx->texture_grass); UnloadTexture(ctx->texture_apple); UnloadTexture(ctx->texture_snake_head); UnloadTexture(ctx->texture_snake_body); game_instance_stop_session(ctx->game_instance); ctx->game_instance = NULL; } Presentation_State presentation_state_ingame; void presentation_state_ingame_init(Presentation_State_Ingame_Context *ctx) { presentation_state_ingame = (Presentation_State) { .name = "Ingame", .context = (void *)ctx, .enter = state_enter, .tick = state_tick, .render = state_render, .exit = state_exit }; }