From 33b28b620d78b0d639c10b7477c0e98268e12e67 Mon Sep 17 00:00:00 2001 From: Klemen Plestenjak Date: Sun, 17 Dec 2023 14:20:13 +0100 Subject: [PATCH] Implement harvest worker AI --- game/buildings.c | 19 ++++++ game/buildings.h | 2 + game/components.c | 7 +++ game/components.h | 6 +- game/main.c | 20 ++++++- game/map_init.c | 20 +++++-- game/systems.h | 19 +++++- game/systems_ai.c | 33 ++++++++++- game/systems_entity.c | 18 ------ game/systems_input.c | 17 ++++-- game/unit_actions.c | 31 +++++++++- game/unit_actions.h | 8 ++- game/unit_ai.c | 132 ++++++++++++++++++++++++++++++++++++++++++ game/unit_ai.h | 46 +++++++++++++++ 14 files changed, 338 insertions(+), 40 deletions(-) diff --git a/game/buildings.c b/game/buildings.c index f5b4008..7e9baf8 100644 --- a/game/buildings.c +++ b/game/buildings.c @@ -4,6 +4,8 @@ #include "game_state.h" #include "map_layers.h" +#include + bool canPlaceBuilding(BzTileMap *map, BuildingType type, BzTile tileX, BzTile tileY) { i32 sizeX, sizeY; getBuildingSize(type, &sizeX, &sizeY); @@ -73,3 +75,20 @@ ecs_entity_t placeBuilding(BzTileMap *map, BuildingType type, BzTile tileX, BzTi return e; } + +Vector2 getPositionNearBuilding(ecs_entity_t building, Vector2 fromPos) { + BZ_ASSERT(ecs_is_alive(ECS, building)); + BZ_ASSERT(ecs_has(ECS, building, Position)); + BZ_ASSERT(ecs_has(ECS, building, Size)); + + Vector2 pos = *ecs_get(ECS, building, Position); + Vector2 size = *ecs_get(ECS, building, Size); + + size = Vector2SubtractValue(size, 10.0f); + + Vector2 dir = Vector2Normalize(Vector2Subtract(fromPos, pos)); + dir = Vector2Multiply(dir, size); + + pos = Vector2Add(pos, dir); + return pos; +} diff --git a/game/buildings.h b/game/buildings.h index 1e306d6..3819bae 100644 --- a/game/buildings.h +++ b/game/buildings.h @@ -9,4 +9,6 @@ bool canPlaceBuilding(BzTileMap *map, BuildingType type, i32 tileX, i32 tileY); ecs_entity_t placeBuilding(BzTileMap *map, BuildingType type, i32 tileX, i32 tileY); +Vector2 getPositionNearBuilding(ecs_entity_t building, Vector2 fromPos); + #endif //PIXELDEFENSE_BUILDINGS_H diff --git a/game/components.c b/game/components.c index 308b016..afddd75 100644 --- a/game/components.c +++ b/game/components.c @@ -1,5 +1,8 @@ #include "components.h" +#include "unit_ai.h" +#include "unit_actions.h" + ECS_TAG_DECLARE(TextureTerrain); ECS_TAG_DECLARE(TextureBuildings); ECS_TAG_DECLARE(TextureEntities); @@ -23,7 +26,9 @@ ECS_COMPONENT_DECLARE(TextureRegion); ECS_COMPONENT_DECLARE(Animation); +ECS_COMPONENT_DECLARE(UnitAI); ECS_COMPONENT_DECLARE(UnitAction); + ECS_TAG_DECLARE(Selectable); ECS_TAG_DECLARE(Selected); @@ -60,7 +65,9 @@ void initComponentIDs(ecs_world_t *ecs) { ECS_COMPONENT_DEFINE(ecs, Animation); + ECS_COMPONENT_DEFINE(ecs, UnitAI); ECS_COMPONENT_DEFINE(ecs, UnitAction); + ECS_TAG_DEFINE(ecs, Selectable); ECS_TAG_DEFINE(ecs, Selected); diff --git a/game/components.h b/game/components.h index 27a74cb..e31aa05 100644 --- a/game/components.h +++ b/game/components.h @@ -5,7 +5,6 @@ #include #include "game_tileset.h" -#include "unit_actions.h" extern ECS_TAG_DECLARE(TextureTerrain); extern ECS_TAG_DECLARE(TextureBuildings); @@ -126,10 +125,7 @@ typedef struct EntityArms { *********************************************************/ extern ECS_COMPONENT_DECLARE(UnitAction); - -typedef struct ActionOverseer { - -} ActionOverseer; +extern ECS_COMPONENT_DECLARE(UnitAI); extern ECS_TAG_DECLARE(Selectable); extern ECS_TAG_DECLARE(Selected); diff --git a/game/main.c b/game/main.c index f7cb9a9..1c74735 100644 --- a/game/main.c +++ b/game/main.c @@ -8,6 +8,8 @@ #include "map_init.h" #include "map_layers.h" #include "buildings.h" +#include "unit_ai.h" +#include "unit_actions.h" #include "pathfinding.h" @@ -112,7 +114,7 @@ bool init(void *userData) { .objectsPerPage = 512 }); game->pools.actions = bzObjectPoolCreate(&(BzObjectPoolDesc) { - .objectSize = sizeof(UnitAction), + .objectSize = sizeof(Action), .objectsPerPage = 1024, }); @@ -176,7 +178,10 @@ bool init(void *userData) { ECS_SYSTEM(ECS, entityFollowPath, EcsOnUpdate, Path); //ECS_SYSTEM(ECS, entityHarvestTaskSystem, EcsOnUpdate, Position, Rotation, HarvestTask); - ECS_SYSTEM(ECS, updateUnitActions, EcsOnUpdate, UnitAction); + ECS_SYSTEM(ECS, handleUnitActionsSystem, EcsOnUpdate, UnitAction); + ECS_SYSTEM(ECS, updateUnitAISystem, EcsOnUpdate, UnitAI, UnitAction); + // Needs to be called after AI update, since it removes finished actions + ECS_SYSTEM(ECS, updateUnitActionsSystem, EcsOnUpdate, UnitAction); //ECS_SYSTEM(ECS, entityUpdateAnimationState, EcsOnUpdate, Velocity, AnimationType); ECS_SYSTEM(ECS, entityUpdateAnimation, EcsOnUpdate, Animation, TextureRegion); @@ -306,7 +311,16 @@ void imguiRender(float dt, void *userData) { while (ecs_iter_next(&it)) { for (i32 i = 0; i < it.count; i++) { ecs_entity_t entity = it.entities[i]; - igText("\tEntity %llu", entity); + igText("Entity %llu", entity); + igText("Actions:"); + const Action *pAction = NULL; + if (ecs_has(ECS, entity, UnitAction)) { + pAction = ecs_get(ECS, entity, UnitAction)->first; + } + while (pAction) { + igText("\t%d: %s", pAction->type, actionTypeToPrettyStr(pAction->type)); + pAction = pAction->next; + } } } break; diff --git a/game/map_init.c b/game/map_init.c index 26e42bc..4b7d937 100644 --- a/game/map_init.c +++ b/game/map_init.c @@ -6,6 +6,9 @@ #include "game_state.h" #include "map_layers.h" +#include "unit_ai.h" +#include "unit_actions.h" + bool initGameObjectsLayer(BzTileMap *map, BzTileObjectGroup *objectGroup) { Game *game = ecs_singleton_get_mut(ECS, Game); for (i32 i = 0; i < objectGroup->objectCount; i++) { @@ -67,15 +70,19 @@ bool initBuildingsLayer(BzTileMap *map, BzTileLayer *layer) { buildingTile = getTileBuilding(buildingTile); if (buildingTile == BUILDING_NONE || ownerTile == 0) continue; // We have a building - TileSize size = {}; - getBuildingSize(buildingTile, &size.sizeX, &size.sizeY); - bzTileLayerSetTile(ownershipLayer, 0, x, y, size.sizeX, size.sizeY); + TileSize tileSize = {}; + getBuildingSize(buildingTile, &tileSize.sizeX, &tileSize.sizeY); + bzTileLayerSetTile(ownershipLayer, 0, x, y, tileSize.sizeX, tileSize.sizeY); const i32 tileWidth = map->tileWidth; const i32 tileHeight = map->tileHeight; ecs_entity_t e = ecs_new_id(ECS); - ecs_set(ECS, e, Position, {.x = x * tileWidth, .y = y * tileHeight}); - ecs_set(ECS, e, TileSize, {.sizeX = size.sizeX * tileWidth, .sizeY = size.sizeY * tileHeight}); + Size size = {.x = tileSize.sizeX * tileWidth, .y = tileSize.sizeY * tileHeight }; + ecs_set_ptr(ECS, e, Size, &size); + ecs_set(ECS, e, Position, { + .x = x * tileWidth + size.x * 0.5f, + .y = y * tileHeight + size.y * 0.5f + }); ownerTile = bzTilesetGetTileID(buildingTileset, ownerTile); ownerTile = getTileBuilding(ownerTile); ecs_set(ECS, e, Owner, {.playerID=ownerTile}); @@ -83,6 +90,9 @@ bool initBuildingsLayer(BzTileMap *map, BzTileLayer *layer) { //if (buildingTile == BUILDINGS_WAREHOUSE) { // ecs_set(ECS, e, Storage, {}); //} + if (buildingTile == BUILDING_KEEP) { + ecs_set(ECS, e, Storage, {.capacity[RES_WOOD] = 1000}); + } //bzTileMapUpdateCollider(&GAME.map, x, y); } diff --git a/game/systems.h b/game/systems.h index f5e6b65..09b3bfa 100644 --- a/game/systems.h +++ b/game/systems.h @@ -7,6 +7,10 @@ typedef struct Game Game; +/********************************** + * Utils + **********************************/ + bool entitySetPath(const ecs_entity_t entity, const Vector2 target, Game *game); /********************************** @@ -17,7 +21,20 @@ bool entitySetPath(const ecs_entity_t entity, const Vector2 target, Game *game); * 0: Game (singleton) * 1: UnitAction */ -void updateUnitActions(ecs_iter_t *it); +void handleUnitActionsSystem(ecs_iter_t *it); + +/* + * 0: Game (singleton) + * 1: UnitAI + * 2: UnitAction + */ +void updateUnitAISystem(ecs_iter_t *it); + +/* + * 0: Game (singleton) + * 1: UnitAction + */ +void updateUnitActionsSystem(ecs_iter_t *it); /********************************** diff --git a/game/systems_ai.c b/game/systems_ai.c index b592651..ea62122 100644 --- a/game/systems_ai.c +++ b/game/systems_ai.c @@ -2,7 +2,10 @@ #include "game_state.h" -void updateUnitActions(ecs_iter_t *it) { +#include "unit_ai.h" +#include "unit_actions.h" + +void handleUnitActionsSystem(ecs_iter_t *it) { Game *game = ecs_singleton_get_mut(ECS, Game); UnitAction *action = ecs_field(it, UnitAction, 1); @@ -11,3 +14,31 @@ void updateUnitActions(ecs_iter_t *it) { handleAction(entity, &action[i], game); } } + +void updateUnitAISystem(ecs_iter_t *it) { + Game *game = ecs_singleton_get_mut(ECS, Game); + UnitAI *unitAI = ecs_field(it, UnitAI, 1); + UnitAction *action = ecs_field(it, UnitAction, 2); + + for (i32 i = 0; i < it->count; i++) { + ecs_entity_t entity = it->entities[i]; + + Action *firstAction = action[i].first; + if (firstAction) + unitAI[i].action = firstAction; + else + unitAI[i].action = NULL; + + updateUnitAI(entity, &unitAI[i], game); + } +} + +void updateUnitActionsSystem(ecs_iter_t *it) { + Game *game = ecs_singleton_get_mut(ECS, Game); + UnitAction *action = ecs_field(it, UnitAction, 1); + + for (i32 i = 0; i < it->count; i++) { + ecs_entity_t entity = it->entities[i]; + updateAction(&action[i], game); + } +} diff --git a/game/systems_entity.c b/game/systems_entity.c index e3b6459..6836973 100644 --- a/game/systems_entity.c +++ b/game/systems_entity.c @@ -194,25 +194,7 @@ void entityFollowPath(ecs_iter_t *it) { } } -static ecs_entity_t findNearestStorage(ResourceType type) { - ecs_filter_t *storageFilter = ecs_filter(ECS, { - .terms = {{ecs_id(Storage)} } - }); - ecs_iter_t it = ecs_filter_iter(ECS, storageFilter); - ecs_entity_t closest = 0; - while (ecs_filter_next(&it)) { - Storage *storage = ecs_field(&it, Storage, 1); - - for (i32 i = 0; i < it.count; i++) { - if (true || storage[i].capacity[type]) { - closest = it.entities[i]; - } - } - } - ecs_filter_fini(storageFilter); - return closest; -} /* void entityHarvestTaskSystem(ecs_iter_t *it) { diff --git a/game/systems_input.c b/game/systems_input.c index d497282..5b5c2bd 100644 --- a/game/systems_input.c +++ b/game/systems_input.c @@ -3,6 +3,9 @@ #include "input.h" #include "buildings.h" #include "pathfinding.h" +#include "unit_ai.h" +#include "unit_actions.h" + #include #include #include @@ -84,11 +87,17 @@ void inputUnitAction(Game *game, InputState *input) { for (i32 i = 0; i < it.count; i++) { const ecs_entity_t entity = it.entities[i]; const Position target = *ecs_get(ECS, taskEntity, Position); - addAction(entity, game, &(const Action) { - .type = ACTION_MOVE_TO, - .as.moveTo.target = target, - .as.moveTo.proximityThreshold = 10.0f, + setUnitAI(entity, game, &(const UnitAI) { + .type = AI_WORKER_HARVEST, + .as.workerHarvest.resource = RES_WOOD, + .as.workerHarvest.target = taskEntity, + .as.workerHarvest.targetPosition = target }); + //addAction(entity, game, &(const Action) { + // .type = ACTION_MOVE_TO, + // .as.moveTo.target = target, + // .as.moveTo.proximityThreshold = 10.0f, + //}); //ecs_set(ECS, entity, HarvestTask, {taskEntity}); goto while_break; } diff --git a/game/unit_actions.c b/game/unit_actions.c index 15001a1..3d4d177 100644 --- a/game/unit_actions.c +++ b/game/unit_actions.c @@ -6,6 +6,7 @@ #include void actionMoveTo(ecs_entity_t entity, Action *action, Game *game) { + if (action->finished) return; const Vector2 target = action->as.moveTo.target; if (!ecs_has(ECS, entity, Path)) { entitySetPath(entity, target, game); @@ -16,18 +17,27 @@ void actionMoveTo(ecs_entity_t entity, Action *action, Game *game) { f32 dst = Vector2Distance(pos, target); if (dst < action->as.moveTo.proximityThreshold) { action->finished = true; + ecs_remove(ECS, entity, Path); } } void actionCollectResource(ecs_entity_t entity, Action *action, Game *game) { + if (action->finished) return; + if (action->elapsed > 2.0f) { + action->finished = true; + ecs_entity_t target = action->as.collectResource.entity; + ecs_delete(ECS, target); + } } void actionDepositResource(ecs_entity_t entity, Action *action, Game *game) { - + if (action->elapsed > 0.2f) + action->finished = true; } void handleAction(ecs_entity_t entity, UnitAction *unitAction, Game *game) { Action *action = unitAction->first; if (action == NULL) return; + if (action->finished || action->failed) return; switch (action->type) { case ACTION_NONE: break; @@ -44,6 +54,12 @@ void handleAction(ecs_entity_t entity, UnitAction *unitAction, Game *game) { BZ_ASSERT(0); break; } + +} +void updateAction(UnitAction *unitAction, Game *game) { + Action *action = unitAction->first; + if (action == NULL) return; + if (action->failed) return; action->elapsed += GetFrameTime(); if (action->finished) { unitAction->first = action->next; @@ -91,3 +107,16 @@ void addAction(ecs_entity_t entity, Game *game, const Action *action) { unitAction->last = newAction; } } + +const char *actionTypeToPrettyStr(ActionType type) { + switch (type) { + case ACTION_MOVE_TO: + return "MOVE TO"; + case ACTION_COLLECT_RESOURCE: + return "COLLECT RESOURCE"; + case ACTION_DEPOSIT_RESOURCE: + return "DEPOSIT RESOURCE"; + default: + return "NONE"; + } +} diff --git a/game/unit_actions.h b/game/unit_actions.h index f490303..206cc68 100644 --- a/game/unit_actions.h +++ b/game/unit_actions.h @@ -19,10 +19,10 @@ typedef struct ActionMoveTo { f32 proximityThreshold; } ActionMoveTo; typedef struct ActionCollectResource { - + ecs_entity_t entity; } ActionCollectResource; typedef struct ActionDepositResource { - + ecs_entity_t entity; } ActionDepositResource; typedef struct Action { @@ -35,6 +35,7 @@ typedef struct Action { f32 elapsed; bool finished; + bool failed; struct Action *next; } Action; @@ -45,7 +46,10 @@ typedef struct UnitAction { } UnitAction; void handleAction(ecs_entity_t entity, UnitAction *unitAction, Game *game); +void updateAction(UnitAction *unitAction, Game *game); void clearActions(ecs_entity_t entity, Game *game); void addAction(ecs_entity_t entity, Game *game, const Action *action); +const char *actionTypeToPrettyStr(ActionType type); + #endif //PIXELDEFENSE_UNIT_ACTIONS_H diff --git a/game/unit_ai.c b/game/unit_ai.c index a0bf66a..ba0d7d6 100644 --- a/game/unit_ai.c +++ b/game/unit_ai.c @@ -1 +1,133 @@ #include "unit_ai.h" +#include "unit_actions.h" +#include "game_state.h" +#include "buildings.h" + +#include + +static ecs_entity_t findNearestStorage(Position pos, ResourceType type, Position *outPos) { + ecs_filter_t *storageFilter = ecs_filter(ECS, { + .terms = {{ecs_id(Storage)}, {ecs_id(Position)} } + }); + ecs_iter_t it = ecs_filter_iter(ECS, storageFilter); + + ecs_entity_t closest = 0; + f32 closestDst = INFINITY; + Position closestPos = Vector2Zero(); + while (ecs_filter_next(&it)) { + Storage *storage = ecs_field(&it, Storage, 1); + Position *storagePos = ecs_field(&it, Position, 2); + + for (i32 i = 0; i < it.count; i++) { + f32 dst = Vector2Distance(pos, storagePos[i]); + if (storage[i].capacity[type] && dst < closestDst) { + closest = it.entities[i]; + closestDst = dst; + closestPos = storagePos[i]; + } + } + } + ecs_filter_fini(storageFilter); + if (outPos) *outPos = closestPos; + return closest; +} +static ecs_entity_t findNearestResource(Game *game, Position pos, ResourceType type, f32 range) { + ecs_entity_t closest = 0; + f32 closestDst = 10000.0f; + + f32 hRange = range * 0.5f; + Rectangle area = {pos.x - hRange, pos.y + hRange, hRange, hRange}; + + BzSpatialGridIter it = bzSpatialGridIter(game->entityGrid, area.x, area.y, area.width, area.height); + + while (bzSpatialGridQueryNext(&it)) { + ecs_entity_t entity = *(ecs_entity_t *) it.data; + if (!ecs_is_alive(ECS, entity)) continue; + if (!ecs_has(ECS, entity, Resource) || !ecs_has(ECS, entity, Position)) + continue; + const Resource *resource = ecs_get(ECS, entity, Resource); + const Position *resPos = ecs_get(ECS, entity, Position); + if (resource->type != type) continue; + + f32 dst = Vector2Distance(pos, *resPos); + if (dst < closestDst) { + closest = entity; + closestDst = dst; + } + } + + return closest; +} + +void aiWorkerBuild(ecs_entity_t entity, UnitAI *unitAI, Game *game) { + +} +void aiWorkerHarvestUpdate(ecs_entity_t entity, UnitAI *unitAI, Game *game) { + const Action *action = unitAI->action; + if (action) return; + + AIWorkerHarvest worker = unitAI->as.workerHarvest; + + Position workerPos = *ecs_get(ECS, entity, Position); + + // Finished all tasks, repeat + ecs_entity_t target = worker.target; + Position targetPos = worker.targetPosition; + if (!ecs_is_alive(ECS, target)) { + // Find new resource + target = findNearestResource(game, targetPos, worker.resource, 20.0f); + if (!target) return; + + worker.target = target; + worker.targetPosition = *ecs_get(ECS, target, Position); + } + // Find the closest warehouse + Position warehousePos = Vector2Zero(); + ecs_entity_t warehouse = findNearestStorage(workerPos, worker.resource, + &warehousePos); + if (!warehouse) { + return; + } + warehousePos = getPositionNearBuilding(warehouse, targetPos); + + const f32 proximityThreshold = 8.0f; + + addAction(entity, game, &(const Action) { + .type = ACTION_MOVE_TO, + .as.moveTo.target = targetPos, + .as.moveTo.proximityThreshold = proximityThreshold, + }); + addAction(entity, game, &(const Action) { + .type = ACTION_COLLECT_RESOURCE, + .as.collectResource.entity = target + }); + addAction(entity, game, &(const Action) { + .type = ACTION_MOVE_TO, + .as.moveTo.target = warehousePos, + .as.moveTo.proximityThreshold = proximityThreshold, + }); + addAction(entity, game, &(const Action) { + .type = ACTION_DEPOSIT_RESOURCE, + .as.depositResource.entity = warehouse + }); + +} + +void updateUnitAI(ecs_entity_t entity, UnitAI *unitAI, Game *game) { + //if (unitAI->onUpdate) unitAI->onUpdate(entity, unitAI, game); + aiWorkerHarvestUpdate(entity, unitAI, game); +} +void setUnitAI(ecs_entity_t entity, Game *game, const UnitAI *unitAI) { + BZ_ASSERT(unitAI); + if (ecs_has(ECS, entity, UnitAI)) { + UnitAI *entityAI = ecs_get_mut(ECS, entity, UnitAI); + if (entityAI->onRemove) entityAI->onRemove(entity, entityAI, game); + } + clearActions(entity, game); + + UnitAI ai = *unitAI; + if (unitAI->onSet) unitAI->onSet(entity, &ai, game); + ecs_set_ptr(ECS, entity, UnitAI, &ai); + if (unitAI->action != NULL) + addAction(entity, game, unitAI->action); +} diff --git a/game/unit_ai.h b/game/unit_ai.h index 1366d42..8c1bab0 100644 --- a/game/unit_ai.h +++ b/game/unit_ai.h @@ -1,5 +1,51 @@ #ifndef PIXELDEFENSE_UNIT_AI_H #define PIXELDEFENSE_UNIT_AI_H +#include + +#include "components.h" +#include "unit_actions.h" + +typedef struct Game Game; + +typedef enum AIType { + AI_NONE, + AI_WORKER_BUILD, + AI_WORKER_HARVEST, + AI_COUNT, +} AIType; + + +typedef struct AIWorkerHarvest { + ResourceType resource; + ecs_entity_t target; + Position targetPosition; +} AIWorkerHarvest; + +typedef struct AIWorkerBuild { + ecs_entity_t buildTarget; +} AIWorkerBuild; + +typedef struct UnitAI UnitAI; + +typedef void (*UnitAIFunc)(ecs_entity_t entity, UnitAI *ai, Game *game); + +typedef struct UnitAI { + AIType type; + union { + AIWorkerHarvest workerHarvest; + AIWorkerBuild workerBuild; + } as; + + Action *action; + + // vtable + UnitAIFunc onSet; + UnitAIFunc onRemove; + UnitAIFunc onUpdate; +} UnitAI; + +void updateUnitAI(ecs_entity_t entity, UnitAI *unitAI, Game *game); +void setUnitAI(ecs_entity_t entity, Game *game, const UnitAI *unitAI); #endif //PIXELDEFENSE_UNIT_AI_H