添加场景、对应场景代码
This commit is contained in:
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,7 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: XnIZvCj/WilIPm5sxv000GrkA0fQi4oGpaqJswmJmlEkEZvwKwlSzlE=
|
||||||
|
DefaultImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: CXwWtS75VH2kRhpqXyENCxWvKc+sMGX3ZmBGWqfZFB4KWUEa9B6DD34=
|
||||||
|
folderAsset: yes
|
||||||
|
DefaultImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
@@ -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:
|
||||||
@@ -1,13 +1,27 @@
|
|||||||
%YAML 1.1
|
%YAML 1.1
|
||||||
%TAG !u! tag:unity3d.com,2011:
|
%TAG !u! tag:yousandi.cn,2023:
|
||||||
--- !u!1045 &1
|
--- !u!1045 &1
|
||||||
EditorBuildSettings:
|
EditorBuildSettings:
|
||||||
m_ObjectHideFlags: 0
|
m_ObjectHideFlags: 0
|
||||||
serializedVersion: 2
|
serializedVersion: 2
|
||||||
m_Scenes:
|
m_Scenes:
|
||||||
- enabled: 1
|
- enabled: 1
|
||||||
path: Assets/2.5D Engine/Demo Scene/DEMO 1.unity
|
path: Assets/Scenes/MainMenu.unity
|
||||||
guid: 99c9720ab356a0642a771bea13969a05
|
guid: a8692b9b19ec95b48918246882a64c8c
|
||||||
|
- enabled: 1
|
||||||
|
path: Assets/Scenes/Gameplay.unity
|
||||||
|
guid: 9de73b30b85a6e049aa627b90c7a30af
|
||||||
|
- enabled: 1
|
||||||
|
path: Assets/Scenes/Scoring.scene
|
||||||
|
guid: 9fc0d4010bbf28b4594072e72b8655ab
|
||||||
|
m_BuildProfiles: []
|
||||||
m_configObjects:
|
m_configObjects:
|
||||||
com.unity.input.settings.actions: {fileID: -944628639613478452, guid: 1581d80d30bc9f14f8be5e0a8b72fc79, type: 3}
|
com.unity.input.settings.actions: {fileID: -944628639613478452, guid: 1581d80d30bc9f14f8be5e0a8b72fc79, type: 3}
|
||||||
m_UseUCBPForAssetBundles: 0
|
m_SlimFeaturesWeixinMiniGame:
|
||||||
|
AllocateOverhead: 1
|
||||||
|
DecompressASTC: 0
|
||||||
|
DecompressDXT: 1
|
||||||
|
DecompressETC: 1
|
||||||
|
FreeTypeSfntPng: 1
|
||||||
|
LightProbe: 2
|
||||||
|
UseStringInternPool: 2
|
||||||
|
|||||||
@@ -15,17 +15,17 @@ EditorUserSettings:
|
|||||||
value: 50025251565751035a5b0a7b44250944144f4e7d7d787336782a4a37b6b7606f
|
value: 50025251565751035a5b0a7b44250944144f4e7d7d787336782a4a37b6b7606f
|
||||||
flags: 0
|
flags: 0
|
||||||
RecentlyUsedSceneGuid-3:
|
RecentlyUsedSceneGuid-3:
|
||||||
value: 5004505e50045a0e5f57097b14250f444615407b287b7e657f704c31b5b36660
|
|
||||||
flags: 0
|
|
||||||
RecentlyUsedSceneGuid-4:
|
|
||||||
value: 5b090d000402580a5c5b087648735e4445151e737b717334757b1832b1b93261
|
value: 5b090d000402580a5c5b087648735e4445151e737b717334757b1832b1b93261
|
||||||
flags: 0
|
flags: 0
|
||||||
RecentlyUsedSceneGuid-5:
|
RecentlyUsedSceneGuid-4:
|
||||||
value: 5055070050010c5a0e5d0d27497a0e441016412c2f7d206178714f61b3b8326b
|
value: 5055070050010c5a0e5d0d27497a0e441016412c2f7d206178714f61b3b8326b
|
||||||
flags: 0
|
flags: 0
|
||||||
RecentlyUsedSceneGuid-6:
|
RecentlyUsedSceneGuid-5:
|
||||||
value: 5a5757560101590a5d0c0e24427b5d44434e4c7a7b7a23677f2b4565b7b5353a
|
value: 5a5757560101590a5d0c0e24427b5d44434e4c7a7b7a23677f2b4565b7b5353a
|
||||||
flags: 0
|
flags: 0
|
||||||
|
RecentlyUsedSceneGuid-6:
|
||||||
|
value: 0209025f575750595c57092149765d444e4e49727e7c7068757b1c65b6e36c3b
|
||||||
|
flags: 0
|
||||||
RecentlyUsedSceneGuid-7:
|
RecentlyUsedSceneGuid-7:
|
||||||
value: 5a55515156575a0b0f56592346260f444f16197c7e7f24697d2a4a32b1b0353e
|
value: 5a55515156575a0b0f56592346260f444f16197c7e7f24697d2a4a32b1b0353e
|
||||||
flags: 0
|
flags: 0
|
||||||
|
|||||||
Reference in New Issue
Block a user