Unity游戏开发:从零构建事件驱动的金钱系统与HUD

Unity游戏开发:从零构建事件驱动的金钱系统与HUD

1. 项目概述与核心目标

最近在复刻《空洞骑士》的Demo,做到第二十一集,终于要开始处理游戏里最让人有“获得感”的部分之一了——吉欧(Geo)金钱系统和基础的HUD(平视显示器)。很多朋友在开发自己的2D动作游戏时,往往会先专注于角色移动、攻击这些核心玩法,把经济系统留到最后。但实际上,一个设计良好的金钱系统,不仅能驱动游戏进程,更是连接玩家行为与游戏世界反馈的核心纽带。这次,我们就来聊聊如何在Unity里,从零搭建一个既稳定又易于扩展的吉欧系统,并初步构建起承载它的HUD Canvas。

简单来说,我们要实现的目标很明确:第一,创建一个GeoManager单例来全局管理玩家的吉欧数量,确保数据持久化且线程安全(虽然Unity主线程操作,但我们要有好的架构意识)。第二,设计一个直观的HUD_GeoDisplay组件,它能实时响应吉欧数量的变化,并播放获取/消耗吉欧的动画反馈。第三,构建一个灵活的GeoPickup可收集物预制体,当玩家角色与之碰撞时,能触发拾取逻辑,并更新HUD。这个过程会涉及到Unity的UI系统(Canvas, TextMeshPro)、单例模式、事件(C# Action/UnityEvent)以及简单的动画状态控制。无论你是刚学完C#基础想找个项目练手,还是已经有一定经验但想优化自己的游戏架构,相信这个从数据到表现层的完整实现过程,都能给你带来一些启发。

2. 系统架构设计与核心思路拆解

在动手写代码之前,我们先花点时间把整个系统的架构理清楚。一个健壮的系统不是一堆脚本的堆砌,而是有清晰的责任划分和数据流。对于吉欧系统,我们可以将其分为三个核心层:数据管理层游戏逻辑层表现层

2.1 三层架构解析

数据管理层的核心是GeoManager。它必须是一个单例(Singleton),因为整个游戏中只应该存在一个权威的吉欧数据源。它的职责包括:

  1. 持有并维护当前的吉欧数量:提供一个属性(如CurrentGeo)供外部读取,但修改必须通过其提供的方法(如AddGeo,SpendGeo)。
  2. 持久化存储:使用PlayerPrefs或更专业的序列化方案(如JSON+文件存储)来保存和加载吉欧数据。考虑到《空洞骑士》这类游戏需要跨场景、甚至退出游戏后仍保持数据,这是必须的。
  3. 提供数据变更的事件:这是连接数据与表现的关键。当吉欧数量发生变化时,GeoManager应该抛出一个事件(例如public static event Action<int> OnGeoChanged),任何关心此变化的UI组件都可以订阅它,从而实现解耦。

游戏逻辑层的核心是GeoPickup。它代表场景中一个具体的、可被玩家拾取的吉欧物体(比如闪闪发光的钱币)。它的职责是:

  1. 定义自身价值:这个拾取物值多少吉欧(例如,小钱币值1,大钱币值5)。
  2. 处理碰撞检测:通过OnTriggerEnter2D检测与玩家的碰撞。
  3. 触发拾取逻辑:碰撞发生后,调用GeoManager.Instance.AddGeo(value)来增加吉欧,然后销毁自身或播放一个“被吸收”的动画。

表现层的核心是HUD_GeoDisplay。它挂载在UI Canvas下的一个专门用于显示吉欧的UI元素上(比如一个包含图标和文本的面板)。它的职责是:

  1. 监听数据变化:在Start()OnEnable()中订阅GeoManager.OnGeoChanged事件。
  2. 更新UI文本:当事件触发时,将新的吉欧数量更新到TextMeshProUGUI组件上。
  3. 提供视觉反馈:不仅仅是改数字。当获得吉欧时,文本可以短暂放大、变色或播放一个“+”的动画;消耗时则可以播放缩小的动画。这能极大地增强玩家的正反馈。

2.2 为什么选择事件驱动?

这里重点说一下事件驱动架构的好处。最直接的做法是,在GeoPickup中拾取后,直接去查找HUD_GeoDisplay并调用它的更新方法。但这样做耦合度太高。如果以后我们想在拾取时除了更新HUD,还要播放全局音效、更新成就系统或者触发任务进度,就需要反复修改GeoPickup脚本。而使用事件驱动,GeoPickup只负责“通知”系统“吉欧增加了X”,至于谁关心、要做什么,它一概不管。GeoManager作为中枢,广播这个消息,HUD_GeoDisplayAudioManagerAchievementManager都可以独立订阅并做出反应。系统的可扩展性和可维护性会好很多。

注意:单例模式的陷阱。虽然单例方便全局访问,但要小心“单例滥用”。确保GeoManager只管理最核心的、全局唯一的数据。不要把它变成什么都往里塞的“垃圾箱”。另外,在Awake()中实现单例时,要做好重复实例的检查与销毁,防止场景切换时产生多个管理器。

3. 核心模块实现与代码详解

理论讲完了,我们进入实战环节。我会按照数据层->逻辑层->表现层的顺序,展示核心代码和实现细节。

3.1 GeoManager:数据中枢的实现

首先创建GeoManager.cs脚本。它不挂载在任何场景物体上,我们通过代码创建它。

using UnityEngine; using System; // 为了使用Action public class GeoManager : MonoBehaviour { // 1. 单例实例 public static GeoManager Instance { get; private set; } // 2. 当前吉欧数量(私有字段+公有属性,便于控制) private int _currentGeo = 0; public int CurrentGeo => _currentGeo; // 3. 定义吉欧变化事件 public static event Action<int> OnGeoChanged; // 4. 定义存储用的键名 private const string PREFS_GEO_KEY = "PlayerGeo"; private void Awake() { // 单例初始化 if (Instance != null && Instance != this) { Destroy(this.gameObject); return; } Instance = this; DontDestroyOnLoad(this.gameObject); // 跨场景不销毁 LoadGeo(); } // 5. 增加吉欧 public void AddGeo(int amount) { if (amount <= 0) { Debug.LogWarning($"尝试增加非正数的吉欧: {amount}"); return; } _currentGeo += amount; SaveGeo(); OnGeoChanged?.Invoke(_currentGeo); // 安全触发事件 Debug.Log($"获得 {amount} 吉欧,当前总计: {_currentGeo}"); } // 6. 消耗吉欧 public bool SpendGeo(int amount) { if (amount <= 0) { Debug.LogWarning($"尝试消耗非正数的吉欧: {amount}"); return false; } if (_currentGeo >= amount) { _currentGeo -= amount; SaveGeo(); OnGeoChanged?.Invoke(_currentGeo); Debug.Log($"消耗 {amount} 吉欧,剩余: {_currentGeo}"); return true; } else { Debug.Log($"吉欧不足!需要 {amount},但只有 {_currentGeo}"); // 这里可以触发一个“钱不够”的UI提示或音效 return false; } } // 7. 持久化方法 private void SaveGeo() { PlayerPrefs.SetInt(PREFS_GEO_KEY, _currentGeo); PlayerPrefs.Save(); // 建议显式保存 } private void LoadGeo() { _currentGeo = PlayerPrefs.GetInt(PREFS_GEO_KEY, 0); // 默认值为0 Debug.Log($"加载吉欧: {_currentGeo}"); } // 8. 重置吉欧(用于调试或新游戏) public void ResetGeo() { _currentGeo = 0; SaveGeo(); OnGeoChanged?.Invoke(_currentGeo); } }

关键点解析:

  • DontDestroyOnLoad: 这行代码保证了GeoManagerGameObject在切换场景时不会被销毁,从而维持了数据的连续性。这是实现跨场景单例的常用方法。
  • 事件的安全调用?.Invoke(): 使用空条件运算符,即使没有订阅者也不会报空引用异常。
  • SpendGeo返回bool: 这是一个很好的设计。消费行为可能失败(钱不够),通过返回值,调用方可以立即知道结果并做出反应(比如显示提示)。
  • 使用PlayerPrefs: 对于Demo或小型项目,PlayerPrefs简单够用。但对于更复杂的存档系统(需要存储多个变量、字典、列表等),建议使用JsonUtilityNewtonsoft.Json将数据类序列化成字符串再存入PlayerPrefs,或者直接读写文件。

3.2 GeoPickup:可收集物的实现

接下来创建GeoPickup.cs,并将其挂载到吉欧预制体上。这个预制体通常是一个带有SpriteRenderer(显示吉欧图片)和Collider2D(设置为Is Trigger)的GameObject。

using UnityEngine; public class GeoPickup : MonoBehaviour { [SerializeField] private int geoValue = 1; // 在Inspector中可设置价值 [SerializeField] private AudioClip pickupSound; // 拾取音效 [SerializeField] private GameObject pickupEffect; // 拾取粒子特效预制体 private bool isCollected = false; // 防止重复拾取 private void OnTriggerEnter2D(Collider2D collision) { // 1. 检查碰撞对象是否是玩家 if (isCollected) return; // 已拾取则直接返回 // 假设玩家有一个"Player"标签或特定的组件,这里用标签检查 if (!collision.CompareTag("Player")) return; // 2. 标记为已拾取,防止同一帧内多次触发 isCollected = true; // 3. 调用GeoManager增加吉欧 if (GeoManager.Instance != null) { GeoManager.Instance.AddGeo(geoValue); } else { Debug.LogError("GeoManager实例未找到!"); } // 4. 播放音效(如果有) if (pickupSound != null && AudioManager.Instance != null) // 假设有AudioManager单例 { AudioManager.Instance.PlaySound(pickupSound); } // 5. 生成拾取特效(如果有) if (pickupEffect != null) { Instantiate(pickupEffect, transform.position, Quaternion.identity); } // 6. 销毁自身(或先播放一个动画再销毁) // 可以先禁用渲染器和碰撞体,播放一个缩放消失的动画,动画结束后再Destroy GetComponent<SpriteRenderer>().enabled = false; GetComponent<Collider2D>().enabled = false; // 假设有一个简单的渐隐动画协程 StartCoroutine(DisappearAnimation()); } private System.Collections.IEnumerator DisappearAnimation() { // 这里可以做一个简单的向上飘动并渐隐的动画 float duration = 0.5f; float timer = 0f; Vector3 startPos = transform.position; while (timer < duration) { timer += Time.deltaTime; float t = timer / duration; transform.position = startPos + Vector3.up * Mathf.Lerp(0f, 1f, t); // 向上移动 // 如果需要渐隐,可以控制SpriteRenderer的Color.a yield return null; } Destroy(gameObject); } }

实操心得:

  • isCollected标志位:非常重要!物理触发可能在极短时间内被调用多次,这个标志位可以确保拾取逻辑只执行一次,避免玩家一次碰撞加了两次钱。
  • 延迟销毁与动画:直接Destroy(gameObject)会很突兀。更好的做法是立即禁用视觉和碰撞组件,然后播放一个简短的“收集动画”(如上飘、渐隐、缩放),动画结束后再销毁。这能提供更平滑的反馈。
  • 依赖检查:在调用GeoManager.Instance前做空检查是好习惯,可以快速定位问题。

3.3 HUD_GeoDisplay:UI表现层的实现

现在来制作HUD。首先在场景中创建UI:GameObject -> UI -> Canvas。将Canvas的Render Mode设置为Screen Space - Overlay。在Canvas下创建一个Panel,命名为GeoHUD,并调整其锚点位置(比如右上角)。在这个Panel里,放一个Image(吉欧图标)和一个TextMeshPro - Text (UI)对象(显示数字)。

然后创建HUD_GeoDisplay.cs脚本并挂载到GeoHUDPanel上。

using UnityEngine; using TMPro; // 引入TextMeshPro命名空间 using System.Collections; public class HUD_GeoDisplay : MonoBehaviour { [SerializeField] private TextMeshProUGUI geoText; // 拖拽赋值 [SerializeField] private Animator animator; // 用于控制反馈动画 [SerializeField] private string gainAnimationTrigger = "Gain"; [SerializeField] private string spendAnimationTrigger = "Spend"; private int displayedGeo; // 当前UI上显示的数字(用于平滑变化) private void Start() { if (geoText == null) { geoText = GetComponentInChildren<TextMeshProUGUI>(); } if (animator == null) { animator = GetComponent<Animator>(); } // 初始化显示:直接获取当前吉欧数 if (GeoManager.Instance != null) { displayedGeo = GeoManager.Instance.CurrentGeo; UpdateUIText(); } // 订阅事件 GeoManager.OnGeoChanged += OnGeoChanged; } private void OnDestroy() { // 非常重要!取消订阅,防止内存泄漏 GeoManager.OnGeoChanged -= OnGeoChanged; } private void OnGeoChanged(int newGeoAmount) { // 计算变化量 int delta = newGeoAmount - displayedGeo; // 更新目标显示值 displayedGeo = newGeoAmount; // 根据变化量触发不同动画 if (delta > 0 && animator != null) { animator.SetTrigger(gainAnimationTrigger); } else if (delta < 0 && animator != null) { animator.SetTrigger(spendAnimationTrigger); } // 更新UI文本(可以在这里做平滑数字变化) // 直接更新: UpdateUIText(); // 或者启动一个协程做数字滚动效果: // StartCoroutine(CountGeoSmoothly(newGeoAmount)); } private void UpdateUIText() { geoText.text = displayedGeo.ToString(); } // 可选的平滑计数协程 private IEnumerator CountGeoSmoothly(int targetGeo) { float duration = 0.5f; // 计数动画时长 float startValue = displayedGeo; float timer = 0f; while (timer < duration) { timer += Time.deltaTime; float t = timer / duration; // 使用缓动函数让动画更自然,例如Mathf.SmoothStep t = Mathf.SmoothStep(0f, 1f, t); int currentDisplay = (int)Mathf.Lerp(startValue, targetGeo, t); geoText.text = currentDisplay.ToString(); yield return null; } // 确保最终显示准确 geoText.text = targetGeo.ToString(); displayedGeo = targetGeo; // 更新内部状态 } }

UI动画设置:

  1. GeoHUDPanel上添加Animator组件。
  2. 在项目窗口中右键Create -> Animator Controller,命名为GeoHUD_Animator,并拖拽给Animator组件。
  3. 双击打开Animator Controller窗口。
  4. 创建两个Trigger类型的参数:GainSpend
  5. Entry状态连接到默认的Idle状态。
  6. 创建两个新的动画状态,比如GainAnimationSpendAnimation
  7. 制作两个简单的动画:GainAnimation可以让文本的Scale从1放大到1.2再弹回1,颜色可以短暂变黄;SpendAnimation可以让文本短暂变红并轻微抖动。将这两个动画片段拖入对应的状态。
  8. 在Animator中设置状态转换:从IdleGainAnimation的条件是GainTrigger被触发;从IdleSpendAnimation的条件是SpendTrigger被触发。两个动画状态都要设置Has Exit Time并勾选,确保播放完后自动回到Idle状态。

踩坑提醒:UI事件订阅与内存泄漏。在Start中订阅事件,必须在OnDestroy中取消订阅。如果这个UI对象被动态销毁(比如切换HUD风格),而不取消订阅,GeoManager中的事件列表会一直保留着一个对已销毁对象方法的引用,导致内存无法释放,这就是内存泄漏。这是一个非常常见且隐蔽的问题。

4. Canvas搭建与HUD布局实战

有了核心逻辑,我们需要一个美观且实用的HUD来承载它。Unity的UI Canvas是功能强大但设置繁琐的系统,合理的设置能避免后续很多缩放和适配问题。

4.1 Canvas与屏幕适配

首先,我们专门为游戏HUD创建一个Canvas。

  1. 在Hierarchy中右键 ->UI -> Canvas,重命名为HUD_Canvas
  2. 选中HUD_Canvas,在Inspector中做如下关键设置:
    • Render Mode:Screen Space - Overlay。这是最常用的HUD模式,UI会渲染在所有3D/2D物体之上。
    • UI Scale Mode:Scale With Screen Size。这是实现多分辨率适配的核心。
    • Reference Resolution: 设置为你设计UI时的基准分辨率,例如1920 x 1080。这意味着你在这个分辨率下摆放UI元素。
    • Screen Match Mode: 推荐设置为Match Width or Height,并将滑块拉到0.5(均衡)或根据游戏偏向(宽屏拉向Width,竖屏拉向Height)。这个设置决定了在不同宽高比的屏幕上,UI是以宽度还是高度为基准进行缩放,能有效防止UI在极端比例下变形或溢出屏幕。

4.2 吉欧显示面板制作

HUD_Canvas下创建UI -> Panel,重命名为Geo_Panel。我们将用它来分组吉欧相关的UI元素。

  1. 锚点(Anchors)设置:这是UI定位的灵魂。选中Geo_Panel,在Rect Transform组件中,点击左上角的方形锚点预设图标。我们希望它固定在屏幕右上角,所以选择右上角的预设(Anchor Preset: Top-Right)。同时按住Alt键点击这个预设,这样Panel的轴心(Pivot)也会对齐到右上角。
  2. 位置与大小:设置Pos XPos Y为负值(例如-50, -50),让Panel距离屏幕边缘有一点内边距。设置WidthHeight为一个合适的值,比如300100
  3. 背景:你可以取消Image组件的勾选,让背景透明,或者调整颜色和透明度,做一个半透明的背景框。

Geo_Panel内创建子对象:

  1. 图标UI -> Image,重命名为Icon。将你的吉欧精灵图(Sprite)拖拽到Source Image。调整其大小和位置,锚点设为Left,并调整Pos XWidth/Height
  2. 文本UI -> Text - TextMeshPro,重命名为Text_Amount。这是显示数字的地方。
    • 锚点设为Stretch,左右拉伸,这样文本长度变化时能自动适应。
    • LeftRightPos值设置为正数(例如6020),让文本区域从图标右侧开始,到Panel右侧结束。
    • TextMeshPro - Text (UI)组件中:
      • Text Input: 先输入“0”。
      • Font Asset: 选择一个像素风或风格匹配的字体。
      • Font Size: 设置一个合适的大小,如48
      • Alignment: 水平左对齐,垂直居中。
      • Color: 设置为金色或白色,高亮显示。
    • 为了更好的视觉效果,可以给文本添加一个OutlineShadow效果(在TextMeshPro组件下方点击Add Component添加)。

最后,将Text_Amount对象拖拽到HUD_GeoDisplay脚本的geoText字段中。如果Panel上有Animator,也一并拖拽赋值。

4.3 扩展HUD内容:灵魂与血量

一个完整的《空洞骑士》风格HUD当然不止吉欧。我们可以用同样的架构思想来扩展。

  1. 灵魂(Soul)系统:创建一个SoulManager单例(管理当前灵魂和灵魂上限),一个HUD_SoulDisplayUI组件。灵魂UI通常是一个容器(比如多个小圆点或一个条状填充图)来显示存量。当角色集中(Focus)时,调用SoulManager.SpendSoul()并播放相应的UI动画。
  2. 血量(Health)系统:创建HealthManager管理玩家和敌人的血量。HUD上的血量通常用“面具”图标表示,每个面具代表一点血。当受伤时,HealthManager触发OnHealthChanged事件,HUD_HealthDisplay监听并更新面具的显示状态(完整、破裂、消失)。
  3. 快速收集栏(Quick Item):可以创建一个ItemManager和对应的HUD栏,用于显示当前装备的护符或法术图标。

布局技巧:将这些面板(Geo, Soul, Health)都作为HUD_Canvas的子对象,并分别设置它们的锚点到屏幕的四个角(如血量在左上,灵魂在左中,吉欧在右上)。使用Horizontal Layout GroupVertical Layout Group组件可以自动排列同一面板内的多个元素(比如多个血格),非常方便。

5. 调试、优化与常见问题排查

系统搭建好了,但在实际运行中总会遇到各种问题。这里记录一些我调试过程中遇到的典型问题和解决方案。

5.1 常见问题速查表

问题现象可能原因排查步骤与解决方案
吉欧数量不显示或显示为01.GeoManager实例未创建或初始化失败。
2.HUD_GeoDisplay未正确订阅事件。
3. UI Text组件未赋值或禁用。
1. 检查场景中是否存在GeoManager的GameObject。可以在Awake中加Debug.Log
2. 在HUD_GeoDisplayStartOnGeoChanged方法开头加Debug.Log,看是否被调用。
3. 检查Hierarchy中Text_Amount对象是否激活,geoText字段是否在Inspector中拖拽赋值。
拾取吉欧后UI不更新1. 事件未触发或订阅关系错误。
2.OnGeoChanged事件被多个脚本订阅,可能被其他脚本错误地重置了数值。
1. 在GeoManagerAddGeo方法里,在调用OnGeoChanged前后加Debug.Log,确认事件触发。
2. 检查项目中是否有其他脚本也订阅了OnGeoChanged并可能修改了geoText。使用Unity的Console窗口的调用堆栈功能,点击事件触发的Log,可以看到所有调用路径。
切换场景后吉欧数量重置GeoManagerGameObject未设置为DontDestroyOnLoad,或被新场景中的另一个实例覆盖。1. 确保GeoManager脚本的Awake中有DontDestroyOnLoad
2. 确保GeoManager是单例,并且在新场景中不会重复创建。检查单例模式代码是否正确销毁了新实例。
拾取音效或特效不播放1.AudioManager或粒子系统未正确设置或引用为空。
2. 拾取物在播放动画前就被销毁。
1. 在Inspector中检查GeoPickup上的pickupSoundpickupEffect字段是否赋值。
2. 确保AudioManager.Instance不为空。
3. 将Destroy(gameObject)改为延迟销毁(如用协程),确保音效和特效有足够时间播放。
UI在不同分辨率下错位Canvas的UI Scale Mode或UI元素的锚点设置不正确。1. 确认CanvasScale ModeScale With Screen Size,并设置合理的Reference Resolution
2. 对每个UI元素(如Geo_Panel),使用锚点预设进行定位,而不是直接设置PosX/PosY。对于需要保持宽高比的元素,可以结合Aspect Ratio Fitter组件。
数字变化生硬,没有反馈感UI仅直接更新文本,缺乏视觉过渡。1. 实现CountGeoSmoothly协程,让数字滚动变化。
2. 为Animator制作更丰富的动画状态,不仅触发放大缩小,还可以加入颜色闪烁、图标抖动等。
PlayerPrefs存储的数据被篡改或丢失玩家可能手动清除数据,或不同版本键名冲突。1. 对于重要数据,考虑使用加密存储(简单的XOR或更复杂的AES)。
2. 在游戏版本更新时,如果存储结构变化,需要编写数据迁移代码。可以为存储数据增加一个版本号键。

5.2 性能优化小贴士

  • UI更新频率OnGeoChanged事件可能在一帧内被频繁触发(比如连续拾取多个吉欧)。如果CountGeoSmoothly协程每次都被重启,会导致数字跳动异常。可以在协程开始前StopCoroutine,或者更优的做法是,在HUD_GeoDisplay中只记录目标值,在Update中平滑地向目标值逼近,这样能保证每帧只更新一次UI,且过渡平滑。
  • 对象池管理GeoPickup在游戏中会频繁生成和销毁。如果吉欧数量很多,频繁的InstantiateDestroy会产生GC(垃圾回收)压力。可以考虑使用对象池(Object Pooling)。预先创建一堆吉欧物体并禁用,需要时从池中取出激活并设置位置,拾取后不是销毁而是放回池中并禁用。Unity 2021版之后提供了内置的ObjectPool类,非常好用。
  • 事件清理:再次强调,任何基于委托/事件的脚本,在OnDestroyOnDisable中取消订阅是必须的编程习惯,否则会导致难以排查的内存泄漏和空引用异常。

5.3 架构扩展思考

当前的系统是一个很好的起点。随着游戏复杂度的增加,我们可以考虑以下扩展:

  1. 引入观察者模式变体:如果觉得静态事件太多难以管理,可以引入一个中央的GameEvent系统,使用ScriptableObject来创建事件资产,实现更松散的耦合。
  2. 数据与表现彻底分离:将GeoManager管理的核心数据(当前吉欧数)抽象成一个PlayerDataScriptableObjectGeoManager只负责读写这个资产。这样存档/读档、数据重置会变得更清晰。
  3. 本地化与格式化:在HUD_GeoDisplayUpdateUIText中,可以使用string.FormatToString(“N0”)来为数字添加千位分隔符,让大数字更易读(如“1,250”)。

实现完这个吉欧系统和基础HUD,看着自己操控的角色在屏幕上奔跑、击碎环境中的钱罐、UI上的数字随之跳动并伴有清脆的音效和动画反馈,那种亲手搭建起游戏世界一环的成就感是无与伦比的。这套模式不仅适用于吉欧,更是你构建任何游戏内资源系统(经验、弹药、能量)的通用蓝图。记住,好的系统是迭代出来的,先让它跑起来,再让它跑得漂亮、跑得稳健。接下来,你就可以基于这个框架,去挑战更复杂的系统,比如商店购买、技能升级等需要消费吉欧的玩法了。