Complete documentation for all greenhouse controllers, central state applier, and backend integration patterns.
unity_module/sample_greenhouse_scenarios.pyThis greenhouse Unity scene is composed of:
GreenhouseStateApplier.cs) that orchestrates everything from a JSON fileAll scripts are designed for Python backend integration. The central applier reads a JSON file at runtime, detects changes, and applies the full greenhouse state automatically. This allows testing backend-style control patterns locally inside Unity before connecting to FastAPI.
unity_module/The unity_module/ folder at the repository root contains the complete Unity Editor source project for the AgriTwin-GH 3D greenhouse scene. Opening this folder in Unity 2022 LTS or later gives you the full editable scene with all assets, scripts, audio, and skyboxes — no additional downloads required.
unity_module/
├── Assets/ # All scene content
│ ├── Scenes/ # Unity scene files
│ │ ├── Environment.unity # Main greenhouse scene (open this in Unity)
│ │ └── SampleScene.unity # Blank reference scene
│ ├── Scripts/ # All 12 C# controller scripts
│ │ ├── GreenhouseStateApplier.cs # Central JSON-driven orchestrator
│ │ ├── CropStageController.cs # Per-plant 6-stage visual swap
│ │ ├── CropHealthIndicator.cs # RGB status lights + blinking
│ │ ├── TimeOfDayController.cs # Skybox, fog, directional light
│ │ ├── FluorescentLightController.cs
│ │ ├── HeaterController.cs
│ │ ├── EnergyCanisterController.cs
│ │ ├── HumidifierController.cs
│ │ ├── WindowFanController.cs
│ │ ├── VentController.cs
│ │ ├── WaterTankFloorController.cs
│ │ └── FreeCameraController.cs
│ ├── Environment/ # Custom greenhouse prefabs and FBX models
│ │ ├── Bld_GreenMouse.prefab # Main greenhouse building structure
│ │ ├── Crop Slot 1/2/3.prefab # Crop container slot variants
│ │ ├── Seeding.prefab # Stage 0 crop model
│ │ ├── Vegetative.fbx # Stage 1 crop model
│ │ ├── Flowering Initiation.prefab # Stage 2 crop model
│ │ ├── Flowering.prefab # Stage 3 crop model
│ │ ├── Unripe.prefab # Stage 4 crop model
│ │ ├── Ripe.prefab # Stage 5 crop model
│ │ ├── Fluorescent Light.fbx # Actuator model — grow lights
│ │ ├── Heater.fbx # Actuator model — heater
│ │ ├── Humidifier (1).fbx # Actuator model — humidifier
│ │ ├── Window fan.fbx # Actuator model — window fan
│ │ ├── Vent.fbx # Actuator model — vent
│ │ ├── Water Tank Floor.fbx # Actuator model — water tank
│ │ ├── Energy Canister.fbx # Actuator model — energy canister
│ │ └── StatusIndicator.prefab # Reusable RGB indicator light
│ ├── AllSkyFree/ # Skybox cubemap pack (11 sky environments)
│ │ ├── Cartoon Base BlueSky/ # Day sky — PNG cubemap + material
│ │ ├── Cartoon Base NightSky/ # Night sky
│ │ ├── Cold Sunset/ Cold Night/ # Sunset and cold-night variants
│ │ ├── Deep Dusk/ Epic_BlueSunset/ # Dusk and dramatic sunset
│ │ └── ... # 11 total sky environments
│ ├── Pandazole_Ultimate_Pack/ # Greenhouse building 3D asset pack
│ │ └── Pandazole Farm Ranch Pack/ # Models, textures, prefabs, materials
│ ├── PolyOne/Stylized Tomato/ # Stylized tomato 3D asset
│ │ └── Model/ Texture/ Prefabs/ # FBX model + textures + diffuse material
│ ├── SimpleSky/ # Alternative skybox pack
│ │ └── Materials/ Models/ Textures/ # Sky sphere material and textures
│ ├── Nature Sound FX/ # WAV audio library for actuator sounds
│ │ ├── Steam/ # Fan / humidifier steam audio (8 WAVs)
│ │ ├── Water/ Wind/ Rain/ # Ambient and environmental audio
│ │ └── ... # 11 sound categories total
│ ├── Imports/ # Externally imported 3D assets
│ │ └── Tomato (2).glb # GLTF tomato model (via GLTFUtility)
│ ├── StreamingAssets/
│ │ └── greenhouse_state.json # Runtime JSON state file read by GreenhouseStateApplier
│ └── Settings/ # URP and render pipeline asset settings
├── Packages/
│ ├── manifest.json # Unity package dependencies
│ └── packages-lock.json # Locked package versions
├── ProjectSettings/ # Full Unity project configuration
│ ├── ProjectSettings.asset # Project name, version, target platform
│ ├── GraphicsSettings.asset # URP pipeline assignment
│ ├── QualitySettings.asset # Quality tiers
│ ├── AudioManager.asset # Global audio settings
│ └── ... # Physics, input, navmesh, VFX, XR settings
└── build/ # WebGL export (Git LFS — Brotli binaries)
├── Build/
│ ├── build.data.br # Scene & asset data (~18 MB, LFS-tracked)
│ ├── build.wasm.br # Unity WASM runtime (LFS-tracked)
│ ├── build.framework.js.br # JS framework loader (LFS-tracked)
│ └── build.loader.js # Bootstrap entry point
├── StreamingAssets/ # Default state JSON for the deployed scene
└── index.html # WebGL entry point
The project uses Universal Render Pipeline (URP) 17.5.0 (Unity 6) with the following key packages defined in Packages/manifest.json:
| Package | Version | Purpose |
|---|---|---|
com.unity.render-pipelines.universal |
17.5.0 | URP — lighting, shaders, post-processing |
com.unity.inputsystem |
1.19.0 | New Input System for camera controls |
com.siccity.gltfutility |
git | Runtime GLTF/GLB model import |
com.unity.ai.navigation |
2.0.11 | NavMesh (reserved for future agents) |
com.unity.timeline |
1.8.11 | Animation timeline support |
com.unity.visualscripting |
1.9.11 | Visual scripting support |
Large binary files in unity_module/ are stored via Git LFS so the repository stays lean:
| Pattern | Examples |
|---|---|
**/*.fbx |
All actuator and crop 3D models |
**/*.glb |
Imported GLTF tomato model |
**/*.png |
Skybox cubemaps, all textures |
**/*.wav |
All audio clips in Nature Sound FX |
**/*.pdf |
AllSkyFree documentation PDF |
build/Build/*.br |
Brotli-compressed WebGL bundle |
unity_module/ folderLibrary/ on first open (takes 2–5 min)Assets/Scenes/Environment.unity — press Play to run the live sceneAssets/StreamingAssets/greenhouse_state.json every second — replace or update this file from Python/FastAPI to drive the 3D scene in real time| Script | Purpose | Type | Key Method(s) |
|---|---|---|---|
| GreenhouseStateApplier.cs | Central orchestrator; reads JSON, detects changes, applies state | Manager | SetState() dispatches to all controllers |
| FluorescentLightController.cs | Controls fluorescent light (status + spots) | Actuator | SetState(bool) |
| HeaterController.cs | Controls heater (indicator + glow) | Actuator | SetState(bool) |
| EnergyCanisterController.cs | Controls energy canister (status light) | Actuator | SetState(bool) |
| HumidifierController.cs | Controls humidifier (light + fog + audio) | Actuator | SetState(bool) |
| WindowFanController.cs | Controls window fan (light + particles + audio) | Actuator | SetState(bool) |
| VentController.cs | Controls vent system (light + particles + audio) | Actuator | SetState(bool) |
| WaterTankFloorController.cs | Controls water tank (light + audio) | Actuator | SetState(bool) |
| CropStageController.cs | Controls one crop’s visual stage | Environment | SetStageByName(string) |
| TimeOfDayController.cs | Controls lighting, skybox, fog, night lights | Environment | SetTimeOfDayByName(string) |
| CropHealthIndicator.cs | Controls RGB health lights + blinking | Environment | SetGreen(), SetYellow(), SetRed(), SetBlinkGreen(bool) |
| FreeCameraController.cs | Provides free camera controls | Utility | (no public state methods) |
Your Unity scene should have this structure:
Scene/
├── GreenhouseManager (Empty GameObject)
│ └── [GreenhouseStateApplier component attached]
│
├── Actuators/ (folder, organizing lights/effects)
│ ├── FluorescentLight
│ │ └── [FluorescentLightController component]
│ ├── Heater
│ │ └── [HeaterController component]
│ ├── EnergyCanister
│ │ └── [EnergyCanisterController component]
│ ├── Humidifier
│ │ └── [HumidifierController component]
│ │ └── StatusIndicator (Light child)
│ │ └── FogEffect (ParticleSystem child)
│ │ └── AudioSource (parent object)
│ ├── WindowFan
│ │ └── [WindowFanController component]
│ │ └── StatusIndicator (Light child)
│ │ └── AirFlow (ParticleSystem child)
│ │ └── AudioSource (parent object)
│ ├── Vent
│ │ └── [VentController component]
│ │ └── StatusIndicator (Light child)
│ │ └── AirFlow (ParticleSystem child)
│ │ └── AudioSource (parent object)
│ └── WaterTank
│ └── [WaterTankFloorController component]
│ └── StatusIndicator (Light child)
│ └── AudioSource (same object)
│
├── Crops/ (folder, organizing 15 crop plants)
│ ├── Crop_01
│ │ └── [CropStageController component]
│ │ ├── Seedling (Model)
│ │ ├── Vegetative (Model)
│ │ ├── FloweringInitiation (Model)
│ │ ├── Flowering (Model)
│ │ ├── Unripe (Model)
│ │ └── Ripe (Model)
│ │ └── CropHealth (ChildObject)
│ │ └── [CropHealthIndicator component]
│ │ ├── GreenLight (Light)
│ │ ├── YellowLight (Light)
│ │ └── RedLight (Light)
│ ├── Crop_02 ... Crop_15
│ │ └── [same structure as Crop_01]
│
├── Environment/ (folder)
│ └── [TimeOfDayController component on main light]
│ └── DirectionalLight (sun/moon)
│ └── NightLights (array of GameObjects for night)
│
├── Camera (MainCamera)
│ └── [FreeCameraController component]
│
└── [Skybox, Fog, Lighting settings]
When a crop grows, it progresses through 6 distinct stages. Each stage has a corresponding GameObject model that becomes visible/invisible.
| Index | Name | Visual | Duration (Typical) | Description |
|---|---|---|---|---|
| 0 | Seedling | Tiny sprout | Days 0–7 | Initial germination; very small plant |
| 1 | Vegetative | Young plant | Days 7–21 | Leaf growth; expanding stem |
| 2 | FloweringInitiation | Budding | Days 21–28 | Buds beginning to form |
| 3 | Flowering | Blooming | Days 28–42 | Full flowers open; peak beauty |
| 4 | Unripe | Young fruit | Days 42–56 | Fruit formed but not mature |
| 5 | Ripe | Ready to harvest | Days 56+ | Fully mature; ready for pickup |
"cropStage": { "stage": "Seedling" }
"cropStage": { "stage": "Vegetative" }
"cropStage": { "stage": "FloweringInitiation" }
"cropStage": { "stage": "Flowering" }
"cropStage": { "stage": "Unripe" }
"cropStage": { "stage": "Ripe" }
Variants accepted (all auto-converted):
"flowering_initiation" → FloweringInitiation"RIPE" → Ripe"vegetative" → VegetativeSetBlinkGreen(false) unless all crops are RipeAll 8 actuators follow the unified SetState(bool) pattern: true = on, false = off.
statusIndicatorLight, fluorescentSpotLight, fluorescentPointLight"fluorescentLight": { "isOn": true | false }statusIndicatorLight, heaterLight"heater": { "isOn": true | false }statusLight"energyCanister": { "isOn": true | false }statusIndicatorLight, fogParticlefogAudio (from parent)"humidifier": { "isOn": true | false }statusIndicatorLight, airFlowParticlefanAudio (from parent)"windowFan": { "isOn": true | false }statusIndicatorLight, airFlowParticleventAudio (from parent)"vent": { "isOn": true | false }statusLight (from StatusIndicator child), audioSource (self)"waterTankFloor": { "isOn": true | false }No additional actuators; lights are controlled by environment systems
Affects global lighting, skybox, fog, and night lights.
| Time | Lighting | Skybox | Fog | Sun Angle | Night Lights |
|---|---|---|---|---|---|
| Morning | Warm (0.75, 0.72, 0.65) | Clear sky | Warm fog (on) | 25°, 30° | Off |
| Afternoon | Bright (1.0, 1.0, 1.0) | Bright blue | Off | 60°, 0° | Off |
| Evening | Orange (1.0, 0.55, 0.3) | Orange sky | Orange fog (on) | 15°, 220° | Off |
| Night | Dark blue (0.4, 0.45, 0.6) | Night sky | Dim fog (on) | -10°, 0° | On |
JSON Values (Case-Insensitive):
"timeOfDay": { "time": "Morning" | "Afternoon" | "Evening" | "Night" }
Controls RGB status lights showing crop health and maturity.
| State | Color | Light | Blink? | Meaning |
|---|---|---|---|---|
| Green | RGB Green | Bright | Auto* | Healthy |
| Yellow | RGB Yellow | Medium | Never | Stressed |
| Red | RGB Red | Bright | Never | Critical |
*Blinking occurs only when all crops are Ripe, regardless of JSON blinkGreen value.
JSON Values (Case-Insensitive):
"cropHealth": {
"state": "Green" | "Yellow" | "Red",
"blinkGreen": false // IGNORED — computed from crop stages
}
Blink Speed: Configurable in CropHealthIndicator Inspector (blinkSpeed = 2.0 = 2 blinks/sec)
JSON File (greenhouse_state.json)
↓
GreenhouseStateApplier.Update() [every 1s]
↓
Change Detected? (compare file content hash)
↓ YES
ParseJson() → GreenhouseState object
↓
ApplyState() → dispatches to 8 controllers + 3 environment systems
↓
Each Controller.SetState() or SetStageByName() or SetTimeOfDayByName()
↓
Scene updates: Lights on/off, Particles play/stop, Models swap, Fog changes
↓
GreenhouseStateApplier.LateUpdate() [every frame]
↓
Re-assert health color (so CropStageController.SetGreen() doesn't override JSON)
↓
Scene persists health color every frame
Assets/
├── Scripts/
│ ├── GreenhouseStateApplier.cs [Central orchestrator]
│ ├── CropStageController.cs [Crop stage visuals]
│ ├── CropHealthIndicator.cs [RGB health lights]
│ ├── TimeOfDayController.cs [Global lighting/sky]
│ ├── FluorescentLightController.cs [Light actuator]
│ ├── HeaterController.cs [Light actuator]
│ ├── EnergyCanisterController.cs [Light actuator]
│ ├── HumidifierController.cs [Effects + audio]
│ ├── WindowFanController.cs [Effects + audio]
│ ├── VentController.cs [Effects + audio]
│ ├── WaterTankFloorController.cs [Effects + audio]
│ ├── FreeCameraController.cs [Utility]
│ └── GREENHOUSE_INTEGRATION_GUIDE.md [This document]
│
└── StreamingAssets/
└── greenhouse_state.json [Runtime state file]
GreenhouseStateApplier.cs is the single central point that controls:
It reads a JSON file from disk at a configurable polling interval, detects changes, and applies updates automatically during Play Mode.
✅ Reads JSON at runtime — no code changes needed to update scene state
✅ Detects changes automatically — only reapplies when file content changes
✅ Supports any crop count — JSON array can have 15, 50, or 100 crops
✅ Safe missing references — logs clear warnings but never crashes
✅ Health color auto-logic — forces Green+blink when all crops are Ripe
✅ LateUpdate assertion — health color persists even if other scripts modify it
✅ Production-ready — clean error handling, verbose logging, easy to extend
"GreenhouseManager")GreenhouseStateApplier componentFluorescent Light — GameObject with FluorescentLightControllerHeater — GameObject with HeaterControllerEnergy Canister — GameObject with EnergyCanisterControllerHumidifier — GameObject with HumidifierControllerWindow Fan — GameObject with WindowFanControllerVent — GameObject with VentControllerWater Tank Floor — GameObject with WaterTankFloorControllerCrop Stages (Array) — Drag all 15 crop GameObjects here (one per slot)Time Of Day — GameObject with TimeOfDayControllerCrop Health — GameObject with CropHealthIndicatorAssets/StreamingAssets/greenhouse_state.json1.0 (check file every 1 second)true (see detailed console output)Path: Assets/StreamingAssets/greenhouse_state.json
If the StreamingAssets folder doesn’t exist, create it:
Assets/
├── StreamingAssets/
│ └── greenhouse_state.json
All actuator scripts follow the unified control pattern:
[SerializeField] private bool isOn prevents accidental manual manipulationSetState(bool state) is the only method the backend callsdebugManualControl toggle for editor testingPurpose: Controls a fluorescent light fixture with status indicator and point lights
Controls:
Public Methods:
SetState(bool state) — main entry point (pass true to turn on)TurnOn() / TurnOff() — convenience helpersInspector Fields:
statusIndicatorLight — Light component for statusfluorescentSpotLight — Main spotlightfluorescentPointLight — Additional point light for ambiancedebugManualControl — Edit in Play Mode for testingPurpose: Controls a heater with indicator light and glow effect
Controls:
Public Methods:
SetState(bool state) — main entry pointTurnOn() / TurnOff() — convenience helpersInspector Fields:
statusIndicatorLight — Status indicatorheaterLight — Heater glow effectdebugManualControl — Edit in Play Mode for testingPurpose: Controls an energy canister with status light
Controls:
Public Methods:
SetState(bool state) — main entry pointTurnOn() / TurnOff() — convenience helpersInspector Fields:
statusLight — Energy status indicatordebugManualControl — Edit in Play Mode for testingPurpose: Controls a humidifier with fog particle effect and audio
Controls:
Public Methods:
SetState(bool state) — main entry pointTurnOn() / TurnOff() — convenience helpersInspector Fields:
statusIndicatorLight — Status indicatorfogParticle — Particle system for fog effectdebugManualControl — Edit in Play Mode for testingAuto-Found:
fogAudio — Looks for AudioSource on parent objectPurpose: Controls a window fan with air flow effect and audio
Controls:
Public Methods:
SetState(bool state) — main entry pointTurnOn() / TurnOff() — convenience helpersInspector Fields:
statusIndicatorLight — Status indicatorairFlowParticle — Particle system for air flowdebugManualControl — Edit in Play Mode for testingAuto-Found:
fanAudio — Looks for AudioSource on parent objectPurpose: Controls a ventilation system with air flow and audio trigger
Controls:
Public Methods:
SetState(bool state) — main entry pointTurnOn() / TurnOff() — convenience helpersInspector Fields:
statusIndicatorLight — Status indicatorairFlowParticle — Particle system for air flowdebugManualControl — Edit in Play Mode for testingAuto-Found:
ventAudio — Looks for AudioSource on parent objectNote: Audio plays only on state changes, not continuously (unlike fan)
Purpose: Controls a water tank with status light and audio
Controls:
StatusIndicator)Public Methods:
SetState(bool state) — main entry pointTurnOn() / TurnOff() — convenience helpersInspector Fields:
debugManualControl — Edit in Play Mode for testingAuto-Found:
statusLight — Looks in children for StatusIndicator child, then its LightaudioSource — Looks on same GameObject for AudioSourcePurpose: Controls a single crop’s visual stage and updates its health indicator
Growth Stages (0-5):
Seedling (0) — Initial stageVegetative (1) — Growth phaseFloweringInitiation (2) — Transition to floweringFlowering (3) — Bloom phaseUnripe (4) — Fruit developingRipe (5) — Ready to harvestPublic Methods:
SetStage(GrowthStage stage) — Pass enum directlySetStageByIndex(int index) — Pass 0-5, clamped automaticallySetStageByName(string stageName) — Pass string like "Ripe" or "flowering_initiation"NextStage() / PreviousStage() — Increment/decrementInspector Fields:
seedling / vegetative / floweringInitiation / flowering / unripe / ripe — GameObjects for each stage (only one visible at a time)healthIndicator — Reference to the CropHealthIndicator (auto-found from parent/children)debugManualControl — Edit in Play Mode for testingAuto-Behavior:
Ripe, automatically calls healthIndicator.SetGreen() and SetBlinkGreen(true)SetBlinkGreen(false)Purpose: Controls scene lighting, skybox, fog, and night lights
Time Periods:
Morning — Warm light, clear sky, some fogAfternoon — Bright sun, no fog, high intensityEvening — Orange light, orange sky, fogNight — Dark blue light, night sky, dim fog, night lights onPublic Methods:
SetTimeOfDay(TimeOfDay timeOfDay) — Pass enum directlySetTimeOfDayByName(string timeName) — Pass "Morning", "Afternoon", "Evening", or "Night"Inspector Fields:
directionalLight — Main sun/moon lightmorningSkybox / afternoonSkybox / eveningSkybox / nightSkybox — Material for each timenightLights — Array of GameObjects to enable only at nightenableKeyboardDebugControl — Press 1/2/3/4 to switch times in Play ModedebugManualControl — Edit in Play Mode for testingPurpose: Controls green/yellow/red status lights with optional blinking
Health States:
Green — Healthy (can blink if all crops are Ripe)Yellow — Moderate stressRed — Critical stressPublic Methods:
SetState(HealthState state) — Pass enum directlySetGreen() / SetYellow() / SetRed() — Direct helpersSetBlinkGreen(bool shouldBlink) — Enable/disable green blinkingInspector Fields:
greenLight / yellowLight / redLight — Light components for each stateblinkGreen — Manual blink toggle (read-only in normal use; GreenhouseStateApplier controls this)blinkSpeed — Speed of green blink (2.0 = 2 blinks per second)debugManualControl — Edit in Play Mode for testingAuto-Behavior (via GreenhouseStateApplier):
state: "Yellow" or "Red" but all crops are Ripe, automatically forced to Green + blinkPurpose: Provides free camera movement and look controls
Controls:
boundsBox if assignedInspector Fields:
boundsBox — Assign a Collider (usually a cube) to constrain camera movementCharacterController component on same GameObject){
"fluorescentLight": { "isOn": true | false },
"heater": { "isOn": true | false },
"energyCanister": { "isOn": true | false },
"humidifier": { "isOn": true | false },
"windowFan": { "isOn": true | false },
"vent": { "isOn": true | false },
"waterTankFloor": { "isOn": true | false },
"cropStage": { "stage": "Seedling|Vegetative|FloweringInitiation|Flowering|Unripe|Ripe" },
"timeOfDay": { "time": "Morning|Afternoon|Evening|Night" },
"cropHealth": {
"state": "Green|Yellow|Red",
"blinkGreen": false // IGNORED — computed from crop stages
}
}
"Ripe", "RIPE", "ripe" all work)cropStage value is applied to every crop in the Inspector array"flowering_initiation" → "FloweringInitiation")blinkGreen field is kept in schema for compatibility but is never read — it is always computed from crop stage{
"fluorescentLight": { "isOn": true },
"heater": { "isOn": false },
"energyCanister": { "isOn": true },
"humidifier": { "isOn": true },
"windowFan": { "isOn": false },
"vent": { "isOn": true },
"waterTankFloor": { "isOn": false },
"cropStage": { "stage": "Ripe" },
"timeOfDay": { "time": "Morning" },
"cropHealth": { "state": "Green", "blinkGreen": false }
}
Ensure your project has:
Assets/
├── Scripts/
│ ├── GreenhouseStateApplier.cs
│ ├── CropStageController.cs
│ ├── CropHealthIndicator.cs
│ ├── TimeOfDayController.cs
│ ├── FluorescentLightController.cs
│ ├── HeaterController.cs
│ ├── EnergyCanisterController.cs
│ ├── HumidifierController.cs
│ ├── WindowFanController.cs
│ ├── VentController.cs
│ ├── WaterTankFloorController.cs
│ ├── FreeCameraController.cs
│ └── GREENHOUSE_INTEGRATION_GUIDE.md (this file)
└── StreamingAssets/
└── greenhouse_state.json
"GreenhouseManager"GreenhouseStateApplier component to itIn the Inspector for GreenhouseStateApplier, drag:
Fluorescent Light ← GameObject with FluorescentLightControllerHeater ← GameObject with HeaterControllerEnergy Canister ← GameObject with EnergyCanisterControllerHumidifier ← GameObject with HumidifierControllerWindow Fan ← GameObject with WindowFanControllerVent ← GameObject with VentControllerWater Tank Floor ← GameObject with WaterTankFloorControllerCrop Stages array — Set Size = 15 (or however many crops you have), then drag each crop GameObject into Element 0–14. Note: All crops receive the same cropStage value from the JSON.Time Of Day ← GameObject with TimeOfDayControllerCrop Health ← GameObject with CropHealthIndicatorAssets/StreamingAssets/greenhouse_state.jsonPress Play — the GreenhouseStateApplier reads the JSON file and applies the state automatically within 1 second (default poll interval).
While Play Mode is running:
Assets/StreamingAssets/greenhouse_state.json
"heater": { "isOn": false } → "heater": { "isOn": true }"cropStage": { "stage": "Seedling" } → "cropStage": { "stage": "Ripe" }"state": "Green" → "state": "Yellow""time": "Morning" → "time": "Night"Save the file (Ctrl+S)
[GreenhouseStateApplier] Crop[0] stage -> Ripe
[GreenhouseStateApplier] CropHealth -> Green (blinking) [auto-overridden: all crops Ripe]
[GreenhouseStateApplier] All states applied successfully.
In the Inspector during Play Mode, tick the Force Refresh checkbox to immediately re-apply without waiting for the next poll interval. The checkbox auto-resets after use.
The current script reads from a local JSON file. To connect to a real Python FastAPI backend:
Find this in GreenhouseStateApplier.cs:
string ReadJsonFromDisk()
{
try
{
if (!File.Exists(_resolvedFilePath))
{
Debug.LogWarning("[GreenhouseStateApplier] JSON file not found: " + _resolvedFilePath);
return null;
}
return File.ReadAllText(_resolvedFilePath);
}
catch (Exception ex)
{
Debug.LogError("[GreenhouseStateApplier] Failed to read JSON file: " + ex.Message);
return null;
}
}
private IEnumerator ReadJsonFromApi()
{
string apiUrl = "http://localhost:8000/state"; // Your FastAPI endpoint
UnityWebRequest request = UnityWebRequest.Get(apiUrl);
yield return request.SendWebRequest();
if (request.result == UnityWebRequest.Result.Success)
{
return request.downloadHandler.text;
}
else
{
Debug.LogError("[GreenhouseStateApplier] API request failed: " + request.error);
return null;
}
}
Change from synchronous to coroutine:
// Before (synchronous)
string json = ReadJsonFromDisk();
// After (coroutine)
StartCoroutine(ReadJsonFromApiCoroutine());
IEnumerator ReadJsonFromApiCoroutine()
{
yield return StartCoroutine(ReadJsonFromApi());
// Continue with parsing and applying...
}
That’s it. Everything else in the pipeline (parsing, change detection, applying to controllers) stays exactly the same. The individual controller scripts need zero changes.
Each layer knows nothing about lower layers and can be tested independently.
Every actuator exposes SetState(bool) — a single, predictable method signature that the central applier can call uniformly. This makes it trivial to add new actuators later.
CropStageController.Update() might call SetGreen() (because a crop is Ripe). If GreenhouseStateApplier.Update() happens to run after that, it would override the health you just set in the JSON.
Solution: GreenhouseStateApplier uses LateUpdate() to re-assert health color after all Update() calls finish. This guarantees the health color you set in JSON persists every frame, regardless of execution order.
When all 15 crops are Ripe, the logic is:
"blinkGreen": false)This is business logic: “A fully mature crop garden should always show green + blinking.” The JSON cannot override this — it’s part of the scene’s DNA.
Since all crops share a single cropStage value, adding or removing crops is trivial:
CropStageController componentGreenhouseStateApplier Inspector, increase Crop Stages array SizecropStage value applies to all crops automatically.SoilSensorController.cs)GreenhouseStateApplier:
public SoilSensorController soilSensor;
void ApplySoilSensor(ActuatorState s)
{
if (s == null) return;
if (!AssertRef(soilSensor, "SoilSensorController")) return;
soilSensor.SetState(s.isOn);
Log("SoilSensor -> " + s.isOn);
}
ApplyState():
ApplySoilSensor(state.soilSensor);
GreenhouseState data class:
public ActuatorState soilSensor;
You can test any controller in isolation by manually calling its methods in the editor:
// Test FluorescentLight
fluorescent.SetState(true);
fluorescent.TurnOff();
// Test CropStage
crop.SetStageByName("Ripe");
crop.NextStage();
// Test Health
health.SetRed();
health.SetBlinkGreen(true);
Or enable debugManualControl in the Inspector to toggle values in Play Mode without touching code.
Library/PackageCache/com.unity.render-pipelines.universal@*/Shaders/Lit.shader_BaseColor updated to leaf green (0.298, 0.686, 0.314, 1)#4CAF50JSON File (disk)
↓
GreenhouseStateApplier (reads file every 1s)
↓
Individual Controllers (apply state)
↓
Scene (lights, particles, audio)
FastAPI Backend (Python)
↓ GET /state
↓
GreenhouseStateApplier (polls API every 1s)
↓
Individual Controllers (apply state)
↓
Scene (lights, particles, audio)
Only the first read step changes. Everything after is identical.
| Issue | Solution |
|---|---|
| Script not compiling | Check for missing references in Inspector |
| Scene not updating on JSON change | Verify file path is correct; check Console for errors |
| Health color won’t change to Red | Check debugManualControl is false; ensure not all crops are Ripe |
| Crop stage not changing | Verify cropStages array is assigned and not empty |
| No blink even though all Ripe | Verify cropHealth is assigned; check health state is “Green” or all-Ripe override should apply |
| Reference warnings in Console | Drag missing controller from scene into the Inspector slot |
| Version | Changes |
|---|---|
| 1.0 | Initial release: 8 actuators, 3 environment controllers, central JSON applier |
| - | Support for 15 crops (expandable) |
| - | LateUpdate health persistence |
| - | All-Ripe auto-blink override logic |
sample_greenhouse_scenarios.pysample_greenhouse_scenarios.py is a Python script that drives the live WebGL greenhouse through a curated set of 15 real-world growing conditions. It replaces manual JSON editing by POSTing each scenario directly to the FastAPI backend, which the Unity WebGL build polls every second.
Why HTTP and not a JSON file? WebGL builds run inside a browser sandbox and cannot read from the local filesystem. Writing
greenhouse_state.jsonon disk has no effect on the deployed scene. The script POSTs toPOST /api/greenhouse-3d/stateinstead — the backend stores the state in memory and the Unity WebGL build retrieves it viaGET /api/greenhouse-3d/state.
scripts/sample_greenhouse_scenarios.py
python scripts/sample_greenhouse_scenarios.py
| Variable | Default | Description |
|---|---|---|
AGRITWIN_NO_SERVER |
0 |
Set to 1 to skip launching main.py (use an already-running backend) |
AGRITWIN_API_URL |
http://localhost:8000 |
Base URL of the FastAPI backend |
main.py as a background processGET /api/system/health to return 200 (up to 120 s)http://localhost:8000/greenhouse-3d/ in the default browsersample_greenhouse_scenarios.py
↓ POST /api/greenhouse-3d/state (JSON body)
FastAPI backend (greenhouse_3d.py — stores state in memory)
↓ GET /api/greenhouse-3d/state (polled every 1 s)
GreenhouseStateApplier.cs (UnityWebRequest coroutine)
↓
Individual Controllers (apply state)
↓
WebGL Scene (lights, particles, audio, crop stages, sky)
15 scenarios covering the full crop lifecycle and edge cases:
| # | Name | Stage | Time | Health |
|---|---|---|---|---|
| 1 | Germination — Pre-Dawn Start | Seedling | Night | Green |
| 2 | Seedling — Morning Warm-Up | Seedling | Morning | Green |
| 3 | Vegetative — Peak Growth Afternoon | Vegetative | Afternoon | Green |
| 4 | Vegetative — Mild Heat Stress Warning | Vegetative | Afternoon | Yellow |
| 5 | Flowering Initiation — Evening Transition | FloweringInitiation | Evening | Green |
| 6 | Full Bloom — Optimal Conditions | Flowering | Afternoon | Green |
| 7 | Full Bloom — Disease Alert (Critical) | Flowering | Night | Red |
| 8 | Unripe Fruit — Night Ripening Mode | Unripe | Night | Green |
| 9 | Unripe Fruit — Water Deficit Stress | Unripe | Morning | Yellow |
| 10 | Ripe — Harvest-Ready (Full Blink) | Ripe | Afternoon | Green |
| 11 | Post-Harvest — Greenhouse Reset (All Off) | Seedling | Night | Green |
| 12 | Emergency — Total System Failure | Flowering | Night | Red |
| 13 | Cold Snap — Winter Night Protocol | Vegetative | Night | Green |
| 14 | Heatwave — Maximum Ventilation | Flowering | Afternoon | Yellow |
| 15 | Ideal Cycle — End-to-End Showcase | Flowering | Afternoon | Green |
| Method | URL | Purpose |
|---|---|---|
POST |
/api/greenhouse-3d/state |
Script writes each scenario here |
GET |
/api/greenhouse-3d/state |
Unity WebGL polls this every second |
The POST body is the standard greenhouse JSON schema (see JSON Schema & Format).
| Code | Meaning |
|---|---|
0 |
All scenarios applied successfully |
1 |
Backend failed to start or an API call failed |
✅ 12 C# Scripts — all production-ready, well-commented
✅ 8 Actuator Controllers — lights, effects, audio (all use SetState(bool))
✅ 3 Environment Controllers — crop stages, time of day, health indicator
✅ 1 Central Applier — reads JSON, auto-applies state
✅ 1 Sample JSON File — ready to edit and test
✅ 1 Integration Guide — this document (everything you need)
GreenhouseManager GameObject in your sceneGreenhouseStateApplier componentAssets/StreamingAssets/greenhouse_state.json)Every actuator is commanded identically: SetState(bool). You pass true or false. That’s it.
The JSON has one cropStage value (not 15). It applies to all 15 crops simultaneously. This matches your real project where the backend sends a single stage value.
Green blinking automatically activates when all crops are Ripe. You cannot manually disable it — it’s built in. Remove it from the JSON or set another state (Yellow/Red) stops blinking.
The applier only re-applies when file content actually changes. Touching the file without changing content = no re-apply (efficient).
Health color persists every frame, even if CropStageController.Update() tries to modify it. The applier always has the final say.
{
"fluorescentLight": { "isOn": true },
"heater": { "isOn": true },
"energyCanister": { "isOn": true },
"humidifier": { "isOn": true },
"windowFan": { "isOn": true },
"vent": { "isOn": true },
"waterTankFloor": { "isOn": true },
"cropStage": { "stage": "Ripe" },
"timeOfDay": { "time": "Afternoon" },
"cropHealth": { "state": "Green", "blinkGreen": false }
}
Result: All lights on, all particles running, all audio playing, every crop shows Ripe model, green light automatically blinking.
{
"fluorescentLight": { "isOn": true },
"heater": { "isOn": false },
"energyCanister": { "isOn": false },
"humidifier": { "isOn": true },
"windowFan": { "isOn": false },
"vent": { "isOn": false },
"waterTankFloor": { "isOn": true },
"cropStage": { "stage": "Seedling" },
"timeOfDay": { "time": "Morning" },
"cropHealth": { "state": "Green", "blinkGreen": false }
}
Result: Lights on for growth, heater off (seedlings sensitive), humidifier on, all crops show Seedling model, morning lighting, green light (no blink).
{
"fluorescentLight": { "isOn": true },
"heater": { "isOn": true },
"energyCanister": { "isOn": true },
"humidifier": { "isOn": false },
"windowFan": { "isOn": true },
"vent": { "isOn": true },
"waterTankFloor": { "isOn": false },
"cropStage": { "stage": "Flowering" },
"timeOfDay": { "time": "Night" },
"cropHealth": { "state": "Red", "blinkGreen": false }
}
Result: All systems active, humidifier off, crops in Flowering, night lighting, red health light (critical stress).
Phase 1: Local Testing (NOW)
└─ Edit JSON manually
└─ Watch scene update in real-time
└─ Verify all actuators work
└─ Test all combinations
Phase 2: Backend Integration (FUTURE)
└─ Replace ReadJsonFromDisk() with UnityWebRequest
└─ Point to your FastAPI /state endpoint
└─ Everything else unchanged
└─ Drop into production
1. Create MyNewController.cs with SetState(bool)
2. Add to GreenhouseStateApplier class:
- Public field: public MyNewController myNew;
- Apply method: void ApplyMyNew(ActuatorState s) { ... }
- Call in ApplyState(): ApplyMyNew(state.myNew);
3. Update GreenhouseState data class:
- public ActuatorState myNew;
4. Update JSON schema and sample file
5. Done. No other scripts touched.
Check in this order:
cropHealth component is assigned in InspectordebugManualControl is false on CropHealthIndicator[GreenhouseStateApplier] messagesforceRefresh checkbox in Inspector to re-apply immediately| Field | Type | Valid Values |
|---|---|---|
fluorescentLight.isOn |
bool | true, false |
heater.isOn |
bool | true, false |
energyCanister.isOn |
bool | true, false |
humidifier.isOn |
bool | true, false |
windowFan.isOn |
bool | true, false |
vent.isOn |
bool | true, false |
waterTankFloor.isOn |
bool | true, false |
cropStage.stage |
string | Seedling, Vegetative, FloweringInitiation, Flowering, Unripe, Ripe (case-insensitive) |
timeOfDay.time |
string | Morning, Afternoon, Evening, Night (case-insensitive) |
cropHealth.state |
string | Green, Yellow, Red (case-insensitive) |
cropHealth.blinkGreen |
bool | true, false (IGNORED — auto-computed) |
ApplyCropHealth() call per frame (minimal)End of Integration Guide