Operators (ZScript)
Note: This page is for ZScript. For ACS operators see here. |
Note: This page is actively being updated. There may be mistakes or missing information. |
This is a list of all operators supported by ZScript and their descriptions.
Table of Operators
The following is table of every valid operator in zscript, along with their order of precedence and associativity. (source)
Arithmetic Operators
+
(addition)
int foo = 1 + 1; // foo = 2
int bar = foo + 1; // bar = 3
-
(subtraction)
int foo = 1 - 1; // foo = 0
int bar = foo - 1; // bar = -1
*
(multiplication)
int foo = 2 * 10; // foo = 20
int bar = foo * 10; // bar = 200
**
(power)
int foo = 2 ** 8; // foo = 256
/
(division)
Note: If both operands are integer numbers, the result of division will be an integer number, truncated (not rounded). At least one of the operands has to be a double value to get a decimal result.
int foo = 10 / 2; // foo = 5
int foo1 = 5 / 2; // foo = 2
double bar = 5 / 2; // bar = 2.0 because 2.5 was truncated
double bar1 = 5.0 / 2; // bar1 = 2.5
double bar2 = 5 / 2.0; // bar2 = 2.5
Round (round to closest), floor (round down) and ceil (round up) can be utilized on a float-point division to set the result to the closest suitable integer number:
double d = (5 / 3.0); // d = 1.666...
int foo = round(5 / 3.0); // foo = 2
int foo1 = ceil(5 / 3.0); // foo1 = 2
int foo2 = floor(5 / 3.0); // foo2 = 1
%
(modulo)
Returns a remainder after dividing integer numbers.
int foo = 5 / 2; // foo = 1 because 5 = 4 * 2 + 1 where 1 is the remainder
This operator can be used to perform something on a fixed time basis in a function that is being called every tic:
override void Tick()
{
Super.Tick();
if (Level.maptime % TICRATE == 0)
{
// This will be called every second while the game isn't paused
}
}
(See Tick)
+
(unary plus)
No-op source. Added for sake of completeness. Can sometimes be useful for aligning numbers.
enum Equality
{
lt = -1,
eq = +0,
gt = +1,
};
-
(unary minus)
Negates a value. Converts a positive value to a negative, and a negative to a positive.
int n;
n = 0;
n = -n; // n is still 0
n = 1;
n = -n; // n is now -1
n = -n; // n is now 1
n = -(-n); // n is still 1
Assignment Operators
=
(assignment)
int foo = 5;
Assignment can be combined with arhitmetic operators as follows:
+=
(assignment + addition)
-=
(assignment + subtraction)
*=
(assignment + multiplication)
/=
(assignment + division)
%=
(assignment + modulo)
Assignment can be combined with bitwise operators as follows:
&=
(assigment + bitwise AND)
|=
(assigment + bitwise OR)
^=
(assigment + bitwise XOR)
<<=
(assigment + bitwise shift left)
>>=
(assigment + bitwise shift right)
>>>=
(assigment + bitwise unsigned shift right)
++
(incrementation)
The increment operator (++
) increases a numeric variable's value by one. It is shorthand for foo += 1
.
int foo = 1;
foo++; // foo = 2
Like other assignment operators, in addition to modifying the variable, the increment operator also returns a value. The value returned depends on whether the operator is placed before or after the variable name.
- Post-increment (
foo++
): Returns the variable's value before it is incremented. - Pre-increment (
++foo
): Returns the variable's value after it has been incremented.
int foo, a, b, c;
foo = 0;
a = foo; // before (0)
b = foo++; // during (0) <- captures value before operation
c = foo; // after (1)
Console.Printf("%d %d %d", a, b, c); // 0 0 1
foo = 0;
a = foo; // before (0)
b = ++foo; // during (1) <- captures value after operation
c = foo; // after (1)
Console.Printf("%d %d %d", a, b, c); // 0 1 1
--
(decrementation)
The decrement operator (--
) decreases a numeric variable's value by one. It is shorthand for foo -= 1
.
int foo = 1;
foo--; // foo = 0
Like other assignment operators, in addition to modifying the variable, the decrement operator also returns a value. The value returned depends on whether the operator is placed before or after the variable name.
- Post-decrement (
foo--
): Returns the variable's value before it is decremented. - Pre-decrement (
--foo
): Returns the variable's value after it has been decremented.
int foo, a, b, c;
foo = 1;
a = foo; // before (1)
b = foo--; // during (1) <- captures value before operation
c = foo; // after (0)
Console.Printf("%d %d %d", a, b, c); // 1 1 0
foo = 1;
a = foo; // before (1)
b = --foo; // during (0) <- captures value after operation
c = foo; // after (0)
Console.Printf("%d %d %d", a, b, c); // 1 0 0
Relational operators
These operators are used in IF/ELSE, FOR, WHILE statements to check for conditions
==
(exactly equal)
Checks if two operands are equal to each other:
if (myNumber1 == myNumber2)
{
// True if both numbers are the same
}
if (myPointer1 == myPointer2)
{
// True if both pointers point to the same object
}
With floats and doubles, outside of specific circumstances ~==
should be used instead, as floating-point error and precision loss means values from different calculations will rarely match exactly.
!=
(not equal)
Using !=
flips the check.
~==
(approximately equal)
Checks if two operands are approximately equal to each other.
With floats and doubles, it checks if they're within 1/65536.0 (0.0000152587890625) distance of each other. Note that this check should almost always be used instead of ==
, as due to the way floating-point numbers work, in particular error and precision loss, means they'll rarely match exactly.
if (myDouble1 ~== myDouble2)
{
// True if the difference between the two values is less than 0.0000152587890625
}
With strings, ~==
performs a case-insensitive check:
string str1 = "Adam";
string str2 = "ADAM";
if (str1 ~== str2)
{
// This check will pass
}
if (str1 == str2)
{
// In contrast, this check will fail because the two strings use
// different capitalization, and '==' is case-sensitive
}
>
(greater)
if (myNumber1 > myNumber2)
{
// True if myNumber1 is greater than myNumber2
}
<
(less)
if (myNumber1 < myNumber2)
{
// True if myNumber1 is less than myNumber2
}
>=
(greater or equal)
if (myNumber1 >= myNumber2)
{
// True if myNumber1 is greater than or equal to myNumber2
}
<=
(less or equal)
if (myNumber1 <= myNumber2)
{
// True if myNumber1 is less than or equal to myNumber2
}
<>=
(3-way comparison)
stub.
Logical operators
Logical operators are usually used in IF/ELSE, WHILE and FOR statements to combine conditions. Note: in case of multiple conditions, the statement will abort as soon as it encounters the first condition that isn't met. As such, stringing multiple conditions (while possibly detrimental to readability) will not increase the performance cost. The only exception is the ternary operator.
No operator
If an operator is skipped in a conditional check, the check is implicitly considered to be a boolean check:
if (myBool)
{
// True if myBool is true
}
// This does the same thing:
if (myBool == true)
{
}
With pointers, it performs a null-check:
if (myPointer)
{
// True if myPointer is not null
}
// This does the same thing:
if (myPointer != null)
{
}
With numeric values, it performs a zero-check:
if (myNumber)
{
// True if myNumber is not 0
}
// This does the same thing:
if (myNumber != 0)
{
}
With String values it checks if the string is not empty:
if (myString)
{
// True if myString is not ""
}
// This does the same thing:
if (myString != "")
{
}
![]() |
Warning: The above check will work on strings but will NOT work on names unless a name was explicitly set to be empty. This is because when a String declared without a value, it's implicitly set to "" , but when a name is declared, it's implicitly set to 'none' .
|
&&
(logical AND)
Used to check if all conditions in a statement are true:
if (<condition1> && <condition2>)
{
// True if both conditions are true
}
while (<condition1> && <condition2>)
{
// loops as long as both conditions are true
}
||
(logical OR)
Used to check if at least one conditions in a statement is true:
if (<condition1> || <condition2>)
{
// True if either condition is true
}
while (<condition1> || <condition2>)
{
// loops as long as either condition is true
}
!
(logical NOT)
Returns 1 (true) iff the input value is falsey, 0 (false) otherwise.
List of falsey values:
0
false
null
'None'
Console.Printf("b %d %d", // bools -> b 1 0
!false, // 1
!true // 0
);
Console.Printf("i %d %d %d", // ints -> i 1 0 0
!0, // 1
!100, // 0
!-1 // 0
);
Console.Printf("f %d %d %d %d %d", // floats -> f 1 1 0 0 0
!0.0, // 1
!-0.0, // 1
!0.5, // 0
!double.infinity, // 0
!double.nan // 0
);
Console.Printf("n %d %d %d", // names -> n 1 0 0
!'None', // 1
!'a', // 0
!'' // 0
);
Console.Printf("p %d %d", // pointers -> p 1 0
!null, // 1
!self // 0
);
TODO: re-purpose the below
Used to invert any condition in a conditional check:
if (foo != bar)
{
// True if foo isn't equal to bar
}
if (<condition1> && !<condition2>)
{
// True if condition1 is true but condition2 is false
}
if (!<condition1> && !<condition2>)
{
// True if both conditions are false
}
if (!<condition1> || !<condition2>)
{
// True if either condition is false
}
if (!(<condition1> && <condition2>))
{
// True if either condition is false
}
This operator can also be used to null-check pointers and zero-check values:
if (myPointer != null)
{
// True if myPointer is NOT null; this is the same as just 'if (mypointer)'
}
if (!myPointer)
{
// True if myPointer IS null; this is the same as 'if (myPointer == null)'
}
if (!foo)
{
// If foo is a string, this is true if foo is "".
// If foo is a pointer, this is true if foo is null.
// If foo is a numeric value, this is true if foo is 0.
}
?
(ternary)
Essentially, syntactic sugar for an IF statement. The basic syntax requires ?
and :
and functions as follows:
<condition>? <result if true> : <result if false>
For example:
int foo = <condition>? 5 : 10; // if condition is true, foo = 5, otherwise foo = 10
Note, a ternary operator can only be used if both possible values are explicitly of the same type. For example, it's not possible to combine a string and a name value, both have to be either strings, or names:
// We can't use "none" here, because double quotes would turn
// it into a string, whereas GetClassName returns a name,
// so we MUST use 'none' in single quotes:
name clsname = target != null? target.GetClassName() : 'none';
// Conversely, we can't use GetClassName() directly, because
// it returns a name, not a string, so the result of
// GetClassName() first has to be cast to a string:
string clsnameStr = target != null? ""..target.GetClassName() : "None";
For actor pointers, null
needs to be explicitly cast as Actor:
// This has a ~50% chance to set 'foo' to the calling
// actor's target; otherwise it'll be set to null:
Actor foo = random(0, 10) >= 5? target : Actor(null); //Just null won't be recognized
Note: nesting ternaries is possible...
int foo = <condition1>? 5 : <condition2>? 10 : <condition3>? 20 : <condition4>? 40 : 50;
...but not recommended. Not only does it reduce readability, it also increases performance cost because the engine has to go through every condition, even when only one of them can be true.
Notes on order of execution
When strung together, AND/OR operators follow several rules:
1. The conditions are always checked one by one, from left to right. This is why a condition like this is perfectly safe:
if (myPointer != null && myPointer.health >= 100)
{
// If myPointer is null, the check stops there, and the second
// condition is never reached, so myPointer.health will never
// cause a VM abort in this case.
}
2. As far as order goes, normally AND checks are executed before OR checks, but it is highly recommended to use parentheses to avoid any ambiguity. For example:
// Ambiguous:
if (<condition1> && <condition2> || <condition3>)
{ }
// Unambiguous:
if ( (<condition1> && <condition2>) || <condition3>)
{
// Executes if both condition 1 and 2 are true,
// OR if condition 3 is true (regardless of the
// values of condition 1 and 2)
}
// Unambiguous:
if (<condition1> && (<condition2> || <condition3>) )
{
// Executes if condition 1 is true, AND either 2 or
// 3 are true.
}
Bitwise operators
&
(bitwise AND)
Usually, used to check if a specific value is present in the bit field:
if (myBitField & foo)
{
// True if foo is contained in myBitField
}
Aside from being used with bit fields, this can also be used with integer fields to make a condition that switches between true and false every set number. For exampe:
override void Tick()
{
Super.Tick();
if (GetAge() & 8)
{
// this will be true for 8 tics
}
else
{
// then this will true for the next 8 tics
}
}
For example, this is how powerups, when about to run out, blink on and off every 8 tics; see IsBlinking.
&=
(bitwise AND assignment)
Usually, used in combination with ~
to remove a value from the bitfield:
myBitField &= ~foo; //foo is removed from myBitField
|
(bitwise OR)
Used to combine multiple values to set them in a bit field:
int myBitField = bitValue1 | bitValue2; //myBitField now contains both values
|=
(bitwise OR assignment)
Adds a value to the bit field:
myBitField |= foo; //foo is added to myBitField
~
(bitwise NOT)
Inverts every bit of a given number (0s become 1s and 1s become 0s). For any integer n, the result of ~n
is equal to -(n + 1)
.
int a, b;
a = 17; // 00000000_00000000_00000000_00010001
b = ~a; // 11111111_11111111_11111111_11101110
Console.Printf("%d, %d", a, b); // 17, -18
a = -256; // 11111111_11111111_11111111_00000000
b = ~a; // 00000000_00000000_00000000_11111111
Console.Printf("%d, %d", a, b); // -256, 255
a = 0; // 00000000_00000000_00000000_00000000
b = ~a; // 11111111_11111111_11111111_11111111
Console.Printf("%d, %d", a, b); // 0, -1
A common use case is to create a bitmask to clear a flag from a bit field:
myBitField &= ~foo; //foo is removed from myBitField
^
(bitwise XOR)
Checks the difference between two values at bit level. For instance:
bool isBitDifferent = (bitField1 ^ bitField2) != 0;
isBitDifferent
will be true if the two bit fields contain different values, false otherwise.
^=
(bitwise XOR assignment)
Flips the bit in the bit field:
myBitField ^= foo;
In this case foo
is added to myBitFieldd
if it wasn't there, and removed from it if it was.
<<
(bitwise left shift)
stub.
>>
(bitwise right shift)
stub.
>>>
(bitwise unsigned right shift)
stub.
Miscellaneous operators
is
(class inheritance)
Used to check if one class is based on another class through inheritance:
if (MyClass is ParentClass)
{
// True if MyClass inherits from ParentClass
}
This can be called both on class type pointers and class instance pointers. For example:
if (target is 'DoomImp')
{
// True if the target actor is a DoomImp or based on it
}
if (target.GetClass() is 'DoomImp')
{
// True if the target's class is DoomImp or based on it
}
..
(string concatenation)
Appends one string to another using the following syntax:
"some text "..myStringValue;
It's possible to append to an empty string as well:
""..myStringValue;
For example an actor can call this:
Console.Printf("I am "..GetTag());
This will print a string like "I am Zombieman" or "I am Shotgun Guy", where the name will be pulled from the actor's Tag (see GetTag).
It is highly recommended to NOT use this for player-facing strings, because this will produce a hardcoded line that cannot be properly localized into other languages. However, this is a useful tool for debugging.
In addition, the ..
operator can perform certain type of casts that don't work with String.Format. For example, it can be used to cast SpriteID to a string, such as:
override void Tick()
{
Super.Tick();
Console.Printf("Current sprite: "..curstate.sprite);
}
This will print the name of the currently used sprite, such as BAL1. This will not obtain frame or rotation, which are stored separately (for that see GetSpriteTexture).
It is unclear whether this behavior is considered valid, so it's not recommended to tie critical functionality of your project to such casts.
::
(scope)
This is unused, only serving as a reserved symbol.
.
(member access)
Used for accessing members of Classes and Structs.
Console.Printf("%d", MyClass.value); // access `value` member of custom class MyClass and
// use the `Printf` member of the Console struct to print it
cross
(crossproduct)
stub.
dot
(dotproduct)
stub.
Precedence
Operator precendence is the order things get calcuted when written in one line. In school you were tought about BEDMAS/PEMDAS, this expands on that.
When looking at an exression like 1+2*3^2/(5-2)
, instead of going left-to-right light a cheap calculator might, you break it down into its basic operations doing the higher precedence ones earlier. If you were to evaluate the entire expressoion left-to-right, you might incorrectly get the answer of 14.2.
Order:
B: (brackets)
E: (exponents)
D/M: (division/multiplications, left to right)
A/S: (addition/subtration, left to right)
Steps:
1) Brackets
1+2*3^2/(5-2) → 1+2*3^2/3
2) Exponents
1+2*3^2/3 → 1+2*9/3
3) Division/Mutiplication, left to right
1+2*9/3 → 1+18/3
1+18/3 → 1+6
4) Addition/Subtration, left to right
1+6 → 7
5) Done
Parentheses
stub.
Short-Circuiting
stub.
Associativity
Associativity defines the order of operations between sets of same (or similar) operators. Addition and subtraction share precedence, as do multiplication and division. When reading statements where these appear in a row, we read them from left-to-right. When programming, there are some operators that do not follow this rule.
Left-to-Right
An expression like a OP b OP c
is parsed as (a OP b) OP c
.
This is typical for arithmetic operators like multiplication and division (5 / 3 * 2
is (5 / 3) * 2
, not 5 / (3 * 2)
).
Right-to-Left
An expression like a OP b OP c
is parsed as a OP (b OP c)
.
This is used for assignment (a = b = 5
is a = (b = 5)
).