简介:这是一份面向H5小游戏开发初学者与前端练手者的神庙逃亡类跑酷游戏完整源码,基于HTML5实现,可自适应电脑与手机双端运行,适合用于学习休闲游戏逻辑、移动端触控适配,也可作为引流型小游戏的二次开发基础。压缩包共53个文件,约7.07MB,包含20个png图像素材、8个atlas图集、6个fnt位图字体、6个js脚本、4个html页面,以及ogg音效、bat启动脚本与配置文件等,覆盖菜单、游戏、商店、暂停、结算等完整场景模块。目前已有591人学习下载。源码还原了神庙逃亡系列的核心元素,提供更刺激的跑酷地图;PC端支持方向键左右移动转向、跳跃与下滑,Esc暂停,手机端通过手指上下左右滑动控制方向,读者可据此研究双端输入适配、图集动画与关卡配置的组织方式,快速搭建可运行的跑酷小游戏原型。
1. 神庙逃亡H5小游戏源码:一份能直接跑在浏览器里的跑酷Demo
前阵子有个做前端的朋友接了个私活,客户想要一个能在手机浏览器里直接玩的跑酷小游戏,预算不高、周期一周。他翻了一圈开源仓库,最后拿到的就是这份神庙逃亡H5小游戏源码。我跟着拆了一遍,结论是:它不是那种只能截图发朋友圈的“演示壳子”,而是一套基于Canvas的完整跑酷逻辑,角色自动前进、左右换道、跳跃滑铲、障碍碰撞、金币收集、分数结算这些核心循环都在,改改素材和数值就能变成自己的东西。适合谁?想学H5游戏循环怎么搭的前端、需要快速出一个小游戏Demo的接单开发者、以及拿它当课程设计底稿的学生。它解决的不是“从零造引擎”的问题,而是“跑酷玩法的最小可运行闭环”这件事——你拿到手,先跑起来,再决定往哪个方向改。
2. 跑酷循环怎么转起来:从入口文件到三条赛道的坐标体系
2.1 先看清这套源码的骨架
拿到一份H5游戏源码,我一般不会急着改代码,而是先花十分钟把目录结构和入口文件摸清楚。这份神庙逃亡H5小游戏源码的组织方式比较典型:一个index.html作为壳,一个主逻辑JS文件承载游戏循环,资源目录里放角色、障碍、金币的贴图,样式文件负责全屏适配。它没有依赖打包工具,也没有npm install那一套,双击index.html或者起一个静态服务器就能跑。这一点对新手特别友好——你不需要先配一遍Webpack才能看到画面。
常见做法是先用本地静态服务器跑,而不是直接双击文件。原因是部分浏览器对file://协议下的图片加载和音频播放有限制,直接打开可能出现贴图空白。起服务器的命令很简单:
# 在源码根目录执行,任选一种 python3 -m http.server 8080 # 或者 npx serve .执行完浏览器访问http://localhost:8080即可。参数说明:端口8080可以换成任意未被占用的端口;如果你机器上Python是2.x,命令换成python -m SimpleHTTPServer 8080。这一步看着不起眼,但它是后面所有调试的前提,贴图加载不出来八成是协议问题而不是代码问题。
2.2 游戏主循环:requestAnimationFrame加时间步长
跑酷游戏的骨架就是一条循环:每帧更新角色位置、更新障碍物位置、检测碰撞、重绘画面。这份源码用的是requestAnimationFrame驱动,而不是setInterval。区别在哪?setInterval在标签页切到后台时仍会按固定间隔触发,回来时容易积压一堆回调导致角色瞬移;requestAnimationFrame跟随浏览器刷新节奏,切后台自动暂停,回来不会“补帧”。这是H5游戏里比较稳妥的选择。
核心循环的写法大致是这样:
// 主循环骨架,dt为两帧之间的时间差(秒) let lastTime = 0; function gameLoop(timestamp) { const dt = (timestamp - lastTime) / 1000; lastTime = timestamp; // 限制单帧最大步长,防止切后台回来时角色瞬移 const safeDt = Math.min(dt, 0.05); update(safeDt); // 更新逻辑:角色、障碍、金币 render(); // 重绘:清屏后按图层顺序绘制 requestAnimationFrame(gameLoop); } requestAnimationFrame(gameLoop);逻辑说明:dt是帧间隔,所有位移都要乘上它,这样不同刷新率的设备上角色速度才一致——60Hz和120Hz屏幕上跑出来的手感不会一个快一个慢。safeDt那行是血泪经验,不加的话用户切出去回个消息再回来,角色可能已经撞墙了。参数上,0.05秒是个经验上限,约等于20帧,再大的间隔直接丢弃。update和render分离是基本纪律,逻辑更新和绘制混在一起,后面加暂停、加结算界面会非常难受。
2.3 三条赛道与坐标映射
神庙逃亡类玩法的空间模型是“三条纵向赛道”。源码里通常用一个lane变量表示当前赛道,取值0、1、2,对应屏幕横向的三个位置。角色左右滑动时lane加减1并做边界钳制,实际绘制坐标由lane乘以赛道宽度再叠加偏移算出来。这个设计的好处是把“逻辑位置”和“屏幕位置”解耦了——你改屏幕适配、改赛道宽度,逻辑层完全不用动。
const LANE_COUNT = 3; let currentLane = 1; // 初始在中间道 function moveLane(direction) { // direction: -1 左移,1 右移 const next = currentLane + direction; if (next < 0 || next >= LANE_COUNT) return; // 边界钳制,不越界 currentLane = next; } function laneToX(lane, canvasWidth) { const laneWidth = canvasWidth / LANE_COUNT; return laneWidth * lane + laneWidth / 2; // 取赛道中心点 }参数说明:LANE_COUNT固定为3,想改成五赛道跑酷就改这个常量,但障碍物生成逻辑也要同步改,否则会出现某条道永远没障碍的尴尬。laneToX返回的是赛道中心横坐标,角色贴图绘制时还要减去自身宽度的一半才是左上角。这里有个容易翻车的点:如果角色贴图不是居中对齐的,换道时视觉上会“偏”,看起来像没对准赛道,实际是绘制锚点没处理。
3. 障碍、金币与碰撞:判定盒怎么设才不冤枉玩家
3.1 障碍物生成与回收池
跑酷游戏不能无限new对象,否则跑几分钟内存就上去了。这份源码里障碍物和金币都用了对象池的思路:预先生成一批对象,用的时候从池里取,不用的时候标记为失效而不是销毁。障碍物从远处(屏幕上方或纵深方向)生成,随游戏推进向玩家方向移动,移出屏幕后回收。生成节奏一般按“距离间隔”而不是“时间间隔”来控制,这样角色速度变化时障碍密度才稳定。
const obstaclePool = []; const POOL_SIZE = 20; // 初始化池 for (let i = 0; i < POOL_SIZE; i++) { obstaclePool.push({ active: false, lane: 0, z: 0, type: 'block' }); } function spawnObstacle() { const obj = obstaclePool.find(o => !o.active); if (!obj) return; // 池满则跳过本次生成,避免无限增长 obj.active = true; obj.lane = Math.floor(Math.random() * 3); obj.z = 1; // 1表示最远处,向0逼近 obj.type = Math.random() > 0.7 ? 'low' : 'block'; // low可滑铲通过 }逻辑说明:z是纵深归一化坐标,1为最远、0为玩家所在平面,每帧按速度递减。type区分高障碍(必须换道或跳)和低障碍(可滑铲),这是跑酷玩法层次感的来源。参数上POOL_SIZE给20是够用的,但如果你的赛道变长、速度变快,池子要相应加大,否则会出现“该出障碍时池子空了”导致难度忽高忽低。这个细节不看代码根本想不到,属于典型的黑匣子。
3.2 碰撞判定:别用整张贴图做判定盒
新手最容易犯的错是拿角色贴图的完整矩形去和障碍物矩形做相交检测,结果就是“明明看着还有缝隙却判定撞了”,玩家体验极差。正确做法是给角色和障碍物各设一个比贴图小的判定盒,通常角色判定盒收窄到贴图的60%左右,障碍物按实际视觉体积设。这份源码里判定逻辑是AABB(轴对齐包围盒)相交,配合赛道编号先做一次粗筛——不同赛道的物体直接跳过检测,省掉大量无谓计算。
function isColliding(player, obstacle) { // 先按赛道粗筛,不同道不可能碰撞 if (player.lane !== obstacle.lane) return false; // 纵深方向接近时才做精细判定 if (Math.abs(player.z - obstacle.z) > 0.08) return false; // 判定盒收窄:角色宽高各取贴图的0.6 const pw = player.width * 0.6, ph = player.height * 0.6; const ox = obstacle.x, oy = obstacle.y; return ( player.x < ox + obstacle.width && player.x + pw > ox && player.y < oy + obstacle.height && player.y + ph > oy ); }参数说明:0.08是纵深容差,调大判定更宽松、调小更苛刻,这个值直接决定游戏“手感”,建议在0.05到0.12之间试。判定盒系数0.6是经验值,角色贴图边缘通常有透明留白,按1.0算必然冤枉玩家。跳跃和滑铲状态下判定盒的纵向位置要跟着变,否则会出现“跳起来了还被地面障碍撞到”的玄学现象——这多半是判定盒没随状态更新。
3.3 金币收集与分数结算
金币的逻辑比障碍简单:同样走对象池,与角色判定盒相交即标记收集、播放音效、分数加一。分数结算一般在角色碰撞障碍后触发,冻结游戏循环、弹出结算面板、显示本局距离和金币数。这里有个小设计值得说:距离分数和金币分数分开累计,距离按角色前进的累计路程算,金币按收集数算,最后加权求和。这样玩家有两条成长线,不会只盯着金币。
let distanceScore = 0; let coinScore = 0; function updateScore(dt, speed) { distanceScore += speed * dt * 10; // 距离分,系数10用于放大数值观感 } function onCoinCollected() { coinScore += 1; // 音效与粒子反馈在这里触发 } function finalScore() { return Math.floor(distanceScore) + coinScore * 50; // 金币权重50 }参数说明:距离系数10和金币权重50都是可调的“数值手感”参数,前者影响分数增长快慢,后者影响玩家对金币的重视程度。调这两个数不需要改任何逻辑,属于策划层面的调整。结算面板弹出时记得把requestAnimationFrame的循环停掉或者置一个paused标志,否则背景还在跑,视觉上很乱。
4. 避坑与排查:跑不起来、卡顿、判定玄学的五条记录
4.1 打开一片空白,控制台报跨域
现象:双击index.html,页面白屏,控制台出现图片加载失败或CORS相关报错。原因:file://协议下浏览器把本地文件当作不同源处理,贴图和音频加载被拦。解决:按2.1节起一个本地静态服务器,用http://localhost访问,问题基本消失。这条踩坑率极高,尤其是习惯直接双击HTML的开发者。
4.2 角色速度在不同手机上不一致
现象:同一份代码,在高刷屏手机上角色跑得飞快,在老设备上慢吞吞。原因:位移直接用了固定像素值,没有乘帧间隔dt。解决:把所有位移、旋转、动画推进都改成“速率乘以dt”的形式,并在主循环里对dt做上限钳制。改完之后不同刷新率下手感才统一,这是H5游戏的基本功。
4.3 玩几分钟后越来越卡
现象:开局流畅,跑两三分钟后帧率明显下降。原因:障碍物或金币对象只创建不回收,数组越堆越长,每帧遍历成本上升。解决:检查是否用了对象池、失效对象是否被正确标记并复用。如果源码里某处是array.push新对象而从不清理,把它改成池化取用。顺带检查有没有在循环里反复创建临时对象,那也是GC压力的来源。
4.4 明明跳过去了却判定撞上
现象:玩家操作跳跃,视觉上角色已经越过障碍,仍然触发碰撞。原因:判定盒没有随跳跃状态更新纵向位置,或者判定盒用了整张贴图尺寸。解决:跳跃时同步更新角色的判定盒y坐标,并把判定盒收窄到贴图的0.6左右;同时确认障碍物的判定盒也做了收窄。调完在低速下多试几次,手感会明显不同。
4.5 切后台回来角色瞬移撞墙
现象:游戏中切到其他标签页,回来瞬间角色已经撞上障碍。原因:requestAnimationFrame暂停期间时间仍在流逝,回来时第一帧的dt极大,位移一步跨过头。解决:主循环里对dt做Math.min(dt, 0.05)钳制,或者监听visibilitychange事件在页面隐藏时暂停、显示时重置lastTime。两种做法可以叠加,后者更彻底。
5. 二次开发与验证:把Demo改成自己的跑酷需要动哪几处
拿到这份源码,多数人不会原样用,而是想换成自己的美术、调自己的难度曲线。我一般按“先换皮、再调参、最后加玩法”的顺序推进,每一步都能独立验证,出问题好定位。
换皮是最安全的一步:把资源目录里的角色、障碍、金币贴图替换成同尺寸的图,文件名保持一致,刷新就能看到效果。这里要注意贴图的透明通道和锚点,新图如果留白比例和原图差太多,判定盒系数要跟着微调,否则又回到“冤枉玩家”的老问题。验证方法很简单:开一局,看角色换道时是否居中、跳跃时脚底是否贴地。
调参是第二步,集中在三个地方:角色前进速度、障碍生成间隔、判定容差。建议做一个简单的参数表放在代码顶部,方便反复试:
| 参数 | 作用 | 建议范围 | 调大后的效果 |
|---|---|---|---|
| baseSpeed | 角色基础前进速度 | 0.3~0.8 | 整体节奏变快,难度上升 |
| spawnGap | 障碍生成距离间隔 | 0.15~0.4 | 障碍变稀疏,难度下降 |
| zTolerance | 纵深判定容差 | 0.05~0.12 | 判定更宽松,更“宽容” |
| hitboxScale | 判定盒缩放系数 | 0.5~0.7 | 判定盒变大,更容易撞 |
调参时一次只动一个,动完跑三局感受差异,别一次改四个然后不知道是哪个起的作用。这是我在多个小游戏项目里养成的习惯,看着笨但省时间。
加玩法是最后一步,也是最能体现差异化的地方。常见扩展方向:加磁铁道具吸附金币、加护盾抵挡一次碰撞、加连续换道的连击加分。加道具时复用现有的对象池和碰撞框架,别另起一套逻辑,否则代码会迅速失控。验证方法:每加一个玩法,先单独测它是否生效,再测它和原有碰撞、结算的交互,最后跑一局完整流程看分数结算是否正确。
有个容易被忽略的验证点:结算面板弹出后,再点“重新开始”,所有状态是否被正确重置——分数归零、对象池清空、角色回到中间道、速度回到初始值。我见过不少Demo在这里翻车,第二局开局分数还带着上一局的尾巴。从那以后我每次改完结算逻辑,都会强制走一遍“开局—撞死—重开—再撞死”的完整闭环,确认两局数据互不污染。希望这份拆解能帮到你,少走几段我踩过的弯路。
本文还有配套的精品资源,点击获取