Create a Component – Arma Reforger

From Bohemia Interactive Community
Jump to navigation Jump to search
(Page creation)
 
(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 does something.
In this example, we will create a Component that teleports humans if they get too close.
{{Wiki|WIP|Code in progress}}




Line 10: Line 9:
=== Component ===
=== Component ===


Create a new file and name it as your component - here, we will go with {{hl|TAG_Component}} so the file should be {{hl|TAG_Component.c}}.
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 TAG_Component : GameComponent // GameComponent > GenericComponent
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|TAG_Component'''Class'''}}.
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 TAG_ComponentClass : GameComponentClass
class TAG_TeleportFieldComponentClass : GameComponentClass
{
{
}
}


class TAG_Component : GameComponent
class TAG_TeleportFieldComponent : GameComponent
{
{
}
}
Line 59: Line 58:


; icon
; icon
: '''unused''': direct path to a {{hl|png}} file, e.g {{hl|WBData/EntityEditorProps/entityEditor.png}}
: 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">
// TODO
[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>
// TODO
[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>


The following code contains code with the implemented attributes:
These attributes's implementation can be a good exercise. As you may have noticed some additional attributes made their way to the code.


<enforce>
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.
// TODO
You can try to figure out how to do it properly then take a peek at {{Link|#Final Code}} to see one possible solution.
</enforce>


Now all there is to do is to place one {{hl|TAG_Component}} entity in the world and see its effects!
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">
// TODO
[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.

By convention, all Component classnames must end with the Component suffix, here TAG_TeleportFieldComponent.
A component script file must be created in the Game module (scripts/Game), otherwise it will not be listed in the Components list!

class TAG_TeleportFieldComponent : GameComponent // GameComponent > GenericComponent { }

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:

[ComponentEditorProps(category: "Tutorial/Component", description: "TODO")] class TAG_TeleportFieldComponentClass : GameComponentClass { } class TAG_TeleportFieldComponent : GameComponent { }

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"
In order for the component to appear in World Editor, scripts must be compiled and reloaded via ⇧ Shift + F7.


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.

[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); } }

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()
The GetOwner() method as well as the IEntity owner (EOnFrame's parameter), although not notnulled, 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

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:

[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; // ... }

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:

[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); } }

↑ Back to spoiler's top