mcpField
P
phaser-gamedev

phaser-gamedev

github.com/chongdashu开发371.1k 次安装41 周前更新
代码仓库

关于 phaser-gamedev

面向 Phaser 3 的 2D 浏览器游戏开发 Skill,指导开发者设计场景、物理、资源和输入,并组织可维护的游戏结构。

主要功能

  1. 01基于场景的游戏架构
  2. 02Arcade 与 Matter 物理指导
  3. 03键盘与指针输入
  4. 04资源加载与 Tilemap
  5. 05动画与性能实践

适用场景

  1. 01规划场景与游戏状态
  2. 02实现 2D 碰撞和移动
  3. 03加载精灵表与 Tiled 地图
  4. 04规避常见 Phaser 实现问题

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/chongdashu/phaserjs-tinyswords --skill phaser-gamedev

安装后在 Claude Code 中输入/phaser-gamedev确认已加载。

包含的文件

除 SKILL.md 外,安装时一起下载的参考资料和脚本。

references/
arcade-physics.md
performance.md
spritesheets-nineslice.md
tilemaps.md

详细介绍

Phaser 游戏开发

使用 Phaser 3 基于场景的架构和物理系统,快速构建精致的 2D 浏览器游戏。

理念:将游戏视为动态系统

游戏不是静态 UI,而是动态系统:实体彼此交互,状态不断演变,玩家输入驱动一切。编写代码前,先进行架构设计。

开始构建前,请思考:

  • 这个游戏需要哪些场景?(启动、菜单、游戏、暂停、游戏结束)
  • 有哪些实体,它们如何交互?
  • 哪些状态需要在场景间保留?
  • 哪种物理模型最合适?(追求速度用 Arcade,追求真实感用 Matter)
  • 玩家会使用哪些输入方式?

核心原则:

  1. 场景优先架构:围绕场景组织游戏,而非全局状态
  2. 组合优于继承:使用游戏对象和组件构建实体
  3. 考虑物理的设计:编写碰撞代码前选择物理系统
  4. 严谨管理资源管线:预加载所有资源,并通过键引用
  5. 与帧率无关:使用增量时间,而非帧计数

游戏配置

每个 Phaser 游戏都从一个配置对象开始。

最简配置
const config = {
  type: Phaser.AUTO,           // WebGL with Canvas fallback
  width: 800,
  height: 600,
  scene: [BootScene, GameScene]
};

const game = new Phaser.Game(config);
完整配置模式
const config = {
  type: Phaser.AUTO,
  width: 800,
  height: 600,
  parent: 'game-container',    // DOM element ID
  backgroundColor: '#2d2d2d',

  scale: {
    mode: Phaser.Scale.FIT,
    autoCenter: Phaser.Scale.CENTER_BOTH
  },

  physics: {
    default: 'arcade',
    arcade: {
      gravity: { y: 300 },
      debug: false              // Enable during development
    }
  },

  scene: [BootScene, MenuScene, GameScene, GameOverScene]
};
物理系统选择
系统适用场景
Arcade平台游戏、射击游戏和大多数 2D 游戏。速度快,AABB 碰撞简单
Matter物理谜题、布娃娃和真实碰撞。速度较慢,但精度更高
无菜单场景、视觉小说、卡牌游戏

场景架构

场景是基本的组织单位。每个场景都有生命周期。

场景生命周期方法
class GameScene extends Phaser.Scene {
  constructor() {
    super('GameScene');        // Scene key for reference
  }

  init(data) {
    // Called first. Receive data from previous scene
    this.level = data.level || 1;
  }

  preload() {
    // Load assets. Runs before create()
    this.load.image('player', 'assets/player.png');
    this.load.spritesheet('enemy', 'assets/enemy.png', {
      frameWidth: 32, frameHeight: 32
    });
  }

  create() {
    // Set up game objects, physics, input
    this.player = this.physics.add.sprite(100, 100, 'player');
    this.cursors = this.input.keyboard.createCursorKeys();
  }

  update(time, delta) {
    // Game loop. Called every frame
    // delta = milliseconds since last frame
    this.player.x += this.speed * (delta / 1000);
  }
}
场景切换
// Start a new scene (stops current)
this.scene.start('GameOverScene', { score: this.score });

// Launch scene in parallel (both run)
this.scene.launch('UIScene');

// Pause/resume scenes
this.scene.pause('GameScene');
this.scene.resume('GameScene');

// Stop a scene
this.scene.stop('UIScene');
推荐的场景结构
scenes/
├── BootScene.js      # Asset loading, progress bar
├── MenuScene.js      # Title screen, options
├── GameScene.js      # Main gameplay
├── UIScene.js        # HUD overlay (launched parallel)
├── PauseScene.js     # Pause menu overlay
└── GameOverScene.js  # End screen, restart option

游戏对象

Phaser 中所有可见内容都是游戏对象。

常见游戏对象
// Images (static)
this.add.image(400, 300, 'background');

// Sprites (can animate, physics-enabled)
const player = this.add.sprite(100, 100, 'player');

// Text
const score = this.add.text(16, 16, 'Score: 0', {
  fontSize: '32px',
  fill: '#fff'
});

// Graphics (draw shapes)
const graphics = this.add.graphics();
graphics.fillStyle(0xff0000);
graphics.fillRect(100, 100, 50, 50);

// Containers (group objects)
const container = this.add.container(400, 300, [sprite1, sprite2]);

// Tilemaps
const map = this.make.tilemap({ key: 'level1' });
精灵创建模式
// Basic sprite
const sprite = this.add.sprite(x, y, 'textureKey');

// Sprite with physics body
const sprite = this.physics.add.sprite(x, y, 'textureKey');

// From spritesheet frame
const sprite = this.add.sprite(x, y, 'sheet', frameIndex);

// From atlas
const sprite = this.add.sprite(x, y, 'atlas', 'frameName');

物理系统

Arcade 物理(推荐默认选项)

适用于大多数 2D 游戏的快速、简单物理系统。

// Enable physics on sprite
this.physics.add.sprite(x, y, 'player');

// Or add physics to existing sprite
this.physics.add.existing(sprite);

// Configure body
sprite.body.setVelocity(200, 0);
sprite.body.setBounce(0.5);
sprite.body.setCollideWorldBounds(true);
sprite.body.setGravityY(300);

// Collision detection
this.physics.add.collider(player, platforms);
this.physics.add.overlap(player, coins, collectCoin, null, this);

function collectCoin(player, coin) {
  coin.disableBody(true, true);  // Remove from physics and hide
  this.score += 10;
}
物理组
// Static group (platforms, walls)
const platforms = this.physics.add.staticGroup();
platforms.create(400, 568, 'ground').setScale(2).refreshBody();

// Dynamic group (enemies, bullets)
const enemies = this.physics.add.group({
  key: 'enemy',
  repeat: 5,
  setXY: { x: 100, y: 0, stepX: 70 }
});

enemies.children.iterate(enemy => {
  enemy.setBounce(Phaser.Math.FloatBetween(0.4, 0.8));
});
Matter 物理

用于真实的物理模拟。

// Config
physics: {
  default: 'matter',
  matter: {
    gravity: { y: 1 },
    debug: true
  }
}

// Create bodies
const ball = this.matter.add.circle(400, 100, 25);
const box = this.matter.add.rectangle(400, 400, 100, 50, { isStatic: true });

// Sprite with Matter body
const player = this.matter.add.sprite(100, 100, 'player');
player.setFriction(0.005);
player.setBounce(0.9);

输入处理

键盘输入
// Cursor keys
this.cursors = this.input.keyboard.createCursorKeys();

// In update()
if (this.cursors.left.isDown) {
  player.setVelocityX(-160);
} else if (this.cursors.right.isDown) {
  player.setVelocityX(160);
}

if (this.cursors.up.isDown && player.body.touching.down) {
  player.setVelocityY(-330);  // Jump
}

// Custom keys
this.spaceKey = this.input.keyboard.addKey(Phaser.Input.Keyboard.KeyCodes.SPACE);

// Key events
this.input.keyboard.on('keydown-SPACE', () => {
  this.fire();
});
指针/鼠标输入
// Click/tap
this.input.on('pointerdown', (pointer) => {
  console.log(pointer.x, pointer.y);
});

// Make object interactive
sprite.setInteractive();
sprite.on('pointerdown', () => {
  sprite.setTint(0xff0000);
});
sprite.on('pointerup', () => {
  sprite.clearTint();
});

// Drag
this.input.setDraggable(sprite);
this.input.on('drag', (pointer, obj, dragX, dragY) => {
  obj.x = dragX;
  obj.y = dragY;
});

动画

创建动画
// In create() - define once
this.anims.create({
  key: 'walk',
  frames: this.anims.generateFrameNumbers('player', { start: 0, end: 3 }),
  frameRate: 10,
  repeat: -1  // Loop forever
});

this.anims.create({
  key: 'jump',
  frames: [{ key: 'player', frame: 4 }],
  frameRate: 20
});

// From atlas
this.anims.create({
  key: 'explode',
  frames: this.anims.generateFrameNames('atlas', {
    prefix: 'explosion_',
    start: 1,
    end: 8,
    zeroPad: 2
  }),
  frameRate: 16,
  hideOnComplete: true
});
播放动画
// Play animation
sprite.anims.play('walk', true);  // true = ignore if already playing

// Play once
sprite.anims.play('jump');

// Stop
sprite.anims.stop();

// Animation events
sprite.on('animationcomplete', (anim, frame) => {
  if (anim.key === 'die') {
    sprite.destroy();
  }
});

资源加载

预加载模式
preload() {
  // Images
  this.load.image('sky', 'assets/sky.png');

  // Spritesheets
  this.load.spritesheet('player', 'assets/player.png', {
    frameWidth: 32,
    frameHeight: 48
  });

  // Atlases (TexturePacker)
  this.load.atlas('sprites', 'assets/sprites.png', 'assets/sprites.json');

  // Tilemaps
  this.load.tilemapTiledJSON('map', 'assets/level1.json');
  this.load.image('tiles', 'assets/tileset.png');

  // Audio
  this.load.audio('bgm', 'assets/music.mp3');
  this.load.audio('sfx', ['assets/sound.ogg', 'assets/sound.mp3']);

  // Progress tracking
  this.load.on('progress', (value) => {
    console.log(`Loading: ${Math.round(value * 100)}%`);
  });
}
启动场景模式
class BootScene extends Phaser.Scene {
  constructor() {
    super('BootScene');
  }

  preload() {
    // Loading bar
    const width = this.cameras.main.width;
    const height = this.cameras.main.height;

    const progressBar = this.add.graphics();
    const progressBox = this.add.graphics();
    progressBox.fillStyle(0x222222, 0.8);
    progressBox.fillRect(width/2 - 160, height/2 - 25, 320, 50);

    this.load.on('progress', (value) => {
      progressBar.clear();
      progressBar.fillStyle(0xffffff, 1);
      progressBar.fillRect(width/2 - 150, height/2 - 15, 300 * value, 30);
    });

    // Load all game assets here
    this.load.image('player', 'assets/player.png');
    // ... more assets
  }

  create() {
    this.scene.start('MenuScene');
  }
}

瓦片地图(Tiled 集成)

加载与创建
preload() {
  this.load.tilemapTiledJSON('map', 'assets/map.json');
  this.load.image('tiles', 'assets/tileset.png');
}

create() {
  const map = this.make.tilemap({ key: 'map' });
  const tileset = map.addTilesetImage('tileset-name-in-tiled', 'tiles');

  // Create layers (match names from Tiled)
  const backgroundLayer = map.createLayer('Background', tileset, 0, 0);
  const groundLayer = map.createLayer('Ground', tileset, 0, 0);

  // Enable collision on specific tiles
  groundLayer.setCollisionByProperty({ collides: true });
  // Or by tile index
  groundLayer.setCollisionBetween(1, 100);

  // Add collision with player
  this.physics.add.collider(this.player, groundLayer);
}
对象图层
// Spawn points from Tiled object layer
const spawnPoint = map.findObject('Objects', obj => obj.name === 'spawn');
this.player = this.physics.add.sprite(spawnPoint.x, spawnPoint.y, 'player');

// Create objects from layer
const coins = map.createFromObjects('Objects', {
  name: 'coin',
  key: 'coin'
});
this.physics.world.enable(coins);

项目结构

推荐的组织方式
game/
├── src/
│   ├── scenes/
│   │   ├── BootScene.js
│   │   ├── MenuScene.js
│   │   ├── GameScene.js
│   │   └── UIScene.js
│   ├── gameObjects/
│   │   ├── Player.js
│   │   ├── Enemy.js
│   │   └── Collectible.js
│   ├── systems/
│   │   ├── InputManager.js
│   │   └── AudioManager.js
│   ├── config/
│   │   └── gameConfig.js
│   └── main.js
├── assets/
│   ├── images/
│   ├── audio/
│   ├── tilemaps/
│   └── fonts/
├── index.html
└── package.json
ES 模块设置
// main.js
import Phaser from 'phaser';
import BootScene from './scenes/BootScene';
import GameScene from './scenes/GameScene';
import { gameConfig } from './config/gameConfig';

const config = {
  ...gameConfig,
  scene: [BootScene, GameScene]
};

new Phaser.Game(config);

应避免的反模式

❌ 混乱的全局状态:将游戏状态存储在 window 或模块全局变量中 为什么不好:错误无法追踪,场景切换会导致状态异常 更好的做法:使用场景数据、注册表或专用状态管理器

❌ 在 Create 中加载:在 create() 而非 preload() 中加载资源 为什么不好:引用资源时,资源可能尚未就绪 更好的做法:始终在 preload() 中加载,并使用 Boot 场景加载所有资源

❌ 依赖帧数的逻辑:使用帧数而非 delta time 为什么不好:游戏速度会随帧率变化 更好的做法:使用 this.speed * (delta / 1000) 确保移动速度一致

❌ 过度使用物理引擎:为简单的平台游戏碰撞使用 Matter 为什么不好:会影响性能,并增加不必要的复杂性 更好的做法:Arcade physics 可满足 90% 的 2D 游戏需求

❌ 臃肿的场景:用一个庞大的场景承载所有游戏逻辑 为什么不好:难以维护,也难以添加功能 更好的做法:为菜单、游戏过程和 UI 覆盖层分别创建场景

❌ 魔法数字:在代码各处散布硬编码数值 为什么不好:无法调整平衡,数值也不一致 更好的做法:使用配置对象和常量文件

❌ 忽略对象池:每帧都创建和销毁对象 为什么不好:内存频繁变动,垃圾回收会造成卡顿 更好的做法:使用带有 setActive(false) / setVisible(false) 的组

❌ 同步访问资源:假设资源会立即加载完成 为什么不好:会出现竞态条件和未定义的纹理 更好的做法:串联场景启动流程,并使用加载事件

❌ 想当然地设定精灵表帧尺寸:不经验证就猜测帧尺寸 为什么不好:尺寸错误会悄无声息地破坏帧;像素级的偏差会逐渐累积,造成画面异常 更好的做法:打开资源文件测量帧尺寸,根据间距和边距计算,并验证计算结果是否正确

❌ 忽略精灵表间距:对于存在间隙的精灵表未指定 spacing 为什么不好:帧的位置会逐渐偏移,后续帧会读取错误的像素区域 更好的做法:检查源资源中各帧之间是否存在间隙;在加载器配置中使用 spacing: N

❌ 硬编码九宫格切片颜色:所有 UI 面板变体都使用同一种背景色 为什么不好:透明的边缘会显露出与不同资源配色不匹配的颜色 更好的做法:为每个资源单独配置背景色;从中间帧(第 4 帧)采样

❌ 对带内边距的帧使用九宫格切片:当图像在每个图块内居中或带有内边距时,仍将整个帧作为切片区域 为什么不好:边缘图块会带入内部填充,在面板内显示为不透明的“侧边条” 更好的做法:将图块裁剪到实际内容边界(alpha bbox),再合成或缓存为纹理;增加约 1px 的重叠并禁用平滑,以避免接缝

❌ 缩放不连续的 UI 图像:拉伸包含内部透明间隙的裁剪条幅行 为什么不好:透明间隔会被拉伸,导致 UI 看起来不连续,或填充部分被边框遮住。 更好的做法:将资源切分为两端和中间部分,只拉伸中间部分,再拼接各部分(重叠约 1px 并禁用平滑),最后按目标尺寸渲染。


差异化指导

重要:游戏实现应根据以下因素进行调整:

  • 游戏类型:平台游戏的物理机制与俯视角射击游戏不同
  • 目标平台:移动端需要触控输入,桌面端可以使用键盘
  • 美术风格:像素艺术使用最近邻缩放,高清美术使用线性缩放
  • 性能需求:精灵较多时使用对象池;精灵较少时可直接创建
  • 复杂度:简单游戏可以内联实现;复杂游戏需要类层次结构

避免千篇一律地采用:

  • 始终使用 800x600 分辨率
  • 始终使用 Arcade physics
  • 始终采用相同的场景结构
  • 不加调整地复制粘贴样板代码

快速参考

常用物理属性
body.setVelocity(x, y)
body.setVelocityX(x)
body.setBounce(x, y)
body.setGravityY(y)
body.setCollideWorldBounds(true)
body.setImmovable(true)        // For static-like dynamic bodies
body.setDrag(x, y)
body.setMaxVelocity(x, y)
实用的场景属性
this.cameras.main              // Main camera
this.physics.world             // Physics world
this.input.keyboard            // Keyboard manager
this.sound                     // Audio manager
this.time                      // Time/clock manager
this.tweens                    // Tween manager
this.anims                     // Animation manager
this.registry                  // Cross-scene data store
this.data                      // Scene-specific data store
必备事件
// Scene events
this.events.on('pause', callback)
this.events.on('resume', callback)
this.events.on('shutdown', callback)

// Physics events
this.physics.world.on('worldbounds', callback)

// Game object events
sprite.on('destroy', callback)
sprite.on('animationcomplete', callback)

另请参阅

  • references/arcade-physics.md - 深入了解 Arcade 物理
  • references/tilemaps.md - 高级瓦片地图技术
  • references/performance.md - 优化策略
  • references/spritesheets-nineslice.md - 精灵表加载(间距/边距)、九宫格 UI 面板、资源检查

请记住

Phaser 为你提供了强大的基础功能——场景、精灵、物理和输入——但架构由你负责。

从系统的角度思考:你需要哪些场景?有哪些实体?它们如何交互?在编写代码前回答这些问题,你的游戏才能随着规模扩大而保持易于维护。

Claude 有能力构建完整、精良的 Phaser 游戏。这些指南为你指明方向——而非设限。

评论

支持 Markdown