Lerp:理解線性插值
翻譯自:Derek Stobbe?Lerp: Understanding Linear Interpolation? ?Oct 7, 2016
blog.problematic.io?
如果你曾經(jīng)想要在玩家受到傷害時(shí)將他們的生命值條從綠色變?yōu)榧t色,根據(jù)能量水平計(jì)算攻擊的傷害,或者在關(guān)卡設(shè)計(jì)中從黑暗平滑地過(guò)渡到光明,那么你很可能使用了線性插值或“l(fā)erp”操作。在游戲編程中,掌握線性插值并對(duì)其應(yīng)用充滿(mǎn)信心是非常寶貴的,但我注意到也存在一些需要當(dāng)心的陷阱,所以我們來(lái)討論一下“l(fā)erp”的確切含義,以及我們?nèi)绾卧诖a中應(yīng)用它。
線性插值背后的思想相當(dāng)簡(jiǎn)單:尋找位于兩個(gè)已知值之間距離的某個(gè)百分比的未知值(從技術(shù)上講,它可以是一組任意大的已知值,但在游戲開(kāi)發(fā)中,我發(fā)現(xiàn)它通常是兩個(gè)值),為了清晰性和一致性,我們稱(chēng)第一個(gè)已知值為“a”,第二個(gè)已知值為“b”,距離百分比為“t”。以函數(shù)的形式,我們可以這樣寫(xiě): lerp(a, b, t).
這是我構(gòu)建的一個(gè)可視化工具。你可以拖動(dòng)點(diǎn)來(lái)實(shí)驗(yàn)兩點(diǎn)之間的線性插值(綠色的點(diǎn)表示插值的值);懸?;蛴|及方程頂部的某個(gè)點(diǎn)或有顏色的部分,將使所有匹配的值加粗:

有幾點(diǎn)需要注意。首先,參數(shù)要么是數(shù)字,要么是由數(shù)字組成的東西。這自然意味著,你可以在浮點(diǎn)數(shù)和整數(shù)之間做插值,而你也可以通過(guò)分別lerping每個(gè)組件的值,在顏色、數(shù)值向量(就像Unity中的Vector2和Vector3類(lèi)型),和由數(shù)字組成的自定義數(shù)據(jù)類(lèi)型之間進(jìn)行插值(Unity通過(guò)靜態(tài)方法為前兩項(xiàng)提供了包裝器 Vector3.Lerp和Color.Lerp)
其次,請(qǐng)注意插值的一個(gè)有趣屬性:
當(dāng)(藍(lán)色)t值為0時(shí),綠色的插值點(diǎn)等于第一個(gè)(紅色)點(diǎn)。當(dāng)t = 1時(shí),插值點(diǎn)等于第二個(gè)(紫色)點(diǎn)?;诖?,你可以推測(cè)當(dāng)t = 0.5時(shí),插值點(diǎn)位于兩者之間,t = 0.75是a到b距離的四分之三,以此類(lèi)推。
請(qǐng)注意,可視化工具允許你探索小于0或大于1的t值,但此時(shí),你不再是插值一個(gè)值,而是外推一個(gè)值。lerp函數(shù)的許多實(shí)現(xiàn)(包括Unity的Mathf.Lerp)將t值夾在0和1之間,這意味著你將無(wú)法重現(xiàn)超出0 > t > 1的結(jié)果:
當(dāng)夾緊輸入時(shí),例如lerp(a, b, 1.12358),給出與lerp(a, b, 1.0)相同的結(jié)果。
考慮到這一點(diǎn),讓我們談?wù)勎医?jīng)常看到開(kāi)發(fā)人員犯的一個(gè)錯(cuò)誤:使用lerp在值之間進(jìn)行插值,其中t值是由時(shí)間驅(qū)動(dòng)的。請(qǐng)看下面的偽代碼:

function charge_attack (target_value) {
? player.attack_power = lerp(0, target_value, elapsed_game_time);
}
function update_game () {
? if (button_held) {
? ? charge_attack(target_value);
? }
}
在這個(gè)例子中,當(dāng)玩家按住按鈕時(shí),我們希望代表玩家攻擊力的某些值在一段時(shí)間內(nèi)(比如一秒鐘)
從初始值0變?yōu)槟繕?biāo)值。
然而,這個(gè)實(shí)現(xiàn)中有一個(gè)bug
還記得lerp函數(shù)的t值是如何被限制在[0,1]范圍內(nèi)的嗎?
假設(shè)我們的elapsed_game_time值(由Time表示,例如Unity里的時(shí)間)
是一個(gè)表示游戲已經(jīng)運(yùn)行的秒數(shù)的浮點(diǎn)數(shù)
并且在游戲開(kāi)始時(shí)從0開(kāi)始遞增,這段代碼只會(huì)在游戲的第一秒按預(yù)期工作
在這之后,“消耗時(shí)間”將是一個(gè)大于1的數(shù)字,并且調(diào)用charge_attack會(huì)
立即將attack_power設(shè)置為目標(biāo)值,而不是隨著時(shí)間的推移不斷增加,讓玩家進(jìn)行完全充滿(mǎn)能量的攻擊而不受懲罰,
從而破壞我們完美調(diào)整的游戲平衡。
我們?nèi)绾谓鉀Q這個(gè)問(wèn)題?
我們需要將t的值重新映射到0到1(包括0和1)之間,最簡(jiǎn)單的方法是緩存用戶(hù)開(kāi)始按下按鈕的時(shí)間。
在我們的示例實(shí)現(xiàn)中,它可能看起來(lái)像這樣:
function charge_attack (target_value) {
? progress = elapsed_game_time - button_pressed_at;
? player.attack_power = lerp(0, target_value, progress);
}
function update_game () {
? if (button_pressed) {
? ? button_pressed_at = elapsed_game_time;
? }
if (button_held) {
? ? charge_attack(target_value);
? }
}
唯一不同的是緩存了按下按鈕的時(shí)間,并從游戲循環(huán)中調(diào)用charge_attack更新attack_power時(shí),當(dāng)前消耗的游戲時(shí)間中減去這個(gè)時(shí)間。
我們可以看到,這適用于我們所期望的1秒的持續(xù)時(shí)間,例如,假設(shè)玩家在3.1秒時(shí)按下按鈕。
在半秒之后,這樣在游戲時(shí)間為3.6秒時(shí),我們計(jì)算的t值是3.6 - 3.1,也就是0.5…正是我們想要的值。
如果我們不想在一秒內(nèi)將attack_power改變?yōu)槟繕?biāo)值,而是想在更長(zhǎng)的一段時(shí)間內(nèi)改變它,比如2.5秒呢?
我們的實(shí)現(xiàn)再次失敗了,因?yàn)樵诎聪掳粹o一秒后,elapsed_game_time - button_pressed_at超過(guò)1.0,并且被夾住了。我們?nèi)绾谓鉀Q這個(gè)問(wèn)題?
好吧,如果我們看看我們當(dāng)前的解決方案,我們可以看到elapsed_game_time - button_pressed_at給了我們一個(gè)小數(shù),
表示我們當(dāng)前在“填充”attack_power的進(jìn)展,使其等于target_value。
我們知道任何數(shù)字除以1等于這個(gè)數(shù)本身,
所以我們可以用公式(elapsed_game_time - button_pressed_at) / 1.0表示我們的進(jìn)度。
巧合的是(或者不是),1.0恰好代表了充能效果持續(xù)時(shí)間的舊需求,所以如果我們把這個(gè)常量換成一個(gè)表示充能時(shí)間的變量,就又回到了正軌:
function charge_attack (target_value) {
? progress = (elapsed_game_time - button_pressed_at) / charge_duration;
? player.attack_power = lerp(0, target_value, progress);
}
function update_game () {
? if (button_pressed) {
? ? button_pressed_at = elapsed_game_time;
? }
if (button_held) {
? ? charge_attack(target_value);
? }
}
一切都按照預(yù)期運(yùn)行,我們還得到一個(gè)額外的好處,就是可以輕松地調(diào)整充能持續(xù)時(shí)間來(lái)控制玩家的攻擊需要多長(zhǎng)時(shí)間才能達(dá)到最大威力。
游戲平衡完好無(wú)損!
正如我希望展示的那樣,在游戲中成功使用線性插值的問(wèn)題域主要涉及到如何將 t 進(jìn)度變量映射到0到1之間的浮點(diǎn)數(shù)。