Author: dwringer
Author Website: https://github.com/dwringer/a3system

Requirements: No addons required

Version: 1.0

Short description: Many times when scripting in SQF it may occur that implementing object-oriented techniques can simplify or enhance certain abstractions.

Date: 2016-10-29 03:08


Comments: (0)
Rating:



 



SQF Classes for ArmA 3

by
dwringer


Description:
Many times when scripting in SQF it may occur that implementing object-oriented techniques can simplify or enhance certain abstractions. The technique of using getVariable/setVariable on a unit or gamelogic to store function code or references specific to that unit's situation is a common pattern found in many missions and modules. Here, that idea is taken one step farther: instead of storing functions on game objects, we store an array of class names (representing a chain of inheritance or a mixture of interfaces). Meanwhile, a master Classes array holds a subarray for each defined class, containing a function definition for each class method. With this arrangement and just a handful of primitive functions, an unlimited variety of classes and methods can be declared on-the-fly while a mission is running.


Installation / Usage:
For usage instructions and information of how to use the SQF Classes for ArmA 3 please refer to the included documentation and/or example mission.

This module relies on the alist/, lambda/, and vectools/ modules (included). These include support functions that have not been particularly optimized, and indeed several of them are suboptimal solutions. This class framework expects certain of these functions to be available, but they can be replaced with any suitable alternative that implements the same interface. In fact, I recommend any serious user of this system consider these functions as a "starter kit" which may be upgraded as convenient.

Because I have only scripted in SQF for my own enjoyment in the Editor, I am not familiar with techniques or best practices for robust server-ready code. It is quite possible that there are fatal security flaws in this approach for online code. I would be happy to make additions and updates to help secure the system for deployment "in the wild", but totally lack the experience to assess the requirements. Specific suggestions are invited and very welcome.

This is a selection of modules drawn from a larger repository I keep at http://www.github.com/dwringer/a3system/. There, more recent versions of these modules can often be found. I also placed other modules I created over the years, which are of variable quality and may or may not be worth a look.

Class layout
By themselves, classes have a name, an initializer method which is declared alongisde the class, and any number of additional methods. The initializer and other methods can all take multiple parameters, although currently variable-parameter-counts are unfortunately not supported. Each instance stores its own inheritance chain under the local variable "class_names".

A new class can be created at any time with a call like so:
["<your-class's-name>", <init_function>] call fnc_class;

"_self" (or something equivalent) is a required first parameter for all functions used as methods. Additional parameters can also be added to the parameter list for the initializer and ordinary methods. Replace <init_function> with an actual function name that accepts the desired number of parameters, or create one with fnc_lambda (below).

Lambda:
To prevent the tediousness of creating a new SQF function or otherwise writing out a function of several parameters every time a method is needed, this module can take advantage of the scripts in the included lambda/ module. For a simple example, a compiled function to compute the volume of a cube accepting three variables can be made in one line:
 _fnc = [["_l", "_w", "_h"], {_l * _w * _h}] call fnc_lambda;

A default initializer for a new class can be made like this:
 _fnc = [["_self"], {_self}] call fnc_lambda;

That is a trivial example which merely returns the instance after creation, but the technique can be generalized.

We can define a class along with its default initialization-method by combining fnc_lambda and fnc_class like this:
["MyClass", 
 [["_self"], {_self}] call fnc_lambda] call fnc_class;

Macros:
We can also take the above class creation using fnc_lambda and fnc_class and rearrange line breaks to make it look more like a declaration in a more traditional language:
  ["MyClass", [["_self"], {
        _self
  }] call fnc_lambda] call fnc_class;

But that is also unwieldy. Instead, we can create macros to standardize this declaration and clean it up slightly:
  DEFCLASS("MyClass") ["_self"] DO {
      _self
  } ENDCLASS;

Perhaps not ideal, but usable.

Method declarations work the same way, except the function takes classname and method name as the first two parameters, and is named fnc_method instead of fnc_class. This is wrapped up in the DEFMETHOD macros:
  DEFMETHOD("MyClass", "a_method") ["_self", "_param1"] DO {
    .. do some stuff ..
  } ENDMETHOD;

Example overview:
This will demonstrate how to create abstract instances and send messages (method calls) to them. The included example is a mission with four men (crew_1..crew_4) and two vehicles (car_1, car_2). You may easily duplicate it in a different map if you don't have Tanoa available. There is a single Radio Alpha trigger, upon whose activation the following scripts are executed:
  CG = ["CrewUnitGroup"] call fnc_new;
  [CG, "assign", crew_1, "driver"] call fnc_tell;
  [CG, "assign", crew_2, "gunner"] call fnc_tell;
  [CG, "assign", crew_3, "driver"] call fnc_tell;
  [CG, "assign", crew_4, "gunner"] call fnc_tell;
  [CG, "assign", car_1, "vehicle"] call fnc_tell;
  [CG, "assign", car_2, "vehicle"] call fnc_tell;
  [CG, "board_instant"] call fnc_tell;

Here, fnc_new is how we create a new instance. It accepts the class name and any init parameters appended to it (here there are none). We could also put a Game Logic in the editor instead of using fnc_new, and in its init use the following:
  _nil = this spawn {
      waitUntil {not isNil "ClassesInitialized"};
      [_this, "CrewUnitGroup"] call fnc_instance;
      .. rest of code ..
  };

The method calls are actually made with fnc_tell, which takes as parameters the class instance, method name, and subsequently each method parameter. Thus, all methods to all classes are defined and called using a standardized syntax and set of functions which have been precompiled.

Since fnc_instance makes an instance out of an existing object, we can also use it for creating class inheritance. Consider the following:
  DEFCLASS("MySubClass") ["_self"] DO {
      [_self, "SuperClass"] call fnc_instance;
      .. rest of code ..
      _self
  } ENDCLASS;

Now, MySubClass inherits all the methods from SuperClass. Each game object keeps an ordered list of its class assignments, so if a method is not found on MySubClass it will be looked up on SuperClass. I have provided the macros SUPER and SUPER_ARGS in include\classes.hpp to facilitate subclassing from within classdef header files.

If you want to create a subclass method that implements a superclass method under a different name, that can be done by looking up the class alist in the Classes global alist and using the function contained there as a new method. In classdef files, this has a macro called ALIAS(subclass, subclass_method, superclass, superclass_method).


Example mission requires Arma 3 Apex, but it can be recreated otherwise by following the tutorial:
- Arma 3 Apex


Known issues:
It is almost certain you will find typos, errors and omissions in this post, the documents, and/or the code. I would be enormously grateful if these errors could be (kindly?) brought to my attention, and I will make every effort to correct them quickly.


Notes:
Good luck, and if anyone creates some useful classes or a cool class tree, I'd love to hear about it. Even if this just serves as an example of what can be done to (ab)use SQF, I am satisfied.


Changelog:
v1.0


Forum topic:
- BI forums




Enable javascript to be able to download from Armaholic please!