User Tools

Site Tools


Sidebar

hpl3:community:scripting:angelscript_tutorial

This is an old revision of the document!


AngelScript Tutorial

Lesson Zero: Introduction

Greetings, reader. Welcome to this tutorial on scripting in SOMA. If you’re here, that means you want to get into modding SOMA but you don’t know how to program the code that makes the game tick. Don’t worry, everyone has to start somewhere, and hopefully, by the time you are through this tutorial series, you will have enough understanding to get started working on your own mods.

About SOMA

SOMA is the latest iteration in a long line of games by Frictional Games, and in true Frictional fashion, the game has been opened up to the public for widespread use. What’s more, the entirety of SOMA was written in the same approach as a modder would use to write a mod, and it’s readily available in the installation folder of your SOMA game for anyone who wants to go in and see how a certain part of the game was made.

Like Frictional’s other games, SOMA was written using the HPL engine. This latest version of the engine is called HPL3, and is in many ways a huge upgrade from the HPL2 that was used for Amnesia. Among other things, the scripting interface was overhauled from the ground up, giving modders a lot more powerful ways to control the engine. This allows for some truly incredible mods, including gameplay mechanics that aren’t even present in SOMA. Want to make a 2D RPG adventure? How about a high-octane racing simulator? Those things and more are possible with HPL3.

Who This Tutorial Is For

This tutorial will be primarily geared toward people who have never programmed before in any language. If you do have some programming experience, you may just want to do a cursory glance over the tutorial series to just get a handle on what some of the similarities and differences there are between SOMA’s scripting language and other popular programming languages.

While this tutorial does take some looks into the interaction between AngelScript and HPL3, it is not for people who are looking for a comprehensive guide to modding in SOMA. This tutorial focuses on the AngelScript language specifically so as to hopefully provide a more rounded programming experience. This tutorial will not teach you how to set up a map to make it playable, but once you have created a map, you can use the knowledge in this tutorial to do the scripting for that map and make it come to life.

What is SOMA’s Scripting Language

The language used to program everything that happens within HPL3 is a language called AngelScript. It’s a language specifically developed for use in modding and scripting environments, most notably video games. Some examples of other games that use AngelScript are Amnesia, Amy, DustForce, and Overgrowth.

AngelScript is most similar to C++ in terms of syntax, so programmers who have experience in developing in C++ should find much of AngelScript quite familiar. However, it’s not quite as verbose as C++, and has taken some aspects from languages such as Java and C# in order to make the coding experience a bit less technical.

What Tools Should I Use to Code in AngelScript

Most popular programming languages nowadays use an Integrated Development Environment, or IDE, to speed up and streamline the development process. An IDE typically contains various helpful features, such as an auto-complete feature (when you start typing, the IDE suggests names of various things that you could be looking for), code generation (type in a code word and the IDE creates a full block of code for you), syntax highlighting (color-coding the script to make it easier to understand what is what) or refactoring (a fancy term for renaming all instances of a certain word or name).

Unfortunately, there is no native AngelScript IDE out there. The closest we have to that is to use CodeLite, a C++ IDE, and reconfigure it so that it (begrudgingly) works for AngelScript. This process is laid out on a special page in the wiki, which will show you step-by-step how to setup Codelite for AngelScript use.

A recent update to CodeLite broke the compatibility for AngelScript, so you will have to try and get an older version. Try going to this page and downloading version 9.0, then follow the wiki instructions.

Alternatively, you can just use a notepad program. It’s a lightweight, no-mess solution that many programmer types sometimes prefer over the bloat and invasiveness that an IDE sometimes entails.

There are many such programs. One popular one is Notepad++, a light upgrade from the default Windows Notepad that offers some IDE features in a slimmed down package. Another alternative is VS Code, a lightweight version of the widely popular Visual Studio IDE. Both of these options offer syntax highlighting, so I recommend that you set it to C++ mode.

What Other Resources Can I Use For Help

You can check around in other sections of the HPL3 wiki. There is a lot of documentation on how certain things work within HPL3, from a function reference (we’ll get to those later) to recommendations on how to import custom assets. There is documentation for AngelScript as well, which you can view on the AngelScript website You can also ask for help or guidance on the Frictional Games forums, or on the SOMA Discord Channel. There are plenty of active people in the community that can help if you get stuck on something, so feel free to ask.


Lesson One: Structure of a Script

The first thing to do is to take a look at what a typical script file for a typical SOMA map would look like. First, launch the LevelEditor program within your SOMA installation file. Once it’s open, save the blank map somewhere on your computer.

When the LevelEditor saves a map for the first time, it creates a bunch of different files (at least 20). For this reason, it’s recommended that you save your map inside an empty folder. Also, for reasons of convenience, it’s also a good idea to save this folder somewhere in the SOMA installation folder, typically within a folder called “mods”. When it’s all said and done, the typical folder layout would look like “SOMA/mods/YourModName/maps/YourMapName/<map files>”.

Now that you’ve done that, go ahead and take a look at the files you just saved. There are quite a lot, but the one we are most interested in is the one that ends in “.hps”.

Open the .hps file in your editor of choice. You should see something resembling the following (which has been formatted for reasons of length):

#include "interfaces/Map_Interface.hps"
#include "base/Inputhandler_Types.hps"
 
#include "helpers/helper_map.hps"
#include "helpers/helper_props.hps"
#include "helpers/helper_effects.hps"
#include "helpers/helper_audio.hps"
#include "helpers/helper_imgui.hps"
#include "helpers/helper_sequences.hps"
#include "helpers/helper_game.hps"
#include "helpers/helper_modules.hps"
#include "helpers/helper_ai.hps"
 
//--------------------------------------------------
/*Place any global values here. These must be const variables as they will not be saved*/
/*This is also the place for enums and classes, but these should be avoided whenever possible*/
//--------------------------------------------------
class cScrMap : iScrMap
{
    //--------------------------------------------
    //////////////////////////////////////////////////////////////////////////////////////////
    // ==============
    // MAIN CALLBACKS
    // ==============
    //{///////////////////////////////////////////////////////////////////////////////////////
    //-------------------------------------------------------
    ////////////////////////////
    // Set up map environment
    void Setup() {}
 
    //-------------------------------------------------------
 
    ////////////////////////////
    // Run first time starting map
    void OnStart() {}
 
    //-------------------------------------------------------
 
    ////////////////////////////
    // Run when entering map
    void OnEnter() {}
 
    //-------------------------------------------------------
 
    ////////////////////////////
    // Run when leaving map
    void OnLeave() {}
 
    //-------------------------------------------------------
 
    ////////////////////////////
    // The player has died.
    void OnPlayerKilled(int alRecentDeaths, const tString&in asSource) {}
 
    //-------------------------------------------------------
 
    ////////////////////////////
    // To get when player makes input (mostly used for debug)
    void OnAction(int alAction, bool abPressed) {}
 
    //-------------------------------------------------------
 
    ////////////////////////////
    // This only used for pure debug purposes when info needs to printed.
    float DrawDebugOutput(cGuiSet @apSet,iFontData @apFont,float afY) { return afY; }
 
    //-------------------------------------------------------
    //} END MAIN CALLBACKS
    //////////////////////////////////////////////////////////////////////////////////////////
    // ==============
    // MAIN FUNCTIONS
    // ==============
    //{///////////////////////////////////////////////////////////////////////////////////////
    //-------------------------------------------------------
    /*Put any variables that are used in more than one scene here.*/
    //-------------------------------------------------------
    /*Put any functions that are used in more than one scene here.*/
    //-------------------------------------------------------
    //} END MAIN FUNCTIONS
    //////////////////////////////////////////////////////////////////////////////////////////
    // ==============
    // SCENE X *NAME OF SCENE*
    // ==============
    //{//////////////////////////////////////////////////////////////////////////////////////
         /////////////////////////////////////////
         // General
         //{//////////////////////////////////////
 
        //-------------------------------------------------------
 
        /*Put any variables that are used by many events in Scene X here.*/
 
        //-------------------------------------------------------
 
        /*Put any functions that are used in more than one event in Scene X here.*/
 
        //-------------------------------------------------------
 
        //} END General
 
         /////////////////////////////////////////
         // Event *Name Of Event*
         //{//////////////////////////////////////
 
         //-------------------------------------------------------
 
         /*Put any variables that are only used in Scene X, Event X here.*/
 
         //-------------------------------------------------------
 
         /*Put any functions that are only used in Scene X, Event X here.*/
 
         //-------------------------------------------------------
 
         //} END Event *Name Of Event*
 
     //} END SCENE X
     /////////////////////////////////////////
     // ==============
     // TERMINALS
     // ==============
     //{//////////////////////////////////////
         //-------------------------------------------------------
 
         /////////////////////////////////////////
         // Terminal *Name Of Terminal*
         //{//////////////////////////////////////
 
         //-------------------------------------------------------
 
         /*Put any variables that are only used Terminal here.*/
 
         //-------------------------------------------------------
 
         /*Put any functions that are only used Terminal here.*/
 
         //-------------------------------------------------------
 
         //} END Terminal *Name Of Terminal*
 
    //} END TERMINALS
}

A bit wordy, perhaps, but it all boils down to the following categories, each of which you will learn about in a particular lesson:

  • Includes (Lesson 6)
  • Class Declaration (Lesson 7)
  • Functions (Lesson 5)
  • Types and Variables (Lesson 2)
  • Callback Functions (Appendix 2)
  • Comments

Most of these aspects are explained in later lessons. There is one that I can teach you about now, however: comments.

Commenting

Most of the things that go into a script is part of the program's execution - you want it to do something here, then do something there, then do something to those two somethings over there. Sometimes, however, you just want to write a reminder of what some code does so that you don't have to go through it all and figure it out later. That's where comments come in. Anything that has been marked as a comment is ignored by the program, so you can type in whatever you want without worrying that it will screw up the program.

There are two types of comments in AngelScript - inline comments and block comments:

    // This is an inline comment
 
    /* 
    This is a block comment
    */

Inline comments are just for a single line. Anything after the comment marker (“//”) becomes a comment, but on the next line, it's back to business as usual.

Block comments are for multiple lines. A block comment is marked as everything between the starting marker (“/*”) and the ending marker (“*/”). This can span many lines, and can even mark your entire program as a comment if you aren't careful.

Hello World

As per tradition, every introductory programming course needs a “Hello World” program, and this tutorial is no exception. In your map’s script, find the section of the code that contains the following:

    ////////////////////////////
    // Run when entering map
    void OnEnter()
    {
 
    }

Inside those curly brackets, add “cLux_AddDebugMessage(“Hello SOMA”);”. Don’t worry what it means just yet. When you’re done, the above code snippet should now look like this:

    ////////////////////////////
    // Run when entering map
    void OnEnter()
    {
        cLux_AddDebugMessage("Hello SOMA");
    }

Go ahead and save your script, if you haven’t done that already.

Now it’s time to start up SOMA in mod development mode. To do this, there’s a file in your SOMA installation directory called “SomaDev.bat”. When you open this file, it starts SOMA in developer mode. For the first little bit, the game will be loading, but once you hear sounds start to play, hit the F1 button. This brings up the developer panel, and on it contains a lot of commands and tools for testing and proofing your map.

For now, scroll down until you find the “Load Map” button. Click that button, then navigate to where you saved your map. Open your map from there (it will be the “.hpm” file that you see). Once you do, you should get basically a black screen with a handful of text around the edges. In the lower left corner, you should see the text “Hello SOMA”.

If you don’t see the text, make sure the developer panel is hidden by pressing F1 again. This is because the game is effectively frozen while the panel is visible by default, so the script may not appear right away if the panel is visible. If you still do not see the text, press F5, which reloads the map and causes it to become visible again.

If all went well, then congratulations. You just created your first SOMA mod. It may not be very shiny, but like I said in Lesson 0, we all have to start somewhere.

So let’s look at what we just did in pieces:

    cLux_AddDebugMessage(“Hello SOMA”);

First, we used the code cLux_AddDebugMessage followed by an opening and (eventually) closing parentheses. This is a function call, which you will learn more about in Lesson 6. For now, just know that this function’s job is to print text onto the screen.

Within the parentheses is some text within quotation marks, “Hello SOMA”. This is what is known as a “string literal”. You’ll learn about them and other types in Lesson 2. The important part to note here is that it is the text that actually appeared in the game itself.

And finally, after the closing parenthesis, there is a lonely little semicolon. That semicolon marks the end of a line of code. Do not forget this: every line of code that isn’t a class or function declaration (more on them later) needs a semicolon at the end of it. If you do not put a semicolon at the end of a line of code, SOMA will complain about it and refuse to run your script. I cannot stress this enough: do not forget the semicolons!!!

I say that, not to make sure you remember the semicolons, but because you will forget about semicolons at least once in a while. No programmer is immune from that mistake, no matter how many decades they’ve been a guru in their field. You’d be surprised how many times I’ve been asked by someone to help them figure out a confusing error in their code, and the cause turned out that they forgot to put a semicolon somewhere. As long as you remember your semicolons, then when the errors happen, you know what to check first.

Alright, now that you’ve taken a satisfactory initial plunge into AngelScript, let’s take a breather. In the next lesson, you start to learn the basics for real.


Lesson Two: Variables and Types

The very first thing that a programmer needs to know about programming is the concept of variables. If you've taken an algebra class in the past, the term may sound familiar, which is helpful as variables in algebra and variables in programming are similar.

Imagine that you needed to write a program that took some numbers and added them up. The first thing you would want to do is to tell the program what those numbers are. Say you told the program the numbers 3, 7, 2, 8, and 5. Well, the program is going to need a way to remember those numbers so that it can work with them later on.

This is where variables come in. A variable is a construct in a program that is designed to hold a value and store it under a label. For example, take a look at the following code.

    int x = 5;

Let's break this down. The int is telling the program what type a variable is, the x tells the program that the variable's name is x, and the the = 5 tells the program what the variable's value is. In short, this line of code creates a new variable x of type int and gives it the value of 5.

There are two things going on in this code. The first is that a variable is being declared, which means that the program is creating a new variable to remember. The second is that a variable is being initialized, which means a variable is being assigned a value for the first time.

Now that our variable has been created (declared), we can access it elsewhere in our program. If, say, we wanted to assign a different value to it, we could do the following:

    int x = 5;
    x = 3;

Note that the int is gone from the second line. That is because the int is only required when declaring a variable for the first time. After that, the program knows what the type of x is, so you don't need to keep telling it.

You can also use variables when assigning a value to other variables:

    int x = 5;
    int y = x;

In this code, we create a variable x and assign it a value of 5. Then we create another variable y and assign it a value of x. This means that we take whatever value is being stored in x and copy it into y. At the end of this code, y will also be set to 5.

Before, I talked about declaring and initializing a variable and how it was convenient that you could do both on the same line of code. It is not necessary to do so, however, and in some situations, you may want to declare a variable without immediately initializing it. You can do that in the following way:

    int x;
    ...
    x = 5;

See how the first line doesn't assign a value to the variable x? This is a declaration without an initialization. The value of a variable that has been declared but not initialized is the default value of the type assigned to the variable, which, in this case, is 0 due to the type being int. (More on types and defaults in a bit.)

Identifiers

The technical term for the name of a variable is an identifier. In general, you can name your variables whatever you want, but there are a number of rules and recommendations for how you do it.

First, the identifier must start with a letter in the alphabet (uppercase or lowercase) or an underscore character (“_”). Any identifier that starts with a number or other symbol will not be recognized and will cause an error in your script. After the start of the variable, you may use letters or numbers as well as an underscore character. You cannot use a space as part of a variable name.

// These are legal identifiers.
int abcdef;
int ABCDEF;
int aBcDeF;
int _abcdef;
int abc123;
 
// These are not legal identifiers.
int 123456;
int 123_456;
int 123abc;
int %$#^%^%;
int abc def;

Second, the identifier cannot be one of a number of specific words. These words are called keywords, and they are reserved by AngelScript to mean various things. The full list of keywords are:

and abstract auto bool break
case cast class const continue
default do double else enum
false final float for from
funcdef function get if import
in inout int interface int8
int16 int32 int64 is mixin
namespace not null or out
override private protected return set
shared super switch this true
typedef uint uint8 uint16 uint32
uint64 void while xor

Following those two rules, you can otherwise name your variables whatever you like. However, a recommendation (a “soft-rule”, if you will) is that you name your variables with an identifier that has to do with that variable's purpose. For example, if you have a variable that stores the water level in a pool, you might name it WaterLevelInPool. While giving your variables ambiguous or nonsense names won't result in an error, it will result in your code being incredibly difficult to read and understand, even by you.

// Good variable names
int WaterLevelInPool;
int ExplosionCount;
int NumberOfKills;
int DistanceToLocation;
 
// Bad variable names
int a;
int vakjnaldkjbv;
int _________;
int QQQQQQQ;

(While as a general rule it's a bad idea to give single letter names to variables, it is acceptable to do so in situations where the variable is used in a temporary setting.)

In addition to this, HPL3 recommends a particular convention for naming variables. It involves prefixing the name of a variable with letters that signify both its type and its scope (more on scopes in Lesson 7). For example, if I had an int variable named count that existed on the class scope, then I would call it mlCount. For a list of the prefixes, see this page on the wiki.

Variable Types

When we talk about a variable's type, we are referring to the kind of value or information that value stores. So far, whenever we've declared a variable, it's been with the type int, which means that the variable stores a number of some description. There are quite a few different types to choose from, however, and which one you decide to go with depends on the role a particular variable will play and what kind of value it will store.

Built-In Types

There are a number of types that are built-in with AngelScript. You can expect to find these types no matter which game you are programming for as long as you are using AngelScript.

Let's start with the integral types of AngelScript. These types are meant to hold numbers without decimal points.

Type Name Value Range Default
int8 -128 to 127 0
int16 -32,768 to 32,767 0
int -2,147,483,648 to 2,147,483,647 0
int64 -9,223,372,036,854,775,808 to 9,223,372,036,854,775,807 0
uint8 0 to 255 0
uint16 0 to 65,535 0
uint 0 to 4,294,967,295 0
uint64 0 to 18,446,744,073,709,551,615 0

These types are categorized based on two factors - size, and signedness.

The size of a type is how much memory it takes up in the program. This size is generally measured in bits. An int8, for example, uses 8 bits to store its value, whereas an int64 uses 64 bits. (The int and uint types use 32 bits, but as 32 bit the most frequently used size, the “32” part of the names have been chopped off for the sake of convenience.)

In computer memory, one bit is one switch in the computer's memory that can either be on or off. This is considered to have a value of 1 (on) or 0 (off). Stringing these bits together, you can use different combinations of bits to refer to larger and larger values. This is known as the binary system. You can learn more about the binary system here.

The signedness of a type is whether or not the values in the type can have a sign, or, in other words, whether or not the type supports negative numbers. If you compare int8 and uint8, you'll see that the range of int8 is -128 to 127, whereas the range of “uint8” is 0 to 255. The number of possible values in both cases is still the same (256 possible values), but the “signed” type has been shifted over so that 0 is in the middle of the range instead of the beginning.

Next, we have what are known as the floating-point types. These types are for representing numbers with decimal points.

Type Name Value Range Smallest Positive Value Maximum Number of Digits Default
float +/- 3.402823466e+38 1.175494351e-38 6 0.0
double +/- 1.79769313486231e+308 2.22507385850720e-308 15 0.0

You can see from the table that the floating-point types aren't quite as straight-forward as the integral types. Instead of a nice clean minimum and maximum value, we have a bunch of decimal numbers in scientific notation. This has to do with how floating-point types are stored in memory. That topic is fairly advanced, so I won't go over it here, but if you're curious, the process is described here.

The size of a float is 32 bits, whereas the size of a double is 64 bits. Size is a factor in floating-point types as well, but instead of being for the range of the value (although that is affected too), the size of a floating-point type has to do with its level of precision. As you can see in the table, the maximum number of digits you can use is 15 digits for a double, but only 6 digits for a float. These aren't hard limitations, however, but merely guarantees of accuracy. If you were to use a float value with more than 6 digits, say, 1234.5678, the program cannot guarantee the precision of that number. If you were to check that number while the program was running, it might have an actual value of the number might actually be something like 1234.562. This is because of rounding errors. The reason for it, again, has to do with how floating-point numbers are stored in memory, but you don't need to worry about the details too much. As long as you stay within the recommended number of digits, it should never be a problem.

The general rule for which type you should use is to use the smallest type that your application will allow. For example, if you know you are never going to deal with values less than 0 or greater than 255, then the uint is a great choice for your variable's type. That being said, the AngelScript runtime within HPL3 is optimized to use numerical types with 32 bits, so the general recommendation is to use int, uint, or float whenever possible.

There is one more built-in type for AngelScript, and it has nothing to do with numbers:

Type Name Possible Values Default
bool true, false false

The bool type name is short for boolean. Boolean values are restricted to one of two values: true or false. Booleans have to do with whether something is or is not. For example, some boolean variables you might store could be:

bool IsLightOn = true;
bool HasPlayerEnteredArea = false;
bool DidILeaveTheOvenOn = true;

Special Types

Beyond the built-in types of AngelScript, there are a number of other types that are specific to HPL3. If you were to go to another game that uses AngelScript (such as Wolfire's Overgrowth), chances are that that game's modding system won't have these types available (though it's possible they will have equivalents).

Type Name Possible Values Default
tString “Hello SOMA” <Empty>

That's all well and good, but how do you get at the values in the array? That is done using something called indexer notation. Let me show you how it works:

    lightLevels[0] = 5.0;
    float lightLevel = lightLevels[0];

The square brackets (along with the number inside them) make up the indexer notation. On the first line, we are taking the value 5.0 and storing it in the array at an index (in this case, index 0). The second line we are taking the value in the array at index 0 and assigning it to the variable “lightLevel”.

You don't have to just use index 0 either. You can use any index you want. A good rule of thumb, however, is to keep your values sequential, which means that if you have 10 values to store, store them in the array at indices 0 through 9.

It might seem odd that I am using index 0 in this example. When I am storing a value in an array at index 0, though, I am storing it in the first index of the array. That's because arrays in AngelScript, like most programming languages, are zero-based, which means the first value is stored at index 0. (One-based arrays might seem more intuitive, but it's easier on the engine if we use zero-based arrays instead, and programmers got sick of doing index - 1 a long time ago.)

In addition to adding a value at a specific index, there are also different ways you can more dynamically access an array. These ways use the array's functions, which we will get into in Appendix 2. For now, these are the more useful functions of an array and how they are used:

Function Name Description Example Usage
length Gets the number of values stored in the array.
push_back Adds a value to the end of the array. (If indices 0-4 are used, the value is stored at index 5.)
resize Resizes the array to the specified length, adding or removing values as necessary.


Lesson Three: Operators and Expressions

So we can create variables now, and we can store values in those variables. That's great and all, but there's only so much we can do with just assigning values. What we want to do is manipulate those values in whatever ways we want.

That is where operators come in. Operators perform an operation (appropriately enough) on one or more values. You've already been introduced to one of the operators: the assignment operator (=). Its job is to take a value on the right and assign it to a variable on the left. When you take several values and manipulate them or assign them to variables, the resulting line of code is called an expression.

There are a bunch of other operators as well, and they generally fall into one of three categories: math operators, boolean operators, and bitwise operators.

Math Operators

The the math operators are what you might expect - general arithmetic. Each one results in a numeric value.

Operator Name Example
+ Addition
int x = 2 + 5; // 7
- Subtraction
int x = 7 - 1; // 6
* Multiplication
int x = 4 * 3; // 12
/ Division
int x = 6 / 3; // 2
% Modulo
int x = 9 % 4; // 1

Those first four aren't really surprising. It's just basic math. That last one may be a new one, but it's not that complex. Basically, “modulo” is another name for “remainder”. So in the example above, when you do the modulo operation on 9 and 4, you perform the integer division on the two numbers and return the remainder (9 / 4 = 8 remainder 1, so 9 % 4 = 1).

When I say that modulo is another name for remainder, that's not entirely true. There are actually subtle differences between a remainder and a modulo, which are described on this website. That being said, when performed on positive numbers, modulo and remainder behave identically, which will in practice account for 99% of all times you will be using modulo in your code.

Some of you may have tilted your head quizzically in the last paragraph when I mentioned doing the “integer division” on some numbers. The reason that I specified what kind of division to perform is that, unlike the other operators, division behaves slightly differently when performed on integer types as opposed to floating-point types. When you perform division on floating-type, it results in what you might expect:

    float f = 5.0 / 2.0; // 2.5

However, doing the same division on integer types, you get a different result:

    int i = 5 / 2; // 2

This is because unlike the other operators, division between two whole numbers can result in a decimal number. However, integer types cannot hold decimal values. Because of this, if you store the result of a division that would be a decimal into an integer variable, the decimal part of the number is discarded. In other words, every decimal value is rounded down to the nearest whole number.

In addition to the math operators shown above that takes two numbers and assigns the result to a variable, there are also a set of math operators that are known as “compound operators”:

(In the following table, the initial value of <html>x</html> is <html>5</html>.)

Operator Name Example
+= Compound Addition
x += 2; // 7
-= Compound Subtraction
x -= 3; // 2
*= Compound Multiplication
x *= 3; // 15
/= Compound Division
x /= 2; // 2
%= Compound Modulo
x %= 2; // 1

As you can see, the compound operators are all the regular math operators joined together with the assignment operator (=). What they do is they take the value of the variable on the left, perform the specified math operation with the value on the right, and then assign the result back to the variable on the left.

These operators are a convenience operation that exists between a regular math operator and an assignment operator. The reason for this is because the following two lines of code perform the exact same operation:

    x = x + 5;
    x += 5;

There are two more math operators to know about - the increment operators:

(In the following table, the initial value of <html>x</html> is <html>5</html>.)

Operator Name Example
++ Increment
x++; // 6
-- Decrement
x--; // 4

Like the compound operators, the increment operators are shorthand for a specific operation. In this case, they are shorthand for adding or subtracting a value by 1. In other words, doing x++ will take the value of x, add 1, and then store the result back into x.

However, there's a special way that the increment operators can be used. They can either be used before the variable (known as “pre-increment”) or after the variable (known as “post-increment”). By themselves, a pre-increment and post-increment do the exact same thing. The difference happens when you use the increment operator alongside an assignment:

    int a = 5;
    int b = ++a;

What do you think the value of a is? How about b? As it turns out, the value of both a and b is 6. The reason for this is because the pre-increment operator ++a increments the value of a before assigning it to b. However, if I had the following instead:

    int a = 5;
    int b = a++;

What do you think the values of a and b are now? The value of a is still 6, but the value of b is 5. That's because with a post-increment operator, the operator waits until the overall expression is complete before incrementing the variable.

Another way of looking at how this works is to see the regular math operator equivalents of the above two examples:

    // Pre-increment equivalent
    int a = 5;
    a = a + 1;
    int b = a;
 
    // Post-increment equivalent
    int a = 5;
    int b = a;
    a = a + 1;

You might be wondering if the compound and increment operators behave identically to their arithmetic operator equivalents, then why even have them? There are two reasons for this. First, it's more convenient to use the shorthand operators when they are available (and never underestimate the value of convenience in programming). Second, when the code gets compiled, there are special instructions that can perform the compound and increment operations more efficiently than their regular math operation counterparts.

String Concatenation

When you are dealing with strings, you can add two string values together using the addition operator (+). This process is called “concatenation”. When you do this, the string value on the right is added onto the end of the string value on the left, and the result is a single string value:

    tString a = "abc";
    tString b = "def";
    tString c = a + b; // "abc" + "def" = "abcdef"

(This works with the addition compound operator as well.)

When dealing with strings and other values, you can concatenate any built-in type with a string as well. When you do this, the type is automatically converted into a string. For example, see the following:

    int x = 123;
    tString s = "abc" + x;
 
    // s becomes "abc" + 123, or "abc123"

Boolean Operators

Where the math operators return a numeric value, the boolean operators returns a bool type value. These operators are split into categories as well: the comparison operators and the logic operators:

Operator Name Example
== Is Equal To
bool b = 3 == 3; // true
!= Not Equal To
bool b = 3 != 3; // false
> Greater Than
bool b = 2 > 1; // true
< Less Than
bool b = 2 < 1; // false
>= Greater Than or Equal To
bool b = 3 >= 3; // true
<= Less Than or Equal To
bool b = 3 <= 3; // true

As you can see, they are fairly self-explanatory. The comparison operators all compare a value on the left with a value on the right, then returns true if the comparison is satisfied or false if not. For example, 1 == 1 would be true, whereas 1 == 2 would be false.

Note the difference between the assignment operator = and the comparison operator ==. One of these operations assigns a value to a variable, whereas the other one compares the values and returns a result. In order to differentiate them, when “sounding out” the code in regular English (or your relative language), always describe a = b as “a equals b” and always describe a == b as “a is equal to b”.

Where the comparison operators compare numerical values, the logic operators compare boolean values:

Operator Name Example
&& Logical AND
bool b = true && true; // true
|| Logical OR
bool b = true || false; // true
^^ Logical XOR
bool b = false ^^ false; // false
! Logical NOT
bool b = !true; // false

Each logical operator has a specific set of rules that determines whether the result of the logical operation between two values is true or false. The way to visualize these rules is using a construct called a truth table, which is a chart that plots all the possible values of the inputs and shows the result of the operation between those values.

First, the Logical AND operator (&&). The rule for this operator is “the result is true of both of the inputs are true”.

x     | y     | x && y
---------------------
false | false | false
true  | false | false
false | true  | false
true  | true  | true

Second, the Logical OR operator (%%||%%). The rule for this operator is “the result is true of either of the inputs are true”.

x     | y     | x || y
---------------------
false | false | false
true  | false | true
false | true  | true
true  | true  | true

Third, the Logical XOR operator (%%^^%%), which is short for “exclusive OR”. The rule for this operator is “the result is true of either of the inputs are true, but not both”.

x     | y     | x ^^ y
---------------------
false | false | false
true  | false | true
false | true  | true
true  | true  | false

The last operator Logical NOT (!) is special in that it only works on a single value. All it does is flip that value to the opposite.

x     | !x
---------------------
false | true
true  | false

Bitwise Operators

The bitwise operators are similar to the logical operators. The difference is that they work on numerical values and they affect the bit values of the number in binary form.

(For the sake of simplicity, we are going to use an imaginary 4-bit type <html>uint4</html> for values in examples for these operators.)

Operator Name Example
& Bitwise-AND
uint4 i = 2 & 1; // 0
| Bitwise-OR
uint4 i = 2 | 1 // 3
^ Bitwise-XOR
uint4 i = 2 ^ 3; // 1
<< Left Shift
uint4 i = 2 << 1; // 4
>> Right Shift
uint4 i = 4 >> 1; // 2
~ Bitwise-NOT
uint4 i = ~4; // 11

These are probably some of the least intuitive examples in this entire lesson. That's because instead of operating on the numbers, bitwise operators operate on the binary data underlying the numbers. For example, in our temporary 4-bit representation, the number 3 is represented in binary as “0011”.

For an explanation of how binary works, see the note in Lesson 2 in the section under “Built-In Types”.

The bitwise-AND, bitwise-OR, and bitwise-XOR operators (&, |, and ^, respectively) follow the same rules as their logical equivalents. (See above for a reminder of what those rules are.) The only difference is that it performs that comparison on the corresponding bits in two numerical values. For an easier visual representation, see the following example:

    uint4 a = 10;    // Binary: 1010
    uint4 b = 6;     // Binary: 0110
    uint4 c = a & b; // Binary: 0010

If you look at the binary representations in that example, pretend that the 0's are false and the 1's are true. You then take the rules for logical AND (&&) and apply them to each binary digit, comparing straight down. The first digit has a 1 in a and a 0 in b, so the result is 0. The second digit has a 0 in a and a 1 in b, so the result is again 0. The third digit has 1 in both a and b so the result there is 1. And the last digit has a 0 in both a and b, so the result is 0. If you then take those result digits “0010”, it becomes the binary representation for the number 2, which is the value that gets stored in c.

This same process happens for the bitwise-or and bitwise not operators as well. In those cases, you merely use the rules from the logical OR (%%||%%) and the logical XOR (%%^^%%) operators, respectively.

Try and guess what the results for the following two examples are:

    // Bitwise-OR
    uint4 a = 10;    // Binary: 1010
    uint4 b = 6;     // Binary: 0110
    uint4 c = a | b; // Binary: ????
 
    // Bitwise-XOR
    uint4 a = 10;    // Binary: 1010
    uint4 b = 6;     // Binary: 0110
    uint4 c = a ^ b; // Binary: ????

(Go ahead, try and work it out. Did you try? Don't continue until you do.)

If you got a binary result of “1110” (which represents 14) for the bitwise-OR operator and a binary result of “1100” (which represents 12) for the bitwise-XOR, then nice work!

The next two operators in the table are the left-shift operator and the right-shift operator (%%<<%% and %%>>%%, respectively). Where the previous bitwise operators performed comparisons on the bits of two values, the shift operators instead take all the bits of a binary number and moves them to the left or right by a specified number of digits. See the following example:

    uint4 a = 6;      // Binary: 0110
    uint4 b = a << 1; // Binary: 1100

As you can see, when a gets left-shifted by 1, all the 1's in the binary moved one digit to the left. The same thing happens when the right-shift operator is used:

    uint4 a = 13;     // Binary: 1101
    uint4 b = a >> 2; // Binary: 0011

All the binary digits of a got shifted two positions to the right. Notice, however, that this caused a 1 to get shifted right off the edge of the binary. When this happens, that 1 ceases to exist - even if you were to then left-shift b, you would only bet 0's back.

An interesting property of the shifting operators is that left-shifting is the same as multiplying by 2 and right-shifting is the same as dividing by 2 (integer division, that is). This is due to the nature of binary being a base-2 system. A similar nature happens with our regular numbers, which exists in a base-10 system - if you were to take a number like 500 and “shift” all the digits to the right, it becomes 50, which happens to be 500 divided by 10.

The last bitwise operator is the bitwise-NOT (~). It acts similarly to the logical NOT (!), and is also known as the bitwise-compliment. What it does is it takes all the bits in a number and flips them - all the 1's become 0, and all the 0's become 1:

    uint4 a = 5;  // Binary: 0101
    uint4 b = ~a; // Binary: 1010

Bitwise-operators are a bit more technical than the other operators, but as you can see, they do have fairly simple rules. Once you understand those rules, they aren't so bad after all!

The bitwise-operators have their own set of compound operators as well. They follow the same concept as the math compound operators - the value in the variable on the left gets put through the operator with the value on the right, and the result is stored back into the variable on the left:

    uint4 a = 2;
    a |= 4;
    // a is 6

Order of Operations

All the operators have what is known as a precedence. This means that when multiple operators are used at once, there is a strict order they follow which determines which ones get evaluated first. For example, if I had the following code:

    int i = 2 + 4 * 3;

Intuition might make you assume that i would have the result of 18, since 2 plus 4 is 6, which when multiplied by 3 gives 18. However, order of operations state that multiplication happens before addition. That would mean that you first have to multiply 4 and 3 to get 12, and 2 plus 12 is 14, which is the value that i will actually end up with.

Here is the full list of operators in order of their precedence (higher in the list means the operator happens first):

++ -- Increment Operators
! Logical NOT
~ Bitwise-NOT
* / % Multiply, Divide, Modulo
<< >> Bitwise Left-Shift, Right-Shift
& Bitwise-AND
^ Bitwise-XOR
| Bitwise-OR
<= < >= > Comparison Operators
== != ^^ Equality Comparison Operators and Logical XOR
&& Logical AND
|| Logical OR
= += -= *= /= %= &= |= ^= <<= >>= Assignment and Compound Operators

Whenever two operators with the same precedence occur in an expression, they are handled from left to right.

One thing you can do to sort of subvert the order of operations, however, is to group part of your expression inside parentheses. Whenever part an expression is within parentheses, that part gets processed first. So if you were to have the above example but with the addition operation within parentheses, you would end up with a value of 18:

    int i = (2 + 4) * 3;

Lesson Four: Statements and Flow Control

In the last lesson, you learned that expressions are lines of code where one or more values are manipulated or assigned to variables through the use of operators. There are other lines of code that do not use operators (or at least uses them in a different way). These lines of code are called statements.

The main use of statements in code is to control how a program moves from one line of code to the next. By default, when you run a program, it starts at the first line of code, moves down one line at a time, then stops once it passes the last line. This is well and good, but it's a bit limited, isn't it? What if you wanted to do part or all of your program multiple times? What if you wanted to skip part of it under certain conditions? These concepts are made possible in programming due to special statements that perform flow control.

Conditions

It is said that when the concept of conditionals enters programming, that is what truly separates a calculator from a computer. Being able to execute some code and skip other code based on a condition is incredibly powerful, and many programs wouldn't have been made possible without it.

If

The first step into conditional programming is the if keyword. When using if, you specify a condition in the form of a boolean value. If the value is true, the code under the if gets executed. If the value is false, then it doesn't.

    int i = 0;
 
    if (true)
    {
        i = 1; // the boolean value is true, so this code executes
    }
 
    if (false)
    {
        i = 2; // the boolean value is false, so this code gets skipped
    }
 
    // at the end of this script, i is 1

The structure of an if statement is a bit different from what you've been used to up to this point, so lets break it down.

First, you have the if keyword. Nothing groundbreaking quite yet. Next, you have a set of parentheses. These parentheses contain the condition that the if uses to decide whether or not to execute its code. (The parentheses are not optional, so don't forget to type them out.)

Within the parentheses, you have the boolean condition. In this case, it just happens to be a literal boolean value (true in the first case, and false in the second).

Then you have a set of curly braces that take up several lines of code. When this happens, the area inside the curly braces is called a code block. Code blocks are used to say “this code belongs to a statement”. In this case, this is the code block that belongs to the if statement - it is what may or may not execute depending on the condition.

In this example, we used literal boolean values as the condition. This is pretty boring, though - the first if statement has true so it will always run and there's no point in having an if at all. The second if statement has false, so it will never run and there's no point in having any of the code inside that block. To make if statements a tad more useful, you can use variables as the condition as well:

    bool b = true;
 
    if (b)
    {
        // Some code that runs if the value in <html>b</html> is true
    }

or even boolean expressions that utilize the comparison operators:

    int a = 5;
    int b = 2;
 
    if (a > b)
    {
        // Some code that runs if the value in <html>a</html> is greater than the value in <html>b</html>
    }

You can also chain together multiple conditions using the logic operators:

    int a = 2;
    int b = 5;
    int c = 9;
 
    if (a < b && b < c)
    {
        // Some code that runs if the value in <html>a</html> is less than the value in <html>b</html> AND the value in <html>b</html> is less than the value in <html>c</html>
    }

As you can see, you can get some pretty imaginative conditions going.

Else

In addition to if, there exists an else keyword. This keyword is a companion to if, and it will run a block of code if the condition within the if statement is false:

    int i = 5;
 
    if (i == 2)
    {
        i = 3; // Doesn't run since <html>i</html> is not equal to 2
    }
    else
    {
        i = 4; // This runs since the condition was false
    }
 
    // At the end of this script, <html>i</html> is 4

As you can see, the code within the else statement's code block only executes if the condition within the if statement is false. The if statement and the else statement used together is informally referred to as an if-else statement.

If-ElseIf-Else

One trick you can do with else statements is that you can use them to chain together multiple if-else segments. This can be called an if-elseif-else statement:

    int i = 5;
 
    if (i < 2)
    {
        i = 1; // Doesn't run since <html>i</html> is not less than 2
    }
    else if (i > 8)
    {
        i = 9; // Also doesn't run since <html>i</html> is not greater than 8
    }
    else 
    {
        i = 6 // All other conditions are false, so this will run
    }
 
    // At the end of this script, <html>i</html> is 6

Mentally, the if statement works just like a logical conditional sentence in plain English (or your own language). In the previous if example, for instance, it can be read out like so:

If the value in <html>i</html> is equal to 2, set <html>i</html> to equal 3.

You can include the else statement in this sentence as well:

If the value in <html>i</html> is equal to 2, set <html>i</html> to equal 3, else set <html>i</html> equal to 2.

Forming your conditions in logical prose like this is a useful way to visualize how you want your conditions to be structured.

Switch

As you can see, you can get some neat little if-elseif-else statement chains going along. But what if you had something like this?

    int i; // Is equal to some number
 
    if (i == 0)
    {
        // Do something if <html>i</html> is 0
    }
    else if (i == 1)
    {
        // Do something if <html>i</html> is 1
    }
    else if (i == 2)
    {
        // Do something if <html>i</html> is 2
    }
    else if (i == 3)
    {
        // Do something if <html>i</html> is 3
    }
    else if (i == 4)
    {
        // Do something if <html>i</html> is 4
    }
    // And so on...

That's a bit tedious, isn't it? Are you going to need to put in a new else-if statement for every single value that i could possibly be equal to?

As it turns out, there is a better way - the switch statement:

    int i; // Is equal to some number
 
    switch (i)
    {
        case 0:
            // Do something if <html>i</html> is equal to 0
            break;
        case 1:
            // Do something if <html>i</html> is equal to 1
            break;
        case 2:
            // Do something if <html>i</html> is equal to 2
            break;
        case 3:
            // Do something if <html>i</html> is equal to 3
            break;
        case 4:
            // Do something if <html>i</html> is equal to 4
            break;
    }

There's several bits of new information in there, so let's break it down:

First, you have the initial switch statement. Like the if statement, you start with the switch keyword followed by a value inside parentheses, which is in turn followed by a code block. Unlike the if statement, the switch statement does not use a boolean condition. Instead, it takes an integer value directly, then executes one of several code possibilities based on what that value is.

Within the switch statement's code block, there are several case keywords. case is used to start one of the “sub-block” sections that the switch statement could run. Each case keyword is followed by a value, denoting what value that case section represents. For example, if the value of i was 3, then the code within the case 3 section would be run.

At the end of each case section is a break keyword. This is another keyword related to flow control, and I will go into it in greater detail later on in this lesson. For now, just know that it marks the end of a case code section.

There are a couple of other ways which you can use a switch statement. First, you can specify a case section that will run if the given value is one of several possibilities:

    int i; // Is equal to some number
 
    switch (i)
    {
        case 0:
        case 1:
        case 2:
            // Do something if <html>i</html> is equal to 0, 1, or 2
            break;
        case 3:
        case 4:
            // Do something if <html>i</html> is equal to 3 or 4
            break;
    }

As you can see, there isn't any code placed inside cases 0, 1, or 3. This causes the case to “fall through” to the next case, making it so the code inside case 2 will run if i is equal to 0 or 1 as well.

You can also specify code to run if the given value doesn't match any of the cases:

    int i; // Is equal to some number
 
    switch (i)
    {
        case 0:
            // Do something if <html>i</html> is equal to 0
            break;
        case 1:
            // Do something if <html>i</html> is equal to 1
            break;
        case 2:
            // Do something if <html>i</html> is equal to 2
            break;
        default:
            // Do something if <html>i</html> is something else entirely
            break;
    }

In this example, if i doesn't match any of the cases (say if i were equal to 3), then the code within the default section will be run instead.

The break within the case sections is optional. If you do not put a break at the end of a case, then it will fall through just as though the that case section was empty. This is handy if you have code that you want to execute on one of several values, but other code that you only want to execute on a specific value:

    int i;
    int a = 1;
    int b = 2;
    int c = 3;
 
    switch (i)
    {
        case 1:
            a = 4; // Runs only if <html>i</html> is equal to 0
        case 2:
            b = 5; // Runs if <html>i</html> is equal to 0 or 1
        case 3:
            c = 6; // Runs if <html>i</html> is equal to 0, 1, or 2
            break;
    }

Loops

Conditionals are just one of the ways to have flow control in your program. Where conditions can make it so you can decide whether or not to execute some code, this section lets you specify whether to run some code over and over again for a specified amount of time. Appropriately enough, these statements are called loops.

While

The fundamental loop statement is called the while loop. It's fairly simple in concept - just run a block of code for as long as a boolean condition is true:

    int x = 0;
 
    while (x < 5)
    {
        x++;
    }

A while loop follows a specific pattern. When the program first reaches the while statement, it first checks the condition. If it is true, then the code within the code block is run. Once the code block finishes, the program goes back to the while statement and checks the condition again. If it's still true, then the code block runs a second time. This process repeats over and over again until the condition becomes false.

In the above example, x is 0. 0 is less than 5, so the program enters the code block, which adds 1 to x. After that, it goes back to the while condition, which checks x again. x is now 1, which is still less than 5, so the code block runs and x gets 1 added to it again. This keeps going for when x is 2, 3, and 4. Once x becomes 5, then the condition is checked once more. x is now no longer less than 5 (as it is now equal to 5). The condition is false, so the program skips down below the code block and continues to run.

One thing to be wary of when dealing with loops is the “endless loop”. This happens when a condition for a loop is such that the condition will never be false. The quintessential example of this is the following:

    while (true)
    {
        // Code in here will run forever
    }
 
    // Code out here will never run

Endless loops can never be broken, so the only way to get out of them is to force-close your program. In the programmer world, this is generally considered to be bad design.

Do-While

A do-while loop is very similar to a while loop with one key difference. Take a look at the following example and see if you can tell what it is:

    int x = 0
 
    do
    {
        x++;
    } while (x < 5);

The do takes the place of the while from a while loop, and the while keyword and condition is moved to after the code block. This reflects the single difference between a while loop and a do-while loop - a do-while loop will always run its code block at least once. More specifically, where the while loop checks its condition before it runs its code block, the do-while loop checks its condition after running its code block each time.

To demonstrate this, look at the following:

    // While loop
    int x = 0;
    while (false)
    {
        x = 1;
    }
 
    // Do-While Loop
    do
    {
        x = 1;
    } while (false);

The condition inside the while statement for both loops is set to false, so it is guaranteed to fail the condition check. For the while loop, this means that the code inside the code block never runs, and x will remain at 0. However, since a do-while loop is guaranteed to run its code block at least once, x will be 1 once the loop is terminated.

For

The while and do-while loop are great for when you want to repeat an action multiple times, but what if you want to operate over a range of values? Remember back in Lesson 2 and we were talking about arrays? Initializing an array was easy - all you had to do was declare an array variable. But what if you wanted to fill it full of values? Before, you would do it like this:

    array<int> arr;
    arr[0] = 0;
    arr[1] = 1;
    arr[2] = 2;
    arr[3] = 3;
    arr[4] = 4;

That's a bit wordy, isn't it? Although, all those lines of code do look repetitive, so this is a case where we can use loops to make it easier:

    array<int> arr;
    int i = 0;
 
    while (i < 5)
    {
        arr[i] = i;
        i++;
    }

That's a bit fancier. Instead of explicitly writing out each number, we use a loop that makes i go through each of the numbers with a loop and assign that number to each index of the array. It's still a bit wordy, though.

As it turns out, though, there's a loop statement designed specifically for this kind of situation: the for loop:

    array<int> arr;
 
    for (int i = 0; i < 5; i++)
    {
        arr[i] = i;
    }

Now that's concise. Let's go over how it all comes together.

First, as usual, the for keyword comes first, followed by some code surrounded by parentheses, and finally a code block. This code is split into three sections - a setup, a condition, and an increase. The setup code is run once, before the loop starts for the first time. Then the condition code is checked similar to the while loop - if the condition is true, then the code block is run. After the code block finishes, the code in the increase is run, followed by the condition getting checked again. This process is repeated (code block, increase, condition) until the condition becomes false.

In the above example, it starts by declaring the i variable and initializing it to 0. Then the condition is checked, which is true since 0 is less than 5. The code block runs, after which the increase section adds 1 to i. i is now 1, which is still less than 5, so the code block runs again. i becomes 2, 3, and 4, but once it becomes 5, the condition becomes false and the loop terminates.

All of the loop statements have been accompanied by a code block surrounded by curly braces, but this isn't actually strictly necessary. Instead, you can have a single line of code following the loop statement, and this line of code becomes the “code block” of the loop. For example, if you have the following loop:

    while (some condition)
    {
        // some code
    }

The brackets can be removed entirely, leaving the following:

    while (some condition)
        // some code

You can do this with the do-while loop and the for loop as well. Keep in mind, though, that this only works if the code body consists of a single line. If you have two lines:

    while (some condition)
        // some code
        // some other code

The second line of code is not recognized as being part of the loop, so it won't run until the loop terminates.

Loop Control

When you set up a loop, it will run until its condition becomes false (even if it has to run forever). However, sometimes, you want to have a bit more control over how the loop runs. For these cases, there are two special keywords - continue and break.

When you use continue in a loop, it tells the loop to skip the rest of the code block and jump straight to checking the condition (or, in the case of a for loop, running the increase code):

    int x = 0;
 
    while (x < 5)
    {
        x++;
        cLux_AddDebugMessage("A");
 
        continue;
 
        cLux_AddDebugMessage("B");
    }

If you had this code running in HPL3, it would print “A” 5 times but “B” only once. That's because the continue causes the “B” line of code to not run as the program skips the rest of the code block and goes right back to the condition.

The second loop control keyword is break. What it does is it causes the loop to stop entirely. It skips the rest of the code block and terminates the loop:

    int x = 0;
 
    while (x < 5)
    {
        x++;
        cLux_AddDebugMessage("A");
 
        break;
 
        cLux_AddDebugMessage("B");
    }

Run in HPL3, this code would only print a single “A”, even though its logic would say that it should loop 5 times. That's because when the program reaches the break, the loop stops right where it is and exits, going to the next code after the loop.

If you remember, break is the same keyword that is used to mark the end of a case section in a switch statement. It's role there is the same as in a loop - it causes the program to exit the switch block instead of falling through to the next case section.


Lesson Five: Functions

So what happens if, as you're making a program, you have a lot of places where you are copying and pasting the same code over and over again? This is repetitive and wordy, not to mention if you find out you have to change that code then you have to change everywhere else that you used that code as well. This is where functions come in.

A function is a construct in programming that takes a section of code and wraps it up into a package that you can then refer to any time you want to call that code. This is immensely useful if like in the hypothetical example you need to run a subsection of your code many times over the course of your program. It also helps you to organize your program, separating your code into easily describable and readable chunks.

Function Structure

The structure of a function is similar to that of the statements that you learned about in the previous chapter:

    void FunctionName()
    {
        // some code goes in here
    }

As you can see, a function consists of several parts.

First, you have the void keyword. This says that the function does not return a value. (More on returning values in a bit.)

Next, you have the function name. This name is an identifier, just like a variable name is an identifier, and it follows the same rules for what you can name it. And just like how you use variable names to get the values within variables, you use the function name when you want to run the function's code.

Then you have a set of empty parentheses. I explain the importance of these parentheses in more detail in the “Parameters” section of this lesson, but know that having opening and closing parentheses is the bare minimum required.

And finally, you have a code block, shown by the curly braces. Just like the statements in Lesson 4, the code inside the code block “belongs” to the function.

So now you have a function defined (that's what it's called when you create a function), but how do you run that function's code? See this example:

    // Elsewhere in your code
    FunctionName();

It's just that simple. All you need to do is write out the name of the function followed by those parentheses again. This is known as calling a function. It tells the program to jump to where you defined the function and run its code. Once it finishes that code, it will jump right back to this spot and continue on.

Functions are everywhere, not just in AngelScript, but in many other popular languages as well. This is because most languages have a special function (usually called Main) that tells the computer where the program should start running.

In fact, nearly all the code you write is going to be within a function somewhere. Remember the “Hello World” example from Lesson 1?

    ////////////////////////////
    // Run when entering map
    void OnEnter()
    {
        cLux_AddDebugMessage("Hello SOMA");
    }

There's a function right there! The function is called OnEnter, and as the comment says, it is called by the HPL3 engine whenever the map is entered.

Parameters

I promised that I would explain the importance of those parameters in greater detail, and I will do that now. The parentheses are for defining a function's parameters.

A parameter (also known as an argument) is how you pass information into the function from outside. You do this like so:

    void FunctionName(int x)
    {
        int y = x + 5;
    }
 
    // ...
    // Elsewhere in your code
 
    FunctionName(2);

Inside the parentheses, we do something that looks a lot like declaring a variable. What this does is it defines a parameter called x which is of type int. When calling the function, we pass an int value inside the parameters, which is what the value of x will be inside the function.

If you've dealt with functions in algebra, this might be a bit familiar:

    f(x)   =     x + 5
    
    f(1)   =   (1) + 5
    f(2)   =   (2) + 5
    f(-12) = (-12) + 5

In the algebra function f(x), when you put a number in for x on the left, you substitute that number everywhere where x appears on the right.

It's also possible to define more than one parameter:

    void FunctionName(int x, tString y)
    {
        tString z = y + x;
    }
 
    // ...
    // Elsewhere in your code
 
    FunctionName(5, "abc");

The parameters are separated by a comma, both when defining them in the function and when passing the values in calling the function. The order that the parameters appear stays the same - the 5 goes to x, and the "abc" goes to y.

Keep in mind that you cannot pass a value of the wrong type to a function. If, for example, you were to do this:

    FunctionName(5, 10);

That second parameter is defined as being a string, but 10 is not a string (not to be confused with "10", which is a string). If you tried to do this, then HPL3 would consider it an error, and your code will not run.

Returning Values

In addition to receiving information, a function can also be set up to give information back to the code where it was called:

    int FunctionName()
    {
        return 5;
    }

There are a couple of new things here.

First, the void from before has been changed to an int. This means that the function will be returning an int value when its code finishes running. You can define this type as anything you want - the function could return a bool, a tString, a float, or anything. (Or nothing, as in the case of our void examples before.)

Second, we have a new keyword in return. What it does is it signals the function to stop running the code (similar to break from Lesson 4) and take the specified value as the return value. Back where the function was called, the code can take that value and do something with it:

    int y = FunctionName();
    // y is 5

Keep in mind, however, that if you define that a function returns a value, you must return a value with return somewhere in the function code. If you had a function like this, for example:

    int FunctionName()
    {
        int x = 5;
    }

Notice that the function was defined with int as the return type, but the code doesn't use return anywhere. This will be recognized as an error by HPL3.

A function must always return a value, so beware of cases where you are putting your return inside conditional statements. Take the following example:

    int FunctionName()
    {
        bool b = false;
        if (b)
        {
            return 5;
        }
    }

Even though there is a return in the function, it is inside an if block. The condition for the if statement is b, which happens to be false. In this case, the if code will not run, which means that the program will never reach the return. This leaves a path in the code in which a return doesn't get called, which will still result in an error. To prevent this, you need to make sure that your function has a return no matter which path your code decides to take:

    int FunctionName()
    {
        bool b = false;
        if (b)
        {
            return 5;
        }
        else
        {
            return 0;
        }
    }

Now there is an else alongside the if, which guarantees that no matter what b is, the function will reach a return at some point.

So now we have both parameters and a return type. Bringing them together, let's create a function that performs a useful action that we might want to do many times over the course of the program.

    int AddFive(int x)
    {
        return x + 5;
    }

Now in our program, whenever we need to add 5 to a value, we can just do this:

    int y = 5;
    int z = AddFive(y);
    // z is 10 (5 + 5)

In this code, y is passed to the function with a value of 5, so x within the function gets that value. The function then adds 5 to that value (resulting in 10) and returns it. Back outside the function, the return value is assigned to the variable z.

If you were to take the above function and compress it down to the following form:

    int AddFive(int)

This is called a function's signature. It takes all the relevant information for identifying a function and discards everything else (return type, function identifier, parameter types). Note that the identifier for the parameter has been removed. This is because the identifier for the parameters are not actually necessary in determining the signature of a function - all it needs to know is how many parameters there are and what types they are.

Note that in this example, y is passed as a parameter. However, nothing that happens inside the function will change the value of y. Even if we did this:

    void AddFiveToY(int y)
    {
        y = y + 5;
    }
 
    // ...
    // Elsewhere in your code
    int y = 5;
    AddFiveToY(y);
    // y is still 5

This is because when you pass a variable as a parameter to a function, it does not pass the variable itself. Instead, it takes the value inside the variable and copies it into the parameter of the function. This is called passing by value, and this happens whever you pass something as a parameter in AngelScript.

The alternative to passing by value is called passing by reference, in which you do actually pass the variable itself to the function. This concept is a bit more advanced for this tutorial series, but I do talk about it in the appendix section.)

Function Overloading

Sometimes, we want to create multiple functions that behave similarly but take different types as their parameters. For example, if we wanted to do the following:

    void PrintIntToDebug(int i)
    {
        cLux_AddDebugMessage("Debug: " + i);
    }
 
    void PrintStringToDebug(tString s)
    {
        cLux_AddDebugMessage("Debug: " + s);
    }

As you can see, these two functions behave almost identically, but since they take two different types as parameters, they cannot be made into a single function. However, there is one thing you can do to make your code simpler - function overloading.

When you overload a function, that means you are defining a function that has the same name as another function. Normally this isn't allowed, but it is when the second function has either a different type for its parameter(s) or a different number of parameters. In this case, we can use function overloading because our two functions have different types as parameters:

    void PrintToDebug(int i)
    {
        cLux_AddDebugMessage("Debug: " + i);
    }
 
    void PrintToDebug(tString s)
    {
        cLux_AddDebugMessage("Debug: " + s);
    }

Now you can call both functions using the same identifier:

    PrintToDebug(5);
    PrintToDebug("abc");

HPL3 is smart enough to look at the type of the value you are passing as a parameter and automatically pick the correct function for that type.

While you can overload functions by having different parameter types, you cannot overload a function by giving it a different return type. Doing so will result in an error when HPL3 tries to run your code. This is because when HPL3 looks at different functions, it only looks at the function name and the parameter types, not the return type. So while the above overload is perfectly legal, the following overload is not:

    // First function, minding its own business
    int SomeFunction(int x)
    {
        return 5;
    }
 
    // This function differs from the first only by return type, so it's not a legal overload
    tString SomeFunction(int x)
    {
        return "abc";
    }


Lesson Six: Calling Other Scripts

When you're writing your program, you can find yourself ending up with a lot of code. If all this code is inside a single file, that can make things difficult to manage - whenever you want to add or change something, you have to pour through hundreds or thousands of lines of code looking for that one spot. Also, what if you want to use some code that was written by someone else? Do you just copy their code and paste it into your own? What if they update their code, forcing you to find everywhere in your code that was updated and perform the update yourself?

Include

As I'm sure you've surmised by now, there's an easier way. There's a special keyword that references a script file and “pastes” it on top of another file. This allows functions and other things to be called from that other file without requiring that the code is in the same place - the include keyword:

code1.hps

    int Add(int a, int b)
    {
        return a + b;
    }

code2.hps

    #include "code1.hps"
 
    // ...
    // Elsewhere in your code
 
    int x = 3;
    int y = 5;
    int z = Add(x, y);

Most of this stuff you've already seen, so I'll focus on the relevant bits. In the first file, we have defined a function called Add, which we want to call from the second file. At the top of the second file, we put the #include statement followed by the name of the first file in quotes. From then on in the second file, we can call the Add function in our script just as if it was defined in the second file to begin with.

A restriction of the include statement is that it must be placed at the top of a file. If you had the following code:

    int x = 3;
    int y = 5;
 
    #include "code2.hps"
 
    int z = Add(x, y);

This code would result in an error, because the include statement is not at the top of a file.

The # before the include keyword is required whenever using an include statement. That is because an include statement is a special kind of statement called a pragma. Instead of working with the program while it is running, a pragma tells the compiler to do a certain task when the script is getting ready to run for the first time. In this case, the include pragma tells the compiler to include references to stuff inside another file so it can be called from the calling file, something that would be much harder to do once the program was already running.

A thing to note about the path that you put inside the quotes. When HPL3 gathers all of its assets, it stores them in a handy reference table where they can be referenced using any part of their path. For example, if you had a file at “script/utility/props/omnitool/omnitool_code.hps”, you could include it using any of the following lines:

    #include "omnitool_code.hps"
    #include "omnitool/omnitool_code.hps"
    #include "props/omnitool/omnitool_code.hps"
    #include "utility/props/omnitool/omnitool_code.hps"
    #include "script/utility/props/omnitool/omnitool_code.hps"

Each of those include statements will work in the exact same way. Simplicity would state that you can just do the first line, while a desire to be explicitly specific would say that you should do the last line. The general advice as to which one you should use is to use the shortest one that illustrates the intent of that script. For example, you can use “omnitool/omnitool_code.hps” to illustrate that it is a group of code intended to work on the Omnitool, or you could use “props/omnitool/omnitool_code.hps” if there are other scripts from the “props” folder you will be importing as well. (You should almost never use the shortest option unless the name of the file itself is enough description.)

Namespaces

The ability to group code from different files together is incredibly useful for keeping your code clean and organized. However, there arises a problem. What if you have two files that look something like this?

code1.hps

    int Add(int x, int y)
    {
        return x + y;
    }

code2.hps

    int Add(int x, int y)
    {
        return x + y + 5;
    }

We have two Add functions in two different scripts, and one does something different than the other. However, if we include both of these script files into a third script file, it will throw an error, because the function signatures are the same. (Remember when I talked about function overloading in Lesson 5?)

We need to be able to resolve this conflict. The simplest way would be to rename the functions themselves to something like AddA and AddB, but that can be tedious and lead to poorly-maintained code. What we need instead is a way to differentiate these two functions based on the purposes they exist for.

As it turns out, there's a keyword for just such an occasion: namespace:

code1.hps

    namespace Code1
    {
        int Add(int x, int y)
        {
            return x + y;
        }
    }

code2.hps

    namespace Code2
    {
        int Add(int x, int y)
        {
            return x + y + 5;
        }
    }

Now the two functions are placed within different namespaces. A namespace is a special block of code that says “everything inside this block must be accessed using the name of the namespace as an identifier”. This is cool, because since the functions now exist in different namespaces, they will no longer conflict with each other.

Now let's see how to access these functions. As it turns out, you do it with a new operator called the scoping operator:

code3.hps

    #include "code1.hps"
    #include "code2.hps"
 
    // ...
    // Elsewhere in your code
 
    int a = 3;
    int b = 5;
 
    int x = Code1::Add(a, b);
    int y = Code2::Add(a, b);

As you can see, the scoping operator is ::. When the scoping operator is placed between a namespace identifier and some other identifier (such as a function or variable name), it tells the program to look for that identifier specifically within that namespace. For example, in the previous code, when we want the Add function within the Code1 namespace, we reference it by typing Code1::Add(...).

One neat thing you can do with namespaces is that you can nest them as well:

    int Add(int a, int b)
    {
        return a + b + a + b;
    }
 
    namespace A
    {
        int Add(int a, int b)
        {
            return a + b;
        }
 
        namespace B
        {
            int Add(int a, int b)
            {
                return a + b + 5;
            }
        }
    }

When referring to nested namespaces, you simply just chain together scoping operators to get to the function you want:

    int a = 3;
    int b = 5;
 
    int x = A::Add(a, b);
    int y = A::B::Add(a, b);

You may have noticed that in this previous example, I had a third Add function as well, outside either of the namespaces. If you want to access that one, you can do it by using the scoping operator to reference the global namespace, which everything is implicitly part of. You can do this by just having the :: before the name without a namespace identifier:

    int x = 3;
    int y = 5;
    int z = ::Add(x, y); // Refers to the global namespace version of <html>Add</html>

Scope

When dealing with namespaces, it's important to understand the concept of scope. Scope talks about the visibility of things like functions and variables, and it determines what other code can see and access them. For example, say we had the following code:

    namespace A
    {
       void DoOtherWork()
       {
           DoWork();
       }
    }
 
    void DoWork()
    {
        DoOtherWork();
    }

(Ignore the infinite loop potential in this code for a moment.)

Inside DoOtherWork, the DoWork function gets called, which is perfectly fine. In turn, DoWork calls DoOtherWork. This, however, is not fine, because DoOtherWork is outside of DoWorks scope.

The general rule of scope is that a new level of scope is created wherever there is a new code block. Things inside a code block can access anything in code blocks above it, but anything outside of a given code block cannot access things that were declared or defined within it. How many statements involve creating new code blocks? Functions do, namespaces do, and even the conditional and looping statements like if and while do.

Take this visual representation, for example:

The colored lines represent levels of scope. Anything can access anything else as long as the place calling it shares all the same colors as the thing being called. For example, DoWork in namespace B can access AddIf in namespace A because DoWork exists in that scope (AddIf has the scope colors red and yellow, which DoWork also has). However, the opposite is not true - AddIf would not be able to call DoWork because DoWork is in a higher scope (DoWork has the scope colors red, yellow, and magenta, but AddIf does not have magenta in its colors).

There are also some errors in this image. In the DoWork function, the y variable is declared within the while loop. The function then tries to assign a value to y once the loop is done, but this is not allowed, because y exists in the scope of the loop and cannot be accessed outside of it. Similarly, in Foo, the function tries to access the z variable of the DoWork function, which is so far outside its scope as to be just silly.

When referring to how “high” of a scope something has, think of the above image as though it were laying on its side with the colors going up like pyramids. Colors that are “higher up” on the pyramid are referred to as having a higher scope. Just remember that when it comes to scopes, higher means more limited.


Lesson Seven: Classes

Using the knowledge you've gained thus far, you can make some pretty respectable programs. In fact, things before this point made up the bulk of what programming was like back when C was the new kid on the block. It's a style known as procedural programming. It's a simple concept - a program has a start and it goes down a list of instructions until it reaches an end.

Nowadays, though, programming has evolved into a myriad of different styles. The one that has grown to be the most popular among the highest-used programming languages is known as object-oriented programming (OOP).

What is Object-Oriented Programming

Imagine you had a program in which you needed to maintain a bunch of related variables. In this scenario, let's pretend that our program needed to simulate a bike down to various details:

  • A bike has two wheels, a seat, and gears.
  • The wheels can be a particular size.
  • The seat can be at various heights.
  • There can be different numbers of gears.
  • The bike itself could be moving at various speeds, or fully stopped.

There's our description of a bike. (Ignore the fact that it leaves a few things out - we don't want to overcomplicate our example.)

Let's make a program now that would store these variables:

	int gearCount;
	int wheelSize;
	float seatHeight;
	float bikeVelocity;

That doesn't look so bad. But what if our program had to simulate more than one bike? How would you store the information on each one? I suppose you could just have groups of variables for each bike:

	int gearCount1;
	int wheelSize1;
	float seatHeight1;
	float bikeVelocity1;
 
	int gearCount2;
	int wheelSize2;
	float seatHeight2;
	float bikeVelocity2;
 
	int gearCount3;
	int wheelSize3;
	float seatHeight3;
	float bikeVelocity3;

That's not very helpful, is it? Sure, we can store information on three bikes, but what about four? Or a hundred? If we have to type out a new set of variables for each bike, this program is going to get really cumbersome really fast.

This situation does look familiar though. Perhaps we can use arrays to simplify things a bit?

	array<int> gearCounts;
	array<int> wheelSizes;
	array<float> seatHeights;
	array<float> bikeVelocities;

This is a bit better. Now we can store information on as many bikes as we want. As a bonus, since we are using arrays, we can also use a loop to access them all instead of referring to each one individually by hand.

This still isn't quite right, though. For one, there isn't any relationship between the arrays apart from their assumed one. In other words, there's nothing in the language that joins together these four arrays in order to set them apart as being intended for a common purpose.

There's also another problem. Each “bike” is represented by the values in the same index across all four arrays. (i.e. Bike “1” would be represented by the data in gearCounts[1], wheelSizes[1], seatHeights[i], and bikeVelocities[1].) But what if someone came along and shoved some information in some arrays without doing it in the others? Now all the data is out of order, and since there's no way to tell which data originally belonged to which bike, there's no way to reverse it, so your entire data structure just became corrupted beyond repair.

What we need is a way to group these four variables together into a single thing so that no matter what, the same four values will always be linked. This is where object-oriented programming comes in.

Classes

In OOP, an “object” is a group of variables (called fields) that join together to form a cohesive whole. Together, they can represent various tangeable concepts that would be incredibly difficult to manage otherwise.

For our bike example, instead of depending on the interoperability of distinct values, we can create an object representation of the bike itself by defining a class:

	class Bike
	{
		int gearCount;
		int wheelSize;
		float seatHeight;
		float bikeVelocity;
	}

From a technical standpoint, there is only one new keyword in there for you - the class keyword. After that keyword comes the identifier for the class, followed by a code block that forms the body of the class.

We can now use this class whenever we want to create a bike instance:

	Bike myBike;

In OOP, an object is the blanket term for a particular thing or concept with defined properties. A class is a bunch of code that represents the blueprint of an object, much like an architect would create a blueprint of a house. An instance represents a single manifestation of that class.

You can think of it like this. The conceptual idea of Toyota Corola is like the object, the blueprints at the Toyota factory are like the class, and an individual car you see driving around is like the instance.

Just to confuse you, programmers in different disciplines sometimes use the terms “object” and “instance” interchangeably. There's not really a way to reconcile this, so I'm just giving you a heads up.

Now that we have an instance, we are going to need to be able to access the fields within that instance. Remember the note back in Lesson 2 about dot-notation? As it turns out, this is exactly how we do it:

	// Getting the value of a field
	int size = bike.WheelSize;
 
	// Setting the value of a field
	myBike.gearCount = 5;

As it turns out, it's not just fields we can have in objects. We can also define functions too:

	class Bike
	{
		int gearCount;
		int wheelSize;
		float seatHeight;
		float bikeVelocity;
 
		// circumference = PI * diameter
		float GetWheelCircumference()
		{
			// cMath_PI is a globally available value equal to PI 
			// (or a close enough approximation to it, rather)
			return wheelSize * cMath_PI;
		}
	}

We access them in the same way we would access fields as well:

	Bike bike;
	bike.wheelSize = 600; // in millimeters
	float wheelCircum = bike.GetWheelCircumference();

The technical description for this accessing process is actually using a new kind of operator known as the member access operator (.). When this operator is used on an instance of an object, it tells the program to “look inside this object and get the member with this name”.

The term “member” is used to describe any part of the inside of a class. Fields, functions, and other things are all considered a “member”.

Now that we have our Bike class all set up, lets see how we can use it in our program:

	array<Bike> bikes;
 
	for (int i = 0; i < 100; i++)
	{
		Bike bike;
		bike.gearCount = 5;
		bike.wheelSize = 600;
		bike.seatHeight = 3.0;
		bike.bikeVelocity = 50;
 
		bikes.push_back(bike);
	}

That's pretty neat, isn't it? Since Bike is just another type (an object type, but still a type), you can easily create an array of them. Plus, all the information for each bike is packed within each Bike instance, so there's no chance of information getting messed up with other bikes. It's all just safely packed in neat little packages that the program itself keeps organized so you don't have to worry about it.

“Regular” types like int or bool are called primitive types because they are intrinsic to the programming language itself. The difference between primitive types and object types is that primitive types merely store a single value, whereas object types can store multiple values or even other object types in their fields. In Lesson 2, the numeric types and the bool type are all primitive types, and everything else are object types.

(The tString type is a bit of a special case in that it's sort of both kinds of types at the same time. Just don't think about that too much.)

Constructors

When an instance of a class gets created, all of the fields in that instance are set to their default values (depending on their types). However, for some object types, you don't want that to happen. Say, for example, our bike had a gear switching field that represented shifting between gears from 1 to 5 (assume there are 5 gears):

	class Bike
	{
		// ... 
		int currentGear;
	}

When you create an instance of Bike, currentGear will have an initial value of 0. That doesn't make any sense, though, because there are only gears one through five. Any other value for currentGear would be an incorrect value.

When defining a class, you can define a special kind of function called a constructor. The job of a constructor is to provide a way to create an instance of an object that you can customize so that various fields have an initial value that you specify. Constructors can also be defined with parameters, allowing code to pass in values that they want the constructor to use.

	class Bike
	{
		// ...
		int currentGear;
 
		Bike()
		{
			currentGear = 1;
		}
 
		Bike(int startingGear)
		{
			currentGear = startingGear;
		}
	}
 
	// ...
	// Elsewhere in your code
 
	// Calling the default constructor without parameters
	Bike bikeWithDefaultGear;
 
	// Calling the second constructor with parameters
	Bike bikeWithSpecificGear(3);

As you can see, constructors are defined similarly to functions. There are two key differences - first, the name of the constructor function must be the same as the name of the class, and second, a constructor does not define a return type.

As far as how they are used, the constructor with parameters defined is called just like a function - by putting parentheses around the parameter(s). The parameterless constructor is called implicitly whenever the instance is declared, so there's no need to have an empty set of parentheses in this case.

Member Qualifying

You know that when you want to access a variable, field, or parameter, you use its name, of course. But what would happen in the following situation?

	class Foo
	{
		int a; // this a is 5
 
		void DoSomething(int a) // this a is 3
		{
			int b = a;
		}
	}

Which a is the code within the function referring to - the field or the parameter? As it turns out, the variable with the highest scope takes precedence, which in this case would be the parameter. (The field is in the class scope whereas the parameter is only in the function scope.) That means that when the function is done, b will have a value of 3.

That's good to know, but what if you needed to access the field instead of the parameter? Do you always have to name the parameter and the field different names to avoid these conflicts?

Well, there's not a strict convention around it, though in my opinion you should avoid doing that anyway just to prevent potential confusion. However, if you do find yourself in a situation where you need to refer to the field when a same-name parameter exists, there is a special keyword for just such an occassion:

	class Foo
	{
		int a; // this a is 5
 
		void DoSomething(int a) // this a is 3
		{
			int b = this.a;
		}
	}

The this keyword performs something called member qualification. What that means is that it ensures that the a being referenced is the field instead of the parameter by “qualifying” it.

What this strictly does is it provides a reference to the current object. For example, if you had the following:

	class Foo
	{
		int a;
 
		void DoSomething()
		{
			this.a = 5;
		}
	}
 
	// ...
	// Elsewhere in your code
 
	Foo foo;
	foo.a = 5;

The foo variable outside of the class and the this keyword inside of the class are both referring to the same underlying code in memory.

There are some conventions that state you should always use the this keyword when referring to a field within the class. This follows the train of thought that you should always be as explicit as possible when programming so as to reduce ambiguity and confusion to zero (or as close as you can get). Personally, I find this convention to be a bit tired and old-fashioned, as it promotes verbose coding practices that cause programming to take longer than it needs to. On the otherhand, there's something to be said about being explicit in cases where your intent isn't obvious. So my personal suggestion is to use this when referring to a member of a parent class just because in the child classes, it's not immediately apparent where that member is defined. (“Parent” classes and “child” classes are covered under the appendix section Inheritance.)


Appendix: Miscellaneous AngelScript Features

This is a list of features in AngelScript that are either infrequently used, can be confusing to understand, or are not as well known. They are not presented in any particular order, so you should treat this section as a reference rather than a contiguous series.

Enums

As an example, let's say you had an object that stored a particular state, such as a car's ignition position:

	class Car
	{
		// 0 = off
		// 1 = accessory
		// 2 = on
		// 3 = starting
		int ignitionPosition;
	}

In order to know the current position of the car's key ignition, you set the value of ignitionPosition as one of several values. The problem is that you need to refer to the comments in order to know what each number means. Just having a value of 0 to 3 isn't very descriptive when you need to set the value elsewhere:

	Car car;
 
	// ...
 
	// Turn on car
	car.ignitionPosition = 3;

Without referring to the comment, could you tell what the 3 means in that code? No, it's just some magic number that has been assigned arbitrary significance.

Instead, you can use an enum, which is short for enumerated type. This type is a special kind of type that instead of having fields and functions, merely stores a list of identifiers. These identifiers are mapped to specific integer values and can be used interchangeably with them:

	enum IgnitionPosition
	{
		IgnitionPosition_Off,
		IgnitionPosition_Accessory,
		IgnitionPosition_On,
		IgnitionPosition_Start
	}

Now you can code your Car class like this:

	class Car
	{
		IgnitionPosition ignitionPosition;
	}

…and assign to it like this:

	// Turn on car
	car.ignitionPosition = IgnitionPosition_Start;

Now instead of relying on the values of magic numbers, you can use defined identifiers that are self-descriptive and clear.

In reality, because enums just assign an identifier to an integer value, IgnitionPosition_Start is equal to 3 because it is the fourth value in the IgnitionPosition list. (The list starts at 0.) So if you're in a situation where for whatever reason you can't pass around your enum type, you can just treat it as an int instead.

Alternatively, you can also specify what values you want a specific enum to be like so:

	enum SomeEnumType
	{
		Zero,
		One,
		OneHundred = 100,
		Two = 2,
		Three, 
		Four
	}

The integer value of OneHundred would be 100, as defined. Likewise, Two would be equal to 2. Three in this list would be equal to 3, because if a value is not explicitly given, the value of an enum is equal to the value of the previous one in the list plus 1.


Constants

Sometimes when you are writing a program, your code will contain what is known as “magic numbers”. These are numbers that you have chosen to fulfill a certain purpose, but that purpose is not illustrated in the number's usage, so anyone who doesn't know what you are doing just has to trust that the number will work. Take, for example, the following code:

    float a = 3.1415 * r * r;

What does the 3.1415 mean in that code? Well, the astute of you will know that it is (approximately) the value of Pi, which makes that code the computation of the area of a circle with the radius r. But there's a reason that we use the term “Pi” when dealing with these values, because it's tedious to have to refer to it by its actual value all the time. These terms are known as constants.

A constant is like a variable, but it is marked with the keyword const to signify that it can never change its value. It might sound a bit arbitrary to have a variable that can't have its value changed, but it's useful because now we can deal with a specific chosen number in a way that avoids the “magic number” problem.

To declare a const, simply declare it like you would a variable with the addition of the const keyword:

    const float Math_PI = 3.1415;

The naming convention for constants varies depending on your programming environment, but in HPL3, the convention has been the name of the “category” that the constant exists given in PascalCase (which means the first letter of each word is capitalized) followed by an underscore and the constant's name. The name of the constant is traditionally given in all caps with underscores used to separate words. For example, a constant that stores the base driving speed of a car in a system might be named TrafficVehicles_CAR_BASE_SPEED.

Now we can now convert the above code to the following:

    float a = Math_PI * r * r;

Now it's pretty obvious as to what this code is doing. It's taking the value of Pi and multiplying it by r and r (which is the programmatic way of representing “r-squared”). Pi times r-squared… that's the textbook definition of the area of a circle.

In HPL3, variables declared in the global scope must be constants. This is due to the way that HPL3's script value saving mechanisms were designed. A way to get around this, however, is to use a class or array. While you would not be able to assign a new object value to the constant, you could still manipulate the class members of a constant object, or add/remove values in a constant array.


Default Parameters

When defining a function, you can have as many or as few parameters as you want. Sometimes, however, you're in a situation where you have several function overloads, each that just adds a single parameter to the end of the last function. As an example, see here:

    int Add(int a, int b)
    {
        return a + b;
    }
 
    int Add(int a, int b, int c)
    {
        return a + b + c;
    }
 
    int Add(int a, int b, int c, int d)
    {
        return a + b + c + d;
    }

(…and I could stretch this example all the way from a to z and beyond, but you get the idea.)

In this situation, adding so many overloads just to satisfy an arbitrary number of conditions gets pretty verbose pretty quickly. Fortunately, there's something you can do to simplify this situation. You can define functions to have default parameters.

Also known as optional parameters, default parameters let you define a function where a certain number of the parameters are optional, and if you call the function without specifying those parameters, they are given a default value instead. In this way, you can have a function with 20 parameters but only need to worry about passing 2 of them, and the program will fill in the rest for you.

The syntax for doing this is pretty simple. All you need to do is to “equals” a parameter to a value when you are declaring it in a function definition:

    int Add(int a, int b, int c = 0, int d = 0)
    {
        return a + b + c + d;
    }

In this function, c and d are set to be optional parameters with a default value of 0. You can call this new function using any of the following ways:

    Add(1, 2);       // returns 3
    Add(1, 2, 3);    // returns 6
    Add(1, 2, 3, 4); // returns 10

In the first call, the values corresponding to the c and d parameters are left out, so the program substitutes the default value 0 in the function code. When it adds the parameters a, b, c, and d together, since c and d default to 0, they have no effect on the addition of a abd b, so you can safely add only two numbers together without worrying about the omission throwing off the result. The same thing happens in the second call, where only d is given the default value. (The fourth call, of course, explicitly assigns values to all parameters, so no defaults get assigned there.)

There is a rule you must follow when declaring optional parameters in a function. The optional parameters must come after the required parameters. This is because if a required parameter came after an optional parameter, you run into cases of ambiguity. Take the following example, for instance:

    int Add(int a, int b = 0, int c, int d = 0)
    {
        return a + b + c + d;
    }

In this example, we have the same Add function, but this time, b and d are the optional parameters. Now imagine that elsewhere in the code, we called the function like this:

    Add(1, 2, 3); // returns... what?

How should the program interpret that call? The 1 corresponds to a, obviously, but does 2 correspond to b and 3 correspond to c, leaving d with 0? Or perhaps 2 corresponds to c and 3 corresponds to d, leaving b with 0? The situation is ambiguous, and the program cannot figure out what your intent was. That's why if you were to define a function like this one, where there are required parameters coming after optional ones, it would result in an error.


Passing Parameters by Reference

Ho boy, strap yourselves in for this one, gents. We're about to delve into a concept of programming that shockingly few people are aware of, even among professional programmers. On that note, keep in mind that the topic of this section is a bit of a difficult one to wrap your head around, so don't feel bad if it takes a few rereads, some experimentation, and perhaps even consulting other tutorials before it finally clicks.

Let's start by (re)defining a couple of terms - value and reference.

Value vs Reference

A value is just that - a value. It's a bit of hard data that directly represents information. An int value could be 5. A bool value could be true. A string value could be "abcdef". It's a simple enough concept to understand, right? When you're talking about a variable's value, you are referring specifically to the information itself stored within the variable.

A reference is a bit trickier. Instead if referring to information, a reference is describing a place in memory where that information is stored. When you access a reference, you are telling the program to look at a particular spot in memory, and depending on your operation, you might be retrieving the value at that location or you might be assigning a new value to that location.

As an example, let's treat variables as possessions. Nothing specific, just things that you might own. When you have a value, you have an item in your hands. (A teddy bear, a personal radio, a hairbrush, whatever.) When you have a reference, you instead have a piece of paper that gives you a place where that item is located, like a street address or a locker number in a storage facility. To get at the “value”, you have to follow the piece of paper to the specified location first so you can see what “value” is stored there.

Back in Lesson 7, I talked about the difference between primitive types and object types. There's another way to describe them - value types and reference types. With primitives, they always store their values within the variable itself, whereas objects store a place in memory where they can look to access their information. This is because primitives always hold a value of a specific primitive type, which always takes a specific amount of memory. (int takes 4 bytes, double takes 8 bytes, and so on.) Objects, however, are created from classes, and the size of a class instance can vary widely depending on a number of factors, such as how many fields the class has, what types they are, whether the field is a reference to another class, etc. So instead, the program dynamically creates the class instance somewhere else and just stores the reference inside the variable.

Does all that make sense? Clear as mud? Cool. A value is a thing itself, whereas a reference is an address to where that thing is. Got it.

Passing Parameters By Reference

Let's take a look at an example function. It shouldn't be anything you haven't seen before:

    void Foo(int a)
    {
        int x = 0;
    }

Nothing special, right? But what if I changed the function to read like this?

    void Foo(int a)
    {
        a = 0;
    }

That's a bit of a surprise, isn't it? I mean, you pass things into a function through the parameters. Why would you ever want to assign a value to the parameter? Did it ever occur to you that you could even do that? What would happen if you did?

As it turns out, nothing, at least, not in this case. Whatever variable you passed in as a parameter to Foo would be unfazed by this. Generally speaking, whenever you pass a variable to a function as a parameter, you are passing in its value. In other words, you are passing it by value. What this means is that when the variable gets passed, what is actually happening is that the program reads the value of the variable, then makes a copy of that value and assigns it to the parameter variable within the function. So if I were to do the following:

    int x = 5;
    Foo(x);

The value of a inside of Foo is 5, but the 5 in a and the 5 in x aren't the “same” 5 (if that makes any sense). As a result, the a and the x end up being completely different variables. That's when I set a to 0 within Foo, it has no effect whatsoever on x.

If it doesn't make sense, imagine then that I have a Lego sculpture that I pass to a friend. The friend wants the sculpture as well, so he builds his own sculpture using the same kinds of pieces that I used in all the same placements and arrangements. Once he's done, he now has a sculpture that is identical to mine. But even though the sculptures are identical, they aren't the same specific sculpture. They are just two sculptures that look the same.

Then imagine that my friend decides to smash his sculpture. Even though our sculptures were identical, they are still different sculptures, so him smashing his sculpture doesn't cause mine to magically explode.

Hopefully that all made some sense to you, because now it's time to introduce the alternative - passing by reference:

    void Foo(int &inout a)
    {
        a = 0;
    }

And lo, there was a new keyword. When used with a parameter, the &inout keyword (notice the ampersand) marks a parameter so the program knows that any variable passed into that parameter should be passed by reference. Now let's take a look at what happens when we call this function:

    int x = 5;
    Foo(x);

As you can see, the syntax for calling the function hasn't changed. What ends up happening, however, is another matter entirely. After this code runs, the value of x will have become 0.

Remember the difference between a value and a reference? How a value is an item whereas a reference is a slip of paper describing that item's location? That's basically what is going on here. Instead of reading and passing the value of the variable, the program instead gets the location of the variable in memory (i.e. the reference) and passes that instead. On the function side, that address is given to the parameter, which acts as an alias to the original variable. The end result is that x and a are essentially labels of the exact same variable, so anything that happens to a will happen to x as well.

Referring back to the sculpture example, now instead of my friend making a copy of my sculpture, he takes the sculpture itself. When he smashes that sculpture, he is now smashing my sculpture, instead of just smashing a copy. So even when my friend hands the sculpture back, I'm still left with a broken sculpture (and minus one friend).

References and Object Types

Assuming you understand the concept, the difference between passing a primitive type variable by value and by reference is pretty straightforward - one passes the value while the other passes a reference to the variable itself. With object types, what's happening is a little less transparent. Let's go ahead and make an example class for our purposes in this section:

    class Bar
    {
        int A;
 
        Bar(int a)
        {
            A = a;
        }
    }

Now let's function which accepts an instance of this class passed by value:

    void Foo(Bar b)
    {
        b.A = 0;
    }

…and let's call it:

    Bar x(5); // Use the constructor to set x.A to an initial value of 5
    Foo(x);

Remember from the previous section that when you pass a variable by value, what gets passed is actually a copy of the value, so anything that happens to the parameter inside the function doesn't change the value of the original variable.

But if that's the case, then something really confusing just happened. After the function call, the value of x.A is 0. That doesn't make any sense. If x was passed by value, then how was the function able to change the value of one of its members?

Well, also remember what I said about objects and reference types. The variable of an object type doesn't store the value of the object itself. Instead, it stores the reference of that object to wherever it is in memory, which the program uses any time you access the object's members. So when you pass this reference by value, the program does, in fact, make a copy of that reference and stores it inside the parameter, but the result is that now you have two completely different variables that both have a reference to the same place.

To use another contrived real-world example, imagine that I had a piece of paper, on which was written the location of my sculpture. My friend requests this location, so I copy the writing on my paper onto a new slip and give that to him. My friend and I now have two completely different pieces of paper, so if he were to rip up his paper, mine would remain intact. However, if we were to both follow the instructions on our papers, we still end up at the same location where my sculpture was stored.

So ass you can see, the instance variable is still being passed by value. What is happening is that you are passing a reference by value, which despite the similar phrasing, is not the same as passing a value by reference. To demonstrate this, see what happens when I change the code in Foo a bit:

    void Foo(Bar a)
    {
        Bar b(0);
        a = b;
    }

Now what happens is that the function creates a new instance of Bar called b, assigns the A field of b to 0, and then directly assigns b to a. Now if we were to look at x from our original call, you would see that x is unchanged. This is because the act of directly assigning a to another instance means you are changing the reference, so now a points to somewhere else in memory entirely, leaving x to be perfectly safe from any further manipulation.

By contrast, if you were to perform this function while passing x by reference:

    void Foo(Bar &inout a)
    {
        Bar b(0);
        a = b;
    }

Now that a is an alias to x, changing the address of a will also change the address of x. In this case, you will have assigned a completely new instance of Bar to x.

As a side note, the &inout keyword has shortcut syntax. Instead, you could just use & when declaring a parameter, and it will have the same effect (you can even omit the space between the keyword and the parameter name if you wish):

    void Foo(Bar &a) // Still passing by reference
    {
        Bar b(0);
        a = b;
    }

In the Java programming language, it's a commonly held belief that primitives are passed by value while objects are passed by reference. This belief makes me want to slap that programmer along with every programming teacher, professor, or mentor that they've ever had. In truth, everything in Java is passed by value. In fact, it's not even possible in Java to pass something by reference (this is by design).

It's stipulated that this confusion was started when the people behind Java decided to refer to instances as “references”, a decision that confuses it with what other languages refer to when they use the term. Whatever the reason, don't be drawn in by this perpetuated mistake - objects in Java are passed by value, just like everything else in that language. (Also, just call them instances, for Pete's sake.)

In and Out

While many languages share the concept of passing by value and by reference, AngelScript goes a step further. In addition to the &inout keyword, it also supplies two additional ones - &in and &out.

Conceptually, the uses of both these keywords are similar to &inout, but their implementation is slightly different.

The &in keyword, when used with a parameter, marks that parameter as being input only. When a parameter is marked as input, attempting to assign to this parameter will result in an error. This can be useful in cases where you are passing a type with a potentially large memory overhead (such as a string) and don't want the program to make arbitrary copies:

    void Foo(tString &in a)
    {
        // a could be a string that's a thousand characters long
        // that's a lot of data to copy just to pass it to a function
 
        a = "abc"; // Error
    }

When declaring a parameter as input, it's generally a good idea to declare it as constant too. This allows the script to be optimized even further by the program and it ensures that no illegal assignments will take place within the function:

    void Foo(const tString &in a)
    {
        // Do something
    }

Conversely, the &out keyword marks a parameter as being output only. This is handy when you are writing a function that you want to be able to return multiple values (since normally you can only return a single value):

    bool TryDivide(int a, int b, int &out result)
    {
        if (b != 0)
        {
            result = a / b;
            return true;
        }
        else
        {
            return false;
        }
    }

In the above example, I have a TryDivide function that attempts to divide two numbers. First, however, it must check if the b is 0. If it is, then it cannot divide the numbers without dividing by 0 (which is an error on many levels). So what happens is that if b is not 0, then the division is performed and assigned to the result, after which it returns true to signify that the division is successful. If b is 0, however, then the function returns false to show that the division couldn't be performed safely.

You can use this function to divide two numbers safely:

    int a = 3;
    int b = 0;
    int result;
    if (TryDivide(a, b, result))
    {
        cLux_AddDebugMessage("Division was successful - the result is " + result);
    }
    else
    {
        cLux_AddDebugMessage("Division by zero averted");
    }

As you can see, the function prevented a potentially deadly error from happening. If we had simply tried to directly divide a and b, it would've resulted in a division by 0, which would have caused your program yo come crashing down around itself in a raging ball of fire.

The TryX technique is a popular one when dealing with situations that need to be specially handled to prevent potential errors from happening. In a situation where a string needs to be converted (aka parsed) to a numeric value, for example, the string might not be a valid number (it could contain letters or something). Trying to convert such a string would result in one of two things - success or an error. If the TryParse approach was implemented instead, however, then the function would return a boolean value showing whether or not a parsing could be performed and, if so, the result of the parsing is handed back to the caller through an &out parameter.


Object Handles


Inheritance

Imagine if you were creating an adventure game. In this game, the player could be one of several classes, such as a Warrior, an Archer, or a Mage. Each of these classes needs some basic fields and functions to make them work, and they might look something like this:

    class Warrior 
    {
        int mlHealth;
        int mlMana;
        int mlDamage;
        array<Item> mvInventory;
        int mlChestHairs;
 
        void DoAttack() {}
        void DoPowerSwing() {}
        void LiftBoulder() {}
    }
 
    class Archer
    {
        int mlHealth;
        int mlMana;
        int mlDamage;
        array<Item> mvInventory;
        int mlEarPointiness;
 
        void DoAttack() {}
        void ShootBow() {}
        void PickLock() {}
    }
 
    class Mage 
    {
        int mlHealth;
        int mlMana;
        int mlDamage;
        array<Item> mvInventory;
        int mlLiverSpots;
 
        void DoAttack() {}
        void CastFireball() {}
        void DetectMagic() {}
    }

As you can see, these three classes are pretty similar, and in fact they have the same four fields and one function. This is a sign of repetition, which is a big red flag in programming.

This is where inheritance comes in. You can take a class and mark it as being “based” on another class. When a class has a base class, it shares all the fields and functions from that base class:

    class HeroClass
    {
        int mlHealth;
        int mlMana;
        int mlDamage;
        array<Item> mvInventory;
 
        void DoAttack() {}
    }
 
    class Warrior : HeroClass
    {
        int mlChestHairs;
 
        void DoPowerSwing() {}
        void LiftBoulder() {}
    }
 
    class Archer : HeroClass
    {
        int mlEarPointiness;
 
        void ShootBow() {}
        void PickLock() {}
    }
 
    class Mage : HeroClass
    {
        int mlLiverSpots;
 
        void CastFireball() {}
        void DetectMagic() {}
    }

That looks much cleaner. Now instead of copying the same fields and functions into each class, they are declared within the base class HeroClass, and since Warrior, Archer, and Mage all inherit from HeroClass (meaning they have specified HeroClass as their base class), they will all have those same fields and functions as well:

    Archer ar = new Archer();
    ar.DoAttack(); // This is perfectly fine

Since Archer, the derived class, inherits from HeroClass, the base class, the ar object in this code has access to all of the fields and functions declared in the base class.

One extremely powerful aspect of inheritance is that anywhere you can use an object of the base class type, you can also use an object of a type that inherits from that base type. For example, you could do the following:

    void DoSomethingHeroic(HeroClass obj)
    {
        // ...
    }
 
    // ...
    // Elsewhere in your code;
 
    Warrior w = new Warrior();
    DoSomethingHeroic(w); // Since Warrior inherits HeroClass, then w is treated like a HeroClass object as well as a Warrior object

Keep in mind that the limitation of inheritance is that a class may only specify one base class to inherit. Trying to inherit from multiple classes will result in an error:

    class BaseClassA {}
 
    class BaseClassB {}
 
    // The following class will result in an error, since a class cannot inherit from more than one base class
    class DerivedClass : BaseClassA, BaseClassB {}

Overriding Base Classes

In addition to using stuff from a base class, a derived class can also override stuff declared in a base class. Let's take our HeroClass again, for example. Say we want to add a DoSpecial function that all derived classes could use:

    class HeroClass
    {
        // ...
        void DoSpecial() {}
    }

This is well and good, but each derived class might want to handle their “special” differently - the warrior might want to do a powerful spin move, while the mage does something fancy with wind and lightning. As it turns out, this is pretty straightforward. By reimplementing the function in the derived classes, we can use the override keyword to specialize each class:

    class Warrior : HeroClass
    {
        void DoSpecial() override
        {
            // Do a spin attack
        }
    }
 
    class Archer : HeroClass
    {
        void DoSpecial() override
        {
            // Shoot five arrows at nearby enemies
        }
    }
 
    class Mage : HeroClass
    {
        void DoSpecial() override
        {
            // Summon a thunderstorm
        }
    }

When a class is inherited by another class, that is inheritance. When an inheriting class changes properties or behaviors of that base class, however, it takes inheritance a step further into a concept called polymorphism.

This might seem like we are falling into the trap of code repetition again, but there is actually a reason for the repetition in this case. Remember that I said that every derived class is treated as though it was of the base class type? Well, that applies to overridden functions as well, except the program takes it one step further. When you have a derived class passed into a function that treats it like a base class and that function calls something that has been overridden, it will automatically call the overriding version:

    void DoSomethingHeroic(HeroClass obj)
    {
        obj.DoSpecial(); // Will automatically call the overriding function
    }
 
    // ...
    // Elsewhere in your code
 
    Warrior w = new Warrior();
    DoSomethingHeroic(w); // Does the spin move
 
    Archer a = new Archer();
    DoSomethingHeroic(a); // Shoots all the arrows
 
    Mage m = new Mage();
    DoSomethingHeroic(m); // Makes it rain

There's another overriding thing going on in derived classes as well. When you create a derived class, the constructor of that class implicitly overrides the constructor of the base class.

Sometimes there are things in the base class function that also need to run whenever you call an overriding version. In these cases, you can use the scoping operator to explicitly call the base class implementation of the function:

    // DerivedClass.DoSomething
    void DoSomething(int a)
    {
        BaseClass::DoSomething(a);
 
        // Do other things
    }

Similarly, in the constructor, the base class's constructor might be doing important initialization that is common to all derived classes. In the constructor of your derived class, you can use the super keyword to call the base class's constructor:

    DerivedClass(int a)
    {
        super(a); // Calls the constructor in the base class
 
        // Do other things
    }

Let's take this overriding concept a bit further. Sometimes, you have a class that is a base class to other derived classes, but is never meant to be used on its own. Take the HeroClass, for example. While your program is going to be passing Warrior, Archer and Mage objects around, it doesn't really make sense to have an object whose type actually is HeroClass.

This is where the concept of “abstract classes” comes in. An abstract class can be inherited from and used as a type for things like fields or parameters, but it can never be instantiated (i.e. have their constructor called directly). Take this code for example:

    // Perfectly fine
    DerivedClass a = new DerivedClass();
 
    // Still fine, since the type of b is actually DerivedClass and is just treated like an AbstractClass object
    AbstractClass b = new DerivedClass(); 
 
    // Throws an error, since abstract classes cannot be instantiated
    AbstractClass c = new AbstractClass(); 

To mark a class as abstract, you simply use the abstract keyword in the class declaration:

    abstract class AbstractClass
    {
        void SomeFunction() {}
    }

The last new keyword for using with inheritance is the final keyword. When used on a class, it marks that class as something that cannot be inherited from:

    final class FinalClass {}
 
    class DerivedClass : FinalClass {} // Throws an error: FinalClass cannot be inherited from

It can also be used on functions to disallow them from being overridden:

    class BaseClass
    {
        final void DoSomethingFinal() {}
    }
 
    class DerivedClass : BaseClass
    {
        void DoSomethingFinal() override {} // Throws an error: DoSomethingFinal cannot be overridden
    }

This is useful if you have a class or function whose operation is not meant to be changed in any way.

As an extreme example, imagine you have a class called SecurityManager with a function CheckCredentials that checked to see if people trying to get through a door had the proper credentials to get through. If that class and function were not finalized, then someone could come along later and create a DerivedSecurityManager with an overriding CheckCredentials that let everyone through the door, regardless of credentials. You can see why you wouldn't want this situation to happen, especially when you are writing code to be used by other people later on.


Interfaces

Overriding a class's functions is one way to implement polymorphism, but another way is to use blueprints that, rather than providing a collection of fields and functions, merely dictates what functions a class should have and leaves it up to the class to implement them. This is done using a construct called an interface.

The best way to explain an interface is to show how they are declared. This is done by using the interface keyword:

    interface iMyInterface
    {
        int mlSomeField;
 
        void DoSomething();
        bool GetSomeBool();
        void DoSomethingWithParam(int param);
    }

As you can see, interfaces are declared a bit differently than classes. Fields are declared the same, but functions are only defined by giving them their identifiers, parameters, and return types. Instead of providing function bodies, they are closed instead with the semicolon. This is because an interface does not provide functions to use, but rather offers a series of functions that classes which use it are required to provide the bodies for:

    class cMyClass : iMyInterface
    {
        void DoSomething()
        {
            // Do something
        }
 
        bool GetSomeBool()
        {
            return true;
        }
 
        void DoSomethingWithParams(int param)
        {
            // Do something with param
        }
    }

When a class declares that it is using a base class, that is known as inheriting the class. When a class declares that it is using an interface, that is known as implementing the interface.

Just like with inheritance, the interface can be used as a type for variables:

    iMyInterface obj = new cMyClass();
    bool bValue = obj.GetSomeBool();

While the practice of giving bodies to functions defined in interfaces is discouraged, AngelScript doesn't explicitly disallow it. If you wanted, you could define an interface with a complete function in it:

    interface iMyInterface
    {
        void DoSomething(); // This function is required to be given a body by an implementing class
        void DoSomethingSpecific()
        {
            // This function body is provided by the interface itself rather than by any implementing classes
        }
    }

The power of interfaces comes from the fact that while a class may only inherit from one base class at the most, it may implement as many interfaces as it wants. You can do this by separating the interface names in the class declaration with a comma. Similarly, if a class is inheriting from a base class and wants to implement an interface, the class name is given first, followed by the names of the interfaces:

    class cComplexClass : cBaseClass, iInterfaceA, iInterfaceB {}

This comes in handy when you have a class with a large number of traits. If for example, you had a custom array class, you might want to implement a number of interfaces that define what it can do:

    class cCustomArray : iIndexable, iEnumerable, iSortable, iCopyable
    {
        // The class contents
    }

In this code, the interface iIndexable guarantees that the class can be accessed by an index, similarly to a normal array. The interface iEnumerable guarantees that the class may also be enumerated (i.e. traversed with a loop). The interface iSortable guarantees that the class provides utility methods for sorting the underlying data into a given order. And the interface iCopyable guarantees that the class contains functions that, when called, will return an object that contains an exact copy of the data within the calling object.

Not only is this capability useful in ensuring that certain functions exist, it also helps group classes that may be similar in certain aspects but not in others. For example, if in addition to our custom array class, we also had custom classes for a binary tree, a linked list, and a dictionary, the interfaces they implement could be used to treat all the classes under a given interface, such as iIndexable:

    iIndexable array = new cCustomArray();
    iIndexable tree = new cBinaryTree();
    iIndexable dict = new cDictionary();
 
    SomeType arrayValue = array.ValueAtIndex(0);
    SomeType treeValue = tree.ValueAtIndex(2);
    SomeType dictValue = dict.ValueAtIndex(5);

As you can see, all three of the objects are based on a different data structure, but because they all implement the iIndexable interface, they are all guaranteed to have an indexer function of some sort, shown here as the ValueAtIndex function.

In many ways, the similarities between inheritance vis a base class and the implementation of an interface can lead to some fuzzy lines in program design. It's fairly common when designing a program for the designer to ask whether they should use an interface or a base class to group similarly intended classes together. The ruling philosophy over the difference between inheritance and interfaces is that a base class describes what a class is, whereas an interface describes what a class does.

For example, with the RPG class script examples from the Inheritance section, the HeroClass base class denoted that Warrior, Archer, and Mage were all hero classes, while they could implement an interface such as iAttacker that would describe that they can attack things. Use this distinction when designing your own class structure for your programs.


Mixin Classes

Sometimes, rather than exploiting the benefits of inheritance or interfaces, you merely want to segment some code into pieces in order to maximize code reuse and efficiency. In these cases, AngelScript provides a somewhat unique feature called a mixin class. Where inheritance and interfaces are intended to extend a class's capabilities or allow for flexibility in cases of variable types, a mixin class is merely intended to provide a class with a pre-defined set of fields and functions.

A mixin class is defined with the mixin keyword:

    mixin class MyMixinClass
    {
        void DoSomething()
        {
            // Do something
        }
    }

This mixin class can now be used in the definition of other classes, which is done in the same way as declaring a base class or interface:

    class cMyClass : MyMixinClass {}

Now, as far as other places in your code are concerned, the cMyClass has the functions in it that were originally declared in MyMixinClass:

    cMyClass obj;
    obj.DoSomething();

Keep in mind that this is not the same thing as inheritance. With inheritance, the derived class is the base class (i.e. a “Circle” is a “Shape”). However, cMyClass is not a MyMixinClass. It is merely taking on the fields and functions of MyMixinClass as if they were its own. To illustrate this, see what happens if you try to instantiate a MyMixinClass variable with an instance of cMyClass:

    MyMixinClass obj = new cMyClass(); // Error

This code would result in an error because cMyClass and MyMixinClass are not compatible types. In fact, MyMixinClass is not a type at all. It cannot be instantiated, nor can variables be declared with it as their type. The only reason it exists is to provide cMyClass with its contents.

This concept is known in programming as syntactic sugar, which describes a programming feature that only exists to make life easier for the programmer in their code. Once the code is compiled, the syntactic sugar is specially processed in a way that, if you were to inspect the machine code after the script is compiled, no trace of the sugar would remain.

I included this section for the sake of completeness, but in my opinion, mixin classes are not good to use. When you define code in one place and it gets used in another, that would be better handled with either inheritance or interfaces. If a class is meant to have a set of functions that relate to a behavior, that is what interfaces are for.

The only time I would recommend using a mixin class is for when you are dealing with a particularly massive class and want to break it into pieces for organizational purposes. C# hosts a feature for this express purpose called “partial classes”, and I recommend anyone interested in mixin classes to read about C#'s partial classes to get a feeling for what I think is the best and only legitimate use case for mixin classes.


Delegates

Up until now, you've been using variables to store data of types. These types could be primitive types, such as int or bool, or they could be object types like cVector3f or cMatrixf. They could be arrays or even names of classes or interfaces that you came up with yourself. What if I told you, though, that you could also use a variable to store a function?

A delegate is, in a nutshell, just that - a variable that stores a pointer to a function. The variable can be used to call that function just as though as you were calling it by name directly.

Funcdef

Before you can learn how to use delegates, you need to understand what a Funcdef is. Funcdef is short for “function definition”, and the basic explanation is that it defines a function “type”. Appropriately, it is declared using the funcdef keyword:

    funcdef bool FunctionType(int, int);

From this code, you can see that after the funcdef keyword, the rest looks like a typical function declaration. One major exception is that the parameters don't have parameter names. That's because the Funcdef only concerns itself with the types of the parameters, and doesn't care about their names.

After this, FunctionType can be used to describe the “type” of any function that shares an identical function signature with it:

    // This function takes two int parameters and returns a bool, so it matches FunctionType
    bool DoSomething(int a, int b)
    {
        return true;
    }

As a general rule, Funcdefs should be declared at the global scope, although there's no rule against declaring them in the class scope.

Using Delegates

Now that we have a Funcdef, we can use it to declare and instantiate a delegate. When declaring a delegate, you use the Funcdef just as you would use a type for a regular variable:

    funcdef void MyDelegateType(int);
 
    // ...
    // Elsewhere in your code
 
    MyDelegateType @delegate;

A delegate is a pointer to a function, so it is necessary to declare it as a pointer object (see Object Handles). From here, you can use the function name of a function to “assign” the function to the delegate:

    void DoSomething(int a) {}
 
    // ... 
    // Elsewhere in your code
 
    delegate = @DoSomething;

Similarly to the declaration, you must use the handle operator to get the pointer to the function and assign it to the delegate.

Now that the delegate has been assigned a function pointer, you can use it as though you were calling the function directly:

    delegate(5); // Calls the DoSomething function that was assigned before

When creating a delegate for a class function, a slightly different approach is required. Because the function is bound to the instance object of the class, you need to create the delegate using the Funcdef constructor, passing a reference to the object's function as a parameter:

    class A 
    {
        void DoSomething(int a) {}
    }
 
    // ...
    // Elsewhere in your code
 
    A a;
    delegate = MyDelegateType(a.DoSomething);

Now that you know how to declare, assign, and use delegates, let me explain why they are useful. Imagine you have a situation where you have a number of functions, and you want to conditionally call one of them based on a value. You could use ifs, of course:

    if (value == 1)
    {
        Function1():
    }
    else if (value == 2)
    {
        Function2():
    }
    // ...
    else if (value == 99)
    {
        Function99():
    }

That immediately throws red flags, so let's do the sane thing and replace it with a switch:

    switch (value)
    {
        case 1:
            Function1();
            break;
        case 2:
            Function2();
            break;
        // ...
        case 99:
            Function99();
            break;
    }

This code is much more efficient, but we run into another problem. I clipped the code in the example, but imagine if I had actually typed out all 99 cases of that if or switch code. It would be unsustainably long, to the point that if you had to find any particular case it would be nontrivially cumbersome to do. Not to mention if you later needed to add one, twenty, or a thousand more.

In this case, you can instead use a delegate to store the function you want to call:

    funcdef void StateFunction();
    StateFunction @state;
 
    // ...
    // Elsewhere in your code
 
    state = @Function99;
    state();

This approach is commonly used when designing game engines. Because each level of each map of each mode of the game's operation needs to be handled differently, using a switch block could require thousands of different cases, so it isn't really an option. Instead, the game stores a delegate that points to a function that handles that particular state (such as the main menu, or the pause screen, or level 2-A).

When something happens in the game that changes its state (for example, when the player presses the pause button), the game performs the corresponding assignment to the state delegate. Then when the next time the game refreshes itself, it will automatically call the new function, making a seamless transition into the new state.


Appendix 2: HPL3 Specifics

This is a list of features that are important to know for SOMA modding, but they don't fit in the main lessons for not being directly related to AngelScript or programming in general, or because they use approaches that are overly specific to SOMA and HPL3.

Callbacks


Timers


Sequences


hpl3/community/scripting/angelscript_tutorial.1523808893.txt.gz · Last modified: 2018/04/15 16:14 by abion47