Unity: Sistema de ataque a distancia en juego 2D

Para inaugurar mi nuevo blog he querido hacer un pequeño tutorial básico sobre algo que he estado implementando actualmente para mi proyecto y que creo que es utilizado en muchísimos juegos de tipo plataformas 2D. Se trata de crear un sencillo sistema de ataque a distancia (por ejemplo lanzar shurikens) en el que podamos configurar tanto su dirección como su velocidad y que, asignado al GameObject de nuestro personaje y pulsando un botón de ataque, podamos lanzar un arma capaz de interactuar con el escenario (por ejemplo dañar a los enemigos). Para centrarnos en la realización de esta mecánica en concreto vamos a suponer que ya contamos con un GameObject que representa a nuestro personaje principal y el cual ya tiene su correspondiente script controlador (en nuestro caso lo llamaremos “PlayerController.cs“) encargado de implementar las acciones básicas del personaje como por ejemplo su movimiento.

Aclaración: En este post, cuando hago uso el término “Shuriken“, en ningún momento me estoy refiriendo al sistema de partículas de Unity, sino simplemente a un tipo de arma arrojadiza comúnmente utilizada por los ninjas que en este caso he usado de ejemplo, pero el sistema es igualmente aplicable para armas de tipo balas, flechas o cualquier otra cosa que so os ocurra 😀 Para evitar confusiones, aquí dejo su definición: shuriken.

Creación del GameObject ‘Shuriken’

Vamos a empezar por crear un nuevo GameObject vacío y que representará a nuestro shuriken. A continuación le añadiremos los siguientes componentes:

  • SphereCollider: nos aseguraremos de que le activaremos su propiedad ‘IsTrigger‘. En mi caso he escogido un collider de tipo esfera ya que es el que mejor se amolda a mi caso concreto, pero se podría usar cualquier otro tipo de collider como por ejemplo un BoxCollider.
  • Rigidbody: debido a que nuestra arma podrá ser configurada para moverse a altas velocidades, recomiendo establecer su propiedad ‘Collision Detection‘ a ‘Continuous‘ para una mejor detección de colisiones.
GameObject 'Shuriken'
GameObject ‘Shuriken’

Una vez creado este GameObject, vamos a añadirle como objeto hijo un GameObject que representará el sprite referente a nuestro shuriken. Este objeto sólo contendrá el componente ‘SpriteRenderer‘ con la imagen que queramos usar para nuestra arma.

Sub GameObject 'SpriteShuriken'
Sub GameObject ‘SpriteShuriken’
Sprite del shuriken
Sprite del shuriken

Aclaración: El motivo por el cual he usado un objeto hijo independiente para el sprite del shuriken, en vez de añadir directamente el componente ‘SpriteRenderer‘ en el objeto padre, es que siempre es mejor hacerlo así por si después decidimos crearle animaciones y no queremos que afecten al transform de nuestro objeto padre.

Creación del script ‘ArmaArrojadiza.cs’

Ahora vamos a crear el script que se encargará de controlar el comportamiento de nuestro shuriken. En este caso, el comportamiento que he implementado para cuando nuestro shuriken colisione con algo ha sido algo tan simple como comprobar que si la colisión se produce contra un enemigo (presuponemos que nuestros enemigos tienen el tag “Enemigo“), entonces decrementamos en 1 el número de vidas de dicho enemigo. Iré comentando lo que hace todo el script en el mismo código:


using UnityEngine;
using System.Collections;

//Creamos un tipo enumerado para definir la dirección
public enum Direccion { Horizontal, Vertical }

public class ArmaArrojadiza : MonoBehaviour
{
   //Variables públicas
   public Direccion DireccionArma = Direccion.Horizontal;
   public float Velocidad = 50.0F;

   //Variables privadas
   private Rigidbody thisRigidbody;

   void Start ()
   {
      thisRigidbody = GetComponent<Rigidbody>();
   }

   void Update ()
   {
      //Establecemos su velocidad y su dirección
      if (DireccionArma == Direccion.Horizontal)
      {
         //Movemos el arma en horizontal
         thisRigidbody.transform.Translate(new Vector3(Velocidad, 0, 0) * Time.deltaTime);
      }
      else
      {
         //Movemos el arma en vertical
         thisRigidbody.transform.Translate(new Vector3(0, Velocidad, 0) * Time.deltaTime);
      }
   }

   void OnTriggerEnter(Collider other)
   {
      if (other.tag == "Enemigo")
      {
         //Si el ataque colisiona contra un objeto con el tag 'Enemigo', se decrementan las vidas de dicho enemigo
         other.gameObject.GetComponent<ComportamientoEnemigo>().Vidas--;

         //Destruimos el objeto cuando colisione contra un enemigo
         Destroy(gameObject);
      }
   }
}

Aclaración: Damos por supuesta la existencia del script “ComportamientoEnemigo” asociado a cada enemigo que tendrá una variable pública llamada “Vidas” (de tipo ‘int’) donde se almacenará el número de vidas que le queden a dicho enemigo.

Asignar el script ‘ArmaArrojadiza.cs’ al GameObject ‘Shuriken’

Una vez tenemos implementado nuestro script, se lo asignamos al GameObject de nuestro shuriken.

Script 'ArmaArrojadiza.cs' asignado en el GameObject 'Shuriken'
Script ‘ArmaArrojadiza.cs’ asignado en el GameObject ‘Shuriken’

Creación del prefab ‘Shuriken’

Ya tenemos construido por completo el objeto que representará a nuestro shuriken, lo que haremos ahora será simplemente crear un prefab de éste simplemente arrastrando el objeto desde nuestra vista de Hierarchy a la carpeta de prefabs de nuestro proyecto.

Lanzar shurikens desde el jugador

Ya que tenemos preparado nuestro prefab ‘Shuriken’, solo nos faltaría hacer que nuestro jugador, a través de la pulsación de un botón de disparo, sea capaz de lanzar shurikens en cualquiera de las 4 direcciones (arriba, abajo, izquierda o derecha). Para ello modificaremos su script ‘PlayerController.cs‘ que, como dije al principio, suponemos que ya tiene asignado el objeto de nuestro personaje.


using UnityEngine;
using System.Collections;
using System;

public class PlayerController : MonoBehaviour
{
   //Creamos una variable pública donde asignar nuestro prefab 'Shuriken'
   public GameObject ShurikenPrefab;

   void Update ()
   {
      //Si pulsamos el botón 'Fire1'...
      if (ShurikenPrefab != null && Input.GetButtonDown("Fire1"))
      {
         //Accedemos al script 'ArmaArrojadiza.cs' del prefab
         ArmaArrojadiza scriptShuriken = ShurikenPrefab.GetComponent<ArmaArrojadiza>();

         if (Input.GetAxis("Vertical") > 0)
         {
            //Ataque hacia arriba
            scriptShuriken.DireccionArma = Direccion.Vertical;
            scriptShuriken.Velocidad = Math.Abs(scriptShuriken.Velocidad);
         }
         else if (Input.GetAxis("Vertical") < 0)
         {
            //Ataque hacia abajo
            scriptShuriken.DireccionArma = Direccion.Vertical;
            scriptShuriken.Velocidad = -Math.Abs(scriptShuriken.Velocidad);
         }
         else if (Input.GetAxis("Horizontal") > 0)
         {
            //Ataque hacia la derecha
            scriptShuriken.DireccionArma = Direccion.Horizontal;
            scriptShuriken.Velocidad = Math.Abs(scriptShuriken.Velocidad);
         }
         else if (Input.GetAxis("Horizontal") < 0)
         {
            //Ataque hacia la izquierda
            scriptShuriken.DireccionArma = Direccion.Horizontal;
            scriptShuriken.Velocidad = -Math.Abs(scriptShuriken.Velocidad);
         }

         //Creamos una instancia del prefab en nuestra escena, concretamente en la posición de nuestro personaje
         Instantiate(ShurikenPrefab, transform.position, Quaternion.identity);
      }
   }
}

Una última mejora

Tal y como está montado ahora mismo, el sistema de ataque funciona, pero ¿qué ocurre si lanzamos un shuriken y éste no colisiona con ningún enemigo?… pues que seguirá avanzando hasta el infinito y ese GameObject nunca será destruido, por tanto ocuparía memoria y si llegamos a lanzar muchos shurikens podría ser catastrófico para el rendimiento del juego. Para solucionar esto haremos uso de un evento que nos proporciona Unity llamado OnBecameInvisible(), lo he descubierto hace poco y me parece maravilloso. El evento funciona nivel de un objeto que contenga un SpriteRenderer y lo que hace es ejecutar el código que le escribamos dentro cuando el objeto salga del ángulo de visión de nuestra cámara. Entonces, para hacer uso de esto, vamos a crearnos un nuevo script llamado “ShurikenDestroyer.cs” y se lo asignaremos al objeto hijo de nuestro prefab (el que contiene el componente ‘SpriteRenderer‘). Aquí el código del script:


using UnityEngine;
using System.Collections;

public class ShurikenDestroyer : MonoBehaviour
{
   void OnBecameInvisible ()
   {
      //Destruimos el objeto padre cuando salga fuera de la pantalla
      Destroy(transform.parent.gameObject);
   }
}

De este modo, cuando cualquiera de nuestros shurikens salgan de la pantalla, serán destruidos automáticamente.

¡Pues eso es todo amigos! El resultado final será que, cada vez que el jugador pulse el botón “Fire1”, se disparará un shuriken en la dirección que estemos pulsando. Recordemos que la velocidad del shuriken puede ser configurada cuando queramos desde el prefab. A continuación os muestro una captura del resultado final en mi proyecto:

Resultado final
Resultado final

Espero que os haya gustado mi primera publicación sobre desarrollo en Unity3D y que, sobre todo, ¡os haya sido útil! 😉 Cualquier comentario o duda ya saben que pueden escribirme en la sección de comentarios del post, ¡muchas gracias y saludos!

Anuncios

44 comentarios en “Unity: Sistema de ataque a distancia en juego 2D

  1. Cual seria la diferencia en rendimiento, a mi parecer usar un mazo para colocar un clavo es algo exagerado, el Sistema Shuriuken de partículas (sin entrar a legacy) requiere mas memoria por sus drawcall y pool de objetos a diferencia de un simple quad con textura o el primitivo SpriteType (ojo me refiero al tipo primitivo) y ser instanciado dentro de un pool estático, con el mismo scrpit ya sea para reconocer cuando sale de la vista de la cámara, toca el collider, o consume el tiempo de existencia sin haber cumplido su objetivo, esto lo comento ya que puedo hacer pruebas en PC y no tendrá ningún efecto, sin embargo si lo compilo para un Android 2.0 con un Snapdragon 1Ghz o mínimo un adreno con 128 VRAM

    Le gusta a 1 persona

    1. En mi caso no estoy haciendo uso en ningún momento del sistema de partículas “Shuriken” que ofrece Unity, cuando hablo de “shurikens” en este post me refiero a un tipo de arma ninja simplmente 😀 (ahora que lo pienso, quizás el título del post lleve a confusión, no lo había pensado). Cuando hablo de rendimiento simplemente hago referencia a lo que supondría tener muchos objetos instanciados en la escena sin ser destruidos ni desactivados nunca.
      Evidentemente este sistema se podría mejorar haciendo uso de “object pooling“, pero he querido centrarme solo en la mecánica en sí de este sistema de ataque, quizás para un futuro post sería buena idea abordar el tema del “object pooling“.
      Muchas gracias por entrar y leer mi blog, espero que poco a poco pueda ir aportando más y mejores recursos 🙂

      Me gusta

    1. Así es Ulises, podríamos usar sin problema las físicas 2D de Unity 😉

      En mi caso estoy haciendo uso de físicas 3D por a una serie de limitaciones que me surgieron debido a que mi personaje principal está controlado mediante un CharacterController y eso me hizo tener algunos problemas referentes a colisiones entre el personaje y demás objetos 2D. También es cierto que cuando comencé mi proyecto, Unity aún no había lanzado su versión con físicas 2D. Supongo que a día de hoy, para un nuevo proyecto similar, replantearía ciertos aspectos de arquitectura para adaptarlo al sistema 2D de Unity.

      Muchas gracias por tu comentario y por entrar en el blog 🙂

      Me gusta

  2. El evento OnBecameInvisible() me parece interesantisimo, yo tenia que rodear por fuera del marco de visión unos bloques destructores para que al colisionar con ellos los destruyera, pero este evento me parece muchísimo más optimo. Muchas Gracias

    Le gusta a 1 persona

    1. Sí, mi planteamiento inicial fue algo muy similar a lo que tú hiciste, pero este evento nos facilita la vida una barbaridad… me declaro muy fan del OnBecameInvisible() 😀 Por cierto, decir que también existe el evento OnBecameVisible() y cuyo funcionamiento es el mismo pero a la inversa, algo que también puede sernos muy útil.

      Gracias por tu comentario, Miguel Ángel!

      Me gusta

  3. Hola colega, muy bueno tu post, podria contactar contigo por via mensajes? para que me asesore cualqueir dificultad que tenga? en estos momentos realizo un proyecto de un video juego 2D de plataforma, pero como soy diseñador grafico, los script se me dificulta un poco, y preguntar sobre otras cosas 😀 Saludos!

    Le gusta a 1 persona

  4. Buenas noches! Enhorabuena por tu post! Está genial. Estoy programando un juego y necesito que un personaje lance unas bolas (algo parecido a lo que tú haces) así que espero que no te moleste que te “robe” algo de código jeje. Pero tengo una duda. Esta parte:
    //Variables privadas
    private Rigidbody thisRigidbody;

    void Start ()
    {
    thisRigidbody = GetComponent();
    }

    ¿Para qué sirve? Copié todo según estaba para irlo modificando poco a poco pero me daba este error:

    `UnityEngine.Component.GetComponent()’ cannot be inferred from the usage. Try specifying the type arguments explicitly

    ¿Te ha pasado alguna vez?

    Muchas gracias y un saludo!

    Le gusta a 1 persona

    1. Hola Jorge! Acabo de darme cuenta de que escribí mal el código justo en la parte que tienes dudas y que por eso seguramente te esté fallando. Ya lo he corregido en este post.

      Como ves, olvidé escribir “Rigidbody” detrás de “GetComponent”.

      Te explico para qué sirve esto:

      GetComponent se encarga de buscar, entre todos los componentes que tiene asignado nuestro objeto, aquel cuyo tipo le indiquemos. En este caso estamos diciéndole que nos busque el componente de tipo Rigidbody, que es el que se encarga de gestionar las físicas de nuestro objeto y que lo asigne a una variable privada (thisRigidbody) para así poder usarla donde lo necesitemos dentro del script, en este caso para mover el objeto mediante “translate”.

      Espero que te sirva de ayuda!

      Gracias por comentar y por seguir mis posts! 🙂

      Me gusta

  5. Buenas, cuando creo el script de la Arma Arrojadiza me da el siguiente error: Assets/Scripts/ArmaArrojadiza.cs(41,42): error CS0411: The type arguments for method `UnityEngine.GameObject.GetComponent()’ cannot be inferred from the usage. Try specifying the type arguments explicitly

    -Gracias por hacer estos increíbles post 😀

    Le gusta a 1 persona

    1. Hola Joaquín! Gracias por comentar. Me acabo de dar cuenta de que tenía una errata en algunas partes donde usaba la instrucción “GetComponent” (parece ser que WordPress me eliminó partes de código que tenían los signos de menor y mayor), ya he corregido el post. Lee también la aclaración que he añadido al final del apartado “Creación del script ‘ArmaArrojadiza.cs’”. Espero que te ayude a solucionar el problema! Si tienes alguna duda, ya sabes! 😉

      Me gusta

      1. Hola de nuevo Juan, creo que ya sé cual es el problema. Y es que al escribir el post, WordPress me estaba eliminando algunos caracteres de mi código, en concreto los símbolos “mayor” y “menor”. Acabo de corregirlo, prueba ahora el código y me comentas.
        Saludos!

        Me gusta

    1. Hola Juan, según lo que te dicen los errores que te salen, parece que te está detectando como duplicada la declaración del tipo enumerado:

      public enum Direccion { Horizontal, Vertical }

      Ya que está definido tanto en el script de ArmaArrojadiza.cs como en el de AtaquePersonaje.cs. Prueba a definirlo solamente en un sitio a ver.

      Aún así lo miraré en cuanto pueda desde mi PC y te confirmo 😉 Gracias!

      Me gusta

      1. Gracias, pero borrar esa parte del código repetida solo me soluciona 1 error. He probado el script solo sin ningún otro script que interfiera y genera 11 errores el código de por sí

        Me gusta

      2. Gracias ya el único error que da es este “Assets/Scripts/AtaquePersonaje.cs(2,32): error CS0246: The type or namespace name `MonoBehaviour’ could not be found. Are you missing a using directive or an assembly reference?”

        Me gusta

  6. Siento dar tanto el follón Santi jajajaja pero aquí hay un error que creo que podría ser otro fallo de algo del post “MissingComponentException: There is no ‘Rigidbody’ attached to the “Shuriken” game object, but a script is trying to access it.
    You probably need to add a Rigidbody to the game object “Shuriken”. Or your script needs to check if the component is attached before using it.
    UnityEngine.Component.get_transform () (at C:/BuildAgent/work/d63dfc6385190b60/artifacts/EditorGenerated/UnityEngineComponent.cs:20)
    ArmaArrojadiza.Update () (at Assets/Scripts/ArmaArrojadiza.cs:27)”.
    También puede ser perfectamente error mío pero tras revisarlo no estoy muy seguro

    Me gusta

  7. Desgraciadamente, el sistema me da fallos…, me gustaría que contactaramos por alguna red social para tratar el tema más de cerca si no te importa jajajajaja. Te lo agreadecería muchísimo y puede que tú consiguieras perfeccionar el post

    Me gusta

  8. Muy bueno. Solo que yo en mi juego tengo un problema. si el Input.GetAxis(“Horizontal” ) == 0
    mi personaje puede estar mirando hacia la derecha o hacia la izquierda y los disparos se efectuaran en función de hacia donde estaba mirando cuando dispare la ultima vez, entonces hai veces que disparo mirando hacia la derecha y al darme la vuelta y situarme mirando hacia la izquierda el InputGetaxis == 0 , el personaje estará mirando hacía la izquierda pero la bala sale hacia la derecha y hasta que no me muevo mas hacia la izquierda para que InputGetAxis sea <0 no dispara hacia la izquierda 😦
    Sabes como puedo solucionarlo?
    Gracias.

    Le gusta a 1 persona

    1. Hola Sancho, gracias por escribir. El problema que tienes de la dirección a la que mira tu personaje yo lo resolvería guardando en una variable la última dirección a la que mira tu personaje en función de si has pulsado izquierda o derecha en el eje horizontal, pero no actualizar dicha variable en caso de que el eje esté en 0. Es decir, hacer algo así:

      Te declaras esta variable global en la clase de tu script de movimiento del personaje:

      bool jugadorHaciaLaDerecha;
      

      En la función Start() inicializas la variable:

      jugadorHaciaLaDerecha = true;
      

      Y en la función Update():

      if (Input.GetAxis(“Horizontal” ) > 0)
      {
         jugadorHaciaLaDerecha = true;
      }
      else if (Input.GetAxis(“Horizontal” ) < 0)
      {
         jugadorHaciaLaDerecha = false;
      }
      

      De este modo, en la variable “jugadorHaciaLaDerecha” siempre tendrás la dirección correcta a la que mira tu personaje en cada momento.

      Me gusta

  9. Hola que tal un tremendo tuto como siempre, sabes tengo un problema… el comportamiento funciona de maravilla, pero cuando habilito el

    void OnBecameInvisible ()
    {
    Destroy(transform.parent.gameObject);
    }

    la escena se carga… “la flecha” se cae sale de la pantalla… se eliminina (destroy) y ya no la puedo volver a lanzar….

    ahora si quito el destroy… siempre se lanza “la flecha” con el efecto deseado… como hago para tener, el arma “original” en la pantalla… para que siempre se lance???

    Espero haberme explicado bien….

    Gracias de antemano.

    Me gusta

    1. Hola KONERIASKINGDOME, gracias por comentar! 🙂

      Entiendo el problema que tienes, aunque no entiendo por qué te está sucediendo ya que, si el prefab está montado correctamente, debería crearte una instancia nueva de éste cada vez que se ejecuta este código:

      Instantiate(ShurikenPrefab, transform.position, Quaternion.identity);
      

      Tu lanza la tienes creada como un prefab, ¿verdad? Es decir:

      1. Creaste la lanza en la escena.
      2. Una vez creada, la arrastraste a una carpeta de tu proyecto para que se creara su prefab.
      3. Seguidamente borraste la instancia de la lanza que quedó en la escena.
      4. Por último arrastraste el prefab creado desde tu carpeta de proyecto donde lo creaste hasta la variable “ShurikenPrefab” del inspector de tu script.

      Si seguiste estos pasos debería funcionar correctamente. ¿Quizás el problema que estás teniendo es que a la variable “ShurikenPrefab” le asignaste un objeto de la escena en vez del prefab en sí?

      Si sigues teniendo el problema, puedes enviarme tu escena y te lo reviso.

      Saludos!

      Me gusta

      1. Che que Crack!!!!! ese fue el problema lo que le habia asignado en el inspector era el elemento de la escena y no el prefabs desde las carpetas… ya funciona de 10… tienes un lugar reservado en los creditos 😉

        Le gusta a 1 persona

    1. Hola Desiree. El script “ArmaArrojadiza.cs” de este post está preparado para armas que se lanzan a distancia, por tanto no valdría para un arma de tipo “cuerpo a cuerpo” como puede ser una espada, para ello habría que usar otro sistema.

      Si lo que quieres es cambiar de arma arrojadiza conforme vayas encontrándotela por la fase, para implementarlo consistiría en crearte los diferentes prefabs referentes a las diferentes armas que quieras y añadirles en su script de “ArmaArrojadiza.cs” un código que hiciera lo siguiente:

      1. Comprobar cuando nuestro personaje pasa por encima del arma.
      2. Si toca el arma, accedería a la variable pública “ShurikenPrefab” del script que controla al personaje y le cambiaría la referencia del prefab del arma actual a la referencia del prefab de la nueva arma.

      Con algo así ya lo tendrías solucionado.

      No te lo puedo implementar aquí porque no lo he hecho nunca y habría que investigar cómo hacerlo (sobre todo lo de cambiar la referencia del prefab), pero esa sería la idea, no creo que sea muy complejo.

      Respecto a la implementación de un ataque cuerpo a cuerpo, quizás me anime a publicarlo en el blog para un futuro 😉

      Gracias por escribir, saludos!

      Me gusta

Responder

Introduce tus datos o haz clic en un icono para iniciar sesión:

Logo de WordPress.com

Estás comentando usando tu cuenta de WordPress.com. Cerrar sesión / Cambiar )

Imagen de Twitter

Estás comentando usando tu cuenta de Twitter. Cerrar sesión / Cambiar )

Foto de Facebook

Estás comentando usando tu cuenta de Facebook. Cerrar sesión / Cambiar )

Google+ photo

Estás comentando usando tu cuenta de Google+. Cerrar sesión / Cambiar )

Conectando a %s