A Guide to Galaxy
Author: AzothHyjal (sc2mapster.com)Tags: advanced trigger  
Source: http://forums.sc2mapster.com/resour...Added 13 years ago 

The purpose of this guide is to acclimate people with the Galaxy language and custom scripting in Starcraft II. If you notice anything amiss or have suggestions, don't hesitate to let me know. This guide is not intended as reference and as such will build up from simple to more complicated concepts.


Part 1

Since this is the first part of the guide, it will start with the basics needed to understand scripting in Galaxy. There may be references to topics not covered until later portions of the guide.

This portion of the guide covers the following topics:


Galaxy vs. GUI
Once familiar with Galaxy it can be much faster to develop in. The GUI editor provided is very powerful but incredibly cumbersome to use, something that can be avoided by developing in Galaxy. It is much easier to re-use code when developing in Galaxy compared to using the GUI.

The GUI provides certain advantages over scripting. It creates syntactically correct code and makes referencing pre-placed objects in your map much easier than in scripting (more on this later).
A Quick Note on the GUI

You can view the code generated by the GUI by opening the trigger editor and going to Data->View Script (CTRL + F11). You can change the display of the triggers to show you the actual Galaxy API calls by going to View->View Raw Data (CTRL + D) in the trigger editor.

You cannot edit the code you see in View Script directly as it will be erased anytime the map is saved and replaced with a generated version of what is in your trigger editor.

Details on how to incorporate custom scripts into your map will be provided in a later section.


General Galaxy Semantics and Concepts
Galaxy is semantically very similar to many other programming languages, most noticeably C. The actual semantics are something that won't be dealt with in tremendous detail in this guide as it can be learned indirectly from looking at code snippets and experimentation. However, there are a few important things to know:

Variables and functions must be declared before they can be used. This means that in order to use some variable foo, Galaxy needs to know what type of variable foo is before it is used. If there is some function bar(), it must either be declared or defined before its use as well. This will be explained in greater detail throughout the guide.

Commented code (prefixed with //) do not have any effect on the output of code - they are visible to the programmer but will not be looked at by the compiler.

A Quick Note on Terminology: Definition vs Declaration
Declaring means saying what something is, while defining is giving meaning to what that something does. Things can be declared and defined at the same time. What is important to the compiler is that a declaration exists before a variable or function is used.


Variables
Variables allow you to store values for later use in your script. The following is an example of a variable declaration:

int foo;

This line creates a variable of type int (integer) named foo. This variable name can then be used in place of an integer to the same effect. For example, consider the following:

foo = 2; // stores the value 2 inside of the variable foo
foo = foo + 3; // adds the variable foo and the number 3, returning 5 and storing the value in foo

Variables are assigned default values if you do not give them a value. In general it is good practice to initialize any variables you plan on using.

There are many other types of variables other than int. A list of them is located here: http://www.sc2mapster.com/api-docs/types/.


Arrays
Arrays allow you to group together a group of the same type of variable under one name. For example, consider the case of creating an RPG with a character selection and wanting to know whether players had chosen their hero. A naive approach would be to create a boolean variable for each player and write the code to change each one individually. A much better approach would be to create an array of booleans which could be indexed by the player number and changed accordingly.

The following is an example of creating an array:

bool[8] selectedHero;

bool is the type of the array, 8 is its size, and selectedHero is its name. This is the same syntax for creating any type of array in Galaxy: type[size] name. What this does is to create a single variable, named selectedHero, that can store up to eight different boolean values.

Once we have declared an array, we can access its members so long as we do so within a local scope in the following manner:

arrayName[index]; // in general
selectedHero[1] = true; // for the example

One important thing to note is that Galaxy is a zero index based language. This means that the start of any array is 0, not 1! selectedHero has eight elements indexed by 0, 1, 2, 3, 4, 5, 6, and 7 - not 8.

Other than their syntax, arrays can be treated in almost in the same fashion as normal variables. However, they cannot be passed or returned from functions (more on this when functions are introduced). In addition, arrays cannot be initialized at the global scope and cannot be assigned to other arrays. What this means is that:

int[5] a;
int[5] b;
a = b;
// error, arrays cannot be used in assignment


Structs
Structs, or records as they are known in the GUI, allow you to create complex variable types that can contain other variables. Structs are similar to arrays in that they cannot be passed to functions or returned from them. Also, like arrays, structs cannot be used in assignment and cannot be accessed in the global scope. Unlike variables, an initial definition of a struct must be provided before it can be used as a variable type.

The following defines a struct:

struct myStructType
{
     int foo;
     bool bar;
};

// In general, this is how a struct is defined:
struct structTypeName
{
    
// fields go here
};

In the above example, myStructType is the struct name and foo and bar are struct member variables. With the above lines of code, a new variable of type myStructType has been defined and can now be used to declare variables.

To use this struct definition, follow the rules for creating a variable of any type:

myStructType someStruct;

This previous line creates a variable of type myStructType named someStruct. Remember that since this is a struct, it cannot be passed or returned from a function. However, the struct member variables can be passed to functions so long as they themselves are not structs (yes, a struct can have a struct as a member variable) and assigned to normally.

To access the fields inside of a struct, use the following syntax:

someStruct.foo = 3;
someBoolVariable = someStruct.bar;
// where it can be assumed someBoolVariable was a previously declared bool

Accessing struct member variables is only possible inside of a local scope, though the struct variable itself can be global.

A Quick Note on Structs: What are they good for?
Structs are excellent ways to organize variables that have a strong relation to each other. They are especially useful ways of organizing large amounts of data into an array. An array of structs can create a data type that is easier to use than several normal arrays. Consider an RPG where the programmer wants to keep track of whether each player has picked a unit, which unit they chose, and how many times they have died. One way to do that with a struct would be the following:

struct playerInfo
{
     bool selectedHero;
     unit hero;
     int numberOfDeaths;
};

playerInfo[6] playerStructs;

// in some function:
playerStructs[1].selectedHero = true;
playerStructs[1].numberOfDeaths = 0;



Typedefs
Typedefs allow the programmer to come up with aliases (different names) for variable types. They use the following syntax:

typedef type name;

Where type is some variable type, either native or user created (a struct or some typedef), and name is the new alias for this type. The type used in this statement can now be referred to by either its original type name, or the newly assigned alias.

typedef int player;
player foo = 1;


Typedefs are useful for making code easier to understand. A script that relies heavily on integers to represent something very specific may be easier to develop and certainly to understand if a relevant typedef is used.


Scope
Scope refers to where in the code a variable or function can be used. There are three types of scope in Galaxy: local, global, and file.

A variable with global scope can be accessed anywhere assuming that the variable has been declared on a line prior to where it is used. To declare something in the global scope, simply write it outside of a function (at the so called "top-level") of your script.

int foo; // not in a function thus has global scope

A variable with file scope can be accessed anywhere in a specific galaxy code file assuming that the variable has been declared on a line prior to where it will be used in the same file. To declare something with file scope, write it like a global variable and prefix it with the keyword static

static int foo; // not in a function thus has global scope
// however, static keyword means this variable can only be accessed in the current galaxy file

A variable with local scope cannot be accessed outside of that local scope, usually meaning the function it is declared in. For example:

void foo(){
     int bar;
// has local scope, declared inside of a function
     bar = 3; // ok, bar exists in the local scope of the function foo
}
bar = 3;
// error, bar does not exist in the global scope





Part 2

This section of the guide will cover more advanced topics. Before continuing, make sure you are familiar with the basic concepts covered in Part 1. They will be used in more complicated scenarios and understanding their basics will be essential.

In unrelated news, the tutorial will likely take on a more personal style of writing since it's far easier for me to do.

This portion of the guide covers the following topics:


Using Galaxy in a Map
There are a few ways to get Galaxy code into your map. One of the easiest ways is to create a "Custom Script" element in the trigger editor. This is done by going to Data->New->New Custom Script within the trigger editor.

Doing this allows you to type Galaxy script directly in the trigger editor. This is one of the best ways to get started with scripting and seeing how things work. These custom scripts will be inserted into your overall map script (as seen by Viewing Script - see Part 1) after any GUI global variables are declared and initialized. In addition, if you have multiple custom scripts, they will be inserted in the order they appear within the GUI. The editor will compile your script every time your map is saved. There is a slight delay between entering code and it being inserted into the map, so you may notice erroneous errors when saving quickly.

Galaxy code can also be inserted in the midst of GUI created triggers. There are many instances where custom script may be entered in place of values in the GUI. In addition, the trigger action "Custom Script" may be used to insert custom written code. There are a few caveats with this approach however: actions take place within triggers, which are functions. This means that code inserted in this fashion is placed in the middle of a function, limiting it somewhat (more details on this in the function section).

The Galaxy compiler is very cryptic and will give fairly terrible error messages when you make a mistake in your code, so compile often so that mistakes are easier to find.

The last way to introduce Galaxy code into your map is to import it. This is the best option for writing a lot of Galaxy code as you can use an outside editor with code completion and other features to make writing code less painful. Using this method, code must be imported using the Modules->Import dialog. Once a galaxy file or series of files is included in this fashion, it can be utilized by your map.

To use imported scripts, they need to be included using the include keyword. For example, if you have a galaxy file called "mycode.galaxy" and you import it into the root directory of your map, you would include it in the following way:

include "mycode"

Note that the .galaxy extension is excluded and that there is no semicolon at the end of this line. The code in your included galaxy file will be inserted where the include statement is located, so anything in your included file will be usable after the line at which it was included.

If you want to include a file immediately at the top of your script, you will be unable to do so using conventional methods (creating a Custom Script), because custom scripts are inserted after global variables and the like. The way around this is to follow the instructions listed in this tutorial. The basic premise is that you will edit your map's main MapScript.galaxy file to include the files you need and run any initialization routines necessary.

If you intend on using only custom scripting and no GUI functionality, you can easily get away with using Custom Scripts with include statements since there will be no risk of some GUI created global variable needing to reference them.

Accessing GUI created variables, functions, and triggers
If you've played around with custom scripts and attempted to use things you've created in the GUI, you may have run into syntax errors that at first glance look correct. The likely issue is how the GUI names its variables.

Global variables are prefixed by gv_. Global triggers are prefixed by gt_. Global functions are prefixed by gf_.

Local variables are prefixed by lv_. Local parameters are prefixed by lp_.

In general, the naming scheme followed by the GUI tends to prefix variables based upon their scope (g for global, l for local) and the style of variable (v for variable, t for trigger, f for function, etc). When in doubt, take a look at the generated map script and look at the names.


Constant Variables
Before explaining constant variables, let's do a quick overview of variables. Variables can be either any of the built in data types (such as int, bool, unit, etc - see Part 1), structs we define, or aliases to these types (typedefs). We can also have arrays of any of these variable types. From this point on, the term variable will refer to any type of variable, whether it is built in, user created, or an array.

Constant variables differ from normal variables in that their value cannot be changed. If their value cannot be changed, you may be wondering how they can get a value at all! Constant variables can only be given a value if they are both declared and defined at the same time. Not only is this the only time they can be given a value, they must be given a value at this time. For example:

const int cantTouchThis = 42; // valid
const int noValue; // ERROR: must give const variables a value at declaration

As can be seen in the prior example, the keyword for making a variable constant is const. Variables declared constant by use of this keyword cannot be used as member variables inside of a struct.

A Variable I Can't Change? What good is that?!
Constant variables are a great way to get rid of "magic values" in your code. Consider the case of creating a tower defense type map where the waves of enemies spawn on regular intervals. If the time of these intervals does not change, using a constant variable makes a lot of sense. If you change the variable, it will affect every portion of your code that uses it, making it easier to maintain and faster to update.

Structs and Arrays: Constants in Disguise
You may remember earlier mention that both arrays and structs could not be used in assignment. This is because both arrays and structs are non modifiable, meaning that their value cannot be changed. This is very similar to a constant variable!

The key difference here is that although the struct and array variables themselves cannot be modified, their contents can be changed, so long as the contents themselves are not considered constant/non modifiable in some regard.

int[5] someArray;
// inside some function:
someArray = 3; // error, can't assign to an array
someArray[1] = 3; // ok, changing the value inside of the array (int)

struct myStructType{
    int foo;
    bool[2] boolArr;
}
// inside some function:
myStructType test;
test = test;
// error, cannot use struct in assignment
test.foo = 3; // ok, foo is a modifiable (non constant) variable
test.boolArr = 2; // error, cannot use array in assignment
test.boolArr[0] = 2; // ok, boolArr[0] has type int and is not constant

It should be no surprise then that:

const int[5] intarr;
const myStruct astruct;
// (where myStruct is some struct definition)

both result in errors, since it is redundant to have a constant array or struct.

Galaxy does not support arays of true constant variables (variables prefixed with the keyword const). In addition, structs cannot contain const member variables.


Multidimensional Arrays
Arrays can be of any type so long as it is not prefixed with const. Let's think about this for a second - this means we can have arrays of arrays!

Arrays of arrays are called multidimensional arrays, since they are easy to picture as various shapes increasing in dimensionality. Consider a simple, 1 dimensional array:

int[5] oneD;

We can picture the various indexes for this array in the following way, which resembles a line (which is one dimensional):

| 0 | 1 | 2 | 3 | 4 |

So what happens when we increase the number of dimensions? There are a few things to note before we move on to this. Arrays are just ways of storing a lot of the same type of variable - though we can have "arrays of arrays," it is not possible to mix types. So before we picture what a multidimensional array looks like, let's look at the syntax for it:

baseType[arrayDim1][arrayDim2][arrayDim3][...] myArray; // in general
bool[2][3] twoD; // a two dimensional bool array of size 2x3

As can be seen, increasing the dimensions of an array is simply a case of adding more brackets to its declaration. Each set of brackets increases the dimension by one. The total number of elements we can store in our array becomes the product of all of these values. For example

bool[2][3][4] threeD; // bool array of size 2x3x4, total elements = 24

We can picture a two dimensional array as a table of values (which looks like a rectangle - a 2D shape):

int[R][C] twoD; // an int array with R rows and C columns

| 00 | 01 | 02 |

| 10 | 11 | 12 |


Where the first number corresponds to the row index, the second number corresponds to the column index. We'd access an element from this array in the following manner:

twoD[row][col];

Increasing the dimensionality to 3, we can picture the array as a cube of values. Going beyond three dimensions requires some creative thinking if you wish to form a picture of the data, but this should not be necessary. The key way of thinking about array data is the following:

Each bracket of your array contains everything to the right of it. For example:

int[5][4][3] myArray;

// where a, b, and c are arbitrary indexes:
myArray[x] // This has five elements, each of which is an int[4][3] array.
myArray[x][y] // This has four elements, each of which is an int[3] array.
myArray[x][y][z] // This has three elements, each of which is an int

So whenever we want to use myArray in some statement, we need to actually access its base type by going through each dimension of the array until we reach it. If it is a 3 dimensional array, this will require three sets of brackets.

Multidimensional arrays follow all of the same rules as normal arrays. Remember that the elements inside of a multidimensional array may be arrays themselves (until you reach the base type), which follow the rules of arrays as well!


Functions
A function is a piece of code that performs a specific task. Consider a case of taking a case of setting a unit's health to some arbitrary percentage and displaying a UI message about this event. A function could be written to do this for any arbitrary unit. This is the example that will be used when going over functions.

Functions, like variables, have names. Function names can only be used to execute (call) a function. Unlike variables, function names cannot be assigned things or assigned

Here is the basic syntax for a function in Galaxy:

returnType functionName( parameter_list )
{
    body_of_function
}


Return Type
The first piece of information you must provide is the return type for a function. The return type can be any variable type that is allowable to return from a function (everything except for structs and arrays). Functions cannot return constant variables.

The return type of a function allows you to treat that function call as if it were a variable of that type. For example:

int someFunction()
{
    return 3;
}

// in some other function:
int x = 2 + someFunction(); // someFunction() has a return type of int and thus can be used as if it were an integer

In addition to normal types, functions can have a special type called void. Void means that the function does not return any information and cannot be used in expressions with other variable types.

Whether a function has a return (non void) type or not (void), it can be called on its own. The return value can be completely ignored. For example:

// using the someFunction defined above
void main(){
    int x;    
    someFunction();
// valid, we ignore the return type
    x = someFunction(); // valid, we use the return type
}

One thing you may have noticed in the above examples was the return statement. The return statement signals that the function should exit immediately and return whatever value follows it. The expression returned does not need to be simple - it can be any arbitrary expression so long as it eventually evaluates to the return type of the function.

bool derp()
{
    return someFunction() > 2;
}


If a function has a return type, it must have a return statement that is reachable no matter what. What this means is that if your code has if statements (to be covered later) in it, the function either needs an independent return outside of this or needs one in an else statement:

bool herp()
{
    if( true )
    {
        return false;
// if this were the only return statement, this would be an error
    }
    else
// the else must have no conditions
    {
        return true;
// having this in an else case with no conditions satisfies the return requirement
    }
    return false;
// this return statement at the top level (outside of any if/while/else) also works
}

What if I want to return more than one variable?
It will often be the case that you wish to have a function affect more than just one variable. While it is not possible for a function to return more than one variable at a time, nothing stops a function from altering a global variable.

So long as a global variable is defined before a function, that function may safely alter the contents of that global variable. In this way, it is possible to write functions that "return" many values at once, since they directly modify existing variables and do not need to return values to do so.

int[3] globalArray;
void modifyArray()
{
    globalArray[0] = 1;
    globalArray[1] = 10;
    globalArray[2] = 100;
}


Parameter List
So now that we understand returning information from functions, let's discuss passing information to a function. In a function declaration, the parameter list allows us to do this. Parameters are optional and are not the only way to get information into a function. Global variables can also be used to get information into a function assuming they are declared before the function is.

Parameters look something like the following:

void someFunc( type1 name1, type2 name2, ... ){} // in general
void someOtherFunc( int foo, bool bar ){} // a real example

The parameter list is a set of type name pairs that describe what kind of information the function takes in. These types can be any type that is allowed to be a parameter (all types except arrays, structs, and constants). Parameters have local scope to the function and cannot conflict with any local variables (more on this later) defined in the function.

When calling a function with parameters, you must pass values, or arguments for each of the parameters listed. For example:

void aFunction( int foo, bool bar ){}
// in some function:
aFunction( 3, true ); // for each parameter in the function declaration, we pass an argument

The type of the argument must match the type of the parameter and they must be passed in the same order as the parameters are listed. The names do not need to match, but all parameters listed must be provided as arguments to the function call. These arguments can be literals (like the number 3, the value true, etc), variables, or any type of expression that evaluates to the proper type.

When a function executes, the parameters will take on the values of the arguments you pass in. Inside of a function, parameters are just like any other variable and can be used as such. One thing to note is that changing a parameter does not change the argument that was passed in. They are distinct variables that exist in different scopes.

Using Parameters
Let's consider the example of setting a unit's life to some arbitrary amount. Here's a function that does that:

void setUnitLife( unit theUnit, fixed life )
{
    life = life / 2.0;
    UnitSetPropertyFixed( theUnit, c_unitPropLife, life );
}


If we pass some unit and some life amount to this function, it will change the life of that unit to half of the life we passed in. Note that though the parameter life changes in the function body, it would not affect whatever value we passed in as an argument.

A Note on Built in Types
Recall that I mentioned that changing a parameter does not affect an argument. While this is true in theory, many of the built in types do not work in exactly this fashion.

Consider the previous example of changing the unit's life to some value. The same unit that we pass in is the one that is modified. How can this be if it is a distinct variable? The reasoning for this is that many of the more complex built in types can be thought of as references to some object. When we pass in a unit, we copy that reference to the unit. If we changed theUnit to refer to some other unit, it would not affect the argument we provided. However, if we do not change its value, it still refers to the same unit that we passed in - thus we can affect it by calling functions such as UnitSetPropertyFixed().

Function Body
The body of a function is the place where you write all of the code for the function - the stuff that actually makes it do something!

If you declare any variables inside of a function, they have local scope to the function and cannot be seen outside of it. All variable declarations in a function must be the first thing that happens in a function. In addition, these variables must all be declared before a single one of them is defined.

Local variables declared and defined properly:

void myFunc()
{
    int herp;
    bool derp;
// one declaration per line at the beginning of the function
    herp = 3; // all definition done after all variables to be used are declared
    derp = true;
}


A function with mistakes:

void myError()
{
    int herp = 3;
    bool derp;
// this will now cause an error since we've defined a variable
    // some code here

    bool foo; // this will cause an error as it is not at the top of the function
}

The variables declared inside of a function share the same scope as any parameters. This means that both local variables and parameters can only be read from inside the function and must not share any names.

Local variables and parameters with the same name as a global variable take precedence.

int hello = 2;
void myFunc()
{
    int hello;
    hello = 3;
// the local hello takes precedence over the global one,
}
// the value of the global hello is still 2

As for the rest of the code inside of a function, what goes in there is entirely dependent on the purpose of the function. Function bodies consist of as many statements as you would like doing whatever they need to. Examples will be provided later in the tutorial when we introduce more complex concepts.


Programming Style
As you write code in Galaxy, it is useful to keep a consistent style and format your code well. This will make it easier for you (and others) to read and maintain.

There are many different ways of indenting your code. Take a look at this article. My personal preference is the Allman style.

A great article on coding style is located here. It covers many facets of well written code.

The most important things to keep in mind are:










Star Depot
Contact      Login