Cocos Creator跑酷源码拆解:从场景树到最小逻辑复刻
简介这款基于 Cocos Creator 的熊猫跑酷项目源码专为需要快速上手横版跑酷玩法的开发者准备可运行于 1.4 及以上版本含完整游戏流程与熊猫角色控制逻辑适合学习资源管理、动画切换与碰撞检测的中初级 Cocos 用户。压缩包内共 256 个文件主要包括场景与预制体prefab、游戏逻辑脚本js/ts、资源配置json/meta、动画anim、纹理图集png/plist及地图配置map等整体仅 3.46MB结构精简便于直接导入 Cocos Creator 工程查看与二次开发。目前已有 3274 人学习下载资料内含亲测可运行的项目源码、跳跃/二段跳/奔跑等动画设计以及单文件日志与说明文档可帮助读者理解跑酷游戏中角色状态机与资源组织方式是练习 Cocos Creator 小游戏开发的实用参考。1. 一个 cocos creator 跑酷项目源码最值钱的不是“跑起来”那一下拿到一个 “cocos creator 跑酷项目源码” 压缩包很多人第一件事就是解压、导入、点播放看到角色跑起来了就以为通关了。我见过不少人在这一关后卡住跳跃总像沾了胶水、二段跳按快一点就变成飞行、随机障碍刷出来直接排成一堵墙。源码能跑不等于你接得住这套代码。这篇笔记会把一套典型跑酷 Demo 拆开讲清楚——场景树怎么组织、脚本各自管什么、最小核心逻辑怎么自己写出来、以及重跑源码时哪些坑必踩。适合刚拿到源码却不知道怎么下手的同学也适合想用最短代码复刻一个最小跑酷 Demo 的开发者。2. 跑酷源码包的结构场景树、脚本职责与阅读顺序拿到源码包先别急着双击场景。跑酷游戏的结构高度同质化先花十分钟把目录和场景树看明白比反复盲跑试错效率高得多。我一般先把 assets 下面的 scripts、scenes、resources 三个目录列一遍再打开主场景逐节点看挂载。目录结构本身能透露很多信息脚本文件特别多的多半是逻辑散乱prefab 集中放在 resources 里的多半是运行时动态加载场景里堆了一大堆节点的多半是美术资源没做合批。跑酷这种玩法之所以适合做教学源码是因为它状态简单玩家永远朝一个方向运动输入只有跳跃键世界要么在跑、要么结束没有存档、任务、背包这些系统。但正因为简单代码质量差异反而被放大。一套结构好的跑酷源码三五个脚本就能撑起完整玩法结构差的光角色移动就要跨三个文件互相取引用。这一章讲的就是怎么快速判断一套跑酷源码是“可读的”还是“只能跑的”。2.1 节点层级跑酷场景为什么都长一个样打开主场景后先别管写满注释的脚本把节点树从上到下看一遍。几乎每套跑酷 Demo 都绕不开下面这张表的组成节点必备组件职责PlayerRigidBody2D Collider2D PlayerController移动、跳跃、被撞判定GroundRigidBody2D静态 Collider2D提供站立面保证物理碰撞ObstacleSpawnerSpawnManager 脚本在视野外生成障碍Background视差脚本或动画滚动背景营造移动感UICanvas 分数文本 结束面板显示得分、游戏失败提示Camera跟随脚本或手动跟随锁定玩家稳定画面首先要注意的是 Ground 节点很多精简版源码把地面做成纯贴图不挂静态刚体角色就会直接往下掉。在这里物理引擎要的不是“看起来像地面”而是“碰撞体真的存在”。主流的实现是纯静态场景用 static 刚体加一个盒子碰撞体跑酷里要让地面跟随玩家水平移动时则用 kinematic 类型。另一个常见做法是把地面分成几段结束时把离玩家最近的一段挪到前方循环复用这时所有段都得是同一个刚体类型混用会出现跳上去瞬间卡一下的现象。Camera 的挂载方式也很值得看有的用脚本每帧把 camera 的 position 直接设成玩家坐标有的挂在 Player 节点下当子节点。后者的平移更顺但跳跃时如果相机的 y 跟着玩家跑画面就会上下颠簸普通跑酷中相机通常只锁定 x 轴。判断一套源码专业程度看相机脚本里是否单独保留 y 偏移就够了。2.2 脚本职责边界谁改位置、谁发事件、谁收状态看跑酷源码的第二件事是把每个脚本开头一段的引用关系列出来。最不理想的结构是“玩家脚本里 new 障碍物”“生成器脚本里直接读玩家刚体速度”这种写法在 Demo 阶段跑得通但你想加一个双倍积分道具时就要同时在三个文件里改逻辑。更好的做法是让脚本之间单向依赖公开接口而不是公开对象。我一般把脚本分成三层。表现层PlayerController 只处理输入、跳跃、物理速度对外发出 onPlayerJump、onPlayerHurt 两个事件管理层SpawnManager 只管生成物体具体生成在哪、间隔多少由参数传入它不关心玩家是怎么移动的状态层GameManager 持有玩家节点引用负责监听事件、更新分数、切换游戏结束场景。三层之间谁都不直接 new 谁的实例靠事件或属性检查器注入。这样拆完你会发现源码里真正需要你理解的核心只有两件事一是物理参数怎么调二是生成间隔与移动速度的换算。其他诸如背景循环、音效播放、UI 刷新都是挂在事件上的附加逻辑不该混进移动代码里。重跑源码时如果发现“我只是把速度调大怎么跳跃也变高了”多半就是位移和跳跃逻辑耦合在了同一个函数里。2.3 源码的正确阅读顺序从入口场景反推到资源读源码不要从 scripts 目录的第一个文件开始。我更建议按下面的顺序来第一步在资源管理器里找到场景文件双击打开。第二步从场景里 Canvas 的顶层节点开始记录每个挂了脚本的节点。第三步点开每个脚本组件看属性检查器里拖了哪些引用只看引用关系先不看代码。第四步从 update 和各类事件回调开始读代码按调用链往下走。第五步回到 assets 下核对 prefab 资源搞清哪些是场景里直接放的哪些是运行时动态加载的。为什么按这个顺序因为跑酷代码的坑大部分不在算法而在“挂载关系”。一个障碍物 prefab 没拖对位置运行时黑屏你查半天也查不到。先看引用能看到“这个脚本依赖哪些对象”再看 update 能看到“这些对象每帧被怎么操作”最后回到资源层才能对得上。若一开始就扎进逻辑里你会陷入一个死循环代码没毛病但运行就是报空引用因为场景里没有给它挂对应的节点。3. 把源码包跑起来的四步版本、导入、参数与首帧上一章看懂了结构这一章讲怎么让它真跑起来。跑酷源码重跑时遇到的第一个问题通常不是逻辑而是版本。“我下载的源码打不开”十个里有八个是 Cocos Creator 版本不匹配。组件序列化格式、内置组件的属性名、物理系统的默认开关在大版本之间都可能变。最好的做法是用源码配套的 Creator 版本打开先看工程目录下的 package.json 或 project 设置确认大版本再决定导入方式。3.1 版本匹配为什么打开项目后场景全是空白的现象很好认你用 3.x 打开一个 2.x 的工程场景文件能打开但节点上的脚本组件全部丢失按钮和文字还在就是没有任何逻辑甚至连报错都没有。原因在于脚本组件序列化时保存的是脚本 uuid 和类名不同版本之间 uid 对应关系不一致编辑器找不到对应组件就静默丢掉了。解决方案分两步。第一步尽量找配套版本打开不要用最新版硬开第二步实在没有配套版本就别指望完整迁移把导出成 prefab 的场景按节点重建脚本重新挂载。具体重建时先建空场景按原场景节点树手动创建节点再在属性检查器里给每个节点挂上对应脚本、拖好引用。这个过程中最值得复用的是参数跳跃高度、速度、初始间隔从原工程属性检查器里抄下来填到新工程里比靠手感重新调要快得多。这里要提醒一句版本迁移不是玄学但确实有很多不可见因素。尽量不要在两个大版本之间反复横跳尤其是在做源码学习时始终在同一版本里验证能把“代码问题”和“版本问题”分开。3.2 三个必调参数移动速度、跳跃力、重力缩放跑酷跑起来后第一件要做的事是调手感而不是加功能。手感由三个参数决定它们分别作用在不同的物理阶段移动速度决定水平推进跳跃力决定跳起瞬间的垂直速度重力缩放决定跳跃弧线的长短和落点节奏。三者必须一起调单独调一个会破坏跳跃的可达性。参数作用调整方向moveSpeed水平速度决定障碍物逼玩家的节奏调大难度上升调小更休闲jumpForce跳跃初速度决定起跳高度调大能过更高的障碍gravityScale重力倍率决定下落速度和跳跃滞空感调大手感更“重”调小更“飘”我一般先固定跳跃力把重力倍率调到起跳后能在空中停留约 0.4 到 0.5 秒再回头调速度保证玩家从起跳到落地能水平移动约两倍的障碍物宽度。这个比例保证每个障碍都是“跳得过但要有节奏”的而不是纯靠反应或者纯靠运气。Cocos Creator 2.x 和 3.x 在物理单位上有差异2.x 按像素走3.x 物理世界按米走同样填一个跳跃力表现完全不同。所以从源码搬运参数时不要只抄数值要连物理单位一并确认。最稳妥的做法是运行时临时加一段日志每帧输出玩家位置和速度看真实数据而不是凭眼睛猜。3.3 首帧白屏与 prefab 加载时序源码能跑起来之后最常见的黑屏问题来自 prefab 加载时序。跑酷的障碍物通常不会在场景里摆满而是在运行时由生成器创建。如果生成器在 start 里直接调用 resources.load 然后立刻取加载结果第一次生成的障碍大概率是空的表现为开场前两秒没有障碍之后突然刷一堆。原因很简单resources.load 是异步的start 里同步取不到结果。解决方法有两个一是在资源管理器里把障碍物 prefab 直接拖到 SpawnManager 的属性检查器上变成场景引用不使用异步加载二是在 resources.load 的回调里做后续初始化把生成逻辑放进回调内。对于学习用源码我强烈建议用第一种因为拖引用能让“哪个 prefab 对应哪种障碍”一目了然新手排错时少一次黑匣子。import { _decorator, Component, Prefab, instantiate } from cc; const { ccclass, property } _decorator; ccclass(SpawnManager) export class SpawnManager extends Component { property({ type: Prefab }) obstaclePrefabs: Prefab[] []; start() { // 这里已经没有异步问题prefab 在场景加载时已经就绪 const prefab this.obstaclePrefabs[0]; const node instantiate(prefab); this.node.addChild(node); } }这段代码里property 声明了 obstaclePrefabs 数组编辑器里直接拖 prefab 进去。start 里拿到的 prefab 一定可用不用等异步回调。重点是养成习惯凡是需要在运行时反复创建的资源优先在属性检查器里拖引用只有确实不知道资源路径时才用 resources.load。这样源码可读性高也不容易踩首帧时序坑。4. 从零复刻一套最小跑酷逻辑三个脚本撑一个 Demo如果你拿到手的源码结构混乱与其硬啃不如照这一章自己写一个最小版本控制在三百行以内跑通之后再回头对照源码你会突然看懂它在干什么。最小系统只需要三个脚本玩家控制器、障碍生成器、游戏管理器。地面和背景用任意美术图拼一下即可。4.1 PlayerController匀速前进与跳跃判定先写玩家控制器。跑酷里玩家不手动控制前后水平速度是恒定的所以移动不用每帧加力而是直接给刚体一个水平速度跳跃则是一次性的向上冲量。注意这里必须用 RigidBody2D 而不是直接改 transform 位置否则碰撞回调不触发撞上障碍也检测不到。import { _decorator, Component, RigidBody2D, input, Input, EventKeyboard, KeyCode, Vec2, Collider2D, Contact2DType } from cc; const { ccclass, property } _decorator; ccclass(PlayerController) export class PlayerController extends Component { property({ type: RigidBody2D }) rigid: RigidBody2D; property moveSpeed 6; property jumpForce 12; property maxJumpCount 2; private jumpCount 0; private onGround false; start() { // 监听键盘跳跃 input.on(Input.EventType.KEY_DOWN, this.onKeyDown, this); // 监听碰撞体的接触开始和结束 const collider this.getComponent(Collider2D); collider.on(Contact2DType.BEGIN_CONTACT, this.onBeginContact, this); collider.on(Contact2DType.END_CONTACT, this.onEndContact, this); } update() { // 水平方向保持匀速垂直方向保留刚体现有速度 this.rigid.linearVelocity new Vec2(this.moveSpeed, this.rigid.linearVelocity.y); } onKeyDown(event: EventKeyboard) { if (event.keyCode KeyCode.SPACE) { this.tryJump(); } } private tryJump() { if (this.jumpCount this.maxJumpCount) return; this.rigid.applyLinearImpulseToCenter(new Vec2(0, this.jumpForce), true); this.jumpCount; } private onBeginContact() { this.onGround true; this.jumpCount 0; } private onEndContact() { this.onGround false; } }这段代码的关键点有三个。一是 update 里只修改水平速度不碰垂直速度这样跳跃产生的向上速度不会被每帧覆盖二是跳跃用 applyLinearImpulseToCenter而不是直接给 linearVelocity因为冲量是在当前速度上叠加二段跳时如果角色还在上升第二次跳跃会推得更高手感上更接近真正的二段跳三是 jumpCount 通过接触回调重置BEGIN_CONTACT 时清零END_CONTACT 时标记离地。参数上moveSpeed 对应水平推进jumpForce 是每次跳跃的冲量大小注意冲量还要乘刚体质量质量越大效果越弱。Cocos 编辑器里刚体默认质量与节点大小有关调 jumpForce 时如果手感变化不符合预期先检查质量是否被意外改过。maxJumpCount 设为 2 就是支持二段跳设回 1 就是普通单段跳。这里的另外一层坑是接触回调不会自动触发需要在项目设置的物理系统里开启 2D 物理并给玩家的碰撞体分组。后面避坑章会细讲。4.2 SpawnManager只在玩家视线外生成障碍生成器负责在玩家前方造障碍。最粗糙的做法是每隔固定秒数生成一个但移动速度一变间隔就不对了。更好的做法是按距离生成玩家每前进一定距离在前方固定偏移处刷一个障碍这样速度怎么改密度不变。import { _decorator, Component, Prefab, instantiate, Vec3 } from cc; const { ccclass, property } _decorator; ccclass(SpawnManager) export class SpawnManager extends Component { property({ type: Prefab }) obstaclePrefabs: Prefab[] []; property minGap 4; // 两个障碍之间的最小水平距离 property spawnOffsetX 10; // 在玩家前方多远生成 private player: Node; private lastSpawnX -100; setPlayer(node: Node) { this.player node; } update() { if (!this.player) return; const currentX this.player.position.x; if (currentX this.spawnOffsetX - this.lastSpawnX this.minGap) { this.spawnOne(currentX this.spawnOffsetX); this.lastSpawnX currentX this.spawnOffsetX; } } private spawnOne(x: number) { const idx Math.floor(Math.random() * this.obstaclePrefabs.length); const node instantiate(this.obstaclePrefabs[idx]); this.node.addChild(node); node.setPosition(new Vec3(x, 0, 0)); node.name obstacle_${x.toFixed(1)}; } }这里要说明的是 spawnOffsetX 和 minGap。spawnOffsetX 是生成点离玩家的距离跑酷里生成点必须在屏幕以外否则障碍是凭空出现的minGap 是最小间距它的数值必须大于最宽障碍物的碰撞体宽度否则两个障碍会叠在一起。若是支持二段跳minGap 还需要考虑跳跃落点范围大致是两个跳跃周期的水平移动距离保证每个障碍之间有地方落脚。把生成位置记录在 lastSpawnX 里而不是每次从场景里查找障碍列表避免了逐帧扫描节点。还有一个细节是 node.name 加上坐标调试时看场景树能直观看到生成顺序。如果发现刷出来的障碍经常连成墙优先检查 minGap而不是调概率随机。4.3 GameManager碰撞结束、积分与重开最后一个脚本管游戏状态。跑酷的状态很简单Running 和 GameOver。玩家撞到障碍后GameManager 停止游戏、弹结束面板、显示分数重开时重置玩家位置、清空场上障碍、把生成器状态归零。import { _decorator, Component, Node, Label } from cc; const { ccclass, property } _decorator; ccclass(GameManager) export class GameManager extends Component { property({ type: Node }) player: Node; property({ type: Label }) scoreLabel: Label; property({ type: Node }) gameOverPanel: Node; private score 0; private running false; start() { this.running true; this.gameOverPanel.active false; this.player.on(Hurt, this.onPlayerHurt, this); } update(dt: number) { if (!this.running) return; this.score dt * 10; this.scoreLabel.string Math.floor(this.score).toString(); } onPlayerHurt() { this.running false; this.gameOverPanel.active true; this.player.getComponent(PlayerController).enabled false; } restart() { this.score 0; this.running true; this.gameOverPanel.active false; this.player.getComponent(PlayerController).enabled true; this.player.setPosition(0, 0, 0); } }GameManager 不直接检测碰撞而是监听 player 上的 Hurt 事件。碰撞检测放回 PlayerController 的接触回调里碰到障碍物就发射 Hurt这样职责分界清楚。分数用 dt 累加比每帧加固定值更抗波动帧率不稳定时分数增速保持一致。restart 里重置玩家位置和脚本开关时别忘了清掉场上残留的障碍物否则重开后第一秒就可能撞上上一次的障碍。这个最小结构里SpawnManager 生成障碍、PlayerController 负责运动和碰撞反馈、GameManager 收尾三个脚本之间只通过事件和属性引用联系。到这一步你已经拥有了一个可玩的最小跑酷 Demo接下来所有的增强都在这三层上扩展。5. 跑酷源码常见问题排查五个翻车点从现象到解法这一章是重跑源码时最容易遇到的实际问题。每一条都是我见过的真实情况按现象、原因、解决三步写排错时优先对照自己的项目逐条排查。5.1 跳跃粘地角色跳起来瞬间被拽回地面现象按下跳跃键角色只离地一点就立刻落地看起来像被地面吸住。调试输出速度时能看到跳跃瞬间垂直速度确实变大了但下一帧就被压回零。原因最常见的是刚体的 Linear Damping线性阻尼被设得过大。线性阻尼会持续衰减刚体速度数值超过 2 时跳跃的初始速度在几帧内被耗光表现就是粘地。另一常见来源是代码里同时修改 transform.position 和刚体速度物理系统认为位置被外部改动会校正回去。解决把刚体组件的 Linear Damping 设为 0Angular Damping 也设为 0。如果项目需要阻尼来表现空气阻力把它控制在 0.1 以内再靠 gravityScale 调整手感而不是用阻尼来压跳跃高度。检查代码里是否在 update 中直接写玩家 transform 位置物理驱动对象的位置必须由刚体决定。5.2 二段跳变无限跳狂按空格角色一直往上飞现象按住空格不松角色连续跳个不停高度不断叠加直到飞出屏幕。单段跳模式下不出现二段跳必现。原因跳跃判定条件只写了 jumpCount 小于上限但 jumpCount 的重置依赖接触回调。如果角色在空中持续与某个碰撞体接触比如头顶擦过一个横梁BEGIN_CONTACT 反复触发jumpCount 被不断清零就变成了无限跳。解决用 onGround 标志位做第一道闸。BEGIN_CONTACT 时先判断碰撞方向只有从上方接触才认为是真正落地END_CONTACT 时再清除 onGround。跳跃时同时要求 onGround 为 true 或 jumpCount 小于 maxJumpCount。简单说跳一次就锁住直到真正落地再放开。另一个辅助手段是给按键做节流用 KEY_DOWN 事件而不是每帧检测 isDown。5.3 障碍物刷成墙随机生成却排成一堵墙现象游戏跑到中段屏幕上出现连续三个甚至更多障碍物紧挨在一起玩家没有任何反应时间。随机生成器看起来没有规律但就是经常扎堆。原因如果生成间隔是按时间算的生成器每隔固定秒数刷一次而玩家速度恰好较快时每次生成的障碍之间水平距离小于障碍宽度就会连成墙。另一个原因是 prefab 内部节点的碰撞体尺寸比视觉图大两个障碍视觉上没挨着碰撞体却重叠了。解决改用按距离生成即上一章的 SpawnManager 写法用 minGap 保证最小间距。同时把障碍 prefab 的碰撞体大小打印到日志里确认 minGap 大于最大碰撞体宽度。还有一个习惯值得推荐生成后延迟一帧再启用碰撞体避免障碍刚生成时就与玩家擦碰。5.4 相机抖动奔跑时画面上下晃现象玩家边跑边跳相机也跟着上下晃或者画面出现细微的拉锯抖动尤其在地形衔接处明显。静态截图看不出问题一跑起来就露馅。原因相机跟随脚本直接每帧把相机 position 设成玩家 position而玩家节点的 y 坐标在跳跃时变化剧烈相机就跟着颠簸。另外物理步进和渲染帧率不一致时直接赋值会产生微小的前瞻或滞后放大抖动。解决相机只锁定 x 轴、保留 y 偏移跳跃时画面不上下跟。需要用 lerp 插值做平滑时把插值系数稳定在 0.1 左右不要超过 0.3否则会有拖影感。更稳的做法是把相机节点的 y 单独固定x 使用 lerp这样既保证前方视野又不跟着跳跃抖。5.5 手机上速度比编辑器慢同一套参数两个平台表现不一致现象编辑器里速度刚好打包到手机后角色移动明显变慢或者反过来更快障碍生成频率也跟着变了。原因最常见的是移动代码没有乘 deltaTime。Cocos 里 RigidBody2D.linearVelocity 的数值是每秒速度本身不依赖帧率但如果移动是用 transform 手动改位置的就必须乘 dt。另一种情况是物理步进频率在手机上被压缩帧率低时物理模拟的步数少了跳跃高度和落地时间都会变。解决统一用物理驱动移动不要混用 transform 和 rigidbody。如果确认代码没问题再把物理系统的时间步长从默认值调小比如把 fixed timestep 设为 1/120消耗略有增加但物理一致性明显变好。调试时先在手机上跑一遍完整 60 秒对比编辑器里的死亡位置分布差异过大就先调物理步长而不是盲目改速度。6. 进阶动态难度、背景循环与配置化手感把 Demo 变成真正能让人玩下去的游戏有三个进阶方向值得投入它们都建立在前面的最小系统上改动量不大但对体验提升很明显。第一个是动态难度。固定难度下玩家玩三十秒就摸清规律之后就是机械重复。常见做法是以距离为自变量持续缩短障碍生成间隔、提高移动速度。速度每秒提升一个极小量间隔缩小有下限保证后期难度陡峭但不至于无解。实现时把这套逻辑放进 GameManager 的 update 里每帧根据当前分数重新计算生成间隔并写入 SpawnManager 的参数不需要改生成器逻辑。第二个是背景无缝循环。跑酷场景要让人感觉一直在前进就得让背景图块循环滚动。我习惯准备两张相同宽度的背景节点并排每帧让它们向玩家后方移动当一张完全离开屏幕时把它挪到另一张的右侧。注意移动速度要与玩家速度一致否则视觉上像背景在拖动而不是玩家在跑。背景的移动可以用简单的节点位移加取模运算实现不占用物理系统。第三个是我最想强调的配置化手感。把 moveSpeed、jumpForce、gravityScale、minGap 这些参数全部集中到一个 Config 节点上的脚本里PlayerController 和 SpawnManager 都在 start 时从 Config 读取数值。这样调手感时不用改逻辑代码纯粹在属性检查器里改参数。我现在的习惯是每次调完后记录一组参数快照到注释里哪天改崩了直接回滚。这三个方向做完你的跑酷源码就不再是“能跑的 Demo”而是一套可以拿去做玩法验证的基底。最后说一个我踩过的教训曾经为了追求难度曲线好看把速度曲线和障碍间隔写在两个脚本里各自累加结果改一个就要同步改另一个版本调了三天才对齐。现在所有难度相关的数值都由 GameManager 统一输出其他脚本只读不改再也没出现过这种问题。希望帮到你。本文还有配套的精品资源点击获取