Nota
El acceso a esta página requiere autorización. Puede intentar iniciar sesión o cambiar directorios.
El acceso a esta página requiere autorización. Puede intentar cambiar los directorios.
Motores de juegos 2D para la web
Descargar el código de muestra
Imagine que hay un programador que desea crear un juego. Este desarrollador mira alrededor para ver qué herramientas están disponibles y queda decepcionado con las opciones. Por lo tanto, este desarrollador decide entonces crear un motor personalizado que satisfará todas las necesidades para el concepto de juegos actual. También cumplirá todos los requisitos de desarrollo de juegos futuros que posiblemente tendrá cualquiera. Y, por tanto, el desarrollador no crea nunca realmente un juego.
Es una tragedia que ocurre con frecuencia. Afortunadamente, este historia ha generado docenas de motores de desarrollo de juegos para todas las plataformas posibles. Estos abarcan una amplia gama de metodologías, herramientas y modelos de licencias. Por ejemplo, JavaScripting.com muestra actualmente 14 bibliotecas de juegos de código abierto dedicadas. No incluye ni siquiera motores de física, bibliotecas de audio y sistemas de entrada especializados para juegos. Con ese tipo de selección, habrá un motor de juego que funcionará bien para cualquier proyecto de juego que se le ocurra.
Este artículo le guiará por tres populares motores de juegos 2D de código abierto para la web: Crafty, Pixi y Phaser. Para comparar y contrastar estas bibliotecas, he portado el juego de Ping de mi primer artículo (msdn.microsoft.com/magazine/dn913185) a cada una de ellas para conocer la experiencia con ellas. Tenga en cuenta que al restringir las cosas a 2D, dejo fuera varios motores de juegos 3D para la web. Los abordaré en el futuro. Por ahora, me centraré en 2D y en las oportunidades que hay ahí.
Crafty
Crafty (craftyjs.com) está pensado principalmente para juegos basados en mosaicos, aunque también funciona bien para una amplia variedad de juegos 2D, incluido mi juego de Ping. La base de Crafty es un sistema de modelos/dependencias de objeto que llama componentes.
Los componentes son similares a las clases que define con acciones y atributos específicos. Cada instancia de un componente se denomina entidad. Los componentes están diseñados para tener muchas combinaciones y crear entidades complejas y con muchas características. Por ejemplo, si desea crear una hoja de sprites en Crafty, use la función SPRITE para generar componentes que representan los sprites. Así es cómo defino los tipos de sprites desde una imagen:
Crafty.sprite("sprites.png", {
PlayerSprite: [0, 128, 128, 128],
OpponentSprite: [0, 0, 128, 128],
BallSprite: [128, 128, 64, 64],
UpSprite: [128,0,64,64],
DownSprite: [128,64,64,64],
LeftSprite: [192,0,64,64],
RightSprite: [192,64,64,64]});
Ahora, si desea crear una entidad de bola que use la imagen de la bola en la hoja de sprites, incluya el componente BallSprite al crear la entidad, mediante la función "e", como se indica a continuación:
Crafty.e("Ball, BallSprite");
Esto no lo dibuja aún en la pantalla. Tendrá que indicar a Crafty que desea que esta entidad se encuentre en 2D (por lo que tendrá una posición "x" e "y") y que quiere que dibuje con elementos DOM:
Crafty.e("Ball, 2D, DOM, BallSprite");
Si desea dibujar sus elementos en un lienzo, es tan sencillo como reemplazar el componente Document Object Model (DOM) por el lienzo.
Las entidades actúan como objetos independientes en la pantalla al reaccionar ante eventos. Como muchas bibliotecas de JavaScript, las entidades de Crafty tienen una función de enlace para reaccionar ante eventos. Es importante recordar que se tratan de eventos específicos de Crafty. Por ejemplo, si desea que una entidad haga algo en cada fotograma, haga que reaccione ante el evento EnterFrame. Esto es exactamente lo que se necesita para mover la bola según quién la tenga y la física adecuada, si es necesario. En la Figura 1 se muestra la inicialización de la entidad de bola con una función de evento EnterFrame que cubre el movimiento de la bola.
Figura 1 La definición de la entidad de bola
Crafty.e("Ball, 2D, DOM, BallSprite, Collision")
.attr({ x: width/2, y: height/2, velocity: [0,0], owner: 'User' })
.bind('EnterFrame', function () {
// If there's an owner, have the ball follow the owner.
// Nudge the ball to the left or right of the player
// so it doesn't overlap.
if (this.owner) {
var owner = Crafty(this.owner);
switch(this.owner) {
case 'User':
this.x = owner.x + 128;
break;
case 'Opponent':
this.x = owner.x - 64;
}
this.y = owner.y + 32;
return;
}
// Bounce the ball off the ceiling or floor.
if (this.y <= 0 || this.y >= (height - 64))
this.velocity[1] = -this.velocity[1];
// Move the ball based on its velocity, which is defined in
// pixels per millisecond.
var millis = 1000 / Crafty.timer.FPS();
this.x += this.velocity[0] * millis;
this.y += this.velocity[1] * millis;
})
Si lo examina detenidamente, verá "this" usado con frecuencia para hacer referencia a las propiedades de entidad. “this.x” y “this.y” de Crafty integrados definen la posición de la bola. Cuando crea la bola con el componente 2D, se agrega esta funcionalidad. La instrucción, "this.velocity", está personalizada para mi código. Puede verla definida como una propiedad mediante la función attr. Lo mismo ocurre con this.owner, que uso para averiguar si cualquier jugador tiene la bola.
Crafty tiene varios componentes integrados, pero no los uso mucho en el ejemplo de Ping. Éstas son algunas:
- Gravedad: Crafty tiene un motor de gravedad simple que inserta automáticamente una entidad hacia abajo. Puede definir cualquier número de tipos de elemento para detener su movimiento.
- Movimiento FourWay/TwoWay/MultiWay: con estos componentes, obtiene una variedad de métodos de entrada de movimiento de estilo de videojuego. TwoWay es para plataformas con opciones de izquierda, derecha y salto enlazables a las claves que desee. FourWay permite el movimiento descendente en las direcciones cardinales. MultiWay permite entradas personalizadas, cada una de ellas con una dirección arbitraria.
- Partículas: Crafty incluye un sistema de partículas rápido, que solo puede usar con el dibujo de lienzo. Como imágenes suaves, redondas y de un solo color, las partículas resultan especialmente adecuadas para humo, incendios y explosiones.
El código para Ping, implementado en Crafty, está disponible en la descarga de código incluida en este artículo. Eche un vistazo para tener una imagen completa de cómo he portado cosas, especialmente el AI para el oponente.
Pixi.js
Pixi.js (pixijs.com) es el representador web 2D close-to-the-metal básico. Pixi.js puede renunciar al lienzo 2D y profundizar directamente en WebGL, lo que le ofrece un aumento significativo del rendimiento. Un juego de Pixi.js tiene dos partes principales: una fase que describe el diseño del juego en un gráfico de escena, y un representador, que realmente dibuja los elementos en la fase a la pantalla mediante el elemento canvas o WebGL. De forma predeterminada, Pixi.js usará WebGL si está disponible:
$(document).ready(function() {
// The stage to play your game.
stage = new PIXI.Stage(0xFFFFFFFF);
lastUpdate = 0;
// autoDetectRenderer() finds the fastest renderer you've got.
renderer = PIXI.autoDetectRenderer(innerWidth, innerHeight);
document.body.appendChild(renderer.view);
});
Pixi.js tiene un gráfico de escena y transformaciones, pero es importante tener en cuenta que deberá representar la escena usted mismo. Por lo tanto, para mi juego de Ping, mantengo el bucle de representación en movimiento con requestAnimationFrame, de la siguiente manera:
function update(time) {
var t = time - lastUpdate;
lastUpdate = time;
ball.update(t);
ai.update();
// New: You actually have to render the scene
renderer.render(stage);
requestAnimationFrame(update);
}
A diferencia de Crafty, Pixi.js no dicta mucho acerca de cómo estructurar su juego, así puedo usar prácticamente el mismo código del juego Ping original, con ligeras modificaciones.
La gran diferencia que verá es que Pixi.js le permite cargar duendecillos como imágenes desde archivos de uno en uno, o como una imagen en mosaico con una herramienta especial denominada TexturePacker. Esta herramienta combinará automáticamente un grupo de imágenes individuales en un único archivo optimizado con un archivo JSON asociado que describe las ubicaciones de las imágenes. He usado TexturePacker para crear la hoja de duendecillos. En la Figura 2 se muestra la función ready actualizada, cargando las imágenes de duendecillos.
Figura 2 Carga de una hoja de sprites en Pixi.js
$(document).ready(function() {
// Create an new instance of a Pixi stage.
stage = new PIXI.Stage(0xFFFFFFFF);
lastUpdate = 0;
// Create a renderer instance.
renderer = PIXI.autoDetectRenderer(innerWidth, innerHeight);
document.body.appendChild(renderer.view);
var images = ["sprites.json"];
var loader = new PIXI.AssetLoader(images);
loader.onComplete = startGame;
loader.load();
});
El cargador es asincrónico, ya que requiere una función a la que llamar para cuando ha realizado la carga. Las inicializaciones de juegos restantes, como la creación del jugador, el oponente y la bola, se encuentran en la nueva función startGame. Para usar estos sprites recién inicializados, utilice el método PIXI.Sprite.from-Frame, que lleva un índice a la matriz de sprites en el archivo JSON.
Una última diferencia que proporciona Pixi.js es un sistema de entrada táctil y de mouse. Para hacer que cualquier sprite esté preparado para la entrada, establezco la propiedad interactiva en "true" y agrego directamente algunos controladores de eventos al sprite. Por ejemplo, para los botones arriba y abajo, enlacé eventos para subir y bajar al jugador, tal como se muestra en la Figura 3.
Figura 3 Enlazar eventos para subir o bajar al jugador
up = new PIXI.Sprite.fromFrame(3);
up.interactive = true;
stage.addChild(up);
up.touchstart = up.mousedown = function() {
player.move(-distance);
}
down = new PIXI.Sprite.fromFrame(4);
down.interactive = true;
stage.addChild(down);
down.touchstart = down.mousedown = function() {
player.move(distance);
}
Es importante tener en cuenta que tenía que agregar manualmente cada sprite en la escena para que fuera visible. Puede consultar mi versión de Ping integrado en Pixi.js en la descarga de código incluida con este artículo.
Phaser
Phaser (phaser.io) es un motor de juego completo que usa Pixi para realizar todas las tareas de representación. Phaser cuenta con una larga lista de características, incluida la animación de sprites basados en marcos, música y sonido, un sistema de estado del juego y física con tres bibliotecas de contribución diferentes.
Dado que ya había portado Ping a Pixi.js, portar a Phaser resultó sencillo. Phaser optimiza la creación de objetos basada en Pixi.js:
ball = game.add.sprite(x, y, 'sprites');
Aunque Phaser puede ser más conciso, hay lugares donde las cosas son más complejas. Por ejemplo, el último parámetro del código anterior, "sprite'" hace referencia a una imagen que se carga al inicio:
game.load.image('sprites', 'sprites.png');
Phaser tiene un sistema de hoja de sprites diferentes para las imágenes de Pixi.js. Puede dividir una imagen en mosaicos. Para usarla, llame a algo parecido a esto:
game.load.spritesheet('sprites', 'mySprites.png',
spriteWidth, spriteHeight, totalSprites);
Para mi ejemplo, en realidad no uso esta función. El código que acabo de mostrar supone que todos los sprites tienen el mismo tamaño, pero la hoja de sprites de Ping tiene sprites de 64 y 128 píxeles. Así, tuve que recortar cada imagen de sprite manualmente. Para recortar la imagen de la bola, establecí un rectángulo de recorte en el sprite:
ball.cropRect = new Phaser.Rectangle(0, 64, 64, 64);
ball.updateCrop();
Espero que esto muestre algo de la flexibilidad de Phaser. Esa flexibilidad se manifiesta además de otras formas. Puede hacer que el juego se base en eventos o buscar eventos y responder a ellos de manera secuencial en una función de actualización. Por ejemplo, para controlar una entrada de clave, puede responder a un evento:
game.input.keyboard.onDownCallback = function(key) {
console.log(key + " was pressed.");
}
o bien, compruebe si la tecla está presionada en el bucle de actualización:
function update() {
if (game.input.keyboard.isDown('a'.charCodeAt(0)) {
console.log("'A' key was pressed");
}
}
Esto es aplicable a muchos eventos en Phaser, usando codificación secuencial o asincrónica según sus preferencias o necesidad. En ocasiones, Phaser proporciona únicamente la última forma de control de eventos. Un buen ejemplo es el sistema de física Arcade, donde debe crear explícitamente una llamada de función para que cada actualización compruebe las colisiones entre objetos:
game.physics.arcade.collide(player, ball, function() {
ballOwner = player;
});
Este código determina si la bola ha entrado en contacto con el sprite del jugador y da el control de la bola al jugador cuando esto ocurre. Consulte mi implementación de Ping en Phaser con la descarga de código para este artículo.
Resumen
Si está buscando un motor de juego de JavaScript 2D dedicado, hay muchas opciones para satisfacer sus necesidades. Estos son dos factores importantes que hay que tener en cuenta al seleccionar un marco:
Use solo las características que necesita: No se deslumbre con muchas características, marcos monolíticos globales y otros accesorios. A menos que los accesorios traten aspectos específicos del juego que desea crear, probablemente estorbarán. Tenga en cuenta además de qué manera se dividen las características en componentes. ¿Seguirá funcionando todo el sistema si quita el sistema de física? ¿Depende el marco del sistema de física para proporcionar otra funcionalidad?
Comprenda cómo funciona cada motor: intente comprender cómo puede personalizar o ampliar el marco. Si está haciendo algo ligeramente fuera de lo normal, es probable que necesite escribir código personalizado que se base en gran medida en el marco que ha elegido. En ese momento, puede escribir una función nueva e independiente o ampliar la funcionalidad del marco. Si amplía el propio marco en lugar de agregarle a él, terminará con más código fácil de mantener.
Los marcos descritos aquí tratan muchos de estos problemas, por lo que definitivamente vale la pena echarles un vistazo. Existen docenas de motores, bibliotecas y marcos, por lo que debe investigar un poco antes de embarcarse en su siguiente aventura de desarrollo de juegos.
Michael Oneppo *es un especialista en tecnología creativa que fue jefe de programas en el equipo de Direct3D de Microsoft. Entre sus esfuerzos recientes se incluye su trabajo como director de tecnología en la tecnología sin ánimo de lucro Library For All y la investigación en un máster en el NYU Interactive Telecommunications Program.*
Gracias al siguiente experto técnico de Microsoft por revisar este artículo: Justin Garrett