算:坐標(biāo)、移動(dòng)與傷害公式實(shí)戰(zhàn)解析)
做2D游戲做到中后期你會(huì)發(fā)現(xiàn)真正卡住進(jìn)度的往往不是美術(shù)資源也不是動(dòng)畫狀態(tài)機(jī)而是藏在各個(gè)邏輯里的計(jì)算。這篇文檔是2D游戲制作系列的計(jì)算篇重點(diǎn)解決 UE4 里坐標(biāo)換算、移動(dòng)手感、戰(zhàn)斗傷害這些天天都要碰的數(shù)學(xué)問(wèn)題。前幾篇已經(jīng)把紙片人顯示、動(dòng)畫、輸入基礎(chǔ)搭好了這一期開始我們把目光從“怎么讓角色動(dòng)起來(lái)”轉(zhuǎn)移到“怎么讓角色動(dòng)得對(duì)、打得準(zhǔn)、數(shù)值不崩”。寫這篇之前我把手上這個(gè)2D橫版Demo里所有涉及計(jì)算的節(jié)點(diǎn)翻了一遍發(fā)現(xiàn)大多數(shù)問(wèn)題都不是節(jié)點(diǎn)不會(huì)用而是對(duì)“算出來(lái)的結(jié)果應(yīng)該是什么”心里沒(méi)數(shù)。坐標(biāo)單位搞混、擊退方向飛上天、傷害莫名其妙變成負(fù)數(shù)這些問(wèn)題用Print String一看就露餡但排查過(guò)程非常浪費(fèi)時(shí)間。所以這篇文檔不講虛的全部圍繞實(shí)際開發(fā)里會(huì)遇到的“計(jì)算”展開適合已經(jīng)搭好2D基礎(chǔ)、正在調(diào)數(shù)值和邏輯的同學(xué)作為備用參考。1. 這一期為什么值得單獨(dú)拿出來(lái)說(shuō)1.1 計(jì)算不像動(dòng)畫和素材錯(cuò)了不會(huì)直接報(bào)錯(cuò)動(dòng)畫資源缺失會(huì)紅屏材質(zhì)節(jié)點(diǎn)斷裂會(huì)顯示紫色你一眼就能看到問(wèn)題。但計(jì)算出錯(cuò)的時(shí)候畫面往往一切正常只有數(shù)值行為不對(duì)角色速度略快、傷害偶爾為負(fù)、鏡頭邊界露出黑邊、敵人在某個(gè)角度被擊飛得很離譜。這類問(wèn)題最麻煩的地方在于它不會(huì)彈錯(cuò)誤提示只會(huì)在特定輸入組合下悄悄出現(xiàn)。我在做這個(gè)2D Demo的過(guò)程中就踩過(guò)一個(gè)典型的坑角色加速移動(dòng)時(shí)我把目標(biāo)速度直接放到了每幀執(zhí)行的邏輯里卻忘了乘DeltaTime。看起來(lái)代碼完全沒(méi)問(wèn)題但60幀和30幀下角色移速差了將近一倍。這種問(wèn)題如果不是單獨(dú)把“計(jì)算”拎出來(lái)審查根本不顯眼。把計(jì)算單獨(dú)成篇的另一個(gè)原因是藍(lán)圖節(jié)點(diǎn)一旦拖多了可讀性會(huì)快速下降。公式邏輯混在事件分發(fā)、狀態(tài)切換中間變量名又起得隨意最后沒(méi)人敢改數(shù)值。我后來(lái)把所有常用計(jì)算統(tǒng)一收進(jìn)Blueprint Function Library每個(gè)函數(shù)只做一件事參數(shù)命名帶單位后綴比如Speed_cm_s、Damage_f。事實(shí)證明后續(xù)調(diào)手感、調(diào)戰(zhàn)斗數(shù)值的效率翻了不止一倍。1.2 這篇文檔覆蓋哪些計(jì)算范圍考慮到2D游戲最核心的體驗(yàn)本文主要覆蓋三塊空間坐標(biāo)換算、移動(dòng)與插值計(jì)算、戰(zhàn)斗與數(shù)值計(jì)算。空間坐標(biāo)這部分解決“對(duì)象到底出現(xiàn)在屏幕哪個(gè)位置”移動(dòng)插值解決“角色手感是否跟手”戰(zhàn)斗數(shù)值解決“傷害公式怎么設(shè)計(jì)才不會(huì)數(shù)值膨脹”。最后還會(huì)補(bǔ)充我實(shí)際調(diào)試中遇到的典型陷阱以及排查手段。不覆蓋的內(nèi)容也提前說(shuō)明骨骼動(dòng)畫、物理模擬的底層算法這些不在討論范圍里。我們用的是純藍(lán)圖實(shí)現(xiàn)項(xiàng)目是簡(jiǎn)單的2D橫版動(dòng)作Demo沒(méi)有引入C擴(kuò)展也沒(méi)有做聯(lián)網(wǎng)同步。計(jì)算層面的思路和參數(shù)設(shè)計(jì)仍然可以通用套用到不同項(xiàng)目上只是具體數(shù)值需要你根據(jù)自己游戲類型調(diào)整。2. 空間計(jì)算坐標(biāo)、像素和視口換算2.1 先搞清楚UE4里的度量單位UE4默認(rèn)的一單位是1厘米這是所有空間計(jì)算的基準(zhǔn)。2D游戲里我們經(jīng)常直接把圖片素材當(dāng)成“像素”來(lái)想但放進(jìn)引擎后這個(gè)“像素”會(huì)被換算成引擎單位。Paper2D項(xiàng)目設(shè)置里有一個(gè)Pixels Per Unreal UnitPPU的概念默認(rèn)情況下可以理解為1像素對(duì)應(yīng)1引擎單位也就是1厘米。這個(gè)比例對(duì)2D游戲非常重要。假設(shè)你導(dǎo)入了一張512×512的立繪PPU設(shè)為1那它在世界里就是512單位寬。如果相機(jī)可視寬度是960單位這張圖就占了屏寬一半還多。如果你的美術(shù)資源全是美術(shù)同學(xué)按1280×720的分辨率出的直接按1:1導(dǎo)入橫版關(guān)卡會(huì)變得很大角色跑起來(lái)需要很長(zhǎng)的路程。我見過(guò)不少新手項(xiàng)目最后所有坐標(biāo)都大得離譜就是沒(méi)意識(shí)到PPU對(duì)世界尺寸的影響。我的做法是先確定“一個(gè)設(shè)計(jì)分辨率下對(duì)應(yīng)多少世界單位”。比如我想讓1920像素寬的屏幕背景正好鋪滿就把相機(jī)OrthoWidth設(shè)為1920默認(rèn)PPU取1那布局時(shí)每1像素就是1引擎單位。如果你覺(jué)得數(shù)值太大也可以把PPU設(shè)為2即2像素對(duì)應(yīng)1單位這樣同樣一張圖導(dǎo)入后邏輯尺寸會(huì)變成一半。確定之后所有角色規(guī)格、攻擊距離、關(guān)卡長(zhǎng)度都按同一套比例來(lái)算后面就不會(huì)亂。2.2 世界坐標(biāo)、視口坐標(biāo)和屏幕坐標(biāo)的換算關(guān)系2D游戲開發(fā)中經(jīng)常需要把某個(gè)Actor的位置換算成屏幕坐標(biāo)。比如血條UI要跟隨角色頭頂、小地圖標(biāo)記要吸附到屏幕邊緣、傷害數(shù)字要在受擊點(diǎn)附近彈出這些都涉及坐標(biāo)空間轉(zhuǎn)換。UE4藍(lán)圖里主要用兩個(gè)節(jié)點(diǎn)Project World Location to Screen負(fù)責(zé)把世界坐標(biāo)轉(zhuǎn)屏幕坐標(biāo)Deproject Screen to World負(fù)責(zé)把屏幕坐標(biāo)轉(zhuǎn)回世界坐標(biāo)方向。這里有一個(gè)容易誤解的地方Deproject返回的是從相機(jī)位置出發(fā)的一條射線不是單一目標(biāo)點(diǎn)。在透視相機(jī)下你要自己取射線上的某一段距離來(lái)定位在2D游戲常用的正交相機(jī)下這條射線和平面的交疊關(guān)系更簡(jiǎn)單基本可以直接當(dāng)作世界坐標(biāo)參考來(lái)用。我封裝的隨機(jī)出生點(diǎn)函數(shù)就是這樣實(shí)現(xiàn)的先拿到Viewport Size把屏幕左下角和右上角分別Deproject到世界空間得到兩個(gè)點(diǎn)然后在這兩點(diǎn)范圍內(nèi)做隨機(jī)插值生成一個(gè)屏幕范圍內(nèi)的世界坐標(biāo)。2.3 正交相機(jī)視野邊界計(jì)算2D游戲鏡頭通常用正交相機(jī)核心參數(shù)是OrthoWidth。很多人只設(shè)置了OrthoWidth就開拍沒(méi)有意識(shí)到視野高度是由寬高比決定的。公式很簡(jiǎn)單可視寬度等于OrthoWidth可視高度等于OrthoWidth除以寬高比。寬高比是Viewport Size的X除以Y比如1920×1080的寬高比約1.78。所以在1920×1080項(xiàng)目里如果你把OrthoWidth設(shè)為1920相機(jī)能看到的高度就是1080正好等于屏幕分辨率。按這個(gè)思路你就可以精確推算角色從關(guān)卡左邊走到右邊時(shí)相機(jī)應(yīng)該在什么范圍移動(dòng)才不會(huì)讓背景露餡。我當(dāng)時(shí)給橫版關(guān)卡做了邊界限制代碼如下CameraHalfWidth OrthoWidth / 2 CameraMinX LeftBoundary CameraHalfWidth CameraMaxX RightBoundary - CameraHalfWidth 相機(jī)X Clamp(角色X, CameraMinX, CameraMaxX)關(guān)卡總寬度3000OrthoWidth設(shè)為960時(shí)CameraHalfWidth為480。那么相機(jī)X只允許在480到2520之間移動(dòng)。這個(gè)計(jì)算不復(fù)雜但能避免最丑的露邊問(wèn)題。如果關(guān)卡左右還有對(duì)稱的裝飾背景我通常會(huì)在LeftBoundary和RightBoundary上額外留出一兩百單位的安全余量防止鏡頭在極端位置看到背景邊緣。3. 移動(dòng)手感計(jì)算從輸入到速度3.1 輸入只是方向速度才是手感軸事件返回的值是-1到1之間的“方向”直接把這個(gè)值加到位置上角色會(huì)有一種“鍵盤騎士”的僵硬感沒(méi)有加速和慣性。要做出2D動(dòng)作游戲那種輕重有別的手感中間必須再加一層速度插值。我常用的結(jié)構(gòu)是先定義目標(biāo)速度目標(biāo)速度等于軸輸入乘以最大跑步速度然后用FInterpTo把當(dāng)前速度平滑逼近目標(biāo)速度。FInterpTo每幀都會(huì)計(jì)算出一個(gè)新值參數(shù)里的InterpSpeed控制逼近速度數(shù)值越大角色越快達(dá)到目標(biāo)速度手感越靈敏。一個(gè)實(shí)際配置示例TargetSpeed InputAxis * 600.0 CurrentSpeedX FInterpTo(CurrentSpeedX, TargetSpeed, DeltaTime, 8.0) NewLocation GetActorLocation() (CurrentSpeedX * DeltaTime * FVector(1,0,0))這里AccelerationRate取8手感偏輕快。如果想做重裝戰(zhàn)士那種起步沉重的角色降到2到3起步會(huì)有明顯延遲感。不同角色類型對(duì)參數(shù)要求差別很大我建議直接把加速度和最大速度做成角色類的變量調(diào)試時(shí)不用改藍(lán)圖邏輯。3.2 為什么所有移動(dòng)都要乘DeltaTime這個(gè)點(diǎn)老生常談但真的是2D項(xiàng)目里出現(xiàn)頻率最高的問(wèn)題。UE4的Tick事件每一幀都會(huì)執(zhí)行但每幀間隔不一樣。60幀下1秒分成60次更新144幀下分成144次。如果每幀移動(dòng)固定距離高幀率角色就跑得更快。所有和速度相關(guān)的位移計(jì)算最終都必須乘DeltaTime。DeltaTime就是這個(gè)“每幀持續(xù)了多少秒”的數(shù)值乘上去之后速度就變成每秒多少單位了。一個(gè)很容易搞混的地方是如果用了MovementComponent并調(diào)用AddMovementInput引擎內(nèi)部通常已經(jīng)幫你處理了DeltaTime這時(shí)候你不要再手動(dòng)疊加一步同一速度的位移否則等于二次加速。我做這個(gè)Demo時(shí)敵人AI的移動(dòng)和角色移動(dòng)用了兩套寫法結(jié)果敵人移動(dòng)速度在不同刷新率下表現(xiàn)不一致。后來(lái)統(tǒng)一改成“速度值乘以DeltaTime再疊加到位置”的寫法問(wèn)題才徹底消失。所以每次新加移動(dòng)邏輯先問(wèn)自己一句這個(gè)數(shù)值是每秒的概念還是每幀的概念3.3 插值不只是用于移動(dòng)插值計(jì)算在2D游戲里還有兩個(gè)很重要的應(yīng)用場(chǎng)景鏡頭跟隨和血量條平滑變化。鏡頭如果直接綁定到角色位置角色跳躍時(shí)鏡頭會(huì)非常生硬。更常用的做法是讓鏡頭位置每幀向目標(biāo)位置靠近一點(diǎn)使用VInterpTo節(jié)點(diǎn)即可。DesiredCameraLocation TargetActor.GetActorLocation() FVector(0, 0, 0) NewCameraLocation VInterpTo(CurrentCameraLocation, DesiredCameraLocation, DeltaTime, 5.0)InterpSpeed數(shù)值建議根據(jù)游戲類型調(diào)快節(jié)奏游戲用8到10鏡頭能迅速跟上角色探索型游戲用3到4鏡頭移動(dòng)更舒緩。插值算法的好處是無(wú)論角色怎么瞬移鏡頭始終有一個(gè)平滑過(guò)渡不會(huì)跳變。血量條、能量條也別直接設(shè)置百分比顯示而是用一個(gè)當(dāng)前顯示值和目標(biāo)百分比做FInterpTo這樣受傷掉血時(shí)血條會(huì)有一段“延遲回落”視覺(jué)反饋遠(yuǎn)比瞬間變化舒服。數(shù)值計(jì)算上就是DisplayPercent FInterpTo(DisplayPercent, TargetPercent, DeltaTime, 4.0)這種插值手感屬于低成本高收益強(qiáng)烈建議都用上。3.4 2D角色朝向與翻轉(zhuǎn)計(jì)算橫版2D角色轉(zhuǎn)向通常不是旋轉(zhuǎn)模型而是翻轉(zhuǎn)Flipbook組件的X方向。藍(lán)圖里判斷輸入方向無(wú)非是比較輸入值的正負(fù)。我建議用Sign函數(shù)而不是直接判斷大于0因?yàn)楦↑c(diǎn)數(shù)的微小波動(dòng)可能導(dǎo)致等于0的狀態(tài)被漏掉。FacingRight InputAxis 0 ? true : false翻轉(zhuǎn)的瞬間有一點(diǎn)需要注意不要每幀去設(shè)置Flipbook的Flip值只在方向改變時(shí)設(shè)置一次否則會(huì)在動(dòng)畫播放過(guò)程中反復(fù)翻轉(zhuǎn)視覺(jué)上會(huì)出現(xiàn)抖動(dòng)。我在關(guān)卡里放了一個(gè)“記錄上一幀朝向”的變量每次只有新方向和舊方向不一致時(shí)才執(zhí)行翻轉(zhuǎn)。4. 打擊判定和擊退計(jì)算4.1 攻擊判定盒的偏移計(jì)算2D動(dòng)作游戲常用BoxOverlap做攻擊判定。攻擊判定盒的位置通常不在角色中心而是偏移到武器前方。這個(gè)偏移值需要根據(jù)角色當(dāng)前的朝向動(dòng)態(tài)計(jì)算朝右時(shí)偏移量為正朝左時(shí)偏移量為負(fù)。每個(gè)角色判斷方式是ForwardOffset FVector(FacingDirection * AttackRange, VerticalOffset, 0) AttackBoxCenter GetActorLocation() ForwardOffset AttackExtent FVector(HalfWidth, HalfHeight, 0)這個(gè)計(jì)算里最關(guān)鍵的是FacingDirection要取整數(shù)。朝右為1朝左為-1而不是直接取得角色當(dāng)前的速度向量因?yàn)榻巧诠羲查g可能處于靜止?fàn)顟B(tài)或擊退狀態(tài)速度向量并不能代表他面朝的方向。我用角色朝向翻轉(zhuǎn)的布爾值來(lái)生成這個(gè)1/-1安全可靠。4.2 擊退方向的正確算法擊退是2D動(dòng)作游戲高頻使用的反饋。很多人圖省事直接取攻擊者到被攻擊者的方向向量再乘以擊退速度。這個(gè)做法在水平地面上問(wèn)題不大但如果攻擊者跳到敵人頭頂向下攻擊被攻擊者就可能被擊飛向上方45度看起來(lái)很夸張。我的處理方式是擊退方向拆成水平和垂直兩部分分別設(shè)計(jì)。水平方向只看攻擊者與被攻擊者的X軸相對(duì)位置計(jì)算出向左或向右的固定方向垂直方向則根據(jù)打擊類型定義普通攻擊通常給一個(gè)輕微向上分量挑飛攻擊才給大幅度向上分量。KnockbackDir_X AttackLocation.X TargetLocation.X ? 1 : -1 KnockbackVector FVector(KnockbackDir_X * HorizontalStrength, VerticalStrength, 0)受擊瞬間直接給被攻擊者疊加一個(gè)速度值之后速度會(huì)被移動(dòng)系統(tǒng)的摩擦和插值消化掉形成“被打退后逐漸停下”的效果。千萬(wàn)不要把擊退向量直接設(shè)置成目標(biāo)位置否則角色會(huì)被硬拉過(guò)去表現(xiàn)非常僵硬。4.3 重復(fù)打擊檢測(cè)的時(shí)間窗計(jì)算用BoxOverlap做攻擊判定時(shí)有一個(gè)很常見的bug攻擊幀持續(xù)過(guò)程中同一個(gè)敵人每幀都會(huì)被檢測(cè)到傷害會(huì)被觸發(fā)多次。解決辦法是給每個(gè)敵人記錄一次“最近受擊時(shí)間”。if (WorldTime - LastHitTime ComboInterval) 跳過(guò)本次傷害 else 記錄LastHitTime并執(zhí)行傷害ComboInterval可以設(shè)成0.2到0.3秒表示這個(gè)攻擊內(nèi)容在一個(gè)短時(shí)間窗內(nèi)不會(huì)被同一攻擊反復(fù)觸發(fā)。如果把時(shí)間窗設(shè)為0.1秒連續(xù)攻擊依然可以靠后幾幀觸發(fā)傷害這正好實(shí)現(xiàn)了連擊的傷害頻率控制。我后來(lái)把這個(gè)邏輯封到了通用函數(shù)里所有近戰(zhàn)武器都復(fù)用同一套判斷省了很多重復(fù)節(jié)點(diǎn)。5. 戰(zhàn)斗數(shù)值和核心循環(huán)計(jì)算5.1 一套可持續(xù)調(diào)整的傷害公式戰(zhàn)斗數(shù)值設(shè)計(jì)沒(méi)有絕對(duì)標(biāo)準(zhǔn)但有兩點(diǎn)核心要求數(shù)值要可控調(diào)試要直觀。我在這套2D Demo里試過(guò)減法公式和除法公式最終選了減法公式加最小傷害保底再配合暴擊和浮動(dòng)系數(shù)?;A(chǔ)公式如下最終傷害 Max(1, 攻擊力 * 技能系數(shù) - 防御力) * 傷害浮動(dòng) * 暴擊系數(shù) * 等級(jí)補(bǔ)正減法公式的好處是每個(gè)屬性含義直觀攻擊力100防御30打出來(lái)的基礎(chǔ)就是70。缺點(diǎn)是當(dāng)防御接近攻擊力時(shí)傷害會(huì)趨近于0所以要用Max函數(shù)強(qiáng)制保底1點(diǎn)傷害否則游戲會(huì)進(jìn)入“你打我不破防我打你也不掉血”的死局。防御減傷更平滑的替代方案是除法公式減傷率等于防御除以防御加常數(shù)傷害永遠(yuǎn)不會(huì)歸零但參數(shù)設(shè)計(jì)不夠直觀。如果做的是暗黑類刷寶游戲用除法公式更合適如果做的是街機(jī)動(dòng)作類減法公式的反饋更加清楚。你先明確自己的游戲類型再去選公式不要兩款游戲都在用同一套。5.2 再看等級(jí)補(bǔ)正的曲線等級(jí)差會(huì)影響傷害我用的是線性補(bǔ)正LevelDiff AttackerLevel - TargetLevel LevelFactor 1 LevelDiff * 0.05每差一級(jí)傷害浮動(dòng)5%。這個(gè)參數(shù)對(duì)同等級(jí)PVE游戲比較友好不會(huì)出現(xiàn)低級(jí)玩家完全打不動(dòng)的情況。等級(jí)差距超過(guò)10級(jí)后補(bǔ)正會(huì)變得很明顯需要根據(jù)你的設(shè)計(jì)目標(biāo)去限制最大補(bǔ)正百分比否則后期刷低級(jí)地圖時(shí)傷害會(huì)溢出過(guò)頭。5.3 暴擊、傷害浮動(dòng)與隨機(jī)數(shù)暴擊判斷用的是標(biāo)準(zhǔn)概率模型。隨機(jī)函數(shù)生成0到1之間的小數(shù)小于暴擊率就判定暴擊暴擊傷害再乘一個(gè)暴擊倍率。這里提醒一個(gè)容易被忽略的點(diǎn)不要直接拿固定概率作為唯一判定玩家連續(xù)多刀不暴擊的體驗(yàn)很差。我模仿經(jīng)典做法做了一個(gè)保底暴擊機(jī)制每次不暴擊暴擊率臨時(shí)增加一定值一旦暴擊保底計(jì)數(shù)清零。這樣雖然沒(méi)有改變長(zhǎng)期概率分布但玩家體感上會(huì)更均勻。傷害浮動(dòng)是數(shù)字不單調(diào)的關(guān)鍵。滿暴擊的傷害數(shù)字永遠(yuǎn)是整數(shù)也不舒服可以加一個(gè)0.9到1.1的隨機(jī)浮動(dòng)最后四舍五入。浮動(dòng)要注意對(duì)最終值進(jìn)行RoundToInt否則會(huì)出現(xiàn)小數(shù)傷害顯示和邏輯都不自然。5.4 冷卻時(shí)間更適合用時(shí)間戳計(jì)算技能冷卻每個(gè)人都有自己的寫法最自然但最容易出錯(cuò)的是每秒自減倒計(jì)時(shí)。這個(gè)方案在暫停游戲、時(shí)間膨脹、調(diào)試跳幀時(shí)會(huì)積累誤差。更穩(wěn)的做法是每次技能釋放時(shí)記錄一個(gè)時(shí)間戳CanUseSkill 當(dāng)前時(shí)間 下一次可使用時(shí)間 下一次可使用時(shí)間 當(dāng)前時(shí)間 冷卻秒數(shù)UI顯示剩余時(shí)間時(shí)再計(jì)算差值剩余冷卻 Max(0, 下一次可使用時(shí)間 - 當(dāng)前時(shí)間)這種方法的好處是無(wú)論中間發(fā)生暫停、減速還是調(diào)時(shí)間Scale冷卻計(jì)算都不會(huì)錯(cuò)。而且斷點(diǎn)調(diào)試時(shí)你只需要看時(shí)間戳變量不用猜“當(dāng)前倒計(jì)時(shí)是不是被多減了”。5.5 狀態(tài)效果的乘算與加算角色可能同時(shí)吃到多個(gè)減益效果。比如被減速30%同時(shí)被“時(shí)間緩速”影響50%。如果直接做加法合計(jì)減速80%效果會(huì)被過(guò)度放大如果用乘法最終速度倍率是0.7乘0.5等于0.35倍也就是減速65%。我統(tǒng)一采用乘算規(guī)則避免玩家疊加多個(gè)減速技能直接把角色定在原地。同類增益也建議乘法疊加比如兩個(gè)10%攻擊加成合起來(lái)不是20%而是21%。差異不大但能抑制無(wú)限堆屬性的數(shù)值膨脹。5.6 經(jīng)驗(yàn)值與升級(jí)循環(huán)計(jì)算升級(jí)曲線我用了一個(gè)二階多項(xiàng)式經(jīng)驗(yàn)需求隨等級(jí)平方增長(zhǎng)低等級(jí)升得快高等級(jí)變慢。公式為經(jīng)驗(yàn)需求 BaseExp (Level - 1) * GrowthFactor (Level - 1)^2 * CurveFactor升級(jí)邏輯寫在事件里用循環(huán)處理一次獲得大量經(jīng)驗(yàn)時(shí)連續(xù)升級(jí)的情況。設(shè)計(jì)時(shí)給最大等級(jí)加一個(gè)上限避免玩家經(jīng)驗(yàn)溢出后循環(huán)執(zhí)行太多轉(zhuǎn)身把游戲卡死。這點(diǎn)在測(cè)試階段輸入異常數(shù)值時(shí)必須考慮。6. 常見計(jì)算事故與排查排障手冊(cè)6.1 零向量歸一化導(dǎo)致的NaN歸一化一個(gè)長(zhǎng)度為零的向量會(huì)得到含有NaN的結(jié)果。2D游戲中最典型的場(chǎng)景是角色站在攻擊者正上方攻擊者和被攻擊者位置完全重合然后你取方向向量做一個(gè)GetSafeNormal。方向向量 目標(biāo)位置 - 自身位置 方向向量 方向向量.GetSafeNormal() // 可能變成 NaN解決辦法很簡(jiǎn)單歸一化之前檢查向量平方長(zhǎng)度是否大于一個(gè)極小值比如0.0001。如果長(zhǎng)度過(guò)小直接使用預(yù)設(shè)的默認(rèn)方向。排查時(shí)一旦看到傷害變成-1.#IND或者角色瞬移優(yōu)先懷疑位置方向計(jì)算里有NaN產(chǎn)生。6.2 除以零和防御力變負(fù)戰(zhàn)斗計(jì)算里盡量避免直接除變量非要除一定要先檢查被除數(shù)是否為0。防御力出現(xiàn)負(fù)數(shù)會(huì)讓傷害公式崩壞屬性配置時(shí)最好加一個(gè)Clamp防呆。我一般所有測(cè)試數(shù)值假人都有一個(gè)“數(shù)值異常檢查”的統(tǒng)一入口進(jìn)入戰(zhàn)斗前打印攻擊、防御、暴擊率三個(gè)值確認(rèn)范圍再開打。6.3 幀率依賴和TimeScale混用前面已經(jīng)講過(guò)DeltaTime的重要性。另外要提醒如果項(xiàng)目里有自定義TimeDilation注意你取到的DeltaTime已經(jīng)被全局時(shí)間膨脹影響了。如果某些計(jì)算需要真實(shí)世界時(shí)間要用App Time相關(guān)的節(jié)點(diǎn)否則暫停游戲后還在偷偷計(jì)算冷卻對(duì)單機(jī)游戲來(lái)說(shuō)體驗(yàn)很差。6.4 浮點(diǎn)數(shù)大小比較的坑藍(lán)圖里比較浮點(diǎn)數(shù)時(shí)用“等于”節(jié)點(diǎn)經(jīng)常不觸發(fā)因?yàn)楦↑c(diǎn)精度誤差可能讓0.10.2不等于0.3。血量百分比、進(jìn)度條這類判斷建議做區(qū)間判斷百分比小于0.01視為空大于0.99視為滿。不要寫嚴(yán)格的相等判斷除非你確實(shí)知道數(shù)值來(lái)源是整數(shù)類型。6.5 常用排查手段速查現(xiàn)象可能原因排查方式角色越跑越快位置疊加漏乘DeltaTime斷開速度累加用Print看單幀數(shù)值傷害為NaN或負(fù)值零向量歸一化、防御為負(fù)在傷害入口處Print后退出函數(shù)鏡頭出現(xiàn)黑邊邊界限制公式錯(cuò)誤在相機(jī)位置處Draw Debug Box冷卻條反復(fù)跳動(dòng)每幀重置了時(shí)間戳檢查時(shí)間戳是否在Tick里被誤更新攻擊命中不觸發(fā)判定盒偏移方向與朝向不一致用Draw Debug Box看檢測(cè)盒6.6 調(diào)試時(shí)優(yōu)先使用Draw Debug和Print String調(diào)試計(jì)算邏輯最忌諱猜。Draw Debug Box和Draw Debug Line可以在場(chǎng)景里直接看到判定范圍、位移路徑和鏡頭邊界。Print String配合Format Text能把你關(guān)心的變量值全部拼合輸出比如角色當(dāng)前速度、當(dāng)前朝向、冷卻剩余時(shí)間。我調(diào)攻擊判定時(shí)會(huì)在事件Graph里臨時(shí)掛各種Print String確認(rèn)每步數(shù)值再逐個(gè)移除。7. 做完計(jì)算篇后的幾點(diǎn)手感和擴(kuò)展方向這一系列寫到這里我最大的感受是2D游戲的計(jì)算并不是越復(fù)雜越好。坐標(biāo)換算、移動(dòng)插值、傷害公式每一塊都是獨(dú)立的小模塊把它們拆開封裝成一個(gè)一個(gè)函數(shù)以后調(diào)試和維護(hù)都輕松了很多。如果你現(xiàn)在也用藍(lán)圖寫2D游戲我強(qiáng)烈建議把常數(shù)值全部做成可在藍(lán)圖細(xì)節(jié)面板調(diào)整的參數(shù)不要寫死在節(jié)點(diǎn)上。調(diào)手感的時(shí)候你每改一次數(shù)字就切到游戲里跑一圈效率高很多。這套計(jì)算邏輯還可以擴(kuò)展的方向不少。比如AI預(yù)判移動(dòng)軌跡、彈道拋物線的命中點(diǎn)計(jì)算、組隊(duì)?wèi)?zhàn)斗里的仇恨值權(quán)重分配都是在現(xiàn)有坐標(biāo)系和數(shù)值公式上疊加的。下一期如果繼續(xù)寫我大概率會(huì)選彈道與追蹤計(jì)算作為主題因?yàn)檫@類邏輯接收的輸入更多計(jì)算鏈條也更長(zhǎng)驗(yàn)證起來(lái)特別有意思。在那之前先把這篇里的坐標(biāo)換算和傷害公式吃透后面會(huì)省力不少。