Skip to main content

platformer-movement

Platformer Movement in FlatRedBall2. Use when implementing platformer mechanics including jumping, ground detection, PlatformerBehavior, PlatformerValues, double jump, air control, variable-height jumps, or side-scrolling movement. Trigger on any platformer-related question.

설치로 이동

소스 정보

저장소
vchelaru/FlatRedBall2
최근 소스 활동
2026년 8월 23일 14:29
감지된 SKILL.md 언어
영어
스타
14
포크
8

설치 방법

기본적으로 소스를 먼저 확인하는 Prompt가 선택됩니다. 직접 명령으로 전환하거나 로컬 사본을 다운로드할 수도 있습니다.

소스 파일 검토

설치 여부를 결정하기 전에 SKILL.md와 SkillsMP에 표시된 보조 파일을 읽어 보세요.

SKILL.md 표시 중

SKILL.md
소스 지침 · 읽기 전용 미리보기
name
platformer-movement
description
Platformer Movement in FlatRedBall2. Use when implementing platformer mechanics including jumping, ground detection, PlatformerBehavior, PlatformerValues, double jump, air control, variable-height jumps, or side-scrolling movement. Trigger on any platformer-related question.
# Platformer Movement ## Overview Platformer movement is provided by two classes in `FlatRedBall2.Movement`: - **`PlatformerValues`** — a plain data class holding movement parameters for one movement mode (Ground, Air, etc.) - **`PlatformerBehavior`** — a component added to an entity that reads input, drives the state machine, and applies velocity/acceleration each frame ## Minimal Setup Movement coefficients are **recommended** in a JSON file for fast tuning and hot-reload. Copy the template from `.claude/templates/PlatformerConfig/player.platformer.json` into the project's `Content/` folder, adjust values, and add `<Content Include="Content/*.json" CopyToOutputDirectory="PreserveNewest" />` to the `.csproj`. For prototypes/tests, equivalent hardcoded values in C# are valid. ```csharp public class Player : Entity { private readonly PlatformerBehavior _platformer = new(); public override void CustomInitialize() { PlatformerConfig.FromJson("Content/player.platformer.json").ApplyTo(_platformer); var keyboard = Engine.Input.Keyboard; // Use .Or() to accept multiple key combos (e.g. Space or Up for jump, Arrows or WASD for move): _platformer.JumpInput = new KeyboardPressableInput(keyboard, Keys.Space) .Or(new KeyboardPressableInput(keyboard, Keys.Up)); _platformer.MovementInput = new KeyboardInput2D(keyboard, Keys.Left, Keys.Right, Keys.Up, Keys.Down) .Or(new KeyboardInput2D(keyboard, Keys.A, Keys.D, Keys.W, Keys.S)); } public override void CustomActivity(FrameTime time) { _platformer.Update(this, time); // call AFTER collisions are resolved } } ``` ## PlatformerConfig JSON `PlatformerConfig.FromJson(path)` deserializes a JSON file into a pure data model; the `ApplyTo` extension method pushes it onto a `PlatformerBehavior`. The config is a model — it does not reference the behavior; `ApplyTo` lives in `PlatformerConfigExtensions`. Template: `.claude/templates/PlatformerConfig/player.platformer.json` **Movement slots** are fixed names mapping to behavior fields: `ground` → `GroundMovement`, `air` → `AirMovement`, `climbing` → `ClimbingMovement` (see "Climbing Ladders" below), `afterDoubleJump` → reserved (parsed but not applied until the behavior wires a double-jump slot). All fields in a slot are nullable; omitted fields fall back to `new PlatformerValues()` defaults. **Jump configuration** supports two mutually-exclusive modes per slot: - **Derived (preferred):** `minJumpHeight` + optional `maxJumpHeight` → calls `SetJumpHeights`. - **Raw (escape hatch):** `JumpVelocity` + `JumpApplyLength` + `JumpApplyByButtonHold` set directly. - Specifying fields from both modes in the same slot throws `InvalidOperationException`. **Gravity gotcha — derived mode uses airborne gravity.** The jump trajectory always runs under the `air` slot's `Gravity` (while grounded, collision cancels gravity — so ground.Gravity never acts on the arc). `ApplyTo` resolves this automatically: derived mode on the ground slot uses `air.Gravity` for its `JumpVelocity`/`JumpApplyLength` math; if no `air` slot is authored, it falls back to the slot's own `Gravity`. Mismatched ground/air gravities with derived-mode jumps will produce correct heights, but the ground slot's `Gravity` field itself is effectively ceremonial for trajectory purposes — keep it equal to `air.Gravity` for a clear mental model. **TimeSpan fields** (`AccelerationTimeX`, `DecelerationTimeX`, `JumpApplyLength`) are represented as seconds (float) in JSON. ## Critical Call-Order Rule `PlatformerBehavior.Update` reads `entity.LastReposition` to determine ground state. `LastReposition` is accumulated by `SeparateFrom` during collision resolution and reset at the start of each `PhysicsUpdate`. **The Screen update order is: PhysicsUpdate → CollisionRelationships → CustomActivity.** This means `Update` called from `CustomActivity` will always see the current frame's collision results — no special wiring needed. ## PlatformerValues Fields | Field | Description | |---|---| | `MaxSpeedX` | Maximum horizontal speed in world units/sec | | `AccelerationTimeX` | Time to reach `MaxSpeedX` from rest. `TimeSpan.Zero` = instant | | `DecelerationTimeX` | Time to stop from `MaxSpeedX`. `TimeSpan.Zero` = instant | | `Gravity` | Downward acceleration (positive value, Y− direction applied internally). Only acts while airborne — collision cancels it while grounded. A ground slot's `Gravity` is used only as a fallback for `SetJumpHeights` when no air slot is present. | | `MaxFallSpeed` | Maximum downward speed (prevents infinite fall acceleration) | | `JumpVelocity` | Upward velocity applied when jump is triggered | | `JumpApplyLength` | How long to sustain `JumpVelocity` after pressing jump | | `JumpApplyByButtonHold` | If true, releasing jump early cuts the jump short | | `SetJumpHeights(min, max?)` | Computes `JumpVelocity`, `JumpApplyLength`, and `JumpApplyByButtonHold` from desired min/max jump heights in world units. `Gravity` must be set first. Prefer this over setting jump fields manually. | ## Ground vs Air values `GroundMovement` is used when `IsOnGround == true`; `AirMovement` is used otherwise. If `GroundMovement` is null, `AirMovement` is used for both states. Common patterns: - Same values for ground and air: assign the same instance to both - Reduced air control: set a lower `MaxSpeedX` or `AccelerationTimeX` in `AirMovement` - Ice: high `AccelerationTimeX` and `DecelerationTimeX` in `GroundMovement` ## Multiple Movement Sets (Water, Ice, Power-ups) A `PlatformerConfig` JSON is a **full description** of a movement state. For a second context (swimming, ice, mud, power-up state) load a second JSON file into a second `PlatformerConfig` field and call `ApplyTo(_platformer)` on whichever one matches the current state. Call it each frame from `CustomActivity` — `ApplyTo` mutates the existing `PlatformerValues` instances in place (zero allocation on the hot path). **Replace semantics, not overlay.** `ApplyTo` makes the behavior reflect the JSON exactly. Slots the JSON omits become `null` (for `GroundMovement`, `AfterDoubleJump`, `ClimbingMovement`) or reset to defaults (for `AirMovement`, which is non-nullable). To disable a slot in the alternate context (e.g. no double jump while swimming), simply omit `afterDoubleJump` from the water JSON — no code-side null-outs. Inside a present slot, fields the JSON omits reset to their `PlatformerValues` defaults, not the previous config's values. **Do not harvest `PlatformerValues` into local fields** and swap them manually — that's more code for the same outcome, and fights the zero-allocation in-place mutation. ## Reading State ```csharp _platformer.IsOnGround // true if entity was pushed upward by a collision this frame _platformer.IsApplyingJump // true while jump sustain is active (button held, duration not yet elapsed) _platformer.DirectionFacing // HorizontalDirection.Left or .Right _platformer.GroundHorizontalVelocity // platform velocity transferred this frame, 0 when not on a moving platform ``` ## Double Jump (Air Jumps) `PlatformerBehavior` only jumps from the ground. Implement air jumps manually in the entity: ```csharp private int _airJumpsRemaining; private const int MaxAirJumps = 1; // 1 = double jump // In CustomActivity, after _platformer.Update(this, time): if (_platformer.IsOnGround) _airJumpsRemaining = MaxAirJumps; if (_platformer.JumpInput.WasJustPressed && !_platformer.IsOnGround && !_platformer.IsApplyingJump && _airJumpsRemaining > 0) { VelocityY = _platformer.AirMovement.JumpVelocity; _airJumpsRemaining--; } ``` Read the jump trigger off `_platformer.JumpInput` rather than the raw keyboard — that way the air jump automatically honors whatever binding (Space+Up, gamepad A, etc.) was configured in `CustomInitialize` via `.Or(...)`. The `!_platformer.IsApplyingJump` guard prevents the air jump from triggering during the sustain phase of the first jump — the player must reach the peak before double-jumping. ## Entity Origin and Shape Offset In platformers, an entity's Y position represents **the feet** — the point collision resolves ground contact against. The **collision shape** must be offset upward so its bottom edge is flush with Y=0; this is non-negotiable, it's what "standing on the ground" means physically: ```csharp // For a 12×28 collision box, offset Y by half the height so the bottom sits at the entity's feet var collisionBox = new AARect { Width = 12, Height = 28, Y = 14, // Height / 2 — centers the box above the entity origin }; Add(collisionBox); ``` **The sprite's `RelativeY` is different — it's art, not physics.** It only needs to match the shape's flush-bottom offset for a strictly side-on camera (Mario-style). A platformer with a tilted camera (Donkey Kong Country-style) shows a sliver of the ground plane, so the sprite's ground-contact point sits inside its bounding box and must be set by eye — see `animation` skill's `references/achx-authoring.md` (Ground-Contact Point section). `RelativeX` stays `0` in both cases since sprites already draw X-centered. **If you skip the shape offset**, the character's feet will not align with the ground. ## Collision Setup Use `BounceFirstOnCollision(elasticity: 0f)` — **not** `MoveFirstOnCollision`. The solid side can be a `TileShapes` (for static level geometry) or an entity factory (for moving platforms, destructible blocks, etc.): ```csharp // Against static tile geometry (preferred for level walls/floors) screen.AddCollisionRelationship(playerFactory, tileShapeCollection) .BounceFirstOnCollision(elasticity: 0f); // Against entity-based solids (moving platforms, breakable walls, etc.) screen.AddCollisionRelationship<Player, MovingPlatform>(playerFactory, platformFactory) .BounceFirstOnCollision(elasticity: 0f); ``` `BounceFirstOnCollision` (which expands to the bounce with the player fully displaced and the solid fixed) both separates the player (populating `LastReposition` for ground detection) and zeroes the velocity component into the surface. Without it, hitting a ceiling leaves the player with upward velocity and they float against it. `MoveFirstOnCollision` only repositions — it never touches velocity, which is wrong for platformer collision. **Entity solids arranged in a grid (brick rows, crate stacks, destructible walls) must set `factory.IsSolidGrid = true`** — otherwise the player snags on seams between adjacent entities (each body resolves separation independently). See `entities-and-factories`. Use `Overlay.DrawSolidSides(factory)` in `CustomActivity` to visualize. ## Slopes and Ramps Set `SlopeMode = SlopeCollisionMode.PlatformerFloor` on the **player's collision relationship** (not on the `TileShapes`) to enable slope collision for polygon tiles. In this mode: - **Polygon tiles push vertically only** (heightmap-based). The polygon's surface Y at the player's center X determines the push. No horizontal component means no snagging at slope seams. - **Rect tiles next to polygon tiles** get their shared face suppressed automatically (like adjacent rects already do). - **Preferential landing**: if the player is falling and standard collision would push them sideways off a ledge edge, they land on top instead. Only fires when the tile's Up face is active (nothing above it). Create slope tiles with `AddPolygonTileAtCell`: ```csharp // Right-triangle slope going up-right (bottom edge flat, hypotenuse top-right to bottom-left) var upRampSlope = Polygon.FromPoints(new[] { new Vector2(-8f, -8f), // bottom-left new Vector2( 8f, -8f), // bottom-right new Vector2( 8f, 8f), // top-right }); tileShapeCollection.AddPolygonTileAtCell(col, row, upRampSlope); var playerVsTiles = AddCollisionRelationship(_playerFactory, tileShapeCollection); playerVsTiles.SlopeMode = SlopeCollisionMode.PlatformerFloor; playerVsTiles.BounceFirstOnCollision(elasticity: 0f); ``` Default is `SlopeCollisionMode.Standard` (SAT collision for polygon tiles), which is correct for top-down games but causes snagging in platformers. `SlopeMode` is per-relationship so the same tile collection can be shared by a platformer player (`PlatformerFloor`) and other entities (`Standard`, e.g. a kicked ball) at the same time. ## Ground Snapping (Slope Adherence) Players who run off a downslope or off the top of an up-ramp onto lower flat ground will briefly go airborne for a frame without snapping — a standard platformer feature eliminates this by "hugging" the entity to a nearby surface. Wiring checklist — all three conditions must hold for snap to fire: 1. The player entity implements `IPlatformerEntity` (exposes `Platformer => _platformer`) 2. A `CollisionRelationship` between the player and a `TileShapes` has `SlopeMode = SlopeCollisionMode.PlatformerFloor` — each such relationship automatically contributes its collection as a snap probe target 3. `PlatformerBehavior.CollisionShape` is set to the player's collision `AARect`, and `PlatformerValues.SlopeSnapDistance > 0` on the active values set (default `8f`), and the entity was on a sloped surface last frame (`CurrentSlope != 0`) ```csharp public class Player : Entity, IPlatformerEntity { private readonly PlatformerBehavior _platformer = new(); public PlatformerBehavior Platformer => _platformer; // ... _platformer.CollisionShape = body; } // In screen: var playerVsSolid = AddCollisionRelationship(_playerFactory, _solid); playerVsSolid.SlopeMode = SlopeCollisionMode.PlatformerFloor; // enables snap + slope collision ``` **No explicit snap target.** A player can have multiple `PlatformerFloor` relationships (solid level, moving platforms, one-way floors, etc.) and every one of them contributes as a snap candidate — the first one to produce a hit within the frame wins, and subsequent relationships no-op for the rest of the frame. **Partial-config throws.** If the active values have `SlopeSnapDistance > 0` and a `PlatformerFloor` relationship dispatches while `CollisionShape` is null, `ConsiderSnappingTo` throws `InvalidOperationException`. Set `SlopeSnapDistance = 0` on values that should opt out of snap instead of leaving `CollisionShape` null. **Debugging snap with `OnSnapDiagnostic`.** If snap isn't firing, assign a callback to get a one-line reason per frame (success or skip): ```csharp _platformer.OnSnapDiagnostic = msg => System.Diagnostics.Debug.WriteLine(msg); ``` Messages begin with `"snap: "` on success or `"skip: <reason>"` when a gate aborted. When the callback is null there is no allocation cost. Feet Y is derived from the shape (`AbsoluteY - Height/2`) at probe time, so the shape can be placed anywhere relative to the entity origin without additional configuration. Tuning lives on `PlatformerValues`: | Field | Default | Meaning | |---|---|---| | `SlopeSnapDistance` | `8f` | Max downward probe distance. `0` disables snap for this values set. Snap is also gated on `CurrentSlope != 0` (was on a sloped surface last frame) — flat-to-flat cliff drops fall ballistically regardless of this value. | | `SlopeSnapMaxAngleDegrees` | `60f` | Surfaces whose upward normal is within this many degrees of straight up qualify as "walkable" for snap. | The mechanism: 1. If the player was grounded last frame, is not grounded this frame, is not rising, and was on a sloped surface last frame (`CurrentSlope != 0`), 2. Raycast straight down from the player's feet by `SlopeSnapDistance`, 3. If it hits a walkable surface (normal within the angle threshold), move the player onto it, zero `VelocityY`, and set `IsOnGround = true`. **Flat-to-flat ledges fall ballistically.** The slope gate means walking off the edge of a flat tile onto a lower flat tile does *not* snap — behaves as a cliff drop, matching classic platformer feel. Snap is specifically for hugging downslopes across tile seams, not for stepping onto lower platforms. **Per-values-set config is intentional.** A walking state wants snap on; a ball/wheel state that wants Sonic-style launches off ramps should set `SlopeSnapDistance = 0` on its `PlatformerValues` so it flies off ramps naturally. **The "was grounded last frame" gate is what makes jumps work.** Without it, snap would yank the player back to the floor on the first frame of every jump. Don't attempt to bypass it. ## Slope Speed Adjustment `PlatformerBehavior.CurrentSlope` (signed degrees, +X-rise positive, `0` when airborne/flat) is refreshed each frame by a short downward raycast contributed by every `PlatformerFloor` relationship. **Defaults are active, not opt-in** — any platformer with slope tiles immediately gets the classic "slow going up, faster going down" feel without additional configuration. | Field | Default | Meaning | |---|---|---| | `UphillFullSpeedSlope` | `0` | Below this, full `MaxSpeedX` going uphill. | | `UphillStopSpeedSlope` | `60` | At/above this, uphill speed = 0. Linearly interpolated between. Set equal to `UphillFullSpeedSlope` to disable slowdown. | | `DownhillFullSpeedSlope` | `0` | Below this, downhill uses unmodified `MaxSpeedX`. | | `DownhillMaxSpeedSlope` | `60` | At/above this, downhill speed is multiplied by `DownhillMaxSpeedMultiplier`. Linearly interpolated between. | | `DownhillMaxSpeedMultiplier` | `1.5` | Peak multiplier. Set to `1` to disable downhill boost. | Under defaults, a 30° slope cuts uphill speed to 50% and boosts downhill speed to 125%; a 45° slope is 25% / 137.5%. Uphill vs downhill is determined by `sign(inputX) == sign(CurrentSlope)`. **Requires `SlopeMode = SlopeCollisionMode.PlatformerFloor`** on the player's collision relationship — without it, `CurrentSlope` stays 0 and the multipliers collapse to 1. When using acceleration, the adjusted max speed drives `AccelerationTimeX` magnitude (speeding up); `DecelerationTimeX` still uses the raw `MaxSpeedX` so braking isn't slowed on an uphill. ## Moving Platforms Standing on another `Entity` with non-zero `VelocityX` automatically transfers that horizontal velocity to the platformer entity for the frame — the player rides the platform with no input, and a jump carries the platform's momentum into the air. No opt-in needed: any bounce relationship between an `IPlatformerEntity` and a regular `Entity` (not a `TileShapes`) gets this behavior whenever the separation pushes the platformer upward. ```csharp AddCollisionRelationship<Player, MovingPlatform>(_playerFactory, _platformFactory) .BounceFirstOnCollision(elasticity: 0f); ``` The platform's own movement (path-follower, ping-pong in `CustomActivity`, etc.) is independent — the engine just reads its `VelocityX` at collision time. Tile collections are excluded by design; moving level geometry should be authored as entities. **Animation gotcha:** standing still on a moving platform leaves the player with `VelocityX != 0` (they inherit the platform's velocity), so drive walk/idle animations off `MovementInput.X`, not `VelocityX` — see the animation section below. ## One-Way Platforms and Drop-Through Jump-through (cloud) platforms are configured on the **collision relationship**, not the behavior — set `relationship.OneWayDirection = OneWayDirection.Up` and `relationship.CanDropThrough = true`. The second flag is required for drop-through to bypass the relationship; leaving it `false` makes the barrier hard (e.g. Yoshi's Island ratchet doors — always blocks, Down+Jump has no effect on it). See the `collision-relationships` skill for the relationship-level semantics. Drop-through is handled by the behavior. Set `PlatformerValues.CanFallThroughOneWayCollision = true` (default) to enable; `false` makes Down+Jump perform a normal jump and airborne Down has no effect. Triggers: - **Grounded Down+Jump** — suppresses one-way collision for one frame and skips the regular jump. After that frame, the entity's `LastPosition` is below the surface, so the one-way gate's positional check naturally prevents re-landing.
GitHub에서 보기
이 SKILL.md는 매우 커서 SkillsMP가 여기에는 첫 섹션만 미리 보여줍니다. GitHub에서 보기