5 svar
247 visningar
thedifference behöver inte mer hjälp
thedifference Online 523
Postad: 30 jun 21:46

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}%")
AlexMu 771
Postad: 30 jun 23:02 Redigerad: 30 jun 23:21

Jag tänker såhär: 

Vi definierar en talföljd ana_n som är ens current_health efter nn iterationer av din while-loop.

Istället för att ha ett DAMAGE_STEP har vi ett tal, NN, som är antalet gånger loopen körs. Då är a0a_0 ens health i början och DD är damage. Sedan definieras ana_n rekursivt enligt:

an+1=an-DN1-f(an)\displaystyle a_{n+1} = a_n - \frac{D}{N}\left(1-f(a_n)\right)

där f(an)f(a_n) returnerar damage reduction för när man har hp ana_n. Du har ju skapat en sådan funktion i ditt skript, och den fungerar bra. Vi låter MIN_DR vara d1d_1, MAX_DR vara d2d_2 och MAX_HEALTH vara hh. Då är

f(x)=d1+d2-d11-xhf(x) = d_1 + \left(d_2-d_1\right)\left(1-\frac xh\right)

Om vi låter ens health från början vara h0h_0 har vi talföljden:

a0=h0a_0 = h_0
an+1=an-DN1-f(an)a_{n+1} = a_n - \frac{D}{N}\left(1-f(a_n)\right) 

Här kommer aNa_N returnera samma sak som din while loop med samma parametrar. Notera att mitt NN och ditt DAMAGE_STEP har förhållandet att produkten av dem blir MAX_HEALTH.  Se bild nedan från desmos: 

N=10N=10 (DAMAGE_STEP=100)

Visa spoiler

 

N=100N=100 (DAMAGE_STEP=10)

Visa spoiler

 

N=1000N=1000 (DAMAGE_STEP=1)

Visa spoiler

Dessa stämmer bra överens med dina numeriska beräkningar (förutom N=10N=10, vet inte varför?)
Det "exakta" värdet med gummibandet bör då bli

limNaN\displaystyle \lim_{N \to \infty} a_N

Då ska vi försöka beräkna detta! Med definitionen av f(x)f(x) kan rekursionen skrivas om, efter lite algebra, till 

an=an-11+Dd1-d2NH+DNd2-1\displaystyle a_n = a_{n-1}\left(1+\frac{D\left(d_{1}-d_{2}\right)}{NH}\right)+\frac{D}{N}\left(d_{2}-1\right)

Talföljden kan alltså skrivas på formen

an=kan-1+ca_n = ka_{n-1} + c, med två konstanter kk och cc

Talföljder på denna form har en sluten formel (exempelvis här på sid 3), som ger oss att 

an=h0kn+1-h0kn-ck-1\displaystyle a_n = \frac{h_0 k^{n+1}-h_0 k^n - c}{k-1} 

Efter insättning av värdena på kk och cc och förenkling får man att

an=H1-d2+1+Dd1-d2NHnH·d2-1+h0·d1-d2d1-d2\displaystyle a_n = \frac{H\left(1-d_{2}\right)+\left(1+\frac{D\left(d_{1}-d_{2}\right)}{NH}\right)^{n}\left(H\cdot\left(d_{2}-1\right)+h_{0}\cdot\left(d_{1}-d_{2}\right)\right)}{d_{1}-d_{2}}

Lösning på limNaN\displaystyle \lim_{N \to \infty} a_N Definiera gx=H1-d2d1-d2+1+Dd1-d2xHxH·d2-1+h0·d1-d2d1-d2\displaystyle g\left(x\right) = \frac{H\left(1-d_{2}\right)}{d_{1}-d_{2}}+\frac{\left(1+\frac{D\left(d_{1}-d_{2}\right)}{xH}\right)^{x}\left(H\cdot\left(d_{2}-1\right)+h_{0}\cdot\left(d_{1}-d_{2}\right)\right)}{d_{1}-d_{2}}

Då är g(N)=aNg(N) = a_N

Det mesta här är konstanter, som vi kan se är g(x)g(x) på formen

gx=c1+c21+c3xx\displaystyle g\left(x\right)=c_1 + c_2\left(1 + \frac{c_3}{x}\right)^x

med konstanter c1c_1, c2c_2, c3c_3.  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??)

limxgx=c1+c2·ec3\lim_{x\to \infty} g\left(x\right) = c_1 + c_2 \cdot e^{c_3}

Insättning av alla värden ger oss då att 

limNaN=H1-d2d1-d2+H·d2-1+h0·d1-d2d1-d2eDd1-d2H\lim_{N \to \infty} a_N = \frac{H\left(1-d_{2}\right)}{d_{1}-d_{2}}+\frac{\left(H\cdot\left(d_{2}-1\right)+h_{0}\cdot\left(d_{1}-d_{2}\right)\right)}{d_{1}-d_{2}}e^{\frac{D\left(d_{1}-d_{2}\right)}{H}} 

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
Också, jag ber om ursäkt om något här på slutet var otydligt, jag blev rätt trött medan jag skrev! 


Tillägg: 1 jul 2025 08:41

I mitten av inlägget råkade jag börja använda HH för att betyda samma sak som MAX_HEALTH. Så egentligen är H=hH=h

Givet pythonkoden och resonemanget ovan känns det som skadereduktionen DR som en funktion av HP kan skrivas:

DR(h)=0,2+0,61-h1000=0,8-0,6h1000

Så när vi tillför en pyttliten skada dx, så minskar HP med:

dH=-dx×(1-DR(H))=-dx×(1-(0,8-0,6H1000))=-dx(0,2+0,6H1000)

Tjohej! Där har du en diffekvation som kan lösas exakt:

dHdx=-0,2+0,6H1000

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 H(x)=a×ebx+c, 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:

H(x)=1333,3¯×e-0,0006x-333,3¯

Utan att bedöma om det är korrekt, kan jag konstatera att det stämmer bra med vad din pythonkod spottar ut.

naytte Online 6757 – Moderator
Postad: 1 jul 16:47 Redigerad: 1 jul 16:55

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.

thedifference Online 523
Postad: 2 jul 21:15 Redigerad: 2 jul 21:17

@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 ee ä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 ee

Svara
Close