制拆解:地形生成、對象池與角色狀態(tài)機(jī)實(shí)戰(zhàn))
簡介這是一份適用于 Unity 引擎的無限跑酷項(xiàng)目源碼面向想快速上手休閑跑酷玩法、學(xué)習(xí)完整項(xiàng)目制作流程的開發(fā)者。項(xiàng)目包含角色控制、障礙生成、計(jì)分與復(fù)活等核心機(jī)制并配有演示視頻便于對照效果。整包共 2003 個(gè)文件大小 600.57MB主要文件包括 C# 腳本、Prefab 預(yù)制體、Shader 著色器、FBX 模型、材質(zhì)、圖片、音頻及 JSON/TXT 配置另有 553 個(gè) Markdown 筆記輔助理解模塊結(jié)構(gòu)8 個(gè)場景配合預(yù)制體已構(gòu)成可運(yùn)行框架。工程設(shè)置中已包含輸入、物理、質(zhì)量等常用參數(shù)避免從零搭建環(huán)境的成本。大量預(yù)制體與場景讓玩法擴(kuò)展更直觀Markdown 與配置資料幫助梳理邏輯降低了學(xué)習(xí)和定制的門檻。已有 468 人學(xué)習(xí)/下載適合 Unity 學(xué)習(xí)者、畢設(shè)學(xué)生和獨(dú)立開發(fā)者作為完整示例既能理解跑酷類游戲的設(shè)計(jì)流程也能作為原型快速迭代。1. 為什么一套 Unity 無限跑酷源碼值得拆開看如果只是下載一套 Unity 無限跑酷源碼打開工程按一下 Play看到角色跑起來就算完那你會(huì)漏掉這個(gè)項(xiàng)目里最有價(jià)值的部分自動(dòng)地形生成、對象池復(fù)用、角色狀態(tài)機(jī)和手機(jī)端性能策略。單看標(biāo)題它像是個(gè)小游戲 Demo但真正能用于商業(yè)立項(xiàng)的跑酷項(xiàng)目恰恰需要把這些機(jī)制吃到透。很多開發(fā)者第一次拿到這類源碼會(huì)遇到地形生成不連續(xù)、內(nèi)存持續(xù)漲、手機(jī)真機(jī)掉幀這類問題如果你知道這些坑在哪這套源碼就是你搭建跑酷類原型的骨架。適合剛接觸完整跑酷流程的新手也適合直接拿來做項(xiàng)目往上疊功能的客戶端開發(fā)。2. 核心機(jī)制拆解跑酷角色為什么能“自動(dòng)向前跑”提示這一章涉及源碼里最常見的三類腳本。如果你只想通過改參數(shù)調(diào)手感也得先明白哪個(gè)腳本控制移動(dòng)、哪個(gè)控制跳躍、哪個(gè)管生成。2.1 自動(dòng)跑動(dòng)的實(shí)現(xiàn)角色留在原地環(huán)境往后退很多跑酷項(xiàng)目的移動(dòng)核心并不是讓角色真的向前跑而是讓角色停留在局部原點(diǎn)附近把地面和障礙物整體向鏡頭方向平移或者給角色剛體一個(gè)恒定速度。兩種方案各有使用場景平移場景組件適合不需要物理碰撞的簡單 2D 跑酷而大多數(shù) 3D 無限跑酷源碼用的還是剛體恒定速度因?yàn)檫@樣可以直接復(fù)用重力、碰撞和跳躍的物理反饋。我習(xí)慣在 FixedUpdate 中強(qiáng)制寫入 z 方向速度而不是在 Update 里給 Transform 加位移。原因是跑酷中跳躍和落地都依賴物理引擎如果用 Transform 直接改 z很可能在碰撞回調(diào)里出現(xiàn)“卡進(jìn)”障礙物的情況。下面是這類源碼里最常見的自動(dòng)移動(dòng)核心using UnityEngine; public class AutoRunner : MonoBehaviour { public float runSpeed 8f; public Rigidbody rb; void FixedUpdate() { Vector3 vel rb.velocity; vel.z runSpeed; rb.velocity vel; } }代碼邏輯很簡單每一幀物理更新前把 z 方向速度強(qiáng)制設(shè)定為固定值其他方向保留物理引擎的結(jié)果。這樣角色會(huì)保持向前跑同時(shí) y 方向的速度由重力控制。參數(shù) runSpeed 是所有手感參數(shù)里第一個(gè)要調(diào)的項(xiàng)它決定障礙物出現(xiàn)在視野后的反應(yīng)時(shí)間。如果 runSpeed 設(shè)成 12而跳躍高度只有 2那很多跳不過去的障礙就會(huì)出現(xiàn)碰撞實(shí)際調(diào)參時(shí)跑速和跳躍力必須一起調(diào)。與直接移動(dòng)場景組件的方式相比剛體方案有個(gè)好處當(dāng)你想做加速帶或者被彈飛道具時(shí)只需要改變速度的插值而不需要處理整個(gè)場景的坐標(biāo)系偏移。常見做法是在場景里放一個(gè) GameManager把 AutoRunner 的速度暴露成屬性由 GameManager 統(tǒng)一管理加速節(jié)奏。2.2 角色狀態(tài)機(jī)跑步、跳躍、滑鏟如何切換跑酷源碼里第二個(gè)核心是角色狀態(tài)機(jī)。最簡單的實(shí)現(xiàn)是枚舉加每幀條件判斷稍復(fù)雜的會(huì)掛 Animator 狀態(tài)機(jī)。源碼里我看到的多半是枚舉加狀態(tài)鎖因?yàn)檫@種實(shí)現(xiàn)好調(diào)試、好擴(kuò)展。關(guān)鍵點(diǎn)在于跳躍的判定必須嚴(yán)格區(qū)分“空中”和“地面”否則容易出現(xiàn)二段跳、無限跳。下面的代碼是一個(gè)簡化版但能跑的狀態(tài)切換邏輯public enum PlayerState { Running, Jumping, Sliding } public class PlayerController : MonoBehaviour { public PlayerState state PlayerState.Running; [Header(GroundCheck)] public Transform groundCheck; public float checkRadius 0.2f; public LayerMask groundLayer; void Update() { if (state PlayerState.Jumping IsGrounded()) state PlayerState.Running; if (Input.GetKeyDown(KeyCode.Space) IsGrounded()) state PlayerState.Jumping; if (Input.GetKeyDown(KeyCode.S) state PlayerState.Running) state PlayerState.Sliding; } bool IsGrounded() { return Physics2D.OverlapCircle(groundCheck.position, checkRadius, groundLayer); } }這段代碼使用 OverlapCircle 做一個(gè)臨時(shí)碰撞判定。groundCheck 必須放在角色腳底偏下的位置而且最好單獨(dú)建一個(gè) Ground 層把地面和障礙物分開否則角色會(huì)把自己的碰撞體誤判成地面。參數(shù) checkRadius 影響手感半徑太小會(huì)導(dǎo)致落地后要過十幾毫秒才進(jìn)入 Running半徑太大會(huì)在地面邊緣提前觸發(fā)跳躍。在復(fù)雜跑酷項(xiàng)目里滑鏟不只是換一個(gè)動(dòng)畫而是要換碰撞體大小否則角色滑鏟時(shí)頭部還是會(huì)撞到上方障礙。源碼里如果保留了 crouch collider記得在狀態(tài)切換時(shí)同步禁用或啟用對應(yīng)子物體。2.3 無限地圖的生成策略預(yù)制體切片和對象池?zé)o限地圖不是真的無限而是用有限的預(yù)制體無限拼接。很多跑酷源碼會(huì)把一整段跑道路徑做成一個(gè) Segment每個(gè) Segment 中包含地面、障礙物、裝飾物、金幣。生成邏輯由 GameManager 根據(jù)玩家位置判斷是否需要在前面再放一個(gè) Segment同時(shí)把身后的 Segment 回收掉。這里最容易寫出的問題是每幀生成、每幀銷毀會(huì)導(dǎo)致實(shí)例化開銷和 GC 峰值。所以成熟源碼里一定會(huì)有對象池。下面是一個(gè)通用對象池腳本同樣適用于障礙物和地面段using System.Collections.Generic; using UnityEngine; public class ObjectPool : MonoBehaviour { public GameObject prefab; private QueueGameObject pool new QueueGameObject(); public GameObject Spawn(Vector3 position) { GameObject go pool.Count 0 ? pool.Dequeue() : Instantiate(prefab); go.transform.position position; go.SetActive(true); return go; } public void Recycle(GameObject go) { go.SetActive(false); pool.Enqueue(go); } }Spawn 時(shí)優(yōu)先從隊(duì)列拿隱藏對象拿不到才 InstantiateRecycle 時(shí)只是關(guān)閉并放入隊(duì)列沒有銷毀。這樣做能夠避免反復(fù)創(chuàng)建對象造成的卡頓。使用時(shí)要注意如果池里對象帶有 Rigidbody回收時(shí)要把速度清零否則剛體會(huì)帶著速度繼續(xù)參與物理運(yùn)算內(nèi)存和 CPU 都會(huì)有額外開銷。生成位置的判斷一般是當(dāng)最后一段的結(jié)束 z 坐標(biāo)小于攝像機(jī)前方預(yù)期生成距離時(shí)從池里取一段掛到那個(gè)坐標(biāo)當(dāng)最早一段的結(jié)束 z 坐標(biāo)跑到攝像機(jī)后方若干個(gè)單位時(shí)回收它。這個(gè)生成距離不宜太近否則玩家會(huì)看到障礙憑空出現(xiàn)也不宜太遠(yuǎn)否則同一屏內(nèi)對象太多Draw Call 會(huì)更高。常見做法是讓它略大于攝像機(jī)遠(yuǎn)裁剪面或者控制在玩家視野內(nèi)約 2 個(gè)屏寬。3. 把源碼跑通環(huán)境、場景、參數(shù)三者對齊3.1 導(dǎo)入前的環(huán)境對齊輸入系統(tǒng)、渲染管線、重力設(shè)置下載源碼后直接打開最容易遇到的是 Input System 沖突?,F(xiàn)在新建 Unity 工程可能默認(rèn)啟用新的 Input System而跑酷源碼如果還在用老的 Input Manager打開后會(huì)出現(xiàn)雙擊報(bào)錯(cuò)、按鈕無響應(yīng)甚至 Project Settings 里面找不到對應(yīng)輸入條目。操作上我一般先做一次環(huán)境統(tǒng)一Edit Project Settings Player Active Input Handling選中 Both。這樣新舊輸入 API 都能用不用著急重構(gòu)源碼。如果源碼明確說只支持舊輸入就把 Active Input Handling 改成 Input Manager然后在 Player Controller 里檢查 Input.GetKeyDown 是否有反應(yīng)。還要檢查渲染管線。很多存量跑酷源碼用的還是內(nèi)置渲染管線材質(zhì)是標(biāo)準(zhǔn) Shader。如果你把工程升到 URP所有材質(zhì)會(huì)變成紫色。遇到這種情況要么把項(xiàng)目遷回內(nèi)置管線要么給材質(zhì)重新選 URP 對應(yīng) Shader。做這一步要看清項(xiàng)目依賴不是所有貼圖都兼容。最后是 Gravity 和物理步長。跑酷項(xiàng)目對重力依賴很強(qiáng)Project Settings Physics 里默認(rèn)的 Gravity 參數(shù)不要亂改。常見做法是保持默認(rèn)重力在角色自身的 Rigidbody 上調(diào)整 Gravity Scale這樣場景里其他物理特效不會(huì)跟著受影響。3.2 場景節(jié)點(diǎn)檢查清單Player、Camera、GameManager、PoolManager跑通前先用場景視圖檢查四個(gè)關(guān)鍵節(jié)點(diǎn)。Player 節(jié)點(diǎn)必須有 Rigidbody 和碰撞體并且固定旋轉(zhuǎn)、固定 z 軸位移。Camera 節(jié)點(diǎn)通常掛在 Player 的某個(gè)子節(jié)點(diǎn)下或者由腳本跟隨 Player如果是腳本跟隨注意有沒有設(shè)定跟隨偏移量。GameManager 節(jié)點(diǎn)上掛著游戲狀態(tài)控制、距離計(jì)數(shù)、失敗判定。PoolManager 節(jié)點(diǎn)下掛著多個(gè) ObjectPool 子物體每一個(gè)對應(yīng)一種預(yù)制體。下面這個(gè)表格是我常用的排查清單照著看能少翻車節(jié)點(diǎn)必須檢查的設(shè)置容易踩的坑PlayerRigidbody 非 Kinematic碰撞體大小適中碰撞體過大導(dǎo)致“空氣墻”Camera跟隨腳本的偏移量正確z 軸抖動(dòng)GameManager生成距離、回收距離一致生成與回收重疊PoolManager池內(nèi) prefab 引用正確多個(gè) prefab 共用一個(gè)池場景里的碰撞層級也要單獨(dú)檢查。跑酷項(xiàng)目通常讓 Player 只跟 Ground 和 Obstacle 碰撞金幣和觸發(fā)區(qū)域用 Trigger不參與物理阻擋。如果項(xiàng)目沒有單獨(dú)建 Ground 層而是讓 Player 跟所有 Trigger 都發(fā)生碰撞就有概率出現(xiàn)金幣卡住角色、跳躍判定報(bào)錯(cuò)這些奇怪現(xiàn)象。3.3 手感參數(shù)默認(rèn)值往往不是你的目標(biāo)跑酷游戲的手感基本就是跑速、跳躍力、重力和判定半徑的組合。一般以默認(rèn)參數(shù)為基礎(chǔ)按“先跑通、再調(diào)整”的順序。先確認(rèn)跑速合適再讓跳躍能穩(wěn)定跳過標(biāo)準(zhǔn)障礙最后調(diào)滑鏟時(shí)長。下面這些參數(shù)最值得從源碼的 Inspector 面板里找出來調(diào)參數(shù)作用調(diào)參建議runSpeed角色前移速度5-10 比較適合新手高于 12 會(huì)很難反應(yīng)jumpForce跳躍初始速度與重力配合跳到最高點(diǎn)要能越過最高障礙jumpGravityScale跳躍時(shí)的額外重力調(diào)大落地更干脆調(diào)小能“飄一點(diǎn)”checkRadius地面檢測半徑0.1-0.3過大會(huì)讓玩家感覺還沒跳完就落地laneSwitchTime換道插值時(shí)間0.1-0.3過短僵硬過長手感太綿跳躍高度的計(jì)算關(guān)系是 h v2 / (2g)。如果你的 jumpForce 是 8重力是 9.81理論最高高度約為 3.26 米。這時(shí)如果最高障礙是 3 米看著能跳過去但連續(xù)跳躍時(shí)會(huì)因?yàn)槠鹛查g的位置誤差卡到邊緣。所以我一般會(huì)把 jumpForce 值調(diào)得偏高一點(diǎn)再把跳躍后的那幾幀輸入屏蔽掉讓手感更寬容。調(diào)參方法上不要用 Game 視圖邊跑邊改效率太低。建議把 runSpeed、jumpForce、GravityScale 這幾個(gè)參數(shù)在 Player 腳本里序列化然后把 Inspector 窗口放到旁邊Play 時(shí)直接改值觀察角色動(dòng)作和 Debug.Log 輸出。遇到邊界值先暫停再調(diào)整減少變量干擾。4. 避坑與常見問題排查五條血淚經(jīng)驗(yàn)4.1 碰撞判定不穩(wěn)角色偶爾被“空氣墻”卡住現(xiàn)象角色在地面上正常跑某一小段路面突然彈起或者跳躍高度明顯變矮像是撞到看不見的物體。原因最常見的是地面段與地面段之間存在重疊的碰撞體或者碰撞體比地面模型大了一點(diǎn)造成微小的凸起。另一個(gè)原因是地面材質(zhì)使用了帶碰撞體的裝飾物開發(fā)者以為是裝飾實(shí)際上它參與了物理碰撞。解決檢查每段地面的 Box Collider 尺寸把 Y 軸厚度壓到貼近模型表面Z 軸方向讓前后兩段之間保留 0.01 的間隙不要重疊。如果依然有凸起在場景視圖里把 Player 碰撞體透明顯示逐幀看哪段路徑有問題。注意修復(fù)后要重新運(yùn)行一遍完整路徑不要只測試起點(diǎn)附近。4.2 對象池回收不及時(shí)導(dǎo)致內(nèi)存持續(xù)上漲現(xiàn)象跑酷運(yùn)行十幾分鐘后幀率開始波動(dòng)內(nèi)存占用持續(xù)上升Profiler 中 GC Alloc 一幀比一幀高。原因很多源碼只在障礙物離開攝像機(jī)視野時(shí)回收卻沒有在整段地面后移時(shí)統(tǒng)一回收同段內(nèi)所有子物體。當(dāng) Segment 回收后其子物體內(nèi)的金幣和裝飾物沒有被回池而是繼續(xù)被 Instantiate 重復(fù)創(chuàng)建等價(jià)于每跑一段就漏一行內(nèi)存。解決把地面段、障礙物、金幣分別放進(jìn)對應(yīng)的 Pool并且讓 Segment 在回收時(shí)主動(dòng)調(diào)用所有子物體的 Recycle。為了更穩(wěn)定我一般會(huì)再寫一個(gè)監(jiān)控如果池內(nèi)對象數(shù)量長期不下降就在 GameManager 里輸出一個(gè)警告。注意回收時(shí)要重置對象的旋轉(zhuǎn)和縮放否則下一個(gè) Spawn 出來的對象可能角度不對。4.3 電腦流暢但手機(jī)上一跑步就掉幀現(xiàn)象編輯器里跑 60 幀很穩(wěn)定打包到安卓或 iOS 之后碰到障礙生成瞬間就會(huì)掉到 30 幀以下發(fā)熱明顯。原因手機(jī)上主要瓶頸在 Draw Call 和物理開銷。編輯器開發(fā)時(shí)往往開著 Profiler但實(shí)際設(shè)備上沒有觀察跑酷場景里大量金幣和裝飾物都是獨(dú)立 Mesh每顆金幣一個(gè) Draw Call一個(gè)屏放幾十個(gè)就會(huì)把移動(dòng)端 GPU 打爆。解決先把所有同種小物件合并進(jìn)同一個(gè)圖集或者在每個(gè) Segment 內(nèi)設(shè)置 LOD遠(yuǎn)處小物件直接隱藏。再看相機(jī)裁剪范圍跑酷游戲可以把 Camera Far Plane 從 500 改到 100減少很多不可見物體。最后檢查是否有空 Update 循環(huán)比如每個(gè)金幣都在做旋轉(zhuǎn)動(dòng)畫會(huì)導(dǎo)致大量 Transform 更新把旋轉(zhuǎn)邏輯搬到 Shader 里或統(tǒng)一合批可以明顯改善。4.4 二段跳變成無限跳現(xiàn)象角色明明在空中連按跳躍鍵卻可以連續(xù)觸發(fā)很多次二段跳功效變成了無限跳。原因狀態(tài)機(jī)沒有保存“已連續(xù)跳躍次數(shù)”只是用 IsGrounded 判斷是否允許起跳。當(dāng)角色在地面邊緣下落時(shí)GroundCheck 的檢測圓因?yàn)榘霃皆O(shè)置過大在空中依然能觸碰到地面邊緣導(dǎo)致狀態(tài)機(jī)認(rèn)為還在“地面”。解決在 PlayerController 里增加 jumpCount 字段。每次從 Running 進(jìn)入 Jumping 時(shí) jumpCount 1在空中再次起跳時(shí) jumpCount 2落地后歸零。這樣無論 GroundCheck 有沒有誤報(bào)都不會(huì)超過二段跳。還有另一個(gè)容易忽略的點(diǎn)GroundCheck 的 transform 盡量放在角色腳底中心不要放在腳尖否則斜坡邊緣誤判概率會(huì)很高。4.5 碰到障礙不觸發(fā)失敗流程現(xiàn)象角色碰撞體已經(jīng)碰到障礙但 GameManager 沒有播放失敗動(dòng)畫也沒有停止跑動(dòng)角色穿過障礙物繼續(xù)跑。原因障礙物碰撞體沒有勾選 Is TriggerPlayer 又是 Kinematic 剛體OnTriggerEnter 不會(huì)被觸發(fā)?;蛘唔?xiàng)目里只寫了 OnCollisionEnter但障礙物沒有碰撞體組件。解決統(tǒng)一約定玩家撞到障礙用 OnCollisionEnter金幣用 OnTriggerEnter。給障礙物加 Box Collider 且不勾 Is Trigger給金幣加 Box Collider 且勾選 Is Trigger。然后在 GameManager 里分別監(jiān)聽 OnCollisionEnter 和 OnTriggerEnter。如果遇到個(gè)別障礙物“穿透”重點(diǎn)檢查這個(gè)障礙物的 Collider 有沒有被禁用、Rigidbody 是不是非 Kinematic。跑酷場景里的障礙物通常不需要物理模擬給它們掛上 Is Trigger 會(huì)省很多物理開銷。5. 從源碼到自己的玩法三處最值得改造的地方5.1 地圖生成器改造加入難度曲線和分段節(jié)奏默認(rèn)源碼里的障礙生成往往是固定間隔跑一會(huì)兒就覺得無聊。要變成能商用的小游戲得讓障礙密度和類型隨距離變化。最簡單的方式是做一個(gè)難度等級表按已跑距離切換生成參數(shù)。[System.Serializable] public class DifficultyLevel { public float startDistance; public float obstacleInterval; public float flyingObstacleRate; } public class DifficultyManager : MonoBehaviour { public DifficultyLevel[] levels; public DifficultyLevel GetLevel(float distance) { DifficultyLevel result levels[0]; foreach (DifficultyLevel level in levels) { if (distance level.startDistance) result level; } return result; } }代碼里 levels 是有序數(shù)組startDistance 從小到大排列。GetLevel 用線性掃一遍就行距離超過某個(gè)節(jié)點(diǎn)就返回對應(yīng)等級。實(shí)際接入時(shí)GameManager 在生成下一個(gè) Segment 前先通過該接口拿到當(dāng)前等級然后決定障礙間隔和飛行障礙概率。難度曲線不要做成線性單調(diào)遞增最好每 500 米升一級中間留 200 米的喘息距離不然玩家壓力太大容易流失。5.2 角色控制擴(kuò)展左右三車道換道與蓄力跳很多 3D 跑酷是上下跳躍和滑鏟但如果要做跑酷比賽玩法需要加上左右換道。換道不能用剛體直接改位置否則會(huì)穿模我一般用協(xié)程插值移動(dòng)保證每一幀位置都是平滑的。public IEnumerator SwitchLane(float targetX, float duration) { Vector3 startPos transform.position; float elapsed 0f; while (elapsed duration) { elapsed Time.deltaTime; float t Mathf.Clamp01(elapsed / duration); Vector3 newPos Vector3.Lerp(startPos, new Vector3(targetX, startPos.y, startPos.z), t); transform.position newPos; yield return null; } }這里 switchLane 的高層邏輯是在外部判斷左右方向計(jì)算目標(biāo) X 坐標(biāo)直接調(diào)用協(xié)程。duration 默認(rèn) 0.15 秒手感比較合適如果跑速很快可以縮到 0.1。換道過程中建議屏蔽跳躍輸入因?yàn)橥瑫r(shí)換道和跳躍會(huì)讓碰撞判定變得復(fù)雜。另一個(gè)常用擴(kuò)展是“蓄力跳”長按跳躍會(huì)增加跳躍高度松開時(shí)按當(dāng)前蓄力值給剛體一個(gè)速度。實(shí)現(xiàn)上不需要改狀態(tài)機(jī)只在 Update 里記錄按鍵時(shí)間然后在 Jump 函數(shù)里把額外速度疊加到 jumpForce 上。5.3 存檔系統(tǒng)從 PlayerPrefs 到結(jié)構(gòu)化 JSON源碼里常見的是用 PlayerPrefs 存儲(chǔ)跑到的最遠(yuǎn)距離和金幣數(shù)。這樣做簡單但跨版本更新時(shí)很容易丟字段特別是添加新道具后舊存檔沒有對應(yīng)字段會(huì)導(dǎo)致讀取異常。我習(xí)慣在源碼里直接改成 JSON 序列化即使未來加字段也好兼容。[System.Serializable] public class SaveData { public int lastDistance; public int coins; public string version; } public class SaveManager { public static void Save(SaveData data) { string json JsonUtility.ToJson(data); PlayerPrefs.SetString(RunSave, json); PlayerPrefs.Save(); } public static SaveData Load() { string json PlayerPrefs.GetString(RunSave); if (string.IsNullOrEmpty(json)) return new SaveData(); return JsonUtility.FromJsonSaveData(json); } }這個(gè)方案的坑在于從舊存檔讀取時(shí)沒有 version 字段會(huì)直接變成默認(rèn)值。更穩(wěn)妥的做法是 Load 后先檢測 version如果是舊的就為缺失字段補(bǔ)默認(rèn)值。不要直接信任 PlayerPrefs 的本地?cái)?shù)據(jù)玩家清理緩存是常態(tài)。如果是移動(dòng)端上線版本存檔里盡量別放敏感數(shù)值只放可恢復(fù)的進(jìn)度本地存檔要做到“刪掉也能重新開始”服務(wù)器同步是后面再考慮的事。5.4 小地圖和暫停菜單的聯(lián)動(dòng)跑酷源碼的另一處值得改的是暫停和恢復(fù)。很多簡單 Demo 一暫停就直接 Time.timeScale 0但這樣做會(huì)導(dǎo)致對象池回收邏輯和協(xié)程被打斷。重回游戲時(shí)角色仍在跑但所有池對象的回收判斷停了一拍后續(xù)可能一連串報(bào)錯(cuò)。最好做一個(gè) GamePause 管理器暫停時(shí)只停止 PlayerController 和 GameManager 的更新不把整個(gè)時(shí)間軸停掉或者是顯式處理協(xié)程。一個(gè)小技巧是暫停時(shí)把所有在跑的對象池身上的 Rigidbody.isKinematic 臨時(shí)設(shè)成 true暫停結(jié)束再恢復(fù)能避免很多意外的物理穿透。6. 手感調(diào)優(yōu)與性能驗(yàn)證最后要做的三個(gè)實(shí)操作源碼本身能跑通之后更重要的是確認(rèn)它在你目標(biāo)設(shè)備上的表現(xiàn)。第一步是用 Unity Profiler 的 CPU Usage 模塊跑 3 分鐘重點(diǎn)看 FixedUpdate 里的物理耗時(shí)和 GC Alloc。如果發(fā)現(xiàn) GC Alloc 一直在漲立刻檢查對象池回收路徑不要等優(yōu)化完再查。第二步是給場景里所有動(dòng)態(tài)生成物體關(guān)閉 shadow casting或者限制陰影距離跑酷場景里陰影是最容易被忽視的性能刺客。第三步是做一個(gè)“輸入回放”測試把玩家在一段路徑里的輸入序列記錄下來再回放同樣的輸入確認(rèn)每次起跳和落地結(jié)果一致。跑酷源碼里最怕的是隨機(jī)地形生成讓手感不可復(fù)現(xiàn)使用固定隨機(jī)種子在測試階段尤其重要上線前再換成隨機(jī)種子即可。我在調(diào)這類項(xiàng)目時(shí)發(fā)現(xiàn)很多開發(fā)者會(huì)把大量時(shí)間花在改美術(shù)和 UI 上忽略了 FixedUpdate 里不斷實(shí)例化對象這種基礎(chǔ)問題。其實(shí)跑酷手感好不好第一看跑速和跳躍的響應(yīng)窗口第二看碰撞體是否干凈第三就是性能余量。從那以后我每次拿到跑酷源碼都會(huì)先強(qiáng)制自己打開 Profiler把物理開銷和 GC 漲勢看一遍確認(rèn)沒有坑再開始改玩法邏輯。希望幫到你。本文還有配套的精品資源點(diǎn)擊獲取