Skip to content

The Parameter

Connor Jakubik edited this page Jan 20, 2025 · 16 revisions
The Fundamentals → The Parameter

Up to date for Platform 0.28.0

Written by Connor Jakubik.


Param Basics

What is a Param?

A Param (short for Parameter) is a general holder of data that can be any of the Space Teams compatible types. The Space Teams Platform uses the Param at the core of pretty much everything dealing with simulation data that changes through time. In order to build out a highly performant multiuser simulation platform, we've built out our own data structures around Params that handle thread-safety and communicate Param value changes in real time to all connected computers.

Param Types

This is the list of Space Teams valid Parameter types:

  • bool (true/false)
  • Integer Variants
    • The maximum value of each of these before overflow is 2^(number) - 1.
    • Signed types have half the range but can be negative.
    • unsigned (positive only)
      • uint8
      • uint16
      • uint32
      • uint64
    • signed
      • int8
      • int16
      • int32 (most common general-use integer)
      • int64
  • duration
    • microsecond (1E-6 s) precision
  • timestamp
    • microsecond (1E-6 s) precision
    • Using the TAI time zone, which is like UTC but without leapseconds (ahead by 37 seconds as of 2023/01/25).
  • uuid (Universally Unique ID)
  • EntityID (not commonly used)
  • EntityRef
    • Explained lower on this page.
  • float (32-bit fractional number)
  • double (64-bit fractional number)
  • char (ASCII 1-byte character)
  • Vectors
    • doubleV2 (2D vector)
    • doubleV3 (3D vector)
    • doubleV4 (4D vector, sometimes quaternion)
  • Matrices
    • doubleM2x2 (2x2 matrix)
    • doubleM3x3 (3x3 matrix)
    • doubleM4x4 (4x4 matrix)
    • doubleM5x5 (5x5 matrix)
    • doubleM6x6 (6x6 matrix)
  • string (text in ASCII 1-byte characters)
  • array
    • This is a special param type, which is a dynamic-size array of a certain other param type.
  • map
    • Explained lower on this page.
  • See more details where SCVarType is mentioned in CoreSCTypes.h.
    • enum SCVarType defines the whole list of Param types
    • SCVarTypeToName shows the mapping between SCVarType enum values and their corresponding labels (which are what is used to define a param type on the human-facing parts of the Platform)
    • SCVarTypeToName_CppFormat shows the mapping between SCVarType enum values and their corresponding C++ class (which is what you use for the C++ template argument)

GenParam

Params are implemented in C++ through the GenParam class (GenParam.h). GenParam objects use polymorphism to cleanly handle holding a "generic" value. All GenParam objects have a common set of shared functionality, which means they can be handled in a common way externally.

A few of the features GenParam ensures exist for all param types:

  • Store and check assumed param type when accessed.
  • Store a timestamp for the simulation-time when the param was last modified.
  • Lots of raw memory manipulation stuff necessary for network socket communication.
  • Print a string showing the value of the parameter.

GenParams use C++ template (function<something>(....)) functions, which means the programmer specifies the exact param type using the corresponding C++ class for any use of a Param function. This can be done as follows.

GenParam<double>* my_param = new GenParam<double>(7.8);

It is possible to construct a param without specifying the initial value, but it's always best to pass in an initial value when possible. Once a GenParam has been constructed, you can retrieve its data as follows.

double x = my_param->Get();

You can forget the above GenParam syntax; it is HIGHLY likely that you won't ever run into it again. This is because the Platform presents Params only through accessor functions on the objects that hold params: GenParamMap and its subclasses like Entity and System.

GenParamMap

So far we've made a fancy way to tack on extra functionality to a normal C++ class. To start making structured data with real-world significance, we need a way to organize Param values with names and groupings. The data structure suitable for this task is a dictionary (C++: map, Python: dict, JSON: object). A dictionary maps key-values to value-values, often with unique keys.

Here's an example of a dictionary (in this case, a JSON object):

{
  "name": "John", 
  "age": 30, 
  "single": true
}

GenParamMap (ParamMap.h) is pretty much a C++ map<string, GenParam> with a bunch of extra functionality. On top of mapping string keys to GenParam values, GenParamMap provides an interface of HasParam/GetParam/SetParam/AddParam/DeleteParam functions that handle the "hard parts" of error-handling, thread-safety, change-tracking, and more. Using a GenParamMap or one of its subclasses, you can read and modify parameter values as well as handle the "extra work" required to implement higher-level features such as replicating changes to all other computers in a simulation.

Most of the time, you'll be using GenParamMaps subclasses (like Entity or System), but all of them have the same Param interface. Here is an example of this interface in action. We assume here that there is an already constructed GenParamMap "pmap" that we can access with the pointer GenParamMap* pmap.

// C++
// Get (a copy of) the value of the Mass param, and
// save that to an intermediate variable.
double mass = pmap->GetParam<double>("Mass");
// Find a new mass value through some calculation.
double new_mass = 0.9 * mass;
// Set the Mass param to the new value
pmap->SetParam<double>("Mass", new_mass);

// Add a new parameter
bool is_heavy = true;
pmap->AddParam<bool>("IsHeavy", is_heavy);

// Use a parameter if it's there
if(pmap->HasParam<SC_DoubleV3>("SomeVector"))
    doSomething(pmap->GetParam<SC_DoubleV3>("SomeVector"));
# Python
# Get (a copy of) the value of the Mass param, and
# save that to an intermediate variable.
mass = pmap.GetParam(st.VarType.double, "Mass")
# Find a new mass value through some calculation.
new_mass = 0.9 * mass
# Set the Mass param to the new value
pmap.SetParam(st.VarType.double, "Mass", new_mass)

# Add a new parameter
is_heavy = True
pmap.AddParam(st.VarType.bool, "IsHeavy", is_heavy)

# Use a parameter if it's there
if pmap.HasParam(st.VarType.doubleV3, "SomeVector"):
    doSomething(pmap.GetParam(st.VarType.doubleV3, "SomeVector"))

ParamMap usage in the Platform

GenParamMap is the base class for many Platform classes:

  • Entity
    • Object in the running simulation, represented by Param values.
    • There is significance to certain parameter values that is automatically handled by some Platform applications to render or interact with the Entity in a certain way.
      • For example, the Location parameter is used to communication the location of the entity relative to some reference frame. This is used to update the graphical representation of that entity.
    • Entities use the change-tracking functionality of GenParamMap to detect changes, additions, and deletions of Params, and
  • EntityConfig
    • A ParamMap that represents the "initial values" of an Entity's parameters.
    • An EntityConfig object can be used to spawn an Entity with matching initial Param values during a running simulation.
    • An EntityConfig that has been saved to a file can be used in a SimConfig as a "Template" (starting point for defining an Entity).
    • During a running simulation, code somewhere in the Platform can spawn an Entity with its initial Param values coming from an EntityConfig.
  • System
    • Logic that reads and modifies Entity parameters in its own update loop.
    • Many of these make up all the "behavior" of a simulation.
    • Usually uses Params for configuration settings or EntityRef references.
    • Further explained in another wiki doc.
  • SystemConfig
    • A ParamMap that represents the "initial values" of a System's parameters.

Entity Params

The C++ code from earlier can be directly reused for the example of accessing and modifying parameters on an Entity:

// C++
// Get (a copy of) the value of the Mass param, and
// save that to an intermediate variable.
double mass = entity->GetParam<double>("Mass");
// Find a new mass value through some calculation.
double new_mass = 0.9 * mass;
// Set the Mass param to the new value
entity->SetParam<double>("Mass", new_mass);

// Add a new parameter
bool is_heavy = true;
entity->AddParam<bool>("IsHeavy", is_heavy);

// Use a parameter if it's there
if(pmap->HasParam<SC_DoubleV3>("SomeVector"))
    doSomething(entity->GetParam<SC_DoubleV3>("SomeVector"));
# Python
# Get (a copy of) the value of the Mass param, and
# save that to an intermediate variable.
mass = entity.GetParam(st.VarType.double, "Mass")
# Find a new mass value through some calculation.
new_mass = 0.9 * mass
# Set the Mass param to the new value
entity.SetParam(st.VarType.double, "Mass", new_mass)

# Add a new parameter
is_heavy = True
entity.AddParam(st.VarType.bool, "IsHeavy", is_heavy)

# Use a parameter if it's there
if entity.HasParam(st.VarType.doubleV3, "SomeVector"):
    doSomething(entity.GetParam(st.VarType.doubleV3, "SomeVector"))

The difference for using Entity instead of GenParamMap is the Entity has utility code in the Platform core that automatically replicates the Param value changes from SetParam<double>("Mass" and AddParam<bool>("IsHeavy" to other computers in the multiuser simulation. So, any param-changing code only needs to run on one computer, and the Platform core netcode keeps every parameter value in sync across the internet by sending change data to other users.

At this point, the Platform's ParamMap-based data structures are starting to resemble more enterprise database software than some normal simulation data structure.

System Instance Params

Systems have parameters too! Their Param functions prefix Inst in front of Param in order to better distinguish them from the Entity ones. This may change, though; param functions without Inst already work the same.

// C++
void SomeSystem::init()
{
    if(GetInstParam<bool>("WaitDuringInit"))
    {
        sc_sleep_microsec(1000000);
    }
}
# Python, in a ST Python System

this = st.GetThisSystem()

if this.GetParam(st.VarType.bool, "WaitDuringInit"):
    time.sleep(1.0)

Notably, Systems do NOT have their param changes replicated to any other computers. This is because Systems are each only supposed to run on one computer in the multiuser simulation.

Nested Param

For organization purposes and other reasons, GenParamMap supports nesting. It does this by itself being another GenParam subclass.

In order to access parameters that aren't at the top level of the ParamMap, use Nested param accessors, which just change out the single key argument (param_key_t) for a keys list (nested_param_key_t), which is easy to create with List Initialization as demonstrated below.

// C++
// Get the mass
double mass = entity->GetParam<double>({"Dynamics","Mass"});
// Reduce the mass
double new_mass = 0.9 * mass;
// Set this new value
entity->SetParam<double>({"Dynamics","Mass"}, new_mass);

// Use a parameter if it's there
if(entity->HasParam<SC_DoubleV3>({"This","That","TheOther","SomeVector"}))
    doSomething();

// Add a new parameter
bool is_heavy = true;
entity->AddParam<bool>({"n1","n2","n3","n4","n5","IsHeavy"}, is_heavy);
// AddParam will generate any missing keys in the nested path if needed.

// Nested Param functions will not compile when you are using initializer-list and there is only one element.
entity->AddParam<bool>({"IsHeavy_2"}, is_heavy); // This errors!
entity->AddParam<bool>("IsHeavy_2", is_heavy); // Use this instead in this case.
# Python (shortened)
# Python nested keys are just a list of keys:
mass = entity.GetParam(st.VarType.double, ["Dynamics","Mass"])
if entity.HasParam(st.VarType.doubleV3, ["This","That","TheOther","SomeVector"]):
    doSomething()

EntityRef

An EntityRef parameter is basically a wrapper around a pointer to an Entity, which can be used across the network like any other parameter. Use the SC_EntityRef type as the type for param Get/Set/Add. SC_EntityRef wraps a non-owning Entity::ref (std::weak_ptr), so you'll need to .lock() it to get an owning Entity::owned (std::shared_ptr). Setting or Adding an EntityRef requires passing a SC_EntityRef as input. You can construct a SC_EntityRef by passing an Entity::owned or Entity::ref value into a SC_EntityRef constructor.

Usage example:

// C++
// "SomeOtherEntity" is the key for an EntityRef param holding a reference to the "other" entity.
// Notice how the key of the param does not necessarily correspond with the "other" entity's name.
Entity::owned other_en = entity->GetParam<SC_EntityRef>("SomeOtherEntity").lock<Entity>();
//NOTE: can also omit <Entity> portion if only need access to Entity_Base functions

// Get (a copy of) the value of the Mass param, and
// save that to an intermediate variable.
double mass = other_en->GetParam<double>("Mass");
# Python
# "SomeOtherEntity" is the key for an EntityRef param holding a reference to the "other" entity.
# Notice how the key of the param does not necessarily correspond with the "other" entity's name.
other_en = entity->GetParam(st.VarType.EntityRef, "SomeOtherEntity")
#NOTE: No need to .lock() in python; we have manipulated the Python API functions to do .lock() internally.
# This could result in an exception if the referenced entity has been deleted from the sim.

# Get (a copy of) the value of the Mass param, and
# save that to an intermediate variable.
mass = other_en.GetParam(st.VarType.double, "Mass")

ParamArrays

Creating Dynamic Arrays

You can create dynamically-sized arrays of GenParams using the special GenParamArray class. The C++ type for these ParamArrays is a std::vector<T> where T is the corresponding C++ type of the array element objects. These arrays must be of a homogenous param type, and support static- or dynamic-sized types. Some Param types may be unsupported for ParamArrays, especially soon after a new param type is added.

Getting a ParamArray from a GenParamMap

As with the GenParams, you'll almost always be using them as part of a GenParamMap. As with before, there are Get/Set/Add functions to help out with this. Here is an example using all of these, assuming we already have an GenParamMap* pmap pointer available to us.

// C++
// Get a dynamic array
std::vector<double> my_array = pmap->GetParamArray<double>("MyArray");
// Add a new value to the returned array
my_array.push_back(42.0);
// NOTE: This DOES NOT modify the "MyArray" parameter on the param map
// To do that, we need to set the new value
pmap->SetParamArray<double>("MyArray", my_array);
// Add a new array to the param map
std::vector<bool> new_array = { true, false, false, true };
pmap->AddParamArray<bool>("NewArray", new_array);
// pmap now has an array at key "NewArray" with value [ true, false, false, true ]
# Python
# Get a dynamic array (will by a python List object)
my_array = pmap.GetParamArray(st.VarType.double, "MyArray")
# Add a new value to the returned array
my_array.append(42.0)
# NOTE: This DOES NOT modify the "MyArray" parameter on the param map
# To do that, we need to set the new value
pmap.SetParamArray(st.VarType.double, "MyArray", my_array)
# Add a new array to the param map
new_array = [ True, False, False, True ]
pmap.AddParamArray(st.VarType.bool, "NewArray", new_array)
# pmap now has an array at key "NewArray" with value [ true, false, false, true ]


Advanced

Creating an empty Param Map

Creating a fresh Param Map is necessary for a few tasks such as generating a payload for the global DispatchEvent() function. Note that this is not the same as manipulating parameters on an Entity, as the standalone Param Map does not replicate changes to anything in a Space Teams sim.

// C++
auto my_pmap = std::make_shared<GenParamMap>();
# Python
my_pmap = st.ParamMap()

Clone this wiki locally