racer home

Onyx Scripting Language

 

Home Extend beyond the boundaries.


Dolphinity Organiser - free planning, project management and organizing software for all your action lists

Introduction

Racer v0.8.39 started having the first incantation of Onyx. Onyx is a highly structured scripting language which will be used to glue the C++ world and scripting together. Since the scripts can be compiled during execution of Racer, the engine can be enhanced with user-defined logic without needing extra coding support inside the Racer engine itself (which is written in C++). This allows people to add logic to parts of the engine, where supported.

Why logic? Because data (parameters) can only go so far; being able to add logic (or code) will increase flexibility in ways that are as yet unforeseeable.

Onyx itself is merely a part of a bigger architectural feature; a game engine where you can define logic & data to create new entities, that behave in a certain way. The architecture for this is a Component-Entity system, and may represent a bit of a learning curve, but it is efficient in rapid prototyping and re-use of code. More information on the (higher level) component entity system can be found here. The Racer you know, with cars participating in a race, can be extended or even replaced entirely with another type of game.

There are 3 different types of textual interfaces/languages present in Racer; console commands (simple stuff like 'volume 15'), car/track scripting (named QScript; more friendly syntax with support for variables and a few types like cars and textures) and Onyx. Onyx is the most complex language of the 3, but also the most flexible. Features of this language are/will be:

An example piece of code to illustrate the looks

#include "racer.oxh"   // Racer engine integration (loaded from data/scripts/onyx/include/racer.oxh)

int t;

void main()
{
  while(1)
  {
    t=t+1;
echo(t); yield;
if(t>=10)
break; } echo(123);
}

Tutorials

You can read on below this list of tutorials, but you may want to jump in immediately and see some code.

Tutorial Description
Hello World Your first Onyx program.
UDP networking Sending and receive UDP network packets.
Open door/trunk Opening car doors/trunks within a script.
Scripted controller Writing your own controller script.
Main game script Explains data/scripts/onyx/main.oxs and its callbacks. Mostly an alternative to using simpler console files (data/scripts/*.rex) like oncar.rex.
Ini files Working with .ini files (such as racer.ini)
Shared variables Global variables that are the same across all scripts, and can be synchronized over the network.

If you're coming from the C++ world, the Onyx vs C++ article will be interesting.

Files involved

Onyx scripts have the extension .oxs (ox=Onyx, s=script). A compiled (thus 'executable') Onyx script has the extension .oxx. Compilation can be done implicitly; the engine will accept either .oxs or .oxx files and compile on the fly to .oxx for simplicity. Include files normally use the extension .oxh instead. They are parsed the same as the .oxs files, but the extension makes it clear the code may be shared amongst other scripts. Normally these .oxh files only contain interface definitions; constants, engine functions and so forth.

The default path where Onyx scripts are stored is data/scripts/onyx (relative to the Racer directory). Entering the console command 'run test.oxs' for example will try and find (and run) data/scripts/onyx/test.oxs.

Background information

You can skip this part if you want.

The name 'Onyx' is derived from the SGI Onyx machines from back in the day. These machines did a lot of things we've only now just seeing happening in regular PC's. Also, Onyx just sounds cool.

The language looks like C++; this may be considered a negative point, but the goal of this language is to be able to handle medium-difficult tasks. For such things, having a highly structured language helps in the end. Whereas console commands are lowest and easiest form of scripting in Racer, it is also the least flexible when you want to add custom behavior.

Goals of the language

Onyx has been setup with these goals in mind:

That said, Onyx is a bit like Unreal's scripting functionality, or vertex/fragment shaders that sit at specific points in a graphics card's pipeline.

Keywords

Here is a list of reserved keywords. You should avoid any keywords that are familiar from other languages, mostly C++.

Group

Keyword

Description
Flow control if if(expr)statement1 [else statement2]
  break Break out of the nearest loop
  loop Infinite loop (like C++'s while(1) or for(;;) )
  do while do <statement> while <expr>;
  for(e;e;e)s For loop, i.e. for(i=0;i<10;i++)echo(i);
Process control wait Waits for an event; see the internal functions below. This is a crucial difference to C++; scripts are supposed to wait for events often. Racer will continue running when all scripts are waiting for some event. An infinitely looping script will currently stall Racer's execution indefinitely.
  yield

Halts the script until an event occurs. In Racer v0.9.0RC5, this event is hardcoded as a frame render. The future should see more flexibility. This presents the option of coroutines, where is a script is run, seemingly in parallel with the gameplay. That is useful for sequenced events (like doing something, wait a second or two, then do another thing) without using complex state checking every frame on in which part the script is running.

Technically, a script that uses yield is forked onto another SPU (script processing unit). 2 SPU's are then running; the original one as the main 'thread' of the script, the second SPU as the one in the yielded part. The original one doesn't run anymore, but the 2nd one does. The SPU's will share code (which is read-only anyway), but also data, but not register state. In practice, this means a script defining a variable like 'float speed' will use the same 'speed' variable for both SPU's; they won't each have their own copy. To the script programmer, it will appear as if the yielded code is running in the same 'environment' as before issueing the yield.

In the future, it may be possible to really spawn a new SPU from a script, given the spawned thread its own set of data. Remember that a virtual machine consist of loaded scripts (compare that to Windows DLL's) and SPU's which run scripts (more than 1 SPU can be running a single script). There is not a one-to-one relationship of SPU vs script, more a one-to-many (1 script running on zero or more SPU's). A script therefore cannot 'run'; only SPU's can run. This is so since a script can be the code (or logic) for multiple objects. A script that would be controlling an entity that hovers over the surface could be used for multiple entities. Therefore, there can be only 1 hover script, but multiple SPU's, each SPU controlling 1 entity. Each SPU then shares the script code, but does not share the data (variables), unless the SPU was forked due to a yield.

  exit Stop the script
Values true The value 1 (type 'int'; Onyx doesn't know the type boolean as is common in C++)
  false The value 0.
Preprocessing #include Include another file (i.e. '#include "racer.oxs"). Behaves like the include file is inserted into the currently compiled file (compare to C++).
Types float Floating point (i.e. 5.3)
  int Integer (i.e. 123)
  string A string; for example: string s; s="hello";
  void Empty type; this is a type with no value and no size. For example: void f(){ x=123; }
  const A constant, i.e. 'const int MY_EVENT=123'
  cast Explicit type-casting, i.e. 'pointer p; int x; x=cast<int>p;'
  pointer A generic pointer. Currently not much used.
  null A special 'null' type which can be used to compare pointers to null for example (if(p==0) will not work since a pointer cannot be compared to an int, but if(p==null) will work).
Math abs float abs(float v) - returns absolute number. I.e. abs(-2.0) = 2.0.
  sin float sin(float v)
  cos float cos(float v)
  tan float tan(float v)
  fmod Returns floating point modulo (float fmod(float numerator,float denumerator) ). I.e. fmod(4.0,1.2) will give 0.4 (3*1.2=3.6, 4.0-3.6=0.4).

Events - OBSOLETE since 14-8-2012

Events are obsolete now; Onyx functions are called by the C++ engine instead. Coroutines may be created to allow for long-during scripts.

Events occur through the engine, and scripts can (and should!) wait for those. This makes efficient scripting possible without lots of busy polling of states. The wait command will wait for an event before the script continues.

Waiting for specific events can allow you to override some behavior. Currently defined waiting points are:

Group

Event

Description
Motion EVENT_MOTION_OUTPUT Triggered just before a motion packet leaves the machine. This can be used to put script-defined signals on the motion output. Support for E2M motion controllers only.
Graphics EVENT_FRAME Triggered just before rendering a frame. The time to adjust model matrices in the future.
Physics EVENT_PHYSICS Triggered at every physics step, so every 1/1000th of a second. These scripts should care about performance, to avoid bringing framerates down too much.

Callbacks

The Panthera game engine can call functions inside Onyx scripts, called 'callbacks'. For example, each frame, the function 'void OnFrame()' is called (if present) in each script. This makes it easy to add functionality that must be performed at specific points in the engine. More detailed information on these callbacks can be found on this page.

Currently defined callbacks are:

Group

Function

Description
Rendering void OnPreFrame()

Called just before rendering a frame. This is the place to adjust entity positions/orientations, and to perform other framerate dependent actions.

Example: void OnPreFrame(){ echo("OnPreFrame"); }

Graphics void OnPaint2D() Called when it's time to paint 2D things (before this, all 3D rendering has already been performed). For example, use PaintText() to paint a text.

For example: void OnPaint2D(){ PaintText("Hello world",100,100); }
  void OnPaint3D()

Called after all 3D rendering has been completed. Everything is still in HDR at this point, so colors will be modified with the current exposure (HDR -> LDR conversion).

For example: void OnPaint3D(){ PaintDot3D(...); }

Events void OnTriggerLine(int n) Triggerline #n has been passed by a registered entity (in Panthera), or a car (in Racer)
  void OnTrackLoaded() Called after a track has fully loaded. At this point, all nodes are present and can be searched for, for example with FindNode().
  OnCarSpawn() Called after a car has been spawned and put into position. At this point, you can modify the car position (CarWarpToPos() for example). v090RC9+
  OnEvent(Event e) Used for communication between entities. Can also be used to send events to Onyx from .rex scripts example using 'onyx event <myevent>', where <myevent> will be sent as a string with e.type==EVENT_CUSTOM.
Process void cleanup()

Called when the SPU will stop running. This should be used to release resources that you may have allocated in other parts of the script. An example:

handle h;
void cleanup(){ if(h)UDPClose(h); }
void main(){ h=UDPOpen("127.0.0.1",25000); }

Resource leaks are very important; specific functionality to track resource leaking will probably be added to Onyx to notify the programmer of mistakes.

Types

The Onyx language looks a lot like C++, and also behaves similarly. The base expression types defined are:

Implicit type conversions are used in assignments and function calls, such as:

float f;
int n;
const int MY_CONST=456;
int a[10];

t=123;
n=5.3;
n=sin(4);  // 2 type conversions: '4' from int to float for sin(), and 'n=' from float back to int.
n=MY_CONST;   // 'n' becomes 456
a[5]=124;

Compared to C++, these notable differences exist in expressions:

The $ keyword

The keyword '$' can be used to end compilation explicitly. This can be useful when testing a few things; instead of commenting out all the ending lines, you can just place a '$' where compilation should stop. An example:

Implicit type conversions are used in assignments and function calls, such as:

float f;
f=123;
echo(f);  // Prints '123'

$ // end compilation

f=f+1;
echo(f);   // We'll never get here

The script above will compile upto the line with the dollar sign, but will not see 'f=f+1'. It will stop compiling, so syntax errors and such in the rest of the code will not be found.

Built-in variables

Here are the predefined variables that are available in Onyx:

Group

Variable name and type

Description
Math float pi 3.141592653589

 

Built-in macros

Here are the predefined macros that are available in Onyx (actually, macros don't currently exist in Onyx, so these can also be thought of as predefined variables). v0.9.0RC9+.

Group

Variable name and type

Description
Lexical analyzer __FUNCTION__ The name of the function currently compiled, i.e. "f"
  __FILE__ The file currently being compiled, i.e. "test.oxs"
  __LINE__ The line number currently being compiled, i.e. 68

 

 

Built-in functions

Some functions come with the base compiler. The Racer engine adds Racer-specific functions on top of the base compiler. Here is a list of base Onyx internal functions that are always present (whether running inside Racer or just in onyx_run.exe).

For a complete list, see this page.

Engine functions

These functions call into the C++ engine to perform more complex operations mostly, or to extract information from the game engine.

For a complete list of the Onyx engine functions, see this page.

Arguments and local variables

It is possible to define (local) variables when you want to use them; they don't need to be defined at the top of the function. For example:

int f(int n)
{
  int a=2,b=4;
  int c;

  c=a+b;

  int d;
  d=c+3;
  return n+d;
}

int x=f(5);
echo(x);

The script above will output '14'.

Pass by Reference

Unlike C++, where values are always passed by value unless you explicitly use pointers, Onyx implicitly passes class instances by reference. For example, the following example will print 100 and 123. In C++, it would print 100 and 100, as 'vt' is copied onto the stack. Onyx will only pass the pointer (address) to 'vt' when it calls function 'f'.

class Vector3
{
  float x,y,z;
};

void func(Vector3 v)
{
  v.x=123;
}

Vector3 vt;
vt.x=100;
echo(vt.x);
func(vt);
echo(vt.x);

Classes

Classes are used for an object-oriented approach to solving problems. An object exhibits functionality, which is normally abstracted a bit away from the actual implementation. Also, all member functions in the class are 'public' by default; any function can be called directly. In the future you may see private functions (functions that can only be called from within other member functions of that class).

Classes in Onyx differ from Onyx in the way that variables need all to be declared before they are used. An example:

// BAD - 'x' referenced before being defined (will generate a compiler error)
class Vector3
{
  void f()
  {
     x=5;
  }
  float x,y,z;
};

// GOOD
class Vector3
{
  float x,y,z;
  void f()
  {
     x=5;
  }
};

Preprocessor (Racer v0.9.0RC8+)

A bit like C++, there is a small preprocessor in the lexical analyzer of Onyx. This allows you to conditionally compile code or leave it out (which is faster than using runtime if/then/else).

An example use of conditional compilation in Onyx:

#define USE_ABC

void main()
{
#ifdef USE_ABC
  echo("USE_ABC is defined");
#else
  echo("USE_ABC is not defined");
#endif
}

Note that #ifdef can be nested.

Caveats

The following quirks exist in Onyx. Their behavior or existence may change in the future.

Advanced

Mostly for people familiar with C or C++, the advanced article describes some more complex features of Onyx.

 


Dolphinity Organiser - free planning, project management and organizing software for all your action lists

(last updated March 17, 2014 )