ARTICLE DETAIL

资讯详情

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

Unity飞行跑酷开发实战:物理控制、关卡生成与性能优化

Unity飞行跑酷开发实战:物理控制、关卡生成与性能优化 简介Dragon Trial龙之试炼是一份基于Unity 2018.2及以上版本的横版飞行跑酷游戏完整项目源码适合Unity游戏开发者学习休闲手游制作与广告变现集成。项目采用点击式操控龙前往目标点射击并收集财富包含换皮友好的资源结构与简洁C#逻辑并已接入AdMob与Unity Ads广告SDK。压缩包共1543个文件含大量png贴图、fbx模型、prefab预制体、mat材质、cs脚本与unity场景等资源层级清晰包体188.7MB便于直接打开工程或按模块替换美术资源。开发者可从中学到移动端优化技巧、慢动作子弹特效、防护罩/磁铁Buff设计、技能升级与皮肤解锁等玩法实现同时通过aar与jar等文件了解Android广告SDK集成结构。目前已有197人学习下载适合想要快速获得可运行跑酷游戏模板或研究飞行射击玩法的初中级Unity开发者。1. 横版飞行跑酷的Unity源码核心不在美术在物理飞行跑酷和地面跑酷最大的区别在垂直轴地面跑酷只需要处理跳跃和落地两个状态飞行跑酷的Y轴永远开放玩家随时能升能降所以判定框设计和输入响应速度的优先级远高于美术资源本身。Dragon Trial 龙之试炼这个标题拆开就是三件事横版卷轴相机、基于物理的飞行控制、持续生成障碍的关卡逻辑全部用C#在Unity里落地。源码的价值不在贴图多细而在把这三件事串起来的那组参数——刚体质量、线性阻力、升降加速度、碰撞判定框尺寸这些才是飞行手感的分水岭。适合两类人读一类是拿完整项目做作品集或毕业设计的新手另一类是写过2D游戏、想搞清楚飞行手感怎么量化、对象池怎么组织、性能瓶颈从哪来的开发者。下文按实际工程里会用的方案讲不抄官方Demo。2. 横版飞行跑酷的移动与物理Rigidbody2D参数与视差滚动2.1 飞行控制的物理模型刚体是唯一可信的位移来源Dragon Trial 这类横版飞行跑酷角色全程在空中没有地面接触也没有跳跃判定物理模型比平台跳跃简单但有个经典陷阱很多人直接改Transform.position结果碰到障碍物时穿透、相机抖动、碰撞时灵时不灵。原因在于Transform位移绕过了物理引擎的积分Unity的碰撞检测基于物理步长采样强行改位置可能在一帧内从障碍物左侧穿到右侧。常见做法是把飞行体挂在Rigidbody2D上用速度赋值而不是AddForce。飞行跑酷追求的是“指哪打哪”的即时响应AddForce会产生惯性累积松开按键角色还要飘一段在需要精确穿缝的玩法里非常致命。给velocity赋值则让速度在下一物理帧直接改变配合线性阻力做减速手感更接近街机飞行游戏。using UnityEngine; [RequireComponent(typeof(Rigidbody2D))] public class DragonFlight : MonoBehaviour { [Header(飞行参数)] public float riseSpeed 6f; // 上升最大速度 public float fallSpeed 4f; // 下降最大速度 public float horizontalSpeed 8f; // 水平前进速度全程恒定 public float inputSmoothing 10f; // 输入平滑系数 private Rigidbody2D rb; private float targetVelocityY; private float currentSmoothY; void Awake() { rb GetComponentRigidbody2D(); rb.gravityScale 0f; // 飞行角色不用重力垂直轴完全交给代码 rb.linearDamping 2f; // 旧版本叫dragUnity 6 改名为linearDamping } void Update() { // 读取输入W/上箭头上升S/下箭头下降 float inputY 0f; if (Input.GetKey(KeyCode.W) || Input.GetKey(KeyCode.UpArrow)) inputY 1f; else if (Input.GetKey(KeyCode.S) || Input.GetKey(KeyCode.DownArrow)) inputY -1f; targetVelocityY inputY 0f ? riseSpeed : (inputY 0f ? -fallSpeed : 0f); } void FixedUpdate() { // 用SmoothDamp做速度过渡避免速度跳变导致画面抖动 float smoothVelocity 0f; currentSmoothY Mathf.SmoothDamp(currentSmoothY, targetVelocityY, ref smoothVelocity, 1f / inputSmoothing); rb.linearVelocity new Vector2(horizontalSpeed, currentSmoothY); } }这段代码里的关键点有三个。第一是gravityScale设成0飞行角色不受重力垂直方向完全交给代码否则会在“上升时抵消重力”上浪费大量调试时间。第二是horizontalSpeed固定为常量跑酷游戏的世界移动速度必须恒定玩家才能建立对障碍物距离的预判水平速度一变后面所有障碍物间距设计全部失效。第三是SmoothDamp只作用于垂直速度水平方向不做平滑这样左右方向即时响应、上下方向有轻微粘滞感玩家会觉得“跟手但不飘”。提示Unity 6 里 Rigidbody2D.velocity 已逐步迁移到 linearVelocity老项目升级时注意API变更。跑酷项目在FixedUpdate内不要写任何会产生GC的代码比如LINQ链式查询或字符串拼接。2.2 视差滚动三层背景让横版飞行有速度感飞行跑酷的“速度感”很大程度来自背景。角色实际飞行速度并不快屏幕上每秒位移有限没有背景参照物玩家根本感觉不到前进。视差滚动的原理是不同层级的背景以不同速度向左移动近景快、远景慢利用运动视差欺骗眼睛。我一般做三层远景的天空云层速度系数0.1-0.2、中景的山脉建筑0.4-0.6、近景的漂浮碎片或云雾0.8-0.95。每层是一个独立Sprite或Tilemap由同一段代码驱动。using UnityEngine; public class ParallaxLayer : MonoBehaviour { public float speedFactor 0.5f; // 相对主场景的移动速度系数 public float backgroundWidth 20f; // 单张背景宽度用于循环拼接 private Transform cam; private float startX; void Start() { cam Camera.main.transform; startX transform.position.x; } void LateUpdate() { // 用相机位置差乘以速度系数实现视差 float camDelta cam.position.x * speedFactor; float x startX camDelta; // 超出半张宽度就回绕实现无限滚动 x Mathf.Repeat(x, backgroundWidth) - backgroundWidth * 0.5f; transform.position new Vector3(x, transform.position.y, transform.position.z); } }核心是Mathf.Repeat它把背景x坐标限制在一张背景图宽度内循环前提是背景图左右可以无缝拼接。做素材时背景图的左右边缘必须内容连续否则每循环一次就能看到一条接缝。另一个细节视差层要放在主相机后方Layer单独设一层避免被后处理或UI误伤。功能验证法把speedFactor调到0背景纹丝不动调到1背景和主场景完全同步飞行时近景拉得比远景快说明视差生效。2.3 相机跟随与垂直边界视野锁定策略横版跑酷的相机通常只跟随X轴Y轴固定或做轻微延迟跟随。Dragon Trial 这类飞行玩法玩家需要上下穿缝如果相机严格锁定角色Y轴升降时屏幕跟着抖很容易晕。我习惯的做法X轴完全跟随Y轴做弹性跟随——角色上升时相机慢半拍跟上同时用Clamp限制Y轴范围保证玩家永远能看到前方一段距离的障碍物。using UnityEngine; public class RunnerCamera : MonoBehaviour { public Transform target; public float followSpeed 5f; public float lookAhead 3f; // 前视距离 public float minY -2f; public float maxY 4f; void LateUpdate() { if (target null) return; // X轴直接跟随目标加前视偏移 float targetX target.position.x lookAhead; // Y轴做延迟跟随钳制上下边界 float targetY Mathf.Clamp(target.position.y, minY, maxY); Vector3 targetPos new Vector3(targetX, targetY, transform.position.z); transform.position Vector3.Lerp(transform.position, targetPos, followSpeed * Time.deltaTime); } }lookAhead是飞行跑酷的关键参数。相机中心偏右给玩家留出“看前方”的空间这个值要按目标平台调竖屏手机和宽屏显示器的可视宽度差异很大。minY和maxY防止玩家飞出屏幕这两个值必须和下一章的障碍物生成范围联动否则会出现“相机看不到障碍物、角色先撞上”的尴尬情况。3. 用C#实现龙之试炼的关卡生成与碰撞判定的关键代码3.1 障碍物生成器区间随机与最小可通行间隙横版飞行跑酷的关卡等于一个从左向右移动的世界加上定时生成的一组障碍物。生成器的核心是“可控随机”不能完全随机否则可能出现连续三个无法通过的关卡不能完全固定否则二周目背板后失去挑战性。控制手段就是两个间距参数水平间距决定反应时间垂直通道宽度决定穿缝精度。using UnityEngine; public class ObstacleSpawner : MonoBehaviour { public GameObject[] obstaclePrefabs; // 障碍物预制体 public float minHorizontalGap 6f; // 障碍物最小水平间距 public float maxHorizontalGap 12f; // 最大水平间距 public float minVerticalGap 3f; // 上下障碍物之间最小垂直通道 private float nextSpawnX; void Start() { // 首个障碍物生成在屏幕右边缘外 nextSpawnX Camera.main.transform.position.x 8f; } void Update() { Camera cam Camera.main; // 相机右边缘 相机x 半屏宽(orthographicSize * aspect) float worldRightX cam.transform.position.x cam.orthographicSize * cam.aspect; if (worldRightX nextSpawnX) { SpawnObstacle(); nextSpawnX worldRightX Random.Range(minHorizontalGap, maxHorizontalGap); } } void SpawnObstacle() { int index Random.Range(0, obstaclePrefabs.Length); // 通道中心高度在正负半个通道宽度内随机 float channelCenterY Random.Range(-minVerticalGap * 0.5f, minVerticalGap * 0.5f); Vector3 pos new Vector3(worldRightX 1f, channelCenterY, 0f); GameObject go PoolManager.Instance.Get(obstaclePrefabs[index], pos, Quaternion.identity); PoolManager.Instance.RecycleLater(go, 10f); } }这个生成器有两个参数决定难度水平间距越小玩家做方向调整的时间越短垂直通道越窄穿缝难度越高。经验值是早期通道宽度大于角色高度的一倍后期压到1.2倍让玩家在“能过但必须精确操作”的边缘反复试探。注意别用Time.time做生成间隔。飞行跑酷的生成必须基于屏幕空间位置驱动因为水平速度恒定基于时间也能做但只要游戏暂停、卡顿或未来加变速机制时间驱动就会错乱。位置驱动永远是对的。3.2 判定框设计碰撞体小于视觉体玩家会感激你飞行跑酷的碰撞判定是口碑分水岭。玩家视角下“擦着过去了”的情况如果因为碰撞体比视觉大而被判死亡这个游戏评分一定直线下滑。标准做法是视觉模型和碰撞体分离碰撞体比视觉模型小20%-30%。using UnityEngine; public class DeathZone : MonoBehaviour { private void OnTriggerEnter2D(Collider2D other) { if (!other.CompareTag(Player)) return; GameManager.Instance.OnPlayerDeath(); } }障碍物使用BoxCollider2D或PolygonCollider2D。图省事就手动调整预制体里碰撞体的Size和Offset各方向缩10%-15%严谨一点在Awake里按比例缩放碰撞体但会影响物理精度而且容易在团队协作时造成“代码改了、碰撞体形状对不上”的困惑。我建议直接手调一个预制体调一次存入预制体即可。对象碰撞体类型视觉体与碰撞体比例备注龙角色BoxCollider2D视觉63x38碰撞体48x24只匹配躯干不包含翅膀和尾巴尖塔障碍BoxCollider2D宽度缩15%高度缩10%底部尖刺部分不参与判定旋转锯齿CircleCollider2D半径缩20%旋转类障碍必须缩小判定金币/火球CircleCollider2D半径缩10%Trigger采集物不需要严格贴合表里最容易被忽略的是旋转类障碍。锯齿、旋转刀片视觉上外缘扫过很大的面积如果按静态碰撞体计算玩家明明躲过了刀尖却被空气斩死。要么缩小判定做“视觉宽容”要么给碰撞体加Physics Material 2D的弹力让它在轻微擦碰时把玩家弹开而不是直接判死。3.3 对象池飞行跑酷不卡顿的Unity性能根基跑酷障碍物生命周期极短生成后几秒就飞出屏幕。直接用Instantiate和Destroy每波障碍物都会触发内存分配和GC运行几十秒后出现明显卡顿。对象池是这类游戏的标配预先创建一批实例用的时候激活不用的时候停用不销毁。using System.Collections; using System.Collections.Generic; using UnityEngine; public class PoolManager : MonoBehaviour { public static PoolManager Instance; private Dictionarystring, QueueGameObject pool new(); void Awake() { Instance this; } public GameObject Get(GameObject prefab, Vector3 position, Quaternion rotation) { string key prefab.name; if (pool.TryGetValue(key, out var queue) queue.Count 0) { GameObject obj queue.Dequeue(); obj.transform.position position; obj.transform.rotation rotation; obj.SetActive(true); return obj; } return Instantiate(prefab, position, rotation); } public void Recycle(GameObject obj) { obj.SetActive(false); string key obj.name.Replace((Clone), ); if (!pool.ContainsKey(key)) pool[key] new QueueGameObject(); pool[key].Enqueue(obj); } public void RecycleLater(GameObject obj, float delay) { StartCoroutine(RecycleCoroutine(obj, delay)); } private IEnumerator RecycleCoroutine(GameObject obj, float delay) { yield return new WaitForSeconds(delay); if (obj ! null) Recycle(obj); } }对象池在飞行跑酷里的收益远大于平台跳跃平台跳跃的敌人数量有限跑酷的障碍物无限生成不回收就是无限增长。写对象池有两个坑第一Instantiate出来的对象名带“(Clone)”后缀必须用Replace去掉否则同一个预制体的回收对象会被当成不同类别第二池子的预生成数量按最长关卡长度估算一般20-40个实例就够跑完整局池子太大反而浪费内存。4. 飞行跑酷手感调优输入响应、升降加速度与Unity性能瓶颈4.1 输入处理的三种方案按键、触摸与虚拟摇杆输入方案取决于发布平台。PC上用W/S和上下箭头手机最简单的方案是单点触控按住屏幕上升、松手下沉。单点触控对飞行跑酷特别友好单手可玩另一只手可以空出来操作别的。private bool isPressing; void Update() { if (Input.touchCount 0) { Touch touch Input.GetTouch(0); isPressing touch.phase TouchPhase.Began || touch.phase TouchPhase.Stationary || touch.phase TouchPhase.Moved; } else { isPressing false; } } void FixedUpdate() { // Update里记录输入状态FixedUpdate里消费输入状态 targetVelocityY isPressing ? riseSpeed : -fallSpeed; }触摸输入有个常见问题在FixedUpdate里直接读Input.GetTouch会遭遇输入状态跨帧。正确模式是Update里记录、FixedUpdate里消费因为Input的状态更新发生在Update阶段物理阶段读到的可能是上一帧的输入。上一节的DragonFlight代码已经用了这个传递模式这个模式在跑酷项目里是通用的。虚拟摇杆方案适合进阶控制但摇杆有两大缺点遮挡屏幕、拇指行程长穿缝场景反应不够快。如果要做摇杆至少要把上升和下降映射到摇杆的Y轴而不是用摇杆方向控制角色飞行方向——那样玩家会很分裂方向是斜的角色却只能上下移动。4.2 升降加速度的三段式设计上升、悬停、下降手感的本质是速度响应曲线。飞行跑酷和平台跳跃不同平台跳跃讲究抛物线落点飞行跑酷讲究“变向是否跟手”。我常用三段式设计上升段响应最快输入后0.05秒内达到最大上升速度体现“升龙”的力量感悬停段无输入时速度归零要快控制在0.1秒内让玩家觉得角色“定得住”下降段带一点加速感模拟重力但不用物理重力手动控制下降曲线参数推荐范围效果riseSpeed5-8 m/s小于5升力不足大于8难以精细穿缝fallSpeed3-5 m/s下降略慢于上升符合收翅下滑直觉inputSmoothing8-15数值越大越跟手过大则画面抖动linearDamping1-3影响速度残留感跑酷建议不超过3Camera followSpeed4-8跟太紧晕太松看不清落点要特别说明linearDamping和SmoothDamp的区别。linearDamping是物理引擎的阻力作用于Rigidbody2D让速度指数衰减SmoothDamp是纯数学插值不和物理引擎的其他力交互。如果两者都用速度收敛变快调参时很难判断到底哪个参数在起作用。我的习惯是物理阻力设一个基准值1.5手感微调全部走SmoothDamp让每个参数各管一件事。4.3 Unity性能瓶颈飞行跑酷最容易遇到的三个坑横版飞行跑酷是最容易跑满60帧的2D类型场景简单、物体少但恰恰如此很多项目连优化都省了结果低端手机一卡一卡。三个坑最常见第一个是物理追帧。Fixed Timestep默认0.02秒对应50Hz物理渲染帧率掉到30fps时物理会“补算”堆积。在Project Settings Time里把Maximum Allowed Timestep从默认的0.333调低到0.1左右掉帧时物理不会疯狂追帧代价是超时部分物理略有跳变配合对象池后几乎不可见。第二个是Draw Call。2D飞行跑酷精灵多图集没打的话几十个障碍物加多层视差背景就有几十个Draw Call。用URP时SRP Batching能自动合批但前提是所有精灵Shader必须统一为Universal Render Pipeline/Sprite混用默认Diffuse或自定义Shader会断批Draw Call翻倍。第三个是GC Alloc。飞行跑酷的Update和FixedUpdate每帧执行任何产生临时对象的写法都会造成持续GC。常见元凶每帧ToArray()/ToList()复制集合、LINQ链式查询的中间对象、字符串拼接、每帧GetComponent。养成在Awake里缓存组件引用的习惯不要在Update里来回找。// 反例每帧字符串拼接GC压力极大 Debug.Log(Position transform.position.x); // 正例只在事件触发时输出 void OnCollisionEnter2D(Collision2D col) { Debug.Log($撞到了 {col.gameObject.name}); }验证GC用Window Analysis Profiler切到CPU Usage在详细调用树里勾选Show All看每帧Hierarchy的GC Alloc数值目标是把每帧分配压到接近0KB。5. 手感验证的量化方法用可视化调参面板校准飞行曲线手感靠感觉调容易走弯路量化以后才知道问题出在哪。我习惯在项目里加一个调试组件用OnDrawGizmos把碰撞判定框、障碍物通道、相机前视位置全部画到Scene视图编辑器里不运行Play模式就能看到场景中的几何信息一眼看出设计意图和实际表现是否一致。public class FlightDebug : MonoBehaviour { public Vector2 hitboxSize new(0.48f, 0.24f); // 角色实际判定框 public Transform nextObstacle; // 下一个障碍物引用 public float channelHeight 3f; // 当前通道宽度 public float lookAhead 3f; // 相机前视距离 void OnDrawGizmos() { Gizmos.color Color.green; Gizmos.DrawWireCube(transform.position, hitboxSize); if (nextObstacle ! null) { Gizmos.color Color.yellow; Gizmos.DrawWireCube(nextObstacle.position, new Vector3(1f, channelHeight, 0f)); } Gizmos.color Color.cyan; Gizmos.DrawWireSphere(transform.position Vector3.right * lookAhead, 0.2f); } }OnDrawGizmos只在编辑器里执行不会进入正式Build可以放心写在游戏代码里。调试面板的UI部分用#if UNITY_EDITOR包裹避免发布版本混入无效代码。最终验证一道关卡的方式先固定所有障碍物水平间距为8米、通道宽度3.5米录制一段玩家操作序列回放30次确认全部通过再把通道缩到2.5米回放同一序列确认失败率达到预期。这套回放测试是飞行跑酷项目里最容易被省略但价值最高的一步它把“手感”从玄学变成可回归验证的指标。注意Unity启用新Input System包后旧版Input.GetKey会失效。不打算做多端多设备适配的项目在Player Settings里保持Active Input Handling为Input Manager旧版即可不用为了新输入系统重写全部控制代码。难度曲线最后一公里匀速生成关卡玩家玩三分钟就会觉得无聊。按距离分段提升难度即可——0-500米用通道宽度的100%500-1500米缩到85%1500米以上缩到70%。曲线不要用分段函数硬切用Mathf.Lerp在每段区间内平滑过渡生成器实时按当前距离计算通道宽度而不是在生成瞬间写死这样玩家遇到的每一组障碍物都处在当前难度曲线上手感才能保持连续。本文还有配套的精品资源点击获取
返回列表