添加场景、对应场景代码
This commit is contained in:
@@ -0,0 +1,124 @@
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using UnityEngine;
|
||||
|
||||
namespace GameFramework
|
||||
{
|
||||
/// <summary>
|
||||
/// 音频片段数据,可挂到 ScriptableObject 或直接 SerializeField。
|
||||
/// </summary>
|
||||
[System.Serializable]
|
||||
public class AudioData
|
||||
{
|
||||
public AudioClip audioClip;
|
||||
[Range(0f, 1f)] public float volume = 1f;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 全局音频管理器(持久单例)。
|
||||
/// 管理 BGM(背景音乐,循环)和 SFX(音效,一次性)。
|
||||
/// 音量通过 PlayerPrefs 持久化。
|
||||
/// </summary>
|
||||
public class AudioManager : PersistentSingleton<AudioManager>
|
||||
{
|
||||
const string PREF_BGM_VOLUME = "AudioManager_BGMVolume";
|
||||
const string PREF_SFX_VOLUME = "AudioManager_SFXVolume";
|
||||
|
||||
AudioSource bgmPlayer; // 背景音乐播放器
|
||||
AudioSource sfxPlayer; // 音效播放器
|
||||
|
||||
float bgmVolume = 1f;
|
||||
float sfxVolume = 1f;
|
||||
|
||||
public float BGMVolume => bgmVolume;
|
||||
public float SFXVolume => sfxVolume;
|
||||
|
||||
protected override void Awake()
|
||||
{
|
||||
base.Awake();
|
||||
// 如果是重复实例,base.Awake 已调用 Destroy,这里直接返回
|
||||
if (Instance != this) return;
|
||||
|
||||
// 创建两个 AudioSource
|
||||
bgmPlayer = gameObject.AddComponent<AudioSource>();
|
||||
bgmPlayer.loop = true;
|
||||
bgmPlayer.playOnAwake = false;
|
||||
|
||||
sfxPlayer = gameObject.AddComponent<AudioSource>();
|
||||
sfxPlayer.loop = false;
|
||||
sfxPlayer.playOnAwake = false;
|
||||
|
||||
// 读取持久化音量
|
||||
bgmVolume = PlayerPrefs.GetFloat(PREF_BGM_VOLUME, 1f);
|
||||
sfxVolume = PlayerPrefs.GetFloat(PREF_SFX_VOLUME, 1f);
|
||||
bgmPlayer.volume = bgmVolume;
|
||||
sfxPlayer.volume = sfxVolume;
|
||||
}
|
||||
|
||||
#region BGM
|
||||
/// <summary>
|
||||
/// 播放背景音乐(循环)。
|
||||
/// </summary>
|
||||
public void PlayBGM(AudioClip clip, bool restart = false)
|
||||
{
|
||||
if (clip == null) return;
|
||||
if (bgmPlayer.isPlaying && bgmPlayer.clip == clip && !restart)
|
||||
return;
|
||||
bgmPlayer.clip = clip;
|
||||
bgmPlayer.volume = bgmVolume;
|
||||
bgmPlayer.Play();
|
||||
}
|
||||
|
||||
public void StopBGM()
|
||||
{
|
||||
if (bgmPlayer.isPlaying)
|
||||
bgmPlayer.Stop();
|
||||
}
|
||||
|
||||
public void PauseBGM() => bgmPlayer.Pause();
|
||||
public void UnpauseBGM() => bgmPlayer.UnPause();
|
||||
|
||||
public void SetBGMVolume(float volume)
|
||||
{
|
||||
bgmVolume = Mathf.Clamp01(volume);
|
||||
bgmPlayer.volume = bgmVolume;
|
||||
PlayerPrefs.SetFloat(PREF_BGM_VOLUME, bgmVolume);
|
||||
PlayerPrefs.Save();
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region SFX
|
||||
/// <summary>
|
||||
/// 播放单个音效。
|
||||
/// </summary>
|
||||
public void PlaySFX(AudioData data)
|
||||
{
|
||||
if (data == null || data.audioClip == null) return;
|
||||
// sfxPlayer.volume 已在 Awake 中设为 sfxVolume,
|
||||
// PlayOneShot 的 volumeScale 再乘以 data.volume,
|
||||
// 最终音量 = sfxVolume * data.volume
|
||||
sfxPlayer.PlayOneShot(data.audioClip, data.volume);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 从列表中随机播放一个音效。
|
||||
/// </summary>
|
||||
public void PlayRandomSFX(AudioData data, List<AudioData> variants = null)
|
||||
{
|
||||
if (variants != null && variants.Count > 0)
|
||||
{
|
||||
PlaySFX(variants[Random.Range(0, variants.Count)]);
|
||||
return;
|
||||
}
|
||||
PlaySFX(data);
|
||||
}
|
||||
|
||||
public void SetSFXVolume(float volume)
|
||||
{
|
||||
sfxVolume = Mathf.Clamp01(volume);
|
||||
PlayerPrefs.SetFloat(PREF_SFX_VOLUME, sfxVolume);
|
||||
PlayerPrefs.Save();
|
||||
}
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: D38YsCytVCoe3CxQzCPwoXB7xeRjwedpI2nk1WvamOgtg/ae5hcGCSs=
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,39 @@
|
||||
using System;
|
||||
using UnityEngine;
|
||||
|
||||
namespace GameFramework
|
||||
{
|
||||
/// <summary>
|
||||
/// 全局游戏状态管理器(持久化单例)。
|
||||
/// 维护 GameState,提供 GameOver 触发入口与 onGameOver 事件。
|
||||
/// </summary>
|
||||
public class GameManager : PersistentSingleton<GameManager>
|
||||
{
|
||||
/// <summary>游戏结束时触发(GameOverScreen 等订阅)。</summary>
|
||||
public static Action onGameOver;
|
||||
|
||||
public static GameState GameState
|
||||
{
|
||||
get => Instance.gameState;
|
||||
set => Instance.gameState = value;
|
||||
}
|
||||
|
||||
[SerializeField] private GameState gameState = GameState.Playing;
|
||||
|
||||
/// <summary>触发游戏结束:设状态 + 触发事件。玩家被敌人抓到时调用。</summary>
|
||||
public static void GameOver()
|
||||
{
|
||||
if (Instance == null) return;
|
||||
GameState = GameState.GameOver;
|
||||
onGameOver?.Invoke();
|
||||
}
|
||||
}
|
||||
|
||||
public enum GameState
|
||||
{
|
||||
Playing,
|
||||
Paused,
|
||||
GameOver,
|
||||
Scoring
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: DyhMsSP+VS+OBQvXR+1P+BSSSFCIom/YSNUnOiYi6jxFYXKFgsHuPno=
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,91 @@
|
||||
using UnityEngine;
|
||||
using UnityEngine.UI;
|
||||
|
||||
namespace GameFramework
|
||||
{
|
||||
/// <summary>
|
||||
/// 游戏结束画面。
|
||||
/// 监听 GameManager.onGameOver 事件,显示结束 Canvas 并暂停游戏。
|
||||
/// 玩家按确认键或点击按钮后跳转到排行榜场景。
|
||||
/// </summary>
|
||||
public class GameOverScreen : MonoBehaviour
|
||||
{
|
||||
[Header("Canvas")]
|
||||
[SerializeField] Canvas gameOverCanvas;
|
||||
|
||||
[Header("Button")]
|
||||
[SerializeField] Button confirmButton;
|
||||
|
||||
[Header("Audio")]
|
||||
[SerializeField] AudioData gameOverSFX;
|
||||
|
||||
[Header("Input")]
|
||||
[SerializeField] KeyCode confirmKey = KeyCode.Return;
|
||||
|
||||
void OnEnable()
|
||||
{
|
||||
GameManager.onGameOver += ShowGameOver;
|
||||
}
|
||||
|
||||
void OnDisable()
|
||||
{
|
||||
GameManager.onGameOver -= ShowGameOver;
|
||||
}
|
||||
|
||||
void Start()
|
||||
{
|
||||
// 初始隐藏
|
||||
if (gameOverCanvas != null)
|
||||
gameOverCanvas.enabled = false;
|
||||
|
||||
if (confirmButton != null)
|
||||
confirmButton.onClick.AddListener(OnConfirmClick);
|
||||
}
|
||||
|
||||
void ShowGameOver()
|
||||
{
|
||||
// 显示结束画面
|
||||
if (gameOverCanvas != null)
|
||||
gameOverCanvas.enabled = true;
|
||||
|
||||
// 暂停游戏
|
||||
if (TimeController.Instance != null)
|
||||
TimeController.Instance.Pause();
|
||||
|
||||
// 显示鼠标
|
||||
Cursor.visible = true;
|
||||
Cursor.lockState = CursorLockMode.None;
|
||||
|
||||
// 选中确认按钮
|
||||
if (confirmButton != null)
|
||||
confirmButton.Select();
|
||||
|
||||
// 播放音效
|
||||
if (AudioManager.Instance != null && gameOverSFX != null)
|
||||
AudioManager.Instance.PlaySFX(gameOverSFX);
|
||||
}
|
||||
|
||||
void Update()
|
||||
{
|
||||
// 游戏结束画面可见时,按确认键跳转
|
||||
if (gameOverCanvas != null && gameOverCanvas.enabled)
|
||||
{
|
||||
if (Input.GetKeyDown(confirmKey) || Input.GetKeyDown(KeyCode.Space))
|
||||
{
|
||||
OnConfirmClick();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void OnConfirmClick()
|
||||
{
|
||||
// 先解除暂停
|
||||
if (TimeController.Instance != null)
|
||||
TimeController.Instance.Unpause();
|
||||
|
||||
// 跳转到排行榜场景
|
||||
if (SceneLoader.Instance != null)
|
||||
SceneLoader.Instance.LoadScoringScene();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: CClOsC78Vi9/cIog8UeuUzecgGTFlu5AQ5iyvdkeMcyG1n6ns9R5ds8=
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,68 @@
|
||||
using UnityEngine;
|
||||
using UnityEngine.UI;
|
||||
|
||||
namespace GameFramework
|
||||
{
|
||||
/// <summary>
|
||||
/// 主菜单 UI 控制器。
|
||||
/// 绑定三个按钮:开始游戏(异步加载)、设置(占位面板)、退出游戏。
|
||||
/// </summary>
|
||||
public class MainMenuUIController : MonoBehaviour
|
||||
{
|
||||
[Header("Buttons")]
|
||||
[SerializeField] Button startButton;
|
||||
[SerializeField] Button settingsButton;
|
||||
[SerializeField] Button quitButton;
|
||||
|
||||
[Header("Settings Panel (placeholder)")]
|
||||
[SerializeField] GameObject settingsPanel;
|
||||
[SerializeField] Button settingsBackButton;
|
||||
|
||||
void Start()
|
||||
{
|
||||
// 绑定按钮事件
|
||||
if (startButton != null)
|
||||
startButton.onClick.AddListener(OnStartClick);
|
||||
|
||||
if (settingsButton != null)
|
||||
settingsButton.onClick.AddListener(OnSettingsClick);
|
||||
|
||||
if (quitButton != null)
|
||||
quitButton.onClick.AddListener(OnQuitClick);
|
||||
|
||||
if (settingsBackButton != null)
|
||||
settingsBackButton.onClick.AddListener(OnSettingsBackClick);
|
||||
|
||||
// 确保设置面板初始关闭
|
||||
if (settingsPanel != null)
|
||||
settingsPanel.SetActive(false);
|
||||
}
|
||||
|
||||
void OnStartClick()
|
||||
{
|
||||
if (SceneLoader.Instance != null)
|
||||
SceneLoader.Instance.LoadGameplayScene();
|
||||
}
|
||||
|
||||
void OnSettingsClick()
|
||||
{
|
||||
if (settingsPanel != null)
|
||||
settingsPanel.SetActive(true);
|
||||
}
|
||||
|
||||
void OnSettingsBackClick()
|
||||
{
|
||||
if (settingsPanel != null)
|
||||
settingsPanel.SetActive(false);
|
||||
}
|
||||
|
||||
void OnQuitClick()
|
||||
{
|
||||
#if UNITY_EDITOR
|
||||
UnityEditor.EditorApplication.isPlaying = false;
|
||||
#else
|
||||
Application.Quit();
|
||||
#endif
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: WyxNsS2pACmL6nb3C9ZLbWufBaEs5AhC21a1IcXRRWytNigGK78ei90=
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,160 @@
|
||||
using UnityEngine;
|
||||
using UnityEngine.UI;
|
||||
|
||||
namespace GameFramework
|
||||
{
|
||||
/// <summary>
|
||||
/// 暂停菜单控制器。
|
||||
/// 按 ESC 切换暂停/继续。
|
||||
/// 暂停时显示菜单 Canvas,提供继续/设置/返回主菜单三个按钮。
|
||||
/// 使用 old Input Manager(Input.GetKeyDown)。
|
||||
/// </summary>
|
||||
public class PauseMenu : MonoBehaviour
|
||||
{
|
||||
[Header("Input")]
|
||||
[SerializeField] KeyCode pauseKey = KeyCode.Escape;
|
||||
|
||||
[Header("Canvas")]
|
||||
[SerializeField] Canvas hudCanvas;
|
||||
[SerializeField] Canvas pauseMenuCanvas;
|
||||
|
||||
[Header("Buttons")]
|
||||
[SerializeField] Button resumeButton;
|
||||
[SerializeField] Button settingsButton;
|
||||
[SerializeField] Button mainMenuButton;
|
||||
|
||||
[Header("Settings Panel (placeholder)")]
|
||||
[SerializeField] GameObject settingsPanel;
|
||||
[SerializeField] Button settingsBackButton;
|
||||
|
||||
[Header("Audio")]
|
||||
[SerializeField] AudioData pauseSFX;
|
||||
[SerializeField] AudioData unpauseSFX;
|
||||
|
||||
bool isPaused = false;
|
||||
|
||||
void Start()
|
||||
{
|
||||
// 绑定按钮
|
||||
if (resumeButton != null)
|
||||
resumeButton.onClick.AddListener(OnResumeClick);
|
||||
if (settingsButton != null)
|
||||
settingsButton.onClick.AddListener(OnSettingsClick);
|
||||
if (mainMenuButton != null)
|
||||
mainMenuButton.onClick.AddListener(OnMainMenuClick);
|
||||
if (settingsBackButton != null)
|
||||
settingsBackButton.onClick.AddListener(OnSettingsBackClick);
|
||||
|
||||
// 初始状态
|
||||
if (pauseMenuCanvas != null)
|
||||
pauseMenuCanvas.enabled = false;
|
||||
if (settingsPanel != null)
|
||||
settingsPanel.SetActive(false);
|
||||
|
||||
// 确保游戏开始时是运行状态
|
||||
if (TimeController.Instance != null)
|
||||
TimeController.Instance.Unpause();
|
||||
if (GameManager.Instance != null)
|
||||
GameManager.GameState = GameState.Playing;
|
||||
}
|
||||
|
||||
void Update()
|
||||
{
|
||||
// 游戏结束时不允许暂停
|
||||
if (GameManager.Instance != null && GameManager.GameState == GameState.GameOver)
|
||||
return;
|
||||
|
||||
if (Input.GetKeyDown(pauseKey))
|
||||
{
|
||||
if (isPaused)
|
||||
Resume();
|
||||
else
|
||||
Pause();
|
||||
}
|
||||
}
|
||||
|
||||
void Pause()
|
||||
{
|
||||
isPaused = true;
|
||||
|
||||
if (TimeController.Instance != null)
|
||||
TimeController.Instance.Pause();
|
||||
|
||||
if (GameManager.Instance != null)
|
||||
GameManager.GameState = GameState.Paused;
|
||||
|
||||
if (hudCanvas != null)
|
||||
hudCanvas.enabled = false;
|
||||
if (pauseMenuCanvas != null)
|
||||
pauseMenuCanvas.enabled = true;
|
||||
|
||||
// 显示鼠标
|
||||
Cursor.visible = true;
|
||||
Cursor.lockState = CursorLockMode.None;
|
||||
|
||||
// 选中继续按钮(手柄/键盘导航用)
|
||||
if (resumeButton != null)
|
||||
resumeButton.Select();
|
||||
|
||||
if (AudioManager.Instance != null && pauseSFX != null)
|
||||
AudioManager.Instance.PlaySFX(pauseSFX);
|
||||
}
|
||||
|
||||
void Resume()
|
||||
{
|
||||
isPaused = false;
|
||||
|
||||
if (TimeController.Instance != null)
|
||||
TimeController.Instance.Unpause();
|
||||
|
||||
if (GameManager.Instance != null)
|
||||
GameManager.GameState = GameState.Playing;
|
||||
|
||||
if (hudCanvas != null)
|
||||
hudCanvas.enabled = true;
|
||||
if (pauseMenuCanvas != null)
|
||||
pauseMenuCanvas.enabled = false;
|
||||
|
||||
// 隐藏鼠标
|
||||
Cursor.visible = false;
|
||||
Cursor.lockState = CursorLockMode.Locked;
|
||||
|
||||
if (AudioManager.Instance != null && unpauseSFX != null)
|
||||
AudioManager.Instance.PlaySFX(unpauseSFX);
|
||||
}
|
||||
|
||||
// 只在 Pause 状态下才响应按钮
|
||||
// Resume 是公开方法因为按钮 onClick 需要调用
|
||||
public void OnResumeClick()
|
||||
{
|
||||
if (isPaused)
|
||||
Resume();
|
||||
}
|
||||
|
||||
void OnSettingsClick()
|
||||
{
|
||||
if (settingsPanel != null)
|
||||
settingsPanel.SetActive(true);
|
||||
}
|
||||
|
||||
void OnSettingsBackClick()
|
||||
{
|
||||
if (settingsPanel != null)
|
||||
settingsPanel.SetActive(false);
|
||||
}
|
||||
|
||||
void OnMainMenuClick()
|
||||
{
|
||||
// 先解除暂停,再加载主菜单
|
||||
isPaused = false;
|
||||
if (TimeController.Instance != null)
|
||||
TimeController.Instance.Unpause();
|
||||
|
||||
if (pauseMenuCanvas != null)
|
||||
pauseMenuCanvas.enabled = false;
|
||||
|
||||
if (SceneLoader.Instance != null)
|
||||
SceneLoader.Instance.LoadMainMenuScene();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: DHgWs3+kUy5RNMyJcnH1NVpwyVBiplJDeLTX0ne96zBO1RYpFbBNLuY=
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,27 @@
|
||||
using UnityEngine;
|
||||
|
||||
namespace GameFramework
|
||||
{
|
||||
/// <summary>
|
||||
/// 持久化单例基类。跨场景保留(DontDestroyOnLoad),重复实例自动销毁。
|
||||
/// 管理器(AudioManager / ScoreManager / GameManager / SceneLoader)都继承它。
|
||||
/// </summary>
|
||||
public class PersistentSingleton<T> : MonoBehaviour where T : Component
|
||||
{
|
||||
public static T Instance { get; private set; }
|
||||
|
||||
protected virtual void Awake()
|
||||
{
|
||||
if (Instance == null)
|
||||
{
|
||||
Instance = this as T;
|
||||
}
|
||||
else if (Instance != this)
|
||||
{
|
||||
Destroy(gameObject);
|
||||
return;
|
||||
}
|
||||
DontDestroyOnLoad(gameObject);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: Wy9L5Hj/VCki7vksVlCr2JOVegJd1LSd6eBd6rLntdvyUmypRGMclXg=
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,58 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using UnityEngine;
|
||||
|
||||
namespace GameFramework
|
||||
{
|
||||
/// <summary>
|
||||
/// 静态 JSON 存档系统。读写 Application.persistentDataPath 下的文件。
|
||||
/// 排行榜等数据通过它持久化。
|
||||
/// </summary>
|
||||
public static class SaveSystem
|
||||
{
|
||||
public static void Save(string saveFileName, object data)
|
||||
{
|
||||
var json = JsonUtility.ToJson(data);
|
||||
var path = Path.Combine(Application.persistentDataPath, saveFileName);
|
||||
|
||||
try
|
||||
{
|
||||
File.WriteAllText(path, json);
|
||||
#if UNITY_EDITOR
|
||||
Debug.Log($"[SaveSystem] 已保存到 {path}");
|
||||
#endif
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
Debug.LogError($"[SaveSystem] 保存失败 {path}\n{exception}");
|
||||
}
|
||||
}
|
||||
|
||||
public static T Load<T>(string saveFileName)
|
||||
{
|
||||
var path = Path.Combine(Application.persistentDataPath, saveFileName);
|
||||
try
|
||||
{
|
||||
var json = File.ReadAllText(path);
|
||||
return JsonUtility.FromJson<T>(json);
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
Debug.LogError($"[SaveSystem] 读取失败 {path}\n{exception}");
|
||||
return default;
|
||||
}
|
||||
}
|
||||
|
||||
public static void DeleteSaveFile(string saveFileName)
|
||||
{
|
||||
var path = Path.Combine(Application.persistentDataPath, saveFileName);
|
||||
try { File.Delete(path); }
|
||||
catch (Exception exception) { Debug.LogError($"[SaveSystem] 删除失败 {path}\n{exception}"); }
|
||||
}
|
||||
|
||||
public static bool SaveFileExists(string saveFileName)
|
||||
{
|
||||
return File.Exists(Path.Combine(Application.persistentDataPath, saveFileName));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: XnoWtHuqUihinx6tGMjdkofi7dRhZE+34X4QUIkChIFcacunTLquUfM=
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,22 @@
|
||||
using UnityEngine;
|
||||
|
||||
namespace GameFramework
|
||||
{
|
||||
/// <summary>
|
||||
/// 挂到每个场景中的一个空物件上,在 Start 时通知 AudioManager 播放该场景的 BGM。
|
||||
/// 这样每个场景可以拥有独立的背景音乐。
|
||||
/// </summary>
|
||||
public class SceneBGM : MonoBehaviour
|
||||
{
|
||||
[SerializeField] AudioClip bgmClip;
|
||||
|
||||
[SerializeField] bool restartIfSameClip = false;
|
||||
|
||||
void Start()
|
||||
{
|
||||
if (bgmClip == null) return;
|
||||
if (AudioManager.Instance != null)
|
||||
AudioManager.Instance.PlayBGM(bgmClip, restartIfSameClip);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: XnoftiOlVi4S3gMTUx4V/hoDxHkZdUwI+zo55+CZeToQ88/hCv3/9Ak=
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,76 @@
|
||||
using System.Collections;
|
||||
using UnityEngine;
|
||||
using UnityEngine.SceneManagement;
|
||||
using UnityEngine.UI;
|
||||
|
||||
namespace GameFramework
|
||||
{
|
||||
/// <summary>
|
||||
/// 场景异步加载器(持久化单例)。带黑色淡入淡出过渡。
|
||||
/// 场景名可在 Inspector 配置,默认 MainMenu / Gameplay / Scoring。
|
||||
///
|
||||
/// 搭建:把 SceneLoader 挂到一个空物体上(放第一个场景 MainMenu 里),
|
||||
/// 它会 DontDestroyOnLoad。transitionImage 是它子物体 Canvas 上的 Image(全屏黑),
|
||||
/// Canvas 设为 Screen Space - Overlay、sortingOrder 设很高(如 1000)。
|
||||
/// </summary>
|
||||
public class SceneLoader : PersistentSingleton<SceneLoader>
|
||||
{
|
||||
[Header("过渡")]
|
||||
[SerializeField] private Image transitionImage;
|
||||
[SerializeField] private float fadeTime = 1.5f;
|
||||
|
||||
[Header("场景名(按你工程里的实际名称填)")]
|
||||
[SerializeField] private string mainMenuScene = "MainMenu";
|
||||
[SerializeField] private string gameplayScene = "Gameplay";
|
||||
[SerializeField] private string scoringScene = "Scoring";
|
||||
|
||||
private Color color;
|
||||
|
||||
public void LoadGameplayScene() => Load(gameplayScene);
|
||||
public void LoadMainMenuScene() => Load(mainMenuScene);
|
||||
public void LoadScoringScene() => Load(scoringScene);
|
||||
public void Load(string sceneName)
|
||||
{
|
||||
StopAllCoroutines();
|
||||
StartCoroutine(LoadingCoroutine(sceneName));
|
||||
}
|
||||
|
||||
private IEnumerator LoadingCoroutine(string sceneName)
|
||||
{
|
||||
var loadingOperation = SceneManager.LoadSceneAsync(sceneName);
|
||||
loadingOperation.allowSceneActivation = false;
|
||||
|
||||
if (transitionImage != null)
|
||||
{
|
||||
transitionImage.gameObject.SetActive(true);
|
||||
color = transitionImage.color;
|
||||
color.a = 0f;
|
||||
|
||||
// 淡入到黑
|
||||
while (color.a < 1f)
|
||||
{
|
||||
color.a = Mathf.Clamp01(color.a + Time.unscaledDeltaTime / fadeTime);
|
||||
transitionImage.color = color;
|
||||
yield return null;
|
||||
}
|
||||
}
|
||||
|
||||
// 等异步加载到 90%
|
||||
yield return new WaitUntil(() => loadingOperation.progress >= 0.9f);
|
||||
|
||||
loadingOperation.allowSceneActivation = true;
|
||||
|
||||
if (transitionImage != null)
|
||||
{
|
||||
// 淡出到透明
|
||||
while (color.a > 0f)
|
||||
{
|
||||
color.a = Mathf.Clamp01(color.a - Time.unscaledDeltaTime / fadeTime);
|
||||
transitionImage.color = color;
|
||||
yield return null;
|
||||
}
|
||||
transitionImage.gameObject.SetActive(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: Dyscsn6uBnk4QvVCnUgEfdjebHgB1cCPXqzAMAYCA/MOi0LDY+gbze4=
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,42 @@
|
||||
using UnityEngine;
|
||||
using UnityEngine.UI;
|
||||
|
||||
namespace GameFramework
|
||||
{
|
||||
/// <summary>
|
||||
/// 得分显示组件。
|
||||
/// 订阅 ScoreManager.onScoreChanged 事件,自动更新 Text。
|
||||
/// 挂到场景中带有 Text 组件的 Canvas 元素上即可。
|
||||
/// </summary>
|
||||
public class ScoreDisplay : MonoBehaviour
|
||||
{
|
||||
[SerializeField] Text scoreText;
|
||||
[SerializeField] string format = "{0}";
|
||||
|
||||
void OnEnable()
|
||||
{
|
||||
ScoreManager.onScoreChanged += UpdateText;
|
||||
}
|
||||
|
||||
void OnDisable()
|
||||
{
|
||||
ScoreManager.onScoreChanged -= UpdateText;
|
||||
}
|
||||
|
||||
void Start()
|
||||
{
|
||||
if (scoreText == null)
|
||||
scoreText = GetComponent<Text>();
|
||||
if (ScoreManager.Instance != null)
|
||||
UpdateText(ScoreManager.Instance.CurrentScore);
|
||||
else
|
||||
UpdateText(0);
|
||||
}
|
||||
|
||||
void UpdateText(int score)
|
||||
{
|
||||
if (scoreText != null)
|
||||
scoreText.text = string.Format(format, score);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: XXgasH+tByg20yGkac4QlsGkLlhL3NQFuRiqy5rX5WQQlWzjXcSBwQY=
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,162 @@
|
||||
using System;
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using UnityEngine;
|
||||
|
||||
namespace GameFramework
|
||||
{
|
||||
/// <summary>
|
||||
/// 单条玩家得分记录。
|
||||
/// </summary>
|
||||
[Serializable]
|
||||
public class PlayerScore
|
||||
{
|
||||
public int score;
|
||||
public string playerName;
|
||||
|
||||
public PlayerScore(int score, string playerName)
|
||||
{
|
||||
this.score = score;
|
||||
this.playerName = playerName;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 可序列化的玩家得分列表(用于 JSON 存档)。
|
||||
/// </summary>
|
||||
[Serializable]
|
||||
public class PlayerScoreData
|
||||
{
|
||||
public List<PlayerScore> list = new List<PlayerScore>();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 得分管理器(持久单例)。
|
||||
/// 维护当前局得分,通过事件通知 UI,并管理 Top-10 排行榜存档。
|
||||
/// </summary>
|
||||
public class ScoreManager : PersistentSingleton<ScoreManager>
|
||||
{
|
||||
const int LEADERBOARD_SIZE = 10;
|
||||
const string SAVE_FILE = "leaderboard.json";
|
||||
|
||||
// 当前局得分
|
||||
int currentScore = 0;
|
||||
|
||||
// 当得分变化时触发,参数为最新得分(动画过程中的中间值也会触发)
|
||||
public static event Action<int> onScoreChanged;
|
||||
// 当得分完成最终增加时触发,参数为最终得分
|
||||
public static event Action<int> onScoreSettled;
|
||||
|
||||
/// <summary>当前得分(只读)。</summary>
|
||||
public int CurrentScore => currentScore;
|
||||
|
||||
/// <summary>默认玩家名,可在 Inspector 设置。</summary>
|
||||
[SerializeField] string defaultPlayerName = "Player";
|
||||
|
||||
protected override void Awake()
|
||||
{
|
||||
base.Awake();
|
||||
if (Instance != this) return;
|
||||
currentScore = 0;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 增加得分(带数字滚动动画)。
|
||||
/// </summary>
|
||||
public void AddScore(int amount)
|
||||
{
|
||||
if (amount <= 0) return;
|
||||
StartCoroutine(ScoreCountUpCoroutine(currentScore, currentScore + amount));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 直接设置得分(无动画)。
|
||||
/// </summary>
|
||||
public void SetScore(int value)
|
||||
{
|
||||
currentScore = Mathf.Max(0, value);
|
||||
onScoreChanged?.Invoke(currentScore);
|
||||
onScoreSettled?.Invoke(currentScore);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 重置当前局得分。
|
||||
/// </summary>
|
||||
public void ResetScore()
|
||||
{
|
||||
currentScore = 0;
|
||||
onScoreChanged?.Invoke(0);
|
||||
onScoreSettled?.Invoke(0);
|
||||
}
|
||||
|
||||
IEnumerator ScoreCountUpCoroutine(int from, int to)
|
||||
{
|
||||
currentScore = to;
|
||||
float duration = 0.5f;
|
||||
float elapsed = 0f;
|
||||
|
||||
while (elapsed < duration)
|
||||
{
|
||||
elapsed += Time.unscaledDeltaTime;
|
||||
float t = Mathf.Clamp01(elapsed / duration);
|
||||
int display = Mathf.RoundToInt(Mathf.Lerp(from, to, t));
|
||||
onScoreChanged?.Invoke(display);
|
||||
yield return null;
|
||||
}
|
||||
|
||||
onScoreChanged?.Invoke(to);
|
||||
onScoreSettled?.Invoke(to);
|
||||
}
|
||||
|
||||
#region 排行榜
|
||||
/// <summary>
|
||||
/// 当前得分是否能进入排行榜 Top-10。
|
||||
/// </summary>
|
||||
public bool HasNewHighScore
|
||||
{
|
||||
get
|
||||
{
|
||||
var data = LoadPlayerScoreData();
|
||||
if (data.list.Count < LEADERBOARD_SIZE)
|
||||
return true;
|
||||
return currentScore > data.list[LEADERBOARD_SIZE - 1].score;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 将当前得分以指定名字保存到排行榜。
|
||||
/// 排行榜按分数降序排列,只保留 Top-10。
|
||||
/// </summary>
|
||||
public void SavePlayerScore(string playerName)
|
||||
{
|
||||
if (string.IsNullOrEmpty(playerName))
|
||||
playerName = defaultPlayerName;
|
||||
|
||||
var data = LoadPlayerScoreData();
|
||||
data.list.Add(new PlayerScore(currentScore, playerName));
|
||||
data.list.Sort((a, b) => b.score.CompareTo(a.score));
|
||||
if (data.list.Count > LEADERBOARD_SIZE)
|
||||
data.list.RemoveRange(LEADERBOARD_SIZE, data.list.Count - LEADERBOARD_SIZE);
|
||||
SaveSystem.Save(SAVE_FILE, data);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 读取排行榜数据。
|
||||
/// </summary>
|
||||
public PlayerScoreData LoadPlayerScoreData()
|
||||
{
|
||||
if (SaveSystem.SaveFileExists(SAVE_FILE))
|
||||
return SaveSystem.Load<PlayerScoreData>(SAVE_FILE);
|
||||
return new PlayerScoreData();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 清空排行榜存档。
|
||||
/// </summary>
|
||||
public void ClearLeaderboard()
|
||||
{
|
||||
SaveSystem.DeleteSaveFile(SAVE_FILE);
|
||||
}
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: DC8WvSKkVSrxM9wqXumasT97xw7kTfGqUXXPsJ9DSMckQZ9SK7ZNkOI=
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,68 @@
|
||||
using UnityEngine;
|
||||
|
||||
namespace GameFramework
|
||||
{
|
||||
/// <summary>
|
||||
/// 得分拾取物。
|
||||
/// 玩家碰到(OnTriggerEnter + tag="Player")后加分、播音效、隐藏自身。
|
||||
/// 可自动复位(可选),用于不吃豆就复活的场景。
|
||||
/// </summary>
|
||||
public class ScorePickup : MonoBehaviour
|
||||
{
|
||||
[SerializeField] int scoreValue = 10;
|
||||
[SerializeField] AudioData pickUpSFX;
|
||||
[SerializeField] float respawnTime = 0f; // 0=不复活(永久消失)
|
||||
|
||||
[Header("Auto Assign")]
|
||||
[SerializeField] bool findScoreManagerAutomatically = true;
|
||||
|
||||
Collider pickupCollider;
|
||||
MeshRenderer meshRenderer;
|
||||
|
||||
void Awake()
|
||||
{
|
||||
pickupCollider = GetComponent<Collider>();
|
||||
meshRenderer = GetComponentInChildren<MeshRenderer>();
|
||||
// 确保有触发器
|
||||
if (pickupCollider != null)
|
||||
pickupCollider.isTrigger = true;
|
||||
}
|
||||
|
||||
void OnTriggerEnter(Collider other)
|
||||
{
|
||||
if (!other.CompareTag("Player")) return;
|
||||
|
||||
// 加分
|
||||
if (ScoreManager.Instance != null)
|
||||
ScoreManager.Instance.AddScore(scoreValue);
|
||||
|
||||
// 音效
|
||||
if (AudioManager.Instance != null && pickUpSFX != null)
|
||||
AudioManager.Instance.PlaySFX(pickUpSFX);
|
||||
|
||||
// 隐藏/复活
|
||||
if (respawnTime > 0f)
|
||||
{
|
||||
SetVisible(false);
|
||||
Invoke(nameof(Respawn), respawnTime);
|
||||
}
|
||||
else
|
||||
{
|
||||
gameObject.SetActive(false);
|
||||
}
|
||||
}
|
||||
|
||||
void SetVisible(bool visible)
|
||||
{
|
||||
if (pickupCollider != null)
|
||||
pickupCollider.enabled = visible;
|
||||
if (meshRenderer != null)
|
||||
meshRenderer.enabled = visible;
|
||||
}
|
||||
|
||||
void Respawn()
|
||||
{
|
||||
SetVisible(true);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: XHMfsiKkWyrjymEGDTiE6sqx10eLr1STeHe+ivg7t7be5KBxWCOljj0=
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,193 @@
|
||||
using System.Collections.Generic;
|
||||
using UnityEngine;
|
||||
using UnityEngine.UI;
|
||||
|
||||
namespace GameFramework
|
||||
{
|
||||
/// <summary>
|
||||
/// 排行榜场景 UI 控制器。
|
||||
/// 如果当前得分能进入 Top-10,先显示输入名字面板;
|
||||
/// 否则直接显示排行榜。
|
||||
/// 修复了源项目 ScoringUIController 中 ShowScoringScreen() 被调用两次的 bug。
|
||||
/// </summary>
|
||||
public class ScoringUIController : MonoBehaviour
|
||||
{
|
||||
[Header("Panels")]
|
||||
[SerializeField] GameObject leaderboardPanel;
|
||||
[SerializeField] GameObject newHighScorePanel;
|
||||
|
||||
[Header("Leaderboard")]
|
||||
[SerializeField] Transform leaderboardContainer; // 包含10个条目的父物体
|
||||
[SerializeField] Text finalScoreText; // 显示当前局最终得分
|
||||
|
||||
[Header("New High Score")]
|
||||
[SerializeField] InputField nameInputField;
|
||||
[SerializeField] Button submitButton;
|
||||
[SerializeField] Button cancelButton;
|
||||
|
||||
[Header("Navigation")]
|
||||
[SerializeField] Button mainMenuButton;
|
||||
|
||||
[Header("Audio")]
|
||||
[SerializeField] AudioData submitSFX;
|
||||
[SerializeField] AudioData cancelSFX;
|
||||
|
||||
void Start()
|
||||
{
|
||||
// 绑定按钮
|
||||
if (submitButton != null)
|
||||
submitButton.onClick.AddListener(OnSubmitClick);
|
||||
if (cancelButton != null)
|
||||
cancelButton.onClick.AddListener(OnCancelClick);
|
||||
if (mainMenuButton != null)
|
||||
mainMenuButton.onClick.AddListener(OnMainMenuClick);
|
||||
|
||||
// 初始隐藏所有面板
|
||||
if (leaderboardPanel != null)
|
||||
leaderboardPanel.SetActive(false);
|
||||
if (newHighScorePanel != null)
|
||||
newHighScorePanel.SetActive(false);
|
||||
|
||||
// 显示当前最终得分
|
||||
if (finalScoreText != null && ScoreManager.Instance != null)
|
||||
finalScoreText.text = ScoreManager.Instance.CurrentScore.ToString();
|
||||
|
||||
// 判断是否进入排行榜
|
||||
// 只调用一次——修复源项目的双重调用 bug
|
||||
if (ScoreManager.Instance != null && ScoreManager.Instance.HasNewHighScore)
|
||||
{
|
||||
ShowNewHighScorePanel();
|
||||
}
|
||||
else
|
||||
{
|
||||
ShowLeaderboard();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 显示新纪录输入面板。
|
||||
/// </summary>
|
||||
void ShowNewHighScorePanel()
|
||||
{
|
||||
if (newHighScorePanel != null)
|
||||
newHighScorePanel.SetActive(true);
|
||||
if (leaderboardPanel != null)
|
||||
leaderboardPanel.SetActive(false);
|
||||
|
||||
// 自动选中输入框
|
||||
if (nameInputField != null)
|
||||
{
|
||||
nameInputField.text = "";
|
||||
nameInputField.ActivateInputField();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 显示排行榜。
|
||||
/// </summary>
|
||||
void ShowLeaderboard()
|
||||
{
|
||||
if (newHighScorePanel != null)
|
||||
newHighScorePanel.SetActive(false);
|
||||
if (leaderboardPanel != null)
|
||||
leaderboardPanel.SetActive(true);
|
||||
|
||||
UpdateLeaderboardDisplay();
|
||||
|
||||
// 选中主菜单按钮(键盘导航用)
|
||||
if (mainMenuButton != null)
|
||||
mainMenuButton.Select();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 更新排行榜显示。
|
||||
/// 遍历 leaderboardContainer 下的子物体,设置排名/得分/名字。
|
||||
/// 每个条目子物体需要有三个 Text 组件,分别在 Rank/Score/Name 子物体上。
|
||||
/// </summary>
|
||||
void UpdateLeaderboardDisplay()
|
||||
{
|
||||
if (ScoreManager.Instance == null) return;
|
||||
if (leaderboardContainer == null) return;
|
||||
|
||||
var data = ScoreManager.Instance.LoadPlayerScoreData();
|
||||
int count = Mathf.Min(data.list.Count, leaderboardContainer.childCount);
|
||||
|
||||
for (int i = 0; i < leaderboardContainer.childCount; i++)
|
||||
{
|
||||
Transform entry = leaderboardContainer.GetChild(i);
|
||||
bool hasData = i < data.list.Count;
|
||||
|
||||
// 查找条目下的三个 Text
|
||||
Text rankText = FindTextByName(entry, "Rank");
|
||||
Text scoreText = FindTextByName(entry, "Score");
|
||||
Text nameText = FindTextByName(entry, "Name");
|
||||
|
||||
if (hasData)
|
||||
{
|
||||
var score = data.list[i];
|
||||
if (rankText != null) rankText.text = $"{i + 1}.";
|
||||
if (scoreText != null) scoreText.text = score.score.ToString();
|
||||
if (nameText != null) nameText.text = score.playerName;
|
||||
entry.gameObject.SetActive(true);
|
||||
}
|
||||
else
|
||||
{
|
||||
if (rankText != null) rankText.text = $"{i + 1}.";
|
||||
if (scoreText != null) scoreText.text = "---";
|
||||
if (nameText != null) nameText.text = "---";
|
||||
entry.gameObject.SetActive(true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 在子物体中按名称查找 Text 组件。
|
||||
/// </summary>
|
||||
Text FindTextByName(Transform parent, string name)
|
||||
{
|
||||
Transform t = parent.Find(name);
|
||||
if (t != null)
|
||||
return t.GetComponent<Text>();
|
||||
// 如果条目本身就是单个 Text(没有子结构),返回自身
|
||||
return parent.GetComponent<Text>();
|
||||
}
|
||||
|
||||
#region Button Handlers
|
||||
void OnSubmitClick()
|
||||
{
|
||||
string playerName = "";
|
||||
if (nameInputField != null)
|
||||
playerName = nameInputField.text;
|
||||
|
||||
if (string.IsNullOrEmpty(playerName))
|
||||
playerName = "Player";
|
||||
|
||||
if (ScoreManager.Instance != null)
|
||||
ScoreManager.Instance.SavePlayerScore(playerName);
|
||||
|
||||
if (AudioManager.Instance != null && submitSFX != null)
|
||||
AudioManager.Instance.PlaySFX(submitSFX);
|
||||
|
||||
ShowLeaderboard();
|
||||
}
|
||||
|
||||
void OnCancelClick()
|
||||
{
|
||||
// 取消 = 用默认名保存后显示排行榜
|
||||
if (ScoreManager.Instance != null)
|
||||
ScoreManager.Instance.SavePlayerScore("Player");
|
||||
|
||||
if (AudioManager.Instance != null && cancelSFX != null)
|
||||
AudioManager.Instance.PlaySFX(cancelSFX);
|
||||
|
||||
ShowLeaderboard();
|
||||
}
|
||||
|
||||
void OnMainMenuClick()
|
||||
{
|
||||
if (SceneLoader.Instance != null)
|
||||
SceneLoader.Instance.LoadMainMenuScene();
|
||||
}
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: W34bsX/7Wy1ExDIK2gVfkr7HYzOwXi//jKqEeettI8EJBU/quoNe7R8=
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,17 @@
|
||||
using UnityEngine;
|
||||
|
||||
namespace GameFramework
|
||||
{
|
||||
/// <summary>
|
||||
/// 场景级单例基类。每个场景里只存在一个实例,切场景后不保留。
|
||||
/// </summary>
|
||||
public class Singleton<T> : MonoBehaviour where T : Component
|
||||
{
|
||||
public static T Instance { get; private set; }
|
||||
|
||||
protected virtual void Awake()
|
||||
{
|
||||
Instance = this as T;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: DSke5C3/Ui2rwbIReBtztfYr2wcvt/9+Lt+vNrdIhkeQ+/WSFb3KT7k=
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,24 @@
|
||||
using UnityEngine;
|
||||
|
||||
namespace GameFramework
|
||||
{
|
||||
/// <summary>
|
||||
/// 时间控制器(场景级单例)。负责暂停/恢复(Time.timeScale)。
|
||||
/// 源项目的子弹时间逻辑已移除,只保留暂停。
|
||||
/// </summary>
|
||||
public class TimeController : Singleton<TimeController>
|
||||
{
|
||||
private float timeScaleBeforePause = 1f;
|
||||
|
||||
public void Pause()
|
||||
{
|
||||
timeScaleBeforePause = Time.timeScale;
|
||||
Time.timeScale = 0f;
|
||||
}
|
||||
|
||||
public void Unpause()
|
||||
{
|
||||
Time.timeScale = timeScaleBeforePause > 0f ? timeScaleBeforePause : 1f;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: Xi5M5HipV3ITPzlXa/Jgdtl3/zjZiWQ9EukfLBaa2ygvlKHBz/lGi8U=
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,45 @@
|
||||
using UnityEngine;
|
||||
using UnityEngine.EventSystems;
|
||||
|
||||
namespace GameFramework
|
||||
{
|
||||
/// <summary>
|
||||
/// UI 事件音效触发器。
|
||||
/// 挂到按钮上,鼠标悬停/选中时播放 selectSFX,点击/提交时播放 submitSFX。
|
||||
/// </summary>
|
||||
public class UIEventTrigger : MonoBehaviour,
|
||||
IPointerEnterHandler,
|
||||
IPointerDownHandler,
|
||||
ISelectHandler,
|
||||
ISubmitHandler
|
||||
{
|
||||
[SerializeField] AudioData selectSFX;
|
||||
[SerializeField] AudioData submitSFX;
|
||||
|
||||
public void OnPointerEnter(PointerEventData eventData)
|
||||
{
|
||||
PlaySFX(selectSFX);
|
||||
}
|
||||
|
||||
public void OnPointerDown(PointerEventData eventData)
|
||||
{
|
||||
PlaySFX(submitSFX);
|
||||
}
|
||||
|
||||
public void OnSelect(BaseEventData eventData)
|
||||
{
|
||||
PlaySFX(selectSFX);
|
||||
}
|
||||
|
||||
public void OnSubmit(BaseEventData eventData)
|
||||
{
|
||||
PlaySFX(submitSFX);
|
||||
}
|
||||
|
||||
void PlaySFX(AudioData data)
|
||||
{
|
||||
if (AudioManager.Instance != null && data != null)
|
||||
AudioManager.Instance.PlaySFX(data);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: Xi8btCqoUX6kUJTWKBTy8tU139tZU/LcCXUb+S3KlOlgTgOA/0wpsGI=
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
Reference in New Issue
Block a user