Call of Duty 5: Scripting Common Tasks

From COD Modding & Mapping Wiki
Revision as of 14:21, 10 November 2008 by Zeroy (talk | contribs) (New page: =Overview= This article provides a few examples of how to perform a few common scripting tasks, and in the process of following these examples further familiarize the reader with the overa...)
(diff) ← Older revision | Latest revision (diff) | Newer revision → (diff)
Jump to navigation Jump to search

Overview

This article provides a few examples of how to perform a few common scripting tasks, and in the process of following these examples further familiarize the reader with the overall techniques of writing scripts. To lead into this examples we will first begin by looking at entities: what they are, how they can be acessed, and how they can be acted upon.


Entities

An entity is one of the most basic foundations of the game. If something can move or be interacted with some capacity, it will be made up of one or more entities. This includes a broad spectrum of game components (too numerous to comprehensively list here), among them the player, both friendly and enemy NPCs, triggers, vehicles, etc.

Acquiring An Entity

There are a couple of ways to get a reference to an entity. One is by the entity number, because every entity has a number which uniquely identifies it amongst all other currently existing entities. However this method is typically only used in terms of getting the number of an entity you already have so you can print that number out in a debugging context.

The typical way to acquire an entity is based on its key-value pair. Most entities are declared in radiant, and it's there that key-value pairs can be set up for it, such as classname, which defines what kind of entity it is, target and targetname, which causes an entity with the target key-value to target any entities whose targetname matches it, and many others. In fact, the file share\raw\radiant\keys.txt is a large list of all key-value pairs that are accessible in script, prefixed by what type of value they represent, and frequently followed by a comment explaining their intended purpose. If you need to create a new key-value pair, you must list the key name in this file for it to be accessible in game, although you should look through the file first as there may already be a key suitable to your needs, including a number of generic keys.

Once you've determined the key-value pair you wish to use to reference your entity, you simply call getent with the value followed by the key. So if you wanted to get an elevator at the north end of the map by its targetname, you can write this:

elevator = getent( "north_elevator", "targetname" );

Furthermore, if you named all the elevators on the map the same, you can acquire all of them at once like this:

elevators = getentarray( "elevator", "targetname" );

which creates the variable "elevators" as an array of all the entities whose targetname is "elevator".


Using An Entity Part 1: Member Variables

Since an entity is a struct, it has a number of member variables which can be accessed through the dot operator. Among these members are any of the key-value pairs listed in the keys.txt which were declared on the entity in radiant. Remember that you can always test for whether a given key is present on an entity with the isDefined function.

There are also a number of member variables that are defined by code. Some, like origin and angles, exist on all entities. Others, like ignoreme for actor entities or alignx for HudElems, are only available on certain subsets of entities.

ent.origin = (100, 0, 0);

if ( !isDefined( enemy.script_noteworthy ) )
{
    enemy.ignoreme = false;
}
else
{
    enemy.ignoreme = enemy.script_noteworthy;
}


Using An Entity Part 2: Functions

A large quantity of the built-in script functions require an entity to act upon, resulting in the following syntax:

level.player playsoundtoplayer( "grenade_nearby_warning" );

Here the soundalias "grenade_nearby_warning" will be played emanating from the player. Of course we can still pass entities to functions as input parameters if we need to:

level.player playsoundtoplayer( "grenade_nearby_warning", teammate );

This time the "grenade_nearby_warning" is heard only by the entity named teammate, but still emanating from the player because that is the entity that the function was called on.


Using An Entity Part 3: Threads

Finally, one of the most common methods of using an entity is to create a thread which runs on it. Much like calling a function on an entity, you simply precede the thread declaration with the enitity variable:

elevator thread rise();

Now that we are familiar with the basic syntax, let's look at some real world uses.


Triggers

There are currently ten different types of triggers available, including trigger_damage, trigger_lookat, trigger_use, and trigger_once. For this example we will be working with what may be the most frequently used trigger type, the trigger_multiple.

A trigger is activated by the player touching the volume of the map which is enclosed by the trigger. When the player first touches the trigger, a notify named "trigger" is sent out on that trigger entity which will wake up any threads that are waiting for that message on that entity. This notify will not be sent again until the next time the player touches the trigger after having already left the trigger's bounds (of course, for a trigger_once, the notify will only fire off at most once). Finally, the isTouching function called on the player with the trigger entity as the input will return true anytime the player is actually touching the trigger.

Now let's create a script that will enable and disable packages of ambient sounds based on which trigger the player is touching. First off we need to create the thread that will manage our triggers:

ambientPackageTrigger()
{
    priority = 1;
    if ( isdefined( self.script_ambientpriority ) )
    {
        priority = self.script_ambientpriority;
    }

    for (;;)
    {
        self waittill( "trigger" );

        if ( isdefined ( self.script_ambientpackage ) )
        {
            activateAmbientPackage( self.script_ambientpackage, priority );
        }
        if ( isdefined ( self.script_ambientroom ) )
        {
            activateAmbientRoom( self.script_ambientroom, priority );
        }

        while ( level.player isTouching( self ) )
        {
            wait 0.1;
        }

        if ( isdefined ( self.script_ambientpackage ) )
        {
            deactivateAmbientPackage( self.script_ambientpackage, priority );
        }
        if ( isdefined ( self.script_ambientroom ) )
        {
            deactivateAmbientRoom( self.script_ambientroom, priority );
        }
    }
}

First off, we intend to use this function as a thread running on our ambientPackage triggers, so we know that self will reference these triggers. Next, we have declared two new key-value pairs in our keys.txt file that we are referencing here: script_ambientpackage, script_ambientroom and script_ambientpriority. Because any combination of these key-values may be used on each trigger, we must first check whether they are defined before trying to use them, because if say the script_ambientroom pair was omitted in radiant, that means we have selected that trigger to not affect our current ambientRoom settings.

We know that script_ambientpriority will not change, so we can initialize our internal priority value once before we enter the infinite loop, rather than checking it every single time the loop runs. Also, we want this thread to run for the lifetime of the level and we currently have no method by which any of these triggers might be destroyed, so we have not set an endOn( "death" ) at the start of the thread.

Finally, let's observe the execution flow of the infinite loop. When the thread first runs exectuion will immediately stop at the waittill, and will not resume again until the player touches the trigger, causing the corresponding notify( "trigger" ) to be sent, ending the waittill, and finally causing execution of the thread to continue. At this point we activate our ambient packages and rooms as requested based on the corresponding key-value pairs.

Now we want to be able to deactivate these ambients when the player ceases being in the trigger, so to do that we use a simple while loop that checks whether the player isTouching() the trigger, and if so we wait a few frames and check again. Once we find the player is not touching the trigger, execution moves forward once more, we deactivate as appropriate, and come back around to the top of the infinite loop, ready to repeat the process.

Note that the vast majority of time in this thread is spent on waits, and while a thread is waiting, it performs at most a negligible amount of per frame execution, so these threads are really not very costly at runtime.

Now that we have a thread prepared, we would like to automate creating a thread for each instance of our ambient triggers. We can do this by using array_thread during the _load::main(), which must be called by the main() function of the level.gsc so that it can perform common initialization steps such as this.

array_thread( getentarray( "ambient_package", "targetname" ), maps\_ambientpackage::ambientPackageTrigger );

array_thread() takes two parameters. The first is an array of structs which are to run a thread, and the second is the thread that they are to run. Notice in this example that because we are referencing our thread function in a different file than where we defined it, we must prefix its name with the file where it is defined and the double colon operator. Also notice that we are using getentarray() to build the array we pass to array_thread(), having selected that we will use the key-value pair of "targetname" with the value "ambient_package" to identify all triggers in the level that we wish to run this thread.

Now that we have our thread defined and our call to array_thread() in _load::main(), all that remains is to place trigger_multiples throughout our level as desired. By supplying these triggers with the proper key-value pairs, script will automatically create and run the appropriate thread for each instance.


Spawning

There are a number of ways to spawn a number of entities from script, chief among them being the aptly named spawn() function, which takes as input the classname for the desired entity (the string that identifies the specific type of entity, such as trigger_multiple or info_player_start), and the position at which you would like the new entity to be created at.

One of the most common uses of spawn() is the creation of a "script_origin", a versatile and lightweight entity. These are used to do brief simple things like play a sound or effect at a particular or random position, or we can link another entity to it to control that entity:

level.player freezeControls(true);
level.player shellshock("default", 5);

org = spawn("script_origin",level.player.origin);
level.player linkto( org );
org rotateYaw(75, .01);
wait (.5);
org rotateYaw(-90, 6);

wait (4);
iprintlnbold( "We need him alive Bond." );
level.player unlink();
level.player freezeControls(false);
org delete();

Here the player's is linked to the newly created script_origin, and because the player's controls have been frozen this affords us control over how the player faces, letting us force him to look at what we want him to. Having shown the player what he needed to see, we unfreeze the controls and unlink from the script_origin. Finally note now that we are finished using the script_origin, we delete it, freeing up that entity slot for possible later use by some other spawned entity.

There are other more specific spawning functions, like the self-explanatory spawnVehicle() and spawnTurret(), but now we'll look at how we can go about spawning AIs (because an empty map with no friendly or enemy NPCs to interact with is typically not all that interesting) with the two functions that use a spawner entity to create actors at runtime, dospawn() and stalingradspawn().

First of all, what exactly is a spawner? A spawner acts as a template for spawning a new AI. Rather than litter the map in radiant with instances of every copy of an actor you might need, you can simply create one instance of each type of actor you'd like to use, set up all the key-value pairs to your liking, and check the spawner checkbox in the entity window. Now you have an actor template accessible to you in script in the form of a spawner.

Each of our two actor spawning functions, dospawn() and stalingradspawn(), are virtually identical as they both run on a spawner entity, and each takes as input the desired value for the targetname key of the newly spawned actor. There is one critical difference, however. stalingradspawn() will always succeed in spawning the new actor, but dospawn() will fail if spawning would create a telefrag or if the player is currently looking at the spawn point. (As a historical aside, the reason it is called stalingradspawn() is because the function was created expressly for use on the stalingrad level of Call of Duty.)

spawner = getent( "enemy_spawner", "targetname" );
enemy1 = spawner stalingradspawn( "enemy1" );
enemy2 = spawner dospawn( "enemy2" );
if ( spawnfailed( enemy2 ) )
{
    return;
}

Notice the check on enemy2 using spawnfailed(), so we can determine whether dospawn() failed, and if so cease operating on that actor.


Sources: Treyarch's Wiki