Add parallax/scrolling background layers (#67) - #87
Merged
Conversation
Covers the SetWrap() spike result (compositor-only, not usable with this engine's direct ifDraw2D rendering path), the resulting manual-tiling approach, a new DrawableParallaxLayer/SceneObjectParallaxLayer pair, and the camera-movement dirty-checking gap that a parallax layer needs but ordinary drawables don't. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…vement mechanism Deeper reading of SceneObject.bs during planning found the camera- movement recompute gap doesn't need touching update()/forceRecompute at all - draw()'s existing objMovedInRelationToCamera() already ORs in camera movement by default, so the fix lives entirely in an overridden findCanvasPosition(), no base-class changes required. Also updates scope: a dedicated examples/parallax replaces the originally-deferred rendererTest demo, since rendererTest never exercises Drawable/ SceneObject/GameEntity for any existing pair. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Implements the skeleton for Task 1 of the parallax layers feature:
- Add SceneObjectType.ParallaxLayer enum value
- Implement DrawableParallaxLayer class with region, parallaxFactor (default {1,1}),
repeatX (default true), and repeatY (default false) properties
- Implement minimal SceneObjectParallaxLayer class to satisfy test requirements
- Add comprehensive test suite for construction and addToScene behavior
All tests pass (442 passed).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
findCanvasPosition computes a parallax-shifted effective world position (camera-delta scaled by 1-factor, relative to a captured reference position) and converts it to canvas space; performDraw blits the region at each computed position. computeTilePositions is a stub returning a single position for now - tiling lands in a later task.
Camera2d's y axis is inverted relative to world space, so ordinary (factor 1) scrolling moves canvas y in the same direction as the camera's own y movement - the opposite sign relationship from x. The plan's test asserted the x-axis sign convention for y too. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…rsion The test assertion for the 'x and y factors are independent' test was incorrect. Camera2d's y-axis is inverted relative to world space (canvas y increases downward, world y increases upward), so ordinary (factor 1) scrolling moves canvas y in the SAME direction as camera movement, not opposite. This is the opposite sign relationship from x-axis. Changed assertion from 'first.y - 20' to 'first.y + 20' to match the actual Camera2d behavior documented in worldPointToCanvasPoint(). All 448 tests now pass.
…dent test Fix() truncates toward zero, not floor, so -Fix(-x) doesn't equal ceil(x) for positive x as the ceiling trick requires - needed Int() (true floor) instead. Separately, the "draws one renderer draw call per tile" test asserted an exact draw-call count that only holds at one specific tile phase (the +1 safety-margin tile lands off-canvas at most other phases, including the default entity position the test used) - picked a fixture position that lands on that phase instead of weakening the assertion. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
computeTilePositions now enumerates a full tile grid honoring DrawableParallaxLayer.repeatX/repeatY and the region's size (scaled by Drawable.scale), via a new computeAxisTilePositions per-axis helper. Two corrections found and confirmed against the plan along the way: - The tile-count ceiling formula needs Int() rather than Fix() - BrightScript's Fix() truncates toward zero, not floor, so -Fix(-x) doesn't compute ceil(x) for non-integer ratios. - The pre-existing "draws at the entity's own canvas position" baseline test now expects a Float canvas position, not an Integer: every tile position (tiled or not) is built via BGE.Math.VectorOps.create(), which reliably produces a Float, correctly matching BGE.Math.Vector's declared `x as float` field. The old single-tile stub had let an Integer canvas position slip through by accident. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
… layer The test's layerB used the shared runFrame() helper, which hardcodes m.entity.updateTransformationMatrix() - not entityB's - leaving entityB stuck at its default identity transform (world position (0,0,0)) instead of its real (100,50). Its second frame also omitted renderer.setupCameraForFrame() entirely, leaving Camera2d's cached projection matrix stale for the moved camera position. Replaced with an explicit, entity-correct sequence for both of layerB's frames. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
findCanvasPosition() recomputes the parallax shift fresh each frame from a frozen referencePosition and the camera's current absolute position - it's a stateless function of the current frame's inputs, not an incremental accumulator. A "several small moves vs one big move" comparison therefore evaluates the identical final expression against the identical final camera position on both paths, so a uniformly-applied premature-rounding bug would affect both equally and the comparison would still pass - caught by a task reviewer working through the arithmetic rather than just trusting the two paths differ. Replaced with a single-frame test whose exact pre-truncation canvas position is deliberately fractional at a point where truncating it (fix(), the real behavior) and rounding it to nearest (cint(), a plausible premature-rounding bug) land on different integers - the only design that can actually catch that class of regression. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…esign The two-path (ten 1px moves vs. one 10px move) comparison couldn't actually fail: findCanvasPosition() recomputes the parallax shift fresh each frame from a frozen referencePosition and the camera's current absolute position, so both paths evaluate the identical final expression against the identical final camera position - a uniformly-applied premature round() would affect both equally. Replaced with a single frame whose pre-truncation canvas position is deliberately fractional at a point where truncating (fix(), correct) and rounding to nearest (cint(), the bug this guards against) land on different integers (94 vs 95), verified by temporarily reintroducing the bug and confirming the test fails as expected. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
… follow Scaffolds a dedicated example: a controllable Player entity, Camera2d.setTarget following it, and three procedurally-painted parallax layers (distant mountains, closer hills, and a fast foreground) at different depths/parallaxFactors. Uses repeatX+repeatY tiling on every layer since DrawableParallaxLayer anchors each tile from a top-left canvas position rather than centering on the owning entity - repeatY:false (as in the original plan sketch) leaves roughly half the canvas uncovered depending on where that corner lands; repeating on both axes keeps the sky-colored tile top covering the gap seamlessly. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Task 7's on-device testing surfaced a real correctness gap: SceneObjectParallaxLayer inherits the default frustum check, which tests the raw (un-shifted) entity position rather than the parallax-shifted one it actually draws at, and doesn't account for tiling making a repeating layer visually infinite regardless of absolute distance. Both cause a layer to vanish once the camera roams far enough from its owning entity - exactly the scenario parallax backgrounds exist for. Fixing this in the engine (Task 8) rather than shipping the example with a documented limitation. Task 9 swaps the example's procedural art for a real 5-layer CC0 asset pack the user provided, renumbering the final quality-gate/PR task to Task 10. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…ing entity Fixes the black-screen bug found during Task 7's on-device testing: a layer's inherited frustum check tested the raw, un-shifted entity position instead of the parallax-shifted position it actually draws at, culling the whole layer once the camera drifted far enough from a static owning entity - regardless of parallax factor or tiling. - computeEffectiveWorldPosition() factors the parallax-shift math out of findCanvasPosition() so the new frustum-check override can share it. - getPositionsForFrustumCheck() now tests the effective (shifted) position instead of the raw one. - isPotentiallyOnScreen() short-circuits true whenever either axis repeats, since a repeating axis is visually infinite regardless of raw distance. - lastCameraPosition is refreshed in isPotentiallyOnScreen() itself, not just in findCanvasPosition(): the brief's original design only updated it in findCanvasPosition(), which doesn't run on a culled frame, so a same-frame camera jump would frustum-test against a stale pre-jump camera position and cull the layer permanently once the camera stopped moving. Verified by hand against the brief's own worked example (100,50 shifting to 385 once the camera reaches 400) before landing on this correction. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Replaces Task 7's three procedurally-painted layers with 5 real CC0 images (foreground-trees, trees, mountains, mountain-far, bg), scaled up to fill the canvas edge-to-edge, with per-layer offsets compensating for DrawableParallaxLayer's lack of anchor/centering support so the scene is framed correctly at spawn rather than relying on repeatY's wraparound. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
User-requested follow-up after seeing examples/parallax running: toggleable debug info overlay and draw-mode cycling, both matching conventions already established in examples/3d's BaseRoom. Also removes util.bs's dead goToNextRoom (flagged during Task 7's review - a single-room example never needs a room cycler). Renumbers the final quality-gate/PR task to Task 11. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Wires the "options" button (rokubot/ECP alias "info") to Game.debugShowUi()
and the "play" button to cycle every entity's draw mode via the existing
updateDrawMode() helper in util.bs, matching examples/3d's BaseRoom
conventions. Also removes util.bs's dead goToNextRoom, unused scaffold
boilerplate since this example has exactly one room.
Note: the task brief called for checking input.isButton("info"), but BGE's
own button-name table (utils.bs buttonNameFromCode) never produces "info" -
it produces "options" for that physical key, and every other example in the
repo checks isButton("options"). Used "options" to match actual engine/example
convention, confirmed by on-device testing (isButton("info") never fired).
Conflated rokubot's ECP key alias "info" with BGE's actual internal button name for that key, which buttonNameFromCode() confirms is "options" - the name isButton() actually checks. Caught by Task 10's implementer, who built it per the (wrong) brief first, found it silently never fired on-device, and corrected it. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…rides The docs still described SceneObjectParallaxLayer as having a single overridden findCanvasPosition(), but a later task also added isPotentiallyOnScreen() and getPositionsForFrustumCheck() overrides for correct frustum culling. Also add a construction snippet, a link to examples/parallax, and a note that DrawableParallaxLayer ignores Drawable's anchor/rotation/color/outline/drawMode fields. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…set divide hazard - Add a comment above newLayer()'s offset computation noting factor/ factorY must stay non-zero (the offset scales as 1/factor and diverges toward divide-by-zero as it approaches 0). - Add an optional zOffset param to newLayer() and give the four background layers (bg, mountainFar, mountains, trees) distinct Z depths instead of all sharing the background entity's single Z - their front-to-back draw order now actually comes from the documented Z-based renderer sort, not from array-sort stability among equal sort keys. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
No existing test exercised the scale-aware tile-spacing math in computeTilePositions() or the drawScaledObject() branch in performDraw(), even though every layer in examples/parallax uses a non-1 scale. Adds one test with scale 2.0 asserting tile spacing reflects the scaled tile width and that a draw call actually happens. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
BGE.DrawableParallaxLayer/BGE.SceneObjectParallaxLayer- a bitmap-based drawable that scrolls at a configurable per-axis fraction of the camera's movement, with optional tiling to cover the viewport. Closes Scrolling / parallax background layers #67.examples/parallax: a realGame/Roomwith camera-follow gameplay, using a real 5-layer CC0 art pack (Luis Zuno / @ansimuz, attributed in-repo), a debug info toggle, and draw-mode cycling.docs/drawables-and-scene-objects.md.specs/for reference.Test plan
npm run check(lint + validate + full Rooibos suite, 457 tests) - cleanexamples/parallaxbuilds and validates clean (npm run build,bsc --validate)rokubot: 5 layers scroll at visibly distinct depth-matched rates, correct front-to-back ordering, no seams, no vanishing after sustained camera movement in either direction, debug overlay toggle and draw-mode cycling both worknpm run check:all-examples/parallaxitself passes; several unrelated examples (3d,asteroids,canvas,hybrid,pixels,pong,snake,terrain) fail due to a pre-existing stalebrighterscriptversion in their ownnode_modules(unrelated to this branch - none of them were touched here)🤖 Generated with Claude Code