Jump to content

Recommended Posts

SQF Classes for ArmA 3
Edited - release updated with a couple of bugfixes - if you downloaded prior to 6pm GMT on 29 October then please re-download to get the corrected files.
 
 
oFOUVjO.jpg
 
 
"If . . . [the] fact [that brutes abstract not] be made the distinguishing property of that sort of animal, I fear a great many of those that pass for men must be reckoned into their number." -- George Berkeley
 

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.

 

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).
 

 
Another example

If you don't have Apex and can't load the example mission, or want to see how this can be injected into a mission very simply, drop the system folders into your mission with the required #include's in your init.sqf (see example code or the docs on GitHub), then place a group of four units in the editor.  Place an empty Hunter HMG next to them and name it "car".  Now, in the group leader's init field, paste the following code:

_nil = this spawn {
    waitUntil {not isNil "ClassesInitialized"};
    CG = ["CrewUnitGroup"] call fnc_new;
    [[["_u", "_role"], {
         [CG, "assign", _u, _role] call fnc_tell;
     }] call fnc_lambda,
     (units group _this) + [car],
     ["driver", "gunner", "cargo", "cargo", "vehicle"]
    ] call fnc_map;
    [CG, "board_instant"] call fnc_tell;
};

As soon as the class system is loaded, this instantiates an abstract CrewUnitGroup instance, then maps an anonymous function (created with fnc_lambda) across two arrays: the units to be assigned, and the roles to which they will be assigned.  The CrewUnitGroup instance is then given the "board_instant" method name (With no parameters - the crew assignments were already set up by the map!) and the mission starts with the vehicle fully crewed.  Go back to the editor and change the vehicle for a Hunter GMG, but keep the same name ("car").  Just like that, your crew uses the new vehicle. Hopefully this provides some ideas to anyone reading this.

 


 
Without further ado, the link to the repository containing the above example mission folder and all required modules is:
 
 

 
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.
 

P.S. 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.

 

"No point in mentioning these bats, I thought. Poor bastard will see them soon enough." -- Raoul Duke
 

Edited - release updated with a couple of bugfixes - if you downloaded prior to 6pm GMT on 29 October then please redownload to get the corrected files.  There were changes made to fnc_tell and fnc_filter related to nil values being passed as parameters.

Edited - added another example section
 

  • Like 2

Share this post


Link to post
Share on other sites
A Tutorial Walkthrough - The Marker Class:

In order to demonstrate the power of this Class system, and hopefully make it seem more approachable, I will walk through creation of a Marker class that adds the much-desired (by me, at least) functionality to instantiate a marker once and then have unlimited freedom to manipulate it, hide it, or show it again.  Without using a Marker class, it takes several calls and the managing of complicated data structures to accomplish what is done here in just a couple of short lines.

 

The Marker class will inherit from the ObjectRoot class (classdef\ObjectRoot.hpp) that is provided with the class system files.  ObjectRoot gives us the ability to send the messages "_getf" <value> and "_setf" <key> <value> [sent with the fnc_tell function] for conveniently managing local instance variables.

 

This walkthrough guides us through the creation of classdef\Marker.hpp, which can be located at https://github.com/dwringer/a3system/blob/master/classdef/Marker.hpp

 

Initialization:

Following the BIS Community Wiki documentation for createMarker, we find that the following minimum information is required to make a new marker:

  • name
  • position
  • shape
  • type (if ICON)
  • size (if RECTANGLE, ELLIPSE)
 

We wish to simplify marker creation as much as possible, so here we will no longer take "marker name" from the user as a creation parameter.  Instead, we will keep a global variable called MarkerIndex and increment it for a unique value we can append to the name of each new marker.



MarkerIndex = 0;


 

Our initialization method for the new Marker class will take the remaining necessary parameters, and we will allow nil to be used in place of "type" or "size" depending on which kind of marker is being made (area or icon).



DEFCLASS("Marker") ["_self", "_position", "_shape", "_type", "_size"] DO {
/* Initialize marker parameters with the minimum required info */
SUPER("ObjectRoot", _self);
[_self, "_setf", "name",
format ["MarkerInstance%1", MarkerIndex]] call fnc_tell;
MarkerIndex = MarkerIndex + 1;
[_self, "_setf", "position", _position] call fnc_tell;
[_self, "_setf", "shape", _shape] call fnc_tell;
if (not isNil "_type") then {
[_self, "_setf", "type", _type] call fnc_tell;
};
if (not isNil "_size") then {
        [_self, "_setf", "size", _size] call fnc_tell;
};
        _self
} ENDCLASS;


 

With this, we can create new Marker instances at will with a call like:



//mkr = ["Marker", <position>, <shape>, <type>, <size>] call fnc_new;
mkr = ["Marker", position player, "ELLIPSE", nil, [10, 10]] call fnc_new;


 

Methods:

This marker will still not be visible - all we have done is configure the new instance.  We still need a "show" method to reveal the marker on the map.  This could be tacked onto the end of our initializer as well, but it needs to exist as a

separate method.



DEFMETHOD("Marker", "show") ["_self"] DO {
/* Create the marker using the loaded configuration */
private ["_marker", "_name", "_position", "_shape", "_type", "_size",
                 "_brush", "_color", "_direction", "_text", "_alpha"];
_name = [_self, "_getf", "name"] call fnc_tell;
_position = [_self, "_getf", "position"] call fnc_tell;
_marker = createMarker [_name, _position];
_shape = [_self, "_getf", "shape"] call fnc_tell;
_type = [_self, "_getf", "type"] call fnc_tell;
_size = [_self, "_getf", "size"] call fnc_tell;
_brush = [_self, "_getf", "brush"] call fnc_tell;
_color = [_self, "_getf", "color"] call fnc_tell;
_direction = [_self, "_getf", "direction"] call fnc_tell;
_text = [_self, "_getf", "text"] call fnc_tell;
_alpha = [_self, "_getf", "alpha"] call fnc_tell;
_marker setMarkerShape _shape;
if (not isNil "_type") then {
_marker setMarkerType _type;
};
if (not isNil "_size") then {
_marker setMarkerSize _size;
};
if (not isNil "_brush") then {
_marker setMarkerBrush _brush;
};
if (not isNil "_color") then {
_marker setMarkerColor _color;
};
if (not isNil "_direction") then {
_marker setMarkerDir _direction;
};
if (not isNil "_text") then {
_marker setMarkerText _text;
};
if (not isNil "_alpha") then {
_marker setMarkerAlpha _alpha;
};
[_self, "_setf", "marker", _marker] call fnc_tell;
} ENDMETHOD;


 

You can see our show method reads a lot more variables than what we configured in the initializer.  This lets us set more than the minimum required for a marker by setting the appropriate instance variables (with "_setf" calls).  More on that in a moment.

 

A call to



[mkr, "show"] call fnc_tell;


will now reveal the marker instance on the map.  In order to hide the marker again, we will use another method: "hide".



DEFMETHOD("Marker", "hide") ["_self"] DO {
/* Remove marker from the map */
deleteMarker ([_self, "_getf", "name"] call fnc_tell);
[_self, "_setf", "marker", nil] call fnc_tell;
} ENDMETHOD;


 

You may note that reconfiguring the marker instance variables using "_setf" will not have any effect on the existing marker on the map.  For that reason, we simply create a "redraw" method to call "hide" and "show" in succession whenever the marker is already visible.



DEFMETHOD("Marker", "redraw") ["_self"] DO {
/* If shown, hide and then recreate the marker */
private ["_marker"];
_marker = [_self, "_getf", "marker"] call fnc_tell;
if (not isNil "_marker") then {
[_self, "hide"] call fnc_tell;
[_self, "show"] call fnc_tell;
};
} ENDMETHOD;


 

Finally, we implement trivial methods to set each configuration variable and then call redraw.



DEFMETHOD("Marker", "set_alpha") ["_self", "_alpha"] DO {
/* Configure alpha (opacity) */
[_self, "_setf", "alpha", _alpha] call fnc_tell;
[_self, "redraw"] call fnc_tell;
} ENDMETHOD;
 
 
DEFMETHOD("Marker", "set_brush") ["_self", "_brush"] DO {
/* Configure brush (area fill type) */
[_self, "_setf", "brush", _brush] call fnc_tell;
[_self, "redraw"] call fnc_tell;
} ENDMETHOD;
 
 
DEFMETHOD("Marker", "set_color") ["_self", "_color"] DO {
/* Configure color (array or Cfg value) */
[_self, "_setf", "color", _color] call fnc_tell;
[_self, "redraw"] call fnc_tell;
} ENDMETHOD;
 
 
DEFMETHOD("Marker", "set_direction") ["_self", "_direction"] DO {
/* Set marker orientation (in degrees) */
[_self, "_setf", "direction", _direction] call fnc_tell;
[_self, "redraw"] call fnc_tell;
} ENDMETHOD;
 
 
DEFMETHOD("Marker", "set_position") ["_self", "_position"] DO {
/* Set marker position */
[_self, "_setf", "position", _position] call fnc_tell;
[_self, "redraw"] call fnc_tell;
} ENDMETHOD;
 
 
DEFMETHOD("Marker", "set_shape") ["_self", "_shape"] DO {
/* Set marker shape (i.e. "RECTANGLE", "ELLIPSE", "ICON") */
[_self, "_setf", "shape", _shape] call fnc_tell;
[_self, "redraw"] call fnc_tell;
} ENDMETHOD;
 
 
DEFMETHOD("Marker", "set_size") ["_self", "_size"] DO {
/* Set size [X x Y] */
[_self, "_setf", "size", _size] call fnc_tell;
[_self, "redraw"] call fnc_tell;
} ENDMETHOD;
 
 
DEFMETHOD("Marker", "set_text") ["_self", "_text"] DO {
/* Configure marker text */
[_self, "_setf", "text", _text] call fnc_tell;
[_self, "redraw"] call fnc_tell;
} ENDMETHOD;
 
 
DEFMETHOD("Marker", "set_type") ["_self", "_type"] DO {
/* Configure marker type (i.e. which icon) */
[_self, "_setf", "type", _type] call fnc_tell;
[_self, "redraw"] call fnc_tell;
} ENDMETHOD;


 

Using:

Combining all of the above together into classdef\Marker.hpp and adding the line #include <classdef\Marker.hpp> to our init.sqf (after including include\classes.hpp) gives us access to the new object interface, and we can use it as one might expect:



// Create and show a new marker:
mkr = ["Marker", position player, "RECTANGLE", nil, [5, 5]] call fnc_new;
[mkr, "show"] call fnc_tell;
 
// Reorient the marker:
[mkr, "set_direction", 45] call fnc_tell;
 
// Change marker color:
[mkr, "set_color", "ColorGreen"] call fnc_tell;
 
// Change marker size:
[mkr, "set_size", [10, 10]] call fnc_tell;
 
// Hide marker again:
[mkr, "hide"] call fnc_tell;


 

This is just one simple example of how to use object orientation with the SQF Classes module to facilitate simpler, more powerful options in the editor and during scenario design.  Markers were a prime candidate for this technique because of the overhead involved every time a marker is to be created, altered, or destroyed.  That overhead can be wrapped up into the class and its methods, and then ignored even as the class can be reused in different projects or even extended via inheritance and subclassing.

 

I hope this may interest you in trying to create your own classes and sharing them with the community, or at least implementing them in your own scenarios to make things easier on yourself!

 

Edited to fix a bug that shows up in certain cases when using nil as a parameter.

Share this post


Link to post
Share on other sites

I'd recommend you to join the arma discord; especially the scripters room is relevant here.

However be warned that some people are rather cynical about OO approaches due various reasons

(previous attempts, limits, performance, afraid of rewrite, and so on).

Still you could gain from the discussion and input there. And may improve adaption/interest.

If it were part of CBA, I would see better chances, yet I cant tell if such is within their scope these days.

In any case kudos for the effort, good presentation and docu!

Share this post


Link to post
Share on other sites

Thank you for the advice, and it is an honor to be recognized by such an illustrious forum member.  You make the discord sound just so inviting, heh, but I might stick my head in there as I am sure you're right that there is much to gain. 

 

I'm not too confident in any of this stuff yet as I'm the only one to my knowledge to be testing it and I never went past trivialities until after I released v1.0 of this class system.  I've been trying to break it, though, for the past few days, and surprisingly it has proven robust and a lot faster than I had expected considering the number of function calls going on at times.

 

I share some cynicism about OO too, and there is something to be said for avoiding it, but it is still the right tool for certain jobs and exactly the tool I was wanting in this case.  I had checked out a couple of other OO packages in SQF, but didn't see anything as high-level and dynamic (or, some might say, hackish and horrifically unsafe).... This is pretty much an abomination of an homage to Python, mixed with Lisp, slopped together like Christmas dinner leftovers.  But as far as I can tell, it works.

 

Thanks again; I still am not committing to anything more than a low profile and a disclaimer that this might not satisfy every use case for now.  But, as time permits, I am working on several systems using this class module, and will be sure to post more information as it is available.

 

I also put everything under the MIT license, and it may be painfully obvious that I did not follow the typical community naming convention of prefixing my initials to any function or variable names.  That is to emphasize how this is meant to be low-level, experimental, and open to all to make of it what you will.  I must leave it at this for now until I get some non-trivial applications going and test things more thoroughly.

  • Like 1

Share this post


Link to post
Share on other sites
Heh, though I am flattered, that is currently somewhat outside of my intended scope with this project.  Since all users of this system have to do is drop the source folders into their mission and add a couple of lines to the init.sqf, it should offer almost as smooth an experience as if it were integrated with an addon.  Since much of the system's power derives from creating one's own classes, users are expected to dig into their mission folder somewhat anyway, although typically this need not go much farther than simple drag'n'drop.

 

I've been working with this system for the last several days now and can report there will be at least one significant addition to the next release (it is already available on GitHub from the master branch's root): A version of fnc_tell that uses spawn instead of call to execute methods.  This returns a handle to the new execution thread and lets much more complicated methods run asynchronously. The new function is called fnc_tells and otherwise works the same way as fnc_tell.

 



 

This function was created to aid in a new module with which I've been experimenting as a test of the class system's robustness. That new system, composed primarily of classes, can be found on GitHub in the "opti/" folder and provides a basic framework for running multi-objective optimization algorithms.  Don't expect high-performance computation here, as not all the systems are well-optimized (or ever going to be extremely fast, honestly). You can, however, expect usable results.  More detailed documentation on this is to come later but it could definitely be worth taking a look at.  A quick and dirty example is provided that relies on the mkcivs module (also on GitHub) and optimizes positions around the player's group location for partial LOS and nearby civilians in the global array "civArray".

 

That example is illustrated here:


 

[An example mission and more will be coming soon]

 

 

EDIT: Ugh... starting to see how ArmA can do "interesting" things with its objects.  Fixed the Optimizer example - _this inside the spawn was not behaving the same way as the object it was supposed to represent by the end.  Not sure why because it seemed to work for the first loop of calls and then return an old version for the last call.  Using the actual object name instead fixed the problem in this case.  I regret nothing!

  • Like 1

Share this post


Link to post
Share on other sites

Please sign in to comment

You will be able to leave a comment after signing in



Sign In Now

×