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)
- Click in the canvas
- Create a Class Block
- Choose ObjectBehaviour at superclass
- Add a name
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.
- adds further inputs. By right clicking and "remove last input" they can be removed.
Map
Creates a map from the input. The order is always key, value, key, value...
- adds further inputs. By right clicking and "remove last input" they can be removed.
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
Regex
Ilios has integrated regex support alike Javascript.
Modifiers:
- i: Caseless
- s: Dot works on all chars
- m: Multiline
- g: Ungreedy
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;
}
}
}