Jump to content
Sign in to follow this  
Kydoimos

Creating Cleaner Looking Scripts

Recommended Posts

Hi all! Just a quick question, and I apologise in advance if it seems a little vague. Now, I've seen some very impressive scripts out there and they are splendidly laid out; so, I was wondering, how does one learn the etiquette behind the layout to scripting? For example, when we have our brackets ( parentheses (), square brackets [], and rounded ones {}), how should they be arranged? How are they generally indented? I've got the hang of adding notes to explain things, so that part I'm good with :D It might sound a bit of a silly question, but I want everything to be as clear as possible. Is there a programme that detects the commands and lays them out accordingly? Or is it just a question of looking at the scripts of others and learning from example?

Oh, and one last thing on the subject of neater, cleaner scripts. When spawning groups of objects, which need to be individually attached, is there a more efficient way to script the composition then something like the below? Creating compositions in this fashion can mean some pretty long-winded SQFs!

Thanks in advance, guys! ;)

_Grill_01 = "Metal_Pole_F" createVehicle position Sewer_Grill; 
_Grill_01 attachTo [sewer_Grill,[0.5,-1.85,0.25]]; 

_Grill_02 = "Metal_Pole_F" createVehicle position Sewer_Grill; 
_Grill_02 attachTo [sewer_Grill,[0.65,-1.85,0.25]]; 

_Grill_03 = "Metal_Pole_F" createVehicle position Sewer_Grill; 
_Grill_03 attachTo [sewer_Grill,[0.8,-1.85,0.25]]; 

_Grill_04 = "Metal_Pole_F" createVehicle position Sewer_Grill; 
_Grill_04 attachTo [sewer_Grill,[0.95,-1.85,0.25]]; 

_Grill_05 = "Metal_Pole_F" createVehicle position Sewer_Grill; 
_Grill_05 attachTo [sewer_Grill,[1.1,-1.85,0.215]]; 

_Grill_06 = "Metal_Pole_F" createVehicle position Sewer_Grill; 
_Grill_06 attachTo [sewer_Grill,[1.25,-1.85,0.1]]; 

_Grill_07 = "Metal_Pole_F" createVehicle position Sewer_Grill; 
_Grill_07 attachTo [sewer_Grill,[1.4,-1.85,0.025]]; 

_Grill_08 = "Metal_Pole_F" createVehicle position Sewer_Grill; 
_Grill_08 attachTo [sewer_Grill,[1.55,-1.85,-0.125]]; 

_Grill_1 = "Metal_Pole_F" createVehicle position Sewer_Grill; 
_Grill_1 attachTo [sewer_Grill,[0.35,-1.85,0.25]]; 

_Grill_2 = "Metal_Pole_F" createVehicle position Sewer_Grill; 
_Grill_2 attachTo [sewer_Grill,[0.2,-1.85,0.205]]; 

_Grill_3 = "Metal_Pole_F" createVehicle position Sewer_Grill; 
_Grill_3 attachTo [sewer_Grill,[0.05,-1.85,0.1]]; 

_Grill_4 = "Metal_Pole_F" createVehicle position Sewer_Grill; 
_Grill_4 attachTo [sewer_Grill,[-0.1,-1.85,-0.15]]; 

_Grill_5 = "Metal_Pole_F" createVehicle position Sewer_Grill; 
_Grill_5 attachTo [sewer_Grill,[-0.25,-1.85,-0.25]]; 

Share this post


Link to post
Share on other sites

It's really all personal preference, I prefer this style:

 if (stuff) then
{
   //code
};

Share this post


Link to post
Share on other sites

For the code you posted it could be done like so, assuming you didn't need the local variables afterwards.

for "_o" from 0 to 14 do {
   _Grill = "Metal_Pole_F" createVehicle position Sewer_Grill; 
   _Grill attachTo [sewer_Grill,[0.5 + 0.15*_o,-1.85,0.25]];
};

Edit: Though I just noticed that the attachTo offset isn't following a linear pattern, so that wouldn't be perfect.

In general, it's an author design decision. I'd say consistency is the most important thing.

Edited by SilentSpike

Share this post


Link to post
Share on other sites
How does one learn the etiquette behind the layout to scripting?

Mostly through pain and suffering. :fighting:

Having programmed in other languages (and having seen different styles) certainly helps to make up your mind with respect to sqf. Either way, rule number one is: pick something, then stick to it. Especially if you're working in a team. Coherence/uniformity is more important, then whether you like your opening brackets (or whatever) on the same line or not.

But if you're unsure, at least check out the links above, and "Indent style" in particular.

And then maybe google some more, this is always a super funny topic. :hammer:

I've got the hang of adding notes to explain things, so that part I'm good with :D

Notes? As in comments? :D

Very well. And again, same thing applies: a standard function header describing inputs, side-effects, and outputs goes a long way. Just make sure your comments are kept up-to-date too when you change your code. There is nothing worse than outdated comments that don't apply any longer. D:

Is there a programme that detects the commands and lays them out accordingly?

Yes. While this is not their original purpose, you often see Linters used to reformat code of all kinds, see: http://en.wikipedia.org/wiki/Lint_%28software%29. I think squint was able to reformat your code too, but as far as I'm aware of, that's no longer maintained.. Not sure what else exists for sqf. But I'd be surprised if there wouldn't be a suitable tool from Mikero to do the job.

But, you might not actually want your code being totally reformatted by some (more or less) stupid program.

There are other options to make sure to write clean code. Namely snippets! If you have no idea what snippets are, watch this video about vim/ultisnips: http://vimcasts.org/episodes/meet-ultisnips/. Any good editor should offer snippets in one way or the other (which you can write/define by yourself), btw.

Snippets not only make sure that you write consistent code (w.r.t. style), they're also comfortable as fuck (i.e. a tremendous speed-up). For-loops? While-loops? Foreach? If? Switch? A private statement? Whatever. You bet I have a snippet for it, tab, tab, tab,, done. Or another for function headers/comments; tab to description, tab to parameters, tab to return value, done) And so on. :cool:

Or is it just a question of looking at the scripts of others and learning from example?

Given how niche sqf is, most people that bother with such kind of things most likely migrated their habits from other languages like C, Java or what not.

Btw. have you ever extracted official BIS pbos like the function module? Doesn't look like they'd be enforcing some standard. Every author seems to do his own kind of thing when it comes to sqf. So, that's maybe not the best place to look for good examples...

...is there a more efficient way to script the composition then something like the below?

Yes, absolutely.

  1. Stop trying to give every object an individual name (even if it's only a "private" variable), and instead...
  2. populate/use lists/arrays instead (map/apply/reduce).

In this example you start out with a list of positions which you wanna transform (map) into another list of something else (here it's a list of metal poles).

_positions = [
  [0.5,-1.85,0.25],
  ...
];
_poles = []; // init an empty array to keep and refer to your poles

{
  private ["_pole"];
  _pole = "Metal_Pole_F" createVehicle position Sewer_Grill;
  _pole attachTo [sewer_Grill, _x];
  _poles pushBack _pole; // register new pole, same as: "_poles set [(count _poles), _pole]", but thread-safe!
} forEach _positions;

// ... 3 years later

{
  deleteVehicle _x; // or what not...
} forEach _poles;

The lesson here is probably to not repeat yourself.

If you happen to catch yourself copy-and-pasting some code (you didnt write those lines out for all the 8 or what not grills, did you? :p), then stop it right there and write a loop or a function or something. Often it can be convenient to write private functions inside the main function, especially if you consider that you can swap out/overwrite such private functions (on certain conditions), which could make your logic easier/the code cleaner...

Another clear sign of "you're doing it wrong" is if you start to attach numbers to your variables. If you have a set of similar objects, then put them in an array (i.e. a list!), where they are "numbered" for free by their indices. And from there you can leverage all the power of list-processing there is. Foreach et al. should be your very best friends. ;)

Lists are awesome to work with and reason about anyways, since you're only working with two very simple things:

  1. The current element in the list (to operate on), and
  2. the rest of the list (which might be empty, then you're done).

Maybe check out (the ideas behind) some functional languages, if you got some time to spare.

Edited by ruebe

Share this post


Link to post
Share on other sites

@All - thanks chaps! Very helpful!

@Ruebe - you legend! :D As Sttosin said above, great post! Really informative! Cheers - I really appreciate the time you've taken there - very kind of you! Thanks!

Share this post


Link to post
Share on other sites

@Ruebe - wow! My scripts are going to be so much faster now! Thanks matey! :D

Share this post


Link to post
Share on other sites

@ruebe - Just out of interest matey, say I wanted to slip in an extra command, something like setDir, which I wanted to execute, how would one implement that? Let us take the example of the poles you've given above. It works beautifully, but I'm a bit foggy on some of the magic that's happening :D I've grasped that the _X is working through the array and attaching each newly created pole, but what would I also like to do to get it to sort through a new array, setting an individual direction for each of the poles in order? This is some amazingly handy scripting advice, by the way, and I really appreciate the help! It'll cut the size of some of my composition scripts by about three thirds! Mass optimisation!

Share this post


Link to post
Share on other sites

You could make the positions array multidimensional:

_positions = [
  [[0.5,-1.85,0.25],   90], 
  [[0.2,-0.85,1.25],   180],
  ...
];
_poles = []; // init an empty array to keep and refer to your poles

{
  private ["_pole"];
  _pole = "Metal_Pole_F" createVehicle position Sewer_Grill;
  _pole attachTo [sewer_Grill, _x select 0];
  _pole setdir (_x select 1);
  _poles pushBack _pole; 
} forEach _positions;

Share this post


Link to post
Share on other sites

@Greenfist - gorgeous! Love it! I didn't think of that! Thanks man! :D

Share this post


Link to post
Share on other sites
I didn't think of that!

Better get used to it! Got a problem? Throw some more lists/arrays at it!

Bonus points for putting even more lists into your lists, hehe. :p

On a more serious note; once you start defining your own datastructures like in your latest example, you might wanna consider to use a bunch of macros to get rid of those magic numbers. Of course I'm speaking about the list/array indices.

So, now you got this:

_positions = [
  [[0.5,-1.85,0.25],   90], 
  [[0.2,-0.85,1.25],   180],
  ...
];

Your datastructure is an array of items, where an item is an array of a position (yet another array), and a direction (scalar).

For now, the position can be retireved by selecting the first element, and the direction is the second element in such an item. All by "convention". In case the potential problems with this aren't obvious: what happens when you latter wanna extend this datastructure by a third element, maybe some kind of offset or what not? You're bound to make it your third element - otherwise you'd need to change all your code; e.g. if you'd whish to place that new element at the second position, before the direction... This might sound a bit contrieved, but it's indeed often the case that one starts with something, and extends it as needed... and then you'd like to keep things somewhat nice and tidy (e.g. by having a nice ordering) - hence those indices gotta change, damn.

The second problem with having your code littered with _x select 0's and _x select 6's ..., is that this is not readable at all. Come back to your code a week or two later and you'll need to look up what the nth element in your custom datastructure is.. What's _x select 13 supposed to be? No friggin clue!

And that's exactly where a macro or two might come in handy:

#define __IDX_POS 0
#define __IDX_DIR 1

and then your forEach-loop reads like this:

{
  private ["_pole"];
  _pole = "Metal_Pole_F" createVehicle position Sewer_Grill;
  _pole attachTo [sewer_Grill, _x select __IDX_POS];
  _pole setdir (_x select __IDX_DIR);
  _poles pushBack _pole; 
} forEach _positions;

Much better! This is not only readable now, but if you come back later at some point to extend/mess with your datastructure, you can simply do:

#define __IDX_POS 0
#define __IDX_OFFSET 1
#define __IDX_DIR 2

...or whatever - without need to change any of your code!

Well, of course, if you hardcode that _positions array, you'd need to change that too, but that's it.

That said, this might still get messy in case the datastructure gets exposed, s.t. you need to manually access individual items outside of this file/script, since now you need to make sure that the macros are defined everywhere... A solution here would be to write a bunch of functions that do the dirty stuff, and keep this all in one file where the datastructure is defined. Then you can use these functions from anywhere, without having to worry about any macros (or indices).

And secondly: macros are evil, mmkay :happy:

Maybe another example helps. Have a look at the following implementation of a dictionary. First the datastructure is defined (including a macro for each index), and then the following functions always use these macros instead of hardcoded magic numbers. These functions are the only guys allowed to access to inards of the dict-datastructure, thus these macros can remain local to this file; so no mess.

scriptName "RUBE3\datastructures\ds_dict.sqf";
/*
   Author:
    rübe

   Description:
    Implementation of a dictionary (or associative array) by means of 
    indirect indexing, using an array for the keys, and a second for 
    the values.

    Complexity: Insertion O(1), lookup O(n), deletion O(n); keys are 
    found with the "find" command.

   Function overview:
    [default]                  call RUBE_dictCreate     => dictionary
    [dictionary, key, value]   call RUBE_dictSet        => void
    [dictionary, key]          call RUBE_dictGet        => value
    [dictionary, key]          call RUBE_dictRemove     => void
    [dictionary, code]         call RUBE_dictForEach    => void
    [dictionary]               call RUBE_dictSize       => integer
    [dictionary]		call RUBE_dictKeys	 => array
*/

/*
   dictionary data structure:
   [
       0: keys (array),
       1: values (array),
       2: default (any)
   ]
*/
#define __RUBE_DICT_KEYS 0
#define __RUBE_DICT_VALUES 1
#define __RUBE_DICT_DEFAULT 2


/*
   Description:
    Creates a new dictionary.

   Parameter(s):
    _this select 0: default value (any; optional)
                    Returned in case of a miss. 
                    Default = nil.

   Returns:
    dictionary (array)
*/
RUBE_dictCreate = {
   private ["_default"];
   _default = nil;
   if ((typeName _this) == "ARRAY") then
   {
       if ((count _this) > 0) then
       {
           _default = _this select 0;
       };
   };

   [
       [],
       [],
       if (isNil {_default}) then { nil } else { _default }
   ]
};

/*
   Description:
    Returns the size of the dictionary.

   Parameter(s):
    _this select 0: a dictionary (array)

   Returns:
    integer
*/
RUBE_dictSize = {
   private ["_dict"];
   _dict = _this select 0;

   (count (_dict select __RUBE_DICT_KEYS))
};

/*
   Description:
    Returns all existing keys in the dictionary.

   Parameter(s):
    _this select 0: a dictionary (array)

   Returns:
    array (of keys or empty)
*/
RUBE_dictKeys = {
   private ["_dict", "_keys"];
   _dict = _this select 0;
   // make a copy
   _keys = + (_dict select __RUBE_DICT_KEYS);
   _keys
};

/*
   Description:
    Inserts/sets (as in overwrites if key already exists) a new
    entry into the dictionary.

   Parameter(s):
    _this select 0: a dictionary (array)
    _this select 1: key (string)
    _this select 2: value (any)

   Returns:
    void
*/
RUBE_dictSet = {
   private ["_dict", "_key", "_value", "_idx"];
   _dict = _this select 0;
   _key = _this select 1;
   _value = _this select 2;

   _idx = (_dict select __RUBE_DICT_KEYS) find _key;

   if (_idx < 0) then
   {
       _idx = count (_dict select __RUBE_DICT_KEYS);
       (_dict select __RUBE_DICT_KEYS) set [_idx, _key];
       (_dict select __RUBE_DICT_VALUES) set [_idx, _value];
   } else {
       (_dict select __RUBE_DICT_VALUES) set [_idx, _value];
   };
};

/*
   Description:
    Returns the value of a dictionary entry. The dictionary's
    default (e.g. nil) is returned, if the key couldn't be found.

   Parameter(s):
    _this select 0: a dictionary (array)
    _this select 1: key (string)

   Returns:
    value (any)
*/
RUBE_dictGet = {
   private ["_dict", "_key", "_idx"];
   _dict = _this select 0;
   _key = _this select 1;

   _idx = (_dict select __RUBE_DICT_KEYS) find _key;

   if (_idx >= 0) exitWith
   {
       ((_dict select __RUBE_DICT_VALUES) select _idx)
   }; 

   if (isNil {_dict select __RUBE_DICT_DEFAULT}) exitWith
   {
       nil
   };

   (_dict select __RUBE_DICT_DEFAULT)
};

/*
   Description:
    Removes an entry from the dictionary.

   Parameter(s):
    _this select 0: a dictionary (array)
    _this select 1: key (string)

   Returns:
    void
*/
RUBE_dictRemove = {
   private ["_dict", "_key", "_idx", "_last"];
   _dict = _this select 0;
   _key = _this select 1;

   _idx = (_dict select __RUBE_DICT_KEYS) find _key;

   if (_idx >= 0) then
   {
       _last = (count (_dict select __RUBE_DICT_KEYS)) - 1;
       if (_last != _idx) then
       {
           (_dict select __RUBE_DICT_KEYS) set [
               _idx, 
               ((_dict select __RUBE_DICT_KEYS) select _last)
           ];
           (_dict select __RUBE_DICT_VALUES) set [
               _idx, 
               ((_dict select __RUBE_DICT_VALUES) select _last)
           ];
       };
       (_dict select __RUBE_DICT_KEYS) resize _last;
       (_dict select __RUBE_DICT_VALUES) resize _last;
   };
};

/*
   Description:
    Executes code for each entry in the dictionary.
    Do not modify the dictionary while traversal!

   Parameter(s):
    _this select 0: a dictionary (array)
    _this select 1: code (code)
                    Gets passed an array as _this where:
                     _this select 0: key (string)
                     _this select 1: value (any)

   Returns:
    void
*/
RUBE_dictForEach = {
   private ["_dict", "_code"];
   _dict = _this select 0;
   _code = _this select 1;

   for "_i" from 0 to ((count (_dict select __RUBE_DICT_KEYS)) - 1) do
   {
       [
           ((_dict select __RUBE_DICT_KEYS) select _i),
           ((_dict select __RUBE_DICT_VALUES) select _i)
       ] call _code;
   };
};

Granted, this code/datastructure probably wont change much in the future, but you get the idea (and you still get the benefits of readable code, so yay!).

Macros - if not overused - can make your life a lot easier.

However, it's a fine line, and then they will come back to haunt you and make your life a nightmare, so watch out! :www:

Edited by ruebe

Share this post


Link to post
Share on other sites

@Ruebe - thanks again, matey, for such a detailed overview! Makes sense! Cool stuff! :D I'm sure this will prove helpful to others too, not just me! Bookmarking the page? Yup, I think so!

Share this post


Link to post
Share on other sites

I wanted to share an example of macros in action from the CBA mod: https://github.com/acemod/ACE3/blob/master/tools/cba/addons/main/script_macros_common.hpp

tihi :sigh:

That's exactly the kind of macro-wizzerdry that I wouldn't really recommend to anyone (well sure, if you use CBA/ACE in your missions, then go ahead and "use" them! I'm talking about writing such stuff from scratch...). At this point/level you're pretty much defining a new language on top of sqf, which can get problematic rather quickly... especially if every derp starts to write his very own special snow-flake macro-language. :ok:

Don't get me wrong, the CBA/ACE guys surely know what they are doing. And they will certainly have good reasons for their decisions. And I guess that's the point I'm trying to make: you most likely don't. So don't try to blindly imitate their heavy macro-useage. But sure, go ahead the moment you actually do see a good reason to define yet another macro. Fair enough. But at this point nobody needs a recommendation for macros; either you know what the heck you're doing, or you probably should not write that macro. :cool:

Personally I try to avoid them as much as possible with the few exceptions of:

  • Macros limited to a single file, never included by some other file (i.e. I'm not inventing new "commands"/language constructs). Mostly to get rid of magic numbers. Simple stuff.
  • Dialogs. And again due to magic numbers. (too bad we can't just define/program dialogs and such stuff with sqf...)

But hey, whatever floats your boat.

:bb:

Edited by ruebe

Share this post


Link to post
Share on other sites

Yeah... I linked it for the shock effect. Like a macro Blitzkrieg.

I don't use macros myself in my SQF, except in Description.ext. However that link has a lot of interesting methods of implementing them, so I think it's worthwhile to look at, even if it's purely out of curiosity. At that same time ... it's quite massive. I remember looking at CBA a few years ago and going "wow" because it looked like macros with their own macros ... all the way down......

Share this post


Link to post
Share on other sites

Notepad++ is your friend and there is an SQF this plugin for it.

Helps me immensely.

Share this post


Link to post
Share on other sites

Wonder why nobody mentioned it already, but I certainly could not script (read: live) without it:

Poseidon

Too many features to mention them all, it absolutely rocks.

If you're even halfway serious about arma scripting it's the tool to go.

Lotta knowledge there ruebe, nice to see you're still around.

Cheers

Share this post


Link to post
Share on other sites

Not sure if that's been mentioned by Ruebe already (great stuff, just skimmed through it) but CfgFunctions is incredibly useful, especially in creating and maintaining a solid and standardised function library throughout several missions or a campaign. (Even if some complain it's slower than functions defined on the fly.)

Apart from that I can only think of a few basic principles I try to follow:

  • Need something more than once? Write a bloody function!
  • Several script snippets doing similar stuff just with slight differences? Put them into a function and parameterise the elements that could change. BIS_fnc_param allows to quickly set up default values without annoying manual checks.
  • Mind the scope! Use tabs or whitespaces to indicate the current scope of a code block.
  • Keep the number of global variables as low as possible as they could be overwritten from anywhere by accident. Better add more parameters to a script than accessing a global var.
  • Control structures (loops, switch, if-then, etc.) are your friends! You shall not fear the loop!
  • Strictly split data from functionality. (Again, parameters are your friends.) Rather set up configs to store some permanent hard-coded data than writing that directly into your scripts.
  • Print function output / local variables: BIS_fnc_error, BIS_fnc_log, etc.
  • Establish testing framework and functions (automatic unit tracking scripts, debug output, etc.).
  • Document your work while coding. Not too many comments, not too few (KISS). Delete ToDos after completion right away.
  • Write function headers with short description, parameter overview and maybe what other functions are needed to run this function. In smaller software systems I also like to comment which functions (or objects / classes in OOP) could call this particular function. Makes reverse engineering less of a pain. Obviously not applicable for larger projects.
  • Capsule functionality! Modular approaches are your friends! CfgFunctions even allows categories (read modules).
  • Information hiding!
  • Low coupling, high cohesion!
  • Read other people's code! Reverse engineer scripts!
  • Software Engineering techniques are your friends as well!
  • Use proper change management tools! Sourceforge for example, or subversion (Apache, Tortoise, etc.) Also simplifies projects involving multiple developers.

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
Sign in to follow this  

×