Creating and using SQF functions

By Spooner

Types of functions

Concurrent (Parallel) functions

To all intents and purposes,

// Compile and run once (slower, uses slightly less memory).
[] execVM "frog.sqf";

is the same as:

// Run once and throw away the compiled function
// (slower, uses slightly less memory).
[] spawn compile preprocessFileLineNumbers "frog.sqf";

or

// Compile once and save the function in memory
(faster, uses slightly more memory)
frogFunction = compile preprocessFileLineNumbers "frog.sqf";

later...

// Run it as many times as you like later without having to
// recompile it...
_result = [] spawn frogFunction;

_result = [] spawn frogFunction;

Both of these will run a script in parallel with the current script, thus immediately continuing with the current script. The new script will start running in the next frame, which would, for example, be after about 0.05s if you are getting 20fps.

Generally, try to avoid starting concurrent functions unless you really intend to run them in parallel with the current function, since they are slightly more costly to run compared functions started with run. Same thing goes if you need it to run RIGHT NOW rather than a bit later.

Immediate functions

// Run once and throw away the compiled function
// (slower, uses slightly less memory).
_result = [] call compile preprocessFileLineNumbers "frog.sqf";

and

// Compile once and save the function in memory
// (faster, uses slightly more memory).
frogFunction = compile preprocessFileLineNumbers "frog.sqf";

later...

// Run it as many times as you like later without having to recompile it...
_result = [] call frogFunction;

_result = [] call frogFunction;

Will do the same sort of thing, but instead of just starting up the other script and leaving it to its own devices, it will immediately run the complete script and then the last value in that script (or last value in a exitWith block that is run) will be put into _result. Only then, will the original script continue on to the next statement.

Loading function files into the game

  • loadFile - this loads the file directly into a string without doing anything to it at all (no preprocessing). If you compile this and run it using any method, then any syntax or runtime errors will be vague. It should only be used when you want a file put into a string so you can read it manually. Don't ever use it for loading functions (i.e. don't use it with compile).
  • preprocessFile- this loads the file into a string, while replacing preprocessor directives (# commands like #include and #define) appropriately. If you compile this string and run it using any method, then any syntax or runtime errors will be vague. Unless you are using OFP, never use this for loading text files for use with compile.
  • preprocessFileLineNumbers- this loads the file into a string, while replacing preprocessor directives (# commands) appropriately. If you compile this string and run it using any method, then any syntax or runtime errors will be detailed, like you see with execVMed files, showing both file-name and exact line the error occured on. This is the only way you should be loading files for use with compile if you are using ArmA (it isn't available in OFP).

Creating functions inside files

Functions can be created inside a function file that is being run with call/spawn/execVM too, using curly braces, {}:

myFunction =
{
    hint "hello world";
    counter = counter + 5;
};

later...

[] call myfunction;

The error messages of functions created in this way will be of the same type as the function file they are in. That is, if the file was run with execVM or loaded with preprocessFileLineNumbers then the functions inside it will also give verbose error messages, but if it was loaded with loadFile or preprocessFile then the messages will be vague.

Dynamic functions

Dynamic functions must be created if you don't know what they will contain until you run the script. They use format to create code on the fly, which is then fed to compile and run just like any other function. These types of function always give vague error messages, regardless of what type of errors the file would normally give. This is one of the reasons they should be avoided whenever possible.

One of the most common uses of dynamic function generation is to create numbered variable names which creates a sort of pseudo-array (var1, var2, var3, etc). However, in the vast majority of cases, this is unnecessary and could be more efficiently accomplished using regularly scripted arrays that don't require code generation and thus are less likely to produce terse error messages for you which will be hard to track down.

Functions, procedures and scripts

Many people will find it confusing that this tutorial uses the term "Function" to generally mean all SQF being run, where most people see files as "scripts" and anything compiled into a variable as a "function".

There is a difference between implementation and usage when it comes to "functions" and "procedures" and "scripts". All SQF is implemented the same as "functions" (runs code and returns a value, even if most of the time this is nil and/or is ignored by the caller) but you can consider individual ArmA "functions" to be conceptually "pure functions" (returns a value and doesn't alter external data, such as using setPos or changing a global variable) or "procedures/subroutines/etc" (may or may not return a value, but may affect external data).

As explained earlier, execVM (and SQF files called from actions, for that matter) is equivalent to spawn compile preprocessFileLineNumbers, so all SQF is really being either called or spawned on some level. Thinking of it another way, there is really absolutely no difference between:

// helloWorld.sqf
hint "Hello World";

helloWorld = compile preprocessFileLineNumbers "helloWorld.sqf";
call helloWorld;

and

helloWorld =
{
    hint "Hello World";
};

call helloWorld;

Advertisement