简介:这是一份适用于 Unity 引擎的无限跑酷项目源码,面向想快速上手休闲跑酷玩法、学习完整项目制作流程的开发者。项目包含角色控制、障碍生成、计分与复活等核心机制,并配有演示视频便于对照效果。整包共 2003 个文件,大小 600.57MB,主要文件包括 C# 脚本、Prefab 预制体、Shader 着色器、FBX 模型、材质、图片、音频及 JSON/TXT 配置;另有 553 个 Markdown 笔记辅助理解模块结构,8 个场景配合预制体已构成可运行框架。工程设置中已包含输入、物理、质量等常用参数,避免从零搭建环境的成本。大量预制体与场景让玩法扩展更直观,Markdown 与配置资料帮助梳理逻辑,降低了学习和定制的门槛。已有 468 人学习/下载,适合 Unity 学习者、毕设学生和独立开发者作为完整示例,既能理解跑酷类游戏的设计流程,也能作为原型快速迭代。
1. 为什么一套 Unity 无限跑酷源码值得拆开看
如果只是下载一套 Unity 无限跑酷源码,打开工程按一下 Play,看到角色跑起来就算完,那你会漏掉这个项目里最有价值的部分:自动地形生成、对象池复用、角色状态机和手机端性能策略。单看标题,它像是个小游戏 Demo,但真正能用于商业立项的跑酷项目,恰恰需要把这些机制吃到透。很多开发者第一次拿到这类源码,会遇到地形生成不连续、内存持续涨、手机真机掉帧这类问题;如果你知道这些坑在哪,这套源码就是你搭建跑酷类原型的骨架。适合刚接触完整跑酷流程的新手,也适合直接拿来做项目往上叠功能的客户端开发。
2. 核心机制拆解:跑酷角色为什么能“自动向前跑”
提示:这一章涉及源码里最常见的三类脚本。如果你只想通过改参数调手感,也得先明白哪个脚本控制移动、哪个控制跳跃、哪个管生成。
2.1 自动跑动的实现:角色留在原地,环境往后退
很多跑酷项目的移动核心并不是让角色真的向前跑,而是让角色停留在局部原点附近,把地面和障碍物整体向镜头方向平移,或者给角色刚体一个恒定速度。两种方案各有使用场景:平移场景组件适合不需要物理碰撞的简单 2D 跑酷;而大多数 3D 无限跑酷源码,用的还是刚体恒定速度,因为这样可以直接复用重力、碰撞和跳跃的物理反馈。
我习惯在 FixedUpdate 中强制写入 z 方向速度,而不是在 Update 里给 Transform 加位移。原因是跑酷中跳跃和落地都依赖物理引擎,如果用 Transform 直接改 z,很可能在碰撞回调里出现“卡进”障碍物的情况。下面是这类源码里最常见的自动移动核心:
using UnityEngine; public class AutoRunner : MonoBehaviour { public float runSpeed = 8f; public Rigidbody rb; void FixedUpdate() { Vector3 vel = rb.velocity; vel.z = runSpeed; rb.velocity = vel; } }代码逻辑很简单:每一帧物理更新前,把 z 方向速度强制设定为固定值,其他方向保留物理引擎的结果。这样角色会保持向前跑,同时 y 方向的速度由重力控制。参数 runSpeed 是所有手感参数里第一个要调的项,它决定障碍物出现在视野后的反应时间。如果 runSpeed 设成 12,而跳跃高度只有 2,那很多跳不过去的障碍就会出现碰撞;实际调参时,跑速和跳跃力必须一起调。
与直接移动场景组件的方式相比,刚体方案有个好处:当你想做加速带或者被弹飞道具时,只需要改变速度的插值,而不需要处理整个场景的坐标系偏移。常见做法是在场景里放一个 GameManager,把 AutoRunner 的速度暴露成属性,由 GameManager 统一管理加速节奏。
2.2 角色状态机:跑步、跳跃、滑铲如何切换
跑酷源码里第二个核心是角色状态机。最简单的实现是枚举加每帧条件判断,稍复杂的会挂 Animator 状态机。源码里我看到的多半是枚举加状态锁,因为这种实现好调试、好扩展。关键点在于跳跃的判定必须严格区分“空中”和“地面”,否则容易出现二段跳、无限跳。
下面的代码是一个简化版但能跑的状态切换逻辑:
public enum PlayerState { Running, Jumping, Sliding } public class PlayerController : MonoBehaviour { public PlayerState state = PlayerState.Running; [Header("GroundCheck")] public Transform groundCheck; public float checkRadius = 0.2f; public LayerMask groundLayer; void Update() { if (state == PlayerState.Jumping && IsGrounded()) state = PlayerState.Running; if (Input.GetKeyDown(KeyCode.Space) && IsGrounded()) state = PlayerState.Jumping; if (Input.GetKeyDown(KeyCode.S) && state == PlayerState.Running) state = PlayerState.Sliding; } bool IsGrounded() { return Physics2D.OverlapCircle(groundCheck.position, checkRadius, groundLayer); } }这段代码使用 OverlapCircle 做一个临时碰撞判定。groundCheck 必须放在角色脚底偏下的位置,而且最好单独建一个 Ground 层,把地面和障碍物分开,否则角色会把自己的碰撞体误判成地面。参数 checkRadius 影响手感,半径太小会导致落地后要过十几毫秒才进入 Running,半径太大会在地面边缘提前触发跳跃。
在复杂跑酷项目里,滑铲不只是换一个动画,而是要换碰撞体大小,否则角色滑铲时头部还是会撞到上方障碍。源码里如果保留了 crouch collider,记得在状态切换时同步禁用或启用对应子物体。
2.3 无限地图的生成策略:预制体切片和对象池
无限地图不是真的无限,而是用有限的预制体无限拼接。很多跑酷源码会把一整段跑道路径做成一个 Segment,每个 Segment 中包含地面、障碍物、装饰物、金币。生成逻辑由 GameManager 根据玩家位置判断,是否需要在前面再放一个 Segment,同时把身后的 Segment 回收掉。
这里最容易写出的问题是:每帧生成、每帧销毁,会导致实例化开销和 GC 峰值。所以成熟源码里一定会有对象池。下面是一个通用对象池脚本,同样适用于障碍物和地面段:
using System.Collections.Generic; using UnityEngine; public class ObjectPool : MonoBehaviour { public GameObject prefab; private Queue<GameObject> pool = new Queue<GameObject>(); public GameObject Spawn(Vector3 position) { GameObject go = pool.Count > 0 ? pool.Dequeue() : Instantiate(prefab); go.transform.position = position; go.SetActive(true); return go; } public void Recycle(GameObject go) { go.SetActive(false); pool.Enqueue(go); } }Spawn 时优先从队列拿隐藏对象,拿不到才 Instantiate;Recycle 时只是关闭并放入队列,没有销毁。这样做能够避免反复创建对象造成的卡顿。使用时要注意,如果池里对象带有 Rigidbody,回收时要把速度清零,否则刚体会带着速度继续参与物理运算,内存和 CPU 都会有额外开销。
生成位置的判断一般是:当最后一段的结束 z 坐标小于摄像机前方预期生成距离时,从池里取一段挂到那个坐标;当最早一段的结束 z 坐标跑到摄像机后方若干个单位时,回收它。这个生成距离不宜太近,否则玩家会看到障碍凭空出现;也不宜太远,否则同一屏内对象太多,Draw Call 会更高。常见做法是让它略大于摄像机远裁剪面,或者控制在玩家视野内约 2 个屏宽。
3. 把源码跑通:环境、场景、参数三者对齐
3.1 导入前的环境对齐:输入系统、渲染管线、重力设置
下载源码后直接打开,最容易遇到的是 Input System 冲突。现在新建 Unity 工程可能默认启用新的 Input System,而跑酷源码如果还在用老的 Input Manager,打开后会出现双击报错、按钮无响应,甚至 Project Settings 里面找不到对应输入条目。
操作上我一般先做一次环境统一:Edit > Project Settings > Player > Active Input Handling,选中 Both。这样新旧输入 API 都能用,不用着急重构源码。如果源码明确说只支持旧输入,就把 Active Input Handling 改成 Input Manager,然后在 Player Controller 里检查 Input.GetKeyDown 是否有反应。
还要检查渲染管线。很多存量跑酷源码用的还是内置渲染管线,材质是标准 Shader。如果你把工程升到 URP,所有材质会变成紫色。遇到这种情况,要么把项目迁回内置管线,要么给材质重新选 URP 对应 Shader。做这一步要看清项目依赖,不是所有贴图都兼容。
最后是 Gravity 和物理步长。跑酷项目对重力依赖很强,Project Settings > Physics 里默认的 Gravity 参数不要乱改。常见做法是保持默认重力,在角色自身的 Rigidbody 上调整 Gravity Scale,这样场景里其他物理特效不会跟着受影响。
3.2 场景节点检查清单:Player、Camera、GameManager、PoolManager
跑通前,先用场景视图检查四个关键节点。Player 节点必须有 Rigidbody 和碰撞体,并且固定旋转、固定 z 轴位移。Camera 节点通常挂在 Player 的某个子节点下,或者由脚本跟随 Player;如果是脚本跟随,注意有没有设定跟随偏移量。GameManager 节点上挂着游戏状态控制、距离计数、失败判定。PoolManager 节点下挂着多个 ObjectPool 子物体,每一个对应一种预制体。
下面这个表格是我常用的排查清单,照着看能少翻车:
| 节点 | 必须检查的设置 | 容易踩的坑 |
|---|---|---|
| Player | Rigidbody 非 Kinematic,碰撞体大小适中 | 碰撞体过大导致“空气墙” |
| Camera | 跟随脚本的偏移量正确 | z 轴抖动 |
| GameManager | 生成距离、回收距离一致 | 生成与回收重叠 |
| PoolManager | 池内 prefab 引用正确 | 多个 prefab 共用一个池 |
场景里的碰撞层级也要单独检查。跑酷项目通常让 Player 只跟 Ground 和 Obstacle 碰撞,金币和触发区域用 Trigger,不参与物理阻挡。如果项目没有单独建 Ground 层,而是让 Player 跟所有 Trigger 都发生碰撞,就有概率出现金币卡住角色、跳跃判定报错这些奇怪现象。
3.3 手感参数:默认值往往不是你的目标
跑酷游戏的手感基本就是跑速、跳跃力、重力和判定半径的组合。一般以默认参数为基础,按“先跑通、再调整”的顺序。先确认跑速合适,再让跳跃能稳定跳过标准障碍,最后调滑铲时长。
下面这些参数最值得从源码的 Inspector 面板里找出来调:
| 参数 | 作用 | 调参建议 |
|---|---|---|
| runSpeed | 角色前移速度 | 5-10 比较适合新手,高于 12 会很难反应 |
| jumpForce | 跳跃初始速度 | 与重力配合,跳到最高点要能越过最高障碍 |
| jumpGravityScale | 跳跃时的额外重力 | 调大落地更干脆,调小能“飘一点” |
| checkRadius | 地面检测半径 | 0.1-0.3,过大会让玩家感觉还没跳完就落地 |
| laneSwitchTime | 换道插值时间 | 0.1-0.3,过短僵硬,过长手感太绵 |
跳跃高度的计算关系是 h = v² / (2g)。如果你的 jumpForce 是 8,重力是 9.81,理论最高高度约为 3.26 米。这时如果最高障碍是 3 米,看着能跳过去,但连续跳跃时会因为起跳瞬间的位置误差卡到边缘。所以我一般会把 jumpForce 值调得偏高一点,再把跳跃后的那几帧输入屏蔽掉,让手感更宽容。
调参方法上,不要用 Game 视图边跑边改,效率太低。建议把 runSpeed、jumpForce、GravityScale 这几个参数在 Player 脚本里序列化,然后把 Inspector 窗口放到旁边,Play 时直接改值,观察角色动作和 Debug.Log 输出。遇到边界值,先暂停再调整,减少变量干扰。
4. 避坑与常见问题排查:五条血泪经验
4.1 碰撞判定不稳:角色偶尔被“空气墙”卡住
现象:角色在地面上正常跑,某一小段路面突然弹起,或者跳跃高度明显变矮,像是撞到看不见的物体。
原因:最常见的是地面段与地面段之间存在重叠的碰撞体,或者碰撞体比地面模型大了一点,造成微小的凸起。另一个原因是地面材质使用了带碰撞体的装饰物,开发者以为是装饰,实际上它参与了物理碰撞。
解决:检查每段地面的 Box Collider 尺寸,把 Y 轴厚度压到贴近模型表面,Z 轴方向让前后两段之间保留 0.01 的间隙,不要重叠。如果依然有凸起,在场景视图里把 Player 碰撞体透明显示,逐帧看哪段路径有问题。注意,修复后要重新运行一遍完整路径,不要只测试起点附近。
4.2 对象池回收不及时导致内存持续上涨
现象:跑酷运行十几分钟后,帧率开始波动,内存占用持续上升;Profiler 中 GC Alloc 一帧比一帧高。
原因:很多源码只在障碍物离开摄像机视野时回收,却没有在整段地面后移时统一回收同段内所有子物体。当 Segment 回收后,其子物体内的金币和装饰物没有被回池,而是继续被 Instantiate 重复创建,等价于每跑一段就漏一行内存。
解决:把地面段、障碍物、金币分别放进对应的 Pool,并且让 Segment 在回收时主动调用所有子物体的 Recycle。为了更稳定,我一般会再写一个监控:如果池内对象数量长期不下降,就在 GameManager 里输出一个警告。注意,回收时要重置对象的旋转和缩放,否则下一个 Spawn 出来的对象可能角度不对。
4.3 电脑流畅但手机上一跑步就掉帧
现象:编辑器里跑 60 帧很稳定,打包到安卓或 iOS 之后,碰到障碍生成瞬间就会掉到 30 帧以下,发热明显。
原因:手机上主要瓶颈在 Draw Call 和物理开销。编辑器开发时往往开着 Profiler,但实际设备上没有观察;跑酷场景里大量金币和装饰物都是独立 Mesh,每颗金币一个 Draw Call,一个屏放几十个就会把移动端 GPU 打爆。
解决:先把所有同种小物件合并进同一个图集,或者在每个 Segment 内设置 LOD,远处小物件直接隐藏。再看相机裁剪范围,跑酷游戏可以把 Camera Far Plane 从 500 改到 100,减少很多不可见物体。最后检查是否有空 Update 循环,比如每个金币都在做旋转动画,会导致大量 Transform 更新;把旋转逻辑搬到 Shader 里或统一合批可以明显改善。
4.4 二段跳变成无限跳
现象:角色明明在空中,连按跳跃键却可以连续触发很多次,二段跳功效变成了无限跳。
原因:状态机没有保存“已连续跳跃次数”,只是用 IsGrounded 判断是否允许起跳。当角色在地面边缘下落时,GroundCheck 的检测圆因为半径设置过大,在空中依然能触碰到地面边缘,导致状态机认为还在“地面”。
解决:在 PlayerController 里增加 jumpCount 字段。每次从 Running 进入 Jumping 时 jumpCount = 1,在空中再次起跳时 jumpCount = 2,落地后归零。这样无论 GroundCheck 有没有误报,都不会超过二段跳。还有另一个容易忽略的点:GroundCheck 的 transform 尽量放在角色脚底中心,不要放在脚尖,否则斜坡边缘误判概率会很高。
4.5 碰到障碍不触发失败流程
现象:角色碰撞体已经碰到障碍,但 GameManager 没有播放失败动画,也没有停止跑动,角色穿过障碍物继续跑。
原因:障碍物碰撞体没有勾选 Is Trigger,Player 又是 Kinematic 刚体,OnTriggerEnter 不会被触发。或者项目里只写了 OnCollisionEnter,但障碍物没有碰撞体组件。
解决:统一约定:玩家撞到障碍用 OnCollisionEnter,金币用 OnTriggerEnter。给障碍物加 Box Collider 且不勾 Is Trigger,给金币加 Box Collider 且勾选 Is Trigger。然后在 GameManager 里分别监听 OnCollisionEnter 和 OnTriggerEnter。如果遇到个别障碍物“穿透”,重点检查这个障碍物的 Collider 有没有被禁用、Rigidbody 是不是非 Kinematic。跑酷场景里的障碍物通常不需要物理模拟,给它们挂上 Is Trigger 会省很多物理开销。
5. 从源码到自己的玩法:三处最值得改造的地方
5.1 地图生成器改造:加入难度曲线和分段节奏
默认源码里的障碍生成往往是固定间隔,跑一会儿就觉得无聊。要变成能商用的小游戏,得让障碍密度和类型随距离变化。最简单的方式是做一个难度等级表,按已跑距离切换生成参数。
[System.Serializable] public class DifficultyLevel { public float startDistance; public float obstacleInterval; public float flyingObstacleRate; } public class DifficultyManager : MonoBehaviour { public DifficultyLevel[] levels; public DifficultyLevel GetLevel(float distance) { DifficultyLevel result = levels[0]; foreach (DifficultyLevel level in levels) { if (distance >= level.startDistance) result = level; } return result; } }代码里 levels 是有序数组,startDistance 从小到大排列。GetLevel 用线性扫一遍就行,距离超过某个节点就返回对应等级。实际接入时,GameManager 在生成下一个 Segment 前,先通过该接口拿到当前等级,然后决定障碍间隔和飞行障碍概率。难度曲线不要做成线性单调递增,最好每 500 米升一级,中间留 200 米的喘息距离,不然玩家压力太大容易流失。
5.2 角色控制扩展:左右三车道换道与蓄力跳
很多 3D 跑酷是上下跳跃和滑铲,但如果要做跑酷比赛玩法,需要加上左右换道。换道不能用刚体直接改位置,否则会穿模;我一般用协程插值移动,保证每一帧位置都是平滑的。
public IEnumerator SwitchLane(float targetX, float duration) { Vector3 startPos = transform.position; float elapsed = 0f; while (elapsed < duration) { elapsed += Time.deltaTime; float t = Mathf.Clamp01(elapsed / duration); Vector3 newPos = Vector3.Lerp(startPos, new Vector3(targetX, startPos.y, startPos.z), t); transform.position = newPos; yield return null; } }这里 switchLane 的高层逻辑是在外部判断左右方向,计算目标 X 坐标,直接调用协程。duration 默认 0.15 秒手感比较合适,如果跑速很快,可以缩到 0.1。换道过程中建议屏蔽跳跃输入,因为同时换道和跳跃会让碰撞判定变得复杂。另一个常用扩展是“蓄力跳”:长按跳跃会增加跳跃高度,松开时按当前蓄力值给刚体一个速度。实现上不需要改状态机,只在 Update 里记录按键时间,然后在 Jump 函数里把额外速度叠加到 jumpForce 上。
5.3 存档系统:从 PlayerPrefs 到结构化 JSON
源码里常见的是用 PlayerPrefs 存储跑到的最远距离和金币数。这样做简单,但跨版本更新时很容易丢字段,特别是添加新道具后,旧存档没有对应字段会导致读取异常。我习惯在源码里直接改成 JSON 序列化,即使未来加字段也好兼容。
[System.Serializable] public class SaveData { public int lastDistance; public int coins; public string version; } public class SaveManager { public static void Save(SaveData data) { string json = JsonUtility.ToJson(data); PlayerPrefs.SetString("RunSave", json); PlayerPrefs.Save(); } public static SaveData Load() { string json = PlayerPrefs.GetString("RunSave"); if (string.IsNullOrEmpty(json)) return new SaveData(); return JsonUtility.FromJson<SaveData>(json); } }这个方案的坑在于,从旧存档读取时没有 version 字段,会直接变成默认值。更稳妥的做法是 Load 后先检测 version,如果是旧的,就为缺失字段补默认值。不要直接信任 PlayerPrefs 的本地数据,玩家清理缓存是常态。如果是移动端上线版本,存档里尽量别放敏感数值,只放可恢复的进度;本地存档要做到“删掉也能重新开始”,服务器同步是后面再考虑的事。
5.4 小地图和暂停菜单的联动
跑酷源码的另一处值得改的是暂停和恢复。很多简单 Demo 一暂停就直接 Time.timeScale = 0,但这样做会导致对象池回收逻辑和协程被打断。重回游戏时角色仍在跑,但所有池对象的回收判断停了一拍,后续可能一连串报错。最好做一个 GamePause 管理器,暂停时只停止 PlayerController 和 GameManager 的更新,不把整个时间轴停掉,或者是显式处理协程。一个小技巧是:暂停时把所有在跑的对象池身上的 Rigidbody.isKinematic 临时设成 true,暂停结束再恢复,能避免很多意外的物理穿透。
6. 手感调优与性能验证:最后要做的三个实操作
源码本身能跑通之后,更重要的是确认它在你目标设备上的表现。第一步是用 Unity Profiler 的 CPU Usage 模块跑 3 分钟,重点看 FixedUpdate 里的物理耗时和 GC Alloc。如果发现 GC Alloc 一直在涨,立刻检查对象池回收路径,不要等优化完再查。第二步是给场景里所有动态生成物体关闭 shadow casting,或者限制阴影距离,跑酷场景里阴影是最容易被忽视的性能刺客。
第三步是做一个“输入回放”测试:把玩家在一段路径里的输入序列记录下来,再回放同样的输入,确认每次起跳和落地结果一致。跑酷源码里最怕的是随机地形生成让手感不可复现,使用固定随机种子在测试阶段尤其重要;上线前再换成随机种子即可。
我在调这类项目时发现,很多开发者会把大量时间花在改美术和 UI 上,忽略了 FixedUpdate 里不断实例化对象这种基础问题。其实跑酷手感好不好,第一看跑速和跳跃的响应窗口,第二看碰撞体是否干净,第三就是性能余量。从那以后我每次拿到跑酷源码,都会先强制自己打开 Profiler,把物理开销和 GC 涨势看一遍,确认没有坑,再开始改玩法逻辑。希望帮到你。
本文还有配套的精品资源,点击获取