添加场景、对应场景代码

This commit is contained in:
JA
2026-06-30 09:46:59 +08:00
parent eeb1907247
commit 1489f33d9d
41 changed files with 3685 additions and 9 deletions
File diff suppressed because it is too large Load Diff
+7
View File
@@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: XnIZvCj/WilIPm5sxv000GrkA0fQi4oGpaqJswmJmlEkEZvwKwlSzlE=
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:
+8
View File
@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: CXwWtS75VH2kRhpqXyENCxWvKc+sMGX3ZmBGWqfZFB4KWUEa9B6DD34=
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:
+124
View File
@@ -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
}
}
+11
View File
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: D38YsCytVCoe3CxQzCPwoXB7xeRjwedpI2nk1WvamOgtg/ae5hcGCSs=
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
+39
View File
@@ -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
}
}
+11
View File
@@ -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:
+91
View File
@@ -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();
}
}
}
+11
View File
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: CClOsC78Vi9/cIog8UeuUzecgGTFlu5AQ5iyvdkeMcyG1n6ns9R5ds8=
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
+68
View File
@@ -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:
+160
View File
@@ -0,0 +1,160 @@
using UnityEngine;
using UnityEngine.UI;
namespace GameFramework
{
/// <summary>
/// 暂停菜单控制器。
/// 按 ESC 切换暂停/继续。
/// 暂停时显示菜单 Canvas,提供继续/设置/返回主菜单三个按钮。
/// 使用 old Input ManagerInput.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();
}
}
}
+11
View File
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: DHgWs3+kUy5RNMyJcnH1NVpwyVBiplJDeLTX0ne96zBO1RYpFbBNLuY=
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
+27
View File
@@ -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:
+58
View File
@@ -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));
}
}
}
+11
View File
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: XnoWtHuqUihinx6tGMjdkofi7dRhZE+34X4QUIkChIFcacunTLquUfM=
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
+22
View File
@@ -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);
}
}
}
+11
View File
@@ -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:
+76
View File
@@ -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);
}
}
}
}
+11
View File
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: Dyscsn6uBnk4QvVCnUgEfdjebHgB1cCPXqzAMAYCA/MOi0LDY+gbze4=
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
+42
View File
@@ -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);
}
}
}
+11
View File
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: XXgasH+tByg20yGkac4QlsGkLlhL3NQFuRiqy5rX5WQQlWzjXcSBwQY=
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
+162
View File
@@ -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
}
}
+11
View File
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: DC8WvSKkVSrxM9wqXumasT97xw7kTfGqUXXPsJ9DSMckQZ9SK7ZNkOI=
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
+68
View File
@@ -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);
}
}
}
+11
View File
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: XHMfsiKkWyrjymEGDTiE6sqx10eLr1STeHe+ivg7t7be5KBxWCOljj0=
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
+193
View File
@@ -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:
+17
View File
@@ -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;
}
}
}
+11
View File
@@ -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:
+24
View File
@@ -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;
}
}
}
+11
View File
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: Xi5M5HipV3ITPzlXa/Jgdtl3/zjZiWQ9EukfLBaa2ygvlKHBz/lGi8U=
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
+45
View File
@@ -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);
}
}
}
+11
View File
@@ -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
+5 -5
View File
@@ -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