Skip to content

Pool Oracle - Prefab Library & Entity Ecosystem Design

Pool Oracle - Prefab Library & Entity Ecosystem Design

Section titled “Pool Oracle - Prefab Library & Entity Ecosystem Design”

This document defines the architecture for RentEarth’s prefab library and entity ecosystem, establishing a single source of truth for all game objects that can be instantiated at runtime. The system integrates with the existing Bakery recipe system and supports both direct prefab references and Addressables-based loading.

Note: Implementation files are located in Assets/Scripts/Pool/Oracle/ namespace RentEarth.Pool.Oracle. The “Oracle” name was chosen because “Library” is a reserved Unity folder that gets gitignored.


flowchart TB
    subgraph BAKERY["BAKERY SYSTEM"]
        PPR[PoolPrefabRecipe<br/>Editor UI]
        TCR[ThemeConfigRecipe]
        SCR[SupabaseConfig<br/>Recipe]

        PPR --> PPC

        subgraph CONFIG["ScriptableObject"]
            PPC[PoolPrefabConfig.asset<br/>- characterPrefabs<br/>- entityPrefabs<br/>- environmentPrefabs<br/>- resourcePrefabs]
        end
    end

    subgraph RUNTIME["RUNTIME SYSTEM"]
        PS[PoolService<br/>Pure C# Logic]
        PB[PoolBehaviour<br/>MonoBehaviour<br/>Transform containers]

        PB --> PS

        PS --> OP[ObjectPool&lt;T&gt;<br/>- Prewarm, Get, Return, Clear<br/>- Active entity tracking by networkId]
    end

    PPC --> PS
CategoryProto EnumCurrent Values
CharacterCharacterTypeKnight, Barbarian, Mage, Rogue, RogueHooded
EntityEntityTypePlayer, NPC, Enemy, Boss
EnvironmentEnvironmentObjectTypeTree, Rock, Bush, Grass
ResourceResourceTypeWood, Stone, Berries, Herbs, None

Create a centralized registry that extends beyond the current config to support:

  • Prefab Metadata: Display name, description, tags, preview thumbnails
  • Variant Chains: Define fallback variants automatically
  • Route Paths: Standardized asset paths for discoverability
  • Validation Rules: Custom validation per prefab type
[CreateAssetMenu(fileName = "PrefabRegistry", menuName = "KBVE/Pool/Prefab Registry")]
public class PrefabRegistry : ScriptableObject
{
[Header("Registry Metadata")]
public string registryId; // Unique identifier
public string displayName; // Human-readable name
public string description; // Purpose description
[Header("Prefab Collections")]
public List<PrefabDefinition> prefabs;
[Header("Route Configuration")]
public string basePath = "Assets/Prefabs"; // Default search path
public List<RouteMapping> routeMappings; // Category -> path mappings
}
[Serializable]
public class PrefabDefinition
{
public string prefabId; // Unique string ID (e.g., "char_knight_v0")
public PoolCategory category; // Entity, Environment, Character, Resource
public int typeId; // Proto enum value
public int variantId; // 0 = default
[Header("Asset Reference")]
public GameObject prefab; // Direct reference (Editor)
public string addressableKey; // Addressables key (Runtime)
[Header("Metadata")]
public string displayName; // "Knight (Default)"
public string description; // Tooltip/docs
public string[] tags; // Searchable tags
public Sprite thumbnail; // Preview image
[Header("Variant Info")]
public int fallbackVariantId = -1; // -1 = no fallback, 0+ = fallback to variant
public bool isDefaultVariant; // True if this is the default for type
[Header("Pool Settings")]
public int poolSize = 10; // Initial pool size
public int maxPoolSize = 50; // Maximum pool size
public bool prewarmOnLoad = true; // Include in prewarm
}
[Serializable]
public class RouteMapping
{
public PoolCategory category;
public string relativePath; // e.g., "Characters", "Environment/Trees"
public string addressableLabel; // Addressables label for batch loading
}

Define standardized paths for prefab organization:

Assets/Prefabs/
├── Characters/
│ ├── Player/
│ │ ├── Knight/
│ │ │ ├── Knight_v0.prefab (default)
│ │ │ ├── Knight_v1.prefab (variant 1)
│ │ │ └── Knight_v2.prefab (variant 2)
│ │ ├── Barbarian/
│ │ ├── Mage/
│ │ └── Rogue/
│ └── NPC/
│ └── [NPC prefabs...]
├── Entities/
│ ├── Player/
│ ├── Enemy/
│ └── Boss/
├── Environment/
│ ├── Trees/
│ │ ├── Tree_Oak_v0.prefab
│ │ ├── Tree_Pine_v0.prefab
│ │ └── ...
│ ├── Rocks/
│ ├── Bushes/
│ └── Grass/
└── Resources/
├── Wood/
├── Stone/
├── Berries/
└── Herbs/

3. Bakery Integration: PrefabLibraryRecipe

Section titled “3. Bakery Integration: PrefabLibraryRecipe”

New recipe that extends PoolPrefabRecipe with:

public class PrefabLibraryRecipe : IRecipe
{
public string Name => "Prefab Library";
public string Description => "Manage prefab registry, routes, and pool configurations";
// Features:
// 1. Registry Browser - View/edit all registered prefabs
// 2. Route Scanner - Auto-discover prefabs from route paths
// 3. Validation Dashboard - Check for missing/broken references
// 4. Bulk Operations - Batch update pool settings
// 5. Export/Import - JSON/YAML serialization for CI/CD
}
flowchart TB
    subgraph PREFAB_LIBRARY["PREFAB LIBRARY"]
        subgraph SIDEBAR["Routes Panel"]
            R1[Characters]
            R2[Entities]
            R3[Environment]
            R4[Resources]
            ADD[+ Add Route]
        end

        subgraph MAIN["Route Configuration"]
            RC[Category: Characters<br/>Path: Assets/Prefabs/Characters<br/>Addressable Label: Characters]
            SCAN[Scan Route]
            VAL[Validate]
        end

        subgraph DISCOVERED["Discovered Prefabs"]
            P1["✓ Knight_v0.prefab - CharacterType.Knight"]
            P2["✓ Knight_v1.prefab - Variant 1"]
            P3["! Knight_v2.prefab - Unregistered"]
            P4["✓ Barbarian_v0.prefab - CharacterType.Barb"]
        end

        subgraph VALIDATION["Validation Status"]
            V1["✓ All CharacterType enums have default prefabs"]
            V2["! Missing: EntityType.Boss (no prefab assigned)"]
            V3["✓ Environment prefabs validated"]
            V4["! 2 orphaned prefabs found (not in registry)"]
        end

        SIDEBAR --> MAIN
        MAIN --> DISCOVERED
        DISCOVERED --> VALIDATION
    end

A comprehensive tagging system for searchable, filterable prefab management:

Tag TypePurposeExamples
FunctionalGameplay behaviorinteractable, destructible, harvestable, npc
VisualRendering/appearanceanimated, static, particle-emitter, lod-enabled
PhysicsCollision/physicssolid, trigger, kinematic, ragdoll
PerformanceOptimization hintsgpu-instanced, batched, lightweight, heavy
SemanticContent classificationfriendly, hostile, neutral, quest-related
[CreateAssetMenu(fileName = "TagRegistry", menuName = "KBVE/Pool/Tag Registry")]
public class PrefabTagRegistry : ScriptableObject
{
[Header("Tag Definitions")]
public List<TagDefinition> tagDefinitions;
[Header("Label Groups")]
public List<LabelGroup> labelGroups; // For batch operations
// Fast lookup by tag name
private Dictionary<string, TagDefinition> _tagLookup;
public bool HasTag(string tagName) => _tagLookup.ContainsKey(tagName);
public TagDefinition GetTag(string tagName) => _tagLookup.GetValueOrDefault(tagName);
}
[Serializable]
public class TagDefinition
{
public string tagName; // "interactable"
public string displayName; // "Interactable Object"
public string description; // "Can be interacted with by player"
public TagCategory category; // Functional, Visual, Physics, etc.
public Color editorColor; // For Bakery UI highlighting
public bool isSystemTag; // Reserved tags (can't be deleted)
[Header("Validation")]
public List<string> requiredComponents; // Components that must exist
public List<string> incompatibleTags; // Can't coexist with these tags
}
[Serializable]
public class LabelGroup
{
public string groupName; // "Combat Entities"
public string[] includedTags; // Tags in this group
public string addressableLabel; // For Addressables batch loading
}
public enum TagCategory
{
Functional,
Visual,
Physics,
Performance,
Semantic,
Custom
}
public class PrefabQuery
{
private readonly PrefabRegistry _registry;
// Fluent query interface
public PrefabQuery WithTag(string tag);
public PrefabQuery WithAnyTag(params string[] tags);
public PrefabQuery WithAllTags(params string[] tags);
public PrefabQuery WithoutTag(string tag);
public PrefabQuery InCategory(PoolCategory category);
public PrefabQuery WithLabel(string label);
public List<PrefabDefinition> Execute();
public PrefabDefinition First();
public int Count();
}
// Usage examples:
var combatEntities = registry.Query()
.WithAnyTag("hostile", "friendly")
.WithTag("animated")
.InCategory(PoolCategory.Entity)
.Execute();
var heavyPrefabs = registry.Query()
.WithTag("heavy")
.WithoutTag("gpu-instanced")
.Execute();

Integration with Unity’s layer system for physics, rendering, and raycasting:

[Serializable]
public class LayerConfiguration
{
[Header("Physics Layers")]
public int defaultLayer = 0; // Default Unity layer
public int playerLayer = 8; // "Player" layer
public int enemyLayer = 9; // "Enemy" layer
public int environmentLayer = 10; // "Environment" layer
public int interactableLayer = 11; // "Interactable" layer
public int projectileLayer = 12; // "Projectile" layer
public int triggerLayer = 13; // "Trigger" layer
[Header("Rendering Layers")]
public uint defaultRenderingLayer = 1; // Default rendering layer mask
public uint outlineRenderingLayer = 2; // For outline effects
public uint shadowOnlyLayer = 4; // Shadow casters only
[Header("Layer Matrix Presets")]
public LayerCollisionPreset[] collisionPresets;
}
[Serializable]
public class LayerCollisionPreset
{
public string presetName; // "Standard Combat"
public LayerMask playerCollidesWith; // What player collides with
public LayerMask enemyCollidesWith; // What enemies collide with
public LayerMask projectileCollidesWith; // What projectiles hit
}
flowchart LR
    subgraph PHYSICS["Physics Layers"]
        L0["[0] Default"]
        L8["[8] Player ●"]
        L9["[9] Enemy ●"]
        L10["[10] Environment ●"]
        L11["[11] Interactable ●"]
        L12["[12] Projectile ●"]
        L13["[13] Trigger ●"]
    end

    subgraph RENDERING["Rendering Layers"]
        R1["[1] Default"]
        R2["[2] Outline"]
        R4["[4] ShadowOnly"]
        R8["[8] Custom1"]
    end

    subgraph PRESETS["Collision Presets"]
        P1["Standard Combat<br/>Player ↔ Enemy<br/>Player ↔ Environment<br/>Projectile ↔ Enemy"]
        P2["Trigger Zones<br/>Player ↔ Trigger"]
    end

    PHYSICS --> PRESETS

6. Variant Architecture (Component Preservation)

Section titled “6. Variant Architecture (Component Preservation)”

Critical design principle: Variants modify visual/behavioral aspects WITHOUT overwriting physics configurations, mesh colliders, or component settings that are expensive to reconfigure.

flowchart TB
    subgraph BASE["Base Prefab (v0)"]
        T[Transform]
        MF[MeshFilter]
        MR[MeshRenderer]
        MC[MeshCollider - PRESERVED]
        RB[Rigidbody - PRESERVED]
        CC[Custom Components]
    end

    subgraph VARIANTS["Variants inherit and can override:"]
        V1["v1: Different materials/textures"]
        V2["v2: Different mesh (same collider bounds)"]
        V3["v3: Different animator controller"]
    end

    BASE --> VARIANTS

    style MC fill:#4CAF50
    style RB fill:#4CAF50
[Serializable]
public class VariantDefinition
{
public int variantId;
public string variantName; // "Armored", "Winter", "Elite"
public string description;
[Header("Inheritance")]
public int baseVariantId = 0; // Which variant this extends
public VariantInheritanceMode inheritanceMode;
[Header("Preserved Components (Never Overwritten)")]
public ComponentPreservationRules preservation;
[Header("Overridable Properties")]
public VariantOverrides overrides;
}
public enum VariantInheritanceMode
{
/// <summary>Full prefab replacement - use for completely different models</summary>
FullReplace,
/// <summary>Runtime property swap - preserves physics/colliders</summary>
PropertyOverride,
/// <summary>Material/texture only - fastest, no structural changes</summary>
MaterialOnly,
/// <summary>Additive - adds components without removing base ones</summary>
Additive
}
[Serializable]
public class ComponentPreservationRules
{
[Header("Physics Preservation")]
public bool preserveMeshCollider = true;
public bool preserveBoxCollider = true;
public bool preserveSphereCollider = true;
public bool preserveCapsuleCollider = true;
public bool preserveRigidbody = true;
public bool preserveJoints = true;
[Header("Transform Preservation")]
public bool preserveChildHierarchy = true;
public bool preserveLocalPositions = true;
public bool preserveLocalRotations = true;
public bool preserveLocalScales = true;
[Header("Component Preservation")]
public string[] preservedComponentTypes; // Full type names to never override
public string[] preservedChildPaths; // Child GameObjects to never modify
[Header("Layer Preservation")]
public bool preservePhysicsLayers = true;
public bool preserveRenderingLayers = true;
}
public interface IPooledEntity
{
string NetworkId { get; set; }
PoolKey PoolKey { get; }
/// <summary>Called when entity is retrieved from pool</summary>
void OnSpawned(EntitySpawnData data);
/// <summary>Called when entity is returned to pool</summary>
void OnDespawned();
/// <summary>
/// Reset entity state WITHOUT destroying physics components.
/// This is called before OnSpawned for re-used entities.
/// </summary>
void OnReset(ComponentPreservationRules rules);
}
public class PooledEntityBase : MonoBehaviour, IPooledEntity
{
// Cached component references - populated once on first spawn
private MeshCollider _meshCollider;
private Rigidbody _rigidbody;
private bool _componentsInitialized;
public virtual void OnReset(ComponentPreservationRules rules)
{
// Cache components on first use
if (!_componentsInitialized)
{
_meshCollider = GetComponent<MeshCollider>();
_rigidbody = GetComponent<Rigidbody>();
_componentsInitialized = true;
}
// Reset transform
transform.localPosition = Vector3.zero;
transform.localRotation = Quaternion.identity;
transform.localScale = Vector3.one;
// Reset rigidbody state (but preserve configuration!)
if (_rigidbody != null && rules.preserveRigidbody)
{
_rigidbody.velocity = Vector3.zero;
_rigidbody.angularVelocity = Vector3.zero;
// DO NOT change: mass, drag, constraints, isKinematic, etc.
}
// MeshCollider - just ensure it's enabled, don't rebuild
if (_meshCollider != null && rules.preserveMeshCollider)
{
_meshCollider.enabled = true;
// DO NOT call: _meshCollider.sharedMesh = ... (expensive!)
}
// Reset any runtime state
ResetRuntimeState();
}
protected virtual void ResetRuntimeState()
{
// Override in subclasses for custom state reset
}
}

Why LOD matters for WebGL: WebGL runs in browsers with strict memory limits (~2GB max), limited GPU memory, and no native mesh streaming. Proper LOD management is essential for maintaining stable frame rates and preventing out-of-memory crashes.

flowchart LR
    subgraph LOD_TIERS["LOD Distance Tiers"]
        LOD0["LOD0 (Full Detail)<br/>0-15m<br/>100% triangles<br/>Full materials<br/>All bones/animations"]
        LOD1["LOD1 (Medium)<br/>15-40m<br/>50% triangles<br/>Simplified mats<br/>Reduced bones"]
        LOD2["LOD2 (Low)<br/>40-80m<br/>25% triangles<br/>Single mat<br/>No bones"]
        LOD3["LOD3/Cull<br/>80m+<br/>Billboard or Cull"]
    end

    LOD0 --> LOD1 --> LOD2 --> LOD3
CategoryLOD0LOD1LOD2CullImpostorShadow Mode
Character (Player)0-15m15-30m30-60m80mNoLOD1AndBelow
Character (NPC)0-10m10-25m25-50m60mYesLOD0Only
Entity (Enemy)0-12m12-30m30-60m70mYesLOD1AndBelow
Environment (Tree)0-20m20-50m50-100m150mYesLOD0Only
Environment (Rock)0-15m15-40m40-80m120mNoLOD0Only
Resource0-8m8-20m20-35m50mNoNone
[Serializable]
public class LODConfiguration
{
[Header("LOD Distances")]
public float[] lodDistances = { 15f, 40f, 80f }; // Transition distances
public float cullDistance = 100f; // Full cull distance
[Header("Quality Settings")]
public LODQualityPreset qualityPreset = LODQualityPreset.Balanced;
public bool useCrossFade = true; // Smooth transitions
public float crossFadeDuration = 0.5f; // Fade time in seconds
[Header("WebGL Optimizations")]
public bool enableAggressiveCulling = true; // More aggressive on WebGL
public bool useImpostors = true; // Billboard at far distances
public bool reduceBoneCountAtDistance = true; // Simplify skinned meshes
public int maxBonesLOD1 = 32; // Max bones for LOD1
public int maxBonesLOD2 = 16; // Max bones for LOD2
[Header("Memory Management")]
public bool streamLODsOnDemand = false; // Load LODs as needed (not WebGL)
public bool unloadHighLODWhenFar = true; // Free memory for distant objects
}
public enum LODQualityPreset
{
Performance, // Aggressive LOD, early culling (mobile/WebGL)
Balanced, // Default settings
Quality, // Higher detail distances
Ultra // Maximum detail (desktop only)
}
public static class WebGLLODOptimizer
{
/// <summary>
/// Apply WebGL-specific optimizations to all pooled entities.
/// Called during pool initialization.
/// </summary>
public static void OptimizeForWebGL(PrefabRegistry registry)
{
#if UNITY_WEBGL
// Reduce LOD distances by 30%
foreach (var prefab in registry.prefabs)
{
if (prefab.lodSettings != null && prefab.lodSettings.useCustomDistances)
{
for (int i = 0; i < prefab.lodSettings.customLodDistances.Length; i++)
{
prefab.lodSettings.customLodDistances[i] *= 0.7f;
}
}
}
// Force impostor mode for complex meshes
foreach (var prefab in registry.prefabs)
{
if (prefab.HasTag("complex") || prefab.HasTag("heavy"))
{
prefab.lodSettings.generateImpostor = true;
}
}
// Disable shadows for environment objects at distance
foreach (var prefab in registry.prefabs)
{
if (prefab.category == PoolCategory.Environment)
{
prefab.lodSettings.shadowMode = ShadowLODMode.LOD0Only;
prefab.lodSettings.shadowCullDistance = 30f;
}
}
#endif
}
/// <summary>
/// Memory-aware LOD adjustment based on current memory pressure.
/// </summary>
public static void AdjustLODForMemoryPressure(float memoryUsagePercent)
{
if (memoryUsagePercent > 0.8f)
{
// Critical: Force lowest LOD for distant objects
QualitySettings.lodBias = 0.5f;
QualitySettings.maximumLODLevel = 1;
}
else if (memoryUsagePercent > 0.6f)
{
// Warning: Reduce detail
QualitySettings.lodBias = 0.7f;
QualitySettings.maximumLODLevel = 0;
}
else
{
// Normal: Restore default
QualitySettings.lodBias = 1.0f;
QualitySettings.maximumLODLevel = 0;
}
}
}

Connect the prefab library to the entity lifecycle:

public interface IPooledEntity
{
string NetworkId { get; set; }
PoolKey PoolKey { get; }
void OnSpawned(EntitySpawnData data);
void OnDespawned();
void OnReset();
}
public struct EntitySpawnData
{
public string NetworkId;
public Vector3 Position;
public Quaternion Rotation;
public int VariantId;
public Dictionary<string, object> CustomData; // Flexible payload
}
sequenceDiagram
    participant Server
    participant Client
    participant SnapshotHandler
    participant PoolService
    participant PrefabRegistry
    participant ObjectPool
    participant IPooledEntity

    Server->>Client: Snapshot (entity)
    Client->>SnapshotHandler: Process
    SnapshotHandler->>PoolService: GetEntity()
    PoolService->>PrefabRegistry: GetPrefab()
    PrefabRegistry-->>PoolService: PrefabDefinition
    PoolService->>ObjectPool: Get()
    ObjectPool-->>PoolService: GameObject
    PoolService->>IPooledEntity: OnSpawned()

  1. Create PrefabDefinition and RouteMapping classes

    • Define the data structures
    • Add serialization support
    • Files: PrefabDefinition.cs, RouteMapping.cs
  2. Create PrefabRegistry ScriptableObject

    • Implement basic CRUD operations
    • Add validation methods
    • File: PrefabRegistry.cs
  1. Create PrefabTagRegistry ScriptableObject

    • Define tag categories and definitions
    • Implement tag validation rules
    • Add label group management
    • File: PrefabTagRegistry.cs
  2. Create LayerConfiguration ScriptableObject

    • Define physics layer mappings
    • Define rendering layer masks
    • Add collision matrix presets
    • File: LayerConfiguration.cs
  3. Implement PrefabQuery API

    • Fluent query interface for tag-based searches
    • Integration with PrefabRegistry
    • Performance-optimized lookups
    • File: PrefabQuery.cs
  1. Create VariantDefinition and preservation rules

    • Define ComponentPreservationRules
    • Implement VariantInheritanceMode enum
    • Add variant override structures
    • File: VariantDefinition.cs
  2. Implement VariantApplicator

    • Material-only variant application
    • Property override application
    • Additive component support
    • Collider/physics preservation
    • Included in: VariantDefinition.cs
  3. Create PrefabLODSettings for WebGL optimization

    • LODConfiguration ScriptableObject
    • Per-prefab LOD settings
    • Impostor and animation LOD support
    • File: PrefabLODSettings.cs
  1. Create PrefabOracleRecipe

    • Route browser UI with tabbed interface (Prefabs, Tags, Layers, LOD, Validation)
    • Prefab scanner with category statistics and badges
    • Registration workflow with JSON export
    • File: Editor/KBVE/Bakery/Recipes/PrefabOracleRecipe.cs
  2. Create Tag Manager UI

    • Tag browser with category filtering
    • Add/remove tags with color indicators
    • System tag protection
    • Initialize defaults functionality
    • Integrated into PrefabOracleRecipe “Tags” tab
  3. Validation Dashboard

    • Missing prefab checks
    • Tag validation across prefabs
    • Layer configuration validation
    • Combined results with severity indicators
    • Integrated into PrefabOracleRecipe “Validation” tab
  1. Update PoolService to use PrefabRegistry

    • Lazy loading from registry (LazyCreateAndGet method)
    • Fallback chain support (Registry.GetPrefab handles fallbacks)
    • Tag-based prefab queries (QueryByTag, HasTag methods)
    • Configuration priority: Oracle → Legacy → Addressables
    • Backward compatible with PoolPrefabConfig
    • File: Pool/PoolService.cs
  2. Implement variant switching

    • Runtime variant application (SwitchVariant method)
    • Hot-swap detection (CanHotSwap)
    • Cold swap with position/rotation preservation
    • Variant tracking in ActiveEntityInfo
    • File: Pool/PoolService.cs
  3. Add Addressables support

    • Async prefab loading (existing, preserved)
    • Label-based batch loading (existing, preserved)
    • Registry-aware prewarm (PrewarmFromRegistryAsync)
    • Per-category async prewarm
  1. Export/Import system

    • JSON serialization for registry (RegistrySerializer.cs)
    • Tag registry export (ExportTagsToJson)
    • CI/CD integration (JSON file format)
    • Import with options (overwrite, create new, resolve paths)
    • Clipboard support (copy/paste JSON)
    • Integrated into Validation tab in PrefabOracleRecipe
  2. Protobuf as Source of Truth

    • Extended pool.proto with PrefabDefinition, PrefabRegistry, LODSettings, TagDefinition messages
    • Proto-to-ScriptableObject sync (ProtoSync.cs)
    • Binary protobuf export/import in RegistrySerializer
    • Enables server-client data alignment for prefab metadata
    • C# classes auto-generated via ./scripts/generate-unity-protos.sh

{Category}_{Type}_{Subtype}_v{Variant}.prefab
Examples:
- Char_Knight_Default_v0.prefab
- Char_Knight_Armored_v1.prefab
- Env_Tree_Oak_v0.prefab
- Env_Tree_Oak_Winter_v1.prefab
- Res_Wood_Log_v0.prefab
{category}_{type}_v{variant}
Examples:
- char_knight_v0
- char_knight_v1
- env_tree_oak_v0
- res_wood_v0
Pool/{Category}
Pool/{Category}/{Type}
Examples:
- Pool/Characters
- Pool/Characters/Knight
- Pool/Environment
- Pool/Environment/Trees

{
"registryId": "main",
"displayName": "Main Prefab Registry",
"version": "1.0.0",
"prefabs": [
{
"prefabId": "char_knight_v0",
"category": "Character",
"typeId": 0,
"variantId": 0,
"assetPath": "Assets/Prefabs/Characters/Knight/Knight_v0.prefab",
"addressableKey": "char_knight_v0",
"displayName": "Knight (Default)",
"tags": ["player", "melee", "starter", "animated", "solid"],
"labels": ["Pool/Characters", "Combat"],
"poolSize": 10,
"maxPoolSize": 50,
"prewarmOnLoad": true,
"isDefaultVariant": true,
"layerSettings": {
"physicsLayer": 8,
"renderingLayerMask": 1,
"applyToChildren": true,
"childOverrides": [
{
"childPath": "Hitbox",
"physicsLayer": 11,
"renderingLayerMask": 1
}
]
},
"variants": [
{
"variantId": 1,
"variantName": "Armored",
"inheritanceMode": "MaterialOnly",
"preservation": {
"preserveMeshCollider": true,
"preserveRigidbody": true,
"preservePhysicsLayers": true
},
"tagOverrides": {
"addTags": ["heavy", "armored"],
"removeTags": []
}
}
]
}
],
"routes": [
{
"category": "Character",
"relativePath": "Characters",
"addressableLabel": "Pool/Characters"
}
]
}
{
"registryId": "default-tags",
"version": "1.0.0",
"tags": [
{
"tagName": "interactable",
"displayName": "Interactable Object",
"description": "Can be interacted with by player",
"category": "Functional",
"editorColor": "#4CAF50",
"isSystemTag": true,
"requiredComponents": ["InteractableComponent"],
"incompatibleTags": []
},
{
"tagName": "solid",
"displayName": "Solid Collider",
"description": "Has physics collision enabled",
"category": "Physics",
"editorColor": "#2196F3",
"isSystemTag": true,
"requiredComponents": [],
"incompatibleTags": ["trigger"]
},
{
"tagName": "trigger",
"displayName": "Trigger Volume",
"description": "Collider is set to trigger mode",
"category": "Physics",
"editorColor": "#FF9800",
"isSystemTag": true,
"requiredComponents": [],
"incompatibleTags": ["solid"]
}
],
"labelGroups": [
{
"groupName": "Combat Entities",
"includedTags": ["hostile", "friendly", "animated"],
"addressableLabel": "Pool/Combat"
}
]
}

RuleDescriptionSeverity
Default VariantEach proto enum value must have a variant 0 prefabError
Prefab ReferenceAll registered prefabs must have valid GameObject referenceError
Unique IDsNo duplicate prefab IDs in registryError
Fallback ChainFallback variants must existWarning
Orphaned PrefabsPrefabs in routes not in registryInfo
Missing RoutesCategories without route mappingsWarning
Tag ExistenceAll applied tags must exist in TagRegistryError
Tag CompatibilityNo incompatible tags on same prefabError
Required ComponentsTags with requiredComponents must have those componentsError
Layer ValidityPhysics layer index must be valid (0-31)Error
Variant PreservationVariants must not override preserved componentsWarning
Collider ConsistencyMeshCollider mesh should match base variantInfo
LOD RequiredAnimated/complex prefabs should have LOD configurationWarning
LOD Distance OrderLOD distances must increase with each levelError
Impostor RecommendedHeavy prefabs should use impostors for WebGLWarning
Shadow LODEnvironment prefabs should limit shadow castingInfo
public class RegistryValidator
{
public ValidationResult Validate(PrefabRegistry registry);
public List<PrefabDefinition> FindOrphanedPrefabs(string routePath);
public List<string> FindMissingEnumCoverage<TEnum>(PoolCategory category);
public bool ValidateFallbackChain(PrefabDefinition prefab);
// Tag Validation
public List<string> ValidateTags(PrefabDefinition prefab, PrefabTagRegistry tagRegistry);
public List<string> FindIncompatibleTags(PrefabDefinition prefab, PrefabTagRegistry tagRegistry);
public List<string> FindMissingRequiredComponents(PrefabDefinition prefab, PrefabTagRegistry tagRegistry);
// Layer Validation
public bool ValidateLayerConfiguration(PrefabLayerSettings settings);
public List<string> FindInvalidLayerReferences(PrefabRegistry registry);
// Variant Validation
public List<string> ValidateVariantPreservation(VariantDefinition variant, GameObject basePrefab);
public bool ValidateColliderConsistency(PrefabDefinition basePrefab, VariantDefinition variant);
}
public class ValidationResult
{
public bool IsValid;
public List<ValidationIssue> Errors;
public List<ValidationIssue> Warnings;
public List<ValidationIssue> Info;
}
public class ValidationIssue
{
public string PrefabId;
public string Rule;
public string Message;
public ValidationSeverity Severity;
public string SuggestedFix; // Actionable fix suggestion
}
public enum ValidationSeverity
{
Error, // Must fix before build
Warning, // Should fix, may cause issues
Info // Informational, optional fix
}

  1. Create new PrefabRegistry asset
  2. Run migration script to copy entries
  3. Update PoolService to check registry first, fallback to config
  4. Deprecate direct PoolPrefabConfig usage over time
  5. Remove old config once all systems migrated
// Migration helper
public static class PoolConfigMigration
{
public static PrefabRegistry MigrateFromConfig(PoolPrefabConfig config)
{
var registry = ScriptableObject.CreateInstance<PrefabRegistry>();
foreach (var entry in config.characterPrefabs)
{
registry.prefabs.Add(new PrefabDefinition
{
prefabId = $"char_{entry.type.ToString().ToLower()}_v{entry.variantId}",
category = PoolCategory.Character,
typeId = (int)entry.type,
variantId = entry.variantId,
prefab = entry.prefab,
isDefaultVariant = entry.variantId == 0
});
}
// ... repeat for other categories
return registry;
}
}

  1. Addressables vs Direct References: Should we fully migrate to Addressables for all prefabs, or keep hybrid approach?

  2. Variant Naming: Should variants be named (e.g., “armored”, “winter”) or just numbered?

    • Decision: Use both - variantId for internal lookups, variantName for display and debugging
  3. LOD Support: Should the pool system handle LOD variants, or is that Unity’s LODGroup responsibility?

    • Decision: Hybrid approach - use Unity LODGroup where available, but pool system manages LOD configuration, distance overrides, impostor generation, and WebGL-specific optimizations. See Section 7 for full LOD integration.
  4. Async Loading UI: How to handle loading states when prefabs aren’t prewarmed?

  5. Hot Reload: Should we support runtime registry updates for development?

  6. Tag Inheritance: Should child prefabs automatically inherit parent tags, or should tags be explicitly defined?

  7. Collider Baking: When a variant has a significantly different mesh, should we support optional collider rebaking with explicit opt-in?

  8. Material Property Blocks: Should variants use MaterialPropertyBlocks for per-instance overrides, or create unique material instances?

  9. Layer Synchronization: How to handle layer changes when prefabs are already instantiated in the world?


9. Content Data Pipeline (Astro → Unity)

Section titled “9. Content Data Pipeline (Astro → Unity)”

Concept: Use MDX files in Astro as the single source of truth for game data (harvest yields, crafting recipes, item stats). Astro build process exports JSON that Unity imports as ScriptableObjects.

flowchart LR
    subgraph ASTRO["ASTRO (Source of Truth)"]
        MDX["content/<br/>trees/oak.mdx<br/>trees/pine.mdx<br/>resources/wood.mdx<br/>resources/stone.mdx"]
        FM["Frontmatter:<br/>harvestYield: 5<br/>respawnTime: 300<br/>requiredTool: axe"]
    end

    subgraph BUILD["Build Process"]
        JSON["content-data.json"]
    end

    subgraph UNITY["UNITY (Runtime Consumer)"]
        SO["Resources/Data/<br/>TreeData.asset<br/>RockData.asset<br/>HerbData.asset"]
        GEN["Generated SO with:<br/>- harvestYield<br/>- respawnTime<br/>- requiredTool<br/>- dropTable[]<br/>- seasonalMods"]
    end

    MDX --> JSON
    FM --> JSON
    JSON --> SO
    JSON --> GEN
---
# content/harvestables/trees/oak.mdx
id: "env_tree_oak"
displayName: "Oak Tree"
category: "Environment"
subCategory: "Trees"
harvest:
baseYield: 5
yieldVariance: 2 # 3-7 logs per harvest
resource: "wood_oak"
toolRequired: "axe"
toolTierMin: 1
timing:
harvestDuration: 3.5 # seconds
respawnTime: 300 # seconds
drops:
- id: "wood_oak"
chance: 1.0
min: 3
max: 7
- id: "acorn"
chance: 0.15
min: 1
max: 2
- id: "bark_oak"
chance: 0.3
min: 1
max: 1
seasonal:
spring: { yieldMod: 1.0 }
summer: { yieldMod: 1.2 }
autumn: { yieldMod: 1.0, extraDrops: ["fallen_leaves"] }
winter: { yieldMod: 0.6 }
---
# Oak Tree
The mighty oak provides sturdy lumber for construction...

Concept: Composites are prefabs built from multiple sub-prefabs (base + attachments). A “mossy rock” is a Rock base with Moss attachment. Composites inherit the base’s physics while adding visual layers.

flowchart TB
    subgraph SIMPLE["Simple Prefab"]
        SP[Rock_v0<br/>└ Mesh<br/>└ Collider<br/>└ Material]
    end

    subgraph COMPOSITE["Composite Prefab"]
        CP["Rock_Mossy_v0 (Composite)"]
        BASE["├ Base: Rock_v0 ◄── Physics source<br/>│   └ Mesh<br/>│   └ MeshCollider (PRESERVED)"]
        ATT["└ Attachments[]<br/>    └ Moss_Patch_01<br/>        └ Mesh (no collider)<br/>        └ offset: (0, 0.2, 0)<br/>    └ Moss_Patch_02<br/>        └ Mesh<br/>        └ offset: (0.3, 0.1, 0)"]
    end

    subgraph CATEGORIES["Composite Categories"]
        C1["Environmental: Rock+Moss, Tree+Vines, Stump+Mushrooms"]
        C2["Structural: Wall+Damage, Floor+Debris, Roof+Snow"]
        C3["Interactive: Chest+Lock, Door+Hinges, Lever+Mechanism"]
        C4["Character: Base+Armor, Base+Accessories, Base+Effects"]
    end
[Serializable]
public class CompositeDefinition
{
[Header("Identity")]
public string compositeId; // "env_rock_mossy_v0"
public string displayName; // "Mossy Boulder"
[Header("Base Prefab")]
public string basePrefabId; // "env_rock_boulder_v0" - source of physics
public bool inheritBaseTags = true;
public bool inheritBaseData = true; // Links to HarvestableData, etc.
[Header("Attachments")]
public CompositeAttachment[] attachments;
[Header("Composite Tags")]
public string[] additionalTags; // ["mossy", "wet", "slippery"]
[Header("Data Overrides")]
public DataOverride[] dataOverrides; // Override base harvestable data
}
[Serializable]
public class CompositeAttachment
{
public string attachmentPrefabId; // "env_moss_patch_v0"
public string attachPointName; // "MossPoint01" or null for offset
[Header("Transform")]
public Vector3 localPosition;
public Vector3 localRotation;
public Vector3 localScale = Vector3.one;
[Header("Variation")]
public bool randomRotationY = true; // Random Y rotation for variety
public float scaleVariance = 0.1f; // ±10% scale variation
[Header("Conditions")]
public string[] requiredConditions; // ["wet_biome", "shaded"]
public float spawnChance = 1.0f; // 0-1, for conditional spawning
}

PhaseFeatureStatusDependencies
Phase 7Content Data PipelineCOMPLETEAstro MDX schema, JSON export
Phase 8Composite Prefab SystemCOMPLETEVariant Architecture (Phase 3)
Phase 9Astro ↔ Unity SyncCOMPLETECI/CD pipeline, content-data.json
Phase 10Composite Editor UICOMPLETEBakery Integration (Phase 4)

  • Game Design Document - Core game design documentation
  • Pathfinding - Navigation and pathfinding systems
  • proto/rentearth/snapshot.proto - Proto definitions for entity types
  • proto/rentearth/pool.proto - Pool category definitions

DateVersionChanges
2025-12-080.14.0PHASE 10: Composite Editor UI: Created CompositeOracleRecipe.cs - full Bakery recipe for managing composite prefabs. Features: split-panel layout with composite list (category filtering, attachment count badges) and full editor panel.
2025-12-080.13.0PHASE 9: Astro ↔ Unity Sync Pipeline: Created astro_sync.py - orchestrates content sync by calling Node.js export-game-data.mjs, then imports JSON to Unity asset files.
2025-12-080.12.0PHASE 8: Composite Prefab System: Created composite prefab architecture in Assets/Scripts/Pool/Oracle/.
2025-12-080.11.0PHASE 7: Content Data Pipeline (Astro → Unity): Created content collections for game data with Zod schemas.
2025-12-080.10.0Unified Proto Generation + Zod Schemas: Rewrote proto_sync.py as single multi-platform generator.
2025-12-080.9.0Server-Client Proto Integration: Added proto export/import buttons to PrefabOracleRecipe UI.
2025-12-080.8.0PHASE 6 COMPLETE (Polish): Extended pool.proto with messages. Created ProtoSync.cs.
2025-12-070.7.0PHASE 4 COMPLETE (Bakery Integration): Created PrefabOracleRecipe with 5-tab UI.
2025-12-070.6.0IMPLEMENTATION COMPLETE (Phases 1-3): Created Pool Oracle system.
2025-12-070.5.0Added Future Extensions: Content Data Pipeline, Composite Prefab Architecture.
2025-12-070.4.0Added Bakery Sidebar Integration with badge counts.
2025-12-070.3.0Added LOD Integration section with WebGL-specific optimizations.
2025-12-070.2.0Added Tag & Label System, Unity Layer Configuration, Variant Architecture.
2025-12-070.1.0Initial design document