ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

Unity 2.5D射击游戏源码拆解:动画状态机与战斗系统实现

Unity 2.5D射击游戏源码拆解:动画状态机与战斗系统实现 简介基于Unity 2D打造的士兵突击射击游戏完整项目源码采用C#编写适配Unity 5.3.4f1及以上版本。游戏以第二次世界大战空战为背景包含步兵突击与战机对战玩法还原苏联、德国、英国等多国任务关卡如不列颠之战、斯大林格勒之战等并设计积分、金币、经验与特殊技能解锁系统可体验历史战役推进过程。资源包共2003个文件约56.55MB主要包含.cs逻辑脚本、.prefab预设、.anim动画、.mat材质、.fbx模型、.png贴图及.wav音频等目录结构清晰便于检索与二次开发。目前已有152人学习下载适合Unity初中级开发者研究2D射击游戏的战斗流程、敌人AI、动画状态机和UI系统也可作为历史题材游戏关卡设计的参考模板。通过源码与配套素材对照分析可快速掌握角色控制、敌人生成、任务结算等完整实现思路。1. 别被“士兵突击”这个标题骗了这是一套能跑的 2.5D 战役射击框架在下载站看到“士兵突击”这个包别真当成某个商业游戏的完整克隆。解压后满屏的 .anim 文件enemy_melee_gongji、cha_lose、cha_1_aim_jiguanqiang 这些按前缀命名的动画资产才是这套 Unity 2D 射击项目源码的骨架。整套资源的本质是正交摄像机下用排序层叠出纵深的 2.5D 场景敌人靠动画事件驱动攻击判定玩家走一套完整的得分、金币、经验与连击技能循环。适合两类人一类是刚把 C# 语法学完、想在一份能跑的工程里看懂动画状态机和脚本怎么接的初学者另一类是做俯视角或横版射击但手感总差一点、想搞明白攻击判定如何严格对齐动画帧的熟手。下面按我拆这份工程时的顺序把动画状态、场景搭建、结算循环和发布排错串一遍。2. 从 .anim 文件反推敌人状态机近战士兵从站到死一共几个状态2.1 资源包里的动画命名规则前缀即状态这份源码里所有敌方近战单位的动作集中在enemy_melee_开头的动画组里。素材作者用的是汉语拼音映射这个命名习惯在中文 Unity 素材包里很常见美术同事一眼就能看懂但写代码的人必须先在纸面上做一层映射再动手建 Animator Controller。对照资源包里的实际文件可以整理出下面这张表.anim 文件拼音映射状态机含义enemy_melee_zhanli.anim站立Idle待机enemy_melee_xingzou.anim行走Walk巡逻移动enemy_melee_gongji.anim攻击Attack 主攻击enemy_melee_gongji2.anim攻击2Attack 连击第二段enemy_melee_gongji3.anim攻击3Attack 连击第三段enemy_melee_shoushang.anim受伤Hurt受击打断enemy_melee_siwang.anim死亡Dead倒地销毁注意攻击动作有 gongji、gongji2、gongji3 三套这通常意味着敌人可以连击或者会随机从三套动作里选一套出手。建议在写代码之前先把这三段动画拖进同一个播放器逐帧过一遍确认每一段的攻击判定帧在什么位置。gongji3 的判定帧往往比 gongji 晚 3 到 5 帧如果统一用一个攻击事件去配打出来的手感就是“刀已经挥过去了伤害还没判”这个细节后面会细说。2.2 用 C# 枚举映射动画状态避免字符串比较把动画剪辑拖进 Animator Controller 后常见做法是关掉所有状态的 Has Exit Time让状态切换完全交给代码控制。我的习惯是建一个与动画组一一对应的枚举然后通过 Animator.SetInteger 驱动状态转换public enum EnemyMeleeState { Idle 0, // enemy_melee_zhanli Walk 1, // enemy_melee_xingzou Attack1 2, // enemy_melee_gongji Attack2 3, // enemy_melee_gongji2 Attack3 4, // enemy_melee_gongji3 Hurt 5, // enemy_melee_shoushang Dead 6 // enemy_melee_siwang }Animator 侧只需要两个参数一个是 int 类型的 State负责主状态流转一个是 int 类型的 AttackIndex取值范围 1 到 3表示当前播放的是第几段攻击。所有状态转换的 Transition Duration 设到 0.05 以下Hurt 和 Dead 的优先级必须高于攻击这样敌人普攻动画放到一半被打断时能立刻切到受击动作或死亡动作打击感才跟得上。这里有一个性能层面的考虑不要在 Update 里用字符串拼isWalking、isAttacking这类 bool 参数名去控制动画敌人数量一多字符串哈希会产生不必要的开销。枚举转 int 再交给 SetInteger是最直接、后续加新状态也容易扩展的做法。真遇到需要把伤害数字和动画进度挂钩时再配合后面要讲的动画事件代码会干净很多。2.3 攻击判定必须用动画事件不要自己写计时器这是整个敌人模块里最重要的一步。三套 gongji 动画虽然在文件管理里看不出差别但打开 Animation 窗口能看到它们的时间轴里已经埋好了事件轨。常见做法是在刀刃前伸到最远的那一帧插入一个 Animation Event函数名统一叫Animation_AttackHitpublic void Animation_AttackHit() { Collider2D hit Physics2D.OverlapCircle(attackPoint.position, attackRange, playerLayer); if (hit ! null) { hit.GetComponentIDamageable().TakeDamage(attackDamage); hit.GetComponentRigidbody2D().AddForce( new Vector2(attackKnockback * facing, 0), ForceMode2D.Impulse); } }为什么不用 Update 里的计时器去碰判定因为动画事件的触发帧完全由动画资源决定策划或美术想调手感时只需要拖动动画里的关键帧代码不用重新编译。反过来如果把判定逻辑写死在 Update 里每改一次攻击帧就要连代码一起返工。这套设计在多人协作的项目里尤其省事。参数说明attackPoint是挂在敌人武器末端的空物体代表攻击判定圆心attackRange是半径按像素换算成 Unity 单位后一般填 0.5 到 0.8playerLayer是玩家所在的 Physics Layer这样近战士兵之间的挥砍不会互相触发判定只有实实在在砍到玩家才进伤害逻辑。三套攻击动画的伤害值我的建议是做成数组配置Attack1 伤害 10Attack2 伤害 8 但附带击退Attack3 伤害 15 但前摇更长这样三套动画都有出场价值玩家也能观察出攻击节奏的差异。3. 2.5D 场景的搭建顺序摄像机、排序层与士兵移动判定3.1 正交相机加 Y 轴纵深2.5D 感的全部来源资源包里描述的“2.5D”并不是什么复杂技术核心就一句话摄像机保持正交投影场景里的墙体、士兵、前层障碍物按 Y 坐标的升降来产生纵向深度。玩家控制角色向上走时角色在屏幕上移动的不只是 X 轴还有 Y 轴于是自然产生“往画面深处走”的错觉。在 Unity 里复现这个效果只需要按下面这张表设置主摄像机摄像机参数推荐值说明ProjectionOrthographic正交投影2D 素材不会产生透视变形Size5 到 6数值越小可同时看到的游戏区域越小人物越放大Clipping Planes0.3 到 302D 场景如果跨度大Near 太小会裁掉远处物件Pixel Perfect开启配合 100 PPU 的素材防止像素边缘模糊场景图层的做法是建四个 Sorting Layer从上到下依次叫 Front、Actor、Level、BackGround。墙体地板放 Level所有可交互的士兵和玩家放 Actor前景遮挡物放 Front整体背景放 BackGround。同一个 Sorting Layer 内部再用 Order in Layer 细分比如同为 Actor 层的玩家优先于敌人显示时玩家的 Order in Layer 设成 10敌人设成 5。判断叠放顺序对不对最省事的办法是运行时打印每个 SpriteRenderer 的bounds.min.y检查对象之间的遮挡关系是否与期望一致。3.2 敌人的移动判定用 Kinematic 刚体扫边界近战士兵要在地面上巡逻最直接的做法是挂 Rigidbody2D把 Body Type 设为 Dynamic、Gravity Scale 设 0然后用 Transform 移动。实测这样做在纯平地上没问题但场景里一旦出现斜坡或台阶Dynamic 刚体会因为碰撞体穿透产生抖动。我的常规做法是换成 Kinematic 刚体加 MovePosition并手动维护巡逻边界public void Patrol() { if (transform.position.x leftBound || transform.position.x rightBound) { facing -facing; Vector3 scale transform.localScale; scale.x Mathf.Abs(scale.x) * facing; transform.localScale scale; } rb.MovePosition(rb.position new Vector2(facing * walkSpeed * Time.fixedDeltaTime, 0)); }leftBound 和 rightBound 是世界坐标下的巡逻边界不是路点物体图纸上定好位置后直接填进 Inspector 就行。MovePosition走物理管线和场景里其他碰撞体的检测不会互相打架这是它优于transform.position直接改坐标的关键原因。转身时不要翻转 Rigidbody2D 的速度向量直接改 Transform 的 scale.x 符号即可如果动画里有朝向相关的 Blend TreeAnimator 会自动根据 scale.x 的方向调整播放。进入攻击范围这一步用 Physics2D.OverlapCircle 做玩家检测就够放在 Update 里没问题。检测半径不要超过 1.2否则近战兵在视觉上还没贴到玩家就会出刀整个 2.5D 纵深感会立刻失真。3.3 机关枪瞄准动画的角度换算与像素校准cha_1_aim_jiguanqiang.anim是玩家举枪瞄准的动画它和鼠标方向是联动的。正交相机下鼠标的世界坐标先转出来再和角色枪口的父节点做差值算出角度Vector3 mouseWorld Camera.main.ScreenToWorldPoint(Input.mousePosition); mouseWorld.z 0f; float rad Mathf.Atan2( mouseWorld.y - aimPivot.position.y, mouseWorld.x - aimPivot.position.x ); aimAnimator.SetFloat(AimX, Mathf.Cos(rad)); aimAnimator.SetFloat(AimY, Mathf.Sin(rad));这里传给 Animator 的不是角度值而是方向向量 AimX、AimYBlend Tree 用 2D Simple Directional 模式做平滑混合这样角色举枪从左上转到右上时动画过渡是连续的不会跳切。注意ScreenToWorldPoint返回的 Z 值在正交相机下没有意义一定要手动归零否则向量差值会算上深度轴角度全乱。2.5D 游戏里有一个具体坑角色站在平台上鼠标点到的地面可能在更远处直接拿鼠标世界坐标算角度会看起来“枪口抬得不够高”。常见做法是把鼠标射线打到地面所在的碰撞器上取交点再拿交点和角色枪口算差向量。配合前面 3.1 说的 Pixel Perfect 开启与 100 PPU 素材准星和枪口才不会出现半个像素的偏移这个校准规则和 2D 像素游戏素材的标准是一致的。4. 战斗结算循环得分、金币、经验与连击技能的完整数据流4.1 GameManager 用事件解耦敌人死亡与结算逻辑资源摘要里提到“每个被杀死或摧毁的敌人都会获得分数、金币和经验”这三项数据如果直接散落在各个敌人脚本里写后期加新敌人会非常痛苦。常见做法是让 GameManager 成为全局唯一的结算入口敌人死亡只负责广播一条事件不关心自己值多少分public event System.Actionint, Vector3 OnEnemyKilled; void Die() { OnEnemyKilled?.Invoke(enemyId, transform.position); Destroy(gameObject, 0.6f); }GameManager 在 OnEnable 里订阅这条事件收到后查一张配置表。dropTable 用Dictionaryint, EnemyDrops初始化enemyId 是每个敌人 Prefab 上配好的唯一编号EnemyDrops 是一个结构体包含 score、gold、exp 三个字段。这样做的好处是以后新增兵种只需要在配置表里多一行GameManager 的代码一行不用改。private float HandleEnemyKilledd(int enemyId, Vector3 pos) { EnemyDrops drops dropTable[enemyId]; AddScore(drops.score); AddGold(drops.gold); AddExp(drops.exp); return drops.score; }没有包含敌人模型的死亡掉落物生成也可以在这个回调里统一处理把掉落物的 Prefab 引用同样挂在 EnemyDrops 配置上。注意字典的 key 一定不要用字符串敌人名场景里同名敌人多字符串查表有哈希开销用 int 枚举做 key后续出配置表也更干净。4.2 三种数值的不同刷新节奏以及 UI 卡顿的根因得分、金币、经验虽然都是加数值但刷新节奏完全不一样。经验是按击杀阶梯式累加金币是击杀瞬间必须立刻跳数字得分则要配合连击加成做平滑滚动。很多项目卡顿的根因就在这一步在 Update 里每帧给 Text 赋值C# 字符串拼接在敌人密集的时候会产生大量临时对象GC 一触发帧率立刻掉下来。我一般会把每项 UI 都做成独立组件用脏标记控制刷新int score; int displayedScore; bool scoreDirty; void AddScore(int value) { score value; scoreDirty true; } void Update() { if (scoreDirty) { scoreText.text score.ToString(); displayedScore score; scoreDirty false; } }displayedScore 和 score 分开是一种常见设计前者是屏幕上正在显示的数后者是逻辑上的真实值。想要“数字滚动”的效果就让 displayedScore 每帧朝 score 逼近比如差值小于 5 时直接对齐否则每次移动差值的 1/8。gold 组件适合即时刷新exp 组件适合做成进度条型三个组件各自独立互不阻塞。这样敌人同时死亡十个也只会在同一帧触发一次 UI 赋值比在 Update 里无条件刷新省掉大量没必要的字符串操作。4.3 连击条纹与特殊技能的阈值映射摘要里提到的“每获得一条条纹您就会获得新的特殊技能”对应的就是 Streak 连击系统。玩家连续击杀敌人连击数加一超过窗口时间没杀敌连击清零。得分时额外乘以连击加成连击段数越高加成越大。这个系统的核心是一张阈值表连击数技能效果实现位置3射速提升 15%修改玩家攻击冷却时间5子弹穿透 1修改子弹 Layer 与穿透计数7全屏预警 2 秒触发视觉提示并暂停敌人移动阈值表在代码里直接用 int 数组存因为这里只有纯读取操作C# 数组访问比 List 少一层间接寻址连续命中时性能更可靠int[] streakThresholds { 3, 5, 7 }; int[] streakSkillIds { 101, 102, 103 }; for (int i 0; i streakThresholds.Length; i) { if (comboCount streakThresholds[i]) { EventBus.Trigger(streakSkillIds[i]); } }注意连击窗口的设计直接决定手感。窗口太短普通玩家打不出第 7 连的技能窗口太长高手会一直保持高倍率导致数值失控。我一般把窗口设在 3 到 5 秒并且把窗口长度也放进配置表这样策划可以单独调每一档连击的持续时间。触发技能时不要直接在 ComboSystem 里改玩家参数而是广播技能事件由玩家控制器决定如何响应避免连击模块和战斗模块耦合成一团。5. 版本兼容与发布排错老项目换新引擎时的四项必查5.1 Active Input Handling 与 2D 素材导入校准资源说明标明支持 Unity 5.3.4f1 或更高版本这类老工程在 2021 之后的版本打开时第一道坎是输入系统。5.3 时代默认写法是Input.GetAxisRaw新版工程如果 Project Settings 里的 Active Input Handling 被切到只支持 Input System Package老代码会直接找不到 API。建议先设为 Both保证新旧输入代码同时可用。2D 素材层面旧项目的 Sprite 通常按 100 PPU 导入新工程模板也默认 100但要检查 Sprite Mode。如果某些角色素材在旧工程里被切成了多张新引擎重新导入可能把模式改回 Single像素风素材的刀光、枪口火焰会用错图集。一个可靠的兜底做法是把像素风素材的 Filter Mode 统一为 Point避免放大时边缘模糊这个在校验准星和枪口位置时尤其明显。5.2 2D 阴影与 World UI 遮挡“Unity 阴影问题”在 2D 项目里的表现和 3D 完全不同。正交相机下如果对 Sprite 开实时阴影边缘锯齿会非常扎眼。更实用的做法是给每个角色脚下挂一张独立的椭圆形阴影 Sprite跟随角色移动Sorting Order 比角色低一位。这个阴影不参与光照计算看起来干净开销也小。世界空间 Canvas 容易被子物体的 SpriteRenderer 遮挡。解决方法是把 Canvas 的 Sorting Order 设成一个高于所有场景层的数值同时 Render Mode 保持 World Space这样人物即使走进墙体后方UI 血条依然显示在最上层。5.3 IL2CPP 产物理解与 WebGL 存档写入发布后如果看到输出目录里有一个较大的 GameAssembly.dll说明构建使用的脚本后端是 IL2CPP。这个文件是 C# 先编译成 C、再被对应平台编译器生成的最终产物不是 .NET 托管的 DLL所以换平台或改版本后必须重新构建不能直接拷贝。对于 2D 游戏IL2CPP 比 Mono 在移动端的启动性能更好发布 Android 或 iOS 时建议默认选 IL2CPP。WebGL 平台有一个容易忽略的问题Application.persistentDataPath下的写入在 WebGL 里依赖 IndexedDB配置不当会写入失败。可以先把 GameManager 里的得分、金币、经验、连击数序列化成一个 SaveSlot 类在桌面平台用File.WriteAllText写进 persistentDataPath验证 JSON 往返无误后再针对 WebGL 把写入调用替换成 IndexedDB 的同步逻辑。这个验证按钮 10 分钟就能写好它不保证发布后一定不丢档但至少能把平台差异卡在发布前暴露出来。本文还有配套的精品资源点击获取
返回列表