
做Unity 2D新手項目Ruby‘s Adventure是繞不開的一課。這是Unity官方放出來的免費2D游戲案例大家習(xí)慣叫它“Ruby的冒險”而“主角和第一腳本”這一段正好是整個項目里從“擺場景”轉(zhuǎn)向“寫邏輯”的關(guān)鍵節(jié)點。你會發(fā)現(xiàn)主角落地之后不再是一張靜態(tài)圖片通過Rigidbody2D、Animator和一份C#腳本它第一次能被鍵盤控制、隨方向切換動畫。這一節(jié)解決的不只是“讓角色動起來”更重要的是讓你理解Unity推薦的組件化協(xié)作方式GameObject只是殼動畫、物理、輸入處理各司其職腳本負(fù)責(zé)把能力串起來。適合誰剛學(xué)完Unity界面操作、準(zhǔn)備進入2D游戲開發(fā)的新手以及想要快速搭一個2D橫版或俯視角色控制器的同學(xué)。下面我按自己實際做這個項目的順序把這節(jié)課拆開講透。1. 項目整體拆解為什么Unity官方教程要讓你先做“主角和第一腳本”1.1 Ruby’s Adventure在2D學(xué)習(xí)路徑中的位置Unity官方學(xué)習(xí)路徑里Ruby‘s Adventure是給“已經(jīng)會基礎(chǔ)操作、但還沒有寫過完整游戲邏輯”的開發(fā)者準(zhǔn)備的。前面你可能已經(jīng)學(xué)會了創(chuàng)建場景、擺放Tilemap、給地面加碰撞器但這些操作都還停留在編輯器層面。到了“主角和第一腳本”這一節(jié)才第一次真正進入代碼驅(qū)動的游戲循環(huán)玩家按方向鍵角色移動動畫切換攝像機跟隨。這個位置安排得很講究。移動控制是所有動作游戲的地基后面你想加敵人巡邏、拾取物品、觸發(fā)機關(guān)、播放對話都離不開“主角能走動”這個大前提。而且Ruby項目刻意把復(fù)雜度控制得很好沒有跳躍、沒有重力、沒有敵人AI主角就是一個在俯視角場景里漫游的小女孩。這種設(shè)計讓新手能把全部注意力放在“腳本是怎么和組件協(xié)作”上而不是被一堆物理參數(shù)淹沒。1.2 主角對象背后的組件化設(shè)計思路很多人第一次接觸Unity會犯一個思維錯誤以為“主角”是某種特殊物體寫代碼時想在一個腳本里把移動、動畫、碰撞全部搞定。實際上Unity的核心思想是組件化。一個名為Ruby的GameObject本質(zhì)上只是一個容器它身上掛了SpriteRenderer負(fù)責(zé)顯示Animator負(fù)責(zé)播放動畫Rigidbody2D負(fù)責(zé)物理模擬BoxCollider2D負(fù)責(zé)碰撞檢測最后再加一個你自己寫的腳本負(fù)責(zé)接收輸入并指揮這些組件工作。這樣的好處是職責(zé)清晰。顯示出問題查SpriteRenderer和動畫面板物理出問題查剛體和碰撞體輸入沒反應(yīng)查腳本和Input設(shè)置。你不用在一個幾百行的腳本里翻找問題根源。后面寫敵人、NPC、道具時你會發(fā)現(xiàn)幾乎所有對象都是同一套模式先組裝組件再寫一個控制腳本。Ruby這個項目就是一次最標(biāo)準(zhǔn)的組件化示范。2. 動手前準(zhǔn)備Unity版本、2D項目模板和官方素材導(dǎo)入2.1 選對Unity版本和項目模板做Ruby‘s AdventureUnity版本建議選LTS版本目前主流是2021 LTS或2022 LTS別追最新版。不是說新版不能用而是這套官方教程用舊版Input Manager寫新版本默認(rèn)可能開啟新輸入系統(tǒng)會讓新手平白多踩一個坑。我自己帶人做這個項目時最常見的卡點是打開項目后腳本報錯一查往往是輸入系統(tǒng)不兼容。創(chuàng)建項目時在Unity Hub里選“2DBuilt-In Render Pipeline”模板即可。如果編輯器里看到Universal 2D模板也能用但對這個項目來說內(nèi)置渲染管線更省事。重點看一個設(shè)置Edit Project Settings Player Other Settings Active Input Handling。如果你用的是Input.GetAxisRaw這種老接口這里需要是Input Manager (Old)或者Both。如果當(dāng)前是Input System Package (New)舊代碼會直接報“InvalidOperationException: You are trying to read Input using the Input class, but you have switched active Input handling to Input System package”解決辦法就是把這個選項改成Both或Old改完重啟編輯器。2.2 導(dǎo)入官方素材與認(rèn)識項目結(jié)構(gòu)素材獲取方式很簡單打開Unity Learn頁面找到Ruby‘s Adventure課程按引導(dǎo)把資源包導(dǎo)入當(dāng)前項目或者在Asset Store里搜索官方2D Beginner教程資源。導(dǎo)入后Project窗口會多出一個文件夾里面通常包含Art、Prefabs、Scenes、Scripts等子目錄。官方示例場景可以直接打開跑起來看看最終效果角色可以在一個2D場景里自由走動播放跑步動畫切換方向。這個素材包對新手特別友好因為它把美術(shù)資源、動畫剪輯、預(yù)制體都準(zhǔn)備好了你不需要自己畫圖也不用手K動畫。我建議拿到素材后先花5分鐘把Prefabs目錄里的Player預(yù)制體拖到場景里看一下重點觀察它的Inspector面板有哪些組件、Animator Controller里掛了哪些狀態(tài)。這種“先看成品再動手拆”的學(xué)習(xí)方式比一上來就自己從零組裝高效得多。2.3 場景與相機初始設(shè)置如果你的場景是空場景拖入Ruby角色后還要調(diào)整相機。2D項目里相機默認(rèn)是Orthographic投影模式意味著沒有透視變形適合做2D游戲。相機的Size參數(shù)決定屏幕上能看到多少世界單位數(shù)值越小畫面拉得越近。Ruby素材的美術(shù)風(fēng)格是小型像素角色相機Size一般設(shè)置在5到8之間具體要看你的Tilemap尺寸。場景里鋪好地面后把相機拖到能看到角色全景的位置即可。另外建議在項目里添加一個“Player”的Sorting Layer。選中Ruby角色在SpriteRenderer組件里可以把Sorting Layer設(shè)為Player并設(shè)一個合適的Order in Layer值。Sorting Layer從上到下決定渲染順序Order in Layer在同層內(nèi)進一步排序。比如地面是Default層角色放在Player層這樣角色永遠(yuǎn)蓋在地面上不會被地面的圖塊擋掉。這是2D開發(fā)里非常基礎(chǔ)但特別重要的層級概念Ruby項目里你就能直觀體會到。3. 創(chuàng)建主角精靈、動畫狀態(tài)機與物理組件的組合3.1 場景中生成玩家角色有兩種方式在場景里創(chuàng)建主角直接拖官方素材包里的Player預(yù)制體或者手動把Ruby的Sprite拖進場景再補組件。新手我建議直接拖預(yù)制體因為玩起來不會出錯。但這里的學(xué)習(xí)重點不是“能跑”而是搞清楚預(yù)制體上每個組件的作用所以哪怕你用預(yù)制體也要逐個打開看看。SpriteRenderer負(fù)責(zé)顯示角色貼圖默認(rèn)會關(guān)聯(lián)一張Ruby的正面立繪。如果你發(fā)現(xiàn)角色顯示異常先檢查SpriteRenderer的Sprite字段有沒有正確賦值再檢查Sprite導(dǎo)入設(shè)置里的Filter Mode和PPUPixels Per Unit。PPU決定這個精靈在世界坐標(biāo)中的大小比如素材是16x16像素、PPU設(shè)為32那這個角色在世界里就是0.5個單位寬。如果角色在畫面里顯得過大或過小先調(diào)PPU和相機Size不要直接暴力改Transform的Scale否則后續(xù)碰撞器不好對齊。3.2 動畫狀態(tài)機與參數(shù)設(shè)計接下來是Animator。主角平時站著不動要播待機動畫移動時要播跑步動畫而且上下左右四個方向最好都有對應(yīng)動作。這套邏輯用Animator Controller實現(xiàn)。打開Animator窗口你會看到狀態(tài)機面板。官方素材包里往往自帶一個Controller里面可能已經(jīng)有一些狀態(tài)和過渡你要做的是確認(rèn)“控制切換的參數(shù)”和“腳本里寫入的參數(shù)”名字一致。如果你是自己從頭創(chuàng)建推薦用2D Blend Tree而不是一堆Transition。Blend Tree適合處理方向運動把Idle、Up、Down、Left、Right這些動畫剪輯混合到一個連續(xù)的狀態(tài)里。具體操作在Animator窗口右鍵 Create State From New Blend Tree雙擊進入Blend Tree然后在Inspector里把Blend Type設(shè)為2D Simple DirectionalParameters選兩個float類型參數(shù)比如moveX和moveY。接著把四個方向的跑步動畫拖進Motion列表分別設(shè)置它們對應(yīng)的坐標(biāo)比如向左是-1,0向右是1,0向上是0,1向下是0,-1。腳本里只要持續(xù)寫入moveX、moveY狀態(tài)機就會自動切換對應(yīng)動畫。參數(shù)類型不要用錯。在Animator的Parameters面板里moveX、moveY要設(shè)為Float后面腳本用anim.SetFloat寫入。如果官方自帶的Controller里參數(shù)名和你腳本不一致比如它叫Speed你在腳本里就寫“Speed”別強行改成moveX。新手最常見的動畫不切換問題十有八九是參數(shù)名對不上。3.3 物理體與碰撞體的實際配置值給角色添加Rigidbody2D時注意幾個關(guān)鍵屬性。Body Type設(shè)為Dynamic這是動態(tài)剛體會受到物理模擬影響Gravity Scale設(shè)為0因為這個2D游戲是俯視角角色不需要重力往下掉。這一步非常重要如果你忘記設(shè)0角色一運行就會往下掉。Constraints里建議勾選Freeze Rotation Z否則角色撞到障礙物時可能會旋轉(zhuǎn)看起來像倒地了一樣。BoxCollider2D要手動調(diào)整大小。直接點Edit Collider按鈕在Scene視圖里把綠色線框拖到和角色身體差不多大小即可注意不要覆蓋住整個Sprite的透明區(qū)域否則會出現(xiàn)“明明沒碰到墻壁卻卡住”的錯覺。不同版本的免費素材里Ruby角色形態(tài)略有差異但原則是碰撞器貼合身體輪廓稍微縮一點都比完全覆蓋好。Ruby項目里撿物品、觸發(fā)對話都是靠碰撞器觸發(fā)所以這一步別馬虎。4. PlayerController腳本移動邏輯的完整實現(xiàn)與逐行解讀4.1 腳本文件命名與MonoBehaviour生命周期在Project窗口右鍵 Create C# Script把腳本命名為PlayerController。注意文件名必須和類名一致這是C#的硬性要求改掉類名卻沒改文件名會導(dǎo)致“The associated script can not be loaded”的報錯。腳本創(chuàng)建后直接拖到Ruby角色上或者用Add Component搜索腳本名掛載。Unity腳本的生命周期方法最開始只需要關(guān)注三個Awake在對象創(chuàng)建時調(diào)用適合獲取組件引用Start在第一幀Update之前調(diào)用適合做初始化Update每幀調(diào)用適合處理輸入和邏輯。還有一個FixedUpdate是固定時間間隔調(diào)用的適合處理物理操作。Ruby項目里移動代碼放Update還是FixedUpdate其實都能跑但為了規(guī)范我下面會給出兩種寫法之間的取舍說明。4.2 基于Input Manager的移動代碼逐行解析下面這份代碼是Ruby項目最經(jīng)典的PlayerController我一行行講using System.Collections; using System.Collections.Generic; using UnityEngine; public class PlayerController : MonoBehaviour { public float speed 5f; private Rigidbody2D rb; private Animator anim; void Start() { rb GetComponentRigidbody2D(); anim GetComponentAnimator(); } void Update() { float moveX Input.GetAxisRaw(Horizontal); float moveY Input.GetAxisRaw(Vertical); Vector2 movement new Vector2(moveX, moveY).normalized * speed; rb.velocity movement; anim.SetFloat(moveX, moveX); anim.SetFloat(moveY, moveY); anim.SetFloat(speed, Mathf.Max(Mathf.Abs(moveX), Mathf.Abs(moveY))); if (moveX 0) { transform.localScale new Vector3(1, 1, 1); } else if (moveX 0) { transform.localScale new Vector3(-1, 1, 1); } } }先看輸入部分。Input.GetAxisRaw(“Horizontal”)會返回-1、0或1分別代表左、不按、右Vertical對應(yīng)上下。注意GetAxisRaw和GetAxis的區(qū)別GetAxis帶平滑濾波松開按鍵后數(shù)值會逐漸回到0角色會有“滑行”手感GetAxisRaw是即時值響應(yīng)更直接適合像素風(fēng)2D游戲。Ruby這個項目里我推薦GetAxisRaw控制更跟手。然后看向量處理。new Vector2(moveX, moveY)直接乘speed斜向移動時會出問題同時按上和右向量長度是√2約1.414角色斜著走會比直線快。加一個.normalized就能把向量長度保持為1這樣四條線方向速度一致。這個細(xì)節(jié)很多人會漏但對游戲手感影響很大。再解釋為什么用rb.velocity而不是transform.Translate。Rigidbody2D是物理組件它的運動應(yīng)該交給物理系統(tǒng)管理。如果用transform.Translate直接改位置會繞過物理模擬可能導(dǎo)致碰撞檢測不準(zhǔn)確甚至穿過墻壁。雖然很多老教程喜歡用transform.Translate但Ruby項目這種2D俯視角游戲用Rigidbody2D.velocity更規(guī)范。由于velocity是按物理幀更新的這里不需要乘Time.deltaTime直接賦速度值即可。動畫部分用anim.SetFloat把moveX、moveY和speed寫進狀態(tài)機。如果Blend Tree只靠moveX、moveY驅(qū)動那speed可以不用但如果你用狀態(tài)機里的Transitionspeed可能作為“是否移動”的開關(guān)。Mathf.Max(Mathf.Abs(moveX), Mathf.Abs(moveY))的意思是取兩個方向絕對值較大的那個這樣斜走時speed也能正常為1。最后是角色翻轉(zhuǎn)。Ruby素材的Sprite默認(rèn)朝左或朝右通過翻轉(zhuǎn)Transform的localScale.x可以讓角色左右轉(zhuǎn)向。這套方法簡單直接但如果素材里已經(jīng)做了四方向動畫你也可以不用localScale而是靠Blend Tree切換左右方向動畫。兩種方案選一種即可不要混用否則會出現(xiàn)角色在某個方向變扁的怪異效果。4.3 基于新Input System的可選方案如果你的Unity項目激活的是新輸入系統(tǒng)上面代碼會報錯。解決方案有兩個一個是前面說過把Active Input Handling改成Both或Old另一個是用新Input System重寫移動邏輯。這里給一個簡化版參考using UnityEngine; using UnityEngine.InputSystem; public class PlayerController : MonoBehaviour { public float speed 5f; private Rigidbody2D rb; private Vector2 moveInput; public void OnMove(InputAction.CallbackContext context) { moveInput context.ReadValueVector2(); } void FixedUpdate() { Vector2 movement moveInput.normalized * speed; rb.velocity movement; } }使用新輸入系統(tǒng)時你要給角色添加PlayerInput組件配置InputAction資產(chǎn)并確保Action名為Move的“Performed”“Canceled”事件指向PlayerController的OnMove方法。這個方案代碼更簡潔但配置過程對新手來說反而多了一層理解成本。我個人的建議是第一次做Ruby項目直接用舊Input Manager跑通邏輯理解“輸入→邏輯→物理→動畫”這條鏈路之后再回來學(xué)新輸入系統(tǒng)。否則你會在輸入系統(tǒng)的配置環(huán)節(jié)陷進去忘了主角為什么要動。4.4 攝像機跟隨的實現(xiàn)與LateUpdate角色動起來之后如果不做攝像機跟隨角色跑兩步就出屏幕了。Ruby項目里攝像機跟隨用的是平滑跟隨。方法很簡單新建腳本CameraFollow掛到主相機上把目標(biāo)設(shè)為Ruby角色。using UnityEngine; public class CameraFollow : MonoBehaviour { public Transform target; public float smoothTime 0.2f; private Vector3 velocity Vector3.zero; void LateUpdate() { if (target null) { return; } Vector3 targetPosition target.position; targetPosition.z -10f; transform.position Vector3.SmoothDamp(transform.position, targetPosition, ref velocity, smoothTime); } }為什么在LateUpdate而不是Update因為Update階段主角剛移動完攝像機在LateUpdate里再跟隨可以保證這一幀的畫面完整所有對象的位置都更新完了相機才挪過去不會出現(xiàn)角色先移動、相機晚半拍的視覺卡頓。SmoothDamp是做阻尼平滑0.2秒的smoothTime意味著相機不是瞬間跳到角色身上而是帶一點追趕效果手感更柔和。如果想做“硬跟隨”直接transform.position target.position再設(shè)z為-10即可。另一個關(guān)鍵點是相機z軸。相機默認(rèn)在z-10的位置視角朝正z方向看。如果跟隨腳本里直接把target.position賦給相機相機的z會變成0然后就看不到場景了。所以不管用哪種跟隨寫法都要把z固定為-10或者用相機初始的z值。5. 調(diào)試與避坑新手做Ruby項目最容易踩的坑5.1 角色不動、動畫不切換的排查順序做Ruby項目新手問得最多的就是“腳本掛上了角色還是不動”。遇到這個問題我建議按這個順序排查。第一步看Console窗口有沒有紅色報錯編譯錯誤會導(dǎo)致所有腳本失效最常見的是類名和文件名不一致、少了一個大括號。第二步看Inspector面板里腳本組件有沒有顯示“No script”或者“Script cannot be loaded”有的話檢查文件名和類名。第三步看Rigidbody2D和Animator組件是否存在腳本里GetComponent如果找不到組件會返回null調(diào)用rb.velocity時會直接報空引用。第四步看Input.GetAxisRaw是否真的拿到了輸入最簡單的驗證方式是在Update里加一行Debug.Log(moveX “,” moveY)運行后按方向鍵看Console輸出。如果數(shù)值始終是0說明輸入系統(tǒng)配置有問題按前面說的檢查Active Input Handling。動畫不切換的坑又是另一類。腳本里anim.SetFloat的參數(shù)名必須和Animator面板里的參數(shù)名完全一致大小寫也要一致。另一個問題是Transition條件設(shè)置錯誤比如你在Blend Tree的入口加了一個“speed大于0.1才切換”的條件但腳本里只寫了moveX、moveY那動畫永遠(yuǎn)切換不了。建議把Blend Tree的參數(shù)直接設(shè)為moveX、moveY腳本同步寫入這兩個值邏輯最簡單。5.2 角色抖動、卡頓與物理參數(shù)失常角色運行時抖動通常是幾個原因疊加的。第一是剛體的Interpolate沒有設(shè)置。Rigidbody2D組件里有一個Interpolate選項默認(rèn)None如果角色移動看起來一頓一頓的改成Interpolate可以平滑渲染幀和物理幀之間的間隙。這個選項不會改變真實物理狀態(tài)只影響視覺表現(xiàn)2D像素游戲加上它觀感會好很多。第二是角色卡在墻壁邊緣。檢查BoxCollider2D的大小是不是比Sprite可視部分大了一圈尤其是貼圖有透明邊時碰撞器默認(rèn)會包住整個貼圖像素范圍。另一種情況是Sorting Layer沒問題但Collider Editor Mode無意間改動了碰撞器偏移了。雙擊角色打開Collider的Edit Collider把邊界重新拉一下。第三是角色運動速度異??旎蛘呒觧ormalized之后移動速度感覺不對。先用Debug.Log把movement向量打出來看看如果speed設(shè)成5但實際感覺像飛檢查腳本里是否同時存在多個移動邏輯比如既有PlayerController又沿用了某個舊腳本兩個腳本同時給rb.velocity賦值就會互相覆蓋。5.3 我在實際項目里的幾個擴展建議Ruby項目跑通之后別急著關(guān)掉。我個人建議趁熱打鐵做兩個擴展。第一個是把移動改成真正的8方向動畫控制。官方示例里大多數(shù)版本只有左右翻轉(zhuǎn)和上下方向感覺像橫版游戲。如果你把Blend Tree的Type設(shè)為2D Freeform Directional并把上下左右動畫都掛進去角色就能朝8個方向自然移動游戲手感立刻提升一個檔次。第二個是加一個“交互鎖”比如角色進入對話、打開菜單時禁用移動輸入。實現(xiàn)方式很簡單在PlayerController里聲明一個public bool canMove trueUpdate開始時判斷if (!canMove) { rb.velocity Vector2.zero; return; }。Ruby后面有對話系統(tǒng)、機關(guān)觸發(fā)這個bool會在你實現(xiàn)NPC交互時起到大作用。還有一個細(xì)節(jié)值得注意腳本對組件的引用最好在Start里獲取一次不要在Update里反復(fù)GetComponent。GetComponent有性能開銷雖然單個角色感覺不到但養(yǎng)成緩存引用的習(xí)慣對后續(xù)開發(fā)復(fù)雜項目非常有幫助。這也是Ruby官方教程里很早就在強調(diào)的代碼習(xí)慣。最后再分享一個我做Ruby項目時常用的調(diào)試思路把角色當(dāng)成一個“信號中心”。輸入產(chǎn)生信號腳本處理信號動畫和物理響應(yīng)信號。出問題時從信號源頭逐段排查按鍵有沒有信號腳本有沒有收到信號組件有沒有響應(yīng)信號。用這個思路你會發(fā)現(xiàn)大多數(shù)問題都能在五分鐘內(nèi)定位。做完整個Ruby項目真正得到的不是那幾十行代碼而是這套排查和拆解問題的能力。后面做任何2D游戲再回頭看你寫的第一個PlayerController就會明白這一課的價值在哪里。