简介:超级英雄飞行题材的3D动作游戏Unity项目源码,基于C#开发,支持Unity 2022.1.6f1及以上版本。项目围绕开放城市环境中的飞行、跳跃、拳击、踢击与射击等操作展开,玩家需完成不同层级的救援与治安任务,适合希望学习Unity角色控制、动作状态机、任务系统及移动端游戏架构的中高级开发者参考。压缩包共2000个文件,包括600余个asset资源、30余个C#脚本、14个prefab预制体、9个shader着色器、27个wav音效以及多张png贴图,另有aar/jar等安卓依赖库用于广告与基础服务接入,整体约60.28MB。资源附带AppLovin等广告SDK相关配置,可直接观察商业化Unity项目的工程组织方式,也便于在此基础上扩展玩法或更换表现层。目前已有124人学习下载,适合作为飞行模拟与超级英雄题材的完整项目范本。
1. Superpower Flying Hero 在解决3D飞行动作的什么问题
Superpower Flying Hero 是 Unity 上一套典型的“超级英雄飞行 + 3D 动作”源码工程,C# 脚本贯穿了角色控制、战斗逻辑和表现层的主链路。把标题拆开看:superpower 对应技能系统和数值设计,flying 决定俯仰、滚转和六自由度移动,hero 则落在第三人称视角、打击感和受击反馈上。三者缺一个,飞行就只是“会跳的普通动作游戏”。
先明确一个前提:这里不预设你拿到了某个具体版本号的完整工程,只顺着这条技术主线讲一套可复现的落地方案——飞行动作游戏和地面动作游戏在物理选型、命中判定、相机平滑和构建发布上各自差在哪里,参数怎么设、C# 代码怎么写、坑在哪里。适合正在做 Unity 飞行原型,或准备把地面 Demo 改成空战玩法的开发者直接参照。
2. Superpower Flying Hero 的飞行控制实现:Rigidbody、推力与相机姿态
2.1 从 CharacterController 到 Rigidbody 的选型差异
飞行角色第一件事是放弃地面动作游戏常用的 CharacterController。CharacterController.Move 按帧驱动控制器滑动,自带碰撞响应,但本质是一个没有速度积分、没有外力堆叠的胶囊体。空中英雄需要的是持续受推力影响、能被打飞硬直、能在惯性中保留速度方向同时又可以快速转向改变运动趋势,这些恰好都在 Rigidbody 的能力边界内。
常见做法是给英雄挂 Rigidbody,把 useGravity 设为 false,然后在 FixedUpdate 里直接改写 velocity 或按需 AddForce。这里有一个明确的分水岭:追求物理可信度用 AddForce + Drag,追求操作响应直接写 velocity。飞行动作游戏几乎都选后者,原因是手柄摇杆推多少就要有多少响应,物理力累计在帧间有可见滞后,玩家会感觉“摇杆灌了水”。“Superpower Flying Hero 的手感立体感,很大程度就看这句 velocity 写入是否干净。
Rigidbody 上还要处理约束:把 rotation 的 X 轴和 Z 轴冻结掉,只允许绕 Y 轴旋转,防止飞行中被击中时角色在空中无规则翻转。飞行时的俯仰和侧倾要用代码叠加在旋转上,而不是依赖物理碰撞产生,否则一次撞到敌人就会导致视角天旋地转。
2.2 最小可用的飞行控制 C# 实现
下面这段 C# 脚本是飞行动作游戏最常见的控制骨架,包含输入采样、加速度插值、垂直升降、侧倾姿态和高速冲刺,可以直接放到 Unity 里跑起来再逐项调参数:
using UnityEngine; [RequireComponent(typeof(Rigidbody))] public class FlyingHeroController : MonoBehaviour { [Header("飞行参数")] public float maxSpeed = 18f; // 水平最大飞行速度 public float acceleration = 12f; // 水平加速度 public float verticalSpeed = 9f; // 垂直起降速度 public float boostMultiplier = 2f; // 冲刺倍率 public float bankAngle = 25f; // 滚转倾斜角 [Header("相机")] public Transform cameraPivot; private Rigidbody rb; private Vector3 currentVel; void Awake() { rb = GetComponent<Rigidbody>(); rb.useGravity = false; rb.interpolation = RigidbodyInterpolation.Interpolate; rb.collisionDetectionMode = CollisionDetectionMode.Continuous; } void FixedUpdate() { // 输入采样:左手柄摇杆与跳跃键(垂直升降) float h = Input.GetAxis("Horizontal"); float v = Input.GetAxis("Vertical"); float up = Input.GetAxis("Jump"); Vector3 camForward = cameraPivot.forward; Vector3 camRight = cameraPivot.right; camForward.y = 0f; camRight.y = 0f; camForward.Normalize(); camRight.Normalize(); // 期望速度:方向以相机为基准,大小来自摇杆 float speedMul = Input.GetKey(KeyCode.LeftShift) ? boostMultiplier : 1f; Vector3 desiredVel = (camForward * v + camRight * h) * maxSpeed * speedMul; desiredVel.y = up * verticalSpeed; // 水平和垂直分别按加速度向目标收敛 currentVel.x = Mathf.Lerp(currentVel.x, desiredVel.x, Time.fixedDeltaTime * acceleration); currentVel.z = Mathf.Lerp(currentVel.z, desiredVel.z, Time.fixedDeltaTime * acceleration); currentVel.y = Mathf.Lerp(currentVel.y, desiredVel.y, Time.fixedDeltaTime * verticalSpeed); rb.velocity = currentVel; // 姿态:朝速度方向看,再叠加左右侧倾 float bankTarget = -h * bankAngle; Quaternion targetRot = Quaternion.LookRotation(rb.velocity.normalized, Vector3.up); targetRot *= Quaternion.Euler(0f, 0f, bankTarget); rb.rotation = Quaternion.Slerp(rb.rotation, targetRot, Time.fixedDeltaTime * 5f); } }代码逻辑说明:水平方向用 Mathf.Lerp 向期望速度收敛而不是直接赋值,目的是让角色从静止到满速有加速过程;Lerp 的系数用 Time.fixedDeltaTime 乘以 acceleration,保证 50Hz 物理帧率下行为一致。垂直方向把 Jump 轴映射到 y 速度,按住上升、松开由外部重力或下压逻辑接管。姿态部分用速度方向做 LookRotation,再乘一个绕本地 Z 轴的侧倾角,转弯时角色会像飞机一样压坡,视觉反馈比平移感强得多。
2.3 关键参数的可调窗口与相机跟随
C# 数字和手感是直接绑定的。maxSpeed 决定“快不快”,acceleration 决定“跟不跟手”。实际工程里的合理范围大致如下:
| 参数 | 建议范围 | 表现特征 |
|---|---|---|
| maxSpeed | 12–20 | 低于 10 像跑步,高于 25 碰撞检测压力陡增 |
| acceleration | 8–15 | 低于 6 漂移感明显,高于 20 几乎无惯性 |
| verticalSpeed | 0.5–0.7 倍 maxSpeed | 垂直过快容易丢失地面参照物 |
| bankAngle | 20–35 度 | 超过 40 度侧倾影响瞄准视野 |
相机是飞行手感的另一半。地面动作游戏相机只绕 yaw 和 pitch 旋转,飞行是六自由度,相机不处理平滑,急转画面会剧烈抖动。推荐“快速跟随 + 慢速回正”策略,用 SmoothDamp 控制跟随目标:
void LateUpdate() { Vector3 lookTarget = target.position + target.velocity.normalized * 3f; Vector3 camForward = Vector3.SmoothDamp( transform.forward, (lookTarget - transform.position).normalized, ref velRef, 0.08f); transform.rotation = Quaternion.LookRotation(camForward, Vector3.up); }smoothTime 取 0.08 到 0.12 之间比较合适:低于 0.05 相机像焊死在角色身后,急转会直接甩画面;高于 0.15 视角回摆明显,玩家会感觉角色天旋地转但画面没跟上。
提示:飞行速度增加时玩家需要更宽视野来判断方向,常见做法是做一个 speedToFov 曲线,0 到 40m/s 把 FOV 从 60 线性推到 85。Unity 里动态改 camera.fieldOfView 几乎没有性能成本,但手感提升非常明显。
3. Flying Hero 的3D战斗模块:状态机、命中判定与打击反馈
3.1 输入事件到技能状态的战斗数据流
飞行动作战斗和地面最大的区别是:每个攻击动作都要能在任意姿态使用,并且产生位移判定。传统地面动作游戏用严格状态机锁住移动,到空中就行不通,因为角色不会“站在地面上”等玩家按完一套连招。
常见做法是用一个轻量的技能状态枚举配 Animator 分层:上半身跑攻击动画,下半身继续飞。攻击输入只改状态枚举和动画参数,不直接控制位移,战斗表现层与飞行层互不阻塞。
public enum HeroAction { IdleFly, // 自由飞行 MeleeA, // 普攻第一段 MeleeB, // 普攻第二段 DashAttack, // 空中急袭:位移 + 伤害 SkillQ // 远程能量弹 }状态切换逻辑集中在 InputHandler:
void Update() { if (Input.GetButtonDown("Fire1") && action == HeroAction.IdleFly) { action = HeroAction.MeleeA; animator.SetTrigger("MeleeA"); } else if (Input.GetButtonDown("Fire1") && action == HeroAction.MeleeA && animator.GetCurrentAnimatorStateInfo(0).normalizedTime > 0.3f) { action = HeroAction.MeleeB; animator.SetTrigger("MeleeB"); } }normalizedTime 大于 0.3 才允许接下一段,这是动作游戏里常用的“可取消窗口”。窗口太小连招按不出来,窗口太大玩家会在第二段输入时反而丢帧。0.3 到 0.45 是近战手感的常见甜点区。
3.2 飞行中的命中判定与攻击判定体积
命中检测绕不开的问题是帧率不稳时武器扫过的动作会“跳帧穿怪”。地面动作游戏普遍用 OverlapSphere 解决:在攻击激活帧做球形检测,命中即结算。但飞行状态角色会把整个碰撞体带离地面,攻击起点和终点都在运动,单一球体做不到有效覆盖。
更可靠的是 OverlapCapsule——起点为角色胸口,终点为武器挥击方向前端的点,radius 为攻击半径,一次性覆盖一套挥拳轨迹:
bool TryHitInArc(float range, float radius, out Collider[] hits) { Vector3 origin = transform.position + Vector3.up * 1.2f; Vector3 end = origin + transform.forward * range; hits = Physics.OverlapCapsule(origin, end, radius, targetLayer); return hits.Length > 0; }使用时按攻击段位不同填 range 和 radius。飞行动作游戏里还有一个地面不会遇到的高发坑:命中判定发生了,但敌人没有受击表现,表现为“打中了不结算”。原因通常是敌人 Animator 的受击 Layer 被移动或飞行 Layer 覆盖了,需要在敌人 Animator 上单独建一层 HitReact,并把层权重设成“不受其他层遮挡”。
| 攻击类型 | Range | Radius | 帧数建议 |
|---|---|---|---|
| 普攻近战 | 2.0–2.5 | 0.8–1.0 | 第 4 帧激活,第 8 帧结束 |
| 空中急袭 | 3.5–4.5 | 1.0–1.2 | 第 3 帧激活,伴随位移 |
| 远程能量弹 | 判定走子弹 | 0.4 | 生成后逐帧位移 |
提示:激活帧在动画事件里打标记比在 Update 里硬算更稳妥,Animator 编辑器中把攻击激活帧标到动画对应位置,逻辑和表现同步成本最低。
3.3 打击反馈:镜头震动、短时慢动作与特效触发器
打击反馈决定“超级英雄挥拳”是否成立。纯动画无法传递冲击力,工程里普遍是“三件套”:镜头震动、短时慢动作、命中粒子。这三者在 Unity 里都不需要插件。
镜头震动可以直接在 LateUpdate 里叠加 Perlin 噪声向量并随时间衰减:
float shakeTimer = 0f; Vector3 shakeOffset; void LateUpdate() { if (shakeTimer > 0f) { shakeTimer -= Time.deltaTime; float p = shakeTimer / shakeDuration; shakeOffset = Random.insideUnitSphere * shakePower * p; transform.localPosition += shakeOffset; } }震动时长推荐 0.1–0.15 秒,幅度不超过 0.5,否则镜头会从“冲击感”变成“画面失误”。慢动作是另一个层面的事:不要动 Time.timeScale,飞行动画、输入和相机都在同一时间尺度下,降速到 0.3 倍 0.15 秒后立刻回 1.0,线性插值回去,实际手感比直接跳变好非常多。
特效与逻辑必须解耦:攻击命中是一个数据事件,特效、音效、震动都是这个事件的监听者。C# 的 event 或 UnityEvent 都能承担这件事。别把特效播放写在同一方法里——一旦后续想换技能特效或加命中音效,改动会顺着调用链上下翻。
4. 把 Unity 项目源码工程化:C# 分层、场景参数与构建链路
4.1 飞行动作游戏的目录设计与 ScriptableObject 数值表
拿到 Unity 源码工程,第一件事不是打开主场景而是看目录分层。飞行动作游戏涉及玩家、战斗、AI、UI、特效多个子系统,脚本互相引用无法避免,但至少在 C# 层面按职责分层:
Assets/ Scripts/ Core/ # 单例、事件系统、全局管理器 Player/ # 飞行控制、相机、技能输入 Combat/ # 命中检测、伤害结算、状态机 Enemy/ # 敌对 AI、追踪与攻击 Effects/ # 特效触发器、震屏、音频 Prefabs/ Scenes/ Configs/ # SkillConfig、AttackData这套结构的核心思想是:表现层(特效音频)和逻辑层(伤害与状态)分开,美术资源不进 C# 脚本目录,数值配置全部走 ScriptableObject。飞行动作游戏技能多、参数杂,硬编码魔法数字后期改一个数值要重构一整个类,不值得。
[CreateAssetMenu(fileName = "Skill_", menuName = "HeroDemo/SkillConfig")] public class SkillConfig : ScriptableObject { public string skillName; public float damage; public float cooldown; public float flyAttackRadius; public GameObject hitFxPrefab; }这种 Unity 内建的资源扩展机制可以直接在 Project 面板右键创建技能配置,策划改伤害、冷却、特效引用,不用碰代码。
4.2 飞行场景必须调的物理、渲染与阴影参数
飞行动作游戏是开放空旷场景,相比室内环境,阴影、雾效和天空盒的差异会直接冲进视野。下面这张表是工程里最常见的必调清单:
| 参数 | 建议值 | 原因 |
|---|---|---|
| Fixed Timestep | 0.02 | 50Hz 物理更新,飞行手感最稳 |
| Rigidbody Collision Detection | Continuous | 高速飞行防止穿墙穿怪 |
| 阴影距离 | 30–60m | 超过 60 阴影闪烁且无收益 |
| Shadow Cascades | 2 级 | 级联太多在开放场景 GPU 压力大 |
| Anti Aliasing | 4x MSAA | 移动端降为 2x |
| 天空盒反射 | 关掉实时反射 | 飞行场景天空占画面比例大 |
频繁出现的“unity阴影问题”在飞行游戏里通常是阴影距离没调:主角飞到高空后地面阴影直接消失,画面像漂在虚空中;把阴影距离拉到 60–80 并适度降低阴影分辨率,能保留视觉深度又不吃掉移动端带宽。
4.3 构建链路:从编辑器到 WebGL 与移动端的典型坑
构建飞行游戏最常见的坑存在于是“运行好好的,构建一跑就崩”。IL2CPP 构建时,C# 里通过反射或字符串调用的类会被裁剪掉,编辑器走 Mono 不触发,但构建到 WebGL 或移动端后在特定路径上报 MissingMethodException。
解法是给运行时反射类打上[Preserve]特性,或者在 Player Settings 里关闭过激托管代码裁剪。真机出问题时先切 Mono 后端跑一遍,能快速定位是否裁剪导致。
WebGL 构建还有一个高频问题是普通平台的存档方案不适用:浏览器里 Application.persistentDataPath 写入背后是 IndexedDB,构建模板若未正确挂载 IDBFS,运行时会报写入失败。处理策略是降低存档频率、分批写入,不要在 Update 里每帧写 PlayerPrefs。
C# 层还有一个容易被忽视的性能点:循环里做字符串拼接或每帧 Debug.Log 输出调试数据,会在手机上卡到掉帧。调试日志先写到内存队列里攒批落盘,比每帧调用 Debug.Log 稳得多。飞行动作游戏本身每帧要同步飞行速度、技能冷却、敌人状态,UI 刷新也要做更新缓存,而不是每帧重新 setActive。
命令行构建可以放进静态方法,方便 CI 或本机批量出包:
public static class BuildScript { public static void BuildWebGL() { var opt = new BuildPlayerOptions { scenes = new[] { "Assets/Scenes/Main.unity" }, locationPathName = "Build/WebGL", target = BuildTarget.WebGL, options = BuildOptions.None }; BuildPipeline.BuildPlayer(opt); } }调用方式:Unity.exe -batchmode -executeMethod BuildScript.BuildWebGL,把这行放进打包脚本,每次出包的场景列表和参数都固定,省掉手动勾选场景的遗漏风险。
5. 用速度数据曲线验证 Flying Hero 的飞行手感
最后这一节给一个能直接落地的手感调优闭环。飞行动作游戏“手感好”本质上就是速度曲线的收敛形状符合直觉。设 maxSpeed 为 18、acceleration 为 12,那么从零速到 90% 最大速度的时间约 0.55 秒;曲线是前陡后缓的指数式收敛,而不是线性直线。线性收敛会让玩家觉得“加速没有力量感”,指数式收敛才有空气被推开的体验。
把实际角色速度按时间采样后拉出来和参考曲线对比,比反复进 Play Mode 试玩可靠得多。写一个小工具在 FixedUpdate 里把速度和耗时写入 CSV:
using System.IO; private StreamWriter sw; void Start() { // 编辑器模式可直接写 DataPath,构建到端侧后建议换到 persistentDataPath sw = new StreamWriter(Application.persistentDataPath + "/vel_log.csv"); } void FixedUpdate() { sw.WriteLine(Time.time + "," + rb.velocity.magnitude); }用 Excel 或 Python 把 CSV 画出来,和参考指数曲线v(t) = 18 * (1 - e^(-t/0.6))叠在一起看。实际曲线腰线比参考明显高,说明 acceleration 太大,速度瞬间到顶,玩家会感觉角色“没有重量”;曲线一直达不到参考,说明有隐藏阻力在拖拽,常见来源是 Rigidbody 默认 drag 没清零,或者代码里还有别的 Transform 层级在同步位移。
把 maxSpeed、acceleration、verticalSpeed 暴露成 Slider 就能在编辑器里实时拖参数,Unity UI 的 Slider.onValueChanged 绑到脚本字段,进 Play Mode 后边飞边拉,直到速度曲线落在参考曲线 ±15% 带宽内。飞行、连招、镜头三者的手感问题,绝大多数都能从这张速度曲线图上定位出来。
本文还有配套的精品资源,点击获取