How to Develop a Game
ScriptableObject
PublishDate: 2025-06-01 | CreateDate: 2025-06-01 | LastModify: 2025-06-01 | Creator:ljf12825

ScriptableObject 是 Unity 中的一种特殊类型的对象,它是用于存储数据的,类似于普通的 C# 类,但它不需要与游戏对象(GameObject)关联

ScriptableObject 主要用于节省内存、提高性能和简化数据的管理。它通常用来存储可重用的数据,如配置、设置、状态信息等

基本概念

ScriptableObject 是 Unity 提供的一种特殊对象类型,允许你将数据持久化到磁盘上,并能够在编辑器中方便地进行编辑和管理。这与普通的 MonoBehaviour 类(需要附加到 GameObject 上)不同,ScriptableObject 并不依赖于场景中的任何对象

主要特点

创建和使用

  1. 创建ScriptableObject类 要创建 ScriptableObject,首先需要继承 ScriptableObject 基类,并为它定义一个静态方法来实例化对象
using UnityEngine;

[CreateAssetMenu(fileName = "NewCharacterData", menuName = "ScriptableObjects/CharacterData")]
public class CharacterData : ScriptableObject
{
    public string characterName;
    public int health;
    public float speed;
}

这个例子中,定义了一个CharacterData类,存储角色的数据(如名字、生命值和速度)

CreateAssetMenu特性使得可以通过右键点击创建该类型的ScriptableObject实例

  1. 创建实例 在Unity编辑器中,可以右键点击项目视图中的文件夹,并选择Create > ScriptableObjects > CharacterData,然后创建一个新的CharacterData实例。它将出现在项目文件中,并且可以像其他资源一样编辑

  2. 使用ScriptableObject 一旦创建了ScriptableObject实例,就可以通过代码引用它,或者将其赋值给其他对象的字段

using UnityEngine;

public class Character : MonoBehaviour
{
    public CharacterData characterData;

    void Start() => Debug.Log($"{characterData.Name}, {characterData.health}, {characterData.Speed}");
}

在这个示例中,Character脚本引用了一个CharacterData类型的字段,并在Start方法中打印出角色的数据。在Unity编辑器中,可以将创建的CharacterData实例拖动到Character脚本的characterData字段上

使用场景和优势

  1. 数据驱动设计 ScriptableObject非常适合用于数据驱动的设计模式,尤其是需要存储大量的静态数据时(例如游戏中的关卡配置、角色属性、武器属性等),使用ScriptableObject可以让你将这些数据与游戏逻辑分离,使其更加模块化和易于管理

  2. 节省内存 由于ScriptableObject实例是引用传递的,而不是每次都创建新对象,它可以显著减少内存开销。多个游戏对象可以共享同一个ScriptableObject实例,避免了每个对象都存储一份重复的数据

  3. 配置和设置 在游戏开发中,很多时候会需要管理一组设置,例如游戏难度、音效音量等,ScriptableObject可以很方便地存储这些配置,并且能在编辑器中直接查看和修改

  4. 序列化复杂数据结构 ScriptableObject支持序列化复杂的数据结构,包括数组、列表、字典等,可以轻松地管理这些数据

常见用法

  1. 创建游戏配置数据 ScriptableObject在游戏配置中非常常见
[CreateAssetMenu(fileName = "GameSettings", menuName = "ScriptableObjects/GameSettings")]
public class GameSettings : ScriptableObject
{
    public float musicVolume;
    public float sfxVolume;
    public bool isFullscreen;
}

然后可以在GameSettings资源中编辑这些值,或者通过脚本加载并应用它们

  1. 创建状态机(State Mechine) 每个状态可以是一个ScriptableObject实例
public abstract class State : ScriptableObject
{
    public abstract void Enter();
    public abstract void Exit();
}
  1. 对象池 在对象池模式中,ScriptableObject可以用来存储池中对象的配置和初始化数据,尤其是当希望将对象池的某些配置(例如对象的预设、初始化数量等)于对象池的管理逻辑分离时,这样做可以提高代码的可重用性、灵活性和维护性

通过将对象池的配置数据存储在ScriptableObject中,我们可以方便地管理和修改池的配置,且无需修改池的实现代码。这种方式将数据和逻辑分离,符合单一职责原则

创建配置的ScriptableObject

using UnityEngine;

[CreateAssetMenu(fileName = "ObjectPoolConfig", menuName = "ScriptableObjects/ObjectPoolConfig", order = 1)]
public class ObjectPoolConfig : ScriptableObject
{
    [Header("Pool Settings")]
    [SerializeField] private GameObject prefab; // 池中对象的预制体
    [SerializeField] private int initialSize = 10; // 初始池大小
    [SerializeField] private int maxSize = 20; // 池的最大容量

    public GameObject Prefab => prefab;
    public int InitialSize => initialSize;
    public int MaxSize => maxSize;
}

这个ScriptableObject负责存储对象池的配置信息,包括池中的对象预设、初始大小和最大容量

对象池实现

using System.Collections.Generic;
using UnityEngine;

public class ObjectPool : MonoBehaviour
{
    [serializeField] private ObjectPoolConfig poolConfig; // 引用配置文件
    private Queue<GameObject> pool = new Queue<GameObject>(); // 对象池

    // 初始化对象池
    public void Initialize()
    {
        for (int i = -; i < poolConfig.InitialSize; ++i)
        {
            GameObject obj = Instantiate(poolConfig.Prefab);
            obj.SetActive(false); // 对象默认不可见
            pool.Enqueue(obj);
        }
    }

    // 获取一个对象
    public GameObject GetObject()
    {
        if (pool.Count > 0)
        {
            GameObject obj = pool.Dequeue();
            obj.SetActive(true); // 激活对象
            return obj;
        }
        else if (pool.Count < poolConfig.MaxSize) // 超过池的最大容量时不再创建新对象
        {
            GameObject obj = Instantiate(poolConfig.Prefab);
            obj.SetActive(true);
            return obj;
        }
        else return null; // 如果池已满,返回null
    }

    // 回收对象
    public void ReturnObject(GameObject obj)
    {
        obj.SetActive(false); // 禁用对象
        pool.Enqueue(obj); // 放回池中
    }
}

这里,ObjectPool类引用了ObjectPoolConfig来获取对象池的配置信息,这样只需要通过编辑器调整ObjectPoolConfig的参数,就可以轻松控制对象池的行为

在Unity编辑器中,右键点击Assets目录,选择Create > ScriptableObjects > ObjectPoolConfig来创建一个新的ObjectPoolConfig文件,然后配置其中的属性

接着,将这个ObjectPoolConfig文件拖到ObjectPool脚本的poolConfig字段中,Unity就会在运行时使用这些配置来初始化对象池

优点

  1. 作为消息中介 ScriptableObject可以用作消息中介,作为不同模块之间的通信桥梁。这种方式可以有效地解耦各个模块,使它们不直接依赖于彼此,提高系统灵活性和可维护性

实现一个简单的游戏事件系统,允许不同的系统之间传递消息

自定义消息中介ScriptableObject\

using UnityEngine;
using UnityEngine.Events;

[CreateAssetMenu(fileName = "GameEvent", menuName = "ScriptableObjects/GameEvent", order = 2)]
public class GameEvent : ScriptableObject
{
    private readonly List<UnityAction> listeners = new List<UnityAction>();

    // 注册监听器
    public void RegisterListener(UnityAction listener) => listeners.Add(listener);

    // 移除监听器
    public void UnregisterListener(UnityAction listener) => listeners.Remove(listener);

    // 触发事件
    public void Raise() => foreach (var listener in listeners) listener.Invoke();
}

GameEvent作为消息中介,维护一个监听器列表。当事件触发时,它会通知所有注册的监听器。监听器可以是任何方法,只要它们符合UnityAction委托的签名
自其他脚本中注册和触发事件

using UnityEngine;

public class Player : MonoBehaviour
{
    [SerializeField] private GameEvent playerHurtEvent;

    public void TakeDamage(int damage)
    {
        // 角色受伤
        Debug.Log("Player took damage: " + damage);

        // 触发受伤事件
        playerHurtEvent.Raise();
    }
}

public class UIManager : MonoBehaviour
{
    [SerializeField] private GameEvent playerHurtEvent;

    void OnEnable() => playerHurtEvent.RegisterListener(OnPlayerHurt); // 注册监听事件

    void OnDisable() => playerHurtEvent.UnregisterListener(OnPlayerHurt); // 移除事件监听

    void OnPlayerHurt() => Debug.Log("Player hurt, update UI"); // 响应受伤事件
}

Player脚本中,角色受到伤害时触发playerHurtEvent事件,在UIManager中,注册了这个事件,并在事件触发时更新UI

优点

注意事项

API

ScriptableObject inherites from Object Implemented in UnityEngine.CoreModule

Static Methods

MethodDescription
CreateInstance创建实例

Message

MessageDescription
Awake当实例被创建时调用
OnDestroy当实例被销毁时调用
OnDisable当超出范围时调用
OnEnable当被加载时调用
OnValidate仅在编辑器状态下,当脚本被加载或值发生改变时调用
Reset恢复默认值

ScriptableObject设计原理

ScriptableObject的原理涉及Unity引擎中的几个关键概念:资源管理、序列化、以及数据共享

ScriptableObject是Unity引擎的资源系统的一部分

ScriptableObject继承自Unity的UnityEngine.Object,这是Unity中所有资源的基类;与普通的MonoBehaviour不同,ScriptableObject不直接绑定到GameObject上,它更像是一个独立的资源对象

资源管理

在Unity中,资源是通过 Asset Database来管理的,而ScriptableObject是其中的一部分。它可以在Project视图中作为资源文件存在,通常是.asset文件。这些资源通过Unity的资源管理系统进行管理,在加载和卸载时可以更高效地共享和复用

每个ScriptableObject实例实际上是一个持久化的资源文件,这些文件与场景分离,能够在场景之间进行共享,甚至能在多个项目之间共享

序列化

ScriptableObject 能够被 Unity 引擎 序列化。这意味着,它的字段可以被保存到磁盘上(比如 .asset 文件),并且可以通过 Unity 编辑器直接编辑和查看。Unity 通过其内置的序列化机制,使得 ScriptableObject 的数据可以在编辑器和运行时之间进行持久化存储。

与普通的 C# 类不同,ScriptableObject 对其字段的修改不需要手动管理存储或写入磁盘,它们会自动序列化到资源文件中。Unity 会根据字段类型将数据转化为可存储的形式(如整数、浮点数、字符串等),并且能够在资源文件中对这些数据进行持久化。

内存共享

Unity 内部采用了对象池的机制来管理 ScriptableObject 实例。多个 GameObjecMonoBehaviour 可以引用同一个 ScriptableObject 实例,而不需要为每个对象创建一个新的实例。这个共享机制显著降低了内存消耗,因为所有引用都指向同一个实例,而不是复制一份新的对象

ScriptableObject与单例模式

ScriptableObject在某些情况下可以代替单例模式,但它和传统的单例模式有一些关键的区别,适用场景也不同

相似性

区别

  1. 生命周期和资源管理
  1. 用途和场景
  1. 线程安全
  1. 序列化和编辑器功能

何时选择ScriptableObject

  1. 数据驱动的设计 如果需要在多个场景或多个对象之间共享数据,且这些数据不需要实时动态计算或修改,那么 ScriptableObject 是一个很好的选择。例如,角色配置、物品数据等,都可以使用 ScriptableObject 来代替单例模式

  2. 避免手动管理生命周期 ScriptableObject 会由 Unity 自动管理生命周期,无需手动控制它的创建和销毁。它的资源化特性非常适合于项目中需要持久化的共享数据

  3. 需要在编辑器中修改数据 如果数据需要频繁修改或调试,而不仅仅是运行时的数据,那么 ScriptableObject 会更加方便,因为它可以在 Unity 编辑器中直接编辑,并且会自动序列化为 .asset 文件进行保存

何时选择单例模式

  1. 需要单一实例控制的场景 如果类需要保持唯一性并且不希望被多个场景或多个对象引用(例如全局游戏管理器,音频管理器等),传统的单例模式更为合适

  2. 无法进行编辑和修改的场景 如果数据在运行时需要根据某些动态条件调整,ScriptableObject 就不太合适。它通常用于静态数据,而动态计算和状态管理可能更适合单例模式

ScriptableSingleton

ScriptableSingleton 是一种结合了 ScriptableObject 和单例模式的设计模式,它继承自 ScriptableObject 利用了 ScriptableObject 的资源管理和共享特性,同时确保数据只有一个实例,并且可以全局访问。这种模式在 Unity 开发中非常常见,特别是在需要保持全局唯一的数据管理对象时

概念

ScriptableSingleton 本质上是一个ScriptableObject,但是它确保在整个项目中只有一个实例,它的使用方式类似于传统的单例模式

关键特点:

实现

  1. 基础实现 创建一个继承自ScriptableObject的类,并在其中实现单例逻辑
using UnityEngine;

public class ScriptableSingleton<T> : ScriptableObject where T : ScriptableObject
{
    private static T _instance;

    // 保证在资源文件中只存在一个实例
    public static T Instance
    {
        get
        {
            if (_instance == null)
            {
                // 尝试从资源文件中加载该实例
                _instance = Resources.Load<T>(typeof(T).Name);
                if (_instance == null)
                {
                    // 如果不存在,创建新的实例并保存
                    _instance = CreateInstance<T>();
                    Debug.LogWarning($"Creating new instance of {typeof(T).Name}.");
                }
            }
            return _instance;
        }
    }
}

在这个基础实现中:

  1. 使用 当需要使用ScriptableSingleton时,只需创建一个继承自ScriptableSingleton的类
[CreateAssetMenu(fileName = "GameSettings", menuName = "ScriptableObjects/GameSettings")]
public class GameSettings : ScriptableSingleton<GameSettings>
{
    public float musicVolume;
    public float sfxVolume;
}

这样,GameSetting类就变成了一个ScriptableSingleton

API

Static Properties

PropertyDescription
instance获得单例的实例,当第一次使用这个属性时,Unity会创建这个单例的实例

Protected Methods

MethodDescription
Save保存当前单例的状态

Static Methods

MethodDescription
GetFilePath获得ScriptableSingleton的文件路径