| name | groups-and-containers |
| description | Use this skill when using Groups or Containers in Phaser 4. Covers organizing game objects, object pooling, batch operations, and nested transforms with Containers. Triggers on: Group, Container, object pool, getFirstDead, children. |
Groups and Containers
Logical grouping (Group), visual grouping with transform inheritance (Container), render-layer grouping (Layer), object pooling, and when to use each in Phaser 4.
Key source paths: src/gameobjects/group/, src/gameobjects/container/, src/gameobjects/layer/
Related skills: ../sprites-and-images/SKILL.md, ../physics-arcade/SKILL.md
Quick Start
const enemies = this.add.group();
enemies.create(100, 200, 'enemy');
enemies.create(300, 200, 'enemy');
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]);
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'));
Core Concepts
Group vs Container vs Layer
| Feature | Group | Container | Layer |
|---|
| Purpose | Logical collection / pool | Visual parent with transform | Render-order bucket |
| On display list | No (children are) | Yes (renders children) | Yes (renders children) |
| Position/rotation/scale | No | Yes (children inherit) | No |
| Children storage | children (Set) | list (Array) | List (Structs.List) |
| Physics | Via physics.add.group() | Limited (offsets if not at 0,0) | No |
| Input | No (children can) | Yes (needs hit area shape) | No |
| Object pooling | Yes (getFirstDead, kill) | No | No |
| Masks | No | Yes (not per-child in Canvas) | Yes |
| Alpha/blend/visible | No (batch via setVisible) | Yes | Yes |
| Nesting | N/A | Container in Container | Cannot go in Container |
| Extends | EventEmitter | GameObject | List |
| Factory | this.add.group() | this.add.container(x, y) | this.add.layer() |
When to Use Each
Group: Managing collections of similar objects (enemies, bullets, coins), object pooling with active/inactive lifecycle, physics group collisions. No shared visual transform. Members can belong to multiple Groups simultaneously.
Container: Children inherit position, rotation, scale, alpha. Composite UI elements (health bars, inventory slots), moving/rotating clusters as one unit, nested transforms. By default exclusive -- a child can only belong to one Container (use setExclusive(false) to override).
Layer: Controlling render order of object batches, applying shared alpha/blend/mask. No position/scale/rotation. Lightweight render bucketing.
Container vs Group at a Glance
- Container has position, rotation, scale, alpha -- Group does not. If you need children to move/rotate as a unit, use Container.
- Container is exclusive by default -- adding a child removes it from its previous Container. Group is non-exclusive; a game object can be in many Groups.
- Container is on the display list -- it renders its children. Group is not on the display list; its children render individually on the Scene.
- Group supports object pooling -- getFirstDead, kill, killAndHide. Container does not.
- Container has performance cost -- each child requires matrix math per frame. Deeper nesting = more cost. Prefer Group or Layer when transforms are not needed.
Common Patterns
Creating and Populating Groups
const gems = this.add.group();
gems.add(existingSprite);
gems.addMultiple([sprite1, sprite2, sprite3]);
const coins = this.add.group({
classType: Phaser.GameObjects.Sprite,
key: 'coin',
quantity: 10,
setXY: { x: 50, y: 300, stepX: 60 },
setScale: { x: 0.5, y: 0.5 }
});
const bullets = this.add.group({
classType: Bullet,
maxSize: 30,
defaultKey: 'bullet',
runChildUpdate: true
});
Object Pooling with getFirstDead
The core pooling pattern: deactivate objects instead of destroying them, then reuse inactive ones.
const bullets = this.add.group({
classType: Phaser.GameObjects.Sprite,
defaultKey: 'bullet',
maxSize: 30
});
function fireBullet(x, y) {
const bullet = bullets.get(x, y);
if (bullet) {
bullet.setActive(true);
bullet.setVisible(true);
bullet.body.velocity.y = -300;
}
}
function killBullet(bullet) {
bullets.killAndHide(bullet);
}
const inactive = bullets.getFirst(false);
const active = bullets.getFirstAlive();
const dead = bullets.getFirstDead(true, x, y);
Pool helper methods on Group:
| Method | Description |
|---|
get(x, y, key, frame) | Shortcut: getFirst(false, true, ...) -- finds inactive or creates |
getFirst(state, createIfNull, x, y, key, frame) | First member matching active state |
getFirstAlive(createIfNull, x, y, key, frame) | First member where active===true |
getFirstDead(createIfNull, x, y, key, frame) | First member where active===false |
getLast(state, createIfNull, x, y, key, frame) | Like getFirst but searches back-to-front |
kill(gameObject) | Sets active=false on a member |
killAndHide(gameObject) | Sets active=false and visible=false |
countActive(value) | Count members where active===value (default true) |
getTotalUsed() | Count of active members |
getTotalFree() | maxSize - active count (remaining pool capacity) |
isFull() | True if children.size >= maxSize |
Physics Groups
Physics groups extend Group with automatic body assignment. See ../physics-arcade/SKILL.md for full details.
const enemies = this.physics.add.group({
key: 'enemy',
quantity: 5,
setXY: { x: 100, y: 100, stepX: 80 }
});
const platforms = this.physics.add.staticGroup();
platforms.create(400, 568, 'ground');
this.physics.add.collider(player, platforms);
this.physics.add.overlap(bullets, enemies, onHit);
Containers with Nested Transforms
const hud = this.add.container(10, 10);
hud.setScrollFactor(0);
const healthBar = this.add.rectangle(0, 0, 200, 20, 0x00ff00);
const healthText = this.add.text(210, -5, '100 HP');
hud.add([healthBar, healthText]);
hud.setPosition(50, 50);
hud.setScale(1.5);
hud.setRotation(0.1);
hud.setAlpha(0.8);
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);
}
Key Container methods:
| Method | Description |
|---|
add(child) / addAt(child, index) | Add Game Object(s); removes from display list |
remove(child, destroyChild) | Remove; optionally destroy |
getAt(index) / getIndex(child) | Access by index |
getByName(name) / getFirst(prop, val) | Query children |
getAll(prop, val) / count(prop, val) | Filtered access and counting |
sort(property) / swap(a, b) / moveTo(child, idx) | Ordering |
each(cb, ctx) / iterate(cb, ctx) | Iteration (iterate passes index) |
setScrollFactor(x, y, updateChildren) | Pass true to also apply to children |
getBounds(output) | Bounding rect of all children |
pointToContainer(source, output) | World point to local space |
setExclusive(value) | When false, children can exist in multiple places |
replace(oldChild, newChild) | Swap one child for another |
setSize(width, height) | Set hit area size (required for input) |
length | Read-only child count |
Layers for Render Ordering
const bgLayer = this.add.layer();
const entityLayer = this.add.layer();
const uiLayer = this.add.layer();
bgLayer.add(this.add.image(400, 300, 'sky'));
entityLayer.add(player);
entityLayer.add(enemy);
uiLayer.add(scoreText);
bgLayer.setDepth(0);
entityLayer.setDepth(1);
uiLayer.setDepth(2);
enemy.setDepth(5);
entityLayer.setAlpha(0.5);
entityLayer.setVisible(false);
entityLayer.setBlendMode(Phaser.BlendModes.ADD);
Bulk Creation with createMultiple
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 },
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
}
});
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 } }
]);
Iterating and Batch Operations on Groups
const enemies = this.add.group();
const all = enemies.getChildren();
const active = enemies.getMatching('active', true);
enemies.setXY(200, 300);
enemies.incX(5);
enemies.setVisible(false);
enemies.propertyValueSet('tintTopLeft', 0xff0000);
enemies.playAnimation('walk');
enemies.setX(100, 50);
enemies.setY(200, 30);
enemies.setXY(100, 200, 50, 30);
enemies.incXY(10, 5);
enemies.angle(0, 15);
enemies.setAlpha(1, -0.1);
enemies.setScale(1, 0, 0.1, 0);
enemies.setDepth(0, 1);
enemies.setOrigin(0.5);
enemies.setBlendMode(Phaser.BlendModes.ADD);
enemies.setTint(0xff0000);
enemies.shuffle();
API Quick Reference
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.
Gotchas
-
Group is NOT on the display list. Its children appear on the Scene display list individually. Moving a Group does nothing visually -- use Container for that.
-
Container has performance overhead. Every child requires extra matrix math per frame. Deep nesting multiplies this. Avoid Containers when a Group or Layer suffices.
-
Container origin is always 0,0. The transform point cannot be changed. Position children relative to (0,0).
-
Container children lose Scene-level depth control. A child's depth only orders within the Container. The Container's own depth positions it in the Scene.
-
Physics + Container is problematic. If a Container is not at (0,0), physics bodies on children will be offset. Avoid physics bodies on Container children.
-
Container children cannot be individually masked in Canvas rendering. Only the Container itself can have a mask. Masks do not stack for nested Containers. Masks do stack in WebGL rendering.
-
Group.get() vs Group.getFirst() differ. get(x, y) is shorthand for getFirst(false, true, x, y) -- finds first inactive member and creates if none found. getFirst(state) defaults to active===false without auto-creating.
-
Layer cannot go inside a Container. Containers can be added to Layers, but not the reverse.
-
Group children Set is unordered. No index-based access. Use getChildren() to get an array snapshot.
-
killAndHide does not remove from the group. It only sets active=false and visible=false. The object stays in the group for reuse.
-
Container.setScrollFactor does not auto-propagate. Pass true as the third argument to also update children: container.setScrollFactor(0, 0, true).
-
Group.create() adds to the Scene display list. But group.add() does NOT unless you pass true as the second argument.
-
Container needs setSize() for input. Containers have no implicit size. You must call container.setSize(width, height) before setInteractive() will work with a hit area.
Source File Map
| File | Description |
|---|
src/gameobjects/group/Group.js | Group class -- pooling, create, getFirst*, kill, batch ops |
src/gameobjects/group/GroupFactory.js | this.add.group() factory registration |
src/gameobjects/group/typedefs/GroupConfig.js | GroupConfig typedef (classType, maxSize, callbacks) |
src/gameobjects/group/typedefs/GroupCreateConfig.js | GroupCreateConfig typedef (key, quantity, setXY, etc.) |
src/gameobjects/container/Container.js | Container class -- list management, nested transforms |
src/gameobjects/container/ContainerFactory.js | this.add.container() factory registration |
src/gameobjects/container/ContainerRender.js | Container WebGL/Canvas render functions |
src/gameobjects/layer/Layer.js | Layer class -- display list bucket with alpha/blend/mask |
src/gameobjects/layer/LayerFactory.js | this.add.layer() factory registration |
src/gameobjects/layer/LayerRender.js | Layer WebGL/Canvas render functions |
src/physics/arcade/ArcadePhysics.js | this.physics.add.group() / staticGroup() |