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!).

viernes, 22 de noviembre de 2013

5) Introducción a estados en UDK. PlayerMove y ProcessMove ¿Cómo modifico cómo se mueve mi Pawn? Adventures

¡Muy buenas!

En esta entrada analizaremos cómo poder modificar el movimiento de nuestro Pawn. Para ello, debemos explorar antes cómo funcionan básicamente los estados integrados en UDK.

Lo primero que hacemos es, irnos a la carpeta Development/Src/Engine/Classes/PlayerController.uc

Una vez allí buscamos "State PlayerWalking"

Allí nos encontraremos el estado que tiene un PlayerController por defecto cuando estamos en el juego. Como vemos, tiene una serie de métodos internos propios independientes de los que hay fuera de las llaves que lo engloban. El esqueleto básico sería algo así:


state PlayerWalking
{
//ignores SeePlayer, HearNoise, NotifyBump, TakeDamage, PhysicsVolumeChange, NextWeapon, PrevWeapon, SwitchToBestWeapon;

event NotifyPhysicsVolumeChange( PhysicsVolume NewVolume )
{
}

//Called when the state begins! Useful when we want to setup things before it starts

event BeginState(Name PreviousStateName)
{
//Acordaos de llamar al método padre para que funcione el estado bien!!!! (Ver PlayerController.uc)
}

//Called when the stateends! Useful when we want to setup things before another one starts

event EndState(Name NextStateName)
{
//Acordaos de llamar al método padre para que funcione el estado bien!!!! (Ver PlayerController.uc)
}

Begin:
}

La nomenclatura sería algo como: [auto] state NameOfTheState [extends NameOfTheParentState]

El flag "auto" se puede colocar antes de declarar el estado para indicar que es un estado inicial. Como ya podéis intuir, podremos pasar de un estado a otro. Ésta acción se realiza mediante GoToState('NombreDelEstado');

Cuando una clase se encuentra en un estado, puede llamar normalmente a los métodos que tiene dentro, pudiendo encapsular de este modo, diferentes comportamientos para los mismos métodos. 

BeginState y EndState, son llamados internamente cada vez que se entra en el estado y se sale de él respectivamente, con información del estado del que venimos y del que vamos. Nótese que los parámetros que reciben no son string, sino Name. La nomenclatura de name es con comilla simple ' ' en lugar de comillas dobles " ".

Por último, debajo observamos un Begin: Aquí comienza una especie de scripting dentro de los hilos de ejecución que pueden darnos más de un quebradero de cabeza. Sólo ciertos comandos y directivas son permitidas en este espacio. Begin no deja de ser una etiqueta pudiéndose desplazarse entre etiquetas mediante goto 'NombreDeEtiqueta'. Hay que tener cuidado con esto, ya que podemos crear un bucle infinito en la ejecución (claramente con goto 'Begin' xD). Igualmente se usan otros comandos como Sleep(númeroDeFramesAEsperar);

Como última mención, si tenéis métodos externos al ámbito del estado que se llaman igual que uno de los internos, se pueden acceder desde los métodos del estado a los exteriores mediante Global.NombreDelMétodo(ParametrosDelMetodo).


Una vez introducidos los estados, pasamos a analizar el estado PlayerWalking:
Añadimos en nuestro NULLPlayerController lo siguiente:

NULLPlayerController.uc
//NOTA: Este símbolo lo coloco para que sepáis que puede haber código donde se encuentre [...]
//En este caso, podéis colocar dónde queráis este pedazo de código, excepto después de DefaultProperties //o dentro de su ámbito. Es decir, dentro del ámbito de la clase, pero fuera de la inicialización de variables

state PlayerWalking
{
ignores SeePlayer, HearNoise, Bump;

function ProcessMove(float DeltaTime, vector NewAccel, eDoubleClickDir DoubleClickMove, rotator DeltaRot)
{
if( Pawn == None )
{
return;
}

if (Role == ROLE_Authority)
{
// Update ViewPitch for remote clients
Pawn.SetRemoteViewPitch( Rotation.Pitch );
}

Pawn.Acceleration = NewAccel;

CheckJumpOrDuck();
}

function PlayerMove( float DeltaTime )
{
local vector X,Y,Z, NewAccel;
local eDoubleClickDir DoubleClickMove;
local rotator OldRotation;
local bool bSaveJump;

if( Pawn == None )
{
GotoState('Dead');
}
else
{
GetAxes(Pawn.Rotation,X,Y,Z);

// Update acceleration.
NewAccel = PlayerInput.aForward*X + PlayerInput.aStrafe*Y;
NewAccel.Z = 0;
NewAccel = Pawn.AccelRate * Normal(NewAccel);

if (IsLocalPlayerController())
{
AdjustPlayerWalkingMoveAccel(NewAccel);
}

DoubleClickMove = PlayerInput.CheckForDoubleClickMove( DeltaTime/WorldInfo.TimeDilation );

// Update rotation.
OldRotation = Rotation;
UpdateRotation( DeltaTime );
bDoubleJump = false;

if( bPressedJump && Pawn.CannotJumpNow() )
{
bSaveJump = true;
bPressedJump = false;
}
else
{
bSaveJump = false;
}

if( Role < ROLE_Authority ) // then save this move and replicate it
{
ReplicateMove(DeltaTime, NewAccel, DoubleClickMove, OldRotation - Rotation);
}
else
{
ProcessMove(DeltaTime, NewAccel, DoubleClickMove, OldRotation - Rotation);
}
bPressedJump = bSaveJump;
}
}

event BeginState(Name PreviousStateName)
{
DoubleClickDir = DCLICK_None;
bPressedJump = false;
GroundPitch = 0;
if ( Pawn != None )
{
Pawn.ShouldCrouch(false);
if (Pawn.Physics != PHYS_Falling && Pawn.Physics != PHYS_RigidBody) // FIXME HACK!!!
Pawn.SetPhysics(Pawn.WalkingPhysics);
}
}

event EndState(Name NextStateName)
{
GroundPitch = 0;
if ( Pawn != None )
{
Pawn.SetRemoteViewPitch( 0 );
if ( bDuck == 0 )
{
Pawn.ShouldCrouch(false);
}
}
}


Begin:
}

//[...]


Como podéis ver, hemos copiado directamente el estado de la clase PlayerController.
El funcionamiento principal reside en los métodos PlayerMove y ProcessMove.

PlayerMove

Inicialmente llama a GetAxes(Pawn.Rotation,X,Y,Z); que divide la rotación del Pawn en tres vectores X, Y, y Z que contendrán los valores de su rotación como vectores. Después, crea una variable NewAccel, con la que, utilizando PlayerInput (clase que tiene el control del mando y teclado), accediendo a aForward y aStrafe (Movimiento hacia delante y lateral respectivamente), rellena unos valores que indicarán hacia donde se mueve el Pawn. 

Después podemos ver que llama a actualizar a la rotación UpdateRotation que se encarga de Pitch y Roll de la cámara así como de llamar a actualizar la rotación de nuestro Pawn (Método FaceRotation). 

También realiza algunas comprobaciones para red y finalmente llama a ProcessMove para dar los últimos retoques a la nueva aceleración del Pawn. Por defecto, Pawn.Acceleration = NewAccel; le da la aceleración tal cual la hemos creado anteriormente en PlayerMove.

Viendo este proceso, ya podemos atacar al sistema de movimiento de nuestro personaje, haciendo que se mueva como nosotros deseamos. Del mismo modo, podemos ver que, al cambiar a tipos de volúmenes se llama a un método event NotifyPhysicsVolumeChange( PhysicsVolume NewVolume )

Podemos trabajar con nuevos estados y maneras de movernos haciendo uso de todas estas herramientas.

Un buen ejercicio ahora, sería el dar controles de SideScroller y realizar una cámara acorde.

3) EJERCICIO Cámaras (Con solución al final)

¡Muy buenas!

Al llegar al final del apartado 3, ¡ya podéis hacer vuestros experimentos con cámaras! Y por ello os propongo un reto:

Tratar de realizar una cámara que siempre siga lateralmente al Pawn, como si fuera de una cámara Sidescroller pero con el Pawn pudiendo moverse libremente.

Para ello os dejo algunas anotaciones:

- Las variables locales en UnrealScript se colocan siempre justo debajo de un método o función. No podéis declararlas justo antes de dónde las necesitéis. Unreal es así de caprichoso... ¡Por cierto! Se declaran como otra variable pero en vez de poner var, se pone local ;)

- Podéis realizar "castings" de tipos en UDK, pero se realiza de manera inversa a como se realiza en C.
Se coloca la clase que queréis convertir primero y entre paréntesis a continuación, la clase a convertir. De este modo, os dejo una de las primeras pistas: Vector (Rotator).

Podéis coger la rotación del Pawn para ver dónde está mirando y convertirlo a un Vector con coordenadas de mundo para poder colocar la cámara dónde deseemos. Del mismo modo, podemos añadir a la rotación del Pawn un valor (offset en grados).

Os recuerdo que la clase Rotator tiene tres variables: Roll, Pitch y Yaw. Yaw es la variable que necesitáis para rotar "alrededor" de un objeto. Igualmente, las unidades de dicho valor están en Unreal Rotator, pero tranquilos, que existe una constante global por la que podéis multiplicar una variable que tenga esas unidades para pasarlas a grados y viceversa. Las constantes son: unrrottodeg y degtounrrot.

- Existe un método llamado Normal que recibe como parámetro un Vector y os lo "escupe" normalizado :D

¡A darle al coco y las mates!


------------------------------------------------------------------------------------------------------------

























¿Ya lo tienes? ¿Seguro? No hagas trampas ehhhh





























******************SOLUCIÓN*********************

Bueno, ahora si. En primer lugar, le decimos a NULLPlayerController.uc que queremos usar nuestra nueva cámara:

NULLPlayerController.uc

class NULLPlayerController extends PlayerController
    //Como vimos antes, hace referencia a un fichero de
    //configuración del que se podrá configurar variables
    //Bajo el apartado [NULLGame.NULLPlayerController]
    config(Game);
 
defaultproperties
{
CameraClass=class'NULLGame.NULLCameraSideScrollerFree'

}


Y ahora la clase nueva de cámara:

NULLCameraSideScrollerFree.uc

class NULLCameraSideScrollerFree extends Camera;
    /**
    * Query ViewTarget and outputs Point Of View.
    *
    * @param    OutVT        ViewTarget to use.
    * @param    DeltaTime    Delta Time since last camera update (in seconds).
    */
    function UpdateViewTarget(out TViewTarget OutVT, float DeltaTime)
    {

local Vector pawnFacingVector;
local Rotator tempRotator;

    // Don’t update outgoing viewtarget during an interpolation
    if( PendingViewTarget.Target != None && OutVT == ViewTarget && BlendParams.bLockOutgoing )
    {
    return;
    }

//Cogemos la rotación de nuestro Pawn para darle un offset de 90 grados
tempRotator = OutVT.Target.Rotation;
//Le damos un offset en Yaw (alrededor del Pawn)
tempRotator.Yaw -= 90.0f*DegToUnrRot;

//Convertimos la rotación del Pawn (Target en nuestro caso) y la convertimos a coordenadas de mundo
pawnFacingVector = Vector(tempRotator);
pawnFacingVector = Normal(pawnFacingVector);

//Le damos la vuelta y lo alargamos una distancia que deseemos. Podemos usar una variable para ello
//(Mucho más correcto) que darle un valor a pelo
pawnFacingVector *= -250.0f;

//Ahora tenemos el vector que nos indica dónde debe colocarse la cámara
//Debemos añadir a dicha posición la posición de nuestro Pawn (Target)
OutVT.POV.Location = OutVT.Target.Location + pawnFacingVector;

//Ahora tenemos que mirar siempre hacia nuestro Pawn!!!
OutVT.POV.Rotation = Rotator(OutVt.Target.Location-OutVT.POV.Location);

    // Apply camera modifiers at the end (view shakes for example)
    ApplyCameraModifiers(DeltaTime, OutVT.POV);

    }

    DefaultProperties
    {
    }





jueves, 21 de noviembre de 2013

7) WorldInfo y GameInfo. Spawneando entre la vida y la muerte (y las reglas de juego)

¡Muy buenas!

Ya estamos aquí otra vez para explicar un poquito otra de las partes que tenemos en UDK y que es muy importante: WorldInfo y GameInfo.

Como bien nos dice Mougli (http://www.moug-portfolio.info/game-info/), GameInfo es el corazón denuestro juego, y debe poseer las reglas de "juego". Entre ellas, se encontrará la manera de spawnear (crear una instancia de nuestro Pawn y PlayerController) y dónde debe realizarlo. Así mismo, determinará cuando termina un juego y cuando no. El término juego en este caso, se refiere a todo lo que ocurre entre la carga de un mapa y otro. ¿Porqué? Porque toda información en GameInfo desaparece con la carga. Deberemos por tanto, tener esto en cuenta para la realización de nuestro juego.

Como curiosidad y dato importante, existe la clase WorldInfo que es la encargada de guardar todo lo relacionado con un mapa: Secuencias de Kismet, Actores, NavigationPoints... Contiene el árbol de escena del mapa así como una instancia de GameInfo. Este planteamiento de programación encaja perfectamente con la estructura utilizada principalmente por este motor: Shooter en primera persona multiplayer. De este modo, tendremos un GameInfo con información de DeathMatch, con información de Capturar la bandera, etc... Y podremos cambiar entre ellos de mapa en mapa y podremos identificar que tipo de GameInfo cargar con el sistema de prefijos en los mapas que tiene UDK (en los ficheros de configuración podemos asignar que prefijos en los nombres de los mapas cargan qué tipos de GameInfo).

Métodos y acceso a GameInfo desde cualquier lugar

Como os he indicado más arriba, desde WorldInfo podéis acceder a cualquier actor de la escena. Pero por supuesto, con un coste. No es recomendable abusar de estas llamadas, pero tampoco escatimar en su llamada si es realmente necesario. El rendimiento deberíais estudiarlo tras hacer que las cosas funcionen, así que, de momento mi recomendación es que no os volváis demasiado locos con esto.

Con la llamada GetWorldInfo() os devolverá la instancia de WorldInfo actual. Desde allí o bien, si tenéis visibilidad directa de WorldInfo, podéis obtener Game (que es el GameInfo actual, quedando del siguiente modo: WorldInfo.Game). Está claro, que si queréis acceder a métodos de vuestro GameInfo particular, por ejemplo NULLGameInfo, deberéis realizar un casting: NULLGameInfo(WorldInfo.Game).

También podéis recorrer todos los controladores y actores desde WorldInfo usando un Foreach y las estructuras de datos WorldInfo.AllControllers y WorldInfo.AllActors. Os recomiendo echarle un vistazo a la clase WorldInfo en caso de que necesitéis acceder desde una clase a algo de la que no tenéis referencia (Por ejemplo, desde GameInfo no tenéis referencia del jugador).

Más información en: http://wiki.beyondunreal.com/UE3:WorldInfo_%28UDK%29


El flow de ejecución

Antes de ponernos manos a la obra, es interesante leer el artículo de la UDN que nos indica el flow de ejecución de UDK.  (http://udn.epicgames.com/Three/UnrealScriptGameFlow.html)

Voy a seguir los pasos de Mougli para explicaros un poco el funcionamiento de esta clase y su utilización. ¡Gracias Mougli!

Propiedades notables en GameInfo.uc

Mougli nos hace mención de todas estas, ya que algunas de ellas las podemos utilizar dependiendo del tipo de juego que queramos realizar. Basta con echar un vistazo a la clase base GameInfo para buscar referencias sobre las mismas y su utilización.

bPauseable
bTeamGame
bDelayedStart
GameDifficulty
MaxSpectatorsAllowed
MaxPlayersAllowed
GoalScore
MaxLives
TimeLimit

SetGameType() de GameInfo.uc e InitGame()


Pues como su nombre indica, le sirve al motor internamente para elegir que tipo de clase se debe utilizar. Por defecto, si le echamos un vistazo, llama al GameInfo utilizado por defecto en los ficheros de configuración (DefaultGame.ini). Igualmente, si utilizamos el sistema de prefijos, un poco más abajo en ese mismo fichero, se seleccionará el juego por defecto según el tipo de mapa.

Por otro lado, tenemos el evento InitGame que es llamado antes que cualquier PreBeginPlay de cualquier actor dentro del motor. Aquí se configura lo necesario con respecto al tipo de juego y podemos utilizarlo para spawnear clases de ayuda. En mi caso, utilizo dicha función para comprobar si estoy utilizando una partida guardada (Siguiendo las directrices del UDKGem de guardado de partidas con JSON aquí: http://udn.epicgames.com/Three/DevelopmentKitGemsSaveGameStates.html). Si se desean realizar diferentes tipos de juego, es ahí donde se debe modificar la función. Podéis echar un vistazo al código del método en la clase base GameInfo del motor.


Estados en la clase GameInfo.uc


Si observamos el código de la clase GameInfo, veremos que contiene diferentes estados. Por defecto (Auto state) se llama al estado PendingMatch. Dicho estado tiene su propia función StartMatch que es la encargada de avisar a todos los actores que la partida comienza. Podéis modificar cuanto queráis a la hora de crear nuevos estados o simplemente, por defecto, utilizar este si no queréis complicaros la vida. Básicamente, este estado termina llamando a Global.StartMatch() cuando se llama a su método StartMatch(). Vemos lo que nos indica la UDN:

Match Start

The actual game, as in the gameplay that happens from the time the player is spawned until the game ends, is often referred to as a match. This is just a term and has no bearing on the type of games that can be created with Unreal Engine 3.

The match is begun when the StartMatch() function of the gametype is called from the PostLogin() event (also called from StartMatch() andStartArbitratedMatch() in the PendingMatchstate). This function is responsible for spawning the player’sPawns and notifying all of the players that the match has begun. 
function StartMatch()
The actual spawning of the players’ Pawns occurs in the RestartPlayer() function. This is called from the StartHumans() and StartBots() functions, which are in turn called from the StartMatch() function. In addition to spawning the Pawns, this function locates a starting spot, e.g. a PlayerStart Actor, to start the player from. 
function RestartPlayer(Controller NewPlayer)

Básicamente, nos dice que la partida comienza con la llamada StartMatch, que se llama desde PostLogin(). En ese momento, StartMatch se encarga de "spawnear" el Pawn del jugador y notificar a todos los jugadores que ha comenzado la partida. 

En la función RestartPlayer() se realiza dicho "spawneado". Aquí es dónde se analiza, de todos los PlayerStart (En categoría Navigation en ActorClasses del ContentBrowser) que existen, y mediante un algoritmo de puntuaciones, dónde debe spawnear el Pawn y su controlador. Aquí deberemos modificar la funcionalidad para spawnear arquetipos si los vamos a utilizar o bien, como es mi caso, spawnear varios Pawns diferentes que queramos controlar si es que tenemos más de uno. No queda otra que analizar paso a paso qué realiza el método y estudiarlo para ver qué podemos y queremos realizar nosotros. Es por ello que esta entrada, para que no se haga eterna, requiere de un esfuerzo mayor por vuestra parte.
Es importante también destacar el método function Pawn SpawnDefaultPawnFor(Controller NewPlayer, NavigationPoint StartSpot) dónde como podréis comprobar en RestartPlayer, se indica que tipo de Pawn debe spawnearse según que controlador y punto de inicio. Exactamente es esta función en la que se debe cambiar la llamada de Spawn clásica por un Spawn(DefaultPawnArchetype.Class,,,StartSpot.Location,StartRotation,DefaultPawnArchetype); para poder utilizar arquetipos como pawns.

Finalmente, la función function Killed( Controller Killer, Controller KilledPlayer, Pawn KilledPawn, class<DamageType> damageType ) es llamada cuando muere un Pawn. Deberemos filtrar que Pawn es el que ha muerto y de ahí, realizar las acciones pertinentes. Por ejemplo, podemos hacer un Fade Out con la cámara programado y activar un timer a una función (SetTimer(3,false,’EndingGame’);) . De manera sencilla, en ese método EndingGame utilizar el comando de consola PC.ConsoleCommand(“RestartLevel”); Por supuesto, deberéis realizar tanta funcionalidad extra y comprobaciones como vuestro proyecto necesite.

Como véis, este apartado, siendo importante, requiere de un esfuerzo por vuestra parte de investigar en el código y entenderlo conociendo las directrices que sigue UDK y las llamadas realizadas en el flow de código.

Detectando el final de la partida

Cuando hablamos de detectar el final de una partida, ya estamos haciendo un acercamiento a lo que son las reglas de juego de nuestro proyecto. En este apartado intervienen algunos métodos que debemos comentar:

  • EndGame, llama a su vez a CheckEndGame y por ende, nos da la pista de tener que llamar a esta función cada vez que nuestro juego pueda terminar. En el ejemplo que Mougli nos indica, cada vez que recolectamos una moneda en un juego en el que hay que recolectarlas todas o por ejemplo, cuando el tiempo necesario para poder recolectarlas todas se ha terminado. Recibe un PlayerReplicationInfo con la información del jugador ganador si lo hay y un string con el mensaje por la causa de haber finalizado la partida.
  • CheckModifiedEndGame, llamado por CheckEndGame y se encarga de detectar si por los mutadores del mapa se termina la partida. Si no se ha terminado se devuelve un false, en caso afirmativo, se notifica a todos los Controllers que ha terminado el juego (eso en CheckEndGame):  
foreach WorldInfo.AllControllers(class'Controller', P)
P.GameHasEnded();
    • RestartGame, una vez terminada una partida, se encarga de reiniciar el mapa y la partida, o de cambiar de mapa.
    • GetNextMap, es el encargado de devolver el siguiente mapa que se cargará. Por defecto devuelve "" y por tanto, se carga el mismo mapa (ver RestartGame). Recordemos que cargar un nuevo mapa, equivale a recargar un nuevo GameInfo y perder la información almacenada ahí. Una manera de evitar esto, es realizar carga por streaming, que como nos indica Mougli, es lo que sospecha se hace en Gears of War, aunque no lo sepa con certeza absoluta.
    El juego de monedas de Mougli, ¡un ejercicio perfecto!

    Pues así es, es un ejercicio perfecto para poder practicar un poco tanta teoría que hemos visto arriba. Además, que va a ser la parte básica para el ejercicio práctico de mis alumnos. No lo copio aquí, ya que el mérito lo tiene nuestro querido Mougli y por tanto, os redirijo allí para ponernos manos a la obra: 





    miércoles, 20 de noviembre de 2013

    6) Colisiones básicas: Recolectando cosillas, ¡Hazte con todas!

    ¡Muy buenas!

    Llegados a este punto, es el momento de explicar las bases de las colisiones en UDK para poder crear nuestros objetos recolectables. Algunos sabréis que existen clases PickUp y demás, importantes para recoger objetos con un inventario. No entraremos en ello todavía, si no en la manera de colisionar con cosas, basándonos en el ejemplo del apartado anterior de recolección de monedas.

    Procedo a traducir mi antigua entrada en http://theudknown.wordpress.com/2013/03/02/6-lets-pickup-something/ ya que allí se cubre en inglés lo necesario para este punto.

    Procedemos pues a crear un pickup simple para nuestro Pawn. Con respecto al ejercicio anterior, de recolección de monedas, podréis aplicar al final el informar a GameInfo de las monedas que recolectamos y de la posibilidad de terminar la partida. En esta explicación nos centramos en las colisiones.

    Lo más importante para la tarea que nos atañe es la colocación de las "flags" de colisión correctamente. Si utilizamos StaticMeshComponents con colisiones, tendremos que asegurarnos que sus flags de colisión sean correctas. Nuestro pickup va a heredar de la clase base Actor.uc

    Las dos variables bool (esas flags de las que hablo) más importantes para la colisión tanto en Actor.uc como en los componentes de malla que utilicemos son las siguientes:

    bCollideActors : Si es false, el actor no reacciona a ningún tipo de colisión (independientemente de los valores de sus componentes, claro, por tanto, deben estar correctamente indicadas en ambos).
    bBlockActors: Si es true, el motor Unreal no permitirá que se solapen dos actores con este valor en dicha flag.

    Los eventos que utiliza Unreal para indicar que se ha realizado una colisión son los siguientes:

    event Touch(Actor Other, PrimitiveComponent OtherComp, vector HitLocation, vector HitNormal):
    Este evento se llama cuando dos actores se solapan, pero sólo ocurre si uno de ellos tiene el flag
    bBlockActors con valor false.
    event Bump(Actor Other, PrimitiveComponent OtherComp, Vector HitNormal)
    Este evento se llama cuando dos actores colisionan con movimiento uno con otro, y sólo si los dos tienen el flag bBlockActors con valor true.

    Esta información se ha extraído de Romero UnrealScript blog

    Contando que nuestro pickup queremos que sea más un "trigger" deberemos configurar sus flags como
    bCollideActors=true y bBlockActors=false.

    Del mismo modo, nos aseguramos que su StaticMeshComponent es ignorado (Quizá prefieras detectar la colisión con el componente de malla en lugar de un componente de primitiva geométrica. En tal caso, hay que configurarlo acorde a lo explicado anteriormente):

    begin object Class=StaticMeshComponent Name=MyStaticMesh
    StaticMesh=StaticMesh’NodeBuddies.3D_Icons.NodeBuddy__BASE_SHORT’
    LightEnvironment=MyLightEnvironment
    bCastDynamicShadow=true
    CollideActors=false
    BlockActors=false
    //BlockRigidBody=true
    //BlockNonZeroExtent=true
    //CanBlockCamera=true
    end object
    Components.Add(MyStaticMesh)
    Si no hemos aplicado las flags de colisión con el componente de malla, lo hacemos por tanto con un componente de primitiva geométrica, en este caso un cilindro (Cyllinder):


    Begin Object Class=CylinderComponent Name=CylinderComp
    CollisionRadius=32
    CollisionHeight=48
    CollideActors=true
    BlockActors=false
    End Object
    Components.Add( CylinderComp )
    CollisionComponent=CylinderComp
    Como he explicado, dependiendo del comportamiento que desees, hay que jugar con la combinación de estas flags en el actor y en sus componentes attachados. La clase Actor indica en su variable CollisionComponent qué ActorComponent es el que utiliza para las colisiones. Al parecer los Actores sólo pueden tener una colisión, pero puedes hacer que extienda su bounding box a varios componentes si en ellos le colocas la variable CheckAlwaysCollision=true. No he hecho muchas pruebas con esto, pero si al iniciar vuestro juego pulsáis Tab os aparecerá la consola de comandos. Desde allí podéis teclear Show Collisions o Show Bounds si hacéis pruebas para activar y desactivar que se vean los mismos con malla alámbrica.

    Coloco ahora un ejemplo de pickup:


    Here it is a really simple example:
    //Placeable is necessary if you want to put this actor from the editor!
    class NULLPickUp extends Actor ClassGroup(NULLGame) placeable;
    event Touch( Actor Other, PrimitiveComponent OtherComp, Vector HitLocation, Vector HitNormal )
    {
    `log(“Touched by “@ Other.Name@”!”);
    }
    DefaultProperties
    {
    //*****************COLLISIONS*************************************
    // If false, the actor does not respond to any type of collision.
    bCollideActors=true
    // If true, the Unreal engine will not allow the two Actors to overlap.
    bBlockActors=false
    Begin Object Name=MyLightEnvironment
    bEnabled=TRUE
    End Object
    Components.Add(MyLightEnvironment)
    begin object Class=StaticMeshComponent Name=MyStaticMesh
    StaticMesh=StaticMesh’NodeBuddies.3D_Icons.NodeBuddy__BASE_SHORT’
    LightEnvironment=MyLightEnvironment
    bCastDynamicShadow=true
    //Scale=0.25
    CollideActors=true
    BlockActors=false
    //BlockRigidBody=true
    //BlockNonZeroExtent=true
    //CanBlockCamera=true
    end object
    Components.Add(MyStaticMesh)
    //Use in the console command the command Show Collision and check if this component collisions thanks to BlockNonZeroExtent
    //If you put that flag to false then no collision happens. In that case, you can put CollideActors from the static mesh = true
    Begin Object Class=CylinderComponent Name=CollisionCylinder
    CollisionRadius=+0020.000000
    CollisionHeight=+0200.000000
    CollideActors=false
    BlockActors=false
    BlockRigidBody=false
    //BlockNonZeroExtent – A non zero extent trace is an axis aligned bounding box check, which is commonly used for player movement. If this is true, then the Actor will block non zero extent traces used in Unrealscript.
    BlockNonZeroExtent=true
    End Object
    Components.Add(CollisionCylinder)
    CollisionComponent=CollisionCylinder
    }
    Como ejercicio, es interesante crearos vuestra clase PickUp y que experimentéis con `log() en las funciones de Touch y Bump y cambiéis las flags de vuestro Pawn y de vuestro PickUp para ver qué llama a qué y cuando. Dentro de `log() podéis colocar valores de variables concatenadas con texto utilizando @. Por ejemplo: `log("[MyPickUp] He tocado "@other@" y concretamente su ActorComponent " @ otherComp);
    Si colocamos esa línea en Touch o Bump de la clase PickUp, nos indicará con qué ha tocado o chocado y el componente en concreto con el que lo ha hecho.

    jueves, 24 de octubre de 2013

    4) Dale al diseñador lo que quiere. Arquetipos para todos

    ¡Muy buenas!

    Aquí nos encontramos de nuevo en el mágico mundo Digimon de la programación en UnrealScript. Hoy con nosotros: ¡Arquetipos! ¿Y qué narices es esto? Instancias de objetos referenciadas que se pueden tocar en tiempo real para el disfrute y frote de la gente que tiene que montar el juego. De este modo, desde el Content Browser, guardaremos en un paquete nuestros propios Pawn con una configuración determinada, que podremos modificar mientras el juego está funcionando. En el código, referenciaremos ese Pawn para que lo lea tal y como lo queramos.
    Igualmente, haremos un "fake" de arquetipo para nuestras cámaras, ya que, de momento, y aunque pueden ser actores también en las escenas, nosotros creamos una clase que no tiene "materialización" en la escena hasta que no comienza el juego. Crearemos una clase NULLCameraTDAttributes, cuyas variables leeremos desde nuestra cámara NULLCameraTopDown, para poder modificar en tiempo real sus características. ¡Al lío!

    Nuestra aventura de hoy nos lleva a adentrarnos en el magico mundo del editor. En el Content Browser, pestaña Actor Classes, buscamos bajo la categoría NULLGame, nuestro NULLPawn. Botón derecho y crear Arquetipo. Nos pide un Paquete, un nombre de agrupamiento y el nombre del arquetipo. Podemos poner NULLCharacters como nombre del paquete, de agrupamiento Characters y como nombre de arquetipo arc_NULLTestPawn.

    Ahora si volvemos al content browser, bajo el apartado Packages abajo a la izquierda por defecto, hay una carpeta llamada NewPackages. La desplegamos y vemos nuestro paquete. Botón derecho y guardar. Lo lleváis a la carpeta Content/NULLContent/ y lo guardáis con la jerarquía de carpetas que queráis como os sintáis más cómodos y ordenados.

    Ya tenemos nuestro arquetipo. Tendremos que buscarlo en el content browser después para conocer su Full Name y dárselo a una de nuestras variables de NULLGameInfo.

    En primer lugar, GameInfo tiene una función encargada de "spawnear" un Pawn por defecto para que PlayerController lo posea (no me pongo érotico, es que PlayerController tiene una función UnPossess y Possess cual espíritu maligno para controlar los Pawns que le indiquemos). Dicha función es "function Pawn SpawnDefaultPawnFor(Controller NewPlayer, NavigationPoint StartSpot)"

    Mi recomendación es que le echéis un ojo en la clase base. Ahora colocamos esta función en la clase que ya tenemos de GameInfo

    NULLGameInfo.uc

    ...
    /**
     * Returns a pawn of the default pawn class
     *
     * @param    NewPlayer - Controller for whom this pawn is spawned
     * @param    StartSpot - PlayerStart at which to spawn pawn
     *
     * @return    pawn
     */
    function Pawn SpawnDefaultPawnFor(Controller NewPlayer, NavigationPoint StartSpot)
    {
        //local class<Pawn> DefaultPlayerClass;
        local Rotator StartRotation;
        local Pawn ResultPawn;

        //Comentamos esta línea ya que no vamos a utilizar una clase para el jugador
        //Si no nuestro propio arquetipo
        //DefaultPlayerClass = GetDefaultPlayerClass(NewPlayer);

        // don't allow pawn to be spawned with any pitch or roll
        StartRotation.Yaw = StartSpot.Rotation.Yaw;

        //ResultPawn = Spawn(DefaultPlayerClass,,,StartSpot.Location,StartRotation);
        //Aquí podéis ver la diferencia entre el original de la función
        //Y como spawneamos nuestro arquetipo.
        //Nos faltará declarar la variable
        //var const archetype Pawn DefaultPawnArchetype;
        //Al inicio de nuestra clase
        ResultPawn = Spawn(DefaultPawnArchetype.Class,,,StartSpot.Location,StartRotation,DefaultPawnArchetype);

        if ( ResultPawn == None )
        {
            `log("[NULLGameInfo] Couldn't spawn player of type "$DefaultPawnArchetype.Class$" at "$StartSpot);
        }
        return ResultPawn;
    }
    ...


    Bien, como indico en los comentarios nos faltará añadir la variable:

    var const archetype Pawn DefaultPawnArchetype;

    y darle un valor en nuestro DefaultProperties. El cual, iremos al editor, buscaremos nuestro Pawn creado en su paquete y copiaremos su nombre total Copy Full Name to Clipboard y lo pegaremos de este modo

    DefaultProperties
    {
      DefaultPawnArchetype=<IntroduzcaAquí SuTabacoGracias>
    }

    Comentamos la línea: //DefaultPawnClass=class'NULLGame.NULLPawn' ya que ya no usamos un Pawn normal, si no un arquetipo.

    Y Voilá. Podremos ejecutar en el Editor nuestro juego y modificar mediante el content browser y seleccionando el arquetipo con F4 sus propiedades.

    Podemos hacer la prueba y buscar otra skeletal Mesh y ver cómo cambia en tiempo real :D

    ¡Por cierto! Si no sabéis ejecutar un tipo de juego concreto en el Editor, es en la pestaña View->WorldProperties->GameType y allí en DefaultGameType y GameType for PIE colocáis vuestra clase de juego NULLGameInfo. Luego le dáis a la flechita verde que hay en la barra de arriba o en la venta que queráis ejecutar el juego y a disfrutar.

    Tened en cuenta, que la malla se carga al iniciar el juego. Si desligáis el skeletalMeshComponent actual mientras se ejecuta, la malla desaparecerá pero no se cargará la nueva hasta volver a iniciar. Al ser placeable, un diseñador puede arrastrar a la escena el arquetipo y comprobar el aspecto final que desee en el juego.

    Ahora podemos, colocar las variables de nuestra cámara en una clase que herede de las más básicas en UDK, que es Object, ocultar todo componente que no nos interese y dejar sólo al descubierto las variables y valores que queramos modificar.

    Para ello generamos una clase llamada NULLCameraTDAttributes:

    NULLCameraTDAttributes.uc

    class NULLCameraTDAttributes extends Object
    HideCategories(Object);

    var (Camera) float altitude;

    defaultproperties
    {
        altitude=500.0f
    }

    HideCategories es un flag que oculta las categorías indicadas separadas por comas que no queramos mostrar en las propiedades desde el editor (con F4). De este modo, creamos una categoría camera (al colocar entre paréntesis en una variable como se indica), con la variable altitude.

    Vamos al editor y creamos nuestro arquetipo de NULLCameraTDAttributes (ojo que seguramente tendréis que desactivar algun booleano de los que aparecen en la pestana Actor Classes del Content Browser para poder ver esta clase) y siguiendo pasos parecidos a los que hemos realizado antes con el Pawn.

    Ahora en nuestra cámara deberemos consultar este valor. Creamos una variable:
    var const NULLCameraTDAttributes CameraAttributes;

    En la funcion UpdateViewTarget(out TViewTarget OutVT, float DeltaTime) cambiamos donde teníamos a pelo la altura de la cámara en unidades de Unreal por CameraAttributes.altitude.

    Ahora nos faltará colocar en nuestra cámara en default properties en valor de la variable CameraAttributes.

    IMPORTANTE: En DefaultProperties de vuestro Pawn, debéis colocar Mesh=NameDelSkeletalMesh para poder modificar la malla del mismo mediante el arquetipo.

    ¡Y ya podemos en tiempo real cambiar la altura de nuestra cámara!

    ¡Nos vemos en el siguiente tutorial!





    viernes, 11 de octubre de 2013

    3) Triunvirato ganador: PlayerController, Camera y Pawn.

    ¡Muy buenas!

    Como vimos anteriormente, en el fichero de configuración DefaultGame.ini,  tenemos que especificar, qué clase se encarga de controlar los "pawns", representaciones físicas en UDK de personajes, puesto que la clase Pawn se encarga de manejar sus atributos y también tenemos un "GameManager" encargado de controlar qué tipo de juego estamos jugando, y cómo funciona el mismo. Para ello heredamos de la clase base de UDK GameInfo.

    Sin más dilación, pasamos a modificar nuestros ficheros base del juego: NULLGameInfo,  NULLPawn, NULLPlayerController.

    NULLGameInfo.uc

    class NULLGameInfo extends GameInfo;

    auto State PendingMatch
    {
    Begin:
        StartMatch();
    }

    defaultproperties
    {
        //Más adelante podremos crear nuestra propia clase para HUD.
        //La especificaremos aquí
        HUDType=class'GameFramework.MobileHUD'
        //La clase que controlará nuestro Pawn. Claramente la que ya tenemos
        PlayerControllerClass=class'NULLGame.NULLPlayerController'
        //Qué tipo de Pawn aparecerá al iniciar el juego por defecto.
        //Más tarde cuando veamos los arquetipos cambiaremos esto
        DefaultPawnClass=class'NULLGame.NULLPawn'
        //Un flag para que no espere al comenzar el juego
        bDelayedStart=false
    }

    Como veis, es sólo un esqueleto y básicamente, está todo explicado con pequeños comentarios.

    NULLPawn.uc

      class NULLPawn extends Pawn
        //Si queremos poder colocarla en el editor, debemos
        //marcar esta clase como placeable
        placeable
        //Como vimos antes, hace referencia a un fichero de
        //configuración del que se podrá configurar variables
        //Bajo el apartado [NULLGame.NULLPlayerController]
        config(Game)
        //ClassGroup nos sirve para que en la pestaña del
        //Content Browser "classes" nos aparezca bajo
        //Un apartado con este nombre <NULLGame> en este caso
        ClassGroup(NULLGame);

    defaultproperties
    {

    //Componente Skeletal Mesh.Malla del personaje con esqueleto
    Begin Object Class=SkeletalMeshComponent Name=MyPawnSkeletalMesh
        //Un SkeletalMesh de la instalación con Unreal Tournament. Si tenéis la limpia
        //Creo que hay otra que debéis buscar en el editor y botón derecho y copiar
        //su Full Name to Clipboard para copiarlo aquí
        SkeletalMesh=SkeletalMesh'UTExampleCrowd.Mesh.SK_Crowd_Robot'
        //AnimSets son colecciones de animaciones para usar con esta malla
        //The udk package should be under UDKGame/Content.
        //Right click on the animset in the content  browser and
        //copy full name to clipboard.
        //There is no animset in the custom installation ready
        //AnimSets(0)=AnimSet’EternalCrisis_Characters.Female.Human_Athletic_AnimSet’
        //The same for the animtree
        //AnimTreeTemplate=AnimTree’EternalCrisis_Characters.Human_Test’
        HiddenGame=FALSE
        HiddenEditor=FALSE
    End Object

    //Add it to the Actor’s components
    Components.Add(MyPawnSkeletalMesh)

    //Very important if we don’t want our character embracing the shadows forever
    Begin Object Class=DynamicLightEnvironmentComponent Name=MyLightEnvironment
        //Different flags from the component
        bUseBooleanEnvironmentShadowing=false
        bIsCharacterLightEnvironment=TRUE
        bSynthesizeSHLight=TRUE
    End Object

    //Adding the component again
    Components.Add(MyLightEnvironment)

    }
    Como veis en esta clase, también he colocado comentarios. Algunos en inglés, ya que he sido un poco perro y los he utilizado de mi antiguo blog. Igualmente, está actualizado con respecto a dicha página.

    Cosillas interesantes en este caso. En primer lugar, el flag placeable en la declaración de la clase, hace que se pueda colocar este tipo de actor en las escenas a través del Editor. También he añadido el flag ClassGroup, que sirve principalmente para tener un poco ordenado el código y poder encontrar más facilmente las clases que creemos a través del Conter Browser, en la pestaña Classes, bajo el nombre NULLGame en nuestro caso.

    Y después entramos en las DefaultProperties. En las clases de UDK, toda variable se coloca en el principio de la clase, declarando básicamente "var tipoDeVar nombreDeVar". Recalco el básicamente, que como ya sabéis hay ciertas estructuras que requieren otra nomenclatura para ser declaradas y que pueden ser Const, Static... Cosas de programadores rancios, ya sabéis a lo que me refiero.

    En vez de poder iniciar dichas variables allí mismo como podríais hacer en C, debéis hacerlo en el interior de las llaves de DefaultProperties. ¡Sed cuidadosos! Colocad exactamente DefaultProperties y debajo la apertura de llave (símbolo {) ya que si la colocáis al lado de DefaultProperties, el compilador, mágico y especialito, se quejará y os dirá una línea de otro lugar como el problema, dándoos más de un quebradero de cabeza. Igualmente, DefaultProperties siempre debajo de todas las funciones de código o el compilador ignorará que está delante a la hora de marcaros la línea donde se producen errores, volviéndonos claramente a un estado primario de primate que arranca teclas de un instrumento que no comprende.

    Con respecto a cómo están formados los actores, se rigen por una composición de componentes. Es algo parecido a Unity pero sin ser visual. Lo cual da más quebraderos de cabeza. Básicamente un actor se forma con piezas (componentes), que disponen de sus propios atributos y variables que configurar. Un componente se crea mediante la fórmula:

    Begin Object Class=<ClaseDelComponente> Name=<NombreQueMeSalgaDelHigo>
    <Valores de variables>
    End Object

    Y posteriormente, posee el actor una estructura llamada Components (básicamente una lista), a la que le "añadimos" los componentes que deseemos. Más información la tenéis en este apartado de la UDN: UDN Components

    Y ahora vamos con NULLPlayerController, controlador de nuestro recién creado Pawn.

    NULLPlayerController.uc

    class NULLPlayerController extends PlayerController
        //Como vimos antes, hace referencia a un fichero de
        //configuración del que se podrá configurar variables
        //Bajo el apartado [NULLGame.NULLPlayerController]
        config(Game);

    Como podéis ver, de momento, no vamos a tocar mucho esta clase. Simplemente me aseguro de heredar de la clase más básica de PlayerController con la que contamos (que a su vez hereda de Controller, pero eso ya es otra historia... Que podéis investigar en la carpera Development/Src/Engine/Classes como ya comenté...).

    Si nos vamos ahora a nuestro IDE, y ejecutamos, veremos que ya está funcionando nuestro Pawn con una cámara que está dentro de él (mirad hacia abajo con el ratón para descubrir las vergüenzas de la malla elegida para vuestro pawn en el SkeletalMesh).

    ¡Ya estamos a medio camino de tener una base decente para trastear! Como indico en el título, la cámara que tenemos es un tanto "limitada". Y nos gustaría poder vernos de algún modo.

    La primera solución se encuentra en configurar nuestro NULLPlayerController un poco más para indicarle que no queremos una cámara por defecto, si no una propia. Primero confeccionaremos la cámara con funcionalidad TopDown y luego la aplicaremos a nuestro "juego". ¡Al ataquerrrl con la cámara! (Que por cierto, la tenéis que crear en Development/Src/NULLGame/Classes por si se os ha olvidado a alguno).

    NULLCameraTopDown.uc

       class NULLCameraTopDown extends Camera;

        /**
        * Query ViewTarget and outputs Point Of View.
        *
        * @param    OutVT        ViewTarget to use.
        * @param    DeltaTime    Delta Time since last camera update (in seconds).
        */
        function UpdateViewTarget(out TViewTarget OutVT, float DeltaTime)
        {

        // Don’t update outgoing viewtarget during an interpolation
        if( PendingViewTarget.Target != None && OutVT == ViewTarget && BlendParams.bLockOutgoing )
        {
        return;
        }

        //AQUÍ colocamos dónde queremos que se coloque la cámara.
        //Esta función nos da una estructura como parámetro de tipo
        //TViewTarget que en su interior tiene un objeto POV
        //Que es el Point of view de la cámara.
        //Tiene un vector Location para modificarla
        //Y también Rotation con el clásico
        //Roll, Pitch y Yaw en unidades de UnrealRotation
        OutVT.POV.Location.Z = 500;
        //Following target in XY plane
        OutVT.POV.Location.X = OutVT.Target.Location.X ;
        OutVT.POV.Location.Y = OutVT.Target.Location.Y ;
        OutVT.POV.Rotation.Pitch = -90 * DegToUnrRot; //DegToUnrRot is a constant to transform degrees to UnrealRotation units
        //UnrRotToDeg exists too

        // Apply camera modifiers at the end (view shakes for example)
        ApplyCameraModifiers(DeltaTime, OutVT.POV);

        }

        DefaultProperties
        {

        }
    Bien, simplemente es para ir dando un pequeño bocado a la cámara y su funcionamiento. Básicamente como explico en los comentarios, sobreescribimos la función de la clase base Camera UpdateViewTarget que es la encargada de colocar la cámara donde debería. Las cámaras tienen muchas otras funciones y dependiendo de lo que necesitéis realizar en vuestro proyecto, tendréis que tocar más cosas. Pero ahora estamos con lo básico :)
    En este caso, tenemos una Top Down camera (vista cenital) a 500 unidades de Unreal de distancia de nuestro Pawn. Ahora sólo falta indicarle a nuestra clase de PlayerController que queremos utilizar esta cámara.

    NULLPlayerController.uc

    class NULLPlayerController extends PlayerController
        //Como vimos antes, hace referencia a un fichero de
        //configuración del que se podrá configurar variables
        //Bajo el apartado [NULLGame.NULLPlayerController]
        config(Game);
       
    defaultproperties
    {
    CameraClass=class'NULLGame.NULLCameraTopDown'

    Simple, sencillo y para toda la familia: Modificamos la variable CameraClass heredada de la clase base PlayerController y le indicamos que es la clase que acabamos de crear.

    Ejecutamos en nuestro IDE ;) Y disfrutamos de la magnificencia de lo logrado (si no es así, toca repasar los pasos anteriores :P).

    Como podéis ver, ya tenemos una base para hacer cosas. Sin embargo, necesitamos el poder del Editor para poder configurar la cámara como deseemos o nuestro propio Pawn. Sobretodo si trabajamos con diseñadores o artistas que les gusta jugar con sliders, valores y demás historias que para nosotros son matemática fría :D

    Pero eso tendrá que ser en la próxima entrega de "4) Dale al diseñador lo que quiere. Arquetipos para todos".

    ¡Un saludo!