PFM: Interfaz en UDK – Creando el menú principal

En otra entrada ya creamos un menú de pausa. Hoy veremos cómo se crea un menú principal, desde el que poder iniciar el juego. Una gran diferencia es que el menú principal será un nivel en sí mismo desde el que se podrá acceder a los niveles de juego propiamente dicho. Para montar el menú principal crearemos un GameType nuevo específico, modificaremos y crearemos otras clases, toquetearemos en los ficheros de configuración… manos a la obra 🙂

Esta entrada es una enorme masa de código, así que veamos lo que pretendemos conseguir (y ahorro esfuerzo a los que pasan el código y van directos al video :P)

El menú principal que crearemos en esta ocasión es muy sencillo. Contendrá 2 botones únicamente, para comenzar el juego y para salir. La complejidad del menú no es importante, pues luego se podrán añadir nuevos botones y funcionalidades, para empezar lo esencial es conseguir el menú funcional.

Al principio ya he mencionado la entrada del menú de pausa. Es muy recomendable pasarse antes por ahí para entender lo que voy a explicar a continuación, pues muchas clases de las que se modifican aquí se crearon en esa entrada.

Para entender mejor lo que se va a hacer desde el comienzo echemos un vistazo a las clases que teníamos y las que se van a crear.

  • PFMHUD: Hereda de UDKHUD. Es la clase básica para mostrar interfaz en pantalla. Anteriormente se encargaba de mostrar la interfaz del juego y el menú de pausa. Se va a generalizar para ser padre de las clases específicas que se usarán para el menú durante el juego y para el menú principal.
  • PFMHUDInGame: Hereda de PFMHUD. Nueva clase que contendrá el comportamiento propio de la interfaz durante el juego y el menú de pausa, extrayéndolo del anterior PFMHUD.
  • PFMHUDEntry: Hereda de PFMHUD. Nueva clase que se encargará de mostrar el menú principal en la pantalla de menú principal (obvio).
  • PFMHUDButton: Hereda de Object. Representa los botones del menú. No se modificará con respecto al menú de pausa.
  • PFMHUDScene: Hereda de Object. Esta clase contenía el conjunto de botones a mostrar en el menú de pausa. Se va a generalizar para contener la funcionalidad común a cualquier tipo de menú.
  • PFMHUDSceneInGameMenu: Hereda de PFMHUDScene. Nueva clase que contiene la funcionalidad específica del menú ingame.
  • PFMHUDSceneEntryMenu: Hereda de PFMHUDScene. Nueva clase que contendrá la funcionalidad específica del menú principal.

El primer paso es refactorizar PFMHUDScene y crear PFMHUDSceneInGameMenu para que el menú de pausa siga mostrándose como antes el  pero con una clase PFMHUDScene más genérica. Básicamente se lleva la funcionalidad propia del menú de pausa a la nueva clase, además de la descripción del propio menú.

PFMHUDScene

class PFMHUDScene extends Object;

var PFMHUD myHUD;       // Referencia al HUD

var array<PFMHUDButton> Buttons;    // Array de botones del menú

function init(PFMHUD PFMHUD)
{
    myHUD = PFMHUD;
}

// Pinta el menú
function Draw (Canvas Canvas, IntPoint MousePosition, bool bMousePressed, bool bMouseReleased)
{
    local int i, clickedButtonId;
    local float RatioX;
    local float RatioY;

    clickedButtonId = -1;   // Inicialmente a -1 => no corresponde a ningún botón

    // Se calcula el ratio para mantener la proporción al dibujar el menú
    RatioX = Canvas.ClipX / 1280;
    RatioY = Canvas.ClipY / 720;

    //Botones
    for(i=0; i<Buttons.Length; i++)
    {
        if(Buttons[i] != none)
        {
            if(Buttons[i].CheckHoverAndClick(Canvas,RatioX,RatioY,MousePosition,bMousePressed,bMouseReleased))
            {
                clickedButtonId = i;
            }
            Buttons[i].Draw(Canvas,RatioX,RatioY);
        }
    }

    // Si se ha clicado un botón
    if(clickedButtonId != -1)
    {
        // Se ejecuta la función asociada
        RunButtonFunction(clickedButtonId);
    }
}

// Ejecuta una función según el índice de botón
function RunButtonFunction(int id);

defaultproperties
{}

PFMHUDSceneInGameMenu

class PFMHUDSceneInGameMenu extends PFMHUDScene;

// Ejecuta una función según el índice de botón
function RunButtonFunction(int id)
{
    if(id == 0)
    {
        // Volver al juego
        PFMHUDInGame(myHUD).ToggleMenu();
    }
    else if(id == 1)
    {
        // Volver al menú
        myHUD.PlayerOwner.ConsoleCommand("Open testMenu");
    }
}

defaultproperties
{
    Begin Object Class=PFMHUDButton Name=But1
        texture(0)=Texture2D'PfmInterface.menus.T_Button_Resume'
        texture(1)=Texture2D'PfmInterface.menus.T_Button_Resume_hover'
        texture(2)=Texture2D'PfmInterface.menus.T_Button_Resume_click'
        bUseTextures=true
        XT=400
        YL=200
        centerX=true
        centerY=false
        Width=300
        Height=100
        U=0
        V=0
        UL=1.0
        VL=1.0
    End Object
    Buttons(0)=But1

    Begin Object Class=PFMHUDButton Name=But2
        texture(0)=Texture2D'PfmInterface.menus.T_Button_Quit'
        texture(1)=Texture2D'PfmInterface.menus.T_Button_Quit_hover'
        texture(2)=Texture2D'PfmInterface.menus.T_Button_Quit_click'
        bUseTextures=true
        XT=400
        YL=350
        centerX=true
        centerY=false
        Width=300
        Height=100
        U=0
        V=0
        UL=1.0
        VL=1.0
    End Object
    Buttons(1)=But2
}

A continuación creamos PFMHUDSceneEntryMenu, que contendrá la funcionalidad específica del menú principal y su descripción.

PFMHUDSceneEntryMenu

Lo más destacable es el uso del comando de consola Open, que servirá para cambiar de nivel al clicar el botón.

class PFMHUDSceneEntryMenu extends PFMHUDScene;

// Ejecuta una función según el índice de botón
function RunButtonFunction(int id)
{
    if(id == 0)
    {
        // Iniciar el juego
        myHUD.PlayerOwner.ConsoleCommand("Open test02");
    }
    else if(id == 1)
    {
        // Salir del juego
        myHUD.PlayerOwner.ConsoleCommand("Quit");
    }
}

defaultproperties
{
    Begin Object Class=PFMHUDButton Name=But1
        texture(0)=Texture2D'PfmInterface.menus.T_Button_New'
        texture(1)=Texture2D'PfmInterface.menus.T_Button_New_hover'
        texture(2)=Texture2D'PfmInterface.menus.T_Button_New_click'
        bUseTextures=true
        XT=400
        YL=200
        centerX=true
        centerY=false
        Width=300
        Height=100
        U=0
        V=0
        UL=1.0
        VL=1.0
    End Object
    Buttons(0)=But1

    Begin Object Class=PFMHUDButton Name=But2
        texture(0)=Texture2D'PfmInterface.menus.T_Button_Quit'
        texture(1)=Texture2D'PfmInterface.menus.T_Button_Quit_hover'
        texture(2)=Texture2D'PfmInterface.menus.T_Button_Quit_click'
        bUseTextures=true
        XT=400
        YL=350
        centerX=true
        centerY=false
        Width=300
        Height=100
        U=0
        V=0
        UL=1.0
        VL=1.0
    End Object
    Buttons(1)=But2
}

Ahora toca hacer lo mismo con las clases de HUD. Primero refactorizamos PFMHUD y creamos PFMHUDInGame, transladando la funcionalidad propia del HUD ingame a la nueva clase y dejando en la antigua las funciones comúnes a ambas.

PFMHUD

class PFMHUD extends UDKHUD;

var const Texture2D CursorTexture;

var bool bShowMenu;         // Mostrar menú

var bool bMousePressed;     // Ratón pulsado
var bool bMouseReleased;    // Ratón soltado

var PFMHUDScene menu;

event DrawHUD()
{
    local PFMPlayerInput PFMPlayerInput;

    Super.DrawHUD();

    // Menú
    if(bShowMenu)
    {
        if(PlayerOwner != none)
        {
            PFMPlayerInput = PFMPlayerInput(PlayerOwner.PlayerInput);
            if(PFMPlayerInput == none)
            {
                return;
            }
        }

        // Fondo negro
        Canvas.SetPos(0,0);
        Canvas.SetDrawColor(10,10,10,64);
        Canvas.DrawRect(Canvas.SizeX,Canvas.SizeY);

        Canvas.DrawColor = WhiteColor;

        // Menú
        if(menu == none)
        {
            InitMenu();
        }
        else
        {
            menu.Draw(Canvas,PFMPlayerInput.MousePosition, bMousePressed, bMouseReleased);
            // Una vez dibujado el menú y procesado el ratón, se ponen los flags a falso
            bMousePressed = false;
            bMouseReleased = false;
        }

        // Cursor
        Canvas.SetPos(PFMPlayerInput.MousePosition.X, PFMPlayerInput.MousePosition.Y);
        Canvas.DrawColor = WhiteColor;
        Canvas.DrawTile(CursorTexture, CursorTexture.SizeX, CursorTexture.SizeY, 0.f, 0.f, CursorTexture.SizeX, CursorTexture.SizeY,, true);
    }
}

event PostRender()
{
    Super.PostRender();
}

function InitMenu()
{
    menu = new class'PFMHUDScene';
    menu.init(self);
    PFMPlayerInput(PlayerOwner.PlayerInput).ResetMousePosition(Canvas.ClipX/2,Canvas.ClipY/2);      //Resetea posición del ratón
}

// Llamada al pulsar ESC desde PFMController. Qué hacer dependerá de cada caso
function PressedESC()
{}

// Llamada desde PFMPlayerController
function MousePressed()
{
    bMousePressed = true;
}

// Llamada desde PFMPlayerController
function MouseReleased()
{
    bMouseReleased = true;
}

defaultproperties
{
    bShowMenu=false

    CursorTexture=Texture2D'PfmInterface.menus.T_Cursor'
}

PFMHUDInGame

class PFMHUDInGame extends PFMHUD;

var const Texture2D CrosshairTexture;

var bool bShowInGameHUD;    // Mostrar HUD ingame

event DrawHUD()
{
    Super.DrawHUD();

    // Se pinta el HUD ingame
    if(bShowInGameHUD)
    {
        if(PlayerOwner != none && PlayerOwner.Pawn != none)
        {
            // Num de enemigos
            if(PFMGame(WorldInfo.Game) != none)
            {
                Canvas.DrawColor = WhiteColor;
                Canvas.Font = class'Engine'.Static.GetLargeFont();
                Canvas.SetPos(Canvas.ClipX * 0.01, Canvas.ClipY * 0.05);
                Canvas.DrawText("ENEMIES:"@PFMGame(WorldInfo.Game).TotalEnemiesAlive);
            }

            // Salud
            Canvas.DrawColor = WhiteColor;
            Canvas.Font = class'Engine'.Static.GetLargeFont();
            Canvas.SetPos(Canvas.ClipX * 0.01, Canvas.ClipY * 0.85);
            Canvas.DrawText("HEALTH:"@PlayerOwner.Pawn.Health);

            // Mirilla
            Canvas.SetPos(Canvas.ClipX * 0.5 - CrosshairTexture.SizeX/2, Canvas.ClipY * 0.5 - CrosshairTexture.SizeY/2);
            Canvas.DrawColor = WhiteColor;
            Canvas.DrawTile(CrosshairTexture, CrosshairTexture.SizeX, CrosshairTexture.SizeY, 0.0f, 0.0f, CrosshairTexture.SizeX, CrosshairTexture.SizeY,, true);
        }
    }
}

function InitMenu()
{
    menu = new class'PFMHUDSceneInGameMenu';
    menu.init(self);
    PFMPlayerInput(PlayerOwner.PlayerInput).ResetMousePosition(Canvas.ClipX/2,Canvas.ClipY/2);      //Resetea posición del ratón
}

// Activa/Desactiva el menú Ingame
function ToggleMenu()
{
    bShowMenu = !bShowMenu;             //Activa/Desactiva Menu
    bShowInGameHUD = !bShowMenu;        //Activa/Desactiva HUD
    PFMPlayerController(PlayerOwner).SetInMenu(bShowMenu);  //Informa a PlayerController
    //PFMPlayerInput(PlayerOwner.PlayerInput).ResetMousePosition(Canvas.ClipX/2,Canvas.ClipY/2);      //Resetea posición del ratón
}

// Llamada al pulsar ESC desde PFMController. Qué hacer dependerá de cada caso
function PressedESC()
{
    // De momento se activa el menu
    ToggleMenu();
}

defaultproperties
{
    bShowInGameHUD=true
    CrosshairTexture=Texture2D'PfmInterface.Crosshair.Crosshair'
}

Creamos la clase PFMHUDEntry, que sólo contiene la funcionalidad necesaria para mostrar el menú principal.

PFMHUDEntry

class PFMHUDEntry extends PFMHUD;

function InitMenu()
{
    menu = new class'PFMHUDSceneEntryMenu';
    menu.init(self);
    PFMPlayerInput(PlayerOwner.PlayerInput).ResetMousePosition(Canvas.ClipX/2,Canvas.ClipY/2);      //Resetea posición del ratón
}

// Llamada al pulsar ESC desde PFMController. Qué hacer dependerá de cada caso
function PressedESC()
{}

defaultproperties
{
    bShowMenu=true
}

El menú principal será un nivel diferente al resto de los demás, pero también habrá que especificar un GameType. Para que se muestre el HUD correcto crearemos un nuevo GameType (muy simple) que determinará el HUD a usar. Además se creará un Controller muy sencillo específico para el menú.

PFMGameMenu

class PFMGameMenu extends UDKGame;

defaultproperties
{
    //Se especifica el controlador
    PlayerControllerClass=class'PFM.PFMPlayerControllerMenu'
    //HUD
    HUDType=class'PFM.PFMHUDEntry'
}

El nuevo controller contendrá funcionalidad genérica de menús que el PFMPlayerController también necesita, por lo que después haremos que este herede del nuevo controlador.

PFMPlayerControllerMenu

class PFMPlayerControllerMenu extends UTPlayerController;

var bool bInMenu;               // Indica al controlador que se encuentra en un menú.

// Llamada al pulsar ESC
exec function ShowMenu()
{
    PFMHUD(myHUD).PressedESC();
}

exec function StartFire( optional byte FireModeNum )
{
    Super.StartFire(FireModeNum);
    if(bInMenu && FireModeNum == 0)
    {
        PFMHUD(myHUD).MousePressed();
    }
}

exec function StopFire(optional byte FireModeNum)
{
    Super.StopFire(FireModeNum);
    if(bInMenu && FireModeNum == 0)
    {
        PFMHUD(myHUD).MouseReleased();
    }
}

defaultproperties
{
    InputClass=class'PFM.PFMPlayerInput'
    bInMenu=true   //En menú siempre
}

Habrá que hacer algunos cambios en PFMPlayerInput y PFMPlayerController.

PFMPlayerInput

class PFMPlayerInput extends UDKPlayerInput within PFMPlayerControllerMenu;

PFMPlayerController

class PFMPlayerController extends PFMPlayerControllerMenu;

Con esto se acaban las tareas de código. Abrimos el editor y creamos un nuevo nivel, que será el que contenga el menú principal. Abrimos las WorldProperties y seleccionamos el GameType por defecto, que será el recién creado PFMGameMenu. Guardamos el nivel.

Dado que ahora queremos que al ejecutar el juego este comience en nuestro menú deberemos modificar un fichero de configuración para ello. Abrimos DefaultEngine.ini (está en UDKGame\Config). Modificamos los parámetros Map y LocalMap, igualándolos al nombre del mapa del menú principal, en mi caso testMenu.udk

[URL]
MapExt=udk
; Any additional map extension to support for map loading.
; Maps without an extension always saved with the above MapExt
AdditionalMapExt=mobile
;Map=UDKFrontEndMap.udk
;LocalMap=UDKFrontEndMap.udk
Map=testMenu.udk
LocalMap=testMenu.udk
TransitionMap=EnvyEntry.udk
EXEName=UTGame.exe
DebugEXEName=DEBUG-UTGame.exe

Ahora ejecutaremos el juego desde el ejecutable del propio UDK, que se encuentra en Binaries\Win64. Es importante recompilar antes para que el fichero de configuración surta efecto. El problema es que la modificación de los ficheros de configuración no fuerza la recompilación de scripts. Para forzarla, modificaremos cualquier clase añadiéndole un espacio y borrándolo, o lo que sea, para que al ejecutar UDK.exe nos pregunte si queremos recompilar los scripts. Una vez hecho, se arrancará el juego, y si todo ha ido bien, este comenzará en nuestro nivel de menú principal, mostrando el menú que hemos creado:

Edit: Con un poco más de trabajo, mejorando el fondo, los botones, etc, puede tenerse algo así:

Banner Blog

Anuncios

16 pensamientos en “PFM: Interfaz en UDK – Creando el menú principal

  1. Tengo una duda sobre cómo afecta esto al resto del juego en cuanto a velocidad.

    ¿Resta velocidad o es apreciable?

    Y luego al compilar, supongo que también será más lento, ¿pero apreciable?

    Lo digo porque si estás haciendo un juego lo mejor sería dejar todo esto para el final, cuando no tengas que reiniciar mil veces para ajustar la IA de los enemigos, por ejemplo.

    • Velocidad en cuanto a fps? No afecta nada al resto del juego, pues el menú principal es un nivel diferente que ejecuta clases de HUD diferentes a las que se usan luego en el juego.
      Y en cuanto al tiempo de compilación.. totalmente inapreciable. A no ser que se tengan miles de clases la compilación es bastante rápida. Y en caso de tener tantas clases se podrían repartir en diferentes paquetes para que se compilen sólo los que han sido modificados. Te tarda mucho en compilar?
      Cuando empecé con el paquete PFM debía tardar en compilar unos 2 segundos, y ahora que tiene unas 45 clases tarda 3, así que apenas hay diferencia.

  2. Pues ni idea de lo que ralentiza cada cosa. La verdad es que me pongo a programar inconscientemente de si lo que estoy haciendo es rápido o no. A veces pienso en el HUD, por ejemplo, que pinta en cada frame todos los marcadores.

    Recuerdo en mi época de programador de Spectrum que se solía dedicar el tercio inferior de la pantalla a los marcadores, y la cosa es que se pintaba todo una vez, y luego sólo se actualizaban los números, las barritas, etc. Pero bueno, muchos megaherzios hemos ganado desde entonces y quizás ya no hace falta optimizar tanto.

    Aparte tengo un PC bastante rápido y eso es bueno para trabajar, pero te puedes llevar la desagradable sorpresa cuando pruebes el juego en un PC normal de que es demasiado lento y en el PC rápido no te dabas cuenta. Por eso estoy todo el rato mirando el stats unit…

    Respecto a distribuir las clases en diferentes paquetes, ¿te refieres a tener PFM, PFM1, etc?
    ¿Sólo se compilaría el directorio que haya tenido algún cambio, no el resto aunque haya referencias cruzadas?
    Supongo que es así, porque si no se compilaría UTgame, UDKgame, Casttlegame, etc.
    De momento no me haría falta, pero es bueno saberlo.

    • Aunque el HUD se pinte cada frame no va a afectar prácticamente al rendimiento, a no ser que se hagan miles de llamadas, creo que afectan más otros efectos.

      Si, haciendo diferentes paquetes sólo se compilan aquellos que se han modificado. Sobre las referencias cruzadas creo que hay modificadores de clase para especificar que cierta clase debe compilarse previamente a otra y también se tiene en cuenta el orden en que se ponga el paquete en el fichero DefaultEngine.ini

  3. Y más que lo que me tarda, es lo tedioso del proceso. Estoy usando Notepad++ para editar, le he configurado F9 para que lance el juego, si hay que compilar me lo pregunta, pero se cierra sola la ventana. La siguiente pulsación ya ejecuta el juego. Creo que había un parámetro para evitar la pregunta, pero no me funciona.

    Lo tedioso es tener que estar pulsando “si” cuando compila, y luego pulsar F9 de nuevo, repetido cientos de veces. Ahí creo que se han lucido los de UDK, porque podrían haber hecho que todo fuese continuo, una sóla ejecución, compliado y después ejecutar el juego.

    • Totalmente de acuerdo, lo que es el “workflow” con UnrealScript es una mierda xD. Tener que cerrar el editor para compilar, compilar primero, luego ejecutar… Es lo que más he visto criticado sobre UDK.

      Por suerte parece que en el Unreal Engine 4 se han puesto las pilas bastante y no hará falta cerrar el editor para modificar el código. De hecho se podrá modificar con el juego ejecutando, se compilará en un proceso paralelo y los cambios se verán en el juego sin necesidad de reiniciarlo, un gran avance.

      Yo lo que tengo son un par de ficheros .bat para hacer la compilación y la ejecución. Para evitar el tener que pulsar el “sí” tengo un .bat que siempre compila:

      C:\UDK\UDK-2012-10\Binaries\Win64\UDK.exe make

      Y luego para probar en cada nivel diferente tengo su .bat correspondiente, por ejemplo:

      C:\UDK\UDK-2012-10\Binaries\Win64\UDK.exe Tower -log

  4. Si lo hacen así será una gozada. Ahora a ver cuándo lo ponen disponible para los independientes. Creo que con el UDK tardaron 3 años. Pero era lo nuevo, ahora que ya están metidos en la dinámica de ofrecer el motor a la gente, quizás lo hagan más rápido. Además tienen competencia dura con el cry, frostbite y todos esos que están saliendo ahora.

    No había caído en lo del make, así no pide confirmación, gracias 🙂

    • Pues sí, sería la leche que no tardaran mucho en sacar un UDK del UE4, pero no sé, viendo que aún le dan mucho soporte al UDK no creo que lo saquen muy pronto.

  5. exelentes tutoriales pero me quede un tanto atorado puesto que me tira un error
    missing ‘<' in 'array' en la clase PFMHUDScene.uc y no entiendo que es. agradeceria tu ayuda.

    • Hola alejandro, me alegro que los encuentres útiles. Parece que al copiar el código WordPress eliminó la definición del tipo de array, que va entre ‘<'. Lo he corregido y debería estar ahora correcto 😉

  6. Excelente Marcos como siempre funciona perfecto

    solo una inquietud como hago para tener un Continuar que mecanica maneja el boton continuar como guarda en que face se quedo.

    y al dar salir o nuevo juego como pones que diga desea salir si o no ??

    muchas gracias me ayudarias mucho Marcos 😀 .

    • Para continuar una partida anterior tendrías que implementar un sistema de Guardado, aquí tienes un tutorial. Lo básico es guardar una clase UnrealScript que contiene la información necesaria para guardar el estado de la partida en un fichero del disco duro. Para eso hay que serializarla, etc.. en el tutorial enseñan lo básico de cómo hacerlo.

      Para lo otro que me preguntas, echale un poco de imaginación, si ya has conseguido crear el menú principal y has entendido el código, seguro que se te ocurre cómo hacer para que al clicar un botón se dejen de mostrar ciertos botones y aparezcan otros. Ánimo 😀

    • Gracias por la respuesta Marcos me pondre a estudiar esto a fondo muchas gracias 😀

  7. Muchas pero muchas gracias marcos funciono perfecto 😀 nose como agradecerte con todo lo que me as ayudado ya logre los botones y salvar la partida encerio gracias
    esperare ansioso tu juego y por lo menos asi te agradecere comprandolo

    Eres Grande Marcos

    • Hola Andres, me podrías decir maso menos como hiciste eso de los botones desea salir si o no, no soy bueno programando y lo necesito para un proyecto, te lo agradecería infinitamente. yhonacuario90@gmail.com

Responder

Introduce tus datos o haz clic en un icono para iniciar sesión:

Logo de WordPress.com

Estás comentando usando tu cuenta de WordPress.com. Cerrar sesión / Cambiar )

Imagen de Twitter

Estás comentando usando tu cuenta de Twitter. Cerrar sesión / Cambiar )

Foto de Facebook

Estás comentando usando tu cuenta de Facebook. Cerrar sesión / Cambiar )

Google+ photo

Estás comentando usando tu cuenta de Google+. Cerrar sesión / Cambiar )

Conectando a %s