Install
$ agentstack add skill-tjboudreaux-cc-plugin-unity-gamedev-tools-unity-behavior-designer ✓ scanned · ✓ verified, works with Claude Code, Cursor, and more.
Security review
✓ PassedNo issues found. Passed automated security review. · v0.1.0 How review works →
- ✓ Prompt-injection patterns
- ✓ Secret / credential exfiltration
- ✓ Dangerous shell & filesystem operations
- ✓ Untrusted network calls
- ✓ Known-malicious package signatures
What it can access
- ✓ Network access No
- ✓ Filesystem access No
- ✓ Shell / process execution No
- ✓ Environment & secrets No
- ✓ Dynamic code execution No
From automated source analysis of v0.1.0. “Used” means the capability is present in the source — more access means more to trust, not that it’s unsafe.
Verified badge
Passed review? Show it. Paste this badge into your README, it links to the public security report.
Reliability & compatibility
Declared compatibility
Compatibility is declared by the source manifest. End-to-end runtime verification is coming, see below.
We're building live execution health for every listing: tool-call success rate, median latency, uptime, and last-checked timestamps, measured, not self-reported. It isn't live yet, so we don't show numbers we can't stand behind.
How agent discovery & health will work →About
Behavior Designer
Overview
Behavior Designer is a behavior tree implementation for Unity AI. This skill covers patterns for creating robust AI behaviors for enemies, NPCs, and game entities.
When to Use
- Enemy AI behavior
- NPC decision making
- Boss fight patterns
- Companion AI
- Any complex state-based AI
Core Concepts
Behavior Tree Structure
Selector (OR logic - first success wins)
├── Sequence (AND logic - all must succeed)
│ ├── Conditional (check condition)
│ └── Action (do something)
├── Sequence
│ ├── Conditional
│ └── Action
└── Action (fallback)
Custom Tasks
Action Task
using BehaviorDesigner.Runtime;
using BehaviorDesigner.Runtime.Tasks;
[TaskCategory("Combat")]
[TaskDescription("Attacks the current target")]
public class AttackTarget : Action
{
[Tooltip("Reference to the ability system")]
public SharedGameObject abilitySystemOwner;
[Tooltip("The ability to activate")]
public SharedString abilityTag;
[Tooltip("Target to attack")]
public SharedGameObject target;
private IAbilitySystem _abilitySystem;
private bool _abilityStarted;
public override void OnStart()
{
_abilityStarted = false;
if (abilitySystemOwner.Value == null)
{
Debug.LogError("AttackTarget: No ability system owner");
return;
}
_abilitySystem = abilitySystemOwner.Value.GetComponent();
}
public override TaskStatus OnUpdate()
{
if (_abilitySystem == null || target.Value == null)
{
return TaskStatus.Failure;
}
if (!_abilityStarted)
{
if (!_abilitySystem.TryActivateAbilityByTag(abilityTag.Value))
{
return TaskStatus.Failure;
}
_abilityStarted = true;
}
// Check if ability is still running
if (_abilitySystem.IsAbilityActive(abilityTag.Value))
{
return TaskStatus.Running;
}
return TaskStatus.Success;
}
public override void OnEnd()
{
// Cleanup if interrupted
if (_abilityStarted && _abilitySystem != null)
{
_abilitySystem.CancelAbilityByTag(abilityTag.Value);
}
}
}
Conditional Task
[TaskCategory("Combat")]
[TaskDescription("Checks if target is within attack range")]
public class IsTargetInRange : Conditional
{
public SharedGameObject target;
public SharedFloat attackRange;
public SharedTransform selfTransform;
public override TaskStatus OnUpdate()
{
if (target.Value == null || selfTransform.Value == null)
{
return TaskStatus.Failure;
}
float distance = Vector3.Distance(
selfTransform.Value.position,
target.Value.transform.position
);
return distance
{
public static implicit operator SharedAbilityData(AbilityData value)
{
return new SharedAbilityData { Value = value };
}
}
// Usage in task
public class UseAbility : Action
{
public SharedAbilityData ability;
public override TaskStatus OnUpdate()
{
if (ability.Value == null)
return TaskStatus.Failure;
// Use ability data
return TaskStatus.Success;
}
}
Global Variables
public class AIGlobalVariables : MonoBehaviour
{
public static AIGlobalVariables Instance { get; private set; }
private GlobalVariables _globalVariables;
private void Awake()
{
Instance = this;
_globalVariables = GlobalVariables.Instance;
}
public void SetPlayerReference(GameObject player)
{
_globalVariables.SetVariable("Player", (SharedGameObject)player);
}
public void SetCombatState(bool inCombat)
{
_globalVariables.SetVariable("GlobalCombatActive", (SharedBool)inCombat);
}
public GameObject GetPlayer()
{
var playerVar = _globalVariables.GetVariable("Player") as SharedGameObject;
return playerVar?.Value;
}
}
Common Patterns
Target Selection
[TaskCategory("Targeting")]
public class FindClosestEnemy : Action
{
public SharedGameObject result;
public SharedFloat searchRadius;
public SharedLayerMask targetLayers;
public SharedTransform selfTransform;
private Collider[] _hitColliders = new Collider[20];
public override TaskStatus OnUpdate()
{
if (selfTransform.Value == null)
return TaskStatus.Failure;
int hitCount = Physics.OverlapSphereNonAlloc(
selfTransform.Value.position,
searchRadius.Value,
_hitColliders,
targetLayers.Value
);
if (hitCount == 0)
{
result.Value = null;
return TaskStatus.Failure;
}
float closestDistance = float.MaxValue;
GameObject closest = null;
for (int i = 0; i ();
_transform = transform;
}
public override TaskStatus OnUpdate()
{
if (target.Value == null)
return TaskStatus.Failure;
if (_ai != null)
{
_ai.destination = target.Value.transform.position;
if (_ai.reachedEndOfPath)
return TaskStatus.Success;
return TaskStatus.Running;
}
// Fallback simple movement
Vector3 direction = (target.Value.transform.position - _transform.position).normalized;
_transform.position += direction * moveSpeed.Value * Time.deltaTime;
float distance = Vector3.Distance(
_transform.position,
target.Value.transform.position
);
return distance ();
if (waypoints.Value == null || waypoints.Value.Count == 0)
{
return;
}
SetDestinationToCurrentWaypoint();
}
public override TaskStatus OnUpdate()
{
if (waypoints.Value == null || waypoints.Value.Count == 0)
return TaskStatus.Failure;
if (_ai == null)
return TaskStatus.Failure;
// Check if reached waypoint
float distance = Vector3.Distance(
transform.position,
waypoints.Value[currentWaypointIndex.Value].position
);
if (distance currentWaypointIndex.Value)
{
_ai.destination = waypoints.Value[currentWaypointIndex.Value].position;
_ai.isStopped = false;
}
}
}
Combat State Machine
// Tree structure for combat AI
/*
Selector (Root)
├── Sequence [Flee when low health]
│ ├── IsHealthLow
│ └── FleeFromTarget
├── Sequence [Attack when in range]
│ ├── HasTarget
│ ├── IsTargetInRange
│ └── Selector [Choose attack]
│ ├── Sequence [Special attack if ready]
│ │ ├── IsSpecialReady
│ │ └── UseSpecialAttack
│ └── UseBasicAttack
├── Sequence [Chase target]
│ ├── HasTarget
│ └── MoveToTarget
└── Patrol [Default behavior]
*/
[TaskCategory("Combat")]
public class IsHealthLow : Conditional
{
public SharedFloat currentHealth;
public SharedFloat maxHealth;
public SharedFloat lowHealthThreshold = 0.2f;
public override TaskStatus OnUpdate()
{
float healthPercent = currentHealth.Value / maxHealth.Value;
return healthPercent ();
}
}
public override TaskStatus OnUpdate()
{
if (_asc == null)
return TaskStatus.Failure;
if (!_activated)
{
var spec = _asc.TryActivateAbilitiesByTag(
GameplayTag.FromString(abilityTag.Value)
);
if (spec == null)
return TaskStatus.Failure;
_activated = true;
if (!waitForCompletion.Value)
return TaskStatus.Success;
}
// Wait for ability to complete
if (_asc.HasActiveAbilityWithTag(GameplayTag.FromString(abilityTag.Value)))
{
return TaskStatus.Running;
}
return TaskStatus.Success;
}
}
Debugging
Behavior Tree Debugging
public class BehaviorTreeDebugger : MonoBehaviour
{
[SerializeField] private BehaviorTree _behaviorTree;
[SerializeField] private bool _logTaskChanges = true;
private void OnEnable()
{
if (_behaviorTree != null)
{
_behaviorTree.OnBehaviorStart += OnBehaviorStart;
_behaviorTree.OnBehaviorRestart += OnBehaviorRestart;
_behaviorTree.OnBehaviorEnd += OnBehaviorEnd;
}
}
private void OnDisable()
{
if (_behaviorTree != null)
{
_behaviorTree.OnBehaviorStart -= OnBehaviorStart;
_behaviorTree.OnBehaviorRestart -= OnBehaviorRestart;
_behaviorTree.OnBehaviorEnd -= OnBehaviorEnd;
}
}
private void OnBehaviorStart(Behavior behavior)
{
if (_logTaskChanges)
{
Debug.Log($"[BT] {name} Started");
}
}
private void OnBehaviorRestart(Behavior behavior)
{
if (_logTaskChanges)
{
Debug.Log($"[BT] {name} Restarted");
}
}
private void OnBehaviorEnd(Behavior behavior)
{
if (_logTaskChanges)
{
Debug.Log($"[BT] {name} Ended");
}
}
}
Best Practices
- Use shared variables - For data passing between tasks
- Keep tasks simple - Single responsibility
- Use conditional aborts - For responsive AI
- Cache component references - In OnStart
- Handle null gracefully - Return Failure on null
- Use task categories - Organize custom tasks
- Profile behavior trees - Can be expensive
- Use external trees - For reusable behaviors
- Document task descriptions - Use TaskDescription attribute
- Test edge cases - Target dies, interrupted, etc.
Troubleshooting
| Issue | Solution | |-------|----------| | Task never completes | Check for infinite Running state | | Variables not synced | Verify shared variable binding | | Abort not working | Check abort type setting | | Performance issues | Reduce tree complexity, cache refs | | Null reference | Add null checks in OnUpdate | | Task not found | Check TaskCategory attribute |
Source & license
This open-source skill is cataloged on AgentStack and links to its original source — we do not rehost the code.
- Author: tjboudreaux
- Source: tjboudreaux/cc-plugin-unity-gamedev
- License: MIT
Install and usage instructions live in the source repository linked above.
Reviews
No reviews yet, be the first.
Write a review
Versions
- v0.1.0 Imported from the upstream source.