PFM: Scripting de las oleadas de enemigos con Kismet

Para un juego en el que los enemigos irán apareciendo por oleadas será importante tener un sistema para organizar y sincronizar estas oleadas. Podría hacerse mediante código pero es un método poco práctico y además, UDK provee una herramienta mucho más adecuada para este tipo de actividades: Kismet.

Kismet es un sistema de «scriptado» visual («scriptar» vendría a ser como guionizar los eventos que van a ocurrir en el juego, decidiendo cuándo y por qué), que permite trabajar con cajas que representan acciones o eventos e irlos conectando de forma visual para obtener el comportamiento deseado. Kismet en UDN

Kismet proporciona multitud de acciones ya predefinidas y eventos que podemos utilizar, pero no cubren todas las posibilidades. Por suerte se pueden crear nuevos nodos que se comporten según queramos y eso es lo que haremos.

El grafo de Kismet que se creará en esta ocasión es el siguiente:

pfm09_01

Nota: La propiedad bLastOfWave de los últimos nodos de cada oleada debe marcarse a TRUE.

Los nodos con forma romboide son eventos, que se activan cuando se cumplen ciertas premisas. En este caso vemos el nodo Player Spawned, que se activa cuando el jugador aparecen en el escenario. Al activarse el evento, este envía una señal de activación al nodo Switch al que está conectado.

El nodo de tipo Switch activa una de sus salidas cada vez que se activa su entrada. Así, la primera vez que se active, el nodo activará la salida 1 (Link 1), la segunda vez activará la salida 2 (Link 2), etc. Por tanto, la primera vez que se active el switch, este activará el nodo Spawn Enemies.

Spawn Enemies es un nodo creado para la ocasión, y permitirá generar las oleadas de enemigos en la posición del PFMEnemySpawner que creamos en el post sobre generación de enemigos. Como es un nodo configurable, podremos acceder a sus propiedades en Kismet y decidir cuántos enemigos se generarán y el intervalo de tiempo entre cada generación de un enemigo. Como vemos la salida está conectada a otro nodo Spawn Enemies, pero con un Delay de 5 segundos. Así, el primer nodo generará los enemigos y 5 segundos después, el segundo nodo generará los suyos. Como vemos este segundo nodo no está conectado a nada en su salida, por lo que parece que el flujo terminaría aquí, pero para eso está el evento WaveComplete.

Este evento se activará cuando el jugador acabe con todos los enemigos de una oleada o estos sean destruidos, lo que dará al comienzo de la siguiente. Al activar el evento, este activará de nuevo el nodo Switch, que activará la salida correspondiente. Así se irán activando las diferentes oleadas.

Antes de ver el código de creación de estos nodos nuevos, veamos el resultado en video.

Como vemos hay 4 grupos de generaciones de enemigos, correspondientes a los 4 nodos configurados en Kismet con diferentes números de enemigos y tiempos de generación. A continuación el código.
PFMSeqAct_SpawnEnemies

Esta clase corresponde al nodo Spawn Enemies. Básicamente lo que hace es invocar a la función SpawnPFMEnemies del PFMEnemySpawner, pasándole los parámetros que se han configurado en el editor.

class PFMSeqAct_SpawnEnemies extends SeqAct_Latent;

var PFMEnemySpawner PFMEnemySpawner;
var () int NumEnemies;                      // Número de enemigos a generar
var () float TimeInterval;                  // Intervalo de tiempo entre enemigos
var() bool bLastOfWave<autocomment=true>;    // Indica que es el último generador de la ola

event Activated()
{
    // Generación de enemigos
    PFMEnemySpawner.SpawnPFMEnemies(NumEnemies, TimeInterval);
    OutputLinks[0].bHasImpulse = true;  // Se activa la salida Out inmediatamente
}

event bool Update(float deltaTime)
{
    if(PFMEnemySpawner.HasEnemiesLeft())
    {
        return true;
    }
    else
    {
        // Si no quedan enemigos por generar se activa la salida Finished
        OutputLinks[1].bHasImpulse = true;
        // Si este es el último de la ola, se informa a PFMGame
        if(bLastOfWave && PFMGame(GetWorldInfo().Game) != none)
        {
            PFMGame(GetWorldInfo().Game).FinishWave();
        }
        return false;
    }
}

defaultproperties
{
    ObjName="Spawn Enemies"
    ObjCategory="PFM"
    bSuppressAutoComment=false

    numEnemies=0
    timeInterval=0.5
    bLastOfWave=false

    VariableLinks.Empty
    VariableLinks(0)=(ExpectedType=class'SeqVar_Object',LinkDesc="PFMEnemySpawner",PropertyName=PFMEnemySpawner,bWriteable=true)
    VariableLinks(1)=(ExpectedType=class'SeqVar_Int',LinkDesc="NumEnemies",PropertyName=NumEnemies,bWriteable=true)
    VariableLinks(2)=(ExpectedType=class'SeqVar_Float',LinkDesc="TimeInterval",PropertyName=TimeInterval,bWriteable=true)

    OutputLinks(0)=(LinkDesc="Out")
    OutputLinks(1)=(LinkDesc="Finished")
    bAutoActivateOutputLinks=false
}

PFMSeqEvent_WaveComplete

El evento que determina que se ha completado una oleada es muy sencillo, pués básicamente sólo se define el evento para que desde otra parte del código pueda activarse este.

class PFMSeqEvent_WaveComplete extends SequenceEvent;

defaultproperties
{
    ObjName="WaveComplete"
    ObjCategory="PFM"
    //VariableLinks.Empty
    bPlayerOnly=false
}

PFMEnemySpawner

Al PFMEnemySpawner que creamos aquí se ha añadido la función HasEnemiesLeft que determina si le quedan enemigos por generar.

class PFMEnemySpawner extends Actor
    placeable;

// Cilindro en el que se hará el spawn de los enemigos, editable en el editor
var() editconst const CylinderComponent	CylinderComponent;

// Enemigos que quedan por "spawnear". Puede configurarse en el editor con un valor inicial.
var() int enemiesLeft;

// Tiempo entre Spawns
var float timeInterval;

auto state Spawning
{
    local Vector newSpawnLocation;

    Begin:
    while(true)
    {
        if(enemiesLeft > 0)
        {
            // Cálculo de localización dentro del cilindro
            newSpawnLocation = calculateNewSpawnLocation();

            // Se intenta hacer spawn del enemigo
            if(SpawnPFMEnemy(newSpawnLocation) != none)
            {
                enemiesLeft--;
                if(PFMGame(WorldInfo.Game) != none)
                {
                    PFMGame(WorldInfo.Game).EnemyCreated();
                }
            }
        }
        Sleep(timeInterval);
    }
}

// Calcula una posición de Spawn dentro del Cilindro
function Vector calculateNewSpawnLocation()
{
    local Vector newSpawnLocation;

    newSpawnLocation = VRand();     //Vector unitario aleatorio
    newSpawnLocation.Z = 0;         //Se elimina la componente Z (vertical)
    Normal(newSpawnLocation);       //Se normaliza el nuevo vector
    newSpawnLocation *= CylinderComponent.CollisionRadius * FRand();  //Se le da una longitud aleatoria
    newSpawnLocation += Location;   //Se suma a la posición del spawner

    return newSpawnLocation;
}

function Pawn SpawnPFMEnemy(Vector spawnLocation)
{
    local AIController EC;
    local Pawn EP;

    // Spawn del EnemyPawn
    EP = spawn(class'PFMEnemyPawnSpeeder',self,,spawnLocation);
    if(EP == none)
    {
        // En caso de no haber sido posible hacer el spawn
        return none;
    }
    // Spawn del EnemyController
    EC = spawn(class'PFMEnemyControllerSpeeder');
    // El EnemyController posee al EnemyPawn
    if(EC != none && EP != none)
    {
        EC.Possess(EP,false);
    }
    return EP;
}

function SpawnPFMEnemies(int num, optional float newTimeInterval)
{
    enemiesLeft += num;
    timeInterval = newTimeInterval;
}

function bool HasEnemiesLeft()
{
    if(enemiesLeft > 0)
    {
        return true;
    }
    return false;
}

defaultproperties
{
    // Cilindro que define el area de spawn
    Begin Object Class=CylinderComponent NAME=CollisionCylinder
		CollideActors=true
		CollisionRadius=+0040.000000
		CollisionHeight=+0040.000000
		bAlwaysRenderIfSelected=true
	End Object
	CollisionComponent=CollisionCylinder
	CylinderComponent=CollisionCylinder
	Components.Add(CollisionCylinder)

    // Sprite para verlo en el editor
    Begin Object Class=SpriteComponent Name=Sprite
        Sprite=Texture2D'EditorResources.S_Actor'
        HiddenGame=False
    End Object
    Components.Add(Sprite)

    enemiesLeft=0       // Número de enemigos que aparecen por defecto al inicio
    timeInterval=1      // 1 segundo por defecto
}

PFMGame

En PFMGame se han añadido bastantes funciones que servirán para saber en todo momento cuántos enemigos quedan en el escenario. Cuando el número de enemigos llega a 0 significa que la oleada se ha destruido, por lo que se activa el evento Wave Complete para pasar a la siguiente.


class PFMGame extends UDKGame;

var PFMEnemySpawner enemySpawner;

var int TotalEnemiesAlive;  // Número total de enemigos que hay vivos
var bool bInWaveGeneration; // Determina si estamos generando enemigos de la ola

simulated function PostBeginPlay()
{
    local PFMEnemySpawner ES;

    super.PostBeginPlay();

    //Se busca el enemySpawner
    foreach DynamicActors(class'PFMEnemySpawner',ES)
        enemySpawner = ES;
}

function EnemyKilled()
{
    TotalEnemiesAlive--;

    // Fin de la ronda
    if(TotalEnemiesAlive <= 0 && !bInWaveGeneration)
    {
        TriggerGlobalEventClass(class'PFMSeqEvent_WaveComplete', self);
    }
}

function EnemyCreated()
{
    TotalEnemiesAlive++;
    bInWaveGeneration = true;      // Si se crean enemigos se activa
}

// LLamado desde la acción de generación de enemigos cuando se genera el último enemigo
function FinishWave()
{
    bInWaveGeneration = false;  // La ola se ha generado
}

exec function SpawnEnemies(int num)
{
    enemySpawner.SpawnPFMEnemies(num);
}

exec function SpawnEnemy()
{
    enemySpawner.SpawnPFMEnemies(1);
}

defaultproperties
{
    TotalEnemiesAlive=0;

    //Se especifica el Pawn por defecto
    DefaultPawnClass=class'PFM.PFMPawn'
    //Se especifica el controlador
    PlayerControllerClass=class'PFM.PFMPlayerController'
    //HUD
    HUDType=class'PFM.PFMHUD'
}

Con esto ya tenemos una manera de generar y sincronizar oleadas de enemigos usando Kismet. En el futuro se mejorará para pemitir crear enemigos de diferentes tipos a la vez.
Banner Blog

16 comentarios en “PFM: Scripting de las oleadas de enemigos con Kismet

  1. Saludos Marcos …..

    haciendo esta parte del blog me han salido dos errores…. referentes al «PFMSeqAct_SpawnEnemies» q no eh podido solucionar … supuestamente tiene q ver junto al
    «PFMEnemySpawner»……

    y el otro a los «SpawnPFMEnemies» en la clase «PFMSeqAct_SpawnEnemies» no se si abra q poner un numero obligatorio o tal vez hize algo q no me eh dado cuenta o no eh caido encuenta aqui te dejo una imagen de los errores.

    «http://fotos.subefotos.com/db1251e046991b01c4aaff9b99b47b1co.png»

    Gracias por todo.

    • Revisa el código. El primer error dice que no encuentra la función HasEnemiesLeft de PFMEnemySpawner, pero sí está, comprueba que esté bien escrito. El segundo error dice que la función SpawnPFMEnemies toma 1 argumento, cuando en PFMEnemySpawner vemos que toma 2. Por eso, comprueba que hayas modificado el código correctamente.

    • Usando el botón derecho del ratón se despliega un menú desde el que puedes acceder a todos los nodos disponibles de Kismet, incluídos aquellos que crees 😉

    • Tienes que abrir el editor de Kismet. Es el botón que tiene una K en la barra de herramientas de la parte superior del editor. Busca algún tutorial básico de Kismet si te ves muy perdido 😛

    • El nodo creado en este caso es una Acción, por lo que estará en el menú New Action > PFM. Si no está ahi es que no se ha compilado correctamente.

  2. ya pude solucionar el problema muchas gracias Marcos por la ayuda, Marcos una pregunta a mi me gustaria que en el hud me aparezca el numero de enemigos que faltan por destruir y coloque este codigo en el hud
    DrawString(«Enemies:»@string(AlarmEnemySpawner(Owner).enemiesLeft),10,110,255,255,255,200);

    pero me aparece en 0 me gustaria saber que tengo mal o como puedo acceder al numero de enemigos.

    muchas Gracias por la atencion

    • Hola Camilo, ¿qué es AlarmEnemySpawner? Si es como mi PFMEnemySpawner lo que hay en enemiesLeft es el número de enemigos que quedan por aparecer en el escenario, no los que quedan por eliminar.
      Para mostrar los enemigos que quedan por destruir tendrías que llevar la cuenta de los enemigos que se crean, almacenando su número por ejemplo en tu clase de GameInfo, y ajustar ese número cuando se van eliminando.

  3. Hola, tengo un warning al compilar el codigo, el error es el siguiente:

    PFMSeqAct_SpawnEnemies.uc(42) : Warning, Unknown property in defaults: lastOfWave=false (looked in PFMSeqAct_SpawnEnemies)

    Supongo que es lo que está ocasionando que al terminar una oleada no empiece la siguiente.

    Gracias de antemano!

    • Hola Ness, por lo que veo hay una pequeña errata. En las defaultproperties de PFMSeqAct_SpawnEnemies, la variable lastOfWave debería ser bLastOfWave, tal como se ha declarado en la parte superior.
      Imagino que es eso, si sigue sin funcionar no dudes en decirmelo.
      Un saludo 🙂

  4. Sí, ese era el problema del Warning, pero el «problema importante» sigue ahi, y esque la primera oleada aparece correctamente, pero al terminarla la siguiente no empieza, la secuencia de kismet la tengo aparentemente como tu, pero con menos nodos de spawn

    Todo parece estar bien, los nodos de link y wave complete se pueden repetir sin problemas, etc.

    Que podria ser? el Pawn o el AI que utilizé? (no son los de tus tutoriales)

    • Comprueba si el evento WaveComplete se está invocando en Kismet con un nodo de Log por ejemplo. También asegúrate que el nodo del final de cada oleada está marcado como bLastOfWave = TRUE, que por lo que veo, en la imagen que muestro no es así…

Deja un comentario