Skip to content

Commit

Permalink
Switch PROJECTILE to use PagedEntityContainer<PROJECTILE> as back…
Browse files Browse the repository at this point in the history
…ing storage

The game still uses `psProjectileList` to maintain stable and
predictable order of iteration for projectiles, but the
individual `PROJECTILE` instances are allocated from the
global `PagedEntityContainer<PROJECTILE>` instance.

`proj_SendProjectileAngled()` now doesn't automatically
add the spawned penetrating projectile to `psProjectileList`,
which is a little step back in terms of ease of use (need
to additional code so that spawned projectiles are added
to the `psProjectileList` manually in relevant cases).

But it allows to remove additional checks inside the
tight loop in `proj_UpdateAll()`, which enumerates all
projectiles currently in play, thus considerably speeding
up this frequent and expensive operation.

Signed-off-by: Pavel Solodovnikov <[email protected]>
  • Loading branch information
ManManson committed Mar 7, 2024
1 parent 9c6b7ba commit dbc938d
Show file tree
Hide file tree
Showing 6 changed files with 99 additions and 69 deletions.
3 changes: 2 additions & 1 deletion src/combat.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -319,7 +319,8 @@ bool combFire(WEAPON *psWeap, BASE_OBJECT *psAttacker, BASE_OBJECT *psTarget, in
CLIP(predict.x, 0, world_coord(mapWidth - 1));
CLIP(predict.y, 0, world_coord(mapHeight - 1));

(void)proj_SendProjectileAngled(psWeap, psAttacker, psAttacker->player, predict, psTarget, bVisibleAnyway, weapon_slot, min_angle, fireTime);
auto* psProj = proj_SendProjectileAngled(psWeap, psAttacker, psAttacker->player, predict, psTarget, bVisibleAnyway, weapon_slot, min_angle, fireTime);
proj_AddActiveProjectile(psProj);
return true;
}

Expand Down
4 changes: 3 additions & 1 deletion src/multistruct.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -243,7 +243,9 @@ bool recvLasSat(NETQUEUE queue)
}

// Give enemy no quarter, unleash the lasat
(void)proj_SendProjectile(&psStruct->asWeaps[0], nullptr, player, psObj->pos, psObj, true, 0);
auto* psProj = proj_SendProjectile(&psStruct->asWeaps[0], nullptr, player, psObj->pos, psObj, true, 0);
proj_AddActiveProjectile(psProj);

psStruct->asWeaps[0].lastFired = gameTime;
psStruct->asWeaps[0].ammo = 1; // abducting this field for keeping track of triggers

Expand Down
147 changes: 85 additions & 62 deletions src/projectile.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
#include "lib/framework/trig.h"
#include "lib/framework/fixedpoint.h"
#include "lib/framework/math_ext.h"
#include "lib/framework/paged_entity_container.h"
#include "lib/gamelib/gtime.h"
#include "lib/sound/audio_id.h"
#include "lib/sound/audio.h"
Expand Down Expand Up @@ -98,12 +99,23 @@ struct DAMAGE
static const uint32_t ProjectileTrackerID = 0xdead0000;
static uint32_t projectileTrackerIDIncrement = 0;

/* The list of projectiles in play */
/* The list of projectiles in play.
* This intermediate container is needed to ensure that projectiles are always
* enumerated in a stable and predictable order, because `globalProjectileStorage`
* may insert new elements in place of old ones, which were previously destroyed,
* thus, modifying the order of iteration. */
static std::vector<PROJECTILE*> psProjectileList;

/* The next projectile to give out in the proj_First / proj_Next methods */
/* The next projectile to give out in the proj_First / proj_Next methods.
* References `psProjectileList` container. */
static ProjectileIterator psProjectileNext;

/// <summary>
/// Global container to allocate and hold instances of `PROJECTILE`
/// within the Warzone's process lifetime.
/// </summary>
static PagedEntityContainer<PROJECTILE> globalProjectileStorage;

/***************************************************************************/

// the last unit that did damage - used by script functions
Expand Down Expand Up @@ -208,17 +220,23 @@ proj_InitSystem()

/***************************************************************************/

// Add allocated projectile `p` to the list of active projectiles (`psProjectileList`)
void proj_AddActiveProjectile(PROJECTILE* p)
{
psProjectileList.emplace_back(p);
}

/***************************************************************************/

// Clean out all projectiles from the system, and properly decrement
// all reference counts.
void
proj_FreeAllProjectiles()
{
for (auto proj: psProjectileList)
{
delete proj;
}
psProjectileList.clear();
psProjectileNext = psProjectileList.end();

globalProjectileStorage.clear();
}

/***************************************************************************/
Expand Down Expand Up @@ -432,42 +450,42 @@ PROJECTILE* proj_SendProjectileAngled(WEAPON *psWeap, SIMPLE_OBJECT *psAttacker,
ASSERT_OR_RETURN(nullptr, psStats != nullptr, "Invalid weapon stats");
ASSERT_OR_RETURN(nullptr, psTarget == nullptr || !psTarget->died, "Aiming at dead target!");

PROJECTILE *psProj = new PROJECTILE(ProjectileTrackerID + ++projectileTrackerIDIncrement, player);
PROJECTILE proj(ProjectileTrackerID + ++projectileTrackerIDIncrement, player);

/* get muzzle offset */
if (psAttacker == nullptr)
{
// if there isn't an attacker just start at the target position
// NB this is for the script function to fire the las sats
psProj->src = target;
proj.src = target;
}
else if (psAttacker->type == OBJ_DROID && weapon_slot >= 0)
{
calcDroidMuzzleLocation((DROID *)psAttacker, &psProj->src, weapon_slot);
calcDroidMuzzleLocation((DROID *)psAttacker, &proj.src, weapon_slot);
/*update attack runs for VTOL droid's each time a shot is fired*/
updateVtolAttackRun((DROID *)psAttacker, weapon_slot);
}
else if (psAttacker->type == OBJ_STRUCTURE && weapon_slot >= 0)
{
calcStructureMuzzleLocation((STRUCTURE *)psAttacker, &psProj->src, weapon_slot);
calcStructureMuzzleLocation((STRUCTURE *)psAttacker, &proj.src, weapon_slot);
}
else // incase anything wants a projectile
{
psProj->src = psAttacker->pos;
proj.src = psAttacker->pos;
}

/* Initialise the structure */
psProj->psWStats = psStats;
proj.psWStats = psStats;

psProj->pos = psProj->src;
psProj->dst = target;
proj.pos = proj.src;
proj.dst = target;

psProj->bVisible = false;
proj.bVisible = false;

// Must set ->psDest and ->expectedDamageCaused before first call to setProjectileDestination().
psProj->psDest = nullptr;
psProj->expectedDamageCaused = objGuessFutureDamage(psStats, player, psTarget);
setProjectileDestination(psProj, psTarget); // Updates expected damage of psProj->psDest, using psProj->expectedDamageCaused.
proj.psDest = nullptr;
proj.expectedDamageCaused = objGuessFutureDamage(psStats, player, psTarget);
setProjectileDestination(&proj, psTarget); // Updates expected damage of proj.psDest, using proj.expectedDamageCaused.

/*
When we have been created by penetration (spawned from another projectile),
Expand All @@ -476,26 +494,26 @@ PROJECTILE* proj_SendProjectileAngled(WEAPON *psWeap, SIMPLE_OBJECT *psAttacker,
if (psAttacker && psAttacker->type == OBJ_PROJECTILE)
{
PROJECTILE *psOldProjectile = (PROJECTILE *)psAttacker;
psProj->born = psOldProjectile->born;
psProj->src = psOldProjectile->src;
proj.born = psOldProjectile->born;
proj.src = psOldProjectile->src;

psProj->prevSpacetime.time = psOldProjectile->time; // Have partially ticked already.
psProj->time = gameTime;
psProj->prevSpacetime.time -= psProj->prevSpacetime.time == psProj->time; // Times should not be equal, for interpolation.
proj.prevSpacetime.time = psOldProjectile->time; // Have partially ticked already.
proj.time = gameTime;
proj.prevSpacetime.time -= proj.prevSpacetime.time == proj.time; // Times should not be equal, for interpolation.

setProjectileSource(psProj, psOldProjectile->psSource);
psProj->psDamaged = psOldProjectile->psDamaged;
setProjectileSource(&proj, psOldProjectile->psSource);
proj.psDamaged = psOldProjectile->psDamaged;

// TODO Should finish the tick, when penetrating.
}
else
{
psProj->born = fireTime; // Born at the start of the tick.
proj.born = fireTime; // Born at the start of the tick.

psProj->prevSpacetime.time = fireTime;
psProj->time = psProj->prevSpacetime.time;
proj.prevSpacetime.time = fireTime;
proj.time = proj.prevSpacetime.time;

setProjectileSource(psProj, psAttacker);
setProjectileSource(&proj, psAttacker);
}

if (psTarget)
Expand All @@ -504,80 +522,80 @@ PROJECTILE* proj_SendProjectileAngled(WEAPON *psWeap, SIMPLE_OBJECT *psAttacker,
int minHeight = std::min(std::max(maxHeight + 2 * LINE_OF_FIRE_MINIMUM - areaOfFire(psAttacker, psTarget, weapon_slot, true), 0), maxHeight);
scoreUpdateVar(WD_SHOTS_ON_TARGET);

psProj->dst.z = psTarget->pos.z + minHeight + gameRand(std::max(maxHeight - minHeight, 1));
proj.dst.z = psTarget->pos.z + minHeight + gameRand(std::max(maxHeight - minHeight, 1));
/* store visible part (LOCK ON this part for homing :) */
psProj->partVisible = maxHeight - minHeight;
proj.partVisible = maxHeight - minHeight;
}
else
{
psProj->dst.z = target.z + LINE_OF_FIRE_MINIMUM;
proj.dst.z = target.z + LINE_OF_FIRE_MINIMUM;
scoreUpdateVar(WD_SHOTS_OFF_TARGET);
}

Vector3i deltaPos = psProj->dst - psProj->src;
Vector3i deltaPos = proj.dst - proj.src;

/* roll never set */
psProj->rot.roll = 0;
proj.rot.roll = 0;

psProj->rot.direction = iAtan2(deltaPos.xy());
proj.rot.direction = iAtan2(deltaPos.xy());


// Get target distance, horizontal distance only.
uint32_t dist = iHypot(deltaPos.xy());

if (proj_Direct(psStats))
{
psProj->rot.pitch = iAtan2(deltaPos.z, dist);
proj.rot.pitch = iAtan2(deltaPos.z, dist);
}
else
{
/* indirect */
projCalcIndirectVelocities(dist, deltaPos.z, psStats->flightSpeed, &psProj->vXY, &psProj->vZ, min_angle);
psProj->rot.pitch = iAtan2(psProj->vZ, psProj->vXY);
projCalcIndirectVelocities(dist, deltaPos.z, psStats->flightSpeed, &proj.vXY, &proj.vZ, min_angle);
proj.rot.pitch = iAtan2(proj.vZ, proj.vXY);
}
psProj->state = PROJ_INFLIGHT;
proj.state = PROJ_INFLIGHT;

// If droid or structure, set muzzle pitch.
if (psAttacker != nullptr && weapon_slot >= 0)
{
if (psAttacker->type == OBJ_DROID)
{
((DROID *)psAttacker)->asWeaps[weapon_slot].rot.pitch = psProj->rot.pitch;
((DROID *)psAttacker)->asWeaps[weapon_slot].rot.pitch = proj.rot.pitch;
}
else if (psAttacker->type == OBJ_STRUCTURE)
{
((STRUCTURE *)psAttacker)->asWeaps[weapon_slot].rot.pitch = psProj->rot.pitch;
((STRUCTURE *)psAttacker)->asWeaps[weapon_slot].rot.pitch = proj.rot.pitch;
}
}

/* put the projectile object in the global list */
psProjectileList.push_back(psProj);
/* put the projectile object in the global list, obtain the stable address for it. */
PROJECTILE& stableProj = globalProjectileStorage.emplace(std::move(proj));

/* play firing audio */
// only play if either object is visible, i know it's a bit of a hack, but it avoids the problem
// of having to calculate real visibility values for each projectile.
if (bVisible || gfxVisible(psProj))
if (bVisible || gfxVisible(&stableProj))
{
// note that the projectile is visible
psProj->bVisible = true;
stableProj.bVisible = true;

if (psStats->iAudioFireID != NO_SOUND)
{

if (psProj->psSource)
if (stableProj.psSource)
{
/* firing sound emitted from source */
audio_PlayObjDynamicTrack(psProj->psSource, psStats->iAudioFireID, nullptr);
audio_PlayObjDynamicTrack(stableProj.psSource, psStats->iAudioFireID, nullptr);
/* GJ HACK: move howitzer sound with shell */
if (psStats->weaponSubClass == WSC_HOWITZERS)
{
audio_PlayObjDynamicTrack(psProj, ID_SOUND_HOWITZ_FLIGHT, nullptr);
audio_PlayObjDynamicTrack(&stableProj, ID_SOUND_HOWITZ_FLIGHT, nullptr);
}
}
//don't play the sound for a LasSat in multiPlayer
else if (!(bMultiPlayer && psStats->weaponSubClass == WSC_LAS_SAT))
{
audio_PlayObjStaticTrack(psProj, psStats->iAudioFireID);
audio_PlayObjStaticTrack(&stableProj, psStats->iAudioFireID);
}
}
}
Expand All @@ -588,11 +606,11 @@ PROJECTILE* proj_SendProjectileAngled(WEAPON *psWeap, SIMPLE_OBJECT *psAttacker,
counterBatteryFire(castBaseObject(psAttacker), psTarget);
}

syncDebugProjectile(psProj, '*');
syncDebugProjectile(&stableProj, '*');

CHECK_PROJECTILE(psProj);
CHECK_PROJECTILE(&stableProj);

return psProj;
return &stableProj;
}

/***************************************************************************/
Expand Down Expand Up @@ -1397,23 +1415,21 @@ PROJECTILE* PROJECTILE::update()
void proj_UpdateAll()
{
WZ_PROFILE_SCOPE(proj_UpdateAll);
static std::unordered_set<PROJECTILE*> spawnedProjectiles;

static std::vector<PROJECTILE*> spawnedProjectiles;
spawnedProjectiles.reserve(psProjectileList.size());
spawnedProjectiles.clear();

// Update all projectiles. Penetrating projectiles may add to psProjectileList.
// Update all projectiles.
// Penetrating projectiles may spawn additional projectiles,
// which will be returned from `PROJECTILE::update()`.
// These need to be added separately to `psProjectileList` later.
for (PROJECTILE* p : psProjectileList)
{
// Don't process penetrating projectiles, which were spawned
// within the same `proj_UpdateAll()` invocation.
if (spawnedProjectiles.count(p) != 0)
{
continue;
}
PROJECTILE* spawned = p->update();
if (spawned)
{
spawnedProjectiles.emplace(spawned);
spawnedProjectiles.emplace_back(spawned);
}
}

Expand All @@ -1424,9 +1440,16 @@ void proj_UpdateAll()
{
return false;
}
delete p;
auto it = globalProjectileStorage.find(*p);
ASSERT(it != globalProjectileStorage.end(), "Invalid projectile, not found in global storage");
globalProjectileStorage.erase(it);
return true;
}), psProjectileList.end());

// Add spawned penetrating projectiles,
// which were collected earlier during the update procedure.
psProjectileList.reserve(psProjectileList.size() + spawnedProjectiles.size());
std::move(spawnedProjectiles.begin(), spawnedProjectiles.end(), std::back_inserter(psProjectileList));
}

/***************************************************************************/
Expand Down
6 changes: 4 additions & 2 deletions src/projectile.h
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,8 @@ bool proj_Shutdown(); ///< Shut down projectile subsystem.
PROJECTILE *proj_GetFirst(); ///< Get first projectile in the list.
PROJECTILE *proj_GetNext(); ///< Get next projectile in the list.

void proj_AddActiveProjectile(PROJECTILE* p); ///< Add allocated projectile `p` to the list of active projectiles

void proj_FreeAllProjectiles(); ///< Free all projectiles in the list.

void setExpGain(int player, int gain);
Expand All @@ -64,13 +66,13 @@ int32_t projCalcIndirectVelocities(const int32_t dx, const int32_t dz, int32_t v

/** Send a single projectile against the given target.
* Returns a non-null pointer to the newly-created projectile in the case of penetrating projectiles.
* The projectile is automatically added to `psProjectileList` global list. */
* The returned projectile needs to be manually added `psProjectileList` global list. */
PROJECTILE* proj_SendProjectile(WEAPON *psWeap, SIMPLE_OBJECT *psAttacker, int player, Vector3i target, BASE_OBJECT *psTarget, bool bVisible, int weapon_slot);

/** Send a single projectile against the given target
* with a minimum shot angle.
* Returns a non-null pointer to the newly-created projectile in the case of penetrating projectiles.
* The projectile is automatically added to `psProjectileList` global list. */
* The returned projectile needs to be manually added `psProjectileList` global list. */
PROJECTILE* proj_SendProjectileAngled(WEAPON *psWeap, SIMPLE_OBJECT *psAttacker, int player, Vector3i target, BASE_OBJECT *psTarget, bool bVisible, int weapon_slot, int min_angle, unsigned fireTime);

/** Return whether a weapon is direct or indirect. */
Expand Down
2 changes: 1 addition & 1 deletion src/projectiledef.h
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ struct PROJECTILE : public SIMPLE_OBJECT

// Returns non-empty pointer if `update()` has spawned an additional projectile,
// which will be true for penetrating projectiles.
// The newly-created projectile will be added to `psProjectileList` automatically.
// The newly-created projectile needs to be manually added to `psProjectileList`.
PROJECTILE* update();

UBYTE state; ///< current projectile state
Expand Down
6 changes: 4 additions & 2 deletions src/wzapi.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -3295,7 +3295,8 @@ wzapi::no_return_value wzapi::fireWeaponAtLoc(WZAPI_PARAMS(std::string weaponNam
WEAPON sWeapon;
sWeapon.nStat = weaponIndex;

(void)proj_SendProjectile(&sWeapon, nullptr, player, target, nullptr, true, 0);
auto* psProj = proj_SendProjectile(&sWeapon, nullptr, player, target, nullptr, true, 0);
proj_AddActiveProjectile(psProj);
return {};
}

Expand All @@ -3317,7 +3318,8 @@ wzapi::no_return_value wzapi::fireWeaponAtObj(WZAPI_PARAMS(std::string weaponNam
WEAPON sWeapon;
sWeapon.nStat = weaponIndex;

(void)proj_SendProjectile(&sWeapon, nullptr, player, target, psObj, true, 0);
auto* psProj = proj_SendProjectile(&sWeapon, nullptr, player, target, psObj, true, 0);
proj_AddActiveProjectile(psProj);
return {};
}

Expand Down

0 comments on commit dbc938d

Please sign in to comment.