Skip to content

Commit

Permalink
Make projectile fire rate precise.
Browse files Browse the repository at this point in the history
Fire delay is no longer restricted to multiples of the game update interval (but
minimum delay is currently one update interval, since combFire is called only
once per update).

Projectiles no longer freeze during the first update interval.

AG should now perform at the advertised rate.

Fixes ticket:2083.
  • Loading branch information
Cyp committed Dec 28, 2011
1 parent c93bfb2 commit 5d339a4
Show file tree
Hide file tree
Showing 6 changed files with 40 additions and 34 deletions.
11 changes: 8 additions & 3 deletions lib/gamelib/gtime.h
Expand Up @@ -166,6 +166,13 @@ static inline WZ_DECL_CONST int quantiseFraction(int numerator, int denominator,
int64_t oldValue = (int64_t)oldTime * numerator/denominator;
return newValue - oldValue;
}
/// Returns numerator/denominator * (newTime - oldTime). Rounds up or down such that the average return value is right, if oldTime is always the previous newTime.
static inline WZ_DECL_CONST Vector3i quantiseFraction(Vector3i numerator, int denominator, int newTime, int oldTime)
{
return Vector3i(quantiseFraction(numerator.x, denominator, newTime, oldTime),
quantiseFraction(numerator.y, denominator, newTime, oldTime),
quantiseFraction(numerator.z, denominator, newTime, oldTime));
}
/// Returns the value times deltaGameTime, converted to seconds. The return value is rounded up or down, such that it is exactly right on average.
static inline int32_t gameTimeAdjustedAverage(int value)
{
Expand All @@ -179,9 +186,7 @@ static inline int32_t gameTimeAdjustedAverage(int numerator, int denominator)
/// Returns the numerator/denominator times deltaGameTime, converted to seconds. The return value is rounded up or down, such that it is exactly right on average.
static inline Vector3i gameTimeAdjustedAverage(Vector3i numerator, int denominator)
{
return Vector3i(quantiseFraction(numerator.x, GAME_TICKS_PER_SEC*denominator, gameTime + deltaGameTime, gameTime),
quantiseFraction(numerator.y, GAME_TICKS_PER_SEC*denominator, gameTime + deltaGameTime, gameTime),
quantiseFraction(numerator.z, GAME_TICKS_PER_SEC*denominator, gameTime + deltaGameTime, gameTime));
return quantiseFraction(numerator, GAME_TICKS_PER_SEC*denominator, gameTime + deltaGameTime, gameTime);
}

void sendPlayerGameTime(void); ///< Sends a GAME_GAME_TIME message with gameTime plus latency to our game queues.
Expand Down
27 changes: 14 additions & 13 deletions src/combat.cpp
Expand Up @@ -72,10 +72,13 @@ bool combFire(WEAPON *psWeap, BASE_OBJECT *psAttacker, BASE_OBJECT *psTarget, in
return false;
}

unsigned fireTime = gameTime - deltaGameTime; // Can fire earliest at the start of the tick.

/*see if reload-able weapon and out of ammo*/
if (psStats->reloadTime && !psWeap->ammo)
{
if (gameTime - psWeap->lastFired < weaponReloadTime(psStats, psAttacker->player))
fireTime = std::max(fireTime, psWeap->lastFired + weaponReloadTime(psStats, psAttacker->player));
if (gameTime <= fireTime)
{
return false;
}
Expand All @@ -85,22 +88,15 @@ bool combFire(WEAPON *psWeap, BASE_OBJECT *psAttacker, BASE_OBJECT *psTarget, in

/* See when the weapon last fired to control it's rate of fire */
firePause = weaponFirePause(psStats, psAttacker->player);
firePause = std::max(firePause, 1u); // Don't shoot infinitely many shots at once.
fireTime = std::max(fireTime, psWeap->lastFired + firePause);

if (gameTime - psWeap->lastFired <= firePause)
if (gameTime <= fireTime)
{
/* Too soon to fire again */
return false;
}

// add a random delay to the fire
// With logical updates, a good graphics gard no longer gives a better ROF.
// TODO Should still replace this with something saner, such as a ±1% random deviation in reload time.
int fireChance = gameTime - (psWeap->lastFired + firePause);
if (gameRand(RANDOM_PAUSE) > fireChance)
{
return false;
}

if (psTarget->visible[psAttacker->player] != UBYTE_MAX)
{
// Can't see it - can't hit it
Expand Down Expand Up @@ -254,8 +250,13 @@ bool combFire(WEAPON *psWeap, BASE_OBJECT *psAttacker, BASE_OBJECT *psTarget, in

/* -------!!! From that point we are sure that we are firing !!!------- */

// Add a random delay to the next shot.
// TODO Add deltaFireTime to the time it takes to fire next. If just adding to psWeap->lastFired, it might put it in the future, causing assertions. And if not sometimes putting it in the future, the fire rate would be lower than advertised.
//int fireJitter = firePause/100; // ±1% variation in fire rate.
//int deltaFireTime = gameRand(fireJitter*2 + 1) - fireJitter;

/* note when the weapon fired */
psWeap->lastFired = gameTime;
psWeap->lastFired = fireTime;

/* reduce ammo if salvo */
if (psStats->reloadTime)
Expand Down Expand Up @@ -336,7 +337,7 @@ 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));

proj_SendProjectileAngled(psWeap, psAttacker, psAttacker->player, predict, psTarget, bVisibleAnyway, weapon_slot, min_angle);
proj_SendProjectileAngled(psWeap, psAttacker, psAttacker->player, predict, psTarget, bVisibleAnyway, weapon_slot, min_angle, fireTime);
return true;
}

Expand Down
13 changes: 6 additions & 7 deletions src/component.cpp
Expand Up @@ -510,7 +510,7 @@ static void displayCompObj(DROID *psDroid, bool bButton)
UDWORD colour;
UBYTE i;

if( (gameTime-psDroid->timeLastHit < GAME_TICKS_PER_SEC/4 ) && psDroid->lastHitWeapon == WSC_ELECTRONIC && !gamePaused())
if (graphicsTime - psDroid->timeLastHit < GAME_TICKS_PER_SEC/4 && psDroid->lastHitWeapon == WSC_ELECTRONIC && !gamePaused())
{
colour = getPlayerColour(rand()%MAX_PLAYERS);
}
Expand Down Expand Up @@ -777,15 +777,15 @@ static void displayCompObj(DROID *psDroid, bool bButton)
if ((psShape->numFrames == 0) || (psShape->animInterval <= 0))
{
//no anim so display one frame for a fixed time
if (gameTime < (psDroid->asWeaps[i].lastFired + BASE_MUZZLE_FLASH_DURATION))
if (graphicsTime >= psDroid->asWeaps[i].lastFired && graphicsTime < psDroid->asWeaps[i].lastFired + BASE_MUZZLE_FLASH_DURATION)
{
pie_Draw3DShape(psShape, 0, 0, brightness, pieFlag | pie_ADDITIVE, EFFECT_MUZZLE_ADDITIVE);
}
}
else
{
// animated muzzle
frame = (gameTime - psDroid->asWeaps[i].lastFired) / psShape->animInterval;
frame = (graphicsTime - psDroid->asWeaps[i].lastFired) / psShape->animInterval;
if (frame < psShape->numFrames)
{
pie_Draw3DShape(psShape, frame, 0, brightness, pieFlag | pie_ADDITIVE, EFFECT_MUZZLE_ADDITIVE);
Expand Down Expand Up @@ -1019,13 +1019,12 @@ void displayComponentObject(DROID *psDroid)
pie_MatRotX(rotation.x);
pie_MatRotZ(rotation.z);

if( (gameTime-psDroid->timeLastHit < GAME_TICKS_PER_SEC) && psDroid->lastHitWeapon == WSC_ELECTRONIC)
if (graphicsTime - psDroid->timeLastHit < GAME_TICKS_PER_SEC && psDroid->lastHitWeapon == WSC_ELECTRONIC)
{
objectShimmy( (BASE_OBJECT*) psDroid );
}

if (psDroid->lastHitWeapon == WSC_EMP &&
(gameTime - psDroid->timeLastHit < EMP_DISABLE_TIME))
if (psDroid->lastHitWeapon == WSC_EMP && graphicsTime - psDroid->timeLastHit < EMP_DISABLE_TIME)
{
Vector3i position;

Expand All @@ -1045,7 +1044,7 @@ void displayComponentObject(DROID *psDroid)
}
else
{
int frame = gameTime/BLIP_ANIM_DURATION + psDroid->id % 8192; // de-sync the blip effect, but don't overflow the int
int frame = graphicsTime/BLIP_ANIM_DURATION + psDroid->id % 8192; // de-sync the blip effect, but don't overflow the int
pie_Draw3DShape(getImdFromIndex(MI_BLIP), frame, 0, WZCOL_WHITE, pie_ADDITIVE, psDroid->visible[selectedPlayer] / 2);
}
pie_MatEnd();
Expand Down
8 changes: 4 additions & 4 deletions src/display3d.cpp
Expand Up @@ -2103,7 +2103,7 @@ void renderStructure(STRUCTURE *psStructure)
dv.y = psStructure->pos.z;
pie_MatBegin();
pie_TRANSLATE(dv.x,dv.y,dv.z);
int frame = gameTime / BLIP_ANIM_DURATION + psStructure->id % 8192; // de-sync the blip effect, but don't overflow the int
int frame = graphicsTime / BLIP_ANIM_DURATION + psStructure->id % 8192; // de-sync the blip effect, but don't overflow the int
pie_Draw3DShape(getImdFromIndex(MI_BLIP), frame, 0, WZCOL_WHITE, pie_ADDITIVE, psStructure->visible[selectedPlayer] / 2);
pie_MatEnd();
return;
Expand Down Expand Up @@ -2162,7 +2162,7 @@ void renderStructure(STRUCTURE *psStructure)
rotation = psStructure->rot.direction;
pie_MatRotY(-rotation);
if (!defensive
&& gameTime2-psStructure->timeLastHit < ELEC_DAMAGE_DURATION
&& graphicsTime - psStructure->timeLastHit < ELEC_DAMAGE_DURATION
&& psStructure->lastHitWeapon == WSC_ELECTRONIC )
{
bHitByElectronic = true;
Expand Down Expand Up @@ -2362,7 +2362,7 @@ void renderStructure(STRUCTURE *psStructure)
if (flashImd[i]->numFrames == 0 || flashImd[i]->animInterval <= 0)
{
// no anim so display one frame for a fixed time
if (graphicsTime < (psStructure->asWeaps[i].lastFired + BASE_MUZZLE_FLASH_DURATION))
if (graphicsTime >= psStructure->asWeaps[i].lastFired && graphicsTime < psStructure->asWeaps[i].lastFired + BASE_MUZZLE_FLASH_DURATION)
{
pie_Draw3DShape(flashImd[i], 0, colour, buildingBrightness, pieFlag | pie_ADDITIVE, EFFECT_MUZZLE_ADDITIVE);
}
Expand Down Expand Up @@ -2416,7 +2416,7 @@ void renderStructure(STRUCTURE *psStructure)
if (flashImd[i]->numFrames == 0 || flashImd[i]->animInterval <= 0)
{
// no anim so display one frame for a fixed time
if (graphicsTime < psStructure->asWeaps[i].lastFired + BASE_MUZZLE_FLASH_DURATION)
if (graphicsTime >= psStructure->asWeaps[i].lastFired && graphicsTime < psStructure->asWeaps[i].lastFired + BASE_MUZZLE_FLASH_DURATION)
{
pie_Draw3DShape(flashImd[i], 0, colour, buildingBrightness, 0, 0); //muzzle flash
}
Expand Down
13 changes: 7 additions & 6 deletions src/projectile.cpp
Expand Up @@ -383,10 +383,10 @@ int32_t projCalcIndirectVelocities(const int32_t dx, const int32_t dz, int32_t v

bool proj_SendProjectile(WEAPON *psWeap, SIMPLE_OBJECT *psAttacker, int player, Vector3i target, BASE_OBJECT *psTarget, bool bVisible, int weapon_slot)
{
return proj_SendProjectileAngled(psWeap, psAttacker, player, target, psTarget, bVisible, weapon_slot, 0);
return proj_SendProjectileAngled(psWeap, psAttacker, player, target, psTarget, bVisible, weapon_slot, 0, gameTime - 1);
}

bool proj_SendProjectileAngled(WEAPON *psWeap, SIMPLE_OBJECT *psAttacker, int player, Vector3i target, BASE_OBJECT *psTarget, bool bVisible, int weapon_slot, int min_angle)
bool proj_SendProjectileAngled(WEAPON *psWeap, SIMPLE_OBJECT *psAttacker, int player, Vector3i target, BASE_OBJECT *psTarget, bool bVisible, int weapon_slot, int min_angle, unsigned fireTime)
{
WEAPON_STATS *psStats = &asWeaponStats[psWeap->nStat];

Expand Down Expand Up @@ -454,9 +454,9 @@ bool proj_SendProjectileAngled(WEAPON *psWeap, SIMPLE_OBJECT *psAttacker, int pl
}
else
{
psProj->born = gameTime;
psProj->born = fireTime; // Born at the start of the tick.

psProj->prevSpacetime.time = gameTime - deltaGameTime; // Haven't ticked yet.
psProj->prevSpacetime.time = fireTime;
psProj->time = psProj->prevSpacetime.time;

setProjectileSource(psProj, psAttacker);
Expand Down Expand Up @@ -670,6 +670,7 @@ static void proj_InFlightFunc(PROJECTILE *psProj, bool bIndirect)
int timeSoFar = gameTime - psProj->born;

psProj->time = gameTime;
int deltaProjectileTime = psProj->time - psProj->prevSpacetime.time;

WEAPON_STATS *psStats = psProj->psWStats;
ASSERT_OR_RETURN( , psStats != NULL, "Invalid weapon stats pointer");
Expand Down Expand Up @@ -779,7 +780,7 @@ static void proj_InFlightFunc(PROJECTILE *psProj, bool bIndirect)
if (psStats->movementModel == MM_HOMINGINDIRECT)
{
int horizontalTargetDistance = iHypot(removeZ(psProj->dst - psProj->pos));
int terrainHeight = std::max(map_Height(removeZ(psProj->pos)), map_Height(removeZ(psProj->pos) + iSinCosR(iAtan2(removeZ(psProj->dst - psProj->pos)), psStats->flightSpeed*2/GAME_UPDATES_PER_SEC)));
int terrainHeight = std::max(map_Height(removeZ(psProj->pos)), map_Height(removeZ(psProj->pos) + iSinCosR(iAtan2(removeZ(psProj->dst - psProj->pos)), psStats->flightSpeed*2*deltaProjectileTime/GAME_TICKS_PER_SEC)));
int desiredMinHeight = terrainHeight + std::min(horizontalTargetDistance/4, HOMINGINDIRECT_HEIGHT_MIN);
int desiredMaxHeight = std::max(psProj->dst.z, terrainHeight + HOMINGINDIRECT_HEIGHT_MAX);
int heightError = psProj->pos.z - clip(psProj->pos.z, desiredMinHeight, desiredMaxHeight);
Expand All @@ -793,7 +794,7 @@ static void proj_InFlightFunc(PROJECTILE *psProj, bool bIndirect)
psProj->dst = psProj->pos + delta*10; // Target missing, so just keep going in a straight line.
}
currentDistance = timeSoFar * psStats->flightSpeed / GAME_TICKS_PER_SEC;
Vector3i step = gameTimeAdjustedAverage(delta * psStats->flightSpeed, targetDistance);
Vector3i step = quantiseFraction(delta * psStats->flightSpeed, GAME_TICKS_PER_SEC*targetDistance, psProj->time, psProj->prevSpacetime.time);
psProj->pos += step;
psProj->rot.direction = iAtan2(removeZ(delta));
psProj->rot.pitch = iAtan2(delta.z, targetDistance);
Expand Down
2 changes: 1 addition & 1 deletion src/projectile.h
Expand Up @@ -64,7 +64,7 @@ bool proj_SendProjectile(WEAPON *psWeap, SIMPLE_OBJECT *psAttacker, int player,

/** Send a single projectile against the given target
* with a minimum shot angle. */
bool proj_SendProjectileAngled(WEAPON *psWeap, SIMPLE_OBJECT *psAttacker, int player, Vector3i target, BASE_OBJECT *psTarget, bool bVisible, int weapon_slot, int min_angle);
bool 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. */
bool proj_Direct(const WEAPON_STATS* psStats);
Expand Down

0 comments on commit 5d339a4

Please sign in to comment.