Ahora toca hablar de los Contenedores. Un contenedor no es mas que la agrupación de un conjunto de elementos, para trabajar con todos ellos como un todo. Los contenedores poseen un DisplayList en donde podemos añadir distintos elementos, uno tras otro como hemos ido haciendo en anteriores capítulos con la clase Stage (la cual es un Contenedor).
Agrupar elementos nos permitirá crear objetos más complejos a la vez que nos ayudará a organizar mejor los distintos elementos de la aplicación. Cada Contenedor posee su propio sistema de coordenadas interno, en donde se ubican cada uno de sus hijos, así como sus propios Eventos y su propia jerarquia.
Para entenderlo mejor, crearemos un Container, y le añadiremos un par de hijos. Para crear un Container escribiremos:
var contenedor = new createjs.Container()
Ahora creamos dos elementos, un círculo y un cuadrado:
circle = new createjs.Shape(); circle.graphics.beginFill('green').drawCircle(0, 0, 20); rect = new createjs.Shape(); rect.graphics.beginFill('blue').drawRect(30, 0, 40, 40);
Seguidamente los añadimos al contenedor con addChild().
container.addChild(circle, rect);
Ahora ese Container tiene dos Shape en su interior, pero no podemos verlo. Deberemos añadir este Container al Stage.
stage.addChild(container); stage.update();
De este modo el Stage renderizará a sus hijos, al encontrarse con el Container éste renderizará los suyos.
Y ahí lo tenemos, por ahora nada especial. Lo interesante es que ese Container posee su propio sistema de coordenadas, es decir, sus hijos están en el sistema de coordenadas 'x' e 'y' del Container, y no del Stage. Por ejemplo, si cambiamos las coordenadas del Contenedor, sus hijos se moverán con él.
container.x = 150; container.y = 100;
Lo mismo ocurrirá con cualquier transformación que hagamos sobre el Container (rotación, alpha, etc). De este modo podremos crear objetos compuestos de otros objetos, y tratarlo como un todo independiente, ayudando a la modularización de nuestra aplicación y facilitándonos la vida a la hora de escribir el código.
var canvas, stage, container, circle, rect;
$(document).ready(function(){
canvas = document.getElementById('canvas');
stage = new createjs.Stage(canvas);
container = new createjs.Container();
circle = new createjs.Shape();
circle.graphics.beginFill('green').drawCircle(20, 20, 20);
rect = new createjs.Shape();
rect.graphics.beginFill('blue').drawRect(40, 0, 40, 40);
container.addChild(circle, rect);
//container.x = 150;
//container.y = 100;
stage.addChild(container);
stage.update();
}
Vamos a divertirnos un poco más creando un Objeto compuesto de varios Shape. Para ello, usaremos un Container como ya hemos aprendido; en él posicionaremos los distintos elementos con el fin de crear una composición sencilla.
Para este ejemplo, la idea será hacer un Tanque de combate controlable por el usuario usando algunos Shape. La estructura será la siguiente:
Una vez definida la estructura, crearemos los Contenedores y elementos necesarios. Empezaremos por el Container más externo, el Tank:
tank = new createjs.Container(); // contendrá otros Containers en su interior.
Ahora crearemos el body, el cual será un simple rectángulo redondeado:
body = new createjs.Shape(); body.graphics.beginFill('#0B610B').drawRoundRect(0,0, 70, 40, 5);
Continuamos con el gun. Será un Contenedor:
gun = new createjs.Container();
Ahora crearemos los hijos que añadiremos a gun. Serán dos Shape, un círculo y un rectángulo.
base = new createjs.Shape(); base.graphics.beginFill('#088A08').drawCircle(0, 0, 12); cannon = new createjs.Shape(); cannon.graphics.beginFill('#088A08').drawRect(0, -4, 36, 8);
Nótese la ubicación de los objetos. Hemos dibujado un círculo en la posición 0,0 (el punto superior izquierdo de este Container) y luego hemos dibujado la base, ajustado en la posición 0, -4 para que encaje bien con la gun. Esto ha sido a base de prueba-error, hasta lograr la distribución deseada.
Seguidamente añadimos los hijos al gun.
gun.addChild(base, cannon); // primero la base y después el cañón
Finalmente, componemos el tank, añadiendo el body y el gun, ubicándolos en una posición adecuada:
gun.x = 24; gun.y = 20; tank.addChild(body, gun);
Sin olvidar añadir todo el tank al Stage.
// ajustamos el 'centro' del tank completo tank.regX = 24; tank.regY = 20; //lo ubicamos en el centro del canvas tank.x = canvas.width / 2; tank.y = canvas.height / 2; //lo añadimos al DisplayList del Stage stage.addChild(tank); // añadir tank, que contiene Shapes y Containers stage.update() // dibujar todo!
Y esto nos dará un resultado como este:
En este último apartado crearemos una pequeña lógica para poder interactuar con el tanque usando el teclado. Moveremos el tank con las teclas WASD (girar, ir hacia adelante e ir hacia atras), así como el gun con otras dos teclas más (rotación).
Primero, definiremos las variables globales con los key code, como hemos hecho en el capìtulo anterior:
var KEY_LEFT = 37, KEY_RIGHT = 39, KEY_W = 87, KEY_S = 83, KEY_A = 65, KEY_D = 68;
Y un array donde guardaremos todas estas teclas:
var pressing = [];
Ahora crearemos la lógica para rotar el gun (teclas de dirección izquierda y derecha)
Escuchamos los eventos de teclado con la función enableInputs() usada en el capítulo anterior.
if(pressing[KEY_LEFT]){ gun.rotation -= 1; } if(pressing[KEY_RIGHT]){ gun.rotation += 1; }
Para mover el tank completo será muy parecido. El giro lo haremos igual, mediante rotación:
if(pressing[KEY_A]){ tank.rotation -= 1; } if(pressing[KEY_D]){ tank.rotation += 1; }
Para conseguir que avance o retroceda usaremos una técnica de trigonometría para GameDevelopment llamada movimiento angular. Básicamente consistirá en calcular cúal será la siguiente ubicación del tank (posición 'x' e 'y') a partir de su ángulo (propiedad rotation).
Estos cálculos se hacen calculando el coseno y el seno de la rotación (el coseno nos dará la posición 'x' y el seno nos dará la posición 'y'). Sin embargo no podremos usar la rotación en grados, deberemos pasarla a radianes.
//obtener posición x Math.cos(tank.rotation * Math.PI / 180) * vel; // multiplicamos por 'vel' para tener un valor más elevado //obtener posición y Math.sin(tank.rotation * Math.PI / 180) * vel;
Con esta sencilla fórmula podremos añadir o sustraer cantidades a las propiedades 'x' e 'y' del tank, y así conseguiremos un movimiento angular.
if(pressing[KEY_W]){ tank.x += Math.cos(tank.rotation * (Math.PI / 180)) * 2; tank.y += Math.sin(tank.rotation * (Math.PI / 180)) * 2; } if(pressing[KEY_S]){ tank.x -= Math.cos(tank.rotation * (Math.PI / 180)) * 2; tank.y -= Math.sin(tank.rotation * (Math.PI / 180)) * 2; }
Este sería el código completo:
var canvas, stage, tank, body, gun, cannon, base, pressing;
var KEY_LEFT = 37,
KEY_RIGHT = 39,
KEY_W = 87,
KEY_S = 83,
KEY_A = 65,
KEY_D = 68;
$(document).ready(function(){
pressing = [];
enableInputs();
canvas = document.getElementById('canvas');
stage = new createjs.Stage(canvas);
tank = new createjs.Container();
body = new createjs.Shape();
body.graphics.beginFill('#0B610B').drawRoundRect(0,0, 70, 40, 5);
gun = new createjs.Container();
base = new createjs.Shape();
base.graphics.beginFill('#088A08').drawCircle(0, 0, 12);
cannon = new createjs.Shape();
cannon.graphics.beginFill('#088A08').drawRect(0, -4, 36, 8);
gun.addChild(base, cannon);
gun.x = 24;
gun.y = 20;
tank.addChild(body, gun);
tank.regX = 24;
tank.regY = 20;
tank.x = canvas.width / 2;
tank.y = canvas.height / 2;
stage.addChild(tank);
createjs.Ticker.setFPS(60);
createjs.Ticker.on('tick', onTick);
};
var onTick = function(e){
if(!e.paused){
//Gun
if(pressing[KEY_LEFT]){
gun.rotation -= 1;
}
if(pressing[KEY_RIGHT]){
gun.rotation += 1;
}
//Tank
if(pressing[KEY_A]){
tank.rotation -= 1;
}
if(pressing[KEY_D]){
tank.rotation += 1;
}
if(pressing[KEY_W]){
tank.x += Math.cos(tank.rotation * (Math.PI / 180)) * 2;
tank.y += Math.sin(tank.rotation * (Math.PI / 180)) * 2;
}
if(pressing[KEY_S]){
tank.x -= Math.cos(tank.rotation * (Math.PI / 180)) * 2;
tank.y -= Math.sin(tank.rotation * (Math.PI / 180)) * 2;
}
stage.update(e);
}
var enableInputs = function(){
document.addEventListener('keydown', function(evt) {
pressing[evt.keyCode] = true;
});
document.addEventListener('keyup', function(evt) {
pressing[evt.keyCode] = false;
});
}
Y este ha sido el final de otro capítulo más en el aprendizaje de CreateJS. Podemos añadir muchas mejoras, como usar texturas en lugar de colores planos, o hacer que dispare al pulsar alguna tecla (hacer aparecer una bala que viaje, usando la técnica del movimiento angular, en la dirección en la que se encuentre mirando el gun), o quizás podríamos usar Sprite en lugar de Shape... pero eso os lo dejo de deberes ;)