NAV Navbar
Logo
General - Audio - Ilios - Lua

Visual Scripting

The visual scripting maps to ilios code. It's a visual representation of code and currently has no extra features compared to code. This may change in the future.

Everything is centered around classes. These classes can be attached to objects and the game via components.

Usage

Right click / left click -> Open block creation

Scroll -> Zoom

Right click on block -> show options

Double click on some blocks -> edit block

Jumpstart

To start you need a class block. Everything is organized in classes. Classes are parts of code that can be attached to objects or the game. They also have a state, so can save information in them.

1)

2) Drag the yellow dot to create a function, you can now override functions from ObjectBehaviour

3) Drag the arguments from the functions to use them, via this you can change the object

4) Now add the class to an object with the components tab, where they are listed under behaviours.

Exec Flow

Execution flow is marked as a white line. It starts at a function and follows the line and executes all blocks in order.

The following blocks can manipulate control flow:

Branch

Depending on if the input is true or false either the second or third output is chosen, afterwards the first is called.

Switch

Depending on the input an output is chosen, afterwards the first output is executed. Switching can be done on string, int or enums. The values must all be constant. If none is found, default is executed.

Foreach

Foreach calls the second output for every object in the input. The value is saved in the local below. It can dragged out. By doubleclicking the name can be changed.

For

For calls first ->var, then ->do, ->incr as long as while is true. Afterwards output 1 is called.

While

As long while is true, output 2 is executed, afterwards output 1.

Return

Ends the function and returns the given value or no value.

Break

Jumps out of the current switch/for/foreach/while.

Continue

Jumps to next for/foreach/while call.

Generic Function Call

Represents a call. Either with exec flow or without.

You can add exec flow to a function call by dragging the exec path to it.

Create Var

Drag a value to somewhere and select "create var". By doubleclicking on the typename you can change the name. It can then be dragged to create a get node.

Assign

The assign node changes the value of the target.

String

The string node concatenates all inputs.

Array

Creates an array from the input objects. The first object determines the types.

Map

Creates a map from the input. The order is always key, value, key, value...

Select

Based on the boolean input A or B is selected.

Opt

If the input is null the other is used.

Range

Creates a range from a to b.

FuncNode

Represents a lambda function. Can currently not be edited, you need to use a type input to create them working.

Call On Func

Calls a function or function pointer.

Call On Func Exec

Calls a function or function pointer with exec flow.

Class Block

Classes are the central blocks in the graph from which all exec flow originates.

You can have one superclass, but as many interfaces as required.

Doubleclick to edit.

Interfaces

Interfaces functions must be implemented, on dragging the function flow the required functions are hinted at.

Function Block

Functions can be static or virtual. You can also select constructors, destructors and static constructors. If you're using behaviours constructors require all the arguments the superclass needs.

Doubleclick to edit.

Constructor

To call superconstructors they need to be the first block after the function block.

Static Constructor

These are always called at loading stage.

Destructor

Field Block

Field block auto create their function on drag. If you don't add get and set, they are auto created.

Doubleclick to edit.

Variable Block

Doubleclick to edit.

Is / As

These nodes are not implemented yet.

Code

Basic Types

bool

A boolean value either true or false.

var b = true;
var b2 = false;
if b
{
    Debug.Print("b is true");
}
b || b2 // or
b && b2 // and
b ^ b2 // xor
!b // not
b == b2
b != b2
b.ToString() // "true"

int

Int is 32bit signed integer, allowing values from −2.147.483.648 to 2.147.483.647.

var i = 32;
i = 0xFF; // hexadecimal 255

// basic operations
i + 1
i - 1
i * 1
i / 1
i % 1 // modulo

i << 1 // bit shift left
i >> 1 // bit shift right
i & 1 // bit wise and
i | 1 // bit wise or
i ^ 1 // bit wise xor
~i // bit wise not
-i
i++ // inc
i-- // dec

> < >= <= == != // comparison operators

int maxVal = int.MaxValue;
int minVal = int.MinValue;

float

Float is 32bit floating point number. Float is not as precise as double which Lua uses, so keep an eye out. Float is the default floating point type.

var f = 1.0;
f = 0.23E2; // * 10 ^ 2
f = 0xFF.Fp0; // hexadecimal representation

+ - * / % // operations

double

Double is a 64bit floating pointer number. Use only if you need the precision.

var d = 1.0d; // use d suffix
d = 0.23E2d;
d = 0xFF.Fp0d;

+ - * / % // operations

int i = d.Bits;

long

Long is a 64bit integer.

var l = 123L;

char

char represents a character in a string and is also the size of an int.

'ä'
.Int

string

Contrary to C# string is not immutable and is a stack type, so it can't be null. String uses a utf8 internal representation, if you want character iteration use wstring, which uses UCS2.

var s = "abcdef";
s = @"abcdef"; // verbatim, no escape sequences, double "" for "
s = "\r\n"; // escape sequence
s = "\xFF\u{1F03}\100"; // escape sequence

// string slicing
string s = "0123456789";
PrintString(s[-3...]);
PrintString(s[...2]);
PrintString(s[..<2]);
PrintString(s[..<5]);
PrintString(s[3...5]);
PrintString(s[3...4]);
PrintString(s[3..<5]);
PrintString(s[7...]);
s[...2] = "abc";
PrintString(s);
s[8...] = "de";
PrintString(s);
s[4...5] = "gh";
PrintString(s);

for i in "0123456789"
{
  Debug.Print(""+i);
}

string s = "ÄÖÜ";
Debug.Print("${#s}"); // 6

wstring ws = L"ÄÖÜ";
Debug.Print("${#ws}"); // 3

var vs = @"\/\x23""asd"; // verbatim string

String Interpolation

Regex

Ilios has integrated regex support alike Javascript.

Modifiers:

var rg = /([A-Z])\w+/g; // new Regex("([A-Z])\\w+","g")
var rg2 = /[a-z]{2,3}/;
var s = "a abc a";
rg2.Match(s);
assert(rg2.AfterMatchText(), " a"); // Ensure that s is still alive or this might crash
assert(rg2.BeforeMatchText(), "a ");

Debug.Print(rg.Match("Abc abc Abc aaa Aaa AAA").ToString());

Debug.Print(/^A-Z\w+$/g.Match("Abc abc Abc aaa Aaa AAA").ToString());

Debug.Print(rg.Replace("Abc abc Abc aaa Aaa AAA", "_$$0_$$1_")); // Replace $$0 = first group

Debug.Print(rg.Map("Abc abc Abc aaa Aaa AAA", x => x.GroupText(0).Lowercased)); // Replace with callback

Debug.Print(string.Join(",", /[a-z\s]+/g.Split("Abc abc Abc aaa Aaa AAA")));

var sarr = new string[];
/[a-z]/g.MatchAll("abcABCabc", [sarr](Regex x)
{
    sarr.Add(x.GroupText(0));
    Debug.Print("${x.GroupText(0)} ${"abcABCabc"[x.Group(0)]} ${x.Group(0)}");
    Debug.Print("${x.BeforeMatchText()} ${"abcABCabc"[x.BeforeMatch()]}");
    Debug.Print("${x.AfterMatchText()} ${"abcABCabc"[x.AfterMatch()]}");
});
Debug.Print(string.Join(",", sarr));

Arrays

Arrays are created by adding [] to the name. Arrays are mutable, can be enumerated and sliced with ranges.

If ranges are accessed that are outside the range, this is a fatal error.

var chars = new char[](4);

var arr = [1,2,3];
int[] arr = [1,2,3];

//Basic Operations
arr[0] = 5;

//Enumerate
for i in arr
{
    Debug.Print(i.ToString());
}

//Slicing
arr[...1];

//LINQ
arr = arr.Select(x=>x+1);
arr = arr.Where(x=>x%2==0);

Maps

var m = [1:2,2:3,3:4];
Map<int,int> m = [1:2,2:3,3:4];

//Basic Operations
var len = #m;
m.Clear();
m.ContainsKey();
m[4] = 5;
m.Remove(4);

//Enumerating
for i in m
{
    Debug.Print(i.Key.ToString());
    Debug.Print(i.Value.ToString());
}

Stack Types vs. Reference Counted Types

Ilios has socalled stack types and reference counted types. Stack types are like value types in C#.

class Test
{
    string s = "test";

    static
    {
        string v = "g";
        Test t = new Test();

        var q = t;
        q.s = "test2";
        Debug.Print(t.s); // -> test2

        string s = v; // this copies the string
        s = "g2";
        Debug.Print(v); // -> g
    }
}


Operators

+ -
* / %
< > <= >= == != = (== = safe)
>> << | & ^ ~
&& ||
! # -

? :

++ --

... ..< ..>
post pre

Cast

is as

Boxing/Unboxing


int i = 55;
object o = i as object; // boxing
int j = o as int; // unboxing

Int/Float/String

int i = int();


Control Flow

Local Variable Definition

var i = 5;
int i = 5;
var i = 5, j = 6;
int i,j;

If

If always uses { }, so it's not necessary to put the condition into ( ).

if i == 5
{

}
elseif j == 6 || i >= 65
{

}
else
{

}

For

for(;;)
{
    break;
    continue;
}
for(var i=0;i < 5;i++)
{

}
var i=0;
for(;i < 5;i++)
{

}

While

while(true)
{
    break;
    continue;
}

Do

do
{
    break;
    continue;
}
while(true);

Foreach

Modifying while iterating

for i in arr
{
}
for i in arr.Range
{
}
for i in 0...5
{
}

Switch

Switch cases do not fall to the next case, so a case must have at least one statement.

Switch can use enums, int and string. All switch values must constant and not be calculated.

switch(a)
{
    case 0:
        break;
    case 1,2,3:
        var i=0;
    default:
        break;
}

Break

Break jumps out of the current for, for-each, switch, while or do.

Continue

Continue jumps back to the start of the current for, for-each, switch, while or do.

Return

int i()
{
    return 0;
}
void k()
{
    return;
}

Range

1...6 // 1 to 6 including 6
1..<6 // 1 to 6 without 6
...6 // up until 6
..<6 // up until 6 without 6
6... // from 6 on

var r = 0..<6;
r.Shuffled() // -> all members in random order
r.ToArray() // -> all members in order
r.Contains(0) // -> true

Type Safety

Ilios type checks every argument and is like Swift more restrictive than other languages. So float can not be implicitly cast to int. Whenever this is needed, you need to explicitly do this: int(i) or float(i).

Function Pointers

Function pointers are just pointers without lifetime management, so they can't capture anything.

The arguments are in order and then the return type, so there is always at least "void".

Ilios distinguishes between member functions with a this pointer (MemFuncPtr) and static functions (FuncPtr).

FuncPtr<int> t = { return 5; };
FuncPtr<int, int> t = f => f + 5;
FuncPtr<void> t = { };

var f = CharacterList.Len;
MemFuncPtr<CharacterList,int> f = CharacterList.Len;
Debug.Print(f(game.CharacterLinks).ToString());

Lambda Functions

Lambda functions are inlined functions that can also use variables from the current scope.

Functions with the x=>x syntax can also use type inference.

By capturing you can refer to variable from the local scope. Value types are copied.

Func<int,int> f1 = s => s + 1;
Func<int,int> f2 = [f1]x=>f1(x);
Debug.Print(f1(3).ToString());
Debug.Print(f2(3).ToString());

var f3 = (int x)=>x;
var f4 = (){};
var f5 = {};

var i = 3;
([i]{ Debug.Print(i.ToString()); })(); // capture

Lifetime Management

Ilios uses reference counting for lifetime management. This can cause reference cycles:

class A1
{
    A2 b;
    ~A2 { Debug.Print("A1 deleted"); }
}
class A2
{
    A1 a;
    ~A2 { Debug.Print("A2 deleted"); }
}
class Test
{
    static
    {
        var a = new A1();
        a.b = new A2();
        a.b.a = a;
        a = null; // objects are not deleted now
    }
}

To fix this you need to use weak, which does not hold ownership.

class A1
{
    A2 a;
    ~A2 { Debug.Print("A1 deleted"); }
}
class A2
{
    weak A1 b;
    ~A2 { Debug.Print("A2 deleted"); }
}
class Test
{
    static
    {
        var a = new A1();
        a.b = new A2();
        a.b.a = a;
        a = null;
    }
}

Reference Call

class Test34
{
    static void T(ref int i)
    {
        i += 60;
    }
    static
    {
        int i=5;
        Test34.T(ref i);
    }
}

String Interpolation

Instead of using + to create assembled strings you can use string interpolation. It automatically searches for a ToString method.

int i = 16;
string s = "$i ${i}";

// Format Options

Debug.Print("${i|x}"); // 10 in hexadecimal

Formatting options are equivalent to Printf options with %. These are not checked, so trying to use an int as a string might crash.

OOP

Classes

class Test
{

}

Inheritance

class Test : Game
{

}

Interfaces, Virtual, Override

interface ITest
{
    void Start();
}

class Base : ITest
{
    void Start()
    {
        Debug.Print("start");
    }
}

Variables

class Test
{
    var i = 0;
    static var j = 0;
    int k;
    outlet int l; // can be set for behaviours in editor
    weak Game g;
}

Functions

class Test
{
    void Test() {}
    static void Test() { }
    static int Test2() { return 0; }

    virtual int Test3() { return 0; }
}
class Test2 : Test
{
    override int Test3() { return 5; }
}

Statics

class Test
{
    static int i = 0;
    static
    {
        Test.i = 5;
        i = 3;
    }
}

Constructor/Destructor

class Test
{
    int i;
    Test() : i(0) {}
    ~Test {}
}

Static Constructor

class Test
{
    static
    {
        Debug.Print("static");
    }
}

Overloading

class Test
{
    static void T(int i) {}
    static void T(int i, int i) {}
    static
    {
        T(4);
        T(4,4);
    }
}

Fields

class Test
{
    int i => 53; // readonly

    int _backing_prop2 = 13; 
    int Prop2 {get => this._backing_prop2; set { this._backing_prop2 = newValue; }} // readwrite
    int Prop4 {get { return this._backing_prop2; } set { this._backing_prop2 = newValue; }}

    int Prop {get;set;} = 52; // autoproperty
    int Prop3 {get;set;}

    virtual int Prop9 => 523;
}
class Test2 : Test
{
    override int Prop9 => 980;
}

Enums

enum E
{
    V1, V2, V3 = 5
}
class Test
{
    E e = E.V1;
}

Templates

Function Templates

class Test
{
    static T F<T>()
    {
        return T();
    }
    static
    {
        Test.F<int>();
    }
}

Class Templates

class Test<T>
{
    T F()
    {
        return T();
    }
}
class T2
{
    static
    {
        var t = new Test<int>();
        t.F();
    }
}

Extensions

class Test
{
}
extension Test
{
    static void F()
    {

    }
}
class T2
{
    static
    {
        Test.F();
    }
}

Type Inference / Specification

class Test
{
    static T F<T>(T i)
    {
        return i;
    }
    static
    {
        var i = Test.F(3); // inferred
        var f = Test.F(3.0); // inferred
        var f2 = Test.F<float>(3); // specified
    }
}

Error Handling

Null Pointer Check

Nearly all object access have an internal null pointer check so the execution will stop but not crash your game.

If a nullpointer check fails the execution of that function is stopped. Currently no references are freed, so if you have many fails the memory will fill up.

Game g = null;
g.Info = ""; // null check fail

Range Check

The same happens for out of bounds accesses for strings and arrays.

int[] i = [];
i[0] = 2; // range check fail

Examples

Box2D Move Object with Mouse

class B2Touch : GameBehaviour
{
  static B2Touch instance;
  b2Body body;
  b2MouseJoint joint;
  b2World world = b2World.Instance;
  override void MouseEvent(MouseEvent evt)
  {
    instance = this;
    var pos = vec2(float(evt.Position.x), float(evt.Position.y)) * 0.02;
    switch(evt.Type)
    {
      case EventType.MouseDown:
        body = null;
        world.QueryAABB(new b2QueryCallback 
        {
          bool ReportFixture(b2Fixture fixture)
          {
            if(fixture.Body.Type == b2BodyType.DynamicBody)
            {
              B2Touch.instance.body = fixture.Body;
              return false;
            }
            Debug.Print(fixture.Body.EntityID.ToString());
            return true;
          }
        }, b2AABB(pos - vec2(0.1,0.1),pos + vec2(0.1,0.1)));
        if(body != null)
        {
          b2MouseJointDef def;
          def.bodyB = body;
          def.bodyA = Objects["ground"].GetBody();
              def.target = pos;
          def.collideConnected = true;
          def.maxForce = 4000.0;
          joint = world.CreateJoint(def);
          body.Awake = true;
        }
        break;
      case EventType.MouseMove:
          if joint != null
          {
              joint.Target = pos;
          }
        break;
      case EventType.MouseUp:
          if joint != null
            {
              world.DestroyJoint(joint);
              joint = null;
         }
        break;
    }
  }
}