Finns det något smart matematiskt knep för en gummibandseffekt?
Gummibandseffekt = ju mer du töjer, ju mer motstånd får du.
Tänk dig ett godtyckligt actionspel där du har hit points (HP). Det fungerar som så att ju lägre din health % är, ju mer damage reduction (DR) har du. På full health har du 20% DR, och på 0 har du 80% (men du är död ;). Din DR går upp linjärt med att din health går ner, så på 50% HP har du 50% DR (20% + hälften av resterande 60%).
Tänk dig nu att du kodar detta spel. Om spelaren har full HP (vi säger att detta är 1000) och får ett slag för 500 skada, hur mycket HP förlorar hen? 400 för att DR på full HP är 20%? Så kan man göra, men man kan även koda en gummibandseffekt som delar upp dessa 500 HP i mindre instanser och omberäknar DR för varje instans, vilket gynnar försvararen.
Vi säger att en sådan instans är 10 "raw damage", alltså skada innan denna mitigation tagits hänsyn till. 10 mot 20% DR blir 8. Anfallaren har gjort 8 skada och har 490 kvar i poolen. Nästa instans på 10 skada ska nu göras mot lite högre DR, eftersom försvararen har 992/1000 HP och således 20.48% DR.
En effekt man märker är att ju mindre instanserna är, ju högre effektiv DR har försvararen. Samtidigt blir det väldigt tramsigt att hålla på och tynga ner koden med att köra pyttesmå instanser. Ju fler instanser, ju fler beräkningar. Min fråga är då om det finns något finurligt sätt att lösa detta matematiskt. Behöver inte vara exakt (går det ens?). Fast inverse square root blev berömt för att det är väldigt snabbt och inte exakt men väldigt nära.
Ni kan förresten köra mitt Python-script och utforska scenariet själva. Om vi använder exemplet från detta inlägg så märkte jag att instansstorleken inte har enorm effekt:
Instansstorlek (DAMAGE_STEP) | Effektiv damage reduction |
100 | 29.04% |
10 | 30.71% |
1 | 30.87% |
Här är scriptet, och det kan köras online här.
MAX_HEALTH = 1000
ATTACK_DAMAGE = 500
MIN_DR = 0.2 # Damage reduction at full health
MAX_DR = 0.8 # Damage reduction at zero health
DAMAGE_STEP = 1 # Points of raw damage to be dealt before DR is recalculated (should be an integer multiple of ATTACK_DAMAGE)
current_health = MAX_HEALTH
invested_damage = 0 # Points of pre-mitigation damage dealt
inflicted_damage = 0 # Points of post-mitigation damage dealt
def current_dr():
return MIN_DR + (MAX_DR - MIN_DR) * (1 - current_health / MAX_HEALTH)
while invested_damage < ATTACK_DAMAGE:
single_strike_damage = DAMAGE_STEP * (1 - current_dr()) # Damage of this attack
inflicted_damage += single_strike_damage # Up the total
current_health -= single_strike_damage # Subtract the post-mitigation damage from the health pool
invested_damage += DAMAGE_STEP # Up the pre-mitigation damage investment
print(f"Effective mitigation: {(1 - (inflicted_damage / ATTACK_DAMAGE)) * 100}%")
Jag tänker såhär:
Vi definierar en talföljd som är ens current_health
efter iterationer av din while-loop.
Istället för att ha ett DAMAGE_STEP
har vi ett tal, , som är antalet gånger loopen körs. Då är ens health i början och är damage. Sedan definieras rekursivt enligt:
där returnerar damage reduction för när man har hp . Du har ju skapat en sådan funktion i ditt skript, och den fungerar bra. Vi låter MIN_DR
vara , MAX_DR
vara och MAX_HEALTH
vara . Då är
Om vi låter ens health från början vara har vi talföljden:
Här kommer returnera samma sak som din while loop med samma parametrar. Notera att mitt och ditt DAMAGE_STEP har förhållandet att produkten av dem blir MAX_HEALTH. Se bild nedan från desmos:
(DAMAGE_STEP
=100)
Visa spoiler

(DAMAGE_STEP=10)
Visa spoiler

(DAMAGE_STEP=1)
Visa spoiler

Dessa stämmer bra överens med dina numeriska beräkningar (förutom , vet inte varför?)
Det "exakta" värdet med gummibandet bör då bli
Då ska vi försöka beräkna detta! Med definitionen av kan rekursionen skrivas om, efter lite algebra, till
Talföljden kan alltså skrivas på formen
, med två konstanter och .
Talföljder på denna form har en sluten formel (exempelvis här på sid 3), som ger oss att
Efter insättning av värdena på och och förenkling får man att
Lösning på
DefinieraDå är .
Det mesta här är konstanter, som vi kan se är på formen
med konstanter , , . Detta har ett känt gränsvärde, en lösning finns här (<- ordet här är en länk, jag vet inte varför det inte syns??)
Insättning av alla värden ger oss då att
Detta tror jag är det "exakta" värdet av gummibandseffekten.
Med de givna parametrarna från tidigare blir effective damage 30.88%, mycket nära
DAMAGE_STEP
= 1
Tillägg: 1 jul 2025 08:41
I mitten av inlägget råkade jag börja använda för att betyda samma sak som MAX_HEALTH. Så egentligen är
Givet pythonkoden och resonemanget ovan känns det som skadereduktionen DR som en funktion av HP kan skrivas:
Så när vi tillför en pyttliten skada dx, så minskar HP med:
Tjohej! Där har du en diffekvation som kan lösas exakt:
Med "kan lösas" menar jag tyvärr inte "av mig just nu", men det finns en exakt/analytiskt lösning som kommer att vara på formen , där b och c är <0.
Å andra sidan frågade du om det finns något finurligt sätt att lösa detta snabbt matematiskt och nämnde den berömda "fast inverse square root" (Quake III, eller hur?). Att ta en potens av e är sannolikt kostsamt, CPU-mässigt, så detta är nog inte ett svar på din fråga.
Tillägg: 1 jul 2025 16:56
OK. Fuskade lite och fick hjälp med diff.ekvationen:
Utan att bedöma om det är korrekt, kan jag konstatera att det stämmer bra med vad din pythonkod spottar ut.
För att komma runt den beräkningsmässiga kostnaden kan man väl skriva ut exponentialfunktionen med dess Taylorutveckling (runt lämplig punkt) och trunkera den vid något lämpligt värde? Tror knappast man behöver mer än typ fyra eller fem termer.
Jag vet inte hur numpy eller vilket libb du nu använder gör beräkningen men sannolikt tar det med för dina ändamål onödig precision.
@AlexMu Fint! Det finns två möjliga svar till varför vi skiljer vid N=10. Det ena är att jag använder floats, vilket datorer som bekant inte hanterar helt perfekt. Har uppdaterat koden så den kör exakt decimalberäkning. Det andra svaret är att du kör dubbelt så många iterationer i samtliga fall. 500 skada, varje steg 100, 5 steg.
@sictransit Ja, Quake 3. Nu är det ren magkänsla här, men jag tror inte potensen kostar så mycket. ChatGPTs svar slutade med: No, it's not computationally expensive in practical terms—modern math libraries make calculating e^x very fast and efficient.
@naytte Ja, det slutgiltiga svaret behöver inte särskilt bra precision. Dock om man gör som i mitt script så vill man ju ha precision i varje steg för att inte avrundingsfel ska skicka allt åt pipsvängen.
Tack ska ni ha =)
Edit: Uppdaterat skript här, kan inte redigera första inlägget längre. Detta kan förresten även hantera "overkill" damage.
from decimal import Decimal, getcontext
getcontext().prec = 50 # Precision of decimal calculations
MAX_HEALTH = 1000
ATTACK_DAMAGE = 500
MIN_DR = Decimal("0.2") # Damage reduction at full health
MAX_DR = Decimal("0.8") # Damage reduction at zero health
DAMAGE_STEP = 10 # Points of raw damage to be dealt before DR is recalculated (ATTACK_DAMAGE should be an integer multiple of this)
current_health = Decimal(MAX_HEALTH)
invested_damage = Decimal("0") # Points of pre-mitigation damage dealt
def current_dr():
return MIN_DR + (MAX_DR - MIN_DR) * (1 - max(0, current_health) / MAX_HEALTH)
while invested_damage < ATTACK_DAMAGE:
single_strike_damage = DAMAGE_STEP * (1 - current_dr()) # Damage of this attack
current_health -= single_strike_damage # Subtract the post-mitigation damage from the health pool
invested_damage += DAMAGE_STEP # Up the pre-mitigation damage investment
print(f"Effective mitigation: {(1 - ((MAX_HEALTH - current_health) / ATTACK_DAMAGE)) * 100}%")
Även om det vore kostsamt för CPUn att beräkna potenser av är väl frågan om den kostnaden spelar någon roll...? Jag menar om du designar ett spel då, inte om det här bara är en roande utmaning i att göra något så optimalt som möjligt.
Moderna datorer har ju enorm beräkningskraft så jag tror inte du som utvecklare behöver beroa dig så mycket om sådana småsaker som att beräkna en potens av .