con un clic
tools-unity-flowcanvas
// FlowCanvas visual scripting patterns for abilities, custom nodes, and graph execution.
// FlowCanvas visual scripting patterns for abilities, custom nodes, and graph execution.
Mobile-specific Unity optimization patterns for memory, battery, thermal, and performance.
Unity Addressables patterns for asset loading, memory management, reference counting, and remote content delivery.
Unity animation patterns including Animancer, state machines, and performance optimization.
Behavior Designer patterns for AI behavior trees including task creation, shared variables, conditionals, and debugging.
Unity Cinemachine camera system patterns including virtual cameras, blending, and state-driven cameras.
Gameplay Ability System patterns for abilities, effects, attributes, and tags including recursion safety and lifecycle management.
| name | tools-unity-flowcanvas |
| description | FlowCanvas visual scripting patterns for abilities, custom nodes, and graph execution. |
FlowCanvas is a visual scripting solution used in Ungodly for ability graphs. This skill covers patterns for creating custom nodes, handling flow execution, and integrating with the ability system.
using FlowCanvas;
using ParadoxNotion.Design;
[Category("Abilities")]
[Description("Base ability flow node")]
public abstract class FN_AbilityNode : FlowNode
{
protected AbilityFlowScriptController Controller =>
(AbilityFlowScriptController)graph.agent;
protected AbilitySystemComponent AbilitySystem =>
Controller?.AbilitySystemComponent;
protected GameplayAbilitySpec AbilitySpec =>
Controller?.CurrentAbilitySpec;
}
[Category("Abilities/Actions")]
[Name("Apply Damage")]
[Description("Applies damage to targets")]
public class FN_ApplyDamage : FN_AbilityNode
{
[Input] public FlowInput enter;
[Output] public FlowOutput exit;
[Input] public ValueInput<List<GameObject>> targets;
[Input] public ValueInput<float> baseDamage;
[Input] public ValueInput<DamageType> damageType;
[Output] public ValueOutput<int> hitCount;
private int _hitCount;
protected override void RegisterPorts()
{
enter = Input("Enter", Flow);
exit = Output("Exit");
targets = Input<List<GameObject>>("Targets");
baseDamage = Input<float>("Base Damage", 10f);
damageType = Input<DamageType>("Damage Type", DamageType.Physical);
hitCount = Output<int>("Hit Count", () => _hitCount);
}
private void Flow(Flow f)
{
_hitCount = 0;
var targetList = targets.value;
if (targetList == null || targetList.Count == 0)
{
f.Call(exit);
return;
}
foreach (var target in targetList)
{
if (target == null) continue;
var damageReceiver = target.GetComponent<IDamageReceiver>();
if (damageReceiver != null)
{
var damageInfo = new DamageInfo
{
Damage = baseDamage.value,
DamageType = damageType.value,
Source = Controller.gameObject
};
damageReceiver.ApplyDamage(damageInfo);
_hitCount++;
}
}
f.Call(exit);
}
}
[Category("Abilities/Flow")]
[Name("Wait For Duration")]
[Description("Waits for specified duration")]
public class FN_WaitDuration : FN_AbilityNode
{
[Input] public FlowInput enter;
[Output] public FlowOutput exit;
[Input] public ValueInput<float> duration;
private Coroutine _waitCoroutine;
protected override void RegisterPorts()
{
enter = Input("Enter", Flow);
exit = Output("Exit");
duration = Input<float>("Duration", 1f);
}
private void Flow(Flow f)
{
if (_waitCoroutine != null)
{
Controller.StopCoroutine(_waitCoroutine);
}
_waitCoroutine = Controller.StartCoroutine(WaitRoutine(f));
}
private IEnumerator WaitRoutine(Flow f)
{
yield return new WaitForSeconds(duration.value);
_waitCoroutine = null;
f.Call(exit);
}
public override void OnGraphStoped()
{
if (_waitCoroutine != null)
{
Controller.StopCoroutine(_waitCoroutine);
_waitCoroutine = null;
}
}
}
[Category("Abilities/Flow")]
[Name("Async Wait")]
[Description("Async wait with cancellation support")]
public class FN_AsyncWait : FN_AbilityNode
{
[Input] public FlowInput enter;
[Output] public FlowOutput exit;
[Output] public FlowOutput cancelled;
[Input] public ValueInput<float> duration;
private CancellationTokenSource _cts;
protected override void RegisterPorts()
{
enter = Input("Enter", Flow);
exit = Output("Exit");
cancelled = Output("Cancelled");
duration = Input<float>("Duration", 1f);
}
private async void Flow(Flow f)
{
_cts?.Cancel();
_cts = new CancellationTokenSource();
try
{
await UniTask.Delay(
TimeSpan.FromSeconds(duration.value),
cancellationToken: _cts.Token
);
f.Call(exit);
}
catch (OperationCanceledException)
{
f.Call(cancelled);
}
}
public override void OnGraphStoped()
{
_cts?.Cancel();
_cts?.Dispose();
_cts = null;
}
}
[Category("Abilities/Events")]
[Name("On Gameplay Event")]
[Description("Triggers on gameplay event")]
public class FN_OnGameplayEvent : FN_AbilityNode, IUpdatable
{
[Output] public FlowOutput onEvent;
[Output] public ValueOutput<GameplayEventData> eventData;
[Input] public ValueInput<GameplayTag> eventTag;
private GameplayEventData _lastEventData;
private bool _eventReceived;
protected override void RegisterPorts()
{
onEvent = Output("On Event");
eventData = Output<GameplayEventData>("Event Data", () => _lastEventData);
eventTag = Input<GameplayTag>("Event Tag");
}
public override void OnGraphStarted()
{
AbilitySystem.OnGameplayEvent += HandleGameplayEvent;
}
public override void OnGraphStoped()
{
AbilitySystem.OnGameplayEvent -= HandleGameplayEvent;
}
private void HandleGameplayEvent(GameplayEventData data)
{
if (data.EventTag.Matches(eventTag.value))
{
_lastEventData = data;
_eventReceived = true;
}
}
public void Update()
{
if (_eventReceived)
{
_eventReceived = false;
onEvent.Call(new Flow());
}
}
}
[Category("Abilities/Values")]
[Name("Get Attribute")]
[Description("Gets an attribute value")]
public class FN_GetAttribute : PureFunctionNode<float>
{
[Input] public ValueInput<AttributeType> attribute;
[Input] public ValueInput<GameObject> target;
protected override void RegisterPorts()
{
attribute = Input<AttributeType>("Attribute");
target = Input<GameObject>("Target");
}
public override float Invoke()
{
if (target.value == null) return 0f;
var attrSet = target.value.GetComponent<AttributeSet>();
if (attrSet == null) return 0f;
return attrSet.GetAttributeValue(attribute.value);
}
}
[Category("Abilities/Math")]
[Name("Calculate Damage")]
[Description("Calculates final damage with modifiers")]
public class FN_CalculateDamage : PureFunctionNode<float>
{
[Input] public ValueInput<float> baseDamage;
[Input] public ValueInput<float> attackPower;
[Input] public ValueInput<float> damageMultiplier;
[Input] public ValueInput<float> critMultiplier;
[Input] public ValueInput<bool> isCrit;
protected override void RegisterPorts()
{
baseDamage = Input<float>("Base Damage", 10f);
attackPower = Input<float>("Attack Power", 100f);
damageMultiplier = Input<float>("Damage Mult", 1f);
critMultiplier = Input<float>("Crit Mult", 1.5f);
isCrit = Input<bool>("Is Crit", false);
}
public override float Invoke()
{
float damage = baseDamage.value * (1 + attackPower.value / 100f);
damage *= damageMultiplier.value;
if (isCrit.value)
{
damage *= critMultiplier.value;
}
return Mathf.Max(0, damage);
}
}
[Category("Abilities/Animation")]
[Name("Play Animation")]
[Description("Plays an Animancer animation")]
public class FN_PlayAnimation : FN_AbilityNode
{
[Input] public FlowInput enter;
[Output] public FlowOutput onComplete;
[Output] public FlowOutput onInterrupt;
[Input] public ValueInput<AnimationClip> clip;
[Input] public ValueInput<float> fadeTime;
[Input] public ValueInput<float> speed;
[Input] public ValueInput<bool> waitForComplete;
private AnimancerState _state;
protected override void RegisterPorts()
{
enter = Input("Enter", Flow);
onComplete = Output("Complete");
onInterrupt = Output("Interrupted");
clip = Input<AnimationClip>("Clip");
fadeTime = Input<float>("Fade Time", 0.25f);
speed = Input<float>("Speed", 1f);
waitForComplete = Input<bool>("Wait", true);
}
private void Flow(Flow f)
{
var clipValue = clip.value;
if (clipValue == null)
{
f.Call(onInterrupt);
return;
}
var animancer = Controller.GetComponent<AnimancerComponent>();
if (animancer == null)
{
f.Call(onInterrupt);
return;
}
_state = animancer.Play(clipValue, fadeTime.value);
_state.Speed = speed.value;
if (waitForComplete.value)
{
_state.Events.OnEnd = () =>
{
_state = null;
f.Call(onComplete);
};
}
else
{
f.Call(onComplete);
}
}
public override void OnGraphStoped()
{
if (_state != null && _state.IsPlaying)
{
_state.Stop();
_state = null;
}
}
}
[Category("Abilities/Animation")]
[Name("Wait Animation Event")]
[Description("Waits for a specific animation event")]
public class FN_WaitAnimationEvent : FN_AbilityNode
{
[Input] public FlowInput enter;
[Output] public FlowOutput onEvent;
[Input] public ValueInput<string> eventName;
private bool _waiting;
private Flow _currentFlow;
protected override void RegisterPorts()
{
enter = Input("Enter", Flow);
onEvent = Output("On Event");
eventName = Input<string>("Event Name", "Hit");
}
private void Flow(Flow f)
{
_waiting = true;
_currentFlow = f;
var eventReceiver = Controller.GetComponent<AnimationEventReceiver>();
if (eventReceiver != null)
{
eventReceiver.OnAnimationEvent += HandleAnimEvent;
}
}
private void HandleAnimEvent(string name)
{
if (!_waiting || name != eventName.value) return;
_waiting = false;
var eventReceiver = Controller.GetComponent<AnimationEventReceiver>();
if (eventReceiver != null)
{
eventReceiver.OnAnimationEvent -= HandleAnimEvent;
}
_currentFlow?.Call(onEvent);
}
public override void OnGraphStoped()
{
if (_waiting)
{
_waiting = false;
var eventReceiver = Controller.GetComponent<AnimationEventReceiver>();
eventReceiver?.OnAnimationEvent -= HandleAnimEvent;
}
}
}
[Category("Abilities/Targeting")]
[Name("Find Targets In Area")]
[Description("Finds targets in a spherical area")]
public class FN_FindTargetsInArea : FN_AbilityNode
{
[Input] public FlowInput enter;
[Output] public FlowOutput exit;
[Output] public FlowOutput noTargets;
[Input] public ValueInput<Vector3> center;
[Input] public ValueInput<float> radius;
[Input] public ValueInput<LayerMask> targetLayers;
[Input] public ValueInput<int> maxTargets;
[Output] public ValueOutput<List<GameObject>> targets;
private List<GameObject> _targets = new();
private Collider[] _hitColliders = new Collider[32];
protected override void RegisterPorts()
{
enter = Input("Enter", Flow);
exit = Output("Exit");
noTargets = Output("No Targets");
center = Input<Vector3>("Center");
radius = Input<float>("Radius", 5f);
targetLayers = Input<LayerMask>("Layers");
maxTargets = Input<int>("Max Targets", 10);
targets = Output<List<GameObject>>("Targets", () => _targets);
}
private void Flow(Flow f)
{
_targets.Clear();
int hitCount = Physics.OverlapSphereNonAlloc(
center.value,
radius.value,
_hitColliders,
targetLayers.value
);
for (int i = 0; i < hitCount && _targets.Count < maxTargets.value; i++)
{
var col = _hitColliders[i];
// Filter self
if (col.gameObject == Controller.gameObject)
continue;
// Validate target
if (col.TryGetComponent<IDamageReceiver>(out _))
{
_targets.Add(col.gameObject);
}
}
if (_targets.Count > 0)
{
f.Call(exit);
}
else
{
f.Call(noTargets);
}
}
}
[Category("Abilities/Effects")]
[Name("Apply Effect")]
[Description("Applies a gameplay effect to targets")]
public class FN_ApplyEffect : FN_AbilityNode
{
[Input] public FlowInput enter;
[Output] public FlowOutput exit;
[Output] public FlowOutput failed;
[Input] public ValueInput<GameplayEffectSO> effectDef;
[Input] public ValueInput<List<GameObject>> targets;
[Input] public ValueInput<float> level;
[Output] public ValueOutput<int> appliedCount;
private int _appliedCount;
protected override void RegisterPorts()
{
enter = Input("Enter", Flow);
exit = Output("Exit");
failed = Output("Failed");
effectDef = Input<GameplayEffectSO>("Effect");
targets = Input<List<GameObject>>("Targets");
level = Input<float>("Level", 1f);
appliedCount = Output<int>("Applied Count", () => _appliedCount);
}
private void Flow(Flow f)
{
_appliedCount = 0;
var effect = effectDef.value;
var targetList = targets.value;
if (effect == null || targetList == null || targetList.Count == 0)
{
f.Call(failed);
return;
}
foreach (var target in targetList)
{
if (target == null) continue;
var targetASC = target.GetComponent<AbilitySystemComponent>();
if (targetASC == null) continue;
var spec = AbilitySystem.MakeOutgoingSpec(effect, level.value);
if (targetASC.ApplyGameplayEffect(spec))
{
_appliedCount++;
}
}
if (_appliedCount > 0)
{
f.Call(exit);
}
else
{
f.Call(failed);
}
}
}
using MemoryPack;
[MemoryPackable]
public partial class SerializableFlowData
{
public string GraphJson;
public Dictionary<string, object> Variables;
[MemoryPackConstructor]
public SerializableFlowData(string graphJson, Dictionary<string, object> variables)
{
GraphJson = graphJson;
Variables = variables;
}
}
| Issue | Solution |
|---|---|
| Node not appearing | Check Category attribute, rebuild |
| Flow not continuing | Verify f.Call(exit) is called |
| Null reference | Add null checks for inputs |
| Memory leak | Clean up in OnGraphStoped |
| Animation stuck | Handle interruption properly |
| Variables not updating | Check port registration |