Skip to content

Commit 705c12f

Browse files
committed
overhaul scripted agent
1 parent 0c494b5 commit 705c12f

File tree

1 file changed

+157
-56
lines changed

1 file changed

+157
-56
lines changed

pufferlib/ocean/impulse_wars/scripted_agent.h

Lines changed: 157 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -10,15 +10,31 @@ const uint8_t NUM_NEAR_PICKUPS = 1;
1010
const float WALL_CHECK_DISTANCE_SQUARED = SQUARED(6.0f);
1111
const float WALL_AVOID_DISTANCE = 4.0f;
1212
const float WALL_DANGER_DISTANCE = 3.0f;
13-
const float WALL_BRAKE_DISTANCE = 20.0f;
14-
const float WALL_BRAKE_SPEED = 12.5f;
15-
const float WALL_BRAKE_TIME = 0.5f;
13+
const float WALL_BURST_CHECK_DISTANCE = 5.0f;
14+
const float WALL_BURST_CHECK_SPEED = 20.0f;
1615
const float BURST_MIN_RADIUS_SQUARED = SQUARED(DRONE_BURST_RADIUS_MIN);
17-
const float MOVE_SPEED_SQUARED = SQUARED(5.0f);
16+
const float STABILIZE_MOVE_SPEED = 5.0f;
17+
const float SHOTGUN_DANGER_DISTANCE = 4.0f;
18+
19+
void addDebugPoint(iwEnv *e, b2Vec2 pos, float size, Color color) {
20+
#ifndef NDEBUG
21+
debugPoint *point = fastCalloc(1, sizeof(debugPoint));
22+
point->pos = pos;
23+
point->size = size;
24+
point->color = color;
25+
cc_array_add(e->debugPoints, point);
26+
#else
27+
MAYBE_UNUSED(e);
28+
MAYBE_UNUSED(pos);
29+
MAYBE_UNUSED(size);
30+
MAYBE_UNUSED(color);
31+
#endif
32+
}
1833

1934
typedef struct castCircleCtx {
2035
bool hit;
2136
b2ShapeId shapeID;
37+
b2Vec2 point;
2238
} castCircleCtx;
2339

2440
float castCircleCallback(b2ShapeId shapeId, b2Vec2 point, b2Vec2 normal, float fraction, void *context) {
@@ -33,6 +49,7 @@ float castCircleCallback(b2ShapeId shapeId, b2Vec2 point, b2Vec2 normal, float f
3349
castCircleCtx *ctx = context;
3450
ctx->hit = true;
3551
ctx->shapeID = shapeId;
52+
ctx->point = point;
3653

3754
return 0.0f;
3855
}
@@ -122,52 +139,79 @@ void pathfindBFS(const iwEnv *e, uint8_t *flatPaths, uint16_t destCellIdx) {
122139
}
123140
}
124141

125-
float distanceWithDamping(const iwEnv *e, const droneEntity *drone, const b2Vec2 direction, const float linearDamping, const float steps) {
126-
float speed = drone->weaponInfo->recoilMagnitude * DRONE_INV_MASS;
127-
if (!b2VecEqual(drone->velocity, b2Vec2_zero)) {
128-
speed = b2Length(b2MulAdd(drone->velocity, speed, direction));
129-
}
130-
131-
const float damping = 1.0f + linearDamping * e->deltaTime;
142+
float distanceWithDamping(const iwEnv *e, const float speed, const float linearDamping, const float steps) {
143+
const float damping = 1.0f + (linearDamping * e->deltaTime);
132144
return speed * (damping / linearDamping) * (1.0f - powf(1.0f / damping, steps));
133145
}
134146

147+
b2Vec2 positionWithDamping(const iwEnv *e, const droneEntity *drone, const b2Vec2 impulse, const float linearDamping, const float steps) {
148+
const b2Vec2 newVel = b2Add(drone->velocity, impulse);
149+
const float speed = b2Length(newVel);
150+
const float distance = distanceWithDamping(e, speed, linearDamping, steps);
151+
const b2Vec2 direction = b2Normalize(newVel);
152+
return b2MulAdd(drone->pos, distance, direction);
153+
}
154+
155+
// returns true if drone can fire in the given direction without hitting
156+
// or getting too close to a death wall before it can fire again
135157
bool safeToFire(iwEnv *e, const droneEntity *drone, const b2Vec2 direction) {
158+
// don't shoot shotgun point blank at walls, the shots will immediately
159+
// bounce back and send the drone flying uncontrollably
160+
if (drone->weaponInfo->type == SHOTGUN_WEAPON) {
161+
const b2Vec2 rayEnd = b2MulAdd(drone->pos, SHOTGUN_DANGER_DISTANCE, direction);
162+
const b2Vec2 translation = b2Sub(rayEnd, drone->pos);
163+
const b2QueryFilter filter = {.categoryBits = PROJECTILE_SHAPE, .maskBits = WALL_SHAPE | FLOATING_WALL_SHAPE};
164+
const b2RayResult rayRes = b2World_CastRayClosest(e->worldID, drone->pos, translation, filter);
165+
if (rayRes.hit) {
166+
return false;
167+
}
168+
}
169+
136170
float shotWait;
137171
if (drone->ammo > 1) {
138172
shotWait = ((drone->weaponInfo->coolDown + drone->weaponInfo->charge) / e->deltaTime) * 1.5f;
139173
} else {
140174
shotWait = ((e->defaultWeapon->coolDown + e->defaultWeapon->charge) / e->deltaTime) * 1.5f;
141175
}
142-
const b2Vec2 invDirection = b2MulSV(-1.0f, direction);
143-
const float recoilDistance = distanceWithDamping(e, drone, invDirection, DRONE_LINEAR_DAMPING, shotWait);
176+
const float recoilSpeed = -drone->weaponInfo->recoilMagnitude * DRONE_INV_MASS;
177+
const b2Vec2 recoil = b2MulSV(recoilSpeed, direction);
178+
const b2Vec2 recoilPos = positionWithDamping(e, drone, recoil, DRONE_LINEAR_DAMPING, shotWait);
144179

145-
// e->debugPoint = b2MulAdd(drone->pos, recoilDistance, invDirection);
180+
addDebugPoint(e, b2MulAdd(drone->pos, 2.0f * DRONE_RADIUS, direction), 0.5f, WHITE);
146181

147182
const b2Vec2 pos = drone->pos;
148-
const b2Vec2 rayEnd = b2MulAdd(pos, recoilDistance, invDirection);
183+
const b2Vec2 rayEnd = recoilPos;
149184
const b2Vec2 translation = b2Sub(rayEnd, pos);
150-
const b2ShapeProxy cirProxy = b2MakeProxy(&pos, 1, DRONE_RADIUS);
185+
float radius = DRONE_RADIUS;
186+
if (drone->shield != NULL) {
187+
radius = DRONE_SHIELD_RADIUS;
188+
}
189+
const b2ShapeProxy cirProxy = b2MakeProxy(&pos, 1, radius);
151190
const b2QueryFilter filter = {.categoryBits = DRONE_SHAPE, .maskBits = WALL_SHAPE | FLOATING_WALL_SHAPE | DRONE_SHAPE};
152191

153192
castCircleCtx ctx = {0};
154193
b2World_CastShape(e->worldID, &cirProxy, translation, filter, castCircleCallback, &ctx);
155194
if (!ctx.hit) {
195+
addDebugPoint(e, recoilPos, 0.5f, LIME);
156196
return true;
157197
} else {
158198
const entity *ent = b2Shape_GetUserData(ctx.shapeID);
159-
if (ent->type == STANDARD_WALL_ENTITY || ent->type == BOUNCY_WALL_ENTITY || ent->type == DRONE_ENTITY) {
199+
if (entityTypeIsWall(ent->type) && ent->type != DEATH_WALL_ENTITY) {
200+
addDebugPoint(e, recoilPos, 0.5f, LIME);
160201
return true;
161202
}
162203
}
163204

205+
addDebugPoint(e, recoilPos, 0.5f, MAROON);
164206
return false;
165207
}
166208

167209
bool weaponSafeForMovement(const droneEntity *drone) {
168210
switch (drone->weaponInfo->type) {
169211
case IMPLODER_WEAPON:
170212
case MINE_LAUNCHER_WEAPON:
213+
case BLACK_HOLE_WEAPON:
214+
case NUKE_WEAPON:
171215
return false;
172216
default:
173217
return true;
@@ -236,6 +280,11 @@ float weaponIdealRangeSquared(const droneEntity *drone) {
236280
}
237281

238282
bool shouldShootAtEnemy(iwEnv *e, const droneEntity *drone, const droneEntity *enemyDrone, const b2Vec2 enemyDroneDirection) {
283+
// don't shoot at shielded enemies with railguns since it will likely
284+
// bounce back and hit us
285+
if (drone->weaponInfo->type == SNIPER_WEAPON && enemyDrone->shield != NULL) {
286+
return false;
287+
}
239288
if (!safeToFire(e, drone, enemyDroneDirection)) {
240289
return false;
241290
}
@@ -257,6 +306,10 @@ bool shouldShootAtEnemy(iwEnv *e, const droneEntity *drone, const droneEntity *e
257306
if (ent == NULL || ent->type != DRONE_ENTITY) {
258307
return false;
259308
}
309+
const droneEntity *hitDrone = ent->entity;
310+
if (hitDrone->idx != enemyDrone->idx) {
311+
return false;
312+
}
260313

261314
return true;
262315
}
@@ -267,42 +320,42 @@ b2Vec2 predictiveAim(const droneEntity *drone, const droneEntity *enemyDrone, co
267320
return b2Normalize(b2Sub(predictedPos, drone->pos));
268321
}
269322

270-
void handleWallProximity(iwEnv *e, const droneEntity *drone, const wallEntity *wall, const float distance, agentActions *actions) {
271-
if (distance > WALL_BRAKE_DISTANCE) {
323+
void scriptedAgentBurst(const droneEntity *drone, agentActions *actions) {
324+
if (drone->chargingBurst) {
272325
return;
326+
} else {
327+
actions->chargingBurst = true;
273328
}
329+
}
274330

331+
void handleWallProximity(iwEnv *e, const droneEntity *drone, const wallEntity *wall, const float distance, agentActions *actions) {
332+
// shoot to move away faster from a death wall if we're too close and it's safe
275333
const b2Vec2 wallDirection = b2Normalize(b2Sub(wall->pos, drone->pos));
276-
const float speedToWall = b2Dot(drone->velocity, wallDirection);
277-
if (speedToWall > WALL_BRAKE_SPEED) {
278-
float damping = DRONE_LINEAR_DAMPING;
279-
if (drone->braking) {
280-
damping *= DRONE_BRAKE_DAMPING_COEF;
281-
}
282-
const float travelDistance = distanceWithDamping(e, drone, wallDirection, damping, WALL_BRAKE_TIME / e->deltaTime);
283-
if (travelDistance >= distance) {
284-
actions->brake = true;
285-
}
334+
if (distance <= WALL_DANGER_DISTANCE && weaponSafeForMovement(drone) && safeToFire(e, drone, wallDirection)) {
335+
actions->aim = b2MulAdd(actions->aim, distance, wallDirection);
336+
scriptedAgentShoot(drone, actions);
286337
}
287-
if (actions->brake || distance <= WALL_AVOID_DISTANCE) {
288-
const b2Vec2 invWallDirection = b2MulSV(-1.0f, wallDirection);
289-
actions->move = b2MulAdd(actions->move, distance, invWallDirection);
338+
// move away from the wall if we're too close
339+
if (distance <= WALL_AVOID_DISTANCE) {
340+
actions->move = b2MulAdd(actions->move, -distance, wallDirection);
290341
}
342+
}
291343

292-
if (distance > WALL_DANGER_DISTANCE) {
344+
// charge burst until we're close enough to a death wall to burst off
345+
// of it
346+
void wallBurst(iwEnv *e, const droneEntity *drone, const float speed, const float distance, agentActions *actions) {
347+
if (distance < DRONE_BURST_RADIUS_MIN) {
348+
scriptedAgentBurst(drone, actions);
293349
return;
294350
}
295351

296-
// shoot to move away faster from a death wall if we're too close and it's safe
297-
if (weaponSafeForMovement(drone) && safeToFire(e, drone, wallDirection)) {
298-
actions->aim = wallDirection;
299-
scriptedAgentShoot(drone, actions);
352+
float damping = DRONE_LINEAR_DAMPING;
353+
if (drone->braking || actions->brake) {
354+
damping *= DRONE_BRAKE_DAMPING_COEF;
300355
}
301-
}
302-
303-
void scriptedAgentBurst(const droneEntity *drone, agentActions *actions) {
304-
if (drone->chargingBurst) {
305-
return;
356+
const float travelDistance = distanceWithDamping(e, b2Length(drone->velocity), damping, e->frameSkip);
357+
if (travelDistance > distance) {
358+
scriptedAgentBurst(drone, actions);
306359
} else {
307360
actions->chargingBurst = true;
308361
}
@@ -314,24 +367,26 @@ agentActions scriptedAgentActions(iwEnv *e, droneEntity *drone) {
314367
return actions;
315368
}
316369

317-
// keep the weapon charged and ready if it needs it
370+
// keep the weapon charged and ready
318371
if (drone->weaponInfo->charge != 0.0f) {
319372
actions.chargingWeapon = true;
320373
actions.shoot = true;
321374
}
322375

323-
// find the nearest death wall or floating wall
376+
// find the N nearest death walls or floating walls
324377
nearEntity nearWalls[MAX_NEAREST_WALLS] = {0};
325378
findNearWalls(e, drone, nearWalls, NUM_NEAR_WALLS);
326379

327-
// find the distance between the closest points on the drone and the nearest wall
380+
// move away from and shoot at death walls if we're too close
381+
float closestWallDistance = FLT_MAX;
328382
for (uint8_t i = 0; i < NUM_NEAR_WALLS; i++) {
329383
const wallEntity *wall = nearWalls[i].entity;
330384
if (wall->type != DEATH_WALL_ENTITY) {
331385
continue;
332386
}
333387

334388
const b2DistanceOutput output = closestPoint(drone->ent, wall->ent);
389+
closestWallDistance = min(closestWallDistance, output.distance);
335390
handleWallProximity(e, drone, wall, output.distance, &actions);
336391
}
337392

@@ -345,9 +400,60 @@ agentActions scriptedAgentActions(iwEnv *e, droneEntity *drone) {
345400
}
346401

347402
const b2DistanceOutput output = closestPoint(drone->ent, floatingWall->ent);
403+
closestWallDistance = min(closestWallDistance, output.distance);
348404
handleWallProximity(e, drone, floatingWall, output.distance, &actions);
349405
}
350406

407+
// TODO: shoot mines around enemy, only if not too close
408+
409+
// if we are moving towards a death wall at a high speed, do everything
410+
// possible to avoid hitting it
411+
const float droneSpeed = b2Length(drone->velocity);
412+
if (drone->braking || drone->chargingBurst || closestWallDistance <= WALL_BURST_CHECK_DISTANCE || droneSpeed >= WALL_BURST_CHECK_SPEED) {
413+
float damping = DRONE_LINEAR_DAMPING;
414+
if (drone->braking || actions.brake) {
415+
damping *= DRONE_BRAKE_DAMPING_COEF;
416+
}
417+
const b2Vec2 recoilPos = positionWithDamping(e, drone, b2Vec2_zero, damping, 0.5f / e->deltaTime);
418+
const b2Vec2 pos = drone->pos;
419+
const b2Vec2 rayEnd = recoilPos;
420+
const b2Vec2 translation = b2Sub(rayEnd, pos);
421+
float radius = DRONE_RADIUS;
422+
if (drone->shield != NULL) {
423+
radius = DRONE_SHIELD_RADIUS;
424+
}
425+
const b2ShapeProxy cirProxy = b2MakeProxy(&pos, 1, radius);
426+
const b2QueryFilter filter = {.categoryBits = DRONE_SHAPE, .maskBits = WALL_SHAPE | FLOATING_WALL_SHAPE};
427+
428+
castCircleCtx ctx = {0};
429+
b2World_CastShape(e->worldID, &cirProxy, translation, filter, castCircleCallback, &ctx);
430+
if (ctx.hit) {
431+
const entity *ent = b2Shape_GetUserData(ctx.shapeID);
432+
if (entityTypeIsWall(ent->type) && ent->type == DEATH_WALL_ENTITY) {
433+
actions.brake = true;
434+
if (drone->shield == NULL) {
435+
wallBurst(e, drone, droneSpeed, b2Distance(drone->pos, ctx.point), &actions);
436+
}
437+
438+
const b2Vec2 droneDirection = b2Normalize(drone->velocity);
439+
if (b2VecEqual(actions.move, b2Vec2_zero)) {
440+
actions.move = b2MulSV(-1.0f, droneDirection);
441+
}
442+
if (b2VecEqual(actions.aim, b2Vec2_zero) && weaponSafeForMovement(drone)) {
443+
actions.aim = droneDirection;
444+
scriptedAgentShoot(drone, &actions);
445+
}
446+
return actions;
447+
}
448+
}
449+
}
450+
451+
// if we're close enough to a wall to need to shoot at it, don't
452+
// worry about enemies
453+
if (!b2VecEqual(actions.aim, b2Vec2_zero)) {
454+
return actions;
455+
}
456+
351457
// get a weapon if the standard weapon is active
352458
if (drone->weaponInfo->type == STANDARD_WEAPON && cc_array_size(e->pickups) != 0) {
353459
nearEntity nearPickups[MAX_WEAPON_PICKUPS] = {0};
@@ -389,12 +495,10 @@ agentActions scriptedAgentActions(iwEnv *e, droneEntity *drone) {
389495
}
390496
}
391497
if (enemyDrone == NULL) {
392-
return actions;
393-
}
394-
395-
// if we're close enough to a wall to need to shoot at it, don't
396-
// worry about enemies
397-
if (!b2VecEqual(actions.aim, b2Vec2_zero)) {
498+
// fight recoil if we're not otherwise moving
499+
if (b2VecEqual(actions.move, b2Vec2_zero) && droneSpeed >= STABILIZE_MOVE_SPEED) {
500+
actions.move = b2MulSV(-1.0f, b2Normalize(drone->velocity));
501+
}
398502
return actions;
399503
}
400504

@@ -423,11 +527,8 @@ agentActions scriptedAgentActions(iwEnv *e, droneEntity *drone) {
423527
}
424528

425529
// fight recoil if we're not otherwise moving
426-
if (b2VecEqual(actions.move, b2Vec2_zero)) {
427-
const float speedSquared = b2LengthSquared(drone->velocity);
428-
if (speedSquared > MOVE_SPEED_SQUARED) {
429-
actions.move = b2MulSV(-1.0f, b2Normalize(drone->velocity));
430-
}
530+
if (b2VecEqual(actions.move, b2Vec2_zero) && droneSpeed >= STABILIZE_MOVE_SPEED) {
531+
actions.move = b2MulSV(-1.0f, b2Normalize(drone->velocity));
431532
}
432533

433534
return actions;

0 commit comments

Comments
 (0)