Cómo construir un juego de carreras cooperativo local
Este tutorial fue escrito en febrero de 2026, para la v2 del motor.
El motor no tiene física integrada — ni velocidad, ni fricción, ni respuesta a colisiones. Vamos a construir todo eso desde cero con un puñado de variables y algo de trigonometría básica. Empezaremos con una pista de carreras hecha con tilemap, le pondremos un auto, y luego agregaremos penalizaciones fuera de pista, un segundo jugador, colisiones entre autos y una meta de tres vueltas.
Drawing the Track
Todo juego de carreras necesita una pista. Vamos a usar el sistema de tilemap para construir un circuito ovalado en la cuadrícula de 16×16.
Necesitamos dos tipos de sprites: road con el flag 0 en true (superficie transitable) y grass con el flag 0 en false (fuera de pista). Un tercer sprite finish marca la línea de meta — flag 0 para la detección de superficie, flag 1 para poder identificarlo después en el conteo de vueltas.
buildTrack llena toda la cuadrícula con pasto, recorta un bucle rectangular de dos tiles de ancho y coloca la línea de meta en la recta superior:
engine.scope(({ start, cls, spr, mset, map, fget }) => {
const sprites = {
grass: [
[
3, 3, 11, 3, 3, 3, 3, 3,
3, 3, 3, 3, 3, 3, 11, 3,
3, 3, 3, 3, 3, 3, 3, 3,
3, 11, 3, 3, 3, 3, 3, 3,
3, 3, 3, 3, 3, 11, 3, 3,
3, 3, 3, 3, 3, 3, 3, 3,
3, 3, 3, 11, 3, 3, 3, 3,
3, 3, 3, 3, 3, 3, 3, 11,
],
[false, false, false, false, false, false, false, false],
],
road: [
[
5, 5, 5, 5, 5, 5, 5, 5,
5, 5, 5, 5, 5, 5, 5, 5,
5, 5, 5, 5, 5, 5, 5, 5,
5, 5, 5, 5, 5, 5, 5, 5,
5, 5, 5, 5, 5, 5, 5, 5,
5, 5, 5, 5, 5, 5, 5, 5,
5, 5, 5, 5, 5, 5, 5, 5,
5, 5, 5, 5, 5, 5, 5, 5,
],
[true, false, false, false, false, false, false, false],
],
finish: [
[
7, 0, 7, 0, 7, 0, 7, 0,
0, 7, 0, 7, 0, 7, 0, 7,
7, 0, 7, 0, 7, 0, 7, 0,
0, 7, 0, 7, 0, 7, 0, 7,
7, 0, 7, 0, 7, 0, 7, 0,
0, 7, 0, 7, 0, 7, 0, 7,
7, 0, 7, 0, 7, 0, 7, 0,
0, 7, 0, 7, 0, 7, 0, 7,
],
[true, true, false, false, false, false, false, false],
],
};
function buildTrack() {
for (let r = 0; r < 16; r++) {
for (let c = 0; c < 16; c++) {
mset(c, r, 'grass');
}
}
for (let c = 2; c <= 13; c++) { mset(c, 2, 'road'); mset(c, 3, 'road'); }
for (let c = 2; c <= 13; c++) { mset(c, 12, 'road'); mset(c, 13, 'road'); }
for (let r = 2; r <= 13; r++) { mset(2, r, 'road'); mset(3, r, 'road'); }
for (let r = 2; r <= 13; r++) { mset(12, r, 'road'); mset(13, r, 'road'); }
mset(7, 2, 'finish');
mset(7, 3, 'finish');
}
function init() {
buildTrack();
}
function draw() {
cls(0);
map();
}
start({ sprites, sounds: {}, init, draw, target });
});
El flag 0 para "superficie transitable" es una convención, no una regla. Podés usar cualquiera de los 8 flags para lo que quieras — solo mantené la consistencia entre tus sprites.
A Single Racer
Necesitamos algo que conducir. El auto es un círculo relleno con una línea apuntando en su dirección — no hacen falta sprites, solo circfill y line.
La dirección es estilo tanque: Izquierda y Derecha rotan el ángulo de orientación, Arriba acelera hacia adelante, Abajo da reversa. Math.cos(angle) y Math.sin(angle) convierten el ángulo en componentes X e Y, lo que nos da la dirección de movimiento. La fricción multiplica la velocidad por un valor justo debajo de 1 en cada frame, así que el auto frena cuando soltás el acelerador. Un límite de velocidad máxima evita que las cosas se descontrolen:
engine.scope(({ start, cls, spr, mset, map, circfill, line, btn, text }) => {
const sprites = {
grass: [
[
3, 3, 11, 3, 3, 3, 3, 3,
3, 3, 3, 3, 3, 3, 11, 3,
3, 3, 3, 3, 3, 3, 3, 3,
3, 11, 3, 3, 3, 3, 3, 3,
3, 3, 3, 3, 3, 11, 3, 3,
3, 3, 3, 3, 3, 3, 3, 3,
3, 3, 3, 11, 3, 3, 3, 3,
3, 3, 3, 3, 3, 3, 3, 11,
],
[false, false, false, false, false, false, false, false],
],
road: [
[
5, 5, 5, 5, 5, 5, 5, 5,
5, 5, 5, 5, 5, 5, 5, 5,
5, 5, 5, 5, 5, 5, 5, 5,
5, 5, 5, 5, 5, 5, 5, 5,
5, 5, 5, 5, 5, 5, 5, 5,
5, 5, 5, 5, 5, 5, 5, 5,
5, 5, 5, 5, 5, 5, 5, 5,
5, 5, 5, 5, 5, 5, 5, 5,
],
[true, false, false, false, false, false, false, false],
],
finish: [
[
7, 0, 7, 0, 7, 0, 7, 0,
0, 7, 0, 7, 0, 7, 0, 7,
7, 0, 7, 0, 7, 0, 7, 0,
0, 7, 0, 7, 0, 7, 0, 7,
7, 0, 7, 0, 7, 0, 7, 0,
0, 7, 0, 7, 0, 7, 0, 7,
7, 0, 7, 0, 7, 0, 7, 0,
0, 7, 0, 7, 0, 7, 0, 7,
],
[true, true, false, false, false, false, false, false],
],
};
const accel = 0.08;
const friction = 0.97;
const turnSpeed = 0.05;
const maxSpeed = 1.5;
let x = 60, y = 24;
let angle = 0;
let speed = 0;
function buildTrack() {
for (let r = 0; r < 16; r++) {
for (let c = 0; c < 16; c++) {
mset(c, r, 'grass');
}
}
for (let c = 2; c <= 13; c++) { mset(c, 2, 'road'); mset(c, 3, 'road'); }
for (let c = 2; c <= 13; c++) { mset(c, 12, 'road'); mset(c, 13, 'road'); }
for (let r = 2; r <= 13; r++) { mset(2, r, 'road'); mset(3, r, 'road'); }
for (let r = 2; r <= 13; r++) { mset(12, r, 'road'); mset(13, r, 'road'); }
mset(7, 2, 'finish');
mset(7, 3, 'finish');
}
function init() {
buildTrack();
}
function update() {
if (btn('ArrowLeft')) angle -= turnSpeed;
if (btn('ArrowRight')) angle += turnSpeed;
if (btn('ArrowUp')) speed += accel;
if (btn('ArrowDown')) speed -= accel;
speed *= friction;
if (speed > maxSpeed) speed = maxSpeed;
if (speed < -maxSpeed / 2) speed = -maxSpeed / 2;
if (Math.abs(speed) < 0.01) speed = 0;
x += Math.cos(angle) * speed;
y += Math.sin(angle) * speed;
if (x < 4) x = 4;
if (x > 124) x = 124;
if (y < 4) y = 4;
if (y > 124) y = 124;
}
function draw() {
cls(0);
map();
circfill(x, y, 3, 8);
line(x, y, x + Math.cos(angle) * 5, y + Math.sin(angle) * 5, 10);
}
start({ sprites, sounds: {}, init, update, draw, target });
});
Una tasa de giro de 0.05 radianes por frame da una dirección suave a 60fps. Demasiado alta y el auto se siente nervioso. Demasiado baja y las curvas se vuelven imposibles. Ajustá esto junto con tu velocidad máxima — los autos más rápidos generalmente necesitan un giro más cerrado.
Staying on Track
Ahora mismo no hay penalización por cortar a través del pasto. Arreglemos eso.
En cada frame, convertimos la posición en píxeles del auto a coordenadas de tile con Math.floor(x / 8), obtenemos el tile con mget y verificamos el flag 0 con fget. En la pista: aceleración normal, fricción normal, velocidad máxima normal. En el pasto: aceleración a la mitad, fricción más fuerte y un límite de velocidad mucho más bajo. El auto no se detiene de golpe — va perdiendo velocidad hasta alcanzar el límite del pasto:
engine.scope(({ start, cls, spr, mset, mget, map, fget, circfill, line, btn, text }) => {
const sprites = {
grass: [
[
3, 3, 11, 3, 3, 3, 3, 3,
3, 3, 3, 3, 3, 3, 11, 3,
3, 3, 3, 3, 3, 3, 3, 3,
3, 11, 3, 3, 3, 3, 3, 3,
3, 3, 3, 3, 3, 11, 3, 3,
3, 3, 3, 3, 3, 3, 3, 3,
3, 3, 3, 11, 3, 3, 3, 3,
3, 3, 3, 3, 3, 3, 3, 11,
],
[false, false, false, false, false, false, false, false],
],
road: [
[
5, 5, 5, 5, 5, 5, 5, 5,
5, 5, 5, 5, 5, 5, 5, 5,
5, 5, 5, 5, 5, 5, 5, 5,
5, 5, 5, 5, 5, 5, 5, 5,
5, 5, 5, 5, 5, 5, 5, 5,
5, 5, 5, 5, 5, 5, 5, 5,
5, 5, 5, 5, 5, 5, 5, 5,
5, 5, 5, 5, 5, 5, 5, 5,
],
[true, false, false, false, false, false, false, false],
],
finish: [
[
7, 0, 7, 0, 7, 0, 7, 0,
0, 7, 0, 7, 0, 7, 0, 7,
7, 0, 7, 0, 7, 0, 7, 0,
0, 7, 0, 7, 0, 7, 0, 7,
7, 0, 7, 0, 7, 0, 7, 0,
0, 7, 0, 7, 0, 7, 0, 7,
7, 0, 7, 0, 7, 0, 7, 0,
0, 7, 0, 7, 0, 7, 0, 7,
],
[true, true, false, false, false, false, false, false],
],
};
const turnSpeed = 0.05;
let x = 60, y = 24;
let angle = 0;
let speed = 0;
let onRoad = true;
function buildTrack() {
for (let r = 0; r < 16; r++) {
for (let c = 0; c < 16; c++) {
mset(c, r, 'grass');
}
}
for (let c = 2; c <= 13; c++) { mset(c, 2, 'road'); mset(c, 3, 'road'); }
for (let c = 2; c <= 13; c++) { mset(c, 12, 'road'); mset(c, 13, 'road'); }
for (let r = 2; r <= 13; r++) { mset(2, r, 'road'); mset(3, r, 'road'); }
for (let r = 2; r <= 13; r++) { mset(12, r, 'road'); mset(13, r, 'road'); }
mset(7, 2, 'finish');
mset(7, 3, 'finish');
}
function isOnRoad(px, py) {
const tile = mget(Math.floor(px / 8), Math.floor(py / 8));
return tile && fget(tile, 0);
}
function init() {
buildTrack();
}
function update() {
onRoad = isOnRoad(x, y);
const accel = onRoad ? 0.08 : 0.04;
const friction = onRoad ? 0.97 : 0.92;
const max = onRoad ? 1.5 : 0.6;
if (btn('ArrowLeft')) angle -= turnSpeed;
if (btn('ArrowRight')) angle += turnSpeed;
if (btn('ArrowUp')) speed += accel;
if (btn('ArrowDown')) speed -= accel;
speed *= friction;
if (speed > max) speed = max;
if (speed < -max / 2) speed = -max / 2;
if (Math.abs(speed) < 0.01) speed = 0;
x += Math.cos(angle) * speed;
y += Math.sin(angle) * speed;
if (x < 4) x = 4;
if (x > 124) x = 124;
if (y < 4) y = 4;
if (y > 124) y = 124;
}
function draw() {
cls(0);
map();
circfill(x, y, 3, 8);
line(x, y, x + Math.cos(angle) * 5, y + Math.sin(angle) * 5, 10);
text(onRoad ? 'road' : 'grass', 2, 2, 7);
}
start({ sprites, sounds: {}, init, update, draw, target });
});
Adding a Second Racer
Cooperativo local significa dos jugadores en un solo teclado. Vamos a extraer el estado del auto en un objeto y escribir una función genérica updateCar que reciba las teclas como parámetros.
El jugador 1 mantiene las flechas. El jugador 2 usa a/d para girar, w para acelerar y s para dar reversa. Cada auto tiene su propia posición, ángulo, velocidad y color. Un factory makeCar mantiene las cosas ordenadas:
engine.scope(({ start, cls, spr, mset, mget, map, fget, circfill, line, btn, text }) => {
const sprites = {
grass: [
[
3, 3, 11, 3, 3, 3, 3, 3,
3, 3, 3, 3, 3, 3, 11, 3,
3, 3, 3, 3, 3, 3, 3, 3,
3, 11, 3, 3, 3, 3, 3, 3,
3, 3, 3, 3, 3, 11, 3, 3,
3, 3, 3, 3, 3, 3, 3, 3,
3, 3, 3, 11, 3, 3, 3, 3,
3, 3, 3, 3, 3, 3, 3, 11,
],
[false, false, false, false, false, false, false, false],
],
road: [
[
5, 5, 5, 5, 5, 5, 5, 5,
5, 5, 5, 5, 5, 5, 5, 5,
5, 5, 5, 5, 5, 5, 5, 5,
5, 5, 5, 5, 5, 5, 5, 5,
5, 5, 5, 5, 5, 5, 5, 5,
5, 5, 5, 5, 5, 5, 5, 5,
5, 5, 5, 5, 5, 5, 5, 5,
5, 5, 5, 5, 5, 5, 5, 5,
],
[true, false, false, false, false, false, false, false],
],
finish: [
[
7, 0, 7, 0, 7, 0, 7, 0,
0, 7, 0, 7, 0, 7, 0, 7,
7, 0, 7, 0, 7, 0, 7, 0,
0, 7, 0, 7, 0, 7, 0, 7,
7, 0, 7, 0, 7, 0, 7, 0,
0, 7, 0, 7, 0, 7, 0, 7,
7, 0, 7, 0, 7, 0, 7, 0,
0, 7, 0, 7, 0, 7, 0, 7,
],
[true, true, false, false, false, false, false, false],
],
};
const turnSpeed = 0.05;
function makeCar(sx, sy, sa, color) {
return { x: sx, y: sy, angle: sa, speed: 0, color };
}
const p1 = makeCar(60, 20, 0, 8);
const p2 = makeCar(60, 28, 0, 12);
function buildTrack() {
for (let r = 0; r < 16; r++) {
for (let c = 0; c < 16; c++) {
mset(c, r, 'grass');
}
}
for (let c = 2; c <= 13; c++) { mset(c, 2, 'road'); mset(c, 3, 'road'); }
for (let c = 2; c <= 13; c++) { mset(c, 12, 'road'); mset(c, 13, 'road'); }
for (let r = 2; r <= 13; r++) { mset(2, r, 'road'); mset(3, r, 'road'); }
for (let r = 2; r <= 13; r++) { mset(12, r, 'road'); mset(13, r, 'road'); }
mset(7, 2, 'finish');
mset(7, 3, 'finish');
}
function isOnRoad(px, py) {
const tile = mget(Math.floor(px / 8), Math.floor(py / 8));
return tile && fget(tile, 0);
}
function updateCar(car, left, right, gas, reverse) {
const onRoad = isOnRoad(car.x, car.y);
const accel = onRoad ? 0.08 : 0.04;
const friction = onRoad ? 0.97 : 0.92;
const max = onRoad ? 1.5 : 0.6;
if (btn(left)) car.angle -= turnSpeed;
if (btn(right)) car.angle += turnSpeed;
if (btn(gas)) car.speed += accel;
if (btn(reverse)) car.speed -= accel;
car.speed *= friction;
if (car.speed > max) car.speed = max;
if (car.speed < -max / 2) car.speed = -max / 2;
if (Math.abs(car.speed) < 0.01) car.speed = 0;
car.x += Math.cos(car.angle) * car.speed;
car.y += Math.sin(car.angle) * car.speed;
if (car.x < 4) car.x = 4;
if (car.x > 124) car.x = 124;
if (car.y < 4) car.y = 4;
if (car.y > 124) car.y = 124;
}
function drawCar(car) {
circfill(car.x, car.y, 3, car.color);
line(car.x, car.y, car.x + Math.cos(car.angle) * 5, car.y + Math.sin(car.angle) * 5, 10);
}
function init() {
buildTrack();
}
function update() {
updateCar(p1, 'ArrowLeft', 'ArrowRight', 'ArrowUp', 'ArrowDown');
updateCar(p2, 'a', 'd', 'w', 's');
}
function draw() {
cls(0);
map();
drawCar(p1);
drawCar(p2);
text('p1: arrows', 2, 2, 8);
text('p2: wasd', 2, 10, 12);
}
start({ sprites, sounds: {}, init, update, draw, target });
});
Car Collisions
Dos autos que se atraviesan no se sienten como una carrera. Necesitamos respuesta a colisiones.
En cada frame, verificamos la distancia entre los centros de los autos. Si es menor que el doble del radio, los autos se superponen. Calculamos la dirección de un auto al otro, empujamos cada auto la mitad de la distancia de superposición a lo largo de esa línea, y reducimos la velocidad de ambos en el impacto — chocá contra alguien y los dos pierden impulso:
const carRadius = 3;
function resolveCollision(a, b) {
const dx = b.x - a.x;
const dy = b.y - a.y;
const dist = Math.sqrt(dx * dx + dy * dy);
const minDist = carRadius * 2;
if (dist < minDist && dist > 0) {
const nx = dx / dist;
const ny = dy / dist;
const overlap = (minDist - dist) / 2;
a.x -= nx * overlap;
a.y -= ny * overlap;
b.x += nx * overlap;
b.y += ny * overlap;
a.speed *= 0.7;
b.speed *= 0.7;
}
}
// dentro de update(), después de mover ambos autos:
resolveCollision(p1, p2);
El factor de amortiguación de 0.7 controla qué tan castigadoras se sienten las colisiones. Valores más bajos hacen que los choques sean devastadores — valores más altos los convierten en empujones suaves. Ajustalo para que coincida con la sensación que buscás.
Three Laps
Una carrera necesita una condición de final. Detectar cuándo un auto cruza la línea de meta no alcanza por sí solo — un jugador podría ir y venir sobre ella para acumular vueltas.
La solución son los checkpoints. Dividimos la pista en tres zonas según la posición en pantalla: arriba-derecha, abajo-derecha y abajo-izquierda. Cada auto registra qué zonas visitó en un Set. Cuando un auto cruza el tile de la línea de meta (flag 1) y pasó por los tres checkpoints, eso es una vuelta. El set se reinicia, el contador sube, y el primer auto en llegar a tres vueltas gana:
engine.scope(({ start, cls, spr, mset, mget, map, fget, circfill, line, btn, text }) => {
const sprites = {
grass: [
[
3, 3, 11, 3, 3, 3, 3, 3,
3, 3, 3, 3, 3, 3, 11, 3,
3, 3, 3, 3, 3, 3, 3, 3,
3, 11, 3, 3, 3, 3, 3, 3,
3, 3, 3, 3, 3, 11, 3, 3,
3, 3, 3, 3, 3, 3, 3, 3,
3, 3, 3, 11, 3, 3, 3, 3,
3, 3, 3, 3, 3, 3, 3, 11,
],
[false, false, false, false, false, false, false, false],
],
road: [
[
5, 5, 5, 5, 5, 5, 5, 5,
5, 5, 5, 5, 5, 5, 5, 5,
5, 5, 5, 5, 5, 5, 5, 5,
5, 5, 5, 5, 5, 5, 5, 5,
5, 5, 5, 5, 5, 5, 5, 5,
5, 5, 5, 5, 5, 5, 5, 5,
5, 5, 5, 5, 5, 5, 5, 5,
5, 5, 5, 5, 5, 5, 5, 5,
],
[true, false, false, false, false, false, false, false],
],
finish: [
[
7, 0, 7, 0, 7, 0, 7, 0,
0, 7, 0, 7, 0, 7, 0, 7,
7, 0, 7, 0, 7, 0, 7, 0,
0, 7, 0, 7, 0, 7, 0, 7,
7, 0, 7, 0, 7, 0, 7, 0,
0, 7, 0, 7, 0, 7, 0, 7,
7, 0, 7, 0, 7, 0, 7, 0,
0, 7, 0, 7, 0, 7, 0, 7,
],
[true, true, false, false, false, false, false, false],
],
};
const turnSpeed = 0.05;
const carRadius = 3;
const totalLaps = 3;
let winner = null;
function makeCar(sx, sy, sa, color) {
return { x: sx, y: sy, angle: sa, speed: 0, color, lap: 0, checkpoints: new Set(), onFinish: false };
}
const p1 = makeCar(60, 20, 0, 8);
const p2 = makeCar(60, 28, 0, 12);
function buildTrack() {
for (let r = 0; r < 16; r++) {
for (let c = 0; c < 16; c++) {
mset(c, r, 'grass');
}
}
for (let c = 2; c <= 13; c++) { mset(c, 2, 'road'); mset(c, 3, 'road'); }
for (let c = 2; c <= 13; c++) { mset(c, 12, 'road'); mset(c, 13, 'road'); }
for (let r = 2; r <= 13; r++) { mset(2, r, 'road'); mset(3, r, 'road'); }
for (let r = 2; r <= 13; r++) { mset(12, r, 'road'); mset(13, r, 'road'); }
mset(7, 2, 'finish');
mset(7, 3, 'finish');
}
function isOnRoad(px, py) {
const tile = mget(Math.floor(px / 8), Math.floor(py / 8));
return tile && fget(tile, 0);
}
function getCheckpoint(px, py) {
if (px > 64 && py < 64) return 0;
if (px > 64 && py >= 64) return 1;
if (px <= 64 && py >= 64) return 2;
return -1;
}
function updateCar(car, left, right, gas, reverse) {
if (winner) return;
const onRoad = isOnRoad(car.x, car.y);
const accel = onRoad ? 0.08 : 0.04;
const friction = onRoad ? 0.97 : 0.92;
const max = onRoad ? 1.5 : 0.6;
if (btn(left)) car.angle -= turnSpeed;
if (btn(right)) car.angle += turnSpeed;
if (btn(gas)) car.speed += accel;
if (btn(reverse)) car.speed -= accel;
car.speed *= friction;
if (car.speed > max) car.speed = max;
if (car.speed < -max / 2) car.speed = -max / 2;
if (Math.abs(car.speed) < 0.01) car.speed = 0;
car.x += Math.cos(car.angle) * car.speed;
car.y += Math.sin(car.angle) * car.speed;
if (car.x < 4) car.x = 4;
if (car.x > 124) car.x = 124;
if (car.y < 4) car.y = 4;
if (car.y > 124) car.y = 124;
const cp = getCheckpoint(car.x, car.y);
if (cp >= 0) car.checkpoints.add(cp);
const tile = mget(Math.floor(car.x / 8), Math.floor(car.y / 8));
const onFinishTile = tile && fget(tile, 1);
if (onFinishTile && !car.onFinish && car.checkpoints.size >= 3) {
car.lap++;
car.checkpoints.clear();
if (car.lap >= totalLaps) winner = car;
}
car.onFinish = onFinishTile;
}
function resolveCollision(a, b) {
const dx = b.x - a.x;
const dy = b.y - a.y;
const dist = Math.sqrt(dx * dx + dy * dy);
const minDist = carRadius * 2;
if (dist < minDist && dist > 0) {
const nx = dx / dist;
const ny = dy / dist;
const overlap = (minDist - dist) / 2;
a.x -= nx * overlap;
a.y -= ny * overlap;
b.x += nx * overlap;
b.y += ny * overlap;
a.speed *= 0.7;
b.speed *= 0.7;
}
}
function drawCar(car) {
circfill(car.x, car.y, carRadius, car.color);
line(car.x, car.y, car.x + Math.cos(car.angle) * 5, car.y + Math.sin(car.angle) * 5, 10);
}
function init() {
buildTrack();
}
function update() {
updateCar(p1, 'ArrowLeft', 'ArrowRight', 'ArrowUp', 'ArrowDown');
updateCar(p2, 'a', 'd', 'w', 's');
resolveCollision(p1, p2);
}
function draw() {
cls(0);
map();
drawCar(p1);
drawCar(p2);
text('p1 lap ' + Math.min(p1.lap + 1, totalLaps) + '/' + totalLaps, 2, 2, 8);
text('p2 lap ' + Math.min(p2.lap + 1, totalLaps) + '/' + totalLaps, 2, 10, 12);
if (winner) {
const label = winner === p1 ? 'p1 wins!' : 'p2 wins!';
text(label, 48, 60, winner.color);
}
}
start({ sprites, sounds: {}, init, update, draw, target });
});
Going Further
- Boost pads — agregá un tile con un flag especial que multiplique la velocidad del auto al pasar por encima
- Oponentes IA — guardá una lista de waypoints alrededor de la pista y dirigí un auto IA hacia el siguiente en cada frame
- Múltiples pistas — intercambiá
buildTrackpor un diseño de tilemap diferente. Guardá los diseños como arrays 2D y elegí uno al azar - Efectos de sonido — usá
sfx()para el sonido del motor, chirrido de neumáticos en el pasto y un timbre cuando se complete una vuelta - Pantalla dividida — usá
camera()para darle a cada jugador su propia vista en una pista más grande, dibujando cada mitad de la pantalla por separado - Mecánica de derrape — al girar a alta velocidad, reducí el agarre y dejá que el auto se deslice de costado por uno o dos frames antes de que el ángulo se ajuste