Click here to Skip to main content
15,881,898 members
Articles / Programming Languages / C#
Article

EezeeScript: A simple embeddable scripting language for .NET

Rate me:
Please Sign up or sign in to vote.
4.96/5 (19 votes)
14 May 2007CPOL10 min read 60.9K   3K   87   7
An API for enhancing any .NET application with a simple scripting language

Screenshot - EezeeScript00.jpg

Introduction

Embedded scripting engines have applications in several areas, including the extension and modification of a software application's core functionalities. In the game development world, scripting provides a means for the game engine developer to hand over control to the designer. This allows him or her to implement the game's plot line events, NPC behaviour, and so on, without the intervention of the game engine developer, who may otherwise need to hard-wire game-play logic into the engine.

This article describes a simple embeddable scripting engine for .NET applications and illustrates its use. Source code and binaries for the scripting engine, as well as an illustrative demo, are included as downloads with the article.

Background

Reading through Alex Varanese's Game Scripting Mastery, I was inspired to undertake a small project to develop a simple scripting system that can be incorporated into .NET applications.
The scripting system, unimaginatively entitled EezeeScript, is a command-based language similar to the one proposed by Mr Varanese in the beginning of the book. The script language is thus not as free-form as a C-style language. However, I adopted some of his ideas to extend the language with control statements and a simple form of procedural constructs. I have also made the script generic with new domain-specific commands available only if specifically registered by the host application. EezeeScript was developed as a .NET assembly for use with .NET applications, but it could easily be ported to C++ for more generic use.

Using the code

This section provides a quick introduction for integrating EezeeScript in a .NET solution and using the API.

Integrating EezeeScript in a .NET solution

The EezeeScript host API can be integrated into an existing .NET solution by adding a reference to the EezeeScript.dll assembly. All API classes are defined within an "EezeeScript" namespace that must be either specified with the using clause or prefixed to the API classes.

Preparing the scripting environment

The scripting system is initialised by creating one or more instances of the ScriptManager class. Each instance represents a scripting environment where scripts can be loaded and executed, and also provides a global variable scope that the scripts can use to share data.

C#
ScriptManager scriptManager = new ScriptManager();

Loading scripts

Once a script manager is available, scripts can be loaded by creating instances of the Script class. The script object's constructor requires a reference to the script manager and a name to identify the script. By default, this name corresponds to a disk filename.

C#
Script script = new Script(scriptManager, "NPC_Wizard.ezs"); 

Preparing scripts for execution

A script object represents only the programming instructions contained within and not it's execution state. To execute the script, an instance of the ScriptContext class must be created. The class's constructor requires a reference to the script to be executed or to one of the script's named blocks if any other is defined. A script reference implies that the main code block will be executed. If a named block is specified, only the code within the block and other blocks called within it will be executed. The script context provides execution control and access to the execution state in terms of the variables defined during execution, the next statement to be executed, and so on. The ScriptContext class represents a running instance of a script. Thus, multiple instances of the same script object can be executed within the same script manager by creating multiple script contexts that reference the same script.

C#
// create a context for the script's main code block
ScriptContext scriptContext = new ScriptContext(script);

// also creates a context for the script's main block
ScriptContext scriptContext = new ScriptContext(script.MainBlock);

// create a context for one of the script's named blocks
ScriptBlock scriptBlock = script.Blocks["WanderAround"];
ScriptContext scriptContext = new ScriptContext(scriptBlock);

Executing scripts

The script context object allows execution of the referenced script via the three variants of its Execute method. This allows execution of scripts for an indefinite amount of time, for a given time interval or up to a maximum number of executed statements.

The first method variant allows the referenced script block to execute indefinitely or until the end of the block is reached. If the script contains an infinite loop, this method will block indefinitely unless an interrupt is generated. The Execute method returns the total number of statements executed since its invocation.

C#
// execute indefinitely, or until termination, or until
// a script interrupt is generated
scriptContext.Execute();

The second variant of the Execute method allows the script context to execute up to a given maximum number of statements. The script context may break out of execution before the maximum is reached if there are no more statements to process or if an interrupt is generated.

C#
// execute up to a maximumum of 10 statements
scriptContext.Execute(10);

The third variant of the Execute method accepts a TimeSpan defining the maximum time interval allowed for script execution. The method may break out of execution earlier than the given interval if there are no more statements to process or if an interrupt is generated. Given a script with a good balance of different statements, a possible use of this method is to determine the speed of the scripting system on the target environment in terms of statements executed per second.

C#
// execute for up to 10 milliseconds
TimeSpan tsInterval = new TimeSpan(0, 0, 0, 0, 10);
scriptContext.Execute(tsInterval);

The second and third variants of Execute may be used to implement a virtual multi-threaded scripting environment. Global variables may be used as semaphores to synchronise concurrently running scripts.

The RPG cut-scene demo included with the EezeeScript library illustrates this concept. Each character in the demo is controlled by a separate script. Global boolean variables are used to allow one character to signal another character to perform an action. It should be noted that for this particular example, a cut-scene might be easier to implement using a single unified script with access to all relevant characters. However, the point of the demo is to demonstrate as many of the features as possible, including virtual multi-threading, inter-script communication and integration with the host application.

Interrupting and resetting scripts

A script context will normally execute its referenced script block indefinitely, for a given time interval, until a given maximum number of statements are executed or until there are no more statements to process. In some cases, it is desirable to break execution prematurely, such as to return control to the host when specific statements are executed, or because a script is too computationally intensive to execute in one go.

EezeeScript provides two ways for generating script interrupts:

  • using the YIELD script instruction to explicitly break execution at a specific point in the script, or
  • enabling the script context object's InterruptOnCustomCommand property to generate an interrupt automatically whenever a custom command is executed

NPC Scripts in the RPG Cut-Scene Demp

The RPG cut-scene demo uses both approaches to allow each character script to be written as if it is meant to run in isolation from other scripts. Control is returned to the associated character object whenever a custom command is processed. This in turn allows the character object to queue and process the corresponding actions. While the actions are in progress, the script context is kept in a suspended state. Once the actions are complete, execution of the script is resumed. The scripts also implement "busy-waiting" synchronisation by setting global boolean variables and using WHILEENDWHILE looping statements. A YIELD instruction in the loop prevents indefinite script lockup.

NPC1 Script NPC2 Script
C#
// initialise signal
SET g_OnTheWay TO TRUE

// walk to meeting point
NPC_MOVE 400 0

// signal arrival to NPC 2
SET g_OnTheWay TO FALSE
C#
// wait for arrival of NPC1
WHILE g_OnTheWay
    YIELD
ENDWHILE

// talk to NPC 1
NPC_SAY "Here you are!" 100

An executing script may be reset via the script context's Reset method. The net effect of invoking this method is that the local variables defined in the context are lost, the execution frame stack is cleared and the statement pointer is reset to point to the first statement in the script block referenced by the script context. Global variables are not affected and persist after a script context is reset.

Custom script loaders

To allow for loading of scripts from other sources -- such as an archive file, network or database -- a custom script loader class can be developed and bound to the script manager used for loading the script. The script loader class may be any class that implements the ScriptLoader interface. The loader is used to retrieve the script specified in the Script class constructor and also any additional included scripts defined within the original script.

C#
// custom script loader class
public class MyScriptLoader
    : ScriptLoader
{
    public List<string> LoadScript(String strResourceName)
    {
        // loader implementation here...
    }
}

// in initialisation code...
ScriptLoader scriptLoader = new MyScriptLoader();
scriptManager.Loader = scriptLoader;</string>

Accessing the local and global variable scope

One approach for allowing a script to communicate with or to control the host application entails the latter polling the local variables of the associated script context and global variables of the associated script manager. It does this by querying the LocalVariables and GlobalVariables properties of the script context object respectively.

C#
// get value of local variable
int iScore = (int) scriptContext.LocalVariables["PlayerScore"];

// get value of global variable
bool bNewQuest 
    = (bool) scriptContext.GlobalVariables["g_WizardQuestAvailable"];

Custom command extensions

A more powerful alternative to allow a script to interface with the host application is to register custom commands with the script manager and assign a script handler to a script context. The script handler in turn provides an implementation for the custom commands. Custom commands are first defined by creating an instance of the CommandPrototype class to define the command's name and parameters.

C#
// define command to move player
CommandPrototype commandPrototype = new CommandPrototype("Player_Move");

// add int parameter for x offset
commandPrototype.AddParameterType(typeof(int));

// add int parameter for y offset
commandPrototype.AddParameterType(typeof(int));

Once the command prototype is defined, it can be registered with the script manager. The command prototype ensures that a corresponding custom command is recognised during runtime and that the parameters passed alongside correspond in number, type and order to those defined in the prototype.

C#
// register new custom command
scriptManager.RegisterCommand(commandPrototype);

Implementing custom commands

Unless a script handler is bound to a script context, custom commands are only validated against their corresponding prototype when processed. Any class that implements the ScriptHandler interface may be used to handle custom script commands. The owner of the script context is a suitable choice for the handler. The handler interface requires the implementer class to provide methods to be executed whenever a custom command is processed, a local or global variable is changed, or an interrupt is generated.

C#
public class Player
// player class is its own script handler
    : ScriptHandler
{
    // called by script context when custom command is processed
    public void OnScriptCommand(ScriptContext scriptContext, String strCommand, List>object< listParameters)
    {
        // identify command by name
        if (strCommand == "Player_Move")
        {
            // read X and Y movement
            offsets m_iMoveX = (int)listParameters[0];
            m_iMoveY = (int)listParameters[1];
        }

        // other command implementations go here...
    }

    // called by script context when local/global var set or changed
    public void OnScriptVariableUpdate(ScriptContext scriptContext, String strIdentifier, object objectValue)
    {
        // do something, e.g. display for debug purposes
    }

    // called by script context when on interrupt
    public void OnScriptInterrupt(ScriptContext scriptContext)
    {
        // do something, e.g. update host state }
    }
}

The script handler object is bound to the respective context via the Handler property.

C#
// script passed via constructor
public Player(..., Script script, ...)
{
    :
    // prepare context
    m_scriptContext = new ScriptContext(script);

    // bind self as a script handler
    m_scriptContext.Handler = this;
    :
}

The ability to define a different handler for every script context allows a custom command to have different implementations and / or contexts depending on the script contexts to which respective script handlers are bound. For example, the implementation for a "Move x y" command within a game might affect the movement of a player character, a non-player character or a projectile.

Error handling

API usage errors, compilation errors and script runtime errors are handled by throwing a ScriptException. Each exception may contain inner exceptions at an arbitrary number of levels. For compiler and runtime errors, the exception also provides a Statement property identifying the statement where the compilation or runtime error occurred.

EezeeScript language reference

Refer to the language reference guide PDF that is included with the EezeeScript binary.

Points of interest

Building this scripting engine was quite an enjoyable experience, as it allowed me to get a simple scripting engine up and running very quickly. I also experimented with more formal compiler concepts, such as using a stack frame to allow for an indefinite level of call nesting. I also tried to make the language as domain-independent as possible -- that is, all commands are pure control statements -- while allowing for domain-specific commands to be registered with the engine as applicable. A major point of interest is that custom commands are integrated with the host application using a subscriber model, where each context can handle command according to its own implementation. This imbues custom commands not only with an implicit context, as in OO methods, but also with the ability to provide a context-sensitive implementation depending on which entity is running the script.

Possible future features for EezeeScript are:

  • Support for more natural assignments using an = operator (currently requires SET {var} TO {val})
  • Support for arbitrary complex arithmetic expressions (e.g. X = (A + B) * C)
  • Support for parameter passing and block-level variable scopes

History

  • 14/May/2007 - First version released.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
Software Developer (Senior)
Malta Malta
This member has not yet provided a Biography. Assume it's interesting and varied, and probably something to do with programming.

Comments and Discussions

 
QuestionQuestion for NPC Pin
thejuster13-Nov-16 20:39
thejuster13-Nov-16 20:39 
AnswerRe: Question for NPC Pin
Colin Vella13-Nov-16 22:34
Colin Vella13-Nov-16 22:34 
Generalhah! Pin
dave.dolan27-May-07 18:24
dave.dolan27-May-07 18:24 
I found myself pouring over the same book, Game Scripting Mastery, and trying to do the same thing, and of course, I found your little example here, and I don't feel the need to continue all on my own. I know I'm a late comer to this party, but cool anyway, especially since I can't find any remnants of Alex Varanese any where online, (perhaps he's tired of signing autographs for the teeming millions of compiler writers he's inspired...) The email address in the book is a bounce, and I can't find any more recent heads or tails. So hello, to you, who just happened to have the same idea I did, except you had it first, and yours is finished! Nice work. Again I'm slain!
GeneralRe: hah! Pin
Colin Vella28-May-07 6:32
Colin Vella28-May-07 6:32 
GeneralRe: hah! Pin
dave.dolan28-May-07 7:15
dave.dolan28-May-07 7:15 
GeneralExcellent Pin
merlin98122-May-07 3:35
professionalmerlin98122-May-07 3:35 
Generalvery good! Pin
chinasf14-May-07 21:37
chinasf14-May-07 21:37 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.