Top-Down Movement
Movement patterns for top-down games like arena shooters, Zelda-like games, RPGs, and twin-stick shooters.
8-Direction Movement
Use Case: Arena shooters, zelda-like games, RPGs
Basic 8-direction movement with constant speed.
Game Definition
import { defineGame } from '@martini-kit/core';
const PLAYER_SPEED = 200; // pixels per second
export const game = defineGame({
setup: ({ playerIds }) => ({
players: Object.fromEntries(
playerIds.map((id, index) => [
id,
{
x: 400,
y: 300,
vx: 0,
vy: 0,
},
])
),
inputs: {} as Record<string, {
left: boolean;
right: boolean;
up: boolean;
down: boolean;
}>,
}),
actions: {
move: {
apply: (state, context, input: {
left: boolean;
right: boolean;
up: boolean;
down: boolean;
}) => {
// Store input for physics update
state.inputs[context.targetId] = input;
const player = state.players[context.targetId];
if (!player) return;
// Calculate movement direction
const dx = (input.right ? 1 : 0) - (input.left ? 1 : 0);
const dy = (input.down ? 1 : 0) - (input.up ? 1 : 0);
// Normalize diagonal movement
if (dx !== 0 && dy !== 0) {
const length = Math.sqrt(dx * dx + dy * dy);
player.vx = (dx / length) * PLAYER_SPEED;
player.vy = (dy / length) * PLAYER_SPEED;
} else {
player.vx = dx * PLAYER_SPEED;
player.vy = dy * PLAYER_SPEED;
}
},
},
// Physics update (called every frame on host)
tick: {
apply: (state, context, input: { delta: number }) => {
const deltaSeconds = input.delta / 1000;
for (const player of Object.values(state.players)) {
// Update position
player.x += player.vx * deltaSeconds;
player.y += player.vy * deltaSeconds;
// Clamp to arena bounds
player.x = Math.max(20, Math.min(780, player.x));
player.y = Math.max(20, Math.min(580, player.y));
}
},
},
},
}); Phaser Scene (Input Handling)
Using InputManager Helper - Automatic WASD/Arrow key handling:
import Phaser from 'phaser';
import { PhaserAdapter, InputManager } from '@martini-kit/phaser';
export class GameScene extends Phaser.Scene {
private adapter!: PhaserAdapter;
private inputManager!: InputManager;
create() {
this.adapter = new PhaserAdapter(runtime, this);
// Automatically handles WASD + Arrow keys
this.inputManager = new InputManager(this.adapter, this, {
type: 'wasd-arrows',
actionName: 'move',
});
// That's it! Input is automatically submitted every frame
}
update(time: number, delta: number) {
// Update physics (host only)
if (this.adapter.isHost()) {
this.runtime.submitAction('tick', { delta });
}
this.adapter.update(time, delta);
}
} Benefits:
- ✅ Handles both WASD and Arrow keys automatically
- ✅ Auto-submits input every frame
- ✅ Just 3 lines instead of 20+
- ✅ Supports gamepad out of the box
Features:
- ✅ Smooth 8-direction movement
- ✅ Diagonal speed normalization
- ✅ Boundary clamping
- ✅ WASD and arrow key support
Analog/Free Movement
Use Case: Racing games, twin-stick shooters
Analog movement with acceleration and deceleration.
Game Definition
const MAX_SPEED = 300;
const ACCELERATION = 800; // pixels/s²
const FRICTION = 600; // deceleration when no input
export const game = defineGame({
setup: ({ playerIds }) => ({
players: Object.fromEntries(
playerIds.map((id) => [
id,
{
x: 400,
y: 300,
vx: 0,
vy: 0,
},
])
),
inputs: {} as Record<string, {
x: number; // -1 to 1
y: number; // -1 to 1
}>,
}),
actions: {
move: {
apply: (state, context, input: { x: number; y: number }) => {
state.inputs[context.targetId] = input;
},
},
tick: {
apply: (state, context, input: { delta: number }) => {
const deltaSeconds = input.delta / 1000;
for (const [playerId, player] of Object.entries(state.players)) {
const inputData = state.inputs[playerId];
if (!inputData) continue;
// Apply acceleration
if (inputData.x !== 0 || inputData.y !== 0) {
player.vx += inputData.x * ACCELERATION * deltaSeconds;
player.vy += inputData.y * ACCELERATION * deltaSeconds;
// Cap at max speed
const speed = Math.sqrt(player.vx ** 2 + player.vy ** 2);
if (speed > MAX_SPEED) {
player.vx = (player.vx / speed) * MAX_SPEED;
player.vy = (player.vy / speed) * MAX_SPEED;
}
} else {
// Apply friction
const speed = Math.sqrt(player.vx ** 2 + player.vy ** 2);
if (speed > 0) {
const frictionAmount = Math.min(FRICTION * deltaSeconds, speed);
player.vx -= (player.vx / speed) * frictionAmount;
player.vy -= (player.vy / speed) * frictionAmount;
}
}
// Update position
player.x += player.vx * deltaSeconds;
player.y += player.vy * deltaSeconds;
// Boundary bounce
if (player.x < 20 || player.x > 780) {
player.vx *= -0.5; // Bounce with energy loss
player.x = Math.max(20, Math.min(780, player.x));
}
if (player.y < 20 || player.y > 580) {
player.vy *= -0.5;
player.y = Math.max(20, Math.min(580, player.y));
}
}
},
},
},
}); Features:
- ✅ Smooth acceleration
- ✅ Friction/deceleration
- ✅ Speed capping
- ✅ Boundary bounce
See Also
- Platformer Movement - Side-scrolling movement with gravity
- Advanced Movement - Mouse/pointer and rotation
- Physics & Collision - Physics integration