Scripting Example – Arma Reforger

From Bohemia Interactive Community
Jump to navigation Jump to search
m (Text replacement - "lang="cpp">" to "lang="C#">")
m (Text replacement - "[[OFPEC tags" to "[[Scripting Tags")
 
(11 intermediate revisions by 3 users not shown)
Line 4: Line 4:
For starters, we are aiming at "get the smallest value from the array" functionality.
For starters, we are aiming at "get the smallest value from the array" functionality.


== Create the Script ==
=== New File ===
* [[Arma Reforger:Resource Manager|Resource Manager]]: '''Resource Browser''' > "Create" button > Script
* [[Arma Reforger: Script Editor| Script Editor]]: {{Controls|RMB}} (on a directory in the Projects window) > "Add New Script..."
* [[Arma Reforger: Script Editor| Script Editor]]: {{Controls|Ctrl|N}} or Plugins > Script Wizard:
{{Feature|important|
'''Always''' prefix the filename and the classname with a [[Scripting Tags|modder tag]] (e.g {{hl|ABC_}}, {{hl|AR15_}}, etc) in order to avoid class conflicts:
* {{hl|'''TAG_'''MyFile.c}}
* {{hl|class '''TAG_'''MyFile}} / {{hl|class '''TAG_'''MyFile : ParentFile}}
Methods and values are not concerned by this.
}}
{{Feature|important|
Scripting modding can only happen in '''Modules''', defined in {{armaR}}'s {{hl|.gproj}} ({{hl|Enfusion Workbench -> Workbench -> Options -> Game Project -> Script Project Manager Settings > Modules}}):
{{{!}} class{{=}}"wikitable valign-top-row-2"
! Module
! core
! gameLib
! game
! workbench
! workbenchGame
{{!}}-
! Directories
{{!}}
* Core
{{!}}
* GameLib
{{!}}
* Game
* GameCode
{{!}}
* Workbench
{{!}}
* WorkbenchGame
{{!}}}
Scripts placed '''outside of those folders''' will be simply '''ignored!'''}}


== Get the Smallest Value ==
== Get the Smallest Value ==
Line 11: Line 52:
==== Creation ====
==== Creation ====
Let's begin by creating the object and its method that will host the code.
Let's begin by creating the object and its method that will host the code.
<syntaxhighlight lang="C#">
<enforce>
class TutorialExample
class TAG_TutorialExample
{
{
void Method()
void Method()
Line 18: Line 59:
}
}
}
}
</syntaxhighlight>
</enforce>
The method exists now. It is important to name objects, methods and variables properly, therefore it has to be more precise than "Method":
The method exists now. It is important to name objects, methods and variables properly, therefore it has to be more precise than "Method":
<syntaxhighlight lang="C#">
<enforce>
class TutorialExample
class TAG_TutorialExample
{
{
void GetMinValue()
void GetMinValue()
Line 27: Line 68:
}
}
}
}
</syntaxhighlight>
</enforce>


==== Parameters ====
==== Parameters ====
The method is empty, takes no parameters and returns nothing. Since we want to provide a list of numbers from which to extract a result, let's add a parameter:
The method is empty, takes no parameters and returns nothing. Since we want to provide a list of numbers from which to extract a result, let's add a parameter:
<syntaxhighlight lang="C#">
<enforce>
class TutorialExample
class TAG_TutorialExample
{
{
void GetMinValue(array<int> numbers)
void GetMinValue(array<int> numbers)
Line 38: Line 79:
}
}
}
}
</syntaxhighlight>
</enforce>
Here, one could pass a {{hl|null}} array to the method, which could create a NULL pointer exception on code run; as an author it is our task to cover such case. There are two ways to do such check:
Here, one could pass a {{hl|null}} array to the method, which could create a NULL pointer exception on code run; as an author it is our task to cover such case. There are two ways to do such check:
<syntaxhighlight lang="C#">
<enforce>
class TutorialExample
class TAG_TutorialExample
{
{
void GetMinValue(array<int> numbers) // the numbers array can be null
void GetMinValue(array<int> numbers) // the numbers array can be null
{
{
if (!numbers) // or if (numbers == null)
if (!numbers) // or if (numbers == null)
{
return; // a null numbers is intercepted and stops the code
return; // a null numbers is intercepted and stops the code
}
}
}
}
}
</syntaxhighlight>
</enforce>
<syntaxhighlight lang="C#">
<enforce>
class TutorialExample
class TAG_TutorialExample
{
{
void GetMinValue(notnull array<int> numbers) // the numbers array cannot be null
void GetMinValue(notnull array<int> numbers) // the numbers array cannot be null
Line 59: Line 98:
}
}
}
}
</syntaxhighlight>
</enforce>
The latter version would make the engine throw an exception before the method is actually called. This is the version with which we are going in this tutorial's scope.
The latter version would make the engine throw an exception before the method is actually called. This is the version with which we are going in this tutorial's scope.


==== Return Value ====
==== Return Value ====
The method is supposed to return the array's smallest value: it should then return a number.
The method is supposed to return the array's smallest value: it should then return a number.
<syntaxhighlight lang="C#">
<enforce>
class TutorialExample
class TAG_TutorialExample
{
{
int GetMinValue(notnull array<int> numbers)
int GetMinValue(notnull array<int> numbers)
Line 71: Line 110:
}
}
}
}
</syntaxhighlight>
</enforce>


{{Feature|informative|Some methods use an {{hl|out}} parameter to provide the result to an already-assigned variable for memory management purpose. This is not going to be the case here for the sake of clarity.}}
{{Feature|informative|Some methods use an {{hl|out}} parameter to provide the result to an already-assigned variable for memory management purpose. This is not going to be the case here for the sake of clarity.}}
Line 120: Line 159:
Following the earlier code conception, here is how it would translate to code:
Following the earlier code conception, here is how it would translate to code:


<syntaxhighlight lang="C#">
<enforce>
class TutorialExample
class TAG_TutorialExample
{
{
int GetMinValue(notnull array<int> numbers)
int GetMinValue(notnull array<int> numbers)
{
{
if (numbers.IsEmpty())
if (numbers.IsEmpty())
{
return 0;
return 0;
}


int result = numbers[0];
int result = numbers[0];
Line 135: Line 172:
{
{
if (number < result)
if (number < result)
{
result = number;
result = number;
}
}
}


Line 143: Line 178:
}
}
}
}
</syntaxhighlight>
</enforce>


=== Commenting ===
=== Commenting ===
Line 161: Line 196:
! Good Example
! Good Example
|- style="vertical-align: top"
|- style="vertical-align: top"
| <syntaxhighlight lang="C#">
| <enforce>
int i = 0; // initialises i to 0
int i = 0; // initialises i to 0
i++; // increments i by 1
i++; // increments i by 1
</syntaxhighlight>
</enforce>
| <syntaxhighlight lang="C#">
| <enforce>
// int i = 0;
// int i = 0;
// i++;
// i++;
Line 172: Line 207:
Print(i);
Print(i);
}
}
</syntaxhighlight>
</enforce>
|}
|}


Line 194: Line 229:
''result'' is set to the array's first value, but {{hl|foreach}} then goes and compares every array element to it, including itself
''result'' is set to the array's first value, but {{hl|foreach}} then goes and compares every array element to it, including itself


<syntaxhighlight lang="C#">
<enforce>
class TutorialExample
class TAG_TutorialExample
{
{
int GetMinValue(notnull array<int> numbers)
int GetMinValue(notnull array<int> numbers)
{
{
if (numbers.IsEmpty())
if (numbers.IsEmpty())
{
return 0;
return 0;
}


int result = numbers[0];
int result = numbers[0];
Line 209: Line 242:
{
{
if (numbers[i] < result) // and here (number → numbers[i])
if (numbers[i] < result) // and here (number → numbers[i])
{
result = numbers[i]; // and here (number → numbers[i])
result = numbers[i]; // and here (number → numbers[i])
}
}
}


Line 217: Line 248:
}
}
}
}
</syntaxhighlight>
</enforce>


So, are we done now?
So, are we done now?
Line 226: Line 257:
The {{hl|Count}} method is called on every {{hl|for}} loop; since the array is not changing its size between each loops, it is considered wasting resource.
The {{hl|Count}} method is called on every {{hl|for}} loop; since the array is not changing its size between each loops, it is considered wasting resource.


<syntaxhighlight lang="C#">
<enforce>
class TutorialExample
class TAG_TutorialExample
{
{
int GetMinValue(notnull array<int> numbers)
int GetMinValue(notnull array<int> numbers)
{
{
if (numbers.IsEmpty())
if (numbers.IsEmpty())
{
return 0;
return 0;
}


int result = numbers[0];
int result = numbers[0];
Line 241: Line 270:
{
{
if (numbers[i] < result)
if (numbers[i] < result)
{
result = numbers[i];
result = numbers[i];
}
}
}


Line 249: Line 276:
}
}
}
}
</syntaxhighlight>
</enforce>
 
But: technically, {{hl|foreach}} is still faster.
 
<enforce>
class TAG_TutorialExample
{
int GetMinValue(notnull array<int> numbers)
{
if (numbers.IsEmpty())
return 0;
 
int result = numbers[0];
 
foreach (int i, int number : numbers) // we use foreach's index
{
if (i > 0 && number < result) // here - one bool check is faster than two results[] .Get method call
result = number;
}
 
return result;
}
}
</enforce>
 
 
The code flow is now optimal, and there is nothing more to be done.
The code flow is now optimal, and there is nothing more to be done.


Except perhaps an {{hl|int.MIN}} check, but… this would be overkill and not worth it!
Except perhaps an {{hl|int.MIN}} check, but… this would be overkill and not worth it!
An optimisation should be a balance between time spent and performance gained.
An optimisation should be a balance between time spent and performance gained.
Something 50% optimal but working will always be better than something 98% optimal but unreleased!
Something 50% optimal but working will always be better than something 98% optimal but unreleased!


{{Feature|informative|For further performance advices, see [[Arma Reforger:Scripting Performance|Scripting Performance]].}}
{{Feature|informative|For further performance advices, see [[Arma Reforger:Scripting Performance|Scripting Performance]].}}


== Get the Average Value ==
== Get the Average Value ==
Line 265: Line 318:
In this section the {{hl|GetMinValue}} method code above will be reused.
In this section the {{hl|GetMinValue}} method code above will be reused.


<syntaxhighlight lang="C#">
<enforce>
class TutorialExample
class TAG_TutorialExample
{
{
int GetAverageValue(notnull array<int> numbers)
int GetAverageValue(notnull array<int> numbers)
Line 272: Line 325:
}
}
}
}
</syntaxhighlight>
</enforce>


But an average is most likely a '''floating point''' number, so {{hl|float}} will be used here.
But an average is most likely a '''floating point''' number, so {{hl|float}} will be used here.


<syntaxhighlight lang="C#">
<enforce>
class TutorialExample
class TAG_TutorialExample
{
{
float GetAverageValue(notnull array<int> numbers)
float GetAverageValue(notnull array<int> numbers)
Line 283: Line 336:
}
}
}
}
</syntaxhighlight>
</enforce>


=== Code Setup ===
=== Code Setup ===
Line 311: Line 364:
Here is the code translation:
Here is the code translation:


<syntaxhighlight lang="C#">
<enforce>
class TutorialExample
class TAG_TutorialExample
{
{
float GetAverageValue(notnull array<int> numbers)
float GetAverageValue(notnull array<int> numbers)
{
{
if (numbers.IsEmpty())
if (numbers.IsEmpty())
{
return 0;
return 0;
}
   
   
int sum = 0;
int sum = 0;
Line 331: Line 382:
}
}
}
}
</syntaxhighlight>
</enforce>


=== Testing ===
=== Testing ===
Line 337: Line 388:
For demonstration purpose, both methods will be setup inside the {{hl|TutorialExample}} class:
For demonstration purpose, both methods will be setup inside the {{hl|TutorialExample}} class:


<syntaxhighlight lang="C#">
<enforce>
class TutorialExample
class TAG_TutorialExample
{
{
int GetMinValue(notnull array<int> numbers)
int GetMinValue(notnull array<int> numbers)
Line 344: Line 395:
if (numbers.IsEmpty())
if (numbers.IsEmpty())
return 0;
return 0;
 
int result = numbers[0];
int result = numbers[0];
 
for (int i = 1, count = numbers.Count(); i < count; i++)
foreach (int i, int number : numbers)
{
{
if (numbers[i] < result)
if (i > 0 && number < result)
{
result = number;
result = numbers[i];
}
}
}


Line 373: Line 422:
}
}
}
}
</syntaxhighlight>
</enforce>


== Debug Console ==
== Debug(Remote) Console ==


The following code can be used in the Debug Console to try and see the result of previous methods - be sure to set the Console's running environment to "Game":
The following code can be used in the Debug(Remote) Console to try and see the result of previous methods - be sure to set the Console's running environment to "Game":


<syntaxhighlight lang="C#">
<enforce>
array<int> values = { 18, 10, 5, 4, 5, 10, 33, 42, 666 }; // setup any wanted value here
array<int> values = { 18, 10, 5, 4, 5, 10, 33, 42, 666 }; // setup any wanted value here
TutorialExample tutorialExample = new TutorialExample();
TutorialExample tutorialExample = new TutorialExample();
Line 385: Line 434:
float avgValue = tutorialExample.GetAverageValue(values);
float avgValue = tutorialExample.GetAverageValue(values);
PrintFormat("Array %1: min value is %2, average is %3", values, minValue, avgValue);
PrintFormat("Array %1: min value is %2, average is %3", values, minValue, avgValue);
</syntaxhighlight>
</enforce>


{{Feature|informative|
{{Feature|informative|

Latest revision as of 12:01, 2 October 2024

This tutorial aims to provide scripting logic basics. The abstract project is to find certain values (smallest, biggest, average, etc) from a list of numbers.

For starters, we are aiming at "get the smallest value from the array" functionality.


Create the Script

New File

Always prefix the filename and the classname with a modder tag (e.g ABC_, AR15_, etc) in order to avoid class conflicts:
  • TAG_MyFile.c
  • class TAG_MyFile / class TAG_MyFile : ParentFile
Methods and values are not concerned by this.
Scripting modding can only happen in Modules, defined in Arma Reforger's .gproj (Enfusion Workbench -> Workbench -> Options -> Game Project -> Script Project Manager Settings > Modules):
Module core gameLib game workbench workbenchGame
Directories
  • Core
  • GameLib
  • Game
  • GameCode
  • Workbench
  • WorkbenchGame
Scripts placed outside of those folders will be simply ignored!

Get the Smallest Value

Method Setup

Creation

Let's begin by creating the object and its method that will host the code.

class TAG_TutorialExample { void Method() { } }

The method exists now. It is important to name objects, methods and variables properly, therefore it has to be more precise than "Method":

class TAG_TutorialExample { void GetMinValue() { } }

Parameters

The method is empty, takes no parameters and returns nothing. Since we want to provide a list of numbers from which to extract a result, let's add a parameter:

class TAG_TutorialExample { void GetMinValue(array<int> numbers) { } }

Here, one could pass a null array to the method, which could create a NULL pointer exception on code run; as an author it is our task to cover such case. There are two ways to do such check:

class TAG_TutorialExample { void GetMinValue(array<int> numbers) // the numbers array can be null { if (!numbers) // or if (numbers == null) return; // a null numbers is intercepted and stops the code } }
class TAG_TutorialExample { void GetMinValue(notnull array<int> numbers) // the numbers array cannot be null { } }

The latter version would make the engine throw an exception before the method is actually called. This is the version with which we are going in this tutorial's scope.

Return Value

The method is supposed to return the array's smallest value: it should then return a number.

class TAG_TutorialExample { int GetMinValue(notnull array<int> numbers) { } }

Some methods use an out parameter to provide the result to an already-assigned variable for memory management purpose. This is not going to be the case here for the sake of clarity.

Code Setup

Conception

The goal here, as decided earlier, is to get the smallest value from the provided array of numbers. To begin, let's start with writing human code before writing machine code, to lay the logic properly:

  • have a "current best result" value
  • read each element of the list
    • if an element is a better candidate (smaller) than the stored one, use this element instead
  • when the end of the list is reached, return the best value

This is fine. But what happens if the list has no values? In that case, we must decide for a default value. Depending on the method's role, default values can change:

  • methods will return -1 to indicate an error or a "not found"
  • methods will return 0 to state "no value = default value"

We are going for the latter here as we are returning a value.

Note that this method could be returning the index of the best candidate's element, and returning -1 (an invalid index) would be a perfectly acceptable answer.

The end logic:

  • have a "current best result" value initialised to zero
  • if the list is empty, return the current best value
  • read each element of the list
    • if an element is a better candidate (smaller) than the stored one, use this element instead
  • when the end of the list is reached, return the best value

But!

What if the array has only one element that is 10? The return value would be 0 here, which is an invalid result. Back to the drawing board:

  • if the list is empty, return 0
  • have a "current best result" initialised to the array first element's value
  • read each element of the list
    • if an element is a better candidate (smaller) than the stored one, use this element instead
  • when the end of the list is reached, return the best value

No more pitfalls in sight, code can be started!

Writing

Following the earlier code conception, here is how it would translate to code:

class TAG_TutorialExample { int GetMinValue(notnull array<int> numbers) { if (numbers.IsEmpty()) return 0; int result = numbers[0]; foreach (int number : numbers) { if (number < result) result = number; } return result; } }

Commenting

One can note that the above code does not contain any comments.

In the past, commenting has been taught to be extensive, to cover the maximum number of lines of code; on the other hand, some people consider commenting as the last resort.

The balance is somewhere in-between, somewhat closer to the latter:

  • a comment should explain why the code is written this way
  • a comment should not tell what the code does; code should be self-explanatory
  • as a last resort in the event of a complex piece of code, a comment can be used to describe what the code actually does - or at least its intention
Bad Example Good Example
int i = 0; // initialises i to 0 i++; // increments i by 1
// int i = 0; // i++; for (int i = 1; i < 10; i++) // starts from 1 to not have 0-based index miscalculation { Print(i); }

Optimisation

«
« Premature optimization is the root of all evil. » – Donald Knuth

The basic rules of coding are as follow:

  • Make it work
  • Make it readable
  • Optimise then

Once the code works, optimisation can be accessed.

What is wrong with this code; it takes the wanted arguments, it returns the expected result, it is fast?
Almost nothing. If we want to care about performance impact, go "straight to the point", avoid useless operations (thus saving cycles):

Remaining issue:

result is set to the array's first value, but foreach then goes and compares every array element to it, including itself

class TAG_TutorialExample { int GetMinValue(notnull array<int> numbers) { if (numbers.IsEmpty()) return 0; int result = numbers[0]; for (int i = 1; i < numbers.Count(); i++) // change has been made here { if (numbers[i] < result) // and here (number → numbers[i]) result = numbers[i]; // and here (number → numbers[i]) } return result; } }

So, are we done now?

No!

As improvement has been done, another loss has been introduced in the process: numbers.Count().
The Count method is called on every for loop; since the array is not changing its size between each loops, it is considered wasting resource.

class TAG_TutorialExample { int GetMinValue(notnull array<int> numbers) { if (numbers.IsEmpty()) return 0; int result = numbers[0]; for (int i = 1, count = numbers.Count(); i < count; i++) // count is defined once { if (numbers[i] < result) result = numbers[i]; } return result; } }

But: technically, foreach is still faster.

class TAG_TutorialExample { int GetMinValue(notnull array<int> numbers) { if (numbers.IsEmpty()) return 0; int result = numbers[0]; foreach (int i, int number : numbers) // we use foreach's index { if (i > 0 && number < result) // here - one bool check is faster than two results[] .Get method call result = number; } return result; } }


The code flow is now optimal, and there is nothing more to be done.

Except perhaps an int.MIN check, but… this would be overkill and not worth it!

An optimisation should be a balance between time spent and performance gained.

Something 50% optimal but working will always be better than something 98% optimal but unreleased!

For further performance advices, see Scripting Performance.

Get the Average Value

Method Setup

In this section the GetMinValue method code above will be reused.

class TAG_TutorialExample { int GetAverageValue(notnull array<int> numbers) { } }

But an average is most likely a floating point number, so float will be used here.

class TAG_TutorialExample { float GetAverageValue(notnull array<int> numbers) { } }

Code Setup

Conception

  • have a "sum" variable set to zero
  • read each element of the list
  • add each element to the sum variable
  • when the end of the list is reached, return the sum divided by the array size

But!

What if the array has no elements? "sum" would remain a valid value (zero), but then the division could be by zero, which is a forbidden operation in computer science.

Every time a division is used, the reflex must be to check if the divisor (the right-hand division element) value can be zero, as this is a crash reason.
  • if the list is empty, return 0
  • have a "sum" variable set to zero
  • read each element of the list
    • add each element to the sum variable
  • when the end of the list is reached, return the sum divided by the array size

Code writing can be started.

Writing

Here is the code translation:

class TAG_TutorialExample { float GetAverageValue(notnull array<int> numbers) { if (numbers.IsEmpty()) return 0; int sum = 0; foreach (int number : numbers) { sum += number; } return sum / numbers.Count(); // numbers.Count() cannot be zero here as it is checked line 5 } }

Testing

For demonstration purpose, both methods will be setup inside the TutorialExample class:

class TAG_TutorialExample { int GetMinValue(notnull array<int> numbers) { if (numbers.IsEmpty()) return 0; int result = numbers[0]; foreach (int i, int number : numbers) { if (i > 0 && number < result) result = number; } return result; } float GetAverageValue(notnull array<int> numbers) { if (numbers.IsEmpty()) return 0; int sum = 0; foreach (int number : numbers) { sum += number; } return sum / numbers.Count(); } }

Debug(Remote) Console

The following code can be used in the Debug(Remote) Console to try and see the result of previous methods - be sure to set the Console's running environment to "Game":

array<int> values = { 18, 10, 5, 4, 5, 10, 33, 42, 666 }; // setup any wanted value here TutorialExample tutorialExample = new TutorialExample(); int minValue = tutorialExample.GetMinValue(values); float avgValue = tutorialExample.GetAverageValue(values); PrintFormat("Array %1: min value is %2, average is %3", values, minValue, avgValue);

Note that for the debug code to run, it is important to have the Console set to "Game" and either:
  • have the game running in World Editor and press "Run"
  • press "Run" then compile (if no World Editor is running)


The code logs to the Output window something like this:

Array 0x000001C150656B48 {18,10,5,4,5,10,33,42,666}: min value is 4, average is 88.1111