Skip to content

Docs: errors in skills/ and the Pixel Art Guide (v4.2.1) #7385

Description

@ais4ocho

Version

  • Phaser Version: 4.2.1 (the cited files are unchanged on master @ 02d8931, checked 2026-09-25)
  • Operating system: N/A (documentation)
  • Browser: N/A (checked in headless Chromium against the npm 4.2.1 build)

Description

The first-party agent skills in skills/ and the Phaser 4 Pixel Art Guide contain some claims that don't match the 4.2.1 source. Each item gives the file and line, the source that decides it, and what we saw at runtime.

  1. skills/animations/SKILL.md, "Per-Frame Duration" (line 247) and gotcha 4 (line 459): per-frame duration is described as "added to the base msPerFrame" and "additive". Since 3.80 (When providing frames with duration to anims.create(), 41.6ms is added to every frame (1 frame time @24 FPS) #6712) it replaces msPerFrame for that frame. getFirstTick and getNextTick in src/animations/Animation.js use state.frameRate === state.currentAnim.frameRate ? state.currentFrame.duration || state.msPerFrame : state.msPerFrame. It is also ignored when play() passes a frameRate different from the animation's own. Runtime: at 10 fps, a frame with duration: 500 shows for 500 ms, not 600.

  2. skills/groups-and-containers/SKILL.md, gotcha 4 (line 384): "A child's depth only orders within the Container." Container never sorts by depth. addHandler takes the child off the display list, so setDepth queues no sort, and ContainerWebGLRenderer / ContainerCanvasRenderer draw container.list in order. Children draw in list order unless you call container.sort('depth'). Layer, by contrast, does sort. Runtime: a depth-10 child added first draws under a depth-0 child added second, in both renderers.

  3. skills/groups-and-containers/SKILL.md, Layer (lines 364–365): the mixin list omits EventEmitter and GameObject. Layer "is now a true GameObject" since 4.1.0 (Layer.js mixins; CHANGELOG-v4.1.0), and that is where the listed Filters and RenderSteps come from. Worth noting that layer instanceof Phaser.GameObjects.GameObject is still false at runtime, although phaser.d.ts declares class Layer extends GameObject (related: Layer is not extended from GameObject class #7295).

  4. skills/cameras/SKILL.md (lines 457–458): const fx = cam.filters.external.addColorMatrix(); fx.grayscale(); throws TypeError: fx.grayscale is not a function. It should be fx.colorMatrix.grayscale() (src/filters/ColorMatrix.js), as skills/filters-and-postfx/SKILL.md already shows.

  5. skills/cameras/SKILL.md (lines 443–444): internal is described as "applied by the system" and external as "for user-added filters". Both lists start empty and both are for user filters. Internal filters apply to things within the camera; external filters apply to the camera itself, in its rendering context (FilterList.js class doc). skills/filters-and-postfx/SKILL.md describes this correctly.

  6. skills/cameras/SKILL.md (line 454): addGlow(0xff0000, 4, 0, false, 0.1, 10) follows Phaser 3's preFX.addGlow(color, outerStrength, innerStrength, knockout, quality, distance) order. The v4 signature is (color, outerStrength, innerStrength, scale, knockout, quality, distance), so scale becomes false. The glow gets zero radius and draws nothing, 0.1 lands in knockout, and 10 lands in quality. A v4 equivalent is addGlow(0xff0000, 4, 0, 1, false, 10, 10).

  7. skills/events-system/SKILL.md, two things:

    • The "BAD: leaks listeners on every scene restart" example (lines 431–434) uses this.input.on('pointerdown', …). But InputPlugin#shutdown calls this.removeAllListeners(), which drops every listener on this.input, including user ones, and KeyboardPlugin does the same. What does accumulate is listeners on this.events, game.events, registry.events and scale. Runtime, across three scene starts: this.input pointerdown listeners 1, 1, 1; game.events blur listeners 1, 2, 3.
    • The "CRITICAL: always clean up" example (lines 130–137) registers its cleanup with this.events.on(SHUTDOWN, …), and that handler accumulates the same way. It should use once.
  8. skills/time-and-timers/SKILL.md, gotcha 9 (line 480): it says a re-added TimerEvent "must not be in a completed state" and that the Clock will reset it, but not why. addEvent does reset elapsed and dispatch state. However, a finished event is destroyed at the Clock's next preUpdate (Clock.js), which clears its callback, scope and args (TimerEvent#destroy). Re-adding it on a later frame silently never fires and leaves it in the Clock's active list. Call reset(config) first, as the skill's "Timer Reset and Reuse" section does.

  9. skills/scale-and-responsive/SKILL.md, "Pixel Art with Max Zoom" (lines 202–215): FIT with zoom: MAX_ZOOM is presented as giving crisp pixel rendering. But ScaleManager#updateScale uses zoom only in the Scale.NONE branch, so FIT sizes the canvas to the parent at a usually non-integer ratio: sharp with pixelArt, but with unevenly sized pixels. Also, MAX_ZOOM is resolved when the config is parsed and is only re-resolved by scale.setMaxZoom(). Runtime, 320×240 in a 1000×700 parent:

    • FIT + MAX_ZOOM displays at 933.33×700, identical to FIT alone.
    • NONE + MAX_ZOOM displays at 640×480.
    • For integer steps you can use Scale.NONE + MAX_ZOOM with setMaxZoom() on resize, or FIT with snap: { width: 320, height: 240 }.
  10. skills/game-setup-and-config/SKILL.md, "v4 Changes from v3" (lines 456–458): three of the listed changes predate v4.

    • Scale.EXPAND is @since 3.80.0.
    • The 1024×768 default size dates from 3.0.0.
    • loader.maxRetries was added in 3.85.0, with default 2.
  11. Phaser 4 Pixel Art Guide (lines 29 and 320): "Apply a Pixelate filter: addBlocky(6)". addBlocky takes a config object, so the 6 is silently ignored and you get a size-4 Blocky filter. It should be addPixelate(6); the guide's own Pixelate section (line 261) is correct.

Related, and possibly an engine issue rather than a docs one: sprite.play({ key, duration }) ignores duration. AnimationState#play fills a missing frameRate from the animation (GetFastValue(key, 'frameRate', anim.frameRate)), so calculateDuration never takes its duration branch unless frameRate: null is also passed. PlayAnimationConfig documents duration as an override.

Example Test Code

// Item 2: depth inside a Container
const c = this.add.container(0, 0);
const red = this.add.rectangle(50, 50, 40, 40, 0xff0000).setDepth(10);
const blue = this.add.rectangle(60, 60, 40, 40, 0x0000ff).setDepth(0);
c.add([red, blue]); // blue draws on top; c.sort('depth') puts red on top

// Item 8: re-adding a finished TimerEvent
const t = this.time.addEvent({ delay: 100, callback: () => console.log('fired') });
this.time.delayedCall(500, () => this.time.addEvent(t)); // 'fired' logs only once

// Item 11: addBlocky with a number
const f = this.cameras.main.filters.external.addBlocky(6);
console.log(f.size); // { x: 4, y: 4 }; addPixelate(6) gives amount 6

Additional Information

Every item was checked against the 4.2.1 source and reproduced in headless Chromium with the npm build. I can open a PR with these corrections if that's easier.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions