feat(audio-kit): 终端 UI 菜单音效与暂停菜单环境音

- 新增 TerminalUIAudio 事件通知与 TerminalUIAudioBehaviour 组件
- TerminalButton/TerminalMenuItemView/SelectItem/SliderItem 接入悬停与点击音效
- AudioManager 监听 TerminalUIEvent 播放 menu_hover/menu_click
- 新增 PauseMenu 环境音状态及恢复逻辑
- 新增 MainMenuStage FMOD 参数控制
- BaseButton 调用基类 DoStateTransition 保证状态事件正确触发
- 为 TerminalChapterPanel Prefab 添加 TerminalUIAudioBehaviour
This commit is contained in:
2026-07-04 23:12:47 +08:00
parent 04536c4b81
commit e015b756f6
16 changed files with 332 additions and 8 deletions
@@ -1053,6 +1053,7 @@ GameObject:
- component: {fileID: 6205916594876141295}
- component: {fileID: 7869115368522661861}
- component: {fileID: 3469418760292733387}
- component: {fileID: 3469418760292733388}
m_Layer: 0
m_Name: < Button
m_TagString: Untagged
@@ -1167,6 +1168,18 @@ MonoBehaviour:
onStateTransition:
m_PersistentCalls:
m_Calls: []
--- !u!114 &3469418760292733388
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 3287442686753485554}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: c4e8f2a19b3d47658e0f1a2b3c4d5e6f, type: 3}
m_Name:
m_EditorClassIdentifier:
--- !u!1 &3870454585511995554
GameObject:
m_ObjectHideFlags: 0
@@ -1179,6 +1192,7 @@ GameObject:
- component: {fileID: 4356273674151839268}
- component: {fileID: 3025403663481808802}
- component: {fileID: 3189088484011772765}
- component: {fileID: 3189088484011772766}
m_Layer: 0
m_Name: '> Button'
m_TagString: Untagged
@@ -1293,6 +1307,18 @@ MonoBehaviour:
onStateTransition:
m_PersistentCalls:
m_Calls: []
--- !u!114 &3189088484011772766
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 3870454585511995554}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: c4e8f2a19b3d47658e0f1a2b3c4d5e6f, type: 3}
m_Name:
m_EditorClassIdentifier:
--- !u!1 &4545690484842323913
GameObject:
m_ObjectHideFlags: 0
@@ -146,6 +146,7 @@ GameObject:
- component: {fileID: 813550245757536296}
- component: {fileID: 2349484527078911041}
- component: {fileID: 7511681537542705576}
- component: {fileID: 7511681537542705577}
m_Layer: 5
m_Name: Right Btn
m_TagString: Untagged
@@ -219,7 +220,7 @@ MonoBehaviour:
m_GameObject: {fileID: 5803930194917653799}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: 4e29b1a8efbd4b44bb3f3716e73f07ff, type: 3}
m_Script: {fileID: 11500000, guid: f5702aa38bfd4746aecc799b153aab91, type: 3}
m_Name:
m_EditorClassIdentifier:
m_Navigation:
@@ -254,6 +255,21 @@ MonoBehaviour:
m_OnClick:
m_PersistentCalls:
m_Calls: []
onStateTransition:
m_PersistentCalls:
m_Calls: []
--- !u!114 &7511681537542705577
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 5803930194917653799}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: c4e8f2a19b3d47658e0f1a2b3c4d5e6f, type: 3}
m_Name:
m_EditorClassIdentifier:
--- !u!1 &7684576435073937010
GameObject:
m_ObjectHideFlags: 0
@@ -266,6 +282,7 @@ GameObject:
- component: {fileID: 6266102391853274815}
- component: {fileID: 2430117081551875153}
- component: {fileID: 4277087511383866260}
- component: {fileID: 4277087511383866261}
m_Layer: 5
m_Name: Left Btn
m_TagString: Untagged
@@ -339,7 +356,7 @@ MonoBehaviour:
m_GameObject: {fileID: 7684576435073937010}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: 4e29b1a8efbd4b44bb3f3716e73f07ff, type: 3}
m_Script: {fileID: 11500000, guid: f5702aa38bfd4746aecc799b153aab91, type: 3}
m_Name:
m_EditorClassIdentifier:
m_Navigation:
@@ -374,6 +391,21 @@ MonoBehaviour:
m_OnClick:
m_PersistentCalls:
m_Calls: []
onStateTransition:
m_PersistentCalls:
m_Calls: []
--- !u!114 &4277087511383866261
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 7684576435073937010}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: c4e8f2a19b3d47658e0f1a2b3c4d5e6f, type: 3}
m_Name:
m_EditorClassIdentifier:
--- !u!1 &8626785295523857742
GameObject:
m_ObjectHideFlags: 0
@@ -17,6 +17,7 @@
Bar,
Dream,
MainMenu,
Empty
Empty,
PauseMenu
}
}
@@ -1,9 +1,11 @@
using System;
using System.Collections;
using System.Collections.Generic;
using AibisDream;
using AibisDream.FixSystem;
using AibisDream.Framework;
using AibisDream.SaveSystem;
using AibisDream.UI;
using FMOD;
using FMOD.Studio;
using FMODUnity;
@@ -16,6 +18,7 @@ namespace AibisDream.Kit
public class AudioManager : Singleton<AudioManager>
{
private const string AmbStateName = "scene";
private const string MainMenuStageParamName = "mainMenuStage";
public EventReference ambEvent;
@@ -28,6 +31,10 @@ namespace AibisDream.Kit
[Tooltip("Maximum volume in dB when slider is at 1")]
[SerializeField] private float maxDecibels = 0f;
[Header("Terminal UI")]
[SerializeField] private string terminalHoverSfx = "event:/ActionFB/menu_hover";
[SerializeField] private string terminalClickSfx = "event:/ActionFB/menu_click";
#region Event实例
@@ -42,6 +49,9 @@ namespace AibisDream.Kit
private List<IUnRegister> _removeTrigger = new();
private AmbState _currentAmbState = AmbState.Main;
private AmbState? _ambStateBeforePauseMenu;
public AmbState CurrentAmbState => _currentAmbState;
#endregion
@@ -65,7 +75,16 @@ namespace AibisDream.Kit
_removeTrigger.Add(EnumEventSystem.Global.Register<EventEnum, FixSceneMode>(EventEnum.FixSceneModeChanged, OnFixSceneModeChange));
// 主菜单 / 退出局内
_removeTrigger.Add(EnumEventSystem.Global.Register(GameLoopEnum.AppStart, OnAppStart));
_removeTrigger.Add(EnumEventSystem.Global.Register(GameLoopEnum.GameStart, OnGameStart));
_removeTrigger.Add(EnumEventSystem.Global.Register(GameLoopEnum.GameQuit, OnGameQuit));
_removeTrigger.Add(EnumEventSystem.Global.Register<TerminalUIEvent, TerminalMainMenuPhase>(
TerminalUIEvent.MainMenuPhase, OnMainMenuPhase));
_removeTrigger.Add(EnumEventSystem.Global.Register(TerminalUIEvent.PauseMenuAmbEnter, OnPauseMenuAmbEnter));
_removeTrigger.Add(EnumEventSystem.Global.Register<TerminalUIEvent, bool>(
TerminalUIEvent.PauseMenuAmbExit, OnPauseMenuAmbExit));
SingleCastEventSystem.Global.Register<TerminalUIAudioEnum>(TerminalUIAudioEnum.Hover, OnTerminalUIHover);
SingleCastEventSystem.Global.Register<TerminalUIAudioEnum>(TerminalUIAudioEnum.Click, OnTerminalUIClick);
}
public override void OnSingletonDestroy()
@@ -80,13 +99,30 @@ namespace AibisDream.Kit
trigger.UnRegister();
}
_removeTrigger.Clear();
SingleCastEventSystem.Global.Unregister(TerminalUIAudioEnum.Hover);
SingleCastEventSystem.Global.Unregister(TerminalUIAudioEnum.Click);
}
private void OnTerminalUIHover()
{
PlaySfx(terminalHoverSfx);
}
private void OnTerminalUIClick()
{
PlaySfx(terminalClickSfx);
}
public AudioSnapshotDto CaptureSnapshot()
{
var ambState = _currentAmbState == AmbState.PauseMenu && _ambStateBeforePauseMenu.HasValue
? _ambStateBeforePauseMenu.Value
: _currentAmbState;
var dto = new AudioSnapshotDto
{
ambState = _currentAmbState.ToString()
ambState = ambState.ToString()
};
foreach (var pair in _musicDict)
@@ -181,16 +217,57 @@ namespace AibisDream.Kit
RuntimeManager.StudioSystem.setParameterByName(AmbStateName, (float)state);
}
private void OnPauseMenuAmbEnter()
{
if (_currentAmbState == AmbState.PauseMenu)
{
return;
}
_ambStateBeforePauseMenu = _currentAmbState;
ChangeAmb(AmbState.PauseMenu);
}
private void OnPauseMenuAmbExit(bool restorePrevious)
{
if (!_ambStateBeforePauseMenu.HasValue)
{
return;
}
if (restorePrevious)
{
ChangeAmb(_ambStateBeforePauseMenu.Value);
}
_ambStateBeforePauseMenu = null;
}
public void SetGlobalParam(string paramName, float value)
{
RuntimeManager.StudioSystem.setParameterByName(paramName, value);
}
public void SetMainMenuStage(int stage)
{
SetGlobalParam(MainMenuStageParamName, stage);
}
private void OnAppStart()
{
ChangeAmb(menuAmbState);
}
private void OnMainMenuPhase(TerminalMainMenuPhase phase)
{
SetMainMenuStage((int)phase);
}
private void OnGameStart()
{
SetMainMenuStage(3);
}
private void OnSceneLoad(string loadingScene)
{
// 场景加载时切换AMB
+13
View File
@@ -85,4 +85,17 @@ namespace AibisDream
TextSpeed,
LocaleChanged
}
public enum TerminalUIAudioEnum
{
Hover,
Click,
}
public enum TerminalUIEvent
{
MainMenuPhase,
PauseMenuAmbEnter,
PauseMenuAmbExit,
}
}
+2 -1
View File
@@ -14,7 +14,8 @@ namespace AibisDream
{
if (!gameObject.activeInHierarchy)
return;
base.DoStateTransition(state, instant);
onStateTransition?.Invoke(TransStateToType(state), instant);
}
+10
View File
@@ -1,4 +1,5 @@
using System;
using AibisDream.UI;
using AibisDream.Utility;
using TMPro;
using UnityEngine;
@@ -37,6 +38,7 @@ namespace AibisDream
private int _curIdx;
private Action<string, string> _onValueChanged;
private bool _isListening;
private bool _isHovered;
private ColorBlock _darkRowButtonColors;
private void Awake()
@@ -58,11 +60,18 @@ namespace AibisDream
public void OnPointerEnter(PointerEventData eventData)
{
if (!_isHovered)
{
_isHovered = true;
TerminalUIAudio.NotifyHover();
}
SetHovered(true);
}
public void OnPointerExit(PointerEventData eventData)
{
_isHovered = false;
SetHovered(false);
}
@@ -167,6 +176,7 @@ namespace AibisDream
_curIdx = CommonUtil.LoopStep(_curIdx, 0, _options.Length - 1, offset);
RefreshValue();
_onValueChanged?.Invoke(_propName, _options[_curIdx].prop);
TerminalUIAudio.NotifyClick();
}
private void RefreshValue()
+9
View File
@@ -1,5 +1,6 @@
using System;
using System.Globalization;
using AibisDream.UI;
using AibisDream.Utility;
using TMPro;
using UnityEngine;
@@ -24,6 +25,7 @@ namespace AibisDream
private string _propName;
private Action<string, string> _onValueChanged;
private bool _isListening;
private bool _isHovered;
private void Awake()
{
@@ -38,11 +40,18 @@ namespace AibisDream
public void OnPointerEnter(PointerEventData eventData)
{
if (!_isHovered)
{
_isHovered = true;
TerminalUIAudio.NotifyHover();
}
SetHovered(true);
}
public void OnPointerExit(PointerEventData eventData)
{
_isHovered = false;
SetHovered(false);
}
@@ -129,17 +129,22 @@ namespace AibisDream.UI
}
public void Close()
{
Close(restoreAmb: true);
}
public void Close(bool restoreAmb)
{
HideAllPages();
_backStack.Clear();
_currentPage = InGameTerminalPage.None;
ContinueIfPaused();
ContinueIfPaused(restoreAmb);
gameObject.SetActive(false);
}
public void CloseToMainMenu()
{
Close();
Close(restoreAmb: false);
GameManager.Instance.QuitGame();
}
@@ -166,10 +171,11 @@ namespace AibisDream.UI
}
GameManager.Instance.PauseGame();
TerminalUIAudio.NotifyPauseMenuAmbEnter();
_pausedByPanel = true;
}
private void ContinueIfPaused()
private void ContinueIfPaused(bool restoreAmb = true)
{
if (!_pausedByPanel)
{
@@ -177,6 +183,7 @@ namespace AibisDream.UI
}
GameManager.Instance.ContinueGame();
TerminalUIAudio.NotifyPauseMenuAmbExit(restoreAmb);
_pausedByPanel = false;
}
@@ -36,6 +36,8 @@ namespace AibisDream.UI
public BaseButton Button => button != null ? button : (button = GetComponent<BaseButton>());
private SelectionType _prevSelectionState = SelectionType.Normal;
private void Awake()
{
button = GetComponent<BaseButton>();
@@ -50,6 +52,7 @@ namespace AibisDream.UI
}
button.onStateTransition.AddListener(OnStateTransition);
button.onClick.AddListener(OnClick);
if (!string.IsNullOrEmpty(defaultLabel))
{
@@ -70,6 +73,7 @@ namespace AibisDream.UI
if (button != null)
{
button.onStateTransition.RemoveListener(OnStateTransition);
button.onClick.RemoveListener(OnClick);
}
}
@@ -96,6 +100,15 @@ namespace AibisDream.UI
private void OnStateTransition(SelectionType state, bool instant)
{
if (button.interactable
&& state == SelectionType.Highlighted
&& _prevSelectionState == SelectionType.Normal)
{
TerminalUIAudio.NotifyHover();
}
_prevSelectionState = state;
switch (state)
{
case SelectionType.Disabled:
@@ -136,6 +149,14 @@ namespace AibisDream.UI
}
}
private void OnClick()
{
if (button != null && button.interactable)
{
TerminalUIAudio.NotifyClick();
}
}
private static bool IsLocalizationKey(string value)
{
return !string.IsNullOrEmpty(value) && value.Contains("_", StringComparison.Ordinal);
@@ -26,6 +26,7 @@ namespace AibisDream.UI
private CanvasGroup _canvasGroup;
private bool _isDanger;
private bool _isSelected;
private SelectionType _prevSelectionState = SelectionType.Normal;
public event Action<TerminalMenuItemView> Clicked;
public event Action<TerminalMenuItemView> Hovered;
@@ -122,11 +123,25 @@ namespace AibisDream.UI
private void HandleClick()
{
if (_button != null && _button.interactable)
{
TerminalUIAudio.NotifyClick();
}
Clicked?.Invoke(this);
}
private void OnButtonStateTransition(SelectionType state, bool instant)
{
if (_button.interactable
&& state == SelectionType.Highlighted
&& _prevSelectionState == SelectionType.Normal)
{
TerminalUIAudio.NotifyHover();
}
_prevSelectionState = state;
if (state is SelectionType.Highlighted or SelectionType.Selected or SelectionType.Pressed)
{
Hovered?.Invoke(this);
@@ -71,6 +71,7 @@ namespace AibisDream.UI
{
if (_introCompletedThisSession)
{
TerminalUIAudio.NotifyMainMenuPhase(TerminalMainMenuPhase.Interactive);
Open(TerminalPage.Start);
return;
}
@@ -79,6 +80,7 @@ namespace AibisDream.UI
{
Debug.LogError("[TerminalPanel] introDirector is not assigned. Opening Start directly.");
_introCompletedThisSession = true;
TerminalUIAudio.NotifyMainMenuPhase(TerminalMainMenuPhase.Interactive);
Open(TerminalPage.Start);
return;
}
@@ -103,6 +105,7 @@ namespace AibisDream.UI
return;
}
TerminalUIAudio.NotifyMainMenuPhase(TerminalMainMenuPhase.Interactive);
StartCoroutine(PlayIntroAndRevealStart());
}
@@ -124,6 +127,7 @@ namespace AibisDream.UI
HideAllPages();
_bootPhase = TerminalBootPhase.PressAnyKey;
TerminalUIAudio.NotifyMainMenuPhase(TerminalMainMenuPhase.Boot);
}
private IEnumerator PlayIntroAndRevealStart()
@@ -0,0 +1,29 @@
using AibisDream;
using AibisDream.Framework;
namespace AibisDream.UI
{
public enum TerminalMainMenuPhase
{
Boot = 1,
Interactive = 2,
}
public static class TerminalUIAudio
{
public static void NotifyHover() =>
SingleCastEventSystem.Global.Trigger(TerminalUIAudioEnum.Hover);
public static void NotifyClick() =>
SingleCastEventSystem.Global.Trigger(TerminalUIAudioEnum.Click);
public static void NotifyMainMenuPhase(TerminalMainMenuPhase phase) =>
EnumEventSystem.Global.Send(TerminalUIEvent.MainMenuPhase, phase);
public static void NotifyPauseMenuAmbEnter() =>
EnumEventSystem.Global.Send(TerminalUIEvent.PauseMenuAmbEnter);
public static void NotifyPauseMenuAmbExit(bool restorePrevious) =>
EnumEventSystem.Global.Send(TerminalUIEvent.PauseMenuAmbExit, restorePrevious);
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 55aaeeece31e179488471980b4f8cc2f
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,57 @@
using AibisDream;
using UnityEngine;
namespace AibisDream.UI
{
[RequireComponent(typeof(BaseButton))]
[DisallowMultipleComponent]
public class TerminalUIAudioBehaviour : MonoBehaviour
{
private BaseButton _button;
private SelectionType _prevState = SelectionType.Normal;
private void Awake()
{
if (TryGetComponent<TerminalButton>(out _))
{
enabled = false;
return;
}
_button = GetComponent<BaseButton>();
_button.onStateTransition.AddListener(OnStateTransition);
_button.onClick.AddListener(OnClick);
}
private void OnDestroy()
{
if (_button == null)
{
return;
}
_button.onStateTransition.RemoveListener(OnStateTransition);
_button.onClick.RemoveListener(OnClick);
}
private void OnStateTransition(SelectionType state, bool instant)
{
if (_button.interactable
&& state == SelectionType.Highlighted
&& _prevState == SelectionType.Normal)
{
TerminalUIAudio.NotifyHover();
}
_prevState = state;
}
private void OnClick()
{
if (_button != null && _button.interactable)
{
TerminalUIAudio.NotifyClick();
}
}
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: c4e8f2a19b3d47658e0f1a2b3c4d5e6f
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant: