Enemy swarm movement

This commit is contained in:
2024-02-12 14:14:23 +01:00
parent c5b6dc0501
commit e69fdeed1f
15 changed files with 402 additions and 24 deletions

View File

@@ -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",

View File

@@ -293,8 +293,7 @@ typedef struct Health {
extern ECS_COMPONENT_DECLARE(Health);
typedef struct Swarm {
int _;
i32 currWaypoint;
} Swarm;
extern ECS_COMPONENT_DECLARE(Swarm);

View File

@@ -7,6 +7,8 @@ enum {
COLL_LAYER_TRANSPARENCY = 7,
};
#define PLAYER_ENEMY PLAYER_BLUE
typedef enum Player {
PLAYER_RED = 0,
PLAYER_BLUE = 1,

View File

@@ -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;
}

View File

@@ -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);

View File

@@ -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;

View File

@@ -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));

View File

@@ -1,6 +1,7 @@
#include "map_init.h"
#include <flecs.h>
#include <stdio.h>
#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;
}

View File

@@ -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--;
}
}

View File

@@ -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);

View File

@@ -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);

View File

@@ -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

View File

@@ -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) {

View File

@@ -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);

View File

@@ -1,7 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<map version="1.10" tiledversion="1.10.2" orientation="orthogonal" renderorder="right-down" width="80" height="50" tilewidth="16" tileheight="16" infinite="0" nextlayerid="10" nextobjectid="20">
<map version="1.10" tiledversion="1.10.2" orientation="orthogonal" renderorder="right-down" width="80" height="50" tilewidth="16" tileheight="16" infinite="0" nextlayerid="10" nextobjectid="21">
<editorsettings>
<export target="maps/map_01.tmj" format="json"/>
<export target="../assets/maps/map_01.tmj" format="json"/>
</editorsettings>
<tileset firstgid="1" source="game.tsx"/>
<layer id="1" name="terrain" width="80" height="50">
@@ -393,12 +393,15 @@
<object id="1" name="camera" x="1119" y="571.5">
<point/>
</object>
<object id="8" name="spawn" x="91.3333" y="665.333">
<object id="8" name="spawn" x="32.0606" y="768.242">
<point/>
</object>
<object id="9" name="p1" x="150" y="510">
<point/>
</object>
<object id="20" name="p0" x="97.5" y="644.5">
<point/>
</object>
<object id="10" name="p2" x="226" y="353.333">
<point/>
</object>