diff --git a/CMakeLists.txt b/CMakeLists.txt index 2df3dee..496359c 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -18,6 +18,8 @@ add_executable(PixelDefense game/systems/systems.c game/systems/systems.h + game/ai_actions.c + game/ai_actions.h game/buildings.c game/buildings.h game/components.c @@ -38,10 +40,6 @@ add_executable(PixelDefense game/sounds.h game/ui_widgets.c game/ui_widgets.h - game/unit_actions.c - game/unit_actions.h - game/unit_ai.c - game/unit_ai.h ) diff --git a/engine/breeze/ai/behaviour_tree.c b/engine/breeze/ai/behaviour_tree.c index 79b26e4..5385751 100644 --- a/engine/breeze/ai/behaviour_tree.c +++ b/engine/breeze/ai/behaviour_tree.c @@ -211,6 +211,17 @@ bool bzBTNodeMatchesState(const BzBTNode *node, const BzBTNodeState *state) { return state && state->node == node; } +BzBTNode *bzBTCompStateGetRunningChild(const BzBTNodeState *state) { + BZ_ASSERT(state->node); + BzBTNodeType type = state->node->type; + bool isComposite = type == BZ_BT_COMP_SELECTOR || + type == BZ_BT_COMP_PARALLEL_SELECTOR || + type == BZ_BT_COMP_SEQUENCE || + type == BZ_BT_COMP_PARALLEL_SEQUENCE; + BZ_ASSERT(isComposite); + return state->as.composite.running; +} + i32 bzBTRepeatStateGetIter(const BzBTNodeState *state) { BZ_ASSERT(state->node && state->node->type == BZ_BT_DECOR_REPEAT); return state->as.repeat.iter; @@ -226,14 +237,14 @@ BzBTState bzBTCreateState(const BzBTStateDesc *desc) { BZ_ASSERT(desc->root); return (BzBTState) { .root = desc->root, - .first = NULL, - .last = NULL, + ._first = NULL, + ._last = NULL, .nodeStatePool = desc->pool, .userData = desc->userData }; } void bzBTDestroyState(BzBTState *state) { - BzBTNodeState *pNodeState = state->first; + BzBTNodeState *pNodeState = state->_first; while (pNodeState) { BzBTNodeState *next = pNodeState->next; bzObjectPoolRelease(state->nodeStatePool, pNodeState); @@ -244,16 +255,16 @@ void bzBTDestroyState(BzBTState *state) { void bzBTStateAppend(BzBTState *state, BzBTNodeState *nodeState) { nodeState->next = NULL; - nodeState->prev = state->last; - if (state->last) - state->last->next = nodeState; + nodeState->prev = state->_last; + if (state->_last) + state->_last->next = nodeState; else - state->first = nodeState; - state->last = nodeState; + state->_first = nodeState; + state->_last = nodeState; } void bzBTStatePop(BzBTState *state, BzBTNodeState *nodeState) { - if (state->first == nodeState) state->first = nodeState->next; - if (state->last == nodeState) state->last = nodeState->prev; + if (state->_first == nodeState) state->_first = nodeState->next; + if (state->_last == nodeState) state->_last = nodeState->prev; BzBTNodeState *next = nodeState->next; BzBTNodeState *prev = nodeState->prev; if (nodeState->prev) @@ -276,6 +287,7 @@ BzBTNodeState *bzBTStatePool(BzBTState *state, const BzBTNode *node) { return nodeState; } void bzBTStateRelease(BzBTState *state, BzBTNodeState *nodeState) { + bzBTStatePop(state, nodeState); bzObjectPoolRelease(state->nodeStatePool, nodeState); } @@ -368,7 +380,7 @@ static inline BzBTStatus bzBTExecuteComposite(const BzBTNode *node, f32 dt, } if (status == BZ_BT_ERROR) { - bzBTStatePop(newState, nodeState); + bzBTStateRelease(newState, nodeState); return BZ_BT_ERROR; } @@ -376,7 +388,7 @@ static inline BzBTStatus bzBTExecuteComposite(const BzBTNode *node, f32 dt, status == BZ_BT_FAIL; if (finished) { // Dummy state is no longer needed - bzBTStatePop(newState, nodeState); + bzBTStateRelease(newState, nodeState); } else { BZ_ASSERT(status == BZ_BT_RUNNING); nodeState->as.composite.running = child; @@ -463,7 +475,7 @@ static inline BzBTStatus bzBTExecuteDecorator(const BzBTNode *node, f32 dt, BZ_ASSERT(nodeState->node == node); nodeState->as.repeat.iter++; if (nodeState->as.repeat.iter >= node->as.repeat.n) { - bzBTStatePop(newState, nodeState); + bzBTStateRelease(newState, nodeState); status = inStatus; break; } @@ -497,7 +509,7 @@ static inline BzBTStatus bzBTExecuteNode(const BzBTNode *node, f32 dt, break; case BZ_BT_ACTION: BZ_ASSERT(node->as.action.fn); - return node->as.action.fn(oldState->userData); + return node->as.action.fn(oldState->userData, dt); } return status; } @@ -506,25 +518,43 @@ BzBTStatus bzBTExecute(BzBTState *state, f32 dt) { BZ_ASSERT(state->nodeStatePool); BZ_ASSERT(bzObjectPoolGetObjectSize(state->nodeStatePool) == bzBTGetNodeStateSize()); BZ_ASSERT(state); - BZ_ASSERT(state->root); + if (state->root == NULL) { + return BZ_BT_FAIL; + } BzBTState newState = { - .first = NULL, - .last = NULL, + ._first = NULL, + ._last = NULL, + .nodeStatePool = state->nodeStatePool }; - BzBTNodeState *first = state->first; + BzBTNodeState *first = state->_first; const BzBTNode *firstNode = first ? first->node : state->root; BzBTStatus status = bzBTExecuteNode(firstNode, dt, first, state, &newState); // Release leftover states - BzBTNodeState *pState = state->first; + BzBTNodeState *pState = state->_first; while (pState) { BzBTNodeState *next = pState->next; bzBTStateRelease(state, pState); pState = next; } - state->first = newState.first; - state->last = newState.last; + state->_first = newState._first; + state->_last = newState._last; + + switch (status) { + case BZ_BT_SUCCESS: + state->onSuccess && state->onSuccess(state->userData, dt); + break; + case BZ_BT_FAIL: + state->onFailure && state->onFailure(state->userData, dt); + break; + case BZ_BT_ERROR: + state->onError && state->onError(state->userData, dt); + break; + default: + break; + } + return status; } diff --git a/engine/breeze/ai/behaviour_tree.h b/engine/breeze/ai/behaviour_tree.h index bcf6258..9b43666 100644 --- a/engine/breeze/ai/behaviour_tree.h +++ b/engine/breeze/ai/behaviour_tree.h @@ -12,7 +12,7 @@ typedef enum BzBTStatus { BZ_BT_ERROR, } BzBTStatus; -typedef BzBTStatus(*BzBTActionFn)(void *data); +typedef BzBTStatus(*BzBTActionFn)(void *data, f32 dt); typedef enum BzBTNodeType { // Composite @@ -38,8 +38,12 @@ typedef struct BzBTNodeState BzBTNodeState; typedef struct BzBTState { const BzBTNode *root; - BzBTNodeState *first; - BzBTNodeState *last; + BzBTNodeState *_first; + BzBTNodeState *_last; + + BzBTActionFn onSuccess; + BzBTActionFn onFailure; + BzBTActionFn onError; BzObjectPool *nodeStatePool; void *userData; @@ -91,6 +95,8 @@ BzBTNode *bzBTNodeNext(const BzBTNode *node); const BzBTNodeState *bzBTNodeStateNext(const BzBTNodeState *state); bool bzBTNodeMatchesState(const BzBTNode *node, const BzBTNodeState *state); +BzBTNode *bzBTCompStateGetRunningChild(const BzBTNodeState *state); + i32 bzBTRepeatStateGetIter(const BzBTNodeState *state); f32 bzBTDelayStateGetElapsed(const BzBTNodeState *state); diff --git a/engine/tests/btree_test.c b/engine/tests/btree_test.c index ea1e081..7e453bf 100644 --- a/engine/tests/btree_test.c +++ b/engine/tests/btree_test.c @@ -7,7 +7,7 @@ BzObjectPool *nodeStatePool = NULL; BzBTNode *printBT = NULL; BzBTState agentState; -BzBTStatus printAction(void *data) { +BzBTStatus printAction(void *data, f32 dt) { bzLogInfo("Hello, world!"); return BZ_BT_SUCCESS; } @@ -48,7 +48,7 @@ void deinit(int *game) { bzObjectPoolDestroy(nodeStatePool); } -void igRenderBTNode(const BzBTNode *node, const BzBTNodeState *state, bool sameLine, i32 depth) { +void igVisualizeBTState(const BzBTNode *node, const BzBTNodeState *state, bool sameLine, i32 depth) { const BzBTNode *child = bzBTNodeChild(node); BzBTNodeType type = bzBTGetNodeType(node); char extraInfo[128]; @@ -109,7 +109,7 @@ void igRenderBTNode(const BzBTNode *node, const BzBTNodeState *state, bool sameL while (child) { if (hasSingleChild) igSameLine(0, 0); - igRenderBTNode(child, state, hasSingleChild, depth); + igVisualizeBTState(child, state, hasSingleChild, depth); child = bzBTNodeNext(child); } } @@ -117,7 +117,7 @@ void igRenderBTNode(const BzBTNode *node, const BzBTNodeState *state, bool sameL void igRenderBT(BzBTState *state) { const BzBTNode *root = state->root; if (igBegin("BehaviourTree", NULL, 0)) { - igRenderBTNode(root, state->first, false, 0); + igVisualizeBTState(root, state->_first, false, 0); } igEnd(); } diff --git a/game/ai_actions.c b/game/ai_actions.c new file mode 100644 index 0000000..3a28d12 --- /dev/null +++ b/game/ai_actions.c @@ -0,0 +1,200 @@ +#include "ai_actions.h" +#include "game_state.h" +#include "components.h" +#include "systems/systems.h" +#include "buildings.h" + +#include + +float shortestArc(float a, float b) { + if (fabs(b - a) < M_PI) + return b - a; + if (b > a) + return b - a - M_PI * 2.0f; + return b - a + M_PI * 2.0f; +} + +BzBTStatus aiMoveTo(AIBlackboard *data, f32 dt) { + Game *game = ecs_singleton_get_mut(ECS, Game); + const Vector2 pos = *ecs_get(ECS, data->entity, Position); + const Vector2 target = data->moveToPos; + f32 dst = Vector2Distance(pos, target); + if (dst < data->proximity) { + ecs_remove(ECS, data->entity, Path); + return BZ_BT_SUCCESS; + } + if (!ecs_has(ECS, data->entity, Path)) { + entitySetPath(data->entity, target, game); + } + if (ecs_has(ECS, data->entity, Orientation)) { + Orientation *orientation = ecs_get_mut(ECS, data->entity, Orientation); + f32 currentAngle = *orientation; + f32 targetAngle = Vector2Angle(pos, target); + f32 dif = shortestArc(currentAngle, targetAngle); + dif = Clamp(dif, -1, 1) * dt * 10; + *orientation += dif; + *orientation = fmodf(*orientation + 180.0f, 360.0f) - 180.0f; + } + return BZ_BT_RUNNING; +} + +BzBTStatus aiResetElapsed(AIBlackboard *data, f32 dt) { + data->elapsed = 0.0f; + return BZ_BT_SUCCESS; +} +BzBTStatus aiFindNextHarvestable(AIBlackboard *data, f32 dt) { + ecs_entity_t harvestTarget = data->as.worker.harvestTarget; + if (ecs_is_alive(ECS, harvestTarget)) { + BZ_ASSERT(ecs_has_id(ECS, harvestTarget, Harvestable)); + // Target still alive, no need to find next harvestable + data->moveToPos = data->as.worker.harvestPos; + return BZ_BT_SUCCESS; + } + + BZ_ASSERT(ecs_has(ECS, data->entity, Worker)); + Worker *worker = ecs_get_mut(ECS, data->entity, Worker); + Game *game = ecs_singleton_get_mut(ECS, Game); + + ResourceType harvestType = data->as.worker.harvestType; + + // Perform spatial search + + ecs_entity_t closest = 0; + f32 closestDst = INFINITY; + Vector2 closestPos = Vector2Zero(); + + const f32 range = 20.0f; + f32 hRange = range * 0.5f; + Vector2 pos = data->as.worker.harvestPos; // Last know harvest pos + 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_id(ECS, entity, Harvestable) || + !ecs_has(ECS, entity, Resource) || + !ecs_has(ECS, entity, Position)) + continue; + Resource resource = *ecs_get(ECS, entity, Resource); + Position resPos = *ecs_get(ECS, entity, Position); + if (resource.type != harvestType) continue; + + f32 dst = Vector2Distance(pos, resPos); + if (dst < closestDst) { + closest = entity; + closestDst = dst; + closestPos = resPos; + } + } + + if (closest) { + data->as.worker.harvestTarget = closest; + data->as.worker.harvestPos = closestPos; + data->moveToPos = closestPos; + return BZ_BT_SUCCESS; + } + + return BZ_BT_FAIL; +} +BzBTStatus aiFindNearestStorage(AIBlackboard *data, f32 dt) { + ecs_filter_t *storageFilter = ecs_filter(ECS, { + .terms = {{ecs_id(Position)}, {ecs_id(Storage)}}, + }); + ecs_iter_t it = ecs_filter_iter(ECS, storageFilter); + + const Vector2 pos = *ecs_get(ECS, data->entity, Position); + + ecs_entity_t closest = 0; + f32 closestDst = INFINITY; + Position closestPos = {INFINITY, INFINITY}; + while (ecs_filter_next(&it)) { + Position *storagePos = ecs_field(&it, Position, 1); + + for (i32 i = 0; i < it.count; i++) { + f32 dst = Vector2Distance(pos, closestPos); + if (closestDst == INFINITY || dst < closestDst) { + closest = it.entities[i]; + closestDst = dst; + closestPos = storagePos[i]; + } + } + } + ecs_filter_fini(storageFilter); + + if (!closest) { + return BZ_BT_FAIL; + } + + data->as.worker.depositTarget = closest; + data->moveToPos = getPositionNearBuilding(closest, pos); + + return BZ_BT_SUCCESS; +} +BzBTStatus aiHarvestRes(AIBlackboard *data, f32 dt) { + BZ_ASSERT(ecs_has(ECS, data->entity, Worker)); + Worker *worker = ecs_get_mut(ECS, data->entity, Worker); + if (worker->carry >= worker->carryCapacity) + return BZ_BT_FAIL; + + ecs_entity_t harvestTarget = data->as.worker.harvestTarget; + if (!ecs_is_alive(ECS, harvestTarget)) + return BZ_BT_FAIL; + + BZ_ASSERT(ecs_has_id(ECS, harvestTarget, Harvestable)); + if (data->elapsed < worker->collectSpeed) { + data->elapsed += dt; + return BZ_BT_RUNNING; + } + data->elapsed = 0; + + // Collect + i32 spareCapacity = worker->carryCapacity - worker->carry; + BZ_ASSERT(spareCapacity >= 0); + i32 collected = harvestEvent(harvestTarget, (HarvestEvent) { + .amount = BZ_MIN(1, spareCapacity) + }); + worker->carry += collected; + + return BZ_BT_SUCCESS; +} +BzBTStatus aiDepositRes(AIBlackboard *data, f32 dt) { + BZ_ASSERT(ecs_has(ECS, data->entity, Worker)); + Worker *worker = ecs_get_mut(ECS, data->entity, Worker); + + if (worker->carry == 0) + return BZ_BT_SUCCESS; + + ecs_entity_t depositTarget = data->as.worker.depositTarget; + + if (!ecs_is_alive(ECS, depositTarget)) + return BZ_BT_FAIL; + + if (data->elapsed < worker->depositSpeed) { + data->elapsed += dt; + return BZ_BT_RUNNING; + } + + depositEvent(depositTarget, (DepositEvent) { + .amount = worker->carry + }); + worker->carry = 0; + + return BZ_BT_SUCCESS; +} +BzBTStatus aiCarryCapacityFull(AIBlackboard *data) { + BZ_ASSERT(ecs_has(ECS, data->entity, Worker)); + Worker *worker = ecs_get_mut(ECS, data->entity, Worker); + if (worker->carry >= worker->carryCapacity) + return BZ_BT_SUCCESS; + return BZ_BT_FAIL; +} +BzBTStatus aiCarryCapacityEmpty(AIBlackboard *data) { + BZ_ASSERT(ecs_has(ECS, data->entity, Worker)); + Worker *worker = ecs_get_mut(ECS, data->entity, Worker); + if (worker->carry == 0) + return BZ_BT_SUCCESS; + return BZ_BT_FAIL; +} diff --git a/game/ai_actions.h b/game/ai_actions.h new file mode 100644 index 0000000..d00a01e --- /dev/null +++ b/game/ai_actions.h @@ -0,0 +1,43 @@ +#ifndef PIXELDEFENSE_AI_ACTIONS_H +#define PIXELDEFENSE_AI_ACTIONS_H + +#include +#include + +#include "components.h" + +typedef struct AIBlackboard { + ecs_entity_t entity; + + Vector2 moveToPos; + + union { + struct { + ResourceType harvestType; + Vector2 harvestPos; + ecs_entity_t harvestTarget; + ecs_entity_t depositTarget; + } worker; + } as; + + f32 proximity; + f32 elapsed; +} AIBlackboard; + + +BzBTStatus aiMoveTo(AIBlackboard *data, f32 dt); +BzBTStatus aiResetElapsed(AIBlackboard *data, f32 dt); + +// Worker + +BzBTStatus aiFindNextHarvestable(AIBlackboard *data, f32 dt); +BzBTStatus aiFindNearestStorage(AIBlackboard *data, f32 dt); +BzBTStatus aiHarvestRes(AIBlackboard *data, f32 dt); +BzBTStatus aiDepositRes(AIBlackboard *data, f32 dt); +BzBTStatus aiCarryCapacityFull(AIBlackboard *data); +BzBTStatus aiCarryCapacityEmpty(AIBlackboard *data); +//BzBTStatus aiIsTargetHarvestable(AIBlackboard *data); +//BzBTStatus aiIsTargetStorage(AIBlackboard *data); + + +#endif //PIXELDEFENSE_AI_ACTIONS_H diff --git a/game/components.c b/game/components.c index 051d9c2..db3d70b 100644 --- a/game/components.c +++ b/game/components.c @@ -1,7 +1,6 @@ #include "components.h" -#include "unit_ai.h" -#include "unit_actions.h" +#include "ai_actions.h" ECS_TAG_DECLARE(GameEntity); @@ -26,8 +25,8 @@ ECS_COMPONENT_DECLARE(Easing); ECS_COMPONENT_DECLARE(Arms); ECS_COMPONENT_DECLARE(Arm); -ECS_COMPONENT_DECLARE(UnitAI); -ECS_COMPONENT_DECLARE(UnitAction); +ECS_COMPONENT_DECLARE(BzBTState); +ECS_COMPONENT_DECLARE(AIBlackboard); ECS_TAG_DECLARE(Selectable); ECS_TAG_DECLARE(Selected); @@ -65,8 +64,8 @@ void initComponentIDs(ecs_world_t *ecs) { ECS_COMPONENT_DEFINE(ecs, Arms); ECS_COMPONENT_DEFINE(ecs, Arm); - ECS_COMPONENT_DEFINE(ecs, UnitAI); - ECS_COMPONENT_DEFINE(ecs, UnitAction); + ECS_COMPONENT_DEFINE(ecs, BzBTState); + ECS_COMPONENT_DEFINE(ecs, AIBlackboard); ECS_TAG_DEFINE(ecs, Selectable); ECS_TAG_DEFINE(ecs, Selected); @@ -191,14 +190,90 @@ void igArm(ecs_world_t *ecs, } -void igUnitAction(ecs_world_t *ecs, - ecs_entity_t entity, ecs_entity_t comp) { +void igVisualizeBTState(const BzBTNode *node, const BzBTNodeState *state, + bool isActive, bool sameLine, i32 depth) { + const BzBTNode *child = bzBTNodeChild(node); + BzBTNodeType type = bzBTGetNodeType(node); + char extraInfo[128]; + extraInfo[0] = '\0'; + + bool hasState = bzBTNodeMatchesState(node, state); + isActive |= hasState; + + switch (type) { + case BZ_BT_DECOR_REPEAT: + if (hasState) { + snprintf(extraInfo, sizeof(extraInfo), " (%d < %d)", + bzBTRepeatStateGetIter(state), + bzBTDecorGetRepeat(node)); + } else { + snprintf(extraInfo, sizeof(extraInfo), " (%d)", bzBTDecorGetRepeat(node)); + } + break; + case BZ_BT_DECOR_DELAY: + if (hasState) { + snprintf(extraInfo, sizeof(extraInfo), " (%.2f < %.2fms)", + bzBTDelayStateGetElapsed(state), + bzBTDecorGetDelay(node)); + } else { + snprintf(extraInfo, sizeof(extraInfo), " (%.2fms)", bzBTDecorGetDelay(node)); + } + break; + case BZ_BT_ACTION: + snprintf(extraInfo, sizeof(extraInfo), " (%s:%p)", + bzBTNodeGetName(node) ? bzBTNodeGetName(node) : "?", + bzBTActionGetFn(node)); + break; + default: + break; + } + + + ImVec4 color = {1.0f, 1.0f, 1.0f, 1.0f}; + if (isActive) + color = (ImVec4) {1.0f, 1.0f, 0.5f, 1.0f}; + + bool hasSingleChild = true; + if (child && bzBTNodeNext(child)) hasSingleChild = false; + + const char *suffix = hasSingleChild ? " > " : ": "; + + if (sameLine) { + igTextColored(color, "%s%s%s", bzBTNodeTypeToStr(type), + extraInfo, suffix); + } else { + igTextColored(color, "%*s%s %s", + depth * 2, "", + bzBTNodeTypeToStr(type), extraInfo, suffix); + depth++; + } + + bool isComposite = type == BZ_BT_COMP_SELECTOR || + type == BZ_BT_COMP_PARALLEL_SELECTOR || + type == BZ_BT_COMP_SEQUENCE || + type == BZ_BT_COMP_PARALLEL_SEQUENCE; + + while (child) { + if (hasSingleChild) igSameLine(0, 0); + bool childActive = isActive && hasSingleChild; + if (hasState && isComposite && !childActive) + childActive = bzBTCompStateGetRunningChild(state) == child; + const BzBTNodeState *childState = state; + if (hasState) + childState = bzBTNodeStateNext(state); + igVisualizeBTState(child, childState, childActive, hasSingleChild, depth); + child = bzBTNodeNext(child); + } +} +void igBzBTState(ecs_world_t *ecs, + ecs_entity_t entity, ecs_entity_t comp) { } -void igUnitAI(ecs_world_t *ecs, - ecs_entity_t entity, ecs_entity_t comp) { +void igAIBlackboard(ecs_world_t *ecs, + ecs_entity_t entity, ecs_entity_t comp) { } + void igWorker(ecs_world_t *ecs, ecs_entity_t entity, ecs_entity_t comp) { diff --git a/game/components.h b/game/components.h index ae46f62..93f14ae 100644 --- a/game/components.h +++ b/game/components.h @@ -159,8 +159,8 @@ typedef struct Arm { } Arm; extern ECS_COMPONENT_DECLARE(Arm); -extern ECS_COMPONENT_DECLARE(UnitAction); -extern ECS_COMPONENT_DECLARE(UnitAI); +extern ECS_COMPONENT_DECLARE(BzBTState); +extern ECS_COMPONENT_DECLARE(AIBlackboard); extern ECS_TAG_DECLARE(Selectable); extern ECS_TAG_DECLARE(Selected); @@ -235,10 +235,13 @@ void igArms(ecs_world_t *ecs, void igArm(ecs_world_t *ecs, ecs_entity_t entity, ecs_entity_t comp); -void igUnitAction(ecs_world_t *ecs, - ecs_entity_t entity, ecs_entity_t comp); -void igUnitAI(ecs_world_t *ecs, - ecs_entity_t entity, ecs_entity_t comp); +void igVisualizeBTState(const BzBTNode *node, const BzBTNodeState *state, + bool isActive, bool sameLine, i32 depth); +void igBzBTState(ecs_world_t *ecs, + ecs_entity_t entity, ecs_entity_t comp); +void igAIBlackboard(ecs_world_t *ecs, + ecs_entity_t entity, ecs_entity_t comp); + void igWorker(ecs_world_t *ecs, ecs_entity_t entity, ecs_entity_t comp); void igUnit(ecs_world_t *ecs, diff --git a/game/entity_factory.c b/game/entity_factory.c index 736583d..27bbe1e 100644 --- a/game/entity_factory.c +++ b/game/entity_factory.c @@ -1,5 +1,6 @@ #include "entity_factory.h" -#include "unit_actions.h" + +#include "ai_actions.h" ecs_entity_t entityCreateEmpty() { ecs_entity_t e = ecs_new_id(ECS); @@ -35,7 +36,11 @@ ecs_entity_t entityCreateWorker(const Position position, Game *game) { .curFrame = 0, .elapsed = 0.0f, }); - ecs_set(ECS, e, UnitAction, { NULL, NULL }); + ecs_set(ECS, e, BzBTState, { + .root = NULL, + .nodeStatePool = game->pools.btNodeState + }); + ecs_set(ECS, e, AIBlackboard, {.entity = e}); ecs_add_id(ECS, e, Selectable); ecs_set(ECS, e, Unit, { .acceleration = 80.0f, diff --git a/game/game_state.h b/game/game_state.h index 401c8b4..92b196e 100644 --- a/game/game_state.h +++ b/game/game_state.h @@ -37,9 +37,14 @@ typedef struct Game { i64 pop; } resources; BzStackAlloc stackAlloc; + struct { + BzBTNode *workerHarvest; + BzBTNode *moveTo; + } BTs; struct { BzObjectPool *pathData; - BzObjectPool *actions; + BzObjectPool *btNode; + BzObjectPool *btNodeState; } pools; struct { bool drawPath; diff --git a/game/main.c b/game/main.c index 979f6eb..2de2cdf 100644 --- a/game/main.c +++ b/game/main.c @@ -12,8 +12,6 @@ #include "map_layers.h" #include "buildings.h" #include "ui_widgets.h" -#include "unit_ai.h" -#include "unit_actions.h" #include "pathfinding.h" #include "sounds.h" @@ -215,10 +213,74 @@ bool init(void *userData) { .objectSize = sizeof(PathData), .objectsPerPage = 512 }); - game->pools.actions = bzObjectPoolCreate(&(BzObjectPoolDesc) { - .objectSize = sizeof(Action), + game->pools.btNode = bzObjectPoolCreate(&(BzObjectPoolDesc) { + .objectSize = bzBTGetNodeSize(), + .objectsPerPage = 64 + }); + game->pools.btNodeState = bzObjectPoolCreate(&(BzObjectPoolDesc) { + .objectSize = bzBTGetNodeStateSize(), .objectsPerPage = 1024, }); + BzObjectPool *nodePool = game->pools.btNode; + // moveTo + { + BzBTNode *root = NULL; + BzBTNode *node = NULL; + root = bzBTMakeRoot(nodePool); + game->BTs.moveTo = root; + + // Just a single action for now + node = bzBTAction(nodePool, root, (BzBTActionFn) aiMoveTo); + bzBTNodeSetName(node, "moveTo"); + } + // worker harvest + { + BzBTNode *root = NULL; + BzBTNode *node = NULL; + root = bzBTMakeRoot(nodePool); + game->BTs.workerHarvest = root; + + //node = bzBTDecorUntilFail(nodePool, root); + + BzBTNode *collectSeq = bzBTCompSequence(nodePool, root, false); + + BzBTNode *untilFail = bzBTDecorUntilFail(nodePool, collectSeq); + { + BzBTNode *untilSeq = bzBTCompSequence(nodePool, untilFail, false); + node = bzBTAction(nodePool, untilSeq, (BzBTActionFn) aiFindNextHarvestable); + bzBTNodeSetName(node, "findNextHarvestable"); + node = bzBTAction(nodePool, untilSeq, (BzBTActionFn) aiMoveTo); + bzBTNodeSetName(node, "moveTo"); + node = bzBTAction(nodePool, untilSeq, (BzBTActionFn) aiResetElapsed); + bzBTNodeSetName(node, "resetElapsed"); + node = bzBTAction(nodePool, untilSeq, (BzBTActionFn) aiHarvestRes); + bzBTNodeSetName(node, "harvestRes"); + node = bzBTDecorInvert(nodePool, untilSeq); + node = bzBTAction(nodePool, node, (BzBTActionFn) aiCarryCapacityFull); + bzBTNodeSetName(node, "carryCapacityFull"); + } + node = bzBTDecorInvert(nodePool, collectSeq); + node = bzBTAction(nodePool, node, (BzBTActionFn) aiCarryCapacityEmpty); + bzBTNodeSetName(node, "carryCapacityEmpty"); + + node = bzBTAction(nodePool, collectSeq, (BzBTActionFn) aiFindNearestStorage); + bzBTNodeSetName(node, "findNearestStorage"); + node = bzBTAction(nodePool, collectSeq, (BzBTActionFn) aiMoveTo); + bzBTNodeSetName(node, "moveTo"); + node = bzBTAction(nodePool, collectSeq, (BzBTActionFn) aiResetElapsed); + bzBTNodeSetName(node, "resetElapsed"); + node = bzBTAction(nodePool, collectSeq, (BzBTActionFn) aiDepositRes); + bzBTNodeSetName(node, "depositRes"); + node = bzBTDecorDelay(nodePool, collectSeq, 1.0f); + + + //node = bzBTAction(nodePool, collectSeq, NULL); + //bzBTNodeSetName(node, "harvest"); + //node = bzBTAction(nodePool, collectSeq, NULL); + //bzBTNodeSetName(node, "moveTo"); + //node = bzBTAction(nodePool, collectSeq, NULL); + //bzBTNodeSetName(node, "deposit"); + } game->frameDuration = 0.16f; @@ -273,7 +335,8 @@ void deinit(void *userData) { bzStackAllocDestroy(&game->stackAlloc); bzObjectPoolDestroy(game->pools.pathData); - bzObjectPoolDestroy(game->pools.actions); + bzObjectPoolDestroy(game->pools.btNode); + bzObjectPoolDestroy(game->pools.btNodeState); bzArrayDestroy(game->drawData); @@ -660,6 +723,14 @@ void igInspectWindow(ecs_entity_t entity, bool *open) { igTagCheckbox("Workable", ECS, entity, Workable); igTagCheckbox("Attackable", ECS, entity, Attackable); } + if (ecs_has(ECS, entity, BzBTState) && + igCollapsingHeader_TreeNodeFlags("BehaviourTree", 0)) { + const BzBTState *state = ecs_get(ECS, entity, BzBTState); + if (state->root) + igVisualizeBTState(state->root, state->_first, true, false, 0); + else + igTextColored((ImVec4) {1, 0, 0, 1}, "NONE"); + } igInspectComp("Resource", entity, ecs_id(Resource), igResource); igInspectComp("Owner", entity, ecs_id(Owner), igOwner); igInspectComp("SpatialGridID", entity, ecs_id(SpatialGridID), igSpatialGridID); @@ -675,8 +746,8 @@ void igInspectWindow(ecs_entity_t entity, bool *open) { igInspectComp("Easing", entity, ecs_id(Easing), igEasing); igInspectComp("Arms", entity, ecs_id(Arms), igArms); igInspectComp("Arm", entity, ecs_id(Arm), igArm); - igInspectComp("UnitAction", entity, ecs_id(UnitAction), igUnitAction); - igInspectComp("UnitAI", entity, ecs_id(UnitAI), igUnitAI); + igInspectComp("BzBTState", entity, ecs_id(BzBTState), igBzBTState); + igInspectComp("AIBlackboard", entity, ecs_id(AIBlackboard), igAIBlackboard); igInspectComp("Worker", entity, ecs_id(Worker), igWorker); igInspectComp("Unit", entity, ecs_id(Unit), igUnit); } @@ -691,8 +762,9 @@ void imguiRender(float dt, void *userData) { igSetNextWindowSize((ImVec2){300, 400}, ImGuiCond_FirstUseEver); igBegin("Debug Menu", NULL, 0); - igText("PathData pool available: %llu", bzObjectPoolGetNumFree(game->pools.pathData)); - igText("Action pool available: %llu", bzObjectPoolGetNumFree(game->pools.actions)); + igText("PathData pool available: %llu", bzObjectPoolGetNumFree(game->pools.pathData)); + igText("BTNode pool available: %llu", bzObjectPoolGetNumFree(game->pools.btNode)); + igText("BTNodeState pool available: %llu", bzObjectPoolGetNumFree(game->pools.btNodeState)); const char *inputState = "NONE"; switch (input->state) { case INPUT_NONE: diff --git a/game/map_init.c b/game/map_init.c index eba098c..4680f6f 100644 --- a/game/map_init.c +++ b/game/map_init.c @@ -8,9 +8,6 @@ #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++) { diff --git a/game/systems/s_ai.c b/game/systems/s_ai.c index f5217ea..a65beaa 100644 --- a/game/systems/s_ai.c +++ b/game/systems/s_ai.c @@ -1,41 +1,47 @@ #include "systems.h" #include "../game_state.h" +#include "../ai_actions.h" -#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); +void updateAISystem(ecs_iter_t *it) { + Game *game = ecs_singleton_get_mut(ECS, Game); + BzBTState *state = ecs_field(it, BzBTState, 1); + + f32 dt = GetFrameTime(); for (i32 i = 0; i < it->count; i++) { ecs_entity_t entity = it->entities[i]; - handleAction(entity, &action[i], game); + if (state[i].root == NULL) { + // No behaviour + continue; + } + if (ecs_has(ECS, entity, AIBlackboard)) { + AIBlackboard *blackboard = ecs_get_mut(ECS, entity, AIBlackboard); + blackboard->entity = entity; + state[i].userData = blackboard; + } + bzBTExecute(&state[i], dt); } } -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); +void setAIBehaviour(ecs_entity_t entity, const BzBTNode *root, + const AIBlackboard *blackboard) { + Game *game = ecs_singleton_get_mut(ECS, Game); + BZ_ASSERT(ecs_has(ECS, entity, BzBTState)); + BZ_ASSERT(!blackboard || ecs_has(ECS, entity, AIBlackboard)); - for (i32 i = 0; i < it->count; i++) { - ecs_entity_t entity = it->entities[i]; - - Action *firstAction = action[i].first; - unitAI[i].action = firstAction; - - updateUnitAI(entity, &unitAI[i], game); + if (blackboard) { + AIBlackboard *b = ecs_get_mut(ECS, entity, AIBlackboard); + *b = *blackboard; + if (b->proximity < 2.0f) + b->proximity = 2.0f; } -} -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); - } + BzBTState *state = ecs_get_mut(ECS, entity, BzBTState); + bzBTDestroyState(state); + *state = bzBTCreateState(&(BzBTStateDesc) { + .root = root, + .pool = game->pools.btNodeState + }); } diff --git a/game/systems/s_input.c b/game/systems/s_input.c index 8d71a1f..3c717b7 100644 --- a/game/systems/s_input.c +++ b/game/systems/s_input.c @@ -4,8 +4,6 @@ #include "../input.h" #include "../buildings.h" #include "../pathfinding.h" -#include "../unit_ai.h" -#include "../unit_actions.h" #include #include @@ -92,12 +90,22 @@ 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); + setAIBehaviour(entity, game->BTs.workerHarvest, &(AIBlackboard) { + .as.worker = { + .harvestType = RES_WOOD, + .harvestTarget = taskEntity, + .harvestPos = target, + }, + .proximity = 6.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, @@ -128,12 +136,18 @@ void inputUnitAction(Game *game, InputState *input) { while (ecs_iter_next(&it)) { for (i32 i = 0; i < it.count; i++) { const ecs_entity_t entity = it.entities[i]; + setAIBehaviour(entity, game->BTs.moveTo, &(AIBlackboard) { + .moveToPos = target, + .proximity = 6.0f, + }); + /* clearActions(entity, game); addAction(entity, game, &(const Action) { .type = ACTION_MOVE_TO, .as.moveTo.target = target, .as.moveTo.proximityThreshold = 6.0f, }); + */ } } ecs_defer_end(ECS); diff --git a/game/systems/systems.c b/game/systems/systems.c index eecd230..600d3e6 100644 --- a/game/systems/systems.c +++ b/game/systems/systems.c @@ -114,10 +114,7 @@ void setupSystems() { ECS_SYSTEM(ECS, entityFollowPath, EcsOnUpdate, Path); ECS_SYSTEM(ECS, entityUpdateArms, EcsOnUpdate, Position, Velocity, Rotation, Orientation, Arms); - 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, updateAISystem, EcsOnUpdate, BzBTState); ECS_SYSTEM(ECS, updateAnimationState, EcsOnUpdate, Animation, TextureRegion); ECS_SYSTEM(ECS, updateAnimation, EcsOnUpdate, Animation, TextureRegion); diff --git a/game/systems/systems.h b/game/systems/systems.h index d120d5c..a825a09 100644 --- a/game/systems/systems.h +++ b/game/systems/systems.h @@ -4,6 +4,7 @@ #include #include "../components.h" +#include "../ai_actions.h" typedef struct Game Game; @@ -19,22 +20,12 @@ bool entitySetPath(const ecs_entity_t entity, const Vector2 target, Game *game); /* * 0: Game (singleton) - * 1: UnitAction + * 1: BzBTState */ -void handleUnitActionsSystem(ecs_iter_t *it); +void updateAISystem(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); +void setAIBehaviour(ecs_entity_t entity, const BzBTNode *root, + const AIBlackboard *blackboard); /**********************************