Skip to main content

PoolModule

Tsvrc.Editor.PoolModule is the generator behind WirePoolAttribute: it decides how many instances of each pooled prefab a project actually needs, generates one serialized slot field per instance, and instantiates and wires them all under a "Pool" child at wire time. This is the densest of the nine modules TsGenerator.CreateModules() registers: the slot-count math is a real dependency graph, not a flat per-type count.

Usage

Register the prefab's type as a pool entry (Configure window, Pools tab), then declare a field for it wherever an instance is needed:

[WirePool][SerializeField] private RoundTimer _roundTimer;

Wire() fills _roundTimer in with one of the generated slot instances, nothing else to call. A pooled type that itself needs another pooled type follows the same pattern one level down: put a [WirePool] field on the pooled behaviour itself, and the dependency-graph math above accounts for it automatically.

Registering a pool type

TsConfig.PoolEntries/TsBuiltinConfig.PoolEntries register which UdonSharpBehaviour prefabs are poolable at all. Registration alone creates zero slots: a type with no [WirePool] field anywhere referencing it resolves to TotalSlots == 0 and is silently skipped in generated output. Slots exist only because something asked for them.

How many slots: a dependency graph, not a flat count

A [WirePool] field can appear two places: on an ordinary scene behaviour (an external reference, "give me one instance of this pooled type"), or on another pooled type itself (an internal dependency, "each instance of pool type P needs its own instance of pool type Q"). The total slot count for type T is:

totalSlots(T) = externalCount(T) + Σ_P (fields-in-P-referencing-T) × totalSlots(P)

computed via a topological walk (ComputeForType, memoized, cycle-detected) so a parent pool type's own slot count is always resolved before a child dependency's count that depends on it. A genuine circular dependency between pool types (P needs Q, Q needs P) is detected and logs an error, excluding the offending type from generation rather than recursing forever.

Every [WirePool] field whose type was never registered as a pool entry logs a warning (deduplicated per declaring-type-plus-field, so a scene with many instances of the same behaviour only logs once). Without it, that field would just stay null forever at runtime with no indication why, disconnecting the symptom from a forgotten registration.

Generated shape and wiring

Each slot becomes [HideInInspector][SerializeField] private {Type} _pool_{Type}_{index};, and _TsPoolStart() calls TsConstruct(this) on every slot whose type is a TsvrcBehaviour. At wire time, Wire() instantiates every slot under a "Pool" child (created lazily, only once a slot actually needs it), assigns each instance to its own scaffold field, then makes a second pass to assign every [WirePool] field across the scene, including internal dependencies inside other pool instances themselves, to its matching freshly-created slot. A slot/target-count mismatch (more or fewer [WirePool] targets than generated slots) is a warning, not a hard failure, since either mismatch usually self-heals on the very next pass once the scene or field declarations catch up.

Wiring is skipped, not partially applied, on a broken compile

Unlike Global, Factory, and Construct, none of which destroy existing scene state on empty input, PoolModule.Wire() has real destructive potential: an empty-looking _poolEntries would otherwise tear down a genuinely populated "Pool" container. Because TotalSlots itself comes from a live, scene-wide reflection scan (ScanExternalRefs/ScanInternalDeps) that's just as fragile to a broken compile as any other live scan, a pass that had to fall back to the cached snapshot (_usedSnapshotFallback) makes Wire() do nothing at all, leaving whatever is already wired in the scene completely alone until a clean compile restores a trustworthy live count.

Already-wired detection

Before tearing anything down, Wire() checks IsPoolAlreadyWired: matching slot count, matching prefab source per slot (via PrefabUtility.GetCorrespondingObjectFromSource), and matching field assignments, and skips the whole rebuild if everything already matches. This avoids destroying and recreating every pool instance (losing any in-scene state on them) on a pass where nothing about the pool configuration actually changed.