Merge pull request 'fix-bug/character-sound' (#344) from fix-bug/character-sound into develop
Reviewed-on: https://git.pthub.vn/Unity/perfect-world-unity/pulls/344
This commit is contained in:
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:b013fb5e1a03c91240682019bd88c08a2e17ad520bb308d2ecec16cc03643250
|
||||
size 302199
|
||||
oid sha256:5cfdae6351b6fac92e853e5e66c95e40da85a9a9b8b45cfdd55f16f0c4e48992
|
||||
size 303064
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:905756eec7a4d6595fe0f1902981c92a6c6c517aceb389eaa279759cefa07b76
|
||||
size 112496
|
||||
oid sha256:7854f585bbc6d2c574b614dbb36ba2600cc3d450ad835fed441f60cfe3363642
|
||||
size 112932
|
||||
|
||||
@@ -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,214 @@
|
||||
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
|
||||
|
||||
/// <summary>
|
||||
/// 2D looping AudioSource used exclusively for host-player movement sounds
|
||||
/// (footsteps, swim, air). Assign in the Inspector on the SFXManager GameObject.
|
||||
/// C++ equivalent: CECHostPlayer::m_pCurMoveSnd ownership moved here.
|
||||
/// </summary>
|
||||
[SerializeField] private AudioSource _moveSoundSource;
|
||||
|
||||
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 without file extension.
|
||||
/// e.g. "SFX\Character\FootStep\LandWalkMaleA.wav"
|
||||
/// → "sfx/character/footstep/landwalkmalea"
|
||||
/// </summary>
|
||||
private static string NormalizePath(string raw)
|
||||
{
|
||||
var path = raw.Replace('\\', '/').ToLowerInvariant();
|
||||
var dotIndex = path.LastIndexOf('.');
|
||||
if (dotIndex > 0)
|
||||
path = path.Substring(0, dotIndex);
|
||||
if (path.StartsWith("sfx/"))
|
||||
path = path.Substring(4);
|
||||
return path;
|
||||
}
|
||||
|
||||
// ────────────────────────────────────────────────────────────────────
|
||||
// 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);
|
||||
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);
|
||||
if (clip != null)
|
||||
source.PlayOneShot(clip);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Plays a looping movement sound by numeric ID on the manager-owned
|
||||
/// <see cref="_moveSoundSource"/>, stopping the current clip when the sound changes.
|
||||
/// Pass id = 0 to stop without starting a new clip.
|
||||
/// C++ equivalent: CECHostPlayer::PlayMoveSound (looping variant)
|
||||
/// </summary>
|
||||
public async UniTaskVoid PlayMoveSoundAsync(int id)
|
||||
{
|
||||
BMLogger.LogError($"HoangDev PlayMoveSoundAsync: id={id}, source={_moveSoundSource}");
|
||||
if (_moveSoundSource == null) return;
|
||||
|
||||
_moveSoundSource.Stop();
|
||||
|
||||
if (id <= 0) return;
|
||||
|
||||
var path = GetSFXPath(id);
|
||||
BMLogger.LogError($"HoangDev PlayMoveSoundAsync: resolved path='{path}'");
|
||||
if (string.IsNullOrEmpty(path)) return;
|
||||
|
||||
if (AddressableManager.Instance.TryGetCachedAudioClip(path, out var clip) && clip != null)
|
||||
{
|
||||
BMLogger.LogError($"HoangDev PlayMoveSoundAsync: found cached clip='{clip.name}'");
|
||||
_moveSoundSource.clip = clip;
|
||||
_moveSoundSource.loop = true;
|
||||
_moveSoundSource.Play();
|
||||
return;
|
||||
}
|
||||
|
||||
await AddressableManager.Instance.WaitUntilInitializedAsync();
|
||||
var loaded = await AddressableManager.Instance.LoadAudioClipAsync(path);
|
||||
if (loaded != null)
|
||||
{
|
||||
BMLogger.LogError($"HoangDev PlayMoveSoundAsync: loaded clip='{loaded.name}'");
|
||||
_moveSoundSource.clip = loaded;
|
||||
_moveSoundSource.loop = true;
|
||||
_moveSoundSource.Play();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 4cfa292fff0815d40b82f32256b3f2cc
|
||||
@@ -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"
|
||||
@@ -0,0 +1,7 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 3a171ad8b44ac09439bd7d2914dc7653
|
||||
TextScriptImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -173,6 +173,10 @@ namespace BrewMonster
|
||||
private BaseVfxObject m_pSelectedGFX;
|
||||
private BaseVfxObject m_pHoverGFX;
|
||||
|
||||
// ── Movement sound (C++ equivalent: m_pCurMoveSnd) ──────────────────
|
||||
private int _curMoveSndId = -1; // currently playing move-sound ID (-1 = none)
|
||||
private int _moveSndUpdateCounter; // 3-tick throttle counter
|
||||
|
||||
// Cursor estimation optimization
|
||||
private Vector2 m_lastMousePosition;
|
||||
private float m_cursorUpdateTimer;
|
||||
@@ -419,6 +423,9 @@ namespace BrewMonster
|
||||
// Update GFXs
|
||||
UpdateGFXs(Time.deltaTime);
|
||||
|
||||
// Update movement sound
|
||||
UpdateMoveSound();
|
||||
|
||||
//m_dwMoveRelDir = 0;
|
||||
m_fVertSpeed = 0.0f;
|
||||
// Auto team / Automatic party grouping
|
||||
@@ -428,52 +435,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);
|
||||
@@ -4179,7 +4140,94 @@ namespace BrewMonster
|
||||
//
|
||||
// CECPlayer::Release();
|
||||
}
|
||||
public int GetCurServiceNPC() { return m_idSevNPC; }
|
||||
public int GetCurServiceNPC() { return m_idSevNPC; }
|
||||
|
||||
// ────────────────────────────────────────────────────────────────────
|
||||
// Move sound — C++ equivalent: CECHostPlayer::UpdateMoveSound / PlayMoveSound
|
||||
// ────────────────────────────────────────────────────────────────────
|
||||
|
||||
/// <summary>
|
||||
/// Selects and plays the correct looping movement SFX based on environment,
|
||||
/// shape, and movement state. Throttled to run once every 3 frames, matching
|
||||
/// the C++ dwUpdateCnt % 3 guard.
|
||||
/// </summary>
|
||||
private void UpdateMoveSound()
|
||||
{
|
||||
_moveSndUpdateCounter++;
|
||||
if (_moveSndUpdateCounter % 3 != 0) return;
|
||||
|
||||
if (m_pWorkMan == null) return;
|
||||
|
||||
int newId;
|
||||
|
||||
if (m_pWorkMan.IsMoving())
|
||||
{
|
||||
if (m_iMoveEnv == (int)MoveEnvironment.MOVEENV_GROUND)
|
||||
{
|
||||
if (IsJumping())
|
||||
{
|
||||
newId = 162;
|
||||
}
|
||||
else if (!m_GndInfo.bOnGround)
|
||||
{
|
||||
newId = 163;
|
||||
}
|
||||
else
|
||||
{
|
||||
int iIndex = m_iProfession * GENDER.NUM_GENDER + m_iGender;
|
||||
int iWalkRunOffset = m_bWalkRun ? 1 : 0;
|
||||
|
||||
if (IsShapeChanged() && (GetShapeID() == (int)ModelResourceType.RES_MOD_ORC_FOX
|
||||
|| GetShapeID() == (int)ModelResourceType.RES_MOD_ORC_FOX2))
|
||||
newId = 170 + iWalkRunOffset;
|
||||
else if (IsShapeChanged() && GetShapeID() == (int)ModelResourceType.RES_MOD_ORC_TIGER)
|
||||
newId = 172 + iWalkRunOffset;
|
||||
else if (m_RidingPet.id != 0)
|
||||
newId = 200 + GetRidingPetSndType(m_RidingPet.id) * 2 + iWalkRunOffset;
|
||||
else
|
||||
newId = (m_bWalkRun ? 130 : 100) + iIndex;
|
||||
}
|
||||
}
|
||||
else if (m_iMoveEnv == (int)MoveEnvironment.MOVEENV_WATER)
|
||||
{
|
||||
float eyeY = GetPosVector3().y + 1.7f; // approximate eye height
|
||||
newId = eyeY > m_GndInfo.fWaterHei ? 160 : 161;
|
||||
}
|
||||
else // MOVEENV_AIR
|
||||
{
|
||||
newId = 164;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
newId = 164;
|
||||
}
|
||||
|
||||
PlayMoveSound(newId);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Switches the looping move sound to <paramref name="id"/>, no-op if already playing.
|
||||
/// C++ equivalent: CECHostPlayer::PlayMoveSound
|
||||
/// </summary>
|
||||
private void PlayMoveSound(int id)
|
||||
{
|
||||
BMLogger.LogError($"HoangDev PlayMoveSound called with id: {id}"); // Debug log to trace sound ID changes
|
||||
if (id == _curMoveSndId) return;
|
||||
_curMoveSndId = id;
|
||||
SFXManager.Instance?.PlayMoveSoundAsync(id).Forget();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns the pet_snd_type for a riding pet, used to select the mount SFX series
|
||||
/// (200 + type*2 + walkRunOffset).
|
||||
/// TODO: look up PET_ESSENCE.pet_snd_type via ElementDataManager when available.
|
||||
/// C++ equivalent: pData->pet_snd_type from PET_ESSENCE
|
||||
/// </summary>
|
||||
private static int GetRidingPetSndType(int petId)
|
||||
{
|
||||
return 0; // default type 0 = horse (walk 200, run 201)
|
||||
}
|
||||
}
|
||||
public sealed class CECHPTraceSpellMatcher : CECHPWorkMatcher
|
||||
{
|
||||
|
||||
+10
-1066
File diff suppressed because one or more lines are too long
@@ -0,0 +1,327 @@
|
||||
# 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
|
||||
↓ strip extension
|
||||
sfx/character/footstep/landwalkmalea
|
||||
↓ strip "sfx/" prefix
|
||||
character/footstep/landwalkmalea
|
||||
```
|
||||
|
||||
> **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