Intensity Engine Scripting API Documentation. (C) 2009 Alon Zakai. License: CC-BY-SA-NC.

Intensity Engine Scripting


  1. Introduction
    1. Terminology
    2. Types of Scripts
    3. The Standard Library
  2. Tutorial
  3. Main Concepts
    1. Logic Entities and State Variables
    2. Main Functions of Logic Entities
    3. Frame-Based Coding
    4. Types of State Variables
    5. Properties of State Variables
    6. Main Logic Entity Classes
    7. The Action System
    8. Other Useful Things
  4. Plugin Example: Stunball Gun
  5. Complete Working Activity: Sketch World
  6. Appendix: Standard Library Reference


Introduction

Scripts in the Intensity Engine are written in JavaScript, while using a custom Intensity Engine API. The custom API consists of



In general, you should only use the latter: Calling directly into C++ should be handled for you by the JavaScript part of the API. However, we mention the former here because you may see it used in actual code, and at times it may be the best way to do something (until the JavaScript part of the API improves). In this document we will therefore talk only about the JavaScript part of the API.


Terminology


Types of Scripts


The Standard Library

The standard library is implemented as 'namespaces', using JavaScript objects. So, for example you would write Map.someFunction() to access someFunction in the 'namespace' Map.


The standard library has two parts: The core, and the optional.



The main namespaces of the standard library are as follows. For now we just give an overview, details will appear later on. A full index of the standard library appears in the Appendix.



Tutorial

The best way to learn is to go over a working example script. Here is a 'hello world' (minimal working example):


// Default materials, etc.

Library.include('library/MapDefaults');

// Textures

Library.include('yo_frankie/');

// Map settings

Map.fogColor(0, 0, 0);
Map.fog(9999);
Map.loadSky("skyboxes/philo/sky3");
Map.skylight(100, 100, 100);
Map.ambient(20);
Map.shadowmapAmbient("0x505050");
Map.shadowmapAngle(300);

//// Player class

registerEntityClass(Player.extend({
    _class: "GamePlayer",
}));

//// Application

ApplicationManager.setApplicationClass(Application.extend({
    _class: "GameApplication",

    getPcClass: function() {
        return "GamePlayer";
    },

    clientOnEntityOffMap: function(entity) {
        entity.position = [600,600,600];
    }
}));

//// Load permanent entities

if (Global.SERVER) {
    var entities = CAPI.readFile("./entities.json");
    loadEntities(entities);
}


Let us go over it piece by piece:




That concludes the code for this simple script. If you run an activity with this script, it will be fairly boring, as there is nothing in the world (well, unless exciting things are in the entities.json file). But this is the general framework for a map script.

Summary of main points in this Chapter:



Main Concepts

Logic Entities and State Variables

Logic entities, or just 'entities', are the important things in an activity, for example, the characters, props, and so forth ('logic' refers to the terms 'game logic', 'business logic', and so forth). Each actual entity is an instance of a particular entity class. You can use inheritance to create such classes, or the component-based system described later.

Logic entities are synchronized between the client and server: When you create a new entity on the server, without your needing to do anything, the engine will tell the clients to 'reflect' that entity, so they are aware of it. Furthermore, the engine will also synchronize some of the entity's attributes in a transparent manner, such attributes are called state variables. Attributes of an entity that are not state variables are not synchronized; a common mistake is to assume that they are by writing to one on the server and trying to read it on the clients. The reason that not all attributes are synchronized is that this would take a significant amount of bandwidth and CPU power, whereas sometimes you need just a 'local' attribute, on just the server or just the client; such attributes can be worked with very fast (V8, the engine running JavaScript in the Intensity Engine, optimizes such variables very significantly).

Here is a simple example of a state variable in an entity class:

var Door = Mapmodel.extend({
    _class: "Door",

    open: new StateBoolean(),

});

(Mapmodel is a class of 'prop' entities. More on them later.) As you can see, state variables are defined as members of the entity class. In this case we define 'open' as a state boolean, which is a state variable that is a boolean, i.e., true or false. (The reason you need to declare the type of the variable - as opposed to normal JavaScript variables - is that the type affects how the variable is transmitted over the network when it is synchronized.) In this example, a door instance can be either open or closed.

With this declaration, we can create an instance of the class as follows:

registerEntityClass(Door);

var someDoor = newEntity("Door");


Note that we need to register the class on both the client and the server, but the creation (using newEntity) can only be done on the server. Note also that newEntity takes the name of a class, not the class itself. It looks up the class among the registered classes, which is why we needed to register our class beforehand (this is necessary because requests to create a class can arrive from the clients in string form).

After calling newEntity in this way, the entity is put into play, and it will be reflected automatically on all the connected clients. You can then do someDoor.open = True; on the server, and that value will be synchronized between the server and all the clients. More specifically,


The logic behind this is that the server is the 'final arbiter' of the value. There must be some arbiter, or otherwise each client (and the server) could set the value independently, and synchronizing the values might lead to unpredictable results, depending on network speeds and so forth. So, by default the server is the arbiter of state variables (but this can be changed; more on that later).

In particular, this leads to the following peculiarity on the client:

// Assume someDoor.open is equal to False beforehand
someDoor.open = True;
var isOpen = someDoor.open; // Still False!

The reason is that it takes time for the request to be sent to the server, and for the server to respond. Until that time, the client still shows the old value. This can be confusing, but really there is no way to get around this aspect of writing distributed applications - it is confusing sometimes.

Let us now add some functionality to our class. To do so in a convenient matter, we will use the component-based entity system, which is generally more convenient than using class inheritance. Instead of inheriting classes, we will build classes out of 'building blocks', or components, also known as 'plugins'. Here is an example of such code:

Library.include('library/Plugins');

var Door = bakePlugins(
    Mapmodel,
    [
        {
            _class: "Door",

            open: new StateBoolean(),

            activate: function() {
                this.open = false;
                this.locked = true;

                this.connect(
                    'onModify_open',
                    this.checkLock
                );
            },

            checkLock: function(value) {
                if (value === true && this.locked) {
                    throw "CancelStateDataUpdate";
                }
            },
        }
    ]
);


bakePlugins takes two parameters: A base class, and an array of 'plugins'/'components'. It returns a new class which is a subclass of the base and has the functionality of all the components. In this example, we provided a single plugin, which as before gives the class a name of "Door" and has a single state variable, "open". There are then two new pieces of code here, the activate and checkLock functions, which we will now describe:



In summary, this example shows how we can use state variables to maintain important information about an entity. Let's think for a minute about how this would work in the bigger picture, and why it makes sense to do it as we did. Assume that 'locked' was in fact a state variable, and therefore accessible on the client, and that when a player clicks to open a door, then that would run a piece of code that look this:

if (!this.locked) this.open = True;

That is, if the door isn't locked, open it (otherwise, perhaps play a sound, or show a message "the door is locked", etc.). This seems like it would work, and that we don't need to check validity on the server at all - the client will only notify the server to open the door if it's unlocked. But the issue is that, with distributed applications, all sorts of things might happen. For example, the door might get locked between the time the player clicks on the door and before that request arrives at the server. In that case, our code above on the server would handle the situation correctly - keep the door closed. (Note that the failure here is silent - the player will simply see the door not open, but not get any other feedback. Typically you would handle this so that the player gets the same feedback they would get for a normal locked door that they try to open.) Similar problems can occur, for example, in capture-the-flag type games, if two players try to grab the flag at almost the same instant - both will send requests to the server to pick up the flag, but it should only let one of them (the first) do so.


Main Functions of Logic Entities

Aside from activate, which we saw above, there are several other important functions that you can use in plugins:


As we saw earlier, you can also define new functions in a plugin, and those will become functions of the class they are baked into.


Frame-Based Coding

As just mentioned, act() and clientAct() are called on each frame. This is different from some scripting APIs that simply let you write long scripts that run continuously (in most such cases, each in a separate thread, or queued after each other), or APIs that let you write event handlers (although, you can see act and clientAct as event handlers, in a way). The main difference is responsiveness vs. throughput.

If each script runs in its own thread, or multiple threads handle events in parallel, that can definitely help throughput, if multiple CPU cores are available. (It can also lead to simpler code in some cases, but not others.) But the price is less predictability and responsiveness, in the sense that we can't time script code to occur exactly in sync with our frames (unless we handle events that occur once per frame - which is exactly what act and clientAct do). In fast-paced games, we do need such predictability and responsiveness: If a player clicks, the effects of that click should occur in the very next frame; likewise, if some event is occurring (say, the player is under a spell), we may want to be able to have a visual effect signal the end of the event on the exact same frame that happens. In both cases, frame-based coding can work, but a complicated multithreaded system that schedules scripts might not.

One consequence of frame-based coding is that we need to be careful what we run in each frame - if we spend too much time in act or clientAct for a particular entity, the entire frame will take too long, which may be noticeable to the player. One way to be careful about this is to schedule code to only occur every so often, for example, a bot might look for new targets only once per second. This can be done in a simple way using a RepeatingTimer, which we will see examples of below.

 

Types of State Variables

We already saw boolean state variables mentioned. Here is a list of all the important kinds (note that you can also define your own, see src/javascript/intensity/Variables.js):



Properties of State Variables

When you define a state variable, you can define some properties for it, using syntax of the following form:

door: new StateBoolean({ clientWrite: false });

(Note the curly brackets - an object is used here.) In the example here we set the property 'clientWrite' to false. This property, and the other important ones, are explained below:



Main Logic Entity Classes

The following are the main entity classes in the API:



The Action System

Every entity has an action queue. You can add actions to the action queue, and they will be performed in order. This is useful for controlling the 'state' of an entity, for example, if a game has a 'death state', we can implement that as follows:

DeathAction = Action.extend({
    _name: 'DeathAction',
    canMultiplyQueue: false,
    canBeCancelled: false,
    secondsLeft: 5.5,

    doStart: function() {
        this.actor.emit('fragged');
        this.actor.canMove = false;
        this.actor.animation = ANIM_DYING|ANIM_RAGDOLL;
    },

    doFinish: function() {
        this.actor.respawn();
    }
});

(This is taken from packages/library/Health.js.)

As you can see, action classes are subclasses of Action. Here we give the action a _name, and set some properties:


Aside from the properties mentioned above (canMultiplyQueue, canBeCancelled, secondsLeft), subclasses of Action can have a few other properties,


In the example above we then overload two functions:


Another useful function to override (which we didn't need in the example above) is the following:


After defining the DeathAction class from before, we can use it as follows:

player.queueAction(new DeathAction());

This will queue a new death action on player. Note that in this example, we would probably do player.clearActions(); before it, to cancel any current actions and make the death action start immediately.

In summary, note that you don't need to use actions - you can accomplish the same results by coding behavior into the act() and clientAct() functions. But you'd probably end up with a lot of conditions and 'spaghetti code'. The action system can make writing clear, extensible code easier.


Other Useful Things


Plugin Example: Stunball Gun

In this chapter we will look over an example of plugin, specifically a 'stunball gun' - a weapon that shoots projectiles that explode and stun their targets, making them move more slowly for a short time. Here is the code, taken from packages/library/guns/Stunball.js:


Library.include('library/Firing');
Library.include('library/Projectiles');


Stunball = Projectiles.Projectile.extend({
    radius: 4,
    color: 0xABCDFF,
    explosionPower: 50.0,
    speed: 90.0,
    timeLeft: 1.0,

    customDamageFunc: function(entity, damage) {
        if (damage > 25) {
            entity.sufferStun(damage);
        }
    },
});


StunballGun = Gun.extend({
    delay: 0.5,
    repeating: false,
    originTag: '',

    handleClientEffect: function(shooter, originPosition, targetPosition, targetEntity) {
        shooter.projectileManager.add( new Stunball(
            originPosition,
            targetPosition.subNew(originPosition).normalize(),
            shooter
        ));

        Sound.play("olpc/AdamKeshen/CAN_1.L.wav", originPosition);
    },
});


StunballVictimPlugin = {

    sufferingStun: new StateBoolean({ clientSet: true }),

    activate: function() {
        this.sufferingStun = false;
    },

    sufferStun: function(stun) {
        if (!this.sufferingStun) {
            this.oldMovementSpeed = this.movementSpeed;
            this.movementSpeed /= 4;
            this.sufferingStun = true;
        }
        this.sufferingStunLeft = stun/10; // seconds
    },

    clientAct: function(seconds) {
        if (this.sufferingStun) {
            var ox = (Math.random() - 0.5)*2*2;
            var oy = (Math.random() - 0.5)*2*2;
            var oz = (Math.random() - 0.5)*2;
            var speed = 150;
            var density = 2;
            Effect.flame(PARTICLE.SMOKE, this.getCenter().add(new Vector3(ox,oy,oz)), 0.5, 1.5, 0x000000, density, 2.0, speed, 0.6, -15);

            if (this === getPlayerEntity()) {
                this.sufferingStunLeft -= seconds;
                if (this.sufferingStunLeft <= 0) {
                    this.movementSpeed = this.oldMovementSpeed;
                    this.sufferingStun = false;
                }
            }
        }
    },
};


Map.preloadSound('olpc/AdamKeshen/CAN_1.L.wav');


Comments and points of interest:


Complete Working Activity: Sketch World

In this chapter we will look over a complete working example activity: Sketch World. If you haven't seen it in action, it can be summarized as an activity where every player can 'write' on the scenery, in a different color, in streaks of light (but you should really see it in action before reading this!). Here is the code:

Library.include('library/Plugins');
Library.include('library/PlayerList');

Library.include('library/MapDefaults');

Library.include('yo_frankie/');

Map.fogColor(0, 0, 0);
Map.fog(9999);
Map.loadSky("skyboxes/philo/sky3");
Map.skylight(100, 100, 100);
Map.ambient(20);
Map.shadowmapAmbient("0x101010");
Map.shadowmapAngle(300);

//// Player class

registerEntityClass(
  bakePlugins(
    Player,
    [
      PlayerList.plugin,
      {
        _class: "GamePlayer",

        markColor: new StateInteger({ clientSet: true }),
        newMark: new StateArrayFloat({ clientSet: true, hasHistory: false }), //, reliable: false }),
        clearMarks: new StateBoolean({ clientSet: true, hasHistory: false }),

        init: function() {
          this.modelName = 'frankie';
        },

        activate: function() {
          this.movementSpeed = 75;

          this.clearMarks = true;
        },

        clientActivate: function() {
          this.marks = [];

          this.connect('client_onModify_newMark', this.onNewMark);
          this.connect('client_onModify_clearMarks', this.resetMarks);
        },

        resetMarks: function() {
          this.marks = [];

          if (this === getPlayerEntity()) {
            function randomChannel() { return integer(Math.random()*256); }
            this.markColor = randomChannel() + (randomChannel() << 8) + (randomChannel() << 16);
            Effect.splash(PARTICLE.SPARK, 15, 1.0, this.position.addNew(new Vector3(0,0,20)), this.markColor, 1.0, 70, 1);
          }
        },

        clientAct: function(seconds) {
          // Show marks
          var color = this.markColor;
          var last;
          forEach(this.marks, function(mark) {
            if (last && mark) {
              Effect.flare(PARTICLE.STREAK, last, mark, 0, color, 1.0);
              Effect.flare(PARTICLE.STREAK, mark, last, 0, color, 1.0);
            }
            last = mark;
          });

          // Create new mark, possibly
          if (this === getPlayerEntity()) {
            var newBatch = this.marks.length === 0 || !this.marks[this.marks.length-1];
            var continuingBatch = this.marks.length > 0 && this.marks[this.marks.length-1];

            if (continuingBatch) {
              Effect.splash(PARTICLE.SPARK, 10, 0.15, this.marks[this.marks.length-1], color, 1.0, 25, 1);
            }

            if (this.pressing) {
              var newPosition = CAPI.getTargetPosition();
              var toPlayer = this.position.subNew(newPosition);
              newPosition.add(toPlayer.normalize().mul(1.0)); // Bring a little out of the scenery
              // Add if a new batch, or otherwise only if not too close
              if (newBatch || !this.marks[this.marks.length-1].isCloseTo(newPosition, 5.0)) {
                this.newMark = newPosition.asArray();
              }
            }
          }
        },

        onNewMark: function(mark) {
          if (mark.length === 3) {
            mark = new Vector3(mark);
          } else {
            mark = null;
          }
          this.marks.push(mark);
          // TODO: Expire old marks beyond a certain number of total marks
        },
      }
    ]
  )
);

//// Application

ApplicationManager.setApplicationClass(Application.extend({
  _class: "SketchWorldApp",

  getPcClass: function() {
    return "GamePlayer";
  },

  getScoreboardText: PlayerList.getScoreboardText,

  clientOnEntityOffMap: function(entity) {
    entity.position = [600,600,600];
  },

  clientClick: function(button, down, position, entity) {
    var player = getPlayerEntity();
    if (!entity) {
      if (button === 1) {
        player.pressing = down;
      } else if (button === 2 && down) {
        player.newMark = []; // Separator
      }
    } else {
      // There are much better ways to code this sort of thing, but as a hack it will work
      if (down && entity._class === 'Mapmodel') {
        player.clearMarks = true;
      }
    }

    return true; // Handled
  },
}));

//// Load permanent entities

if (Global.SERVER) { // Run this only on the server - not the clients
  var entities = CAPI.readFile("./entities.json");
  loadEntities(entities);
}


Comments and points of interest:


Appendix: Standard Library Reference

Comments appear inside the source code files. For now, they are the best documentation of individual classes, functions, parameters, etc. This section gives an overview of the files, so you know where to look for things.

The core standard library (src/javascript/intensity) includes:


The optional standard library (packages/library) includes (as of version 1.2 of the extra standard library):