Add chase/evade behaviour

This commit is contained in:
2024-02-13 18:27:53 +01:00
parent e6ddafd3e2
commit 24abf94faa
8 changed files with 189 additions and 59 deletions

View File

@@ -47,11 +47,89 @@ BzBTStatus aiResetElapsed(AIBlackboard *data, f32 dt) {
return BZ_BT_SUCCESS; return BZ_BT_SUCCESS;
} }
#define ENEMY_NEARBY_DST 26.0f
BzBTStatus aiIsEnemyNearby(AIBlackboard *data, f32 dt) { 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; return BZ_BT_FAIL;
} }
BzBTStatus aiEvadeTarget(AIBlackboard *data, f32 dt) { BzBTStatus aiAttackEnemy(AIBlackboard *data, f32 dt) {
return BZ_BT_SUCCESS; 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) { BzBTStatus aiFindNextHarvestable(AIBlackboard *data, f32 dt) {

View File

@@ -9,6 +9,7 @@
typedef struct AIBlackboard { typedef struct AIBlackboard {
ecs_entity_t entity; ecs_entity_t entity;
ecs_entity_t seenEnemy;
Vector2 moveToPos; Vector2 moveToPos;
union { union {
@@ -29,7 +30,8 @@ BzBTStatus aiMoveTo(AIBlackboard *data, f32 dt);
BzBTStatus aiResetElapsed(AIBlackboard *data, f32 dt); BzBTStatus aiResetElapsed(AIBlackboard *data, f32 dt);
BzBTStatus aiIsEnemyNearby(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 // Worker

View File

@@ -1,6 +1,7 @@
#include "entity_factory.h" #include "entity_factory.h"
#include "ai_actions.h" #include "ai_actions.h"
#include "systems/systems.h"
ecs_entity_t entityCreateEmpty() { ecs_entity_t entityCreateEmpty() {
ecs_entity_t e = ecs_new_id(ECS); 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); HitBox hitbox = getEntityHitBoxRec(tileID);
ecs_entity_t e = entityCreateEmpty(); 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; f32 scl = size / region.rec.width;
ecs_set(ECS, e, Size, {region.rec.width * scl, region.rec.height * scl}); ecs_set(ECS, e, Size, {region.rec.width * scl, region.rec.height * scl});
hitbox.x *= scl; hitbox.x *= scl;
@@ -86,6 +90,9 @@ ecs_entity_t entityCreateSoldier(const Position position, Player player, Game *g
unit.maxDamage = 10.0f; unit.maxDamage = 10.0f;
unit.attackCooldown = 1.0f; unit.attackCooldown = 1.0f;
ecs_set_ptr(ECS, e, Unit, &unit); ecs_set_ptr(ECS, e, Unit, &unit);
setAIBehaviour(e, game->BTs.unit, &(AIBlackboard) {
.moveToPos = position,
});
return e; return e;
} }
ecs_entity_t entityCreateWarrior(const Position position, Player player, Game *game) { 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.maxDamage = 22.0f;
unit.attackCooldown = 1.8f; unit.attackCooldown = 1.8f;
ecs_set_ptr(ECS, e, Unit, &unit); ecs_set_ptr(ECS, e, Unit, &unit);
setAIBehaviour(e, game->BTs.unit, &(AIBlackboard) {
.moveToPos = position,
});
return e; return e;
} }
@@ -118,6 +128,10 @@ ecs_entity_t entityCreateWorker(const Position position, Player player, Game *ga
.lastChanged = -1000.0f .lastChanged = -1000.0f
}); });
setAIBehaviour(e, game->BTs.unit, &(AIBlackboard) {
.moveToPos = position,
});
return e; return e;
} }

View File

@@ -91,8 +91,9 @@ typedef struct Game {
BzStackAlloc stackAlloc; BzStackAlloc stackAlloc;
struct { struct {
BzBTNode *workerHarvest; BzBTNode *worker;
BzBTNode *moveTo; BzBTNode *unit;
BzBTNode *unitEvasive;
} BTs; } BTs;
struct { struct {
BzObjectPool *pathData; BzObjectPool *pathData;

View File

@@ -259,25 +259,52 @@ bool init(void *userData) {
}); });
game->pools.btNode = bzObjectPoolCreate(&(BzObjectPoolDesc) { game->pools.btNode = bzObjectPoolCreate(&(BzObjectPoolDesc) {
.objectSize = bzBTGetNodeSize(), .objectSize = bzBTGetNodeSize(),
.objectsPerPage = 64 .objectsPerPage = 40
}); });
game->pools.btNodeState = bzObjectPoolCreate(&(BzObjectPoolDesc) { game->pools.btNodeState = bzObjectPoolCreate(&(BzObjectPoolDesc) {
.objectSize = bzBTGetNodeStateSize(), .objectSize = bzBTGetNodeStateSize(),
.objectsPerPage = 1024, .objectsPerPage = 1024,
}); });
BzObjectPool *nodePool = game->pools.btNode; BzObjectPool *nodePool = game->pools.btNode;
// moveTo // unit (aggressive)
{ {
BzBTNode *root = NULL; BzBTNode *root = NULL;
BzBTNode *node = NULL; BzBTNode *node = NULL;
root = bzBTMakeRoot(nodePool); root = bzBTMakeRoot(nodePool);
game->BTs.moveTo = root; game->BTs.unit = root;
// Just a single action for now BzBTNode *sel = bzBTCompSelector(nodePool, root);
BzBTNode *seq = bzBTCompSequence(nodePool, root); BzBTNode *attackSeq = bzBTCompSequence(nodePool, sel);
node = bzBTAction(nodePool, seq, (BzBTActionFn) aiMoveTo); {
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"); 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 // evade
BzBTNode *evade = NULL; BzBTNode *evade = NULL;
@@ -290,18 +317,16 @@ bool init(void *userData) {
node = bzBTAction(nodePool, evadeSeq, (BzBTActionFn) aiIsEnemyNearby); node = bzBTAction(nodePool, evadeSeq, (BzBTActionFn) aiIsEnemyNearby);
bzBTNodeSetName(node, "enemyNearby"); bzBTNodeSetName(node, "enemyNearby");
node = bzBTAction(nodePool, evadeSeq, (BzBTActionFn) aiEvadeTarget); node = bzBTAction(nodePool, evadeSeq, (BzBTActionFn) aiEvadeEnemy);
bzBTNodeSetName(node, "evadeTarget"); bzBTNodeSetName(node, "evadeTarget");
} }
} }
// worker harvest // worker harvest
{ {
BzBTNode *root = NULL; BzBTNode *root = NULL;
BzBTNode *node = NULL; BzBTNode *node = NULL;
root = bzBTMakeRoot(nodePool); root = bzBTMakeRoot(nodePool);
game->BTs.workerHarvest = root; game->BTs.worker = root;
BzBTNode *pSel = bzBTCompPSelector(nodePool, root); BzBTNode *pSel = bzBTCompPSelector(nodePool, root);
bzBTSubTree(evade, pSel); bzBTSubTree(evade, pSel);

View File

@@ -377,7 +377,7 @@ void loadMap(Game *game, const char *path, bool mainMenu) {
if (nearest.entity == 0) continue; if (nearest.entity == 0) continue;
ResourceType resType = ecs_get(ECS, nearest.entity, Resource)->type; 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 = { .as.worker = {
.harvestType = resType, .harvestType = resType,
.harvestTarget = nearest.entity, .harvestTarget = nearest.entity,

View File

@@ -115,6 +115,16 @@ void inputUnitAction(Game *game, InputState *input) {
const i32 numUnits = ecs_query_entity_count(query); const i32 numUnits = ecs_query_entity_count(query);
BZ_ASSERT(numUnits > 0); 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; const MouseButton actionBtn = input->mapping.secondaryBtn;
input->cursor = CURSOR_NONE; input->cursor = CURSOR_NONE;
@@ -166,47 +176,46 @@ void inputUnitAction(Game *game, InputState *input) {
qsort(harvestables, numHarvestables, sizeof(*harvestables), entityFloatPairCmp); qsort(harvestables, numHarvestables, sizeof(*harvestables), entityFloatPairCmp);
i32 idxHarvestable = 0; i32 idxHarvestable = 0;
ecs_defer_begin(ECS); for (i32 i = 0; i < unitIdx; i++) {
ecs_iter_t it = ecs_query_iter(ECS, query); if (idxHarvestable >= numHarvestables)
while (ecs_query_next(&it)) { break;
for (i32 i = 0; i < it.count; i++) { ecs_entity_t entity = units[i];
if (idxHarvestable >= numHarvestables)
break;
ecs_entity_t entity = it.entities[i];
EntityFloatPair harvestEntity = {0, 0}; EntityFloatPair harvestEntity = {0, 0};
while (idxHarvestable < numHarvestables) { while (idxHarvestable < numHarvestables) {
harvestEntity = harvestables[idxHarvestable++]; harvestEntity = harvestables[idxHarvestable++];
Harvestable *harvestable = ecs_get_mut(ECS, harvestEntity.entity, Harvestable); Harvestable *harvestable = ecs_get_mut(ECS, harvestEntity.entity, Harvestable);
if (harvestable->harvestCount >= harvestable->harvestLimit) if (harvestable->harvestCount >= harvestable->harvestLimit)
continue; continue;
harvestable->harvestCount++; harvestable->harvestCount++;
break; 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,
});
} }
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; 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 // Unit place position
Vector2 *positions = bzStackAlloc(&game->stackAlloc, sizeof(*positions) * numUnits); Vector2 *positions = bzStackAlloc(&game->stackAlloc, sizeof(*positions) * numUnits);
Vector2 start = Vector2Zero(); Vector2 start = Vector2Zero();
@@ -254,13 +263,14 @@ void inputUnitAction(Game *game, InputState *input) {
for (i32 i = 0; i < unitIdx; i++) { for (i32 i = 0; i < unitIdx; i++) {
ecs_entity_t entity = entities[i].entity; ecs_entity_t entity = entities[i].entity;
setAIBehaviour(entity, game->BTs.moveTo, &(AIBlackboard) { setAIBehaviour(entity, game->BTs.unit, &(AIBlackboard) {
.moveToPos = positions[i], .moveToPos = positions[i],
.proximity = 1.0f, .proximity = 1.0f,
}); });
} }
} }
bzStackAllocFree(&game->stackAlloc, units);
} }
void updatePlayerInput() { 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++) { for (i32 i = 0; i < input->numUnits; i++) {
DrawCircleV(input->unitPlacePos[i], 2.0f, RED); DrawCircleV(input->unitPlacePos[i], 2.0f, RED);
} }

View File

@@ -406,11 +406,11 @@ void drawMainMenuUI(Game *game, f32 dt) {
if (uiMainMenuButton("Play", true)) { if (uiMainMenuButton("Play", true)) {
setScreen(game, SCREEN_GAME); setScreen(game, SCREEN_GAME);
unloadMap(game); unloadMap(game);
//loadMap(game, "assets/maps/tree_test.tmj"); //loadMap(game, "assets/maps/tree_test.tmj", false);
//loadMap(game, "assets/maps/entity_test.tmj"); //loadMap(game, "assets/maps/entity_test.tmj", false);
//loadMap(game, "assets/maps/worker_test.tmj"); //loadMap(game, "assets/maps/worker_test.tmj", false);
//loadMap(game, "assets/maps/battle_test.tmj"); loadMap(game, "assets/maps/battle_test.tmj", false);
loadMap(game, "assets/maps/map_01.tmj", false); //loadMap(game, "assets/maps/map_01.tmj", false);
} }
if (uiMainMenuButton("Settings", true)) { if (uiMainMenuButton("Settings", true)) {
setScreen(game, SCREEN_SETTINGS); setScreen(game, SCREEN_SETTINGS);