Scripting: Best Practices – Arma Reforger
Lou Montana (talk | contribs) m (1 revision imported) |
Lou Montana (talk | contribs) m (Text replacement - "[[OFPEC tags" to "[[Scripting Tags") |
||
(13 intermediate revisions by the same user not shown) | |||
Line 1: | Line 1: | ||
{{TOC|side}} | {{TOC|side}} | ||
== Getting | == Getting Started == | ||
In the domain of development, any rule is a rule of thumb. If a rule states for example that it is better that a line of code doesn't go over 80 characters, it doesn't mean that any line '''''must not''''' go over 80 characters; sometimes, the situation needs it. | In the domain of development, any rule is a rule of thumb. | ||
If a rule states for example that it is better that a line of code doesn't go over 80 characters, it doesn't mean that any line '''''must not''''' go over 80 characters; sometimes, the situation needs it. | |||
If the code has a good structure, '''do not''' change it to enforce a single arbitrary rule. If many of them are not implemented/not respected, changes should be applied; again, this is according to one's judgement. | If the code has a good structure, '''do not''' change it to enforce a single arbitrary rule. If many of them are not implemented/not respected, changes should be applied; again, this is according to one's judgement. | ||
Line 9: | Line 10: | ||
== Best | == Best Practices == | ||
{{Feature|informative|See {{Link|Arma Reforger:Scripting: Conventions|Scripting Conventions}} for all the conventions to date.}} | |||
See Scripting | === Code Format === | ||
* '''Reminder:''' chosen indentation for Enfusion is {{ | * '''Reminder:''' chosen indentation for Enfusion is {{Link|https://en.wikipedia.org/wiki/Indentation_style#Allman_style|Allman style}} | ||
* '''Reminder:''' indentation is done with '''tabulations''' | * '''Reminder:''' indentation is done with '''tabulations''' | ||
* Use empty space. Line return, spaces before and after brackets, if this improves readability, use it: space is free | * Use empty space. Line return, spaces before and after brackets, if this improves readability, use it: space is free | ||
Line 21: | Line 22: | ||
** it also hinders debugger's usage, e.g in the event of an inlined {{hl|if}} | ** it also hinders debugger's usage, e.g in the event of an inlined {{hl|if}} | ||
=== Variable | === Variable Format === | ||
* Name variables and functions properly: code must be readable by a human being, e.g variables like '''u''' instead of '''uniform''' should not exist. | * Name variables and functions properly: code must be readable by a human being, e.g variables like '''u''' instead of '''uniform''' should not exist. | ||
** '''i''' is an accepted iteration variable name (e.g in {{hl|for}} loops). | ** '''{{hl|i}}''' is an accepted iteration variable name (e.g in {{hl|for}} loops). | ||
* Prefix any public content (classes, global methods, global variables) with a [[ | * Prefix any public content (classes, global methods, global variables) with a [[Scripting Tags|Creator Tag]] in order to prevent conflicts with other mods. | ||
* Use the closest value type whenever possible; using {{hl|auto}} for a known variable type makes code | * Use the closest value type whenever possible; using {{hl|auto}} for a known variable type makes code more obscure and prevents autocompletion. | ||
=== Code Structuration === | === Code Structuration === | ||
Line 36: | Line 34: | ||
A series of development principles to follow in order to ensure an easy code maintenance and lifetime. | A series of development principles to follow in order to ensure an easy code maintenance and lifetime. | ||
{{Feature|informative|See {{ | {{Feature|informative|See {{Link|https://en.wikipedia.org/wiki/SOLID}}.}} | ||
==== DRY ==== | ==== DRY ==== | ||
'''D'''on't '''R'''epeat '''Y'''ourself. If within the same class, the same code or the same pattern is written in various places, write a protected method and use appropriate parameters. | '''D'''on't '''R'''epeat '''Y'''ourself. If within the same class, the same code or the same pattern is written in various places, write a protected method and use appropriate parameters. | ||
{{Feature|informative|See {{ | {{Feature|informative|See {{Link|https://en.wikipedia.org/wiki/Don't_repeat_yourself}}.}} | ||
==== Logical Simplifications ==== | ==== Logical Simplifications ==== | ||
Line 48: | Line 46: | ||
==== Examples ==== | ==== Examples ==== | ||
{| class="wikitable" | {| class="wikitable valign-top" | ||
! Improvable | ! Improvable | ||
! Good | ! Good | ||
|- | |||
| < | |- | ||
| <enforce> | |||
auto number = 42; | auto number = 42; | ||
Animal cutePet = new Dog(); | Animal cutePet = new Dog(); | ||
</ | </enforce> | ||
| < | | <enforce> | ||
int number = 42; | int number = 42; | ||
Dog cutePet = new Dog(); | Dog cutePet = new Dog(); | ||
</ | </enforce> | ||
|- | |||
| < | |- | ||
for (int i = | | <enforce> | ||
int i = 0; | |||
string result = ""; | |||
SCR_MyClass obj = null; | |||
</enforce> | |||
| <enforce> | |||
int i; // default value = 0 - see {{Link|Arma Reforger:Scripting: Values#Integer|Values - integer}} | |||
string result; // default value = "" (a string cannot be null) | |||
SCR_MyClass obj; // default value = null | |||
</enforce> | |||
|- | |||
| <enforce> | |||
// a method call is more expensive than a bool check | |||
if (obj.MustBeTreated() || m_bTreatAllObjects) | |||
Print(obj); | |||
if (obj.MustBeTreated() && m_bTreatAllObjects) | |||
Print(obj); | |||
</enforce> | |||
| <enforce> | |||
// cheap checks go first, expensive checks (method calls) go after | |||
if (m_bTreatAllObjects || obj.MustBeTreated()) | |||
Print(obj); | |||
if (m_bTreatAllObjects && obj.MustBeTreated()) | |||
Print(obj); | |||
</enforce> | |||
|- | |||
| <enforce> | |||
// many identical method calls | |||
if (obj.MustBeTreated() && obj.GetObject()) | |||
Print("Result: " + obj.GetObject().m_sValue1 + " " + obj.GetObject().m_sValue2); | |||
</enforce> | |||
| <enforce> | |||
// "bigger", non-repetitive code can be beneficial for performance and readability | |||
if (obj.MustBeTreated()) | |||
{ | |||
SCR_Object subObj = obj.GetObject(); | |||
if (subObj) | |||
Print("Result: " + subObj.m_sValue1 + " " + subObj.m_sValue2); | |||
} | |||
</enforce> | |||
|- | |||
| <enforce> | |||
foreach (SCR_Object obj : list) | |||
{ | |||
Method(obj); // one method call per iteration | |||
} | |||
void Method(SCR_Object obj) | |||
{ | |||
if (!obj) | |||
return; | |||
Print(obj.m_sName + " has a value of " + obj.m_sValue); | |||
} | |||
</enforce> | |||
| <enforce> | |||
foreach (SCR_Object obj : list) // the least method calls, the better | |||
{ | |||
if (!obj) | |||
continue; | |||
Print(obj.m_sName + " has a value of " + obj.m_sValue); | |||
} | |||
</enforce> | |||
|- | |||
| <enforce> | |||
bool IsObjectAlive(SCR_Object obj) | |||
{ | |||
if (!obj) | |||
return false; | |||
if (obj.m_Health > 0) // keep this structure for complex code | |||
return true; | |||
else | |||
return false; | |||
} | |||
</enforce> | |||
| <enforce> | |||
bool IsObjectAlive(SCR_Object obj) | |||
{ | |||
return obj && obj.m_Health > 0; | |||
} | |||
</enforce> | |||
|- | |||
| <enforce> | |||
bool IsObjectValid(SCR_Object obj) | |||
{ | |||
if (!obj) | |||
return false; | |||
return true; | |||
} | |||
</enforce> | |||
| <enforce> | |||
bool IsObjectValid(SCR_Object obj) | |||
{ | |||
return obj != null; // for readability | |||
} | |||
</enforce> | |||
|- | |||
| <enforce> | |||
for (int i; i < list.Count(); i++) // list.Count() is called on every iteration | |||
{ | |||
// ... | |||
} | |||
</enforce> | |||
| <enforce> | |||
for (int i, count = list.Count(); i < count; i++) // only one list.Count() call | |||
{ | |||
// ... | |||
} | |||
</enforce> | |||
|- | |||
| <enforce> | |||
for (int i, count = list.Count(); i < count; i++) | |||
{ | |||
if (list[i]) // first | |||
Print(list[i]); // and second .Get(i) method calls | |||
} | |||
</enforce> | |||
| <enforce> | |||
foreach (SCR_Object obj : list) // foreach is faster for start-to-end iterating | |||
{ | |||
if (obj) | |||
Print(obj); // no additional method call | |||
} | |||
</enforce> | |||
|- | |||
| <enforce> | |||
for (int i, count = list.Count(); i < count; i++) | |||
{ | |||
PrintFormat("Object #%1 = %2", i, list[i]); | |||
} | |||
</enforce> | |||
| <enforce> | |||
foreach (int i, SCR_Object obj : list) // iteration index is available this way too | |||
{ | |||
PrintFormat("Object #%1 = %2", i, obj); | |||
} | |||
</enforce> | |||
|- | |||
| <enforce> | |||
// declaring an 'obj' every loop generates a pointer release each time | |||
foreach (SCR_ParentObject parent : list) | |||
{ | |||
SCR_Object obj = parent.m_Object; | |||
if (obj) | |||
Print(obj.m_sName); | |||
} | |||
</enforce> | |||
| <enforce> | |||
SCR_Object obj; // external declaration = only one release at the end of the scope | |||
foreach (SCR_ParentObject parent : list) | |||
{ | { | ||
obj = parent.m_Object; | |||
if (obj) | |||
Print(obj.m_sName); | |||
} | } | ||
</ | </enforce> | ||
| < | |||
|- | |||
| <enforce> | |||
array<SCR_Object> toRemove = {}; | |||
foreach (SCR_Object obj : bigArray) | |||
{ | { | ||
if (obj.m_bShouldBeRemoved) | |||
toRemove.Insert(obj); | |||
} | } | ||
foreach (SCR_Object obj : toRemove) | |||
{ | { | ||
bigArray.RemoveItem(obj); // or RemoveItemOrdered if order is important | |||
} | } | ||
</ | </enforce> | ||
| < | | <enforce> | ||
for (int i = bigArray.Count() - 1; i >= 0; i--) // reverse iterating | |||
for (int i = | |||
{ | { | ||
if (bigArray[i].m_bShouldBeRemoved) | |||
bigArray.Remove(i); // or RemoveItemOrdered if order is important | |||
} | } | ||
</ | </enforce> | ||
< | |- | ||
| <enforce> | |||
if (a) | |||
{ | { | ||
if (b) | |||
{ | |||
if (c) // also known as Hadouken code | |||
Method(true); | |||
else | |||
Method(false); | |||
} | |||
} | } | ||
</ | </enforce> | ||
|- | | <enforce> | ||
| < | if (a && b) | ||
Method(c); | |||
</enforce> | |||
|- | |||
| <enforce> | |||
if (a) | if (a) | ||
{ | { | ||
Method(a); | |||
if (b) | if (b) | ||
{ | { | ||
if (c) | Method(b); | ||
if (c) // another Hadouken code, with complications | |||
{ | { | ||
Method( | Method(c); | ||
return 42; | |||
} | } | ||
else | else | ||
{ | { | ||
return -1; | |||
} | } | ||
} | |||
else | |||
{ | |||
return -1; | |||
} | } | ||
} | } | ||
else | |||
{ | { | ||
return -1; | |||
} | } | ||
</ | </enforce> | ||
|- | | <enforce> | ||
| | if (!a) | ||
< | return -1; // this is called early return and helps funnel down the code | ||
int i | |||
Method(a); | |||
if (!b) | |||
return -1; | |||
Method(b); | |||
if (!c) | |||
return -1; | |||
Method(c); | |||
return 42; | |||
</enforce> | |||
|- | |||
| <enforce> | |||
int i; | |||
if (a) | if (a) | ||
i++; | i++; | ||
if (b) | if (b) | ||
i++; | i++; | ||
if (c) | if (c) | ||
i++; | i++; | ||
</ | // etc | ||
| | </enforce> | ||
< | | <enforce> | ||
int i | int i; | ||
array<bool> conditions = { a, b, c }; | array<bool> conditions = { a, b, c, /* etc */ }; | ||
foreach (bool condition : conditions) | foreach (bool condition : conditions) | ||
{ | { | ||
if (condition) | if (condition) | ||
i++; | i++; | ||
} | } | ||
</ | </enforce> | ||
|- | |||
| <enforce> | |||
|- | |||
| < | |||
Initialise(player1, 1); | Initialise(player1, 1); | ||
Initialise(player2, 2); | Initialise(player2, 2); | ||
Line 170: | Line 348: | ||
Initialise(player5, 5); | Initialise(player5, 5); | ||
Initialise(player6, 6); | Initialise(player6, 6); | ||
</ | </enforce> | ||
| < | | <enforce> | ||
array<IEntity> list = { player1, player2, player3, player4, player5, player6 }; | array<IEntity> list = { player1, player2, player3, player4, player5, player6 }; | ||
foreach (int i, item : list) | foreach (int i, IEntity item : list) | ||
{ | { | ||
Initialise(item, i + 1); | Initialise(item, i + 1); | ||
} | } | ||
</ | </enforce> | ||
<enforce> | |||
// or, better, one method call that initialises all of them | |||
Initialise(list); // numbering is then done inside the method, if possible | |||
Initialise(list, 1); // otherwise the starting number can be provided | |||
</enforce> | |||
|} | |} | ||
==== Code Comments ==== | ==== Code Comments ==== | ||
Code comments are surprisingly '''not''' a must-have; code organisation combined to variable names should be enough to be read by a human, '''then''' comment can be used: | Code comments are surprisingly '''not''' a must-have for inside code; code organisation combined to variable names should be enough to be read by a human, '''then''' comment can be used: | ||
* 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 | |||
On the other hand, ''documentation'' is more than welcome as it provides information from the outside without having to read the code. Enfusion uses {{Link|Doxygen}}. | |||
==== Files | ==== Files Organisation ==== | ||
{{Feature|informative|See | {{Feature|informative|See {{Link|Arma Reforger:Directory Structure}} to know how/where to organise script files (Scripts\GameCode).}} | ||
* Have one class/enum per file | * Have one class/enum per file | ||
** Small classes/enums can always be grouped together in the same file, provided they are part of the same system or only used there | ** Small classes/enums can always be grouped together in the same file, provided they are part of the same system or only used there | ||
* Use (sub-)directories to group related classes | * Use (sub-)directories to group related classes together | ||
{{GameCategory|armaR|Modding| | {{GameCategory|armaR|Modding|Guidelines|Scripting}} |
Latest revision as of 12:01, 2 October 2024
Getting Started
In the domain of development, any rule is a rule of thumb. If a rule states for example that it is better that a line of code doesn't go over 80 characters, it doesn't mean that any line must not go over 80 characters; sometimes, the situation needs it.
If the code has a good structure, do not change it to enforce a single arbitrary rule. If many of them are not implemented/not respected, changes should be applied; again, this is according to one's judgement.
With that being said, let's go!
Best Practices
Code Format
- Reminder: chosen indentation for Enfusion is Allman style
- Reminder: indentation is done with tabulations
- Use empty space. Line return, spaces before and after brackets, if this improves readability, use it: space is free
- One-lining (putting everything in one statement) memory improvement is most of the time not worth the headache it gives when trying to read it: don't overuse it
- it also hinders debugger's usage, e.g in the event of an inlined if
Variable Format
- Name variables and functions properly: code must be readable by a human being, e.g variables like u instead of uniform should not exist.
- i is an accepted iteration variable name (e.g in for loops).
- Prefix any public content (classes, global methods, global variables) with a Creator Tag in order to prevent conflicts with other mods.
- Use the closest value type whenever possible; using auto for a known variable type makes code more obscure and prevents autocompletion.
Code Structuration
SOLID
A series of development principles to follow in order to ensure an easy code maintenance and lifetime.
DRY
Don't Repeat Yourself. If within the same class, the same code or the same pattern is written in various places, write a protected method and use appropriate parameters.
Logical Simplifications
If the code has too many repetitions, make a common method as stated above.
If the code has too many levels, it is time to split it and rethink it.
Examples
Improvable | Good |
---|---|
auto number = 42;
Animal cutePet = new Dog(); |
|
int i; // default value = 0 - see Values - integer
string result; // default value = "" (a string cannot be null)
SCR_MyClass obj; // default value = null | |
// a method call is more expensive than a bool check
if (obj.MustBeTreated() || m_bTreatAllObjects)
Print(obj);
if (obj.MustBeTreated() && m_bTreatAllObjects)
Print(obj); |
// cheap checks go first, expensive checks (method calls) go after
if (m_bTreatAllObjects || obj.MustBeTreated())
Print(obj);
if (m_bTreatAllObjects && obj.MustBeTreated())
Print(obj); |
// many identical method calls
if (obj.MustBeTreated() && obj.GetObject())
Print("Result: " + obj.GetObject().m_sValue1 + " " + obj.GetObject().m_sValue2); |
// "bigger", non-repetitive code can be beneficial for performance and readability
if (obj.MustBeTreated())
{
SCR_Object subObj = obj.GetObject();
if (subObj)
Print("Result: " + subObj.m_sValue1 + " " + subObj.m_sValue2);
} |
foreach (SCR_Object obj : list)
{
Method(obj); // one method call per iteration
}
void Method(SCR_Object obj)
{
if (!obj)
return;
Print(obj.m_sName + " has a value of " + obj.m_sValue);
} |
foreach (SCR_Object obj : list) // the least method calls, the better
{
if (!obj)
continue;
Print(obj.m_sName + " has a value of " + obj.m_sValue);
} |
bool IsObjectAlive(SCR_Object obj)
{
if (!obj)
return false;
if (obj.m_Health > 0) // keep this structure for complex code
return true;
else
return false;
} |
|
for (int i, count = list.Count(); i < count; i++)
{
if (list[i]) // first
Print(list[i]); // and second .Get(i) method calls
} |
foreach (SCR_Object obj : list) // foreach is faster for start-to-end iterating
{
if (obj)
Print(obj); // no additional method call
} |
foreach (int i, SCR_Object obj : list) // iteration index is available this way too
{
PrintFormat("Object #%1 = %2", i, obj);
} | |
// declaring an 'obj' every loop generates a pointer release each time
foreach (SCR_ParentObject parent : list)
{
SCR_Object obj = parent.m_Object;
if (obj)
Print(obj.m_sName);
} |
SCR_Object obj; // external declaration = only one release at the end of the scope
foreach (SCR_ParentObject parent : list)
{
obj = parent.m_Object;
if (obj)
Print(obj.m_sName);
} |
array<SCR_Object> toRemove = {};
foreach (SCR_Object obj : bigArray)
{
if (obj.m_bShouldBeRemoved)
toRemove.Insert(obj);
}
foreach (SCR_Object obj : toRemove)
{
bigArray.RemoveItem(obj); // or RemoveItemOrdered if order is important
} |
for (int i = bigArray.Count() - 1; i >= 0; i--) // reverse iterating
{
if (bigArray[i].m_bShouldBeRemoved)
bigArray.Remove(i); // or RemoveItemOrdered if order is important
} |
if (a)
{
if (b)
{
if (c) // also known as Hadouken code
Method(true);
else
Method(false);
}
} |
if (a && b)
Method(c); |
if (a)
{
Method(a);
if (b)
{
Method(b);
if (c) // another Hadouken code, with complications
{
Method(c);
return 42;
}
else
{
return -1;
}
}
else
{
return -1;
}
}
else
{
return -1;
} |
if (!a)
return -1; // this is called early return and helps funnel down the code
Method(a);
if (!b)
return -1;
Method(b);
if (!c)
return -1;
Method(c);
return 42; |
Initialise(player1, 1);
Initialise(player2, 2);
Initialise(player3, 3);
Initialise(player4, 4);
Initialise(player5, 5);
Initialise(player6, 6); |
// or, better, one method call that initialises all of them
Initialise(list); // numbering is then done inside the method, if possible
Initialise(list, 1); // otherwise the starting number can be provided |
Code Comments
Code comments are surprisingly not a must-have for inside code; code organisation combined to variable names should be enough to be read by a human, then comment can be used:
- 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
On the other hand, documentation is more than welcome as it provides information from the outside without having to read the code. Enfusion uses Doxygen.
Files Organisation
- Have one class/enum per file
- Small classes/enums can always be grouped together in the same file, provided they are part of the same system or only used there
- Use (sub-)directories to group related classes together