Add scripting API events in Visual Script

This commit is contained in:
Wojtek Figat
2021-01-22 10:55:13 +01:00
parent 4042d466d3
commit cbbfb16628
11 changed files with 708 additions and 38 deletions

View File

@@ -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
}
/// <summary>
/// Gets the method parameters count (valid for methods only).
/// Gets the method parameters count (valid for methods and events only).
/// </summary>
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;

View File

@@ -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<string> _tooltips = new List<string>();
private List<uint> _functionNodesIds = new List<uint>();
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();
}
/// <inheritdoc />
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();
}
}
/// <summary>
/// The nodes for that group.
/// </summary>
@@ -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),
}
},
};
}
}

View File

@@ -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)
{

View File

@@ -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))

View File

@@ -331,12 +331,13 @@ namespace FlaxEditor.Surface
/// <param name="typeID">The node archetype ID.</param>
/// <param name="location">The location.</param>
/// <param name="customValues">The custom values array. Must match node archetype <see cref="NodeArchetype.DefaultValues"/> size. Pass null to use default values.</param>
/// <param name="beforeSpawned">The custom callback action to call after node creation but just before invoking spawn event. Can be used to initialize custom node data.</param>
/// <returns>Created node.</returns>
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<SurfaceNode> 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
/// <param name="nodeArchetype">The node archetype.</param>
/// <param name="location">The location.</param>
/// <param name="customValues">The custom values array. Must match node archetype <see cref="NodeArchetype.DefaultValues"/> size. Pass null to use default values.</param>
/// <param name="beforeSpawned">The custom callback action to call after node creation but just before invoking spawn event. Can be used to initialize custom node data.</param>
/// <returns>Created node.</returns>
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<SurfaceNode> 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);

View File

@@ -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<string, ushort>(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<NodeArchetype>(),
};
_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<NodeArchetype>)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<NodeArchetype>)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();

View File

@@ -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<ScriptingTypeHandle, StringView>(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<Variant> 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<VisualScriptingBinaryModule, &VisualScriptingBinaryModule::OnScriptsReloading>(&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<Variant>& 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<VisualScript>(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;
}
}

View File

@@ -125,9 +125,22 @@ public:
StringAnsi Name;
};
struct EventBinding
{
ScriptingTypeHandle Type;
String Name;
Array<Method*, InlinedAllocation<4>> BindedMethods;
};
struct Instance
{
Array<Variant> Params;
Array<EventBinding> EventBindings;
};
private:
Dictionary<Guid, Array<Variant>> _instances;
Dictionary<Guid, Instance> _instances;
ScriptingTypeHandle _scriptingTypeHandle;
ScriptingTypeHandle _scriptingTypeHandleCached;
StringAnsiView _typename;
@@ -179,6 +192,13 @@ public:
/// <returns>The created instance or null if failed.</returns>
API_FUNCTION() ScriptingObject* CreateInstance();
/// <summary>
/// Gets the Visual Script instance data.
/// </summary>
/// <param name="instance">The object instance.</param>
/// <returns>The data or invalid instance (not VS or missing).</returns>
Instance* GetScriptInstance(ScriptingObject* instance) const;
/// <summary>
/// Gets the value of the Visual Script parameter of the given instance.
/// </summary>
@@ -307,6 +327,7 @@ private:
#if USE_EDITOR
void OnScriptsReloading();
#endif
static void OnEvent(ScriptingObject* object, Span<Variant>& parameters, const ScriptingTypeHandle& eventType, const StringView& eventName);
public:

View File

@@ -14,6 +14,10 @@
#include "MException.h"
#include "Scripting.h"
#include "StdTypesContainer.h"
#include "Events.h"
Dictionary<Pair<ScriptingTypeHandle, StringView>, void(*)(ScriptingObject*, void*, bool)> ScriptingEvents::EventsTable;
Delegate<ScriptingObject*, Span<Variant>&, const ScriptingTypeHandle&, const StringView&> ScriptingEvents::Event;
ManagedBinaryModule* GetBinaryModuleCorlib()
{

View File

@@ -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"
/// <summary>
/// The helper utility for binding and invoking scripting events (eg. used by Visual Scripting).
/// </summary>
class FLAXENGINE_API ScriptingEvents
{
public:
/// <summary>
/// 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).
/// </summary>
/// <remarks>
/// 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.
/// </remarks>
static Dictionary<Pair<ScriptingTypeHandle, StringView>, void(*)(ScriptingObject*, void*, bool)> EventsTable;
/// <summary>
/// The action called when any scripting event occurs. Can be used to invoke scripting code that binded to this particular event.
/// </summary>
/// <remarks>
/// Delegate parameters: event caller instance (null for static events), event invocation parameters list, event type name (full), event name.
/// </remarks>
static Delegate<ScriptingObject*, Span<Variant>&, const ScriptingTypeHandle&, const StringView&> Event;
};

View File

@@ -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<void(");
for (var i = 0; i < eventInfo.Type.GenericArgs.Count; i++)
for (var i = 0; i < paramsCount; i++)
{
if (i != 0)
contents.Append(", ");
@@ -1156,6 +1156,56 @@ namespace Flax.Build.Bindings
contents.Append(" else").AppendLine();
contents.AppendFormat(" {0}{1}.Unbind(f);", bindPrefix, eventInfo.Name).AppendLine();
contents.Append(" }").AppendLine().AppendLine();
// Generic scripting event invoking wrapper (calls scripting code from C++ delegate)
CppIncludeFiles.Add("Engine/Scripting/Events.h");
contents.Append(" ");
if (eventInfo.IsStatic)
contents.Append("static ");
contents.AppendFormat("void {0}_Wrapper(", eventInfo.Name);
for (var i = 0; i < paramsCount; i++)
{
if (i != 0)
contents.Append(", ");
contents.Append(eventInfo.Type.GenericArgs[i]).Append(" arg" + i);
}
contents.Append(')').AppendLine();
contents.Append(" {").AppendLine();
if (paramsCount == 0)
contents.AppendLine(" Variant* params = nullptr;");
else
contents.AppendLine($" Variant params[{eventInfo.Type.GenericArgs.Count}];");
for (var i = 0; i < paramsCount; i++)
{
var paramType = eventInfo.Type.GenericArgs[i];
var paramName = "arg" + i;
var paramValue = GenerateCppWrapperNativeToVariant(buildData, paramType, classInfo, paramName);
contents.Append($" params[{i}] = {paramValue};").AppendLine();
}
contents.AppendLine($" ScriptingEvents::Event({(eventInfo.IsStatic ? "nullptr" : "(ScriptingObject*)this")}, Span<Variant>(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<void(");
for (var i = 0; i < paramsCount; i++)
{
if (i != 0)
contents.Append(", ");
contents.Append(eventInfo.Type.GenericArgs[i]);
}
contents.Append(")> 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<ScriptingTypeHandle, StringView>({classTypeNameNative}::TypeInitializer, StringView(TEXT(\"{eventInfo.Name}\"), {eventInfo.Name.Length}))] = (void(*)(ScriptingObject*, void*, bool)){classTypeNameInternal}Internal::{eventInfo.Name}_Bind;");
}
foreach (var fieldInfo in classInfo.Fields)
{