UnrealScript: Working with Tick Function

Level: Intermediate

Source Code:

Looper.uc (1 KB)

VariableMover.uc (5 KB)

Tick is a function called via engine notification per frame. Specifically for actors to have this function [event] readily available, bStatic must be false (Advanced -> bStatic). The parameter DeltaTime is the time passed since the last tick in seconds. You can use Disable('Tick') and Enable('Tick') to toggle this in the cases where you need it as a recurring on-and-off event. This function is called rather quickly --relatively speaking, which you can see by setting up the function and using a log or message to display every call. Tick generally looks as follows:

function Tick( float DeltaTime )

{ action }

This is useful behind the scenes, but to illustrate it more explicitly, we'll be looking at its visual potential. Consider the following script:

class NewTrigger expands Triggers;

var Pawn P;

function Touch( actor Other )

{

P = Pawn(Other);

}

function Tick( float DeltaTime )

{

if (P.Fatness < 255)

P.Fatness += 1;

}

Have a pawn touch the trigger, and you can see the pawn expanding consistently.

Now you can see that Tick is a function that is very useful for consistent movements. Suppose you want something to circle around a point with radius r. Let's make that happen. First, consider that we might not be at the center, and an offset might be needed. PostBeginPlay is called after the game is set up, so we can make checks before the player actually acts. We'll also declare our variables:

var() bool bUseX;
var() bool bUseY;
var() bool bUseZ;
var() float Radius;

var bool bInitialized;
var vector InitialLocation;
var float RotAngle;

function PostBeginPlay()
{
InitialLocation = Location;

    if (bUseX)
    InitialLocation.X = 0;

    if (bUseY)
    InitialLocation.Y = 0;

    if (bUseZ)
    InitialLocation.Z = 0;
}

 

As you can infer, InitialLocation will use the actor's starting location unless specified. We'll only be using X and Z in this example, but of course you can further extend it to use whatever vector component you want. Let's set up a basic trigger function. Upon being called, it will simply change a boolean that will make sure Tick doesn't do anything until we want it to.

function Trigger( actor Other, pawn EventInstigator )
{
bInitialized = True;
}

The issue is that we're using a vector, but we want a circle, and we can't simply plot every point on a circle by changing the vector in rectangular coordinates. Therefore, we convert rectangular to polar coordinates.

function RectangularToPolar(float r, float A, out float X, out float Y)
{
    X = r*cos(A);
    Y = r*sin(A);
}

Note: To see a greater application of this, check out the Teleport Tutorial for Creation Kit


The radius is already defined as per the user's modifications to the actor. Therefore the only changing inbound argument is angle A. If you call, 360 degrees will make a full circle, so a continuous increase in the float RotAngle won't change anything outside of the angle --although in a sense it might be safe to reset the float depending on the length of the run. Let's set up Tick:

function Tick( float DeltaTime )   
{
local vector VFinal;

    VFinal = InitialLocation;

    if (bInitialized)
    {
    RotAngle += 0.1;
    
    RectangularToPolar(Radius, RotAngle, VFinal.X, VFinal.Z);
    SetLocation( VFinal );
    }
}

If you want Y to change instead, set the third argument to VFinal.Y instead of VFinal.X. RotAngle's increase rate determines the rate of revolution. If you set this up in Unreal Editor, first set up the variables, then add a trigger to activate it. It will rotate around the specified point.

This script on its own may not be too applicable to level design or gameplay, but we can change that by instead acquiring the rotation of a certain object instead. Suppose you add a new actor variable. Now instead of increasing RotAngle, we set it to the specified rotation of that actor as so: RotAngle = Actor.Rotation.Axis; Where axis is the axis of rotation you specify. This would work as long as we convert Unreal Units to Radians in order for our Rectangular to Polar conversion to remain accurate.

function float RadiansToUU(float Rad)
{
    return (32768.f/pi)*Rad;
}

function float UUToRadians(float UU)
{
    return (pi/32768.f)*UU;
}


So the final value of RotAngle would be UUToRadians(Actor.Rotation.Axis)

But that only involves a single consistent change in motion. We can now see that Tick is very useful in that it is essentially continuous time (delta time). This brings to mind the idea of motion as a function of time. We're going to make a mover that accelerates or decreases in acceleration over time --this is different from MoveByTime and GlideByTime in that the former is linear and the latter uses other variables to control motion in a nonspecific way. First, our variables:

var() name AttachTag;
var() rotator RotateRate[8];    //set nonzero value to specify which axes are rotating
var() bool BypassLimit;    //disregard limit not reached (recommended)

//Time Vars
var(Time) bool bInitialized;
var(Time) byte CurrentPhase;

var(Math) int Exponent[8];
var(Math) float Initial[8];
var(Math) float Offset[8];
var(Math) float Multiplier[8];
var(Math) float Limit[8];
var(Math) int IsDecreasing[8];

//internal
var int TimeCap;
var byte PreviousPhase;
var byte Queue[16];
var float IntRate; //internal rate

Let's ignore the usage of queue for now. Our mover will have two different initial states, one for simple acceleration, and one for varying acceleration. Let's first write the simpler one. First we're going to set up our trigger function. Suppose the user wants a delay in the starting process, similar to DelayTime in the standard mover. We'll use SetTimer( Delay, bRepeating ) to start the timer, whereupon Timer() will be called, which will simply change a boolean, bInitialized. If the user doesn't want a delay, bInitialized will immediately become true.

State() FunctionByTime
{

    //initialize
    function Trigger(actor Other, pawn EventInstigator)
    {
        if (Initial[0] > 0)
        {
        SetTimer(Initial[0], false);
        }
        
        else
        {
        bInitialized = True;
        }
    }

    function Timer()
    {
    bInitialized = True;
    Enable('Tick');
    }

We want acceleration over time. You might recall the physics concept of acceleration. If acceleration is constant, in the kinematic equation of motion Vf = Vo + at, velocity does not change as acceleration is 0. This means for an object to actually increase in velocity, acceleration must vary over time. It's simpler than it might seem: we simply set up a variable which will be the value of an equation. In a sense you can make your own equation as long as it is a function of time, but we want a simple and effective way to increase or decrease acceleration. We're going to use f(t) = M(t - o)a, where multiplier M is multiplied by quantity time t minus an offset o to the an exponent a. Now we simply place this into a variable as such:

        Rate = (Multiplier[0])*((T - Offset[0])**Exponent[0]);

We're using member 0 of the arrays involved because in this case, we're only working with one phase. This object will eventually reach a limit, whereupon it no longer increases in acceleration. Therefore as TimeCap is considered time, it will be less than the limit until it reaches it, as you can see below. In this case I've also separated the axes by if conditions. GetRotationByTime is our function containing the equation Rate.

    function Tick( float DeltaTime )
    {

        if (bInitialized)
        {    
            if (TimeCap < Limit[0])    //to do: compact all into one with an out rotator
            {
            TimeCap += 1;
            }
    
            if (RotateRate[0].Roll > 0)            //roll
            {
            RotationRate.Roll = GetRotationByTime(TimeCap);
            SetRotation( Rotation + RotationRate );
            }

            if (RotateRate[0].Pitch > 0)            //pitch
            {
            RotationRate.Pitch = GetRotationByTime(TimeCap);
            SetRotation( Rotation + RotationRate );
            }

            if (RotateRate[0].Yaw > 0)            //yaw
            {
            RotationRate.Yaw = GetRotationByTime(TimeCap);
            SetRotation( Rotation + RotationRate );
            }
        }
    }

    function int GetRotationByTime(float T)
    {
    local float Rate;

        Rate = (Multiplier[0])*((T - Offset[0])**Exponent[0]);
        //    BroadcastMessage("tick"@Rate@"time"@TimeCap, false, 'event');
        return int(Rate);
    }

But this only uses one phase and eventually stops increasing in acceleration and velocity becomes constant--that is, acceleration becomes 0. Suppose we want vary acceleration to a virtually infinite extent --in this case, not so infinite wink.

Let's set up our FunctionByPhase state by setting up the same function with the equation as we did previously. The catch is that this time we're both compacting the rotator check into one single function, and we're considering that the object can decrease in acceleration, which will require a different function, because we want the user to modify the equation for that, too:

    function rotator AddRotationByPhase(float T, byte Phase) //increasing
    {
    local float Rate;
    local byte bPitch, bRoll, bYaw;
    local rotator FinalRot;


        Rate = (Multiplier[Phase])*((T - Offset[Phase])**Exponent[Phase]);

        IntRate = Rate;

        CheckAxes(bPitch, bRoll, bYaw);

            if (bPitch==1)        
            FinalRot.Pitch = int(Rate);

            if (bRoll==1)
            FinalRot.Roll = int(Rate);

            if (bYaw==1)
            FinalRot.Yaw = int(Rate);

            BroadcastMessage("tick"@finalrot@"rate"@rate@"timecap:"@timecap, false, 'event');
            return FinalRot;
    }

    function rotator SubtractRotationByPhase(float T, byte Phase) //Initial-M(x-O)^2
    {
    local float Rate;
    local rotator FinalRot;
    local byte bPitch, bRoll, bYaw;

    Rate = ( intRate - ((Multiplier[Phase])*((T - Offset[Phase])**Exponent[Phase]))    );
    BroadcastMessage("tick"@finalrot@"rate"@rate@"timecap:"@timecap, false, );
        CheckAxes(bPitch, bRoll, bYaw);

            if (bPitch==1)        
            FinalRot.Pitch = int(Rate);

            if (bRoll==1)
            FinalRot.Roll = int(Rate);

            if (bYaw==1)
            FinalRot.Yaw = int(Rate);

        return FinalRot;
    }

IntRate is the saved rate of the previous phase, going into the next phase. In this case phases can only go forward unless the user modifies the limit of the phases specifically, but regardless, they will all be consecutive. This means that it's best to simply increase phases, instead of switching them arbitrarily. You will notice that CheckAxes is called. This basically checks all axes in one single function as outbound parameters.

    function CheckAxes(out byte bChangePitch, out byte bChangeRoll, out byte bChangeYaw)
    {    //separate function for when change by axis is implemented

        if (RotateRate[CurrentPhase].Pitch != 0)
        bChangePitch = 1;

        if (RotateRate[CurrentPhase].Roll != 0)
        bChangeRoll = 1;

        if (RotateRate[CurrentPhase].Yaw != 0)
        bChangeYaw = 1;
    }

This might strike some people as strange, but it is more useful if you want to extend this script to vary in acceleration by axis as well. Now let's write the Tick function:

State() FunctionByPhase //do not use if you have no idea what you're doing
{
    function Trigger( actor Other, pawn EventInstigator )
    {
    bInitialized = True;
    }

    function Tick( float DeltaTime )
    {
    local float f;

        if (bInitialized)
        {    
            if (TimeCap < Limit[CurrentPhase])
            {
            TimeCap += 1;
            }

            if (TimeCap >= Limit[CurrentPhase])
            {
            CheckQueue();
            }
    
            if (!IncreasePhase(CurrentPhase))    //--
            {
            RotationRate = SubtractRotationByPhase(TimeCap, CurrentPhase);
            SetRotation( Rotation + RotationRate );
            }

            else    //++
            {
            RotationRate = AddRotationByPhase(TimeCap, CurrentPhase);
            SetRotation( Rotation + RotationRate );
            }
        }
    }
}

    function bool IncreasePhase(byte Current)
    {
        if (IsDecreasing[Current] != 0)
        {
        return False;
        }

        else return True;
    }

The last boolean function checks whether to decrease or increase acceleration. We have everything set up except a way to change phases. You can do this by writing a script for a new actor under Triggers. Simply make a variable (var() VariableMover NewVar) and set it to the desired mover or use a localized variable (local VariableMover Object) in a function and use a ForEach loop to call all such Object to your specifications, in both cases, have the script call the function SetPhase(byte Phase):

    function SetPhase(byte Phase)
    {

        if ((TimeCap < Limit[CurrentPhase])&&(!BypassLimit))
        AddToQueue(Phase);

        if (!bInitialized)
        return;

        else if (CurrentPhase != Phase)
        {
        PhaseChange(CurrentPhase, Phase);
        }
    }

    function PhaseChange(byte oldPhase, byte NewPhase )
    {
        PreviousPhase = oldPhase;
        CurrentPhase = NewPhase;

    }

Lastly, the queue system, which is techncially not necessary, but in the case that you want to wait for an object to reach a limit before changing phases if the trigger has called it before it has reached the limit, here is a simple way to do so:

 

    function AddToQueue(byte NextPhase)
    {
    local int i;

        for (i=0; i<8; i++)
        {
            if (Queue[i] == 0)
            break;
        }

    Queue[i] = NextPhase;
    }

    function CheckQueue()
    {
    local int i;

        for (i=0; i<8; i++)
        {
            if (Queue[i] != 0)
            {
            PhaseChange(CurrentPhase, Queue[i]);
            break;
            }
        }
    }

In the cases of calling functions from other scripts (external function calls), you must have the function defined outside of a state before it can be called inside one. Since we don't need it called inside a state, write the phase changing functions outside of the initial state.