load sound into map collection
This commit is contained in:
@@ -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.
|
||||
Reference in New Issue
Block a user