简介:这份基于Unity3D的ACT游戏设计与实现毕业设计资料,面向计算机、软件工程等专业需要完成游戏方向毕业设计的学生,内容围绕一款以战斗为核心的2D动作游戏展开,覆盖绪论、开发工具及相关技术、游戏设计、测试与优化等完整论文结构。技术层面涉及C#逻辑编程、UGUI与Text Mesh Pro界面、Cinemachine虚拟相机、有限状态机角色行为切换、ScriptableObject武器切换系统以及Unity物理碰撞检测,并包含玩家死亡判定、按键输入处理、人物与敌人模块、状态机模块等设计说明。资源包共1个docx文件,约11.34MB,论文与源码内容集中于该文档内,目录章节清晰,便于按模块查阅。目前已有192人学习下载。适合希望借鉴完整开发流程、代码组织方式与低配置设备优化思路的读者,也可为开题报告、任务书与答辩准备提供参考。
1. 从论文标题拆到可跑的ACT:Unity3D项目到底要解决哪几件事
很多人拿到“基于Unity3D的ACT游戏的设计与实现(论文+源码)”这类资料,第一反应是找一份能跑的工程,然后对着源码改数值。真正卡住进度的往往不是缺少源码,而是ACT游戏里几个互相咬合的系统没有事先定好边界:输入缓冲、动画状态、攻击判定、受击反馈、锁定与相机。它们任何一个先写死,后面连招和敌人AI都会返工。ACT区别于普通RPG或平台跳跃的核心,是帧窗口和取消关系:前摇多久、判定几帧、后摇能不能被闪避或下一段攻击取消。适合做毕设、独立Demo或需要一份可拆解Unity3D源码结构的人。下面按“角色控制器 → 战斗判定 → 敌人与关卡 → 排错与复现”推一遍,代码可直接落进Unity3D工程。
2. Unity3D ACT角色控制器:输入缓冲、状态机与移动手感
2.1 为什么ACT角色的移动和攻击不能全写在Update里
Unity3D的Update跟着渲染帧走,帧率波动时输入采样和动画事件容易错位;FixedUpdate跟着物理时钟走,适合刚体移动和碰撞查询。ACT游戏里攻击判定往往由动画事件触发,如果移动、转向、攻击冷却全塞在Update,低帧率下会出现“按了闪避却原地吃刀”或“连招窗口忽长忽短”。常见做法是:输入在Update采样并写入缓冲,移动和重力在FixedUpdate执行,状态切换由状态机统一收口,动画事件只负责开关判定和结算伤害。这样调参时能清楚知道改的是输入窗口、物理速度还是动画曲线,而不是在一堆if里猜。
2.2 用有限状态机管理角色:接口与切换代码
状态机不需要上很重的框架,ACT角色常用五到八个状态:待机、移动、冲刺、攻击、受击、闪避、死亡。每个状态只处理自己关心的输入和物理,退出时清理标志位,避免上一状态的速度或无敌帧残留。
public interface IState { void Enter(); void Tick(float dt); // Update调用,处理输入与动画参数 void FixedTick(float dt); // FixedUpdate调用,处理刚体移动 void Exit(); } public class StateMachine { private IState current; public IState Current => current; public void Change(IState next) { if (next == null || next == current) return; current?.Exit(); current = next; current.Enter(); } public void Tick(float dt) => current?.Tick(dt); public void FixedTick(float dt) => current?.FixedTick(dt); }逻辑说明:Change先执行旧状态的Exit,再执行新状态的Enter,保证攻击状态退出时能清掉Hitbox和连招索引。Tick里不要直接改刚体速度,只做输入采集、动画参数写入和状态切换判断;FixedTick里根据当前状态给Rigidbody.velocity或MovePosition赋值。参数dt在Update里传Time.deltaTime,在FixedUpdate里传Time.fixedDeltaTime,不要混用,否则重力与移动速度会随帧率漂移。
2.3 输入缓冲与连招窗口:参数表
玩家在攻击后摇里提前按下一段攻击,系统应该记住这个输入,等取消窗口一开就触发,而不是要求精确到某一帧。输入缓冲窗口一般设0.12到0.18秒,低于0.1秒连按容易丢,高于0.2秒会“吞输入”,角色突然打出玩家没想打的连段。
public class InputBuffer { public float window = 0.15f; private float lastAttackTime = -99f; public void MarkAttack() => lastAttackTime = Time.time; public bool ConsumeAttack() { if (Time.time - lastAttackTime <= window) { lastAttackTime = -99f; // 消费后清空,防止一次输入触发两段 return true; } return false; } }参数说明:window是缓冲有效期,按手感在0.12到0.18之间调;lastAttackTime记录按下攻击键的时刻;ConsumeAttack返回true后立刻清空,避免同一帧既进第一段又进第二段。连招窗口不放在输入缓冲里,而是放在动画状态上:每段攻击动画的60%到85%区间允许取消到下一段,后摇最后0.1秒允许取消到闪避。这样输入缓冲负责“记住”,连招窗口负责“许可”,两个概念不要混。
| 参数 | 推荐范围 | 作用 |
|---|---|---|
| 输入缓冲窗口 | 0.12~0.18s | 提前按键的有效期 |
| 连招取消窗口 | 动画60%~85% | 允许接下一段攻击 |
| 后摇闪避取消 | 最后0.08~0.15s | 保留操作上限 |
| 角色转向速度 | 720~1080度/秒 | 攻击时是否急转 |
| 重力 | 25~35 | 比现实重力更跟手 |
2.4 把角色挂进场景:Rigidbody、CapsuleCollider与Animator最小配置
角色根节点挂Rigidbody,勾选Freeze Rotation X/Z,防止物理把角色推倒;Interpolate设为Interpolate,减少移动抖动;Collision Detection在ACT里常用Continuous,避免快速冲刺穿墙。CapsuleCollider高度比角色模型高一点,半径不要包住武器,武器判定另用Hitbox。Animator关闭Apply Root Motion,位移由代码控制,否则动画位移会和刚体速度打架;Update Mode选Normal或Animate Physics取决于是否在动画事件里做物理查询,常见做法是Normal加OverlapBox主动查询。Animator Controller里攻击状态不要自动跳转,用代码在合适帧调用CrossFadeInFixedTime,并给动画事件留出稳定名称。
3. ACT战斗判定与动画驱动:Hitbox、受击框与连招取消
3.1 攻击判定选Trigger还是OverlapBox
用OnTriggerEnter做攻击判定上手快,但依赖物理更新,快速移动或低帧率下可能漏掉敌人;而且Trigger一旦开启,多帧持续触发会让同一刀打出多次伤害。ACT更稳的做法是:动画事件在判定帧调用一次Physics.OverlapBox或OverlapSphere,主动查询敌人层,命中后立即结算,再关闭判定。这样伤害次数由动画事件决定,不由物理帧数决定。
public class Hitbox : MonoBehaviour { public LayerMask targetLayer; public Vector3 halfExtents = new Vector3(0.6f, 0.6f, 0.8f); public int damage = 10; public Transform hitPoint; public void DealDamage() { Collider[] hits = Physics.OverlapBox( hitPoint.position, halfExtents, hitPoint.rotation, targetLayer ); foreach (var hit in hits) { var receiver = hit.GetComponent<IDamageable>(); receiver?.TakeDamage(new DamageInfo(damage, hitPoint.forward)); } } }逻辑说明:OverlapBox在调用瞬间做一次空间查询,不会因为物理帧漏掉。targetLayer只勾敌人层,避免打到玩家自己或场景装饰。halfExtents是盒子半尺寸,近战轻攻击可设0.5×0.5×0.7,重攻击设0.8×0.8×1.2。hitPoint是攻击判定中心,一般放在角色前方,跟随动画或骨骼。DealDamage由动画事件调用一次,不要每帧调用,否则一刀多段伤害。
3.2 用动画事件驱动Hitbox开关的代码
动画事件名尽量用英文且带前缀,避免和美术改动画时冲突。攻击动画开始只开判定,判定帧结算伤害,收招时关判定并清理受击列表。
public class AttackAnimationEvent : MonoBehaviour { public Hitbox hitbox; public void AE_HitboxOn() { hitbox.gameObject.SetActive(true); } public void AE_HitboxDamage() { hitbox.DealDamage(); } public void AE_HitboxOff() { hitbox.gameObject.SetActive(false); } }逻辑说明:AE_HitboxOn在攻击前摇结束帧触发,AE_HitboxDamage在命中帧触发一次,AE_HitboxOff在后摇开始帧关闭。参数上,hitbox引用挂载Hitbox组件的子物体,子物体平时关闭,减少无谓查询。注意动画事件不会自动去重,如果动画被过渡打断,可能只触发On不触发Off,所以状态机退出攻击状态时也要强制关闭Hitbox。
3.3 受击硬直、击退与顿帧的参数设计
受击反馈是ACT手感的来源,参数不能只写一个“受伤动画”。轻攻击硬直短,重攻击硬直长,击退方向取攻击者朝向,顿帧只冻结角色动画和物理,不冻结UI和音乐。
| 参数 | 推荐范围 | 说明 |
|---|---|---|
| 轻击硬直 | 0.25~0.4s | 可接下一段连招 |
| 重击硬直 | 0.5~0.8s | 给玩家确认时间 |
| 击退距离 | 0.5~2.0m | 根据体重和攻击强度 |
| 顿帧时长 | 0.03~0.08s | 命中瞬间的卡肉感 |
| 受击无敌帧 | 0.1~0.3s | 防止多段判定重复打 |
顿帧不建议直接改Time.timeScale,因为它会影响UI、音频和输入采样。常见做法是给受击者加一个计时器,在计时期间把Animator.speed和移动速度临时归零,计时结束恢复。击退用Rigidbody.AddForce或直接改速度,方向用attacker.forward,并限制水平击退,避免敌人被挑到天上。
3.4 连招取消表与状态转移条件
连招不是简单的“按一下换下一个动画”,而是一张取消表。每段攻击在动画的某个区间内允许取消到指定状态,区间之外输入无效。下面这张表可以直接放进ScriptableObject或Excel,再由代码转成配置。
| 当前段 | 允许取消到 | 取消窗口 | 条件 |
|---|---|---|---|
| 攻击1 | 攻击2 | 动画60%~85% | 输入缓冲有攻击 |
| 攻击1 | 闪避 | 动画75%~95% | 输入闪避且耐力足够 |
| 攻击2 | 攻击3 | 动画55%~80% | 输入缓冲有攻击 |
| 攻击3 | 闪避 | 动画80%~100% | 输入闪避 |
| 任意攻击 | 受击 | 命中后立即 | 受击优先级最高 |
代码里用归一化时间AnimatorStateInfo.normalizedTime判断窗口,整数部分表示循环次数,小数部分才是当前段进度。参数上,窗口太早会让连招看起来没有收招,太晚则玩家觉得“按了没反应”。受击优先级要高于攻击,否则玩家会被自己的连招锁死。
4. 敌人AI、锁定与关卡节奏:ACT游戏源码里常被忽略的三块
4.1 轻量敌人行为状态机
敌人AI不需要一上来就上行为树,三到五个状态足以支撑毕设Demo:巡逻、追击、攻击、受击、死亡。追击用距离判断,攻击用冷却和朝向判断,受击由伤害接口打断。
public enum EnemyState { Patrol, Chase, Attack, Hurt, Dead } public class EnemyBrain : MonoBehaviour { public Transform player; public float chaseRange = 8f; public float attackRange = 1.8f; public float attackCooldown = 1.2f; private EnemyState state = EnemyState.Patrol; private float lastAttackTime = -99f; void Update() { if (state == EnemyState.Dead || state == EnemyState.Hurt) return; float dist = Vector3.Distance(transform.position, player.position); if (dist <= attackRange && Time.time - lastAttackTime >= attackCooldown) { state = EnemyState.Attack; lastAttackTime = Time.time; } else if (dist <= chaseRange) { state = EnemyState.Chase; } else { state = EnemyState.Patrol; } } }逻辑说明:chaseRange和attackRange是核心参数,追击范围应大于攻击范围1.5到3倍,否则敌人会在边缘反复切换。attackCooldown控制攻击频率,近战杂兵常用1.0到1.8秒。Hurt状态由IDamageable.TakeDamage设置,并在硬直结束后回到追击。注意敌人不要每帧做FindObjectOfType,玩家引用在场景初始化时赋一次。
4.2 锁定目标与相机跟随
锁定是ACT战斗的节奏器。按锁定键时从玩家前方扇形范围内选最近敌人,把相机LookAt目标切到敌人与玩家的中点,角色朝向由输入和相机共同决定。
public class LockOnSystem : MonoBehaviour { public Transform player; public float lockRadius = 12f; public float switchAngle = 45f; public LayerMask enemyLayer; public Transform cameraPivot; private Transform currentTarget; public void TryLock() { Collider[] enemies = Physics.OverlapSphere(player.position, lockRadius, enemyLayer); float bestScore = float.MaxValue; Transform best = null; foreach (var e in enemies) { Vector3 dir = e.transform.position - player.position; float angle = Vector3.Angle(player.forward, dir); if (angle > 90f) continue; float score = dir.sqrMagnitude + angle * 0.1f; if (score < bestScore) { bestScore = score; best = e.transform; } } currentTarget = best; } void LateUpdate() { if (currentTarget == null) return; Vector3 mid = (player.position + currentTarget.position) * 0.5f; cameraPivot.position = Vector3.Lerp(cameraPivot.position, mid, 8f * Time.deltaTime); cameraPivot.LookAt(currentTarget); } }逻辑说明:lockRadius控制锁定搜索半径,太小锁不到远处敌人,太大容易锁到屏幕外目标。switchAngle用于左右切目标,常见45到60度。cameraPivot用LateUpdate跟随,保证在角色移动之后更新,减少抖动。锁定后角色移动方向要重新映射到相机平面,否则会出现“锁定后按前却绕圈”。
4.3 用ScriptableObject配置关卡与敌人数据
把敌人血量、攻击力、移动速度、攻击间隔放进ScriptableObject,场景里只挂配置引用,改数值不用重新编译。UGUI血条和伤害数字也从同一份配置取颜色和缩放。
[CreateAssetMenu(fileName = "EnemyConfig", menuName = "ACT/EnemyConfig")] public class EnemyConfig : ScriptableObject { public string enemyName; public int maxHp = 100; public int damage = 10; public float moveSpeed = 3.5f; public float attackRange = 1.8f; public float attackCooldown = 1.2f; public Color hpBarColor = Color.red; }逻辑说明:CreateAssetMenu让配置在Project右键菜单创建。maxHp、damage、moveSpeed是运行时只读参数,不要在敌人脚本里再写一份。hpBarColor给UGUI血条用,避免每个敌人手动调。注意ScriptableObject在打包后是只读的,运行时修改不会保存回资源,做存档要另用JSON或PlayerPrefs。
4.4 用UGUI做血条、连击数与伤害数字
UGUI血条用Image.fillAmount,比缩放RectTransform更省性能。连击数用一个计时窗口,超过1.5到2.5秒没有新命中就清零。伤害数字用对象池,命中时从池里取一个Text或TMP_Text,设置位置和数值,向上飘完回收。
public class ComboCounter : MonoBehaviour { public float resetTime = 2.0f; public TMP_Text comboText; private int count; private float lastHitTime; public void OnHit() { count++; lastHitTime = Time.time; comboText.text = count.ToString(); } void Update() { if (count > 0 && Time.time - lastHitTime > resetTime) { count = 0; comboText.text = ""; } } }逻辑说明:resetTime是连击窗口,ACT常用1.5到2.5秒,重击可单独延长。comboText建议放在Canvas的独立层,避免频繁重建整个UI。伤害数字对象池的容量按同屏最大命中数预估,近战Demo设20到30个足够;每次回收时重置颜色和动画状态,否则下一次取出会带着上一次的透明度。
5. 从可玩Demo到可复现源码:Profiler排错、打包与版本记录
5.1 用Profiler定位ACT掉帧:动画与物理查询
ACT掉帧常见三处:Animator开销、物理查询、GC分配。打开Profiler,看Animator.Update是否超过2ms,如果角色和敌人共用多层Animator,检查是否每层都在每帧求值;Physics.OverlapBox在攻击帧集中调用,数量多时看Physics.Processing峰值;GC Alloc里如果每帧有Collider[]分配,把OverlapBoxNonAlloc或OverlapSphereNonAlloc换上去,并缓存数组。伤害数字和血条不要每次命中都Instantiate,对象池能去掉大部分GC。
5.2 打包前必须检查的序列化与资源引用
| 检查项 | 常见错误 | 处理 |
|---|---|---|
| ScriptableObject引用 | 打包后配置丢失 | 放进Resources或Addressables,场景内直接引用 |
| Animator状态名 | 代码硬编码字符串拼错 | 用Animator.StringToHash缓存 |
| Layer与Tag | 敌人层未加入LayerMask | 打包前在PlayerSettings确认层索引一致 |
| Input System | 旧Input与新Input混用 | 统一Active Input Handling,避免打包后无输入 |
| 动画事件 | 函数名重构后丢失 | 重构前搜一遍AE_前缀,或改用StateMachineBehaviour |
5.3 让论文和源码可复现的版本记录方式
Unity3D工程最怕“在我机器上能跑”。把Unity版本、Git commit、Layer表、关键参数导出成build_info.json,跟可执行文件放同一目录;源码里所有ScriptableObject的默认值用一份DefaultConfig资产兜底,避免换机器后引用变空。动画事件的函数名和攻击判定帧用一张attack_frame.csv记录,改动画时同步更新,比在文档里写“使用最新版”更容易定位问题。
本文还有配套的精品资源,点击获取