Primer
Be you a grizzled game development veteran or new to the world of programming, this guide will get you started with SkookumScript and touch on all aspects of the language and its tool suite.
Once you have read this Primer, you will have a decent enough grasp of SkookumScript to be ready to experiment and be dangerous. Enjoy!
Initial installation
If you haven’t done so already, now is the time for you to install the SkookumScript Unreal Engine 4 Plugin and SkookumDemo project (includes setting up Unreal Engine 4 if you haven’t already done so and using its world editor).
NOTE Both the SkookumScript UE4 Plugin and UE4 itself can be downloaded as pre-built binaries, or you can build them yourself, which enables you to pull them apart and put them back together according to your liking even with different game engines, as well as contribute fixes and improvements.
Start the Runtime and the SkookumIDE
Ensure that that the the UE4 Editor is up. This may be referred to as the runtime.
If you are using the UE4 Editor, make sure you have a project loaded – we recommend the SkookumDemo project. To launch the SkookumIDE (if it isn’t already up and connected) press the button on the UE4 editor toolbar.
NOTE When using the UE4 Editor, many types of SkookumScript code can only be executed when your project is running – for example any commands that reference game objects or commands that take time to complete. No game objects are created/available when just in UE4 editor mode.
So if it isn’t already running, start your UE4 project. Press the “Play” icon on the UE4 toolbar or press Alt+P in the UE4 Editor to “Play in the editor (PIE)”.
To get mouse control when the game has captured it and you want to use the mouse to click on the Skookum IDE, etc. – press Shift+F1 in the Unreal Editor.
NOTE The SkookumIDE is an independent application. It can be used to edit scripts without being connected to the runtime so it stays up even after the runtime (UE4 Editor) is closed. If it is still up when you restart the runtime, it will just reconnect. If you don’t need it after you shutdown the runtime, you can just close it.
TIP The SkookumScript UE4 Plugin remembers the SkookumIDE state from the last time you launched the UE4 Editor. If the SkookumIDE was up when you shut the UE4 Editor down, the next time you run the UE4 Editor, it will automatically launch and connect to the SkookumIDE. If the UE4 Editor loses its connection with the SkookumIDE for any any reason, just press the button on the UE4 toolbar to launch the SkookumIDE and connect. This will also reconnect to the SkookumIDE if it is already running though not connected to the runtime. If the SkookumIDE is already running and already connected to the UE4 Editor then pressing the button will bring the SkookumIDE to the foreground.
Learn the Workbench basics
First, check out the Workbench widgets which are command consoles in the SkookumIDE that are used to evaluate code snippets on a live running project. Workbenches will be used extensively throughout this Primer.
In particular, make sure to go over the Workbench basics including Printing to the Log and Parser error checking. You can also skip ahead of this Primer a bit and go through all the examples in the Tutorial Workbench. Then come back and continue this Primer.
SkookumScript the language
SkookumScript is a new language. Even though many of its concepts and constructs are pulled from existing languages, it is a unique creation with unusual elements that make effective SkookumScript code different in character from code written in its relatives. Straightforward translation of code from a different language to SkookumScript is unlikely to capture the full potential and delight of SkookumScript code written with its strengths and idioms in mind. Once you have a full grasp of SkookumScript, you are likely to come at solutions to problems from directions you didn’t previously even know existed.
First and foremost, SkookumScript is designed for gameplay logic and interactive concurrent time-flow. Everything about SkookumScript originates from this core goal including many custom constructs built right into the language.
SkookumScript is a true object-oriented embedded programming language which is compiled and efficient. It is statically type-checked with many types inferred and the parser doing as much work for you as it can. The syntax aims to be light and brief with a high signal to noise ratio—to get a lot done in a small amount of code. SkookumScript also takes into account that it isn’t the only tool in your toolbox so it is designed to be complimentary to existing tools (such as world editors and other coding environments), development pipelines and languages (such as C++ and UE4 Blueprints).
SkookumScript looks different from other languages in a few key ways, such as its use of square brackets [ ]
for code blocks, and its use of whitespace between expressions, arguments and list elements instead of special characters or the end of the line. This may make things interesting as you first start to learn the language, though any differences will fade away as you quickly pick up SkookumScript and spend more and more time on what you want to create rather than how you want to create it.
SkookumScript has been developed over many years on numerous projects and studios from little indie games to award winning AAA titles.
We on the SkookumScript team promise that SkookumScript will change the way that you develop and you’ll wonder how you got by without it in the past.
So now that we are done with the intro and the pleasantries, here follows a step-by-step description of the SkookumScript language.
Comments
Before we go deep in the Primer, let’s cover comments since they will be used in many of the examples.
Comments in SkookumScript are similar to C++:
Single-line comments begin at any point on a line with two forward slashes //
and it continues until the end of the line.
Multi-line comments start with a forward slash and then an asterisk /*
and they run until their end which is an asterisk and a slash */
– the reverse of the start. Unlike C++ multi-line comments, SkookumScript comments can be nested inside of other multiline comments. This enables commenting out large swaths of code quickly even if the code already has multi-line comments.
/* This is an example of a multi-line comment.
It can be a single line, multiple lines or used
in the middle of code just like other whitespace. */
if /*like this*/ test?
[
do_this
do_that
]
/* [ML1] Multi-line comments
do_this
/* [ML2] can nest other comment blocks
do_that
do_stuff // Including single line comments
*/ // end of [ML2]
do_other
*/ // end of [ML1]
do_end
No matter how deeply immersed you are in an area now you can be sure that the mists of time will clear much of it away.
Thanks, past me! I forgot and was about to rewrite this code. – Future you
I know, this is the second time we thought to rewrite this code. The first time there was no comment so we forgot the peril and wasted a bunch of time. – Past you
See the comments reference for details on the comment syntax.
Expressions
An expression is a combination of one or more specific commands, queries, values or names that is computed sometimes with side-effects to produce a result value. Expressions are the smallest elements that describe an action in SkookumScript.
Performing the action and doing whatever computation is necessary is called code evaluation or often code execution. In SkookumScript, everything is an expression and all expressions return a result when evaluated.
Simple whitespace (which includes comments) is used to separate / delimit between expressions—or even no whitespace if the separation is obvious to the parser. Expressions do not need an end of expression / statement delimiter symbol (such as ;
, .
or newline)—the SkookumScript parser knows what is and is not a valid expression and it determines where one expression ends and another begins.
Same expressions with different spacing
println("expr1") println("expr2") println("expr3")
println("expr1")
println("expr2")
println("expr3")
println("expr1")
println("expr2")
println("expr3")
TIP Multiple expressions can be grouped together and treated as a single expression using a code block.
There several kinds of expressions:
- Literals
- Identifiers
- Variable Primitives – create temporary and bind
- Invocations
- Type Primitives
- Flow-control
Literals
A literal is a way of writing a specific value for a new object you want to create.
SkookumScript has several different types of literals:
- Integer (
42
,-123
,123_456_789
,2r10101
,8r52
,16rdeadbeef
) - Real (
0.0
,3.14159
,.5
,-.33
,7.5e-8
) - Boolean (
true
,false
) - String (
"Hello, world!"
,"first" + " second"
,"Line 1\nLine2"
) - Symbol (
'idle'
,'playing bongo'
,'eating sandwich'
) - List (
{1 2 3}
,{1 "two" 3.0}
,String{}
) - Closure (
()[do_stuff]
,10.do[do_stuff]
)
Integer
You can’t get far in a programming language without numbers.
Integers are whole numbers without a fractional part and they can be positive, zero or negative.
NOTE The placement of the minus -
symbol determines whether it applies to a negative integer literal -42
, a negated operator -num
or a subtract operator num - 42
—see Subtract operator vs. negative number or negated.
See the full Integer syntax here.
Also check out the Integer
class in the SkookumIDE by selecting it using the Classes widget and look at its members in the Members widget. See the common operators sections for assignment, math and comparison.
Specifying a radix (or base)
These integers are all in decimal which is radix (or base) 10.
In programming, it is often handy to be able to use a different radix than than the default radix 10. To specify a particular radix, prefix the radix you want to use followed by an r
and then digits or letters that are valid for that radix that represent the number you want.
// These all are the same as:
42 // Decimal
2r10101 // Binary
8r52 // Octal
10r42 // Decimal - redundant though valid
16r2a // Hexadecimal
21r2 // radix 21 - sure why not?
36r16 // Haxatrigesimal - largest allowed radix
Digit grouping separator
When you have long numbers, it can be handy to mark groups of thousands (or however you want) so you don’t get lost in digits. SkookumScript allows you to do this with optional underscores _
between digits.
123_456_789
The underscore is a common digit separator used by several languages including C#, Swift, Ruby, Python, Java, Perl, Ada and Eiffel. Space, comma ,
or single quote '
cannot be used to mark groupings since they are used for other language syntax elements.
Real
Real literals are used to create numbers that include a fractional part (real numbers) and they are represented by objects of the class type Real
. (Real
objects use the traditional floating-point mechanism shared with similar types often known as float
or double
in other languages such C++.)
42.0
0.0
-123.0
3.14159
// If the leading part is zero you can omit it
.5
-.33
See the full Real syntax here.
Also check out the Real
class in the SkookumIDE by selecting it using the Classes widget and look at its members in the Members widget. See the common operators sections for assignment, math and comparison.
E-notation
Real values can also be written using E-notation as a shorthand for Scientific notation (m * 10n).
E-notation: real value e
power of ten
4.2e1 // 42
1e6 // 1 000 000
-1e6 // -1 000 000
7.5e-8 // 0.000 000 075
-7.5e-8 // -0.000 000 075
Expected number class types
If the parser expects an Integer
value to be passed as an argument, then it will not accept a Real
value as an argument or vice-versa.
This gives the error:
The argument supplied to operator parameter num
was expected to be an object of the type Integer
and was given the type Real
which is not compatible.
SkookumScript does not coerce (automatically convert) from one type to another. (Though you can explicitly convert from an Integer
type to a Real
type using the conversion operator 2>>Real
or calling the conversion method 2.Real
. We will go into details of conversion later.)
However, if the parser knows that a Real
type is expected and if the real value that you want to supply happens to have no fractional part (such as -1.0
, 0.0
, 1.0
, 123.0
) then you can omit the .0
. So even though it looks like an integer literal, it is actually a real literal (such as -1
, 0
, 1
, 123
). This is not coercion—the parser just automatically adds the .0
on for you and treats it as a Real
type.
The 2
below is a real literal since the +
operator on 40.0
, which is a Real
type, also expects the operand (the value passed to the operator) to be a Real
type.
The parser sees the 2
and realizes that what you really mean is 2.0
.
It is nice to be lazy when you can. The SkookumScript parser tries to understand what you want and has your back.
Boolean (true
and false
)
Numbers are great, though what would a programming language be without logic?
SkookumScript has a Boolean literal with two possible values: true
and false
. They are both of the single class type Boolean
.
Boolean values can be used to store logical states and they are especially useful when working with conditional expressions such as if
, when
and unless
that allow changing the control flow of code. More details about conditional expressions can be found here.
Also check out the Boolean
class in the SkookumIDE by selecting it using the Classes widget and examine its members in the Members widget. In particular, note the Boolean logical operators and
, or
, xor
, nand
, nor
, nxor
and not
that are frequently used with Boolean values and their associated methods.
Converting to and from Boolean values
Unlike some other languages, in SkookumScript only false
is accepted as a negative truth value – no other type or value such as 0
, nil
, empty string or empty list is implicitly accepted or coerced as a false value. Similarly, true
is the only accepted positive value – no other values are implicitly accepted as a true value.
If you really need to however, you can explicitly convert from a Boolean
type to an Integer
type using the conversion operator true>>Integer
or calling the conversion method true.Integer
– and the reverse 0>>Boolean
or 0.Boolean
. You can also call obj.not_nil?
to convert something that could be nil
to false
when nil
or to true
otherwise. Likewise you can call list.empty?
or list.filled?
.
String
Zero or more characters surrounded by double quotes "
is known as a string literal and it creates a String
object. The string itself is the characters between the double quotes.
Strings can span more than one line, in which case they will store the newline character for the end of each line.
One or more string literals with a concatenation operator +
between them will be parsed as a single string. The concatenation operator is necessary since the parser will otherwise consider the adjacent strings to be separate string literals that create separate string objects.
String internals
Strings are internally stored as reference counted null-terminated (0) arrays of ANSI characters (to ensure compliance with native C++). Copying from one string to another is quite efficient since they only copy a pointer to a shared string reference structure and increment a reference count.
The internal representation will likely be stored in a Unicode compliant mechanism in the future.
See the full string syntax here.
Also check out the String
class in the SkookumIDE by selecting it using the Classes widget and look at its members in the Members widget. See the common operators sections for assignment and comparison.
Escaped special characters
The backslash \
is used to escape (or substitute) special characters in a string (or symbol) literal. The most commonly used special character is probably the newline \n
—expect to see it everywhere.
Characters other than the specials of a
, b
, f
, n
, r
, t
and v
just pass right through unchanged such as quotes \'
\"
and the backslash itself \\
.
Escape | Character substitution | Hex |
---|---|---|
\a | bell/alert | 07 |
\b | backspace | 08 |
\f | formfeed | 0c |
\n | newline | 0a |
\r | carriage return | 0d |
\t | tab | 09 |
\v | vertical tab | 0b |
\integer-literal | ANSI character | from integer |
\? | resolves to itself such as \' and \" |
— |
When an ANSI character code is specified using \
and an integer literal, it can specify a radix so the number can be in the form easiest for the situation: decimal, hexadecimal, etc.
These all specify the asterisk *
character:
Here is an example that includes double quotes and newlines:
Symbol
Symbols are like a hybrid between strings and enumerations (or integers). They can be just about any sequence of characters like a string though they are as simple and as efficient as enumerations internally.
They look similar to a string though they use single quotes and they are limited to no more than 255 characters.
Symbols are what SkookumScript uses to store all of its internal representations for the names of aspects language.
See the full symbol syntax here.
The UE4 equivalent for Symbol
is often the C++ FName
class which is called the Name
class in SkookumScript.
Also check out the Symbol
class in the SkookumIDE by selecting it using the Classes widget and look at its members in the Members widget. See the common operators sections for assignment and comparison.
List
Lists group together objects so they can be treated as a single List
object. List objects (also sometimes called collections or arrays in other languages depending on their implementation) store zero or more objects referred to as items in an ordered, indexable (they can be retrieved by their position number starting at zero) group that can dynamically change in length. The same object can simultaneously be in several positions in the same list.
A list literal makes a List
object using a series of expressions surrounded by curly brackets {
}
(also known as braces) marking the beginning and the ending of the list. Items are separated by whitespace. You can alternatively separate items with a comma ,
though they are optional (just as they are with routine arguments) and should only be used if they aid readability.
Here are some simple list literals:
Each item expression is executed in order and the resulting object is added to the list as an item.
Lists can have more than one item class type.
Simple lists infer the item class type by examining the items in the list literal.
If you want to have an empty list that will use a particular item class type, the type must be specified using a mechanism such as an item typed list.
The syntax of list literals can be simple or tricky depending on the class type of the list, the class type(s) of the items in the list and whether any initial items are present. See the full list reference here for all the details.
Also see some examples of using lists here, check out the List
class in the SkookumIDE by selecting it using the Classes widget and look at its powerful and useful members in the Members widget.
Closure: the basics
Closures are a sophisticated and powerful construct that touch on more language concepts than we’ve gone over in the Primer to this point, so for now we’ll only go over their basics. They are a literal so they are included here for completeness, though we’ll detail them later after their building block concepts have been introduced.
A closure literal (also called closures in Swift and known as lambda expressions in C++ or blocks in Ruby and Smalltalk) is essentially a series of expressions grouped together like an anonymous (without a name) routine that also captures (closes around) references to any variables that were created in the surrounding context (outside of the group of expressions). This allows you to treat code as an object so you can pass code to and return from routines and delay its evaluation until needed. This allows callbacks, custom control flow, and all manner of amazing and zany concepts.
Related concepts in other languages go by many names including: lambda functions, anonymous functions, function literals, function objects and delegates. These do not necessarily have all the same features as closures though they are similar.
Some examples of closures:
This is just a taste—don’t worry about having full mastery of closures yet. See Closure invoke operator to learn how to call closures.
See the full closure reference here.
Identifiers
SkookumScript uses names called identifiers to store and refer to values and objects.
SkookumScript is an object-oriented language and all values are objects so the terms value and object may be used interchangeably. We will cover the details of the object-oriented aspects of SkookumScript later in the Primer.
There are several types of identifiers:
- variables including temporary variables and parameters (
value
,bad_guy
,trigger_region4
,success?
) - class identifiers (
Object
,String
,Integer
) - reserved identifiers (
nil
,this
,this_class
,this_code
,this_mind
) - data members (
@hit_points
,position.@x
,@@random
,BefuddledEarthling.@@heard_vogon_poetry_count
) - object IDs (
@'RoboChar1'
,Enemy@'Bad Bart'
,BefuddledEartling@?'Arthur'
,Trigger@#'Trigger by door12'
)
Variables
A temporary variable (also known as a local variable) is explicitly declared (created) by using an exclamation mark !
followed by the identifier name to call it. This is called a create temporary expression.
This tells the parser that the variable my_variable
can be used to reference a value and to remember it.
Think of it like a roll call that shouts (!) a new variable name into existence.
The !
in a SkookumScript create temporary expression is similar to the auto
keyword in C++, the var
keyword in C#/Javascript/Swift, :=
in Go or local
in Lua. Declare a local temporary variable and implicitly infer the class type.
The variable identifier name must start with a lowercase letter and then any combination of upper and lowercase letters, numbers and underscore characters _
. If an identifier name will be referring to a Boolean
value (true
or false
), then the name may also end with a question mark ?
—such named variables are called predicate (or alternatively query) variables.
The SkookumScript variable standard naming convention uses snake_case (also known as C naming style) with all lowercase letters and words separated with an underscore character.
var
, bad_guy
, trigger_region4
, success?
Other styles can be used as desired to match the convention of your project or company.
NOTE A create temporary expression may not be used as a send argument though it can be used as a return argument.
Once a variable is created, it is used without the exclamation mark and evaluates to whatever object that it refers to.
TIP When you evaluate a temporary variable, it only exists in the scope where it was created—such as the routine or closure. This is also the case for interactively evaluating code snippets in Workbench widgets. The variable from a create temporary will only be remembered by the parser for that one evaluation. If you evaluate a new code snippet afterwards, any previous variables are forgotten. It will also need a create temporary in order to use a variable with the same name and the value of the variable will not be remembered from previous evaluations.
Parameter variables look and act just like temporary variables, though they come from the parameters of a routine or closure.
Initial binding
Variables are always bound (refer) to an object. If no object is initially specified to be bound to, then a variable will automatically bind itself to the global nil
object.
NOTE In SkookumScript, even simple types such as Integer
and Boolean
are always stored as objects, and bound by reference. Unlike some languages such as C#, SkookumScript does not store primitive value types and object types differently—everything is an object.
If you evaluate the following create temporary, the result will be nil
. Likewise if you evaluate the my_variable
variable on its own.
The create temporary can also specify an initial object to bind to. To bind an initial object, follow the create temporary with a bind operator (a colon :
) and an expression.
Most of the symbols used in SkookumScript come from or are inspired from other languages. The use of a colon :
to bind a variable with its data originates from key / value pair mappings in YAML, which took it from JSON, which was inspired from REBOL.
The initial object bound to a parameter variable is the argument that is passed into the routine or closure when it is called.
The initial class type of a variable comes from the object that it is bound to.
The class type is inferred by the SkookumScript parser, it does not need to be explicitly specified.
Rebinding
A variable can be rebound to other objects at any point.
In the above example, the type of the variable was String
and then it was changed to a different object that also had the String
type.
A variable can be bound to objects with different types too and the SkookumScript parser automatically keeps track for you.
In the above example, the type of the variable was String
and then it was changed to a different object that had the Integer
type.
Predicate variables may only be set to Boolean
values. Any attempt to set a predicate variable to a non-Boolean
will have a parse error.
This will give the error:
Tried to bind to type Integer
when Boolean
was expected!
Query/predicate temporary variables ending with a question mark may only be bound to a Boolean
true
/false
expression.
NOTE A bind expression may not be used as an argument—only a named argument may be used which looks similar.
Class Identifiers
Class identifiers are just the name of a class and the result of their evaluation is a class object. Class objects can be stored in variables and passed around just like any other object. Every class in SkookumScript is unique and globally accessible.
A class name must start with an uppercase letter and then any combination of upper and lowercase letters, numbers and underscore characters _
.
The SkookumScript class standard naming convention uses PascalCase (also known as upper camel case) with each word capitalized and all other letters lowercase. Special classes used for generic programming (types to be specified later) end with an underscore.
String
, InvokedBase
, AIPerceptionSystem
, Vector3
, ThisClass_
Other styles can be used as desired to match the convention of your project or company.
Reserved Identifiers
Reserved identifiers are built-in names that refer to common objects. They look like regular variables and are used similarly too though they are always present and cannot be used for identifier names for temporary variables or parameter names.
nil
: the single instance of theNone
class. It is used to represent nothing or the absence of a value. (It goes by several different names in other languages, such asNULL
(C),nullptr
(C++), andnull
(C#/JavaScript).)this
: the current / reciever object that is running the current code. (Some languages use the identifierself
orMe
.)this_class
: the class of the current object.this_code
: the invoked code object that represents the currently running code. Useful for metaprogramming. See theInvokedBase
class, its subclasses and its members in the SkookumIDE to see the calls that can be made on it.this_mind
: the mind object that is updating the current routine. See Mind objects and the Master Mind.
Data members
Data members are subcomponents of objects (known as data structures in non-object-oriented languages) that are essentially variable identifiers that belong to an object.
We haven’t gone into much of the details of SkookumScript object-oriented programming at this point in the Primer, so we’ll just mention how data members are used rather than how they are created.
Instance data
Object (or class instance) data members belong to a single object and start with an a single at symbol @
followed by the same characters used for temporary variables: @x
, @hit_points
, @heard_vogon_poetry?
All instance data members are part of an object and you can specify their owner object with an expression followed by a dot: position.@x
, this.@hit_points
, arthur.@heard_vogon_poetry?
If you want to use the object from the current scope, it can be omitted and this.
will be inferred. So @hit_points
is the same as this.@hit_points
.
Class data
Class data members belong to a whole class of objects including the class object itself and start with two at symbols @@
followed by the same characters used for temporary variables: @@random
, @@hit_point_max
, @@heard_vogon_poetry_count
All class data members are part of a class and you can specify their owner class with an expression followed by a dot: Object.@@random
, this_class.@@hit_point_max
, BefuddledEarthling.@@heard_vogon_poetry_count
If you want to use the class from the current scope, it can be omitted and this_class.
will be inferred. So @@max_hit_points
is the same as this_class.@@max_hit_points
.
All instance objects have access to their class data members too. Class data members can be referenced from instance objects: guy.@@hit_point_max
, arthur.@@heard_vogon_poetry_count?
. Class data members can also be accessed directly from within instance routines: @@hit_point_max
, @@heard_vogon_poetry_count?
See the Data Members reference page to learn more about data members including their declaration, initialization and use.
Object IDs
Object IDs are a feature unique to SkookumScript—they allow game / simulation objects, concepts and assets to be referred to by a combination of class type and object name in code.
Object IDs can be used for game characters, trigger regions, sound effects, user interface elements, mission names, cars, cutscenes, props, doors, geographical locations, art assets, game events, AI / behaviour types, animations or anything that you can associate a name to.
Object IDs start with an optional class name, next an at symbol @
(and a question mark ?
for a potential reference or a hash mark #
for an object ID identifier) then a symbol literal for the lookup name of the object.
Also see the full Object ID reference.
Reference @
An object ID using @
returns the named object and has a runtime error if the object cannot be found.
Potential Reference @?
An object ID using @?
returns the named object or nil
if the object cannot be found.
NOTE Since this could be either an Enemy
object or nil
, the class type is a union class <Enemy|None>
which we will go over in more detail later in the Primer.
Identifier @#
An object ID using @#
returns the identifier for a named object in the type most appropriate for the runtime.
The UE4 identifier class type is Name
in SkookumScript and Blueprints (FName
in C++), so that is the result type for object IDs when using UE4.
So Enemy@?'RoboChar42'
is essentially the same as 'RoboChar42'.Name
Invocations
Coding is about as close as you can get to actual magic—where the exertion of imagination and the arrangement of the correct wording brings pure thought-stuff into being. And like magic, the lifeblood of coding is invocations.
invocation (noun)
- Magic. the incantation or formula used to conjure up a magical entity for aid, protection, inspiration, or the like
- Computing. a subroutine call
An invocation or invoking a routine is also known as running, calling or executing a routine. Routines are a sequence of one or more expressions that perform a specific task, packaged as a unit.
Like any expression, all routines return a result object.
SkookumScript has two types of routines:
Methods
Methods are routines that are immediate (essentially instant and take no time) returning within the same update frame.
“Frames” are a common unit of time in SkookumScript since they are usually the main time driving mechanism in games and simulations. All the work done within a frame obviously takes up time (precious milliseconds!), though for a simulation all the work within a frame is usually best conceptualized as taking place within the same frozen instant of time.
Each frame, taken as a whole of all its work takes up time—which for many games is one frane per sixtieth of a second (1/60).
Method names are similar to variable identifier names—they must start with a lowercase letter and then any combination of upper and lowercase letters, numbers and underscore characters _
.
Methods can return any type of object. If a method has a Boolean
(true
or false
) result, then the method name may also end with a question mark ?
—such methods are called predicate (or query) methods.
The SkookumScript method naming convention uses snake_case (also known as C naming style) with all lowercase letters and words separated with an underscore character.
print
, path_stop
, some_long_method_name
, empty
, empty?
Other styles can be used as desired to match the convention of your project or company.
There are also special methods that have exceptions to this naming, including constructors (start with a single exclamation mark !
then optional identifier: !
, !copy
, !xyz
), destructors (two exclamation marks !!
), conversion calls (same name as the class being converted to: String
, Integer
, Name
) and operator calls (+
, *=
, =
, >=
, --
). See the method name syntax and “Adding a new routine” in the New Class or Member pane.
Coroutines
Coroutines are routines that are durational (may take time) returning either within the same update frame (like a method) or one or more update frames later.
They are the lifeblood of SkookumScript and are different in their design and use than what other languages may also call coroutines. The term “coroutine” was used since it is the computer science term most closely resembling this powerful SkookumScript routine type.
Coroutine names are similar to method names though they must start with an underscore _
and then like a method, a lowercase letter and then any combination of upper and lowercase letters, numbers and underscore characters.
TIP The initial underscore helps make calls that take time stand out at a glance in SkookumScript code.
The SkookumScript coroutine naming convention uses an initial underscore then snake_case with all lowercase letters and words separated with an underscore character.
_do
, _wait
, _play_sound
, _path_to_actor
Other styles can be used as desired to match the convention of your project or company.
Invoking a coroutine results in an InvokedCoroutine
object which stores the state of a (running or completed) coroutine. Manipulating such objects is fairly advanced so we won’t get into their details here.
There is much more about coroutines that we will get into later in the primer, though for now think of them as sophisticated methods that time.
Calling routines and passing arguments
To Invoke or call a routine, specify the name of the routine to call followed by any arguments that you want to pass along to the routine in parentheses ( )
. If there are no arguments to pass along, then the parentheses are optional.
Invocations with no arguments
Some pseudocode showing invocations without any arguments.
Remember that all routine calls are on objects, so the above are the same as:
They could be called on other objects too:
Here are some actual zero argument calls. You can evaluate their individual lines in the Workbench widgets in the SkookumIDE.
Invocations with arguments
As an argument expression is encountered, it is evaluated and its resulting object is passed by reference to the routine—only the memory address of the object is passed to the routine rather than a new copy of the object. The class type of the argument result object must be compatible with the desired type of the routine’s parameter matching that argument.
Some pseudocode showing invocations with a single argument:
Some actual single argument calls:
TIP While the SkookumIDE cursor is inside the brackets of a routine, it will supply pop-up hints on the names, class types and any default values of arguments and mark the current argument the cursor is on or about to type.
If an argument passed to a routine is not a match for the expected type of the routine, you will get a parser error.
This will have the parser error:
The argument supplied to parameter named seconds
was expected to be of type Real
and it is type String
which is not compatible.
Some routines take two or more arguments which are separated with whitespace. A combination of whitespace and a comma may also be used to separate arguments.
SkookumScript standard style is to prefer whitespace separation over commas unless the use of commas is more readable in a particular context.
Some pseudocode showing invocations with two arguments:
Arguments are evaluated in the order in which they are encountered in the code—left to right and top to bottom. So in the examples above, the arg1
expressions will always be evaluated before the arg2
expressions.
Routines may have any amount of arguments from zero to hundreds, though more than a few can be unwieldy.
Some pseudocode showing invocations with three and more arguments:
Group Arguments
Routines can have group parameters which allow zero or more arguments to be passed into that single parameter. The arguments are stored in a List
with the parameter name when they are used in the code bode of the routine. Arguments that match a group parameter are called group arguments. A call to a routine with group parameters will accept as many group arguments to that parameter once the first argument in that index position matches the expected class type and all the arguments following will also be grouped together until there are no more arguments or there is an argument that does not match.
A good example of a routine with group arguments is the println()
method that we have already been using. The println()
method takes any number of arguments, converts them all to strings, concatenates them all together, adds a newline and then prints it all out.
We aren’t covering routine parameters yet, though here is what the parameters for println()
, which uses a group parameter, looks like:
It shows that it expects zero or more arguments of the Object
class (all classes match with Object
since it is the superclass of all classes) and to put them all in a List
object named objs_as_strs
.
Also see the List
methods appending()
, assigning()
, inserting()
, intersecting_same()
, matching_same()
, removing_same()
, unioning_same()
which all use group arguments.
Default parameters and
omitting arguments
Each parameter in a routine can have a default argument expression. Any such parameter can have an argument passed to it like normal or it can be omitted and the default argument will be used instead. This is useful for arguments that often have a common value that would be used. Default values are also used for niggly detail arguments that rarely need to be specified.
For example, the _wait()
coroutine has a seconds
parameter that has a default argument of 0.0
:
So you can call it with an argument like normal:
Or you can omit the argument and let it use the default argument:
Any omitted trailing arguments will try to use default arguments to fill the gaps.
Let’s say there is a hypothetical method2()
that has two default arguments:
SkookumScript also allows you to skip default arguments. You simply use a comma ,
to mark where an argument would have been and then follow with any arguments that you do want. If there is an argument prior to a skipped argument then the prior argument must be followed by a comma too.
So for further examples, assume hypothetical methods method0()
, method1()
, method2()
and method3()
with 0, 1, 2 and 3 default arguments respectively. Here are a bunch of the different ways you could call them:
Named arguments
You can also specify arguments by the parameter name you want to match them to during an invocation using named arguments. These can be especially handy with invocations that have many default arguments and you want to use most defaults except for one or two arguments at the end (far right). In most languages you would be required to specify all the arguments before the ones that you want to specify. In SkookumScript, simply specify the name of the parameter to match, follow it with a colon and then the argument expression. This looks very similar to variable rebinding. Once a named argument is used, any arguments that follow must also be named arguments.
Imagine a hypothetical method5()
:
Here is how you could call it with all the first four parameters using defaults and you want param5
to be different than the default:
NOTE Any named arguments are evaluated in their matching parameter order and not necessarily the order that they appear in the call.
Return arguments
All invocations are expressions and all expressions immediately return a result. The overall single result object returned by an invocation is called the primary result. Sometimes it can be useful to have more than one object returned by an invocation and one of the mechanisms that SkookumScript provides for this is return arguments. Return arguments follow any initial send arguments and a semicolon ;
.
Send arguments pass a reference to an object into a routine or closure and return arguments pass back a reference to an object and bind it to a variable or data member. The binding of a return argument is only done after an invocation has completed, which could be after multiple update frames for a coroutine.
Any object that a return argument variable was bound to before the invocation has completed will be replaced with a new binding after the invocation has completed.
If the first time a variable is used is with a return argument, its create temporary expression can be made right in the location of the return argument. Just add an exclamation mark—it’s a nice shorthand.
method(send_arg1 send_arg2; !return_arg1 !return_arg2)
If return arguments did not exist you could use send arguments to get results back though it is much more convoluted. You must first create some objects to pass into a routine by reference, then in the called routine these objects will have modifying routines called on them or they will be modified using an assignment, then after the called routine completes you can use them.
For example the Actor@eyes_view_point()
routine is written with send parameters:
Returned send arguments
// Actor@eyes_view_point() using send parameters
(Vector3 out_location, RotationAngles out_rotation)
// Here is how it would be called and used:
// Make some default location and rotation objects
!loc: Vector3!
!rot: RotationAngles!
// Call routine that modifies the objects
actor.eyes_view_point(loc rot)
// Use them
do_stuff(loc rot)
It would be much cleaner if written with return arguments:
// Actor@eyes_view_point() using return parameters
(; Vector3 out_location, RotationAngles out_rotation)
// Here is how it would be called and used:
// Call routine that creates the objects
// and binds to loc and rot
actor.eyes_view_point(; !loc !rot)
// Use them
do_stuff(loc rot)
Here are some working examples of return arguments:
Prints out:
found?:true idx:5
nil
And this:
Called a few times randomly prints out:
name:Zaphod idx:2
nil
name:Trillian idx:3
nil
name:Ford idx:1
nil
name:Trillian idx:3
nil
name:Arthur idx:0
nil
Closures as arguments
Closures can be passed as arguments just like any other object.
If the desired type of a closure is known from the surrounding context, then its parameters can be inferred. You still need to differentiate between a closure and a simple code block, so if you omit the parameters, the closure needs to start with a closure receiver caret ^
.
TIP Occasionally, it can be a bit mystifying to know where parameter names come from if parameters are inferred, though you can just go to the definition of a routine using Alt+G to see them. Many parameter names are the same across different routines (such as idx
or item
) and after a while you will get familiar with their use.
Inferring closure parameters saves a lot of typing and can simplify the code making it easier to read.
TIP Sometimes, especially with nested closures and loops, you may not want the parameters for closures to be inferred—see how to change the inferred item variable name in a do loop in this forum post.
Closure tail arguments
If a closure is the last parameter of the send parameter list (the tail) then the invocation brackets are not necessary. This mechanism for an invocation is called closure tail arguments. This helps remove the clutter of many brackets and makes code more readable.
If there are send arguments before the tail closure they are placed between the routine identifier name and the closure. The routine identifier acts as the beginning bracket and the closure acts as the ending bracket.
If closure tail argument invocation needs to use return arguments, a semicolon ;
is placed after the closure and is followed by the return arguments.
TIP You can build many of your own routines that look like native language primitives using closure tail arguments. To get ideas, you can see a few example routines that use closures as their last argument by using the Skookum IDE Members widget and search for routines that have “do” in their name. You can examine matching routines to see how they are written and most of them have example code of how they are called in their comments.
Closure tail arguments are nifty, clean and powerful and they are a language construct fairly unique to SkookumScript.
As a bit of trivia, the Swift programming language has a similar feature called trailing closures. SkookumScript originally had it implemented on November 21, 2013 and Swift was publicly released on June 2, 2014 so it must be an example of convergent evolution in language design or great mad computer scientists think alike.
The Ruby language has anonymous blocks and block arguments that also work similarly.
Binary and unary operator calls
Operators are constructs that act like routines though they differ a bit in their form and sometimes their semantics. All operators are syntactic sugar for regular method call equivalents.
There are three forms of operators:
- infix binary operators:
expr1 op expr2
Whereexpr1
is the receiver object the operator method is applied to andexpr2
is the argument operand that is passed to the operator method. - prefix unary operators:
op expr
Whereexpr
is the receiver object the operator method is applied to. - postfix unary operators:
expr op
Whereexpr
is the receiver object the operator method is applied to.
Operand expressions
Both binary and prefix operators take as much of an operand expression following the operator as possible. In other words, even if a smaller part of an expression could stand on its own as a single expression, the parser will instead make a larger single compound expression if there is more code that can be part of the expression. If you want to group operators and expressions in some particular way use code blocks [ ]
to group expressions and specify order.
For example:
There are four major categories of operators:
- assignment (
:=
) - math (
+
,+=
,-
,-=
,*
,*=
,/
,/=
, prefix-
, postfix++
, postfix--
) - comparison (
=
,~=
,>
,>=
,<
,<=
) - Boolean (
and
,or
,xor
,nand
,nor
,nxor
, prefixnot
)
Assignment operator :=
Assignment is a binary operator :=
used to copy the logical value from the object on the right hand side to the logical value of the object on the left hand side.
How this is precisely done depends on the class type of the object and how its assignment method is written. Assignment methods typically return the receiver object so that they may be chained together (also called stringization).
The assignment operator is syntactic sugar for the assign()
method.
See the assign()
method in classes that allow assignment to learn any specific information for a class type.
SkookumScript uses :=
for assignment like the languages Smalltalk, Pascal, ML, ALGOL, Simula, Eiffel and others rather than =
like the C family of languages and others. For a more in-depth look on this topic, see the Wikipedia articles Assignment versus equality and Confusion with assignment operators.
When to use assignment :=
or binding :
In some ways, assignment :=
and binding :
are very similar and when to use one or the other can be confusing.
- Assignment
:=
- copies the contents of an object to a different object usually of the same class type.
- The object currently referenced by the identifier being assigned to must be already constructed and compatible for the assignment.
- Multiple identifiers can reference the same object and if its contents are changed with an assignment then all the identifiers that reference that same object will be affected.
- Binding
:
- changes the object reference (the address or pointer in memory) the identifier refers to which may be a different class type.
- The old object previously referenced by the identifier being bound is unaffected (unless this was the last reference for the old object in which case it will be destructed and freed–we will go into more detail on this later).
- Other identifiers that continue to reference the same old object before a bind are not affected—they just keep on referencing the old object.
Big objects
Big complex objects such as characters (NPCs) in a game usually cannot copy all their contents (name, position, hit points, animation, etc.) to a completely different character so they tend to only ever use binding to copy a reference from one identifier to another.
Big object assign vs. bind
!bob: GoodGuy@'Bob'
!target: BadGuy@'Bad Bart'
// Has player glare at target (Bad Bart)
player._glare_at(target)
// Copy info from Bob and store in Bad Bart
// Depending on game engine integration
// probably will not work and be an error
target := bob
// Have target reference Bob
target: bob
// Has player glare at target (Bob)
player._glare_at(target)
Small/value objects
Setting a variable to a small object such as an Integer
object often could use either an assignment or a bind. Both are fairly quick and simple.
Small/value object assign vs. bind
!jump_count: 1 // obj1
// Has player jump jump_count (1) times
jump_count.do[player._jump_up]
jump_count := 2 // obj2
// Copies 2 value from obj2 into obj1 and discards obj2
// Has player jump jump_count (2) times
jump_count.do[player._jump_up]
jump_count: 42 // obj3
// Rebinds jump_count to obj3 (42) and discards obj1
// Has player jump jump_count (42) times
jump_count.do[player._jump_up]
So in this case, both the assignment jump_count := 2
and the rebind jump_count: 42
achieved the same end goal—the value referred to by jump_count
changed.
This only really becomes interesting when the same object reference is shared by multiple identifiers.
Shared objects and assign vs. bind
!jump_height: 100.0 // obj1
!low: 50.0 // obj2
!high: 200.0 // obj3
// Has player jump by jump_height (obj1: 100.0)
player._jump_up(jump_height)
// Copy info from `low` (obj2: 50.0)
// and store in `jump_height` (obj1)
jump_height := low
// jump_height still bound to obj1
// which is overwritten with 50.0
// obj2 is also still 50.0
// Has player jump by jump_height (obj1: 50.0)
player._jump_up(jump_height)
// Change jump_height (obj1) contents to 75.0
jump_height := 75.0
// low (obj2) is still 50.0
// Has player jump by jump_height (obj1: 75.0)
player._jump_up(jump_height)
// Rebind `jump_height` (was bound to obj1)
// to object bound to `high` (obj3: 200.0)
jump_height: high
// obj1 is no longer referenced
// `jump_height` is now was bound to obj3: 200.0
// Has player jump by jump_height (obj3: 200.0)
player._jump_up(jump_height)
// Change high (obj3) contents to 150.0
high := 150.0
// jump_height (obj3) is bound to the same object and also 150.0
// Has player jump by jump_height (obj3: 150.0)
player._jump_up(jump_height)
So changing an object that is referenced by multiple identifiers will change them all. Often this is a desired behavior, though sometimes it is unexpected and not desired.
Math operators
You will find standard math operators in classes such as Integer
and Real
, though they can be in any class.
Operator | Method | Example Op | Example Method |
---|---|---|---|
+ |
add() |
num + 2 |
num.add(2) |
+= |
add_assign() |
num += 10 |
num.add_assign(10) |
- |
subtract() |
num - 2 |
num.subtract(2) |
-= |
subtract_assign() |
num -= 10 |
num.subtract_assign(2) |
* |
multiply() |
num * 2 |
num.multiply(2) |
*= |
multiply_assign() |
num *= 2 |
num.multiply_assign(2) |
/ |
divide() |
num / 2 |
num.divide(2) |
/= |
divide_assign() |
num /= 2 |
num.divide_assign(2) |
- |
negated() |
-num |
num.negated() |
++ |
increment() |
num++ |
num.increment() |
-- |
decrement() |
num-- |
num.decrement() |
NOTE There is no prefix version of increment ++num
or decrement --num
so the postfix versions of these operators typically, such as with the Integer
and Real
classes, make any changes (increment/decrement) and then return the changed object.
NOTE Like the Smalltalk language, math operators do not have a precedence order in SkookumScript. Use code blocks [ ]
to group expressions and specify order.
Subtract operator vs.
negative number or negated
Since SkookumScript does not need any sort of delimiter between expressions, it is possible to confuse these two possible uses of the minus -
symbol:
- a single expression comprised of one expression subtracting a second expression
expr1 - expr2
- two separate expressions with the second expression being a negative number
-42
or an expression using the negated prefix operator-expr
The SkookumScript syntax follows the rule-of-thumb that most humans would use visually:
In order to be recognized as single subtract -
operator expression and not an expression followed by a second expression starting with a minus sign, the minus symbol -
must either have whitespace following it or no whitespace on either side.
So for example:
Comparison operators
The comparison (or relational) operators are used to determine logical or structural equality and order and they all return true
or false
. They are used in classes such as Integer
, Real
, String
and Symbol
though they can be in any class.
Operator | Method | Example Op | Example Method |
---|---|---|---|
= |
equal?() |
num = 42 |
num.equal?(42) |
~= |
not_equal?() |
num ~= 42 |
num.not_equal?(42) |
> |
greater?() |
num > 42 |
num.greater?(42) |
>= |
greater_or_equal?() |
num >= 42 |
num.greater_or_equal?(42) |
< |
less?() |
num < 42 |
num.less?(42) |
<= |
less_or_equal?() |
num <= 42 |
num.less_or_equal?(42) |
SkookumScript uses =
to test for logical equality which is the same as the international mathematical standard, Smalltalk, Pascal, ML, ALGOL, Simula, Eiffel and others rather than ==
like the C family of languages and others. For a more in-depth look on this topic, see the Wikipedia articles Assignment versus equality and Confusion with assignment operators.
Likewise, ~=
is used for not equal which is same as Smalltalk, Lua, and MATLAB rather than !=
. This is partly because !
in SkookumScript generally means “create” rather than “not”.
TIP To compare physical equality (whether two expressions refer to the same object—in other words their memory addresses are identical) the same?()
method is used. For example: obj1.same?(obj2)
For comparison in other languages, testing if references are identical/equivalent in C++ uses &obj1 == &obj2
, JavaScript and Swift use obj1 === obj2
, C# uses object.RefrenceEquals(obj1, obj2)
, Python uses obj1 is obj2
and Ruby uses obj1.equal?(obj2)
.
Boolean operators
You will find standard Boolean (or logical) operators in the Boolean
class, though they can be in any class.
Operator | Method | Example Op | Example Method |
---|---|---|---|
and |
and() |
test1? and test2? |
test1?.and(test2?) |
or |
or() |
test1? or test2? |
test1?.or(test2?) |
xor |
xor() |
test1? xor test2? |
test1?.xor(test2?) |
nand |
nand() |
test1? nand test2? |
test1?.nand(test2?) |
nor |
nor() |
test1? nor test2? |
test1?.nor(test2?) |
nxor |
nxor() |
test1? nxor test2? |
test1?.nxor(test2?) |
not |
not() |
not test? |
test?.not |
TIP Be particularly attentive with the not
operator when grouping operand expressions—they take as big of an expression as possible and you may want to specify what part of the expression it affects. If it would be easier to use it as a postfix method, just add .not
to an expression.
Words are used for Boolean operations rather than symbols such as &
/&&
or |
/||
since their meaning is obvious, they are so few characters and they get syntax highlighting in the SkookumIDE to help them stand out.
It may also surprise some to learn that C++ and other C family languages also allow the word versions for Boolean logical operators of and
, or
, xor
and not
to be used interchangeably with &&
, ||
, ^
and !
.
TIP Bitwise manipulation in SkookumScript is done with methods rather than symbol based operators. Several such methods starting with bit_
can be found in the Integer
class: bit_and()
, bit_not()
, bit_or()
, bit_set?()
, bit_shift_down()
, bit_shift_up()
and bit_xor()
. Since SkookumScript is a high-level language, bitwise operations should not be frequently used so bitwise symbol based operators have been omitted by design.
Short-circuit evaluation
Short-circuit evaluation (or minimal evaluation) is when the second operand of a binary operator is evaluated only if the first operand is not sufficient to determine the value of the expression. For example, if the first operand of the Boolean
class and
evaluates to false
then the overall result will be false
and there is no need to evaluate the second operand. Because of this conditional or lazy evaluation, short-circuit operators are, in effect, control structures rather than simple operator calls.
Short-circuit evaluation is often used as a gate to skip or avoid undesired side effects of a following test.
For example, using the Boolean
and operator which is short-circuiting:
So guy.sees_player?
won’t even be called if guy.alive?
returns false
.
NOTE Only the and
, or
, nand
and nor
operators in the Boolean
class (and possibly any subclasses) use short-circuit evaluation. If these operators are used on other classes, they will not use short-circuit evaluation.
Boolean
short-circuit operators are:
Operator | Skip op2 if op1 | Result if shorted |
---|---|---|
and |
false |
false |
or |
true |
true |
nand |
false |
true |
nor |
true |
false |
Instantiation (creating new objects)
Object instantiation creates an instance object for a particular class type—in other words, it is used to make new objects.
In order to understand instantiation calls, we will need to know a little bit about instance constructor methods. Constructor methods are used to initialize an object and there are two main types: default constructors with a single exclamation mark (!
) or named constructors which start with an exclamation mark followed with an identifier name starting with a lowercase letter (!copy
, !xyz
, !new
, !null
).
TIP In SkookumScript, the exclamation mark !
is often used to mark when new things are created.
An instantiation call is made by optionally specifying the name of the class of object to create and then the name of the constructor to call to initialize the object.
Example Instantiations
// Default constructors
!mind: Mind!
!vector: Vector3!
// Named constructors
!vec1: Vector3!xyz(1 2 3)
!vec2: Vecotr3!copy(vec1)
In addition to the default constructor !
, the copy constructor !copy
is a very useful and common constructor. It makes a new copy of an object from another object of the same type.
Objects created via instantiation in SkookumScript are in principle created on the memory heap (though in practice objects are often created using reused memory pools, shared engine structures and other mechanisms used for memory and speed efficiency) so instantiation is somewhat analogous to the new
operator in C++.
Literals automatically create an object instance as well as giving it an initial value. There are only 7 literal class types and thousands of classes that don’t have a literal syntax, so object instantiation is one of the key mechanisms used to create all the other types of class objects.
The simple class types that have literals also have default constructors, though it usually looks more aesthetically pleasing to use a literal to create them rather than to use instantiation.
Instantiations for classes with literals
!int1: 0
!int2: Integer!
!real1: 0.0
!real2: Real!
!bool1: false
!bool2: Boolean!
!str1: ""
!str2: String!
!str1: ''
!str2: Symbol!
!str1: {}
!str2: List!
Instantiation with inferred class
Available Sk 3.0.5509 and up
If the desired class type for an expression is known by the parser, then the class name can be omitted from the front of the instantiation and the class will be inferred by the parser.
Inferred instantiations
// Needs class since parser doesn't know desired type
!vec: Vector3!
vec.distance(Vector3!xyz(1 2 3))
// Vector3 inferred
vec.distance(!xyz(1 2 3))
vec.distance(Vector3!left)
// Vector3 inferred
vec.distance(!left)
// It even works for default constructors
vec.distance(Vector3!)
// Vector3 inferred
vec.distance(!) // Looks interesting though it works
vec.near_any?(
500 Vector3!xyz(3 2 1) Vector3! Vector3!forward Vector3!scalar(4.2))
// Vector3 inferred
vec.near_any?(500 !xyz(3 2 1) ! !forward !scalar(4.2))
Places where the parser knows the desired class type include:
- arguments
- default parameter values
- binding to data members
- binding to predicates (
Boolean
) if
/when
/unless
/case
test expressions- list items
- the
change
(Mind
) expression.
Expression Instantiation
Expression instantiation is a form of syntactic sugar to shorten an object instantiation. Instead of using a class name to do the instantiation, an expression is used both to determine the class of the object to create and to be used as the first argument.
To make a copy of a String
object you could use the String
copy constructor:
Using an expression instantiation, this can be written more concisely:
This can be used with any constructor that has its first argument as the same type of class as the class being constructed.
Since copy constructors are used so frequently, if the copy
part of !copy
is omitted and just !
is used, the parser assumes the full !copy
. So the copy can be written even more concisely:
So all of these are equivalent:
So whenever you see an exclamation mark after an expression, you know that it is making a new copy of the result object by calling the copy constructor.
This is an easy mechanism to ensure that you pass a copy of an object to a method rather than just passing the reference of an object. This essentially mimics argument passing by value.
It is also handy to allow a routine to accept a simple object reference and then to make a copy of the object in the body of the routine if the routine needs to modify it or if it needs to hold onto the object across frames such as with coroutines.
Making a copy in a routine
// Definition for shout3() closure
!shout3: (String message)
[
// Make a copy of the parameter before modifying it
!message_copy: message!
message_copy.uppercase
// Print out uppercase version of message three times
3.do[println(message_copy "!")]
]
!str: "don't panic"
// Send str as a reference
shout3(str)
// str unchanged
println(str)
Modified copies
Often you’ll want to make a different version of an existing object and you want to keep the original object unmodified. One way you can do this is to first make a copy of the original object and then to call a modifying method on the new copy.
Expression instances have one more shortcut to offer—they can also figure out if a particular named constructor exists or if it can break the !name
apart and use the copy constructor !copy()
and a separate method name()
. So you can also do this:
This is how the parser figures out the expression instantiation str1!uppercase
:
- First the parser determines the result object class type of the expression
str1
. In this case it isString
. - Next the parser looks for a
String!uppercase()
constructor. It doesn’t have such a constructor.- If it were found, it would call
String!uppercase(str)
and be done.
- If it were found, it would call
- Next it determines if it has a copy constructor
!copy()
. It does.- And it determines if it has a method called
uppercase()
. It does. - If both are found, it calls
String!copy(str).uppercase()
- And it determines if it has a method called
So all of these are equivalent:
str1!uppercase
is more concise than String!copy(str1).uppercase
. And if an !uppercase()
constructor is added in the future (which would probably be more efficient than two calls), then it will use that instead: String!uppercase(str)
Whenever you see some expression and !name
you now know that it means make a new copy and modify it. For this reason, many of the methods in objects are modifying methods since you can always use !
+ modding_method
to make a non-modifying version.
Making a copy in a routine
// Definition for shout3() closure
!shout: (String message)
[
// Make a modified copy of the parameter
!message_copy: message!uppercase
// Print out uppercase version of message three times
3.do[println(message_copy "!")]
]
!str: "don't panic"
// Send str as a reference
shout3(str)
// str unchanged
println(str)
Index operator {idx}
The index operator { }
is generally used for indexing into and retrieving a sub-part of an object, especially List
objects, though it can be made available to any class.
List
objects use the index operator to retrieve an item object from a specified 0-based index position.
For example: list{idx}
List index operator {}
!list: {0 "one" 2.0 'three'}
println(list{0}) // 0
println(list{1}) // "one"
println(list{3}) // 'three'
The index operator { }
is just syntactic sugar for the at()
method. Any class that has at()
such as List
can also use the index operator.
List index method at()
!list: {0 "one" 2.0 'three'}
// So this method call is the same as list{1}
list.at(1)
See the at()
method in classes that have it to learn any specific information for a class type.
Index set operator {idx}:
The index set operator { }:
is very similar to the index operator though it sets a sub-part of an object. It also can be made available to any class.
List
objects use the index set operator to set an item object reference at the specified 0-based index position to the specified object reference.
For example: list{idx}: obj
List index set operator {}:
!list: {0 "one" 2.0 'three'}
// Replace the old object item reference with a new one
list{1}: "ONE" // {0 "ONE" 2.0 'three'}
list{0}: nil // {nil "ONE" 2.0 'three'}
list{3}: 'Three' // {nil "ONE" 2.0 'Three'}
Similar to a variable, the index set operator essentially rebinds the item slot in the list to the object reference.
The index set operator { }
is just syntactic sugar for the at_set()
method. Any class that has at_set()
such as List
can also use the index set operator.
List index set method at_set()
!list: {0 "one" 2.0 'three'}
list.at_set(1 "ONE") // {0 "ONE" 2.0 'three'}
list.at_set(0 nil) // {nil "ONE" 2.0 'three'}
list.at_set(3 'Three') // {nil "ONE" 2.0 'Three'}
See the at_set()
method in classes that have it to learn any specific information for a class type.
TIP To assign an object in-place and not replace (or rebind) it with a new object, use the index operator and an assignment (or other modifying methods) on the resulting object.
List item assignment
!list: {0 "one" 2.0 'three'}
// Keep the same object and change its content
list{1} := "ONE" // {0 "ONE" 2.0 'three'}
list{0} := 42 // {42 "ONE" 2.0 'three'}
list{3} := 'Three' // {42 "ONE" 2.0 'Three'}
Apply operator %
An apply operator %
calls a routine on zero, one or more objects (in the case of a list) and comes in two varieties based on whether it is used on a list or a non-list.
List apply
If the apply operator is called on an expression that is a List
object, then the routine call that follows the %
is called on each item in the list respectively and once completed the list itself is returned. Any arguments are evaluated for each call. If the list is empty, then the routine is not called and any arguments are not evaluated.
The apply operator works differently for methods than coroutines. If a method is applied then it is called and completed with each item in succession and then the list object is returned as the result.
If a coroutine is applied then it is called on each item concurrently (all starting in the same update frame) and the next expression is run only after all the coroutines for each item in the list have completed.
Example apply coroutine in SkookumDemo
// Get a list of all the Enemy robots and have
// them all simultaneously path to the player.
Enemy.instances%_path_to_actor(player_pawn)
// Once the *last* robot has pathed to the player
// then have an explosion around the player.
player_pawn._boom
There is another variant of the apply operator called the apply race operator %>
which calls the coroutine concurrently like the apply operator though it aborts any coroutines that have not yet completed as soon as the first coroutine completes—i.e. whichever item finishes its coroutine first. So they all run a race and the fastest wins.
Example apply race coroutine in SkookumDemo
// Get a list of all the Enemy robots and have
// them all simultaneously path to the player.
Enemy.instances%>_path_to_actor(player_pawn)
// Once the *first* robot has pathed to the player
// then have an explosion around the player.
player_pawn._boom
Non-list apply
If the apply operator is called on an expression that is not a List
object, then either it will call the routine following the %
if the expression is non-nil
or if the expression is a nil
object then the call is ignored – the routine is not called and any arguments are not evaluated and nil
is returned. So again:
- non-
nil
: routine call is made just as if%
were a.
nil
: routine call is ignored andnil
is returned
TIP In a way, non-list objects are treated either like a one item list (non-nil
) or like an empty list (nil
).
Class type casting <>
is covered later in the primer, though if the apply operator acts on an expression which is also a union type with the None
class (class used by nil
) then it also implicitly casts away any None
. So in the example above, the type returned by Enemy.find_named()
is <Enemy|None>
(as in it could return an Enemy
object or nil
) and the apply operator implicitly gets rid of None
and changes the type to just Enemy
.
The apply operator %
on a non-list is similar to calling methods through optional chaining ?.
in Swift.
Closure invoke operator
A closure invoke operator ()
(called a call operator in C++ and some other languages) is used to invoke or call a closure. It looks just like calling any other routine except it is used on an expression that evaluates to a closure object.
Closure invoke operator example
!triple: (Integer num)[num * 3]
// Use closure invoke operator () on closure triple
triple(4)
// returns 12
NOTE Any captured variables in a closure object have their references bound when a closure literal is initially evaluated. If a variable has been rebound :
to a new object by the time a closure object is invoked, then the captured variables will still be bound to whatever objects they were bound to during the initial evaluation of the closure literal.
Closure with capture invoke
!value: 3
// Capture `value` variable
!multiply: (Integer num)[num * value]
println(multiply(4))
// 12
// Modify existing `value` object:
// Assign value to -2
value := -2
// multiply still references the same object as `value`
println(multiply(4))
// -8
// Rebind `value` to new 10 object:
value: 10
// multiply still references the old `value` (-2) object
// and not new `value` (10) object
multiply(4)
// returns -8 (not 40)
Once a closure object is invoked, it may modify objects referred to by any captured variables. The captured objects and any changes made on them will be kept stored in the closure object.
Closure invoke modifying a capture
!value: 0
// Capture `value` variable
!by_twos:
()
[
// increment captured value
value += 2
]
println("result:" by_twos() " value:" value)
// result:2 value:2
println("result:" by_twos() " value:" value)
// result:4 value:4
// Modify existing `value` object:
// Assign value to -10
value := -10
println("result:" by_twos() " value:" value)
// result:-8 value:-8
println("result:" by_twos() " value:" value)
// result:-6 value:-6
// Rebind `value` to new 10 object:
value: 10
// value (-6) in by_twos now disconnected
// from value (10) in outer scope
println("result:" by_twos() " value:" value)
// result:-4 value:10
println("result:" by_twos() " value:" value)
// result:-2 value:10
SkookumScript does not have a invoke operator for functional objects since closures are much simpler to create and more powerful.
Conversion calls
Conversion methods describe the steps needed to convert one type of an object to another. A conversion method is located in the class type that is being converted from (conversion source) and it has the same name as the class that it is converting to (conversion target).
Calling a conversion method
!str42: "42"
!int42: str42.Integer // Convert String to Integer
!real42: str42.Real // Convert String to Real
!name42: str42.Name // Convert String to Name
!sym42: str42.Symbol // Convert String to Symbol
Each conversion source class may only have one conversion method for a given conversion target class.
TIP Conversion calls may not pass any arguments. (Though they may in the future.) If a more complex conversion is needed, then either:
- create a normal method on the source class with as many arguments as needed that returns the target class
- create an appropriately named constructor method in the target class and it can have as many arguments as needed and then you instantiate the target class using the constructor with the source object as one of the arguments.
NOTE Conversion methods are used by class conversion primitives obj>>ClassType
. Class conversion calls can be used in the place of class conversion primitives, though unlike class conversion primitives they must always explicitly specify the conversion target class name.
Type Primitives
SkookumScript’s type system is powerful, flexible, safe, and gets out of the way as much as possible. Unlike many other scripting languages, SkookumScript is fully statically type-checked at compile-time, which catches many bugs before a game runs.
Static types are like a quantum superposition of all possible execution paths. So static types are the truest form of live code!
- Johnathan Edwards Down the rabbit hole of types (Alarming Development)
From time to time, you will need to convert one type of an object to another using a class conversion and sometimes you will need to help the SkookumScript parser out and tell it what type an object really is with a class cast or two.
Class Conversion >>
A class conversion operator >>
is used to take an existing object of one class type and to construct a new object of a different class type.
To convert an object, place two greater than symbols >>
after the object expression and then specify the name of the class that you want to convert to.
Class conversion examples
// Original types
!str: "123" // String
!answer: 42 // Integer
!pi: 3.1416 // Real
// Converted types
!str2int: str>>Integer
!str2real: str>>Real
!str2symbol: str>>Symbol
!str2name: str>>Name
!int2real: answer>>Real
!int2str: answer>>String
// Sometimes a conversion isn't perfect:
// A 3.1416 Real converts to just a 3 Integer
!real2int: pi>>Integer
!real2str: pi>>String
Remember Conversion Calls? They are the methods that class conversions call to do the actual work. Whenever a class conversion is needed, the appropriate class conversion call is used internally. If the desired conversion call doesn’t exist, then there is no conversion path and a class conversion to that class will give a parser error.
TIP Every class has a String()
conversion method, so any class can be converted to a string >>String
. The default String()
conversion method just returns the class name of an object though many classes have their own custom String()
conversion methods.
Redundant conversion
If you try to convert an object to the type that it already is and the parser knows this, it won’t let you do it since it is redundant.
This gives the error:
The expression being converted is already known to be an instance of the class String so converting it to an instance of the class String is redundant.
If at compile time the parser doesn’t know that the type of the object will already be the type that you are converting to, it will allow the class conversion. During runtime if the object is already the type being converted to, then the class conversion will do nothing.
Inferred conversion class
If the parser knows the desired type for an expression, such as an argument being passed to a routine, then the class conversion can have its class type inferred. All that is needed is the >>
and the class name can be omitted.
Conversion with inferred class
!name_str: "Marvin"
// Assume there is a use_named(Named name) method
use_named(name_str>>)
// Just the same as if this were used:
use_named(name_str>>Name)
By design, coercion (implicit conversion) is not allowed. You must at least explicitly use >>
to indicate that a conversion to the expected type is intentional.
No implicit conversion
!name_str: "Marvin"
// Coercion (implicit conversion) is not allowed
use_named(name_str)
This gives the error:
The argument supplied to parameter named name
was expected to be of type Name
and it is type String
which is not compatible.
However, String
has a Name()
conversion method so it can easily be converted with the >>
class conversion operator.
Conversion variations
There are several ways to convert an object to the type you want:
- class conversion
obj>>String
- can infer the type
obj>>
- does no action if already the target type
- can infer the type
- conversion call
obj.String
- always called
- very intuitive and looks nice
- arbitrary custom method
obj.as_string
- can have any number of arguments
- can have several differently named variants
- less obvious to discover, though
as_
recommended as a prefix
- constructor in target type
String!from_my_type(obj)
- can have any number of arguments
- can have several differently named constructors:
TargetType!from_my_type1()
,TargetType!from_my_type2()
- doesn’t look as nice as class conversion or conversion call
Conversions in all variations
!name_str: "Startibartfast"
// When parser does not know desired type:
!str2name1: name_str>>Name
!str2name2: name_str.Name
!str2name3: Name!(name_str)
// There could also be some method such as `as_name()`
// that does conversion though it won't be as obvious
// for a user to discover it.
!str2name4a: name_str.as_name
// Could also have arguments and return arguments
!str2name4b: name_str.as_name(arg1 arg2; !rarg1 !rarg2)
// When parser knows desired type:
// Assume there is a use_named(Named name) method
// Inferred Name class
use_named(name_str>>)
// Not inferring Name class
use_named(name_str>>Name)
use_named(name_str.Name)
use_named(Name!(name_str))
use_named(name_str.as_name)
Class Cast <>
A class cast operator <>
is a hint to the parser to use a particular class type for an expression. It is called a hint since the object is only being labelled differently which may allow the parser to use it in an otherwise different manner, the object itself is not changed. At runtime all objects know their type, though when code is parsed (at compile-time) the parser may not know what specific type an object will be.
To cast an object, place a less than followed with a greater than symbol <>
after an expression and then specify the name of the class that the parser should assume the resulting object will be.
The SkookumScript cast operator <>
is similar to the :?>
operator in F#, a combination of the static_cast<>()
and dynamic_cast<>()
in C++, the as!
operator in Swift or the as
in Rust.
If code can be written in such a way that no casting is needed, then that is should probably be preferred over code that requires casting.
When you do need to cast a type, you should only do so when you know it to be safe. Either you know this based on the flow of the code logic or various tests are make to ensure that a cast will be valid. If the type is not what you say it will be, during runtime an error will be raised to alert you of the discrepancy.
Though casts are considered just a parser hint, they will still do a sanity typecheck during runtime in debug builds and raise an error if the class type isn’t what is expected. Casts do nothing in non-debug builds.
So far in the Primer we haven’t talked much about object-oriented programming or the class hierarchy. In order to better understand class casting it helps to work with some example classes and their superclass and subclass relationships.
There are essentially three categories of casting in SkookumScript:
- downcasting from a superclass to a subclass
- reduce casting from several potentially unrelated class types to one class type
- illegal casts including equivalence casting to the same type of class, upcasting from a subclass to a superclass and unrelated casting where there is no direct path to change from one class to another
Downcasting
Downcasting (also called type refinement) is used to switch from a superclass down to one of its subclasses - such as Actor
to Enemy
(see the example class tree). Once casted, you may then use all the members of the subclass that were not available to the superclass.
// The HitResult class has an @actor data member and
// the parser only knows that it is an Actor type.
// You may want to cast to an Enemy subclass:
if hit_result.@actor.class_of(Enemy)
[
// _go_berzerk() is a coroutine in the Enemy class
hit_result.@actor<>Enemy._go_berzerk
]
Reduce Casting
A reduce cast narrows from one of several possible types to a single type. SkookumScript has a special union class type where the parser knows that an expression is going to be one of several possible types, though it doesn’t know which one it will be until runtime.
For example, a list with several item class types has items that are a union of all their known classes.
// List with item type <Symbol|Integer|String|Real>
!list: {'zero' 1 "two" 3.0}
// Error since parser doesn't know which type it will be
//list{2}.uppercase
// Reduce possible types to just String
list{2}<>String.uppercase
In the code above, the parser knows that String
is one of the possible classes in <Symbol|Integer|String|Real>
so it assumes you know what you are doing and allows it.
Casting off nil/None
Expressions often will evaluate to either a specific class or the nil
object. For example, @?'RoboChar1'
or Actor.find_named("RoboChar1")
will either return an actor named “RoboChar1” or nil
if there is currently no such actor. The type is <Actor|None>
where None
is the class of the nil
object.
If you are sure it is an Actor
then you can cast it and access Actor
members.
// type is <Actor|None>
// Could be robot (of class Actor) or nil
!robo1: @?'RoboChar1'
if robo1.not_nil?
[
// Reduce to just Actor
println(robo1<>Actor.actor_location)
]
Combined Downcast and Reduce Casts
You can also do a combination of a downcast and a reduce cast.
// type is <Actor|None>
// Could be robot (of class Actor) or nil
!robo1: @?'RoboChar1'
if robo1.not_nil?
[
// Reduce to Actor and downcast it to Enemy
println(robo1<>Enemy._go_berzerk)
]
Illegal Casts
If a cast seems redundant or incorrect, the parser will give an error saying that it is illegal.
An equivalence cast is redundant since the expression is already that class.
An up cast from a subclass to a superclass is redundant since the expression is already matching that superclass.
An unrelated cast has no derivation path from the current class to the desired class.
For a unary (single) class, it is a unrelated cast if it is not a subclass.
For a union class, it is a unrelated cast if none of its set of classes has a derivation path.
None of the item classes above are an Enemy
or a subclass of Enemy
.
Inferred Casting
If the desired type of a cast can be figured out by the parser using the surrounding context, then the type can be omitted after the cast operator <>
and the class to cast to will be inferred.
<SomeClass|None>
will remove the None
and reduce down to just SomeClass
since it is the more interesting class and by definition you cannot do anything interesting with nil
. This is similar to an apply operator %
on a non-list object.
Passing an expression as an argument will infer the class type of the argument.
// Infer `Enemy` by getting rid of `None`
Enemy@?'RoboChar1'<>._go_berzerk
// Infer `Enemy` since that is the type desired by _chase()
Enemy@'RoboChar1'._chase(@?'RoboChar2'<>)
!robo1: Enemy@'RoboChar1'
!robo2: @?'RoboChar2'
if robo2.not_nil?
[robo1._chase(robo2<>)]
Next » Unreal Engine 4 Plugin quick start
« Previous Unreal Engine 4 Plugin installation and setup