Skip to content
Work in progress. These docs describe Spawnite at launch, and some parts are still being built.

Side-scrollers

A side-scroller plays on the side plane of the 2D frame: x runs across the screen, y up it, and the camera looks along z. Three pieces turn the engine’s character into a platformer’s:

  • The plane lock. The character controller’s plane setting keeps a character on x and y.
  • One-way platforms. A collider’s oneWay flag lets a character jump up through a platform and stand on it.
  • Parallax layers. ParallaxLayer scrolls a background at a share of the camera’s speed.

Everything else is the engine’s own: the character’s walk, its jump and coyote time, moving platforms, and Camera2D with plane={Plane2D.Side}.

This level stands a ledge and a one-way platform on the grid’s floor, locks the character to the side plane, and draws a range of hills behind at half the camera’s speed:

packages/engine/test/outside/Platformer.tsx
import {
box,
CharacterSpawn,
Entity,
Landscape,
Plane2D,
} from "@spawnite/engine";
import { Camera2D, ParallaxLayer } from "@spawnite/engine/2d";
// A side-scroller's level on the grid's floor: a ledge 1 m high and 8 m
// long, a one-way platform 2 m above its right half, and a range of hills
// behind at half the camera's speed.
export function Platformer() {
return (
<Landscape map="grid">
<CharacterSpawn
position={[-3, 1, 0]}
controller={{ plane: Plane2D.Side, jumpHeight: 2.5 }}
movement={{ coyoteSeconds: 0.1 }}
/>
<Entity
position={[0, 0.5, 0]}
collider={{ shape: box({ size: [8, 1, 4] }) }}
/>
<Entity
position={[2, 2.85, 0]}
collider={{ shape: box({ size: [4, 0.3, 4] }), oneWay: true }}
/>
<Camera2D plane={Plane2D.Side} />
<ParallaxLayer scrollFactor={0.5} repeatSize={[16, 0]}>
<mesh position={[0, 3, -10]}>
<planeGeometry args={[16, 6]} />
<meshBasicMaterial color="#3b5dc9" />
</mesh>
</ParallaxLayer>
</Landscape>
);
}

List characters(), from @spawnite/engine/characters, and cameras(), from @spawnite/engine/cameras, in defineGame’s plugins. CharacterSpawn stands the character with its feet at its position. The sample’s jumpHeight of 2.5 m clears the platform 2 m up, where the default 1.2 m would not.

A character locked to the side plane, below, walks on the level’s tiles and bodies alone, with no map under them, as a Godot or Unity 2D level does. For pits, set the scene’s killBelowHeight below the level, so a character that falls past it comes back where the game says. A character that is not locked needs a map’s ground to walk on, and stays within the map’s edge.

controller={{ plane: Plane2D.Side }} holds a character’s z where it stands. The walk keeps its directions, and the faces it slides along, in the x and y plane. So a slope, a wall or a boulder moves the character across and up, never into the screen. Input, impulses, pushes, platform carries and the recovery from an overlap move it on x and y too. A teleport moves the plane with the character, as Godot’s axis lock does, where Unreal’s bConstrainToPlane declares a fixed plane instead.

The characters plugin steers a locked character as Godot’s and Unity’s platformer templates do. The move stick’s x runs it across, at its acceleration while it speeds up and at its deceleration while it slows or turns back. Its body faces left or right. The stick’s up and down run nothing, so a held diagonal runs at full speed.

A locked NPC moves through the controller, since the ground’s navmesh does not lie in the side plane. A ground path from walkTo never reaches a locked character: the movement drops the path with a development warning. Move an NPC across the level from a system of your own with moveCharacter.

oneWay: true on a collider makes a platform a character jumps up through and stands on from above, as Godot’s one_way_collision and Unity’s PlatformEffector2D do. The walk meets one of its faces only where the face points up and the character comes down onto it. A character that overlaps it, from below or from the side, passes through it. A platform that rises into a character from below catches it and carries it, as a lift does.

A one-way collider is a convex shape and no trigger: a box, a sphere, a capsule, a cylinder, a cone or a convex hull. The types hold it on a declaration, and setCollider throws on a change that breaks it. A straight move that enters a convex shape through a face it passes leaves it without meeting an upward face from outside. A mesh, a heightfield or voxels cannot promise that.

The flag is the walk’s alone. Queries, weapons, sight and dynamic bodies meet a one-way platform from every side, as Godot’s raycasts do. isOnOneWay, on CharacterTrait and on the result of moveCharacter, says whether the character stands on one.

A moving platform is an entity with a kinematic body, body={{ bodyType: RigidBodyType.Kinematic }}, as Godot’s AnimatableBody2D is. It carries what stands on it, and a solid one pushes what it meets. Bob moves one up and down in place; it is the behaviours() plugin’s, from @spawnite/engine/behaviours. For a path of your own, a system sets the platform’s pose each step with setNextPose.

moveCharacter(step, character, { velocity, dropThrough: true }) drops the character through the one-way platform it stands on. It leaves the floor at once, and the walk passes every one-way platform at the height it dropped from until it lands. Two platforms side by side drop it together at their seam, and a lower platform still catches it. A drop counts as a jump for coyote time and keeps no upward part of a moving platform’s velocity. Off a one-way platform, dropThrough does nothing.

The characters plugin drops a locked character by the movement’s dropThroughInput:

  • DropThroughInput.DownJump, the default, drops on the jump pressed with the stick held down, as MapleStory does. On solid ground the same press jumps.
  • DropThroughInput.Down drops on the stick held down, as Terraria does. Held, it drops through each platform the character lands on.
  • DropThroughInput.None leaves the drop to a game’s own movement, which calls moveCharacter with dropThrough.

ParallaxLayer moves its children at a share of the camera’s movement. A scrollFactor of 0 keeps a layer with the camera, as a sky, 1 moves it with the world, and a share between draws a distance. A factor above 1 draws a foreground. It takes one number, or a pair for across and up. Phaser calls the same setting scrollFactor, and Godot scroll_scale.

The layer reads the scene’s Camera2D after the camera moves each frame, so it never trails the camera. Under a pixel-perfect camera it stands on the camera’s texel grid, within half a texel of its exact place. The camera’s shake moves every layer alike.

The layer takes two more settings:

  • origin is the point on the plane where the children stand as written: while the camera’s frame is centred there, they draw where they are written. It is [0, 0] unless set.
  • repeatSize repeats the children every so many metres across and up, 0 for no repeat on an axis. Copies cover the frame however far the camera goes. Under a pixel-perfect camera a repeat is a whole number of texels, or a development error names the nearest one.

Depth orders the layers: on the side plane, a layer at a negative z draws behind the scene and one at a positive z in front. A layer is a view, so a room never mounts it. Its children are meshes. An Entity inside one throws in development, since the loop places an entity in the world, where the layer’s move would not reach it.

A layer places its children in world metres, so mount it at the scene’s root or inside Landscape, which does not move, and under no moved, turned or scaled group. It measures its repeats on the camera’s plane, which an orthographic Camera2D draws at every depth alike. Under a perspective Camera2D, give a deep layer children wide enough and no repeat. Development warns of either case.

  • Tile map layers lie on the ground plane alone, so a side-scroller’s level is built from colliders.
  • A locked character walks no path: a platformer’s path planner, with its jumps and drops, is not built.
  • The default movement keeps no jump buffer. The platformer kit keeps one, though it does not steer on the side plane yet.
  • Dynamic bodies meet a one-way platform from every side.
  • The one-way margin, 1 cm, is fixed, and so is its surface arc: any face that points up at all holds a character coming down onto it, as Unity’s 180-degree arc does.