:從零搭建到打包的完整指南)
簡(jiǎn)介這是一份面向Unity初學(xué)者與在校學(xué)生的2D打怪物格斗游戲完整工程可直接作為期末大作業(yè)或課程設(shè)計(jì)參考。項(xiàng)目實(shí)現(xiàn)了角色上下左右跳躍移動(dòng)、攻擊怪物、血量條顯示、分?jǐn)?shù)統(tǒng)計(jì)與游戲背景切換等核心玩法并支持背景音樂(lè)調(diào)節(jié)和多個(gè)功能按鈕適合用來(lái)練習(xí)Unity基礎(chǔ)組件、碰撞檢測(cè)與UI交互。壓縮包為zip格式大小約50.78MB內(nèi)含工程源碼與相關(guān)資源文件解壓后可直接導(dǎo)入U(xiǎn)nity編輯器運(yùn)行調(diào)試。目前已有2400人學(xué)習(xí)下載熱度較高。通過(guò)這份資源讀者可以拿到一套可直接運(yùn)行的完整游戲案例理解角色控制、怪物AI、血量與計(jì)分系統(tǒng)的實(shí)現(xiàn)思路并在此基礎(chǔ)上修改參數(shù)、替換素材快速完成自己的期末作品或入門(mén)練習(xí)。1. 從零搭一個(gè)能打的 Unity 格斗游戲期末大作業(yè)為什么選它最穩(wěn)很多同學(xué)做 Unity 期末大作業(yè)時(shí)第一反應(yīng)是做個(gè)打磚塊或者見(jiàn)縫插針因?yàn)榻坛潭?、代碼短。但如果你想讓老師一眼記住同時(shí)又能真正練到 Unity 的核心機(jī)制打怪物格斗游戲其實(shí)是性?xún)r(jià)比最高的選擇。它天然覆蓋了狀態(tài)機(jī)、碰撞檢測(cè)、動(dòng)畫(huà)事件、UI 血條、音效反饋這幾個(gè)必考知識(shí)點(diǎn)代碼量可控美術(shù)資源用免費(fèi)素材就能湊齊初學(xué)者花兩三天也能跑出一個(gè)完整可玩的 Demo。這篇文章面向的是剛學(xué) Unity 不久、需要交期末大作業(yè)、又不想只做換皮打磚塊的初學(xué)者。我會(huì)按實(shí)際開(kāi)發(fā)順序從場(chǎng)景搭建、角色控制、攻擊判定、怪物 AI、血條 UI 到打包提交把每一步的參數(shù)和踩坑點(diǎn)講清楚。你跟著做最后手里會(huì)有一個(gè)能運(yùn)行、能演示、代碼結(jié)構(gòu)清晰的格斗游戲項(xiàng)目而不是一堆散裝腳本。2. 場(chǎng)景搭建與角色控制先把「能動(dòng)能打」跑通2.1 項(xiàng)目初始設(shè)置與素材導(dǎo)入打開(kāi) Unity Hub 新建一個(gè) 2D 項(xiàng)目模板選 2D Core名字隨便起比如MonsterFighter。創(chuàng)建完成后先別急著寫(xiě)代碼把項(xiàng)目結(jié)構(gòu)理清楚。在 Assets 下建幾個(gè)文件夾Scripts、Sprites、Animations、Prefabs、Audio。這個(gè)習(xí)慣能讓你后面找東西不抓狂老師翻你工程的時(shí)候也會(huì)覺(jué)得你靠譜。素材方面初學(xué)者不用自己畫(huà)。Unity Asset Store 里搜2D fighter或者pixel monster篩選 Free 資源下載幾個(gè)帶 idle、walk、attack、hurt 動(dòng)畫(huà)的角色包。導(dǎo)入后把貼圖拖進(jìn)Sprites文件夾動(dòng)畫(huà)文件拖進(jìn)Animations。注意導(dǎo)入貼圖時(shí)選中圖片在 Inspector 里把Texture Type確認(rèn)為Sprite (2D and UI)Pixels Per Unit保持默認(rèn) 100 就行。如果角色顯示得太大或太小調(diào)這個(gè)值不要直接改 Transform 的 Scale不然后面物理和動(dòng)畫(huà)會(huì)亂。場(chǎng)景里建一個(gè)空物體叫Ground加BoxCollider2D用來(lái)做地面碰撞。再建一個(gè)Player空物體把角色 Sprite 拖進(jìn)去作為子物體給 Player 加Rigidbody2D和CapsuleCollider2D。Rigidbody2D 的Gravity Scale設(shè)成 3 左右Freeze Rotation Z勾上防止角色被撞得旋轉(zhuǎn)。Collider 的大小調(diào)到剛好包住角色身體不要包到武器武器判定后面單獨(dú)做。2.2 用狀態(tài)機(jī)思路寫(xiě)角色移動(dòng)和跳躍初學(xué)者最容易犯的錯(cuò)是把移動(dòng)、跳躍、攻擊全塞在一個(gè)Update里用一堆 bool 互相判斷最后自己都理不清。正確做法是用一個(gè)簡(jiǎn)單的枚舉狀態(tài)機(jī)。新建腳本PlayerController.cs掛在 Player 上。using UnityEngine; public class PlayerController : MonoBehaviour { public float moveSpeed 5f; public float jumpForce 12f; public Transform groundCheck; public LayerMask groundLayer; private Rigidbody2D rb; private Animator anim; private bool isGrounded; private float moveInput; // 狀態(tài)枚舉后續(xù)攻擊狀態(tài)也加在這里 private enum State { Idle, Run, Jump, Attack } private State currentState; void Start() { rb GetComponentRigidbody2D(); anim GetComponentInChildrenAnimator(); } void Update() { moveInput Input.GetAxisRaw(Horizontal); // 地面檢測(cè)以 groundCheck 為圓心畫(huà)小圓碰到 groundLayer 就算落地 isGrounded Physics2D.OverlapCircle(groundCheck.position, 0.1f, groundLayer); if (Input.GetButtonDown(Jump) isGrounded) { rb.velocity new Vector2(rb.velocity.x, jumpForce); } // 根據(jù)輸入切換狀態(tài) if (isGrounded) currentState Mathf.Abs(moveInput) 0.01f ? State.Run : State.Idle; else currentState State.Jump; UpdateAnimation(); } void FixedUpdate() { rb.velocity new Vector2(moveInput * moveSpeed, rb.velocity.y); } void UpdateAnimation() { anim.SetBool(isRunning, currentState State.Run); anim.SetBool(isJumping, currentState State.Jump); // 翻轉(zhuǎn)角色朝向 if (moveInput ! 0) transform.localScale new Vector3(Mathf.Sign(moveInput), 1, 1); } }這段代碼的邏輯說(shuō)明Update負(fù)責(zé)讀輸入和狀態(tài)判斷FixedUpdate負(fù)責(zé)物理移動(dòng)兩者分開(kāi)是 Unity 的官方推薦做法。groundCheck是一個(gè)空物體放在角色腳底半徑 0.1 的圓形檢測(cè)足夠判斷是否落地。groundLayer在 Inspector 里設(shè)成 Ground 層記得給 Ground 物體指定 Layer。Mathf.Sign用來(lái)翻轉(zhuǎn)角色朝向比手動(dòng)寫(xiě) if-else 簡(jiǎn)潔。參數(shù)方面moveSpeed設(shè) 5 比較跟手jumpForce設(shè) 12 能跳過(guò)一人高的平臺(tái)。如果你覺(jué)得跳躍太飄把 Rigidbody2D 的Gravity Scale調(diào)到 4 或 5。動(dòng)畫(huà)狀態(tài)機(jī)里建兩個(gè) Bool 參數(shù)isRunning和isJumping分別連到 Run 和 Jump 動(dòng)畫(huà)過(guò)渡條件記得取消Has Exit Time否則動(dòng)畫(huà)會(huì)卡頓。2.3 攝像機(jī)跟隨與邊界限制攝像機(jī)跟隨是格斗游戲的標(biāo)配。新建腳本CameraFollow.cs掛在 Main Camera 上。using UnityEngine; public class CameraFollow : MonoBehaviour { public Transform target; public float smoothSpeed 0.125f; public Vector2 offset new Vector2(0, 1); public float minX -10f; public float maxX 10f; void LateUpdate() { if (target null) return; Vector3 desiredPos new Vector3( Mathf.Clamp(target.position.x offset.x, minX, maxX), target.position.y offset.y, -10f ); transform.position Vector3.Lerp(transform.position, desiredPos, smoothSpeed); } }LateUpdate在角色移動(dòng)之后執(zhí)行避免攝像機(jī)抖動(dòng)。smoothSpeed越小跟隨越慢0.125 是常見(jiàn)值。minX和maxX限制攝像機(jī)左右范圍防止跑出地圖看到空白。offset的 y 設(shè) 1 讓角色稍微偏下視野更舒服。注意攝像機(jī) z 軸保持 -102D 游戲固定這個(gè)值。3. 攻擊判定與怪物 AI讓?xiě)?zhàn)斗真正打起來(lái)3.1 用動(dòng)畫(huà)事件觸發(fā)攻擊判定攻擊判定不要用Input.GetKeyDown直接扣血那樣會(huì)出現(xiàn)「按鍵就掉血」的假手感。正確做法是動(dòng)畫(huà)播放到出拳那一幀用動(dòng)畫(huà)事件調(diào)用判定函數(shù)。在 Attack 動(dòng)畫(huà)的最后一幀附近右鍵Add Animation Event函數(shù)名填OnAttackHit。using UnityEngine; public class PlayerAttack : MonoBehaviour { public Transform attackPoint; public float attackRange 0.5f; public LayerMask enemyLayer; public int attackDamage 1; private Animator anim; void Start() { anim GetComponentInChildrenAnimator(); } void Update() { if (Input.GetKeyDown(KeyCode.J)) { anim.SetTrigger(Attack); } } // 動(dòng)畫(huà)事件調(diào)用出拳瞬間執(zhí)行 public void OnAttackHit() { Collider2D[] hitEnemies Physics2D.OverlapCircleAll( attackPoint.position, attackRange, enemyLayer); foreach (Collider2D enemy in hitEnemies) { enemy.GetComponentMonsterHealth()?.TakeDamage(attackDamage); } } // 在 Scene 視圖畫(huà)出攻擊范圍方便調(diào)試 void OnDrawGizmosSelected() { if (attackPoint null) return; Gizmos.color Color.red; Gizmos.DrawWireSphere(attackPoint.position, attackRange); } }attackPoint放在角色拳頭位置attackRange設(shè) 0.5 左右enemyLayer指定 Monster 層。OnDrawGizmosSelected讓你在 Scene 視圖里看到紅色圓圈調(diào)范圍的時(shí)候不用瞎猜。?.是空引用保護(hù)防止怪物沒(méi)有MonsterHealth組件時(shí)報(bào)錯(cuò)。動(dòng)畫(huà)狀態(tài)機(jī)里加一個(gè) Trigger 參數(shù)Attack從 Idle 和 Run 都能過(guò)渡到 Attack過(guò)渡時(shí)間設(shè) 0.1 秒取消Has Exit Time。3.2 怪物巡邏與追擊的簡(jiǎn)易 AI怪物 AI 不需要太復(fù)雜初學(xué)者用「巡邏 發(fā)現(xiàn)玩家就追 進(jìn)入攻擊范圍就打」三段式就夠了。新建MonsterAI.cs。using UnityEngine; public class MonsterAI : MonoBehaviour { public float patrolSpeed 1.5f; public float chaseSpeed 3f; public float detectRange 5f; public float attackRange 1f; public Transform player; public Transform patrolPointA; public Transform patrolPointB; public int damage 1; private Rigidbody2D rb; private Animator anim; private Transform currentPatrolTarget; private float attackCooldown 1f; private float lastAttackTime; void Start() { rb GetComponentRigidbody2D(); anim GetComponentInChildrenAnimator(); currentPatrolTarget patrolPointB; } void Update() { float distanceToPlayer Vector2.Distance(transform.position, player.position); if (distanceToPlayer attackRange) { rb.velocity Vector2.zero; anim.SetBool(isMoving, false); TryAttack(); } else if (distanceToPlayer detectRange) { ChasePlayer(); } else { Patrol(); } } void Patrol() { anim.SetBool(isMoving, true); Vector2 dir (currentPatrolTarget.position - transform.position).normalized; rb.velocity new Vector2(dir.x * patrolSpeed, rb.velocity.y); if (Vector2.Distance(transform.position, currentPatrolTarget.position) 0.2f) { currentPatrolTarget currentPatrolTarget patrolPointA ? patrolPointB : patrolPointA; } FlipSprite(dir.x); } void ChasePlayer() { anim.SetBool(isMoving, true); Vector2 dir (player.position - transform.position).normalized; rb.velocity new Vector2(dir.x * chaseSpeed, rb.velocity.y); FlipSprite(dir.x); } void TryAttack() { if (Time.time - lastAttackTime attackCooldown) return; lastAttackTime Time.time; anim.SetTrigger(Attack); // 實(shí)際傷害在動(dòng)畫(huà)事件里調(diào)用 DealDamage } public void DealDamage() { if (Vector2.Distance(transform.position, player.position) attackRange) { player.GetComponentPlayerHealth()?.TakeDamage(damage); } } void FlipSprite(float dirX) { if (Mathf.Abs(dirX) 0.01f) transform.localScale new Vector3(Mathf.Sign(dirX), 1, 1); } }邏輯說(shuō)明Update里按距離分三種情況優(yōu)先級(jí)是攻擊 追擊 巡邏。attackCooldown防止怪物攻速過(guò)快1 秒是合理值。DealDamage同樣掛在動(dòng)畫(huà)事件上保證傷害和動(dòng)畫(huà)同步。patrolPointA和patrolPointB是兩個(gè)空物體放在場(chǎng)景里標(biāo)記巡邏路線(xiàn)。detectRange設(shè) 5attackRange設(shè) 1這兩個(gè)值根據(jù)你角色和怪物的大小微調(diào)。3.3 血條 UI 與受傷反饋血條用 Unity 自帶的 Slider 就行。在 Canvas 下建兩個(gè) Slider一個(gè)叫PlayerHealthBar一個(gè)叫MonsterHealthBar。把 Slider 的Interactable取消勾選Transition設(shè)為 None。然后寫(xiě)Health.cs統(tǒng)一管理。using UnityEngine; using UnityEngine.UI; public class Health : MonoBehaviour { public int maxHealth 5; public Slider healthBar; public bool isPlayer; private int currentHealth; void Start() { currentHealth maxHealth; if (healthBar ! null) { healthBar.maxValue maxHealth; healthBar.value currentHealth; } } public void TakeDamage(int amount) { currentHealth - amount; currentHealth Mathf.Clamp(currentHealth, 0, maxHealth); if (healthBar ! null) healthBar.value currentHealth; // 受傷閃紅 StartCoroutine(FlashRed()); if (currentHealth 0) Die(); } System.Collections.IEnumerator FlashRed() { SpriteRenderer sr GetComponentInChildrenSpriteRenderer(); if (sr null) yield break; Color original sr.color; sr.color Color.red; yield return new WaitForSeconds(0.1f); sr.color original; } void Die() { if (isPlayer) { UnityEngine.SceneManagement.SceneManager.LoadScene( UnityEngine.SceneManagement.SceneManager.GetActiveScene().name); } else { Destroy(gameObject); } } }maxHealth玩家設(shè) 5怪物設(shè) 3數(shù)值小一點(diǎn)戰(zhàn)斗節(jié)奏快。FlashRed用協(xié)程做受傷反饋0.1 秒足夠明顯又不刺眼。玩家死亡直接重載當(dāng)前場(chǎng)景怪物死亡銷(xiāo)毀物體。注意healthBar在 Inspector 里拖對(duì)應(yīng)的 Slider玩家和怪物各掛一個(gè)Health勾選isPlayer區(qū)分。4. 避坑與排查初學(xué)者最容易翻車(chē)的 5 個(gè)地方4.1 攻擊判定打不中Gizmos 圓圈明明碰到了現(xiàn)象動(dòng)畫(huà)事件觸發(fā)了OverlapCircleAll也執(zhí)行了但怪物不掉血。原因九成是 Layer 沒(méi)設(shè)對(duì)。enemyLayer在 Inspector 里要選 Monster 層同時(shí)怪物物體的 Layer 也要改成 Monster。如果怪物是子物體Layer 要設(shè)在掛 Collider 的那一層。解決選中怪物Inspector 頂部 Layer 下拉選 Monster如果沒(méi)有就 Add Layer 新建。然后確認(rèn)PlayerAttack腳本的enemyLayer字段選的是 Monster不是 Default。4.2 角色跳躍后卡在空中或者落地檢測(cè)一直為 false現(xiàn)象按跳躍能跳但落地后isGrounded還是 false導(dǎo)致跳不起來(lái)。原因是groundCheck的位置不對(duì)或者groundLayer沒(méi)設(shè)。解決在 Scene 視圖里選中 Player看groundCheck空物體的位置是不是在腳底。如果角色 Collider 底部在 y0groundCheck的 y 設(shè) -0.5 左右。groundLayer在 Inspector 里選 Ground 層同時(shí) Ground 物體的 Layer 也要改成 Ground。檢測(cè)半徑 0.1 如果還不行調(diào)到 0.15。4.3 動(dòng)畫(huà)播放但角色不移動(dòng)或者移動(dòng)時(shí)動(dòng)畫(huà)不切換現(xiàn)象按方向鍵角色滑著走但 Idle 動(dòng)畫(huà)一直在播。原因是 Animator 參數(shù)名和代碼里寫(xiě)的不一致或者過(guò)渡條件沒(méi)設(shè)對(duì)。解決打開(kāi) Animator 窗口看 Parameters 里是不是有isRunning和isJumping拼寫(xiě)要和代碼完全一致大小寫(xiě)敏感。然后點(diǎn) Idle 到 Run 的過(guò)渡箭頭Conditions 里加isRunning為 true取消Has Exit Time。Run 到 Idle 同理isRunning為 false。4.4 怪物攻擊沒(méi)有傷害或者傷害觸發(fā)多次現(xiàn)象怪物揮拳動(dòng)畫(huà)播了但玩家不掉血或者一次攻擊掉好幾滴血。原因是動(dòng)畫(huà)事件沒(méi)加或者加在了多個(gè)幀上。解決打開(kāi)怪物 Attack 動(dòng)畫(huà)在出拳那一幀右鍵 Add Animation Event函數(shù)名填DealDamage。檢查時(shí)間軸上是不是只有一個(gè)事件標(biāo)記多了就刪掉。另外確認(rèn)MonsterAI腳本掛在怪物身上DealDamage是 public 方法。4.5 打包后 UI 血條不顯示或者分辨率錯(cuò)亂現(xiàn)象在 Editor 里跑得好好的Build 出來(lái)血條不見(jiàn)了。原因是 Canvas 的Render Mode設(shè)成了Screen Space - Camera但沒(méi)指定攝像機(jī)或者 UI 縮放沒(méi)適配。解決選中 CanvasRender Mode改成Screen Space - Overlay這是最省事的。然后 Canvas Scaler 的UI Scale Mode選Scale With Screen SizeReference Resolution設(shè) 1920x1080Match設(shè) 0.5。這樣不管什么分辨率血條都在該在的位置。5. 進(jìn)階技巧用對(duì)象池和音效讓手感上一個(gè)臺(tái)階5.1 對(duì)象池管理怪物生成避免頻繁 Instantiate如果你的游戲有刷怪邏輯直接Instantiate和Destroy會(huì)產(chǎn)生 GC 卡頓。初學(xué)者可以用一個(gè)簡(jiǎn)單的對(duì)象池。新建ObjectPool.cs。using System.Collections.Generic; using UnityEngine; public class ObjectPool : MonoBehaviour { public GameObject monsterPrefab; public int poolSize 10; private QueueGameObject pool new QueueGameObject(); void Start() { for (int i 0; i poolSize; i) { GameObject obj Instantiate(monsterPrefab); obj.SetActive(false); pool.Enqueue(obj); } } public GameObject GetMonster(Vector3 position) { if (pool.Count 0) { GameObject obj Instantiate(monsterPrefab); obj.SetActive(true); return obj; } GameObject monster pool.Dequeue(); monster.transform.position position; monster.SetActive(true); return monster; } public void ReturnMonster(GameObject monster) { monster.SetActive(false); pool.Enqueue(monster); } }poolSize設(shè) 10 夠用怪物死亡時(shí)不Destroy而是調(diào)用ReturnMonster放回池子。這樣運(yùn)行久了也不會(huì)卡。注意怪物腳本里Die方法要改成調(diào)用對(duì)象池的ReturnMonster而不是Destroy(gameObject)。5.2 音效與打擊感三個(gè)必須加的音效點(diǎn)格斗游戲的手感一半靠音效。至少加三個(gè)揮拳破空聲、擊中反饋聲、受傷呻吟聲。在PlayerAttack的OnAttackHit里擊中敵人時(shí)播放hitSound在Health.TakeDamage里播放hurtSound。AudioSource 掛在角色上PlayOneShot播放短音效不要用Play否則會(huì)打斷。音量Volume設(shè) 0.7 左右太高會(huì)爆。AudioClip 在 Asset Store 搜free fight sound就能找到。5.3 用宏定義區(qū)分 Editor 和打包環(huán)境熱詞里提到的unity宏定義在這里很有用。比如你想在 Editor 里顯示 Gizmos 調(diào)試但打包后不顯示可以用#if UNITY_EDITOR包住OnDrawGizmosSelected。再比如你想在 Editor 里按 R 鍵直接重開(kāi)打包后禁用也可以加宏。這個(gè)技巧能讓你的代碼更專(zhuān)業(yè)老師看到會(huì)覺(jué)得你考慮周全。#if UNITY_EDITOR void Update() { if (Input.GetKeyDown(KeyCode.R)) { UnityEngine.SceneManagement.SceneManager.LoadScene( UnityEngine.SceneManagement.SceneManager.GetActiveScene().name); } } #endif最后說(shuō)一個(gè)我自己的習(xí)慣每次改完代碼先按 CtrlS 保存再回 Unity 等編譯完成看到右下角沒(méi)有紅色報(bào)錯(cuò)再運(yùn)行。這個(gè)習(xí)慣幫我省了無(wú)數(shù)次「改了沒(méi)生效」的玄學(xué)時(shí)間。項(xiàng)目做完后把Scripts文件夾里的腳本按功能分好注釋寫(xiě)清楚打包前在 Build Settings 里把場(chǎng)景加進(jìn)去選 Windows 或 Mac 平臺(tái)點(diǎn) Build 就行。希望幫到你。本文還有配套的精品資源點(diǎn)擊獲取