Unity3D RPG Core | 15 人物基本属性

这节开始介绍游戏数据相关的内容。视频中介绍了 ScriptableObject 的方式,它能像其他 Unity 资产文件一样存储和管理,比如之前的 pipeline setting 文件也是 ScriptableObject 的形式。

1. 创建 ScriptableObject 脚本

我们创建一个 C# 脚本,命名为 CharacterDataScriptableObject。如代码清单 1 所示,我们让新建的类继承于 ScriptableObject 类。接着我们在类中定义和角色相关的属性,比如这边定义了最大生命值、当前生命值、基础防御力、当前防御力。

指定 CreateAssetMenu 特性,可以让我们在资产菜单中指定创建 ScriptableObject 对应的资产文件。menuName 设置菜单中的项名称,fileName 指定创建文件的默认名称。

代码清单 1 ScriptableObject脚本
  1. using System.Collections;
  2. using System.Collections.Generic;
  3. using UnityEngine;
  4.  
  5. [CreateAssetMenu(fileName = "Character Data", menuName = "ScriptableObject/Character Data")]
  6. public class CharacterDataScriptableObject : ScriptableObject
  7. {
  8.     public int m_maxHealth;
  9.     public int m_currentHealth;
  10.     public int m_baseDefence;
  11.     public int m_currentDefence;
  12. }

视频中默认的目录是二级的。Unity2021 上如果想要设置多级目录,可以将 menuName 指定为 "ScriptableObject/Character Data" 这样。

2. 创建 ScriptableObject 资产文件

回到 Unity 界面,如图 1 所示,我们在资产窗口中点开菜单,可以发现有了新增的内容。

图1 新的目录项

我们首先为人物角色创建对应的文件,命名为 PlayerData。如图 2 所示,我们可以在 Inspector 窗体中像之前一样设置属性值,非常方便。同样的,我们可以为史莱姆也创建一个对应的文件,也是使用相同的“模板”。

图2 设置属性

3. 创建访问脚本

直接在之前的人物控制代码中使用上述 CharacterDataScriptableObject 类是可以的。但是视频中应该是为了让数据这块功能更加独立,需要在对象上再单独挂一个脚本组件(需要继承 MonoBehaviour)。

我们新建 CharacterData 脚本。如代码清单 2 所示,主要就是封装 CharacterDataScriptableObject 类型的成员,我们通过访问器来访问其中的成员。

有一点需要说明,视频中的 CharacterDataScriptableObject 是公有的。这边把它设置为私有的原因是认为公有权限会破坏模块独立的初衷,同时也“浪费”了所写的访问器。但是我们还需要在 Unity 界面中指定对应的 CharacterDataScriptableObject 资产,所以使用 SerializeField 特性修饰。

代码清单 2 访问脚本
  1. using System.Collections;
  2. using System.Collections.Generic;
  3. using UnityEngine;
  4.  
  5. public class CharacterData : MonoBehaviour
  6. {
  7.     [SerializeField]
  8.     private CharacterDataScriptableObject m_characterData = null;
  9.  
  10.     public int MaxHealth
  11.     {
  12.         get { return m_characterData != null ? m_characterData.m_maxHealth : 0; }
  13.         set { if (m_characterData != null) m_characterData.m_maxHealth = value; }
  14.     }
  15.  
  16.     public int CurrentHealth
  17.     {
  18.         get { return m_characterData != null ? m_characterData.m_currentHealth : 0; }
  19.         set { if (m_characterData != null) m_characterData.m_currentHealth = value; }
  20.     }
  21.  
  22.     public int BaseDefence
  23.     {
  24.         get { return m_characterData != null ? m_characterData.m_baseDefence : 0; }
  25.         set { if (m_characterData != null) m_characterData.m_baseDefence = value; }
  26.     }
  27.  
  28.     public int CurrentDefence
  29.     {
  30.         get { return m_characterData != null ? m_characterData.m_currentDefence : 0; }
  31.         set { if (m_characterData != null) m_characterData.m_currentDefence = value; }
  32.     }
  33. }

SerializeField 特性可以使私有变量在 Inspector 窗体中指定。

4. 挂载访问脚本

如图 3 所示,我们把访问脚本挂载在人物角色对象上,并指定数据为 PlayerData

图3 挂载访问脚本

回到之前的角色控制脚本中,如代码清单 3 所示,定义了 CharacterData 成员(第 3 行),并获取相关组件(第 9 行)。

代码清单 3 访问数据
  1. public class PlayerController : MonoBehaviour
  2. {
  3.     CharacterData m_characterData;
  4.  
  5.     void Awake()
  6.     {
  7.         m_navMeshAgent  = GetComponent<NavMeshAgent>();
  8.         m_animator      = GetComponent<Animator>();
  9.         m_characterData = GetComponent<CharacterData>();
  10.     }
  11.  
  12.     // Start is called before the first frame update
  13.     void Start()
  14.     {
  15.         MouseManager.GetInstance().OnMoveMouseClick   += DoMoveAction;
  16.         MouseManager.GetInstance().OnAttackMouseClick += DoAttackAction;
  17.  
  18.         Debug.Log("Player MaxHealth=" + m_characterData.MaxHealth);
  19.         m_characterData.MaxHealth = 200;
  20.         Debug.Log("Player MaxHealth=" + m_characterData.MaxHealth);
  21.     }
  22. }

第 18 至 20 行,我们编写了测试代码,测试数据的读写。如图 4 所示,结果符合预期。

图4 测试数据读写