uw-scriptable-object-arch-c4960b — independently scanned and version-tracked by SaferSkills.
SaferSkills independently audited uw-scriptable-object-arch-c4960b (Agent Skill) and scored it 100/100 (green). The audit ran 55 deterministic rules across Security, Supply Chain, Maintenance, Transparency, and Community; it found 0 high-severity and 0 lower-severity findings. The full rule-by-rule trace and per-finding evidence are below. Free, methodology-open.
Findings & checks · 0 flagged
Every scanned point with the score it earned and what moved between them.
First recorded scan — no prior version to compare against.
The primary manifest — the file an agent reads to learn what this artifact does.
Generate SO patterns for data, events, and runtime collections.
docs/ProjectConfig.yaml for:architecture_pattern — "so-first" (default, this skill is primary) or "di-first" (SOs still hold data, but events become Commands — see uw-dependency-injection).mcp.unity_mcp — if true, call refresh_unity after creating files.docs/TDD_Template.md for data architecture decisions.docs/NAMING_CONVENTIONS.md for SO file naming (PascalCase: SwordData.asset)..asmdef — create with uw-unity-feature-scaffold if needed.Store game-tunable values in a ScriptableObject. Designers edit them in the Inspector without touching code.
using UnityEngine;
[CreateAssetMenu(fileName = "New WeaponData", menuName = "Game/Data/Weapon Data")]
public class WeaponData : ScriptableObject
{
[Header("Combat")]
[SerializeField] private float _damage = 10f;
[SerializeField] private float _fireRate = 0.5f;
[SerializeField] private float _range = 15f;
[Header("Ammo")]
[SerializeField] private int _maxAmmo = 30;
[SerializeField] private float _reloadTime = 1.5f;
[Header("Feel")]
[SerializeField] private ShakeProfile _hitShakeProfile;
[SerializeField] private AudioClip _fireSound;
[SerializeField] private GameObject _muzzleFlashPrefab;
// Read-only properties — game code reads, designers tune in Inspector
public float Damage => _damage;
public float FireRate => _fireRate;
public float Range => _range;
public int MaxAmmo => _maxAmmo;
public float ReloadTime => _reloadTime;
public ShakeProfile HitShakeProfile => _hitShakeProfile;
public AudioClip FireSound => _fireSound;
public GameObject MuzzleFlashPrefab => _muzzleFlashPrefab;
}Decouple systems by broadcasting events through a ScriptableObject asset. Any system can subscribe without knowing who raises the event.
Parameterless event:
using System;
using UnityEngine;
[CreateAssetMenu(fileName = "New GameEvent", menuName = "Game/Events/Game Event")]
public class GameEvent : ScriptableObject
{
private event Action _onRaise;
public void Raise() => _onRaise?.Invoke();
public void Subscribe(Action listener) => _onRaise += listener;
public void Unsubscribe(Action listener) => _onRaise -= listener;
}Typed event channels for events that carry data:
using System;
using UnityEngine;
[CreateAssetMenu(fileName = "New IntEvent", menuName = "Game/Events/Int Event")]
public class IntEvent : ScriptableObject
{
private event Action<int> _onRaise;
public void Raise(int value) => _onRaise?.Invoke(value);
public void Subscribe(Action<int> listener) => _onRaise += listener;
public void Unsubscribe(Action<int> listener) => _onRaise -= listener;
}
[CreateAssetMenu(fileName = "New FloatEvent", menuName = "Game/Events/Float Event")]
public class FloatEvent : ScriptableObject
{
private event Action<float> _onRaise;
public void Raise(float value) => _onRaise?.Invoke(value);
public void Subscribe(Action<float> listener) => _onRaise += listener;
public void Unsubscribe(Action<float> listener) => _onRaise -= listener;
}Usage:
// Publisher (doesn't know who listens)
[SerializeField] private IntEvent _onScoreChanged;
_onScoreChanged.Raise(newScore);
// Subscriber (doesn't know who publishes)
[SerializeField] private IntEvent _onScoreChanged;
private void OnEnable() => _onScoreChanged.Subscribe(UpdateScoreUI);
private void OnDisable() => _onScoreChanged.Unsubscribe(UpdateScoreUI);Track live objects (active enemies, collectibles, spawned projectiles) without FindObjectsOfType. Objects register themselves on spawn and unregister on destroy.
using System.Collections.Generic;
using UnityEngine;
[CreateAssetMenu(fileName = "New RuntimeSet", menuName = "Game/Sets/Runtime Set")]
public class RuntimeSet<T> : ScriptableObject
{
private readonly List<T> _items = new();
public IReadOnlyList<T> Items => _items;
public int Count => _items.Count;
public void Add(T item)
{
if (!_items.Contains(item))
_items.Add(item);
}
public void Remove(T item) => _items.Remove(item);
private void OnDisable() => _items.Clear(); // Prevent stale refs across play sessions
}Concrete sets (needed because Unity can't serialize open generics):
[CreateAssetMenu(fileName = "New EnemySet", menuName = "Game/Sets/Enemy Set")]
public class EnemyRuntimeSet : RuntimeSet<EnemyController> { }Self-registering pattern:
public class EnemyController : MonoBehaviour
{
[SerializeField] private EnemyRuntimeSet _activeEnemies;
private void OnEnable() => _activeEnemies.Add(this);
private void OnDisable() => _activeEnemies.Remove(this);
}| Pattern | Naming | Example |
|---|---|---|
| Data Container | {Type}Data | WeaponData, EnemyData |
| Event Channel | {Action}Event | OnDeathEvent, ScoreChangedEvent |
| Typed Event | {Type}Event | IntEvent, FloatEvent, StringEvent |
| Runtime Set | {Type}Set or {Type}RuntimeSet | EnemySet, CollectibleRuntimeSet |
uw-game-feel-integrator — SO data containers pair well with feel parameters (ShakeProfile, tween durations stored in the SO).uw-unity-test-runner — test event channels (subscribe, raise, verify callback) and runtime sets (add, remove, clear) in EditMode tests.uw-unity-editor-tools for designer-friendly SO inspectors (preview panels, validation).uw-dependency-injection — SO data stays, events become Commands.If your project needs any of these, check ProjectConfig.yaml -> architecture_pattern and consider switching to di-first with the uw-dependency-injection skill:
DI + SO coexist: In DI-first projects, ScriptableObjects still hold data — they're injected into the container as instances. Events are replaced by Commands.
[CreateAssetMenu] on every ScriptableObject.[SerializeField] private + public read-only properties for data fields.Action/Action<T> delegates for events, not UnityEvents (better performance, no boxing).OnDisable() to prevent stale references across play sessions.OnDisable — matching every Subscribe with an Unsubscribe..asmdef.NAMING_CONVENTIONS.md): SwordData.asset.ProjectConfig.yaml -> mcp.unity_mcp is true, call refresh_unity after creating files.~30 seconds. Free. No account. Every finding cites a rule and a line of evidence.