diff --git a/Source/Editor/Scripting/ScriptType.cs b/Source/Editor/Scripting/ScriptType.cs
index 7f49bdc75..53c84a1b5 100644
--- a/Source/Editor/Scripting/ScriptType.cs
+++ b/Source/Editor/Scripting/ScriptType.cs
@@ -53,6 +53,8 @@ namespace FlaxEditor.Scripting
return fieldInfo.IsPublic;
if (_managed is PropertyInfo propertyInfo)
return (propertyInfo.GetMethod == null || propertyInfo.GetMethod.IsPublic) && (propertyInfo.SetMethod == null || propertyInfo.SetMethod.IsPublic);
+ if (_managed is EventInfo eventInfo)
+ return eventInfo.GetAddMethod().IsPublic;
if (_custom != null)
return _custom.IsPublic;
return false;
@@ -72,6 +74,8 @@ namespace FlaxEditor.Scripting
return fieldInfo.IsStatic;
if (_managed is PropertyInfo propertyInfo)
return (propertyInfo.GetMethod == null || propertyInfo.GetMethod.IsStatic) && (propertyInfo.SetMethod == null || propertyInfo.SetMethod.IsStatic);
+ if (_managed is EventInfo eventInfo)
+ return eventInfo.GetAddMethod().IsStatic;
if (_custom != null)
return _custom.IsStatic;
return false;
@@ -178,7 +182,7 @@ namespace FlaxEditor.Scripting
}
///
- /// Gets the method parameters count (valid for methods only).
+ /// Gets the method parameters count (valid for methods and events only).
///
public int ParametersCount
{
@@ -186,6 +190,8 @@ namespace FlaxEditor.Scripting
{
if (_managed is MethodInfo methodInfo)
return methodInfo.GetParameters().Length;
+ if (_managed is EventInfo eventInfo)
+ return eventInfo.EventHandlerType.GetMethod("Invoke").GetParameters().Length;
if (_custom != null)
return _custom.ParametersCount;
return 0;
diff --git a/Source/Editor/Surface/Archetypes/Function.cs b/Source/Editor/Surface/Archetypes/Function.cs
index b7584a2b8..340d234ab 100644
--- a/Source/Editor/Surface/Archetypes/Function.cs
+++ b/Source/Editor/Surface/Archetypes/Function.cs
@@ -1,6 +1,7 @@
// Copyright (c) 2012-2021 Wojciech Figat. All rights reserved.
using System;
+using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Reflection;
@@ -1180,6 +1181,12 @@ namespace FlaxEditor.Surface.Archetypes
[TypeReference(typeof(object), nameof(IsTypeValid))]
public ScriptType Type;
+ public Parameter(ref ScriptMemberInfo.Parameter param)
+ {
+ Name = param.Name;
+ Type = param.Type;
+ }
+
private static bool IsTypeValid(ScriptType type)
{
return SurfaceUtils.IsValidVisualScriptFunctionType(type) && !type.IsVoid;
@@ -1654,6 +1661,7 @@ namespace FlaxEditor.Surface.Archetypes
base.OnSpawned();
// Setup initial signature
+ var defaultSignature = _signature.Node == null;
CheckFunctionName(ref _signature.Name);
if (_signature.ReturnType == ScriptType.Null)
_signature.ReturnType = new ScriptType(typeof(void));
@@ -1661,8 +1669,11 @@ namespace FlaxEditor.Surface.Archetypes
SaveSignature();
UpdateUI();
- // Start editing
- OnEditSignature();
+ if (defaultSignature)
+ {
+ // Start editing
+ OnEditSignature();
+ }
// Send event
for (int i = 0; i < Surface.Nodes.Count; i++)
@@ -1890,6 +1901,254 @@ namespace FlaxEditor.Surface.Archetypes
}
}
+ private abstract class EventBaseNode : SurfaceNode, IFunctionsDependantNode
+ {
+ private ComboBoxElement _combobox;
+ private Image _helperButton;
+ private bool _isBind;
+ private bool _isUpdateLocked = true;
+ private List _tooltips = new List();
+ private List _functionNodesIds = new List();
+ private ScriptMemberInfo.Parameter[] _signature;
+
+ protected EventBaseNode(bool isBind, uint id, VisjectSurfaceContext context, NodeArchetype nodeArch, GroupArchetype groupArch)
+ : base(id, context, nodeArch, groupArch)
+ {
+ _isBind = isBind;
+ }
+
+ private bool IsValidFunctionSignature(ref VisualScriptFunctionNode.Signature sig)
+ {
+ if (!sig.ReturnType.IsVoid || sig.Parameters == null || sig.Parameters.Length != _signature.Length)
+ return false;
+ for (int i = 0; i < _signature.Length; i++)
+ {
+ if (_signature[i].Type != sig.Parameters[i].Type)
+ return false;
+ }
+ return true;
+ }
+
+ private void UpdateUI()
+ {
+ if (_isUpdateLocked)
+ return;
+ _isUpdateLocked = true;
+ if (_combobox == null)
+ {
+ _combobox = (ComboBoxElement)_children[4];
+ _combobox.TooltipText = _isBind ? "Select the function to call when the event occurs" : "Select the function to unbind from the event";
+ _combobox.SelectedIndexChanged += OnSelectedChanged;
+ _helperButton = new Image
+ {
+ Location = _combobox.UpperRight + new Vector2(4, 3),
+ Size = new Vector2(12.0f),
+ Parent = this,
+ };
+ _helperButton.Clicked += OnHelperButtonClicked;
+ }
+ int toSelect = -1;
+ var handlerFunctionNodeId = Convert.ToUInt32(Values[2]);
+ _combobox.ClearItems();
+ _tooltips.Clear();
+ _functionNodesIds.Clear();
+ var nodes = Surface.Nodes;
+ var count = _signature != null ? nodes.Count : 0;
+ for (int i = 0; i < count; i++)
+ {
+ if (nodes[i] is VisualScriptFunctionNode functionNode)
+ {
+ // Get if function signature matches the event signature
+ functionNode.GetSignature(out var functionSig);
+ if (IsValidFunctionSignature(ref functionSig))
+ {
+ if (functionNode.ID == handlerFunctionNodeId)
+ toSelect = _functionNodesIds.Count;
+ _functionNodesIds.Add(functionNode.ID);
+ _tooltips.Add(functionNode.TooltipText);
+ _combobox.AddItem(functionSig.ToString());
+ }
+ }
+ }
+ _combobox.Tooltips = _tooltips.Count != 0 ? _tooltips.ToArray() : null;
+ _combobox.Enabled = _tooltips.Count != 0;
+ _combobox.SelectedIndex = toSelect;
+ if (toSelect != -1)
+ {
+ _helperButton.Brush = new SpriteBrush(Editor.Instance.Icons.Search12);
+ _helperButton.Color = Color.White;
+ _helperButton.TooltipText = "Navigate to the handler function";
+ }
+ else if (_isBind)
+ {
+ _helperButton.Brush = new SpriteBrush(Editor.Instance.Icons.Add48);
+ _helperButton.Color = Color.Red;
+ _helperButton.TooltipText = "Add new handler function and bind it to this event";
+ _helperButton.Enabled = _signature != null;
+ }
+ else
+ {
+ _helperButton.Enabled = false;
+ }
+ ResizeAuto();
+ _isUpdateLocked = false;
+ }
+
+ private void OnHelperButtonClicked(Image img, MouseButton mouseButton)
+ {
+ if (mouseButton != MouseButton.Left)
+ return;
+ if (_combobox.SelectedIndex != -1)
+ {
+ // Focus selected function
+ var handlerFunctionNodeId = Convert.ToUInt32(Values[2]);
+ var handlerFunctionNode = Surface.FindNode(handlerFunctionNodeId);
+ Surface.FocusNode(handlerFunctionNode);
+ }
+ else if (_isBind)
+ {
+ // Create new function that matches the event signature
+ var surfaceBounds = Surface.AllNodesBounds;
+ Surface.ShowArea(new Rectangle(surfaceBounds.BottomLeft, new Vector2(200, 150)).MakeExpanded(400.0f));
+ var node = Surface.Context.SpawnNode(16, 6, surfaceBounds.BottomLeft + new Vector2(0, 50), null, OnBeforeSpawnedNewHandler);
+ Surface.Select(node);
+
+ // Bind this function
+ SetValue(2, node.ID);
+ }
+ }
+
+ private void OnBeforeSpawnedNewHandler(SurfaceNode node)
+ {
+ // Initialize signature to match the event
+ var functionNode = (VisualScriptFunctionNode)node;
+ functionNode._signature = new VisualScriptFunctionNode.Signature
+ {
+ Name = "On" + (string)Values[1],
+ IsStatic = false,
+ IsVirtual = false,
+ Node = functionNode,
+ ReturnType = ScriptType.Void,
+ Parameters = new VisualScriptFunctionNode.Parameter[_signature.Length],
+ };
+ for (int i = 0; i < _signature.Length; i++)
+ functionNode._signature.Parameters[i] = new VisualScriptFunctionNode.Parameter(ref _signature[i]);
+ }
+
+ private void OnSelectedChanged(ComboBox cb)
+ {
+ if (_isUpdateLocked)
+ return;
+ var handlerFunctionNodeId = Convert.ToUInt32(Values[2]);
+ var selectedID = cb.SelectedIndex != -1 ? _functionNodesIds[cb.SelectedIndex] : 0u;
+ if (selectedID != handlerFunctionNodeId)
+ {
+ SetValue(2, selectedID);
+ UpdateUI();
+ }
+ }
+
+ public void OnFunctionCreated(SurfaceNode node)
+ {
+ UpdateUI();
+ }
+
+ public void OnFunctionEdited(SurfaceNode node)
+ {
+ UpdateUI();
+ }
+
+ public void OnFunctionDeleted(SurfaceNode node)
+ {
+ // Deselect if that function was selected
+ var handlerFunctionNodeId = Convert.ToUInt32(Values[2]);
+ if (node.ID == handlerFunctionNodeId)
+ _combobox.SelectedIndex = -1;
+
+ UpdateUI();
+ }
+
+ public override void OnSurfaceLoaded()
+ {
+ base.OnSurfaceLoaded();
+
+ // Find reflection information about event
+ _signature = null;
+ var isStatic = false;
+ var eventName = (string)Values[1];
+ var eventType = TypeUtils.GetType((string)Values[0]);
+ var member = eventType.GetMember(eventName, MemberTypes.Event, BindingFlags.Public | BindingFlags.Static | BindingFlags.Instance);
+ if (member && SurfaceUtils.IsValidVisualScriptEvent(member))
+ {
+ isStatic = member.IsStatic;
+ _signature = member.GetParameters();
+ TooltipText = SurfaceUtils.GetVisualScriptMemberInfoDescription(member);
+ }
+
+ // Setup instance box (static events don't need it)
+ var instanceBox = GetBox(1);
+ instanceBox.Visible = !isStatic;
+ if (isStatic)
+ instanceBox.RemoveConnections();
+ else
+ instanceBox.CurrentType = eventType;
+
+ _isUpdateLocked = false;
+ UpdateUI();
+ }
+
+ public override void OnValuesChanged()
+ {
+ base.OnValuesChanged();
+
+ UpdateUI();
+ }
+
+ ///
+ public override void OnDestroy()
+ {
+ _combobox = null;
+ _helperButton = null;
+ _tooltips.Clear();
+ _tooltips = null;
+ _functionNodesIds.Clear();
+ _functionNodesIds = null;
+ _signature = null;
+
+ base.OnDestroy();
+ }
+ }
+
+ private sealed class BindEventNode : EventBaseNode
+ {
+ public BindEventNode(uint id, VisjectSurfaceContext context, NodeArchetype nodeArch, GroupArchetype groupArch)
+ : base(true, id, context, nodeArch, groupArch)
+ {
+ }
+
+ public override void OnSurfaceLoaded()
+ {
+ Title = "Bind " + (string)Values[1];
+
+ base.OnSurfaceLoaded();
+ }
+ }
+
+ private sealed class UnbindEventNode : EventBaseNode
+ {
+ public UnbindEventNode(uint id, VisjectSurfaceContext context, NodeArchetype nodeArch, GroupArchetype groupArch)
+ : base(false, id, context, nodeArch, groupArch)
+ {
+ }
+
+ public override void OnSurfaceLoaded()
+ {
+ Title = "Unbind " + (string)Values[1];
+
+ base.OnSurfaceLoaded();
+ }
+ }
+
///
/// The nodes for that group.
///
@@ -2031,6 +2290,50 @@ namespace FlaxEditor.Surface.Archetypes
null, // Default value
},
},
+ new NodeArchetype
+ {
+ TypeID = 9,
+ Create = (id, context, arch, groupArch) => new BindEventNode(id, context, arch, groupArch),
+ Title = string.Empty,
+ Flags = NodeFlags.VisualScriptGraph | NodeFlags.NoSpawnViaGUI,
+ Size = new Vector2(260, 60),
+ DefaultValues = new object[]
+ {
+ string.Empty, // Event type
+ string.Empty, // Event name
+ (uint)0, // Handler function nodeId
+ },
+ Elements = new[]
+ {
+ NodeElementArchetype.Factory.Input(0, string.Empty, true, typeof(void), 0),
+ NodeElementArchetype.Factory.Input(2, "Instance", true, typeof(object), 1),
+ NodeElementArchetype.Factory.Output(0, string.Empty, typeof(void), 2, true),
+ NodeElementArchetype.Factory.Text(2, 20, "Handler function:"),
+ NodeElementArchetype.Factory.ComboBox(100, 20, 140),
+ }
+ },
+ new NodeArchetype
+ {
+ TypeID = 10,
+ Create = (id, context, arch, groupArch) => new UnbindEventNode(id, context, arch, groupArch),
+ Title = string.Empty,
+ Flags = NodeFlags.VisualScriptGraph | NodeFlags.NoSpawnViaGUI,
+ Size = new Vector2(260, 60),
+ DefaultValues = new object[]
+ {
+ string.Empty, // Event type
+ string.Empty, // Event name
+ (uint)0, // Handler function nodeId
+ },
+ Elements = new[]
+ {
+ NodeElementArchetype.Factory.Input(0, string.Empty, true, typeof(void), 0),
+ NodeElementArchetype.Factory.Input(2, "Instance", true, typeof(object), 1),
+ NodeElementArchetype.Factory.Output(0, string.Empty, typeof(void), 2, true),
+ NodeElementArchetype.Factory.Text(2, 20, "Handler function:"),
+ NodeElementArchetype.Factory.ComboBox(100, 20, 140),
+ }
+ },
};
}
}
diff --git a/Source/Editor/Surface/SurfaceNode.cs b/Source/Editor/Surface/SurfaceNode.cs
index 8726359e9..ae094c2be 100644
--- a/Source/Editor/Surface/SurfaceNode.cs
+++ b/Source/Editor/Surface/SurfaceNode.cs
@@ -187,7 +187,10 @@ namespace FlaxEditor.Surface
var titleLabelFont = Style.Current.FontLarge;
for (int i = 0; i < Children.Count; i++)
{
- if (Children[i] is InputBox inputBox)
+ var child = Children[i];
+ if (!child.Visible)
+ continue;
+ if (child is InputBox inputBox)
{
var boxWidth = boxLabelFont.MeasureText(inputBox.Text).X + 20;
if (inputBox.DefaultValueEditor != null)
@@ -195,12 +198,12 @@ namespace FlaxEditor.Surface
leftWidth = Mathf.Max(leftWidth, boxWidth);
leftHeight = Mathf.Max(leftHeight, inputBox.Archetype.Position.Y - Constants.NodeMarginY - Constants.NodeHeaderSize + 20.0f);
}
- else if (Children[i] is OutputBox outputBox)
+ else if (child is OutputBox outputBox)
{
rightWidth = Mathf.Max(rightWidth, boxLabelFont.MeasureText(outputBox.Text).X + 20);
rightHeight = Mathf.Max(rightHeight, outputBox.Archetype.Position.Y - Constants.NodeMarginY - Constants.NodeHeaderSize + 20.0f);
}
- else if (Children[i] is Control control)
+ else if (child is Control control)
{
if (control.AnchorPreset == AnchorPresets.TopLeft)
{
diff --git a/Source/Editor/Surface/SurfaceUtils.cs b/Source/Editor/Surface/SurfaceUtils.cs
index 18f2e9693..74f1d6130 100644
--- a/Source/Editor/Surface/SurfaceUtils.cs
+++ b/Source/Editor/Surface/SurfaceUtils.cs
@@ -406,6 +406,11 @@ namespace FlaxEditor.Surface
return member.IsField && IsValidVisualScriptType(member.ValueType);
}
+ internal static bool IsValidVisualScriptEvent(ScriptMemberInfo member)
+ {
+ return member.IsEvent && member.HasAttribute(typeof(UnmanagedAttribute));
+ }
+
internal static bool IsValidVisualScriptType(ScriptType scriptType)
{
if (scriptType.IsGenericType || !scriptType.IsPublic || scriptType.HasAttribute(typeof(HideInEditorAttribute), true))
diff --git a/Source/Editor/Surface/VisjectSurfaceContext.cs b/Source/Editor/Surface/VisjectSurfaceContext.cs
index d436cd91e..81b72bf4e 100644
--- a/Source/Editor/Surface/VisjectSurfaceContext.cs
+++ b/Source/Editor/Surface/VisjectSurfaceContext.cs
@@ -331,12 +331,13 @@ namespace FlaxEditor.Surface
/// The node archetype ID.
/// The location.
/// The custom values array. Must match node archetype size. Pass null to use default values.
+ /// The custom callback action to call after node creation but just before invoking spawn event. Can be used to initialize custom node data.
/// Created node.
- public SurfaceNode SpawnNode(ushort groupID, ushort typeID, Vector2 location, object[] customValues = null)
+ public SurfaceNode SpawnNode(ushort groupID, ushort typeID, Vector2 location, object[] customValues = null, Action beforeSpawned = null)
{
if (NodeFactory.GetArchetype(_surface.NodeArchetypes, groupID, typeID, out var groupArchetype, out var nodeArchetype))
{
- return SpawnNode(groupArchetype, nodeArchetype, location, customValues);
+ return SpawnNode(groupArchetype, nodeArchetype, location, customValues, beforeSpawned);
}
return null;
}
@@ -348,8 +349,9 @@ namespace FlaxEditor.Surface
/// The node archetype.
/// The location.
/// The custom values array. Must match node archetype size. Pass null to use default values.
+ /// The custom callback action to call after node creation but just before invoking spawn event. Can be used to initialize custom node data.
/// Created node.
- public SurfaceNode SpawnNode(GroupArchetype groupArchetype, NodeArchetype nodeArchetype, Vector2 location, object[] customValues = null)
+ public SurfaceNode SpawnNode(GroupArchetype groupArchetype, NodeArchetype nodeArchetype, Vector2 location, object[] customValues = null, Action beforeSpawned = null)
{
if (groupArchetype == null || nodeArchetype == null)
throw new ArgumentNullException();
@@ -387,6 +389,7 @@ namespace FlaxEditor.Surface
}
node.Location = location;
OnControlLoaded(node);
+ beforeSpawned?.Invoke(node);
node.OnSurfaceLoaded();
OnControlSpawned(node);
diff --git a/Source/Editor/Surface/VisualScriptSurface.cs b/Source/Editor/Surface/VisualScriptSurface.cs
index ccabc8f2f..335860c52 100644
--- a/Source/Editor/Surface/VisualScriptSurface.cs
+++ b/Source/Editor/Surface/VisualScriptSurface.cs
@@ -2,6 +2,11 @@
//#define DEBUG_INVOKE_METHODS_SEARCHING
//#define DEBUG_FIELDS_SEARCHING
+//#define DEBUG_EVENTS_SEARCHING
+
+#if DEBUG_INVOKE_METHODS_SEARCHING || DEBUG_FIELDS_SEARCHING || DEBUG_EVENTS_SEARCHING
+#define DEBUG_SEARCH_TIME
+#endif
using System;
using System.Collections.Generic;
@@ -103,7 +108,7 @@ namespace FlaxEditor.Surface
private static void OnActiveContextMenuShowAsync()
{
Profiler.BeginEvent("Setup Visual Script Context Menu (async)");
-#if DEBUG_INVOKE_METHODS_SEARCHING || DEBUG_FIELDS_SEARCHING
+#if DEBUG_SEARCH_TIME
var searchStartTime = DateTime.Now;
var searchHitsCount = 0;
#endif
@@ -338,13 +343,65 @@ namespace FlaxEditor.Surface
}
}
}
+ else if (member.IsEvent)
+ {
+ var name = member.Name;
+
+ // Skip if searching by name doesn't return a match
+ var members = scriptType.GetMembers(name, MemberTypes.Event, BindingFlags.Public | BindingFlags.Instance | BindingFlags.Static | BindingFlags.DeclaredOnly);
+ if (!members.Contains(member))
+ continue;
+
+ // Check if field is valid for Visual Script usage
+ if (SurfaceUtils.IsValidVisualScriptEvent(member))
+ {
+ var groupKey = new KeyValuePair(scriptTypeName, 16);
+ if (!_cache.TryGetValue(groupKey, out var group))
+ {
+ group = new GroupArchetype
+ {
+ GroupID = groupKey.Value,
+ Name = groupKey.Key,
+ Color = new Color(109, 160, 24),
+ Tag = _version,
+ Archetypes = new List(),
+ };
+ _cache.Add(groupKey, group);
+ }
+
+ // Add Bind event node
+ var bindNode = (NodeArchetype)Archetypes.Function.Nodes[8].Clone();
+ bindNode.DefaultValues[0] = scriptTypeTypeName;
+ bindNode.DefaultValues[1] = name;
+ bindNode.Flags &= ~NodeFlags.NoSpawnViaGUI;
+ bindNode.Title = "Bind " + name;
+ bindNode.Description = SurfaceUtils.GetVisualScriptMemberInfoDescription(member);
+ bindNode.SubTitle = string.Format(" (in {0})", scriptTypeName);
+ ((IList)group.Archetypes).Add(bindNode);
+
+ // Add Unbind event node
+ var unbindNode = (NodeArchetype)Archetypes.Function.Nodes[9].Clone();
+ unbindNode.DefaultValues[0] = scriptTypeTypeName;
+ unbindNode.DefaultValues[1] = name;
+ unbindNode.Flags &= ~NodeFlags.NoSpawnViaGUI;
+ unbindNode.Title = "Unbind " + name;
+ unbindNode.Description = bindNode.Description;
+ unbindNode.SubTitle = bindNode.SubTitle;
+ ((IList)group.Archetypes).Add(unbindNode);
+
+#if DEBUG_EVENTS_SEARCHING
+ Editor.LogWarning(scriptTypeTypeName + " -> " + member.GetSignature());
+ searchHitsCount++;
+#endif
+ }
+ }
}
}
// Add group to context menu (on a main thread)
FlaxEngine.Scripting.InvokeOnUpdate(() =>
{
-#if DEBUG_INVOKE_METHODS_SEARCHING || DEBUG_FIELDS_SEARCHING
+#if DEBUG_SEARCH_TIME
var addStartTime = DateTime.Now;
#endif
lock (_locker)
@@ -352,12 +409,12 @@ namespace FlaxEditor.Surface
_taskContextMenu.AddGroups(_cache.Values);
_taskContextMenu = null;
}
-#if DEBUG_INVOKE_METHODS_SEARCHING || DEBUG_FIELDS_SEARCHING
+#if DEBUG_SEARCH_TIME
Editor.LogError($"Added items to VisjectCM in: {(DateTime.Now - addStartTime).TotalMilliseconds} ms");
#endif
});
-#if DEBUG_INVOKE_METHODS_SEARCHING || DEBUG_FIELDS_SEARCHING
+#if DEBUG_SEARCH_TIME
Editor.LogError($"Collected {searchHitsCount} items in: {(DateTime.Now - searchStartTime).TotalMilliseconds} ms");
#endif
Profiler.EndEvent();
diff --git a/Source/Engine/Content/Assets/VisualScript.cpp b/Source/Engine/Content/Assets/VisualScript.cpp
index 729ee0a5e..da1b97687 100644
--- a/Source/Engine/Content/Assets/VisualScript.cpp
+++ b/Source/Engine/Content/Assets/VisualScript.cpp
@@ -6,6 +6,7 @@
#include "Engine/Content/Factories/BinaryAssetFactory.h"
#include "Engine/Scripting/MException.h"
#include "Engine/Scripting/Scripting.h"
+#include "Engine/Scripting/Events.h"
#include "Engine/Scripting/ManagedCLR/MClass.h"
#include "Engine/Scripting/ManagedCLR/MMethod.h"
#include "Engine/Scripting/ManagedCLR/MField.h"
@@ -347,7 +348,7 @@ void VisualScriptExecutor::ProcessGroupParameters(Box* box, Node* node, Value& v
const auto instanceParams = stack.Stack->Script->_instances.Find(stack.Stack->Instance->GetID());
if (param && instanceParams)
{
- value = instanceParams->Value[paramIndex];
+ value = instanceParams->Value.Params[paramIndex];
}
else
{
@@ -371,7 +372,7 @@ void VisualScriptExecutor::ProcessGroupParameters(Box* box, Node* node, Value& v
const auto instanceParams = stack.Stack->Script->_instances.Find(stack.Stack->Instance->GetID());
if (param && instanceParams)
{
- instanceParams->Value[paramIndex] = tryGetValue(node->GetBox(1), 1, Value::Zero);
+ instanceParams->Value.Params[paramIndex] = tryGetValue(node->GetBox(1), 1, Value::Zero);
}
else
{
@@ -1004,6 +1005,125 @@ void VisualScriptExecutor::ProcessGroupFunction(Box* boxBase, Node* node, Value&
if (returnedImpulse && returnedImpulse->HasConnection())
eatBox(node, returnedImpulse->FirstConnection());
break;
+ }
+ // Bind/Unbind
+ case 9:
+ case 10:
+ {
+ const bool bind = node->TypeID == 9;
+ auto& stack = ThreadStacks.Get();
+ if (!stack.Stack->Instance)
+ {
+ // TODO: add support for binding to events in static Visual Script
+ LOG(Error, "Cannot bind to event in static Visual Script.");
+ PrintStack(LogType::Error);
+ break;
+ }
+ const auto object = stack.Stack->Instance;
+
+ // Find method to bind
+ VisualScriptGraphNode* methodNode = nullptr;
+ const auto graph = stack.Stack && stack.Stack->Script ? &stack.Stack->Script->Graph : nullptr;
+ if (graph)
+ methodNode = graph->GetNode((uint32)node->Values[2]);
+ if (!methodNode)
+ {
+ LOG(Error, "Missing function handler to bind to the event.");
+ PrintStack(LogType::Error);
+ break;
+ }
+ VisualScript::Method* method = nullptr;
+ for (auto& m : stack.Stack->Script->_methods)
+ {
+ if (m.Node == methodNode)
+ {
+ method = &m;
+ break;
+ }
+ }
+ if (!method)
+ {
+ LOG(Error, "Missing method to bind to the event.");
+ PrintStack(LogType::Error);
+ break;
+ }
+
+ // Find event
+ const StringView eventTypeName(node->Values[0]);
+ const StringView eventName(node->Values[1]);
+ const StringAsANSI<100> eventTypeNameAnsi(eventTypeName.Get(), eventTypeName.Length());
+ const ScriptingTypeHandle eventType = Scripting::FindScriptingType(StringAnsiView(eventTypeNameAnsi.Get(), eventTypeName.Length()));
+
+ // Find event binding callback
+ auto eventBinder = ScriptingEvents::EventsTable.TryGet(Pair(eventType, eventName));
+ if (!eventBinder)
+ {
+ LOG(Error, "Cannot bind to missing event {0} from type {1}.", eventName, eventTypeName);
+ PrintStack(LogType::Error);
+ break;
+ }
+
+ // Evaluate object instance
+ const auto box = node->GetBox(1);
+ Variant instance;
+ if (box->HasConnection())
+ instance = eatBox(node, box->FirstConnection());
+ else
+ instance.SetObject(object);
+ if (!instance.AsObject)
+ {
+ LOG(Error, "Cannot bind event to null object.");
+ PrintStack(LogType::Error);
+ break;
+ }
+ // TODO: check if instance is of event type (including inheritance)
+
+ // Add Visual Script method to the event bindings table
+ const auto& type = object->GetType();
+ Guid id;
+ if (Guid::Parse(type.Fullname, id))
+ break;
+ if (const auto visualScript = (VisualScript*)Content::GetAsset(id))
+ {
+ if (auto i = visualScript->GetScriptInstance(object))
+ {
+ VisualScript::EventBinding* eventBinding = nullptr;
+ for (auto& b : i->EventBindings)
+ {
+ if (b.Type == eventType && b.Name == eventName)
+ {
+ eventBinding = &b;
+ break;
+ }
+ }
+ if (bind)
+ {
+ // Bind to the event
+ if (!eventBinding)
+ {
+ eventBinding = &i->EventBindings.AddOne();
+ eventBinding->Type = eventType;
+ eventBinding->Name = eventName;
+ }
+ eventBinding->BindedMethods.Add(method);
+ if (eventBinding->BindedMethods.Count() == 1)
+ (*eventBinder)(instance.AsObject, object, true);
+ }
+ else if (eventBinding)
+ {
+ // Unbind from the event
+ if (eventBinding->BindedMethods.Count() == 1)
+ (*eventBinder)(instance.AsObject, object, false);
+ eventBinding->BindedMethods.Remove(method);
+ }
+ }
+ }
+
+ // Call graph further
+ const auto returnedImpulse = &node->Boxes[2];
+ if (returnedImpulse && returnedImpulse->HasConnection())
+ eatBox(node, returnedImpulse->FirstConnection());
+ break;
}
default:
break;
@@ -1304,7 +1424,7 @@ Asset::LoadResult VisualScript::load()
// Update instanced data from previous format to the current graph parameters scheme
for (auto& e : _instances)
{
- auto& instanceParams = e.Value;
+ auto& instanceParams = e.Value.Params;
Array valuesCache(MoveTemp(instanceParams));
instanceParams.Resize(count);
for (int32 i = 0; i < count; i++)
@@ -1319,7 +1439,7 @@ Asset::LoadResult VisualScript::load()
// Reset instances values to defaults
for (auto& e : _instances)
{
- auto& instanceParams = e.Value;
+ auto& instanceParams = e.Value.Params;
instanceParams.Resize(count);
for (int32 i = 0; i < count; i++)
instanceParams[i] = Graph.Parameters[i].Value;
@@ -1421,13 +1541,17 @@ void VisualScript::CacheScriptingType()
_scriptingTypeHandle = ScriptingTypeHandle(&binaryModule, typeIndex);
binaryModule.Scripts.Add(this);
-#if USE_EDITOR
- // When first Visual Script gets loaded register for other modules unload to clear runtime execution cache
+ // Special initialization when the first Visual Script gets loaded
if (typeIndex == 0)
{
+#if USE_EDITOR
+ // Register for other modules unload to clear runtime execution cache
Scripting::ScriptsReloading.Bind(&binaryModule);
- }
#endif
+
+ // Register for scripting events
+ ScriptingEvents::Event.Bind(VisualScriptingBinaryModule::OnEvent);
+ }
}
auto& type = _scriptingTypeHandle.Module->Types[_scriptingTypeHandle.TypeIndex];
type.ManagedClass = baseType.GetType().ManagedClass;
@@ -1550,7 +1674,7 @@ ScriptingObject* VisualScriptingBinaryModule::VisualScriptObjectSpawn(const Scri
VisualScript* visualScript = VisualScriptingModule.Scripts[params.Type.TypeIndex];
// Initialize instance data
- auto& instanceParams = visualScript->_instances[object->GetID()];
+ auto& instanceParams = visualScript->_instances[object->GetID()].Params;
instanceParams.Resize(visualScript->Graph.Parameters.Count());
for (int32 i = 0; i < instanceParams.Count(); i++)
instanceParams[i] = visualScript->Graph.Parameters[i].Value;
@@ -1608,6 +1732,56 @@ void VisualScriptingBinaryModule::OnScriptsReloading()
#endif
+void VisualScriptingBinaryModule::OnEvent(ScriptingObject* object, Span& parameters, const ScriptingTypeHandle& eventType, const StringView& eventName)
+{
+ if (object)
+ {
+ // Object event
+ const auto& type = object->GetType();
+ Guid id;
+ if (Guid::Parse(type.Fullname, id))
+ return;
+ if (const auto visualScript = (VisualScript*)Content::GetAsset(id))
+ {
+ if (auto instance = visualScript->GetScriptInstance(object))
+ {
+ for (auto& b : instance->EventBindings)
+ {
+ if (b.Type != eventType || b.Name != eventName)
+ continue;
+ for (auto& m : b.BindedMethods)
+ {
+ VisualScripting::Invoke(m, object, parameters);
+ }
+ }
+ }
+ }
+ }
+ else
+ {
+ // Static event
+ for (auto& asset : Content::GetAssetsRaw())
+ {
+ if (const auto visualScript = ScriptingObject::Cast(asset.Value))
+ {
+ for (auto& e : visualScript->_instances)
+ {
+ auto instance = &e.Value;
+ for (auto& b : instance->EventBindings)
+ {
+ if (b.Type != eventType || b.Name != eventName)
+ continue;
+ for (auto& m : b.BindedMethods)
+ {
+ VisualScripting::Invoke(m, object, parameters);
+ }
+ }
+ }
+ }
+ }
+ }
+}
+
const StringAnsi& VisualScriptingBinaryModule::GetName() const
{
return _name;
@@ -1702,7 +1876,7 @@ bool VisualScriptingBinaryModule::GetFieldValue(void* field, const Variant& inst
LOG(Error, "Missing parameters for the object instance.");
return true;
}
- result = instanceParams->Value[vsFiled->Index];
+ result = instanceParams->Value.Params[vsFiled->Index];
return false;
}
@@ -1721,7 +1895,7 @@ bool VisualScriptingBinaryModule::SetFieldValue(void* field, const Variant& inst
LOG(Error, "Missing parameters for the object instance.");
return true;
}
- instanceParams->Value[vsFiled->Index] = value;
+ instanceParams->Value.Params[vsFiled->Index] = value;
return false;
}
@@ -1735,7 +1909,7 @@ void VisualScriptingBinaryModule::SerializeObject(JsonWriter& stream, ScriptingO
const auto instanceParams = asset->_instances.Find(object->GetID());
if (instanceParams)
{
- auto& params = instanceParams->Value;
+ auto& params = instanceParams->Value.Params;
if (otherObj)
{
// Serialize parameters diff
@@ -1746,7 +1920,7 @@ void VisualScriptingBinaryModule::SerializeObject(JsonWriter& stream, ScriptingO
{
auto& param = asset->Graph.Parameters[paramIndex];
auto& value = params[paramIndex];
- auto& otherValue = otherParams->Value[paramIndex];
+ auto& otherValue = otherParams->Value.Params[paramIndex];
if (value != otherValue)
{
param.Identifier.ToString(idName, Guid::FormatType::N);
@@ -1798,7 +1972,7 @@ void VisualScriptingBinaryModule::DeserializeObject(ISerializable::DeserializeSt
if (instanceParams)
{
// Deserialize all parameters
- auto& params = instanceParams->Value;
+ auto& params = instanceParams->Value.Params;
for (auto i = stream.MemberBegin(); i != stream.MemberEnd(); ++i)
{
StringAnsiView idNameAnsi(i->name.GetString(), i->name.GetStringLength());
@@ -1865,6 +2039,11 @@ ScriptingObject* VisualScript::CreateInstance()
return scriptingTypeHandle ? scriptingTypeHandle.GetType().Script.Spawn(ScriptingObjectSpawnParams(Guid::New(), scriptingTypeHandle)) : nullptr;
}
+VisualScript::Instance* VisualScript::GetScriptInstance(ScriptingObject* instance) const
+{
+ return instance ? _instances.TryGet(instance->GetID()) : nullptr;
+}
+
Variant VisualScript::GetScriptInstanceParameterValue(const StringView& name, ScriptingObject* instance) const
{
CHECK_RETURN(instance, Variant());
@@ -1874,7 +2053,7 @@ Variant VisualScript::GetScriptInstanceParameterValue(const StringView& name, Sc
{
const auto instanceParams = _instances.Find(instance->GetID());
if (instanceParams)
- return instanceParams->Value[paramIndex];
+ return instanceParams->Value.Params[paramIndex];
LOG(Error, "Failed to access Visual Script parameter {1} for {0}.", instance->ToString(), name);
return Graph.Parameters[paramIndex].Value;
}
@@ -1893,7 +2072,7 @@ void VisualScript::SetScriptInstanceParameterValue(const StringView& name, Scrip
const auto instanceParams = _instances.Find(instance->GetID());
if (instanceParams)
{
- instanceParams->Value[paramIndex] = value;
+ instanceParams->Value.Params[paramIndex] = value;
return;
}
LOG(Error, "Failed to access Visual Script parameter {1} for {0}.", instance->ToString(), name);
@@ -1913,7 +2092,7 @@ void VisualScript::SetScriptInstanceParameterValue(const StringView& name, Scrip
const auto instanceParams = _instances.Find(instance->GetID());
if (instanceParams)
{
- instanceParams->Value[paramIndex] = MoveTemp(value);
+ instanceParams->Value.Params[paramIndex] = MoveTemp(value);
return;
}
}
diff --git a/Source/Engine/Content/Assets/VisualScript.h b/Source/Engine/Content/Assets/VisualScript.h
index 0a7f34b1c..cf8dccce3 100644
--- a/Source/Engine/Content/Assets/VisualScript.h
+++ b/Source/Engine/Content/Assets/VisualScript.h
@@ -125,9 +125,22 @@ public:
StringAnsi Name;
};
+ struct EventBinding
+ {
+ ScriptingTypeHandle Type;
+ String Name;
+ Array> BindedMethods;
+ };
+
+ struct Instance
+ {
+ Array Params;
+ Array EventBindings;
+ };
+
private:
- Dictionary> _instances;
+ Dictionary _instances;
ScriptingTypeHandle _scriptingTypeHandle;
ScriptingTypeHandle _scriptingTypeHandleCached;
StringAnsiView _typename;
@@ -179,6 +192,13 @@ public:
/// The created instance or null if failed.
API_FUNCTION() ScriptingObject* CreateInstance();
+ ///
+ /// Gets the Visual Script instance data.
+ ///
+ /// The object instance.
+ /// The data or invalid instance (not VS or missing).
+ Instance* GetScriptInstance(ScriptingObject* instance) const;
+
///
/// Gets the value of the Visual Script parameter of the given instance.
///
@@ -307,6 +327,7 @@ private:
#if USE_EDITOR
void OnScriptsReloading();
#endif
+ static void OnEvent(ScriptingObject* object, Span& parameters, const ScriptingTypeHandle& eventType, const StringView& eventName);
public:
diff --git a/Source/Engine/Scripting/BinaryModule.cpp b/Source/Engine/Scripting/BinaryModule.cpp
index c802caff2..689b552f8 100644
--- a/Source/Engine/Scripting/BinaryModule.cpp
+++ b/Source/Engine/Scripting/BinaryModule.cpp
@@ -14,6 +14,10 @@
#include "MException.h"
#include "Scripting.h"
#include "StdTypesContainer.h"
+#include "Events.h"
+
+Dictionary, void(*)(ScriptingObject*, void*, bool)> ScriptingEvents::EventsTable;
+Delegate&, const ScriptingTypeHandle&, const StringView&> ScriptingEvents::Event;
ManagedBinaryModule* GetBinaryModuleCorlib()
{
diff --git a/Source/Engine/Scripting/Events.h b/Source/Engine/Scripting/Events.h
new file mode 100644
index 000000000..11f1643ee
--- /dev/null
+++ b/Source/Engine/Scripting/Events.h
@@ -0,0 +1,36 @@
+// Copyright (c) 2012-2021 Wojciech Figat. All rights reserved.
+
+#pragma once
+
+#include "ScriptingType.h"
+#include "Engine/Core/Delegate.h"
+#include "Engine/Core/Types/Span.h"
+#include "Engine/Core/Types/Pair.h"
+#include "Engine/Core/Types/Variant.h"
+#include "Engine/Core/Types/StringView.h"
+#include "Engine/Core/Collections/Dictionary.h"
+
+///
+/// The helper utility for binding and invoking scripting events (eg. used by Visual Scripting).
+///
+class FLAXENGINE_API ScriptingEvents
+{
+public:
+
+ ///
+ /// Global table for registered even binder methods (key is pair of type and event name, value is method that takes instance with event, object to bind and flag to bind or unbind).
+ ///
+ ///
+ /// Key: pair of event type name (full), event name.
+ /// Value: event binder function with parameters: event caller instance (null for static events), object to bind, true to bind/false to unbind.
+ ///
+ static Dictionary, void(*)(ScriptingObject*, void*, bool)> EventsTable;
+
+ ///
+ /// The action called when any scripting event occurs. Can be used to invoke scripting code that binded to this particular event.
+ ///
+ ///
+ /// Delegate parameters: event caller instance (null for static events), event invocation parameters list, event type name (full), event name.
+ ///
+ static Delegate&, const ScriptingTypeHandle&, const StringView&> Event;
+};
diff --git a/Source/Tools/Flax.Build/Bindings/BindingsGenerator.Cpp.cs b/Source/Tools/Flax.Build/Bindings/BindingsGenerator.Cpp.cs
index 6cdc5462b..11fe6a414 100644
--- a/Source/Tools/Flax.Build/Bindings/BindingsGenerator.Cpp.cs
+++ b/Source/Tools/Flax.Build/Bindings/BindingsGenerator.Cpp.cs
@@ -1091,19 +1091,19 @@ namespace Flax.Build.Bindings
{
if (!useScripting)
continue;
- CppIncludeFiles.Add("Engine/Scripting/ManagedCLR/MEvent.h");
+ var paramsCount = eventInfo.Type.GenericArgs.Count;
// C# event invoking wrapper (calls C# event from C++ delegate)
+ CppIncludeFiles.Add("Engine/Scripting/ManagedCLR/MEvent.h");
contents.Append(" ");
if (eventInfo.IsStatic)
contents.Append("static ");
contents.AppendFormat("void {0}_ManagedWrapper(", eventInfo.Name);
- for (var i = 0; i < eventInfo.Type.GenericArgs.Count; i++)
+ for (var i = 0; i < paramsCount; i++)
{
if (i != 0)
contents.Append(", ");
- contents.Append(eventInfo.Type.GenericArgs[i]);
- contents.Append(" arg" + i);
+ contents.Append(eventInfo.Type.GenericArgs[i]).Append(" arg" + i);
}
contents.Append(')').AppendLine();
contents.Append(" {").AppendLine();
@@ -1112,11 +1112,11 @@ namespace Flax.Build.Bindings
contents.AppendFormat(" mmethod = {1}::GetStaticClass()->GetMethod(\"Internal_{0}_Invoke\", {2});", eventInfo.Name, classTypeNameNative, eventInfo.Type.GenericArgs.Count).AppendLine();
contents.Append(" CHECK(mmethod);").AppendLine();
contents.Append(" MonoObject* exception = nullptr;").AppendLine();
- if (eventInfo.Type.GenericArgs.Count == 0)
+ if (paramsCount == 0)
contents.AppendLine(" void** params = nullptr;");
else
- contents.AppendLine($" void* params[{eventInfo.Type.GenericArgs.Count}];");
- for (var i = 0; i < eventInfo.Type.GenericArgs.Count; i++)
+ contents.AppendLine($" void* params[{paramsCount}];");
+ for (var i = 0; i < paramsCount; i++)
{
var paramType = eventInfo.Type.GenericArgs[i];
var paramName = "arg" + i;
@@ -1139,7 +1139,7 @@ namespace Flax.Build.Bindings
contents.Append("bool bind)").AppendLine();
contents.Append(" {").AppendLine();
contents.Append(" Function(params, {paramsCount}), {classTypeNameNative}::TypeInitializer, StringView(TEXT(\"{eventInfo.Name}\"), {eventInfo.Name.Length}));");
+ contents.Append(" }").AppendLine().AppendLine();
+
+ // Scripting event wrapper binding method (binds/unbinds generic wrapper to C++ delegate)
+ contents.AppendFormat(" static void {0}_Bind(", eventInfo.Name);
+ contents.AppendFormat("{0}* obj, void* instance, bool bind)", classTypeNameNative).AppendLine();
+ contents.Append(" {").AppendLine();
+ contents.Append(" Function f;").AppendLine();
+ if (eventInfo.IsStatic)
+ contents.AppendFormat(" f.Bind<{0}_Wrapper>();", eventInfo.Name).AppendLine();
+ else
+ contents.AppendFormat(" f.Bind<{1}Internal, &{1}Internal::{0}_Wrapper>(({1}Internal*)instance);", eventInfo.Name, classTypeNameInternal).AppendLine();
+ contents.Append(" if (bind)").AppendLine();
+ contents.AppendFormat(" {0}{1}.Bind(f);", bindPrefix, eventInfo.Name).AppendLine();
+ contents.Append(" else").AppendLine();
+ contents.AppendFormat(" {0}{1}.Unbind(f);", bindPrefix, eventInfo.Name).AppendLine();
+ contents.Append(" }").AppendLine().AppendLine();
}
// Fields
@@ -1299,6 +1349,9 @@ namespace Flax.Build.Bindings
foreach (var eventInfo in classInfo.Events)
{
contents.AppendLine($" ADD_INTERNAL_CALL(\"{classTypeNameManagedInternalCall}::Internal_{eventInfo.Name}_Bind\", &{eventInfo.Name}_ManagedBind);");
+
+ // Register scripting event binder
+ contents.AppendLine($" ScriptingEvents::EventsTable[Pair({classTypeNameNative}::TypeInitializer, StringView(TEXT(\"{eventInfo.Name}\"), {eventInfo.Name.Length}))] = (void(*)(ScriptingObject*, void*, bool)){classTypeNameInternal}Internal::{eventInfo.Name}_Bind;");
}
foreach (var fieldInfo in classInfo.Fields)
{