1 2 3 4 | ||
Editor: DonovanBaarda
Time: 2023/09/25 15:21:05 GMT+10 |
||
Note: Fix max() -> min() error in blocking. |
changed: - blockDamage = max(attackDamage, weaponBlock * (self.S - self.Smin)) blockDamage = min(attackDamage, weaponBlock * (self.S - self.Smin))
The RealisticFatigue document was a bit too messy and Oblivion specific. This is an attempt to tidy and simplify it into something that can be used for other games. In particular, "stamina" is a better name than "fatigue".
This is a free to use human health and stamina model for computer games. It attempts to define a simple model of how health, encumbrance, stamina, and gradients affect movement and physical actions for games. It is based on realistic models and research, but simplified and easy to tune for playability vs realism. The recommended tuning is a compromise that sacrifices realism for playability to make stamina a fun tactical element without being an annoying limitation.
It is based on modelling the short-term energy stored in blood and muscles that burns with physical activity and restores from other longer-term energy sources and respiration within minutes. It does not model longer-term effects like running to collapse, metabolising food, or sleep deprivation, but it could be extended to include them.
Health is modeled as a measure of how much damage and blood-loss can be taken before death, and is translated into a fraction of muscles that are damaged or blood that is lost and cannot be applied to physical actions.
Stamina is modeled as the fraction of undamaged unfatigued muscles and/or oxygenated blood available for performing physical actions. All physical actions burn stamina and are affected by the amount of stamina available.
Encumbrance is modeled as the fraction of a maximum sustainable lift being carried, and is translated into a fraction of muscles that are preoccupied with lifting and cannot be applied to other actions. Lifting also burns stamina, and contributes to the actors total weight which affects movement.
The effects of gradients on movement are taken into account for calculating movement speeds and stamina burned.
There is a Python implementation of this model available at http://minkirri.apana.org.au/~abo/projects/oblivion/StaminaModel/. Note this model is possibly more up to date than this description.
The following attributes are in the range 0.0 (empty) to 1.0 (full). Note that there can be underlying health/strength/endurance/etc attributes that translate these into different numbers, but from the fatigue models point of view, the only thing that matters is the fraction of full health/stamina/encumbrance:
H is the fraction of life remaining after taking damage. S is the fraction of un-damaged, un-fatigued muscles available for lifting/moving/fighting/etc. E is the fraction of max lift worth of equipment currently carried.
Additionally, the following dynamic attributes are derived from H and E to represent weight, fraction of muscles damaged and blood-loss by wounds, and fraction of muscles preoccupied by lifting::
M the actors total weight including encumbrance in bodyweights. Smin is the minimum stamina fraction required to lift what the actor is carrying. Smax is the max stamina fraction limit of the muscles not damaged and blood-loss from wounds.
The H, S, E values could be displayed using bars on a HUD. The Smin value could be shown using a black min stamina marker on the bottom end of the stamina bar. The Smax value could be indicated as a red max stamina marker on the top end of the stamina bar.
The following attributes adjust how quickly you get tired and recover. They are normally constants but could be tweaked per-character based on some endurance attribute:
F = staminaBurn = 0.04, the rate active muscles fatigue in fraction/second. R = staminaReturn = 0.04, the rate fatigued muscles recover in fraction/second.
The following attributes adjust the maximum movement rates. They could be constants, class based, or character based on other attributes:
moveMax = 12.0, max flat running speed in m/s. climbMax = 3.0, max vertical climbing speed in m/s. jumpMax = 1.5, max vertical jump height in m.
Note that R/(F+R) is the "steady state" or minimum S value reached for constant running, so the the R/F ratio is important. Studies on muscles show F is 0.008~0.033, R is 0.0026~0.013, and R/F is 0.3~0.5. However running doesn't involve continuously clenching all muscles so F is less. The model closely matches world records for 100m, 200m, 400m, and 800m with R=0.13, F=0.0165, moveMax=12.0. For gameplay reasons speeding it up to F=0.04, R=0.04 might be better.
For moveMax, record 100m top speeds average about 10m/sec and peak at 12m/sec. Depending on how fast it feels moveMax=10~12m/s should be OK.
Studies on walking/running on gradients show going uphill is about 12.5x as expensive/meter as running horizontally, so realistically climbMax=moveMax/12.5. However, record 15m speed climbing top speed is 6.26sec, or about 2m/sec, compared to 100m record top speeds of about 12m/sec, which suggests climbing vertically uses only ~6x the energy as horizontal running and half as much as uphill running. I suspect that climbing using both arms and legs allows nearly double the power output compared to running with just legs, but the energy/meter is the same, so once you run down the energy stored in your muscles and are limited by respiration, you would slow to the expected 1/12.5 climbing rate. It often surprises players how much affect climbing really has, and games tend to have exaggerated gradients. For playability I suggest climbMax=2.0~4.0m/s.
The (unverified) world record for a vertical jump is around 1.5m, but anything over 1m is considered extraordinary, with 0.75m being the average for professional footballers and basketballers. Micheal Jordan could reputedly jump 1.2m. The average taken for a group of typical medical students was 0.56m for men and 0.35m for women. For jumping up onto surfaces the current world standing box jump records are around 1.44m (making that claimed 1.5M vertical jump very questionable), and for running box jumps around 1.74m (1.8m unofficial). Depending on how the game engine handles jumping onto platforms (can you jump and duck at the top to get onto a higher platform?), you may want to use a hero box jump jumpMax=1.8m, or even super-hero/fantasy jumpMax=2m, otherwise jumpMax=1.5 is a good playability compromise.
All the following constants affect various parts of the model. I suggest not changing them, and instead changing the static attributes to adjust how fast/slow actors fatigue, recover, and move.
The following constant affects the total actor weight in bodyweights including encumbrance::
weightMult = 1.0 # The max sustainable lift in bodyweights.
The following constants affect the fraction of muscles required for lifting::
liftExp = 2.0 # can be turned up to delay the onset of encumbrance effects. liftMult = (0.5 * weightMult)^liftExp = 0.25 # can be turned down to reduce the severity of encumbrance effects.
The following constants affect the fraction of muscles damaged by wounds::
woundExp = 2.0 # can be turned up to delay the onset of wound effects. woundMult = 1.0 # can be turned down to reduce the severity of wound effects.
The following constants affect the relative energy costs of performing actions::
runMult = 1.0 # running cost per moveMax distance every second. walkMult = 4.0 # walking cost per moveMax distance at moveMax speed every second. climbMult = 2.0 # climbing up cost per climbMax vertical distance every second. downMult = climbMult/5 = 0.4 # climbing down cost per climbMax vertical distance every second. jumpMult = 1.0 # jumping cost per jumpMax vertical distance every jump.
The following constants are used to smooth vertical movement and calculate jump speeds::
climbRC = 0.2 # climb rate low-pass filter timeconstant to average vertical movement over the last 0.2 seconds. g = 9.81 # The acceleration from gravity for calculating jumpSpeeds from jumpHeights.
A real max lift is about 2x your own body weight, but anything over 1x is unsustainable. I suggest weightMult=1.0 so that the top-half of the encumbrance bar doesn't go unused and give players an unrealistic impression of how much they can carry. If you want you can allow temporary heavy lifts where E > 1.0 (over encumbered) and have them drop it or collapse when S < Smin. Note the largest professional rugby league player is 133Kg, though 100~125Kg is probably more realistic for a big hero's bodyweight.
Note setting woundMult > 1.0 means actors will run out of stamina and collapse before they die, even when unencumbered. Setting woundmult < 1.0 means actors will die before they collapse unless they are very encumbered.
The relative energy cost constants are for the the static movement speed attributes of the actor, and relative to the base running cost runMult=1.0. Downhill movement is negative work and studies show it is about 1/5th the cost of uphill movement. So for realistic uphill running have climbMax=1.0 climbMult=1.0 downMult=0.2. However, for actual vertical climbing realistic values are climbMax=2.0, climbMult=2.0, downMult=0.4. For playability we keep the vertical climbing costs, but increase the movement speed to climbMax=3.0 climbMult=2.0, downMult=0.4. This means that you burn stamina 2x as fast climbing at 1/4 the speed of running for 8x the cost/meter. I suggest keeping jumpMult/climbMult = jumpMax/climbMax so the cost of climbing and jumping is the same per vertical meter. Using jumpMult=1.0 means a jump burns as much stamina as running for 1 second.
The actors total weight in bodyweights affects movement speeds and jump heights. It is calculated as:
self.M = 1.0 + weightMult * self.E
Encumbrance preoccupies muscles, leaving less available for running/attacking/etc, and there is a minimum stamina required to lift everything you are carrying. These preoccupied muscles are also subtracted from S when considering how fast you can run, hard you can hit etc:
self.Smin = liftMult*self.E^liftExp
Note RF implements this as an additional "drain" on S, but that is slightly miss-leading as it is not really damaged/fatigued muscles and thus should not affect fatigue indicators like panting etc.
Wounds are modelled as damaged muscles, reducing your available muscles for running/fighting/etc. It applies a constant "drain" or cap on your stamina:
self.Smax = 1 - woundMult*(1-self.H)^woundExp
Whenever you are damaged/healed and your health changes, it damages/restores some of both fatigued and unfatigued muscles. There should be a change in S that reflects this. It can be calculated each frame from the change in Smax like this:
newSmax = 1 - woundMult*(1-self.H)^woundExp self.S += (newSmax - self.Smax) * self.S/self.Smax self.Smax = newSmax
Note this ensures that at low stamina a hit winds you a bit, but not too much.
Moving on a gradient affects movement speeds and stamina burn rates. The climbRate can be calculated from the surface gradient directly, or from movement over the last frame. Because vertical movement is so expensive, a small amount of jitter/bob in the actors movement can translate into a huge stamina burn. To avoid this we low-pass filter the calculated climb rate to give the average vertical movement over the last 0.2 seconds. You need to be wary of things like lifts to ensure that dz changes from going up/down on lifts doesn't burn stamina and make actors collapse. RF detects this by checking the actor is running/walking and the gradient is not too steep:
dd = math.sqrt(dz*dz + dx*dx + dy*dy) if (dd > 0.0) and (self.isRunning or self.isWalking): # We can calculate climbRate either from movement or from the surface angle if known. # climbRateNow = sin(slopeAngle) * (self.moveMax/self.climbMax) climbRateNow = (dz / dd) * (self.moveMax/self.climbMax) else: climbRateNow = 0.0 self.climbRate = (dt*climbRateNow + climbRC*self.climbRate)/(dt + climbRC)
Where:
dx,dy,dz is x,y,z travel distance in the last frame. dd is total travel distance in the last frame. slopeAngle is the elevation angle of the direction travelled. dt is the time since the last frame. climbRateNow is the climb rate this frame. climbRate is the average climb rate for the past 0.2 seconds .
Note that climbRate is in climbMax vertical distances moved per moveMax total distances moved so that it can be used directly with the energy cost tuning constants. As gradients get steeper the running/walking costs taper off and get replaced by the vertical climbing cost. Vertical movement costs are constant per meter so linear with speed. From the climbRate we can calculate the factors for the vertical movement cost, the affect on movement cost, and the affect on movement speed::
slopeClimbMult = max(self.climbRate, -self.climbRate*downMult/climbMult) slopeMoveMult = 1.0/(self.climbRate**2 + 1.0) slopeSpeedMult = 1.0/(slopeClimbMult + slopeMoveMult)
Where:
slopeClimbMult is a scaling factor on vertical movement cost for the slope. slopeMoveMult is a scaling factor on horizontal movement cost for the slope. slopeSpeedMult is a scaling factor on movement speed for the slope.
The cost of running per meter is constant, which means the cost per second varies linearly with speed. The cost of walking per meter varies with speed, so the cost per second varies to the square of speed. This means walking gets cheaper the slower you go, and at high speeds it is more efficient to run. At about 2~2.5m/second walking is the same cost per meter as running, so any faster than that it is more efficient to run. At about 1~1.3m/second walking is 1/2 the cost per meter as running. In this model we approximate it as moveMax/4 is when walking=running cost, and moveMax/8 is when it's 1/2 the cost, giving walkMult=4.0, though walkMult=5.0 might be more realistic. Combining in the gradient effects we can calculate the stamina burn and speed each frame:
# Calc the running/walking speed multipliers. runSpeedMult = self.moveEffortMult*(self.S-self.Smin)/self.M walkSpeedMult = math.sqrt(runMult/walkMult * runSpeedMult) # Calculate the running/walking state by comparing movement speeds. self.isRunning = runSpeedMult > walkSpeedMult self.isWalking = not self.isRunning and walkSpeedMult > 0.0 # Calc the fraction of muscles used for lifting. if self.isStanding: Slift = self.Smin else: Slift = 0.0 # Calculate move speed scale and fraction of muscles used for moving. if self.isRunning: moveScale = slopeSpeedMult * runSpeedMult Smove = slopeMoveMult * runMult * self.M * moveScale elif self.isWalking: moveScale = slopeSpeedMult * walkSpeedMult Smove = slopeMoveMult * walkMult * self.M * moveScale**2 else: moveScale = 0.0 Smove = 0.0 # Calc the fraction of muscles used for climbing. Sclimb = slopeClimbMult * climbMult * self.M * moveScale # Calc fraction of undamaged, fatigued muscles that are resting. Srest = (self.Smax - self.S) # Calc the moveSpeed and adjust S for the frame. self.moveSpeed = moveScale * self.moveMax self.S += (self.staminaReturn*Srest - self.staminaBurn*(Slift + Smove + Sclimb)) * dt
where:
moveEffortMult is the fraction of effort spent on moving. runSpeedMult is the running speed factor for effort, stamina, and weight. walkSpeedMult is the walking speed factor for effort, stamina, and weight. moveScale is movement speed as a fraction of moveMax. Slift is the fraction of all muscles applied to lifting. Smove is the fraction of all muscles applied to moving. Sclimb is the fraction of all muscles applied to climbing. Srest is the fraction of muscles that are fatigued and resting. moveSpeed is the movement speed in meters/sec.
For a given level of effort, we pick to either walk or run based on which would be faster. Use moveEffortMult=1.0 to go as fast as possible, and moveEffortMult=0.16 for an efficient but not too slow walk.
If you are not standing and are collapsed, lying down, or sitting, then you are not actually lifting anything so Slift=0.0. We don't make Smin zero in this case to make it clear what your minimum stamina is to stand. The moveSpeed and Smove depend on if you are running or walking.
This is how stamina is burned by jumping, and how jump height varies with stamina and encumbrance:
onJump(jump=1.0): jumpScale = jump * (S - Smin)/M jumpHeight = jumpScale * jumpMax jumpVelocity = sqrt(2*g*jumpHeight) S -= F * jumpMult * self.M * jumpScale
Where:
jump = 1.0 is the fraction of efford expended on jumping. jumpScale is the jump height as a fraction of max jump height. jumpHeight is the height of the jump in meters. jumpVelocity is take-off velocity in meters/sec (for physics models). g = 9.81m/s^2 is the gravitational constant.
Landing after a jump or fall also burns energy similar to Jumping, but it's negative work, which is about 1/5 the cost as positive work, so landing after falling from a height should burn about 1/5 the energy as jumping to that height. The stamina available at the time of the landing also gives a limit to the height you can safely land from, which should be 5x the height you can jump. Any further than that and you have insufficient spare muscles/strength to "land" yourself without injury. Note this means that for a super hero jumpMax=1.5m, unencumbered and fully prepared you should be able to "land" unharmed from 7.5m, which seems pretty super-hero. Perhaps a more realistic height would be 3x jump height.
In a game, rather than using falling height, you want to use landing velocity, which can be used to calculate an equivalent falling height.
In general, attacking is about hitting your enemy with energy, and blocking is absorbing/deflecting that energy. Stamina is energy, so stamina burned should be proportional to (potential) attack damage. Absorbing energy with a block is like negative work. Studies show negative work is 1/5 the cost of positive work, so blocking should use about 1/5 the stamina of attacking. Blocking is pitting defence energy vs attack energy, so the amount of damage that can be blocked with a given weapon should be about the same as its attack damage, and the rest of the damage gets through. Blocking a big attack with a little block can't deflect all the damage. The amount of damage delivered/deflected with a weapon should be proportional to the available stamina. Attacking/blocking is kinetic energy, which is proportional to V^2, which means attack speed should be proportional to the square-root of available stamina. For balance, the damage * speed for all weapons should be about the same.
This is the basic model, which can then be extended by adding damage, block, speed, and reach factors for different weapons, attacks, etc to take into account some are more effective at attacking/blocking than others.
The speed and effectiveness of attacks depends on available stamina, and also burns stamina:
attackDamage = weaponDamage * (self.S - self.Smin) attackSpeed = weaponSpeed * sqrt(self.S - self.Smin) attackTime = 1/attackSpeed self.S -= self.F * attackMult * attackDamage
Where:
attackMult~=0.5/healthMax is the energy cost of attacking. weaponDamage is the base attack damage for the weapon. weaponSpeed is the base weapon speed in attacks/second. attackDamage is damage dealt from the attack. attackSpeed is attack/block speed in attacks/second, attackTime is the duration of the attack/block in seconds.
For blocks, you could do exactly the same thing, or you could make the block cost depend on the damage blocked:
blockDamage = min(attackDamage, weaponBlock * (self.S - self.Smin)) blockSpeed = weaponSpeed * sqrt(self.S - self.Smin) blockTime = 1/blockSpeed self.S -= self.F * blockMult * blockDamage self.health -= attackDamage - blockDamage
Where:
blockMult~=0.1/healthMax is the energy cost of blocking. weaponBlock is the base block damage for the weapon. weaponSpeed is the base weapon speed in attacks/second. blockDamage is damage dealt from the attack. blockSpeed is attack/block speed in attacks/second, blockTime is the duration of the attack/block in seconds.
The models E,H,S can be calculated from game attributes like this:
E = encumb/encumbMax H = health/healthMax S = stamina/staminaMax
Where:
health is the actors current "health". healthMax is the actors maximum "health". stamina is the actors current "stamina". staminaMax is the actors maximum "stamina". encumb is the actors current encumbrance in equivalent Kg. encumbMax is the actors maximum encumbrance in equivalent Kg.
Note that although encumb is in Kg, it should take into account how awkward things are to carry, not just their weight. Long clumsy or sharp objects should have a higher encumbrance than their raw weight. Putting things in convenient carrying containers like backpacks should reduce awkward objects encumbrance back towards their real weight.