sábado, 11 de enero de 2014

11) AI (WIP actualizar con info de Pylons)



Muy buenas!


Hoy vamos a explicar funcionalidad básica de los NavMesh para realizar una AI que busque un objetivo. En este caso, dicho objetivo serán un grupo de nodos custom (que los marcaremos como de destino sólo para evitar que generen caminos automáticamente y utilizarlos sólo como referencias de un Waypoint). Igualmente, describiré la forma de realizar un nuevo estado que persiga al jugador. Del mismo modo, de manera sencilla, se pueden generar otros estados y cambiar entre unos y otros mediante Kismet y con lo aprendido a la hora de crear eventos y lanzarlos (y por supuesto de crear nuevos estados).


Sin más, procedemos a ver el código para nuestros nodos custom a seguir por la patrulla
:
class UKNCustomPathNode extends NavigationPoint placeable;
DefaultProperties
{
   //Here we are assigning a texture to be shown in the editor for this object. You can modify the texture as you wish 
   //(this is the common "apple" sprite and this lines of code are not necessary if you don't want to change it[inherited from NavigatioNPoint])
   Begin Object NAME=Sprite
      Sprite=Texture2D'EditorResources.S_Pickup'
   End Object

   //This flag is necessary to UDK not to calculate a path with them automatically
   bDestinationOnly=true
}


Como se puede ver en el código, estos nodos no son más que NavigationPoint que no son calculados cuando compilamos caminos gracias a su flag “bDestinationOnly”. Podremos, desde la pestaña de Actors en el ContentBrowser (recordemos, botoncillo con logo de UDK en la barra superior de herramientas) arrastrar o crearlos y colocarlos en la escena seleccionándolos y botón derecho del ratón-> Crear aquí.


Es importante que nos fijemos en sus propiedades con F4 para saber su nombre (pestaña Object) porque después tendremos que asignárselo en el orden que deseemos al crear en camino de patrulla a seguir. Este método es un poco engorroso pero creo que, igual que ocurre con el motor Unity, resulta mejor a la hora de trabajar junto a diseñadores y sin necesidad de tocar código para poder cambiar diferentes patrullas de nuestros enemigos. Por supuesto, existen otras formas de realizarlo, aunque yo haya elegido esta como la que considero más sencilla para mi alumnado.


Acto seguido, pasamos a crear un UKNAIPawn que serán los pawns de nuestros enemigos. Particularmente, si deseamos tener varios tipos de enemigos con funcionalidades iguales (Exceptuando estética) sin necesidad de tener que crear una clase para cada uno de ellos, podremos trabajar con Arquetipos. Mi intención es explicarlos en una entrada más adelante. De momento, ahí va el código de nuestro peón para nuestra AI que patrulla:

class UKNAIPawn extends GamePawn
placeable;
// Dynamic light environment component to help speed up lighting calculations for the pawn
var(Pawn) const DynamicLightEnvironmentComponent LightEnvironment;
// Ground speed of the pawn, display as Ground Speed in the editor
var(Pawn) const float UserGroundSpeed<DisplayName=Ground Speed>;
// Internal int which stores the desired yaw of the pawn
var int DesiredYaw;
// Internal int which store the current yaw of the pawn
var int CurrentYaw;
// Internal int which stores the current pitch of the pawn
var int CurrentPitch;
/**
* AI custom points
*
*
*
*/
var (Paths) array<UKNPathNode> patrolPoints; //patrol actors to follow by AI
var (Paths) float perceptionDistance<DisplayName= Perception Distance>; //Distance to detect player
/**
* Called when the pawn is first initialized
*/
simulated function PostBeginPlay()
{
Super.PostBeginPlay();
//To apply speed and be shown in the Editor pressing F4
GroundSpeed = UserGroundSpeed;
//It’s controller MUST be spawned in order to Possess it!    SpawnDefaultController();}defaultproperties{    ControllerClass=class’UKNGameInfo.UKNAIController’     // Ground speed of the pawn    UserGroundSpeed=300.f    // Set physics to falling    Physics=PHYS_Falling    // Remove the sprite component as it is not needed    Components.Remove(Sprite)    // Create a light environment for the pawn    Begin Object Name=MyLightEnvironment        bSynthesizeSHLight=true        bIsCharacterLightEnvironment=true        bUseBooleanEnvironmentShadowing=false    End Object    Components.Add(MyLightEnvironment)    LightEnvironment=MyLightEnvironment    // Create a skeletal mesh component for the pawn    Begin Object Name=MySkeletalMeshComponent        bCacheAnimSequenceNodes=false        AlwaysLoadOnClient=true        AlwaysLoadOnServer=true        CastShadow=true        BlockRigidBody=true        bUpdateSkelWhenNotRendered=false        bIgnoreControllersWhenNotRendered=true        bUpdateKinematicBonesFromAnimation=true        bCastDynamicShadow=true        RBChannel=RBCC_Untitled3        RBCollideWithChannels=(Untitled3=true)        LightEnvironment=MyLightEnvironment        bOverrideAttachmentOwnerVisibility=true        bAcceptsDynamicDecals=false        bHasPhysicsAssetInstance=true        TickGroup=TG_PreAsyncWork        MinDistFactorForKinematicUpdate=0.2f        bChartDistanceFactor=true        RBDominanceGroup=20        Scale=1.f        bAllowAmbientOcclusion=false        bUseOnePassLightingOnTranslucency=true        bPerBoneMotionBlur=true    End Object    Mesh=MySkeletalMeshComponent    Components.Add(MySkeletalMeshComponent)}


Como vemos, nuestro Pawn controlado por AI va a contener un array que contiene elementos de nuestra clase Custom de nodos (patrolPoints). Igualmente tenemos una variable que utilizaremos para detectar al player cuando se encuentre a una determinada distancia (para poder configurarlo en el Editor tiene (NombreDePestaña) en la variable. Ojo a la función PostBeginPlay, donde se spawnea su controlador y éste posee a este Pawn (SpawnDefaultController). También recordad que los valores de la malla del personaje los podéis cambiar a vuestro gusto. Estos son los que utilicé yo para algunas pruebas, con lo cual, seguramente sea necesario modificarlos para vuestro proyecto.


Por último, necesitaremos crear la clase más importante: El Controlador. Aquí es donde desarrollaremos nuestra máquina de estados y donde se hace uso de la NavMesh para poder ser utilizado. Coloco como siempre el código y después procedo a explicar. Recordad que tenéis comentarios en el propio código (no lo copiéis y peguéis sin más o puede que no comprendais correctamente su funcionamiento):

class UKNAIController extends AIController;
var Actor Player;
var color DrawColor;
var Vector TempDest;
var float PlayerDistance;
/**
* Follow AI custom path nodes variables
*
*
*
*/
var array<UKNPathNode> patrolNodes; //Nodes to follow in order
var UKNPathNode currentPathNode; //Current path node to follow
var int currentPathNodeID;
var array<float> idleTimers;
var float currentIdleTime;
var float perceptionDistance; //Distance to detect player
var int direction; //Current direction following path
var bool followPath; //The pawn is following the path
//This event is called when it takes control of a pawn (p.e. in SpawnDefaultController called in the Pawn class)
event Possess(Pawn inPawn, bool bVehicleTransition)
{
local UKNAIPawn myPawn;
super.Possess(inPawn, bVehicleTransition);
//This enables pawn movement
Pawn.SetMovementPhysics();
myPawn = UKNAIPawn(Pawn);
if(myPawn!= none)
{ //El pawn a poseer es de tipo de enemigo que patrulla -> Asignamos las variables necesarias para ello
//Básicamente copiamos los valores que hemos asignado en el Editor al Pawn (la única clase que podemos ver al ser placeable)
patrolNodes = myPawn.patrolPoints;
idleTimers = myPawn.idleTimers;
perceptionDistance = myPawn.perceptionDistance;
}
}
//Estado en el que no hace nada. Especial atención a Sleep(1). Es necesario dormir un frame este estado o el programa entrará en bucle infinito
//Como prueba, simplemente espera un frame y entra en el estado de patrulla. Como se puede observar, se puede colocar la lógica que se desee
auto state Idle
{
event Tick(float DeltaTime)
{
if(Pawn != none && Pawn.Acceleration != vect(0,0,0))
{
`log(“Decreasing acceleration in Pawn: “@Pawn.Name);
Pawn.Acceleration = vect(0,0,0);
}
super.Tick(DeltaTime);
}
begin:
Sleep(1);
GoToState(‘Patrol’);
}
state Patrol
{
//Evento llamado por el motor que es nativo y que detecta que el pawn ve otro. He comentado que lo persiga si lo ve
event SeePlayer(Pawn Seen)
{
local float distanceToSeenPlayer;
distanceToSeenPlayer= 0.0f;
Super.SeePlayer(Seen);
Player = Seen;
distanceToSeenPlayer = VSize(Player.Location – Pawn.Location);
/*if(distanceToSeenPlayer <= perceptionDistance)
{
GoToState(‘Chase’);
}*/
}
event BeginState(Name PreviousStateName)
{
direction = 1; //Negative if it’s following the inverse path
super.BeginState(PreviousStateName);
//Set current node to follow
if(patrolNodes.Length > 0)
{
//There is a patrol path
if(self.currentPathNodeID >= patrolNodes.Length) //Path has changed and last path point was not reset.Using first point instead
currentPathNodeID=0;
self.currentPathNode = patrolNodes[currentPathNodeID];
}
else
{
GoToState(‘Idle’);
}
}
function bool FindNavMeshPath()
{
//Clear cache and constraints. You need Path constraints to modify behaviour and a PathGoal
NavigationHandle.PathConstraintList = none;
NavigationHandle.PathGoalList = none;
//Create constraints
class’NavMeshPath_Toward’.static.TowardGoal(NavigationHandle,currentPathNode);
class’NavMeshGoal_At’.static.AtActor(NavigationHandle,currentPathNode,25);
//Find Path
return NavigationHandle.FindPath();
}
begin:
if(Pawn.ReachedDestination(currentPathNode)) //Hemos alcanzado el nodo
{
if((currentPathNodeID == (patrolNodes.Length-1)) && direction > 0)
{
direction *= -1;
currentPathNodeID = patrolNodes.Length -1;
}
else if(currentPathNodeID == 0 && direction < 0)
{
direction *= -1;
currentPathNodeID = 0;
}
currentPathNodeID += direction;
currentPathNode = patrolNodes[currentPathNodeID];
}
else if(NavigatioNHandle.ActorReachable(currentPathNode))
{
FlushPersistentDebugLines();
MoveToward(currentPathNode,currentPathNode,50);
}
else
{
if(FindNavmeshPath())
{
NavigationHandle.SetFinalDestination(currentPathNode.Location);
FlushPersistentDebugLines();
DrawColor.R = 255;
NavigationHandle.DrawPathCache(,true,DrawColor);
if(NavigationHandle.GetNextMoveLocation(TempDest,Pawn.GetCollisionRadius()))
{
DrawDebugLine(Pawn.Location,TempDest,0,255,0,true);
DrawDebugSphere(TempDest,16,20,0,0,255,true);
MoveTo(TempDest,Player);
}
}
else
{
GoToState(‘Idle’);
}
}
goto ‘begin’;
}
state Chase
{
function bool FindNavMeshPath()
{
//Clear cache and constraints. You need Path constraints to modify behaviour and a PathGoal
NavigationHandle.PathConstraintList = none;
NavigationHandle.PathGoalList = none;
//Create constraints
class’NavMeshPath_Toward’.static.TowardGoal(NavigationHandle,Player);
class’NavMeshGoal_At’.static.AtActor(NavigationHandle,Player,25);
//Find Path
return NavigationHandle.FindPath();
}
begin:
Player=GetALocalPlayerController().Pawn;
PlayerDistance = VSize(Pawn.Location – Player.Location);
if(PlayerDistance < PlayerDistanceMinimumToShoot)
{
GoToState(‘Shoot’);
}
else if(NavigatioNHandle.ActorReachable(Player))
{
FlushPersistentDebugLines();
MoveToward(Player,Player,50);
}
else
{
if(FindNavmeshPath())
{
NavigationHandle.SetFinalDestination(Player.Location);
FlushPersistentDebugLines();
DrawColor.R = 255;
NavigationHandle.DrawPathCache(,true,DrawColor);
if(NavigationHandle.GetNextMoveLocation(TempDest,Pawn.GetCollisionRadius()))
{
DrawDebugLine(Pawn.Location,TempDest,0,255,0,true);
DrawDebugSphere(TempDest,16,20,0,0,255,true);
MoveTo(TempDest,Player);
}
}
else
{
GoToState(‘Idle’);
}
}
goto ‘begin’;
}
DefaultProperties
{
DrawColor=(R=255,G=0,B=0)
currentPathNodeID=0
}


dónde colocamos las directrices que debe seguir nuestra clase NavigationHandler, encargada de encontrar los caminos. Para ello, debemos configurarla para que sepa de qué modo debe encontrar ese camino y cual es su “meta”.
function bool FindNavMeshPath()
{
//Clear cache and constraints. You need Path constraints to modify behaviour and a PathGoal
//More information about Path constraints evaluators and Goals here:
NavigationHandle.PathConstraintList = none;
NavigationHandle.PathGoalList = none;
//Create constraints
class’NavMeshPath_Toward’.static.TowardGoal(NavigationHandle,Player);
class’NavMeshGoal_At’.static.AtActor(NavigationHandle,Player,25);
//Find Path
return NavigationHandle.FindPath();
}
Básicamente, utilizamos las clases estáticas de las constraints que modificarán el comportamiento de NavigationHandle a la hora de encontrar un camino (método estático FindPath). En este caso, el goal indicado es un Actor que lo he llamado Player (class’NavMeshGoal_At’.static.AtActor(NavigationHandle,Player,25);). En el caso de las patrullas, deberemos indicar aquí cual es el siguiente nodo a seguir.


Por supuesto, en el estado Patrol, se comprueba si hemos alcanzado el nodo que debemos seguir ( if(Pawn.ReachedDestination(currentPathNode)) //Hemos alcanzado el nodo ) y modificar currentPathNode al siguiente que debemos seguir en nuestra patrulla. En este caso, cuando llega al nodo final, el Pawn dará media vuelta y hará el camino contrario. Una vez más, os indico que ésta es una manera de hacer las cosas y que deberéis realizar los cambios pertinentes entendiendo su funcionamiento para que se adecúe a las necesidades de vuestro proyecto.


Antes de poner en marcha todo lo realizado, nos queda realizar trabajo en el editor. Básicamente debéis colocar un NavMesh Pylon y arrastrarlo a la escena. Estos pilares poseen un radio y dicho radio puede configurarse para ser más amplio. Varios pilones pueden convivir confluyendo sus radios de acción para combinarse entre si y abarcar una mayor superficie en un mapa determinado, pero NUNCA debemos colocar un pilar dentro del radio de acción de otro pilar.

Configuración de NavMesh (clase Scout.uc)



Los NavMesh siguen una serie de pautas para su creación mediante otra clase llamada Scout. Para poder configurar cómo se genera para su utilización, debemos crear nuestra propia clase Scout con los valores que deseemos (Su utilidad, por ejemplo, es la de configurar si diferentes tamaños de Pawn de altura van a poder o no moverse dependiendo de por qué lugares):






Creamos una clase de Scout:






Class NULLScout extends Scout;

defaultproperties
{




//Limpiamos cualquier path que hubiera anteriormente ya que utilizaremos la altura y el ancho
//de nuestro Pawn para poder utilizarlo
PathSizes.Empty


PathSizes.Add((Desc=Human,Radius=180,Height=330))

//Vemos que se pueden incluir en PathSize varios conjuntos de radios y altura con una //descripción

NavMeshGen_EntityHalfHeight=165

//Restamos a este Offset a MaxPolyHeight para saber el bounding final del NavMesh en altura

NavMeshGen_StartingHeightOffset=140

//Un número mayor que la altura de nuestro pawn

NavMeshGen_MaxPolyHeight=175

}

Una vez creada esta clase, debemos ir al fichero de configuración DefaultEngine.ini (UDKGame/Config) y cambiar



[Engine.Engine]
ScoutClassName=NULLGame.NULLScout

Volvemos al editor y recompilamos los paths (el botoncito con nodos con forma de K) y veremos como la generación de la malla del NavMesh se ha modificado.


Colocando los PathNodes y testeando


Una vez realizado esto, podéis colocar vuestros custom nodes en la escena y arrastrar a la misma un actor del tipo de vuestro enemigo. Allí deberemos acceder a sus propiedades y añadir los pathnodes a seguir. Para ello, podemos bloquear con el icono arriba a la derecha, las propiedades de nuestro Pawn. Así, podremos seleccionar objetos en la escena sin que nos cambie a sus propiedades en esta ventana. 
Una vez hecho esto, podemos pasar a ir colocándolos con el iconito de flecha o + dentro del array dinámico sin problemas. La otra opción, más tediosa es comprobar el nombre de la instancia de cada nodo y escribir en su lista de nodos custom (la del Pawn) cada uno de ellos siguiendo un patrón como este:
UKNPathNode’NombreDelMapa.TheWorld:PersistentLevel.UDKPathNode_NUMERODEINSTANCIA’
Fijáos bien si tenéis diferentes niveles cargados a la vez, ya que la parte de PersistentLevel seguramente cambie según el Level en el que se encuentre. Seguramente esta información la amplíe en un futuro con una explicación más gráfica de esta última parte.

ENLACES IMPORTANTES:

Funciones latentes dentro de la etiqueta Begin,

http://udn.epicgames.com/Three/MasteringUnrealScriptStates.html#LATENT%20FUNCTIONS

Constraints y Goal Evaluators
http://udn.epicgames.com/Three/NavMeshConstraintsAndGoalEvaluators.html

Documento técnico de Animation Mesh
http://udn.epicgames.com/Three/NavigationMeshTechnicalGuide.html

10) Don't press the RED BUTTON!!! Configuración de DefaultInput y funciones Exec

Muy buenas,
Hoy vamos a realizar un pequeño experimento, y para ello necesitamos presentar la clase PlayerInput. PlayerInput es una clase auxiliar de PlayerController para desligar de éste la parte de “bindeo” de teclas e input de la propia clase.
La estructura básica de PlayerInput es esta:
class NULLPlayerInput extends PlayerInput within NULLPlayerController;
//Within makes this class to be part of NULLPlayerController and we can access it using Outer
DefaultProperties
{
}
Tendremos que añadir en NULLPlayerController en defaultProperties
InputClass=class ‘NULLGame.NULLPlayerInput’
Por supuesto, ya sabéis como modificar ese valor para que se adecue a vuestro proyecto.
Una vez hecho esto, echemos un vistazo a las funciones exec para saber en qué consisten :

PlayerInput

PlayerController y PlayerInput pueden utilizar un tipo de funciones especiales indicadas por el prefijo exec antes de colocar function en su declaración tal que así:

exec function NombreDeLaFuncion( float amount = 100)

Podéis consultar información sobre este tipo de funciones en la UDN:  UDN link
Todas las funciones que sean declaradas con exec, pueden ser llamadas desde la consola de comandos de UDK. ¿Cómo activarla? Cuando ejecutéis el juego, pulsáis tab. ¡Ahí está!

Uno de los comandos típicos es "exit" o "quit" para salir del juego.

En ese caso tenemos un valor por defecto que es 100 si no usamos ninguno. Recordad que los parámetros por defecto se coloquen los últimos. En la consola de comandos, se llamaría a dicha función como NombreDeLaFuncion 50 para pasarle el valor 50. Por supuesto, si no colocamos ningún parámetro, cogería por defecto 100.

La primera utilidad que se os puede venir a la cabeza, es como un sistema de debug. Como vimos en la parte de HUD, podemos crear funciones que realicen `log() con información que queramos consultar, o bien utilizar "trucos" o modificar valores en tiempo real. No es la manera más cómoda de hacerlo, pero no deja de ser un recurso más.

A parte de esto, en el fichero de configuración DefaultInput.ini (situado en UDKGame/Config), podemos "bindear" teclas o grupos y acciones de las mismas a este tipo de funciones. Con esto, ya podemos ver, que su otra utilidad es la de crear un enlace entre el input de vuestros periféricos y vuestro código.

Si consultáis DefaultInput os podéis encontrar entre sus entrañas líneas como estas:

.Bindings=(Name=”RightMouseButton”,Command=”EnableAimingMode | OnRelease DisableAimingMode”)

En Name, se coloca el nombre del código de tecla o KeyBind (consultar UDN para ver los existentes y más información al respecto). Cuando dicha tecla se pulsa, se ejecuta la función exec con el nombre EnableAimingMode. El símbolo | indica una concatenación de eventos que pueden llamar a otra función, en este caso, OnRelease, indica que cuando dejamos de pulsar la tecla, se llama a la función DisableAimingMode.

Vamos a ver un poco cómo funciona el fichero DefaultInput.ini:
;—————————————————————————————–
; BINDINGS THAT ARE REMOVED FROM BASEINPUT.INI
;—————————————————————————————–
Los comentarios en los ficheros ini se realizan colocando un ; como primer carácter de línea (se ignora lo que vaya detrás). Si véis un + o un - tiene que ver con declaraciones anteriores y herencias entre ficheros ini, siendo respectivamente cada símbolo para añadir o quitar funcionalidad (dependiendo de las líneas donde se encuentren como primer carácter ese + o ese -).
.Bindings=(Name=”Enter”,Command=”GBA_Use”)
Como ves, los bindings tienen el nombre asignado de la tecla o acción (luego vemos lo que son acciones, pero básicamente es agrupar input para unificar teclado con un mando por ejemplo). En command se escribe el comando que deseamos como si lo estuviéramos realizando en la consola.
Colocar el .Bindings lo que hace es asignar un valor al Binding indicado en Name. Pero también podemos eliminarlos colocando un -Bindings.
Para añadirlos es utilizando el signo + como podéis comprobar en partes del mismo fichero:
+Bindings=(Name=”XboxTypeS_DPad_Up”,Command=”GBA_ToggleMinimap”)
-Bindings=(Name=”Escape”,Command=”CloseEditorViewport | onrelease ShowMenu”)
En ésta última linea, podemos ver que Command posee un | onrelease y el nombre de otra función exec. Eso significa que cuando la tecla o acción asignada se pulsa, se llama a la función exec primera y cuando se deja de pulsar se llama a la función tras onrelease.
De este modo ya podemos intuir cómo podemos realizar un cambio de estado de PlayerController o simplemente modificar el comportamiento de nuestro personaje a través de este tipo de funciones. Hay mucha información y más detallada sobre estos temas en la UDN:
KeyBind,  Con información de teclas y acciones para asignar
Una vez estudiado un poco este funcionamiento, conocemos lo básico para realizar una llamada a una función propia de nuestro CharacterController. Procederemos a modificar nuestro UKNPlayerInput:
class NULLPlayerInput extends PlayerInput within NULLPlayerController;
//Within makes this class to be part of NULLPlayerController and we can access it using Outer
exec function ShowMeDaMoneyBaby(float amount = 0)
{
`log(“- Here it is all my money! Please don’t hurt me – ” @amount@ ” $$ “);
}
exec function ReturnYourDirtyMoney()
{
`log(” I don’t want your dirty money! “);
}
DefaultProperties
{
}
Como podemos ver, podemos asignarle un valor a dicha función. En este caso lo utilizaremos para comprobar que podemos pasar por parámetro valores en en la consola de comandos, para quizá utilizar alguna función a modo de debug.
La función ReturnYourDirtyMoney la utilizaremos al soltar la tecla.
Para hacerlo funcionar, vamos a DefaultInput.ini y añadamos valores:
.Bindings=(Name=”X”,Command=”ShowMeDaMoneyBaby | OnRelease ReturnYourDirtyMoney”)
Ahora probemos nuestro ejemplo, pulsando X se mostrará un mensaje y al soltarlo el otro. Además podemos probar que nos muestra diferentes valores si llamamos al comando por la consola de comandos y le pasamos un parámetro.
Se puede extrapolar este funcionamiento a cualquier cosa que queráis, como cambios de cámara, de estado, etc…

sábado, 14 de diciembre de 2013

9) Powered by Kismet!

¡Muy buenas!
A continuación vamos a ver las bases para poder crear vuestras propias cajitas de Kismet. Para el que no sepa qué es Kismet, es un sistema de nodos que utiliza UDK para realizar "fragmentos" de código que actúen sobre actores en el mapa. Es un envoltorio de código para que se puedan crear ciertos comportamientos por medio de un entorno visual. Se utiliza fundamentalmente para "allanar" terreno a los diseñadores y poder trabajar de manera más independiente sin depender tanto del programador. 
En Kismet, existen 3 tipos de nodos (cajitas):
  1. Actions
  2. Events
  3. Conditionals
Vamos a centrarnos en las primeras ya que la estructura general es la misma en todas.

Action

La estructura básica de las acciones es la siguiente:
// extend UIAction if this action should be UI Kismet Action instead of a Level Kismet Action
class NULLSeqAction_Example extends SequenceAction;
var Object NameOfTheVariableInTheCodeSeeAtTheTopOfThisClass;
//Esta clase recibe este evento cuando recibe una señal positiva de otro nodo en //algún input
event Activated()
{
if(InputLinks[0].bHasImpulse)
{ //Si recibimos un impulso positivo de el primer elemento dentro de nuestro arrat de entradas, ejecutamos este bloque de código.
OutputLinks[0].bHasImpulse = true;
}
else
{ //If we don’t have a positive pulse, then our output is false
OutputLinks[0].bHasImpulse = false;
}
}
defaultproperties
{
ObjName=”NULLExample Action” //Nombre del nodo tal como se ve en el editor //de Kismet
ObjCategory=”NULLGame Actions” //Nombre de la categoría a la que pertenece //para poder ordenar mejor nuestros nodos de Kismet
InputLinks(0)=(LinkDesc=”Name of the input (connection) from the left side of the node”) //Démonos cuenta de que cuando hablemos de Eventos no necesitamos //InputLinks
//They are just input to select different behaviour in the node. InputLinks(n), //n=0,1,2,3,4,5…
VariableLinks.Empty //Clean up inherited variables
//Variables are connected to the node from the bottom
VariableLinks(0)=(
ExpectedType=class’SeqVar_Object’, //This is the most generic class, but you can see in Engine/Classes/SeqVar_* other like SeqVar_Int
LinkDesc=”Name of this variable shown in editor”,
PropertyName=NameOfTheVariableInTheCodeSeeAtTheTopOfThisClass,
bWriteable=true) //If you can modify this variable or it is just read only
OutputLinks(0)=(LinkDesc=”Output”) //Different output from the right side of the node
}
Tened cuidado si copiáis y pegáis el código (no deberíais hacerlo, recordad las sabias palabras de José Luis), pero como se que lo vais hacer, el que avisa no es traidor. Los comentarios entre los parámetros en InputLinks no funcionarán. Borradlos cuando los hayáis leído y entendido o para hacer pruebas.
Recordad, que podemos acceder al grafo de escena desde la clase WorldInfo y si bien, tenemos la función GetWorldInfo() que nos devuelve una referencia del mismo. Si trabajamos desde ciertas clases como Actor, podemos consultarlo directamente como WorldInfo. Recordad también que WorldInfo posee una referencia al GameInfo que estáis utilizando actualmente y que podéis realizar comparativas haciendo castings y comparando con none. 
También podéis acceder al PlayerController local con la función GetALocalPlayerController().
Como imagináis, podéis modificar variables declaradas en VariableLink que conecten con vuestros nodos de Kismet si tienen el flag Writeable (Si no utilizan una copia de sus valores). Podéis hacer un casting del tipo de objeto que queréis con simplemente declarar una variable en la clase con el nombre que designéis en PropertyName. Por supuesto, si no puede hacerlo, su valor será none.
(var MyClass myPropertyVariableName)
Como ejercicio, tratad de crear un nodo Action que reciba un pulso positivo en su input y éste haga que escriba mediante `log() algo. Conectamos esta Action con un Evento Trigger touch (añadimos al mapa, un trigger con botón derecho, AddActor->Add Trigger) y teniéndolo seleccionado en el mapa, en la ventana de Kismet, seleccionamos usar un Evento con este trigger. Colocamos en sus propiedades en la ventana de Kismet la variable MaxTriggerCount=0, para recibir todos los eventos de Touch y no sólo el número >0 que le indiquemos (es un problema bastante común, ya que por defecto esta variable está a 1 y sólo detectará Touch una vez si no lo modificamos).

Events

La estructura de la clase de eventos es similar a las acciones con algún cambio en sus defaultproperties:
// extend UIEvent if this event should be UI Kismet Event instead of a Level Kismet Event
class NULLSeqEvent_Example extends SequenceEvent;
defaultproperties
{
ObjName=”NULLSeqEvent_Example”
ObjCategory=”NULLGame Events”
VariableLinks.Empty //Clean up inherited variables
//Variables are connected to the node from the bottom
VariableLinks(0)=(
ExpectedType=class’SeqVar_Object’, //This is the most generic class, but you can see in Engine/Classes/SeqVar_* other like SeqVar_Int
LinkDesc=”Name of this variable shown in editor”,
PropertyName=NameOfTheVariableInTheCodeSeeAtTheTopOfThisClass,
bWriteable=true) //If you can modify this variable or it is just read only
OutputLinks(0)=(LinkDesc=”Output”) //Different output from the right side of the node
//Exclusive!
MaxTriggerCount=0 //Max number of times this event could be triggered
bPlayerOnly=false //Not only triggeable by PlayerController
}
Para poder lanzar estos eventos, necesitamos indicar qué clases van a utilizarlos:
En defaultProperties de las clases que queremos que los utilicen, añadimos:
SupportedEvents.Add(class’SeqEvent_*’) 
o si queremos controlar el orden de prioridad si utilizan varios eventos...
SupportedEvents(n)=class’SeqEvent_*’ donde n es la prioridad cuanto menor su número, mayor su prioridad (al estar en la estructura de datos SupportedEvents antes).
Hay que tener cuidado al heredar de otra clase si utiliza eventos, ya que podríamos "chafar" algunos de los eventos que utiliza el padre. Utilizad Add en estos casos para concatenar eventos utilizados o bien, reescribir con el nuevo orden todos los eventos que soporta vuestra clase.
Una vez hemos preparado una clase para que reciba los eventos, si es placeable, podremos en Kismet, al tener una instancia de dicha clase en la escena, aplicarle un evento de Kismet propio de ese tipo. Para activarlo, tenemos dos opciones diferentes:
Global, activa todos los eventos de un tipo TriggerGlobalEventClass(class’SeqEvent_*’,self)
Local, activa el evento * del objeto actual TriggerEventClas(class’SeqEvent_*’,self)
Si quieres activar más de un evento, consultad ActivateEventClass (link)
Este es un primer acercamiento a la programación de nodos de Kismet. Los Conditionals tienen un funcionamiento similar, aunque de base son menos utilizados.

viernes, 13 de diciembre de 2013

U0) Bases de programación en Unity

¡Muy buenas!

Ya estamos por aquí a puntito de comenzar con Unity. No entraré en detalles de cómo lo podéis instalar o del propio interfaz. Me centraré en algunas clases prácticas de cómo realizar un pequeño juego de plataformas con Unity 3D. Desde luego, va a ser un viaje sencillo y con clases bastante básicas. Muy posiblemente entre en algunos detalles de dilemas con los que os podéis encontrar a la hora de tomar decisiones de cómo realizar ciertas cosas con código. Tomadlo como lo que es, una de las opciones que tenéis disponibles y recordando, que procuraré ser lo más claro y sencillo posible (tampoco quiero profundizar en estructuras que puedan resultar complicadas si no es necesario).

Por supuesto, recordad una cosa: Hay mil formas de hacer las cosas. Lo importante es que os encontréis cómodos a la hora de trabajar con el código que estáis haciendo y que, vosotros mismos, os deis cuenta de aquellas estructuras o formas de realizar vuestro código que se adecuen a aquello que queréis crear. Y recordad una gran máxima: "Si parece que funciona, funciona". No os volváis locos con la perfección, ni intentéis crear el código perfecto. Simplemente porque creo que es como buscar el Santo Grial. Simplemente, cread aquello que necesitéis y si más adelante necesitáis más modularidad en el código o cosas más complejas, vosotros mismos os daréis cuenta de ello. Tampoco os obsesionéis con el rendimiento: Primero que las cosas funcionen y después le dais al coco de cómo se puede hacer más eficiente, SÓLO si os hace falta. No tiene sentido si vuestro juego va con la fluidez que deseáis poneros con quebraderos de cabeza cuando os podéis poner a trabajar en otras partes del mismo.

Organizando las cosas. Jerarquía de carpetas

Es muy importante tener las cosas en un sitio donde, más adelante, podáis encontrarlas fácilmente. Igual que en UDK colocábamos un prefijo en todos los scripts para saber que son nuestros, aquí tendremos que trabajar con muchas otras cosas que no son código, y conviene que os creéis una jerarquía de carpetas donde situar cada cosa, del mismo modo que crear una nomenclatura clara para la denominación de cada cosa (normalmente indicada en un documento técnico de diseño).
Mi recomendación:

En la jerarquía de carpetas (La pestaña Project en Unity), podéis crear bajo la carpeta Assets todas las carpetas que deseéis. Allí crearía algunas básicas...

- Scenes, para guardar las escenas de juego en su interior.
- Code, para colocar vuestros scripts de código y clases. Podéis subdividir tanto como queráis (Navigation, CameraControl, Abstract, Shaders...)
- Materials, para colocar materiales. Se recomienda jerarquizar subcarpetas en su interior también.
- Models, para colocar mallas importadas en FBX o en el formato que deseéis.
- Textures, para texturas.
- Prefabs, para colocar, cuando veamos lo que son más adelante, los prefabs que deseemos. 

GameObject, la base de Unity


Muy bien, pues como podéis ver, Unity funciona con escenas que debéis guardar en disco (como si fueran los mapas de UDK). Arriba, en la barra de opciones tenemos GameObject. Si pinchamos en él, nos aparecerá Create Empty o Create Other. Básicamente, la primera opción nos generará un GameObject desnudo. ¿Y qué es un GameObject? Pues es una entidad en Unity que tiene un Componente llamado Transform que es el encargado de darle una posición en el espacio y una orientación.
¿Interesante verdad? Se parece mucho a los ActorComponents de UDK. Pero no es igual. Como vemos, podemos pinchar en crear un GameObject y nos aparecerá un gizmo con las coordenadas del espacio en la pestaña de Scene y también en la pestaña de Hierarchy. Si lo pinchamos o seleccionamos tanto en la pestaña de Scene o de Hierarchy, en la pestaña inspector normalmente a la derecha, nos encontraremos con que aparece el componente transform el cual podemos toquetear al gusto.
Bien, visto esto, ya tenemos claro dónde podemos encontrar las cosas en Unity:
En la pestaña Hierarchy aparece el árbol de escena, con todos los objetos e hijos de estos (Que aparecerán tabulados bajo sus padres y que podremos desplegar pinchando en la flecha que tendrán los mismos a su izquierda).
En la propia escena, en la pestaña Scene, podemos seleccionar elementos. 
En la pestaña Inspector, veremos los componentes que componen cada GameObject y sus variables "visibles" para poder modificarlas.

Dicho esto, vemos la primera gran utilidad de Unity. Los Componentes no son más que clases de código que heredan de una clase especial llamada MonoBehaviour que les proporciona los eventos y llamadas a métodos necesarios por el motor. Así mismo, las variables globales de dichas clases, son mostradas por el Inspector y lo mejor: PUEDEN MODIFICARSE EN TIEMPO REAL mientras se ejecuta el juego.

¿Cómo se hace esto? Muy sencillo: Veis que arriba existen botones parecidos a los de un reproductor de música. Efectivamente, el botón con flecha es para comenzar a ejecutar todos los scripts MonobeHaviour de los GameObjects en la escena actual. En la pestaña Scene se mostrará exactamente lo que la cámara principal renderice en ese momento.

Para hacer una pequeña prueba de lo que os comento, basta con crear en la pestaña GameObject->Create Other->Cube. Lo colocamos en la pestaña de Scene en una posición en la que podamos verlo por la cámara. Si no tenemos cámara, es el momento de crear una en la misma pestaña GameObject->Create Other->Camera.
También necesitaremos una luz para poder iluminar nuestra escena: GameObject->Create Other-> Point Light.

Muy bien, ya podemos ejecutar nuestra escena y seleccionando nuestro Cube en Hierarchy, podremos modificar su posición numéricamente, yendo a la pestaña Inspector y modificando su Position dentro de su Componente Transform.

Componentes

¿Cómo añado un componente? Existen algunos ya "creados" en Unity para, por ejemplo, utilizar el motor de físicas con sus colisiones y rigidbody. Así mismo, para poder tener ciertas mallas de figuras geométricas simples.
En la barra superior, tenemos la pestaña Component con un listado de dichos componentes.

Es importante señalar la importancia de los componentes RigidBody, en el apartado Physics así como Box Collider, o Capsule Collider.

¿Cómo creamos nuestros scripts de comportamientos? Podemos hacerlos de muchas maneras. Desde la pestaña de Project, podemos con el botón derecho crear un script de programación (Create X Script). En este curso, utilizaremos C# como lenguaje para nuestros scripts, al tener riqueza de librerías externas para utilizar y al ser un lenguaje que podemos utilizar fuera de Unity al ser utilizado para programación en general. Javascript puede resultar más sencillo, pero para mi gusto está más relacionado con la programación web y es allí donde debe permanecer :)

Nuestro primer contacto con la API de Unity (Script reference) Clase MonoBehaviour

Una vez creéis un script (acordaos de colocarlo en un lugar ordenado dentro de vuestra jerarquía de carpetas para no sembrar el caos), podéis hacer doble click en él y acceder desde el IDE que instala por defecto Unity: Monodevelop. Como veréis es muy sencillo de utilizar y posee herramientas más que de sobra para comenzar con Unity. Se puede utilizar otros IDEs, y se deben especificar en la barra de arriba, Edit->Preferences->External Tools. Monodevelop está perfectamente configurado e integrado para trabajar con Unity de modo que a vuestra elección queda utilizar otro (aunque nosotros utilicemos Monodevelop para este curso, que además es gratuito :D ).

Bien, la misma jerarquía de assets que tenemos en el proyecto de Unity debería apareceros en Monodevelop, de modo que podéis buscar y crear scripts desde el propio IDE. Además, se puede debuggear "attachando" al hilo de ejecución de Unity para que lo inspeccione (la tecla que tiene una flecha grande, que al pulsarla os indicará con el hilo que deseáis trabajar).

Ahora si llega el momento de conocer dónde se encuentra la documentación de referencia de Scripts para Unity: http://docs.unity3d.com/Documentation/ScriptReference/

Lo primero que haremos será echarle un vistazo a la clase MonoBehaviour:

En el apartado Messages podemos ver todos los eventos a los que llama el motor y el momento en el que lo hace documentado. Los más básicos que nos interesan ahora mismo son:

- Awake, Como nos indica la documentación, se llama después de que se hayan cargado todos los objetos en escena, de modo que es un sitio seguro para buscar otros objetos en la escena y guardarnos una referencia de ellos. Además, sólo se llama una vez en todo el tiempo de vida del objeto. Se llama aunque el componente no esté activo por defecto en la escena (todos los componentes tienen un tick en el inspector para activarlos o desactivarlos). Ideal para utilizar como "Constructor" o método que configura referencias y valores de variables y atributos.
- Start, Llamado antes de que se realice la primera llamada al evento Update. Del mismo modo que Awake, se llama sólo una vez en el tiempo de vida del objeto. Pero no se llamará si el script no está activo en el momento de inicialización. Ideal para utilizar como "Constructor" o método que configura referencias y valores de variables y atributos.
- Update, Llamado cada "tick" del motor o frame. Mediante la clase estática Time.deltaTime, no deja de ser la clase principal para colocar en el "loop" de ejecución del programa. Aquí se realizará la mayor parte del comportamiento del script.
- LateUpdate, Como su nombre indica, se ejecuta después de las llamadas de Update. Es interesante su uso, por ejemplo, para recolocar cámaras que siguen ciertos objetos, ya que éstos se habrán movido con anterioridad en la llamada a Update.
-FixedUpdate, Recomendado su uso como un Update cuando tenemos que interactuar o utilizar temas de físicas.
-OnEnable, Cuando el componente se activa, se hace una llamada a este método.
-OnLevelWasLoaded, Como su nombre indica, al cargar una escena, se realiza una llamada a este método. Muy útil para recorrer todos los objetos de la escena y cargar valores que se deseasen haber guardado (SaveGameManager).

Además de todos los eventos que se llaman, también tenemos variables importantes para el comportamiento de los Componentes:

- enabled, booleano que indica si está activo o no dicho componente.
- renderer, referencia al primer componente de tipo Renderer que tenga el GameObject si tiene alguno, si no será null
- rigidbody, referencia al componente RigidBody del GameObject si tiene alguno, si no será null
- transform, referencia al componente Transform del GameObject (con el que le damos la posición, posición local y rotación del GameObject)
- name, nombre del GameObject

Mi primer script de navegación, ¿Qué componentes voy a necesitar?

Para realizar un primer ejemplo de navegación, necesitaremos:

- Components->Physics-> RigidBody
- Components->Physics-> Capsule Collider 

Si al añadir el RigidBody se os coloca ya un Collider, podéis con el botón derecho en el Inspector, eliminar dicho componente con Remove Component.

Al igual que hemos hecho antes, es interesante ver el funcionamiento de RigidBody y de los Collider. 

En Monobehaviour debemos consultar para las colisiones:

- OnCollision*
- OnTrigger*

Y en RigidBody es importante leer la documentación completa de cómo funciona.

Un poco de mates. Métodos y funciones interesantes antes de empezar.

Para empezar, tenemos que tener claros algunos métodos y funciones utilizadas en las clases Vector3 y Quaternion. Estas clases son las utilizadas para colocar puntos y vectores en el espacio y rotaciones respectivamente.
Para ello, debemos volver de nuevo al Script reference:

Funciones matemáticas con float.

No hay mucho que explicar. Todo son funciones matemáticas para trabajar con float. Entre las más utilizadas están Sqrt para hacer raíces cuadradas (recordad no abusar ya que es costoso aunque la implementaciónd e Unity espero sea eficiente y aproximada, también tened cuidado con eso), así como el clásico Lerp para realizar interpolación lineal entre dos valores. Encontraremos Lerp también en la clase Vector más adelante y en Quaternion.

Funciones matemáticas con Vectores de 3 dimensiones:

El método Normalize() hace que el Vector3 desde el que lo llamemos, se quede normalizado. Si es demasiado pequeño, se convertirá en un Vector3 vacío (0,0,0). También podemos devolver una copia del mismo utilizando su variable normalized.
Es interesante destacar la interpolación lineal entre vectores con Lerp http://docs.unity3d.com/Documentation/ScriptReference/Vector3.Lerp.html

Mediante RotateTowards, utilizaremos un Vector como una dirección que como un punto en el espacio. Claramente su uso nos vendrá bien para poder orientar nuestro personaje mediante vectores (luego veremos con Quaternion cómo conseguir un vector o cómo convertir un vector dirección en un Quaternion).

El código que aparece es exactamente de lo que os hablo a la hora de calcular y modificar rotaciones de nuestro personaje.

Funciones y métodos con Quaterniones

Para representar rotación, Unity utiliza cuaterniones. Las funciones Euler, Lerp y LookRotation (así como el método SetLookRotation) son las más comúnmente utilizadas. La primera sirve para rotar con grados euler, la segunda para realizar interpolación lineal entre dos Quaterniones (que también puede hacerse esféricamente mediante Slerp) y LookRotation nos va a devolver un Quaternion, a partir de un Vector dirección y un vector que apunte hacia arriba.

Al contrario que UDK, debemos darnos cuenta de que las coordenadas de mundo, están colocadas de diferente forma. El eje Y es el correspondiente a up o altura, mientras que el eje Z es el correspondiente a forward (o mirar hacia delante). 

Recoger el Input de teclado o periféricos.

Unity tiene una clase estática llamada Input (http://docs.unity3d.com/Documentation/ScriptReference/Input.html) donde almacena cada frame los estímulos recibidos por periféricos. Se pueden configurar "Buttons" y "Axis" (hacer que esos estímulos hagan referencia a un nombre específico). Buttons es, como su nombre indica, para pulsaciones (tiene eventos propios para comprobar si el botón se ha pulsado, si se ha dejado de pulsar, para consultar su valor... GetButtonDown, GetButtonUp), Axis para simular ejes como crucetas de mandos. Un Button o un Axis no deja de ser un valor entre dos float (normalmente -1 y 1 para los Axis y 0 y 1 para los button) que dependen del periférico en cuestión relacionado. Podemos acceder a teclas directamente con GetKey* conociendo los códigos de las letras por defecto que podemos utilizar (http://docs.unity3d.com/Documentation/ScriptReference/KeyCode.html). Si trabajamos en otras plataformas, debemos consultar la documentación para conocer qué KeyCode corresponde a dichos inputs.
Para configurar cómo se tratarán los valores de los inputs, debemos buscar en la barra Edit->Project Settings->Input. Ahí nos aparecerá en la ventana de Inspector los valores de los input por defecto que tenemos configurados. 
Podemos obtener el valor sin modificar por Unity de los Input utilizando funciones que contienen la palabra Raw como GetAxisRaw. Esto es interesante si queremos realizar nuestra propia modificación de los valores de Input (los mandos de consola por ejemplo tienen una DeadZone, que es un área en la que los valores que nos devuelven pueden no ser fiables ya que por el paso del tiempo, la propia fabricación del mando, pueden tener desviados su centro). Es muy interesante el siguiente artículo en caso de que queráis aventuraros a realizar vuestro propio "wrapper" clase estática con vuestro tratamiento de input (cosa harto interesante si vais a trabajar vuestro proyecto en diferentes plataformas). (http://gamasutra.com/blogs/JoshSutphin/20130415/190541/Doing_Thumbstick_Dead_Zones_Right.php)

Manos a la obra. Scriptando que es gerundio.






viernes, 29 de noviembre de 2013

8) Vamos con el HUD. Sistema de debug por consola (si no tenéis debugger o para consultar estados de clases de un sólo golpe)

¡Muy buenas!


Hoy nos centraremos en crear un HUD sencillito para el ejercicio que vamos arrastrando de recolección de monedas y ya de paso, crearemos un pequeño sistema de debug por consola para mostrar en el HUD valores que creamos importantes en ciertas clases. Por supuesto, es opcional y una herramienta más para aquellos que no tengan un debugger en condiciones configurado para trabajar con UDK (cosa que parece ser más frecuente de lo que debería).

En primer lugar analizamos la función más importante de la clase HUD (development/Src/Engine/Classes/HUD.uc):

HUD.uc

/**
 * The Main Draw loop for the hud.  Gets called before any messaging.  Should be subclassed
 */
function DrawHUD()
{
local vector ViewPoint;
local rotator ViewRotation;

if ( bShowOverlays && (PlayerOwner != None) )
{
Canvas.Font = GetFontSizeIndex(0);
PlayerOwner.GetPlayerViewPoint(ViewPoint, ViewRotation);
DrawActorOverlays(Viewpoint, ViewRotation);
}
PlayerOwner.DrawHud( Self );
}

Como véis, debemos hacer un override de dicho método para pintar lo que sea necesario en nuestro HUD. También vemos que podemos delegar parte del HUD a PlayerController (llamada PlayerOwner.DrawHud(self) al final). Éste a su vez, delegará en DrawHUD de Pawn y de PlayerInput (que veremos más adelante su uso). También es importante fijarnos que pinta el HUD dependiendo del lugar en el que está mirando la cámara y demás, y por tanto, si queréis experimentar haciendo HUDs raros, tenéis que meter mano por aquí también.

Aquí tengo un ejemplo de utilización de pintado de HUD de un antiguo ejercicio. En él, se pinta un reloj (una textura y un rectángulo de colores según su estado, para indicar el tiempo que queda (¿Os suena de algo verdad?).

ESATHUD.uc

class ESATHUD extends HUD;


var CanvasIcon clockIcon;
var Texture2D clockTex;
var int clock;
var int FullClock;
var int MediumClock;
var int LowClock;



function ResetTimer(int newValue, int current)
{

FullClock=newValue;
MediumClock=FullClock * 0.66f; //2/3 Full
LowClock=FullClock * 0.33f; //1/3 de Full
SetTimer( 1, true, 'ClockTimer');
clock = current;
}

function ClearMyTimers()
{
ClearTimer('ClockTimer');
}

function ClockTimer()
{
//`log("Clock: "@clock@" ; FULL: "@FullClock);
if(clock > 0)
{
clock--;
}
}

function DrawHUD()
{
super.DrawHUD();

//Canvas.DrawIcon(clockIcon, 0, 0);
//Colocas "El cursor" de dibujo dónde deseas dibujar en la pantalla

Canvas.SetPos(100,15);
Canvas.SetDrawColor(255, 255, 255); // White
Canvas.DrawTile(clockTex,32.0f,32.0f,0.0f,0.0f,256.0f,256.0f,,,);

Canvas.Font = class'Engine'.static.GetLargeFont();
Canvas.SetDrawColor(255, 255, 255); // White
Canvas.SetPos(70, 15);

Canvas.DrawText(clock);

if(clock < LowClock)
{
Canvas.SetDrawColor(255, 0, 0); // Red
}
else if (clock < MediumClock)
{
Canvas.SetDrawColor(255, 255, 0); // Yellow
}
else
{
Canvas.SetDrawColor(0, 255, 0); // Green
}

Canvas.SetPos(200, 15);
Canvas.DrawRect(20 * clock, 30); 

}


/**
* Puedes llamar a la llamada desde la consola ShowDebug y la etiqueta que quieras. En este caso
* Como puedes ver más abajo, existe la etiqueta GameInfo, que llama a una función de display debug
* que tiene tu GameInfo
* Para desactivar el mostrado de debug basta con colocar en consola el comando ShowDebug
*
*/
function ShowDebugInfo(out float out_YL, out float out_YPos)
{

if( ShouldDisplayDebug( 'GameInfo' ) )
{
ESATGameInfo(WorldInfo.Game).DisplayDebug(self, out_YL, out_YPos);
}
else
{
if( ShouldDisplayDebug( 'Pawn' ) )
{
(ESATGameInfo(WorldInfo.Game).GetALocalPlayerController().Pawn).DisplayDebug(self, out_YL, out_YPos);
}
else
{
super.ShowDebugInfo( out_YL, out_YPos );
}
}
}

DefaultProperties
{
clockTex=Texture2D'ESAT-Assets.Time'
clockIcon=(Texture=Texture2D'ESAT-Assets.Time')
FullClock=30

//IMPORTANT to post render actors!!
bShowOverlays=true
}


Como podéis ver en DrawHUD, lo primero que hacemos es llamar a la clase padre (también podemos copiar y pegarla antes o en su defecto modificar lo que necesitemos). El funcionamiento del HUD es bastante simple. Consta de un Canvas en el que debemos colocar un cursor en la posición que deseemos pintar o escribir (Canvas.SetPos(100,15);). Una vez allí, podemos pintar tiles con DrawTile, texto con DrawText, iconos con DrawIcon, rectángulos con DrawRect. Podemos echarle un vistazo a la clase base Canvas.uc para ver todas las posibilidades. 

La función ShowDebugInfo, se utiliza como sistema de debug en otras clases. Por ejemplo, la función en la clase GameInfo de este mismo proyecto es esta:

ESATGameInfo.uc

[...]

simulated function DisplayDebug(HUD HUD, out float out_YL, out float out_YPos)
{
   local string T;
   local Canvas Canvas;
 
   Canvas = HUD.Canvas;
 
   out_YPos += out_YL;
 
   Canvas.SetPos(4, out_YPos);
   Canvas.SetDrawColor(255,0,0);
 
   T = "GameInfo [" $ GetDebugName() $ "]";
   Canvas.DrawText(T, FALSE);
   out_YPos += out_YL;
   Canvas.SetPos(4, out_YPos);
 
   Canvas.SetDrawColor(255,255,255);
 
   Canvas.DrawText("Score:" @ Score @ "InternalDataValue:" @ InternalDataValue, FALSE);
   out_YPos += out_YL;
   Canvas.SetPos(4,out_YPos);
 
 
   Canvas.DrawColor.B = 255;
   Canvas.DrawText(" STATE:" @ GetStateName(), FALSE);
   out_YPos += out_YL;
   Canvas.SetPos(4,out_YPos);

Canvas.DrawColor.B = 255;
   Canvas.DrawText(" bWaitingToStartMatch:" @ bWaitingToStartMatch, FALSE);
   out_YPos += out_YL;
   Canvas.SetPos(4,out_YPos);

Canvas.DrawColor.B = 255;
   Canvas.DrawText(" bUsingArbitration:" @ bUsingArbitration, FALSE);
   out_YPos += out_YL;
   Canvas.SetPos(4,out_YPos);

}

[...]

Como véis, se pintan en el HUD los valores que deseemos ver más adelante al abrir la consola de comandos (tabulador al iniciar el juego) y escribiendo ShowDebug y el nombre de la clase que hayamos indicado en la clase HUD.

¡Ejercicio!

Ahora toca realizar un HUD para nuestro juego de monedas. Una vez realizado, ya estará la funcionalidad básica del proyecto que se os pedirá inicialmente (si, mis alumnos no se pueden quejar, que el aprobado lo tienen con leer y practicar un poquillo, que aunque parezca mentira, ¡a veces no hacéis ni eso pillastres!).