Robert Johnsen - Handling Cutscene Delays

From NWN Lexicon
Jump to: navigation, search

Robert Johnsen - Handling Cutscene Delays

Robery Johnsen (Skyrmir) - Handling Cutscene Delays

When a script is executed, the script processing routine reads the commands in the script and determines what to do with them. Normal commands are processed immediately, as they are encountered in the logic flow of the script. These are commands such as arithmetic statements:

    nDemons = nDemons + 7;

declarative commands like:

    effect eEffect = EffectVisualEffect(VFX_FNF_IMPLOSION);

and ordinary functions like:

    ApplyEffectToObject(DURATION_TYPE_INSTANT, eEffect, object oTarget);

while Action commands are first parsed then queued for execution, in the order encountered.

The command:

    void DelayCommand(float fSeconds, action aActionToDelay)

is just another ordinary function, when viewed by itself. That means it is processed immediately when it is encountered, just like the other commands in its class. What it causes to happen, though, is not ordinary. The target command, aActionToDelay in the prototype above, is placed on a special time-ordered queue for later execution, based on the other parameter in the prototype, fSeconds.

Okay, you say, I already know all that stuff. Sure you do, but you may not have thought of it just that way before. The important thing said above is that the DelayCommand() command is processed immediately when it is encountered. This means any arguments in its parameter list are processed right then, using values available right then. Why is that important? It lets us use variables, set up in the normal flow of the program, to determine the delay value. So instead of coding:

we can say

    float fd = 3.0; 
    DelayCommand(fd, ActionMoveToObject(oTarget);

with the same effect. This is because although the Move command will delayed, the Delay command itself is executed on the spot, so the current value of the variable fd is is used to determine the delay (in this case, 3.0 seconds) from the start of execution of the script.

This can be very important in cutscenes, especially, where the use of DelayCommand() (or its more civilized cousin, the Gestalt command system which incorporates delay values into its base command set)is common.

Most cutscenes are written in a straight-forward fashion, as a string of DelayCommand() (or Gestalt) statements, each with its own delay, usually ascending in value to make the commands execute consecutively with an appropriate period of time between them to simulate real life time flow. An example of such a sequence (using DelayCommand, but equally applicable to Gestalt system equivalents) might be:

    // set up some useful variables
    object oDoor = GetWaypointByTag("door");
    // now for the action
    DelayCommand(3.0, ApplyCommand(oPC, ActionMoveToObject(oTarget)));
    DelayCommand(7.0, ApplyCommand(oPC, ActionSpeakString("Hi There!")));
    DelayCommand(10.0, ApplyCommand(oTarget, ActionSpeakString("Hi Yourself!")));
    DelayCommand(15.0, ApplyCommand(oTarget, ActionMoveToObject(oDoorway);
    DelayCommand(20.0, ApplyCommand(oPC, ActionSpeakString("Hey! Where are you going?")));
    DelayCommand(23.0, ApplyCommand(oTarget, ActionSpeakString("Out!")));

Now assuming the delays are about right to make all this play realistically (the Gestalt system has some tools to help make such things as walk times come out right) the PC will move to the target NPC and talk a bit. Then the NPC will (rudely) walk away to a nearby doorway, followed by another brief conversational exchange.

Okay, that works fine. But suppose we want to change it? Let's say we want the NPC to say "I'm bored!" after a short delay in the first conversation, to give him a reason to walk away. "Easy!" you say. "Just put another Delay/Speak line after the 'Hi Yourself!' line, and er ... change all the delays from there on down."

Yeah. That's the problem. Inject one measly line and you are liable to have to change EVERY subsequent delay value, to the end of the cutscene script! In our example that's not such a big deal, but some cutscenes get huge...

A brief diversion. The script language allows for an arithmetic statement of the for:

    nDemons += 7;

which is the exact equivalent of the arithmetic assignment statement example at the start of this article:

    nDemons = nDemons + 7;

It's just a shortcut, but a useful one. It also turns out that assignment statements can be embedded in commands, so let's try something. Here's the same stupid scene again, which will perform exactly as before, but with the assignment statement technique embedded in the Delay commands:

    // set up some useful variables
    object oDoor = GetWaypointByTag("door");
    // delay variable
    float fd = 0.0;
    // now for the action
    DelayCommand((fd+=3.0), ApplyCommand(oPC, ActionMoveToObject(oTarget)));
    DelayCommand(fd+=4.0), ApplyCommand(oPC, ActionSpeakString("Hi There!")));
    DelayCommand(fd+=3.0), ApplyCommand(oTarget, ActionSpeakString("Hi Yourself!")));
    DelayCommand(fd+=5.0), ApplyCommand(oTarget, ActionMoveToObject(oDoorway);
    DelayCommand(fd+=5.0), ApplyCommand(oPC, ActionSpeakString("Hey! Where are you going?")));
    DelayCommand(fd+=3.0), ApplyCommand(oTarget, ActionSpeakString("Out!")));

We added a variable, fd, which we update each Delay command. The value of fd at the time of execution of the associated Delay command is exactly the same as in the first version of the scene. So what have we gained? Well, if we now add the command:

    DelayCommand(fd+=4.0), ApplyCommand(oTarget, ActionSpeakString("I'm bored!!")));

After the "Hi Yourself!" line, no further changes to the script are required! It flows just fine, with an extra statement by the NPC interjected, and the rest of the sequence delayed appropriately to make it all flow as before. No more massive script changes to make simple scene changes.

There are other variations on this same theme. Multiple variables can be used, to divide the cutscene into manageable sections. At some useful point, just put the line:

    float fk = fd + 3.0;

or whatever, and use fk in subsequent Delay command delays. Note that fk does not have to depend on fd, either. It can control a totally independent sequence...

Also, sometimes there can be parallel sequences within a main sequence (Two or more things happening at once, starting at the same base time). These too can be handled with separate variables, or by simply not updating the variable itself until the sequences re-converge. Similar techniques will undoubtedly occur to you as you develop your cutscenes.

Simple, isn't it? Have fun.

Jasperre additional: Here is a demo example of a cutscene, done by Robert, which uses the Bioware functions and most of what is explained above, to make a clean script and a clean cutscene. For more advanced cutscenes, some kind of include file to hold important and repeated functions, or using Gesalts scripting cutscene tools available on the vault, is usually recommended.

//:: Name Cut Scene - Demo
//:: FileName cs_demo
//:: Copyright (c) 2001 Bioware Corp.
The PC runs for the exit, pursued by quakes and
lightning.  The door is locked!  So the PC runs
instead for the portal, desperately hoping
it will provide some protection from the attack.
He/she reaches the portal and is teleported out.
//:: Created By: R Johnsen
//:: Created On: 08/12/04
void main()
    // get the player, who must be a PC
    object oPC = GetEnteringObject();
    if (!GetIsPC(oPC)) return;
    // define places
    object oDoor = GetObjectByTag("door");
    object oPortal = GetObjectByTag("portal");
    object oTarget = GetObjectByTag("target");
    // define distances
    float fDist1 = GetDistanceBetween(oPC, oDoor);
    float fDist2 = GetDistanceBetween(oDoor, oPortal);
    // Time constants (can be varied to make the cutscene flow properly)
    // running times - using the equation d = rt, and assuming
    // the PC will use the normal running time of 4.0 m/sec
    float fTime1 = fDist1 / 4.0;
    float fTime2 = fDist2 / 4.0;
    // time spent attempting to open the door
    float fDoorTime = 5.0;
    // start time - the delay before the action starts
    float fStartTime = 3.0;
    // total active cutscene time, based on the PC's actions
    float fTotalTime = fTime1 + fDoorTime + fTime2;
    // effects
    effect eShake = EffectVisualEffect(VFX_FNF_SCREEN_SHAKE);
    effect eLightning = EffectVisualEffect(VFX_IMP_LIGHTNING_M);
    effect ePortal = EffectVisualEffect(VFX_IMP_UNSUMMON);
    // Delay variable for the PC
    float fPC = 0.0;
    // Delay variable for the attack effects
    float fX = fPC;
    // Start the Cutscene
    AssignCommand(oPC, ClearAllActions());
    // the effects (going on in parallel with the PC's actions)
    while (fX < fTotalTime)
        , ApplyEffectToObject(DURATION_TYPE_INSTANT, eShake, oPC));
        , ApplyEffectToObject(DURATION_TYPE_INSTANT, eLightning, oPC));
        fX += 3.0;
    // the PC runs toward the door
    , AssignCommand(oPC, ActionMoveToObject(oDoor, TRUE, 0.0)));
    // the PC tries to open the door
    , AssignCommand(oPC, PlayAnimation(ANIMATION_FIREFORGET_STEAL)));
    , AssignCommand(oPC, SpeakString("The door is locked!")));
    // the PC runs for the portal
    , AssignCommand(oPC, ActionMoveToObject(oPortal, TRUE, 0.0)));
    // the PC is teleported out of danger
    , ApplyEffectToObject(DURATION_TYPE_INSTANT, ePortal, oPC));
   , AssignCommand(oPC, ActionJumpToObject(oTarget)));
   // End of cutscene
    , SetCutsceneMode(oPC, FALSE));

 author: Jasperre