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

No hay comentarios:

Publicar un comentario