Skip to content

Commit

Permalink
Collision damage patch 4 (#3784)
Browse files Browse the repository at this point in the history
Fix some issues with collision and impulse handling
  • Loading branch information
SethDGamre authored Oct 10, 2024
1 parent d866bba commit 6c315a5
Show file tree
Hide file tree
Showing 2 changed files with 54 additions and 18 deletions.
2 changes: 1 addition & 1 deletion gamedata/modrules.lua
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ local modrules = {
},

movement = {
allowUnitCollisionDamage = false, -- default: true if using QTPFS pathfinder. Do unit-unit (skidding) collisions cause damage?
allowUnitCollisionDamage = true, -- default: true if using QTPFS pathfinder. Do unit-unit (skidding) collisions cause damage?
allowUnitCollisionOverlap = false, -- can mobile units collision volumes overlap one another? Allows unit movement like this (video http://www.youtube.com/watch?v=mRtePUdVk2o ) at the cost of more 'clumping'.
allowCrushingAlliedUnits = true, -- default: false. Can allied ground units crush each other during collisions? Units still have to be explicitly set as crushable using the crushable parameter of Spring.SetUnitBlocking.
allowGroundUnitGravity = false, -- default: true. Allows fast moving mobile units to 'catch air' as they move over terrain.
Expand Down
70 changes: 53 additions & 17 deletions luarules/gadgets/unit_collision_damage_behavior.lua
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ function gadget:GetInfo()
date = "2024.8.29",
license = "GNU GPL, v2 or later",
layer = 0,
enabled = false
enabled = true
}
end

Expand All @@ -24,12 +24,15 @@ local collisionVelocityThreshold = 99 / Game.gameSpeed
local validCollisionAngleMultiplier = math.cos(math.rad(20)) --degrees

-- Decrease this value to make units move less from impulse. This defines the maximum impulse allowed, which is (maxImpulseMultiplier * mass) of each unit.
local maxImpulseMultiplier = 165 / Game.gameSpeed
local maxImpulseMultiplier = 5.5

-- elmo/s, converted to elmo/frame. If a unit is launched via explosion faster than this, it is instantly slowed to the derivitive velLengthOffsetCap.
--to save performance and reduce unit hesitation from nominal impulse, impulse values below (minImpulseMultiplier * mass) returns 0 impulse.
local minImpulseMultiplier = 1

-- elmo/s, converted to elmo/frame. If a unit is launched via explosion faster than this, it is instantly slowed. If unit speed/gameSpeed is greater or canFly = true, speed/gameSpeed is used instead.
local velocityCap = 330 / Game.gameSpeed

--measured in elmos per frame. If velocity is above this threshold, it will be slowed until below this threshold.
--measured in elmos per frame. If velocity is above this threshold, it will be slowed until below this threshold so long as its initial velocity was greater than velocityCap.
local velocitySlowdownThreshold = 30 / Game.gameSpeed

--any weapondef impulseFactor below this is ignored to save performance
Expand All @@ -51,22 +54,39 @@ local mathAbs = math.abs

local fallDamageMultipliers = {}
local unitsMaxImpulse = {}
local unitsMinImpulse = {}
local weaponDefIDImpulses = {}
local transportedUnits = {}
local unitMasses = {}
local unitDefData = {}
local weaponDefIgnored = {}
local unitInertiaCheckFlags = {}
local fallingKillQueue = {}
local launchedUnits = {}
local fallingUnits = {}

local gameFrame = 0
local velocityWatchFrames = 300 / Game.gameSpeed
local velLengthOffsetCap = velocityCap * 0.75 --Empirically chosen. Ensures that the resultant velocity reduction is below the cap due to how Velocity Length is usually larger than any single XYZ velocity.

for unitDefID, unitDef in ipairs(UnitDefs) do
unitDefData[unitDefID] = {}
if unitDef.canFly then
unitDefData[unitDefID].canFly = true
end
if unitDef.speed and unitDef.speed > 0 then
if unitDefData[unitDefID].canFly then
unitDefData[unitDefID].velocityCap = unitDef.speed / Game.gameSpeed
else
unitDefData[unitDefID].velocityCap = math.max(unitDef.speed / Game.gameSpeed, velocityCap)
end
else
unitDefData[unitDefID].velocityCap = velocityCap
end

local fallDamageMultiplier = unitDef.customParams.fall_damage_multiplier or 1.0
fallDamageMultipliers[unitDefID] = fallDamageMultiplier * fallDamageMagnificationFactor
unitsMaxImpulse[unitDefID] = unitDef.mass * maxImpulseMultiplier
unitsMinImpulse[unitDefID] = unitDef.mass * minImpulseMultiplier
unitMasses[unitDefID] = unitDef.mass
end

Expand All @@ -82,7 +102,6 @@ for weaponDefID, wDef in ipairs(WeaponDefs) do
end
end


local function maxDamage(damages)
local damage = damages[0]
for i = 1, #damages do
Expand All @@ -106,8 +125,12 @@ local function getImpulseMultiplier(unitDefID, weaponDefID, damage)
impulseFactor = weaponDefIDImpulses[weaponDefID].impulseFactor or 1
end
local impulse = (damage + impulseBoost) * impulseFactor
local maxImpulse = unitsMaxImpulse[unitDefID]
local impulseMultiplier = mathMin(maxImpulse/impulse, 1) -- negative impulse values are not capped.
local impulseMultiplier
if impulse < unitsMinImpulse[unitDefID] then
impulseMultiplier = 0
else
impulseMultiplier = mathMin(unitsMaxImpulse[unitDefID]/impulse, 1) -- negative impulse values are not capped.
end
return impulseMultiplier
end

Expand Down Expand Up @@ -141,7 +164,9 @@ function gadget:UnitPreDamaged(unitID, unitDefID, unitTeam, damage, paralyzer, w
if not weaponDefIgnored[weaponDefID] and weaponDefID >= 0 then --this section handles limiting maximum impulse
local impulseMultiplier = 1
impulseMultiplier = getImpulseMultiplier(unitDefID, weaponDefID, damage)
unitInertiaCheckFlags[unitID] = gameFrame + velocityWatchFrames
if not unitInertiaCheckFlags[unitID] and impulseMultiplier ~= 0 then
unitInertiaCheckFlags[unitID] = {expirationFrame = gameFrame + velocityWatchFrames, velocityCap = unitDefData[unitDefID].velocityCap}
end
return damage, impulseMultiplier
elseif (weaponDefID == groundCollisionDefID or weaponDefID == objectCollisionDefID) and (isValidCollisionDirection(unitID) or fallingUnits[unitID]) then
local healthRatioMultiplier, health = massToHealthRatioMultiplier(unitID, unitDefID)
Expand All @@ -161,21 +186,29 @@ function gadget:UnitUnloaded(unitID, unitDefID, unitTeam, transportID, transpor
fallingUnits[unitID] = true --units falling from transports should take collision damagee from any trajectory, including when bouncing off of other objects.
end

function gadget:UnitEnteredAir(unitID, unitDefID, unitTeam)
if not transportedUnits[unitID] and not unitDefData[unitDefID].canFly then
launchedUnits[unitID] = true
end
end

function gadget:UnitLeftAir(unitID, unitDefID, unitTeam)
fallingUnits[unitID] = nil
launchedUnits[unitID] = nil
end

function gadget:UnitDestroyed(unitID, unitDefID, unitTeam, attackerID, attackerDefID, attackerTeam)
transportedUnits[unitID] = nil
unitInertiaCheckFlags[unitID] = nil
launchedUnits[unitID] = nil
fallingUnits[unitID] = nil
end

function gadget:GameFrame(frame)
for unitID, expirationFrame in pairs(unitInertiaCheckFlags) do
for unitID, data in pairs(unitInertiaCheckFlags) do
if not transportedUnits[unitID] and not spGetUnitIsDead(unitID) then
local velX, velY, velZ, velocityLength = spGetUnitVelocity(unitID)

if velocityLength > velocityCap then -- Sheer off extreme velocities within acceptable range
if not data.velocityReduced and velocityLength > data.velocityCap then
local verticalVelocityCapThreshold = 0.07 --value derived from empirical testing to prevent fall damage and goofy trajectories from impulse
local horizontalVelocity = math.sqrt(velX^2 + velZ^2)
local newVelY = mathAbs(mathMin(horizontalVelocity * verticalVelocityCapThreshold, velY))
Expand All @@ -186,14 +219,15 @@ function gadget:GameFrame(frame)
newVelYToOldVelYRatio = 1
end

local scale = velLengthOffsetCap / mathMax(mathAbs(velX), mathAbs(newVelY), mathAbs(velZ))
local scale = data.velocityCap / mathMax(mathAbs(velX), mathAbs(newVelY), mathAbs(velZ))

velX = velX * scale * newVelYToOldVelYRatio
velZ = velZ * scale * newVelYToOldVelYRatio

spSetUnitVelocity(unitID, velX, newVelY, velZ)
expirationFrame = frame + velocityWatchFrames
elseif velocityLength > velocitySlowdownThreshold then
data.velocityReduced = true
data.expirationFrame = frame + velocityWatchFrames
elseif launchedUnits[unitID] and velocityLength > velocitySlowdownThreshold then
local decelerateHorizontal = 0.98 --Number empirically tested to produce optimal deceleration without looking goofy.
local decelerateVertical
if velY < 0 then
Expand All @@ -202,12 +236,14 @@ function gadget:GameFrame(frame)
decelerateVertical = 0.92 --Number empirically tested to produce optimal deceleration without looking goofy.
end
spSetUnitVelocity(unitID, velX * decelerateHorizontal, velY * decelerateVertical, velZ * decelerateHorizontal)
expirationFrame = frame + velocityWatchFrames
elseif expirationFrame < frame then
data.expirationFrame = frame + velocityWatchFrames
elseif data.expirationFrame < frame then
unitInertiaCheckFlags[unitID] = nil
launchedUnits[unitID] = nil
end
else
unitInertiaCheckFlags[unitID] = nil
launchedUnits[unitID] = nil
end
end

Expand Down

0 comments on commit 6c315a5

Please sign in to comment.