2026-03-29 — SFX Options for All Assigned Slots
Summary
Added alternative option files for every currently-assigned SFX slot so the user can audition and swap without losing the existing working sounds.
AUDIO.md
Added ## SFX Options — Assigned Slots (Alternatives) section to playinstigator_docs/Games/Ritual & Ruin/AUDIO.md with per-slot tables listing all downloaded files and Freesound URLs.
---
Next Steps
- Open Unity, navigate to
Assets/ProtoV2/Audio/SFX/Options/ in Project window
- Audition each folder to find preferred alternatives
- Copy chosen file to correct
SFX/ subfolder, rename, re-assign in Inspector
---
2026-03-29 — Audio Variation System + SFX Options
Summary
Built centralized audio variation system and sourced replacement options for all unassigned SFX slots.
---
AudioVariation System
Created Assets/ProtoV2/Scripts/Audio/AudioVariation.cs — a single [Serializable] class embedded in every audio script as one Inspector foldout. Replaces the scattered per-script variation fields that were being duplicated.
Parameters: pitch ±, volume ±, startTime (random clip offset), low-pass cutoff ±, reverb ±
Methods:
Init(go) — called in Awake, caches AudioLowPassFilter / AudioReverbFilter if present
PlayOneShot(source, clip, vol) — variation + PlayOneShot
PlayLoop(source, clip, vol) — variation + pitch/filter + Play() with loop=true
PlayFromOffset(source, clip, vol) — variation + startTime + Play() non-looping (throttled impacts)
All 6 audio scripts updated to use it: AudioManager, PlayerAudioSource, AltarAudioSource, BloodEmitterAudioSource (two instances — loop + burst), FloorImpactAudio, UIAudioManager.
Bug fixed: FloorImpactAudio.SharedImpactClip was never being set at runtime — floor blood impacts were silent. Fixed by adding bloodImpactClip field to AudioManager and wiring the static in Awake(). Requires assigning blood_impact.ogg to the new "Floor Impact" field on the AudioManager GO in Inspector.
---
SFX Audit
Checked which audio slots are actually assigned vs. missing. Found that several "removed" clips still have files on disk but are unassigned in the Inspector:
| Slot | File | Status |
|---|---|---|
| deathClip | SFX/Character/player_death.ogg | Exists but unsuitable |
| formTransformClip | SFX/Character/form_transform.ogg | Exists but unsuitable |
| flowLoopClip | SFX/Fluid/emitter_flow.ogg | Exists but unsuitable |
| All 5 UIAudioManager clips | SFX/UI/*.ogg | Exist but unsuitable |
---
SFX Options Downloaded
Downloaded and organised replacement options to Assets/ProtoV2/Audio/SFX/Options/:
| Folder | Files | Sources |
|---|---|---|
| Options/Death/ | 9 | Kenney Sci-Fi Sounds (slime, explosionCrunch), Kenney Impact Sounds (impactSoft_heavy) |
| Options/Transform/ | 8 | Kenney Sci-Fi Sounds (forceField x5, computerNoise x2) |
| Options/FlowLoop/ | 9 | Kenney Sci-Fi Sounds (spaceEngineLow, engineCircular, spaceEngineSmall) |
| Options/UI_Click/ | 11 | Kenney Interface Sounds (click x5), Kenney UI Audio (click x5, mouseclick) |
| Options/UI_Hover/ | 12 | Kenney UI Audio (rollover x6), Kenney Interface Sounds (scroll x4, tick x2) |
| Options/UI_Pause/ | 15 | Kenney Interface Sounds (toggle x4, switch x7, open x2, close x2) |
| Options/UI_PlayerJoin/ | 7 | Kenney Interface Sounds (confirmation x4, bong, pluck x2) |
| Options/UI_Transition/ | 9 | Kenney Interface Sounds (maximize x5, glitch x4) |
All Kenney files are CC0. Total: 80 auditionable options across 8 slots.
Additional Freesound CC0 and Pixabay options (40+ more) documented in AUDIO.md with direct URLs for manual download (Freesound requires login).
---
AUDIO.md Updated
playinstigator_docs/Games/Ritual & Ruin/AUDIO.md updated with:
- Corrected slot statuses (file-exists-but-unsuitable vs. truly unassigned)
- Full Options section per slot — downloaded files + manual-download Freesound/Pixabay links
- How-to-assign workflow
- Credits table updated with Kenney Sci-Fi Sounds and Impact Sounds
---
Next Steps
- Audition Options files in Unity, pick one per slot, rename and assign in Inspector
- Assign
blood_impact.ogg to AudioManager GO's "Floor Impact" field (fix for silent floor impacts)
- For slots where no Kenney option feels right: download preferred Freesound/Pixabay option using the URLs in AUDIO.md
---
Evolution Size Fix + Asset Catalog + Resolved Issue Cleanup (2026-03-29)
---
1. Terminal Evolution Size Fix
Issue
Terminal evolution (Tier 2) scale multiplier of 1.5× made the jellyfish too large to pass through floor holes.
Fix
File: Assets/ProtoV2/Scripts/JellyfishVisuals.cs — line 194
Scale progression (final):
| Tier | Scale | Visual cues |
|---|---|---|
| Base (0) | 1.0× | White tint, squish 0.20 |
| Enhanced (1) | 1.2× | Blue tint, squish 0.28 |
| Terminal (2) | 1.3× | Gold tint, squish 0.38, pulse 0.75× faster |
Terminal is still visually distinct (gold color, faster pulse, more squish, slightly larger) but now fits through floor holes.
---
2. Resolved Issues Cleanup
Removed the following items from the open blockers list — all confirmed resolved:
- TQ-4 (Pause + ObiFluid): ObiFluid particles correctly pause/resume with
Time.timeScale = 0. No code changes needed.
- maxSurfaceChunks: Already reduced to 2000 in ObiSolver Inspector. No further action needed.
- ObiNativeList finalizer: Mitigated by D3D11 (GPU buffers handled more gracefully). Lower priority, not blocking.
Open blockers: None.
---
3. Asset Catalog
Created Confirmed/strategy/Asset Catalog.md — full inventory of 38 purchased Unity assets with applicability ratings for Ritual & Ruin.
Summary:
- Tier 1 (use these): Feel, Amplify Shader Editor, Amplify Shader Pack, Obi Rope, Obi Softbody, Motion Blur, Magic Effects FREE, DOTween
- Tier 2 (potentially useful): Elemental Spells VFX, Easy Save, KayKit Platformer Pack, Weather Maker, Fantasy RPG GUI, Obi Cloth
- Tier 3 (not relevant): Humanoid character packs, city environments, turret assets, tank, fruit market, 2D tools
Priority integration order: Feel → Obi Rope → Motion Blur → Magic Effects FREE → Easy Save → Amplify Shader Editor
---
4. Evolution Visual Distinction — Next Steps
Current visual distinction between tiers (post-fix):
- Size: 1.0 → 1.2 → 1.3 (subtle)
- Color: white → blue → gold ✅ clear
- Squish: 0.20 → 0.28 → 0.38 (subtle)
- Pulse rate: unchanged → unchanged → 0.75× faster ✅ readable
Suggested enhancements using owned assets:
- Feel: Add camera shake burst + flash on evolution event (
OnEvolution subscriber)
- Magic Effects FREE: Spawn a VFX burst prefab at player position on evolution
- Obi Rope: Upgrade tentacles to physical simulation (more dramatic swing on evolution)
- Amplify Shader Editor: Add emissive glow increase per tier to jellyfish bell material
Design discussion needed on which to prioritize.
---
Jellyfish Squish + Emitter Indicator + Scroll Camera Fix (2026-03-29)
---
1. Jellyfish Cap Horizontal Squish
Issue
XZ bulge on pulse was 30% of squish amount — too subtle.
Fix
File: Assets/ProtoV2/Scripts/JellyfishVisuals.cs
XZ bulge doubled — bell now visibly widens on each pulse. Spring recovery unchanged.
---
2. Emitter Indicator — No Longer Lands on Players
Issue
BloodEmitterIndicator.DrawCircle() used Physics.Raycast with no layer filter. When a player flew between the emitter and the floor, the raycast hit the player's collider, placing the circle on top of the player.
Root Cause
All objects (players, floors, walls) are on the Default layer — no layer separation to filter against.
Fix
File: Assets/ProtoV2/Scripts/BloodSystem/BloodEmitterIndicator.cs
Replaced physics raycast entirely with chunk-coordinate lookup. We already know the grid layout — no need to cast rays:
Walks top→middle→bottom floor. Finds first floor that has a solid chunk at the emitter's XZ grid position. Surface Y derived from floor root + ChunkHeight/2. Removed raycastLength field entirely.
---
3. Camera Scroll — Single Source of Truth for Floor Spacing
Issue
ScrollingFloorCamera had its own [SerializeField] float floorVerticalSpacing = 5f independent from FloorManager.floorVerticalSpacing. If the two values differed in the Inspector, the camera would scroll to the wrong Y position.
Fix
File: Assets/ProtoV2/Scripts/CameraSystem/ScrollingFloorCamera.cs — CalculateFloorYPosition()
Camera's own floorVerticalSpacing field is now only a fallback for editor gizmos. At runtime, both floor generation and camera scroll use the same value from FloorManager.
Current values (defaults)
FloorManager.floorVerticalSpacing = 5f — adjust this to change floor spacing
ScrollingFloorCamera.scrollDuration = 2.5f — how long the scroll animation takes
---
4. Ritual Completion VFX — AltarVFXController
Status
Feel (More Mountains) and Magic Effects FREE (Hovl Studio) are now imported.
New File
Assets/ProtoV2/Scripts/BloodSystem/AltarVFXController.cs — attach alongside AltarParticleConsumer on the Altar prefab.
Drives:
OnMeterFull → spawn looping countdown VFX (e.g. Magic circle.prefab or Healing circle.prefab)
OnRitualComplete → destroy countdown VFX + spawn completion burst (e.g. Red energy explosion.prefab) + play MMF_Player feedbacks (screen shake, flash, audio)
OnAltarReset → destroy countdown VFX
Inspector Wiring Required (on Altar prefab)
1. Add AltarVFXController component
2. Create child GO with MMF_Player, add Screen Shake + Camera Flash + Audio feedbacks → assign to completionFeedbacks
3. Assign completionVFXPrefab = Red energy explosion.prefab (Hovl Studio Magic Effects)
4. Assign countdownVFXPrefab = Magic circle.prefab or Healing circle.prefab
5. Tune completionVFXYOffset and countdownVFXYOffset to sit correctly at altar ground level
Also Available
- Elemental Spells Full Pack VFX — still in
.unitypackage archive, not yet imported. Contains additional burst/explosion options for ritual completion.
---
5. Lobby Scene — Tentacle Scale Fix
Issue
JellyfishVisuals tentacle rim positions were set in world-space Inspector values. characterScale in LobbyScene was 1.0 while MatchScene uses 0.25. At scale 1, the bell is 4× larger than the rim radius (0.4), so tentacles appeared hidden inside the player body.
Fix — Two Parts
Part A: LobbyScene.unity — MultiplayerManager.characterScale changed 1 → 0.25 so all scenes use the same scale.
Part B: JellyfishVisuals.cs — BuildTentacles() and UpdateTentacles() now multiply all spatial values by transform.lossyScale.x (ws):
Inspector values are now local-space (as if characterScale = 1). ws scales them to world-space at runtime, so tentacles are always proportionally correct regardless of characterScale.
Part C: PlayerPrefab.prefab Inspector values converted from old world-space to new local-space (÷ 0.25 = × 4):
| Field | Before (world-space) | After (local-space) |
|---|---|---|
| rimRadius | 0.4 | 1.6 |
| rimY | 0.56 | 2.24 |
| segmentLength | 0.15 | 0.6 |
| startWidth | 0.15 | 0.6 |
| endWidth | 0.04 | 0.16 |
At characterScale = 0.25, ws = 0.25, so effective world-space values are identical to before.
---
Dev Log — 2026-03-29 — MMFeedbacks Integration
Summary
Full MMFeedbacks (Feel) integration across all major gameplay events in Ritual & Ruin. Every significant player action, match event, and altar moment now triggers camera shake and/or freeze-frame feedback.
---
What Was Done
AltarVFXController — Namespace Fix
- File:
Assets/ProtoV2/Scripts/BloodSystem/AltarVFXController.cs
- Added
using CupFairy.BloodSystem; to fix CS0246 compile error (AltarParticleConsumer not found)
- Controller was already written; this unblocked it from compiling
Script Changes — MMF_Player Fields Added
All scripts received using MoreMountains.Feedbacks; + a [SerializeField] private MMF_Player field + a ?.PlayFeedbacks(transform.position) call at the relevant event moment:
| Script | Field | Trigger |
|---|---|---|
| DeathHandler.cs | deathFeedback | Before OnDied?.Invoke() |
| UnifiedBar.cs | evolutionFeedback | Before OnEvolution?.Invoke() in TriggerEvolution() |
| CharacterController1.cs | pulseFeedback | After PulsedThisFrame = true in pulse timer |
| TransformController.cs | formToggleFeedback | At start of Toggle() |
| MatchCountdown.cs | countdownBeatFeedback, goFeedback | Each countdown beat tick; GO! display |
| MatchManager.cs | teamEliminatedFeedback, matchEndFeedback | RecordTeamEliminated(); EndMatch() |
PlayerPrefab — 4 Feedback Child GOs
Added 4 new child GameObjects under the PlayerPrefab root, each with a Transform + MMF_Player component:
| GO Name | fileID | Wired to Script Field | Feedbacks |
|---|---|---|---|
| DeathFeedbacks | &1111111111111111103 | DeathHandler.deathFeedback | FreezeFrame 0.08s + PositionShake 0.4s/range 0.5 |
| EvolutionFeedbacks | &1111111111111111106 | UnifiedBar.evolutionFeedback | FreezeFrame 0.05s + PositionShake 0.3s/range 0.3 |
| PulseFeedbacks | &1111111111111111109 | CharacterController1.pulseFeedback | PositionShake 0.1s/range 0.05 (subtle — fires every 0.35s) |
| FormToggleFeedbacks | &1111111111111111112 | TransformController.formToggleFeedback | PositionShake 0.2s/range 0.15 |
All 4 Transform fileIDs (1111111111111111102, 105, 108, 111) added to root Transform m_Children list (fileID 5020478598309948295).
MatchScene — 4 Feedback Child GOs + Scene Components
Previously added (pre-this-session):
MMPositionShaker on Main Camera
MMTimeManager GO in scene
- 4 feedback child GOs under MatchCountdown and MatchManager
This session — populated the MMF_Player feedbacks in each GO:
| GO Name | fileID | Wired to Script Field | Feedbacks |
|---|---|---|---|
| CountdownBeatFeedbacks | &1482767222 | MatchCountdown.countdownBeatFeedback | PositionShake 0.1s/range 0.1 |
| GoFeedbacks | &872176054 | MatchCountdown.goFeedback | FreezeFrame 0.05s + PositionShake 0.3s/range 0.3 |
| TeamEliminatedFeedbacks | &247481994 | MatchManager.teamEliminatedFeedback | FreezeFrame 0.06s + PositionShake 0.35s/range 0.4 |
| MatchEndFeedbacks | &1200068797 | MatchManager.matchEndFeedback | FreezeFrame 0.1s + PositionShake 0.5s/range 0.5 |
Altar Prefab — Completion Feedbacks (Previous Session)
Already done: CompletionFeedbacks child MMF_Player has:
- MMF_Light (existing)
- MMF_FreezeFrame (0.05s)
- MMF_PositionShake (0.3s/range 0.3, random XY)
---
Feedback Intensity Rationale
| Event | Intensity | Reasoning |
|---|---|---|
| Match end | Strongest (0.1s freeze, 0.5 shake) | Game-ending moment, maximum impact |
| Player death | Strong (0.08s freeze, 0.5 shake) | High-stakes moment |
| Team eliminated | Medium-strong (0.06s freeze, 0.4 shake) | Significant match event |
| GO! start | Medium (0.05s freeze, 0.3 shake) | Match start signal |
| Evolution | Medium (0.05s freeze, 0.3 shake) | Power-up moment |
| Altar completion | Medium (0.05s freeze, 0.3 shake) | Ritual payoff |
| Form toggle | Subtle (0.2s shake/0.15 range) | Frequent action, shouldn't fatigue |
| Countdown beat | Subtle (0.1s shake/0.1 range) | Rhythmic, very frequent |
| Staccato pulse | Minimal (0.1s shake/0.05 range) | Fires every 0.35s, must be imperceptible individually |
---
Technical Notes
- All feedbacks use
managed reference YAML format (FeedbacksList: + references: RefIds:) required by MMF_Player v3
- Scene MMF_Players use a hybrid format (
Feedbacks: [] legacy field + FeedbacksList: + references:) — both must be present
MMTimeManager GO required in scene for FreezeFrame to function (timescale manipulation)
MMPositionShaker on Camera required to receive PositionShake events (channel 0)
Owner field in each feedback data points to the MMF_Player component's own fileID
- All PositionShake feedbacks:
RandomizeDirectionX: 1, RandomizeDirectionY: 1, RandomizeDirectionZ: 0 (2D shake only, no depth)
---
Files Modified
Assets/ProtoV2/Scripts/BloodSystem/AltarVFXController.cs
Assets/ProtoV2/Scripts/DeathHandler.cs
Assets/ProtoV2/Scripts/UnifiedBar.cs
Assets/ProtoV2/Scripts/CharacterController1.cs
Assets/ProtoV2/Scripts/TransformController.cs
Assets/ProtoV2/Scripts/MatchCountdown.cs
Assets/ProtoV2/Scripts/MatchManager.cs
Assets/ProtoV2/Prefabs/PlayerPrefab.prefab (4 new GO+Transform+MMF_Player blocks)
Assets/ProtoV2/Scenes/MatchScene.unity (4 MMF_Player feedbacks populated)
---
Obi Fluid Memory Leak — Continued Investigation (2026-03-29)
Continued from 2026-03-28_ObiFluid_MemoryLeak_Fix.md. Previous session patched the rendering-side leak (VolumePass materials, MaterialPropertyBlock, AsyncGPUReadback closures). This session found and addressed the simulation-side leak.
---
Monitoring Setup
PowerShell script at C:/Users/ReconUnPro/AppData/Local/Temp/monitor_mem2.ps1 — polls every 5s:
- Game process working set (MB) via
Get-Process
- System RAM available / used / % via
Get-CimInstance Win32_OperatingSystem
---
Isolation Test Results
Build #006 — All Obi Rendering Disabled
Disabled three layers to fully strip rendering:
1. Removed ObiFluidRendererFeature from m_RendererFeatures in PC_Renderer.asset
2. ObiParticleRenderer → m_Enabled: 0 on BloodEmitter.prefab
3. ObiFluidSurfaceMesher → m_Enabled: 0 on BloodEmitter.prefab
Result: Memory still grew at ~161 MB/s. Flush/scene reload gave no relief.
Conclusion: Leak exists on BOTH sides:
- Rendering side: ~320 MB/s (patched in prior session, confirmed by rate drop)
- Simulation side: ~161 MB/s (still present — this session's target)
---
Patches Applied This Session
Patch 3 — `ObiSolver.PushActiveParticles()` (previously applied)
Pre-allocate activeParticles to allocParticleCount before clear so EnsureCapacity never fires mid-emission.
Patch 4 — `ComputeSolverImpl.SetActiveParticles()` (previously applied)
Guard: only call AsComputeBuffer if buffer is null. Otherwise UploadFullCapacity().
Patch 5 — `ObiSolver.PushSimplices()`
File: Assets/Obi/Scripts/Common/Solver/ObiSolver.cs
Added before simplices.Clear():
Why: dirtySimplices is set every time a particle is activated (every emission frame). Without pre-allocation, EnsureCapacity fires repeatedly as the active count grows, each time doubling capacity and nulling m_ComputeBuffer.
Patch 6 — `ComputeSolverImpl.SetSimplices()`
File: Assets/Obi/Scripts/Common/Backends/Compute/Solver/ComputeSolverImpl.cs
Replaced unconditional AsComputeBuffer calls on simplices (line 515) and cellCoords (line 516):
Result of builds #006-#007: Memory still grew at ~161 MB/s. These patches were correct but insufficient — the ROOT CAUSE was elsewhere.
---
Root Cause Found — Bloated Blueprint Capacity
The Discovery
Deep Obi source analysis by Opus architect agent identified the actual culprit.
BloodFluid.asset had capacity: 20000 per emitter.
With 9 emitters active (3 emitters/floor × 3 floors), the solver pre-allocates:
- Total particle slots: 9 × 20,000 = 180,000
positions.count = 180,000 (used by SetCapacity)
This caused ComputeParticleGrid.SetCapacity() to create:
| Buffer | Size |
|---|---|
| neighbors (180k × 2 × 128 neighbors × 4B) | 184 MB |
| All 20+ grid buffers total | ~230 MB |
| colliderContacts readback buffer | ~46 MB |
| 26 particle arrays in ParticleCountChanged | ~170 MB |
~650 MB allocated at match start — before a single particle is emitted.
The D3D12 driver pools these on disposal (never returns to OS), so repeated SetCapacity calls accumulate permanently. colliderContacts.Readback() in ObiSolver.RequestReadback() fires every frame (whenever OnCollision != null, which our scripts always satisfy), staging a ~46 MB readback buffer per frame into D3D12's READBACK heap.
Why 20,000 Was Wrong
Our ObiEmitter runs in burst mode:
- 7 particles/sec × 1.5s burst every 5s = ~2.1 particles/sec average
- Particle lifespan: 60s
- Peak live particles per emitter: 2.1 × 60 = ~126
20,000 capacity = 158× overprovision. The default Obi blueprint value was never adjusted for our actual usage.
The Fix
Assets/ProtoV2/Blueprints/BloodFluid.asset line 1559:
200 gives 1.6× headroom over peak (126 live particles). With 9 emitters:
- Total particle slots: 9 × 200 = 1,800 (vs 180,000)
neighbors buffer: 1.8 MB (vs 184 MB) — 100× reduction
colliderContacts readback: ~460 KB (vs 46 MB) — 100× reduction
---
Secondary Root Cause — ObiNativeList Finalizer (Scene Reload Doesn't Help)
ObiNativeList.~ObiNativeList() calls DisposeOfComputeBuffer() from the GC finalizer thread. GraphicsBuffer.Dispose() must run on the main thread — called from the wrong thread, it silently fails. GPU buffers are permanently orphaned and survive scene reload.
This explains why Flush/scene restart never released memory — not a leak per se but a one-time orphan per solver lifetime.
Status: Not patched yet. Lower priority now that capacity is fixed.
---
Build Log
| Build | Change | Sim Leak Rate | Notes |
|---|---|---|---|
| #004 (prev) | Rendering patches | ~481 MB/s | Rendering still enabled |
| #005 | m_Active: 0 attempt | ~481 MB/s | Wrong YAML field — fluid still rendered |
| #006 | All rendering disabled | ~161 MB/s | Confirmed sim-side leak |
| #007 | SetSimplices patch | ~161 MB/s | Patch correct but not root cause |
| #008 | capacity: 200 + SetSimplices | ~134 MB/s | Baseline polluted (monitoring started mid-match at 20 GB); capacity patches correct but D3D12 root cause not yet identified |
| #009 | ObiSolver disabled | ~135 MB/s (polluted) / flat after reboot | Confirmed leak stops when match ends even without Obi; polluted D3D12 baseline made rate unreliable |
| #010 | ObiSolver disabled + D3D11 | ~0 MB/s | Flat 1280 MB throughout full match — confirmed D3D12 deferred-release pool is root cause |
| #011 | Full Obi re-enabled + D3D11 | ~0.1 MB/s | FIXED — 1337→1347 MB over 90s. Stable. |
---
Root Cause (Final)
The leak was D3D12-specific deferred-release pool behaviour, not a true memory leak in code.
- D3D12:
GraphicsBuffer.Dispose() queues the buffer for deferred release. The driver returns memory to an internal pool, NOT to the OS. New allocations commit fresh OS pages instead of reusing pooled ones. Pool grows monotonically for the lifetime of the process.
- D3D11: Disposed resources are returned to the driver pool immediately and reused for new allocations. Working set stays flat.
Obi's emit/die particle lifecycle creates a small but steady stream of new GraphicsBuffer allocations per frame (SetSimplices, readbacks). On D3D11 these are reused. On D3D12 they permanently expand the pool.
Fix applied: ProjectSettings.asset — Standalone Windows graphics API forced to Direct3D11 (m_APIs: 02000000, m_Automatic: 0).
---
What Didn't Work / Lessons
m_Enabled: 0 on a URP ScriptableRendererFeature — WRONG field. URP checks m_Active, not m_Enabled.
- Removing ObiFluidRendererFeature from
m_RendererFeatures — correct way to disable URP features in YAML.
SetSimplices/SetActiveParticles guards — correct patches but not the root cause. The leak was D3D12 pool behaviour, not allocation frequency or size.
- Capacity 20000→200 — correct and reduces one-time static allocation by 100×, but doesn't fix D3D12 pool growth.
- Polluted D3D12 baseline (no reboot between test builds) made rate measurements unreliable across builds. Always reboot when comparing rates.
- Source-only analysis hit limits — binary isolation (disable ObiSolver, then switch API) was faster than reading code.
---
Remaining Items
- [x] Re-enable rendering (ObiFluidRendererFeature, ObiParticleRenderer, ObiFluidSurfaceMesher)
- [x] Verify memory is stable with rendering re-enabled — CONFIRMED 0.1 MB/s
- [x] Force D3D11 for all Standalone builds —
ProjectSettings.asset permanently set (m_APIs: 02000000, m_Automatic: 0)
- [ ] Fix ObiNativeList finalizer thread issue (GPU buffer orphaning on scene reload) — lower priority, one-time per session; mitigated by D3D11
- [ ] Update
maxSurfaceChunks if needed now that particle count is much smaller
---
Status: RESOLVED (2026-03-29)
Memory leak investigation is complete. Game is stable at ~0.1 MB/s (effectively flat). All Standalone builds will use Direct3D11 permanently for as long as Obi Fluid is in the project. Proceeding to next development phase.
---
2026-03-31 — Centralised Audio Management Panel
Summary
Added a GameAudioData ScriptableObject as a single source of truth for all 22 audio clip slots across the game. Includes a rich custom Inspector panel with category foldouts, clip-count badge, preview buttons, and auto-link tools.
---
Problem
Audio clips were scattered across 5 different components on 4 different prefabs/GOs:
AudioManager (MatchScene GO) — 9 clips
PlayerAudioSource (PlayerPrefab) — 5 clips
AltarAudioSource (Altar prefab) — 3 clips
BloodEmitterAudioSource (BloodEmitter prefab) — 2 clips
UIAudioManager (DontDestroyOnLoad GO) — 5 clips
To reassign a clip you had to hunt through 4 prefabs. No way to see the full audio inventory at a glance.
---
Solution
New Files
Assets/ProtoV2/Scripts/Audio/GameAudioData.cs — ScriptableObject holding all 22 AudioClip fields + sfxGroup / uiGroup AudioMixerGroup refs, grouped by category:
- Match/Countdown, Floor Scroll, Match Outcome, Outlast Ticker, Floor Impact
- Player, Altar, Blood Emitter, UI
Assets/ProtoV2/Scripts/Editor/GameAudioDataEditor.cs — Custom Inspector with:
- Bold header "Ritual & Ruin — Audio Data"
HelpBox badge: "X / 22 clips assigned" (warning if incomplete)
- Per-category colour-coded foldouts (persisted via EditorPrefs) each showing
assigned/total
- Every slot: object field +
▶ preview button (AudioUtil reflection, fallback safe)
- "Find & Link Clips from SFX Folder" button — scans
Assets/ProtoV2/Audio/SFX/ and auto-assigns by filename pattern matching
- "Find & Link Mixer Groups" button — loads
GameAudioMixer from Resources and assigns SFX + UI groups
Assets/ProtoV2/Scripts/Editor/GameAudioDataWizard.cs — Menu items under Tools/Ritual & Ruin/Audio/:
- Create GameAudioData Asset — creates
Assets/ProtoV2/Audio/GameAudioData.asset, selects + pings it
- Auto-Link Clips & Mixers — one-shot auto-fills the asset from the SFX folder + mixer, logs all assignments
Modified Files (backward-compatible)
Each of the 5 audio components gained:
1. [Header("Audio Data")] [SerializeField] private GameAudioData _audioData; as the first serialized field
2. An if-block at the top of Awake() that copies clips from _audioData into the existing private fields when assigned
Components affected:
AudioManager.cs — 9 clips + sfxGroup
PlayerAudioSource.cs — 5 clips + sfxGroup
AltarAudioSource.cs — 3 clips + sfxGroup
BloodEmitterAudioSource.cs — 2 clips + sfxGroup
UIAudioManager.cs — 5 clips + uiGroup
Backward compatible: if _audioData is null, all individual Inspector-assigned clips continue to work unchanged.
---
How to Set Up
1. Tools/Ritual & Ruin/Audio/Create GameAudioData Asset — creates the asset
2. Select it in Project → Inspector shows the full panel
3. Click "Find & Link Clips from SFX Folder" + "Find & Link Mixer Groups" to auto-fill
4. Assign the asset to the _audioData slot on: AudioManager (MatchScene), UIAudioManager (MainMenu scene), PlayerPrefab, Altar prefab, BloodEmitter prefab
Or use Tools/Ritual & Ruin/Audio/Auto-Link Clips & Mixers to do steps 2+3 from a menu item.
---
Files Changed
| File | Change |
|---|---|
| Assets/ProtoV2/Scripts/Audio/GameAudioData.cs | NEW — ScriptableObject, 22 clips + 2 mixer groups |
| Assets/ProtoV2/Scripts/Editor/GameAudioDataEditor.cs | NEW — Custom Inspector panel |
| Assets/ProtoV2/Scripts/Editor/GameAudioDataWizard.cs | NEW — Create + auto-link menu items |
| Assets/ProtoV2/Scripts/Audio/AudioManager.cs | +_audioData field + Awake copy block |
| Assets/ProtoV2/Scripts/Audio/PlayerAudioSource.cs | +_audioData field + Awake copy block |
| Assets/ProtoV2/Scripts/Audio/AltarAudioSource.cs | +_audioData field + Awake copy block |
| Assets/ProtoV2/Scripts/Audio/BloodEmitterAudioSource.cs | +_audioData field + Awake copy block |
| Assets/ProtoV2/Scripts/Audio/UIAudioManager.cs | +_audioData field + Awake copy block |
---
2026-03-31 — Obi Rope + Obi Softbody Jellyfish Upgrade
What Was Done
Replaced the procedural LineRenderer spring-chain tentacles with proper Obi Rope physics tentacles, and added an Obi Softbody jelly core system. Both packages (Obi Rope 7.x, Obi Softbody 7.x) were already imported by the developer.
---
Files Changed
`Assets/ProtoV2/Scripts/JellyfishVisuals.cs` — Full rewrite (tentacle system only)
Removed:
- All LineRenderer fields:
_tentacles, _tentacleMaterials, _segmentPositions, _rimOffsetsLocal
- Inspector params:
segmentCount, segmentLength, dampFactor, startWidth, endWidth, swayAmplitude, swayFrequency
- Methods:
BuildTentacles(), UpdateTentacles(), BuildTentacleMaterial()
Added:
[SerializeField] ObiSolver _solver — auto-found via FindObjectOfType if null; tentacles skipped gracefully if solver unavailable (bell squish + hover bob still work)
- Inspector params:
ropeLength=0.9, ropeVisualThickness=0.05, ropeBendCompliance=0.01, ropeParticleCount=8
- Runtime arrays:
_ropes, _blueprints, _anchorTransforms, _ropeMaterials
BuildRimOffsets() — same XZ rim circle math as before
BuildObiRopeTentacles() — creates 6 ObiRope actors at Start(), each parented under ObiSolver
UpdateAnchorTransforms() — updates anchor Transform world positions each frame (same formula as old anchor calc: world Y from VisualRoot + rim XZ from root direction)
CreateRopeMaterial() — URP/Unlit transparent material, team-colored
Unchanged: UpdateHoverBob(), UpdateBellSquish(), ApplyEvolutionTint(), SetTentacleColor(), all evolution/color fields, OnDestroy() cleanup pattern.
Rope setup per tentacle:
1. Anchor Transform child of root GO (NOT VisualRoot — avoids inheriting bell tilt)
2. ObiRopeBlueprint: 2 control points (root at 0,0,0 → tip at 0,-length,0), tapered thickness (tip = 40% of root), root color opaque → tip alpha=0
3. ObiRope + ObiPathSmoother + ObiRopeExtrudedRenderer (DefaultRopeSection)
4. ObiParticleAttachment.Static on groups[0] → anchor transform
5. Parent under solver LAST (triggers AddToSolver)
---
`Assets/ProtoV2/Scripts/JellyfishSoftCore.cs` — New
ObiSoftbody "jelly interior" component. Add to PlayerPrefab root. Requires a pre-baked blueprint (run wizard below).
At Start(): creates a sphere-shaped ObiSoftbody GO under the solver, dynamically pin-attached to the character root via ObiParticleAttachment.Dynamic with configurable _compliance (default 0.002 — elastic spring lag). Rendered with a translucent blue sphere.
Exposes:
PulseSquish(float inwardSpeed) — applies inward radial velocity to all particles on pulse
SetColor(Color) — evolution tinting
IsReady — safe call guard
---
`Assets/ProtoV2/Scripts/Editor/JellyfishSoftCoreSetupWizard.cs` — New
Menu: Ritual & Ruin / Setup Jellyfish Soft Core
One-click editor wizard that:
1. Gets Unity's built-in sphere mesh via GameObject.CreatePrimitive(PrimitiveType.Sphere)
2. Creates ObiSoftbodyBlueprint, assigns mesh, calls GenerateImmediate()
3. Saves blueprint to Assets/ProtoV2/SoftbodyBlueprints/JellyfishCoreSoftbody.asset
4. Opens PlayerPrefab via EditPrefabContentsScope, adds JellyfishSoftCore to root, wires blueprint
---
How to Use After Compilation
Obi Rope Tentacles (automatic)
- Assign ObiSolver reference on JellyfishVisuals in PlayerPrefab Inspector, OR leave null (auto-found at runtime)
- Enter Play mode → 6 ObiRope tentacles simulate under the solver, physically trailing behind character movement
Obi Softbody Jelly Core (opt-in)
1. Wait for scripts to compile
2. Run Ritual & Ruin / Setup Jellyfish Soft Core from the Unity menu
3. Enter Play mode → translucent sphere lags behind character with elastic follow
Tuning parameters (JellyfishSoftCore Inspector):
_compliance: 0.001 (tight) → 0.01 (very sloshy)
_coreOffset: default (0, 1.5, 0) — positions core inside bell
_coreScale: default 0.35 — sphere radius
_coreColor: default translucent blue-white (0.5, 0.85, 1, 0.25)
Tuning parameters (JellyfishVisuals Inspector):
ropeLength: 0.9 — tentacle length
ropeVisualThickness: 0.05 — cross-section radius
ropeBendCompliance: 0.01 — stiffness (0=rigid, 0.1=very floppy)
ropeParticleCount: 8 — simulation resolution
---
Architecture Notes
- Rope GOs are parented under ObiSolver, NOT under the character. Anchor Transforms (children of character root) follow the character each frame; ObiParticleAttachment.Static pins rope particle 0 to each anchor.
- Anchor Transforms are children of the root (not VisualRoot) so they don't inherit pour tilt or bell squish rotation.
OnDestroy() cleans up all rope GOs, blueprint ScriptableObjects (DestroyImmediate), materials, and anchor GOs — no leaks on respawn/rematch.
- If ObiSolver not found at Start, bell squish and hover bob still work — only tentacles are skipped.
- JellyfishSoftCore is fully optional. No changes to JellyfishVisuals required for softbody to work.
---
Design Vault Reference
- Asset Catalog Tier 1: Obi Rope ✅ implemented, Obi Softbody ✅ implemented
- Jellyfish Visuals confirmed doc: "Future: replace with ObiRopes" ✅ done
---
2026-04-03 — Tentacle Revert: LineRenderer Spring-Chain
What Changed
Reverted jellyfish tentacles from ObiRope back to LineRenderer spring-chain.
Root cause for revert: ObiRope tentacles are physical actors — on every bell pulse the VisualRoot squishes, which moves the ObiParticleAttachment anchor, causing particles to bounce violently. The tentacles should trail softly, not react to the bell pulse physics.
Implementation
JellyfishVisuals.cs — full rewrite
- 6 LineRenderer tentacles, 6 segments each
- Spring chain: segment 0 snaps to anchor, segments 1+ lerp toward
(prev + Vector3.down * segLen) then distance-constrained to fixed length
- Auto-scaling: all size values multiplied by
transform.lossyScale.x at Start() so tentacles look correct at any character scale (match=0.25, lobby=0.69)
- Inspector values are scale=1 normalized
Prefab values (scale=1 normalized)
| Field | Value | World value @ scale=0.25 |
|---|---|---|
| rimRadius | 1.3 | 0.325 |
| rimY | 1.5 | 0.375 (anchor above floor) |
| segmentLength | 0.25 | 0.0625/segment, 0.375 total = reaches floor |
| startWidth | 0.6 | 0.15 |
| endWidth | 0.16 | 0.04 |
Key insight on segmentLength: Originally set to 0.6 (= 0.9 world units total at match scale) — tentacles extended 0.525 units below floor, making only ~2 segments visible. Corrected to 0.25 (= 0.375 world units total) so all 6 segments are visible above floor. At lobby scale 0.69: 0.25 × 0.69 × 6 = 1.035 world units = exactly matches anchor height. Both scenes auto-balance.
TentacleSetupWizard.cs
Added "Ritual & Ruin/Cleanup Jellyfish Tentacles (Revert to LineRenderer)" menu item that removes TentacleSolver GO and TentacleAnchor_* from VisualRoot. ObiRope setup code kept shelved for future reference.
Visual Result
Tentacles appear as a radial fringe of colored lines immediately around each jellyfish body. From the isometric top-down camera angle, downward-hanging tentacles are foreshortened — appears as a small colored cluster rather than long dangling appendages. Correctly attached to characters, correctly auto-scaled, correctly team-colored.
---
2026-04-04 — Tilt Control in Rebind UI + In-Match Menu Hints
What Changed
Three pending gaps from the 2026-03-23 manual tilt session completed:
1. Tilt (right stick) added to all rebind UIs
2. Auto-tilt toggle wiring completed (PlayerMenuController + PlayerSettingsPanel)
3. Per-player menu hint text added to in-match HUD and settings panel
---
Tilt in Rebind UIs
Tilt was fully implemented in the input system but never surfaced to players as a rebindable control.
Files changed
InputRebindMenuUI.cs
- Added
"Tilt" to rebindableActions. The UGUI lobby rebind panel now shows a Tilt row for controller players. Keyboard players see no row (Tilt has no keyboard binding — skipped automatically by GetBindingIndexForControlScheme returning -1).
PlayerSettingsPanel.cs
- Added
"Tilt" to rebindableActions. Same behavior in the in-game settings panel.
InputRebindUIController.cs (UI Toolkit lobby panel)
BindingNames / ActionMappings: added "tilt" / "Tilt" at index 6
ControllerPresetBindings: added tilt as 4th element per preset:
- L-Stick + Buttons →
rightStick
- L-Stick + Triggers →
rightStick
- R-Stick + Buttons →
leftStick (rightStick already used for Move in this preset)
- D-Pad + Buttons →
rightStick
ApplyPreset() controller branch: calls ApplyControllerMovePreset(tiltAction, presetPaths[3]) — correct because Tilt is Value/Vector2 (same structure as Move)
---
Auto-Tilt Toggle Wiring
PlayerMenuController and PlayerSettingsPanel code was complete since 2026-03-23. The missing piece was editor wiring. The existing PlayerMenuSetupWizard (Tools > Player Menu Setup) confirmed everything was already in place from a prior run:
Toggle behavior: visible only for controller players (keyboard always uses auto-tilt; hidden for them). Players access it via MenuOpen (Start/Escape) → Settings panel.
---
Per-Player Menu Hint Text
New UI prompt system so players know how to open/close the per-player settings panel during a match.
`PlayerMenuController.cs`
- Added
[SerializeField] TextMeshProUGUI menuHintText
Start(): sets initial text, subscribes to PlayerSetup.OnInputModeChanged
UpdateHintText() sets text based on current state:
| State | Controller | Keyboard |
|---|---|---|
| Menu closed | START — Settings | ESC — Settings |
| Menu open | START — Close Settings | ESC — Close Settings |
| Waiting for controller | Waiting for controller… | — |
- Fires on
OpenMenu(), CloseMenu(), and any device change event
`PlayerSettingsPanel.cs`
- Added
[SerializeField] TextMeshProUGUI instructionText
- Set in
Show() based on player's input mode:
- Controller:
START or B — Close
- Keyboard:
ESC — Close
Unity wiring — `MenuHintWiringWizard.cs` (new editor script)
Runs via Tools > Wire Menu Hint Text. One-shot, rerunnable.
- PlayerPrefab: Created
MenuHintText TMP child under BarCanvas, anchored below the health bar. Font size 3 (world-space canvas), 65% opacity white, centre-aligned. Wired to PlayerMenuController.menuHintText.
- LobbyScene / PlayerSettingsPanelCanvas/PanelRoot: Created
InstructionText TMP anchored to panel bottom, font size 13, grey. Wired to PlayerSettingsPanel.instructionText.
- LobbyScene saved.