Aspid.FastTools is a Unity toolset that eliminates routine boilerplate. Inside: a convenient SerializeReference workflow (an inspector type picker and a project-wide reference audit window), Roslyn source generators and analyzers, and runtime and editor utilities — from a serializable System.Type to fluent UI Toolkit extensions.
- Getting Started
- Installation — UPM git URL,
.unitypackage, Asset Store
- Installation — UPM git URL,
- Features
- Serializable Type System —
System.Typeas a serialized field, with a searchable type-picker window - SerializeReference Selector — a type-picker dropdown for
[SerializeReference]fields, plus tooling that finds and repairs broken references - ProfilerMarker — source-generated, per-call-site profiler markers
- Enum System — serializable enum → value maps,
[Flags]-aware - ID System (Beta) — asset-assignable names mapped to stable integer IDs
- VisualElement Extensions — fluent UI Toolkit tree building in code
- SerializedProperty Extensions — chainable typed setters and reflection helpers
- IMGUI Layout Scopes — disposable
Begin*/End*wrappers withRectaccess - Editor Helper Extensions — display names for scripts in custom editors
- Serializable Type System —
- Extras
- Claude Code Plugin — skills that teach Claude Code this package
- Donate
- License
Install Aspid.FastTools via UPM: in the Package Manager click + → Install package from git URL… and paste one of the URLs below.
Migrating from
com.aspid.fasttools: the package was renamed totech.aspid.fasttoolsin May 2026. Unity treats it as a different package, so installs of the old id never receive updates — remove thecom.aspid.fasttoolsentry fromPackages/manifest.jsonand installtech.aspid.fasttoolsvia one of the URLs below.
The upm branch always points to the latest stable release:
https://github.com/VPDPersonal/Aspid.FastTools.git#upm
To install a specific version, target the immutable per-release tag upm/<version> — e.g. upm/1.0.0 once the 1.0.0 release is out (see Releases for the list of available versions):
https://github.com/VPDPersonal/Aspid.FastTools.git#upm/<version>
Prefer a manual install? Download the .unitypackage from the Releases page, or get the package from the Unity Asset Store.
Preview
The upm-preview branch always points to the latest preview release (rc, beta, alpha, …):
https://github.com/VPDPersonal/Aspid.FastTools.git#upm-preview
Specific preview versions use the same per-release tag scheme:
https://github.com/VPDPersonal/Aspid.FastTools.git#upm-preview/1.0.0-rc.7
Allows serializing a System.Type reference in the Unity Inspector. The selected type is stored as an assembly-qualified name and resolved lazily on first access.
Two variants are available:
SerializableType— stores any typeSerializableType<T>— stores a type constrained toTor its subclasses
Both support implicit conversion to System.Type.
using UnityEngine;
using Aspid.FastTools.Types;
public abstract class Ability : MonoBehaviour
{
public abstract void Activate();
}
public sealed class AbilitySelector : MonoBehaviour
{
[SerializeField] private SerializableType<Ability> _abilityType;
private void Start()
{
var ability = (Ability)gameObject.AddComponent(_abilityType.Type);
ability.Activate();
}
}Adds a type-picker button next to a field in the Inspector: it opens a hierarchical, searchable window listing only the types compatible with the given base types (with several bases, a candidate must satisfy all of them; with no arguments, any type qualifies). What picking a type does depends on the field shape:
string— the field receives the assembly-qualified name of the chosen type;SerializableType/SerializableType<T>— narrows the built-in selector; the attribute's base types are intersected with the generic argumentT;- a
[SerializeReference]managed reference — the chosen type is instantiated into the field right away (see SerializeReference Selector).
The attribute is editor-only ([Conditional("UNITY_EDITOR")]) and carries no runtime cost.
using UnityEngine;
using Aspid.FastTools.Types;
public interface IStackable { }
public abstract class AbilityModifier
{
public abstract void Apply();
}
public sealed class AbilitySelector : MonoBehaviour
{
// string — stores the assembly-qualified name of the chosen type.
// Each element of the array is its own picker constrained to AbilityModifier.
[TypeSelector(typeof(AbilityModifier))]
[SerializeField] private string[] _modifierTypes;
// SerializableType — narrows the picker the field already has.
[TypeSelector(typeof(AbilityModifier))]
[SerializeField] private SerializableType _modifierType;
// SerializableType<T> — T already narrows the picker on its own; the base
// types of the attribute intersect with it: only AbilityModifier
// implementations that are also IStackable qualify.
[TypeSelector(typeof(IStackable))]
[SerializeField] private SerializableType<AbilityModifier> _stackableModifierType;
// For a [SerializeReference] field picking a type immediately creates
// an instance and assigns it to the field. With no arguments the attribute
// offers subtypes of the field's own type (here — AbilityModifier).
// Required = true flags an unset field: an inspector warning
// plus a violation for the build/CI gate.
[TypeSelector(Required = true)]
[SerializeReference] private AbilityModifier _modifier;
}Beyond the base types, the attribute exposes Allow (whether abstract classes and interfaces are listed) and Required (an unset field — an inline warning and a violation for the build/CI gate). Covered separately in the reference: dynamic base types via member references and tuning how a candidate type appears in the picker — or keeping it out of the picker entirely — with [TypeSelectorDisplay].
A searchable, namespace-hierarchical type-picker popup — the same picker opened by [TypeSelector] and SerializableType, also available as a public API. The window offers:
- Hierarchical namespace organization
- Text search with filtering
- Keyboard navigation (Arrow keys, Enter, Escape; Space toggles a favorite)
- Breadcrumb trail with back navigation (Left arrow or a click on a crumb)
- Assembly disambiguation for types with identical names
- Favorites (★ on hover) and Recent (last picks) sections on the root page — stored locally per project (
EditorPrefs, never committed), hidden while searching - A
<None>option pinned at the top and a ✓ mark on the current value — its row is pre-selected on open - Type counters on namespace/group rows and section headers
- Generic type support — picking an open generic walks through its type parameters and emits the constructed type
- Favorites/Recent tuning (on/off, Recent capacity) in the Settings tab of the SerializeReference window
Picking an open generic walks through its argument page and returns the constructed type:
The window is available as a public API (TypeSelectorWindow.Show) — open it from any editor code (custom inspectors, EditorWindow, menu items) when you need a type picker outside the standard SerializableType / [TypeSelector] flow. Signature and parameters: Types.md.
A serializable struct that renders a type-switching dropdown in the Inspector. Add it as a field to a base class — picking a subtype rewrites m_Script on the SerializedObject, effectively changing the component or ScriptableObject to the chosen subtype.
The dropdown is automatically constrained to subtypes of the class that declares the field. No additional configuration is required.
using UnityEngine;
using Aspid.FastTools.Types;
public abstract class EnemyBase : MonoBehaviour
{
[SerializeField] private ComponentTypeSelector _enemyType;
[SerializeField] [Min(0)] private float _health = 100f;
public abstract void Attack();
}
public sealed class FastEnemy : EnemyBase
{
[SerializeField] [Min(0)] private float _speed = 25f;
public override void Attack() =>
Debug.Log($"Fast enemy strikes! (speed: {_speed})");
}Notes on the type-switching dropdown's behavior:
- Because the dropdown owns type-switching, the Inspector's built-in Script row is hidden while the selector is present — you change the type only through the dropdown (UIToolkit inspectors only; the legacy IMGUI inspector draws that row itself).
Full reference: Types.md
A drop-in type-picker dropdown for [SerializeReference] fields. Add [TypeSelector] next to [SerializeReference] and the Inspector replaces the default managed-reference UI with a searchable, hierarchical type-picker window. You choose which concrete implementation of the field's type is instantiated, right in the Inspector; <None> clears the reference.
using System;
using UnityEngine;
using System.Collections.Generic;
using Aspid.FastTools.Types;
public interface IWeapon
{
void Fire();
}
[Serializable]
public sealed class Pistol : IWeapon
{
[SerializeField] [Min(0)] private int _damage = 10;
public void Fire() => Debug.Log($"Pistol: {_damage} dmg");
}
public sealed class Loadout : MonoBehaviour
{
[SerializeReference] [TypeSelector]
private IWeapon _primary;
[SerializeReference] [TypeSelector]
private List<IWeapon> _sidearms;
}The attribute is editor-only ([Conditional("UNITY_EDITOR")]) and carries no runtime cost. It works on single fields, arrays, and List<T>, in both IMGUI and UIToolkit inspectors. The same attribute also drives string and SerializableType fields — see TypeSelectorAttribute.
| Capability | What it does |
|---|---|
| Pick an implementation | The list shows the concrete, non-UnityEngine.Object classes assignable to the field's type. [TypeSelector(typeof(IMelee))] narrows it to IMelee implementations. |
| Inline inspector | The selected instance's serialized fields are drawn under a foldout. |
| Open generics | Modifier<T> and the like: arguments are inferred from a closed-generic field, or picked on a second page inside the picker. |
| Data preserved | Switching type carries over fields shared by name and serialized shape instead of resetting them to defaults. |
| Copy / Paste | Right-click the header to copy the value and paste it as an independent instance into any compatible field. |
| Multi-object editing | A mixed selection shows a mixed dropdown; picking a type or pasting applies an independent instance to each object in one Undo group. |
| Compile-time checks | Roslyn analyzer: AFT0004 (error) — the type derives from UnityEngine.Object; AFT0005 (warning) — the picker would be empty. |
A missing type (renamed or deleted) shows a yellow notice instead of a silent clear: Fix re-points the type while keeping its data, and Smart Fix suggests the most likely replacement ([MovedFrom], a moved namespace, a near-miss name) — applied in one click, never automatically. A shared reference (two fields holding one instance) is flagged and split with Make unique. Bulk repair lives in the Asset References and Project References tabs (Tools → Aspid 🐍 → FastTools): the first maps an asset's whole managed-reference graph from its YAML, the second scans every .prefab / .asset / .unity in the project, fixes broken references group-by-group and bakes [MovedFrom] renames into the files.
Project Settings → Aspid FastTools → SerializeReference controls breakage detection, auto de-aliasing of duplicated list elements, excluded scan folders, and the build/CI gate (Off / Warn / Fail) — committed values live in ProjectSettings/SerializeReferenceSharedSettings.asset, so teammates and CI behave identically. The same options are mirrored in the window's Settings tab and at Preferences → Aspid FastTools; headless CI runs the same check via SerializeReferenceCiGate.RunCheck.
Full reference: SerializeReferences.md · window tabs, settings & CI gate: SerializeReferenceTooling.md
Provides source-generated ProfilerMarker registration. The generator creates a static marker per call-site, identified by the calling method and line number.
using UnityEngine;
public class MyBehaviour : MonoBehaviour
{
private void DoSomething1()
{
using var _ = this.Marker();
// Some code
}
private void DoSomething2()
{
using (this.Marker())
{
// Some code
using var _ = this.Marker().WithName("Calculate");
// Some code
}
}
}Generated code
using Unity.Profiling;
using System.Runtime.CompilerServices;
internal static class __MyBehaviourProfilerMarkerExtensions
{
private static readonly ProfilerMarker DoSomething1_Marker_Line_7 = new("MyBehaviour.DoSomething1 (7)");
private static readonly ProfilerMarker DoSomething2_Marker_Line_13 = new("MyBehaviour.DoSomething2 (13)");
private static readonly ProfilerMarker DoSomething2_Marker_Line_16 = new("MyBehaviour.Calculate (16)");
public static ProfilerMarker.AutoScope Marker(this MyBehaviour _, [CallerLineNumberAttribute] int line = -1)
{
#if ENABLE_PROFILER
if (line is 7) return DoSomething1_Marker_Line_7.Auto();
if (line is 13) return DoSomething2_Marker_Line_13.Auto();
if (line is 16) return DoSomething2_Marker_Line_16.Auto();
#endif
return default;
}
}Provides serializable enum-to-value mappings configurable from the Inspector.
A serializable collection of EnumValue<TValue> entries with a configurable default value. Implements IEnumerable<KeyValuePair<Enum, TValue>>.
GetValue returns the mapped value, falling back to the configured default when the key is missing. [Flags] enums are supported: matching uses HasFlag and treats 0-valued members correctly.
using System;
using UnityEngine;
using Aspid.FastTools.Enums;
public enum DamageType { Physical, Fire, Ice, Poison }
[Flags]
public enum StatusEffect { None = 0, Burning = 1, Frozen = 2, Slowed = 4, Stunned = 8 }
public sealed class DamageDealer : MonoBehaviour
{
[SerializeField] private EnumValues<float> _damageMultipliers;
// Flag combinations (e.g. Burning | Slowed) match via HasFlag and first-hit wins,
// so list composite entries BEFORE their constituent flags.
[SerializeField] private EnumValues<float> _speedMultipliersByStatus;
public float GetMultiplier(DamageType type) => _damageMultipliers.GetValue(type);
public float GetSpeedModifier(StatusEffect effects) => _speedMultipliersByStatus.GetValue(effects);
}In the Inspector, select the enum type in the EnumValues header, then assign a value for each enum member. Right-click the property to open a context menu with Populate Missing Enum Members — it appends an entry for every enum member not yet in the list, seeded with the current Default Value.
The typed counterpart of EnumValues<TValue> for the common case where the enum type is already known in code. The enum is fixed by the generic argument, so the Inspector's type picker is disabled and lookups are compile-time safe. Lookups are also boxing-free — keys are compared as cached numeric values — and foreach over either variant binds to a struct enumerator, so iteration does not allocate. Implements IEnumerable<KeyValuePair<TEnum, TValue>>.
public sealed class HitEffect : MonoBehaviour
{
// The type picker in the Inspector is disabled — the enum is fixed to DamageType.
[SerializeField] private EnumValues<DamageType, Color> _damageColors;
public Color GetColor(DamageType type) => _damageColors.GetValue(type);
}Lookup semantics (including [Flags] handling) are identical to EnumValues<TValue>.
Beta: the ID System is currently in beta. The public API, generated code layout and editor workflow may change in future releases.
Maps asset-assignable names to stable integer IDs stored in an IdRegistry ScriptableObject, with full int ↔ string lookups at runtime. Use the resulting int in switch statements and Dictionary keys without paying for string comparisons.
1. Declare a partial struct implementing IId. The source generator adds the required fields and property automatically:
using Aspid.FastTools.Ids;
public partial struct EnemyId : IId { }Generated code
public partial struct EnemyId
{
[SerializeField] private string __stringId; // editor-only field, stripped from player builds
[SerializeField] private int _id;
public int Id => _id;
}Misuse is caught at compile time by generator diagnostics (AFID001 — missing partial, AFID002 — a colliding member); generic structs and containing types are supported.
2. Create the registry asset and bind it to the struct type in its Inspector:
Assets → Create → Aspid → Id Registry
3. Use the struct as a serialized field. The Inspector shows a dropdown of registered names; the selector window also lets you create new entries on the fly:
using UnityEngine;
using Aspid.FastTools.Ids;
[CreateAssetMenu]
public class EnemyDefinition : ScriptableObject
{
[UniqueId] [SerializeField] private EnemyId _id;
}
public class EnemySpawner : MonoBehaviour
{
[SerializeField] private EnemyId _targetEnemy;
private void Spawn()
{
int id = _targetEnemy.Id; // stable integer, safe for switch / Dictionary
}
}Marks a field as requiring a unique value across all assets of the declaring type. The Inspector shows a warning if two assets share the same ID.
[Conditional("UNITY_EDITOR")]
public sealed class UniqueIdAttribute : PropertyAttribute { }ScriptableObject in Aspid.FastTools.Ids that stores (int, string) entries and keeps the lookup tables available at runtime. Each name is assigned a stable, auto-incrementing ID that never changes when other entries are added or removed.
Runtime lookups cover both directions — TryGetId / TryGetName, Contains, enumeration of (id, name) pairs — and the generic counterpart IdRegistry<T> adds typed overloads. Entries are added, renamed and removed only through the registry inspector, not a public runtime API.
Full reference: Ids.md
Fluent extension methods for building UIToolkit trees in code. All methods return T (the element itself) for chaining.
A reactive editor for an AbilityConfig ScriptableObject — title and status pill in the header, and a Warning HelpBox that toggles based on ManaCost.
[CustomEditor(typeof(AbilityConfig))]
internal sealed class AbilityConfigEditor : Editor
{
public override VisualElement CreateInspectorGUI()
{
var config = (AbilityConfig)target;
var badge = new Label()
.SetFontSize(10).SetUnityFontStyleAndWeight(FontStyle.Bold)
.SetPaddingX(10).SetPaddingY(3).SetBorderRadius(10).SetBorderWidth(1);
var helpBox = new HelpBox("This ability costs no mana — is that intentional?", HelpBoxMessageType.Warning)
.SetMarginTop(8).SetBorderRadius(6);
Refresh();
return new VisualElement()
.SetBorderRadius(10).SetBorderWidth(1).SetPaddingX(14).SetPaddingY(12)
.AddChild(new VisualElement()
.SetFlexDirection(FlexDirection.Row).SetAlignItems(Align.Center)
.AddChild(new Label(target.GetScriptName()).SetFlexGrow(1).SetFontSize(15))
.AddChild(badge))
.AddChild(new PropertyField(serializedObject.FindProperty("_manaCost")).AddValueChanged(_ => Refresh()))
.AddChild(helpBox);
void Refresh()
{
var isFree = config.ManaCost is 0;
badge.SetText(isFree ? "FREE" : $"{config.ManaCost} MP");
helpBox.SetDisplay(isFree ? DisplayStyle.Flex : DisplayStyle.None);
}
}
}Full reference: VisualElementExtensions.md
Chainable extensions on SerializedProperty for synchronizing the owning SerializedObject, writing typed values, and reflecting on the underlying field.
property
.Update()
.SetVector3(Vector3.up)
.SetBool(true)
.ApplyModifiedProperties();The package covers:
- Update / Apply —
Update,UpdateIfRequiredOrScript,ApplyModifiedProperties. - Typed setters —
SetValue(generic dispatch) andSetXxxfor every common Unity-serializable type, from primitives toGradientandAnimationCurve— each with a pairedSetXxxAndApplyvariant. - Enum setters —
SetEnumFlagandSetEnumIndex(each +AndApply). - Arrays —
SetArraySize,AddArraySize,RemoveArraySize(each +AndApply). - References —
SetManagedReference,SetObjectReference,SetExposedReference, andSetBoxed(Unity 6+). - Reflection helpers —
GetPropertyType,GetFieldInfo,GetDeclaringInstancefor resolving the C# member and runtime instance behind a property.
Full reference: SerializedPropertyExtensions.md
Three ref struct scopes — VerticalScope, HorizontalScope, ScrollViewScope — wrap EditorGUILayout.Begin* / End*. Each exposes a Rect property and calls the matching End* method on Dispose:
using (VerticalScope.Begin())
{
EditorGUILayout.LabelField("Item 1");
EditorGUILayout.LabelField("Item 2");
}
var scrollPos = Vector2.zero;
using (ScrollViewScope.Begin(ref scrollPos))
{
EditorGUILayout.LabelField("Scrollable content");
}All Begin overloads match the corresponding EditorGUILayout.Begin* signatures (optional GUIStyle, GUILayoutOption[], scroll view options), plus an out Rect variant for drawing into the group's rect.
Display-name helpers for Unity objects in custom editors:
| Method | Returns |
|---|---|
GetScriptName() |
The object's display name — ObjectNames.GetInspectorTitle when the type has [AddComponentMenu], otherwise the nicified type name |
GetScriptNameWithIndex() |
The same name plus a count suffix when the GameObject holds several components of the same type — e.g. "Audio Source (2)" |
[CustomEditor(typeof(MyBehaviour))]
public class MyBehaviourEditor : Editor
{
public override VisualElement CreateInspectorGUI()
{
// "My Behaviour" — or "Custom Name" if [AddComponentMenu("Custom Name")] is present
var name = target.GetScriptName();
// "My Behaviour (2)" when a second component of the same type exists
var nameWithIndex = ((Component)target).GetScriptNameWithIndex();
return new Label(name);
}
}If you use Claude Code, the companion Aspid.Claude.Plugins marketplace ships the aspid-fasttools plugin — a set of skills that teach Claude Code this package's conventions and APIs.
Warning
The plugin is still in beta — its skills and commands may change between releases.
Add the marketplace and install the plugin:
/plugin marketplace add VPDPersonal/Aspid.Claude.Plugins/plugin install aspid-fasttools@aspid-claude-pluginsIncluded skills:
aspid-id-struct— scaffold a newIIdstruct and[UniqueId]fields for the ID System.aspid-profiler-marker— insertthis.Marker()call sites with the rightusing/scope shape.aspid-visual-element-fluent— build editor or runtime UI using the fluentVisualElementextensions.
This project is developed on a voluntary basis. If you find it useful, you can support its development by purchasing the package on the Unity Asset Store — that helps allocate more time to improving and maintaining Aspid.FastTools.
Aspid.FastTools is distributed under the MIT License. Release history lives in the CHANGELOG.










