En este post voy a tratar de mostrarles los conceptos y algunos ejemplos sobre Herencia y Polimorfismo, que son claves a la hora de programar un juego (o cualquier aplicación). Estos conceptos les van a ayudar a tener el juego un poco más estructurado y de fácil lectura y corrección, ya que ese suele ser un gran problema de muchas personas a la hora de programar: La fácil depuración.
CONCEPTOS BÁSICOS DE HERENCIA
DEFINICIÓN:
Es la relación entre una clase general y una más específica. Por ejemplo: Si creamos una clase “Character” y dos clases “MainCharacter” y “NPC”, ambas podrían HEREDAR todas las variables y funciones creadas en la clase “Character”, ya que ambas SON un character.
¿Cuántas veces creamos las clases “Hero”, “Hero2”, “Enemy”, “EnemyWithMorePower”, “Boss” y pensamos: “Fuck… Es el mismo código copypasteado pero estoy agregando 2 ó 3 líneas nuevas”? Pues bien… La programación orientada a objetos viene a solventar este tipo de dramas, entre otros.
Imaginemos el caso más típico:
CLASE “Hero.cs”:
using System.Collections; using System.Collections.Generic; using UnityEngine; public class Hero : MonoBehaviour { public int energy; public bool isDead; public bool isJumping; public GameObject weapon; public int ammoCount; void Start () { } void Update () { } public void Jump() { } public void Attack() { } public void Dead() { } }
Según podemos ver en este simple script, todo este contenido bien podría encajar tanto para cualquiera de nuestros enemigos, como para el boss, o para un segundo o tercer jugador. Quizás en algún enemigo o boss agregaríamos un par de cosas más. El punto es que si lo hacemos sin tener un conocimiento de herencia muy probablemente caigamos en copiar y pegar el mismo contenido en diferentes scripts. Esto hace que muchas veces el código de uno falle y el otro no al agregar cosas y se nos haga mucho más tediosa la búsqueda del error. Además de que tomamos un camino más tedioso.
Podríamos definir la herencia de muchas maneras, pero creo que una entendible sería la “construcción de un objeto a partir de otro ya existente”. A lo que me refiero es que podemos programar sólo una vez una clase que contenga todo lo “común” al resto de clases que son similares y hacer que el resto tome los valores desde ESA clase.
Tomemos como ejemplo Dark Souls. Hay un sin fin de personajes. ¿Se imaginan si tuvieramos que programar CADA UNO de ellos? Veamos una especie de diagrama de cómo más o menos serían los scripts:
Cada una de las clases que vemos en el diagrama corresponden a un personaje diferente, y he aquí la cuestión: TODOS son personajes, y por eso tenemos arriba de todo la clase “Character”. Las demás clases Heredan de esta. Esto significa que el resto de las clases adoptan todas las cosas que programamos y automáticamente pasan a ser TAMBIÉN de ellas.
O sea que ahora nuestra clase “Hero” va a tener TODAS las propiedades que contiene “Character”, y se podría decir que HEREDAN esta clase. Si lo leemos de otra manera, vemos que nuestro player ES un “Hero”, que a la ves ES un “Character”.
Pareciera un quilombo total. Pero la verdad es que es mucho más sencillo y práctico. Intentemos verlo de una manera más sencilla.
Si “Hero” y “Enemy” SON un “Character” y tienen las mismas cosas que este… ¿Para qué programarlos de nuevo? Directamente hagamos que HEREDEN las propiedades de “Character”. Siguiendo el mismo concepto… Si “ChaosWitch” es una “Wizard”, que a su vez es un “Enemy”, que a su vez es un “Character”… ¿Por qué no hacer que HEREDE las propiedades de estos?
Ahora… La pregunta del millón: ¿Cómo carajos implemento esto en mi proyecto? Bueno… La realidad es que ya lo implementaste muchas veces sin darte cuenta. Veamos dónde:
Cuando creamos una clase en Unity (o comúnmente llamado “script”), tiene más o menos esta forma:
using System.Collections; using System.Collections.Generic; using UnityEngine; public class Enemy : MonoBehaviour { //Se ejecuta al inicializar... void Start () { } //Se llama al pintar la pantalla... void Update () { } }
PREGUNTA RÁPIDA: ¿De qué clase hereda Enemy?
Si queda clara la respuesta y el “por qué”, entonces ya quedará claro también cómo hacer que nuestra clase “Hero”, herede de “Character” ¿Verdad?
Si no es así, veamos un ejemplo a continuación:
using System.Collections; using System.Collections.Generic; using UnityEngine; public class Hero : Character { //Se ejecuta al inicializar... void Start () { } //Se llama al pintar la pantalla... void Update () { } }
Con esto ahora podemos crear todo lo común a TODOS los personajes en un solo lugar (la clase “Character”), y luego heredar las clases que compartan sus funciones (también llamados “métodos”), o mejor dicho: heredar las clases que también SEAN characters.
PREGUNTA RÁPIDA: Así como quedó ahora ¿La clase “Hero” puede ser agregada a un GameObject como componente?
POLIMORFISMO ¿QUÉ ES Y CON QUÉ SE COME?
DEFINICIÓN:
Se refiere a la capacidad que tienen algunos objetos de responder de maneras diferentes ante los llamados a los mismos métodos. Por ejemplo, si yo llamo al método “Shoot()” de una clase “Sniper” va a disparar de manera muy diferente que si llamo al método “Shoot()” de una clase “Robot”. Quizás el sniper elija sacar un rifle y el robot elija sacar una minigun de su brazo. Ambos realizan la opción de DISPARAR pero también ambos lo hacen de manera DIFERENTE.
Algo muy importante a tener en cuenta, más que nada porque así lo van a encontrar en la gran mayoría de páginas y demás, es que a las funciones se les dice “métodos”. Esto es más que nada una jerga de la programación orientada a objetos. Vamos a llamar así a las funciones de ahora en más para acostumbrarnos.
Para comenzar con este tema, vamos a crear nuevamente la clase “Character”, de la siguiente manera:
using System.Collections; using System.Collections.Generic; using UnityEngine; public class Character : MonoBehaviour { public bool isDead; public int ammoCount; public void Shoot() { //Si no está muerto y tiene balas... //Crear bullet. //Disparar bullet. //Restar ballas. } }
Teniéndolo de esta manera, lo que va a pasar es que cualquier clase que herede directamente de “Character”, va a disparar EXACTAMENTE de la misma manera que todos. En este caso, como no es eso lo que querémos, vamos a hacer un pequeño cambio, que consiste en agregar la palabra “virtual“. Esto le va a decir al compilador que el contenido de este método (palabra clave, guiño, guiño), PODRÍA llegar a ejecutar código diferente al ser llamado.
using System.Collections; using System.Collections.Generic; using UnityEngine; public class Character : MonoBehaviour { public bool isDead; public int ammoCount; virtual public void Shoot() { //Si no está muerto y tiene balas... //Crear bullet. //Disparar bullet. //Restar ballas. } }
Entonces, al agregar esta palabra, permitimos que cualquier clase que herede de “Character” pueda sobreescribir la acción que realiza el método “Shoot()” y permitir con esto que cada clase pueda optar por pisarlo y escribir su propio funcionamiento. Por ejemplo, imaginemos que la clase “Robot” no dispara de la misma manera que el resto de los characters. En tal caso, cuando vamos a SOBREESCRIBIR la función “Shoot()”, debemos declararla nuevamente en en la clase “Robot” solamente que le agregamos, en lugar de “virtual“, la palabra clave “override“. Nos quedaría de la siguiente manera:
using System.Collections; using System.Collections.Generic; using UnityEngine; public class Robot : Character { override public void Shoot() { } }
TIP:
Acá, al igual que en el caso de virtual, es lo mismo poner “override public” que “public override”. No importa si la palabra “override” o “virtual” va antes o después de “public”. De hecho, si nosotros escribimos en Visual Studio “override” nos despliega una lista de funciones QUE PODEMOS sobreescribir. Al confirmar la que queremos Visual Studio pone “public override”. Hago esta aclaración para que después no se asusten o desesperen si el compilador les cambia el orden.
Quizás nuestro robot antes de disparar tenga que desplegar el arma y nosotros tengamos que esperar hasta que termine de hacerlo para poder disparar. En ese caso, es muy útil sobreescribir la función original para que ESPECÍFICAMENTE EL ROBOT dispare de otra manera. Podría solamente poner una variable en true y esperar a que termine de desplegar, luego, llamar a “Shoot()”. Un ejemplo sería el siguiente:
using System.Collections; using System.Collections.Generic; using UnityEngine; public class Robot : Character { public bool isDeploying; public bool isReadyToShoot; public override void Shoot() { isDeploying = true; } private void Update() { if(isReadyToShoot) { //Si no está muerto... //Crear bullet. //Disparar bullet. //Restar ballas. //¡Shoot to Fleitas! } } }
En este ejemplo, nuestro robot esperaría a terminar de desplegar el arma (por supuesto, esto es pseudocódigo). Si luego de desplegar el arma, para hacer que el robot dispare nos encontramos en la situación de que copypasteamos exactamente el mismo código de la clase “Character”, ¡TRANQUILOS! Esto se arragla muy fácil con un pequeño cambio:
using System.Collections; using System.Collections.Generic; using UnityEngine; public class Robot : Character { public bool isDeploying; public bool isReadyToShoot; public override void Shoot() { isDeploying = true; } private void Update() { if(isReadyToShoot) { base.Shoot(); } } }
¿Notan el cambio?
¿Qué hace la palabra clave “base“?
Si recuerdan la palabra clave “this“, que se usa bastante, la palabra clave “base” tiene un funcionamiento similar. “this” hace referencia a “ESTA” clase, o sea a la clase en la cual se use. Por ejemplo, si uso “this” en la clase “Robot”, se va a referir a la instancia de “Robot”, si la uso en “Character”, se refiere a la instancia de “Character”. En cambio, la palabra clave “base” se refiere a la instancia DEL PADRE, o sea, a la instancia de la clase de la cual HEREDA.
PREGUNTA:
Si escribo “base.Shoot()” ¿A cuál de las dos funciones “Shoot()” me estoy refiriendo?
UNA VARIABLE, MUCHOS TIPOS… ¿O NO?
Esto es un concepto que al principio puede parecer raro, pero voy a arrancar con una pregunta.
PREGUNTA:
Si creo una variable de tipo “Character” ¿Qué tipos de datos puedo guardar dentro?
La respuesta es sencilla: CUALQUIER OBJETO QUE HEREDE DE “Character”. Pero… ¿Por qué?
Vamos a suponer lo siguiente: Si yo pongo un recipiente con golosinas y les digo: “En este recipiente SOLAMENTE pueden poner GOLOSINAS”. Suponiendo esto, ustedes podrían poner: Caramelos, Alfajores, Chicles y cualquier cosa que SEA una GOLOSINA. Incluso, un Flinn Paff, porque es un CARAMELO, y también una GOLOSINA. Dicho de otra manera, la clase “FlinnPaff” hereda de la clase “Caramelo” que a su vez hereda de la clase “Golosina”. Por eso pueden ponerlo en el recipiente. Si ustedes pusieran un gato, después de que les dije que sólo pongan golosinas, el compilador (o sea yo en este caso), les tiraría error.
Un ejemplo más práctico de esto es en videojuegos, ya que en un juego en el cual tenemos dos personajes, que pueden ser cualquiera de los 20 o 30 que podemos llegar a tener, podríamos en tal caso crear dos variables de tipo “Character” y poner dentro a CUALQUIER personaje, ya que todos heredarían de “Character”.
Esta práctica nos facilita mucho la programación, porque al tocar una tecla determinada, nosotros podemos decirle a la variable “player1”, que puede ser de tipo “Character” que dispare SIN IMPORTAR A CUÁL PERSONAJE TENGA DENTRO ALOJADO. Por ejemplo:
public Character player1; public Character player2;
Luego, en otra parte del código, yo podría escribir:
if (Input.GetKey(KeyCode.Space)) { player1.Shoot(); }
Sin importar cuál de todos mis personajes esté dentro. Y con esto logramos hacer más “genérico” y reutilizable nuestro código.
Por supuesto, lo opuesto a esto no se puede hacer por razones lógicas. Si yo les digo: “Pongan en el recipiente SOLAMENTE Flinn Paff”. Ustedes no pueden poner CUALQUIER golosina, sino SOLAMENTE Flinn Paff. Si ustedes crearan una variable de tipo “FlinnPaff” únicamente podrían poner en ella objetos que sean de tipo “FlinnPaff” para abajo, o sea, que hereden de “FlinnPaff”.
TIPOS DE ATRIBUTOS
Bueno… Esta parte es bastante más tranquila. Esto influye más que nada al momento de programar en grupo para evitar que otro se manda alguna cagada con el código principal.
Por lo general, hay un programador que hace una base del proyecto, y otro u otros que trabajan SOBRE esa base. Es como una empresa, con distintos sectores. De esta manera es más fácil culpar al verdadero responsable. ;).
Los atributos son:
public
Cualquier parte del código puede tener acceso a esta variable, método, etc.
private
Cualquier variable, método, etc creada con este atributo va a significar que SOLAMENTE la clase que la creo la puede acceder. Esto significa que si la clase “Robot” creo una variable privada llamada “myParts”, nadie más aparte de esta clase va a poder ver o acceder a dicha variable.
protected
Al usar este atributo lo que estamos diciendo es que cualquier esta clase, y cualquiera que herede de ella, puede acceder a la variable, método, etc. O sea que si creo algo con el atributo “protected” en la clase “Character”, ese algo va a poder ser visto por la misma clase, y por todas las que hereden de ella. Se podría decir que: “queda todo entre familia”.
internal
Esto significa que pueden tener acceso a la variable, método, etc todas las clases que estén dentro del mismo “namespaces“. Si no ponemos ningún atributo, predeterminadamente este sería el que elije el compilador. En Unity3D sólo existe un “namespaces“, así que da lo mismo usarlo o no. Es más util al programar sobre otra plataforma como C# para Escritorio o Formularios.
EXTRA
Esto lo pongo a modo de extra, ya que no se usa demasiado. Pero para mí es bastante útil.
Si nosotros creamos una variable en una clase base (a esta altura ya debería haber quedado claro qué es una clase base), de un tipo de dato cualquiera, podemos hacer que esa misma variable, al pasar a sus hijos, cambie de tipo. Les pongo el ejemplo del caso en el cual yo uso esto. Imaginemos la clase “Character”:
using System.Collections; using System.Collections.Generic; using UnityEngine; public class Character : MonoBehaviour { public bool isDead; public int ammoCount; virtual public void Shoot() { //Si no está muerto... //Crear bullet. //Disparar bullet. //Restar ballas. } }
Solamente por heredar de MonoBehaviour, ya tiene un montón de variables y métodos dentro que quizás algunos no nos interesan porque Unity3D ya no los usa (de hecho tira error si los queremos usar). Por ejemplo, si tenemos un componente Rigidbody en nuestro GameObject, y guardarlo en una variable llamada “rigidbody”, nos va a tirar error porque la variable ya existe en la clase base “MonoBehaviour”. Pero si la intentamos usar, nos tira error porque dice que está en desuso. Pues bien, yo quiero guardar mi componente dentro de una variable llamada así y Unity3D no debería decirme nada. Así que hacemos lo siguiente:
using System.Collections; using System.Collections.Generic; using UnityEngine; public class Character : MonoBehaviour { public bool isDead; public int ammoCount; //Al crear la variable de esta manera la "ocultamos". //No la borramos, no deja de existir en la clase base. new public Rigidbody rigidbody; private void Start() { rigidbody = this.GetComponent<Rigidbody>(); } virtual public void Shoot() { //Si no está muerto... //Crear bullet. //Disparar bullet. //Restar ballas. } }
Es una practica muy común, de hecho como verán también Unity3D lo hace, el crear variables que se llamen igual que su tipo, pero en minúsculas. De hecho pueden ver que por heredar de MonoBehaviour tienen variables como: gameObject, rigidbody, rigidbody2D, renderer, etc, cuyos tipos de datos son: GameObject, Rigidbody, Rigidbody2D, Renderer, etc respectivamente.
CUESTIONARIO
Cuestionario rápido sobre lo aprendido en el post. Esto es más que nada para un breve repaso. Algunas preguntas tienen más de una respuesta correcta, pero con que se marque una está bien. Al final se muestra cuántas se respondieron bien.