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 19000 to 19999 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 authority 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. |
Entity |
A component set | A data structure that represents an arbitrary collection of components. |
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/vector3.schema"; for Vector3f and Vector3d , and import "improbable/standard_library.schema" for Coordinates , in your schema file. |
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 type
s, enum
s 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 singleT
value. - A
list<T>
represents zero or moreT
values. - A
map<K, V>
represents a map from keys of typeK
to values of typeV
.
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.
Annotations
Schemalang supports annotating schema definitions with an instance of any user-defined schema type. For example, you can annotate enums, events or commands. These annotations do not affect generated code for the Worker SDK; however, they are available in the JSON representation of schema, for use in custom code generation. The following shows all the schema definitions that you can annotate.
package improbable.example;
type SomeAnnotation{}
// Note: You can use either SomeAnnotation or SomeAnnotation() for an empty schema type.
[SomeAnnotation] // Annotating the enum definition 'Color'.
enum Color {
[SomeAnnotation()] // Annotating the enum value definition 'RED'.
RED = 0;
}
[SomeAnnotation] // Annotating the schema type 'SomeDataType'.
type SomeDataType {
// Note: You can use a type for annotation regardless of where the type is declared.
[Nested(1)] // Annotating the schema type 'Nested' with itself.
type Nested {int32 a = 1;}
[SomeAnnotation] // Annotating the field 'some_field'.
option<int32> some_field = 1;
}
[SomeAnnotation] // Annotating the component 'SomeComponent'.
component SomeComponent {
id = 120; // Note: You can't annotate component IDs.
[SomeAnnotation] // Annotating the field 'some_field'.
SomeDataType some_field = 1;
[SomeAnnotation] // Annotating the command 'some_command'.
command SomeDataType some_command(SomeDataType);
[SomeAnnotation] // Annotating the event 'some_event'.
event SomeDataType some_event;
}
[SomeAnnotation] // Annotating the component 'SomeOtherComponent'.
component SomeOtherComponent {
id = 121;
data SomeDataType; // Note: You can't annotate the keyword 'data'.
}
Syntax
The example above showed a user-defined type without fields being used as an annotation. However, you can also use a user-defined type with fields.
- Each field must be instantiated with a value of its corresponding type.
- Fields can be named or unnamed, for example
SomeType(field_a = 1)
orSomeType(1)
. - An instance of a type cannot have a mix of named and unnamed fields.
- In the case that fields are unnamed, they are expected to be given in the order declared in the schema type definition.
- Note that field IDs do not affect the expected ordering.
The following is an example of more complex schema types:
enum SomeEnum {
FOO = 1;
}
type ComplexType {
type Nested {int32 a = 1;}
bool bool_value = 1;
int32 int_value = 2;
float float_value = 3;
string string_value = 4;
bytes bytes_value = 5;
EntityId id_value = 6;
Nested type_value = 7;
SomeEnum enum_value = 8;
}
type Collections {
option<int32> option_value = 1;
list<int32> list_value = 2;
map<string, int32> map_value = 3;
}
// Note: Type names are interpreted according to the current scope,
// though you can also use fully-qualified names.
[ComplexType(true, 32, 5.0, "foo", "bar", 5, ComplexType.Nested(50), SomeEnum.FOO)]
type AnnotatedType1 {}
// Note: '_' is used for an empty option.
[Collections(option_value = _, list_value = [], map_value = {})]
type AnnotatedType2 {}
[Collections(option_value = 1, list_value = [1, 2], map_value = {"foo":1, "bar":2})]
type AnnotatedType3 {}
Syntax highlighting plugins
There are several community projects for schema syntax highlighting: