One of the
lesser known features of UnrealScript is the ability for programmers to
define their own operators. The Unrealscript Reference barely touches on
the subject, merely saying it's possible, but doesn't go into any detail.
It doesn't even provide the syntax for creating them. However, this
document will go into detail on how unrealscript operators work, as well
as providing some interesting examples. All of the information in this
document I've found out myself, so I'm sure some of it may not be totally
accurate.
Declaring a basic operatorThe syntax for declaring a basic
operator is as follows:
final operator([Precedence]) [Return Type] [Name] (
[Parameter1], [Parameter2]
) { }
|
Let's break that down.
NameThe name/identifier of the operator. This can be:
- A string identifier, like a normal function.
- A symbol, like +,-,*,/,#,:,etc
- A group of 2 symbols. For some reason the compiler limits you to 18
different symbol groups.
They are: <<, >>, !=, <=,
>=, ++, --, +=, -=, *=, /=, &&, ||, ^^, ==, **, ~=, @=
- The compiler also allows a special 3 symbol operator, >>>.
Return Type
Just like any normal unrealscript function, an operator can have a
return type. Works exactly the same. Naturally, if you want the
operator to do anything, you're going to have to return the result of the
operation between parameter 1 and 2.
Parameters
Almost the same as unrealscript functions with one catch, a standard
operator expects exactly 2 parameters. The first parameter will be the one
on the left side of the operator, the second one will be the right side.
Precedence
This allows you to specify the operator's precedence, or how tightly it
will bind. The lower the number, the tighter the operator will bind. When
you use a complex expression like A = 4 * 5 + 6, Unrealscript will 'group'
the operators by precedence. For example, the exponentiation operator's
precedence is 12, multiplication is 16, and addition is 20. Consider the
expression:
If
MyOperator has a precedence of 10, it'll bind tighter than division (16),
so the expression will be evaluated as:
A = B / (2 MyOperator 5); | On
the other hand, if MyOperator has a precedence of 20, it would evaluate
as:
A = (B \ 2) MyOperator 5; |
The valid range for precedence values is 0-255. Here's a list of the
existing values used in the built in operators:
Precedence |
Symbol |
Operation |
12 |
** |
Exponentiation |
16 |
* |
Multiplication |
16 |
/ |
Division |
16 |
Dot |
Dot Product |
16 |
Cross |
Cross Product |
18 |
% |
Modulus |
20 |
+ |
Addition |
20 |
- |
Subtraction |
22 |
<< |
Left Shift |
22 |
>> |
Right Shift |
22 |
>>> |
Double Right Shift |
24 |
< |
Less Than |
24 |
> |
Greater Than |
24 |
<= |
Less Or Equal |
24 |
>= |
Greater Or Equal |
24 |
== |
Equality |
24 |
~= |
Case Insensitive Equality |
26 |
!= |
Not Equal |
28 |
& |
Bitwise And |
28 |
| |
Bitwise Or |
28 |
^ |
Bitwise Xor |
30 |
&& |
And |
30 |
|| |
Or |
32 |
^^ |
Xor |
Basic Example
Here's a simple sample operator to demonstrate what we've covered so
far. This will calculate the average of the left and right side:
final operator(18) int : ( int A, int B
) { return (A + B) / 2; }
Middle = 10 : 2; // Middle = 6
Middle = 10 + 2 : 4; // Middle = (10 + 2) : 4 = 8
| I
made the precedence for this operator 18, so the addition will bind
tighter in this case.
Advanced operators
Preoperators / PostoperatorsThese are another form of operator
that you can implement, an example of which would be ++ or --. Syntax for
declaring a pre/postoperator is similar to that of regular operators:
final [postoperator|preoperator] [Return Type] [Name] (
[Parameter] ) { }
| Everything
is the same, except you change 'operator' to preoperator or postoperator.
Also, pre/postoperators only take one parameter - in the case of a
preoperator that would be the variable right after the operator, and in
the case of a postoperator, it'll be the variable right before the
operator. For example, ++i would pass i into the appropriate preoperator,
while i++ would pass it into a postoperator. While you don't have to do
this, a preoperator should return the data as it was before it was
modified, and a postoperator should do the inverse, as that's the whole
point :-).
Out ParamsYou can use the 'out' keyword, just as you can in
normal unrealscript functions, before any parameter to an operator. This,
as you might expect, allows you to modify the actual variables passed to
the operator. Also, if your operator has an out parameter, it can be used
as an expression all by itself. For example, let's take an int
postoperator '#' that divides the integer it's invoked on by 10, but
doesn't modify the original integer:
final postoperator int # ( int A
); { return A / 10; }
i = 100;
b = i#; // b = 10, i = 100 (i is NOT affected)
i#; // Wrong.. can't do this unless we declare the operator with
'out int A'
| Now,
let's try the same thing, but using out.
final postoperator int # ( out int A ); {
return A /= 10; // Notice how A is modified here }
i = 100;
b = i#; // b = 10, i = 10 (i IS affected, because of the out
keyword)
i#; // i = 1
|
Static operatorsOne thing I've failed to mention thus far, is you
can make most operators 'static', as long they don't access any non-static
member data, or call any non-static functions.
static final postoperator int # ( out int A
); { return A /=
10; }
| This
does the exact same thing that the previous example does, but has some
performance benefits, as the operator isn't re-instantiated every time it
is executed.
Examples
String multiplicationHere's a simple operator that will take a
string, and 'multiply' it by an integer, returning the string repeated
that many times:
static final operator(22) string * ( coerce string A, int
B ) { local string Build; local int
i;
for(i=0;i<B;i++) Build
= Build$A; return
Build; }
|
Vector operatorsSome simple vector operators, since most
operators that you could want for vector have already been defined.
// Extend vector by an unit length static final
preoperator vector ++ ( out vector A ) { return A
+= Normal(A); }
// Shrink operator by an unit length static final preoperator
vector -- ( out vector A ) { return A -=
Normal(A); }
// Same thing, but postop static final postoperator vector ++
( out vector A ) { local vector
B; B = A; A +=
Normal(A); return B; }
static final postoperator vector -- ( out vector A
) { local vector B; B =
A; A += Normal(A); return
B; }
|
Color OperatorsDue to the lack of any color operators, let's make
a few:
// Lighten color by 1 static final preoperator color ++
( out color A
) { A.R++; A.G++; A.B++; return
A; }
// Darken color by 1 static final preoperator color -- ( out
color A
) { A.R--; A.G--; A.B--; return
A; }
// Postoperator version static final postoperator color ++ (
out color A ) { local color
Copy; Copy =
A; A.R++; A.G++; A.B++; return
Copy; }
// Postoperator version static final postoperator color -- (
out color A ) { local color
Copy; Copy =
A; A.R--; A.G--; A.B--; return
Copy; }
// Remember that averaging operator we just used? Now it's
suddenly useful, so I've copied it into here final operator(18)
int : ( int A, int B ) { return (A + B) / 2; }
// Interpolate 2 colors static final operator(22) color Mix (
color A, color B ) { local Color
Result; Result.R = A.R : B.R; Result.G
= A.G : B.G; Result.B = A.B :
B.B; return Result; }
// UT Provides a * operator for colors, but no /. Ramp a color by
a float static final operator(16) color / ( color A, float B
) { local Color Result; Result.R =
A.R / B; Result.G = A.G / B; Result.B
= A.B / B; return Result; }
// Same thing, but this one affects the color static final
operator(34) color /= ( out color A, float B ) { A
= A / B; return A; }
// UT Provides *, not *=, so let's implement it static final
operator(34) color *= ( out color A, float B ) { A
= A * B; return A; }
// Add a byte value to each component static final
operator(20) color + ( color A, byte B ) { local
Color Result; Result.R = A.R +
B; Result.G = A.G + B; Result.B = A.B
+ B; return Result; }
// Subtract a byte value to each component static final
operator(20) color - ( color A, byte B ) { local
Color Result; Result.R = A.R -
B; Result.G = A.G - B; Result.B = A.B
- B; return Result; }
// Out versions of the operators static final operator(34)
color += ( out color A, byte B ) { A = A +
B; return A; }
static final operator(34) color -= ( out color A, byte B
) { A = A - B; return A; }
// Out version of the operator UT provides static final
operator(34) color += ( out color A, color B ) { A
= A + B; return A; }
static final operator(34) color -= ( out color A, color B
) { A = A - B; return
A; }
|
Actor Destruction
// We can't use static because we're calling Destroy(), a
non-static function final postoperator Actor DIEDIEDIE ( out
Actor NearlyDead
) { NearlyDead.Destroy(); return
NearlyDead; }
SomeActor DIEDIEDIE; // It is now
dead.
|
Abuse
Dynamic array implementationAlways wanted to use those elusive
array<>'s, but found the GetPropertyText/SetPropertyText interface
to be a problem? This is where operators come in. Here I provide a simple
interface to a dynamic array of integers (although this is easily
adaptable to support various types). You set elements of a dynamic array
with 'Array << (Index:Data)' (totally arbitrary syntax I made up),
and access an element with 'Array<Index>'.
class TestDynArray extends CommandLet;
// Our struct to store the index/data pairs created using the :
operator struct SetGroup { var int
Index; var int NData; };
// The left side of the Array construct... the one that
actually does something. final operator(50) int < ( DynArray
A, int B ) { return A.Get( B ); }
// The right side of the Array construct... does absolutely
nothing, it's just there to make the syntax pretty (complete the
brackets). final postoperator int > ( int A
) { return A; }
// Sets an element in a dynamic array, taking a index/data pair
as the right side. final operator(50) DynArray << ( out
DynArray A, SetGroup B ) { A.Set( B.Index, B.NData
); return A; }
// Creates a index/data pair final operator(23) SetGroup : (
int A, int B ) { local SetGroup
C; C.Index = A; C.NData =
B; return C; }
// Just a test function to show that we can use all sorts of
expressions within the index/data pairs function int
TestFunc() { return 10; }
function int main( string parm ) { local
DynArray Arr; local int i; local
setgroup g;
// Instantiate a DynArray Arr = new
class'DynArray';
// Set some elements Arr <<
(5:78); Arr << (1:30); Arr
<< (TestFunc():69);
// And log them Log( Arr<5> @
Arr<1> @ Arr<TestFunc()> ); return
1; }
// Interface to dynamic arrays. Evil. class DynArray extends
Object;
var array<int> Data; var int CacheData[1024]; var int
Num; var bool bCached;
// Parse the elements out of a string like (2,3,5,2,4,6,2), and
store them in our cache function
Recache() { local int i; local
string Nightmare; local int
NextPos; local int z; Num =
0; Nightmare =
GetPropertyText("Data"); Nightmare = Right(
NightMare, Len(NightMare)-1 ); Nightmare = Left(
NightMare, Len(NightMare)-1 ); for(i = InStr(
Nightmare, "," );i >
0;z++) { CacheData[Num++]
= int( Left(Nightmare, i) ); Nightmare =
Mid( Nightmare, i + 1, Len( Nightmare )
); i = InStr( Nightmare, ","
); if ( i == -1 && Len(NightMare)
> 0 ) CacheData[Num++] =
int( Nightmare ); } bCached =
true; }
// Set an element by building a string like (3,3,5,9), and
recache. function Set( int Index, int Data
) { local string Build; local int
i; Recache(); CacheData[Index] =
Data; if ( Index > Num-1
) Num = Index+1; Build =
Build $
"("; for(i=0;i<Num;i++) { Build
= Build $ CacheData[i]; if ( i != Num-1
) Build = Build $
","; } Build = Build $
")"; SetPropertyText("Data",
Build); bCached = true; }
// Get a cached element function int Get( int Index
) { if ( !bCached
) Recache();
return
CacheData[Index]; }
|
Lazy struct fillingThis example shows how to implement an easy
method of populating a struct or an array.
class TestLazyStruct extends CommandLet;
struct StringGroup { var string
Strings[20]; var int Num; };
// We could use , for the separator, but unfortunatly that messes
up function calls (although that // makes for some interesting
possibilities...) final operator(50) StringGroup : ( StringGroup
A, string B ) { A.Strings[A.Num++] =
B; return A; }
// Group the two initial strings final operator(52)
StringGroup : ( string A, string B ) { local
StringGroup C; C.Strings[C.Num++] =
A; C.Strings[C.Num++] = B; return
C; }
// Test it function int main( string parm
) { local StringGroup
SomeStrings; local int i; // Fill the
struct SomeStrings = ("String1": "AnotherString": "A
third string": "The next one is a variable":
parm); for(i=0;i<SomeStrings.Num;i++) Log(SomeStrings.Strings[i]); return
1; }
|
Implementing a crude LogfAnd finally, we design a simple
formatted log function, using a variation of the technique used above. You
use the : operator to group together a bunch of strings and floats(or
int's), and then pass the resulting struct into a function that'll parse
the format and grab the needed strings/numbers out of the struct.
class TestLogf extends CommandLet;
// This will hold all of our parameters struct
AnythingGlob { var string
StringGlob[20]; var int StringNum; var
float NumberGlob[20]; var int
NumberNum; var int num; };
// Add a string to a glob final operator(98) AnythingGlob : (
anythingglob B, string A
) { B.StringGlob[B.StringNum++] =
A; return B; }
// Add a float to a glob final operator(98) AnythingGlob : (
anythingglob B, float A
) { B.NumberGlob[B.NumberNum++] =
A; return B; }
// Create the glob out of 2 strings final operator(99)
AnythingGlob : ( string A, string B ) { local
AnythingGlob C; C.StringGlob[C.StringNum++] =
A; C.StringGlob[C.StringNum++] =
B; return C; }
// Create the glob out of 2 floats final operator(99)
AnythingGlob : ( float A, float B ) { local
AnythingGlob C; C.NumberGlob[C.NumberNum++] =
A; C.NumberGlob[C.NumberNum++] =
B; return C; }
// Create the glob out of a string and a float final
operator(99) AnythingGlob : ( string A, float B
) { local AnythingGlob
C; C.StringGlob[C.StringNum++] =
A; C.NumberGlob[C.NumberNum++] =
B; return C; }
// Create the glob out of a float and a string final
operator(99) AnythingGlob : ( float B, string A
) { local AnythingGlob
C; C.StringGlob[C.StringNum++] =
A; C.NumberGlob[C.NumberNum++] =
B; return C; }
function int main( string parm ) { local
AnythingGlob Test; Logf( "Testing... with a string
'%s', an int '%i', and a float %f": "Test": 6: 6.4
); Logf( "This commandlet was passed '%s'": parm
); Test = ( "Using prebuilt glob: %s %f %i %s":
"Hello": 3.1415926: 4: "Bye" ); Logf( Test
); return 1; }
// The actual Logf... an anythingglob of parameters function
Logf( AnythingGlob params ) { local int
i; local string Format,FinalS,
Flag; local int sglbnum; local int
fglbnum;
Format =
params.StringGlob[sglbnum++]; for
(i=0;i<=Len(format);i++) { if
(Mid(format, i, 1) ==
"%") { i++; Flag
= Mid(format,i,1); if ( Flag
== "s" ) FinalS =
FinalS $
params.StringGlob[sglbnum++]; else
if ( Flag == "i"
) FinalS = FinalS
$
string(int(params.NumberGlob[fglbnum++])); else
if ( Flag == "f"
) FinalS = FinalS
$
string(params.NumberGlob[fglbnum++]); else { FinalS
= FinalS $
"%"; i--; } } else
FinalS = FinalS $ Mid(format, i,
1); } Log(FinalS); }
|
|