关于 groups-and-containers
面向 Phaser 4 开发者的技能指南,讲解 Group、Container 和 Layer 的差异及用法,涵盖对象池、变换继承与渲染排序。
主要功能
- 01比较 Group、Container 与 Layer
- 02使用 Group 管理对象池
- 03通过 Container 继承变换
- 04用 Layer 控制渲染顺序
- 05提供 API 与代码示例
适用场景
- 01复用子弹等对象以减少重复创建
- 02将 HUD 元素作为整体移动
- 03为对象批次设置渲染顺序
- 04选择合适的分组方式管理游戏对象
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ phaserjs/ phaser --skill groups-and-containers安装后在 Claude Code 中输入/groups-and-containers确认已加载。
详细介绍
组与容器
逻辑分组(Group)、带变换继承的视觉分组(Container)、渲染层级分组(Layer)、对象池,以及在 Phaser 4 中何时使用它们。
关键源码路径: src/gameobjects/group/、src/gameobjects/container/、src/gameobjects/layer/
相关技能: ../sprites-and-images/SKILL.md、../physics-arcade/SKILL.md
快速开始
// In a Scene's create() method:
// --- Group: logical collection, no transform, great for pooling ---
const enemies = this.add.group();
enemies.create(100, 200, 'enemy'); // creates Sprite at (100,200)
enemies.create(300, 200, 'enemy');
// --- Container: visual parent with inherited transform ---
const hud = this.add.container(10, 10);
const icon = this.add.image(0, 0, 'heart');
const label = this.add.text(20, 0, 'x3');
hud.add([icon, label]); // children move/scale/rotate with hud
// --- Layer: render-ordering bucket, no position/scale ---
const bgLayer = this.add.layer();
const fgLayer = this.add.layer();
bgLayer.add(this.add.image(400, 300, 'sky'));
fgLayer.add(this.add.sprite(400, 300, 'player'));
核心概念
Group 与 Container 与 Layer 对比
| 特性 | Group | Container | Layer |
|---|---|---|---|
| 用途 | 逻辑集合 / 对象池 | 带变换的视觉父级 | 渲染顺序桶 |
| 是否在显示列表中 | 否(其子级在) | 是(渲染子级) | 是(渲染子级) |
| 位置/旋转/缩放 | 否 | 是(子级继承) | 否 |
| 子级存储 | children(Set) | list(Array) | List(Structs.List) |
| 物理 | 通过 physics.add.group() | 有限(若不在 0,0 则使用偏移) | 否 |
| 输入 | 否(子级可以) | 是(需要命中区域形状) | 否 |
| 对象池 | 是(getFirstDead、kill) | 否 | 否 |
| 遮罩 | 否 | 是(在 Canvas 中不针对单个子级) | 是 |
| Alpha/混合/可见性 | 否(通过 setVisible 批量设置) | 是 | 是 |
| 嵌套 | 不适用 | Container 中嵌套 Container | 不能放入 Container |
| 继承自 | EventEmitter | GameObject | List |
| 工厂方法 | this.add.group() | this.add.container(x, y) | this.add.layer() |
何时使用各自
Group: 管理同类对象的集合(敌人、子弹、金币)、使用 active/inactive 生命周期的对象池、物理组碰撞。没有共享的视觉变换。成员可以同时属于多个 Group。
Container: 子级继承位置、旋转、缩放、alpha。复合 UI 元素(血条、物品栏槽位)、作为单个单元移动/旋转的群组、嵌套变换。默认是独占的——一个子级只能属于一个 Container(使用 setExclusive(false) 来覆盖)。
Layer: 控制对象批次的渲染顺序、应用共享的 alpha/混合/遮罩。没有位置/缩放/旋转。轻量级渲染分桶。
Container 与 Group 一览
- Container 有位置、旋转、缩放、alpha——Group 没有。如果你需要子级作为一个单元移动/旋转,请使用 Container。
- Container 默认是独占的——添加一个子级会将其从之前的 Container 中移除。Group 是非独占的;一个游戏对象可以属于多个 Group。
- Container 在显示列表中——它会渲染其子级。Group 不在显示列表中;其子级在 Scene 上单独渲染。
- Group 支持对象池——getFirstDead、kill、killAndHide。Container 不支持。
- Container 有性能开销——每个子级每帧都需要矩阵运算。嵌套越深 = 开销越大。当不需要变换时,优先使用 Group 或 Layer。
常见模式
创建和填充 Group
// Empty group, add existing objects
const gems = this.add.group();
gems.add(existingSprite);
gems.addMultiple([sprite1, sprite2, sprite3]);
// Group with config -- creates children automatically
const coins = this.add.group({
classType: Phaser.GameObjects.Sprite,
key: 'coin',
quantity: 10, // overrides frameQuantity
setXY: { x: 50, y: 300, stepX: 60 },
setScale: { x: 0.5, y: 0.5 }
});
// Custom class type with pool limit
const bullets = this.add.group({
classType: Bullet, // must accept (scene, x, y, key, frame)
maxSize: 30,
defaultKey: 'bullet',
runChildUpdate: true // calls child.update() each frame
});
使用 getFirstDead 进行对象池
核心对象池模式:停用对象而不是销毁它们,然后重用已停用的对象。
// Setup pool
const bullets = this.add.group({
classType: Phaser.GameObjects.Sprite,
defaultKey: 'bullet',
maxSize: 30
});
// Fire a bullet -- get() finds first inactive member or creates one
function fireBullet(x, y) {
const bullet = bullets.get(x, y);
if (bullet) {
bullet.setActive(true);
bullet.setVisible(true);
bullet.body.velocity.y = -300; // if physics enabled
}
}
// Deactivate when off-screen or on hit
function killBullet(bullet) {
bullets.killAndHide(bullet); // sets active=false, visible=false
// If using physics, also reset the body:
// bullet.body.stop();
}
// Alternative: manual getFirst
const inactive = bullets.getFirst(false); // first where active===false
const active = bullets.getFirstAlive(); // first where active===true
const dead = bullets.getFirstDead(true, x, y); // first inactive, create if null
Group 上的对象池辅助方法:
| 方法 | 描述 |
|---|---|
get(x, y, key, frame) | 快捷方式:getFirst(false, true, ...)——查找不活跃的对象或创建 |
getFirst(state, createIfNull, x, y, key, frame) | 第一个匹配活跃状态 state 的成员 |
getFirstAlive(createIfNull, x, y, key, frame) | 第一个 active===true 的成员 |
getFirstDead(createIfNull, x, y, key, frame) | 第一个 active===false 的成员 |
getLast(state, createIfNull, x, y, key, frame) | 类似 getFirst,但从后往前搜索 |
kill(gameObject) | 将成员的 active 设置为 false |
killAndHide(gameObject) | 将 active 设置为 false 并将 visible 设置为 false |
countActive(value) | 统计 active===value 的成员数量(默认 true) |
getTotalUsed() | 活跃成员的数量 |
getTotalFree() | maxSize - 活跃数量(剩余池容量) |
isFull() | 如果 children.size >= maxSize 则为 true |
物理组
物理组扩展了 Group,支持自动分配刚体。完整细节请参见 ../physics-arcade/SKILL.md。
// Arcade Physics group -- every member gets a dynamic body
const enemies = this.physics.add.group({
key: 'enemy',
quantity: 5,
setXY: { x: 100, y: 100, stepX: 80 }
});
// Static physics group -- immovable bodies
const platforms = this.physics.add.staticGroup();
platforms.create(400, 568, 'ground');
// Collide physics groups with each other
this.physics.add.collider(player, platforms);
this.physics.add.overlap(bullets, enemies, onHit);
带嵌套变换的容器
// HUD that follows camera
const hud = this.add.container(10, 10);
hud.setScrollFactor(0); // pinned to camera
const healthBar = this.add.rectangle(0, 0, 200, 20, 0x00ff00);
const healthText = this.add.text(210, -5, '100 HP');
hud.add([healthBar, healthText]);
// Move everything at once
hud.setPosition(50, 50);
// Scale and rotate propagate to children
hud.setScale(1.5);
hud.setRotation(0.1);
// Alpha affects all children
hud.setAlpha(0.8);
// Nested containers
const inventory = this.add.container(300, 500);
for (let i = 0; i < 5; i++) {
const slot = this.add.container(i * 55, 0);
slot.add([
this.add.rectangle(0, 0, 48, 48, 0x333333),
this.add.image(0, 0, `item-${i}`)
]);
inventory.add(slot); // Container inside Container
}
Container 的关键方法:
| 方法 | 描述 |
|---|---|
add(child) / addAt(child, index) | 添加游戏对象;会将其从显示列表中移除 |
remove(child, destroyChild) | 移除;可选销毁 |
getAt(index) / getIndex(child) | 按索引访问 |
getByName(name) / getFirst(prop, val) | 查询子对象 |
getAll(prop, val) / count(prop, val) | 过滤访问与计数 |
sort(property) / swap(a, b) / moveTo(child, idx) | 排序 |
each(cb, ctx) / iterate(cb, ctx) | 迭代(iterate 会传入索引) |
setScrollFactor(x, y, updateChildren) | 传入 true 也会应用到子对象 |
getBounds(output) | 所有子对象的包围矩形 |
pointToContainer(source, output) | 将世界坐标转换为局部空间 |
setExclusive(value) | 为 false 时,子对象可同时存在于多个位置 |
replace(oldChild, newChild) | 用另一个子对象替换某个子对象 |
setSize(width, height) | 设置命中区域大小(输入所必需) |
length | 只读的子对象数量 |
用于渲染排序的 Layer
const bgLayer = this.add.layer();
const entityLayer = this.add.layer();
const uiLayer = this.add.layer();
// Add objects -- they render in layer order, then by depth within layer
bgLayer.add(this.add.image(400, 300, 'sky'));
entityLayer.add(player);
entityLayer.add(enemy);
uiLayer.add(scoreText);
// Control depth of layers themselves
bgLayer.setDepth(0);
entityLayer.setDepth(1);
uiLayer.setDepth(2);
// Children set depth within their layer
enemy.setDepth(5); // relative to entityLayer, not the Scene
// Apply shared effects to entire layer
entityLayer.setAlpha(0.5);
entityLayer.setVisible(false);
entityLayer.setBlendMode(Phaser.BlendModes.ADD);
使用 createMultiple 批量创建
// createMultiple accepts a GroupCreateConfig or array of them
const coins = this.add.group();
coins.createMultiple({
key: 'coin',
quantity: 20,
setXY: { x: 50, y: 100, stepX: 40, stepY: 0 },
setScale: { x: 0.5, y: 0.5 },
setRotation: { value: 0, step: 0.1 }, // each rotated 0.1 more than previous
setAlpha: { value: 1 },
setOrigin: { x: 0.5, y: 0.5 },
setDepth: { value: 5 },
gridAlign: {
width: 5,
height: 4,
cellWidth: 48,
cellHeight: 48,
x: 100,
y: 200
}
});
// Multiple configs at once (creates two different sets)
coins.createMultiple([
{ key: 'gold-coin', quantity: 10, setXY: { x: 50, y: 100, stepX: 30 } },
{ key: 'silver-coin', quantity: 10, setXY: { x: 50, y: 200, stepX: 30 } }
]);
对 Group 进行迭代与批量操作
const enemies = this.add.group();
// Get all children as array
const all = enemies.getChildren();
const active = enemies.getMatching('active', true);
// Batch property operations
enemies.setXY(200, 300);
enemies.incX(5); // add 5 to each member's x
enemies.setVisible(false);
enemies.propertyValueSet('tintTopLeft', 0xff0000);
enemies.playAnimation('walk');
// Stepping: apply incremental values across members
enemies.setX(100, 50); // first at x=100, next at 150, then 200...
enemies.setY(200, 30); // first at y=200, next at 230, then 260...
enemies.setXY(100, 200, 50, 30); // combined X and Y stepping
enemies.incXY(10, 5); // add 10 to each x, 5 to each y
enemies.angle(0, 15); // first at 0 deg, next at 15, then 30...
enemies.setAlpha(1, -0.1); // first at 1.0, next at 0.9, then 0.8...
enemies.setScale(1, 0, 0.1, 0); // scaleX: 1.0, 1.1, 1.2... (stepX=0.1)
enemies.setDepth(0, 1); // depth 0, 1, 2, 3...
enemies.setOrigin(0.5);
enemies.setBlendMode(Phaser.BlendModes.ADD);
enemies.setTint(0xff0000);
enemies.shuffle(); // randomize order in the group
API 快速参考
Group (Phaser.GameObjects.Group)
Factory: this.add.group(children?, config?)
Config types:
GroupConfig -- classType, name, active, maxSize, defaultKey, defaultFrame,
runChildUpdate, createCallback, removeCallback
GroupCreateConfig -- key (required), classType, frame, quantity, visible, active,
repeat, yoyo, frameQuantity, max, setXY, setRotation,
setScale, setOrigin, setAlpha, setDepth, setScrollFactor,
hitArea, gridAlign
Key members: children (Set), classType, maxSize, defaultKey, defaultFrame,
active, runChildUpdate
Lifecycle: create, createMultiple, add, addMultiple, remove, clear, destroy
Queries: getFirst, getFirstAlive, getFirstDead, getLast, get, getChildren,
getLength, getMatching, contains, countActive, getTotalUsed,
getTotalFree, isFull
Pool: get, getFirstDead, kill, killAndHide
Bulk ops: setX/Y/XY, incX/Y/XY, setAlpha, setVisible, toggleVisible,
playAnimation, propertyValueSet, propertyValueInc, setOrigin,
setDepth, shuffle, setBlendMode, setTint
Container (Phaser.GameObjects.Container)
Factory: this.add.container(x?, y?, children?)
Extends: GameObject
Mixins: AlphaSingle, BlendMode, ComputedSize, Depth, Mask, Transform, Visible
Key members: list (Array), exclusive, maxSize, scrollFactorX/Y
Children: add, addAt, remove, removeAt, removeBetween, removeAll
Queries: getAt, getIndex, getByName, getFirst, getAll, getRandom, count
Ordering: sort, swap, moveTo, moveUp, moveDown, sendToBack, bringToTop,
moveAbove, moveBelow, reverse
Transform: pointToContainer, getBounds, getBoundsTransformMatrix
Iteration: each(cb, ctx), iterate(cb, ctx, ...args)
Config: setExclusive, setScrollFactor(x, y, updateChildren)
Property: length (read-only child count)
Layer (Phaser.GameObjects.Layer)
Factory: this.add.layer(children?)
Extends: Phaser.Structs.List
Mixins: AlphaSingle, BlendMode, Depth, Filters, Mask, RenderSteps, Visible
Key members: scene, displayList, sortChildrenFlag
Children: add, remove (inherited from List)
Settings: setAlpha, setBlendMode, setDepth, setVisible, setMask, setName,
setActive, setState, setData, getData
No position, rotation, scale, scroll factor, input, or physics.
Cannot be added to a Container. Containers can be added to Layers.
注意事项
-
Group 不在显示列表上。 它的子对象会各自出现在 Scene 显示列表中。移动 Group 在视觉上没有任何效果——若要实现这一点,请使用 Container。
-
Container 有性能开销。 每个子对象每帧都需要额外的矩阵运算。深层嵌套会成倍放大这一开销。当 Group 或 Layer 足够用时,请避免使用 Container。
-
Container 的原点始终是 0,0。 变换点无法更改。请相对于 (0,0) 定位子对象。
-
Container 的子对象会失去 Scene 级别的深度控制。 子对象的
depth仅在 Container 内部决定顺序。Container 自身的 depth 决定它在 Scene 中的位置。 -
物理 + Container 存在问题。 如果 Container 不在 (0,0),子对象上的物理刚体将会偏移。请避免在 Container 子对象上使用物理刚体。
-
在 Canvas 渲染中,Container 的子对象无法单独设置遮罩。 只有 Container 本身可以拥有遮罩。遮罩不会为嵌套的 Container 叠加。在 WebGL 渲染中遮罩会叠加。
-
Group.get() 与 Group.getFirst() 不同。
get(x, y)是getFirst(false, true, x, y)的简写——查找第一个非活动成员,若未找到则创建。getFirst(state)默认为active===false,且不会自动创建。 -
Layer 不能放进 Container 中。 Container 可以添加到 Layer,但反过来不行。
-
Group 的子对象 Set 是无序的。 无法按索引访问。使用
getChildren()获取数组快照。 -
killAndHide 不会从组中移除对象。 它只设置
active=false和visible=false。该对象仍留在组中以便复用。 -
Container.setScrollFactor 不会自动传播。 传入
true作为第三个参数以同时更新子对象:container.setScrollFactor(0, 0, true)。 -
Group.create() 会添加到 Scene 显示列表。 但
group.add()不会,除非你传入true作为第二个参数。 -
Container 需要 setSize() 才能接收输入。 Container 没有隐式大小。在
setInteractive()能与命中区域配合工作之前,你必须调用container.setSize(width, height)。
源文件映射
| 文件 | 描述 |
|---|---|
src/gameobjects/group/Group.js | Group 类 —— 对象池、create、getFirst*、kill、批量操作 |
src/gameobjects/group/GroupFactory.js | this.add.group() 工厂注册 |
src/gameobjects/group/typedefs/GroupConfig.js | GroupConfig 类型定义(classType、maxSize、callbacks) |
src/gameobjects/group/typedefs/GroupCreateConfig.js | GroupCreateConfig 类型定义(key、quantity、setXY 等) |
src/gameobjects/container/Container.js | Container 类 —— 列表管理、嵌套变换 |
src/gameobjects/container/ContainerFactory.js | this.add.container() 工厂注册 |
src/gameobjects/container/ContainerRender.js | Container 的 WebGL/Canvas 渲染函数 |
src/gameobjects/layer/Layer.js | Layer 类 —— 带 alpha/blend/mask 的显示列表容器 |
src/gameobjects/layer/LayerFactory.js | this.add.layer() 工厂注册 |
src/gameobjects/layer/LayerRender.js | Layer 的 WebGL/Canvas 渲染函数 |
src/physics/arcade/ArcadePhysics.js | this.physics.add.group() / staticGroup() |