From e69fdeed1f44a12d37e3265ee1741ab87470bf9e Mon Sep 17 00:00:00 2001 From: Klemen Plestenjak Date: Mon, 12 Feb 2024 14:14:23 +0100 Subject: [PATCH] Enemy swarm movement --- assets/maps/map_01.tmj | 172 ++++++++++++++++++++++++++++++++++++++-- game/components.h | 3 +- game/constants.h | 2 + game/entity_factory.c | 51 +++++++++++- game/entity_factory.h | 4 +- game/game_state.h | 6 ++ game/main.c | 2 +- game/map_init.c | 24 ++++++ game/systems/s_entity.c | 106 ++++++++++++++++++++++++- game/systems/s_ui.c | 4 +- game/systems/systems.c | 1 + game/systems/systems.h | 11 +++ game/wave.c | 27 ++++++- game/wave.h | 4 +- rawAssets/map_01.tmx | 9 ++- 15 files changed, 402 insertions(+), 24 deletions(-) diff --git a/assets/maps/map_01.tmj b/assets/maps/map_01.tmj index 085c577..443caf1 100644 --- a/assets/maps/map_01.tmj +++ b/assets/maps/map_01.tmj @@ -447,8 +447,8 @@ "type":"", "visible":true, "width":10, - "x":1126.83666666667, - "y":592.916666666667 + "x":1126.84, + "y":592.917 }, { "gid":28, @@ -459,8 +459,8 @@ "type":"", "visible":true, "width":10, - "x":1140.66666666667, - "y":594.666666666667 + "x":1140.67, + "y":594.667 }, { "gid":28, @@ -471,8 +471,8 @@ "type":"", "visible":true, "width":10, - "x":1125.66666666667, - "y":609.666666666667 + "x":1125.67, + "y":609.667 }, { "gid":28, @@ -484,7 +484,7 @@ "visible":true, "width":10, "x":1139, - "y":608.666666666667 + "y":608.667 }, { "gid":28, @@ -520,6 +520,162 @@ "width":0, "x":1119, "y":571.5 + }, + { + "height":0, + "id":8, + "name":"spawn", + "point":true, + "rotation":0, + "type":"", + "visible":true, + "width":0, + "x":32.0605727272727, + "y":768.242090909091 + }, + { + "height":0, + "id":9, + "name":"p1", + "point":true, + "rotation":0, + "type":"", + "visible":true, + "width":0, + "x":150, + "y":510 + }, + { + "height":0, + "id":20, + "name":"p0", + "point":true, + "rotation":0, + "type":"", + "visible":true, + "width":0, + "x":97.5, + "y":644.5 + }, + { + "height":0, + "id":10, + "name":"p2", + "point":true, + "rotation":0, + "type":"", + "visible":true, + "width":0, + "x":226, + "y":353.333 + }, + { + "height":0, + "id":11, + "name":"p3", + "point":true, + "rotation":0, + "type":"", + "visible":true, + "width":0, + "x":331.333, + "y":254 + }, + { + "height":0, + "id":12, + "name":"p4", + "point":true, + "rotation":0, + "type":"", + "visible":true, + "width":0, + "x":458, + "y":214.667 + }, + { + "height":0, + "id":13, + "name":"p5", + "point":true, + "rotation":0, + "type":"", + "visible":true, + "width":0, + "x":613.333, + "y":198.667 + }, + { + "height":0, + "id":14, + "name":"p6", + "point":true, + "rotation":0, + "type":"", + "visible":true, + "width":0, + "x":744, + "y":226.667 + }, + { + "height":0, + "id":15, + "name":"p7", + "point":true, + "rotation":0, + "type":"", + "visible":true, + "width":0, + "x":830.667, + "y":266.667 + }, + { + "height":0, + "id":16, + "name":"p8", + "point":true, + "rotation":0, + "type":"", + "visible":true, + "width":0, + "x":912, + "y":325.333 + }, + { + "height":0, + "id":17, + "name":"p9", + "point":true, + "rotation":0, + "type":"", + "visible":true, + "width":0, + "x":982, + "y":389.333 + }, + { + "height":0, + "id":18, + "name":"p10", + "point":true, + "rotation":0, + "type":"", + "visible":true, + "width":0, + "x":1038.67, + "y":465.333 + }, + { + "height":0, + "id":19, + "name":"p11", + "point":true, + "rotation":0, + "type":"", + "visible":true, + "width":0, + "x":1098.67, + "y":530 }], "opacity":1, "type":"objectgroup", @@ -528,7 +684,7 @@ "y":0 }], "nextlayerid":10, - "nextobjectid":8, + "nextobjectid":21, "orientation":"orthogonal", "renderorder":"right-down", "tiledversion":"1.10.2", diff --git a/game/components.h b/game/components.h index 19b7ade..ceace4f 100644 --- a/game/components.h +++ b/game/components.h @@ -293,8 +293,7 @@ typedef struct Health { extern ECS_COMPONENT_DECLARE(Health); typedef struct Swarm { - - int _; + i32 currWaypoint; } Swarm; extern ECS_COMPONENT_DECLARE(Swarm); diff --git a/game/constants.h b/game/constants.h index 64c106b..4674eaa 100644 --- a/game/constants.h +++ b/game/constants.h @@ -7,6 +7,8 @@ enum { COLL_LAYER_TRANSPARENCY = 7, }; +#define PLAYER_ENEMY PLAYER_BLUE + typedef enum Player { PLAYER_RED = 0, PLAYER_BLUE = 1, diff --git a/game/entity_factory.c b/game/entity_factory.c index 1101bd8..86fde68 100644 --- a/game/entity_factory.c +++ b/game/entity_factory.c @@ -115,10 +115,57 @@ ecs_entity_t entityCreateWorker(const Position position, Player player, Game *ga return e; } -ecs_entity_t entityRecruit(EntityType type, Position position, Player player, Game *game) { +ecs_entity_t entityCreateSwarmGoblin(const Position position, Player player, Game *game) { + ecs_entity_t e = entityCreateBaseUnit(position, 10.0f, player, ENTITY_GOBLIN, ANIM_IDLE, game); + ecs_set(ECS, e, Swarm, { + .currWaypoint = 0, + }); + ecs_set(ECS, e, Health, { + .startHP = 40.0f, + .hp = 40.0f, + .lastChanged = -1.0f + }); + + Unit *unit = ecs_get_mut(ECS, e, Unit); + unit->minDamage = 5.0f; + unit->maxDamage = 10.0f; + unit->attackCooldown = 1.0f; + unit->maxSpeed = 20.0f; + + return e; +} +ecs_entity_t entityCreateSwarmOrc(const Position position, Player player, Game *game) { + ecs_entity_t e = entityCreateBaseUnit(position, 10.0f, player, ENTITY_ORC, ANIM_IDLE, game); + ecs_set(ECS, e, Swarm, { + .currWaypoint = 0, + }); + ecs_set(ECS, e, Health, { + .startHP = 80.0f, + .hp = 80.0f, + .lastChanged = -1.0f + }); + + Unit *unit = ecs_get_mut(ECS, e, Unit); + unit->minDamage = 8.0f; + unit->maxDamage = 22.0f; + unit->attackCooldown = 1.8f; + unit->maxSpeed = 12.0f; + + return e; +} + +ecs_entity_t entityCreate(EntityType type, Position position, Player player, Game *game) { switch (type) { case ENTITY_WORKER: - entityCreateWorker(position, player, game); + return entityCreateWorker(position, player, game); + case ENTITY_SOLDIER: + return entityCreateSoldier(position, player, game); + case ENTITY_WARRIOR: + return entityCreateWarrior(position, player, game); + case ENTITY_GOBLIN: + return entityCreateSwarmGoblin(position, player, game); + case ENTITY_ORC: + return entityCreateSwarmOrc(position, player, game); default: return 0; } diff --git a/game/entity_factory.h b/game/entity_factory.h index d4df203..b318424 100644 --- a/game/entity_factory.h +++ b/game/entity_factory.h @@ -11,8 +11,10 @@ ecs_entity_t entityCreateBaseUnit(const Position position, f32 size, Player play ecs_entity_t entityCreateSoldier(const Position position, Player player, Game *game); ecs_entity_t entityCreateWarrior(const Position position, Player player, Game *game); ecs_entity_t entityCreateWorker(const Position position, Player player, Game *game); +ecs_entity_t entityCreateSwarmGoblin(const Position position, Player player, Game *game); +ecs_entity_t entityCreateSwarmOrc(const Position position, Player player, Game *game); -ecs_entity_t entityRecruit(EntityType type, Position position, Player player, Game *game); +ecs_entity_t entityCreate(EntityType type, Position position, Player player, Game *game); void getEntityCost(EntityType type, i32 cost[RES_COUNT]); bool canAffordEntity(EntityType type, PlayerResources res); diff --git a/game/game_state.h b/game/game_state.h index 31bc9ea..378140b 100644 --- a/game/game_state.h +++ b/game/game_state.h @@ -60,6 +60,8 @@ typedef struct PlayerResources { i64 popCapacity; } PlayerResources; +#define MAX_SWARM_WAYPOINTS 16 + typedef struct Game { GameScreen screen; GameScreen nextScreen; @@ -80,6 +82,10 @@ typedef struct Game { WaveInfo waveInfo; ecs_entity_t keepEntity; + Vector2 swarmWaypoints[MAX_SWARM_WAYPOINTS]; + i32 swamNumWaypoints; + Vector2 swarmSpawn; + BzStackAlloc stackAlloc; struct { BzBTNode *workerHarvest; diff --git a/game/main.c b/game/main.c index 0c5ef97..0d26369 100644 --- a/game/main.c +++ b/game/main.c @@ -480,7 +480,7 @@ void update(float dt, void *userData) { break; } - updateWave(&game->waveInfo, dt); + updateWave(&game->waveInfo, game, dt); SoundState *soundState = ecs_singleton_get_mut(ECS, SoundState); soundsUpdate(soundState, getCameraBounds(game->camera)); diff --git a/game/map_init.c b/game/map_init.c index fa4de7d..34aad02 100644 --- a/game/map_init.c +++ b/game/map_init.c @@ -1,6 +1,7 @@ #include "map_init.h" #include +#include #include "building_factory.h" #include "components.h" @@ -10,14 +11,37 @@ #include "utils.h" #include "systems/systems.h" +i32 getSwarmWaypointIdx(u32 id) { + char buf[4]; + for (i32 i = 0; i < MAX_SWARM_WAYPOINTS; i++) { + snprintf(buf, sizeof(buf), "p%d", i); + if (id == bzStringDefaultHash(buf)) + return i; + } + return -1; +} + bool initGameObjectsLayer(BzTileMap *map, BzTileObjectGroup *objectGroup) { Game *game = ecs_singleton_get_mut(ECS, Game); + game->swamNumWaypoints = 0; for (i32 i = 0; i < objectGroup->objectCount; i++) { BzTileObject object = objectGroup->objects[i]; if (bzStringDefaultHash("camera") == object.id) { game->camera.target.x = object.shape.x; game->camera.target.y = object.shape.y; } + i32 swarmIdx = getSwarmWaypointIdx(object.id); + if (swarmIdx != -1) { + game->swarmWaypoints[swarmIdx] = (Vector2) { + object.shape.x, + object.shape.y, + }; + game->swamNumWaypoints = BZ_MAX(game->swamNumWaypoints, swarmIdx + 1); + } + if (bzStringDefaultHash("spawn") == object.id) { + game->swarmSpawn.x = object.shape.x; + game->swarmSpawn.y = object.shape.y; + } } return true; } diff --git a/game/systems/s_entity.c b/game/systems/s_entity.c index a2e51cc..67adf54 100644 --- a/game/systems/s_entity.c +++ b/game/systems/s_entity.c @@ -256,6 +256,110 @@ void entityMoveToTarget(ecs_iter_t *it) { } } +void entityMoveSwarm(ecs_iter_t *it) { + Game *game = ecs_singleton_get_mut(ECS, Game); + Position *pos = ecs_field(it, Position, 1); + Velocity *vel = ecs_field(it, Velocity, 2); + HitBox *hb = ecs_field(it, HitBox, 3); + Swarm *swarm = ecs_field(it, Swarm, 4); + Owner *owner = ecs_field(it, Owner, 5); + Steering *steer = ecs_field(it, Steering, 6); + + for (i32 i = 0; i < it->count; i++) { + + // Vector2 align = Vector2Zero(); // Alignment (match velocity) + Vector2 avoid = Vector2Zero(); // Separation + Vector2 target = Vector2Zero(); + Vector2 cohesion = Vector2Zero(); // Cohesion (move towards center) + + const f32 FRIEND_RADIUS = 22.0f; + const f32 ENEMY_RADIUS = 52.0f; + + const Vector2 center = entityGetCenter(pos[i], hb[i]); + + const f32 RANGE = BZ_MAX(FRIEND_RADIUS, ENEMY_RADIUS); + BzSpatialGridIter spatialIt = bzSpatialGridIter(game->entityGrid, + center.x - RANGE, center.y - RANGE, + RANGE * 2.0f, RANGE * 2.0f); + + i32 numFriends = 0; + while (bzSpatialGridQueryNext(&spatialIt)) { + ecs_entity_t other = *(ecs_entity_t *) spatialIt.data; + if (!ecs_has(ECS, other, Owner)) + continue; + Owner otherOwner = *ecs_get(ECS, other, Owner); + bool isFriend = owner[i].player == otherOwner.player; + + if (!ecs_has(ECS, other, Position) || !ecs_has(ECS, other, HitBox)) + continue; + Position otherPos = *ecs_get(ECS, other, Position); + Rectangle otherHB = *ecs_get(ECS, other, HitBox);; + Vector2 otherCenter = entityGetCenter(otherPos, otherHB); + f32 dst = Vector2Distance(center, otherCenter); + const f32 MIN_AVOID_DST = 18.0f; + const f32 MIN_ENEMY_DST = ENEMY_RADIUS; + if (isFriend) { + if (dst < MIN_AVOID_DST) { + Vector2 dif = Vector2Subtract(center, otherCenter); + dif = Vector2Scale(Vector2Normalize(dif), MIN_AVOID_DST - dst); + avoid = Vector2Add(avoid, dif); + } + Vector2Add(cohesion, otherCenter); + numFriends++; + } else { + if (dst < MIN_ENEMY_DST) { + //DrawCircleV(otherCenter, 2.0f, RED); + Vector2 dif = Vector2Subtract(otherCenter, center); + dif = Vector2Scale(Vector2Normalize(dif), MIN_ENEMY_DST - dst); + target = Vector2Add(target, dif); + } + } + } + + if (numFriends > 0) { + cohesion = Vector2Divide(cohesion, (Vector2) { numFriends, numFriends }); + cohesion = Vector2Subtract(cohesion, center); + } + + + //bzLogInfo("%d %d", numFriends, numEnemies); + + const f32 noiseRange = 10.0f; + Vector2 noise = { + randFloatRange(-noiseRange, noiseRange), + randFloatRange(-noiseRange, noiseRange) + }; + Vector2 followWaypoint = Vector2Zero(); + if (swarm[i].currWaypoint < game->swamNumWaypoints) { + Vector2 waypoint = game->swarmWaypoints[swarm[i].currWaypoint]; + Vector2 waypointDir = Vector2Subtract(waypoint, center); + followWaypoint = waypointDir; + + f32 dst = Vector2Distance(center, waypoint); + const f32 WAYPOINT_THRESHOLD = 54.0f; + if (dst < WAYPOINT_THRESHOLD) { + swarm[i].currWaypoint++; + } + } + const f32 AVOID_FACTOR = 1.0f; + const f32 TARGET_FACTOR = 2.2f; + const f32 COHESION_FACTOR = 0.10f; + const f32 WAYPOINT_FACTOR = 0.45f; + const f32 NOISE_FACTOR = 0.2f; + Vector2 move = Vector2Zero(); + move = Vector2Add(move, Vector2Scale(avoid, AVOID_FACTOR)); + move = Vector2Add(move, Vector2Scale(target, TARGET_FACTOR)); + //move = Vector2Add(move, Vector2Scale(cohesion, COHESION_FACTOR)); + move = Vector2Add(move, Vector2Scale(followWaypoint, WAYPOINT_FACTOR)); + move = Vector2Add(move, Vector2Scale(noise, NOISE_FACTOR)); + + //bzLogInfo("%.2f %.2f", move.x, move.y); + //DrawLineV(center, Vector2Add(center, Vector2Scale(Vector2Normalize(avoid), 100)), ORANGE); + + steer[i] = move; + } +} + void entityFollowPath(ecs_iter_t *it) { const Game *game = ecs_singleton_get(ECS, Game); @@ -307,7 +411,7 @@ void updateBuildingRecruitment(ecs_iter_t *it) { slot->elapsed = 0; PlayerResources *playerRes = &game->playerResources[player]; playerRes->pop--; - entityRecruit(slot->entityType, placePos, player, game); + entityCreate(slot->entityType, placePos, player, game); slot->numRecruiting--; } } diff --git a/game/systems/s_ui.c b/game/systems/s_ui.c index 44b891e..758d02c 100644 --- a/game/systems/s_ui.c +++ b/game/systems/s_ui.c @@ -391,8 +391,8 @@ void drawMainMenuUI(Game *game, f32 dt) { //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"); + //loadMap(game, "assets/maps/battle_test.tmj"); + loadMap(game, "assets/maps/map_01.tmj"); } if (uiMainMenuButton("Settings", true)) { setScreen(game, SCREEN_SETTINGS); diff --git a/game/systems/systems.c b/game/systems/systems.c index 4751d8b..31fbdaa 100644 --- a/game/systems/systems.c +++ b/game/systems/systems.c @@ -181,6 +181,7 @@ void setupSystems() { ECS_SYSTEM(ECS, entityUpdate, EcsOnUpdate, Position, HitBox, Velocity, Unit, Owner, SpatialGridID); ECS_SYSTEM(ECS, entityMoveToTarget, EcsOnUpdate, Position, Velocity, TargetPosition, Steering); + ECS_SYSTEM(ECS, entityMoveSwarm, EcsOnUpdate, Position, Velocity, HitBox, Swarm, Owner, Steering); ECS_SYSTEM(ECS, entityFollowPath, EcsOnUpdate, Path); ECS_SYSTEM(ECS, updateBuildingRecruitment, EcsOnUpdate, Owner, Building, BuildingRecruitInfo); diff --git a/game/systems/systems.h b/game/systems/systems.h index 445a2ef..092910d 100644 --- a/game/systems/systems.h +++ b/game/systems/systems.h @@ -143,6 +143,17 @@ void entityUpdate(ecs_iter_t *it); */ void entityMoveToTarget(ecs_iter_t *it); +/* + * 0: Game (singleton for waypoints) + * 1: Position + * 2: Velocity + * 3: HitBox + * 4: Swarm + * 5: Owner + * 6: Steering + */ +void entityMoveSwarm(ecs_iter_t *it); + /* * 0: Game (singleton) for object pool * 1: Path diff --git a/game/wave.c b/game/wave.c index 3cb7988..edad18a 100644 --- a/game/wave.c +++ b/game/wave.c @@ -1,6 +1,8 @@ #include "wave.h" #include "game_state.h" #include "components.h" +#include "entity_factory.h" +#include "utils.h" WaveInfo getWaveInfo(i32 idx) { BZ_ASSERT(idx >= 0); @@ -24,14 +26,33 @@ WaveInfo getWaveInfo(i32 idx) { return info; } -void updateWave(WaveInfo *wave, f32 dt) { - wave->orcsElapsed += dt; - wave->goblinsElapsed += dt; +static Vector2 randomizeSpawnPos(Vector2 spawnPoint, i32 range) { + spawnPoint.x += randFloatRange(-range, range); + spawnPoint.y += randFloatRange(-range, range); + return spawnPoint; +} + +void updateWave(WaveInfo *wave, Game *game, f32 dt) { wave->elapsed += dt; if (wave->elapsed < wave->data.timeBeforeStart) return; wave->started = true; + wave->orcsElapsed += dt; + wave->goblinsElapsed += dt; + f32 timeForGoblin = 1.0f / wave->data.goblinSendRate; + if (wave->goblinsElapsed >= timeForGoblin) { + Vector2 spawnPos = randomizeSpawnPos(game->swarmSpawn, 20); + entityCreate(ENTITY_GOBLIN, spawnPos, PLAYER_ENEMY, game); + wave->goblinsElapsed -= timeForGoblin; + } + + f32 timeForOrc = 1.0f / wave->data.orcSendRate; + if (wave->orcsElapsed >= timeForOrc) { + Vector2 spawnPos = randomizeSpawnPos(game->swarmSpawn, 20); + entityCreate(ENTITY_ORC, spawnPos, PLAYER_ENEMY, game); + wave->orcsElapsed -= timeForOrc; + } } bool isWaveSendingOver(const WaveInfo *wave) { diff --git a/game/wave.h b/game/wave.h index 0a8d59c..00ada46 100644 --- a/game/wave.h +++ b/game/wave.h @@ -25,6 +25,8 @@ typedef struct WaveInfo { #define NUM_WAVES 5 +typedef struct Game Game; + static WaveData predefWaves[NUM_WAVES] = { { 10, 1.0f, 20, 2.0f, 0, 5 * 60 }, { 20, 1.0f, 40, 2.0f, 0, 2 * 60 }, @@ -35,7 +37,7 @@ static WaveData predefWaves[NUM_WAVES] = { WaveInfo getWaveInfo(i32 idx); -void updateWave(WaveInfo *wave, f32 dt); +void updateWave(WaveInfo *wave, Game *game, f32 dt); bool isWaveSendingOver(const WaveInfo *wave); bool isWaveOver(const WaveInfo *wave); diff --git a/rawAssets/map_01.tmx b/rawAssets/map_01.tmx index 5eabcea..c74c37e 100644 --- a/rawAssets/map_01.tmx +++ b/rawAssets/map_01.tmx @@ -1,7 +1,7 @@ - + - + @@ -393,12 +393,15 @@ - + + + +