跳到正文
cccskills
免费在 GitHub 公开

tools-unity-flowcanvas

FlowCanvas visual scripting patterns for abilities, custom nodes, and graph execution.

中文简介正在准备中。当前显示原文说明。

查看安装方法

包含的文件(1)

  • SKILL.md16.7 KB

SKILL.md(原文)

安装前可以查看交给 AI 代理的指令内容。

FlowCanvas (ParadoxNotion)

Overview

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.

When to Use

  • Ability visual scripting
  • Designer-friendly logic
  • Complex ability sequences
  • Event-driven ability flow
  • Reusable ability patterns

Core Concepts

FlowNode Structure

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

Custom Flow Nodes

Action Node

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

Wait Node (Async)

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

UniTask Wait Node

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

Event Node

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

Value Nodes

Getter Node

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

Math Node

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

Animation Integration

Play Animation Node

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

Wait For Animation Event

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

Targeting Nodes

Find Targets Node

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

Effect Application

Apply Gameplay Effect Node

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

MemoryPack Serialization

Custom Formatter for FlowCanvas

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

Best Practices

  1. Inherit from FN_AbilityNode for ability-specific nodes
  2. Handle cancellation in OnGraphStoped
  3. Use ValueInput defaults for optional parameters
  4. Validate inputs before execution
  5. Clean up coroutines and subscriptions
  6. Use async nodes for long operations
  7. Cache component references when possible
  8. Provide clear node names and descriptions
  9. Use categories for organization
  10. Test node interruption scenarios

Troubleshooting

IssueSolution
Node not appearingCheck Category attribute, rebuild
Flow not continuingVerify f.Call(exit) is called
Null referenceAdd null checks for inputs
Memory leakClean up in OnGraphStoped
Animation stuckHandle interruption properly
Variables not updatingCheck port registration

评价

还没有评价。欢迎分享你的使用感受。

评论

无需登录即可发表。欢迎分享使用感受、提问,或说明在哪些环境下无法运行。

还没有评论。来写第一条吧。

提交后会立即公开。请勿填写邮箱、电话等个人信息。URL 最多 2 个。

0 / 1000 字

此表单受 reCAPTCHA 保护,适用 Google 的隐私权政策和服务条款。

同一仓库的技能

adopt

免费

Brownfield onboarding — audits existing project artifacts for template format compliance (not just existence), classifies gaps by impact, and produces a numbered migration plan. Run this when joining an in-progress project or upgrading from an older template version. Distinct from /project-stage-detect (which checks what exists) — this checks whether what exists will actually work with the template's skills.

中文简介正在准备中。当前显示原文说明。

IdoCohen560/claude-unity-game-studio222026年7月8日 更新

Creates an Architecture Decision Record (ADR) documenting a significant technical decision, its context, alternatives considered, and consequences. Every major technical choice should have an ADR.

中文简介正在准备中。当前显示原文说明。

IdoCohen560/claude-unity-game-studio222026年7月8日 更新

Validates completeness and consistency of the project architecture against all GDDs. Builds a traceability matrix mapping every GDD technical requirement to ADRs, identifies coverage gaps, detects cross-ADR conflicts, verifies engine compatibility consistency across all decisions, and produces a PASS/CONCERNS/FAIL verdict. The architecture equivalent of /design-review.

中文简介正在准备中。当前显示原文说明。

IdoCohen560/claude-unity-game-studio222026年7月8日 更新

art-bible

免费

Guided, section-by-section Art Bible authoring. Creates the visual identity specification that gates all asset production. Run after /brainstorm is approved and before /map-systems or any GDD authoring begins.

中文简介正在准备中。当前显示原文说明。

IdoCohen560/claude-unity-game-studio222026年7月8日 更新

Audits game assets for compliance with naming conventions, file size budgets, format standards, and pipeline requirements. Identifies orphaned assets, missing references, and standard violations.

中文简介正在准备中。当前显示原文说明。

IdoCohen560/claude-unity-game-studio222026年7月8日 更新

Generate per-asset visual specifications and AI generation prompts from GDDs, level docs, or character profiles. Produces structured spec files and updates the master asset manifest. Run after art bible and GDD/level design are approved, before production begins.

中文简介正在准备中。当前显示原文说明。

IdoCohen560/claude-unity-game-studio222026年7月8日 更新

查看 IdoCohen560 的全部技能

报告此技能的问题