Creando Tower Defense en Unity, Parte 1

El campo


  • Creando un campo de mosaico.
  • Buscar rutas usando la búsqueda de amplitud.
  • Implemente soporte para baldosas vacías y finales, así como baldosas de pared.
  • Edición de contenido en modo juego.
  • Visualización opcional de campos de cuadrícula y caminos.

Esta es la primera parte de una serie de tutoriales sobre cómo crear un juego de defensa de torre simple. En esta parte, consideraremos crear un campo de juego, encontrar un camino y colocar las baldosas y paredes finales.

El tutorial fue creado en Unity 2018.3.0f2.


Un campo listo para usar en un juego de fichas de género de defensa de la torre.

Juego de Tower Defense


La defensa de la torre es un género en el que el objetivo del jugador es destruir multitudes de enemigos hasta que alcancen su punto final. El jugador cumple su objetivo construyendo torres que atacan a los enemigos. Este género tiene muchas variaciones. Crearemos un juego con un campo de mosaico. Los enemigos se moverán por el campo hacia su punto final, y el jugador creará obstáculos para ellos.

Asumiré que ya has estudiado una serie de tutoriales sobre la gestión de objetos .

El campo


El campo de juego es la parte más importante del juego, por lo que lo crearemos primero. Este será un objeto de juego con su propio componente GameBoard , que puede inicializarse configurando el tamaño en dos dimensiones, para lo cual podemos usar el valor de Vector2Int . El campo debería funcionar con cualquier tamaño, pero elegiremos el tamaño en otro lugar, por lo que crearemos un método de Initialize común para esto.

Además, visualizamos el campo con un cuadrángulo, que denotará la tierra. No haremos que el objeto de campo en sí sea un cuadrilátero, sino que le agregaremos un objeto quad secundario. Tras la inicialización, haremos que la escala XY de la tierra sea igual al tamaño del campo. Es decir, cada mosaico tendrá un tamaño de una unidad de medida cuadrada para el motor.

 using UnityEngine; public class GameBoard : MonoBehaviour { [SerializeField] Transform ground = default; Vector2Int size; public void Initialize (Vector2Int size) { this.size = size; ground.localScale = new Vector3(size.x, size.y, 1f); } } 

¿Por qué establecer explícitamente el terreno al valor predeterminado?
La idea es que todo lo personalizable a través del editor de Unity sea accesible a través de campos ocultos serializados. Es necesario que estos campos solo se puedan cambiar en el inspector. Desafortunadamente, el editor de Unity mostrará constantemente una advertencia del compilador de que el valor nunca se asigna. Podemos suprimir esta advertencia estableciendo explícitamente el valor predeterminado para el campo. También puede asignar null , pero lo hice para mostrar explícitamente que simplemente usamos el valor predeterminado, que no es una referencia verdadera a tierra, por lo que usamos el default .

Cree un objeto de campo en una nueva escena y agregue un quad secundario con un material que se parezca a la tierra. Como estamos creando un juego prototipo simple, un material verde uniforme será suficiente. Gire el quad 90 ° a lo largo del eje X para que quede en el plano XZ.




Campo de juego.

¿Por qué no posicionar el juego en el plano XY?
Aunque el juego tendrá lugar en el espacio 2D, lo renderizaremos en 3D, con enemigos en 3D y una cámara que se puede mover en relación con cierto punto. El plano XZ es más conveniente para esto y corresponde a la orientación estándar de skybox utilizada para la iluminación ambiental.

El juego


A continuación, cree un componente del Game que será responsable de todo el juego. En esta etapa, esto significará que está inicializando el campo. Solo hacemos que el tamaño sea personalizable a través del inspector y forzamos al componente a inicializar el campo cuando se activa. Usemos el tamaño predeterminado de 11 × 11.

 using UnityEngine; public class Game : MonoBehaviour { [SerializeField] Vector2Int boardSize = new Vector2Int(11, 11); [SerializeField] GameBoard board = default; void Awake () { board.Initialize(boardSize); } } 

Los tamaños de campo solo pueden ser positivos y tiene poco sentido crear un campo con un único mosaico. Entonces limitemos el mínimo a 2 × 2. Esto se puede hacer agregando el método OnValidate , limitando a la fuerza los valores mínimos.

  void OnValidate () { if (boardSize.x < 2) { boardSize.x = 2; } if (boardSize.y < 2) { boardSize.y = 2; } } 

¿Cuándo se llama a Onvalidate?
Si existe, el editor de Unity lo llama para los componentes después de cambiarlos. Incluso al agregarlos al objeto del juego, después de cargar la escena, después de volver a compilar, después de cambiar en el editor, después de cancelar / reintentar y después de restablecer el componente.

OnValidate es el único lugar en el código donde puede asignar valores a los campos de configuración de componentes.


Objeto del juego

Ahora, cuando inicies el modo de juego, recibiremos un campo con el tamaño correcto. Durante el juego, coloque la cámara de modo que se vea todo el tablero, copie su componente de transformación, salga del modo de juego y pegue los valores del componente. En el caso de un campo de 11 × 11 en el origen, para obtener una vista conveniente desde arriba, puede colocar la cámara en posición (0.10.0) y girarla 90 ° a lo largo del eje X. Dejaremos la cámara en esta posición fija, pero es posible cámbialo en el futuro.


Cámara sobre el campo.

¿Cómo copiar y pegar valores de componentes?
A través del menú desplegable que aparece cuando hace clic en el botón con el engranaje en la esquina superior derecha del componente.

Azulejo prefabricado


El campo consta de azulejos cuadrados. Los enemigos podrán moverse de una ficha a otra, cruzando los bordes, pero no en diagonal. El movimiento siempre ocurrirá hacia el punto final más cercano. Denotemos gráficamente la dirección del movimiento a lo largo del mosaico con una flecha. Puede descargar la textura de la flecha aquí .


Flecha sobre un fondo negro.

Coloque la textura de la flecha en su proyecto y habilite la opción Alfa como transparencia . Luego cree un material para la flecha, que puede ser el material predeterminado para el que se selecciona el modo de recorte, y seleccione la flecha como la textura principal.


Material de flecha.

¿Por qué usar el modo de renderizado recortado?
Le permite ocultar la flecha utilizando la canalización de representación estándar de Unity.

Para denotar cada ficha en el juego, utilizaremos el objeto del juego. Cada uno de ellos tendrá su propio quad con material de flecha, tal como el campo tiene un quad de tierra. También agregaremos mosaicos al componente GameTile con un enlace a su flecha.

 using UnityEngine; public class GameTile : MonoBehaviour { [SerializeField] Transform arrow = default; } 

Cree un objeto de mosaico y conviértalo en un prefabricado. Los mosaicos estarán al ras con el suelo, así que levante un poco la flecha hacia arriba para evitar problemas de profundidad al renderizar. También aleja un poco, para que haya poco espacio entre las flechas adyacentes. Un desplazamiento Y de 0.001 y una escala de 0.8 que sea igual para todos los ejes funcionará.




Azulejo prefabricado.

¿Dónde está la jerarquía de mosaicos prefabricados?
Puede abrir el modo de edición prefabricado haciendo doble clic en el activo prefabricado, o seleccionando el prefabricado y haciendo clic en el botón Abrir Prefabricado en el inspector. Puede salir del modo de edición prefabricada haciendo clic en el botón con una flecha en la esquina superior izquierda de su encabezado de jerarquía.

Tenga en cuenta que las fichas en sí no tienen que ser objetos del juego. Solo son necesarios para rastrear el estado del campo. Podríamos usar el mismo enfoque que para el comportamiento en la serie de tutoriales de Object Management . Pero en las primeras etapas de juegos simples o prototipos de objetos de juego, estamos muy contentos. Esto se puede cambiar en el futuro.

Tenemos azulejos


Para crear mosaicos, el GameBoard debe tener un enlace al mosaico prefabricado.

  [SerializeField] GameTile tilePrefab = default; 


Enlace al mosaico prefabricado.

Luego puede crear sus instancias utilizando un doble bucle sobre dos dimensiones de cuadrícula. Aunque el tamaño se expresa como X e Y, organizaremos los mosaicos en el plano XZ, así como el campo en sí. Dado que el campo está centrado en relación con el origen, debemos restar el tamaño correspondiente menos uno dividido por dos de los componentes de la posición del mosaico. Tenga en cuenta que esta debe ser una división de coma flotante, de lo contrario no funcionará para tamaños pares.

  public void Initialize (Vector2Int size) { this.size = size; ground.localScale = new Vector3(size.x, size.y, 1f); Vector2 offset = new Vector2( (size.x - 1) * 0.5f, (size.y - 1) * 0.5f ); for (int y = 0; y < size.y; y++) { for (int x = 0; x < size.x; x++) { GameTile tile = Instantiate(tilePrefab); tile.transform.SetParent(transform, false); tile.transform.localPosition = new Vector3( x - offset.x, 0f, y - offset.y ); } } } 


Instancias creadas de azulejos.

Más tarde necesitaremos acceso a estos mosaicos, por lo que los rastrearemos en una matriz. No necesitamos una lista, porque después de la inicialización, el tamaño del campo no cambiará.

  GameTile[] tiles; public void Initialize (Vector2Int size) { … tiles = new GameTile[size.x * size.y]; for (int i = 0, y = 0; y < size.y; y++) { for (int x = 0; x < size.x; x++, i++) { GameTile tile = tiles[i] = Instantiate(tilePrefab); … } } } 

¿Cómo funciona esta tarea?
Esta es una tarea vinculada. En este caso, esto significa que estamos asignando un enlace a la instancia de mosaico tanto al elemento de matriz como a la variable local. Estas operaciones realizan lo mismo que el código que se muestra a continuación.

 GameTile t = Instantiate(tilePrefab); tiles[i] = t; GameTile tile = t; 

Busca un camino


En esta etapa, cada mosaico tiene una flecha, pero todas apuntan en la dirección positiva del eje Z, que interpretaremos como norte. El siguiente paso es determinar la dirección correcta para el mosaico. Hacemos esto al encontrar el camino que los enemigos deben seguir hasta el punto final.

Vecinos de azulejos


Los caminos van de baldosa en baldosa, en el norte, este, sur u oeste. Para simplificar la búsqueda, haga que GameTile enlaces a sus cuatro vecinos.

  GameTile north, east, south, west; 

Las relaciones entre vecinos son simétricas. Si el mosaico es el vecino oriental del segundo mosaico, entonces el segundo es el vecino occidental del primero. Agregue un método estático general a GameTile para definir esta relación entre dos mosaicos.

  public static void MakeEastWestNeighbors (GameTile east, GameTile west) { west.east = east; east.west = west; } 

¿Por qué usar un método estático?
Podemos convertirlo en un método de instancia con un solo parámetro, y en este caso lo llamaremos eastTile.MakeEastWestNeighbors(westTile) o algo así. Pero en los casos en que no está claro en cuál de los mosaicos se debe invocar el método, es mejor usar métodos estáticos. Los ejemplos son los métodos de Distance y Dot de la clase Vector3 .

Una vez conectado, nunca debería cambiar. Si esto sucede, cometimos un error en el código. Puede verificar esto comparando ambos enlaces antes de asignar valores null y mostrando un error si es incorrecto. Puede usar el método Debug.Assert para Debug.Assert .

  public static void MakeEastWestNeighbors (GameTile east, GameTile west) { Debug.Assert( west.east == null && east.west == null, "Redefined neighbors!" ); west.east = east; east.west = west; } 

¿Qué hace Debug.Assert?
Si el primer argumento es false , muestra un error de condición, utilizando el segundo argumento si se especifica. Dicha llamada se incluye solo en las versiones de prueba, pero no en las versiones de lanzamiento. Por lo tanto, esta es una buena manera de agregar comprobaciones durante el proceso de desarrollo que no afectarán la versión final.

Agregue un método similar para crear relaciones entre los vecinos del norte y del sur.

  public static void MakeNorthSouthNeighbors (GameTile north, GameTile south) { Debug.Assert( south.north == null && north.south == null, "Redefined neighbors!" ); south.north = north; north.south = south; } 

Podemos establecer esta relación al crear mosaicos en GameBoard.Initialize . Si la coordenada X es mayor que cero, entonces podemos crear una relación este-oeste entre los mosaicos actuales y anteriores. Si la coordenada Y es mayor que cero, entonces podemos crear una relación norte-sur entre el mosaico actual y el mosaico de la línea anterior.

  for (int i = 0, y = 0; y < size.y; y++) { for (int x = 0; x < size.x; x++, i++) { … if (x > 0) { GameTile.MakeEastWestNeighbors(tile, tiles[i - 1]); } if (y > 0) { GameTile.MakeNorthSouthNeighbors(tile, tiles[i - size.x]); } } } 

Tenga en cuenta que los mosaicos en los bordes del campo no tienen cuatro vecinos. Una o dos referencias vecinas permanecerán null .

Distancia y dirección


No obligaremos a todos los enemigos a buscar constantemente el camino. Esto debe hacerse solo una vez por mosaico. Luego, los enemigos podrán solicitar desde la ficha en la que se encuentran dónde avanzar. Almacenaremos esta información en GameTile agregando un enlace al siguiente mosaico de ruta. Además, también guardaremos la distancia al punto final, expresada como el número de fichas que se deben visitar antes de que el enemigo llegue al punto final. Para los enemigos, esta información es inútil, pero la usaremos para encontrar los caminos más cortos.

  GameTile north, east, south, west, nextOnPath; int distance; 

Cada vez que decidamos que necesitamos buscar rutas, necesitaremos inicializar los datos de ruta. Hasta que se encuentre el camino, no hay un mosaico siguiente y la distancia puede considerarse infinita. Podemos imaginar esto como el valor entero máximo posible de int.MaxValue . Agregue un método genérico ClearPath para restablecer GameTile a este estado.

  public void ClearPath () { distance = int.MaxValue; nextOnPath = null; } 

Solo se pueden buscar rutas si tenemos un punto final. Esto significa que el mosaico debe convertirse en el punto final. Tal mosaico tiene una distancia de cero, y no tiene el último mosaico, porque el camino termina en él. Agregue un método genérico que convierta un mosaico en un punto final.

  public void BecomeDestination () { distance = 0; nextOnPath = null; } 

En última instancia, todas las fichas deben convertirse en un camino, por lo que su distancia ya no será igual a int.MaxValue . Agregue una propiedad getter conveniente para verificar si el mosaico actualmente tiene una ruta.

  public bool HasPath => distance != int.MaxValue; 

¿Cómo funciona esta propiedad?
Esta es una entrada abreviada para una propiedad getter que contiene solo una expresión. Hace lo mismo que el código que se muestra a continuación.

  public bool HasPath { get { return distance != int.MaxValue; } } 

El operador de flecha => también se puede usar individualmente para obtener y establecer propiedades, para los cuerpos de métodos, constructores y en otros lugares.

Crecemos un camino


Si tenemos un mosaico con un camino, entonces podemos dejar que crezca un camino hacia uno de sus vecinos. Inicialmente, el único mosaico con el camino es el punto final, por lo que comenzamos desde la distancia cero y lo incrementamos desde aquí, moviéndonos en la dirección opuesta al movimiento de los enemigos. Es decir, todos los vecinos inmediatos del punto final tendrán una distancia de 1, y todos los vecinos de estos mosaicos tendrán una distancia de 2, y así sucesivamente.

Agregue un método oculto GameTile para hacer crecer la ruta a uno de sus vecinos, especificado a través del parámetro. La distancia al vecino es uno más que el mosaico actual, y la ruta del vecino indica el mosaico actual. Este método solo debe llamarse para los mosaicos que ya tienen una ruta, así que verifiquemos esto con aserción.

  void GrowPathTo (GameTile neighbor) { Debug.Assert(HasPath, "No path!"); neighbor.distance = distance + 1; neighbor.nextOnPath = this; } 

La idea es que llamemos a este método una vez para cada uno de los cuatro vecinos del mosaico. Dado que algunos de estos enlaces serán null , lo comprobaremos y detendremos la ejecución, si es así. Además, si un vecino ya tiene un camino, entonces no debemos hacer nada y también dejar de hacerlo.

  void GrowPathTo (GameTile neighbor) { Debug.Assert(HasPath, "No path!"); if (neighbor == null || neighbor.HasPath) { return; } neighbor.distance = distance + 1; neighbor.nextOnPath = this; } 

La forma en que GameTile rastrea a sus vecinos es desconocida para el resto del código. Por lo tanto, GrowPathTo está oculto. GrowPathTo métodos generales que le indican al mosaico que haga crecer su camino en una determinada dirección, llamando indirectamente a GrowPathTo . Pero el código que busca en todo el campo debe realizar un seguimiento de qué mosaicos se visitaron. Por lo tanto, haremos que devuelva un vecino o null si se termina la ejecución.

  GameTile GrowPathTo (GameTile neighbor) { if (!HasPath || neighbor == null || neighbor.HasPath) { return null; } neighbor.distance = distance + 1; neighbor.nextOnPath = this; return neighbor; } 

Ahora agregue métodos para cultivar caminos en direcciones específicas.

  public GameTile GrowPathNorth () => GrowPathTo(north); public GameTile GrowPathEast () => GrowPathTo(east); public GameTile GrowPathSouth () => GrowPathTo(south); public GameTile GrowPathWest () => GrowPathTo(west); 

Amplia búsqueda


GameBoard debe GameBoard que todos los mosaicos contengan los datos de ruta correctos. Hacemos esto realizando una búsqueda de amplitud. Comencemos con el mosaico de punto final, y luego crezca el camino hacia sus vecinos, luego hacia los vecinos de estos mosaicos, y así sucesivamente. Con cada paso, la distancia aumenta en uno, y los caminos nunca crecen en la dirección de las fichas que ya tienen caminos. Esto asegura que todos los mosaicos como resultado apunten a lo largo de la ruta más corta al punto final.

¿Qué hay de encontrar una ruta usando A *?
El algoritmo A * es el desarrollo evolutivo de la búsqueda de amplitud. Es útil cuando buscamos el único camino más corto. Pero necesitamos todos los caminos más cortos, por lo que A * no ofrece ninguna ventaja. Para ver ejemplos de búsqueda de amplitud y A * en una cuadrícula de hexágonos con animación, consulte la serie de tutoriales sobre mapas de hexágonos .

Para realizar la búsqueda, necesitamos rastrear los mosaicos que agregamos a la ruta, pero de los cuales aún no hemos crecido la ruta. Esta colección de azulejos a menudo se llama la frontera de búsqueda. Es importante que los mosaicos se procesen en el mismo orden en que se agregan al borde, así que usemos la Queue . Más tarde, tendremos que realizar la búsqueda varias veces, así que configurémoslo como el campo de GameBoard .

 using UnityEngine; using System.Collections.Generic; public class GameBoard : MonoBehaviour { … Queue<GameTile> searchFrontier = new Queue<GameTile>(); … } 

Para que el estado del campo de juego sea siempre verdadero, debemos encontrar las rutas al final de Initialize , pero poner el código en un método FindPaths separado. En primer lugar, debe despejar la ruta de todos los mosaicos, luego hacer que un mosaico sea el punto final y agregarlo al borde. Primero seleccionemos el primer mosaico. Como los tiles son una matriz, podemos usar el foreach sin temor a la contaminación de la memoria. Si luego pasamos de una matriz a una lista, también tendremos que reemplazar los bucles foreach for bucles for .

  public void Initialize (Vector2Int size) { … FindPaths(); } void FindPaths () { foreach (GameTile tile in tiles) { tile.ClearPath(); } tiles[0].BecomeDestination(); searchFrontier.Enqueue(tiles[0]); } 

Luego, necesitamos tomar un mosaico del borde y hacer crecer un camino hacia todos sus vecinos, agregándolos a todos al borde. Primero nos moveremos hacia el norte, luego hacia el este, sur y finalmente hacia el oeste.

  public void FindPaths () { foreach (GameTile tile in tiles) { tile.ClearPath(); } tiles[0].BecomeDestination(); searchFrontier.Enqueue(tiles[0]); GameTile tile = searchFrontier.Dequeue(); searchFrontier.Enqueue(tile.GrowPathNorth()); searchFrontier.Enqueue(tile.GrowPathEast()); searchFrontier.Enqueue(tile.GrowPathSouth()); searchFrontier.Enqueue(tile.GrowPathWest()); } 

Repetimos esta etapa, mientras que hay mosaicos en el borde.

  while (searchFrontier.Count > 0) { GameTile tile = searchFrontier.Dequeue(); searchFrontier.Enqueue(tile.GrowPathNorth()); searchFrontier.Enqueue(tile.GrowPathEast()); searchFrontier.Enqueue(tile.GrowPathSouth()); searchFrontier.Enqueue(tile.GrowPathWest()); } 

Hacer crecer un camino no siempre nos lleva a un nuevo mosaico. Antes de agregar a la cola, debemos verificar el valor de null , pero podemos posponer la verificación de null hasta después de la salida de la cola.

  GameTile tile = searchFrontier.Dequeue(); if (tile != null) { searchFrontier.Enqueue(tile.GrowPathNorth()); searchFrontier.Enqueue(tile.GrowPathEast()); searchFrontier.Enqueue(tile.GrowPathSouth()); searchFrontier.Enqueue(tile.GrowPathWest()); } 

Mostrar los caminos


Ahora tenemos un campo que contiene las rutas correctas, pero hasta ahora no vemos esto. Debe configurar las flechas para que apunten a lo largo del camino a través de sus mosaicos. Esto se puede hacer girándolos. Como estos giros son siempre los mismos, agregamos al GameTile un campo Quaternion estático para cada una de las direcciones.

  static Quaternion northRotation = Quaternion.Euler(90f, 0f, 0f), eastRotation = Quaternion.Euler(90f, 90f, 0f), southRotation = Quaternion.Euler(90f, 180f, 0f), westRotation = Quaternion.Euler(90f, 270f, 0f); 

Agregue también el método general ShowPath . Si la distancia es cero, entonces el mosaico es el punto final y no hay nada que señalar, así que desactive su flecha. De lo contrario, active la flecha y establezca su rotación. La dirección deseada se puede determinar comparando nextOnPath con sus vecinos.

  public void ShowPath () { if (distance == 0) { arrow.gameObject.SetActive(false); return; } arrow.gameObject.SetActive(true); arrow.localRotation = nextOnPath == north ? northRotation : nextOnPath == east ? eastRotation : nextOnPath == south ? southRotation : westRotation; } 

Llame a este método para todos los mosaicos al final GameBoard.FindPaths.

  public void FindPaths () { … foreach (GameTile tile in tiles) { tile.ShowPath(); } } 


Formas encontradas.

¿Por qué no convertimos la flecha directamente en GrowPathTo?
Separar la lógica y la visualización de la búsqueda. Más tarde haremos la visualización deshabilitada. Si las flechas no aparecen, no necesitamos rotarlas cada vez que llamamos FindPaths.

Cambiar prioridad de búsqueda


Resulta que cuando el punto final es la esquina suroeste, todos los caminos van exactamente hacia el oeste hasta llegar al borde del campo, después de lo cual giran hacia el sur. Aquí todo es cierto, porque en realidad no hay caminos más cortos hacia el punto final, porque los movimientos diagonales son imposibles. Sin embargo, hay muchos otros caminos más cortos que pueden parecer más bonitos.

Para comprender mejor por qué se encuentran dichos caminos, mueva el punto final al centro del mapa. Con un tamaño de campo impar, es solo un mosaico en el medio de la matriz.

  tiles[tiles.Length / 2].BecomeDestination(); searchFrontier.Enqueue(tiles[tiles.Length / 2]); 


Punto final en el centro.

El resultado parece lógico si recuerda cómo funciona la búsqueda. Como agregamos vecinos en el orden noreste-sur-oeste, el norte tiene la máxima prioridad. Como estamos haciendo la búsqueda en orden inverso, esto significa que la última dirección en la que hemos viajado es hacia el sur. Es por eso que solo unas pocas flechas apuntan hacia el sur y muchas apuntan hacia el este.

Puede cambiar el resultado estableciendo las prioridades de las direcciones. Cambiemos de este a sur. Entonces tenemos que obtener la simetría norte-sur y este-oeste.

  searchFrontier.Enqueue(tile.GrowPathNorth()); searchFrontier.Enqueue(tile.GrowPathSouth()); searchFrontier.Enqueue(tile.GrowPathEast()); searchFrontier.Enqueue(tile.GrowPathWest()) 


El orden de búsqueda es norte-sur-este-oeste.

Parece más bonito, pero es mejor que los caminos cambien de dirección, acercándose al movimiento diagonal donde se verá natural. Podemos hacer esto invirtiendo las prioridades de búsqueda de los mosaicos vecinos en un patrón de tablero de ajedrez.

En lugar de averiguar qué tipo de mosaico estamos procesando durante la búsqueda, agregamos a la GameTilepropiedad general que indica si el mosaico actual es una alternativa.

  public bool IsAlternative { get; set; } 

Estableceremos esta propiedad en GameBoard.Initialize. Primero, marque los mosaicos como alternativa si su coordenada X es par.

  for (int i = 0, y = 0; y < size.y; y++) { for (int x = 0; x < size.x; x++, i++) { … tile.IsAlternative = (x & 1) == 0; } } 

¿Qué hace la operación (x & 1) == 0?
— (AND). . 1, 1. 10101010 00001111 00001010.

. 0 1. 1, 2, 3, 4 1, 10, 11, 100. , .

AND , , . , .

En segundo lugar, cambiamos el signo del resultado si su coordenada Y es par. Entonces crearemos un patrón de ajedrez.

  tile.IsAlternative = (x & 1) == 0; if ((y & 1) == 0) { tile.IsAlternative = !tile.IsAlternative; } 

Como FindPathsmantenemos el mismo orden que la búsqueda de baldosas alternativa, pero para que sea de nuevo a todos los otros azulejos. Esto forzará el camino hacia el movimiento diagonal y creará zigzags.

  if (tile != null) { if (tile.IsAlternative) { searchFrontier.Enqueue(tile.GrowPathNorth()); searchFrontier.Enqueue(tile.GrowPathSouth()); searchFrontier.Enqueue(tile.GrowPathEast()); searchFrontier.Enqueue(tile.GrowPathWest()); } else { searchFrontier.Enqueue(tile.GrowPathWest()); searchFrontier.Enqueue(tile.GrowPathEast()); searchFrontier.Enqueue(tile.GrowPathSouth()); searchFrontier.Enqueue(tile.GrowPathNorth()); } } 


Orden de búsqueda variable.

Cambio de azulejos


En este punto, todas las fichas están vacías. Un mosaico se usa como punto final, pero además de la ausencia de una flecha visible, se ve igual que todos los demás. Agregaremos la capacidad de cambiar las fichas colocando objetos sobre ellas.

Contenido del azulejo


Los objetos de mosaico en sí mismos son simplemente una forma de rastrear la información de mosaico. No modificamos estos objetos directamente. En su lugar, agregue contenido separado y colóquelo en el campo. Por ahora, podemos distinguir entre mosaicos vacíos y mosaicos de punto final. Para indicar estos casos, cree una enumeración GameTileContentType.

 public enum GameTileContentType { Empty, Destination } 

A continuación, cree un tipo de componente GameTileContentque le permita establecer el tipo de su contenido a través del inspector, y el acceso al mismo será a través de una propiedad getter común.

 using UnityEngine; public class GameTileContent : MonoBehaviour { [SerializeField] GameTileContentType type = default; public GameTileContentType Type => type; } 

Luego crearemos prefabricados para dos tipos de contenido, cada uno de los cuales tiene un componente GameTileContentcon el tipo especificado correspondiente. Usemos un cubo aplanado azul para designar mosaicos de punto final. Como es casi plano, no necesita un colisionador. Para prefabricar contenido vacío, use un objeto de juego vacío.

destino

vacio

Prefabricados del punto final y contenido vacío.

Le daremos el objeto de contenido a los mosaicos vacíos, porque entonces todos los mosaicos siempre tendrán el contenido, lo que significa que no necesitaremos verificar la igualdad de los enlaces a los contenidos null.

Fábrica de contenido


Para que el contenido sea editable, también crearemos una fábrica para esto, utilizando el mismo enfoque que en el tutorial de Gestión de objetos . Esto significa que GameTileContentdebe realizar un seguimiento de su fábrica original, que debe configurarse solo una vez, y enviarse de vuelta a la fábrica en el método Recycle.

  GameTileContentFactory originFactory; … public GameTileContentFactory OriginFactory { get => originFactory; set { Debug.Assert(originFactory == null, "Redefined origin factory!"); originFactory = value; } } public void Recycle () { originFactory.Reclaim(this); } 

Esto supone la existencia GameTileContentFactory, por lo tanto, crearemos un tipo de objeto programable para esto con el método requerido Recycle. En esta etapa, no nos molestaremos con la creación de una fábrica completamente funcional que utilice los contenidos, por lo que haremos que simplemente destruya los contenidos. Posteriormente, será posible agregar la reutilización de objetos a la fábrica sin cambiar el resto del código.

 using UnityEngine; using UnityEngine.SceneManagement; [CreateAssetMenu] public class GameTileContentFactory : ScriptableObject { public void Reclaim (GameTileContent content) { Debug.Assert(content.OriginFactory == this, "Wrong factory reclaimed!"); Destroy(content.gameObject); } } 

Agregue un método oculto Geta la fábrica con un prefab como parámetro. Aquí nuevamente omitimos la reutilización de objetos. Crea una instancia del objeto, establece su fábrica original, lo mueve a la escena de fábrica y lo devuelve.

  GameTileContent Get (GameTileContent prefab) { GameTileContent instance = Instantiate(prefab); instance.OriginFactory = this; MoveToFactoryScene(instance.gameObject); return instance; } 

La instancia se ha movido a la escena de contenido de fábrica, que se puede crear según sea necesario. Si estamos en el editor, antes de crear una escena, debemos verificar si existe, en caso de que la perdamos de vista durante un reinicio en caliente.

  Scene contentScene; … void MoveToFactoryScene (GameObject o) { if (!contentScene.isLoaded) { if (Application.isEditor) { contentScene = SceneManager.GetSceneByName(name); if (!contentScene.isLoaded) { contentScene = SceneManager.CreateScene(name); } } else { contentScene = SceneManager.CreateScene(name); } } SceneManager.MoveGameObjectToScene(o, contentScene); } 

Solo tenemos dos tipos de contenido, así que solo agregue dos campos de configuración prefabricados para ellos.

  [SerializeField] GameTileContent destinationPrefab = default; [SerializeField] GameTileContent emptyPrefab = default; 

Lo último que debe hacerse para que la fábrica funcione es crear un método general Getcon un parámetro GameTileContentTypeque reciba una instancia del prefabricado correspondiente.

  public GameTileContent Get (GameTileContentType type) { switch (type) { case GameTileContentType.Destination: return Get(destinationPrefab); case GameTileContentType.Empty: return Get(emptyPrefab); } Debug.Assert(false, "Unsupported type: " + type); return null; } 

¿Es obligatorio agregar una instancia separada de contenido vacío a cada mosaico?
, . . , - , , , , . , . , , .

Creemos un activo de fábrica y configuremos sus enlaces a prefabricados.


Fábrica de contenido

Y luego pasa el Gameenlace a la fábrica.

  [SerializeField] GameTileContentFactory tileContentFactory = default; 


Juego con una fábrica.

Tocando un azulejo


Para cambiar el campo, debemos poder seleccionar un mosaico. Lo haremos posible en el modo de juego. Emitiremos un rayo en la escena en el lugar donde el jugador hizo clic en la ventana del juego. Si el rayo se cruza con el azulejo, entonces el jugador lo tocó, es decir, debe cambiarse. Gamemanejará la entrada del jugador, pero será responsable de determinar qué mosaico tocó el jugador GameBoard.

No todos los rayos se cruzan con la baldosa, por lo que a veces no recibiremos nada. Por lo tanto, agregamos al GameBoardmétodo GetTile, que siempre siempre regresa inicialmente null(esto significa que no se encontró el mosaico).

  public GameTile GetTile (Ray ray) { return null; } 

Para determinar si un rayo ha cruzado un mosaico, necesitamos llamar Physics.Raycastespecificando el rayo como argumento. Devuelve información sobre si hubo una intersección. Si es así, entonces podemos devolver el mosaico, aunque todavía no sabemos cuál, por lo que por ahora lo devolveremos null.

  public GameTile TryGetTile (Ray ray) { if (Physics.Raycast(ray) { return null; } return null; } 

Para saber si hubo una intersección con un mosaico, necesitamos más información sobre la intersección. Physics.Raycastpuede proporcionar esta información utilizando el segundo parámetro RaycastHit. Este es el parámetro de salida, que se indica con la palabra que está outdelante de él. Esto significa que una llamada al método puede asignar un valor a la variable que le pasamos.

  RaycastHit hit; if (Physics.Raycast(ray, out hit) { return null; } 

Podemos incrustar la declaración de las variables utilizadas para los parámetros de salida, así que hagámoslo.

  if (Physics.Raycast(ray, out RaycastHit hit) { return null; } 

No nos importa con qué colisionador se produjo la intersección, solo usamos la posición de intersección XZ para determinar el mosaico. Obtenemos las coordenadas del mosaico agregando la mitad del tamaño del campo a las coordenadas del punto de intersección y luego convirtiendo los resultados a valores enteros. Como resultado, el índice de mosaico final será su coordenada X más la coordenada Y multiplicada por el ancho del campo.

  if (Physics.Raycast(ray, out RaycastHit hit)) { int x = (int)(hit.point.x + size.x * 0.5f); int y = (int)(hit.point.z + size.y * 0.5f); return tiles[x + y * size.x]; } 

Pero esto solo es posible cuando las coordenadas del mosaico están dentro del campo, por lo que lo comprobaremos. Si este no es el caso, no se devolverá el mosaico.

  int x = (int)(hit.point.x + size.x * 0.5f); int y = (int)(hit.point.z + size.y * 0.5f); if (x >= 0 && x < size.x && y >= 0 && y < size.y) { return tiles[x + y * size.x]; } 

Cambio de contenido


Para que pueda cambiar el contenido del mosaico, agréguelo a la GameTilepropiedad general Content. Su captador simplemente devuelve los contenidos, y el colocador descarta los contenidos anteriores, si los hay, y coloca los nuevos contenidos.

  GameTileContent content; public GameTileContent Content { get => content; set { if (content != null) { content.Recycle(); } content = value; content.transform.localPosition = transform.localPosition; } } 

Este es el único lugar en el que necesita verificar el contenido null, porque inicialmente no tenemos contenido. Para garantizar, ejecutamos afirmar para que no se llame al establecedor con null.

  set { Debug.Assert(value != null, "Null assigned to content!"); … } 

Y finalmente, necesitamos una entrada del jugador. Convertir clic del ratón en el haz se puede hacer llamando ScreenPointToRaycon Input.mousePositionun argumento. La llamada debe realizarse para la cámara principal, a la que se puede acceder a través Camera.main. Agregue la propiedad c para esto Game.

  Ray TouchRay => Camera.main.ScreenPointToRay(Input.mousePosition); 

Luego agregamos un método Updateque verifica si se presionó el botón principal del mouse durante la actualización. Para hacer esto, llame Input.GetMouseButtonDowncon cero como argumento. Si la tecla ha sido presionada, procesamos el toque del jugador, es decir, tomamos el mosaico del campo y establecemos el punto final como su contenido, tomándolo de fábrica.

  void Update () { if (Input.GetMouseButtonDown(0)) { HandleTouch(); } } void HandleTouch () { GameTile tile = GetTile(TouchRay); if (tile != null) { tile.Content = tileContentFactory.Get(GameTileContentType.Destination); } } 

Ahora podemos convertir cualquier mosaico en un punto final presionando el cursor.


Varios puntos finales.

Hacer el campo correcto


Aunque podemos convertir los mosaicos en puntos finales, esto no afecta las rutas hasta ahora. Además, aún no hemos establecido contenido vacío para mosaicos. Mantener la corrección y la integridad del campo es una tarea GameBoard, así que vamos a darle la responsabilidad de establecer el contenido del mosaico. Para implementar esto, le daremos un enlace a la fábrica de contenido a través de su método Intialize, y lo usaremos para dar a todos los mosaicos una instancia de contenido vacío.

  GameTileContentFactory contentFactory; public void Initialize ( Vector2Int size, GameTileContentFactory contentFactory ) { this.size = size; this.contentFactory = contentFactory; ground.localScale = new Vector3(size.x, size.y, 1f); tiles = new GameTile[size.x * size.y]; for (int i = 0, y = 0; y < size.y; y++) { for (int x = 0; x < size.x; x++, i++) { … tile.Content = contentFactory.Get(GameTileContentType.Empty); } } FindPaths(); } 

Ahora Gametengo que transferir mi fábrica al campo.

  void Awake () { board.Initialize(boardSize, tileContentFactory); } 

¿Por qué no agregar un campo de configuración de fábrica al GameBoard?
, , . , .

Como ahora tenemos varios puntos finales, lo cambiamos GameBoard.FindPathspara que llame BecomeDestinationa cada uno y los agregue todos al borde. Y eso es todo lo que se necesita para admitir múltiples puntos finales. Todas las demás fichas se borran como de costumbre. Luego eliminamos el punto final fijo en el centro.

  void FindPaths () { foreach (GameTile tile in tiles) { if (tile.Content.Type == GameTileContentType.Destination) { tile.BecomeDestination(); searchFrontier.Enqueue(tile); } else { tile.ClearPath(); } } //tiles[tiles.Length / 2].BecomeDestination(); //searchFrontier.Enqueue(tiles[tiles.Length / 2]); … } 

Pero si podemos convertir los mosaicos en puntos finales, entonces deberíamos poder realizar la operación inversa, convertir los puntos finales en mosaicos vacíos. Pero entonces podemos obtener un campo sin puntos finales. En este caso, FindPathsno podrá realizar su tarea. Esto sucede cuando el borde está vacío después de la inicialización de la ruta para todas las celdas. Denotamos esto como un estado no válido del campo, devolviendo falsey completando la ejecución; de lo contrario regrese al final true.

  bool FindPaths () { foreach (GameTile tile in tiles) { … } if (searchFrontier.Count == 0) { return false; } … return true; } 

La forma más fácil de implementar soporte para eliminar puntos finales, convirtiéndolo en una operación de cambio. Al hacer clic en los mosaicos vacíos, los convertiremos en puntos finales, y al hacer clic en los puntos finales, los eliminaremos. Pero ahora se dedica a cambiar el contenido GameBoard, por lo que le daremos un método general ToggleDestination, cuyo parámetro es el mosaico. Si el mosaico es el punto final, entonces vacíelo y llame FindPaths. De lo contrario, lo convertimos en el punto final y también lo llamamos FindPaths.

  public void ToggleDestination (GameTile tile) { if (tile.Content.Type == GameTileContentType.Destination) { tile.Content = contentFactory.Get(GameTileContentType.Empty); FindPaths(); } else { tile.Content = contentFactory.Get(GameTileContentType.Destination); FindPaths(); } } 

Agregar un punto final nunca puede crear un estado de campo no válido, y eliminar un punto final puede. Por lo tanto, verificaremos si se ejecutó con éxito FindPathsdespués de vaciar el mosaico. De lo contrario, cancele el cambio, volviendo el mosaico al punto final y vuelva FindPathsa llamar para volver al estado correcto anterior.

  if (tile.Content.Type == GameTileContentType.Destination) { tile.Content = contentFactory.Get(GameTileContentType.Empty); if (!FindPaths()) { tile.Content = contentFactory.Get(GameTileContentType.Destination); FindPaths(); } } 

¿Se puede hacer la validación más eficiente?
, . , . , . FindPaths , .

Ahora al final Initializepodemos llamar ToggleDestinationcon el mosaico central como argumento, en lugar de llamar explícitamente FindPaths. Esta es la única vez que comenzamos con un estado de campo no válido, pero se garantiza que terminaremos con el estado correcto.

  public void Initialize ( Vector2Int size, GameTileContentFactory contentFactory ) { … //FindPaths(); ToggleDestination(tiles[tiles.Length / 2]); } 

Finalmente, forzamos a Gamellamar en ToggleDestinationlugar de configurar el contenido del mosaico.

  void HandleTouch () { GameTile tile = board.GetTile(TouchRay); if (tile != null) { //tile.Content = //tileContentFactory.Get(GameTileContentType.Destination); board.ToggleDestination(tile); } } 


Múltiples puntos finales con rutas correctas.

¿No deberíamos prohibirle a Game configurar el contenido del mosaico directamente?
. . , Game . , .

Las paredes


El objetivo de la torre de defensa es evitar que los enemigos lleguen al punto final. Este objetivo se logra de dos maneras. Primero, los matamos y, en segundo lugar, los ralentizamos para que haya más tiempo para matarlos. En el campo de mosaico, el tiempo se puede estirar, aumentando la distancia que los enemigos deben recorrer. Esto se puede lograr colocando obstáculos en el campo. Por lo general, estas son torres que también matan enemigos, pero en este tutorial nos limitaremos solo a las paredes.

Contenido


Los muros son otro tipo de contenido, así que agreguemos GameTileContentTypeun elemento a ellos.

 public enum GameTileContentType { Empty, Destination, Wall } 

Luego cree el muro prefabricado. Esta vez crearemos un objeto de juego con el contenido del mosaico y le agregaremos un cubo secundario, que estará en la parte superior del campo y llenará todo el mosaico. Haga que tenga una altura de media unidad y guarde el colisionador, porque las paredes pueden superponerse visualmente a parte de las fichas detrás de él. Por lo tanto, cuando un jugador toca una pared, influirá en la casilla correspondiente.

raíz

cubo

prefabricado

Muro prefabricado.

Agregue la pared prefabricada a la fábrica, tanto en el código como en el inspector.

  [SerializeField] GameTileContent wallPrefab = default; … public GameTileContent Get (GameTileContentType type) { switch (type) { case GameTileContentType.Destination: return Get(destinationPrefab); case GameTileContentType.Empty: return Get(emptyPrefab); case GameTileContentType.Wall: return Get(wallPrefab); } Debug.Assert(false, "Unsupported type: " + type); return null; } 


Fábrica con pared prefabricada.

Enciende y apaga las paredes


Agregue al GameBoardmétodo de encendido / apagado de las paredes, como lo hicimos para el punto final. Inicialmente, no verificaremos el estado incorrecto del campo.

  public void ToggleWall (GameTile tile) { if (tile.Content.Type == GameTileContentType.Wall) { tile.Content = contentFactory.Get(GameTileContentType.Empty); FindPaths(); } else { tile.Content = contentFactory.Get(GameTileContentType.Wall); FindPaths(); } } 

Brindaremos soporte para cambiar solo entre mosaicos vacíos y mosaicos, sin permitir que los muros reemplacen directamente los puntos finales. Por lo tanto, solo crearemos un muro cuando el mosaico esté vacío. Además, los muros deberían bloquear la búsqueda del camino. Pero cada ficha debe tener un camino hacia el punto final, de lo contrario los enemigos se atascarán. Para hacer esto, nuevamente necesitamos usar la validación FindPathsy descartar los cambios si crearon un estado de campo incorrecto.

  else if (tile.Content.Type == GameTileContentType.Empty) { tile.Content = contentFactory.Get(GameTileContentType.Wall); if (!FindPaths()) { tile.Content = contentFactory.Get(GameTileContentType.Empty); FindPaths(); } } 

Encender y apagar paredes se usará con mucha más frecuencia que encender y apagar puntos finales, por lo que haremos que las paredes se cambien en el Gametoque principal. Los puntos finales se pueden cambiar con un toque adicional (generalmente el botón derecho del mouse), que se puede reconocer pasando a un Input.GetMouseButtonDownvalor de 1.

  void Update () { if (Input.GetMouseButtonDown(0)) { HandleTouch(); } else if (Input.GetMouseButtonDown(1)) { HandleAlternativeTouch(); } } void HandleAlternativeTouch () { GameTile tile = board.GetTile(TouchRay); if (tile != null) { board.ToggleDestination(tile); } } void HandleTouch () { GameTile tile = board.GetTile(TouchRay); if (tile != null) { board.ToggleWall(tile); } } 


Ahora tenemos las paredes.

¿Por qué obtengo grandes espacios entre las sombras de paredes adyacentes en diagonal?
, , , . , , far clipping plane . , far plane 20 . , MSAA, .

Asegurémonos también de que los puntos finales no puedan reemplazar directamente los muros.

  public void ToggleDestination (GameTile tile) { if (tile.Content.Type == GameTileContentType.Destination) { … } else if (tile.Content.Type == GameTileContentType.Empty) { tile.Content = contentFactory.Get(GameTileContentType.Destination); FindPaths(); } } 

Bloqueo de búsqueda de ruta


Para que los muros bloqueen la búsqueda de la ruta, es suficiente que no agreguemos mosaicos con muros al borde de búsqueda. Esto se puede hacer obligando a GameTile.GrowPathTono devolver los azulejos con paredes. Pero el camino aún debe crecer en la dirección de la pared, de modo que todas las fichas en el campo tengan un camino. Esto es necesario porque es posible que una ficha con enemigos se convierta de repente en una pared.

  GameTile GrowPathTo (GameTile neighbor) { if (!HasPath || neighbor == null || neighbor.HasPath) { return null; } neighbor.distance = distance + 1; neighbor.nextOnPath = this; return neighbor.Content.Type != GameTileContentType.Wall ? neighbor : null; } 

Para asegurarse de que todos los mosaicos tengan una ruta, GameBoard.FindPathsdeben verificar esto después de que se complete la búsqueda. Si este no es el caso, el estado del campo no es válido y debe devolverse false. No es necesario actualizar la visualización de la ruta para estados no válidos, porque el campo volverá al estado anterior.

  bool FindPaths () { … foreach (GameTile tile in tiles) { if (!tile.HasPath) { return false; } } foreach (GameTile tile in tiles) { tile.ShowPath(); } return true; } 


Los muros afectan el camino.

Para asegurarse de que las paredes tengan los caminos correctos, debe hacer que los cubos sean translúcidos.


Paredes transparentes.

Tenga en cuenta que el requisito de corrección de todos los caminos no permite que los muros encierren una parte del campo en el que no hay un punto final. Podemos dividir el mapa, pero solo si hay al menos un punto final en cada parte. Además, cada pared debe estar adyacente a un mosaico vacío o punto final, de lo contrario no podrá tener una ruta. Por ejemplo, es imposible hacer un bloque sólido de paredes de 3 × 3.

Ocultar el camino


La visualización de las rutas nos permite ver cómo funciona la búsqueda de rutas y asegurarnos de que sea correcta. Pero no necesita mostrarse al jugador, o al menos no necesariamente. Por lo tanto, proporcionemos la capacidad de apagar las flechas. Esto se puede hacer agregando al GameTilemétodo general HidePath, que simplemente deshabilita su flecha.

  public void HidePath () { arrow.gameObject.SetActive(false); } 

El estado de asignación de ruta es parte del estado del campo. Agregue un GameBoardcampo booleano al valor predeterminado igual falsepara rastrear su estado, así como una propiedad común como getter y setter. El colocador debe mostrar u ocultar rutas en todos los mosaicos.

  bool showPaths; public bool ShowPaths { get => showPaths; set { showPaths = value; if (showPaths) { foreach (GameTile tile in tiles) { tile.ShowPath(); } } else { foreach (GameTile tile in tiles) { tile.HidePath(); } } } } 

Ahora el método FindPathsdebería mostrar rutas actualizadas solo si la representación está habilitada.

  bool FindPaths () { … if (showPaths) { foreach (GameTile tile in tiles) { tile.ShowPath(); } } return true; } 

Por defecto, la visualización de ruta está deshabilitada. Apague la flecha en el mosaico prefabricado.


La flecha prefabricada está inactiva por defecto.

Lo hacemos para que Gamecambie el estado de visualización cuando se presiona una tecla. Sería lógico usar la tecla P, pero también es una tecla de acceso rápido para habilitar / deshabilitar el modo de juego en el editor de Unity. Como resultado, la visualización cambiará cuando se use la tecla de acceso rápido para salir del modo de juego, lo que no se ve muy bien. Entonces usemos la tecla V (abreviatura de visualización).


Sin flechas

Visualización de cuadrícula


Cuando las flechas están ocultas, se hace difícil discernir la ubicación de cada mosaico. Agreguemos las líneas de la cuadrícula. Descargue una textura de malla de borde cuadrado desde aquí que se puede usar como un contorno de mosaico separado.


Textura de malla.

No agregaremos esta textura individualmente a cada mosaico, sino que la aplicaremos al suelo. Pero haremos esta cuadrícula opcional, así como la visualización de rutas. Por lo tanto, agregaremos al GameBoardcampo de configuración Texture2Dy seleccionaremos una textura de malla para él.

  [SerializeField] Texture2D gridTexture = default; 


Campo con textura de malla.

Agregue otro campo booleano y una propiedad para controlar el estado de la visualización de la cuadrícula. En este caso, el colocador debe cambiar el material de la tierra, que puede implementarse llamando a la GetComponent<MeshRenderer>tierra y obteniendo acceso a la propiedad del materialresultado. Si es necesario mostrar la mainTexturecuadrícula , asignaremos la textura de la cuadrícula a la propiedad del material. De lo contrario, asignárselo null. Tenga en cuenta que cuando cambie la textura del material, se crearán duplicados de la instancia de material, de modo que se vuelva independiente del activo de material.

  bool showGrid, showPaths; public bool ShowGrid { get => showGrid; set { showGrid = value; Material m = ground.GetComponent<MeshRenderer>().material; if (showGrid) { m.mainTexture = gridTexture; } else { m.mainTexture = null; } } } 

Cambiemos Gamela visualización de la cuadrícula con la tecla G.

  void Update () { … if (Input.GetKeyDown(KeyCode.G)) { board.ShowGrid = !board.ShowGrid; } } 

Además, agregue la visualización de malla predeterminada a Awake.

  void Awake () { board.Initialize(boardSize, tileContentFactory); board.ShowGrid = true; } 


Cuadrícula sin escala.

Hasta ahora tenemos un borde alrededor de todo el campo. Coincide con la textura, pero eso no es lo que necesitamos. Necesitamos escalar la textura principal del material para que coincida con el tamaño de la cuadrícula. Puede hacerlo llamando al método de SetTextureScalematerial con el nombre de la propiedad de textura ( _MainTex ) y el tamaño bidimensional. Podemos usar directamente el tamaño del campo, que se convierte indirectamente en un valor Vector2.

  if (showGrid) { m.mainTexture = gridTexture; m.SetTextureScale("_MainTex", size); } 

sin

con

Cuadrícula escalada con visualización de ruta activada y desactivada.

Entonces, en esta etapa, obtuvimos un campo funcional para un juego de fichas del género de defensa de la torre. En el próximo tutorial agregaremos enemigos.

Repositorio

PDF

Source: https://habr.com/ru/post/449798/


All Articles