Skip to main content

Rolling Ball

A

Roll left

D

Roll right

Jump

game.ts
const renderLayers = {
foreground: 1 << 0,
};

const gravity = new Vector2(0, -700);
const terrainWidth = 8000;

export const createRollingBallGame = async (): Promise<Game> => {
const { game, world, renderContext, time } = createGame('demo-game');

// The terrain render system draws directly, outside the sprite pipeline
// (see createTerrainRenderEcsSystem), and owns the frame's one real
// clear; without `none` here, createRenderEcsSystem's own clear would
// wipe the terrain right before drawing the sprites on top of it.
renderContext.clearStrategy = CLEAR_STRATEGY.none;

const cameraEntity = createCamera(world, {
isStatic: true,
cullingMask: renderLayers.foreground,
verticalWorldUnits: DEMO_VERTICAL_WORLD_UNITS,
});

const physicsWorld = new PhysicsWorld({ gravity });

const { rollInput, jumpInput } = createInputs(world, time);

const terrain = await createTerrain(world, renderContext, {
totalWidth: terrainWidth,
border: {
textureUrl: 'img/kenney_pattern-pack/PNG/Default/pattern_19.png',,
tileSize: new Vector2(160, 150),
tint: new Color(0.6, 0.6, 0.6, 1),
},
fill: {
textureUrl: 'img/kenney_pattern-pack/PNG/Default/pattern_37.png',,
tileSize: new Vector2(30, 30),
tint: new Color(0.4, 0.29, 0.18, 1),
},
borderWidth: 30,
borderBlend: 5,
});

const spawnPosition = new Vector2(
terrain.spawnX,
terrain.worldSurfaceYAt(terrain.spawnX) + 60,
);

const player = await createPlayer(
world,
renderContext,
renderLayers.foreground,
spawnPosition,
);

const playerPosition = world.getComponent<PositionEcsComponent>(
player.entity,
positionId,
)!;

// Start the camera already centered on the ball, rather than easing in
// from the world origin on the first frame.
const cameraPosition = world.getComponent<PositionEcsComponent>(
cameraEntity,
positionId,
)!;

cameraPosition.world = spawnPosition.clone();
cameraPosition.local = spawnPosition.clone();

// Roll input must be applied before the motor system, which must in turn
// run before createPhysicsSyncEcsSystem, so this tick's input reaches
// this tick's physics step - see the Applying Forces guide's registration
// order caution. The jump/respawn and camera-follow systems read the
// physics step's results, so they run after it instead. The terrain
// render system must run before createRenderEcsSystem so the ball's
// sprite draws on top of the terrain mesh, not underneath it.
world.addSystem(createRollEcsSystem(rollInput));
world.addSystem(createAngularVelocityMotorEcsSystem(time));
world.addSystem(createPhysicsSyncEcsSystem(physicsWorld, time));
world.addSystem(
createJumpEcsSystem(
physicsWorld,
player.body,
terrain.body,
jumpInput,
spawnPosition,
),
);
world.addSystem(createCameraFollowEcsSystem(playerPosition, time));
world.addSystem(createTerrainRenderEcsSystem(renderContext, terrain.mesh));
world.addSystem(createRenderEcsSystem(renderContext));

return game;
};

A standalone showcase of TerrainShape: a long course whose smooth silhouette comes from a Catmull-Rom curve through sparse, randomly-placed control points, triangulated into a single mesh and textured with a tileable grass-like border blending into a tileable dirt-like fill. TerrainShape's own collision points are sampled from that exact same curve, so what's drawn always matches what the ball touches. Roll input drives the ball's AngularVelocityMotorEcsComponent, and friction against the terrain - ordinary collision resolution, nothing special-cased - turns that spin into rolling motion up and down the hills. A small camera-follow system keeps the ball in view as it travels, and a jump impulse fires while grounded (tracked via PhysicsWorld.collisionStarts/collisionEnds against the terrain body).