Anteriormente ya hemos creado un PNJ, pero era algo de prueba y para aprender. En esta ocasión crearemos el primer personaje que aparecerá en el juego final, así que es un gran día :lol:.
El Speeder es un pequeño robot arácnido cuyo único fin es alcanzar el objetivo (PFMTarget) y autodestruirse para causarle el mayor daño posible. No hará caso del jugador, por lo que su inteligencia será bastante sencilla. Vamos a ver cómo ha sido su desarrollo, desde el diseño inicial a la programación de su comportamiento.
Diseño conceptual
Desde el comienzo el aspecto que se ha buscado es el de un sencillo robot con forma arácnida.
Primeros bocetos guarreros:
Diseño provisional:
Se probaron diversos diseños para la cabeza:
Finalmente se optó por una cabeza más plana y sencilla. El motivo es que se quiere dar la impresión de que el robot es de inteligencia simple, por lo que no debe tener un «cerebro» tan grande:
Modelado 3D
Decidido más o menos el diseño final, se comienza con un modelo inicial básico para comprobar tamaño y proporciones.
Probándolo en UDK.
Modelo final del Speeder, ya más refinado.
Rigging
Para poder animar el modelo hay que crear un esqueleto y aplicarlo al mismo. Este esqueleto no se ve luego, son sólo unas guías para animar.
Modelo final en UDK
Le faltan texturas!
Texturizado
Para texturizar el modelo primero se hace un Unwrap del mismo, que es básicamente desdoblar todos sus polígonos sobre una superficie plana sobre la que se podrá pintar la textura:Una vez hecho el Unwrap ya se puede pasar a crear las texturas. En este caso he usado 2 texturas: Una para el color difuso (izquierda) y otra para el canal Emisivo (derecha). El canal Emisivo no se ve influenciado por la iluminación, de modo que esas partes de la textura se verán cuando el personaje esté en la oscuridad.
He aquí el material creado para el Speeder. La parte más compleja es el canal Emisivo, pues se le aplica una función Seno que varía con el tiempo para que se ilumine de forma intermitente.
Y aquí el resultado dentro de UDK:
En la oscuridad. Como se ve los visores siguen iluminados.
Animación
Para el Speeder no es necesaria más que una animación, la de caminar:
Y el AnimTree será muy sencillo:UnrealScript
Y ahora la parte más «divertida», la programación 😀
Como hemos comentado la inteligencia del Speeder es sencilla. Su objetivo es dirigirse al PFMEnemyTarget y una vez allí autodestruirse para inflingirle daño.
PFMEnemyTarget
En primer lugar se modifica el PFMEnemyTarget, añadiéndole un cilindro de colisión para determinar cuándo un enemigo está suficientemente cerca. Este cilindro es modificable en el editor.
class PFMEnemyTarget extends Actor placeable; // Cilindro que abarca toda la zona objetivo var() editconst const CylinderComponent CylinderComponent; defaultproperties { // Cilindro que define la zona objetivo Begin Object Class=CylinderComponent NAME=CollisionCylinder CollideActors=true CollisionRadius=+0200.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.LookTarget' HiddenGame=False End Object Components.Add(Sprite) bCollideActors=true }
PFMEnemyController
En PFMEnemyController ya tenemos el comportamiento necesario para navegar por el mapa y dirigirse al objetivo, por lo que el controlador del Speeder heredará de esta clase directamente. No obstante se ha añadido una función para notificar al controlador que el Pawn que posee ha llegado al objetivo. Esta función pasa al estado AtFinalTarget, que en PFMEnemyController está vacío, pero en el controlador del Speeder lo sobreescribiremos para que se inice la autodestrucción.
// Cada enemigo hará cosas diferentes state AtFinalTarget { } // Usada por el Pawn para notificar que se ha colisionado con el Target function NotifyTouchTarget(PFMEnemyTarget target) { if(target == FinalTarget) { GoToState('AtFinalTarget'); } }
PFMEnemyPawn
Esta clase si se ha modificado sustancialmente. Código completo:
class PFMEnemyPawn extends UDKPawn placeable; // Efectos de destrucción var ParticleSystem DestructionExplosion; var SoundCue ExplosionSound; var float DestructionDamage; //Daño de destrucción var float DestructionDamageRadius; //Radio del daño de destrucción event Touch( Actor Other, PrimitiveComponent OtherComp, vector HitLocation, vector HitNormal ) { Super.Touch(Other,OtherComp,HitLocation,HitNormal); if(PFMEnemyTarget(Other) != None) { PFMEnemyController(Controller).NotifyTouchTarget(PFMEnemyTarget(Other)); } } state Dying { event BeginState(Name PreviousStateName) { Super.BeginState(PreviousStateName); } Begin: // Efectos de destrucción SpawnDestructionEffects(); // Daño radial HurtRadius(DestructionDamage,DestructionDamageRadius,class'UTDmgType_Rocket',50000,Location); // Destrucción Destroy(); } // Activa los efectos de destrucción function SpawnDestructionEffects() { // Particulas de explosión if(DestructionExplosion != None) { WorldInfo.MyEmitterPool.SpawnEmitter(DestructionExplosion, Location, rotator(vect(0.0,0.0,1.0))); } // Sonido de destrucción if (ExplosionSound != None) { PlaySound(ExplosionSound, true); } } defaultproperties { // Iluminación de entorno del Pawn Begin Object Class=DynamicLightEnvironmentComponent Name=MyLightEnvironment bSynthesizeSHLight=TRUE bIsCharacterLightEnvironment=TRUE bUseBooleanEnvironmentShadowing=FALSE InvisibleUpdateTime=1 MinTimeBetweenFullUpdates=.2 End Object Components.Add(MyLightEnvironment) // SkeletalMesh Begin Object Class=SkeletalMeshComponent Name=EnemyMesh SkeletalMesh=SkeletalMesh'Test.Enemy.Enemy' AnimSets(0)=AnimSet'Test.Enemy.EnemyAnimSet' AnimTreeTemplate=AnimTree'Test.Enemy.EnemyAnimTree' LightEnvironment=MyLightEnvironment //bEnableSoftBodySimulation=True //bSoftBodyAwakeOnStartup=True bAcceptsLights=True //Scale3D=(X=0.25,Y=0.25,Z=0.5) End Object Mesh=EnemyMesh Components.Add(EnemyMesh) // Cilindro de colisión Begin Object Name=CollisionCylinder CollisionRadius=40.0 CollisionHeight=25.0 BlockNonZeroExtent=true BlockZeroExtent=true BlockActors=true CollideActors=true End Object CollisionComponent=CollisionCylinder Components.Add(CollisionCylinder) // Se especifica el controlador ControllerClass=class'PFM.PFMEnemyController' // Efectos de destrucción DestructionExplosion=ParticleSystem'WP_RocketLauncher.Effects.P_WP_RocketLauncher_RocketExplosion' ExplosionSound=SoundCue'A_Character_BodyImpacts.BodyImpacts.A_Character_RobotImpact_BodyExplosion_Cue' DestructionDamage=10 DestructionDamageRadius=60 GroundSpeed=400.0 //Velocidad máxima en suelo RotationRate=(Pitch=40000,Yaw=40000,Roll=40000) // Ratio de rotación SightRadius=+05000.000000 // Max distancia de vista }
Se ha añadido el evento Touch para detectar cuándo un Pawn ha alcanzado su objetivo. En ese momento se invoca la función NotifyTouchTarget de PFMEnemyController. Se ha modificado el estado Dying para que al morir se genere una explosión y un sonido, evitando que el personaje desaparezca sin mas.
PFMEnemyControllerSpeeder
El controlador del Speeder es muy sencillo. Al heredar todo el comportamiento de movimiento de PFMEnemyController solo hay que añadir que cuando entre al estado AtFinalTarget inicie la autodestrucción, de lo que se encarga el Pawn.
class PFMEnemyControllerSpeeder extends PFMEnemyController; state AtFinalTarget { Begin: SelfDestroy(); } function SelfDestroy() { PFMEnemyPawnSpeeder(Pawn).InitSelfDestroy(); } defaultproperties { }
PFMEnemyPawnSpeeder
Como es común con otros personajes, en el Pawn definimos el SkeletalMesh y demás componentes. En este caso además, se incluye la funcionalidad para autodestruirse que posee el Speeder. Al activar el estado SelfDestroy se reproduce un sonido y hace que el Speeder se destruya activando el estado Dying (heredado de PFMEnemyPawn)
class PFMEnemyPawnSpeeder extends PFMEnemyPawn; var SoundCue ArmedSound; //Sonido previo a autodestrucción state SelfDestroy { Begin: PlaySound(ArmedSound, true); Sleep(0.5); GoToState('Dying'); } function InitSelfDestroy() { GoToState('SelfDestroy'); } defaultproperties { // SkeletalMesh Begin Object Name=EnemyMesh SkeletalMesh=SkeletalMesh'Enemies.Speeder.Speeder' AnimSets(0)=AnimSet'Enemies.Speeder.AS_Speeder' AnimTreeTemplate=AnimTree'Enemies.Speeder.AT_Speeder' LightEnvironment=MyLightEnvironment PhysicsAsset=PhysicsAsset'Enemies.Speeder.Speeder_Physics' //bEnableSoftBodySimulation=True //bSoftBodyAwakeOnStartup=True bAcceptsLights=True //Scale3D=(X=0.25,Y=0.25,Z=0.5) End Object //Components.Add(EnemyMesh) // Cilindro de colisión Begin Object Name=CollisionCylinder CollisionRadius=20.0 CollisionHeight=13.0 BlockNonZeroExtent=true BlockZeroExtent=true BlockActors=true CollideActors=true End Object CollisionComponent=CollisionCylinder Components.Add(CollisionCylinder) // Se especifica el controlador ControllerClass=class'PFM.PFMEnemyControllerSpeeder' ArmedSound=SoundCue'A_Vehicle_Cicada.SoundCues.A_Vehicle_Cicada_TargetLock' GroundSpeed=200.0 //Velocidad máxima en suelo MaxStepHeight=15.0 DrawScale=1 }
Eso es todo! Veamos qué tal funciona el Speeder.
Esta parte es muy didáctica, me ha gustado!. ¿Pero podrías indicarnos con detalle cómo se hace el UnWrap de un modelo?
El unwrapping de un modelo es un asunto más relacionado con el modelado que con UDK, y hay muchas maneras de hacerlo. Según el programa de modelado que se use habrá diferentes técnicas, pero es un tema muy común, por lo que hay muchos tutoriales sobre ello en internet.
El objetivo es asignar a cada vértice del modelo una coordenada de la textura, o visto de forma inversa, desdoblar el modelo y aplanarlo sobre la textura, parecido a como se hacen los patrones de la ropa.
Muy buen post. Me ha gustado mucho como pasas de la parte del diseño conceptual (bocetos) al modelado (supongo en 3ds Max) y posteriormente la importación al UDK. La parte de programación a mi me gusta menos, pero lo expones todo bastante claro.
A la espera del siguiente
Gracias Alejandro 🙂
Ciertamente la programación es la parte menos amigable, sobre todo para el que no le guste, pero es la única manera de dar «vida» a los personajes.
Saludos
Saludos Marcos ….
me habia puesto inactivo un tiempo ya q me enferme y estuve maluco un tiempo.
pero volvi para q me aclares mas dudas xd.. espero no te moleste XD…
en esta parte tengo un problema…. q cuando llegan al punto indicado los uñecos no estallan….
pero les puedo matar yo y funciona de hasta el sonido hasta la explosion….
pero por mas q le eche cabeza y le alla modificado quede igual….
y no se si tambien hacen daño.. las arañitas … aunque creo q en un parte dice un radio de daño tambien… espero no estar equivocado…
Gracias por todo ^_^
Cuando te atasques en algo que no funciona como debería debes intentar detectar hasta qué punto está funcionando. En este caso como has visto, lo primero que se llama es el evento Touch del Pawn. Este evento comprueba si la araña ha llegado al target y en ese caso indica al controller que ha llegado, lo que hace que posteriormente se destruya. Si como dices al llegar al target no se destruye, será que un punto de la cadena no está funcionando, lo que debemos saber es cuál. Para averiguarlo se me ocurre por ejemplo que llames a Destroy() directamente en el evento Touch. De este modo, si verdaderamente entra en ese evento, la araña se destruirá. Si no se destruye, significará que no se está activando el evento Touch y que por tanto no está entrando realmente en el target por algún motivo, no sé si me explico. Si has usado el código tal cual debe ser que no entra en Touch porque si lo hiciera la araña se destruiría. Quizá el target no está bien posicionado? Haz algunas pruebas a ver.
Pingback: PFM: Diseño y creación de enemigo Razor | El Blog de Marcos
El diseño final de la cabeza del Speeder parece un producto de apple, o un robot aspirador de esos automaticos. xD Me ha encantado.
Por cierto, ¿En que ordenador realizas el trabajo?¿Puedes poner mas o menos sus prestaciones?
Gracias.
Gracias xD. Sí, mi intención era que tuvieran un aspecto muy limpio y sencillo, casi minimalista, no que fueran los típicos robots llenos de tornillos por todos lados. Quizá por eso recuerda a un producto Apple xD
Mi ordenador es un i5, 8GB de RAM y una 560 Ti. No es la repera pero tira bastante bien de momento 😉
Muy buenas Marcos y ya que estamos feliz año (mejor tarde que nunca) repasando tus tutos me e encotrado con algo, nose si estare equivocado pero para que funcione bien el «PFMEnemyTarget» en la linea 21 en lugar de ser «Begin Object Name=Sprite» no deberia ser «Begin Object Class=SpriteComponent Name=Sprite»? o talvez no afecta?
Hola Manu, feliz año igualmente! 🙂
Efectivamente tienes razón! No sé por qué WordPress siempre me elimina esa parte de «Class=…» al poner el código y tengo que corregirlo manualmente, pero en este caso se me pasó. Gracias por señalarlo!