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.
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 typeTOpcode
inlib/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.
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.
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
andy
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.
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).
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. fromPTrack
, 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.
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.
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 useGFeature
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-accessibleGFeature
properties -
GFeature
-defined and script-accessibleGFeature
methods
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 otherGVars
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.
The SM_Object
class implements these critical class methods:
-
addProp( propName, gvar)
- add a property of typegvar
namedpropName
-
getProp( propName )
- return thegvar
namedpropName
-
addMethod( methodName, tmethod )
- adds atmethod
namedmethodName
, heretmethod
is either a Javascript function OR an array of functions using the signature(agent,state)=>void
(this is theTOpcode
type defined inlib/t-script.d.ts
) -
getMethod( methodName )
- returns thetmethod
associated withmethodName
, which is either a Javascript function OR an array ofTOpcode
. - getter/setter
value
that returns the 'value' of this object. This is used byGVar
classes to return/set property values, and returns the name forGAgent
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.
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 providedTMethod
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 forGAgent
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 convertingExpressionAST
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 theAGENT_UPDATE
simulation phase. -
queueThinkMessage( sm_message )
- the main entry point for receiving events to execute during theAGENT_THINK
simulation phase. -
queueExecMessage ( sm_message )
- the main entry point for receiving events to execute during theAGENT_EXEC
simulation phase.
-
-
Adds API for extending a
GAgent
with the capabilities of aGFeature
-
addFeature( featureName )
- adds theGFeature
named infeatureName
to a list ofGFeatures
used by thisGAgent
instance -
getFeature( featureName )
- retrieves theGFeature
by name from thisGAgent
instance's GFeature list -
featProp( featureName, propName )
- retrieves theGVar
inside of the namedGFeature
namedpropName
. Note that the definition of Agent PropertyGVar
is distinct from Feature PropertyGVar
. These are public properties and are accessible by Scripts using special syntax. -
featVar( fname, privateVarName )
- retrieves a "private" property used by aGFeature
internally, not intended for use by student script authors.
-
-
Adds Movement-specific Javascript properties that are used for
GFeature
development- controlled by a student input
modePuppet
- controlled by a code
modeAuto
- not controlled (doesn't move) modeStatic`
- controlled by a student input
-
Adds Selection-specific Javascript properties used for determining how to display non-mutually-exclusive visual state in addition to the
visible
andactive
Javascript properties:-
selected
- thisGAgent
is the current selection -
hovered
- thisGAgent
is pointed-at but not acted-on yet with a click (mouse hover) -
grouped
- thisGAGent
is part of a group ofGAgents
-
captive
- thisGAgent
is being actively manipulated (drag and drop)
-
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.
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 defaultGVar
properties andGFeature
capabilities that the student has chosen in their script -
init
programs the starting values for theGAgent
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 aGAgent
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 everyAGENT_UPDATE
phase -
Blueprint.think
runs during everyAGENT_THINK
phase -
Blueprint.exec
runs during everyAGENT_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.
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'sEXEC
queue. -
FEATURES_THINK
- Features that need to make some decisions about GAgents it is tracking do so here, queuing an action to run duringEXEC
-
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 fromUPDATE
andTHINK
execute here. Sources are global conditions -
VIS_UPDATE
- Create derived data for use in rendering display objects -
VIS_RENDER
- Draw to screen
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.
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 ofGAgent
as they are created from a Blueprint.- This adds properties specific for the
GFeature
to operate in theGAgent
instance - This also adds a "hook" for the
GAgent
instead fo look-up theGFeature
module and run its variou methods
- This adds properties specific for the
- To access script-accessible properties defined by a
GFeature
, use theGAgent
API methodfeatGetProp(featName,propName)
instead ofgetProp(propName)
- To access script-accessible methods defined by a
GFeature
, use theGAgent
API methodfeatExec(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:
- define your the script-accessible methods in the constructor after calling
super()
- define your
GFeature
-specificGVar
properties in thedecorate(agent)
function, which is called by the Simulation Engine when creating instances ofGAgent
from a Blueprint that defines the use of aGFeature
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