World Editor Tool – Arma Reforger
Jump to navigation
Jump to search
Lou Montana (talk | contribs) (Page creation) |
Lou Montana (talk | contribs) m (Text replacement - "{{Link|OFPEC tags" to "{{Link|Scripting Tags") |
||
(6 intermediate revisions by the same user not shown) | |||
Line 17: | Line 17: | ||
* Open Script Editor | * Open Script Editor | ||
* In an addon, create a new script in {{hl|WorkbenchGame/WorldEditor}} - name it {{hl|{{Link| | * In an addon, create a new script in {{hl|WorkbenchGame/WorldEditor}} - name it {{hl|{{Link|Scripting Tags|TAG_}}TutorialTool.c}} (must end with {{hl|Tool}} by convention) | ||
* Double-click the file to open it | * Double-click the file to open it | ||
* Press {{Controls|Ctrl|T}} to use the {{Link|Arma Reforger:Script Editor: Fill From Template Plugin|Script Template plugin}} | * Press {{Controls|Ctrl|T}} to use the {{Link|Arma Reforger:Script Editor: Fill From Template Plugin|Script Template plugin}} | ||
Line 49: | Line 49: | ||
== Example == | == Example == | ||
In this example, we will use {{Link/Enfusion|armaR|BaseWorld}} and {{Link/Enfusion|armaR|WorldEditorAPI}} methods to detect entities in a radius around the cursor's world position | In this example, we will use {{Link/Enfusion|armaR|BaseWorld}}, {{Link/Enfusion|armaR|WorldEditorTool}} and {{Link/Enfusion|armaR|WorldEditorAPI}} methods to: | ||
* use {{Controls|Ctrl|MSW}} to set the detection radius | |||
* detect entities in a radius around the cursor's world position | |||
* draw lines from the cursor to them | |||
* turn them upside-down on double-click {{Controls|LMB2}} | |||
* delete them on {{Controls|Backspace}} keypress: | |||
{{Feature|important| | {{Feature|important| | ||
Line 59: | Line 64: | ||
<enforce> | <enforce> | ||
#ifdef WORKBENCH | #ifdef WORKBENCH | ||
[WorkbenchToolAttribute(name: "TAG_TutorialTool", description: "Set the wanted radius, move the mouse to highlight found entities, then:\n- double-click to | [WorkbenchToolAttribute( | ||
name: "TAG_TutorialTool", | |||
description: "Set the wanted radius, move the mouse to highlight found entities, then:" | |||
+ "\n- double-click to turn them upside-down" | |||
+ "\n- press BACKSPACE to delete them", | |||
awesomeFontCode: 0xF188)] | |||
class TAG_TutorialTool : WorldEditorTool | class TAG_TutorialTool : WorldEditorTool | ||
{ | { | ||
[Attribute(defvalue: "30", params: " | [Attribute(defvalue: "30", uiwidget: UIWidgets.Slider, params: MIN_RADIUS.ToString() + " " + MAX_RADIUS + " 0.1")] | ||
protected float m_fRadius; | protected float m_fRadius; | ||
[Attribute(defvalue: " | [Attribute(defvalue: "0", desc: "If checked, the cursor stops on the hovered entity; otherwise it only considers the ground as an obstacle")] | ||
protected bool m_bCursorOnObject; | protected bool m_bCursorOnObject; | ||
Line 76: | Line 86: | ||
protected ref array<IEntity> m_aNearbyEntities; | protected ref array<IEntity> m_aNearbyEntities; | ||
float m_cursorX; | |||
float m_cursorY; | |||
protected BaseWorld m_World; | protected BaseWorld m_World; | ||
protected ref SCR_DebugShapeManager m_DebugShapeManager; | protected ref SCR_DebugShapeManager m_DebugShapeManager; | ||
protected static const float MIN_RADIUS = 1; | |||
protected static const float MAX_RADIUS = 100; | |||
//------------------------------------------------------------------------------------------------ | //------------------------------------------------------------------------------------------------ | ||
protected override void OnMouseMoveEvent(float x, float y) | protected override void OnMouseMoveEvent(float x, float y) | ||
{ | { | ||
m_World = m_API.GetWorld(); | |||
if (!m_World) | if (!m_World) | ||
return; | return; | ||
m_cursorX = x; | |||
m_cursorY = y; | |||
RefreshShapes(); | |||
} | } | ||
Line 99: | Line 110: | ||
protected override void OnKeyPressEvent(KeyCode key, bool isAutoRepeat) | protected override void OnKeyPressEvent(KeyCode key, bool isAutoRepeat) | ||
{ | { | ||
if (key == KeyCode. | if (key == KeyCode.KC_BACK) | ||
DeleteEntities(); | DeleteEntities(); | ||
} | } | ||
Line 107: | Line 118: | ||
{ | { | ||
if (buttons == WETMouseButtonFlag.LEFT) | if (buttons == WETMouseButtonFlag.LEFT) | ||
TurnEntitiesUpsideDown(); | |||
} | |||
//------------------------------------------------------------------------------------------------ | |||
protected override void OnWheelEvent(int delta) | |||
{ | |||
if (GetModifierKeyState(ModifierKey.CONTROL)) | |||
{ | |||
m_fRadius += delta / 120; | |||
if (m_fRadius < MIN_RADIUS) | |||
m_fRadius = MIN_RADIUS; | |||
else if (m_fRadius > MAX_RADIUS) | |||
m_fRadius = MAX_RADIUS; | |||
RefreshShapes(); | |||
UpdatePropertyPanel(); | |||
} | |||
} | } | ||
Line 113: | Line 140: | ||
protected override void OnActivate() | protected override void OnActivate() | ||
{ | { | ||
m_DebugShapeManager = new SCR_DebugShapeManager(); | m_DebugShapeManager = new SCR_DebugShapeManager(); | ||
} | } | ||
Line 121: | Line 147: | ||
{ | { | ||
m_aNearbyEntities = null; | m_aNearbyEntities = null; | ||
m_DebugShapeManager = null; | |||
} | |||
//------------------------------------------------------------------------------------------------ | |||
protected void RefreshShapes() | |||
{ | |||
m_DebugShapeManager.Clear(); | |||
vector cursorWorldPos; | |||
if (!GetCursorWorldPosition(m_cursorX, m_cursorY, cursorWorldPos)) | |||
return; // not on screen | |||
FillNearbyEntitiesArray(cursorWorldPos, m_fRadius); | |||
m_DebugShapeManager | |||
m_DebugShapeManager.AddSphere(cursorWorldPos, m_fRadius, m_SphereColour.PackToInt(), ShapeFlags.NOOUTLINE | ShapeFlags.DEPTH_DITHER); | |||
DrawLinesFromPosToEntities(cursorWorldPos, m_aNearbyEntities); | |||
} | } | ||
Line 138: | Line 177: | ||
//------------------------------------------------------------------------------------------------ | //------------------------------------------------------------------------------------------------ | ||
protected | protected void FillNearbyEntitiesArray(vector centre, float radius) | ||
{ | { | ||
m_aNearbyEntities = {}; | |||
m_World.QueryEntitiesBySphere( | |||
centre, | |||
radius, | |||
return | QueryEntitiesCallbackMethod, | ||
queryFlags: EQueryEntitiesFlags.STATIC | EQueryEntitiesFlags.DYNAMIC | EQueryEntitiesFlags.WITH_OBJECT); | |||
// as QueryEntitiesBySphere gets entities by sphere-AABB intersection, we shave by origin distance | |||
for (int i = m_aNearbyEntities.Count() - 1; i >= 0; --i) | |||
{ | |||
if (vector.Distance(m_aNearbyEntities[i].GetOrigin(), centre) > radius) | |||
m_aNearbyEntities.Remove(i); | |||
} | |||
} | |||
//------------------------------------------------------------------------------------------------ | |||
protected bool QueryEntitiesCallbackMethod(IEntity e) | |||
{ | |||
if (!e) | |||
return false; | |||
m_aNearbyEntities.Insert(e); | |||
return true; | |||
} | } | ||
Line 157: | Line 214: | ||
//------------------------------------------------------------------------------------------------ | //------------------------------------------------------------------------------------------------ | ||
protected void | protected void TurnEntitiesUpsideDown() | ||
{ | { | ||
if (!m_aNearbyEntities || m_aNearbyEntities.IsEmpty()) | if (!m_aNearbyEntities || m_aNearbyEntities.IsEmpty()) | ||
Line 171: | Line 228: | ||
continue; | continue; | ||
m_API. | float angleZ; | ||
if (!entitySource.Get("angleZ", angleZ)) | |||
continue; | |||
angleZ += 180; | |||
angleZ = Math.Repeat(angleZ + 180, 360) - 180; | |||
m_API.SetVariableValue(entitySource, null, "angleZ", angleZ.ToString()); | |||
} | } | ||
m_API.EndEntityAction(); | m_API.EndEntityAction(); | ||
RefreshShapes(); | |||
} | } | ||
Line 191: | Line 257: | ||
m_API.EndEntityAction(); | m_API.EndEntityAction(); | ||
RefreshShapes(); | |||
} | } | ||
} | } |
Latest revision as of 12:03, 2 October 2024
ⓘ Not to be confused with World Editor Plugin.
This tutorial teaches how to create a World Editor-specific tool.
A World Editor tool appears in the Tools bar unlike plugins that appear in the plugins list.
Setup
- Open Script Editor
- In an addon, create a new script in WorkbenchGame
/WorldEditor - name it TAG_TutorialTool.c (must end with Tool by convention) - Double-click the file to open it
- Press Ctrl + T to use the Script Template plugin
- In its window, select "Class Type: WorldEditorTool", leave the other fields blank/default
- A World Editor tool skeleton is inserted.
- In the WorkbenchToolAttribute, fill the description field with "Tool description and quick usage guide"
- Make the plugin inherit from WorldEditorPlugin instead of WorkbenchPlugin
- Uncomment the Run() method and write Print("It works!"); in it, then save the file (default Ctrl + S)
- Reload Workbench scripts via Reload Scripts option located in Plugins → Settings (default shortcut: Ctrl + ⇧ Shift + R)
- The TAG_TutorialTool tool should appear in the World Editor's Tools list, available in the top bar - click on the defined icon
- The tool's panel appears, with Tool description and quick usage guide text and the "Run" button - click on the Run button
- "It works!" gets printed in the output console.
WorldEditorTool API
The WorldEditorTool API offers some interesting things:
- the m_API variable is a direct access to the WorldEditorAPI instance
- the GetModifierKeyState() and the On* events methods allow to intercept user input
- the UpdatePropertyPanel() method allows to refresh the tool's panel in case its values are changed by script.
WorldEditorAPI API
Example
In this example, we will use BaseWorld, WorldEditorTool and WorldEditorAPI methods to:
- use Ctrl + to set the detection radius
- detect entities in a radius around the cursor's world position
- draw lines from the cursor to them
- turn them upside-down on double-click
- delete them on ⟵ keypress:
#ifdef WORKBENCH
[WorkbenchToolAttribute(
name: "TAG_TutorialTool",
description: "Set the wanted radius, move the mouse to highlight found entities, then:"
+ "\n- double-click to turn them upside-down"
+ "\n- press BACKSPACE to delete them",
awesomeFontCode: 0xF188)]
class TAG_TutorialTool : WorldEditorTool
{
[Attribute(defvalue: "30", uiwidget: UIWidgets.Slider, params: MIN_RADIUS.ToString() + " " + MAX_RADIUS + " 0.1")]
protected float m_fRadius;
[Attribute(defvalue: "0", desc: "If checked, the cursor stops on the hovered entity; otherwise it only considers the ground as an obstacle")]
protected bool m_bCursorOnObject;
[Attribute(defvalue: "0.5 1 0.5 0.25", desc: "Sphere colour")]
protected ref Color m_SphereColour;
[Attribute(defvalue: "1 1 0 1", desc: "Line colour")]
protected ref Color m_LineColour;
protected ref array<IEntity> m_aNearbyEntities;
float m_cursorX;
float m_cursorY;
protected BaseWorld m_World;
protected ref SCR_DebugShapeManager m_DebugShapeManager;
protected static const float MIN_RADIUS = 1;
protected static const float MAX_RADIUS = 100;
//------------------------------------------------------------------------------------------------
protected override void OnMouseMoveEvent(float x, float y)
{
m_World = m_API.GetWorld();
if (!m_World)
return;
m_cursorX = x;
m_cursorY = y;
RefreshShapes();
}
//------------------------------------------------------------------------------------------------
protected override void OnKeyPressEvent(KeyCode key, bool isAutoRepeat)
{
if (key == KeyCode.KC_BACK)
DeleteEntities();
}
//------------------------------------------------------------------------------------------------
protected override void OnMouseDoubleClickEvent(float x, float y, WETMouseButtonFlag buttons)
{
if (buttons == WETMouseButtonFlag.LEFT)
TurnEntitiesUpsideDown();
}
//------------------------------------------------------------------------------------------------
protected override void OnWheelEvent(int delta)
{
if (GetModifierKeyState(ModifierKey.CONTROL))
{
m_fRadius += delta / 120;
if (m_fRadius < MIN_RADIUS)
m_fRadius = MIN_RADIUS;
else if (m_fRadius > MAX_RADIUS)
m_fRadius = MAX_RADIUS;
RefreshShapes();
UpdatePropertyPanel();
}
}
//------------------------------------------------------------------------------------------------
protected override void OnActivate()
{
m_DebugShapeManager = new SCR_DebugShapeManager();
}
//------------------------------------------------------------------------------------------------
protected override void OnDeActivate()
{
m_aNearbyEntities = null;
m_DebugShapeManager = null;
}
//------------------------------------------------------------------------------------------------
protected void RefreshShapes()
{
m_DebugShapeManager.Clear();
vector cursorWorldPos;
if (!GetCursorWorldPosition(m_cursorX, m_cursorY, cursorWorldPos))
return; // not on screen
FillNearbyEntitiesArray(cursorWorldPos, m_fRadius);
m_DebugShapeManager.AddSphere(cursorWorldPos, m_fRadius, m_SphereColour.PackToInt(), ShapeFlags.NOOUTLINE | ShapeFlags.DEPTH_DITHER);
DrawLinesFromPosToEntities(cursorWorldPos, m_aNearbyEntities);
}
//------------------------------------------------------------------------------------------------
protected bool GetCursorWorldPosition(float x, float y, out vector cursorWorldPos)
{
vector traceStart, traceDir;
TraceFlags flags = TraceFlags.WORLD;
if (m_bCursorOnObject)
flags |= TraceFlags.ENTS;
return m_API.TraceWorldPos(x, y, flags, traceStart, cursorWorldPos, traceDir) && cursorWorldPos != vector.Zero;
}
//------------------------------------------------------------------------------------------------
protected void FillNearbyEntitiesArray(vector centre, float radius)
{
m_aNearbyEntities = {};
m_World.QueryEntitiesBySphere(
centre,
radius,
QueryEntitiesCallbackMethod,
queryFlags: EQueryEntitiesFlags.STATIC | EQueryEntitiesFlags.DYNAMIC | EQueryEntitiesFlags.WITH_OBJECT);
// as QueryEntitiesBySphere gets entities by sphere-AABB intersection, we shave by origin distance
for (int i = m_aNearbyEntities.Count() - 1; i >= 0; --i)
{
if (vector.Distance(m_aNearbyEntities[i].GetOrigin(), centre) > radius)
m_aNearbyEntities.Remove(i);
}
}
//------------------------------------------------------------------------------------------------
protected bool QueryEntitiesCallbackMethod(IEntity e)
{
if (!e)
return false;
m_aNearbyEntities.Insert(e);
return true;
}
//------------------------------------------------------------------------------------------------
protected void DrawLinesFromPosToEntities(vector worldPos, notnull array<IEntity> entities)
{
foreach (IEntity entity : entities)
{
m_DebugShapeManager.AddLine(worldPos, entity.GetOrigin(), m_LineColour.PackToInt());
}
}
//------------------------------------------------------------------------------------------------
protected void TurnEntitiesUpsideDown()
{
if (!m_aNearbyEntities || m_aNearbyEntities.IsEmpty())
return;
m_API.BeginEntityAction();
IEntitySource entitySource;
foreach (IEntity nearbyEntity : m_aNearbyEntities)
{
entitySource = m_API.EntityToSource(nearbyEntity);
if (!entitySource)
continue;
float angleZ;
if (!entitySource.Get("angleZ", angleZ))
continue;
angleZ += 180;
angleZ = Math.Repeat(angleZ + 180, 360) - 180;
m_API.SetVariableValue(entitySource, null, "angleZ", angleZ.ToString());
}
m_API.EndEntityAction();
RefreshShapes();
}
//------------------------------------------------------------------------------------------------
protected void DeleteEntities()
{
if (!m_aNearbyEntities || m_aNearbyEntities.IsEmpty())
return;
m_API.BeginEntityAction();
foreach (IEntity nearbyEntity : m_aNearbyEntities)
{
m_API.DeleteEntity(m_API.EntityToSource(nearbyEntity));
}
m_API.EndEntityAction();
RefreshShapes();
}
}
#endif