Get SpatialOS

Sites

Menu

Schemalang reference

This page is a reference to schemalang, used to write schema files.

For information about how to design what goes into components, see Designing components.

Directory structure and files

The schema for a world is stored in the schema subdirectory of a project, using ‘schema files’ (files with the .schema extension).

Package definition

Each schema file must have a package definition at the top (eg package foo.bar;).

You can arrange the files in arbitrary directory structures in the schema directory. For example, you could put the code for the package foo.bar in the directory schema/foo/bar.schema.

Comments

Schemalang supports both // line comments and /* */ block comments.

Components

Define components using the component keyword.

Components contain:

  • (required) an explicit component ID
  • (optional) properties (with a type, a name, and an explicit property ID)
  • (optional) events
  • (optional) commands

An example component:

package improbable.example;

component Health {
  id = 1234;
  uint32 current_health = 1; // a property
  uint32 max_health = 2; // another property
}

IDs

Component IDs must be unique across the entire schema (including library dependencies with their own schemas). Property IDs must be unique within the component.

These two types of IDs are essential for backward compatibility: they let you change the schema without breaking existing snapshots.

Component IDs below 100 and from 190000 to 199999 are reserved for Improbable use.

Component events

Define an event using the event T name; syntax. For example:

package improbable.example;

type SwitchToggled {
  int64 time = 1;
}

component Switch {
  id = 1234;
  bool is_enabled = 1;
  event SwitchToggled toggled;
}

The above example allows the component to trigger a toggled event that contains a time field. Other workers can react to this event.

Component commands

Define a command using the command keyword as follows:

package improbable.example;

type DamageRequest {
  uint32 amount = 1;
}

type DamageResponse {}

component Health {
  id = 1234;
  uint32 health = 1;
  command DamageResponse damage(DamageRequest);
}

Commands require both a request and a response type. The request type is the data that is sent to the worker with write access to the component. The response type is sent back to the worker that issued the command.

Types

Primitive types

The primitive types available are:

Syntax Type Notes
bool Boolean True or false.
uint32, uint64 Unsigned integer Variable-length encoding; smaller values use fewer bits.
int32, int64 Signed integer Variable-length encoding; smaller values use fewer bits. Negative values are represented in the usual two’s-complement manner, and so use the maximum number of bits.
sint32, sint64 Zig-zag signed integer Variable-length zig-zag encoding; smaller absolute values use fewer bits. More space-efficient than int32 or int64 when values are likely to be negative.
fixed32, fixed64, sfixed32, sfixed64 Fixed-width integer Fixed-width encoding (always 4 or 8 bytes depending on type); more space-efficient when values are likely to be very large.
float, double Floating-point
string, bytes String of characters or bytes Strings should always be either ASCII or UTF-8.
EntityId ID of an entity A special version of int64 used to store the ID of a SpatialOS entity. IDs are > 0.
improbable.Coordinates, improbable.Vector3d, improbable.Vector3f Coordinates represents positions in space, while the other two represent vectors such as velocity. Both Coordinates and Vector3d are 3D vectors of doubles, representing absolute positions and differences between positions in 3D space, respectively. Vector3f is a 3D vector of floats.

To use these types, include import "improbable/standard_library.schema"; in your schema file.

The following types are currently not supported in the Unreal SDK due to Blueprint limitations:

uint32, uint64, int64, sint32, sint64, fixed32, fixed64, sfixed32, sfixed64, double

User-defined types

You can define and reuse custom types, using the type keyword. Custom types consist of field definitions which look exactly the same as in components.

You can also define types within the scope of an outer type, to make the types to similary nested in generated code. Refer to nested types from elsewhere by writing out the path (perhaps relative to the current scope) with dots as separators.

Here is an example:

package improbable.example;

import "improbable/vector3.schema";

type Movement {
  Vector3d target_position = 1;
  Vector3d target_direction = 2;
}

type Foo {
  type Nested {
    int32 nested_int = 1;
  }

  int32 foo_int = 1;
  double foo_double = 2;
}

type Bar {
  type Nested {}

  Foo foo = 1;
  // Resolves to Bar.Nested
  Nested bar_nested = 2;
  // Resolves to Foo.Nested
  Foo.Nested foo_nested = 3;
}

Again, field IDs must be unique within the type.

Enumerations

You can define enumerations and use them like built-in types. Like types, enums can be defined within the scope of an outer type.

Example:

package improbable.example;

enum Color {
  RED = 0;
  GREEN = 1;
  BLUE = 2;
}

type ColorData {
  // enum Color could be defined inside this scope, too!
  Color color = 1;
}

Collection types

The collection types available are:

  • An option<T> represents either no value (empty) or a single T value.
  • A list<T> represents zero or more T values.
  • A map<K, V> represents a map from keys of type K to values of type V.

Collection type fields can be transient.

Example:

package improbable.example;

type SomeDataType {
  option<int32> an_integer = 1;
  list<SomeDataType> more_data = 2;
  map<EntityId, EntityId> entity_to_entity_map = 3;
}

You can’t create nested collections (like lists of lists, maps of lists, lists of options, and so on) directly. Use wrapper types instead:

package improbable.example;

type Data {
  type InnerList {
    list<int32> value = 1;
  }
  list<InnerList> list_of_list = 1;
}

Importing types

Schema files can use types defined in other schema files by importing the files. For example, if a file called foo/bar.schema contains the following:

package foo.bar;

type Baz {}

Another schema file can use the type Baz like this:

package improbable.example;
import "foo/bar.schema";

component BazComponent {
  id = 1234;
  foo.bar.Baz baz = 1;
}

The path of the file is relative to the schema directory (either the schema directory of the SpatialOS project, or the schema directory of some other library dependency).

To avoid ambiguity when working with packages sharing similar names, you can prefix references to user-defined types with a dot, to indicate a fully-qualified name (including package). For example, to refer to Baz above, you could write .foo.bar.Baz.

Naming

Names of properties, events and commands must be in lowercase_with_underscores. Names of types (components, user-defined types and enumerations) must be in UpperCamelCase.

Advanced components

Reusable data types

To allow multiple components to share a data type, instead of defining a property inline, components can reference an external user-defined type for a property. Use the syntax data T; as follows:

package improbable.example;

type SomeData {
  int32 value = 1;
}

component SomeComponent {
  id = 1234;
  data SomeData;
}

component AnotherComponent {
  id = 1235;
  data SomeData;
}

You can’t mix this syntax with in-line property definition, or combine multiple data types this way. For example, the following definitions are not valid and won’t compile:

package improbable.example;

type SomeData {
  int32 value = 1;
}

component ThisComponentWontCompile {
  id = 1234;
  data SomeData;
  int32 extra_property = 2;
}

type OtherData {
  int32 value = 3;
}

component ThisComponentAlsoWontCompile {
  id = 1337;
  data SomeData;
  data OtherData;
}

Transient fields

Collection type fields can be marked transient. The data in these fields won’t be saved in snapshots taken from a deployment, or loaded from a snapshot at the start of a deployment.

This makes clearing per-deployment state in snapshots easier. For example, a list of unprocessed player moves might be needed in a deployment but it is unlikely to be something that needs to be persisted across deployments. Similarly, you may want per-deployment player abilities and a per-deployment score. By marking the fields transient, as below, you can take a snapshot and start a deployment from it without having to manually clear the fields.

package improbable.example;

component PlayerState {
  id = 1234;
  transient list<Move> moves_to_process = 1;
  transient map<AbilityName, Ability> active_abilities = 2;
  Score score = 3;
}

type Score {
  transient option<int32> deployment_score = 1;
  int32 alltime_score = 2;
}

For entities that don’t need to be persisted across deployments, use the Persistence component.

Syntax highlighting plugins

There are several community projects for schema syntax highlighting:

Was this page helpful?

Thanks for letting us know!

Thanks for your feedback

Need more help? Ask on the forums