En este tutorial se explican algunos de los patrones de programación en Unity con C#, esta es la explicación que da Wikipedia sobre que es un patrón de programación para los que no esten relacionados con el termino:
En ingenieria de software, un patrón de diseño de software es una solución reusable para un problema que ocurre comunmente en un contexto especifico en el diseño de software. No es un diseño final que puede ser transformado directamente en código o lenguaje maquina. Es una descripción o template sobre como podemos resolver un problema, el cual puede ser usado en diferentes situaciones. Los patrones de diseño son buenas practicas que un programador puedes usar par resolver un problema común cuando de diseña una aplicación o un sistema.
En programación orientada a objetos, el Command Pattern es un patrón de diseño de comportamiento en el cual un objeto es usado para encapsular toda la información necesaria para ejecutar una acción o activar algún evento en otro momento. Esta información incluye el nombre del método, el objeto que es propietario del método y los valores de los parametros del método.
Un ejemplo donde el Command Pattern es realmente útil, es cuando le damos la opción a nuestros usuarios de cambiar los controles del juego, en Unity podemos escribir una función similar a esta:
if(Input.GetKeyDown(KeyCode.A)){FireWeapon();}Que podemos hacer si el usuario quiera disparar su arma presionado la tecla U en lugar de la A? Para hacer que esto pase, debemos buscar la forma de reemplazar FireWeapon(); con algo más general, por esto es que necesitamos el Command Pattern. Iniciamos definiendo una clase base llamada Command.
publicabstractclassCommand{publicabstractvoidExecute();}Luego FireWeapon hereda de la clase base. Necesitamos una clase que no haga nada, de esta forma podemos hacer el Switch entre la U y la A cuando se dispare el arma.
publicclassFireWeapon:Command{publicoverridevoidExecute(){FireTheWeapon();}}publicclassDoNothing:Command{publicoverridevoidExecute(){}}Como estaba antes nuestro código solo podiamos disparar nuestra arma presionando la tecla A, ahora facilmente podemos reemplazar FireWeapon(), con un Execute() que es más general presionando la tecla U. De esta forma haciendo uso de una interfaz podemos seleccionar una nueva configuración y en lugar de usar el Button A usamos el Button U, lo que hace mucho más facíl, que usar el FireWeapon al presionar una tecla determinada o fija.
CommandbuttonU=newFireWeapon();CommandbuttonA=newDoNothing();if(Input.GetKeyDown(KeyCode.U)){buttonU.Execute();//Ahora podemos llamar el método FireTheWeapon() en el método Execute() en la clase FireWeapon }if(Input.GetKeyDown(KeyCode.A)){buttonA.Execute();//No hace nada por que el método Execute() en la clase DoNothing esta vacio.}Pero esto no es todo, Si estamos salvando los commands, estamos enviandolos a una lista, entonces podemos deshacer los commands. Digamos por ejemplo que tenemos una editor de niveles, en el cual estamos poniendo arboles. Luego podemos usar el Command Pattern para deshacer el ultimo o todos los comandos en el caso que ubiquemos alguno en una posición erronea.
Al usar la misma lista cuando deshacemos algo podemos crear una función que haga un replay, si recordamos la posición inicial, simplemente recorremos los commands que salvamos y Unity va a reproducir todo lo que hicimos. Esta es una buena forma de salvar la posición y la orientación en cada frame de todos nuestros objetos en la escena, luego repetir las posiciones haciendo un loop en la lista.
La idea en este script es que tenemos un box gameobject, luego podemos mover la caja usando las teclas WASD, pero gracias al command pattern podemos deshacer los movimientos con la tecla Z, luego reproducir todos los movimientos desde el principio con la tecla R.
Lo que se necesita en la escena es una caja y un objeto vacio, agregamos el script InputHandler.cs al objeto vacio y agregamos la caja al espacio definido en el script. Con esto debe funcionar el ejemplo.
usingUnityEngine;usingSystem.Collections;usingSystem.Collections.Generic;namespaceCommandPattern{publicclassInputHandler:MonoBehaviour{//The box we control with keyspublicTransformboxTrans;//The different keys we needprivateCommandbuttonW,buttonS,buttonA,buttonD,buttonB,buttonZ,buttonR;//Stores all commands for replay and undopublicstaticList<Command>oldCommands=newList<Command>();//Box start position to know where replay beginsprivateVector3boxStartPos;//To reset the coroutineprivateCoroutinereplayCoroutine;//If we should start the replaypublicstaticboolshouldStartReplay;//So we cant press keys while replayingprivateboolisReplaying;voidStart(){//Bind keys with commandsbuttonB=newDoNothing();buttonW=newMoveForward();buttonS=newMoveReverse();buttonA=newMoveLeft();buttonD=newMoveRight();buttonZ=newUndoCommand();buttonR=newReplayCommand();boxStartPos=boxTrans.position;}voidUpdate(){if(!isReplaying){HandleInput();}StartReplay();}//Check if we press a key, if so do what the key is binded to publicvoidHandleInput(){if(Input.GetKeyDown(KeyCode.A)){buttonA.Execute(boxTrans,buttonA);}elseif(Input.GetKeyDown(KeyCode.B)){buttonB.Execute(boxTrans,buttonB);}elseif(Input.GetKeyDown(KeyCode.D)){buttonD.Execute(boxTrans,buttonD);}elseif(Input.GetKeyDown(KeyCode.R)){buttonR.Execute(boxTrans,buttonZ);}elseif(Input.GetKeyDown(KeyCode.S)){buttonS.Execute(boxTrans,buttonS);}elseif(Input.GetKeyDown(KeyCode.W)){buttonW.Execute(boxTrans,buttonW);}elseif(Input.GetKeyDown(KeyCode.Z)){buttonZ.Execute(boxTrans,buttonZ);}}//Checks if we should start the replayvoidStartReplay(){if(shouldStartReplay&&oldCommands.Count>0){shouldStartReplay=false;//Stop the coroutine so it starts from the beginningif(replayCoroutine!=null){StopCoroutine(replayCoroutine);}//Start the replayreplayCoroutine=StartCoroutine(ReplayCommands(boxTrans));}}//The replay coroutineIEnumeratorReplayCommands(TransformboxTrans){//So we can't move the box with keys while replayingisReplaying=true;//Move the box to the start positionboxTrans.position=boxStartPos;for(inti=0;i<oldCommands.Count;i++){//Move the box with the current commandoldCommands[i].Move(boxTrans);yieldreturnnewWaitForSeconds(0.3f);}//We can move the box againisReplaying=false;}}}Este scriipt recoge todos los comandos que necesitamos, es posible que sea necesario tener una clase por cada script cuando estamos creando nuestro propio command pattern.
usingUnityEngine;usingSystem.Collections;usingSystem.Collections.Generic;namespaceCommandPattern{//The parent classpublicabstractclassCommand{//How far should the box move when we press a buttonprotectedfloatmoveDistance=1f;//Move and maybe save commandpublicabstractvoidExecute(TransformboxTrans,Commandcommand);//Undo an old commandpublicvirtualvoidUndo(TransformboxTrans){}//Move the boxpublicvirtualvoidMove(TransformboxTrans){}}//// Child classes//publicclassMoveForward:Command{//Called when we press a keypublicoverridevoidExecute(TransformboxTrans,Commandcommand){//Move the boxMove(boxTrans);//Save the commandInputHandler.oldCommands.Add(command);}//Undo an old commandpublicoverridevoidUndo(TransformboxTrans){boxTrans.Translate(-boxTrans.forward*moveDistance);}//Move the boxpublicoverridevoidMove(TransformboxTrans){boxTrans.Translate(boxTrans.forward*moveDistance);}}publicclassMoveReverse:Command{//Called when we press a keypublicoverridevoidExecute(TransformboxTrans,Commandcommand){//Move the boxMove(boxTrans);//Save the commandInputHandler.oldCommands.Add(command);}//Undo an old commandpublicoverridevoidUndo(TransformboxTrans){boxTrans.Translate(boxTrans.forward*moveDistance);}//Move the boxpublicoverridevoidMove(TransformboxTrans){boxTrans.Translate(-boxTrans.forward*moveDistance);}}publicclassMoveLeft:Command{//Called when we press a keypublicoverridevoidExecute(TransformboxTrans,Commandcommand){//Move the boxMove(boxTrans);//Save the commandInputHandler.oldCommands.Add(command);}//Undo an old commandpublicoverridevoidUndo(TransformboxTrans){boxTrans.Translate(boxTrans.right*moveDistance);}//Move the boxpublicoverridevoidMove(TransformboxTrans){boxTrans.Translate(-boxTrans.right*moveDistance);}}publicclassMoveRight:Command{//Called when we press a keypublicoverridevoidExecute(TransformboxTrans,Commandcommand){//Move the boxMove(boxTrans);//Save the commandInputHandler.oldCommands.Add(command);}//Undo an old commandpublicoverridevoidUndo(TransformboxTrans){boxTrans.Translate(-boxTrans.right*moveDistance);}//Move the boxpublicoverridevoidMove(TransformboxTrans){boxTrans.Translate(boxTrans.right*moveDistance);}}//For keys with no bindingpublicclassDoNothing:Command{//Called when we press a keypublicoverridevoidExecute(TransformboxTrans,Commandcommand){//Nothing will happen if we press this key}}//Undo one commandpublicclassUndoCommand:Command{//Called when we press a keypublicoverridevoidExecute(TransformboxTrans,Commandcommand){List<Command>oldCommands=InputHandler.oldCommands;if(oldCommands.Count>0){CommandlatestCommand=oldCommands[oldCommands.Count-1];//Move the box with this commandlatestCommand.Undo(boxTrans);//Remove the command from the listoldCommands.RemoveAt(oldCommands.Count-1);}}}//Replay all commandspublicclassReplayCommand:Command{publicoverridevoidExecute(TransformboxTrans,Commandcommand){InputHandler.shouldStartReplay=true;}}}Un Patrón Flyweight es un objeto que minimiza el uso de la memoria, compartiendo la mayor cantidad de datos posibles con otros objetos similares, es una forma de usar objetos en gran cantidad, cuando una simple representación repetida utiliza una cantidad inaceptable de memoria.
La idea básica es que, si tenemos una gran cantidad de objetos en nuestro juego, existe una gran probabilidad que se pueda incrementar el rendimiento de nuestro juego, haciendo estos objetos mucho más livianos. Para que esto funcione nuestros objetos deben compartir la mayor cantidad de datos que son similares para todos los objetos. Si ellos comparten algún dato, entonces podemos crear el dato una sola vez y luego almacenar una referencia a este dato en el objeto. Puede sonar un poco complicado pero es algo sencillo.
Un ejemplo en el cual se puede aplicar el uso de este patrons es si estamos creando un nivel y agregamos árboles, cada árbol es igual que los otros, hay algo que comparten en común, tienen la misma maya y la misma textura, lo único que no comparten es la posición.
Ahora veamos como implementar el patrón Flyweight, para este ejemplo utilizamos Aliens con miles de ojos, brazos y piernas, para esto necesitamos una clase alien que describa el alien.
usingSystem.Collections;usingSystem.Collections.Generic;usingUnityEngine;namespaceFlyweightPattern{//Class that includes lists with position of body partspublicclassAlien{publicList<Vector3>eyePositions;publicList<Vector3>legPositions;publicList<Vector3>armPositions;}}Creamos los aliens y generamos nuevas posiciones para cada parte del cuerpo de cada Alien.
usingSystem.Collections;usingSystem.Collections.Generic;usingUnityEngine;//Flyweight design pattern main classnamespaceFlyweightPattern{publicclassFlyweight:MonoBehaviour{//The list that stores all aliensList<Alien>allAliens=newList<Alien>();List<Vector3>eyePositions;List<Vector3>legPositions;List<Vector3>armPositions;voidStart(){//List used when flyweight is enabledeyePositions=GetBodyPartPositions();legPositions=GetBodyPartPositions();armPositions=GetBodyPartPositions();//Create all aliensfor(inti=0;i<10000;i++){AliennewAlien=newAlien();//Add eyes and leg positions//Sin el flyweightnewAlien.eyePositions=GetBodyPartPositions();newAlien.armPositions=GetBodyPartPositions();newAlien.legPositions=GetBodyPartPositions();//Con el flyweight//newAlien.eyePositions = eyePositions;//newAlien.armPositions = legPositions;//newAlien.legPositions = armPositions;allAliens.Add(newAlien);}}//Generate a list with body part positionsList<Vector3>GetBodyPartPositions(){//Create a new listList<Vector3>bodyPartPositions=newList<Vector3>();//Add body part positions to the listfor(inti=0;i<1000;i++){bodyPartPositions.Add(newVector3());}returnbodyPartPositions;}}}Para ver lo que hace nuestro código y el patrón que estamos implementando, agregamos el Flyweight.cs a un objeto vacío, presionamos play en Unity y abrimos el profiler, luego vamos a la sección Memory, vamos a ver la siguiente imagen.
La parte donde debemos poner atención es Mono, como podemos leer en este link, Unity explica que es el “total heap size and used heap size used by Managed Code - this memory is garbage collected” actualmente está más o menos en 484.1MB, este número es el que vamos a reducir usando el Flyweight Pattern.
Para agregar el Flyweight Pattern, asumimos que cada Alien tiene la misma posición para cada una de las partes de su cuerpo, puede que no sea realista, pero lo que estamos haciendo es una prueba sobre como funciona este patrón, para ver su funcionamiento comentamos la sección que dice “Sin el flyweight: y descomentamos la que dice “Con flyweight”, podemos ver el ejemplo en la siguiente imagen.
Luego de hacer esto, ejecutamos de nuevo nuestro código en Unity, abrimos el profiler y vamos de nuevo a la sección Memory, revisamos de nuevo donde dice Mono, y podemos ver como se redujo considerablemente a 14.3 MB.
El patrón Observer es un patrón de diseño de software en el cual un objeto, llamado subject, mantiene una lista de sus dependientes, llamados observers, estos son notificados automáticamente cuando algún estado cambia, usualmente llamando alguno de sus métodos.
La idea básicamente es que podemos usar el observer pattern cuando necesitamos que muchos objetos reciban una actualización cuando otro objeto cambia. El observer pattern es actualmente la columna vertebral de muchos programas y aplicaciones, por eso es importante conocerlo.
Uno de los ejemplos es cuando tenemos logros o achievements en nuestro juego, esto puede ser un poco complicado para implementar si tenemos muchos logros y cada uno se desbloquea por medio de un comportamiento diferente, pero es mucho más fácil de implementar si conocemos el observer pattern.
Para ver el funcionamiento del Observer Pattern en Unity, tenemos tres boxes que van a saltar cuando una esfera esté cerca del centro del mapa.
Cada caja debe tener un rigidbody agregado para poder que salte, luego necesitamos un objeto vacío llamado _GameController que va a contener el Script GameController que va estar pendiente de todo.
El script del GameController se ve de la siguiente forma:
usingSystem.Collections;usingSystem.Collections.Generic;usingUnityEngine;namespaceObserverPattern{publicclassGameController:MonoBehaviour{publicGameObjectsphereObj;//The boxes that will jumppublicGameObjectbox1Obj;publicGameObjectbox2Obj;publicGameObjectbox3Obj;//Will send notifications that something has happened to whoever is interestedSubjectsubject=newSubject();voidStart(){//Create boxes that can observe events and give them an event to doBoxbox1=newBox(box1Obj,newJumpLittle());Boxbox2=newBox(box2Obj,newJumpMedium());Boxbox3=newBox(box3Obj,newJumpHigh());//Add the boxes to the list of objects waiting for something to happensubject.AddObserver(box1);subject.AddObserver(box2);subject.AddObserver(box3);}voidUpdate(){//The boxes should jump if the sphere is cose to origoDebug.Log((sphereObj.transform.position).magnitude);if((sphereObj.transform.position).magnitude<7f){subject.Notify();}}}}El observer pattern básico se compone en dos partes:
- El Subject contiene la lista de todos los observers interesados en obtener información cuando algo sucede, cuando ocurre algún evento, este envía la información a todos los interesados.
- Los observers son los objetos interesados en hacer algo cuando el evento ocurre.
El siguiente script es la clase subject que envía la información a los observers:
usingSystem.Collections;usingSystem.Collections.Generic;usingUnityEngine;namespaceObserverPattern{//Invokes the notificaton methodpublicclassSubject{//A list with observers that are waiting for something to happenList<Observer>observers=newList<Observer>();//Send notifications if something has happenedpublicvoidNotify(){for(inti=0;i<observers.Count;i++){//Notify all observers even though some may not be interested in what has happened//Each observer should check if it is interested in this eventobservers[i].OnNotify();}}//Add observer to the listpublicvoidAddObserver(Observerobserver){observers.Add(observer);}//Remove observer from the listpublicvoidRemoveObserver(Observerobserver){}}}usingSystem.Collections;usingSystem.Collections.Generic;usingUnityEngine;namespaceObserverPattern{//Wants to know when another object does something interesting publicabstractclassObserver{publicabstractvoidOnNotify();}publicclassBox:Observer{//The box gameobject which will do somethingGameObjectboxObj;//What will happen when this box gets an eventBoxEventsboxEvent;publicBox(GameObjectboxObj,BoxEventsboxEvent){this.boxObj=boxObj;this.boxEvent=boxEvent;}//What the box will do if the event fits it (will always fit but you will probably change that on your own)publicoverridevoidOnNotify(){Jump(boxEvent.GetJumpForce());}//The box will always jump in this casevoidJump(floatjumpForce){//If the box is close to the groundif(boxObj.transform.position.y<2f){boxObj.GetComponent<Rigidbody>().AddForce(Vector3.up*jumpForce);}}}}usingSystem.Collections;usingSystem.Collections.Generic;usingUnityEngine;namespaceObserverPattern{//EventspublicabstractclassBoxEvents{publicabstractfloatGetJumpForce();}publicclassJumpLittle:BoxEvents{publicoverridefloatGetJumpForce(){return30f;}}publicclassJumpMedium:BoxEvents{publicoverridefloatGetJumpForce(){return60f;}}publicclassJumpHigh:BoxEvents{publicoverridefloatGetJumpForce(){return90f;}}}Para configurar toda la escena y que funcione agregamos el script _GameController a un empty, luego agregamos al _GameController en el espacio de las variables definidas, la esfera y los tres cubos. Cada uno de los scripts que están en el ejemplo deben estar agregados en Unity, no se deben agregar a ninguno de los elementos en la escena, si vemos el código del _GameController el realiza esta tarea.
Luego ejecutamos y movemos en la escena la esfera, hasta que su magnitud sea menor de 7, para permitir que se muevan los cubos, podemos verificar en la consola el valor de la magnitud de la esfera para saber en que valor esta y poder activar el movimiento de los cubos.
El State Pattern, es una patrón de diseño de Software de comportamiento, este patrón es usado en programación para encapsular un comportamiento variable para el mismo objeto, basado en su estado interno.
La idea del patrón es que lo que estamos tratando de controlar tiene diferentes estados, si tenemos un carro este se puede manejar, frenar, reversar o puede esta parqueado, para escribir este comportamiento de un carro en código no es muy sencillo como se pueda pensar. Si el carro avanza al frente y frenamos, luego no queremos que reverse si el botón del freno y la reversa son el mismo. Qué sucede si estamos reversando y presionando el botón para ir adelante? Debemos frenar o acelerar? Que pasa si tenemos más de 4 estados diferentes? PAra esto necesitamos un state pattern que elimine todos los if else anidado y cada uno de esos estados con un montón de booleanos.
Lo primero que debemos preguntarnos es: Cuáles son todos los posibles estados o situaciones en las cuales el objeto se puede encontrar? La idea de dividir el comportamiento en diferentes estados en los que el objeto puede estar es llamado Finite State Machine o FSM. El objeto puede solamente estar en un estado a la vez, no podemos acelerar y presionar el freno al mismo tiempo.
Es muy común usar enums cuando estamos codificando una máquina de estados en C#, el siguiente código puede dar un ejemplo sobre nuestro carro.
enumCarFSM{Forward,Brake,Reverse,Park}//Default stateCarFSMcarMode=CarFSM.Park;//And then you change the state with something likeif(carMode==CarFSM.Park&&IsPressingGasPedal()){carMode=CarFSM.Forward;}elseif(carMode==CarFSM.Forward&&IsPressingBrakePedal()){carMode==CarFSM.Brake;}//...and so onPara ilustrar el State Pattern vamos a crear dos criaturas, un Skeleton y un Creeper, el Skeleton atacará desde una distancia con un arco y una flecha, el Creeper atacará cuando está cerca, si el jugador está lejos de ambos enemigos, van a caminar en direcciones aleatorias esperando que su objetivo está cerca.
Para implementar el State Pattern vamos a crear lo siguiente:
- Un plano que va servir como terreno para los enemigos y el jugador.
- Una esfera que va ser el jugador que quieren eliminar los enemigos.
- Una caja verde que va a simbolizar al Creeper.
- Una caja blanca que simboliza el Skeleton.
- Un objeto vacío llamado _GameController.
La escena se debe ver de la siguiente forma:
El Script del GameController se ve así:
usingSystem.Collections;usingSystem.Collections.Generic;usingUnityEngine;namespaceStatePattern{publicclassGameController:MonoBehaviour{publicGameObjectplayerObj;publicGameObjectcreeperObj;publicGameObjectskeletonObj;//A list that will hold all enemiesList<Enemy>enemies=newList<Enemy>();voidStart(){//Add the enemies we haveenemies.Add(newCreeper(creeperObj.transform));enemies.Add(newSkeleton(skeletonObj.transform));}voidUpdate(){//Update all enemies to see if they should change state and move/attack playerfor(inti=0;i<enemies.Count;i++){enemies[i].UpdateEnemy(playerObj.transform);}}}}Nuestro State Pattern consiste en una clase Enemy y dos clases hijas para cada enemy, la idea básica es primero actualizamos el estado del enemigo, que puede ser Attack, Flee, Stroll or Move, esta última en dirección al enemigo para atacar, todo esto verificando si puede cambiar entre un estado o el otro, dependiendo del estado en el cual se encuentre el enemigo. Si el enemigo está rondando y el jugador está a cierta distancia, entonces el enemigo iniciar la cacería del jugador.
La diferencia principal entre el Creeper y el Skeleton is que el Skeleton puede atacar desde una distancia, mientras que el Creeper debe estar muy cerca del jugador para poder atacar.
La clase Enemy es así:
usingSystem.Collections;usingSystem.Collections.Generic;usingUnityEngine;namespaceStatePattern{//The enemy base classpublicclassEnemy{protectedTransformenemyObj;//The different states the enemy can be inprotectedenumEnemyFSM{Attack,Flee,Stroll,MoveTowardsPlayer}//Update the enemy by giving it a new statepublicvirtualvoidUpdateEnemy(TransformplayerObj){}//Do something based on a stateprotectedvoidDoAction(TransformplayerObj,EnemyFSMenemyMode){floatfleeSpeed=10f;floatstrollSpeed=1f;floatattackSpeed=5f;switch(enemyMode){caseEnemyFSM.Attack://Attack playerbreak;caseEnemyFSM.Flee://Move away from player//Look in the opposite directionenemyObj.rotation=Quaternion.LookRotation(enemyObj.position-playerObj.position);//MoveenemyObj.Translate(enemyObj.forward*fleeSpeed*Time.deltaTime);break;caseEnemyFSM.Stroll://Look at a random positionVector3randomPos=newVector3(Random.Range(0f,100f),0f,Random.Range(0f,100f));enemyObj.rotation=Quaternion.LookRotation(enemyObj.position-randomPos);//MoveenemyObj.Translate(enemyObj.forward*strollSpeed*Time.deltaTime);break;caseEnemyFSM.MoveTowardsPlayer://Look at the playerenemyObj.rotation=Quaternion.LookRotation(playerObj.position-enemyObj.position);//MoveenemyObj.Translate(enemyObj.forward*attackSpeed*Time.deltaTime);break;}}}}La clase Skeleton se ve así:
usingSystem.Collections;usingSystem.Collections.Generic;usingUnityEngine;namespaceStatePattern{//The skeleton classpublicclassSkeleton:Enemy{EnemyFSMskeletonMode=EnemyFSM.Stroll;floathealth=100f;publicSkeleton(TransformskeletonObj){base.enemyObj=skeletonObj;}//Update the creeper's statepublicoverridevoidUpdateEnemy(TransformplayerObj){//The distance between the Creeper and the playerfloatdistance=(base.enemyObj.position-playerObj.position).magnitude;switch(skeletonMode){caseEnemyFSM.Attack:if(health<20f){skeletonMode=EnemyFSM.Flee;}elseif(distance>6f){skeletonMode=EnemyFSM.MoveTowardsPlayer;}break;caseEnemyFSM.Flee:if(health>60f){skeletonMode=EnemyFSM.Stroll;}break;caseEnemyFSM.Stroll:if(distance<10f){skeletonMode=EnemyFSM.MoveTowardsPlayer;}break;caseEnemyFSM.MoveTowardsPlayer://The skeleton has bow and arrow so can attack from distanceif(distance<5f){skeletonMode=EnemyFSM.Attack;}elseif(distance>15f){skeletonMode=EnemyFSM.Stroll;}break;}//Move the enemy based on a stateDoAction(playerObj,skeletonMode);}}}La clase Creeper se ve así:
usingSystem.Collections;usingSystem.Collections.Generic;usingUnityEngine;namespaceStatePattern{//The creeper classpublicclassCreeper:Enemy{EnemyFSMcreeperMode=EnemyFSM.Stroll;floathealth=100f;publicCreeper(TransformcreeperObj){base.enemyObj=creeperObj;}//Update the creeper's statepublicoverridevoidUpdateEnemy(TransformplayerObj){//The distance between the Creeper and the playerfloatdistance=(base.enemyObj.position-playerObj.position).magnitude;switch(creeperMode){caseEnemyFSM.Attack:if(health<20f){creeperMode=EnemyFSM.Flee;}elseif(distance>2f){creeperMode=EnemyFSM.MoveTowardsPlayer;}break;caseEnemyFSM.Flee:if(health>60f){creeperMode=EnemyFSM.Stroll;}break;caseEnemyFSM.Stroll:if(distance<10f){creeperMode=EnemyFSM.MoveTowardsPlayer;}break;caseEnemyFSM.MoveTowardsPlayer:if(distance<1f){creeperMode=EnemyFSM.Attack;}elseif(distance>15f){creeperMode=EnemyFSM.Stroll;}break;}//Move the enemy based on a stateDoAction(playerObj,creeperMode);}}}Luego de agregar el Script GameController a un GameObject Empty y agregar los elementos en cada una de las variables, ejecutamos y podemos ver su comportamiento, para poder ver cada uno de los estados en funcionamiento lo que debemos hacer es mover nuestra Esfera que hace el papel del player, para que esto active cada uno de los estados de los enemigos.
Vamos a poder ver como el Skeleton se queda un poco alejado del player, mientras que el Creeper se acerca al jugador, cuando nos alejamos empiezan a girar en su punto simulando una ronda o patrullaje.
El patrón Subclass Sandbox Pattern describe una idea básica, este es muy útil cuando tenemos varias subclases similares, cuando necesitamos hacer un pequeño cambio, lo que hacemos es cambiar la clase base, mientras que todas las subclases no necesitan ser tocadas. Entonces la clase base provee todas las operaciones a las clases derivadas.
Para este ejemplo solo necesitamos dos scripts, un GameController y un Superpower, el cual va a incluir varias clases.
usingSystem.Collections;usingSystem.Collections.Generic;usingUnityEngine;namespaceSubclassSandbox{publicclassGameController:MonoBehaviour{//A list that will store all superpowersList<Superpower>superPowers=newList<Superpower>();voidStart(){superPowers.Add(newSkyLaunch());superPowers.Add(newGroundDive());}voidUpdate(){//Activate each superpower each updatefor(inti=0;i<superPowers.Count;i++){superPowers[i].Activate();}}}}usingSystem.Collections;usingSystem.Collections.Generic;usingUnityEngine;namespaceSubclassSandbox{//This is the base classpublicabstractclassSuperpower{//This is the sandbox method that a subclass has to have its own version ofpublicabstractvoidActivate();//All of the operations a derived class needs to perform - called from Activate()protectedvoidMove(floatspeed){Debug.Log("Moving with speed "+speed);}protectedvoidPlaySound(stringcoolSound){Debug.Log("Playing sound "+coolSound);}protectedvoidSpawnParticles(){}}//SubclassespublicclassSkyLaunch:Superpower{//Has to have its own version of Activate()publicoverridevoidActivate(){//Add operations this class has to performMove(10f);PlaySound("SkyLaunch");SpawnParticles();}}publicclassGroundDive:Superpower{//Has to have its own version of Activate()publicoverridevoidActivate(){//Add operations this class has to performMove(15f);PlaySound("GroundDive");SpawnParticles();}}}Si agregamos el script GameController a un objeto vacío en Unity y presionamos Play, vamos a ver que no sucede nada, excepto por los mensajes que aparecen en la consola de Unity, con esto podemos ver la idea general de este patrón y cómo implementarlo.
Cuando hacemos un juego, muchas veces necesitamos encontrar la posición del enemigo que está más cerca al jugador. La manera más común de hacer esto es almacenar todos los enemigos en una lista, buscar en la lista todos los enemigos y por cada enemigo calcular la distancia al jugador. Este código en C# encuentra al enemigo más cercano:
//Soldier is a friendly soldierGameObjectFindClosestEnemySlow(GameObjectsoldier){GameObjectclosestEnemy=null;floatbestDistSqr=Mathf.Infinity;//Loop through all enemiesfor(inti=0;i<enemySoldiers.Count;i++){//The distance sqr between the soldier and this enemyfloatdistSqr=(soldier.transform.position-enemySoldiers[i].transform.position).sqrMagnitude;//If this distance is better than the previous best distance, then we have found an enemy that's closerif(distSqr<bestDistSqr){bestDistSqr=distSqr;closestEnemy=enemySoldiers[i];}}returnclosestEnemy;}Esto puede funcionar si uno tiene pocos enemigos, pero si uno tiene un campo de batalla con muchos soldados y enemigos, por cada soldado amigo tenemos que buscar en la lista de todos los enemigos, si tenemos 100 soldados amigos, vamos a tener que repetir el cálculo 100 veces.
Una mejor forma es almacenar todos los enemigos en una estructura de datos espacial que organice los objetos por sus posiciones. Podemos almacenar ambos objetos movibles, pero también objetos estáticos como obstáculos en esta estructura de datos. Una solución común es dividir el área en un grid 2D luego asociamos cada celda a los enemigos en ella. Luego para encontrar el enemigo más cercano, necesitamos saber en cual celda está el player y luego obtener los enemigos en esa celda, luego buscamos cuál de los enemigos en la celda es el más cercano al jugador.
La idea acá es que vamos a tener esferas cazando cubos, las esferas son amigas mientras que los cubos son llamados enemigos. En Unity agregamos un objeto vacío llamado GameController, luego agregamos dos objetos vacíos llamado Enemy Soldiers y Friendly Soldiers a los cuales vamos a emparentar los cubos y las esferas para tener un espacio de trabajo limpio, luego agregamos el terreno donde van a estar todo los elementos. Para que esto funcione la esquina del plano debe empezar en la posición (0,0,0), para esto la posición debe ser (25,0,25) y la escala (5,1,5). La razón de esto es que debemos trasladar las coordenadas de los cubos y las esferas a posiciones en el grid, es más fácil si todo inicia en (0,0,0). Todo se debe ver como en la siguiente imagen:
También necesitamos una esfera y un cubo, ambos deben ser prefabs. También necesitamos materiales, la razón de esto es que vamos a cambiar el material del cubo para detectar cual de ellos está más cerca.
Ahora necesitamos agregar un nuevo script llamado GameController. Este Script agrega nuevos cubos y esferas de forma aleatoria en el mapa, este va manejar el cambio de los colores y también incluye la versión lenta para encontrar el enemigo más cercano, agregamos lo siguiente:
usingSystem.Collections;usingSystem.Collections.Generic;usingUnityEngine;namespaceSpatialPartitionPattern{publicclassGameController:MonoBehaviour{publicGameObjectfriendlyObj;publicGameObjectenemyObj;//Change materials to detect which enemy is the closestpublicMaterialenemyMaterial;publicMaterialclosestEnemyMaterial;//To get a cleaner workspace, parent all soldiers to these empty gameobjectspublicTransformenemyParent;publicTransformfriendlyParent;//Store all soldiers in these listsList<Soldier>enemySoldiers=newList<Soldier>();List<Soldier>friendlySoldiers=newList<Soldier>();//Save the closest enemies to easier change back its materialList<Soldier>closestEnemies=newList<Soldier>();//Grid datafloatmapWidth=50f;intcellSize=10;//Number of soldiers on each teamintnumberOfSoldiers=100;//The Spatial Partition gridGridgrid;voidStart(){//Create a new gridgrid=newGrid((int)mapWidth,cellSize);//Add random enemies and friendly and store them in a listfor(inti=0;i<numberOfSoldiers;i++){//Give the enemy a random positionVector3randomPos=newVector3(Random.Range(0f,mapWidth),0.5f,Random.Range(0f,mapWidth));//Create a new enemyGameObjectnewEnemy=Instantiate(enemyObj,randomPos,Quaternion.identity)asGameObject;//Add the enemy to a listenemySoldiers.Add(newEnemy(newEnemy,mapWidth,grid));//Parent itnewEnemy.transform.parent=enemyParent;//Give the friendly a random positionrandomPos=newVector3(Random.Range(0f,mapWidth),0.5f,Random.Range(0f,mapWidth));//Create a new friendlyGameObjectnewFriendly=Instantiate(friendlyObj,randomPos,Quaternion.identity)asGameObject;//Add the friendly to a listfriendlySoldiers.Add(newFriendly(newFriendly,mapWidth));//Parent it newFriendly.transform.parent=friendlyParent;}}voidUpdate(){//Move the enemiesfor(inti=0;i<enemySoldiers.Count;i++){enemySoldiers[i].Move();}//Reset material of the closest enemiesfor(inti=0;i<closestEnemies.Count;i++){closestEnemies[i].soldierMeshRenderer.material=enemyMaterial;}//Reset the list with closest enemiesclosestEnemies.Clear();//For each friendly, find the closest enemy and change its color and chase itfor(inti=0;i<friendlySoldiers.Count;i++){//Soldier closestEnemy = FindClosestEnemySlow(friendlySoldiers[i]);//The fast version with spatial partitionSoldierclosestEnemy=grid.FindClosestEnemy(friendlySoldiers[i]);//If we found an enemyif(closestEnemy!=null){//Change materialclosestEnemy.soldierMeshRenderer.material=closestEnemyMaterial;closestEnemies.Add(closestEnemy);//Move the friendly in the direction of the enemyfriendlySoldiers[i].Move(closestEnemy);}}}//Find the closest enemy - slow versionSoldierFindClosestEnemySlow(Soldiersoldier){SoldierclosestEnemy=null;floatbestDistSqr=Mathf.Infinity;//Loop thorugh all enemiesfor(inti=0;i<enemySoldiers.Count;i++){//The distance sqr between the soldier and this enemyfloatdistSqr=(soldier.soldierTrans.position-enemySoldiers[i].soldierTrans.position).sqrMagnitude;//If this distance is better than the previous best distance, then we have found an enemy that's closerif(distSqr<bestDistSqr){bestDistSqr=distSqr;closestEnemy=enemySoldiers[i];}}returnclosestEnemy;}}}Luego están los Soldados, estos heredan de una clase Soldier
usingSystem.Collections;usingSystem.Collections.Generic;usingUnityEngine;namespaceSpatialPartitionPattern{//The soldier base class for enemies and friendlypublicclassSoldier{//To change materialpublicMeshRenderersoldierMeshRenderer;//To move the soldierpublicTransformsoldierTrans;//The speed the soldier is walking withprotectedfloatwalkSpeed;//Has to do with the grid, so we can avoid storing all soldiers in an array//Instead we are going to use a linked list where all soldiers in the cell //Are linked to each otherpublicSoldierpreviousSoldier;publicSoldiernextSoldier;//The enemy doesnt need any outside informationpublicvirtualvoidMove(){}//The friendly has to move which soldier is the closestpublicvirtualvoidMove(Soldiersoldier){}}}usingSystem.Collections;usingSystem.Collections.Generic;usingUnityEngine;namespaceSpatialPartitionPattern{//The enemy cube being chased by the spherespublicclassEnemy:Soldier{//The position the soldier is heading for when movingVector3currentTarget;//The position the soldier had before it moved, so we can see if it should change cellVector3oldPos;//The width of the map to generate random coordinated within the mapfloatmapWidth;//The gridGridgrid;//Init enemypublicEnemy(GameObjectsoldierObj,floatmapWidth,Gridgrid){//Save what we need to savethis.soldierTrans=soldierObj.transform;this.soldierMeshRenderer=soldierObj.GetComponent<MeshRenderer>();this.mapWidth=mapWidth;this.grid=grid;//Add this unit to the gridgrid.Add(this);//Init the old posoldPos=soldierTrans.position;this.walkSpeed=5f;//Give it a random coordinate to move towardsGetNewTarget();}//Move the cube randomly across the mappublicoverridevoidMove(){//Move towards the targetsoldierTrans.Translate(Vector3.forward*Time.deltaTime*walkSpeed);//See if the the cube has moved to another cellgrid.Move(this,oldPos);//Save the old positionoldPos=soldierTrans.position;//If the soldier has reached the target, find a new targetif((soldierTrans.position-currentTarget).magnitude<1f){GetNewTarget();}}//Give the enemy a new target to move towards and rotate towards that targetvoidGetNewTarget(){currentTarget=newVector3(Random.Range(0f,mapWidth),0.5f,Random.Range(0f,mapWidth));//Rotate towards the targetsoldierTrans.rotation=Quaternion.LookRotation(currentTarget-soldierTrans.position);}}}usingSystem.Collections;usingSystem.Collections.Generic;usingUnityEngine;namespaceSpatialPartitionPattern{//The friendly sphere which is chasing the enemy cubespublicclassFriendly:Soldier{//init friendlypublicFriendly(GameObjectsoldierObj,floatmapWidth){this.soldierTrans=soldierObj.transform;this.walkSpeed=2f;}//Move towards the closest enemy - will always move within its gridpublicoverridevoidMove(SoldierclosestEnemy){//Rotate towards the closest enemysoldierTrans.rotation=Quaternion.LookRotation(closestEnemy.soldierTrans.position-soldierTrans.position);//Move towards the closest enemysoldierTrans.Translate(Vector3.forward*Time.deltaTime*walkSpeed);}}}Si pensamos un poco, es la esfera amiga la que casa a los cubos enemigos, pero solo cuando los cubos se están moviendo aleatoriamente, las esferas las que actualmente son el enemigo. La parte un poco confusa es que no estamos almacenando los soldados que est´na es cada celda en un array, en lugar estan en una linkedlist, entonces cada soldados está referenciando a otro soldado en la misma lista, luego otro soldado se refiere a otro soldado y así sucesivamente.
usingSystem.Collections;usingSystem.Collections.Generic;usingUnityEngine;namespaceSpatialPartitionPattern{publicclassGrid{//Need this to convert from world coordinate position to cell positionintcellSize;//This is the actual grid, where a soldier is in each cell//Each individual soldier links to other soldiers in the same cellSoldier[,]cells;//Init the gridpublicGrid(intmapWidth,intcellSize){this.cellSize=cellSize;intnumberOfCells=mapWidth/cellSize;cells=newSoldier[numberOfCells,numberOfCells];}//Add a unity to the gridpublicvoidAdd(Soldiersoldier){//Determine which grid cell the soldier is inintcellX=(int)(soldier.soldierTrans.position.x/cellSize);intcellZ=(int)(soldier.soldierTrans.position.z/cellSize);//Add the soldier to the front of the list for the cell it's insoldier.previousSoldier=null;soldier.nextSoldier=cells[cellX,cellZ];//Associate this cell with this soldiercells[cellX,cellZ]=soldier;if(soldier.nextSoldier!=null){//Set this soldier to be the previous soldier of the next soldier of this soldier (linked lists ftw)soldier.nextSoldier.previousSoldier=soldier;}}//Get the closest enemy from the gridpublicSoldierFindClosestEnemy(SoldierfriendlySoldier){//Determine which grid cell the friendly soldier is inintcellX=(int)(friendlySoldier.soldierTrans.position.x/cellSize);intcellZ=(int)(friendlySoldier.soldierTrans.position.z/cellSize);//Get the first enemy in gridSoldierenemy=cells[cellX,cellZ];//Find the closest soldier of all in the linked listSoldierclosestSoldier=null;floatbestDistSqr=Mathf.Infinity;//Loop through the linked listwhile(enemy!=null){//The distance sqr between the soldier and this enemyfloatdistSqr=(enemy.soldierTrans.position-friendlySoldier.soldierTrans.position).sqrMagnitude;//If this distance is better than the previous best distance, then we have found an enemy that's closerif(distSqr<bestDistSqr){bestDistSqr=distSqr;closestSoldier=enemy;}//Get the next enemy in the listenemy=enemy.nextSoldier;}returnclosestSoldier;}//A soldier in the grid has moved, so see if we need to update in which grid the soldier ispublicvoidMove(Soldiersoldier,Vector3oldPos){//See which cell it was in intoldCellX=(int)(oldPos.x/cellSize);intoldCellZ=(int)(oldPos.z/cellSize);//See which cell it is in nowintcellX=(int)(soldier.soldierTrans.position.x/cellSize);intcellZ=(int)(soldier.soldierTrans.position.z/cellSize);//If it didn't change cell, we are doneif(oldCellX==cellX&&oldCellZ==cellZ){return;}//Unlink it from the list of its old cellif(soldier.previousSoldier!=null){soldier.previousSoldier.nextSoldier=soldier.nextSoldier;}if(soldier.nextSoldier!=null){soldier.nextSoldier.previousSoldier=soldier.previousSoldier;}//If it's the head of a list, remove itif(cells[oldCellX,oldCellZ]==soldier){cells[oldCellX,oldCellZ]=soldier.nextSoldier;}//Add it bacl to the grid at its new cellAdd(soldier);}}}Si agregamos todos los script y los elementos necesarios, al presionar play vamos a ver un funcionamiento similar a este:
Podemos ver que cada esfera no está casando cada cubo si no tiene un cubo en su celda, este es un efecto secundario muy interesante que se puede usar para hacer una patrulla de objetos.
Todo el contenido fue traducido de la siguiente página: Habrador









