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.