diff --git a/CMakeLists.txt b/CMakeLists.txt index f2c6eb7..217e8c9 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -24,6 +24,7 @@ add_executable(PixelDefense game/pathfinding.h game/systems.h game/systems_entity.c + game/systems_input.c game/systems_ui.c ) diff --git a/game/game_state.h b/game/game_state.h index a76c724..04dc92c 100644 --- a/game/game_state.h +++ b/game/game_state.h @@ -19,7 +19,7 @@ typedef struct InputState { MouseButton RMB; MouseButton MMB; KeyboardKey ESC; - f32 DRAG_LIMIT; + f32 CLICK_LIMIT; // Common Vector2 mouseDown; Vector2 mouseDownWorld; @@ -30,7 +30,8 @@ typedef struct InputState { TilePosition buildingPos; TileSize buildingSize; // SELECTED_UNITS - // SELECTED_OBJECTS + ecs_entity_t *entities; + // SELECTED_OBJECT // SELECTED_BUILDING } InputState; @@ -43,7 +44,6 @@ typedef struct Game { BzSpatialGrid *entityGrid; f32 frameDuration; ecs_entity_t entity; - InputState input; struct { i64 wood; i64 iron; @@ -66,5 +66,6 @@ typedef struct Game { extern ecs_world_t *ECS; extern ECS_COMPONENT_DECLARE(Game); +extern ECS_COMPONENT_DECLARE(InputState); #endif //PIXELDEFENSE_GAME_STATE_H diff --git a/game/main.c b/game/main.c index 2167b69..272fb7f 100644 --- a/game/main.c +++ b/game/main.c @@ -14,6 +14,7 @@ #include ECS_COMPONENT_DECLARE(Game); +ECS_COMPONENT_DECLARE(InputState); ecs_world_t *ECS = NULL; @@ -50,21 +51,27 @@ bool init(void *userData) { ECS = ecs_init(); - ECS_COMPONENT_DEFINE(ECS, Game); initComponentIDs(ECS); // For ecs explorer //ecs_singleton_set(ECS, EcsRest, {0}); //ECS_IMPORT(ECS, FlecsMonitor); + ECS_COMPONENT_DEFINE(ECS, Game); ecs_singleton_set(ECS, Game, {}); Game *game = ecs_singleton_get_mut(ECS, Game); - game->input.LMB = MOUSE_LEFT_BUTTON; - game->input.RMB = MOUSE_RIGHT_BUTTON; - game->input.MMB = MOUSE_MIDDLE_BUTTON; + ECS_COMPONENT_DEFINE(ECS, InputState); + ecs_singleton_set(ECS, InputState, {}); + InputState *input = ecs_singleton_get_mut(ECS, InputState); - game->input.ESC = KEY_ESCAPE; + input->LMB = MOUSE_LEFT_BUTTON; + input->RMB = MOUSE_RIGHT_BUTTON; + input->MMB = MOUSE_MIDDLE_BUTTON; + input->ESC = KEY_ESCAPE; + input->CLICK_LIMIT = 0.2f; + + input->entities = bzArrayCreate(ecs_entity_t, 16); // init pools @@ -156,6 +163,7 @@ bool init(void *userData) { void deinit(void *userData) { BZ_UNUSED(userData); Game *game = ecs_singleton_get_mut(ECS, Game); + InputState *input = ecs_singleton_get_mut(ECS, InputState); bzTileMapDestroy(&game->map); bzTilesetDestroy(&game->terrainTileset); @@ -163,10 +171,12 @@ void deinit(void *userData) { bzTilesetDestroy(&game->entitiesTileset); Game gameCopy = *game; + InputState inputCopy = *input; ecs_fini(ECS); ECS = NULL; + bzArrayDestroy(inputCopy.entities); bzObjectPoolDestroy(gameCopy.pools.pathData); bzSpatialGridDestroy(gameCopy.entityGrid); } @@ -174,74 +184,12 @@ void deinit(void *userData) { void update(float dt, void *userData) { BZ_UNUSED(userData); - Game *game = ecs_singleton_get_mut(ECS, Game); char titleBuf[32]; snprintf(titleBuf, sizeof(titleBuf), "FPS: %d | %.2f ms", GetFPS(), GetFrameTime() * 1000); SetWindowTitle(titleBuf); - ImGuiIO *io = igGetIO(); - if (io->WantCaptureMouse || io->WantCaptureKeyboard) - return; - - if (IsKeyDown(KEY_W)) game->camera.target.y -= 20; - if (IsKeyDown(KEY_S)) game->camera.target.y += 20; - if (IsKeyDown(KEY_A)) game->camera.target.x -= 20; - if (IsKeyDown(KEY_D)) game->camera.target.x += 20; - - if (IsKeyDown(KEY_Q)) game->camera.rotation--; - if (IsKeyDown(KEY_E)) game->camera.rotation++; - - - game->camera.zoom += ((float) GetMouseWheelMove() * 0.05f); - BzTileMap *map = &game->map; - - InputState *input = &game->input; - if (IsMouseButtonPressed(input->LMB)) { - game->input.mouseDown = GetMousePosition(); - game->input.mouseDownWorld = GetScreenToWorld2D(game->input.mouseDown, game->camera); - game->input.mouseDownElapsed = 0; - } else if (IsMouseButtonDown(input->LMB)) { - game->input.mouseDownElapsed += dt; - } - - switch (input->state) { - case INPUT_NONE: - if (IsMouseButtonUp(input->LMB)) { - - } - break; - case INPUT_BUILDING: - if (IsKeyPressed(input->ESC) || - IsMouseButtonPressed(input->RMB) || - input->building == 0) { - input->state = INPUT_NONE; - input->building = 0; - break; - } - Vector2 worldPos = GetScreenToWorld2D(GetMousePosition(), game->camera); - int tileX = (int) worldPos.x / map->tileWidth; - int tileY = (int) worldPos.y / map->tileHeight; - BZ_ASSERT(input->building); - BzTile sizeX = 0, sizeY = 0; - getBuildingSize(input->building, &sizeX, &sizeY); - bool canPlace = canPlaceBuilding(&game->map, input->building, tileX, tileY); - if (canPlace && IsMouseButtonDown(input->LMB)) { - placeBuilding(&game->map, input->building, tileX, tileY); - } - input->buildingCanPlace = canPlace; - input->buildingPos = (TilePosition) {tileX, tileY}; - input->buildingSize = (TileSize) {sizeX, sizeY}; - break; - case INPUT_SELECTED_UNITS: - break; - case INPUT_SELECTED_OBJECT: - break; - case INPUT_SELECTED_BUILDING: - break; - } - - + updatePlayerInput(NULL); } static bool isUnitObstructed(f32 x, f32 y, BzTileMap *map) { @@ -299,27 +247,44 @@ static void placeUnits(i32 numUnits, f32 unitSpacing, Vector2 start, Vector2 end void render(float dt, void *userData) { BZ_UNUSED(userData); Game *game = ecs_singleton_get_mut(ECS, Game); + InputState *input = ecs_singleton_get_mut(ECS, InputState); ClearBackground(RAYWHITE); BeginMode2D(game->camera); bzTileMapDraw(&game->map); - InputState *input = &game->input; + Vector2 worldPos = GetScreenToWorld2D(GetMousePosition(), game->camera); switch (input->state) { case INPUT_NONE: + if (IsMouseButtonDown(input->LMB) && input->mouseDownElapsed > input->CLICK_LIMIT) { + Vector2 start = input->mouseDownWorld; + Vector2 end = worldPos; + if (start.x > end.x) { + f32 tmp = start.x; + start.x = end.x; + end.x = tmp; + } + if (start.y > end.y) { + f32 tmp = start.y; + start.y = end.y; + end.y = tmp; + } + Rectangle area = {start.x, start.y, end.x - start.x, end.y - start.y}; + DrawRectangleLines(area.x, area.y, area.width, area.height, RED); + } break; case INPUT_BUILDING: { - Color placeColor = game->input.buildingCanPlace ? + Color placeColor = input->buildingCanPlace ? (Color) {0, 255, 0, 200} : (Color) {255, 0, 0, 200}; BzTile width = game->map.tileWidth; BzTile height = game->map.tileHeight; - DrawRectangleLines(game->input.buildingPos.x * width, - game->input.buildingPos.y * height, - game->input.buildingSize.sizeX * width, - game->input.buildingSize.sizeY * height, placeColor); + DrawRectangleLines(input->buildingPos.x * width, + input->buildingPos.y * height, + input->buildingSize.sizeX * width, + input->buildingSize.sizeY * height, placeColor); break; } case INPUT_SELECTED_UNITS: @@ -414,11 +379,12 @@ void render(float dt, void *userData) { void imguiRender(float dt, void *userData) { BZ_UNUSED(userData); Game *game = ecs_singleton_get_mut(ECS, Game); + InputState *input = ecs_singleton_get_mut(ECS, InputState); igSetNextWindowSize((ImVec2){300, 400}, ImGuiCond_FirstUseEver); igBegin("Debug Menu", NULL, 0); const char *inputState = "NONE"; - switch (game->input.state) { + switch (input->state) { case INPUT_NONE: inputState = "NONE"; break; @@ -437,6 +403,21 @@ void imguiRender(float dt, void *userData) { } igText("Input state: %s", inputState); if (igCollapsingHeader_TreeNodeFlags("Selection", 0)) { + switch (input->state) { + case INPUT_SELECTED_UNITS: + igText("Selected units:"); + for (i32 i = 0; i < bzArraySize(input->entities); i++) { + igText("\t%llu", bzArrayGet(input->entities, i)); + } + break; + case INPUT_SELECTED_OBJECT: + break; + case INPUT_SELECTED_BUILDING: + break; + default: + igText("NONE"); + break; + } } if (igCollapsingHeader_TreeNodeFlags("Resources", 0)) { @@ -448,11 +429,11 @@ void imguiRender(float dt, void *userData) { } if (igCollapsingHeader_TreeNodeFlags("BuildMenu", 0)) { for (int i = 0; i < BUILDINGS_COUNT; i++) { - if (igSelectable_Bool(getBuildingStr(i), game->input.building == i, 0, (ImVec2){0, 0})) - game->input.building = i; + if (igSelectable_Bool(getBuildingStr(i), input->building == i, 0, (ImVec2){0, 0})) + input->building = i; } - if (game->input.building) - game->input.state = INPUT_BUILDING; + if (input->building) + input->state = INPUT_BUILDING; } if (igCollapsingHeader_TreeNodeFlags("DebugDraw", 0)) { diff --git a/game/systems.h b/game/systems.h index faa90bc..0c90ff6 100644 --- a/game/systems.h +++ b/game/systems.h @@ -70,4 +70,17 @@ void renderColliders(ecs_iter_t *it); */ void renderDebugPath(ecs_iter_t *it); + + +/********************************** + * Entity Systems + **********************************/ + +/* + * Task: + * 0: Game (singleton) + * 0: InputState (singleton) + */ +void updatePlayerInput(ecs_iter_t *it); + #endif //PIXELDEFENSE_SYSTEMS_H diff --git a/game/systems_input.c b/game/systems_input.c new file mode 100644 index 0000000..fe0f0f6 --- /dev/null +++ b/game/systems_input.c @@ -0,0 +1,202 @@ +#include "systems.h" +#include "game_state.h" +#include "buildings.h" +#include "pathfinding.h" +#include +#include + +void pickEntity(BzSpatialGrid *entityGrid, Vector2 point, ecs_entity_t **outEntities); +void pickEntities(BzSpatialGrid *entityGrid, Rectangle area, ecs_entity_t **outEntities); + +void updatePlayerInput(ecs_iter_t *it) { + f32 dt = GetFrameTime(); + ImGuiIO *io = igGetIO(); + if (io->WantCaptureMouse || io->WantCaptureKeyboard) + return; + + Game *game = ecs_singleton_get_mut(ECS, Game); + InputState *input = ecs_singleton_get_mut(ECS, InputState); + if (IsKeyDown(KEY_W)) game->camera.target.y -= 20; + if (IsKeyDown(KEY_S)) game->camera.target.y += 20; + if (IsKeyDown(KEY_A)) game->camera.target.x -= 20; + if (IsKeyDown(KEY_D)) game->camera.target.x += 20; + + if (IsKeyDown(KEY_Q)) game->camera.rotation--; + if (IsKeyDown(KEY_E)) game->camera.rotation++; + + + game->camera.zoom += ((float) GetMouseWheelMove() * 0.05f); + BzTileMap *map = &game->map; + + if (IsMouseButtonPressed(input->LMB)) { + input->mouseDown = GetMousePosition(); + input->mouseDownWorld = GetScreenToWorld2D(input->mouseDown, game->camera); + input->mouseDownElapsed = 0; + } else if (IsMouseButtonDown(input->LMB)) { + input->mouseDownElapsed += dt; + } + + Vector2 worldPos = GetScreenToWorld2D(GetMousePosition(), game->camera); + int tileX = (int) worldPos.x / map->tileWidth; + int tileY = (int) worldPos.y / map->tileHeight; + switch (input->state) { + case INPUT_NONE: + if (IsMouseButtonReleased(input->LMB) && input->mouseDownElapsed > input->CLICK_LIMIT) { + // Dragging + + Vector2 start = input->mouseDownWorld; + Vector2 end = worldPos; + if (start.x > end.x) { + f32 tmp = start.x; + start.x = end.x; + end.x = tmp; + } + if (start.y > end.y) { + f32 tmp = start.y; + start.y = end.y; + end.y = tmp; + } + Rectangle pickArea = {start.x, start.y, end.x - start.x, end.y - start.y}; + + pickEntities(game->entityGrid, pickArea, &input->entities); + if (bzArraySize(input->entities) > 0) { + input->state = INPUT_SELECTED_UNITS; + break; + } + + } else if (IsMouseButtonReleased(input->LMB)) { + // Click + + // 1. Entity + pickEntity(game->entityGrid, worldPos, &input->entities); + if (bzArraySize(input->entities) > 0) { + input->state = INPUT_SELECTED_UNITS; + break; + } + + // 2. Object + + // 3. Building + } + break; + case INPUT_BUILDING: + if (IsKeyPressed(input->ESC) || + IsMouseButtonPressed(input->RMB) || + input->building == 0) { + input->state = INPUT_NONE; + input->building = 0; + break; + } + BZ_ASSERT(input->building); + BzTile sizeX = 0, sizeY = 0; + getBuildingSize(input->building, &sizeX, &sizeY); + bool canPlace = canPlaceBuilding(&game->map, input->building, tileX, tileY); + if (canPlace && IsMouseButtonDown(input->LMB)) { + placeBuilding(&game->map, input->building, tileX, tileY); + } + input->buildingCanPlace = canPlace; + input->buildingPos = (TilePosition) {tileX, tileY}; + input->buildingSize = (TileSize) {sizeX, sizeY}; + break; + case INPUT_SELECTED_UNITS: + if (IsKeyPressed(input->ESC)) { + input->state = INPUT_NONE; + break; + } + if (IsMouseButtonPressed(input->LMB)) { + bzArrayFor(input->entities, i) { + ecs_entity_t entity = bzArrayGet(input->entities, i); + ecs_remove(ECS, entity, Path); + + const Position *start = ecs_get(ECS, entity, Position); + Path path = {NULL, 0}; + findPath(&(PathfindingDesc) { + .start=(TilePosition) { + start->x / game->map.tileWidth, + start->y / game->map.tileHeight, + }, + .target=(TilePosition) {tileX, tileY}, + .map=&game->map, + .outPath=&path, + .pool=game->pools.pathData + }); + if (!path.paths) continue; + ecs_set_ptr(ECS, entity, Path, &path); + input->state = INPUT_NONE; + } + + } + break; + case INPUT_SELECTED_OBJECT: + break; + case INPUT_SELECTED_BUILDING: + break; + } +} + +static bool getEntityBounds(ecs_entity_t entity, Position *outPos, Size *outSize, + Rectangle *outBounds) { + if (!ecs_is_alive(ECS, entity)) + return false; + const Position *pos = ecs_get(ECS, entity, Position); + if (!pos) + return false; + const Size *size = ecs_get(ECS, entity, Size); + if (!size) + return false; + + if (outPos) { + *outPos = *pos; + } + if (outSize) { + *outSize = *size; + } + if (outBounds) { + *outBounds = (Rectangle) { + pos->x - size->x * 0.5f, + pos->y - size->y * 0.5f, + size->x, size->y + }; + } + return true; +} + +void pickEntity(BzSpatialGrid *entityGrid, Vector2 point, ecs_entity_t **outEntities) { + BZ_ASSERT(*outEntities); + bzArrayClear(*outEntities); + BzSpatialGridIter it = bzSpatialGridIter(entityGrid, point.x, point.y, 0.0f, 0.0f); + f32 closestDst = INFINITY; + ecs_entity_t closest = 0; + while (bzSpatialGridQueryNext(&it)) { + ecs_entity_t entity = *(ecs_entity_t *) it.data; + Vector2 pos; + Rectangle bounds; + if (!getEntityBounds(entity, &pos, NULL, &bounds)) continue; + + if (!CheckCollisionPointRec(point, bounds)) continue; + + f32 curDst = Vector2Distance(point, pos); + if (closestDst > curDst) { + closestDst = curDst; + closest = entity; + } + } + if (closest) { + bzArrayPush(*outEntities, closest); + } +} + +void pickEntities(BzSpatialGrid *entityGrid, Rectangle area, ecs_entity_t **outEntities) { + BZ_ASSERT(*outEntities); + bzArrayClear(*outEntities); + + BzSpatialGridIter it = bzSpatialGridIter(entityGrid, area.x, area.y, area.width, area.height); + while (bzSpatialGridQueryNext(&it)) { + ecs_entity_t entity = *(ecs_entity_t *) it.data; + Rectangle bounds; + if (!getEntityBounds(entity, NULL, NULL, &bounds)) continue; + + if (!CheckCollisionRecs(area, bounds)) continue; + bzArrayPush(*outEntities, entity); + } +}