From 24abf94faa5e72603e1af3969e67db8ae35107d1 Mon Sep 17 00:00:00 2001 From: Klemen Plestenjak Date: Tue, 13 Feb 2024 18:27:53 +0100 Subject: [PATCH] Add chase/evade behaviour --- game/ai_actions.c | 82 ++++++++++++++++++++++++++++++++++++++++-- game/ai_actions.h | 4 ++- game/entity_factory.c | 16 ++++++++- game/game_state.h | 5 +-- game/main.c | 47 ++++++++++++++++++------ game/map_init.c | 2 +- game/systems/s_input.c | 82 +++++++++++++++++++++++------------------- game/systems/s_ui.c | 10 +++--- 8 files changed, 189 insertions(+), 59 deletions(-) diff --git a/game/ai_actions.c b/game/ai_actions.c index 68d99d4..f26a10f 100644 --- a/game/ai_actions.c +++ b/game/ai_actions.c @@ -47,11 +47,89 @@ BzBTStatus aiResetElapsed(AIBlackboard *data, f32 dt) { return BZ_BT_SUCCESS; } +#define ENEMY_NEARBY_DST 26.0f + BzBTStatus aiIsEnemyNearby(AIBlackboard *data, f32 dt) { + if (data->seenEnemy && ecs_is_alive(ECS, data->seenEnemy)) { + Position enemyPos = *ecs_get(ECS, data->seenEnemy, Position); + Position pos = *ecs_get(ECS, data->entity, Position); + + if (Vector2Distance(enemyPos, pos) > ENEMY_NEARBY_DST) + return BZ_BT_SUCCESS; + } + + const f32 range = 20.0f; + Position pos = *ecs_get(ECS, data->entity, Position); + HitBox hb = *ecs_get(ECS, data->entity, HitBox); + Vector2 center = entityGetCenter(pos, hb); + Owner owner = *ecs_get(ECS, data->entity, Owner); + + ecs_entity_t closest = 0; + f32 closestDst = 10000.0f; + + Game *game = ecs_singleton_get_mut(ECS, Game); + BzSpatialGridIter it = bzSpatialGridIter(game->entityGrid, + center.x - range, center.y - range, + center.x + range, center.y + range); + while (bzSpatialGridQueryNext(&it)) { + ecs_entity_t other = *(ecs_entity_t *) it.data; + if (!ecs_is_alive(ECS, other)) continue; + + if (!ecs_has(ECS, other, Owner) || + ecs_get(ECS, other, Owner)->player == owner.player) + continue; + + Position otherPos = *ecs_get(ECS, other, Position); + HitBox otherHB = *ecs_get(ECS, other, HitBox); + Vector2 otherCenter = entityGetCenter(otherPos, otherHB); + + f32 dst = Vector2Distance(center, otherCenter); + if (dst > range) continue; + + if (!bzTileMapCanRayCastLine(&game->map, center, otherCenter)) + continue; + + if (dst < closestDst) { + closest = other; + closestDst = dst; + } + } + + if (closest) { + data->seenEnemy = closest; + return BZ_BT_SUCCESS; + } + return BZ_BT_FAIL; } -BzBTStatus aiEvadeTarget(AIBlackboard *data, f32 dt) { - return BZ_BT_SUCCESS; +BzBTStatus aiAttackEnemy(AIBlackboard *data, f32 dt) { + if (!ecs_is_alive(ECS, data->seenEnemy)) + return BZ_BT_SUCCESS; + + Position enemyPos = *ecs_get(ECS, data->seenEnemy, Position); + Position pos = *ecs_get(ECS, data->entity, Position); + + Vector2 dif = Vector2Subtract(enemyPos, pos); + // Overload steering + Steering *steering = ecs_get_mut(ECS, data->entity, Steering); + *steering = Vector2Normalize(dif); + + return BZ_BT_RUNNING; + +} +BzBTStatus aiEvadeEnemy(AIBlackboard *data, f32 dt) { + if (!ecs_is_alive(ECS, data->seenEnemy)) + return BZ_BT_SUCCESS; + + Position enemyPos = *ecs_get(ECS, data->seenEnemy, Position); + Position pos = *ecs_get(ECS, data->entity, Position); + + Vector2 dif = Vector2Subtract(pos, enemyPos); + // Overload steering + Steering *steering = ecs_get_mut(ECS, data->entity, Steering); + *steering = Vector2Normalize(dif); + + return BZ_BT_RUNNING; } BzBTStatus aiFindNextHarvestable(AIBlackboard *data, f32 dt) { diff --git a/game/ai_actions.h b/game/ai_actions.h index ff57e08..de4823d 100644 --- a/game/ai_actions.h +++ b/game/ai_actions.h @@ -9,6 +9,7 @@ typedef struct AIBlackboard { ecs_entity_t entity; + ecs_entity_t seenEnemy; Vector2 moveToPos; union { @@ -29,7 +30,8 @@ BzBTStatus aiMoveTo(AIBlackboard *data, f32 dt); BzBTStatus aiResetElapsed(AIBlackboard *data, f32 dt); BzBTStatus aiIsEnemyNearby(AIBlackboard *data, f32 dt); -BzBTStatus aiEvadeTarget(AIBlackboard *data, f32 dt); +BzBTStatus aiAttackEnemy(AIBlackboard *data, f32 dt); +BzBTStatus aiEvadeEnemy(AIBlackboard *data, f32 dt); // Worker diff --git a/game/entity_factory.c b/game/entity_factory.c index 9869ff6..ab0c73e 100644 --- a/game/entity_factory.c +++ b/game/entity_factory.c @@ -1,6 +1,7 @@ #include "entity_factory.h" #include "ai_actions.h" +#include "systems/systems.h" ecs_entity_t entityCreateEmpty() { ecs_entity_t e = ecs_new_id(ECS); @@ -18,7 +19,10 @@ ecs_entity_t entityCreateBaseUnit(const Position position, f32 size, Player play }; HitBox hitbox = getEntityHitBoxRec(tileID); ecs_entity_t e = entityCreateEmpty(); - ecs_set_ptr(ECS, e, Position, &position); + ecs_set(ECS, e, Position, { + position.x - size * 0.5f, + position.y + size * 0.5f, + }); f32 scl = size / region.rec.width; ecs_set(ECS, e, Size, {region.rec.width * scl, region.rec.height * scl}); hitbox.x *= scl; @@ -86,6 +90,9 @@ ecs_entity_t entityCreateSoldier(const Position position, Player player, Game *g unit.maxDamage = 10.0f; unit.attackCooldown = 1.0f; ecs_set_ptr(ECS, e, Unit, &unit); + setAIBehaviour(e, game->BTs.unit, &(AIBlackboard) { + .moveToPos = position, + }); return e; } ecs_entity_t entityCreateWarrior(const Position position, Player player, Game *game) { @@ -100,6 +107,9 @@ ecs_entity_t entityCreateWarrior(const Position position, Player player, Game *g unit.maxDamage = 22.0f; unit.attackCooldown = 1.8f; ecs_set_ptr(ECS, e, Unit, &unit); + setAIBehaviour(e, game->BTs.unit, &(AIBlackboard) { + .moveToPos = position, + }); return e; } @@ -118,6 +128,10 @@ ecs_entity_t entityCreateWorker(const Position position, Player player, Game *ga .lastChanged = -1000.0f }); + setAIBehaviour(e, game->BTs.unit, &(AIBlackboard) { + .moveToPos = position, + }); + return e; } diff --git a/game/game_state.h b/game/game_state.h index 9f954b3..291afd9 100644 --- a/game/game_state.h +++ b/game/game_state.h @@ -91,8 +91,9 @@ typedef struct Game { BzStackAlloc stackAlloc; struct { - BzBTNode *workerHarvest; - BzBTNode *moveTo; + BzBTNode *worker; + BzBTNode *unit; + BzBTNode *unitEvasive; } BTs; struct { BzObjectPool *pathData; diff --git a/game/main.c b/game/main.c index 6da6e50..8123cf5 100644 --- a/game/main.c +++ b/game/main.c @@ -259,25 +259,52 @@ bool init(void *userData) { }); game->pools.btNode = bzObjectPoolCreate(&(BzObjectPoolDesc) { .objectSize = bzBTGetNodeSize(), - .objectsPerPage = 64 + .objectsPerPage = 40 }); game->pools.btNodeState = bzObjectPoolCreate(&(BzObjectPoolDesc) { .objectSize = bzBTGetNodeStateSize(), .objectsPerPage = 1024, }); BzObjectPool *nodePool = game->pools.btNode; - // moveTo + // unit (aggressive) { BzBTNode *root = NULL; BzBTNode *node = NULL; root = bzBTMakeRoot(nodePool); - game->BTs.moveTo = root; + game->BTs.unit = root; - // Just a single action for now - BzBTNode *seq = bzBTCompSequence(nodePool, root); - node = bzBTAction(nodePool, seq, (BzBTActionFn) aiMoveTo); + BzBTNode *sel = bzBTCompSelector(nodePool, root); + BzBTNode *attackSeq = bzBTCompSequence(nodePool, sel); + { + node = bzBTAction(nodePool, attackSeq, (BzBTActionFn) aiIsEnemyNearby); + bzBTNodeSetName(node, "isEnemyNearby"); + + node = bzBTAction(nodePool, attackSeq, (BzBTActionFn) aiAttackEnemy); + bzBTNodeSetName(node, "attackEnemy"); + } + node = bzBTAction(nodePool, sel, (BzBTActionFn) aiMoveTo); bzBTNodeSetName(node, "moveTo"); - bzBTDecorDelay(nodePool, seq, 6.0f); + bzBTDecorDelay(nodePool, sel, 2.0f); + } + // Unit (evasive) + { + BzBTNode *root = NULL; + BzBTNode *node = NULL; + root = bzBTMakeRoot(nodePool); + game->BTs.unitEvasive = root; + + BzBTNode *sel = bzBTCompSelector(nodePool, root); + BzBTNode *evadeSeq = bzBTCompSequence(nodePool, sel); + { + node = bzBTAction(nodePool, evadeSeq, (BzBTActionFn) aiIsEnemyNearby); + bzBTNodeSetName(node, "isEnemyNearby"); + + node = bzBTAction(nodePool, evadeSeq, (BzBTActionFn) aiEvadeEnemy); + bzBTNodeSetName(node, "evadeEnemy"); + } + node = bzBTAction(nodePool, sel, (BzBTActionFn) aiMoveTo); + bzBTNodeSetName(node, "moveTo"); + bzBTDecorDelay(nodePool, sel, 2.0f); } // evade BzBTNode *evade = NULL; @@ -290,18 +317,16 @@ bool init(void *userData) { node = bzBTAction(nodePool, evadeSeq, (BzBTActionFn) aiIsEnemyNearby); bzBTNodeSetName(node, "enemyNearby"); - node = bzBTAction(nodePool, evadeSeq, (BzBTActionFn) aiEvadeTarget); + node = bzBTAction(nodePool, evadeSeq, (BzBTActionFn) aiEvadeEnemy); bzBTNodeSetName(node, "evadeTarget"); } - - } // worker harvest { BzBTNode *root = NULL; BzBTNode *node = NULL; root = bzBTMakeRoot(nodePool); - game->BTs.workerHarvest = root; + game->BTs.worker = root; BzBTNode *pSel = bzBTCompPSelector(nodePool, root); bzBTSubTree(evade, pSel); diff --git a/game/map_init.c b/game/map_init.c index 14891c4..f468517 100644 --- a/game/map_init.c +++ b/game/map_init.c @@ -377,7 +377,7 @@ void loadMap(Game *game, const char *path, bool mainMenu) { if (nearest.entity == 0) continue; ResourceType resType = ecs_get(ECS, nearest.entity, Resource)->type; - setAIBehaviour(workers[i].entity, game->BTs.workerHarvest, &(AIBlackboard) { + setAIBehaviour(workers[i].entity, game->BTs.worker, &(AIBlackboard) { .as.worker = { .harvestType = resType, .harvestTarget = nearest.entity, diff --git a/game/systems/s_input.c b/game/systems/s_input.c index 5d6050c..a7c30a7 100644 --- a/game/systems/s_input.c +++ b/game/systems/s_input.c @@ -115,6 +115,16 @@ void inputUnitAction(Game *game, InputState *input) { const i32 numUnits = ecs_query_entity_count(query); BZ_ASSERT(numUnits > 0); + ecs_entity_t *units = bzStackAlloc(&game->stackAlloc, sizeof(*units) * numUnits); + i32 unitIdx = 0; + ecs_iter_t it = ecs_query_iter(ECS, query); + while (ecs_query_next(&it)) { + for (i32 i = 0; i < it.count; i++) { + ecs_entity_t entity = it.entities[i]; + units[unitIdx++] = entity; + } + } + const MouseButton actionBtn = input->mapping.secondaryBtn; input->cursor = CURSOR_NONE; @@ -166,47 +176,46 @@ void inputUnitAction(Game *game, InputState *input) { qsort(harvestables, numHarvestables, sizeof(*harvestables), entityFloatPairCmp); i32 idxHarvestable = 0; - ecs_defer_begin(ECS); - ecs_iter_t it = ecs_query_iter(ECS, query); - while (ecs_query_next(&it)) { - for (i32 i = 0; i < it.count; i++) { - if (idxHarvestable >= numHarvestables) - break; - ecs_entity_t entity = it.entities[i]; + for (i32 i = 0; i < unitIdx; i++) { + if (idxHarvestable >= numHarvestables) + break; + ecs_entity_t entity = units[i]; - EntityFloatPair harvestEntity = {0, 0}; - while (idxHarvestable < numHarvestables) { - harvestEntity = harvestables[idxHarvestable++]; - Harvestable *harvestable = ecs_get_mut(ECS, harvestEntity.entity, Harvestable); - if (harvestable->harvestCount >= harvestable->harvestLimit) - continue; - harvestable->harvestCount++; - break; - } - Position target = *ecs_get(ECS, harvestEntity.entity, Position); - HitBox targetHB = *ecs_get(ECS, harvestEntity.entity, HitBox); - target = entityGetCenter(target, targetHB); - - f32 proximity = 4.0f; - - Worker *worker = ecs_get_mut(ECS, entity, Worker); - worker->carryRes = resource.type; - - setAIBehaviour(entity, game->BTs.workerHarvest, &(AIBlackboard) { - .as.worker = { - .harvestType = resource.type, - .harvestTarget = harvestEntity.entity, - .harvestPos = target, - }, - .proximity = proximity, - }); + EntityFloatPair harvestEntity = {0, 0}; + while (idxHarvestable < numHarvestables) { + harvestEntity = harvestables[idxHarvestable++]; + Harvestable *harvestable = ecs_get_mut(ECS, harvestEntity.entity, Harvestable); + if (harvestable->harvestCount >= harvestable->harvestLimit) + continue; + harvestable->harvestCount++; + break; } + Position target = *ecs_get(ECS, harvestEntity.entity, Position); + HitBox targetHB = *ecs_get(ECS, harvestEntity.entity, HitBox); + target = entityGetCenter(target, targetHB); + + f32 proximity = 4.0f; + + Worker *worker = ecs_get_mut(ECS, entity, Worker); + worker->carryRes = resource.type; + + setAIBehaviour(entity, game->BTs.worker, &(AIBlackboard) { + .as.worker = { + .harvestType = resource.type, + .harvestTarget = harvestEntity.entity, + .harvestPos = target, + }, + .proximity = proximity, + }); } - ecs_defer_end(ECS); } return; } + if ((taskEntity = queryEntity(game->entityGrid, input->mouseWorld, ecs_id(Owner))) && ecs_get(ECS, taskEntity, Owner)->player != game->player) { + input->cursor = CURSOR_ATTACK; + } + // Unit place position Vector2 *positions = bzStackAlloc(&game->stackAlloc, sizeof(*positions) * numUnits); Vector2 start = Vector2Zero(); @@ -254,13 +263,14 @@ void inputUnitAction(Game *game, InputState *input) { for (i32 i = 0; i < unitIdx; i++) { ecs_entity_t entity = entities[i].entity; - setAIBehaviour(entity, game->BTs.moveTo, &(AIBlackboard) { + setAIBehaviour(entity, game->BTs.unit, &(AIBlackboard) { .moveToPos = positions[i], .proximity = 1.0f, }); } } + bzStackAllocFree(&game->stackAlloc, units); } void updatePlayerInput() { @@ -450,7 +460,7 @@ void drawPlayerInputUIGround() { } } - if (input->unitPlacePos) { + if (input->unitPlacePos && input->cursor == CURSOR_NONE && input->numUnits >= 2) { for (i32 i = 0; i < input->numUnits; i++) { DrawCircleV(input->unitPlacePos[i], 2.0f, RED); } diff --git a/game/systems/s_ui.c b/game/systems/s_ui.c index b171dd6..7090d24 100644 --- a/game/systems/s_ui.c +++ b/game/systems/s_ui.c @@ -406,11 +406,11 @@ void drawMainMenuUI(Game *game, f32 dt) { if (uiMainMenuButton("Play", true)) { setScreen(game, SCREEN_GAME); unloadMap(game); - //loadMap(game, "assets/maps/tree_test.tmj"); - //loadMap(game, "assets/maps/entity_test.tmj"); - //loadMap(game, "assets/maps/worker_test.tmj"); - //loadMap(game, "assets/maps/battle_test.tmj"); - loadMap(game, "assets/maps/map_01.tmj", false); + //loadMap(game, "assets/maps/tree_test.tmj", false); + //loadMap(game, "assets/maps/entity_test.tmj", false); + //loadMap(game, "assets/maps/worker_test.tmj", false); + loadMap(game, "assets/maps/battle_test.tmj", false); + //loadMap(game, "assets/maps/map_01.tmj", false); } if (uiMainMenuButton("Settings", true)) { setScreen(game, SCREEN_SETTINGS);