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