欢迎光临
我们一直在努力

第12章 游戏积分系统深度构建:《暗黑王朝》资源管理的完整实现

第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 能量系统的设计理念

    在《暗黑王朝》中,能量系统承担着平衡游戏节奏的重要功能。能量决定了玩家能够进行多少次游戏,随着时间推移自动恢复,玩家也可以消耗金币购买能量。

    能量系统的设计要点包括:

  • 上限限制:设置能量上限,防止玩家过度囤积
  • 时间恢复:每隔一段时间恢复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 测试场景的搭建

    为了测试积分系统,需要搭建一个包含以下元素的测试场景:

  • 金币显示UI:显示当前金币数量
  • 能量显示UI:显示当前能量和恢复倒计时
  • 测试按钮:增加金币、消耗金币、增加能量、消耗能量
  • 保存测试:显示保存状态,模拟游戏关闭再启动
  • 以下是一个测试控制器的实现:

    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实现。我们详细讲解了存储原理、数据加载流程,并引入了简单的异或加密来增加数据安全性。

    最后,通过测试控制器的实现,验证了积分系统的各项功能。常见问题排查部分总结了《暗黑王朝》开发过程中遇到的实际问题及解决方案。

    通过本章的学习,读者应当能够独立设计和实现一套完整的游戏积分系统,并理解经济系统设计的基本原则。积分系统看似简单,却是连接玩家操作与游戏反馈的纽带,是构建沉浸式游戏体验的重要组成部分。

    赞(0)
    未经允许不得转载:171主机测评 » 第12章 游戏积分系统深度构建:《暗黑王朝》资源管理的完整实现
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址