CHAPTER 3: we're writing our first SQF script

Having absorbed the previous two chapters, let's now try to practice and write a script for Arma 3. No, we will not write "hello world!" on the screen, the script should do something cool in the game - that's the key! This chapter is long, there is no need to swallow it whole in one sitting. Bit by bit. As much, as we can embrace at once.  



1. Our goal

Here we want the pointed artillery unit to automatically fire at the opponents known to the player's unit. 

We aim for a certain minimum here - for the simplicity of the example. Naturally, there is a wide field for further expansion and enrichment of the script: better choice of targets, automatic detection of available artillery, aiming with regard to target movement, simulation of sight/observer errors (inaccurate fire), player control over fire and more. That's how dozens of lines can gradually and imperceptibly turn into thousands over time...  



2. What must contain a script, that will do this for us?

We are able to anticipate some of the requirements in advance, others will come to light when testing the script. Our algorithm:

a) must check, if there are any targets on the map;
b) must check, if the player's unit knows about them;
c) must check, that the appointed artillery unit has an ammunition;
d) it must check, that there are targets within range of the artillery;
e) must choose one target among them;
f) should check, that the target is not too close to the player or allied units (if we want to avoid friendly fire);
g) it must make the artillery to fire on the target;
h) he should do the above all the time, not just once.



3. We're looking for helpful SQF commands

It may take a while until we familiarize more with available commands, in the end we will gather for respective points e.g. something like this:


a)

allUnits - gives us a collection (array) of all alive units on the map;

foreach - a kind of loop - useful when you want to do something with every element of some array (allUnits);

if - one of the pillars of the script's logical construction. The given code will be executed only if the given condition is met (optionally another code can be executed when the condition is not met);

andornot - logical operators often needed when formulating conditions. They mean "both" and "this or this", "not this";

getFriend - tells if given two sides are friendly or hostile to each other;

player - returns the player's unit - we will determine who is the friend and who is the enemy in reference to this unit;

side - gives the side to which a given unit belongs;

pushBack - adds the indicated value to the given array - as the last of the array;

count - gives the number of elements in the given array. It will allow us to make sure, there is someone to shoot at.

For each alive unit we will check, if it belongs to the side hostile to the side of the player's unit. If so - we will add it to the array of targets. If not, we will add it to the allies array - for the purposes of subsection f).


b)

knowsAbout - an enemy unit will go to the array of potential targets only, if the player's unit "knows" about it.

c)

someAmmo - probably the easiest way to check if the unit has any ammo.


d)

inRangeOfArtillery - checks, if the given position is within the range of the given artillery unit for the given type of artillery ammunition;

position - gives the position of the selected object (coordinates [x,y,z] in meters from the lower left corner of the map);

currentMagazine - gives the currently used type of ammunition.


e) 

selectRandom - gives a randomly selected element of a given array. The target selection method is an interesting issue by itself. Picking a target at random is probably the easiest way, so we will stop there, but I guess, that many people will prefer to think of something more advanced, something that will reasonably prioritize targets.


f) 

findIf - searches the given array until it finds an element, that meets the given condition;

distance - gives the distance between two positions/objects.

g)

doArtilleryFire - a specialized command ordering the given artillery unit to fire at a given position on the map with a given type of ammunition and the number of shells.


h)

while - kind of loop. The code contained in it will be executed cyclically, time after time, as long as the given condition is met, e.g. as long the given artillery unit is not destroyed and is manned;


sleep - suspends the further execution of the code, where this command is used for a given number of seconds;

alive - checks if a given entity is alive;

gunner - returns the unit manning the gunner's position in the vehicle/static weapon system (returns "no object" - "null" - if gunner is missing);

isNull - checks, if the variable provided contains "no object". This happens, when the object saved in a given variable is e.g. deleted with a script command (disappears/ceases to exist in the simulation);

spawn - - executes the given code "separately", that is simultaneously. Then further lines of the script are executed without waiting for the code run with this command to be executed to the end. It will be useful that our loop does not block the execution of any code placed under it. We don't plan to add anything now, but who knows, maybe we'll want to later. Minus: simultaneous scripts must share the processor power available to them in each frame. This can cause delays in their execution with many computationally intensive scripts. Here we don't need to worry about this.


Note that the documentation for each of these commands gives its syntax (or syntaxes), that is, the correct notation of the command along with the values ​​it requires to form a complete expression. It also indicates, what kind of value, if any, will be returned.

In addition, each such expression must end with a semicolon to separate them from the following ones. The last expression is an exception, because there is nothing to separate it from - here the semicolon is optional. A good practice  to improve script readability is to finish the line and start a new one after each semicolon. The same applies to assigning values to variables, i.e. defining variables. We'll see how it all looks in a moment.

Code placed //after two slashes lub /*between such symbols*/ is ignored by the game engine - it is not executed. This allows you to place commentaries inside the code or deactivate selected parts of the script as needed (mainly during testing and debugging).



4. We have required ingredients, let's put things together...

Here is the ready code:

_handle = [] spawn
{
sleep 1;
_arty = arty1;
_mySide = side player;

while {not (isNull _arty) and {(alive _arty)}} do
{
_gunner = gunner _arty;

if (not (isNull _gunner) and {(alive _gunner)}) then
{
_targets = [];
_allies = [];
_cMag = currentMagazine _arty;

{
_uSide = side _x;
if ((_uSide getFriend _mySide) < 0.6) then
{
if ((player knowsAbout _x) > 1) then
{
if ((position _x) inRangeOfArtillery [[_arty],_cMag]) then
{
_targets pushBack _x
}
}
}
else
{
_allies pushBack _x
};
}
foreach allUnits;
if ((count _targets) > 0) then
{
_tgt = selectRandom _targets;

if ((_allies findIf {((_tgt distance _x) < 2000)}) < 0) then
{
_arty doArtilleryFire [(position _tgt),_cMag,2]
};
};
};

sleep 30;
};
};


Lines will be executed in the order in which you read this text: from left to right and from top to bottom.

If now we are not interested in anything except how this script works, we go straight to point 7, although the part omitted this way explains the issues, that without this part may remain incomprehensible. 



5. Script overview - general notes

Why is the code "outstretched" on many lines? Must it be this way? No. Theoretically, you can write everything in one, very long line and it will work the same. It doesn't make a difference to the computer. Makes a difference to us. The loose layout with specific arrangement of brackets is designed to make reading and editing the code as easy as possible - it makes it more transparent and understandable. Details depend on personal preferences in this regard.  Expressions that start with the _ underscore are variables. The underline at the beginning means, that they are so-called local variables. For now, there is no reason to know exactly what that means. Generally speaking - they only exist in this chunk of code (see below - curly braces) in which they were defined (equal sign), the rest of the script doesn't see them. There are also global variables, only one in this script: arty1. Defined global variables can also be used outside of the code/script in which they were defined. There are some nuances here, but this is not the knowledge we need now. The act of defining the variable itself is marked with an equal sign. The variable does not exist for lines of code executed before its definition. 

The names of the variables are almost arbitrary - they depend on our creativity. For occupational hygiene names should associate with variable's meaning. They cannot start with a number and cannot be identical to the names of script commands. They also do not tolerate some exotic symbols. There are also some prudent practices regarding the names of global variables, that allow to avoid situations, where two different scripts run in the scenario define global variables with the same names, "stealing" them from each other, which usually ends in a litany of errors (there cannot be two variables of the same name at the same time). 


6. About parentheses

Before we discuss the code line by line, we need to pay some attention to parentheses and their use. First and foremost - the brackets in the code exist in pairs (with not important now exceptions). Unpaired brackets are one of the more common and harder to spot errors, that break the code. 

You can think of pairs of brackets as bags in which we put something, including very often other bags. An unpaired parenthesis is a leaky bag, from which everything spills out.  

Secondly, we mainly use three types of parentheses in SQF scripts. 

round brackets - (take from me not what I contain, but the result of what I contain). 

Typical applications: 

a) defining conditions, i.e. expressions, which result in a Boolean value, i.e. one, that can be equal to true or false. Used in if-then (-else) constructs and in the syntax of other commands, that require true or false statements. Example:

((_tgt distance _x) < 200)

we read as follows: "object _tgt is less than 200 meters away from _x". Well, this sentence may be true for two objects, then the content of the red pair of brackets is "true", or not true, and then it is "false". The result of this line is therefore the Boolean value.

b) when the notation without brackets is not read correctly by the command or when we are not sure in this regard and we do not want to check (excess pairs of brackets are not harmful here)... :) Example:

((_tgt distance _x) < 200)

Will the game engine understand the code without these brackets?

(_tgt distance _x < 200)

sometimes this is not explicitly described in the command documentation and it can vary. In this situation, we can either test the variant without brackets and find out, which case is it for given command, or put something in brackets "just in case".

Sign < expects a number from both sides. 

_tgt distance _x

returns a number, but if you are not sure, that the code will not be read like this:

_tgt distance | _x < 200

we put the command with the "entourage" into brackets:

(_tgt distance _x)


This guarantees, that the engine will first calculate the distance between objects, and only the result of this calculation - that distance - will equate with the number 200 to see, if this distance is less than 200 meters. Without parentheses, it may be a problem with such reading this line, where the code tries to compare with the number 200 the object _x (_x <200), which is absurd, and as a result of this in the syntax of the distance command it considers that it lacks the second point required to measure distance (_tgt distance ?) - two errors breaking the code. 

If we decide to test the above code without an internal pair of brackets, it turns out that it is not necessary here. The code works without it. Of course, this test knocks us out of the rhythm when writing the code and requires more time, than simply inserting a pair of brackets. Personally, I prefer to have the habit of placing such things in brackets - I prefer some redundancy than looking later for problems in the code in places where the brackets were necessary and I didn't put them in rush. For counterexample, there are no parentheses in this context:

if _obj1 distance _obj2 < 200 then

and this causes an error. Notations working correctly:

if (_obj1 distance _obj2 < 200) then
if ((_obj1 distance _obj2) < 200) then
if (((_obj1) distance (_obj2)) < 200) then //but that's too much...

Additional brackets also help me with the readability of the code. In the end - as you prefer.


As we can see, scripting is not only about our code being correctly executed, but also about working with it in as convenient for us manner, as possible. It's like order on the workbench - as long, as we have all the necessary raw materials and tools, we are able to do our work, regardless of whether everything is intuitively, neatly ordered in separate boxes, or thrown onto one, big pile, because we considered the boxes redundant and unnecessary. The only question is, in which situation we will do our work faster, with less effort and in a better mood. And since intuitiveness is a subjective thing, two scripts doing the same thing written by two people may differ from each other and there may be not an unambiguous answer, which one is written better.   


square brackets - [I am a set/array of values, ​​that I contain].

There is not much to add here. The pairs of these brackets are an array value, i.e. a series of values. Note: the correct and frequent situation is, when it will be an empty array with no value inside - usually temporarily.


curly brackets - {I contain some code}.

This type of parentheses indicates the value of code type. The included script may or may not generate some value, that a pair of parentheses containing it returns as long as this code is executed, e.g. via the call command, if this value is placed at the very end of the code contained therein. 

Example 1:

_dst = call {(object1 distance object2)}

will return and write to the variable _dst a number equal to the distance expressed in meters between object1 and object2, because this is the last thing contained inside the curly brackets pair. On this principle is based the definition and use of so-called functions.

Example 2:

call {arty1 doArtilleryFire [(position player),(currentMagazine arty1),2]

These brackets do not return any value - according to the documentation, the doArtilleryFire command does not return anything, but the code will cause artillery fire by arty1 unit in the scenario (attention - on the player's position!), if the range etc. conditions ar met. 

Typical applications:

a) a portion of the code put inside the brackets this way can be saved under the variable name as a function, same way, as other types of values. Without going into too much detail, functions are just pieces of code, which usually return some value, stored under a variable. Function variables are very useful especially in large scripts, where an identical piece of code is needed many times. Then we minimize the size of the code, using a short variable instead of a copy of the code stored in this variable, and by changing the code in the function we introduce the same change at one go wherever it is used in the script. In our sample script, we do not use functions. 

An example of a function that calculates azimuth in degrees from one point to another with a possible random spread:



 And its exemplary use in the script:



So we have the calculated angle (_angle) in one line of code instead of a dozen or so. _angle takes the same value as the variable _angleAzimuth0 present in the function because it is the last element of the function code.

b) multiple command syntax requires this type of value. For example, constructions like "if so, do {code}, otherwise do {another code}". This application is common and visible in our sample script. Bah. We have there portions of code, that contain portions of code containing portions of code etc. Funny isn't it?



7. Script overview - details

Phew. Finally, it's time to discuss our artillery fire script in detail. Meaning of the lines one by one:

_handle = [] spawn

This line runs our entire script. It does this with the spawn command which, as we remember, runs the given code in parallel to what is next/below. In our case, if we look at the braces, there is nothing under the code executed by this command.  The _handle variable stores the value returned by the spawn command. We can do without it:

[] spawn

will also work. However, the value stored under _handle can be used somewhere later, e.g. to manually terminate our script (since it is based on a potentially endless loop...). 

A pair of square brackets is an array of values ​​- parameters, that we want to pass inside the "spawn" code in accordance with the syntax of the spawn command. As you can see - we don't want to transfer anything, the array is empty. Theoretically, this parenthesis would also be negligible. Same:

spawn

 would also work. 

The equal sign means the act of defining a variable - assigning its value. From this point, the _handle variable exists and has a value equal to what the spawn command returns. 

After the spawn command, its syntax expects the code to be run by the command. This code is not in the same line. It starts in the next (we remember - issues of code clarity). For the computer, however, it is still the part of this command's expression, because it ignores transfers to new lines, and instead considers the semicolon as the end of the "sentence". 

So, contrary to appearances, the rest of our script is actually the part of the syntax of this spawn command here. Let's go further:

{

In the next line we open the "bag" with the code, which is closed only by the last of the brackets }; in our script. This is the code attached to the spawn command.  


Horizontal shift of the code with the help of tabs makes it easier for us to recognize this fact - each step of the shift is another "bag in the bag".  

sleep 1;

We wait 1 second before we go further. Such a stop is not always necessary, but it is rather a good practice - the very beginning of the scenario is a time, when certain matters are only just initializing in turn. Some scripts, that run at the very beginning of the scenario, without such a pause may not work, if they require something, that has not yet been initiated in that split of second. 

_arty = arty1;

The next line is already inside the spawn code. Defines the local variable _arty by assigning to it an object type value - in our intention it will be an artillery unit set in the scenario via editor and named arty1Here it will be a BLUFOR mortar. It should be manned by the gunner - the AI ​​unit, as a crew. The very name of the object itself will constitute a defined global variable in the scenario, so we could skip this line, and everywhere in the script, where we write _arty, write arty1 instead. Well, but we wanted to be so elegant and use local variables everywhere... This has also other reason - it makes easier to change the name of the artillery unit used in the script - we only have to do it in one line, instead of many. 

Just in case the illustration showing how to name the object in the editor (right-click the object, select Attributes...), make sure, that we clicked the mortar itself, not its crew, enter the name and confirm with OK:



_mySide = side player;

As the _mySide variable, we define for later use a side value meaning the side for which the player's character is fighting (should be the same or allied with the side of the artillery unit, we set!).

So we saved two variables inside the "spawn" code, but before the loop contained lower - these are values, ​​that should stay unchanged during the gameplay, so there is no need to redefine them in each loop cycle. In this way, we save a little bit of computing power. 

A blank line follows, which is only relevant for my subjective code readability.

while {not (isNull _arty) and {(alive _arty)}} do

We begin our main loop, in which most of the interesting things will happen. This is a loop, that is started by an inseparable tandem of commands: while-do. According to the syntax of this loop, between them is located a portion of code, whose task is to return the Boolean value (true/false). This value is checked at the very beginning of each loop cycle and if it is false, the loop will end immediately. Thus, a portion of code between while and do is a condition of the the loop continuation, to be checked once per cycle (not in real time!). The condition consists of two components connected by the "and" operator, which means, that both components must be "true" for the whole condition to be "true". If any of these components becomes "false" - the loop will end. 

First component: not (isNull _arty) we understand as follows: "object  _arty exists on the map in the scenario". The loop will end, if something "deletes" our mortar. If our script is the only code running in the scenario - this should not happen. But if for some reason we also use another script, that can remove a unit from the map, you need to be prepared for such an eventuality. Otherwise, if you remove the mortar, the script will throw errors (because of the command trying to do something with a mortar, that is not there), and this is inelegant. 


Well, someone smart will ask, but since this condition is checked once per cycle, it may happen, that the removal of the mortar will occur during the cycle and before the cycle ends, errors will appear. True. In our case, the chance is small, but not zero. This condition is therefore not 100% sufficient safeguard. To be sure, you would need to set additional lines to leave the loop if the mortar disappears, just before each line, that references to the _arty variable. I don't do it for the sake of code logic clarity. 

The second component: {(alive _arty)} means: "the _arty object is not destroyed." The loop will end, if our mortar is destroyed during the game - its object still exists, but it only presents the wreck. The same caveats apply here, that apply to the first component. Note that this component, unlike the first, got an extra pair of braces. This is a trick used in logical condition expressions. The code would work in this form:

while {not (isNull _arty) and (alive _arty)} do

The difference is that here the second component is checked even, when it is already known, that the whole expression will return "false" anyway, because the first component is "false" (and both would have to be "true" to give the overall result "true"). The addition of these brackets causes, that the testing of the second component (and similarly subsequent, if any) is skipped, if the result of the whole expression is already determined by the already checked condition components. This saves computing power and in some situations (not here) prevents errors, if further components are correctly calculated only, if the previous ones give a specific result. The "do" is followed by a portion of code, that will be executed cyclically (time after time) - it is again moved to the next lines:

{

This line opens the aforementioned portion of the looped code (and we are still in the "bag" of the spawn code)).

 _gunner = gunner _arty;

Under the variable _gunner we  save the AI unt (object value) - the soldier, who occupies the gunner position in our artillery unit. We define this variable inside the loop, and therefore again in each cycle, because it may happen, that the initial gunner is killed, deleted or leaves the mortar, and another unit (e.g. through a separate script) can take its place. So we need to "refresh" the value of the _gunner variable in each cycle. Why do we need this "gunner"? Only for the next line:

if (not (isNull _gunner) and {(alive _gunner)}) then

Artillery cannot fire without a gunner, the rest of the code does not make sense if the gunner does not exist or is dead. So we check it before we take the next step. We use the if-else commands tandem, between which we will find a condition similar to the one between "while" and "do", but enclosed in parentheses. It subject to the same laws, the only difference is, that this refers to the gunner object, not to the mortar. A portion of the code attached after "then" (almost all the rest of the script de facto) will be executed only, if the mortar is manned by an alive gunner.

Let's summarize the previous steps:
    • we launched the code (spawn);
    • in this code we have launched a loop (while-do);
    • in the loop we set a condition on which the execution of the essential part of the code (if-then) depends on each cycle.


_targets = [];

The _targets variable will be our bag, for now empty, into which we will throw possible targets to shoot at. We create it anew in each cycle.

_allies = [];

In turn, we'll throw allies (including the player's character) into the _allies bag.

_cMag = currentMagazine _arty;

As _cMag, we write a variable of string type (simple text) - the name of the "magazine" currently used by the mortar, i.e. ammunition. This is not the colloquial name, that appears on the screen, but the so-called classname, that each type of magazine has, but also objects, weapons and other stuff. Class names are used, among others, in scripts. Sample magazine class name:
"30rnd_9x21_mag"

{

We are starting a forEach loop by opening the code attached to it. In this case, the syntax is that, so we attach the code before the command, not after, as it is in the case of the while-do loop.

_uSide = side _x;

We see here the variable _x. We didn't define it. The _x variable is a special variable, that is automatically defined inside the code attached to the forEach loop. The forEach loop executes the attached code for each element of the array, that is attached after this command (as we'll see below). Inside the attached code the variable _x indicates the current element of this array. If we attach an array of numbers [1,2,3] to the forEach loop, then the attached code will be executed for each of them, after which the loop will end. In its first cycle, _x will be equal to 1, in the second: 2, and in the last: 3. 

Here we have attached the array returned by the allUnits command, containing all living units (people) currently present on the map. So our loop will have as many cycles, as many units there is on the map, and in each subsequent cycle _x will mean the next unit.  So, under the _uSide variable, we save the side, on which the _x unit fights.

if ((_uSide getFriend _mySide) < 0.6) then

If the side of _x unit is hostile to the side of the player's unit, then...

if ((player knowsAbout _x) > 1) then

... if the player's unit knows enough about the _x unit, then...

if ((position _x) inRangeOfArtillery [[_arty],_cMag]) then

...if the position of the _x unit on the map is within the range of our mortar's fire, then...

_targets pushBack _x

... the _x unit is put into _targets, an array prepared for potential targets to fire at.

else

Otherwise, i.e. if the _x unit is not hostile to the player's unit...

_allies pushBack _x

...without further conditions goes to the bag holding the player's allies. Interestingly, the player's unit will also go here.

}

We close the code attached to the forEach command...

foreach allUnits;

...and in fact the rest of the forEach loop syntax has already been discussed.

We got two bags (collections) from the above: one with potential targets, the other with all allies. To the point!

if ((count _targets) > 0) then

We count the contents of the bag with the targets. If there is something inside - we can shoot.

_tgt = selectRandom _targets;


We choose the target using the simplified method for our example purposes, i.e. draw one at random from the bag. _tgt means an enemy unit - the unfortunate one, who will be targeted by our mortar. If a set of _targets was empty, here the script would be indignant, spit out the error logs and went away. We avoid these excesses thanks to the condition, that counts the contents of the bag. 

if ((_allies findIf {((_tgt distance _x) < 200)}) < 0) then

This condition is met if no unit from the allies bag is closer to the target, than 200 meters. To better understand the above entry, it is worth studying the previously linked documentation of findIf. In some respects, it looks like a forEach loop: it has an attached code with the special _x variable, and it has an attached array, for which it executes the attached code. But the code attached here must return a Boolean value and the loop ends, when the code for any of the elements of the array (_x) returns "true". This will happen, if any of the allies is closer, than 200 meters from the picked target. When exited, the findIf loop returns a number: index (item number) of the array element, for which we received "true", which means it shows us with the finger, which delinquent as the first of the array meets the condition contained in the attached code.

Note: these strange people, programmers, like to start counting not from 1, but from 0 (yes, I know...). This is the case for items in the array (set): the first element has the index 0, not 1. The second element has 1 and so on. Only this fact explains the entire above expression to us. Everything to the left of the < character reflects the index of the first element in the set that meets the given condition. If it does not find any - returns the number -1. So -1 to the left of < means, that there is no ally closer to the target than 200 meters, that is: we're free to fire! Hence to the right of < is 0 - we are waiting for a value less, than zero on the left. Any other will mean, that someone is too close to the target to shoot. 

_arty doArtilleryFire [(position _tgt),_cMag,2]

What can I say... Finally we fire! Twice! Because!

The next three lines close the next bags with the code. The last thing to do in the while-do cycle is cigarette break:

sleep 30;

Each cycle ends with a 30-second "nap". Why? Without a pause, the loop would do cycles at a rate of one per frame or maybe even more often. As a result, the mortar would not be able to aim, reload and shoot, and the code itself would be very "computationally heavy" for the processor. It would eat all the time (the loop is conditionally "perpetual") a lot more resources, than it must. Pause dilutes the atmosphere of intensive calculations.

And why 30? From experience and intuition. That much seems not too much and not too little for our needs: the mortar will be able to hit one target per cycle - every half minute. And since the projectile can fly longer than 30 seconds, it is possible to fire a new salvo before the previous one reach the target.

And why sleep here and not at the beginning of the while-do loop? It's better here, because at the beginning of the cycle the while-do loop checks its continuation condition. If we put sleep 30; immediately after this check, there will be 30 seconds for the condition just checked to cease to apply, but the loop will not detect it until it completes the whole cycle, which means errors. It is better, that all calculations and actions are carried out immediately after checking the condition, as long as it is "fresh", followed by a pause.

Then it remains to close the bag with the code of the while-do loop and the bag of all code, that we started with the spawn command. 


8. Now let's check if the algorithm works

It is important to realize, that a newly written script usually doesn't work. The larger it is, the more often it will be that way. More about this in the chapter on debugging, this chapter presents the code already checked, combed and working.  

Here lies the prepared demonstration scenario to run in the editor:

Boom script (.zip file, Dropbox)

For a dessert, a trial run:  


No comments:

Post a Comment