Hola desarrolladores de Flash, bienvenidos a la segunda parte de mi tutorial del juego de defensa de la torre. En la primera parte, desarrollamos el mecanismo básico de crear torretas y hacer que disparen hacia el punto de clic del mouse. ¡Pero para eso no son las torretas! En esta parte extenderemos el juego para incluir enemigos, inteligencia artificial básica (AI) en torretas y algunos elementos más del juego. Estás listo?
Este es el juego que vamos a crear en este tutorial:
Haga clic en los círculos de color naranja para colocar torretas. Los círculos rojos son enemigos, y el número en cada uno representa sus puntos de golpe..
En el tutorial anterior desarrollamos un juego que tenía marcadores de posición para las torretas. Podríamos desplegar torretas haciendo clic en esos marcadores de posición, y las torretas apuntaban al puntero del mouse y disparaban balas hacia el punto donde el usuario hacía clic..
Terminamos con un Principal
Clase que tenía el bucle de juego y la lógica del juego. Aparte de eso tuvimos el Torreta
clase que no tenía mucho, excepto el actualizar
Función que hizo girar la torreta..
Previamente creamos las balas en. Principal
clase y adjunto un ENTER_FRAME
oyente para moverlo. La bala no tenía suficientes propiedades antes para considerarla como una clase separada. Pero en un juego así, las balas pueden tener muchas variedades como la velocidad, el daño, etc., por lo que es una buena idea sacar el código de la viñeta y encapsularlo en un espacio separado. Bala
clase. Vamos a hacerlo.
Crear una nueva clase llamada Bala
, extendiendo el Duende
clase. El código básico para esta clase debe ser:
paquete import flash.display.Sprite; La clase pública Bullet extiende Sprite public function Bullet ()
A continuación ponemos el código para dibujar el gráfico de bala, tomado de Principal
, en Bala
. Como hicimos con el Torreta
clase, creamos una función llamada dibujar
en el Bala
clase:
función privada draw (): void var g: Graphics = this.graphics; g.beginFill (0xEEEEEE); g.drawCircle (0, 0, 5); g.endFill ();
Y a esta función la llamamos Bala
constructor:
función pública Bullet () draw ();
Ahora agregamos algunas propiedades a la bala. Agrega cuatro variables: velocidad
, velocidad_x
, rápido
y dañar
, antes de Bala
constructor:
velocidad de var privado: Número; private var speed_x: Number; private var speed_y: Number; daño de var público: int;
¿Cuáles son estas variables para?
velocidad
: Esta variable almacena la velocidad de la bala..velocidad_x
y rápido
: Estos almacenan los componentes x e y de la velocidad, respectivamente, de modo que el cálculo de la división de la velocidad en sus componentes no tiene que hacerse una y otra vez. dañar
: Esta es la cantidad de daño que la bala puede hacer a un enemigo. Mantenemos esta variable pública, ya que la necesitaremos en nuestro bucle de juego en el Principal
clase. Inicializamos estas variables en el constructor. Actualiza tu Bala
constructor:
Función pública Bullet (ángulo: Número) velocidad = 5; daño = 1; speed_x = Math.cos (angle * Math.PI / 180) * speed; speed_y = Math.sin (angle * Math.PI / 180) * speed; dibujar();
Observe la ángulo
Variable que recibimos en el constructor. Esta es la dirección (en grados) en que se moverá la bala. Acabamos de romper el velocidad
en sus componentes xey, y los almacena en caché para uso futuro.
Lo último que queda en el Bala
clase es tener un actualizar
Función que se llamará desde el bucle del juego para actualizar (mover) la bala. Agregue la siguiente función al final de la Bala
clase:
actualización de la función pública (): void x + = speed_x; y + = speed_y;
¡Bingo! Hemos terminado con nuestro Bala
clase.
Movimos un montón de código de bala de Principal
clase a su propio Bala
clase, por lo que una gran cantidad de código permanece sin uso en Principal
y mucho necesita ser actualizado.
Primero, borra el crearBullet ()
y moveBullet ()
funciones También quite el bullet_speed
variable.
A continuación, vaya a la disparar
Funciona y actualízala con el siguiente código:
función privada de disparo (e: MouseEvent): void para cada (var torreta: Torreta en torretas) var new_bullet: Bullet = new Bullet (turret.rotation); new_bullet.x = turret.x + Math.cos (turret.rotation * Math.PI / 180) * 25; new_bullet.y = turret.y + Math.sin (turret.rotation * Math.PI / 180) * 25; addChild (new_bullet);
Ya no usamos el crearBullet
función para crear bala en lugar de utilizar el Bala
constructor y pasar la torreta de rotación
para ello, que es la dirección del movimiento de la bala, por lo que no necesitamos almacenarla en la bala. rotación
propiedad como hicimos antes. Además, no adjuntamos ningún oyente a la bala ya que la bala se actualizará desde dentro del bucle del juego a continuación.
Ahora que necesitamos actualizar las viñetas del bucle del juego, necesitamos una referencia de ellas para almacenarlas en algún lugar. La solución es la misma que para las torretas: crear una nueva Formación
llamado balas
y empujar las balas en él como se crean.
Primero declare una matriz justo debajo de la torretas
declaración de matriz:
var privado ghost_turret: Torreta; torr de var privado: Array = []; balas var privadas: Array = [];
Ahora para poblar esta matriz. Lo hacemos cada vez que creamos una nueva bala, así que, en el disparar
función. Agregue lo siguiente justo antes de agregar la bala al escenario:
var new_bullet: Bullet = new Bullet (turret.rotation); new_bullet.x = turret.x + Math.cos (turret.rotation * Math.PI / 180) * 25; new_bullet.y = turret.y + Math.sin (turret.rotation * Math.PI / 180) * 25; bullets.push (new_bullet); addChild (new_bullet);
Al igual que la forma en que actualizamos las torretas en el bucle del juego, también actualizaremos las balas. Pero esta vez, en lugar de usar un para cada
bucle, usaremos un básico para
lazo. Antes de esto, debemos agregar dos variables a la parte superior del bucle del juego, de modo que sepamos qué variables se usan dentro del bucle del juego y las podemos liberar para la recolección de basura.
torreta var: torreta; var bullet: Bullet;
Continúa y agrega el siguiente código al final del ciclo de juego:
para (var i: int = bullets.length - 1; i> = 0; i--) bullet = bullets [i]; si (! bullet) continúa; bullet.update ();
Aquí atravesamos todas las balas en el escenario cada fotograma y llamamos a sus actualizar
Función que les hace moverse. Tenga en cuenta aquí que iteramos el balas
matriz en sentido inverso. ¿Por qué? Veremos esto por delante.
Ahora que tenemos un torreta
variable declarada en el exterior ya, no necesitamos declararla nuevamente dentro del para cada
lazo de torretas. Modificarlo a:
para cada (torreta en las torretas) turret.update ();
Finalmente agregamos la condición de verificación de límites; esto fue previamente en la bala ENTER_FRAME
Pero ahora lo comprobamos en el bucle de juego:
si (bullet.x < 0 || bullet.x > stage.stageWidth || bullet.y < 0 || bullet.y > stage.stageHeight) bullets.splice (i, 1); bullet.parent.removeChild (bullet); continuar;
Verificamos si la bala está fuera del límite de la etapa, y si es así, primero eliminamos su referencia de balas
matriz utilizando el empalme
función, y luego retire la bala del escenario y continúe con la siguiente iteración. Así es como debería verse tu bucle de juego:
función privada gameLoop (e: Event): void var turret: Turret; var bullet: Bullet; para cada (torreta en las torretas) turret.update (); para (var i: int = bullets.length - 1; i> = 0; i--) bullet = bullets [i]; si (! bullet) continúa; bullet.update ();
Si ahora ejecuta el juego, debería tener la misma funcionalidad que en la Parte 1, con un código que es mucho más limpio y organizado..
Ahora agregamos uno de los elementos más importantes del juego: el enemigo. Lo primero es crear una nueva clase llamada Enemigo
extendiendo el Duende
clase:
paquete import flash.display.Sprite; clase pública Enemy extiende Sprite public function Enemy ()
Ahora agregamos algunas propiedades a la clase. Agrégalos antes de tu Enemigo
constructor:
private var speed_x: Number; private var speed_y: Number;
Iniciamos estas variables en la Enemigo
constructor:
función pública Enemigo () speed_x = -1.5; speed_y = 0;
A continuación creamos el dibujar
y actualizar
funciones para el Enemigo
clase. Estos son muy similares a los de Bala
. Agregue el siguiente código:
función privada draw (): void var g: Graphics = this.graphics; g.beginFill (0xff3333); g.drawCircle (0, 0, 15); g.endFill (); actualización de la función pública (): void x + = speed_x; y + = speed_y;
En nuestro juego necesitamos tener muchos eventos que tengan lugar en ciertos momentos o repetidamente en ciertos intervalos. Tal tiempo puede lograrse usando un contador de tiempo. El contador es solo una variable que se incrementa a medida que pasa el tiempo en el juego. Lo importante aquí es cuándo y en qué cantidad incrementar el contador. Hay dos formas en que la sincronización se realiza generalmente en los juegos: basado en el tiempo y basado en el marco.
La diferencia es que la unidad de juego paso a paso se basa en tiempo real (es decir, el número de milisegundos transcurridos), pero en un juego basado en cuadros, la unidad de paso se basa en unidades de cuadros (es decir, el número de cuadros pasados).
Para nuestro juego vamos a utilizar un contador basado en cuadros. Tendremos un contador que iremos incrementando en uno en el bucle del juego, que se ejecuta en cada cuadro, y básicamente nos dará la cantidad de cuadros que han pasado desde que comenzó el juego. Continúe y declare una variable después de las otras declaraciones de variables en el Principal
clase:
var privado ghost_turret: Torreta; torr de var privado: Array = []; balas var privadas: Array = []; private var global_time: Number = 0;
Incrementamos esta variable en el bucle de juego en la parte superior:
global_time ++;
Ahora, en base a este contador, podemos hacer cosas como crear enemigos, que haremos a continuación..
Lo que queremos hacer ahora es crear enemigos en el campo después de cada dos segundos. Pero estamos tratando con marcos aquí, ¿recuerdas? Entonces, ¿después de cuántos marcos debemos crear enemigos? Bueno, nuestro juego está funcionando a 30 FPS, incrementando así el tiempo global
Contador 30 veces por segundo. Un simple cálculo nos dice que 3 segundos = 90 cuadros.
Al final del bucle de juego agrega lo siguiente Si
bloquear:
if (global_time% 90 == 0)
¿Sobre qué es esa condición? Utilizamos el operador de módulo (%), que da el resto de una división, por lo que global_time% 90
nos da el resto cuando tiempo global
se divide por 90
. Verificamos si el resto es. 0
, ya que este solo será el caso cuando tiempo global
es un múltiplo de 90
- es decir, la condición vuelve cierto
cuando tiempo global
es igual a 0
, 90
, 180
y así sucesivamente ... De esta manera, logramos un disparo en cada 90 cuadros o 3 segundos.
Antes de que creamos al enemigo, declara otra matriz llamada enemigos
justo debajo de la torretas
y balas
formación. Esto será usado para almacenar referencias a enemigos en el escenario..
var privado ghost_turret: Torreta; torr de var privado: Array = []; balas var privadas: Array = []; enemigos var privados: Array = []; private var global_time: Number = 0;
También declarar un enemigo
variable en la parte superior del bucle de juego:
global_time ++; torreta var: torreta; var bullet: Bullet; enemigo enemigo: enemigo;
Finalmente agregue el siguiente código dentro del Si
bloque que creamos anteriormente:
enemigo = nuevo enemigo (); enemigo.x = 410; enemy.y = 30 + Math.random () * 370; enemigos.push (enemigo); addChild (enemigo);
Aquí creamos un nuevo enemigo, lo colocamos al azar a la derecha del escenario, lo empujamos en el enemigos
Arreglo y agrégalo al escenario..
Al igual que actualizamos las balas en el bucle del juego, actualizamos a los enemigos. Pon el siguiente código debajo de la torreta para cada
lazo:
para (var j: int = enemigos.longitud - 1; j> = 0; j--) enemigo = enemigos [j]; enemy.update (); si (enemigo.x < 0) enemies.splice(j, 1); enemy.parent.removeChild(enemy); continue;
Al igual que hicimos un control de límites para las balas, también verificamos si hay enemigos. Pero para los enemigos, simplemente verificamos si salieron del lado izquierdo del escenario, ya que solo se mueven de derecha a izquierda. Deberías ver enemigos que vienen de la derecha si corres el juego ahora..
Todo enemigo tiene algo de vida / salud y también la nuestra. También mostraremos la salud restante sobre los enemigos. Permite declarar algunas variables en el Enemigo
clase para las cosas de la salud:
private var health_txt: TextField; salud var privada: int; private var speed_x: Number; private var speed_y: Number;
Inicializamos el salud
Variable en el constructor siguiente. Agregue lo siguiente a la Enemigo
constructor:
salud = 2;
Ahora inicializamos la variable de texto de salud para mostrar en el centro del enemigo. Lo hacemos en el dibujar
función:
health_txt = new TextField (); health_txt.height = 20; health_txt.width = 15; health_txt.textColor = 0xffffff; health_txt.x = -5; health_txt.y = -8; health_txt.text = health + ""; addChild (health_txt);
Todo lo que hacemos es crear una nueva. Campo de texto
, establecer su color, posicionarlo y establecer su texto en el valor actual de salud
Finalmente agregamos una función para actualizar la salud del enemigo:
función pública updateHealth (cantidad: int): int salud + = cantidad; health_txt.text = health + ""; salud de retorno
La función acepta un entero para agregar al estado, actualiza el texto del estado y devuelve el estado final. Llamaremos a esta función desde nuestro circuito de juego para actualizar la salud de cada enemigo y detectar si aún está vivo..
Primero modifiquemos nuestro disparar
funcionar un poco Reemplace el existente disparar
Funcionar con el siguiente:
función privada disparar (torreta: Torreta, enemigo: Enemigo): void var angle: Número = Math.atan2 (enemy.y - turret.y, enemy.x - turret.x) / Math.PI * 180; turret.rotation = ángulo; var new_bullet: Bullet = new Bullet (angle); new_bullet.x = turret.x + Math.cos (turret.rotation * Math.PI / 180) * 25; new_bullet.y = turret.y + Math.sin (turret.rotation * Math.PI / 180) * 25; bullets.push (new_bullet); addChild (new_bullet);
los disparar
La función ahora acepta dos parámetros. La primera es una referencia a una torreta que hará el disparo; El segundo es una referencia a un enemigo hacia el que disparará..
El nuevo código aquí es similar al presente en el Torreta
clase actualizar
función, pero en lugar de la posición del ratón ahora usamos las coordenadas del enemigo. Así que ahora puedes eliminar todo el código de la actualizar
función de la Torreta
clase.
Ahora, ¿cómo hacer que las torretas disparen a los enemigos? Bueno, la lógica es simple para nuestro juego. Hacemos que todas las torretas disparen al primer enemigo en el. enemigos
formación. ¿Qué? Vamos a poner un código y luego tratar de entender. Sume las siguientes líneas al final de la para cada
bucle utilizado para actualizar las torretas:
para cada (torreta en las torretas) turret.update (); para cada (enemigo en enemigos) disparar (torreta, enemigo); descanso;
Para cada torreta ahora lo actualizamos, luego iteramos el enemigos
array, dispara al primer enemigo en el array y rompe el loop. Así que, esencialmente, cada torreta dispara al primer enemigo creado, ya que siempre está al principio de la matriz. Intenta correr el juego y deberías ver torretas disparando a los enemigos..
Pero espera, ¿en qué fluye esa corriente de bala? Parece que están disparando muy rápido. Veamos porque.
Como sabemos, el bucle de juego se ejecuta en cada fotograma, es decir, 30 veces por segundo en nuestro caso, por lo que la declaración de disparo que agregamos en el paso anterior se llama a la velocidad de nuestro bucle de juego y, por lo tanto, vemos un flujo de balas. Parece que también necesitamos un mecanismo de tiempo dentro de las torretas. Cambiar a la Torreta
Clase y agregue el siguiente código:
private var local_time: Number = 0; privado var reload_time: int;
hora local
: Nuestro contador se llama hora local
en contraste con el tiempo global
en el Principal
clase. Esto es por dos razones: primero, porque esta variable es local a la Torreta
clase; segundo, porque no siempre avanza como nuestro tiempo global
variable - se reiniciará muchas veces durante el curso del juego. tiempo de recarga
: Este es el tiempo requerido por la torreta para volver a cargar después de disparar una bala. Básicamente es la diferencia de tiempo entre dos disparos de bala por una torreta. Recuerda que todas las unidades de tiempo en nuestro juego son en términos de marcos.. Incrementar el hora local
variable en el actualizar
funciona e inicializa el tiempo de recarga
en el constructor:
actualización de la función pública (): void local_time ++;
función pública Turret () reload_time = 30; dibujar();
A continuación agregue las siguientes dos funciones al final de la Torreta
clase:
función pública isReady (): Boolean return local_time> reload_time; función pública reset (): void local_time = 0;
está listo
devuelve verdadero solo cuando el actual hora local
es mayor que el tiempo de recarga
, es decir, cuando la torreta se ha recargado. Y el Reiniciar
función simplemente restablece la hora local
Variable, para iniciarla recargando de nuevo..
Ahora de vuelta en el Principal
clase, modifique el código de disparo en el bucle del juego que agregamos en el paso anterior al siguiente:
para cada (torreta en las torretas) turret.update (); si (! turret.isReady ()) continúa; para cada (enemigo en enemigos) disparar (torreta, enemigo); turret.reset (); descanso;
Así que si ahora la torreta no está lista (está listo()
devoluciones falso
), continuamos con la siguiente iteración del bucle de la torreta. Verás que las torretas se disparan en un intervalo de 30 cuadros o 1 segundo ahora. Guay!
Todavía algo no está bien. Las torretas disparan a los enemigos independientemente de la distancia entre ellos. Lo que falta aquí es el distancia de una torreta. Cada torreta debe tener su propio rango dentro del cual puede disparar a un enemigo. Agrega otra variable a la Torreta
clase llamada distancia
y configurarlo para 120
dentro del constructor:
privado var reload_time: int; private var local_time: Number = 0; rango de var privado: int;
función pública Turret () reload_time = 30; rango = 120; dibujar();
También agrega una función llamada canShoot
al final de la clase:
función pública canShoot (enemigo: enemigo): booleano var dx: Number = enemy.x - x; var dy: Number = enemy.y - y; si (Math.sqrt (dx * dx + dy * dy) <= range) return true; else return false;
Cada torreta puede disparar a un enemigo solo cuando cumple con ciertos criterios; por ejemplo, puede dejar que la torreta dispare solo a enemigos rojos con menos de la mitad de su vida y no más de 30px de distancia. Toda esa lógica para determinar si la torreta es capaz de disparar o no a un enemigo entrará en el canShoot
función, que devuelve cierto
o falso
segun la logica.
Nuestra lógica es simple. Si el enemigo está dentro del rango de retorno. cierto
; de lo contrario devuelve falso. Entonces, cuando la distancia entre la torreta y el enemigo (Math.sqrt (dx * dx + dy * dy)
) es menor o igual que distancia
, vuelve cierto
. Un poco más de modificación en la sección de disparos del bucle de juego:
para cada (torreta en las torretas) turret.update (); si (! turret.isReady ()) continúa; para cada (enemigo en enemigos) if (turret.canShoot (enemigo)) disparar (torreta, enemigo); turret.reset (); descanso;
Ahora solo si el enemigo está dentro del alcance de la torreta, la torreta disparará.
Una parte muy importante de cada juego es la detección de colisiones. En nuestro juego el control de colisión se realiza entre balas y enemigos. Estaremos agregando el código de detección de colisión dentro de la para cada
bucle que actualiza las balas en el bucle del juego.
La lógica es simple. Por cada bala atravesamos el enemigos
Arreglar y comprobar si hay una colisión entre ellos. Si es así, eliminamos la bala, actualizamos la salud del enemigo y salimos del bucle para controlar a otros enemigos. Vamos a añadir algún código:
para (i = bullets.length - 1; i> = 0; i--) bullet = bullets [i]; // si la viñeta no está definida, continúe con la siguiente iteración si (! bullet) continúa; bullet.update (); si (bullet.x < 0 || bullet.x > stage.stageWidth || bullet.y < 0 || bullet.y > stage.stageHeight) bullets.splice (i, 1); bullet.parent.removeChild (bullet); continuar; para (var k: int = enemigos.longitud - 1; k> = 0; k--) enemigo = enemigos [k]; if (bullet.hitTestObject (enemigo)) bullets.splice (i, 1); bullet.parent.removeChild (bullet); if (enemy.updateHealth (-1) == 0) enemigos.splice (k, 1); enemy.parent.removeChild (enemigo); rotura;
Utilizamos ActionScript's hitTestObject
Función para comprobar la colisión entre la bala y el enemigo. Si se produce la colisión, la bala se elimina de la misma manera que cuando abandona el escenario. La salud del enemigo se actualiza utilizando la actualizarSalud
método, al cual bala
es dañar
Se pasa propiedad. Si el actualizarSalud
función devuelve un número entero menor o igual que 0
, esto significa que el enemigo está muerto y lo eliminamos de la misma manera que la bala.
Y nuestra detección de colisiones está hecha.!
Recuerda que atravesamos los enemigos y las balas en sentido inverso en nuestro circuito de juego. Vamos a entender por qué. Supongamos que utilizamos un ascendente para
lazo. Estamos en el índice i = 3
y eliminamos una bala de la matriz. En la eliminación del artículo en la posición 3
, su espacio es llenado por el elemento luego en la posición 4
. Así que ahora el artículo previamente en la posición 4
Me senté 3
. Después de la iteración yo
incrementos por 1
y se convierte en 4
y así el artículo en la posición 4
está chequeado.
Vaya, ¿ves lo que acaba de pasar? Acabamos de perder el artículo ahora en la posición 3
que cambió de lugar como resultado del empalme. Y así usamos un revés para
bucle que elimina este problema. Puedes ver porque.
Vamos a agregar algunas cosas extra para que el juego se vea bien. Agregaremos funcionalidad para mostrar el rango de una torreta cuando el mouse se encuentre sobre ella. Cambiar a la Torreta
clase y añadir algunas variables a ella:
rango de var privado: int; privado var reload_time: int; private var local_time: Number = 0; cuerpo var privado: Sprite; private var range_circle: Sprite;
Siguiente actualice el dibujar
función a lo siguiente:
función privada draw (): void range_circle = new Sprite (); g = range_circle.graphics; g.beginFill (0x00D700); g.drawCircle (0, 0, rango); g.endFill (); range_circle.alpha = 0.2; range_circle.visible = false; addChild (range_circle); cuerpo = nuevo Sprite (); var g: Graphics = body.graphics; g.beginFill (0xD7D700); g.drawCircle (0, 0, 20); g.beginFill (0x800000); g.drawRect (0, -5, 25, 10); g.endFill (); addChild (cuerpo);
Rompemos los gráficos de la torreta en dos partes: el cuerpo y el gráfico de rango. Hacemos esto para dar un orden a las diferentes partes de la torreta. Aquí requerimos la range_circle
para estar detrás del cuerpo de la torreta, y así lo agregamos primero al escenario. Finalmente, agregamos dos escuchas de mouse para alternar el gráfico de rango:
función privada onMouseOver (e: MouseEvent): void range_circle.visible = true; función privada onMouseOut (e: MouseEvent): void range_circle.visible = false;
Ahora adjunte los oyentes a los eventos respectivos al final del constructor:
body.addEventListener (MouseEvent.MOUSE_OVER, onMouseOver); body.addEventListener (MouseEvent.MOUSE_OUT, onMouseOut);
Si ejecutas el juego y tratas de desplegar una torreta, verás un parpadeo al pasar sobre los marcadores de posición. Porqué es eso?
Recuerda que configuramos el ratón habilitado
propiedad de la torre del fantasma de falso
? Hicimos eso porque, la torreta fantasma estaba capturando eventos del mouse al interponerse entre el mouse y el marcador de posición. La misma situación ha vuelto a aparecer, ya que la torreta tiene dos hijos ahora, su cuerpo y el sprite de rango, que capturan los eventos del mouse en medio.
La solución es la misma. Podemos establecer su individualidad. ratón habilitado
propiedades para falso
. Pero una mejor solución es configurar la torre del fantasma. ratón niños
propiedad a falso
. Lo que esto hace es restringir a todos los hijos de la torre fantasma de recibir eventos del mouse. Ordenado, ¿eh? Adelante, ponlo en falso
en el Principal
constructor:
ghost_turret = torreta nueva (); ghost_turret.alpha = 0.5; ghost_turret.mouseEnabled = false; ghost_turret.mouseChildren = false; ghost_turret.visible = falso; addChild (ghost_turret);
Problema resuelto.
Podríamos ampliar esta demostración para incluir características mucho más avanzadas y convertirla en un juego jugable. Algunos de los cuales podrían ser:
Veamos qué puedes obtener de esta demostración básica. Estaré encantado de escuchar sus juegos de defensa de la torre y sus comentarios o sugerencias para la serie..