基于Phaser的纯js小游戏开发

从一个网页小游戏合集,看 Phaser 项目的分层设计:SGame 架构与实现原理

很多小游戏项目最初都很简单:创建一个 Canvas,监听键盘事件,然后在循环里不断更新坐标。可当游戏数量从一个增加到六个,并且还要兼容桌面键盘、手机触控、中英文切换、关卡切换和自动化测试时,继续把所有逻辑写在同一个文件里,很快就会变得难以维护。

SGame 是一个基于 Vite、原生 JavaScript 和 Phaser 3 开发的网页小游戏合集,目前包含俄罗斯方块、贪吃蛇、玛丽跳跃、坦克大战、保卫萝卜和连连看。本文不逐行介绍源码,而是从架构角度说明它如何组织多个游戏,并重点分析俄罗斯方块和玛丽跳跃的实现原理。

一、整体架构:把“游戏规则”和“画面表现”分开

SGame 最核心的设计原则可以概括为一句话:

game/ 决定游戏怎么算,scenes/ 决定游戏如何输入、运动和显示。

项目的主要目录如下:

src/
├── audio/                  # 程序化音效
├── game/                   # 不依赖 Phaser 画面的规则层
│   ├── tetris/
│   ├── snake/
│   ├── mary-jump/
│   ├── tank/
│   ├── carrot-defense/
│   └── link-match/
├── scenes/                 # Phaser 场景与表现层
├── ui/                     # 手机触控与公共 UI
├── i18n.js                 # 中英文文本
├── main.js                 # Phaser 初始化和场景注册
└── styles.css              # 页面与移动端布局

整个运行关系可以表示为:

flowchart LR INPUT["键盘 / 触屏 / 摇杆"] --> SCENE["Phaser Scene"] SCENE --> MODEL["纯 JavaScript 游戏规则"] MODEL --> STATE["棋盘、分数、生命、胜负状态"] STATE --> SCENE SCENE --> RENDER["Canvas 绘制、物理、动画、音效"] CONFIG["关卡与数值配置"] --> MODEL CONFIG --> SCENE I18N["中英文文本"] --> SCENE MOBILE["MobileControls"] --> SCENE

这种分层带来三个直接好处。

第一,规则可以脱离浏览器测试。例如俄罗斯方块的移动、旋转、消行不需要启动 Phaser,只要实例化 TetrisGame 就能验证结果。

第二,输入方式不会污染核心规则。键盘左键、手机方向按钮和摇杆最终都只是调用同一个 move(-1),模型不需要知道操作来自哪里。

第三,表现层可以自由调整。移动端画布尺寸、像素纹理、摄像机跟随和爆炸动画都可以改变,而不必重写计分和胜负逻辑。

二、场景如何组织一款游戏

Phaser 中每款游戏对应一个 Scene。场景通常负责以下工作:

  1. create() 中创建角色、图形、物理组和 HUD;
  2. 绑定键盘与手机触控;
  3. update() 中读取持续输入并推进游戏;
  4. 调用规则模型修改状态;
  5. 根据模型状态重绘画面、播放音效或切换关卡。

main.js 负责注册所有场景。大厅本身也是一个场景,玩家选择游戏后,通过 scene.start() 切换到目标 Scene。每个游戏退出时再回到大厅,因此六款游戏共享同一个 Phaser 实例,而不是创建六个独立网页。

移动端由 MobileControls 提供统一控制面板。不同游戏只声明自己需要哪些按钮:俄罗斯方块需要左右、下降、旋转和直落;玛丽需要移动、跳跃和发射;保卫萝卜与连连看则主要依靠点击画布,只保留暂停、重开和返回按钮。

三、俄罗斯方块:二维棋盘上的状态演算

俄罗斯方块很适合展示“规则层与表现层分离”的价值。它看起来是一个动画游戏,本质上却是一组二维数组运算。

1. 棋盘与方块如何表示

棋盘是一个 10×20 的二维数组。空位置保存 null,已经固定的方块保存颜色值。

七种方块则由若干局部坐标组成。例如 T 方块可以表示为:

T: {
  cells: [[1, 0], [0, 1], [1, 1], [2, 1]]
}

活动方块还拥有自己的 xy。绘制或碰撞时,把局部坐标与方块位置相加,就能得到每个格子在棋盘中的真实坐标。

SGame 使用“七袋随机”生成方块:把七种方块打乱后逐个取出,用完再重新打乱。这样既保留随机性,也避免玩家长时间等不到某一种方块。

2. 所有移动都先生成候选状态

俄罗斯方块没有直接修改位置,而是先构造一个候选方块:

const candidate = {
  ...activePiece,
  x: activePiece.x + direction,
};

if (!collides(candidate)) {
  activePiece.x = candidate.x;
}

collides() 会检查候选方块是否越过左右边界、落到棋盘底部,或者与已经固定的格子重叠。只有候选状态合法,才提交本次移动。

旋转也是同样的思路:先计算旋转后的坐标,再依次尝试 0、-1、1、-2、2 的水平偏移。这是一种简化的踢墙机制,方块靠近墙壁时仍有机会完成旋转。

3. 自动下落与键盘输入为何不会冲突

左右键、旋转和直落使用键盘事件立即触发;自动下落则由 Scene 的 update() 累计 delta 时间。

dropAccumulator += delta;

if (dropAccumulator >= model.dropInterval) {
  model.tick();
  dropAccumulator = 0;
}

浏览器中的 JavaScript 是单线程执行的,因此“左移”和“自动下落”不会同时争抢坐标。它们会按顺序发生:先尝试更新 x,随后自动下落再基于新的位置尝试更新 y。左移失败也不会阻止下落。

手机方向按钮在按下时立即移动一次,持续按住后由 update() 按固定间隔连发;摇杆则在每一帧读取方向。这使不同设备上的触控手感更加稳定。

4. 固定、消行与计分

stepDown() 发现下方发生碰撞时,活动方块不能继续下降,模型会执行以下流程:

写入棋盘 → 检查满行 → 删除满行 → 顶部补空行
→ 更新分数和等级 → 生成下一个方块

一次消除一至四行分别使用不同的基础分数,再乘以当前等级。每累计消除十行提升一级,同时缩短自动下落间隔,最低可压缩到 90 毫秒。

幽灵落点也没有单独维护状态,而是复制当前方块,不断尝试 y + 1,直到下一步发生碰撞。Scene 再用半透明描边绘制这个位置。

因此,俄罗斯方块的画面只是模型状态的投影:模型负责证明“这个位置是否合法”,Scene 负责把合法结果画出来。

四、玛丽跳跃:配置驱动的横版物理世界

如果说俄罗斯方块是离散的格子运算,那么玛丽跳跃则是连续的物理模拟。它使用 Phaser Arcade Physics 处理重力、速度、静态平台和重叠检测。

1. 关卡不是手写场景,而是配置数据

玛丽目前包含十个关卡。每关配置宽度、重力、敌人速度、出生点、终点、平台、金币和道具。

关卡生成器先创建七段地面,在地面之间留下沟壑,再按照高度模板生成悬浮平台。随着关卡编号变化,道路间距、世界宽度、重力和敌人速度逐步增加。

Scene 读取配置后创建物理世界:

配置数据
→ 静态平台组
→ 玩家精灵
→ 金币与道具组
→ 巡逻敌人组
→ 终点旗帜

摄像机跟随玩家移动,而 HUD 使用固定滚动系数,因此关卡背景在滚动,分数、关卡名和提示仍停留在屏幕上。

2. 为什么跳跃需要缓冲和土狼时间

如果只允许“角色接触地面时按下跳跃”,操作会非常生硬:玩家可能提前几十毫秒按键,或者刚离开平台边缘才按键,游戏都会拒绝跳跃。

SGame 增加了两种宽容机制:

  • 跳跃缓冲:按键后的 160 毫秒内持续保留跳跃意图;
  • 土狼时间:离开平台后的 110 毫秒内仍允许起跳。

当收到跳跃输入时,Scene 并不一定立刻修改速度,而是记录 jumpQueuedUntil。随后 consumeQueuedJump() 同时检查缓冲是否有效,以及角色是否着地或仍处于土狼时间内。

普通跳跃和大跳共用这一套流程,只是向上的初速度不同。键盘可以使用独立的大跳键,手机端也支持连续触发形成大跳操作。

这类设计不会改变关卡规则,却会显著改善游戏手感,是平台游戏中非常实用的细节。

3. 敌人、踩踏与受伤判定

敌人会在配置指定的 minXmaxX 之间巡逻。碰到边界或墙体后,Scene 反转其水平速度。

玩家与敌人重叠时,不能简单地一律判负。规则层通过两个条件判断是否踩踏成功:

  1. 玩家正在下降或接近跳跃顶点;
  2. 玩家位置明显高于敌人。

踩踏成功后,敌人被销毁,玩家获得向上的反弹速度并增加分数。否则进入受伤流程:小玛丽挑战失败;大玛丽或火焰玛丽先退回普通状态,并获得短暂无敌时间。

4. 蘑菇、火焰花与火球

道具同样来自关卡配置。吃到蘑菇后,Scene 调整角色显示尺寸并进入“大玛丽”状态;吃到火焰花后进入火焰状态,允许创建火球。

火球拥有发射冷却和水平速度,与敌人重叠后销毁双方并结算击敌分数。能力状态可以带入下一关,而重试当前关卡时恢复到该关开始时的状态。

这说明关卡切换不能只传递分数,还要明确哪些状态属于“全局进度”,哪些状态应该在失败后回滚。SGame 在启动下一场景时传递关卡编号、分数和能力状态,从而把这些规则表达得比较清楚。

5. 用阶段状态控制复杂流程

玛丽场景通过 phase 区分不同阶段:

playing → paused
playing → lost
playing → stage-clear → 下一关
playing → complete

update() 只在 playing 阶段处理移动和敌人行为。这样暂停、失败和过关提示出现时,不会继续执行物理输入,也不会重复触发结算。

五、移动端与多游戏共存的工程处理

网页小游戏不能只考虑桌面浏览器。SGame 针对移动端做了几项统一处理:

  • 根据游戏切换方向按钮或摇杆,并分别保存操作模式;
  • 为不同场景设置独立画布尺寸,避免手机端内容过小或被裁切;
  • 禁止按钮长按触发文本选择、翻译和浏览器菜单;
  • 在部分移动浏览器上使用 Canvas 渲染,降低 WebGL 纹理导致黑屏的风险;
  • 对横竖屏转换后的点击坐标进行修正,保证塔防和连连看的画布点击仍然准确;
  • 所有按钮、HUD 和游戏提示通过统一的 i18n 字典切换中英文。

这些功能都放在公共层中,而不是复制到每个游戏里。新增游戏时,只要声明控制配置、注册 Scene 并补充翻译文本,就能复用现有移动端能力。

六、为什么规则层值得单独测试

动画和画面适合通过浏览器验证,但很多错误其实属于纯规则问题。例如:

  • 方块能否穿过边界;
  • 旋转失败后是否错误修改坐标;
  • 消行后棋盘是否仍保持 20 行;
  • 玛丽从侧面碰到敌人是否被误判为踩踏;
  • 道具得分和关卡数量是否正确。

SGame 使用 Node 的断言测试直接实例化各游戏模型。测试不需要加载图片、创建 Canvas 或启动物理引擎,因此执行速度快,也更容易定位问题。

结语

SGame 的代码规模并不算庞大,但它已经具备一个多游戏项目需要面对的大多数问题:规则与表现分离、场景管理、配置驱动关卡、统一输入、移动端适配、国际化和自动化测试。

俄罗斯方块展示了如何把游戏还原成可验证的离散状态变化;玛丽跳跃则展示了如何在物理引擎之上组织关卡、输入宽容、角色能力和阶段状态。

对小游戏项目来说,架构的目的不是增加抽象层,而是让每个新玩法都能沿着清晰的路径加入:规则放在规则层,画面交给 Scene,设备差异交给公共控制层。这样项目从一款游戏扩展到六款时,仍然可以保持可读、可测和可继续演进。

开源代码:

https://github.com/hdwang123/sgame
https://gitee.com/hdwang123/sgame

posted @ 2026-07-02 09:39  追极  阅读(12)  评论(0)    收藏  举报