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:
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.
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.
Que pena molestarlo tanto marcos pero me gustaria saber como creo el nodo
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 😉
pero en que parte lo creo marcos no he podido hacerlo :S perdón por la intensidad
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 😛
no eso yo lo entiendo
muy bn yo ya he he hecho cosas en kismet pero no logro es crear el nodo personalizado
Simplemente debes crear el código necesario, compilarlo y al arrancar el editor ya aparecerá en el menú de Kismet
Que pena volverlo a molestar marcos pero no he podido selecciono el PFMEnemySpawner y en el kismet me aparecen estas opciones pero ninguna se parece a la que esta en la imagen
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.
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.
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 🙂
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í…