load sound into map collection

This commit is contained in:
VDH
2026-04-14 17:50:19 +07:00
parent 553d191788
commit a56554c19b
9 changed files with 601 additions and 1145 deletions
@@ -2,6 +2,7 @@ using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Cysharp.Threading.Tasks;
using UnityEngine;
using UnityEngine.AddressableAssets;
using UnityEngine.AddressableAssets.ResourceLocators;
@@ -12,6 +13,7 @@ namespace BrewMonster.Scripts
public class AddressableManager : MonoSingleton<AddressableManager>
{
private bool _isInitialized = false;
private UniTaskCompletionSource _initializationTcs;
private Dictionary<string, AsyncOperationHandle<GameObject>> _loadedPrefabAssets = new();
private Dictionary<string, AsyncOperationHandle<TextAsset>> _loadedTextAssets = new();
@@ -31,10 +33,21 @@ namespace BrewMonster.Scripts
public bool IsInitialized() => _isInitialized;
/// <summary>
/// Returns immediately if already initialized; otherwise waits for the
/// Addressables initialization callback without per-frame polling.
/// </summary>
public UniTask WaitUntilInitializedAsync()
{
if (_isInitialized) return UniTask.CompletedTask;
return _initializationTcs.Task;
}
protected override void Initialize()
{
base.Initialize();
_isInitialized = false;
_initializationTcs = new UniTaskCompletionSource();
Addressables.InitializeAsync().Completed += OnInitializeComplete;
}
@@ -68,6 +81,7 @@ namespace BrewMonster.Scripts
if (handle.Status == AsyncOperationStatus.Succeeded)
{
_isInitialized = true;
_initializationTcs.TrySetResult();
BMLogger.Log($"AddressableManager: Initialized");
}
else
@@ -26,37 +26,6 @@ namespace BrewMonster
m_pNPCMan = EC_ManMessageMono.Instance?.CECNPCMan;
}
private const float SkillSfxVolume = 1f;
private void PlaySkillSfxLazy(string address, Vector3 worldPos)
{
if (string.IsNullOrEmpty(address)) return;
var mgr = AddressableManager.Instance;
if (mgr == null) return;
if (mgr.TryGetCachedAudioClip(address, out var clip) && clip != null)
{
AudioSource.PlayClipAtPoint(clip, worldPos, SkillSfxVolume);
return;
}
PlaySkillSfxLazyAsync(address, worldPos).Forget();
}
private async UniTaskVoid PlaySkillSfxLazyAsync(string address, Vector3 worldPos)
{
var mgr = AddressableManager.Instance;
if (mgr == null) return;
await UniTask.WaitUntil(() => mgr.IsInitialized());
var loaded = await mgr.LoadAudioClipAsync(address);
if (loaded != null)
{
// use worldPos for 3D spatial sound, or Vector3.zero for non-spatial sound
AudioSource.PlayClipAtPoint(loaded, Vector3.zero, SkillSfxVolume);
//AudioSource.PlayClipAtPoint(loaded, worldPos, SkillSfxVolume);
}
}
/// <summary>
/// Get the skill GFX composer
/// 获取技能特效组合器
@@ -384,7 +353,7 @@ namespace BrewMonster
m_flyGfxInstance = GameObject.Instantiate(prefab, pos, prefab.transform.rotation);
PlaySkillSfxLazy(m_pComposer.GetFlySfxPath(), pos);
SFXManager.Instance?.PlaySkillSfxAtPointAsync(m_pComposer.GetFlySfxPath(), pos).Forget();
// If m_bTraceTarget is true, add to tracking list when spawned
// 如果m_bTraceTarget为true,在生成时添加到跟踪列表
@@ -499,7 +468,7 @@ namespace BrewMonster
if (string.IsNullOrEmpty(hitSfxPath))
hitSfxPath = m_pComposer.GetHitSfxPath();
}
PlaySkillSfxLazy(hitSfxPath, vTarget);
SFXManager.Instance?.PlaySkillSfxAtPointAsync(hitSfxPath, vTarget).Forget();
// If m_bTraceTarget is true, add to tracking list (don't auto-destroy)
// 如果m_bTraceTarget为true,添加到跟踪列表(不自动销毁)
@@ -0,0 +1,164 @@
using System.Collections.Generic;
using Cysharp.Threading.Tasks;
using UnityEngine;
using BrewMonster.Scripts;
namespace BrewMonster.Scripts
{
/// <summary>
/// Translates numeric SFX IDs (from the original C++ sound.txt table) into
/// Addressable audio paths, then loads and plays them via AddressableManager.
///
/// C++ equivalent: CECGameRun::m_SoundStringTable + GetOrLoadSoundFromTable()
/// </summary>
public class SFXManager : MonoSingleton<SFXManager>
{
private readonly Dictionary<int, string> _soundTable = new();
private const string SoundTableResourcePath = "sound"; // Assets/Resources/sound.txt
protected override void Initialize()
{
base.Initialize();
LoadSoundTable();
}
// ────────────────────────────────────────────────────────────────────
// Startup: parse sound.txt
// ────────────────────────────────────────────────────────────────────
private void LoadSoundTable()
{
var textAsset = Resources.Load<TextAsset>(SoundTableResourcePath);
if (textAsset == null)
{
BMLogger.LogError("SFXManager: sound.txt not found in Resources/");
return;
}
var lines = textAsset.text.Split('\n');
foreach (var line in lines)
{
var trimmed = line.Trim();
if (string.IsNullOrEmpty(trimmed)) continue;
var tabIndex = trimmed.IndexOf('\t');
if (tabIndex < 0) continue;
var idStr = trimmed.Substring(0, tabIndex).Trim();
var rawPath = trimmed.Substring(tabIndex + 1).Trim().Trim('"');
if (!int.TryParse(idStr, out int id)) continue;
if (string.IsNullOrEmpty(rawPath)) continue; // empty entry — intentional silence
_soundTable[id] = NormalizePath(rawPath);
}
BMLogger.Log($"SFXManager: Loaded {_soundTable.Count} SFX entries from sound.txt");
}
/// <summary>
/// Convert Windows backslash path from sound.txt into a forward-slash
/// lowercase Addressables address.
/// e.g. "SFX\Character\FootStep\LandWalkMaleA.wav"
/// → "sfx/character/footstep/landwalkmalea.wav"
///
/// Adjust this method if your Addressables catalog uses a different format.
/// </summary>
private static string NormalizePath(string raw)
{
return raw.Replace('\\', '/').ToLowerInvariant();
}
// ────────────────────────────────────────────────────────────────────
// Public API
// ────────────────────────────────────────────────────────────────────
private const float SkillSfxVolume = 1f;
/// <summary>
/// Play a skill SFX by Addressable address at a world position.
/// Uses the cache fast-path when the clip is already loaded; otherwise waits
/// for AddressableManager initialization (no per-frame lambda polling) before
/// loading and playing the clip.
/// </summary>
public async UniTaskVoid PlaySkillSfxAtPointAsync(string address, Vector3 worldPos)
{
if (string.IsNullOrEmpty(address)) return;
var mgr = AddressableManager.Instance;
if (mgr == null) return;
if (mgr.TryGetCachedAudioClip(address, out var clip) && clip != null)
{
AudioSource.PlayClipAtPoint(clip, worldPos, SkillSfxVolume);
return;
}
await mgr.WaitUntilInitializedAsync();
var loaded = await mgr.LoadAudioClipAsync(address);
if (loaded != null)
AudioSource.PlayClipAtPoint(loaded, Vector3.zero, SkillSfxVolume);
}
/// <summary>
/// Resolve a numeric SFX ID to its normalized Addressable path.
/// Returns null if the ID is not in the table or maps to an empty entry.
/// </summary>
public string GetSFXPath(int id)
{
return _soundTable.TryGetValue(id, out var path) ? path : null;
}
/// <summary>
/// Load (if not already cached) and play a sound by its numeric ID.
/// Maps directly to CECHostPlayer::PlayMoveSound / SFX_INFO::Start logic.
///
/// <paramref name="source"/> controls spatialization — pass a 2D source for
/// movement sounds or a 3D world-space source for positional SFX.
///
/// No-op if the ID is unmapped, maps to an empty entry, or the clip fails
/// to load.
/// </summary>
public async UniTaskVoid PlaySFXByIdAsync(int id, AudioSource source)
{
if (source == null) return;
var path = GetSFXPath(id);
if (string.IsNullOrEmpty(path)) return;
// Fast path: clip already in Addressables cache
if (AddressableManager.Instance.TryGetCachedAudioClip(path, out var cached))
{
source.PlayOneShot(cached);
return;
}
// Slow path: load from Addressables bundle
var clip = await AddressableManager.Instance.LoadAudioClipAsync(path).ToUniTask();
if (clip != null)
source.PlayOneShot(clip);
}
/// <summary>
/// Awaitable variant — use when you need to know when playback has been
/// dispatched (e.g. chaining movement sound updates).
/// </summary>
public async UniTask PlaySFXByIdAsyncAwaitable(int id, AudioSource source)
{
if (source == null) return;
var path = GetSFXPath(id);
if (string.IsNullOrEmpty(path)) return;
if (AddressableManager.Instance.TryGetCachedAudioClip(path, out var cached))
{
source.PlayOneShot(cached);
return;
}
var clip = await AddressableManager.Instance.LoadAudioClipAsync(path).ToUniTask();
if (clip != null)
source.PlayOneShot(clip);
}
}
}
@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 4cfa292fff0815d40b82f32256b3f2cc
+79
View File
@@ -0,0 +1,79 @@
100 "SFX\Character\FootStep\LandWalkMaleA.wav"
101 "SFX\Character\FootStep\LandWalkFemaleA.wav"
102 "SFX\Character\FootStep\LandWalkMaleA.wav"
103 "SFX\Character\FootStep\LandWalkFemaleA.wav"
104 "SFX\Character\FootStep\LandWalkMaleA.wav"
105 "SFX\Character\FootStep\LandWalkFemaleA.wav"
106 ""
107 "SFX\Character\FootStep\LandWalkFemaleA.wav"
108 "SFX\Character\FootStep\LandWalkBeastA.wav"
109 ""
110 "SFX\Character\FootStep\LandWalkMaleA.wav"
111 "SFX\Character\FootStep\LandWalkFemaleA.wav"
112 "SFX\Character\FootStep\LandWalkMaleA.wav"
113 "SFX\Character\FootStep\LandWalkFemaleA.wav"
114 "SFX\Character\FootStep\LandWalkMaleA.wav"
115 "SFX\Character\FootStep\LandWalkFemaleA.wav"
116 "SFX\Character\FootStep\LandWalkMaleA.wav"
117 "SFX\Character\FootStep\LandWalkFemaleA.wav"
118 "SFX\Character\FootStep\LandWalkMaleA.wav"
119 "SFX\Character\FootStep\LandWalkFemaleA.wav"
120 "SFX\Character\FootStep\LandWalkMaleA.wav"
121 "SFX\Character\FootStep\LandWalkFemaleA.wav"
122 "SFX\Character\FootStep\LandWalkMaleA.wav"
123 "SFX\Character\FootStep\LandWalkFemaleA.wav"
130 "SFX\Character\FootStep\LandRunMaleA.wav"
131 "SFX\Character\FootStep\LandRunFemaleA.wav"
132 "SFX\Character\FootStep\LandRunMaleA.wav"
133 "SFX\Character\FootStep\LandRunFemaleA.wav"
134 "SFX\Character\FootStep\LandRunMaleA.wav"
135 "SFX\Character\FootStep\LandRunFemaleA.wav"
136 ""
137 "SFX\Character\FootStep\LandRunFemaleA.wav"
138 "SFX\Character\FootStep\LandRunBeastA.wav"
139 ""
140 "SFX\Character\FootStep\LandRunMaleA.wav"
141 "SFX\Character\FootStep\LandRunFemaleA.wav"
142 "SFX\Character\FootStep\LandRunMaleA.wav"
143 "SFX\Character\FootStep\LandRunFemaleA.wav"
144 "SFX\Character\FootStep\LandRunMaleA.wav"
145 "SFX\Character\FootStep\LandRunFemaleA.wav"
146 "SFX\Character\FootStep\LandRunMaleA.wav"
147 "SFX\Character\FootStep\LandRunFemaleA.wav"
148 "SFX\Character\FootStep\LandRunMaleA.wav"
149 "SFX\Character\FootStep\LandRunFemaleA.wav"
150 "SFX\Character\FootStep\LandRunMaleA.wav"
151 "SFX\Character\FootStep\LandRunFemaleA.wav"
152 "SFX\Character\FootStep\LandRunMaleA.wav"
153 "SFX\Character\FootStep\LandRunFemaleA.wav"
160 "SFX\Character\FootStep\Swim.wav"
161 "SFX\Character\FootStep\Float.wav"
162 ""
163 ""
164 ""
170 "SFX\Character\FootStep\LandWalkFoxA.wav"
171 "SFX\Character\FootStep\LandRunFoxA.wav"
172 "SFX\Character\FootStep\LandWalkTigerA.wav"
173 "SFX\Character\FootStep\LandRunTigerA.wav"
200 "SFX\Character\FootStep\LandWalkPetHorse.wav"
201 "SFX\Character\FootStep\LandRunPetHorse.wav"
202 "SFX\Character\FootStep\LandWalkPetBear.wav"
203 "SFX\Character\FootStep\LandRunPetBear.wav"
204 "SFX\Character\FootStep\LandWalkPetPuma.wav"
205 "SFX\Character\FootStep\LandRunPetPuma.wav"
206 "SFX\Character\FootStep\LandWalkPetDinasaur.wav"
207 "SFX\Character\FootStep\LandRunPetDinasaur.wav"
208 "SFX\Character\FootStep\LandWalkPetQilin.wav"
209 "SFX\Character\FootStep\LandRunPetQilin.wav"
210 "SFX\Character\FootStep\LandWalkPetAuto.wav"
211 "SFX\Character\FootStep\LandRunPetAuto.wav"
212 "SFX\Character\FootStep\LandRunFrog.wav"
213 "SFX\Character\FootStep\LandRunFrog.wav"
214 "SFX\Character\FootStep\LandRunPenguin.wav"
215 "SFX\Character\FootStep\LandRunPenguin.wav"
216 "SFX\Character\FootStep\LandRunRhinoceros.wav"
217 "SFX\Character\FootStep\LandRunRhinoceros.wav"
+7
View File
@@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: 3a171ad8b44ac09439bd7d2914dc7653
TextScriptImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:
-46
View File
@@ -428,52 +428,6 @@ namespace BrewMonster
// UpdatePosWing();
}
//public void HandleMovement()
//{
// // 1) Kiểm tra grounded bằng SphereCast ngắn dựa trên radius + skinWidth
// isGrounded = GroundCheck(out lastGroundHit);
// m_GndInfo.bOnGround = isGrounded;
// // 2) Input tạm thời: giữ nguyên như bạn
// //if (UnityEngine.Input.GetKeyDown(KeyCode.LeftShift)) SetStatusRun(true);
// //if (UnityEngine.Input.GetKeyUp(KeyCode.LeftShift)) SetStatusRun(false);
// //if (UnityEngine.Input.GetKeyDown(KeyCode.Space)) HandleJump();
// // 3) Trọng lực / sticky
// if (isGrounded && playerVelocity.y < 0f)
// {
// // Đè nhẹ để bám đất (tránh nhấp-nháy)
// playerVelocity.y = -2f;
// }
// else
// {
// playerVelocity.y += gravityValue * Time.deltaTime;
// }
// // 4) Chuyển động phẳng
// float x = joystick.Horizontal;
// float z = joystick.Vertical;
// Vector3 move = new Vector3(x, 0, z);
// move = Vector3.ClampMagnitude(move, 1f);
// if (move != Vector3.zero)
// {
// Vector3 finalMove = (move * playerSpeed) + (playerVelocity.y * Vector3.up);
// controller.Move(finalMove * Time.deltaTime);
// transform.forward = move;
// m_MoveCtrl.GroundMove(Time.deltaTime);
// m_MoveCtrl.SendMoveCmd(playerTransform.position, controller.velocity, (int)GPMoveMode.GP_MOVE_RUN);
// m_aabb.Center = EC_Utility.ToA3DVECTOR3(playerTransform.position) +
// new A3DVECTOR3(0.0f, m_aabb.Extents.y, 0.0f);
// m_aabb.CompleteMinsMaxs();
// m_aabbServer.Center = EC_Utility.ToA3DVECTOR3(playerTransform.position) +
// new A3DVECTOR3(0.0f, m_aabbServer.Extents.y, 0.0f);
// m_aabbServer.CompleteMinsMaxs();
// }
// else
// {
// }
//}
private void JoystickRelease(JoystickRealeaseEvent joystickRealeaseEvent)
{
// _playerStateMachine.ChangeState(_idleState);
File diff suppressed because one or more lines are too long
+323
View File
@@ -0,0 +1,323 @@
# SFX Manager — Unity C# Implementation
This document covers the Unity C# port of the Perfect World SFX ID-to-file lookup system,
implemented in [`Assets/PerfectWorld/Scripts/Sound/SFXManager.cs`](../Assets/PerfectWorld/Scripts/Sound/SFXManager.cs).
For the full C++ source analysis this is based on, see
[`perfect-world-source/docs/SFX_FLOW.md`](../../perfect-world-source/perfect-world-source/docs/SFX_FLOW.md).
---
## Table of Contents
1. [Architecture Overview](#1-architecture-overview)
2. [Data Source: sound.txt](#2-data-source-soundtxt)
3. [Startup Flow](#3-startup-flow)
4. [Playing a Sound by ID](#4-playing-a-sound-by-id)
5. [Sound ID Reference Table](#5-sound-id-reference-table)
6. [Path Normalization](#6-path-normalization)
7. [Usage Examples](#7-usage-examples)
8. [C++ → C# Mapping](#8-c--c-mapping)
9. [Extension Points](#9-extension-points)
---
## 1. Architecture Overview
```
┌──────────────────────────────────────────────────────────┐
│ CALLER CODE │
│ (movement controller, skill handler, animation event) │
│ │
│ SFXManager.Instance.PlaySFXByIdAsync(id, audioSource) │
└────────────────────────┬─────────────────────────────────┘
│ int id
┌────────────────────────▼─────────────────────────────────┐
│ SFXManager │
│ │
│ _soundTable: Dictionary<int, string> │
│ key = numeric SFX ID (from sound.txt) │
│ value = normalized Addressable path │
│ │
│ GetSFXPath(id) ──► string path │
└────────────────────────┬─────────────────────────────────┘
│ string path (Addressable address)
┌────────────────────────▼─────────────────────────────────┐
│ AddressableManager │
│ │
│ TryGetCachedAudioClip(path) ──► cache hit │
│ LoadAudioClipAsync(path) ──► cache miss → load │
└────────────────────────┬─────────────────────────────────┘
│ AudioClip
AudioSource.PlayOneShot(clip)
```
---
## 2. Data Source: sound.txt
**Location:** [`Assets/Resources/sound.txt`](../Assets/Resources/sound.txt)
Loaded at startup via `Resources.Load<TextAsset>("sound")` — synchronous, no Addressables
overhead required.
### Format
Each non-blank line is a tab-separated pair:
```
<id>\t"<Windows path>"
```
Examples:
```
100 "SFX\Character\FootStep\LandWalkMaleA.wav"
160 "SFX\Character\FootStep\Swim.wav"
162 ""
164 ""
```
- **Empty path** (`""`) means silence is intended for that ID. These entries are skipped
during parsing and `GetSFXPath` returns `null` for them.
- **Blank lines** between ID groups are ignored.
---
## 3. Startup Flow
```
SFXManager.Awake()
└─ Initialize()
└─ LoadSoundTable()
├─ Resources.Load<TextAsset>("sound")
├─ Split by newline
└─ For each line:
├─ Split on first tab → idStr, rawPath
├─ int.TryParse(idStr) → id
├─ Strip surrounding quotes from rawPath
├─ Skip if empty (intentional silence IDs)
└─ _soundTable[id] = NormalizePath(rawPath)
```
On success, `BMLogger` prints the count of loaded entries:
```
SFXManager: Loaded 52 SFX entries from sound.txt
```
**C++ equivalent:** `CECGameRun::ImportSoundStringTable("Configs\\sound.txt")`
→ stores in `m_SoundStringTable` (`map<int, AString>`).
---
## 4. Playing a Sound by ID
```mermaid
flowchart TD
A[SFXManager.Initialize] --> B["Resources.Load<TextAsset>('sound')"]
B --> C[Parse each line into Dictionary int,string]
C --> D[_soundTable ready]
E[Caller: PlaySFXById(id, source)] --> F[_soundTable lookup by id]
F -->|"not found / empty"| G[return - no sound]
F -->|found path| H[TryGetCachedAudioClip]
H -->|cache hit| I[source.PlayOneShot(clip)]
H -->|cache miss| J["AddressableManager.LoadAudioClipAsync(path)"]
J --> I
```
Call chain detail:
```
PlaySFXByIdAsync(id, source)
├─ GetSFXPath(id)
│ ├─ not found / empty ──► return (no sound)
│ └─ found path
├─ AddressableManager.TryGetCachedAudioClip(path)
│ ├─ hit ──► source.PlayOneShot(clip) [sync, no await]
│ └─ miss
│ └─ await LoadAudioClipAsync(path).ToUniTask()
│ └─ source.PlayOneShot(clip)
```
Two variants are provided:
| Method | Return | When to use |
|--------|--------|-------------|
| `PlaySFXByIdAsync(id, source)` | `UniTaskVoid` | Fire-and-forget (movement, most SFX) |
| `PlaySFXByIdAsyncAwaitable(id, source)` | `UniTask` | When you need to `await` dispatch completion |
**C++ equivalent:** `CECHostPlayer::PlayMoveSound(id)`
`CECGameRun::GetOrLoadSoundFromTable(id)``AMSoundBuffer::Play(loop)`.
---
## 5. Sound ID Reference Table
Mirrors the C++ `UpdateMoveSound` ID selection logic:
| ID | Condition | Sound |
|----|-----------|-------|
| `100 + prof*2 + gender` | Ground, walking | Walk footsteps |
| `130 + prof*2 + gender` | Ground, running | Run footsteps |
| `160` | Water, above surface | Swimming |
| `161` | Water, submerged | Floating underwater |
| `162` | Ground, jumping | Jump (empty in current data) |
| `163` | Ground, falling | Fall (empty in current data) |
| `164` | Air / not moving | Silence |
| `170` | Fox shape, walking | Fox walk |
| `171` | Fox shape, running | Fox run |
| `172` | Tiger shape, walking | Tiger walk |
| `173` | Tiger shape, running | Tiger run |
| `200 + type*2 + 0` | Riding pet, walking | Mount walk |
| `200 + type*2 + 1` | Riding pet, running | Mount run |
Pet mount type offsets (200-series):
| type | Walk ID | Run ID | Mount |
|------|---------|--------|-------|
| 0 | 200 | 201 | Horse |
| 1 | 202 | 203 | Bear |
| 2 | 204 | 205 | Puma |
| 3 | 206 | 207 | Dinosaur |
| 4 | 208 | 209 | Qilin |
| 5 | 210 | 211 | Auto |
| 6 | 212 | 213 | Frog |
| 7 | 214 | 215 | Penguin |
| 8 | 216 | 217 | Rhinoceros |
---
## 6. Path Normalization
`NormalizePath` converts the Windows-style path from `sound.txt` into a Unity
Addressables address:
```
"SFX\Character\FootStep\LandWalkMaleA.wav"
↓ strip quotes
SFX\Character\FootStep\LandWalkMaleA.wav
↓ replace \ with /
SFX/Character/FootStep/LandWalkMaleA.wav
↓ ToLowerInvariant()
sfx/character/footstep/landwalkmalea.wav
```
> **Important:** Your Addressables catalog addresses must match this format exactly.
> If your catalog registers assets with a different case, prefix, or extension, adjust
> `NormalizePath()` in `SFXManager.cs` accordingly.
---
## 7. Usage Examples
### Movement sound (2D, looping)
```csharp
// In your movement controller, called every frame (throttle as needed)
[SerializeField] private AudioSource _footstepSource; // 2D AudioSource on the player
private async void UpdateMoveSound(int profession, int gender, bool isRunning)
{
int id = isRunning
? 130 + profession * 2 + gender
: 100 + profession * 2 + gender;
SFXManager.Instance.PlaySFXByIdAsync(id, _footstepSource).Forget();
}
```
### Water / air sounds
```csharp
private void PlayEnvironmentSound(MoveEnv env, bool aboveSurface)
{
int id = env switch
{
MoveEnv.Water => aboveSurface ? 160 : 161,
MoveEnv.Air => 164, // silence
_ => 164
};
SFXManager.Instance.PlaySFXByIdAsync(id, _footstepSource).Forget();
}
```
### Mount riding
```csharp
private void PlayMountSound(int petSoundType, bool isRunning)
{
int id = 200 + petSoundType * 2 + (isRunning ? 1 : 0);
SFXManager.Instance.PlaySFXByIdAsync(id, _mountSource).Forget();
}
```
### Checking if a path exists before loading
```csharp
string path = SFXManager.Instance.GetSFXPath(id);
if (path != null)
{
// path is a valid Addressables address, proceed
}
```
---
## 8. C++ → C# Mapping
| C++ | C# |
|-----|----|
| `CECGameRun::m_SoundStringTable` | `SFXManager._soundTable` |
| `ImportSoundStringTable()` | `SFXManager.LoadSoundTable()` |
| `GetSoundStringFromTable(id)` | `SFXManager.GetSFXPath(id)` |
| `GetOrLoadSoundFromTable(id)` | `SFXManager.PlaySFXByIdAsync(id, source)` |
| `AMSoundBufferMan::Load2DSound()` | `AddressableManager.LoadAudioClipAsync()` |
| `AMSoundBuffer::Play(loop)` | `AudioSource.PlayOneShot(clip)` |
| `CECSoundCache` quota system | Not implemented — Addressables handles memory |
| `AMSoundBufferMan` LRU cache | `AddressableManager._loadedAudioAssets` dict |
---
## 9. Extension Points
### Adding 3D positional SFX (Path B equivalent)
The `PlaySFXByIdAsync` signature accepts any `AudioSource`. For 3D sounds, attach an
`AudioSource` to the world-space object (character bone, ornament, etc.) and pass it:
```csharp
// 3D source positioned on the character bone
await sfxManager.PlaySFXByIdAsyncAwaitable(sfxId, boneAudioSource);
```
### Looping movement sounds
The current implementation uses `PlayOneShot` (one-shot). For a looping movement sound
(matching `AMSoundBuffer::Play(bLoop=true)`), set `AudioSource.loop = true` and assign
`AudioSource.clip` directly instead of using `PlayOneShot`:
```csharp
var clip = await AddressableManager.Instance.LoadAudioClipAsync(path).ToUniTask();
if (clip != null)
{
source.clip = clip;
source.loop = true;
source.Play();
}
```
### Releasing audio from cache
When a scene unloads or a character is destroyed, release SFX clips via:
```csharp
AddressableManager.Instance.ReleaseAsset(sfxPath);
```
The `AddressableManager` deferred release system (`_releaseAssetTimeout`) handles the
timed cleanup automatically.