Engine Concepts - theRAPTLab/gsgo GitHub Wiki

created feb 26, 2021

If you want to learn about how GEMSTEP system elements like GAgent, GFeature, and GVar work inside the simulation engine, this is the document for you.

If you want to know how to use and add GEMSTEP keywords, that is a different document [TBD]. The concepts in this document, however, may help understand some of the overall context that make them work.

Blueprint Compiler Overview

The GEMSTEP SIM module works by performs a series of actions on all the objects in the simulation many times a second, looping from the last action to the first each time. This series of actions is called the "simulation loop" or "simloop".

The main object that students will be creating are "Agents" that are created from "Blueprints".

When discussing code related to implementing an Agent or a Blueprint as experienced by the student, we use the 'agent instance' or 'GAgent'.

Blueprints allow students to define what happens during specific parts of the simulation loop using a "script". In later versions of GEMSTEP the scripting interface will be a graphical wizard of some kind that creates the raw script format, but in our early prototype we have defined a higher-level scripting language that is converted into the low-level script.

The low-level raw script format is based on "ScriptUnits". A single ScriptUnit is an array of Javascript primitives (numbers, strings, booleans, arrays, objects) with some special extensions:

[ keyword, arg1, arg2, ... ]

where keyword is a script "command", and arg1, arg2, ... are a variable list of parameters for that command. An array of ScriptUnits comprises a "Script"

[
  [ keyword, arg1, arg2 ],
  [ keyword, arg1 ],
  [ keyword, arg1, arg2, arg3 ]
]

Because it's hard to write programs as arrays of strings, the higher-level text-based language is used to generate the script format. We call the higher-level language "ScriptText" to distinguish it from Script and ScriptUnit during technical discussion. In the student-facing tool, they will not see ScriptText.

The equivalent ScriptText looks like this:

keyword arg1 arg2
keyword arg1
keyword arg1 arg2 arg3

This ScriptText is converted into a Script, which is an array of ScriptUnits, which itself is an array Javascript primitives. This Script is compiled into a "Program" which is an array of Javascript functions, each using the with same function signature:

[
  ( agent, state ) => { /* code for ScriptUnit 1 */ },
  ( agent, state ) => { /* code for ScriptUnit 2 */ },
  ( agent, state ) => { /* code for ScriptUnit 3 */ },
]

NOTE: the function signature for (agent,state)=>{} is defined as type TOpcode in lib/t-script.d.ts

The conversion from each ScriptUnit to function is handled by a "keyword definition" which captures the arguments and returns a new function based on them. These are defined in individual files in sim/script/keywords, and also handle "serialization" (converting a ScriptUnit into text that can be sent over the network) and "wizard UI". This makes it easy to add keywords and keep all the necessary logic in a single file.

Programs are "executed" by providing an agent instance and a state object to each function. The state object is used to convey persistent state from function to function as they are invoked; it is how a function receives data from initial invocation of the program and passes data to the next function in the array. Execution is handled by each individual agent instance through its exec() function, and it handled by the simulation engine.

Although there is a single Blueprint that defines a specific kind of Agent, the Blueprint has to create several different programs. These are defined indatacore/dc-script-bundle and are currently as follows:

THESE PROGRAM TYPES ALL USED DIRECTLY BY AGENT BLUEPRINT MAKERS

define     defines props when an agent instance is created
init       runs just before the simulation starts
update     runs during the `AGENT_UPDATE` phase of the simloop
think      runs during the `AGENT_THINK` phase, which is for AI
exec       runs during the `AGENT_EXEC` phase, which actually changes agent

THESE PROGRAM TYPES ARE USED BY SYSTEM TO SUPPORT THE SIMULATION

condition  runs during `SIM_CONDITION` to test sets of agents
event      runs during `SIM_EVENT` phase to process async events

THESE PROGRAM TYPES ARE USED TO IMPLEMENT CONDITIONS

test       defines a program that returns either true or false
conseq     program that runs if a test is true
alter      program that runs if a test is false

In ScriptText, we use special compiler flags called "pragma" to indicate (1) the name of the Blueprint to compile and (2) the program that the subsequent ScriptUnits should be added to util the next pragma changes it. The current pragmas are defined as a keyword definition located in sim/script/keywords/_pragma.tsx; we use a leading underscore to differentiate between "system keywords" (which you should not touch) and "script designer keywords" that you can add at will.

The resulting Program is stored in a dictionary declared in datacore/dc-agents, which maps from a Blueprint name (the "agent type") to the "Program bundle" that contains a subset of the programs described above. The programs are retrieved during Agent instantiation at the beginning of a simulation run. Some are used to define the Agent's additional "agent properties" and "agent features" that collect pre-written behaviors that would be difficult for a student to write. Others are injected directly into each agent instance to change their behavior during different phases of the simulation loop.

NOTE: Keyword authoring is a subject until itself which will be described in another document.

In summary:

  • ScriptText is converted into ScriptUnits
  • ScriptUnits are converted into a function with a specific function signature
  • A collect of ScriptUnits is called a Program
  • There are different kinds of Programs that are defined for any Blueprint
  • Programs are run by the simulation agent at specific times in the simulation loop
  • Keywords definitions handle the conversion of ScriptUnits into functions, wizardUI, and serialization/deserialization as needed.

Agent Capabilities

In this section, "Agent" refers to the concept of an agent as students experience and model them. The "Scripting Interface" is the GUI wizard that students use to make their own Agents by authoring one or more "Blueprints" that are used to make Agents of a certain kind. The "Script" refers to the code that students put inside their Blueprints to customize their agents' behaviors and interactions.

Basic Agent Elements

All Agents have some basic "Properties" (hold data) and "Methods" (do a code action). Additional Properties can be added through the "Blueprint" definition to customize the behavior. Methods, however, can not be defined by students. They can only be invoked.

Methods will be described in another part of this document, but you can think of them as 'functions' or 'subroutines'

All Agents have the following basic Properties:

  • the x and y location in the simulation world (currently defined as a 1000x1000 unit space with the origin in the center of the screen)
  • the skin of the agent, which describes what the agent should look like.
  • the name of the agent (this is a string for a particular agent in the sim)

All Agents also have some special properties that affect how they act in the sim:

  • the type of the agent (this is the name of the Blueprint used to create it) READ ONLY
  • the visible property sets whether the agent is visible on the screen
  • the active property sets whether the agent is 'alive' or 'dead'

In the design of the simulation engine, using a a built-in property should immediately cause a change in the system. For example, setting the x property.

Students can modify the Script inside their Blueprints to change these properties using simple conditional and assignment statements. Students can also define their own properties of the number, string, and boolean types to model their role in the system they are modeling.

Advanced Agent Capabilities

The basic properties allow Students to author basic simulation models that have a position on the screen (x and y properties) and a visual representation (skin and visible properties). However, Students will want to make more "interesting" behaviors that go beyond the basics, such as:

  • A visual appearance that has several different states, poses, or animations
  • A visual that "points" in a direction and has a rotation property
  • Agents that "follow" other agents and track them
  • Agents that can create new agents
  • etc.

While students can attempt to write these themselves by creating elaborate Script structures, it makes sense to provide these advanced capabilities within the simulation. We call these advanced capabilities "Features" or GFeatures, which are named collection of properties and code that can be added to a Blueprint.

The intent behind the "Feature" idea is to help students stay focused on their modeling assignment, not coding or computation. Many desirable behaviors of an agent that are simple to ask for are actually quite difficult to implement with simple properties. By defining Features, we also make it possible to develop the simulation engine in a modular, self-contained fashion.

Students add each Feature by name in their Blueprint. The set of Features that we know are in the simulation from the specification are:

  • Costume - adds the pose and animation properties and code to change them
  • Movement - adds the option of controlling the movement of an agent through direct input or a built-in behavior like "follow" or "wander".

We have also inferred additional Features that may be needed by students:

  • Population - allows agents to manage a population of agents by creating and killing them. The population is a single object in the system so it is possible to count the number of agents that are in it, detect certain thresholds, etc.
  • Timer - allows agents to do calculations and actions based on time measurement

Features are "added" to a Blueprint by name (e.g. Movement). The properties and methods that the Feature adds are used in the same way as built-in/student-defined ones with the addition of the FeatureName to indicate which property you are changing (like a "namespace" in modular code).

A Detour into Simulation Phases

To understand the following sections, it might be helpful to understand how the Simulation Engine is modeled.

Students can think of Agents as the focal point of their modeling. As deveopers, we need to define each "phase" of the simulation as it runs so we don't clobber values or use values that are out-of-date. To try to guarantee this, we define "Simuation Phases" that are controlled by the PhaseMachine controller defined in sim/api-sim-gameloop.js

Simulation Phases designate the order of operation to process all the Agents (defined by our GAgent class in lib/class-gagent.js) as well as conditions, drawing, GUI updates, etc. The general idea is to make sure each phase has the most recent data it needs to perform its own calculations.

Here's a simplified version of our Simulation Phases that run for every interation of the Simulation.

  • GET INPUTS reads all input states (e.g. from PTrack, keyboards, joysticks, annotation devices) and captures their state for use by subsequent phases. It is also when messages sent between objects are handled. It's guaranteed not to change for the remainder of the loop.
  • UPDATE calculates any automatic changes that happen, such as the passage of simulation time, updating of sprite and physics information, and any other periodic housecleaning necessary such as clearing buffers. Update only handles "dumb" updates of data, but does not analyze or perform an action.
  • THINK is when objects in the simulation that need "thinking time" to look at inputs and the freshly-updated state and decide what to do next. This is where we put AI-type behavior code, deciding what to do but not yet acting on it.
  • EXEC is the phase where simulation objects can change themselves OR send messages to other objects.
  • EVAL is used by overall simulation logic to test for "end of simulation" or "ending conditions met" and other derived data that is useful for "keeping score" and "enforcing rules" related to gameplay.
  • RENDER takes all the previous state and actualy draws something to the screen. This includes both objects in the simulation and other screen elements outside of it.

These phases are executed in the order they are defined, and completely finish before the next phase is allowed to run. Code modules "opt-in" to receiving notification from a phase for efficiency.

Agents in Simulation Phases

NOTE: The full list of simulation phases are defined in sim/api-sim-gameloop.js

Our Agents are implemented using the GAgent class, which maintains a list of all the GAgent instances that are in the world. In practice, developers adding new capabilities to the simulation engine would pick the appropriate phase(s) where they will insert their code.

A Tour of GAgent

In this section, "Agent" refers to the concept of an agent as students experience and model them. We use the term **GAgent**to refer to the GEMSTEP code that implements the concept of an Agent. Likewise, we use GFeature to refer to the GEMSTEP code that implements a particular "Feature" as described above.

The GAgent class (defined in lib/class-gagent) implement the notion of Agent. It is derived from a base class SM_Object (lib/class-sm-object) that implements the mechanism for managing a dictionaries of Agent Properties and of Agent Methods.

The purpose of SM_Object is to provide a common interface for retrieving properties and methods using the same access mechanism in our stack-based scripting engine. The SM in SM_Object stands for "Stack Machine", which is the name of our simple Javascript-based virtual machine.

If you are writing code to add new capabilities to GAgent, you'll be working with the following API methods to access and manipulate these data types with Javascript

  • script-accessible Agent Properties (named properties of type GVar )
  • script-accessible GVar Properties (yes, GVar has its own properties too)
  • script-accessible GVar Methods (yes, GVar has its own methods too)
  • GFeature-defined and script-accessible GFeature properties
  • GFeature-defined and script-accessible GFeature methods

Properties and GVar Types

A GVar is the implementation of an Agent Property. These are the ones created by students defining their Blueprints. There are current four basic types, defined in sim/vars/, and are all derived from the SM_Object base class that is shared with GAgent.

  • GVarString - implements a String + comparison/utility operations
  • GVarNumber - implements a Number + comparison/utility operations
  • GVarBoolean - implements a Boolean + comparison/utility operations
  • GVarDictionary - implements a Map of strings to other GVars

The reason that Agent Properties are represented by GVars instead of as regulat Javascript types is because we want students to be able to invoke property operations by name (e.g. prop 'foo' setTo 10) from a drop-down menu in the GUI wizard. Encapsulating types in this way allows us to expicitly define what can be made available to the scripting GUI.

Shared Capabilities between GVars and GAgents

The SM_Object class implements these critical class methods:

  • addProp( propName, gvar) - add a property of type gvar named propName
  • getProp( propName ) - return the gvar named propName
  • addMethod( methodName, tmethod ) - adds a tmethod named methodName, here tmethod is either a Javascript function OR an array of functions using the signature (agent,state)=>void (this is theTOpcode type defined in lib/t-script.d.ts)
  • getMethod( methodName ) - returns the tmethod associated with methodName, which is either a Javascript function OR an array of TOpcode.
  • getter/setter value that returns the 'value' of this object. This is used by GVar classes to return/set property values, and returns the name for GAgent instances.

The SM_Object properties/methods defined using these class methods are accessible by Scripts and therefore are accessible to students. Regular class methods are NOT accessible from Scripts, but can be made accessible when designing Script Keywords.

GAgent Class Capabilities

Like GVars, the GAgent class extends SM_Object with Agent-specific attributes.

  • Adds common Agent Properties:

    • x, y
    • name
    • skin
    • visible
    • active
  • Adds API for script execution in the context of the GAgent

    • exec( tmethod, context, ...args ) - runs the provided TMethod with the provided global context object, and initial arguments, returning the results as a Javascript object or value type. This is the main entry point for GAgent instance to run/evaluate several types of program produced by a compiled student Script.
    • helper evaluateArgs( args[], context ) - used to ensure an array of arguments contains only values by converting ExpressionAST into values.
  • Adds API for queueing messages to GAgent instances (inter-agent messaging)

    • queueUpdateMessage( sm_message ) - the main entry point for receiving events to execute during the AGENT_UPDATE simulation phase.
    • queueThinkMessage( sm_message ) - the main entry point for receiving events to execute during the AGENT_THINK simulation phase.
    • queueExecMessage ( sm_message ) - the main entry point for receiving events to execute during the AGENT_EXEC simulation phase.
  • Adds API for extending a GAgent with the capabilities of aGFeature

    • addFeature( featureName ) - adds the GFeature named in featureName to a list of GFeatures used by this GAgent instance
    • getFeature( featureName ) - retrieves the GFeature by name from this GAgent instance's GFeature list
    • featProp( featureName, propName ) - retrieves the GVar inside of the named GFeature named propName. Note that the definition of Agent Property GVar is distinct from Feature Property GVar. These are public properties and are accessible by Scripts using special syntax.
    • featVar( fname, privateVarName ) - retrieves a "private" property used by a GFeature internally, not intended for use by student script authors.
  • Adds Movement-specific Javascript properties that are used for GFeature development

    • controlled by a student inputmodePuppet
    • controlled by a code modeAuto
    • not controlled (doesn't move) modeStatic`
  • Adds Selection-specific Javascript properties used for determining how to display non-mutually-exclusive visual state in addition to the visible and active Javascript properties:

    • selected - this GAgent is the current selection
    • hovered - this GAgent is pointed-at but not acted-on yet with a click (mouse hover)
    • grouped - this GAGent is part of a group of GAgents
    • captive - this GAgent is being actively manipulated (drag and drop)

GAgents and the Simulation Engine

A GAgent instance can be thought of a "persistent memory context" for a mini-computer running many programs one-after-the-other. Each program (compiled from the Script and stored in compiled form in the Blueprint) can inspect, change, and execute properties and other code defined inside the GAgent instance. Much of the work performed by the Script Engine is managing each memory context and assuring that is is running the correct code defined by the script at the right time in the simulation loop. I

Another way of thinking about it is to think of a GAgent as a bag of numbers that are changed over time by programs scripted by students. Some of those numbers have a special meaning to the Simulation Engine, such as the skin property and the x, y coordinates. We can add additional special meaning through the GFeature system, without requiring students to create complicated technical features from scratch.

Technical Description (skip if you want)

The Simulation Engine maintains a list of Blueprints that students can use to create multiple instances in the world. Technically speaking, a Blueprint is a collection of subprograms that are run during specific phases of the Simulation Engine lifecycle. The job of the Script Compiler is to create that collection of subprograms from the Script provided by student and store it.

When the simulation starts, it has a list of every Agent that the student wants to put into the world by defining a list containing Blueprint name, the desired instance name, and a set of initial property values. The Simulation Engine creates each instance by going through the list and invoking the code stored in the Blueprint. The code receives the GAgent instance to operate on, as well as any starting parameters that are provided to change how that code behaves.

At initialization time, a plain GAgent instance is created. This instance is then fed the Blueprint.define and Blueprint.init programs, executing them immediately using GAgent.exec( program ) API method.

  • define programs the default GVar properties and GFeature capabilities that the student has chosen in their script
  • init programs the starting values for the GAgent instance. There are actually two initial sets of values; the default values defined in the Blueprint and the per-instance values that a student may want to set (e.g. giving a GAgent instance a specific name). We don't currently support the latter in the engine at this time, but it is an anticipated feature.

The Simulation Engine also injects programs into each GAgent instance to run during specific phases:

  • Blueprint.update runs during every AGENT_UPDATE phase
  • Blueprint.think runs during every AGENT_THINK phase
  • Blueprint.exec runs during every AGENT_EXEC phase

Additionally, the Simulation Engine injects Blueprint.condition programs into the global condition checking phase CONDITIONS_UPDATE. That is because this code doesn't run inside a GAgent even if it is defined inside its Blueprint. The when AgentA touches AgentB type clauses are an example of this, because they operate on the set of all instances of a Blueprint or pairs of instances of differing Blueprint types. Instead, the condition definition is copied from the Blueprint into a Global Conditions dictionary and is executed during CONDITIONS_UPDATE. A condition consists of a test program and the consequent program that runs if the test is true. The consequent is injected into every passing GAgent instance's execQueue so it is executed at the appropriate time.

Specific Simulation Phases

In the section A Detour into Simulation Phases we described a simplifed version of the Simulation Engine lifecycle. The actual phase names that are important for GAgent authoring are defined in sim/api-sim-gameloop and are as follows:

DURING SIMULATION STARTUP

  • LOAD_ASSETS - allows modules to asynchronously load images, data (e.g. sprite images) before running the next phase
  • RESET - tells modules that the simulation state is resettting so get ready for a new run
  • WAIT - tells modules that the simulation is waiting for all programs and data to be loaded
  • PROGRAM - tells modules to compile programs and data into Blueprints
  • INIT - tells modules that we're about to run the simulation, so prepare yourself
  • READY - tells modules that the simulation engine has initialized and is about to start ticking through the update cycle

DURING SIMULATION TICK

  • AGENTS_UPDATE - GAgent instances run their 'every frame' scripts

  • FEATURES_UPDATE- Features that need to update internal data structures that are shared by all GAgents that use it do so here

  • CONDITIONS_UPDATE - Scripted interactions between types of GAgent (e.g. A touches B) are evaluated. This is comprised of a TEST, its CONSEQUENT and an optional ALTERNATE. The results of tests are cached, while the CONSEQUENT or ALTERNATE is queued as a message to the affected agents's EXEC queue.

  • FEATURES_THINK - Features that need to make some decisions about GAgents it is tracking do so here, queuing an action to run during EXEC

  • AGENTS_THINK - GAgents that implement AI behaviors that are not student-scripted runs here, deciding what to do next and queuing that action.

  • FEATURES_EXEC - Features that need to run code on all affected agents would do so here.

  • AGENTS_EXEC - Queued commands from UPDATE and THINK execute here. Sources are global conditions

  • VIS_UPDATE - Create derived data for use in rendering display objects

  • VIS_RENDER - Draw to screen

A Tour of GFeature

Adding advanced Agent capabilities is done by creating a new GFeature in the sim/features directory and importing the feature into sim/sim-features to make it accesible to the simulation engine. This allows developers to extend the capabilities without touching the core GAgent source code.

A GFeature is implemented by extending the GFeature base class in lib/class-gfeature. There is a template named feat-template.ts that you can copy and rename. Instructions are included in that file's comments.

Theory of Operation

The GFeature class is similar to GAgent in that it has API methods to access script-accessible methods and properties, but uses different conventions.

  • Instead of extending the GAgent class directly, GFeature modules work by "decorating" existing instances of GAgent as they are created from a Blueprint.
    • This adds properties specific for the GFeature to operate in the GAgent instance
    • This also adds a "hook" for the GAgent instead fo look-up the GFeature module and run its variou methods
  • To access script-accessible properties defined by a GFeature, use the GAgent API methodfeatGetProp(featName,propName) instead of getProp(propName)
  • To access script-accessible methods defined by a GFeature, use the GAgent API method featExec(featName, methodName, ...args) to invoke it.

The GFeature base class in lib/class-gfeature contains utility class methods to help connect your special-purpose code to a GAgent instance. If you look at sim/features/feat-template you will see that there are a few main things you must do:

  1. define your the script-accessible methods in the constructor after calling super()
  2. define your GFeature -specific GVar properties in the decorate(agent) function, which is called by the Simulation Engine when creating instances of GAgent from a Blueprint that defines the use of a GFeature

Modular Programming with GFeature

In the simplest case, you can use GFeature to add any code in Javascript that does something interesting by reading/modifying existing GAgent properties and reacting to them. However, you do also need to know WHEN and HOW to run your code during the correct Simulation Phase otherwise you will destabilize the simulator.

we'll develop this section further as we get deep into adding new capabilities to GEMSTEP in 2021

⚠️ **GitHub.com Fallback** ⚠️