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…