Roll left
Roll right
Jump
const renderLayers = {
foreground: 1 << 0,
};
const gravity = { x: 0, y: -700 };
const terrainWidth = 8000;
export const createRollingBallGame = async (): Promise<Game> => {
const { game, world, renderContext, time } = createGame('demo-game');
const cameraEntity = createCamera(world, {
isStatic: true,
cullingMask: renderLayers.foreground,
verticalWorldUnits: DEMO_VERTICAL_WORLD_UNITS,
});
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: { x: 160, y: 150 },
tint: new Color(0.6, 0.6, 0.6, 1),
},
fill: {
textureUrl: 'img/kenney_pattern-pack/PNG/Default/pattern_37.png',,
tileSize: { x: 30, y: 30 },
tint: new Color(0.4, 0.29, 0.18, 1),
},
borderWidth: 30,
borderBlend: 5,
});
const spawnPosition = {
x: terrain.spawnX,
y: terrain.worldSurfaceYAt(terrain.spawnX) + 60,
};
const player = await createPlayer(
world,
renderContext,
renderLayers.foreground,
spawnPosition,
gravity,
);
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 = Vec2.clone(spawnPosition);
cameraPosition.local = Vec2.clone(spawnPosition);
const collisionPairs: CollisionPair[] = [];
const collisionManifolds: CollisionManifold[] = [];
const contactConstraints: ContactConstraint[] = [];
// Roll input must be applied before the motor system, which must in turn
// run before the broad/narrow-phase and resolution systems, 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 resolution 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(createGravityEcsSystem(time));
world.addSystem(createBroadPhaseEcsSystem(collisionPairs));
world.addSystem(
createNarrowPhaseEcsSystem(collisionPairs, collisionManifolds),
);
world.addSystem(
createCollisionResolutionEcsSystem(
collisionManifolds,
contactConstraints,
time,
),
);
world.addSystem(
createJumpEcsSystem(
collisionManifolds,
player.entity,
terrain.entity,
jumpInput,
spawnPosition,
),
);
world.addSystem(createCameraFollowEcsSystem(playerPosition, time));
world.addSystem(createTerrainRenderEcsSystem(renderContext));
world.addSystem(createRenderEcsSystem(renderContext));
world.addSystem(createEulerIntegrationEcsSystem(time));
return game;
};
A standalone showcase of TerrainCollider: 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. TerrainCollider'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 this tick's collisionManifolds against the terrain entity).