第12章 游戏积分系统深度构建:《暗黑王朝》资源管理的完整实现
在《暗黑王朝》这样一款写实风格的射击手游中,积分系统不仅仅是分数的简单累加,而是玩家与游戏世界进行价值交换的核心机制。金币作为软货币,让玩家能够购买武器弹药、解锁新装备;能量作为硬通货,限制玩家的单次游戏时长,平衡游戏节奏。一套设计良好的积分系统,需要实现计分器的实时更新、数据的跨场景持久化存储,以及与玩家操作的无缝交互。本章将从建立积分系统的架构设计入手,逐步展开金币计分器和能量计分器的UGUI实现,最终通过PlayerPrefs完成游戏积分的保存与加载。
12.1 积分系统的架构设计
12.1.1 积分系统的分层架构
在设计《暗黑王朝》的积分系统时,我们采用了经典的三层架构:表现层、逻辑层、数据层。这种分层设计的核心优势在于职责清晰、易于扩展、便于调试。
表现层由UGUI组件构成,包括显示金币数量的Text文本框、显示能量数值的Slider滑动条或Text文本。表现层不包含任何业务逻辑,只负责将数据呈现给玩家,并将玩家的输入传递给逻辑层。
逻辑层是积分系统的核心,由ScoreManager类实现。它负责处理积分的增减逻辑、验证操作的合法性、触发相应的UI更新事件,并与数据层交互完成数据的持久化。
数据层处理数据的存储和加载。对于《暗黑王朝》这样的手游项目,我们采用Unity内置的PlayerPrefs作为数据存储方案,它可以存储简单的键值对数据,适合保存玩家的分数、解锁的关卡等轻量级信息。
根据Unity官方文档的建议,设计积分系统时需要明确资源的来源(Source)和去处(Sink)。金币的来源包括击败敌人、完成任务、开启宝箱;金币的去处包括购买武器、弹药、医疗包。能量的来源主要是时间恢复;能量的去处是玩家进入关卡消耗能量。理解这些资源的流向,有助于我们设计出平衡的经济系统。
12.1.2 单例模式在积分管理中的应用
由于积分系统需要在多个场景之间共享数据,我们采用单例模式来确保ScoreManager的唯一性。根据Unity官方开发者社区的经验,单例模式是游戏开发中管理全局数据的常用模式。
以下是一个基础的积分管理器单例实现:
using UnityEngine;
namespace DarkOrder.Score
{
/// <summary>
/// 积分管理器单例 – 负责所有积分数据的逻辑处理
/// </summary>
public class ScoreManager : MonoBehaviour
{
[Header("积分数据")]
[SerializeField]
private int m_Coins = 0; // 金币数量
[SerializeField]
private int m_Energy = 100; // 能量数量
[SerializeField]
private int m_MaxEnergy = 100; // 最大能量
// 单例实例
private static ScoreManager s_Instance;
// 公开属性
public static ScoreManager Instance
{
get { return s_Instance; }
}
public int Coins
{
get { return m_Coins; }
private set { m_Coins = value; }
}
public int Energy
{
get { return m_Energy; }
private set { m_Energy = Mathf.Clamp(value, 0, m_MaxEnergy); }
}
public int MaxEnergy
{
get { return m_MaxEnergy; }
private set { m_MaxEnergy = value; }
}
private void Awake()
{
// 单例模式实现
if (s_Instance != null && s_Instance != this)
{
Destroy(gameObject);
return;
}
s_Instance = this;
DontDestroyOnLoad(gameObject);
// 加载保存的数据
LoadScores();
}
/// <summary>
/// 增加金币
/// </summary>
public void AddCoins(int amount)
{
if (amount <= 0) return;
m_Coins += amount;
SaveCoins();
// 触发UI更新事件
OnCoinsChanged?.Invoke(m_Coins);
}
/// <summary>
/// 减少金币
/// </summary>
public bool SpendCoins(int amount)
{
if (amount <= 0) return true;
if (m_Coins < amount) return false;
m_Coins -= amount;
SaveCoins();
OnCoinsChanged?.Invoke(m_Coins);
return true;
}
/// <summary>
/// 消耗能量进入关卡
/// </summary>
public bool SpendEnergy(int amount)
{
if (amount <= 0) return true;
if (m_Energy < amount) return false;
m_Energy -= amount;
SaveEnergy();
OnEnergyChanged?.Invoke(m_Energy, m_MaxEnergy);
return true;
}
/// <summary>
/// 恢复能量
/// </summary>
public void RestoreEnergy(int amount)
{
if (amount <= 0) return;
m_Energy = Mathf.Min(m_Energy + amount, m_MaxEnergy);
SaveEnergy();
OnEnergyChanged?.Invoke(m_Energy, m_MaxEnergy);
}
// 事件定义
public System.Action<int> OnCoinsChanged;
public System.Action<int, int> OnEnergyChanged;
// 数据保存方法将在后续实现
private void SaveCoins() { }
private void SaveEnergy() { }
private void LoadScores() { }
}
}
这个单例管理器通过在Awake中判断实例是否存在来确保全局唯一性,并调用DontDestroyOnLoad让管理器在场景切换时不被销毁。这样,任何场景中的脚本都可以通过ScoreManager.Instance访问积分数据。
12.2 金币计分器的完整实现
12.2.1 UGUI文本组件的配置
金币计分器需要显示在游戏界面的角落,通常使用TextMeshPro – Text组件实现。TextMeshPro相比传统的Unity UI Text组件,具有更清晰的字体渲染效果和更丰富的样式控制能力。
在Unity中创建金币计分器的步骤:
创建Canvas:如果场景中还没有Canvas,在Hierarchy窗口中右键选择UI > Canvas。Canvas是所有UI元素的容器。
添加背景图:为了美观,可以创建一个Image作为金币显示区域的背景。右键Canvas选择UI > Image,设置合适的颜色和大小。
添加金币图标:创建一个Image作为金币图标,将Source Image设置为金币图标精灵。
添加TextMeshPro文本:右键选择UI > Text – TextMeshPro。在Inspector中设置文本的字体、大小、颜色等属性。
设置锚点:将整个金币面板的锚点设置为右上角,这样无论屏幕分辨率如何变化,金币显示都会保持在屏幕右上角固定位置。
在代码中获取TextMeshPro组件的引用:
using TMPro;
public class CoinDisplay : MonoBehaviour
{
[SerializeField]
private TextMeshProUGUI m_CoinText;
private void Start()
{
if (m_CoinText == null)
m_CoinText = GetComponent<TextMeshProUGUI>();
}
private void OnEnable()
{
// 订阅积分变化事件
if (ScoreManager.Instance != null)
{
ScoreManager.Instance.OnCoinsChanged += UpdateCoinDisplay;
UpdateCoinDisplay(ScoreManager.Instance.Coins);
}
}
private void OnDisable()
{
// 取消订阅,避免内存泄漏
if (ScoreManager.Instance != null)
{
ScoreManager.Instance.OnCoinsChanged -= UpdateCoinDisplay;
}
}
private void UpdateCoinDisplay(int coins)
{
if (m_CoinText != null)
{
m_CoinText.text = coins.ToString();
}
}
}
12.2.2 金币增减逻辑的实现
金币的增减逻辑需要与游戏中的具体行为绑定。例如,击败敌人后增加金币,购买武器时消耗金币。
以下是在敌人死亡时增加金币的实现:
using UnityEngine;
namespace DarkOrder.AI
{
public class EnemyDrop : MonoBehaviour
{
[Header("掉落配置")]
[SerializeField]
private int m_CoinDropAmount = 10;
[SerializeField]
private float m_DropProbability = 1f; // 掉落概率,1为100%
private EnemyHealth m_EnemyHealth;
private void Start()
{
m_EnemyHealth = GetComponent<EnemyHealth>();
if (m_EnemyHealth != null)
{
// 订阅死亡事件
m_EnemyHealth.OnDeath.AddListener(OnEnemyDeath);
}
}
private void OnEnemyDeath()
{
// 概率判定
if (Random.value > m_DropProbability)
return;
// 增加金币
if (ScoreManager.Instance != null)
{
ScoreManager.Instance.AddCoins(m_CoinDropAmount);
// 可选:生成金币拾取物
SpawnCoinPickup();
}
}
private void SpawnCoinPickup()
{
// 实例化金币拾取物预制体
// 让玩家可以主动拾取,增强交互感
}
}
}
当玩家在商店购买物品时,需要消耗金币:
using UnityEngine;
using UnityEngine.UI;
namespace DarkOrder.UI
{
public class ShopItem : MonoBehaviour
{
[Header("商品配置")]
[SerializeField]
private string m_ItemId;
[SerializeField]
private int m_Price = 100;
[SerializeField]
private Button m_BuyButton;
private void Start()
{
if (m_BuyButton != null)
{
m_BuyButton.onClick.AddListener(OnBuyButtonClick);
}
// 初始检查金币是否足够
UpdateButtonState();
// 订阅金币变化事件
if (ScoreManager.Instance != null)
{
ScoreManager.Instance.OnCoinsChanged += OnCoinsChanged;
}
}
private void OnDestroy()
{
if (ScoreManager.Instance != null)
{
ScoreManager.Instance.OnCoinsChanged -= OnCoinsChanged;
}
}
private void OnCoinsChanged(int coins)
{
UpdateButtonState();
}
private void UpdateButtonState()
{
if (m_BuyButton != null && ScoreManager.Instance != null)
{
m_BuyButton.interactable = (ScoreManager.Instance.Coins >= m_Price);
}
}
private void OnBuyButtonClick()
{
if (ScoreManager.Instance == null) return;
// 尝试消耗金币
if (ScoreManager.Instance.SpendCoins(m_Price))
{
// 购买成功,发放物品
GrantItem();
// 播放购买成功音效
AudioManager.Instance.PlaySound("purchase_success");
}
else
{
// 金币不足,提示玩家
UIManager.Instance.ShowMessage("金币不足");
// 播放失败音效
AudioManager.Instance.PlaySound("purchase_failed");
}
}
private void GrantItem()
{
// 根据物品ID发放对应物品到玩家背包
Debug.Log($"购买成功: {m_ItemId}");
}
}
}
12.2.3 金币动画与视觉反馈
为了让金币增减的视觉反馈更加生动,可以添加简单的动画效果。当金币数量变化时,让数字有短暂的缩放效果。
以下是一个金币动画组件的实现:
using UnityEngine;
using TMPro;
using System.Collections;
namespace DarkOrder.UI
{
[RequireComponent(typeof(TextMeshProUGUI))]
public class CoinPulseEffect : MonoBehaviour
{
[Header("动画参数")]
[SerializeField]
private float m_PulseDuration = 0.2f;
[SerializeField]
private float m_PulseScale = 1.3f;
private TextMeshProUGUI m_CoinText;
private Vector3 m_OriginalScale;
private Coroutine m_PulseCoroutine;
private void Start()
{
m_CoinText = GetComponent<TextMeshProUGUI>();
m_OriginalScale = transform.localScale;
// 订阅金币变化事件
if (ScoreManager.Instance != null)
{
ScoreManager.Instance.OnCoinsChanged += OnCoinsChanged;
}
}
private void OnDestroy()
{
if (ScoreManager.Instance != null)
{
ScoreManager.Instance.OnCoinsChanged -= OnCoinsChanged;
}
}
private void OnCoinsChanged(int coins)
{
// 触发脉冲动画
if (m_PulseCoroutine != null)
{
StopCoroutine(m_PulseCoroutine);
}
m_PulseCoroutine = StartCoroutine(PulseRoutine());
}
private IEnumerator PulseRoutine()
{
float elapsedTime = 0;
// 放大
while (elapsedTime < m_PulseDuration / 2)
{
float t = elapsedTime / (m_PulseDuration / 2);
float scale = Mathf.Lerp(1f, m_PulseScale, t);
transform.localScale = m_OriginalScale * scale;
elapsedTime += Time.deltaTime;
yield return null;
}
elapsedTime = 0;
// 恢复
while (elapsedTime < m_PulseDuration / 2)
{
float t = elapsedTime / (m_PulseDuration / 2);
float scale = Mathf.Lerp(m_PulseScale, 1f, t);
transform.localScale = m_OriginalScale * scale;
elapsedTime += Time.deltaTime;
yield return null;
}
transform.localScale = m_OriginalScale;
m_PulseCoroutine = null;
}
}
}
12.3 能量计分器的完整实现
12.3.1 能量系统的设计理念
在《暗黑王朝》中,能量系统承担着平衡游戏节奏的重要功能。能量决定了玩家能够进行多少次游戏,随着时间推移自动恢复,玩家也可以消耗金币购买能量。
能量系统的设计要点包括:
根据Unity官方指南,能量系统应该让玩家处于无聊和沮丧之间的"甜蜜点"——既不会因为能量无限而失去挑战性,也不会因为能量恢复过慢而感到挫败。
12.3.2 能量UI的多种表现形式
能量在UI中可以有多种表现形式:数字文本、进度条、图标阵列等。在《暗黑王朝》中,我们采用组合式UI:图标阵列显示剩余能量数量,进度条显示距离下一能量恢复的时间。
以下是能量进度条UI控制器的实现:
using UnityEngine;
using UnityEngine.UI;
using TMPro;
namespace DarkOrder.UI
{
public class EnergyDisplay : MonoBehaviour
{
[Header("UI组件")]
[SerializeField]
private Slider m_EnergySlider;
[SerializeField]
private TextMeshProUGUI m_EnergyText;
[SerializeField]
private TextMeshProUGUI m_TimerText;
[Header("能量图标阵列")]
[SerializeField]
private GameObject m_EnergyIconPrefab;
[SerializeField]
private Transform m_IconContainer;
[SerializeField]
private int m_MaxIcons = 10;
private Image[] m_EnergyIcons;
private void Start()
{
// 初始化图标阵列
InitializeIcons();
// 订阅能量变化事件
if (ScoreManager.Instance != null)
{
ScoreManager.Instance.OnEnergyChanged += OnEnergyChanged;
// 初始更新
OnEnergyChanged(ScoreManager.Instance.Energy, ScoreManager.Instance.MaxEnergy);
}
// 启动定时器更新恢复时间
InvokeRepeating(nameof(UpdateRecoveryTimer), 0f, 1f);
}
private void InitializeIcons()
{
if (m_IconContainer == null || m_EnergyIconPrefab == null) return;
// 清除现有图标
foreach (Transform child in m_IconContainer)
{
Destroy(child.gameObject);
}
// 创建新图标
m_EnergyIcons = new Image[m_MaxIcons];
for (int i = 0; i < m_MaxIcons; i++)
{
GameObject icon = Instantiate(m_EnergyIconPrefab, m_IconContainer);
m_EnergyIcons[i] = icon.GetComponent<Image>();
}
}
private void OnEnergyChanged(int currentEnergy, int maxEnergy)
{
// 更新进度条
if (m_EnergySlider != null)
{
m_EnergySlider.maxValue = maxEnergy;
m_EnergySlider.value = currentEnergy;
}
// 更新数字文本
if (m_EnergyText != null)
{
m_EnergyText.text = $"{currentEnergy}/{maxEnergy}";
}
// 更新图标阵列
UpdateIcons(currentEnergy, maxEnergy);
}
private void UpdateIcons(int currentEnergy, int maxEnergy)
{
if (m_EnergyIcons == null) return;
int iconCount = Mathf.Min(maxEnergy, m_MaxIcons);
for (int i = 0; i < iconCount; i++)
{
if (m_EnergyIcons[i] != null)
{
// 前currentEnergy个图标亮起,其余变暗
m_EnergyIcons[i].color = (i < currentEnergy) ? Color.white : new Color(0.3f, 0.3f, 0.3f, 1f);
}
}
}
private void UpdateRecoveryTimer()
{
if (ScoreManager.Instance == null) return;
// 能量已满时不显示倒计时
if (ScoreManager.Instance.Energy >= ScoreManager.Instance.MaxEnergy)
{
if (m_TimerText != null)
{
m_TimerText.text = "已满";
}
return;
}
// 计算距离下次恢复的时间
// 实际项目中需要从专门的EnergyRecoveryManager获取剩余秒数
// 这里简化为显示占位符
if (m_TimerText != null)
{
// m_TimerText.text = $"{minutes}:{seconds:D2}";
m_TimerText.text = "05:30";
}
}
private void OnDestroy()
{
if (ScoreManager.Instance != null)
{
ScoreManager.Instance.OnEnergyChanged -= OnEnergyChanged;
}
CancelInvoke();
}
}
}
12.3.3 能量恢复机制的实现
能量恢复需要计时器逻辑,即使在游戏关闭后也需要继续计时。这需要在游戏启动时计算从上次关闭到当前的时间差,恢复相应的能量。
以下是一个能量恢复管理器的实现:
using UnityEngine;
using System;
namespace DarkOrder.Score
{
/// <summary>
/// 能量恢复管理器 – 处理时间恢复逻辑
/// </summary>
public class EnergyRecoveryManager : MonoBehaviour
{
[Header("恢复参数")]
[SerializeField]
private int m_RecoveryIntervalSeconds = 300; // 5分钟恢复1点
[SerializeField]
private int m_MaxEnergy = 100;
private float m_RecoveryTimer;
private void Start()
{
// 加载上次离线时间,计算离线期间恢复的能量
ProcessOfflineRecovery();
// 启动恢复计时器
m_RecoveryTimer = 0;
}
private void Update()
{
if (ScoreManager.Instance == null) return;
// 能量已满,不恢复
if (ScoreManager.Instance.Energy >= m_MaxEnergy)
return;
// 计时器累加
m_RecoveryTimer += Time.deltaTime;
// 达到恢复间隔,恢复1点能量
if (m_RecoveryTimer >= m_RecoveryIntervalSeconds)
{
m_RecoveryTimer -= m_RecoveryIntervalSeconds;
ScoreManager.Instance.RestoreEnergy(1);
}
}
/// <summary>
/// 处理离线恢复
/// </summary>
private void ProcessOfflineRecovery()
{
// 获取上次退出的时间戳
long lastExitTime = PlayerPrefs.GetString("LastExitTime", DateTime.UtcNow.ToString());
DateTime lastTime;
if (DateTime.TryParse(lastExitTime, out lastTime))
{
DateTime now = DateTime.UtcNow;
TimeSpan elapsed = now – lastTime;
// 计算离线期间应恢复的能量
int recoveredEnergy = Mathf.FloorToInt((float)elapsed.TotalSeconds / m_RecoveryIntervalSeconds);
if (recoveredEnergy > 0 && ScoreManager.Instance != null)
{
int newEnergy = Mathf.Min(
ScoreManager.Instance.Energy + recoveredEnergy,
m_MaxEnergy
);
// 直接设置能量值
typeof(ScoreManager).GetProperty("Energy").SetValue(ScoreManager.Instance, newEnergy);
}
}
// 更新保存逻辑将在应用退出时处理
}
private void OnApplicationQuit()
{
// 保存退出时间
PlayerPrefs.SetString("LastExitTime", DateTime.UtcNow.ToString());
PlayerPrefs.Save();
}
}
}
12.4 积分数据的持久化存储
12.4.1 PlayerPrefs的工作原理
PlayerPrefs是Unity内置的轻量级数据存储方案,以键值对的形式存储数据。在Windows平台上,数据存储在注册表中;在Android平台上,存储在SharedPreferences中;在iOS平台上,存储在NSUserDefaults中。
PlayerPrefs支持三种数据类型:int、float、string。存储方法对应为SetInt、SetFloat、SetString;读取方法对应为GetInt、GetFloat、GetString。读取时可以指定默认值,当键不存在时返回该默认值。
需要注意的是,Set方法调用后数据只保存在内存中,调用PlayerPrefs.Save()后才会写入持久化存储。如果游戏崩溃,未保存的数据将会丢失。因此,在关键操作后应及时调用Save()。
12.4.2 积分保存与加载的完整实现
在ScoreManager中完善数据保存和加载的方法:
using UnityEngine;
namespace DarkOrder.Score
{
public class ScoreManager : MonoBehaviour
{
// … 之前的代码 …
[Header("存储键名")]
[SerializeField]
private string m_CoinKey = "PlayerCoins";
[SerializeField]
private string m_EnergyKey = "PlayerEnergy";
[SerializeField]
private string m_MaxEnergyKey = "PlayerMaxEnergy";
/// <summary>
/// 加载所有保存的数据
/// </summary>
private void LoadScores()
{
// 加载金币
m_Coins = PlayerPrefs.GetInt(m_CoinKey, 0);
// 加载能量
m_Energy = PlayerPrefs.GetInt(m_EnergyKey, 100);
// 加载最大能量
m_MaxEnergy = PlayerPrefs.GetInt(m_MaxEnergyKey, 100);
Debug.Log($"积分加载完成 – 金币: {m_Coins}, 能量: {m_Energy}/{m_MaxEnergy}");
}
/// <summary>
/// 保存金币数据
/// </summary>
private void SaveCoins()
{
PlayerPrefs.SetInt(m_CoinKey, m_Coins);
PlayerPrefs.Save(); // 立即保存,确保数据安全
Debug.Log($"金币已保存: {m_Coins}");
}
/// <summary>
/// 保存能量数据
/// </summary>
private void SaveEnergy()
{
PlayerPrefs.SetInt(m_EnergyKey, m_Energy);
PlayerPrefs.SetInt(m_MaxEnergyKey, m_MaxEnergy);
PlayerPrefs.Save();
Debug.Log($"能量已保存: {m_Energy}/{m_MaxEnergy}");
}
/// <summary>
/// 重置所有数据(用于测试)
/// </summary>
public void ResetAllData()
{
PlayerPrefs.DeleteKey(m_CoinKey);
PlayerPrefs.DeleteKey(m_EnergyKey);
PlayerPrefs.DeleteKey(m_MaxEnergyKey);
PlayerPrefs.Save();
m_Coins = 0;
m_Energy = m_MaxEnergy;
OnCoinsChanged?.Invoke(m_Coins);
OnEnergyChanged?.Invoke(m_Energy, m_MaxEnergy);
Debug.Log("所有积分数据已重置");
}
}
}
12.4.3 数据加密与防作弊考虑
对于商业游戏,简单的PlayerPrefs存储容易被玩家修改。在《暗黑王朝》中,我们引入了简单的异或加密来增加修改难度:
using System.Text;
using UnityEngine;
namespace DarkOrder.Score
{
/// <summary>
/// 加密存储辅助类
/// </summary>
public static class SecurePlayerPrefs
{
private const string ENCRYPTION_KEY = "DarkOrder2025"; // 实际项目中应从安全位置获取
/// <summary>
/// 保存加密的整数值
/// </summary>
public static void SetInt(string key, int value)
{
string stringValue = value.ToString();
string encryptedValue = Encrypt(stringValue);
PlayerPrefs.SetString(key, encryptedValue);
}
/// <summary>
/// 获取解密的整数值
/// </summary>
public static int GetInt(string key, int defaultValue = 0)
{
if (!PlayerPrefs.HasKey(key))
return defaultValue;
string encryptedValue = PlayerPrefs.GetString(key);
string decryptedValue = Decrypt(encryptedValue);
int result;
if (int.TryParse(decryptedValue, out result))
return result;
return defaultValue;
}
private static string Encrypt(string input)
{
if (string.IsNullOrEmpty(input)) return input;
byte[] inputBytes = Encoding.UTF8.GetBytes(input);
byte[] keyBytes = Encoding.UTF8.GetBytes(ENCRYPTION_KEY);
for (int i = 0; i < inputBytes.Length; i++)
{
inputBytes[i] = (byte)(inputBytes[i] ^ keyBytes[i % keyBytes.Length]);
}
return System.Convert.ToBase64String(inputBytes);
}
private static string Decrypt(string input)
{
if (string.IsNullOrEmpty(input)) return input;
try
{
byte[] inputBytes = System.Convert.FromBase64String(input);
byte[] keyBytes = Encoding.UTF8.GetBytes(ENCRYPTION_KEY);
for (int i = 0; i < inputBytes.Length; i++)
{
inputBytes[i] = (byte)(inputBytes[i] ^ keyBytes[i % keyBytes.Length]);
}
return Encoding.UTF8.GetString(inputBytes);
}
catch
{
return string.Empty;
}
}
}
}
12.5 积分系统的完整测试与调试
12.5.1 测试场景的搭建
为了测试积分系统,需要搭建一个包含以下元素的测试场景:
以下是一个测试控制器的实现:
using UnityEngine;
using UnityEngine.UI;
namespace DarkOrder.Testing
{
public class ScoreTestController : MonoBehaviour
{
[Header("测试按钮")]
[SerializeField]
private Button m_AddCoinsButton;
[SerializeField]
private Button m_SpendCoinsButton;
[SerializeField]
private Button m_ResetDataButton;
[Header("调试显示")]
[SerializeField]
private Text m_DebugText;
private void Start()
{
if (m_AddCoinsButton != null)
m_AddCoinsButton.onClick.AddListener(TestAddCoins);
if (m_SpendCoinsButton != null)
m_SpendCoinsButton.onClick.AddListener(TestSpendCoins);
if (m_ResetDataButton != null)
m_ResetDataButton.onClick.AddListener(TestResetData);
// 订阅事件更新调试显示
if (ScoreManager.Instance != null)
{
ScoreManager.Instance.OnCoinsChanged += UpdateDebugDisplay;
ScoreManager.Instance.OnEnergyChanged += UpdateDebugDisplay;
UpdateDebugDisplay(ScoreManager.Instance.Coins);
}
}
private void TestAddCoins()
{
if (ScoreManager.Instance != null)
{
ScoreManager.Instance.AddCoins(100);
}
}
private void TestSpendCoins()
{
if (ScoreManager.Instance != null)
{
bool success = ScoreManager.Instance.SpendCoins(50);
Debug.Log($"消费金币: {(success ? "成功" : "失败 – 余额不足")}");
}
}
private void TestResetData()
{
if (ScoreManager.Instance != null)
{
ScoreManager.Instance.ResetAllData();
}
}
private void UpdateDebugDisplay(int coins)
{
if (m_DebugText != null && ScoreManager.Instance != null)
{
m_DebugText.text = $"金币: {coins}\\n能量: {ScoreManager.Instance.Energy}/{ScoreManager.Instance.MaxEnergy}";
}
}
private void UpdateDebugDisplay(int energy, int maxEnergy)
{
if (m_DebugText != null && ScoreManager.Instance != null)
{
m_DebugText.text = $"金币: {ScoreManager.Instance.Coins}\\n能量: {energy}/{maxEnergy}";
}
}
private void OnDestroy()
{
if (ScoreManager.Instance != null)
{
ScoreManager.Instance.OnCoinsChanged -= UpdateDebugDisplay;
ScoreManager.Instance.OnEnergyChanged -= UpdateDebugDisplay;
}
}
}
}
12.5.2 常见问题排查
根据《暗黑王朝》开发经验,积分系统常见问题及解决方案如下:
问题1:场景切换后积分重置
- 检查ScoreManager是否调用了DontDestroyOnLoad
- 确保没有在场景中重复创建ScoreManager实例
问题2:UI不更新
- 确认UI组件已订阅事件
- 检查事件触发逻辑是否在正确的时机调用
- 使用Debug.Log验证数值是否确实发生变化
问题3:数据保存失败
- 确保调用PlayerPrefs.Save()
- 检查键名是否一致
- 在Android/iOS真机测试,编辑器行为可能与真机不同
问题4:能量恢复不准确
- 检查时间恢复逻辑是否使用Time.deltaTime累加
- 验证离线恢复的时间计算是否正确
- 考虑使用服务器时间替代本地时间,防止玩家篡改
12.6 本章小结
本章围绕《暗黑王朝》的积分系统,从架构设计到具体实现,系统讲解了金币计分器和能量计分器的完整开发流程。
在积分系统架构部分,我们采用三层架构分离表现层、逻辑层和数据层,通过单例模式确保数据全局唯一。ScoreManager作为核心管理器,负责所有积分数据的逻辑处理和事件分发。
金币计分器的实现涵盖了UGUI文本组件的配置、金币增减逻辑的编写、以及与敌人系统和商店系统的交互。通过OnCoinsChanged事件,UI组件能够实时响应数据变化,脉冲动画效果增强了视觉反馈。
能量计分器的设计体现了游戏经济系统的平衡理念。能量上限限制玩家单次游戏时长,时间恢复机制鼓励每日登录,消耗场景确保游戏具有挑战性。能量UI采用了进度条、数字文本、图标阵列的组合形式,为玩家提供丰富的信息层次。
积分数据的持久化存储通过PlayerPrefs实现。我们详细讲解了存储原理、数据加载流程,并引入了简单的异或加密来增加数据安全性。
最后,通过测试控制器的实现,验证了积分系统的各项功能。常见问题排查部分总结了《暗黑王朝》开发过程中遇到的实际问题及解决方案。
通过本章的学习,读者应当能够独立设计和实现一套完整的游戏积分系统,并理解经济系统设计的基本原则。积分系统看似简单,却是连接玩家操作与游戏反馈的纽带,是构建沉浸式游戏体验的重要组成部分。






