PFM: Diseño y creación de enemigo Razor (y parte 2)

En la primera parte vimos los pasos que se siguieron desde el diseño conceptual del Razor hasta su modelado, animación, texturizado y programación, consiguiendo que persiguiera al jugador por el escenario. En esta última parte veremos cómo se han implementado los ataques del Razor, que serán cuerpo a cuerpo. Para ello, nos basaremos en lo aprendido en las entradas sobre combate cuerpo a cuerpo (parte 1 y parte 2). La diferencia será que usaremos AnimNotifies para que sea cada animación la que indique al código el inicio y final de la fase de ataque en cada caso.

Lo primero es tener las animaciones de ataque. Para darle variedad he hecho 3 diferentes, que se usarán de forma aleatoria:

Una vez importadas en UDK se especifican para cada animación los AnimNotifies. Los AnimNotifies es una manera que tiene UDK de invocar funciones de código desde la propia animación. Si por ejemplo tuviéramos una animación de un personaje corriendo, podrían usarse las notificaciones para que se oyera un sonido de pisada cuando la animación lo sugiera. Hay varios tipos de AnimNotifies, en este caso he usado los de tipo Script, que llaman automáticamente a la función de código que se les especifique. Esta función debe existir en el Pawn que ejecute esta animación concreta.

Como se ve en las animaciones de ataque del Razor, tienen una fase en la que el personaje se prepara para lanzar el ataque, una segunda fase en la que lanza el ataque y una última fase en la que recupera la posición normal. Con los AnimNotifies se indica al código el momento en que comienza la segunda fase (y por tanto el trazado para detectar colisiones) y en qué momento termina (para dejar de detectar colisiones). Así, para cada animación se crean 2 notificaciones, especificando el tiempo en que se producen y el nombre de la función a invocar en cada caso:

pfm13_01Igual que al implementar el combate cuerpo a cuerpo con el personaje protagonista, habrá que añadir al árbol de animaciones un nodo AnimNodeSlot en el que poder cargar las animaciones desde el código (renombrado en mi caso como RazorAnimNode):

pfm13_02

Además se crearon sockets en la base y punta de cada una de las patas del Razor (6 sockets en total), usados a la hora de detectar las colisiones con el entorno.

A continuación los cambios en el código.

PFMEnemyControllerRazor

Como veremos el código del controller no ha variado demasiado:


class PFMEnemyControllerRazor extends PFMEnemyController;

var PFMPawn PlayerPawn;     // Referencia al Pawn del jugador
var PFMPawn PlayerPawnAux;     // Referencia al Pawn del jugador

// Estado inicial. Búsqueda de jugador
auto state SearchPlayer
{
Begin:
    foreach DynamicActors(class'PFMPawn', PlayerPawnAux)
    {
        PlayerPawn = PlayerPawnAux;
        // Se queda con el que pille (En principio sólo habrá uno)
    }

    if(PlayerPawn != none)
    {
        `log(">>>> Encontrado PlayerPawn");
        GoToState('HuntPlayer');
    }

    `log(">>>> No encontrado Player");
    Sleep(1.0);
    GoTo 'Begin';
}

state HuntPlayer
{
    // Calcula el camino al objetivo
    function bool FindNavMeshPath()
    {
        NavigationHandle.PathConstraintList = none;
        NavigationHandle.PathGoalList = none;
        // Create constraints
        class'NavMeshPath_Toward'.static.TowardGoal( NavigationHandle, PlayerPawn );
        class'NavMeshGoal_At'.static.AtActor( NavigationHandle, PlayerPawn, 32 );
        // Find path
        return NavigationHandle.FindPath();
    }

Begin:

    if(PlayerPawn == None)
    {
        GoToState('SearchPlayer');
    }

    if ( NavigationHandle == None )
    {
        `log(">>>>>>MEC!!! - NavigationHandle Erroneo");
        InitNavigationHandle();
    }
    else
    {
        InitNavigationHandle();
    }

    if( NavigationHandle.ActorReachable(PlayerPawn))
    {
        `log(">>>> Puedo llegar directamente al Pawn");
        MoveToward(PlayerPawn, PlayerPawn, 32);

        if(Pawn.ReachedDestination(PlayerPawn))
        {
            GoToState('AttackPlayer');
        }
    }
    else if (FindNavMeshPath())
    {
        //NavigationHandle.DrawPathCache(,true);
        NavigationHandle.SetFinalDestination(PlayerPawn.Location);

        if(NavigationHandle.GetNextMoveLocation(NextDest, Pawn.GetCollisionRadius()*5))
        {
            `log(">>>> Me muevo al siguiente punto");
            MoveTo(NextDest,none,128.0);
        }
        else
        {
            `log(">>>> No hay siguiente punto en el camino");
            MoveToward(PlayerPawn);
        }
    }
    else
    {
        `log(">>>> No se encuentra camino posible");
        MoveToward(PlayerPawn);
    }

    goto 'Begin';
}

state AttackPlayer
{

Begin:

    if(PlayerPawn == None)
    {
        GoToState('SearchPlayer');
    }

    Sleep(0.1);
    if(!Pawn.ReachedDestination(PlayerPawn))
    {
        GoToState('HuntPlayer');
    }

    PFMEnemyPawnRazor(Pawn).StartAttack();

    Sleep(0.4);
    if(!Pawn.ReachedDestination(PlayerPawn))
    {
        PFMEnemyPawnRazor(Pawn).StopAttack();
        GoToState('HuntPlayer');
    }

    Sleep(1.5);
    if(!Pawn.ReachedDestination(PlayerPawn))
    {
        GoToState('HuntPlayer');
    }
    goto 'Begin';
}

defaultproperties
{
}

Lo más destacable es la adición del estado AttackPlayer, que se activará cuando el Razor esté lo suficientemente cerca del jugador, y cuyo objetivo será activar los ataques en el Pawn del Razor. Adicionalmente se realizan algunas comprobaciones durante el ataque para que este se interrumpa si el jugador se pone fuera del alcance del mismo.

PFMEnemyPawnRazor

Si ya se ha entendido el código del combate cuerpo a cuerpo (parte 1 y parte 2) este no sorprenderá demasiado, siendo incluso algo más sencillo:


class PFMEnemyPawnRazor extends PFMEnemyPawn;

var array <Name> AttackAnimations;         // Nombres de las animaciones a usar
var AnimNodeSlot RazorAnimNode;             // Nodo de animaciones

// Sockets
var Name backBaseSocketName;
var Name backTipSocketName;
var Name rightBaseSocketName;
var Name rightTipSocketName;
var Name leftBaseSocketName;
var Name leftTipSocketName;

// Sockets usados en este momento
var Name baseSocketName;
var Name tipSocketName;

var array<Vector> PreviousPoints;       // Puntos de la pata en el frame anterior
var array<Vector> CurrentPoints;        // Puntos de la pata en el frame actual
var int numPoints;                      // Número de puntos totales en que se divide la pata

var bool bTracing;                      // Determina si hay que hacer el cálculo del traceado o no
var bool bHitPlayer;                    // Determina si se ha impactado con el jugador

var SoundCue HurtSound;                 // Sonido de daño

simulated event PostInitAnimTree(SkeletalMeshComponent SkelComp)
{
    Super.PostInitAnimTree(SkelComp);

    // Se busca la referencia del nodo de animaciones
    RazorAnimNode = AnimNodeSlot(SkelComp.FindAnimNode('RazorAnimNode'));
}

simulated function PostBeginPlay()
{
    Super.PostBeginPlay();

    ResetTraceSwing(0);
}

function StartAttack()
{
    local int anim;

    anim = Rand(AttackAnimations.Length);

    //Se resetea el sistema de traceado
    ResetTraceSwing(anim);

    //Se activa la animación correspondiente
    RazorAnimNode.PlayCustomAnim(AttackAnimations[anim], 1.5,0.05,0.1,false,true);
}

function StopAttack()
{
    RazorAnimNode.StopCustomAnim(0.2);
    NotifyStopTrace();
}

simulated event Tick(float DeltaTime)
{
    super.Tick(DeltaTime);

    if(bTracing)
    {
        TraceSwing();
    }
}

//Resetea el sistema de traceado
function ResetTraceSwing(int anim)
{
    local Vector BaseSocketLocation, TipSocketLocation;
    local int i;

    // Posición de los sockets
    if(anim == 0)
    {
        baseSocketName = backBaseSocketName;
        tipSocketName = backTipSocketName;
    }
    else if(anim == 1)
    {
        baseSocketName = rightBaseSocketName;
        tipSocketName = rightTipSocketName;
    }
    else
    {
        baseSocketName = leftBaseSocketName;
        tipSocketName = leftTipSocketName;
    }

    BaseSocketLocation = GetSocketLocation(baseSocketName, Mesh);
    TipSocketLocation = GetSocketLocation(tipSocketName, Mesh);

    // Reseteo de los arrays
    PreviousPoints.Remove(0,PreviousPoints.length);
    CurrentPoints.Remove(0,PreviousPoints.length);

    // Valores iniciales
    for(i = 0; i < numPoints; i++)
    {
        PreviousPoints.AddItem(VLerp(BaseSocketLocation,TipSocketLocation,i/float(numPoints-1)));
        CurrentPoints.AddItem(VLerp(BaseSocketLocation,TipSocketLocation,i/float(numPoints-1)));
    }

    bHitPlayer = false; // Se resetea
}

// Traceado de impactos
function TraceSwing()
{
    local Vector BaseSocketLocation, TipSocketLocation;
    local int i;
    local Actor HitActor;
    local Vector HitLocation, HitNormal, Momentum;

    // Posición de los sockets
    BaseSocketLocation = GetSocketLocation(baseSocketName, Mesh);
    TipSocketLocation = GetSocketLocation(tipSocketName, Mesh);

    // Actualización de posiciones
    for(i = 0; i < numPoints; i++)
    {
        CurrentPoints[i] = VLerp(BaseSocketLocation,TipSocketLocation,i/float(numPoints-1));
    }

    // Cálculo de impactos
    for(i = 0; i < numPoints; i++)
    {
        // Se recorren los actores interceptados por el traceado
        foreach TraceActors(class'Actor', HitActor, HitLocation, HitNormal, PreviousPoints[i], CurrentPoints[i])
        {
            // Si se impacta con el jugador (y es la primera vez en este ataque)
            if(PFMPawn(HitActor) != None && !bHitPlayer)
            {
                bHitPlayer = true;  // Impactado jugador
                Momentum = Normal(HitLocation - Location) * 60000;
                HitActor.TakeDamage(10, Controller, HitLocation, Momentum, class'DamageType');
                //SpawnHitEffects(HitLocation,HitNormal);
            }
        }
    }

    // Debug lines
    /*for(i = 0; i < numPoints; i++)
    {
        DrawDebugLine(PreviousPoints[i],CurrentPoints[i],0,255,0,true);
    }*/

    // Actualización de puntos anteriories
    for(i = 0; i < numPoints; i++)
    {
        PreviousPoints[i] = CurrentPoints[i];
    }
}

function PlayHit(float Damage, Controller InstigatedBy, vector HitLocation, class<DamageType> damageType, vector Momentum, TraceHitInfo HitInfo)
{
    if (HurtSound != None)
	{
		PlaySound(HurtSound, true);
	}

    Super.PlayHit(Damage,InstigatedBy,HitLocation,damageType, Momentum, HitInfo);
}

function vector GetSocketLocation(Name SocketName, SkeletalMeshComponent SMC)
{
    local SkeletalMeshSocket SMS;

    if (SMC != none)
    {
        SMS = SMC.GetSocketByName(SocketName);
        if (SMS != none)
        {
            return SMC.GetBoneLocation(SMS.BoneName);
        }
    }
}

function NotifyStartTrace()
{
    bTracing = true;
}

function NotifyStopTrace()
{
    bTracing = false;
}

defaultproperties
{
    // SkeletalMesh
    Begin Object Name=EnemyMesh
        SkeletalMesh=SkeletalMesh'Enemies.Razor.SM_Razor'
        AnimSets(0)=AnimSet'Enemies.Razor.AS_Razor'
        AnimTreeTemplate=AnimTree'Enemies.Razor.AT_Razor'
        LightEnvironment=MyLightEnvironment
//        PhysicsAsset=PhysicsAsset''
        //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=30.0
        CollisionHeight=46.0
        BlockNonZeroExtent=true
        BlockZeroExtent=true
        BlockActors=true
        CollideActors=true
    End Object
    CollisionComponent=CollisionCylinder
    Components.Add(CollisionCylinder)

    // Se especifica el controlador
    ControllerClass=class'PFM.PFMEnemyControllerRazor'

    AttackAnimations=("Razor_Attack1","Razor_Attack2","Razor_Attack3")

    backBaseSocketName="BackBase"
    backTipSocketName="BackTip"
    rightBaseSocketName="RightBase"
    rightTipSocketName="RightTip"
    leftBaseSocketName="LeftBase"
    leftTipSocketName="LeftTip"

    numPoints=5

    Health=200
    GroundSpeed=600.0           //Velocidad máxima en suelo
    MaxStepHeight=15.0
    DrawScale=1.0

    HurtSound=SoundCue'Enemies.Razor.S_Razor_Hurt'

}

Las funciones StartAttack y StopAttack son llamadas desde el controlador y se encargan de iniciar el ataque o pararlo. StartAttack escoge aleatoriamente una animación de las disponibles y la ejecuta en el nodo de animación. Una vez iniciada la animación correspondiente, serán los AnimNotifies de dicha animación los encargados de invocar las funciones NotifyStartTrace y NotifyStopTrace, que se encargan respectivamente de activar y desactivar el trazado que detecta posibles colisiones. En la función Tick (que se ejecuta siempre cada frame) vemos que si el trazado está activado se hace el cálculo de colisiones con TraceSwing (casi idéntica a la de combate cuerpo a cuerpo, salvo variaciones), y si no está activo no se hace.

Ahora los Razor hacen algo más que molestar:


Banner Blog

Anuncios

82 pensamientos en “PFM: Diseño y creación de enemigo Razor (y parte 2)

  1. ¿Esta línea está completa?

    if(NavigationHandle.GetNextMoveLocation(NextDest, Pawn.GetCollisionRadius()*5))

    Me da: Error, bad or missing expression for token: NextDext, in call to “GetNextMoveLocation”, parameter 1

    Veo que extiendes de la clase del otro enemigo, yo la estoy haciendo independiente, no sé si será por eso (me da un error en playhit que seguro que sí).

    Estoy tratando de hacer funcionar primero el movimiento y luego ya revisaré el otro pawn del que extiendes para intentar recomponerlo todo en el nuevo.

    • Si, la línea está completa, lo que pasa que está usando la variable NextDest, que no está declarada en esta clase. En mi caso la tengo declarada en PFMEnemyController (del tutorial de NavigationMesh) y como PFMEnemyControllerRazor extiende de esa también hereda dicha variable. Si quieres hacer la clase única tendrás que declarar NextDest arriba:

      var Vector NextDest;

  2. Eso era, gracias. Ya tengo el enemigo buscando rutas. El que tenía antes era tonto y se entretenía en cada esquina. Ahora voy a por lo de las tortas.

  3. Tengo dos problemillas, el primero es que da error en la función PlayHit:
    Redefinition of “function playhit” differs from original

    La anulo y todo parece funcionar, el enemigo me persigue, me encuentra y empieza a lanzarme golpes.

    Pero… parece que no encuentra el socket tip y sólo me alcanza si el socket base toca mi Pawn. He revisado los nombres de los sockets y la existencia de éstos y todo esta bien.

    Mi duda es si para las colisiones el socket debe estar “dentro” de la malla, porque lo que he hecho es colocar el socket base en el antebrazo, en la muñeca derecha del pawn, justo antes de la mano.
    Y el socket tip lo he colocado en el mismo hueso, en el antebrazo, pero lo he extendido un par de metros fuera, como si fuese una espada imaginaria. Pero ese socket está en el aire, y además en el mismo bone del base.
    Comprobando las animaciones veo que describe un arco en horizontal, plano. O sea, que el “espadazo” alcanza a mi pawn en dos metros al menos. Y los notifys están bien.

    Pero parece que la colisión no existe, sólo cuando el socket base impacta. Hasta el socket tip es ignorado. Mi duda es si el socket tip debe estar contenido en una malla, o si debe estar en otro bone diferente al base.

    • El socket no importa que esté contenido en una malla, ya que lo único que necesitamos es su posición en el mundo para hacer el cálculo de colisiones.

      El problema es que en la función GetSocketLocation se obtiene la posición del hueso al que está asignado el socket. Por eso si asignas dos sockets al mismo hueso y aunque desplaces uno de ellos, la posición que te devolverá la función es la del hueso.

      Podrías modificar la función para que en vez de sacar la posición del hueso, saque la posición del socket. Existe la función GetSocketWorldLocationAndRotation que te devuelve la posición y rotación del socket. No recuerdo si hay algún motivo por el que no usé esta función pero con ella GetSocketLocation quedaría algo así (no la he probado):


      function vector GetSocketLocation(Name SocketName, SkeletalMeshComponent SMC)
      {
      local SkeletalMeshSocket SMS;
      local vector SocketLocation;

      if (SMC != none)
      {
      SMC.GetSocketWorldLocationAndRotation(SocketName, SocketLocation);
      return SocketLocation;
      }
      }

    • No me funcionó, pero de todas formas para mi caso es mejor como lo tengo, ya que lo que golpea realmente es el arma. Si tuviese que hacerlo de otra forma supongo que pondría el socket base en una articulación y el tip en la siguiente articulación, en bones distintos, que será como lo tienes hecho en tu caso.

      ¿Y lo del Playhit para qué sirve y como puedo hacer que funcione?

    • Si, yo los sockets los pongo en huesos diferentes y sin desplazarlos, por eso tal como está funciona bien.

      PlayHit es una función de Pawn que se llama cuando este recibe un daño. Se puede usar para que al recibir daño salga algún efecto de partículas, o se emita un sonido o cualquier cosa. Se suele usar para “mostrar” de alguna manera los daños.

      En mi caso la sobreescribo para que los Razor emitan un sonido cuando se les ataca. La razón de que no funcionara es que faltaba un “menor que” y un “mayor que” de nuevo… Ya está corregido y debería funcionar.

    • Ahora sí funciona, le puse un cue y lo reproduce al recibir el espadazo.

      ¿Tienes planeado implementar el mismo sistema de notifys al pawn propio para la espada?

      Ahora estoy liado con el pawn modular, que he conseguido que funcione con todo, morphs, físicas… por ahora no le estoy viendo ningún inconveniente y sí muchas ventajas.

    • Pues en principio lo dejaré con los timers como está. El sistema de combate del personaje principal es un poco diferente así que de momento lo dejaré así. Si me sobra tiempo quizá lo revise.

  4. Bueno, sigo sin resolver las dudas de antes, pero a base de no dormir esta noche lo he solucionado. He puesto una espada en el socket de la mano, y en la comprobación de la colisión uso los sockets de la espada en lugar de los del pawn. Y funciona bastante bien.

    La espada inicialmente aparece en la espalda y pasa a la mano en el momento de ataque. Tras finalizar este debe volver al socket de la espalda, pero funciona cuando quiere.

    El vídeo:

  5. ¿Cómo puedo asignar variables en el Pawn enemigo y consultarlas desde su controller?

    Lo que quiero hacer es que en la definición del Pawn se incluyan variables para lo que el Pawn puede o no hacer. Por ejemplo en default properties he puesto “PuedePerseguir=1”, declarando la variable como byte al principio.

    El problema viene luego en el controller, porque no sé como acceder a esa variable. He probado cosas como “Pawn(Pawn).PuedePerseguir”. Sin éxito.

    Tampoco sé cómo llamar a una función de ese Pawn, porque por ejemplo he hecho una función en el Pawn para cambiar el WeaponType que se llama “EnGuardia” y así cambiar la postura de idle cuando porta el arma. Lo llamo desde el controller con “TESTRazorPawn(Pawn).EnGuardia();” y funciona. Pero ¿cómo sería para quitar “TESTRazorPawn” y hacer referencia al Pawn que esté usando el controlador?

    • La manera en que lo haces es la correcta. Con TESTRazorPawn(Pawn).EnGuardia() accedes al Pawn que usa el controlador. Digamos que el TESTRazorPawn es necesario para que el programa sepa a qué tipo de Pawn estás accediendo.

      El Controller guarda una referencia al Pawn en una variable de tipo Pawn, que es por así decirlo, el Pawn más genérico. Tu TESTRazorPawn es una versión del Pawn más específica, pero no deja de ser un Pawn y por eso puede guardarse una referencia en una variable de tipo Pawn. Lo único es que cuando quieres acceder a variables o funciones propias de TESTRazorPawn, debes especificar que quieres acceder a esa “versión” del Pawn, y por eso hay que hacer lo que se llama un casting, especificando que el Pawn es un TESTRazorPawn: TestRazorPawn(Pawn).

      A partir de eso ya puedes acceder a todas las variables y funciones de TESTRazorPawn, incluídas las de Pawn, UDKPawn o cualquiera de los Pawn “padres” de TESTRazorPawn, ya que se heredan.

      Esto de la herencia, castings, etc son conceptos fundamentales de programación orientada a objeto (C++, Java…), así que entendidos en UnrealScript sirven igual para otros lenguajes.

  6. El caso es que TESTRazorPawn sería una de las clases que maneja el controller. La idea es tener varias clases de pawn usando el mismo controller.

    Lo que yo estaba haciendo era obtener el nombre del pawn que estaba usando el controller e intentar sacar los datos así. Más o menos es esto:

    en el Pawn:
    var byte PuedePerseguir;
    (en default properties:) PuedePerseguir=1

    y en el controller:
    var Pawn Enemigo;
    var byte PuedePerseguir
    (en possess:) Puederseguir=Enemigo.PuedePerseguir;

    Y da error en esa línea, no reconoce “.PuedePerseguir”

    Lo que no entiendo es que cambiando a:
    Puederseguir=Enemigo.Health;
    funciona, siendo Health otra variable declarada en defaultproperties.

    • Es lo que te comentaba, TestRazorPawn hereda todo lo que tiene la clase Pawn. Health es una variable definida en la clase Pawn, por tanto puedes acceder a su valor sin hacer el casting del Pawn a TestRazorPawn. Pero para acceder a variables o funciones definidas en TestRazorPawn, debes hacer el casting.

      Por otro lado la variable Pawn Enemigo te sobra, el controller ya tiene una variable Pawn que se llama Pawn (osea, en Controller hay un var Pawn Pawn) que guarda el Pawn que maneja.

      Es totalmente válido tener un solo controller y varios pawns diferentes PERO si el comportamiento de los Pawns va a ser muy diferente entre sí, quizá te convendría crear controllers diferentes con un controller padre común que tenga las funcionalidades compartidas por todos los controladores. Es el mismo caso que mi PFMEnemyController, que es padre de PFMEnemyControllerSpeeder y PFMEnemyControllerRazor.

  7. Bueno, con el casting “PPerseguir=TESTRazorPawn(Pawn).PuedePerseguir;” funciona perfectamente, pero al incluir el nombre de la clase ya no es posible usar ese controlador con varias clases, sino sólo con TESTRazorPawn.

    Habría que cambiar ese nombre con el nombre de la clase que esté utilizando el controller. He probado usando en el controller “var class Miclase;”, “var class Miclase;”, “var class Miclase;”, etc, pero todas dan “none” como resultado.

    “Pawn” no devuelve la clase en sí, sino la clase seguida de un “_” y un índice. He probado a intentar separar el primer trozo hasta el “_” usando MID, pero por lo visto no se puede hacer al ser operaciones sobre strings y siendo la variable de tipo class.

    ¿Hay forma de conocer el nombre exacto de la clase que posee el controller?

    Había pensado usar el método que dices de ir extendiendo a partir de una base con las rutinas más complejas, pero prefiero usar variables con un sólo controller. Piensa que puedes tener muchos enemigos distintos comportándose más o menos igual, pero con variables que indiquen si te persiguen nada más verte, o a qué distancia, o si están haciendo algo mientras esperan o simplemente quietos.
    Tener varios controllers para todos puede ser algo dificil de mantener, sobre todo cuando estás aprendiendo y cada día rehaces todo.

  8. Lo que te devuelve Pawn cuando haces un log por ejemplo, es el nombre de la instancia. Clase sólo hay una, pero puede y suele haber varias instancias (en este caso tantas como enemigos haya en el escenario) y es por eso que tienen un índice.

    Entonces, el problema básico es saber qué Pawn exactamente tiene el controller. Andar obteniendo el nombre y hacer operaciones de strings en el mismo es algo poco recomendable (y muy poco “elegante”). Pongamos que tienes un TestPawn1 y un TestPawn2. Puedes hacer lo siguiente para comprobar qué Pawn tiene el controller:

    if(TestPawn1(Pawn) != None)
    {
    // Ya estamos seguros que el Pawn es un TestPawn1
    TestPawn1(Pawn).funcionX();
    // Resto del comportamiento de TestPawn1
    }
    else if (TestPawn2(Pawn) != None)
    {
    TestPawn2(Pawn).funcionY();
    // etc
    }

    Si quieres hacerlo así con un sólo Controller perfecto, no es ningún problema. Eso sí, si los Pawns son prácticamente iguales yo haría un TestPawn padre, quedando los hijos super simples, con sólo algunas propiedades en el defaultproperties. (Un ejemplo de esto son mis PFMWeapon1 y PFMWeapon2, que extienden de PFMWeapon y únicamente tienen su sección de defaultproperties propia).

  9. Con lo de extender al testpawn padre se han solucionado los problemas. Te cuento lo que he hecho.

    Tal y como sugeriste, he creado una clase TESTNPCgenerico donde va todo el código referente a los pawns, incluyendo las funciones de ataque y demás.
    Así los ficheros para cada pawn se dejan sólo con la definición del mesh y las variables propias de cada uno. Gran invento lo de extender clases 🙂

    En el controller ya no hace falta conocer el pawn para llamar a funciones de éste. Las llamadas las hago refiriéndome a TESTNPCcontroller, por ejemplo:

    TESTNPCgenerico(Pawn).StopAttack();

    y lo hace en el Pawn que esté manejando el controller en ese momento.

    Ahora falta la cuestión de conocer las variables de comportamiento de cada Pawn. Visto que es complicadillo, voy a usar un sistema parecido al que me comentas. En el evento possess voy a determinar qué tipo de pawn es usando InStr sobre Pawn, y dependiendo del resultado inicializo las variables.

    De nuevo, gracias por todo.

    • La programación orientada a objeto tiene estas ventajas 😀
      Pero ¿por qué prefieres hacer operaciones de strings a usar los castings? La comprobación es mucho más sencilla y rápida con el casting:

      if(TestPawn1(Pawn) != None)

  10. La duda del día: quiero que el player, cuando reciba un golpe, haga una animación. La animación se reproduce desde una function en el código del player, TESTPawn.

    La función habría que llamarla desde el traceado de impactos, que está en TESTNPCgenerico.
    He probado “TESTPawn.recibegolpe” y no sabe qué es TESTPawn.

    He puesto entonces “Super(TESTPawn).recibegolpe” y me da el error:
    Error, ‘Super’: ‘TESTNPCgenerico’ does not extend ‘TESTPawn’.

    ¿Es posible llamar a funciones del player desde ahí, o tendría que meter el código de la animación en TESTNPCgenerico?

  11. Desde un Pawn no tienes acceso a otro, ya que en principio no tienen ninguna relación. El único momento en que el TestNPDGenerico tiene una referencia al Pawn es en TraceSwing, donde al impactar con un objeto se llama a HitActor.TakeDamage para que el HitActor reciba el daño.

    Ese HitActor no es de clase Pawn, sino Actor, que es la clase padre de todas las entidades del juego. Es decir, las funciones de Actor las tiene también Pawn. Así, podrías sobreescribir la función TakeDamage en tu Pawn e iniciar la animación en dicha función. Al sobreescribir una función se sustituye su comportamiento anterior por uno nuevo. En tu Pawn podrías escribir:


    event TakeDamage(int DamageAmount, Controller EventInstigator, vector HitLocation, vector Momentum, class <DamageType> DamageType, optional TraceHitInfo HitInfo, optional Actor DamageCauser)
    {
    // Se llama a la versión de la función no sobreescrita, para que haga lo que tiene que hacer
    Super.TakeDamage(DamageAmount, EventInstigator, HitLocation, Momentum, DamageType, HitIndo, DamageCauser);

    // Y ahora llamas a tu función que activa la animación
    funcionX();
    }

    De igual manera podrías hacerlo con PlayHit() tal como he hecho en el Razor para que haga un sonido cuando le golpean. Podrías sobreescribirla en tu Pawn y en ella activar la animación.

    Por cierto Super lo que hace es acceder al padre de una clase. Así se puede llamar a una función que esté tanto en el padre como en el hijo, escogiendo la que se quiera.

  12. ¿Entonces cómo puede saberse lo que está haciendo otro Pawn? Por ejemplo cuando uno inicia un golpe, otro podría iniciar una animación de cubrirse del golpe. Y a la hora del tracing comprobar que está cubierto o no para llamar al TakeDamage o detener la animación con un choque de espadas.

    • Siempre puedes obtener una referencia a otro Pawn durante el juego. Lo que no tienes es una referencia directa desde el principio como el caso del Controller con su Pawn. Los Razor por ejemplo obtienen una referencia del Pawn del jugador en el controller para conocer su posición y perseguirlo.

      Para lo que dices un Pawn podría indicarle a otro que le va a atacar, y el segundo decidiría si cubrirse, ejecutando las animaciones pertinentes. Pero no sería el Pawn 1 el que le dijera que se cubriese, esa lógica sería del Pawn 2. El Pawn 1 sólo le diría “te estoy atacando” y el Pawn 2 sería el que actuase en consecuencia. Por eso te recomendaba sobreescribir TakeDamage o PlayHit en lugar de crear una función extra en el Pawn que se llamase desde el TestNPC.

  13. Sí, he sobreescrito TakeDamage con buen resultado. Pero ¿cómo le diría el Pawn1 al 2 que le está atacando? Es decir, cuál sería el procedimiento, por encima.

    • Yo crearía en Pawn 2 una función para indicarle que alguien le está atacando y ahí respondería al ataque con animación o lo que fuera. Entonces desde el Pawn 1, que tiene una referencia al Pawn 2, llamaría a la función cuando Pawn 1 fuese a iniciar el ataque. Lo que es importante es que en el código del Pawn 1 no se sabe lo que hace el Pawn 2, simplemente se le indica que se le está atacando, y es el Pawn 2 el que debe reaccionar.

    • Vale, he hecho que 1 se cubra cuando 2 ataca, pero ahora me encuentro con problemas que no sé resolver.

      En controller2 saco un número aleatorio en una variable, si es 0 se cubre, si no, no.

      En el pawn2 antes de llamar al takedamage tendría que ver si la variable anterior es 0, pero no sé cómo acceder a ella.

      Al final todo es por no saber cómo acceder a los datos de otras clases, se vuelve el código muy ratonero.

    • TIenes varias opciones:

      -Puedes acceder al controller2 desde Pawn2 con:
      Controller2(Controller).loquesea

      -Puedes hacer que el controller decida, es decir: Pawn1 le indica a Pawn2 que le ataca, Pawn2 le indica a controller2 con una función que le están atacando, en esa función controller2 decide si se cubre o no, llamando o no a la función correspondiente del Pawn2 para que se cubra.

      Esa variable, ¿la sacas cada vez que el Pawn recibe un ataque? ¿Por qué ha de estar en el controller?

    • La variable es un número aleatorio que saco en “state AttackPlayer”, aunque es verdad que podría sacarla en la funcion de ataque en el pawn en lugar del controller.

      Primero se consulta la variable nada mas creada para llamar a la función del player para cubrirse.

      Luego se consulta de nuevo en el pawn, en traceswing, para determinar si tengo que llamar al takedamage o no. Lo que finalmente hice fue llamarlo con un damage de 10 si no estaba cubriéndose y con un damage de 1 si lo estaba. Luego en el takedamage si es 1 hago una animación de parar el golpe y si es > 1 pues ya la animación de daño.

      Ahora faltaría una forma de saber si el player estaba orientado al atacante.

      La protección aleatoria se la he puesto al pawn player, porque aún no sé como manejar el botón derecho del ratón…

    • El botón derecho por defecto viene configurado para hacer un disparo secundario pero si quieres usarlo para cubrir por ejemplo, puedes crear una función exec en tu controller como:

      exec function Cubrirse()
      {
      //....
      }

      y modificar el fichero DefaultInput.ini de la carpeta UDKGame/Config para remapear el botón derecho para que ejecute esa función. Buscas esta línea:

      .Bindings=(Name="RightMouseButton",Command="GBA_AltFire")

      y la cambias por

      .Bindings=(Name="RightMouseButton",Command="Cubrirse")

  14. Gracias. Ya he resuelto lo de cubrirse solo cuando está frente al enemigo. He usado esto:

    Orientacion = Normal(Pawn.location – TESTPawn(PlayerPawn).location) Dot vector( TESTPawn(PlayerPawn).rotation );

    siendo “Orientacion” una float. Da entre -1 y 1 según la orientación entre ambos pawns.

    Ahora voy a ver si implemento lo mismo en el pawn enemigo. Estoy pensando, aún sin verlo, que si hago que el player envíe la orden de cubrirse al enemigo, primero voy a tener que ver qué enemigos tengo enfrente. Habrá que hacer un bucle antes del golpe para eso.

    Aparte, creo que lo suyo sería implementar a los enemigos un inventario con las armas que lleven. En tu caso el arma es parte del Razor, pero en el mío deberían ser independientes.

  15. Estoy tratando de usar el sistema de los notify para la espada en el pawn player, pero me encuentro con el problema de que todo el código de la gestión del arma está en la clase WeaponBlade, y los notify afectan a la clase del Pawn.

    Tengo dudas sobre cómo se hacen estas cosas, ¿normalmente cada arma de un juego es una clase independiente que lleva su propio código del trace (como has hecho en el player) o el código del trace suele ir en el pawn?

    ¿Se podría hacer que tanto player como enemigos usaran la misma clase weaponblade, de modo que las armas fuesen independientes del pawn que las posea?

    • Una solución sería acceder al arma desde el Pawn. Si no recuerdo mal, el arma actual se guarda en la variable Weapon de Pawn. Para acceder a ella podrías hacer PFMWeaponBlade(Weapon) y ya podrías acceder a sus funciones y variables.

      La forma “correcta” de hacer esto sería que el Pawn se encargase de las animaciones de ataque y el arma de hacer el trace, pues es lo más lógico. Pero en programación a veces no compensa hacer todo correcto. En mi caso, como sabía de antemano que sólo iba a haber un arma cuerpo a cuerpo, preferí meter todo el código relacionado en el propio arma, incluyendo la gestión de las animaciones. Si hubiera varias armas cuerpo a cuerpo lo suyo sería crear una super-clase con el código común de trazado de colisiones.

      Y sí, podría hacerse lo que dices de que un mismo arma pudieran usarla diferentes Pawns, y sería lo suyo en un juego en el que tal caso pueda darse. En esta caso lo implementaría como te comento en el párrafo anterior, para que el arma sea independiente del Pawn y se limite a calcular el tracing, mientras los Pawns ejecutan las animaciones necesarias.

      La verdad es que yo he implementado tanto el caso en que el arma se encarga de todo como el caso en que el Pawn es el propio “arma”. Falta la versión intermedia que sería esa, en la que cada uno se encarga de lo que debe :P.

    • Habría que cambiar también TESTWeapon.uc, porque hace referencia a TESPawn. Creo que sería mejor empezar desde 0… 🙂

    • Por ejemplo, en TESTWeapon.uc tenemos dos líneas con referencias a TESTPawn:

      TESTPawn(Instigator).SetWeaponType(WeaponType);

      Mesh.SetLightEnvironment(TESTPawn(Instigator).LightEnvironment);

      Para que fuera usable por todos los pawns ¿cómo se podría reemplazar “TESTPawn(Instigator)”?

    • Podrías hacer un Pawn genérico que fuese padre de todos los Pawn que te interese y así hacer TESTFatherPawn(Instigator) para cualquier caso.

    • Ahí estoy, de momento he conseguido que todo funcione menos un par de cosas, siempre relacionadas con llamar a funciones de una clase a otra.
      Esto de la programación orientada a objetos es nuevo para mí, tiene sus ventajas como extender código, pero lo de comunicar clases me está suponiendo más de un dolor de cabeza.

  16. Después de tener pesadillas con castings e instigator, más o menos lo tengo. Ahí va un vídeo:

    Aparte de ser un poco cutre en AI y tener poca variedad de animaciones, lo que quiero conseguir ahora es que el arma la coloque en la espalda después de usarla. En el pawn enemigo lo conseguí antes de meterle el inventario. Simplemente cambiaba el arma de socket.
    Pero ahora el arma es de inventario y la de la espalda es un socket, y no sé aún como averiguar cual es el skeletalmesh de la última arma en uso para ponerla en el socket.
    Lo que termina pasando es que la primera vez que saca y guarda el arma funciona bien, pero en ese momento hay dos espadas, la del inventario y la del socket. La segunda vez que saca la espada, la del socket se queda en la espalda (la alternativa es tener las dos en la mano, superpuestas).

    Y en el player creo que va un poco diferente, con lo del timeequipping para hacer las animaciones, que no he usado aún.

    Después quiero ver lo de los pickups, para dejar armas en el suelo y que se puedan recoger.

    • Mmm se me ocurre una solución. Podrías crear un componente para la espada en el Pawn y hacer un attach o detach cuando convenga.

      Sería algo así:
      Defines una variable para guardar el componente

      var SkeletalMeshComponent SwordMesh;

      y en defaultproperties hacer como para definir el mesh del personaje:

      // Mesh de la espada
      Begin Object Class=SkeletalMeshComponent Name=SkeletalMeshComponentSwordMesh
      SkeletalMesh=SkeletalMesh'Paquete.Grupo.SM_Sword'
      LightEnvironment=MyLightEnvironment
      bEnableSoftBodySimulation=True
      bSoftBodyAwakeOnStartup=True
      bAcceptsLights=True
      CastShadow=true
      bCastDynamicShadow=true
      End Object
      SwordMesh=SkeletalMeshComponentSwordMesh

      Así el componente está asociado al Pawn y puedes engancharlo al socket cuando quieras o desengancharlo con:

      Mesh.AttachComponentToSocket(SwordMesh, SocketName);

      Mesh.DetachComponent(SwordMesh);

      Es una manera, pero quizá haya otra mejor.

  17. Así es justamente como lo tenía. Y en el pawn enemigo funcionaba perfecto, en el inicio se la pegaba al socket de la espalda. Al cambiar el modo WeaponType se lo cambiaba a la mano. Como es el mismo skeletal pues simplemente desaparecía de la espalda y se pasaba a la mano. Para guardarla lo mismo.

    Pero al usar el inventario, tengo dos skeletal para la espada, el que incluyo en el pawn, tal y como dices, y el que incluyo en el inventory. Si te fijas en el vídeo, en los enemigos funciona, al perseguirte la pasan de la espalda a la mano. Y cuando se paran, que no se ve en el vídeo, la pasan a la espalda.
    Pero hay un problema cuando la vuelven a sacar, que la de la espalda se queda ahí. Pienso que es porque hay justamente dos skeletal en juego.

    Y de todas formas con ese método sólo podría hacerlo con la espada, pero como en el inventario lleva dos espadas y tres escopetas…
    La cosa está en saber como se llama el skeletal del inventory que se esté usando, si es que es posible acceder a él y manejarlo para ponerlo en un socket.

    • En el Pawn se guarda el arma usada actualmente en Weapon. Puedes acceder a su Mesh con Weapon.Mesh y transformarlo en un SkeletalMeshComponent con SkeletalMeshComponent(Weapon.Mesh). Lo que no he probado es si puedes usar el Componente de un actor en otro, quizá si.

  18. Ya he conseguido hacerlo andar, el fallo estaba en que para quitar la espada de la espalda la pasaba al socket de la mano. Lo que he hecho es un “DetachComponent” para simplemente vaciar el socket de la espalda. El de la mano lo hace automáticamente con el inventario.

    Lo que hago es incluir el skeletalmesh del arma en defaultproperties. Tiene que ser la misma que la que use en el inventario. ¿hay alguna forma de obtener el skeletalmesh de la primera arma del inventario?
    Porque la espada en defaultproperties es un SkeletalMeshComponent, y la del inventario es una “class”.

    Con el tema del player aún no he visto cómo hacerlo, porque con la rueda del ratón las armas aparecen instanáneamente, aunque el timeequipping sea de 1 segundo o más. Pulsando las teclas 1, 2, 3, etc, sí que hace pausa. Luego está la tecla “Q”, que parece que es para lo mismo.

    • En el Pawn puedes recorrer las armas del inventario buscando las que sean de la clase que quieres:

      local Weapon W;

      ForEach InvManager.InventoryActors( class’Weapon’, W )

      Y una vez que tengas el arma probar lo que te puse por arriba pero con W

      >>> Puedes acceder a su Mesh con Weapon.Mesh y transformarlo en un SkeletalMeshComponent con SkeletalMeshComponent(Weapon.Mesh). Lo que no he probado es si puedes usar el Componente de un actor en otro, quizá si.

  19. Funciona, con eso puedo conocer el arma del inventario y colocarsela en el inicio. Pero algo ocurre a diferencia con el método anterior, que saca el arma y cuando la vuelve a guardar, desaparece. He probado cambiando el orden de los comandos y lo más que he conseguido es que no salga el arma nunca.
    Además al quitar la definición del arma en defaultproperties desaparece el lightenviroment y el arma no se ilumina bien.
    Al final he terminado por usar el sistema anterior y asignar en defaultproperties el mismo skeletalmesh de la primera arma del inventario.

  20. Bueno, después de unos días he hecho algunos progresos. He puesto un nodo para animaciones de cintura para arriba, así se puede golpear con la espada mientras se está en movimiento. La jugabilidad así es mucho más dinámica, ya no te puedes escapar tan fácilmente de los enemigos. Y mola eso de ver llegar al enemigo con la espada en alto para atizarte.

    También llevan ragdoll al morir, y la IA tiene tres estados iniciales, movimiento aleatorio, en espera o ejecutando una animación. Según cada tipo de enemigo, te persiguen si te ven, o sólo si les atacas. O huyen. Aún me queda ponerles navmesh en el movimiento aleatorio y en el de huída, ahora mismo se van tropezando con todo.
    He puesto un vídeo en youtube enseñando todo eso.

    Otra vez gracias por tus tutoriales y tu paciencia.

  21. marcos primero quiero agradecerle por los tutoriales son muy buenos me han ayudado mucho y es gratificante saber que hay personas que comparten sus conocimientos.

    por otro lado marcos me gustaría saber como puedo hacer para que mi enemigo tenga un arma y me dispare 😀

    • De nada Camilo!
      Los enemigos al fin y al cabo son Pawns como el propio jugador. Esto significa que el Pawn de los enemigos puede incorporar un arma de la misma manera que lo hace el Pawn del jugador. Una vez que conseguimos que el Pawn tenga un arma visible, faltará conseguir que dispare. Esto habrá que hacerlo en el Controller del enemigo, detectando cúando queremos disparar y activando los disparos del arma de manera similar a como se hace en el controller del jugador.
      marcosdiez.wordpress.com/2013/01/17/pfm-disparando-en-udk/
      No es una explicación superdetallada pero espero que te sirva 😉

  22. Hola Marcos, ¿que evento tendria que usar para que mi enemigo (que es ciego) me persigua al oirme?, más adelante tendre que hacer que, una vez este enfrente mio me ataque como tus Razors, a ver si lo consigo XD

    Un saludo.

    • Hola Dan, hay un evento del Controller llamado HearNoise, que sirve justo para detectar sonidos emitidos por otros actores. Usando eso quizá puedas implementar ese comportamiento, ánimo xD

  23. Aaah si ese comando lo conocia, la cosa es que ya le puse pero el robot ni caso, le disparo alrededor, salto y sigue clavado en su sito. He conseguido que no me vea, pero es que no hace nada XD

    Tmb hay k tener en cuenta que no tengo ni idea de programacion, esto es lo k pasa cuando te mandan hacer un juego sin haber dado programación :S

    Lo que he hecho ha sido poner en el Controller de mi enemigo:

    auto state Escuchando
    {
    // Se ha oido al jugador
    event Hearnoise(Pawn HearPlayer)
    {
    playerPawn = HearPlayer;
    GoToState(‘Persiguiendo’);
    }
    }

    Es muy probable que este mal, pero ya te digo ni idea. Luego me gustaria hacer que cuando mi personaje este agachado sea dificil detectarlo, quiza con un volumen que, tanto player como enemy tengan y cuando colisionen me persiga, tal que asi: http://www.youtube.com/watch?v=Pk8jw4-dGWk. La cosa seria que cuando me agache el radio de la esfera decredca, pero eso ya es el siguiente nivel XD

    Muchas gracias por tu ayuda, si puedes seguir aconsejandome estaria muy agradecido.

    Un saludo,
    Dan.

    • Pues.. UDK no es el motor más sencillo para hacer un juego sin saber programación xD
      Creo que para activar el evento HearNoise, el actor que produce el sonido debe invocar MakeNoise cuando lo emite y así el que está escuchando se entere.
      De todas maneras si no sabes programar quizá sería mejor que aprendieras a usar la herramienta Kismet (de UDK), que en teoría permite hacer juegos completos sin programar nada. O implementar primero las cosas que veas en tutoriales y poco a poco hacer cosas nuevas…
      Suerte!

  24. Llevo toda la vida haciendo escenarios con UDK, es mi motor favorito (ademas, no tenemos elección) jajaja, como ya te digo, mi mayor problema es la programación, nunca he dado nada de programacion, ni java, ni C++ etc… Kismet se me da bastante bien, pero tiene limites, almenos para los amateurs como yo.

    Probaré buscando por “Makenoise” a ver que me encuentro XD

    Muchas gracias por los consejos, sigue haciendo tutos pá los que no tenemos ni fuck’n idea como yo XD

    Un Saludo,
    Dan.

  25. Hola Marcos estoy intentando crear un enemigo pero al reconstruir los scripts me saltan dos errores:

    Error, Bad or missing expresion in ‘=’ En la linea 80 del (PFMEnemyPawnRazor).

    ni idea de por que…

    un saludo y gracias.

    • ok creo que di con la solución un problema de escritura xD

      gracias y enhorabuena por el blog¡¡¡

    • bueno ya esta todo en marcha, pero ahora tengo un problema con el ragdoll al morir mi enemigo atraviesa el suelo… y como se puede acelerar la rotación?? cuando corren no les da tiempo a dar la vuelta y se chocan contra los muros

    • Creo recordar que la velocidad de rotación se modifica con la variable RotationRate, que puedes sobreescribir en la sección de defaultproperties de la siguiente manera:
      RotationRate=(Pitch=40000,Yaw=40000,Roll=40000)

  26. hoola marcoss decirte que conseguí solucionar todo peo ahora surgen otros problemas…

    -uno, nose como hacer para que los enemigos no atraviesen el suelo al morir…se activa el ragdoll y en principio todo bien menos eso… atraviesan el suelo al morir¡¡

    -y el otro es que me gustaría girasen mas rápido para no chocarse contra los muros…vamos darles mas margen de maniobra…

    te paso un vídeo donde se ve mejor:

  27. Marcos disculpa me dodrias ayudar en una cosilla me puedes indicar como aria para rotar a un enemigo es decir le puedo dar un setLocation si problema es el SetRotation practicamente hace que se teletransporte a esa posicion es decir asoma girado a donde le digo pero como ago para que gire gradualmente ? gracias desde ya marcos XD

    • Tanto SetLocation como SetRotation son funciones que modifican la posición y rotación de manera instantánea. Nunca he probado a hacer que un personaje rote gradualmente pero creo que hay una función llamada SetDesiredRotation que podría ayudar. Suerte

    • Gracias Marcos no funciono pero me diste una idea de como hacerlo la verdad en el fondo esas funciones usan setRotation asi que lo use creando un tick en un timer para girar y funciona muy bien gracias por la ayuda XD

  28. Hola marcos, me ha surgido un problema y es que el personaje me persigue pero no me pega queda en estado Mesh cuando me encuentra duro y sin hacer nada.

  29. Los enemigos que tengo van muy bien salvo por un inconveniente , y es que cuando llegan al lado de mi pawn se quedan quietos y no atacan , si me muevo me persiguen pero cuando paran ellos se detienen a mi lado y solo ejecutan la animacion de idle y siguen sin atacar ni hacer daño

  30. alguien me podria arrojar algo de luz sobre este tema? mi hermano ya ha preguntado por el problema antes y se a artado de buscar por google y nada , todo es relacionado con ut pawn y dice que no le sirve el codigo.
    Si alguien sabe como solucionarlo porfabor agamelo saver. Gracias

  31. Hola ya configuré todo pero cuando el enemigo llega a mi Pawn no ataca se queda mirando ( como el PNJ sencillo ) aunque esta utilizando el mismo controller que el de el ejemplo del razor.

    Los scripts compilan sin error, los sockets, animaciones y todo lo demás está configurado tal cual está en el ejemplo del razor, pero por alguna razón cuando mi enemigo se pone al lado de mi pawn este no ataca, de hecho a veces se queda la animación de correr aunque ya esta pegado al pawn.

    • Hola jose. Por lo que me comentas podría ser un problema de que los enemigos no sepan que han llegado al jugador. En la línea:
      MoveToward(PlayerPawn, PlayerPawn, 32);
      El 32 indica el radio del área alrededor del punto al que van. Es decir, si el enemigo está dentro de ese área habrá llegado a su destino (el jugador) y por tanto atacará.
      Lo que creo que puede estar pasando es que tu jugador tenga un cilindro de colisión mayor de 32 y por ello los enemigos nunca lleguen a estar en ese área porque chocan con el jugador.
      Puedes probar a aumentar el área a ver si es eso.
      Suerte!

  32. Hola Marcos gracias a tus tutoriales e avanzado muchísimo, pero tengo este problema desde el año pasado y estamos 2015 xd.

    y es que mi enemigo me persigue mas no ejecuta las animaciones espero que puedas ayudarme te estaría muy agradecido:

    class PFMEnemyPawnRazor extends PFMEnemyPawn;

    var array AttackAnimations; // Nombres de las animaciones a usar
    var AnimNodeSlot RazorAnimNode; // Nodo de animaciones

    // Sockets
    var Name backBaseSocketName;
    var Name backTipSocketName;
    var Name rightBaseSocketName;
    var Name rightTipSocketName;
    var Name leftBaseSocketName;
    var Name leftTipSocketName;

    // Sockets usados en este momento
    var Name baseSocketName;
    var Name tipSocketName;

    var array PreviousPoints; // Puntos de la pata en el frame anterior
    var array CurrentPoints; // Puntos de la pata en el frame actual
    var int numPoints; // Número de puntos totales en que se divide la pata

    var bool bTracing; // Determina si hay que hacer el cálculo del traceado o no
    var bool bHitPlayer; // Determina si se ha impactado con el jugador

    var SoundCue HurtSound; // Sonido de daño

    simulated event PostInitAnimTree(SkeletalMeshComponent SkelComp)
    {
    Super.PostInitAnimTree(SkelComp);

    // Se busca la referencia del nodo de animaciones
    RazorAnimNode = AnimNodeSlot(SkelComp.FindAnimNode(‘RazorAnimNode’));
    }

    simulated function PostBeginPlay()
    {
    Super.PostBeginPlay();

    ResetTraceSwing(0);
    }

    function StartAttack()
    {
    local int anim;

    anim = Rand(AttackAnimations.Length);

    //Se resetea el sistema de traceado
    ResetTraceSwing(anim);

    //Se activa la animación correspondiente
    RazorAnimNode.PlayCustomAnim(AttackAnimations[anim], 1.5,0.05,0.1,false,true);
    }

    function StopAttack()
    {
    RazorAnimNode.StopCustomAnim(0.2);
    NotifyStopTrace();
    }

    simulated event Tick(float DeltaTime)
    {
    super.Tick(DeltaTime);

    if(bTracing)
    {
    TraceSwing();
    }
    }

    //Resetea el sistema de traceado
    function ResetTraceSwing(int anim)
    {
    local Vector BaseSocketLocation, TipSocketLocation;
    local int i;

    // Posición de los sockets
    if(anim == 0)
    {
    baseSocketName = backBaseSocketName;
    tipSocketName = backTipSocketName;
    }
    else if(anim == 1)
    {
    baseSocketName = rightBaseSocketName;
    tipSocketName = rightTipSocketName;
    }
    else
    {
    baseSocketName = leftBaseSocketName;
    tipSocketName = leftTipSocketName;
    }

    BaseSocketLocation = GetSocketLocation(baseSocketName, Mesh);
    TipSocketLocation = GetSocketLocation(tipSocketName, Mesh);

    // Reseteo de los arrays
    PreviousPoints.Remove(0,PreviousPoints.length);
    CurrentPoints.Remove(0,PreviousPoints.length);

    // Valores iniciales
    for(i = 0; i < numPoints; i++)
    {
    PreviousPoints.AddItem(VLerp(BaseSocketLocation,TipSocketLocation,i/float(numPoints-1)));
    CurrentPoints.AddItem(VLerp(BaseSocketLocation,TipSocketLocation,i/float(numPoints-1)));
    }

    bHitPlayer = false; // Se resetea
    }

    // Traceado de impactos
    function TraceSwing()
    {
    local Vector BaseSocketLocation, TipSocketLocation;
    local int i;
    local Actor HitActor;
    local Vector HitLocation, HitNormal, Momentum;

    // Posición de los sockets
    BaseSocketLocation = GetSocketLocation(baseSocketName, Mesh);
    TipSocketLocation = GetSocketLocation(tipSocketName, Mesh);

    // Actualización de posiciones
    for(i = 0; i < numPoints; i++)
    {
    CurrentPoints[i] = VLerp(BaseSocketLocation,TipSocketLocation,i/float(numPoints-1));
    }

    // Cálculo de impactos
    for(i = 0; i < numPoints; i++)
    {
    // Se recorren los actores interceptados por el traceado
    foreach TraceActors(class'Actor', HitActor, HitLocation, HitNormal, PreviousPoints[i], CurrentPoints[i])
    {
    // Si se impacta con el jugador (y es la primera vez en este ataque)
    if(PFMPawn(HitActor) != None && !bHitPlayer)
    {
    bHitPlayer = true; // Impactado jugador
    Momentum = Normal(HitLocation – Location) * 60000;
    HitActor.TakeDamage(10, Controller, HitLocation, Momentum, class'DamageType');
    //SpawnHitEffects(HitLocation,HitNormal);
    }
    }
    }

    // Debug lines
    /*for(i = 0; i < numPoints; i++)
    {
    DrawDebugLine(PreviousPoints[i],CurrentPoints[i],0,255,0,true);
    }*/

    // Actualización de puntos anteriories
    for(i = 0; i < numPoints; i++)
    {
    PreviousPoints[i] = CurrentPoints[i];
    }
    }

    function PlayHit(float Damage, Controller InstigatedBy, vector HitLocation, class damageType, vector Momentum, TraceHitInfo HitInfo)
    {
    if (HurtSound != None)
    {
    PlaySound(HurtSound, true);
    }

    Super.PlayHit(Damage,InstigatedBy,HitLocation,damageType, Momentum, HitInfo);
    }

    function vector GetSocketLocation(Name SocketName, SkeletalMeshComponent SMC)
    {
    local SkeletalMeshSocket SMS;

    if (SMC != none)
    {
    SMS = SMC.GetSocketByName(SocketName);
    if (SMS != none)
    {
    return SMC.GetBoneLocation(SMS.BoneName);
    }
    }
    }

    function NotifyStartTrace()
    {
    bTracing = true;
    }

    function NotifyStopTrace()
    {
    bTracing = false;
    }

    defaultproperties
    {
    // SkeletalMesh
    Begin Object Name=EnemyMesh
    SkeletalMesh=SkeletalMesh’Orvinik_FBX.SkeletalMesh.Adapa_prueba’
    AnimSets(0)=AnimSet’Orvinik_FBX.animsset.Anim_Adapa’
    AnimTreeTemplate=AnimTree’Orvinik_FBX.animsset.Adan_prueba’
    LightEnvironment=MyLightEnvironment
    // PhysicsAsset=PhysicsAsset”
    //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=30.0
    CollisionHeight=44.0
    BlockNonZeroExtent=true
    BlockZeroExtent=true
    BlockActors=true
    CollideActors=true
    End Object
    CollisionComponent=CollisionCylinder
    Components.Add(CollisionCylinder)

    // Se especifica el controlador
    ControllerClass=class’PFM.PFMEnemyControllerRazor’

    AttackAnimations=(“Adapa_blade01″,”Adapa_blade02″,”Adapa_blade01″)

    backBaseSocketName=”BackBase”
    backTipSocketName=”BackTip”
    rightBaseSocketName=”RightBase”
    rightTipSocketName=”RightTip”
    leftBaseSocketName=”LeftBase”
    leftTipSocketName=”LeftTip”

    numPoints=5

    Health=200
    GroundSpeed=600.0 //Velocidad máxima en suelo
    MaxStepHeight=15.0
    DrawScale=1.3

    HurtSound=SoundCue’Enemies.Razor.S_Razor_Hurt’

    }

    ___________________________________________

    class PFMEnemyControllerRazor extends PFMEnemyController;

    var PFMPawn PlayerPawn; // Referencia al Pawn del jugador
    var PFMPawn PlayerPawnAux; // Referencia al Pawn del jugador

    // Estado inicial. Búsqueda de jugador
    auto state SearchPlayer
    {
    Begin:
    foreach DynamicActors(class’PFMPawn’, PlayerPawnAux)
    {
    PlayerPawn = PlayerPawnAux;
    // Se queda con el que pille (En principio sólo habrá uno)
    }

    if(PlayerPawn != none)
    {
    `log(“>>>> Encontrado PlayerPawn”);
    GoToState(‘HuntPlayer’);
    }

    `log(“>>>> No encontrado Player”);
    Sleep(1.0);
    GoTo ‘Begin’;
    }

    state HuntPlayer
    {
    // Calcula el camino al objetivo
    function bool FindNavMeshPath()
    {
    NavigationHandle.PathConstraintList = none;
    NavigationHandle.PathGoalList = none;
    // Create constraints
    class’NavMeshPath_Toward’.static.TowardGoal( NavigationHandle, PlayerPawn );
    class’NavMeshGoal_At’.static.AtActor( NavigationHandle, PlayerPawn, 32);
    // Find path
    return NavigationHandle.FindPath();
    }

    Begin:

    if(PlayerPawn == None)
    {
    GoToState(‘SearchPlayer’);
    }

    if ( NavigationHandle == None )
    {
    `log(“>>>>>>MEC!!! – NavigationHandle Erroneo”);
    InitNavigationHandle();
    }
    else
    {
    InitNavigationHandle();
    }

    if( NavigationHandle.ActorReachable(PlayerPawn))
    {
    `log(“>>>> Puedo llegar directamente al Pawn”);
    MoveToward(PlayerPawn, PlayerPawn, 32);

    if(Pawn.ReachedDestination(PlayerPawn))
    {
    GoToState(‘AttackPlayer’);
    }
    }
    else if (FindNavMeshPath())
    {
    //NavigationHandle.DrawPathCache(,true);
    NavigationHandle.SetFinalDestination(PlayerPawn.Location);

    if(NavigationHandle.GetNextMoveLocation(NextDest, Pawn.GetCollisionRadius()*5))
    {
    `log(“>>>> Me muevo al siguiente punto”);
    MoveTo(NextDest,none,128.0);
    }
    else
    {
    `log(“>>>> No hay siguiente punto en el camino”);
    MoveToward(PlayerPawn);
    }
    }
    else
    {
    `log(“>>>> No se encuentra camino posible”);
    MoveToward(PlayerPawn);
    }

    goto ‘Begin’;
    }

    state AttackPlayer
    {

    Begin:

    if(PlayerPawn == None)
    {
    GoToState(‘SearchPlayer’);
    }

    Sleep(0.1);
    if(!Pawn.ReachedDestination(PlayerPawn))
    {
    GoToState(‘HuntPlayer’);
    }

    PFMEnemyPawnRazor(Pawn).StartAttack();

    Sleep(0.4);
    if(!Pawn.ReachedDestination(PlayerPawn))
    {
    PFMEnemyPawnRazor(Pawn).StopAttack();
    GoToState(‘HuntPlayer’);
    }

    Sleep(1.5);
    if(!Pawn.ReachedDestination(PlayerPawn))
    {
    GoToState(‘HuntPlayer’);
    }
    goto ‘Begin’;
    }

    defaultproperties
    {
    }

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