ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

Unity格斗游戏期末大作业:从零搭建到打包的完整指南

Unity格斗游戏期末大作业:从零搭建到打包的完整指南 简介这是一份面向Unity初学者与在校学生的2D打怪物格斗游戏完整工程可直接作为期末大作业或课程设计参考。项目实现了角色上下左右跳跃移动、攻击怪物、血量条显示、分数统计与游戏背景切换等核心玩法并支持背景音乐调节和多个功能按钮适合用来练习Unity基础组件、碰撞检测与UI交互。压缩包为zip格式大小约50.78MB内含工程源码与相关资源文件解压后可直接导入Unity编辑器运行调试。目前已有2400人学习下载热度较高。通过这份资源读者可以拿到一套可直接运行的完整游戏案例理解角色控制、怪物AI、血量与计分系统的实现思路并在此基础上修改参数、替换素材快速完成自己的期末作品或入门练习。1. 从零搭一个能打的 Unity 格斗游戏期末大作业为什么选它最稳很多同学做 Unity 期末大作业时第一反应是做个打砖块或者见缝插针因为教程多、代码短。但如果你想让老师一眼记住同时又能真正练到 Unity 的核心机制打怪物格斗游戏其实是性价比最高的选择。它天然覆盖了状态机、碰撞检测、动画事件、UI 血条、音效反馈这几个必考知识点代码量可控美术资源用免费素材就能凑齐初学者花两三天也能跑出一个完整可玩的 Demo。这篇文章面向的是刚学 Unity 不久、需要交期末大作业、又不想只做换皮打砖块的初学者。我会按实际开发顺序从场景搭建、角色控制、攻击判定、怪物 AI、血条 UI 到打包提交把每一步的参数和踩坑点讲清楚。你跟着做最后手里会有一个能运行、能演示、代码结构清晰的格斗游戏项目而不是一堆散装脚本。2. 场景搭建与角色控制先把「能动能打」跑通2.1 项目初始设置与素材导入打开 Unity Hub 新建一个 2D 项目模板选 2D Core名字随便起比如MonsterFighter。创建完成后先别急着写代码把项目结构理清楚。在 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 就行。如果角色显示得太大或太小调这个值不要直接改 Transform 的 Scale不然后面物理和动画会乱。场景里建一个空物体叫Ground加BoxCollider2D用来做地面碰撞。再建一个Player空物体把角色 Sprite 拖进去作为子物体给 Player 加Rigidbody2D和CapsuleCollider2D。Rigidbody2D 的Gravity Scale设成 3 左右Freeze Rotation Z勾上防止角色被撞得旋转。Collider 的大小调到刚好包住角色身体不要包到武器武器判定后面单独做。2.2 用状态机思路写角色移动和跳跃初学者最容易犯的错是把移动、跳跃、攻击全塞在一个Update里用一堆 bool 互相判断最后自己都理不清。正确做法是用一个简单的枚举状态机。新建脚本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; // 状态枚举后续攻击状态也加在这里 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); } // 根据输入切换状态 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); // 翻转角色朝向 if (moveInput ! 0) transform.localScale new Vector3(Mathf.Sign(moveInput), 1, 1); } }这段代码的逻辑说明Update负责读输入和状态判断FixedUpdate负责物理移动两者分开是 Unity 的官方推荐做法。groundCheck是一个空物体放在角色脚底半径 0.1 的圆形检测足够判断是否落地。groundLayer在 Inspector 里设成 Ground 层记得给 Ground 物体指定 Layer。Mathf.Sign用来翻转角色朝向比手动写 if-else 简洁。参数方面moveSpeed设 5 比较跟手jumpForce设 12 能跳过一人高的平台。如果你觉得跳跃太飘把 Rigidbody2D 的Gravity Scale调到 4 或 5。动画状态机里建两个 Bool 参数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在角色移动之后执行避免摄像机抖动。smoothSpeed越小跟随越慢0.125 是常见值。minX和maxX限制摄像机左右范围防止跑出地图看到空白。offset的 y 设 1 让角色稍微偏下视野更舒服。注意摄像机 z 轴保持 -102D 游戏固定这个值。3. 攻击判定与怪物 AI让战斗真正打起来3.1 用动画事件触发攻击判定攻击判定不要用Input.GetKeyDown直接扣血那样会出现「按键就掉血」的假手感。正确做法是动画播放到出拳那一帧用动画事件调用判定函数。在 Attack 动画的最后一帧附近右键Add Animation Event函数名填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); } } // 动画事件调用出拳瞬间执行 public void OnAttackHit() { Collider2D[] hitEnemies Physics2D.OverlapCircleAll( attackPoint.position, attackRange, enemyLayer); foreach (Collider2D enemy in hitEnemies) { enemy.GetComponentMonsterHealth()?.TakeDamage(attackDamage); } } // 在 Scene 视图画出攻击范围方便调试 void OnDrawGizmosSelected() { if (attackPoint null) return; Gizmos.color Color.red; Gizmos.DrawWireSphere(attackPoint.position, attackRange); } }attackPoint放在角色拳头位置attackRange设 0.5 左右enemyLayer指定 Monster 层。OnDrawGizmosSelected让你在 Scene 视图里看到红色圆圈调范围的时候不用瞎猜。?.是空引用保护防止怪物没有MonsterHealth组件时报错。动画状态机里加一个 Trigger 参数Attack从 Idle 和 Run 都能过渡到 Attack过渡时间设 0.1 秒取消Has Exit Time。3.2 怪物巡逻与追击的简易 AI怪物 AI 不需要太复杂初学者用「巡逻 发现玩家就追 进入攻击范围就打」三段式就够了。新建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); // 实际伤害在动画事件里调用 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里按距离分三种情况优先级是攻击 追击 巡逻。attackCooldown防止怪物攻速过快1 秒是合理值。DealDamage同样挂在动画事件上保证伤害和动画同步。patrolPointA和patrolPointB是两个空物体放在场景里标记巡逻路线。detectRange设 5attackRange设 1这两个值根据你角色和怪物的大小微调。3.3 血条 UI 与受伤反馈血条用 Unity 自带的 Slider 就行。在 Canvas 下建两个 Slider一个叫PlayerHealthBar一个叫MonsterHealthBar。把 Slider 的Interactable取消勾选Transition设为 None。然后写Health.cs统一管理。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玩家设 5怪物设 3数值小一点战斗节奏快。FlashRed用协程做受伤反馈0.1 秒足够明显又不刺眼。玩家死亡直接重载当前场景怪物死亡销毁物体。注意healthBar在 Inspector 里拖对应的 Slider玩家和怪物各挂一个Health勾选isPlayer区分。4. 避坑与排查初学者最容易翻车的 5 个地方4.1 攻击判定打不中Gizmos 圆圈明明碰到了现象动画事件触发了OverlapCircleAll也执行了但怪物不掉血。原因九成是 Layer 没设对。enemyLayer在 Inspector 里要选 Monster 层同时怪物物体的 Layer 也要改成 Monster。如果怪物是子物体Layer 要设在挂 Collider 的那一层。解决选中怪物Inspector 顶部 Layer 下拉选 Monster如果没有就 Add Layer 新建。然后确认PlayerAttack脚本的enemyLayer字段选的是 Monster不是 Default。4.2 角色跳跃后卡在空中或者落地检测一直为 false现象按跳跃能跳但落地后isGrounded还是 false导致跳不起来。原因是groundCheck的位置不对或者groundLayer没设。解决在 Scene 视图里选中 Player看groundCheck空物体的位置是不是在脚底。如果角色 Collider 底部在 y0groundCheck的 y 设 -0.5 左右。groundLayer在 Inspector 里选 Ground 层同时 Ground 物体的 Layer 也要改成 Ground。检测半径 0.1 如果还不行调到 0.15。4.3 动画播放但角色不移动或者移动时动画不切换现象按方向键角色滑着走但 Idle 动画一直在播。原因是 Animator 参数名和代码里写的不一致或者过渡条件没设对。解决打开 Animator 窗口看 Parameters 里是不是有isRunning和isJumping拼写要和代码完全一致大小写敏感。然后点 Idle 到 Run 的过渡箭头Conditions 里加isRunning为 true取消Has Exit Time。Run 到 Idle 同理isRunning为 false。4.4 怪物攻击没有伤害或者伤害触发多次现象怪物挥拳动画播了但玩家不掉血或者一次攻击掉好几滴血。原因是动画事件没加或者加在了多个帧上。解决打开怪物 Attack 动画在出拳那一帧右键 Add Animation Event函数名填DealDamage。检查时间轴上是不是只有一个事件标记多了就删掉。另外确认MonsterAI脚本挂在怪物身上DealDamage是 public 方法。4.5 打包后 UI 血条不显示或者分辨率错乱现象在 Editor 里跑得好好的Build 出来血条不见了。原因是 Canvas 的Render Mode设成了Screen Space - Camera但没指定摄像机或者 UI 缩放没适配。解决选中 CanvasRender Mode改成Screen Space - Overlay这是最省事的。然后 Canvas Scaler 的UI Scale Mode选Scale With Screen SizeReference Resolution设 1920x1080Match设 0.5。这样不管什么分辨率血条都在该在的位置。5. 进阶技巧用对象池和音效让手感上一个台阶5.1 对象池管理怪物生成避免频繁 Instantiate如果你的游戏有刷怪逻辑直接Instantiate和Destroy会产生 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设 10 够用怪物死亡时不Destroy而是调用ReturnMonster放回池子。这样运行久了也不会卡。注意怪物脚本里Die方法要改成调用对象池的ReturnMonster而不是Destroy(gameObject)。5.2 音效与打击感三个必须加的音效点格斗游戏的手感一半靠音效。至少加三个挥拳破空声、击中反馈声、受伤呻吟声。在PlayerAttack的OnAttackHit里击中敌人时播放hitSound在Health.TakeDamage里播放hurtSound。AudioSource 挂在角色上PlayOneShot播放短音效不要用Play否则会打断。音量Volume设 0.7 左右太高会爆。AudioClip 在 Asset Store 搜free fight sound就能找到。5.3 用宏定义区分 Editor 和打包环境热词里提到的unity宏定义在这里很有用。比如你想在 Editor 里显示 Gizmos 调试但打包后不显示可以用#if UNITY_EDITOR包住OnDrawGizmosSelected。再比如你想在 Editor 里按 R 键直接重开打包后禁用也可以加宏。这个技巧能让你的代码更专业老师看到会觉得你考虑周全。#if UNITY_EDITOR void Update() { if (Input.GetKeyDown(KeyCode.R)) { UnityEngine.SceneManagement.SceneManager.LoadScene( UnityEngine.SceneManagement.SceneManager.GetActiveScene().name); } } #endif最后说一个我自己的习惯每次改完代码先按 CtrlS 保存再回 Unity 等编译完成看到右下角没有红色报错再运行。这个习惯帮我省了无数次「改了没生效」的玄学时间。项目做完后把Scripts文件夹里的脚本按功能分好注释写清楚打包前在 Build Settings 里把场景加进去选 Windows 或 Mac 平台点 Build 就行。希望帮到你。本文还有配套的精品资源点击获取
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进