Create a Component – Arma Reforger
Lou Montana (talk | contribs) (Page creation) |
Lou Montana (talk | contribs) (Add code) |
||
(2 intermediate revisions by the same user not shown) | |||
Line 2: | Line 2: | ||
A {{Link|Arma Reforger:World Editor}} Component is a code element that can be placed as a child (well, as a ''component'') of an {{Link|Arma Reforger:Create an Entity|entity}} from the World Editor's '''Add Component''' button. | A {{Link|Arma Reforger:World Editor}} Component is a code element that can be placed as a child (well, as a ''component'') of an {{Link|Arma Reforger:Create an Entity|entity}} from the World Editor's '''Add Component''' button. | ||
In this example, we will create a Component that | In this example, we will create a Component that teleports humans if they get too close. | ||
Line 10: | Line 9: | ||
=== Component === | === Component === | ||
Create a new file and name it as your component - here, we will go with {{hl| | Create a new file and name it as your component - here, we will go with {{hl|TAG_TeleportFieldComponent}} so the file should be {{hl|TAG_TeleportFieldComponent.c}}. | ||
{{Feature|informative|By convention, all Component classnames must end with the {{hl|Component}} suffix.}} | {{Feature|informative|By convention, all Component classnames must end with the {{hl|Component}} suffix, here {{hl|TAG_TeleportField'''Component'''}}.}} | ||
{{Feature|important|A component script file '''must''' be created in the '''Game''' module ({{hl|scripts/Game}}), otherwise it will not be listed in the Components list!}} | {{Feature|important|A component script file '''must''' be created in the '''Game''' module ({{hl|scripts/Game}}), otherwise it will not be listed in the Components list!}} | ||
<enforce> | <enforce> | ||
class | class TAG_TeleportFieldComponent : GameComponent // GameComponent > GenericComponent | ||
{ | { | ||
} | } | ||
Line 23: | Line 22: | ||
Like an Entity, a Component requires a Component Class declaration. This allows it to be visible in {{Link|Arma Reforger:World Editor}}. | Like an Entity, a Component requires a Component Class declaration. This allows it to be visible in {{Link|Arma Reforger:World Editor}}. | ||
The name must be '''exactly''' the Component name suffixed by {{hl|Class}}, here {{hl| | The name must be '''exactly''' the Component name suffixed by {{hl|Class}}, here {{hl|TAG_TeleportFieldComponent'''Class'''}}. | ||
A Component Class is usually placed just above the Component definition as such: | A Component Class is usually placed just above the Component definition as such: | ||
<enforce> | <enforce> | ||
[ComponentEditorProps(category: "Tutorial/Component", description: "TODO")] | [ComponentEditorProps(category: "Tutorial/Component", description: "TODO")] | ||
class | class TAG_TeleportFieldComponentClass : GameComponentClass | ||
{ | { | ||
} | } | ||
class | class TAG_TeleportFieldComponent : GameComponent | ||
{ | { | ||
} | } | ||
Line 59: | Line 58: | ||
; icon | ; icon | ||
: '' | : set the component's icon in World Editor's UI - direct path to a {{hl|png}} file, e.g <enforce inline>icon: "WBData/ComponentEditorProps/componentEditor.png"</enforce> | ||
{{Feature|informative|In order for the component to appear in {{Link|Arma Reforger:World Editor}}, scripts '''must''' be compiled and reloaded ''via'' {{Controls|Shift|F7}}.}} | {{Feature|informative|In order for the component to appear in {{Link|Arma Reforger:World Editor}}, scripts '''must''' be compiled and reloaded ''via'' {{Controls|Shift|F7}}.}} | ||
Line 72: | Line 71: | ||
Let's use the {{hl|Component}}'s <enforce inline>OnPostInit()</enforce> method to call code. | Let's use the {{hl|Component}}'s <enforce inline>OnPostInit()</enforce> method to call code. | ||
<enforce> | <enforce methods="QueryEntitiesCallbackMethod"> | ||
// | [ComponentEditorProps(category: "Tutorial/Component", description: "Teleport humans that are too close to the entity")] | ||
class TAG_TeleportFieldComponentClass : ScriptComponentClass | |||
{ | |||
} | |||
class TAG_TeleportFieldComponent : ScriptComponent | |||
{ | |||
protected float m_fCheckDelay; | |||
protected ref array<IEntity> m_aNearbyCharacters; | |||
protected static const float CHECK_PERIOD = 0.25; | |||
protected static const float CHECK_RADIUS = 10; | |||
//------------------------------------------------------------------------------------------------ | |||
override void EOnFrame(IEntity owner, float timeSlice) | |||
{ | |||
super.EOnFrame(owner, timeSlice); | |||
vector ownerPos = owner.GetOrigin(); | |||
m_fCheckDelay -= timeSlice; | |||
if (m_fCheckDelay <= 0) | |||
{ | |||
m_fCheckDelay = CHECK_PERIOD; | |||
m_aNearbyCharacters.Clear(); | |||
owner.GetWorld().QueryEntitiesBySphere(ownerPos, CHECK_RADIUS, QueryEntitiesCallbackMethod, null, EQueryEntitiesFlags.DYNAMIC | EQueryEntitiesFlags.WITH_OBJECT); | |||
Print("There are " + m_aNearbyCharacters.Count() + " human entities around the teleporter."); | |||
foreach (IEntity character : m_aNearbyCharacters) | |||
{ | |||
vector charPos = character.GetOrigin(); | |||
vector vectorDir = vector.Direction(owner.GetOrigin(), charPos).Normalized(); | |||
character.SetOrigin(charPos + 5 * vectorDir); | |||
} | |||
} | |||
} | |||
//------------------------------------------------------------------------------------------------ | |||
// QueryEntitiesCallback type | |||
protected bool QueryEntitiesCallbackMethod(IEntity e) | |||
{ | |||
if (!e || !ChimeraCharacter.Cast(e)) // only humans | |||
return false; | |||
m_aNearbyCharacters.Insert(e); | |||
return true; | |||
} | |||
//------------------------------------------------------------------------------------------------ | |||
protected override void OnPostInit(IEntity owner) | |||
{ | |||
m_aNearbyCharacters = {}; | |||
SetEventMask(owner, EntityEvent.FRAME); | |||
} | |||
} | |||
</enforce> | </enforce> | ||
This code does multiple things: | |||
* in <enforce inline>OnPostInit()</enforce> the nearby characters array is initialised and the "on frame" event mask is set, allowing <enforce inline>EOnFrame</enforce> to be executed every frame | |||
* in <enforce inline>EOnFrame()</enforce> we query entities by sphere (by straight line distance from point to point) and fill the nearby characters array through the <enforce inline>QueryEntitiesCallbackMethod()</enforce> callback method | |||
* still in <enforce inline>EOnFrame()</enforce> we move all detected entities 5 metres back using in <enforce inline>IEntity.SetOrigin()</enforce> | |||
{{Feature|informative| | |||
The <enforce inline>GetOwner()</enforce> method as well as the <enforce inline>IEntity owner</enforce> (<enforce inline>EOnFrame</enforce>'s parameter), although not {{hl|notnull}}ed, never return null. | |||
If they do, there is a '''much''' bigger problem than a null owner, given a component cannot exist without an entity. | |||
}} | |||
=== Add Properties === | === Add Properties === | ||
Now, we can declare properties with the {{hl|Attribute}} in order to be able to adjust some settings from the World Editor interface. The following code only contains the added attributes: | Now, we can declare properties with the {{hl|Attribute}} decorator in order to be able to adjust some settings from the World Editor interface. The following code only contains the added attributes: | ||
<enforce> | <enforce> | ||
// | [ComponentEditorProps(category: "Tutorial/Component", description: "Warn then teleport humans that are too close to the entity")] | ||
class TAG_TeleportFieldComponentClass : ScriptComponentClass | |||
{ | |||
} | |||
class TAG_TeleportFieldComponent : ScriptComponent | |||
{ | |||
/* | |||
Teleportation | |||
*/ | |||
[Attribute(defvalue: "10", desc: "Distance at which the field draws a line to its target to warn it about teleportation", category: "Teleportation")] | |||
protected float m_fWarningRadius; | |||
[Attribute(defvalue: "2", desc: "Distance at which the field triggers the teleportation", params: "0.25 10 0.25", category: "Teleportation")] | |||
protected float m_fTriggerRadius; | |||
[Attribute(defvalue: "10", desc: "Distance at which the teleportation places the unit from the teleporter", category: "Teleportation")] | |||
protected float m_fTeleportDistance; | |||
/* | |||
Line Drawing | |||
*/ | |||
[Attribute(defvalue: "1 0.75 0 1", desc: "The line's colour", category: "Line Drawing")] | |||
protected ref Color m_LineColour; | |||
[Attribute(defvalue: "1", desc: "Whether or not the line must fade in/out with transparency based on distance", category: "Line Drawing")] | |||
protected bool m_bLineFadeInOut; | |||
[Attribute(defvalue: "0 1 0", desc: "The line offset from entities's origins", category: "Line Drawing")] | |||
protected vector m_vOffset; | |||
/* | |||
Performance | |||
*/ | |||
[Attribute(defvalue: "0.25", desc: "Duration between proximity checks", category: "Performance")] | |||
protected float m_fCheckPeriod; | |||
// ... | |||
} | |||
</enforce> | </enforce> | ||
These attributes's implementation can be a good exercise. As you may have noticed some additional attributes made their way to the code. | |||
The goal is to draw a line from the object to the entity in order to warn them they ''will'' be teleported if they keep on getting closer. | |||
You can try to figure out how to do it properly then take a peek at {{Link|#Final Code}} to see one possible solution. | |||
Now all there is to do is to | Now all there is to do is to attach a {{hl|TAG_TeleportFieldComponent}} component to a world entity and see its effects! | ||
Line 98: | Line 200: | ||
The final file content can be found here: | The final file content can be found here: | ||
<spoiler text="Show File Content"> | <spoiler text="Show File Content"> | ||
<enforce> | <enforce methods="QueryEntitiesCallbackMethod"> | ||
// | [ComponentEditorProps(category: "Tutorial/Component", description: "Warn then teleport humans that are too close to the entity")] | ||
class TAG_TeleportFieldComponentClass : ScriptComponentClass | |||
{ | |||
} | |||
class TAG_TeleportFieldComponent : ScriptComponent | |||
{ | |||
/* | |||
Teleportation | |||
*/ | |||
[Attribute(defvalue: "10", desc: "Distance at which the field draws a line to its target to warn it about teleportation", category: "Teleportation")] | |||
protected float m_fWarningRadius; | |||
[Attribute(defvalue: "2", desc: "Distance at which the field triggers the teleportation", params: "0.25 10 0.25", category: "Teleportation")] | |||
protected float m_fTriggerRadius; | |||
[Attribute(defvalue: "10", desc: "Distance at which the teleportation places the unit from the teleporter", category: "Teleportation")] | |||
protected float m_fTeleportDistance; | |||
/* | |||
Line Drawing | |||
*/ | |||
[Attribute(defvalue: "1 0.75 0 1", desc: "The line's colour", category: "Line Drawing")] | |||
protected ref Color m_LineColour; | |||
[Attribute(defvalue: "1", desc: "Whether or not the line must fade in/out with transparency based on distance", category: "Line Drawing")] | |||
protected bool m_bLineFadeInOut; | |||
[Attribute(defvalue: "0 1 0", desc: "The line offset from entities's origins", category: "Line Drawing")] | |||
protected vector m_vOffset; | |||
/* | |||
Performance | |||
*/ | |||
[Attribute(defvalue: "0.25", desc: "Duration between proximity checks", category: "Performance")] | |||
protected float m_fCheckPeriod; | |||
protected float m_fCheckDelay; | |||
protected int m_iTempLineColour; | |||
protected ref array<ref Shape> m_aShapes; | |||
protected ref array<IEntity> m_aNearbyCharacters; | |||
//------------------------------------------------------------------------------------------------ | |||
//! Draw a debug line between two entities | |||
//! \param[in] from | |||
//! \param[in] to | |||
//! \param[in] offset | |||
protected Shape DrawLine(notnull IEntity from, notnull IEntity to, vector offset) | |||
{ | |||
vector points[2] = { from.GetOrigin() + offset, to.GetOrigin() + offset }; | |||
float distance = vector.Distance(points[0], points[1]); | |||
if (m_bLineFadeInOut) | |||
{ | |||
int alpha255 = 255 * (1 - ((distance - m_fTriggerRadius) / (m_fWarningRadius - m_fTriggerRadius))); | |||
m_iTempLineColour = m_iTempLineColour & 0x00FFFFFF | (alpha255 << 24); | |||
return Shape.CreateLines(m_iTempLineColour, ShapeFlags.TRANSP, points, 2); | |||
} | |||
else | |||
{ | |||
return Shape.CreateLines(m_iTempLineColour, 0, points, 2); | |||
} | |||
} | |||
//------------------------------------------------------------------------------------------------ | |||
override void EOnFrame(IEntity owner, float timeSlice) | |||
{ | |||
super.EOnFrame(owner, timeSlice); | |||
vector ownerPos = owner.GetOrigin(); | |||
m_fCheckDelay -= timeSlice; | |||
if (m_fCheckDelay <= 0) | |||
{ | |||
m_fCheckDelay = m_fCheckPeriod; | |||
m_aNearbyCharacters.Clear(); | |||
owner.GetWorld().QueryEntitiesBySphere(ownerPos, m_fWarningRadius, QueryEntitiesCallbackMethod, null, EQueryEntitiesFlags.DYNAMIC | EQueryEntitiesFlags.WITH_OBJECT); | |||
} | |||
m_aShapes.Clear(); | |||
m_aShapes.Reserve(m_aNearbyCharacters.Count()); | |||
foreach (IEntity character : m_aNearbyCharacters) | |||
{ | |||
vector characterPos = character.GetOrigin(); | |||
if (vector.Distance(characterPos, ownerPos) > m_fTriggerRadius) // in the warning zone | |||
{ | |||
m_aShapes.Insert(DrawLine(owner, character, m_vOffset)); // draw line | |||
} | |||
else // in the trigger zone | |||
{ | |||
vector dir = vector.Direction(ownerPos, characterPos).Normalized(); | |||
character.SetOrigin(ownerPos + dir * m_fTeleportDistance); // teleport | |||
} | |||
} | |||
} | |||
//------------------------------------------------------------------------------------------------ | |||
// QueryEntitiesCallback type | |||
protected bool QueryEntitiesCallbackMethod(IEntity e) | |||
{ | |||
if (!e || !ChimeraCharacter.Cast(e)) // only humans | |||
return false; | |||
m_aNearbyCharacters.Insert(e); | |||
return true; | |||
} | |||
//------------------------------------------------------------------------------------------------ | |||
protected override void OnPostInit(IEntity owner) | |||
{ | |||
m_aShapes = {}; | |||
m_aNearbyCharacters = {}; | |||
m_iTempLineColour = m_LineColour.PackToInt(); | |||
SetEventMask(owner, EntityEvent.FRAME); | |||
} | |||
} | |||
</enforce> | </enforce> | ||
</spoiler> | </spoiler> |
Latest revision as of 13:07, 7 May 2024
A World Editor Component is a code element that can be placed as a child (well, as a component) of an entity from the World Editor's Add Component button.
In this example, we will create a Component that teleports humans if they get too close.
Declaration
Component
Create a new file and name it as your component - here, we will go with TAG_TeleportFieldComponent so the file should be TAG_TeleportFieldComponent.c.
Component Class
Like an Entity, a Component requires a Component Class declaration. This allows it to be visible in World Editor. The name must be exactly the Component name suffixed by Class, here TAG_TeleportFieldComponentClass. A Component Class is usually placed just above the Component definition as such:
The class is decorated using ComponentEditorProps; the category is where the Component will be found using e.g the Add Component button - see below.
ComponentEditorProps
- category
- the "Create" tab's category in which the Component can be found
- description
- unused (for now)
- color
- the bounding box's unselected line colour - useful only when visible is set to true
- visible
- have the bounding box always visible - drawn in color
- insertable
- configRoot
- unused
- icon
- set the component's icon in World Editor's UI - direct path to a png file, e.g icon: "WBData/ComponentEditorProps/componentEditor.png"
Filling
The Component is now visible in World Editor's "Add Component" UI, the next step is to make it do something.
Add Code
Let's use the Component's OnPostInit() method to call code.
This code does multiple things:
- in OnPostInit() the nearby characters array is initialised and the "on frame" event mask is set, allowing EOnFrame to be executed every frame
- in EOnFrame() we query entities by sphere (by straight line distance from point to point) and fill the nearby characters array through the QueryEntitiesCallbackMethod() callback method
- still in EOnFrame() we move all detected entities 5 metres back using in IEntity.SetOrigin()
Add Properties
Now, we can declare properties with the Attribute decorator in order to be able to adjust some settings from the World Editor interface. The following code only contains the added attributes:
These attributes's implementation can be a good exercise. As you may have noticed some additional attributes made their way to the code.
The goal is to draw a line from the object to the entity in order to warn them they will be teleported if they keep on getting closer. You can try to figure out how to do it properly then take a peek at Final Code to see one possible solution.
Now all there is to do is to attach a TAG_TeleportFieldComponent component to a world entity and see its effects!
Final Code
The final file content can be found here: