元某人也元某人也

ScriptableObject:数据、事件与配置

SO 解决什么问题、什么时候不该用,以及用 SO 当事件通道的写法。

本文目录

SO

Script ableObject,继承自UnityEngine.ScriptableObject 的资源,可以像预制体和材质一样,保存在项目的Assets文件夹中,成为一个文件

核心作用:

把“数据”从逻辑代码中抽离出来,编程可以独立的可配置资源

作用

常用于配置游戏数据,技能Buff设计,事件通道

优点

1.内存优化,节省冗余:多个敌人使用同一份数据(比如“哥布林属性”),内存中只存在一份 SO 实例,而不是每个敌人身上挂一个重复脚本和一堆字段。

  1. 脱离场景,独立管理:SO 是项目资产,不依赖任何场景。即使场景被删除,数据依然保留,可以跨场景调用,也方便版本控制。

  2. 实时热重载与快速迭代:在运行模式下修改 SO 的数值,停止运行后修改仍然保留(这是很多开发者最爱的特性)。可以边运行边调参数,调好后数值不丢失。

  3. 引用强类型,不易出错:技能 SO 可以直接引用音效 SO、图标 Sprite 等。拖动引用比写字符串路径更安全。

缺点

  1. SO 不是 GameObject,无法像 MonoBehaviour 那样拥有 Transform、Collider 等组件,所以不能直接在物体上挂载组件。

  2. 无法绑定 UI 事件:按钮的 onClick 等事件只能指向场景中挂载了脚本的对象,不能直接指向 SO(需要中间脚本)。

  3. 容易产生大量小文件:如果不加节制(比如每一把武器、每一个敌人类型都做一个 SO),项目里会多出成百上千个 .asset 文件,管理起来很麻烦。

  4. 数据修改即全局影响:多个对象共享同一个 SO,若在运行时被代码修改了数据,所有使用该 SO 的对象都会受到影响。虽然提供了运行时复制机制(Instantiate),但会增加复杂度。

  5. 引用关系需要手动维护:SO 之间的引用如果丢失(移动文件等),需要逐个手动修复,大规模重构时可能较痛苦。

  6. 不适合频繁更改的动态数据:例如角色实时血量、当前位置等运行时状态,用普通变量就好,用 SO 反而不合适。

当SO充当事件通道

这就是事件通道:一个 ScriptableObject 作为管道,发者只管发,收者只管收,双方完全解耦。

SO文件充当的是中间的人的身份
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
// GameEventSO.cs
using UnityEngine;
using UnityEngine.Events;

[CreateAssetMenu(menuName = "Events/Game Event")]
public class GameEventSO : ScriptableObject
{
    // 可以携带参数的事件(传一个 GameObject 表示死者)
    [System.Serializable]
    public class UnityGameObjectEvent : UnityEvent<GameObject> { }

    public UnityGameObjectEvent OnRaise;

    public void Raise(GameObject sender)
    {
        OnRaise?.Invoke(sender);
    }
}
玩家脚本在需要触发时,把自身传出去
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// PlayerHealth.cs
using UnityEngine;

public class PlayerHealth : MonoBehaviour
{
    public GameEventSO playerDiedEvent;  // 引用 .asset

    public int health = 100;

    public void TakeDamage(int damage)
    {
        health -= damage;
        if (health <= 0)
        {
            Die();
        }
    }

    private void Die()
    {
        // 触发事件,把自身传出去
        playerDiedEvent.Raise(gameObject);
        // 可以在此处播放死亡动画等自身逻辑
    }
}
监听事件

可以传入多个系统,进行订阅

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// AudioManager.cs
using UnityEngine;

public class AudioManager : MonoBehaviour
{
    public GameEventSO playerDiedEvent;

    private void OnEnable()
    {
        playerDiedEvent.OnRaise.AddListener(OnPlayerDied);
    }

    private void OnDisable()
    {
        playerDiedEvent.OnRaise.RemoveListener(OnPlayerDied);
    }

    private void OnPlayerDied(GameObject player)
    {
        Debug.Log("播放死亡音效");
        // AudioSource.PlayClipAtPoint(...);
    }
}

SO事件相对于传统的事件系统,不需要手动的取消订阅事件,直接写在OnEnable()和 OnDisable()方法中能够在挂载物体销毁时,引用转为空

仅用于交流学习,记录学习心得