Juegos en Red - Grado en Diseño y Desarrollo de Videojuegos
2026-09-09
Framework open source de HTML5 para videojuegos
Ventajas:
Tecnologías:
Dos opciones disponibles:
Phaser.AUTO)¿Cuándo usar cada uno?
Navegador moderno:
Obtener Phaser 3:
Descarga directa phaser.io/download/stable
GitHub github.com/phaserjs/phaser
Node npm install phaser
¿Qué es el Canvas?
<canvas>) para dibujar gráficosSistema de coordenadas:
Objeto de configuración define parámetros fundamentales:
const config = {
type: Phaser.AUTO, // Renderizado automático
width: 800, // Ancho del canvas
height: 600, // Alto del canvas
physics: {
default: 'arcade', // Motor de físicas
arcade: {
gravity: { y: 300 } // Gravedad vertical
}
},
scene: {
preload: preload, // Función de carga
create: create, // Función de creación
update: update // Función de actualización
}
};
const game = new Phaser.Game(config);preload() - Cargar recursos
Nota
El primer parámetro es el identificador que usaremos después
create() - Crear objetos del juego
function create() {
// Añadir imagen de fondo centrada
this.add.image(400, 300, 'cielo');
// Crear plataforma con físicas
this.plataforma = this.physics.add.image(400, 500, 'suelo');
this.plataforma.setImmovable(true); // No se mueve
this.plataforma.body.setAllowGravity(false); // No le afecta la gravedad
}update(time, delta) - Bucle del juego
time (tiempo total), delta (desde último frame)Tip
Usar delta para movimiento consistente en diferentes dispositivos
¿Qué es una escena?
Ejemplos comunes:
Forma implícita (simple):
Forma explícita (recomendada):
Nota
Las funciones preload/create/update siempre están dentro de una escena
Cambiar entre escenas:
start(): Inicia escena y detiene la actuallaunch(): Inicia escena en paralelo (mantiene actual)stop(): Detiene completamente una escenaControlar estado:
pause() / resume(): Pausar/reanudar (sigue visible)sleep() / wake(): Dormir/despertar (no visible)Gestionar orden:
bringToTop() / sendToBack(): Mover al frente/fondomoveAbove() / moveBelow(): Posicionar relativamenteDefinir escenas en el array de configuración:
Añadir/eliminar dinámicamente:
start() vs launch():
Importante
Diferencia clave: start reemplaza, launch superpone escenas
pause() / resume():
update() pero sigue renderizandosleep() / wake():
update() y renderizadoEscena principal del juego:
Escena del menú de pausa:
class MenuPausa extends Phaser.Scene {
constructor() {
super('MenuPausa');
}
create(datos) {
// Recibir datos de la escena que pausó
console.log('Pausado desde:', datos.escenaOrigen);
// Crear fondo semitransparente
let overlay = this.add.rectangle(400, 300, 800, 600, 0x000000, 0.7);
// Botón continuar
let botonContinuar = this.add.text(400, 300, 'Continuar', {
fontSize: '32px'
}).setOrigin(0.5);
botonContinuar.setInteractive();
botonContinuar.on('pointerdown', () => {
this.scene.stop(); // Cerrar menú
this.scene.resume(datos.escenaOrigen); // Reanudar juego
});
}
}Enviar datos al cambiar de escena:
Recibir datos en la escena destino:
class SiguienteEscena extends Phaser.Scene {
constructor() {
super('SiguienteEscena');
}
create(datos) {
// Recibir los datos como parámetro
console.log(datos.puntuacion); // 100
console.log(datos.nivel); // 2
console.log(datos.nombre); // 'Jugador1'
this.add.text(100, 100, `Puntuación: ${datos.puntuacion}`);
}
}También funciona con launch() y otros:
Crear efectos visuales suaves:
duration: Tiempo en milisegundosonUpdate: Callback durante transiciónprogress: Valor de 0 a 1data: Objeto con datos para la escena destinoControl del orden de renderizado:
Proceso en dos pasos:
preload() con identificador únicocreate() usando el identificadorfunction preload() {
// Cargar con identificador único
this.load.image('personaje', 'assets/personaje.png');
this.load.image('fondo', 'assets/fondo.jpg');
}
function create() {
// Añadir al canvas en posición (x, y)
this.add.image(400, 300, 'fondo');
let sprite = this.add.image(200, 150, 'personaje');
}Advertencia
Siempre cargar en preload() antes de usar en create()
Origen por defecto: centro de la imagen
Tip
Cambiar origen útil para alinear objetos o rotaciones específicas
Escalar, voltear y rotar:
let jugador = this.add.image(400, 300, 'personaje');
// Escalar uniformemente
jugador.setScale(1.5); // 150% del tamaño
// Escalar independientemente
jugador.setScale(2, 0.5); // Ancho x2, alto x0.5
// Voltear
jugador.flipX = true; // Espejo horizontal
jugador.flipY = true; // Espejo vertical
// Rotar (en radianes)
jugador.rotation = Math.PI / 4; // 45 gradosNota
π radianes = 180 grados
Controlar qué se dibuja encima:
depth funciona como capasArcade Physics:
Matter Physics:
Configuración en el objeto config:
Tip
Activar debug: true durante desarrollo para ver colisiones
Diferencia entre objetos con y sin físicas:
Importante
Usar physics.add en lugar de solo add para habilitar físicas
Configurar comportamiento físico:
this.jugador = this.physics.add.image(100, 450, 'personaje');
// No salir de los límites del canvas
this.jugador.setCollideWorldBounds(true);
// Rebote al chocar (0 = no rebota, 1 = rebote perfecto)
this.jugador.setBounce(0.3);
// Velocidad inicial (píxeles/segundo)
this.jugador.setVelocity(100, -50);
// Aceleración (píxeles/segundo²)
this.jugador.setAcceleration(50, 0);Detectar teclas específicas:
Nota
keydown se dispara al presionar, keyup al soltar
Detectar combinaciones de teclas:
Objeto para flechas, espacio y shift:
function create() {
// Crear objeto de cursores
this.cursors = this.input.keyboard.createCursorKeys();
}
function update() {
// Resetear velocidad
this.jugador.setVelocityX(0);
// Comprobar teclas presionadas
if (this.cursors.left.isDown) {
this.jugador.setVelocityX(-160);
} else if (this.cursors.right.isDown) {
this.jugador.setVelocityX(160);
}
// Saltar solo si está en el suelo
if (this.cursors.up.isDown && this.jugador.body.touching.down) {
this.jugador.setVelocityY(-330);
}
}Detectar secuencias de teclas:
function create() {
// Combo con letras
let combo1 = this.input.keyboard.createCombo('KONAMI');
// Combo Konami: ↑ ↑ ↓ ↓ ← → ← →
let combo2 = this.input.keyboard.createCombo(
[38, 38, 40, 40, 37, 39, 37, 39]
);
// Detectar cuando se completa
this.input.keyboard.on('keycombomatch', (combo) => {
console.log('¡Código secreto desbloqueado!');
this.activarModoEspecial();
});
}API unificada para ratón y pantallas táctiles:
function create() {
// Detectar clic/toque
this.input.on('pointerdown', (pointer) => {
if (pointer.leftButtonDown()) {
console.log('Clic izquierdo:', pointer.x, pointer.y);
}
if (pointer.rightButtonDown()) {
console.log('Clic derecho');
}
});
// Detectar cuando se suelta
this.input.on('pointerup', (pointer) => {
console.log('Botón soltado');
});
// Detectar movimiento
this.input.on('pointermove', (pointer) => {
console.log('Posición:', pointer.x, pointer.y);
});
}Dos formas de detectar interacciones:
Collider:
Overlap:
Tip
Collider para plataformas, Overlap para coleccionables
Collider para plataformas:
function create() {
let jugador = this.physics.add.image(100, 450, 'personaje');
let plataforma = this.physics.add.image(400, 500, 'suelo');
plataforma.setImmovable(true); // No se mueve al impacto
plataforma.body.setAllowGravity(false); // No le afecta la gravedad
// Colisión con resolución física
this.physics.add.collider(jugador, plataforma);
}Overlap para coleccionables:
Para objetos inmóviles (plataformas, paredes):
function create() {
// Crear grupo estático
let plataformas = this.physics.add.staticGroup();
// Añadir plataformas
plataformas.create(400, 568, 'suelo')
.setScale(2)
.refreshBody(); // Actualizar tras modificar
plataformas.create(600, 400, 'suelo');
plataformas.create(50, 250, 'suelo');
// Colisión con todas
this.physics.add.collider(this.jugador, plataformas);
}Para objetos con físicas completas:
// Crear 12 estrellas espaciadas
let estrellas = this.physics.add.group({
key: 'estrella',
repeat: 11, // 1 + 11 = 12 total
setXY: { x: 12, y: 0, stepX: 70 } // Cada 70px
});
// Aplicar propiedades a cada una
estrellas.children.iterate((estrella) => {
estrella.setBounceY(Phaser.Math.FloatBetween(0.4, 0.8));
});
// Overlap para recogerlas
this.physics.add.overlap(this.jugador, estrellas, (j, e) => {
e.disableBody(true, true); // Desactivar
this.puntuacion += 10;
});Ejecutar código cuando ocurre colisión:
Process callback (condición):
true para procesar, false para ignorarEjemplo: no recibir daño temporalmente
function puedeRecibirDaño(jugador, enemigo) {
// Solo procesar si no es invulnerable
return !jugador.invulnerable;
}
function dañar(jugador, enemigo) {
jugador.invulnerable = true;
jugador.setTint(0xff0000); // Rojo
// Volver vulnerable tras 2 segundos
this.time.delayedCall(2000, () => {
jugador.invulnerable = false;
jugador.clearTint();
});
}Propiedades útiles para mecánicas:
touching.down: Tocando suelotouching.up: Tocando techotouching.left/right: Tocando paredesblocked.*: Similar pero solo objetos inmóvilesfunction update() {
// Verificar si toca el suelo
if (this.jugador.body.touching.down) {
console.log('En el suelo');
}
// Saltar solo si está en el suelo
if (this.cursors.up.isDown &&
this.jugador.body.touching.down) {
this.jugador.setVelocityY(-330);
}
// Detectar si está contra pared
if (this.jugador.body.touching.left) {
console.log('Pared izquierda');
}
}Problema en juegos multijugador:
Solución: Patrón Command
Ventajas principales:
Clase abstracta con interfaz común:
Nota
Esta es la base que heredarán todos los comandos específicos
Definir el comando específico:
Tip
Cada comando guarda toda la información necesaria para ejecutarse
Implementar la lógica del comando:
class MovePaddleCommand extends Command {
// ... constructor ...
execute() {
const speed = 300; // Píxeles por segundo
if (this.direction === 'up') {
this.paddle.setVelocityY(-speed);
} else if (this.direction === 'down') {
this.paddle.setVelocityY(speed);
} else { // 'stop'
this.paddle.setVelocityY(0);
}
}
// Métodos serialize() y getPlayer() en siguiente slide...
}Preparar para transmisión por red:
Nota
Solo se envían datos esenciales, no referencias a objetos
Coordina comandos locales y remotos:
Ejecutar comandos del jugador local:
class CommandProcessor {
// ... constructor ...
process(command) {
const player = command.getPlayer();
// Solo ejecutar si es jugador local
if (player && player.authority === 'LOCAL') {
command.execute(); // Respuesta inmediata
// Transmitir a otros jugadores
if (this.network && this.network.isConnected()) {
this.network.send(command.serialize());
}
}
}
}Importante
La autoridad LOCAL evita ejecutar el comando dos veces
Procesar comandos de otros jugadores:
Nota
Comandos remotos solo se ejecutan al recibirlos por red
Reconstruir comandos desde JSON:
class CommandProcessor {
// ... receiveCommand() ...
deserialize(data, player) {
switch(data.type) {
case 'MOVE_PADDLE':
return new MovePaddleCommand(player, data.direction);
case 'SHOOT':
return new ShootCommand(player, data.x, data.y);
default:
console.warn('Comando desconocido:', data.type);
return null;
}
}
}Configurar escena con autoridad:
class GameScene extends Phaser.Scene {
constructor() {
super('GameScene');
this.commandProcessor = new CommandProcessor();
}
create() {
// Paleta local (controlada por este jugador)
this.localPaddle = this.physics.add.image(50, 300, 'paddle');
this.localPaddle.id = 'player1';
this.localPaddle.authority = 'LOCAL';
this.localPaddle.setCollideWorldBounds(true);
// Continúa en siguiente slide...
}
}Configurar paleta remota y registrar:
create() {
// ... localPaddle ...
// Paleta remota (controlada por otro jugador)
this.remotePaddle = this.physics.add.image(750, 300, 'paddle');
this.remotePaddle.id = 'player2';
this.remotePaddle.authority = 'REMOTE';
this.remotePaddle.setCollideWorldBounds(true);
// Registrar ambos jugadores
this.commandProcessor.players.set('player1', this.localPaddle);
this.commandProcessor.players.set('player2', this.remotePaddle);
// Configurar input
this.cursors = this.input.keyboard.createCursorKeys();
}Convertir input a comandos:
update() {
// Crear comando según tecla presionada
let command;
if (this.cursors.up.isDown) {
command = new MovePaddleCommand(this.localPaddle, 'up');
} else if (this.cursors.down.isDown) {
command = new MovePaddleCommand(this.localPaddle, 'down');
} else {
command = new MovePaddleCommand(this.localPaddle, 'stop');
}
// Procesar (ejecuta local y envía a red)
this.commandProcessor.process(command);
}Jugador Local:
update()MovePaddleCommandCommandProcessor.process() ejecuta inmediatamenteJugador Remoto:
CommandProcessor.receiveCommand() deserializaMovePaddleCommand con datos recibidosTransparencia de red:
Preparado para evolución:
Escalabilidad:
Estructura básica:
preload, create, update (dentro de escenas)create()Físicas:
Input:
Arquitectura:
Beneficios:
