简单介绍一下Unity中的ScriptableObject

ScriptableObject的本质

ScriptableObject是Unity引擎中的一个特殊基类,允许你创建不依附于游戏对象的数据容器,以资产(Asset)形式存储在项目中。这些资产:

  • 可在编辑器中创建和配置
  • 在构建后作为资产打包
  • 可通过Resources或AssetBundle加载
  • 不需要实例化即可访问其数据

ScriptableObject在内存管理上与MonoBehaviour根本不同 - 它们不绑定到场景层次结构,而是作为独立资产存在。

示例:

[CreateAssetMenu(fileName = "NewData", menuName = "Game/Data")]
public class GameData : ScriptableObject
{public string gameName;public float difficulty = 1.0f;public Color themeColor = Color.blue;
}

 


ScriptableObject与普通类的关键区别

特性ScriptableObject  普通C#类
序列化 Unity原生序列化需要自定义序列化
资产创建可在编辑器中创建和管理仅代码中实例化
Inspector支持完整Inspector支持需要自定义编辑器
引用持久性跨场景持久需要单例或静态变量
内存占用作为资产加载,共享实例每次实例化独立内存
预览能力可直接在编辑器查看/编辑运行时才能查看

ScriptableObject与普通类的优点比较

与普通的C#类相比,ScriptableObject在Unity开发中具有以下显著优势:

  1. 数据持久化
    ScriptableObject实例可以保存为.asset文件,成为项目中的资产。这种持久化特性允许数据在编辑器中保存并重复使用,而普通类的数据通常只能在运行时生成或从外部加载,无法直接在编辑器中管理。
  2. 编辑器集成
    ScriptableObject与Unity编辑器深度集成,开发者可以在Inspector窗口中创建、编辑和预览其实例。这种直观的界面非常适合非程序员(例如设计师或艺术家),让他们能够轻松调整游戏数据,而无需触碰代码。
  3. 内存效率
    ScriptableObject在运行时是共享的,多个游戏对象可以引用同一个ScriptableObject实例,共享其数据。这避免了重复创建相同数据的开销,特别适合处理大量相似对象时节省内存。而普通类的实例通常是独立的,每个实例都会占用额外的内存。
  4. 灵活性
    由于ScriptableObject支持序列化,它可以存储复杂的数据结构,例如列表、数组,甚至通过自定义序列化支持字典等。这为开发者设计数据模型提供了很大的灵活性。普通类虽然也能定义复杂数据,但缺乏与Unity编辑器的直接集成。
  5. 解耦合
    使用ScriptableObject可以将数据与逻辑分离,从而提高代码的模块化和可维护性。例如,角色的属性数据可以存储在ScriptableObject中,而角色的行为逻辑则由MonoBehaviour实现,两者独立且互不干扰。普通类则往往需要将数据和逻辑混在一起,容易导致代码耦合。

ScriptableObject的实际应用场景

1. 配置数据和游戏设置

ScriptableObject是存储各种游戏配置的理想选择:

[CreateAssetMenu(fileName = "GameSettings", menuName = "Game/Settings")]
public class GameSettings : ScriptableObject
{[Header("Game Balance")][Range(0.1f, 2f)]public float globalDifficultyMultiplier = 1f;[Header("Player Settings")]public float baseHealth = 100f;public float baseSpeed = 5f;[Header("Economy")]public int startingGold = 500;public AnimationCurve experienceCurve;[Header("Audio")][Range(0, 1)]public float musicVolume = 0.8f;[Range(0, 1)]public float sfxVolume = 1f;
}

这种方式允许策划直接在编辑器中调整游戏参数,而无需修改代码:

  • 可以创建多个配置变体(如Easy、Medium、Hard难度设置)
  • 便于调试和测试不同配置
  • 避免硬编码值,提高可维护性

2. 数据驱动设计

ScriptableObject是实现数据驱动设计的强大工具,特别适合RPG等数据密集型游戏:

[CreateAssetMenu(fileName = "NewItem", menuName = "Inventory/Item")]
public class ItemData : ScriptableObject
{public string itemName;public Sprite icon;[TextArea] public string description;public ItemType type;public ItemRarity rarity;public int maxStackSize = 1;public float weight = 0.1f;public List<ItemEffect> effects;public GameObject prefab;[Header("Equipment Properties")]public EquipmentSlot slot;public List<StatModifier> statModifiers;[Header("Vendor Properties")]public int baseBuyPrice;public int baseSellPrice;
}

优势:

  • 所有物品数据集中管理,易于平衡和调整
  • 策划可直接创建和修改物品,无需编程支持
  • 新物品添加不需要重新编译代码
  • 物品数据可预览,直接显示图标等视觉元素

3. 事件系统与架构解耦

ScriptableObject可以作为系统间通信的中介,创建松耦合架构:

[CreateAssetMenu(fileName = "GameEvent", menuName = "Events/Game Event")]
public class GameEvent : ScriptableObject
{private List<GameEventListener> listeners = new List<GameEventListener>();public void Raise(){// 从后向前遍历以支持监听器在回调中移除自身for (int i = listeners.Count - 1; i >= 0; i--){listeners[i].OnEventRaised();}}public void RegisterListener(GameEventListener listener){if (!listeners.Contains(listener))listeners.Add(listener);}public void UnregisterListener(GameEventListener listener){if (listeners.Contains(listener))listeners.Remove(listener);}
}

配合使用的组件:

public class GameEventListener : MonoBehaviour
{public GameEvent Event;public UnityEvent Response;private void OnEnable(){Event.RegisterListener(this);}private void OnDisable(){Event.UnregisterListener(this);}public void OnEventRaised(){Response.Invoke();}
}

这种架构优势:

  • 系统完全解耦,发送者不需要知道接收者
  • 事件可在编辑器中配置和连接
  • 可视化事件流,易于调试
  • 避免了单例或静态管理器的复杂性

4. 运行时数据管理与持久化

ScriptableObject可作为运行时数据容器,跨场景持久化数据:

[CreateAssetMenu(fileName = "PlayerProgress", menuName = "Game/Player Progress")]
public class PlayerProgress : ScriptableObject
{[Header("Character Stats")]public string playerName;public int level = 1;public float experience = 0;public int skillPoints = 0;[Header("Game Progress")]public List<string> unlockedLevels = new List<string>();public List<string> completedQuests = new List<string>();public Dictionary<string, bool> achievements = new Dictionary<string, bool>();[Header("Inventory")]public List<InventoryItem> inventory = new List<InventoryItem>();public int gold = 0;// 加载/保存方法public void SaveToPlayerPrefs(){// 序列化逻辑}public void LoadFromPlayerPrefs(){// 反序列化逻辑}// 重置数据方法public void ResetToDefaults(){level = 1;experience = 0;// 重置其他字段...}
}

使用示例:

public class GameManager : MonoBehaviour
{[SerializeField] private PlayerProgress playerProgress;private void Start(){// 加载存档playerProgress.LoadFromPlayerPrefs();}public void SaveGame(){playerProgress.SaveToPlayerPrefs();}// 在退出前自动保存private void OnApplicationQuit(){SaveGame();}
}

优势:

  • 数据自然在场景加载间保持
  • 单一数据源,避免数据冗余
  • 易于保存/加载
  • 在编辑器中可视化运行时状态

5. 基于原型的对象池

ScriptableObject可以作为预制体工厂,用于高效的对象生成:

[CreateAssetMenu(fileName = "EnemyPrototype", menuName = "Entities/Enemy Prototype")]
public class EnemyPrototype : ScriptableObject
{public string enemyName;public GameObject prefab;[Header("Core Stats")]public float health;public float damage;public float speed;[Header("Behavior")]public float aggroRange;public AIBehaviorType behaviorType;[Header("Rewards")]public int experienceReward;public ItemDropData[] possibleDrops;[Header("Pool Settings")]public int initialPoolSize = 5;public int maxPoolSize = 20;// 对象池引用(运行时)[HideInInspector] private Queue<GameObject> pool;public void InitializePool(){pool = new Queue<GameObject>();for (int i = 0; i < initialPoolSize; i++){CreateNewInstance();}}private GameObject CreateNewInstance(){GameObject instance = Instantiate(prefab);Enemy enemyComponent = instance.GetComponent<Enemy>();if (enemyComponent != null){// 配置敌人实例enemyComponent.Configure(this);}instance.SetActive(false);pool.Enqueue(instance);return instance;}public GameObject GetInstance(Vector3 position, Quaternion rotation){if (pool == null)InitializePool();GameObject instance = pool.Count > 0 ? pool.Dequeue() : (pool.Count < maxPoolSize ? CreateNewInstance() : null);if (instance != null){instance.transform.position = position;instance.transform.rotation = rotation;instance.SetActive(true);}return instance;}public void ReturnInstance(GameObject instance){instance.SetActive(false);pool.Enqueue(instance);}
}

优势:

  • 集中管理对象创建逻辑
  • 减少重复代码
  • 池配置与对象定义合并
  • 便于平衡和调整

6. 面向数据的能力和技能系统

ScriptableObject非常适合定义可组合的技能和能力:

[CreateAssetMenu(fileName = "NewAbility", menuName = "Abilities/Ability")]
public class Ability : ScriptableObject
{public string abilityName;public Sprite icon;[TextArea] public string description;public float cooldown = 1f;public float castTime = 0.5f;public AbilityType type;[Header("Resource Cost")]public ResourceType costType;public float costAmount;[Header("Effects")]public List<AbilityEffect> effects;[Header("Visual & Audio")]public GameObject castVFX;public AudioClip castSFX;// 技能逻辑public virtual bool CanUse(CharacterStats stats){return stats.GetResource(costType) >= costAmount;}public virtual void Use(CharacterStats stats, Transform target){if (!CanUse(stats))return;stats.ConsumeResource(costType, costAmount);// 应用效果foreach (var effect in effects){effect.Apply(stats, target);}// 播放视觉和音效if (castVFX != null)Instantiate(castVFX, target.position, Quaternion.identity);if (castSFX != null)AudioManager.Instance.PlaySound(castSFX);}
}// 技能效果基类
[System.Serializable]
public abstract class AbilityEffect
{public abstract void Apply(CharacterStats source, Transform target);
}// 具体效果示例
[CreateAssetMenu(fileName = "DamageEffect", menuName = "Abilities/Effects/Damage")]
public class DamageEffect : AbilityEffect
{public DamageType damageType;public float baseDamage;public float statMultiplier = 1f;public StatType scalingStat = StatType.Strength;public override void Apply(CharacterStats source, Transform target){Damageable targetDamageable = target.GetComponent<Damageable>();if (targetDamageable == null)return;float statValue = source.GetStatValue(scalingStat);float totalDamage = baseDamage + (statValue * statMultiplier);targetDamageable.TakeDamage(new DamageInfo{amount = totalDamage,type = damageType,source = source.gameObject});}
}

优势:

  • 可直观组合技能效果
  • 非程序员可创建新技能
  • 运行时技能效果可动态添加/移除
  • 技能数据便于平衡

ScriptableObject的其他应用

变量引用架构

ScriptableObject可用于创建高度灵活的变量引用系统:

[CreateAssetMenu(fileName = "FloatVariable", menuName = "Variables/Float")]
public class FloatVariable : ScriptableObject
{[SerializeField] private float value;public float Value{get => value;set => this.value = value;}public void SetValue(float value){this.value = value;}public void ApplyChange(float amount){value += amount;}
}// 引用器,可选择使用常量或变量引用
[System.Serializable]
public class FloatReference
{public bool useConstant = true;public float constantValue;public FloatVariable variable;public float Value => useConstant ? constantValue : variable.Value;
}

使用示例:

public class Character : MonoBehaviour
{// 可以在Inspector中选择使用常量或变量引用public FloatReference moveSpeed;public FloatReference maxHealth;// 使用共享变量作为观察者public FloatVariable currentHealth;private void Update(){float speed = moveSpeed.Value;// 使用速度值...}public void TakeDamage(float damage){currentHealth.ApplyChange(-damage);if (currentHealth.Value <= 0){Die();}}
}

这种架构实现了:

  • 数据与逻辑解耦
  • 灵活选择内联值或共享变量
  • 简化数据绑定
  • 便于调试共享数据

状态机数据模型

ScriptableObject非常适合定义AI行为状态:

[CreateAssetMenu(fileName = "AIState", menuName = "AI/State")]
public class AIState : ScriptableObject
{[TextArea] public string stateDescription;[Header("Transitions")]public List<AIStateTransition> transitions;[Header("Behaviors")]public List<AIBehavior> behaviors;// 状态进入/退出逻辑public virtual void OnEnter(AIController controller) {foreach (var behavior in behaviors){behavior.OnEnter(controller);}}public virtual void OnUpdate(AIController controller){// 执行行为foreach (var behavior in behaviors){behavior.OnUpdate(controller);}// 检查转换条件foreach (var transition in transitions){if (transition.CanTransition(controller)){controller.ChangeState(transition.targetState);break;}}}public virtual void OnExit(AIController controller){foreach (var behavior in behaviors){behavior.OnExit(controller);}}
}// 行为基类
[System.Serializable]
public abstract class AIBehavior
{public virtual void OnEnter(AIController controller) { }public abstract void OnUpdate(AIController controller);public virtual void OnExit(AIController controller) { }
}

这种方式让AI行为变得可视化和可编辑,极大提高了设计迭代速度。


ScriptableObject的局限性与注意事项

尽管ScriptableObject非常强大,但也有一些局限性需要注意:

1. 运行时修改问题

在运行时对ScriptableObject的修改在编辑器模式下会持久化,这可能导致意外结果:

// 解决方案:在开始时重置到默认值
private void OnEnable()
{hideFlags = HideFlags.DontSave;  // 阻止运行时更改保存ResetToDefaultValues();
}// 或创建运行时副本
[RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.BeforeSceneLoad)]
private static void CreateRuntimeInstances()
{var settings = Instantiate(Resources.Load<GameSettings>("DefaultGameSettings"));GameSettingsManager.Instance.currentSettings = settings;
}

2. 序列化限制

ScriptableObject使用Unity序列化系统,有一些限制:

  • 不支持多态序列化(需要自定义编辑器或序列化器)
  • 不能直接序列化接口
  • 不能序列化泛型集合(需要使用SerializableHashSet等辅助类)

3. 内存管理

ScriptableObject作为资产加载,其生命周期与一般游戏对象不同:

  • 在编辑器中可能不会被卸载
  • 在运行时调用Resources.UnloadUnusedAssets()才会卸载
  • 大型ScriptableObject资产应考虑按需加载/卸载

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.rhkb.cn/news/42279.html

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

ubuntu24.04.2 NVIDIA GeForce RTX 4060笔记本安装驱动

https://www.nvidia.cn/drivers/details/242281/ 上面是下载地址 sudo chmod x NVIDIA-Linux-x86_64-570.133.07.run # 赋予执行权限把下载的驱动复制到家目录下&#xff0c;基本工具准备&#xff0c;如下 sudo apt update sudo apt install build-essential libglvnd-dev …

LabVIEW 布尔控件回车键触发程序退出

在 LabVIEW 开发过程中&#xff0c;部分用户可能会遇到按下回车键&#xff08;Enter&#xff09;后&#xff0c;程序意外退出的问题。该问题主要源于布尔控件的属性设置冲突&#xff0c;包括键分配、数据绑定及 Tab 键行为等。本文将详细分析问题根源&#xff0c;并提供一套完整…

分布式系统面试总结:3、分布式锁(和本地锁的区别、特点、常见实现方案)

仅供自学回顾使用&#xff0c;请支持javaGuide原版书籍。 本篇文章涉及到的分布式锁&#xff0c;在本人其他文章中也有涉及。 《JUC&#xff1a;三、两阶段终止模式、死锁的jconsole检测、乐观锁&#xff08;版本号机制CAS实现&#xff09;悲观锁》&#xff1a;https://blog.…

WebWorkers在项目中的使用案例

Worker | 文档 worker 线程的关闭在主线程和 worker 线程都能进行操作&#xff0c;但对 worker 线程的影响略有不同。 // main.js&#xff08;主线程&#xff09; const myWorker new Worker(/worker.js); // 创建worker myWorker.terminate(); // 关闭worker 复制代码 // wor…

vue ts+Windi CSS

1、创建vue项目 trae&#xff08;字节&#xff09;打开一个空文件夹 npm install -g vue/cli vue create my-project cd my-project vue add typescript npm run serve vue项目创建完成 2、安装windicss vue add windicss vue.config.js配置 npm install vue-router …

【HTML 基础教程】HTML 编辑器

HTML 编辑器推荐 可以使用专业的 HTML 编辑器来编辑 HTML&#xff0c;菜鸟教程为大家推荐几款常用的编辑器&#xff1a; VS Code&#xff1a;Visual Studio Code - Code Editing. RedefinedSublime Text&#xff1a;http://www.sublimetext.com/在线编辑器&#xff1a;HTML/C…

文件上传的小点总结(2)

4.黑名单绕过(.htaccess方法) 源码一打开&#xff0c;遇到这样的黑名单是不是看的头皮发麻&#xff0c;这么多后缀都禁用。 .htaccess可以启用或禁用apache的功能&#xff0c;利用这个特点&#xff0c;我们可以使用该文件来禁用上述黑名单功能&#xff0c;从而上传**文件。 简…

mysql--主从复制--部署

MySQL 主从复制部署教程 一、主节点&#xff08;Master&#xff09;配置 1. 创建目录结构 mkdir -p /usr/local/src/mysql_demo/master_replica/{logs,configFile,data}2. 编写主节点的 MySQL 配置文件 my.cnf 路径&#xff1a;/usr/local/src/mysql_demo/master_replica/co…

Qt弹出新窗口并关闭(一个按钮)

参考&#xff1a;Qt基础 练习&#xff1a;弹出新窗口并关闭的两种实现方式&#xff08;两个按钮、一个按钮&#xff09;_qt打开一个窗口另一个关闭-CSDN博客 实现&#xff1a; 一个按钮&#xff0c;点击一次&#xff0c;按钮的名字从open window变为close window&#xff0c;…

游戏引擎学习第185天

回顾并计划今天的内容 我们完成了开始整理这些数据的工作&#xff0c;但我们还没有机会真正去查看这些数据的具体内容&#xff0c;因为我们只是刚刚开始了数据整理的基本工作。我们收集了大量的信息&#xff0c;但到目前为止&#xff0c;仍然没有足够的可视化工具来帮助我们理…

《一本书讲透Elasticsearch:原理、进阶与工程实践》读书笔记

1&#xff1a;es的组成部分&#xff1a; Elasticsearch 引擎&#xff1a;核心组件&#xff0c;处理索引和搜索请求 Kibana&#xff1a;es的可视化的数据界面&#xff0c;用于分析和展示数据 Beats&#xff08;可选&#xff09;轻量级的日志采集器 2&#xff1a;基本概念 es开…

[React 进阶系列] 组合组件 复合组件

[React 进阶系列] 组合组件 & 复合组件 今天写个人项目练手的时候搜到了一个比价有趣的实现&#xff0c;于是用了一下&#xff0c;发现这个 concept 不是特别的熟&#xff0c;于是上网找了下&#xff0c;返现了一个叫 复合组件(compound components) 的概念。搜索了一下后…

HarmonyOS NEXT 鸿蒙中关系型数据库@ohos.data.relationalStore API 9+

核心API ohos.data.relationalStore API 9 数据库 数据库是存储和管理数据的系统 数据库&#xff08;Database&#xff09;是一个以特定方式组织、存储和管理数据的集合&#xff0c;通常用于支持各种应用程序和系统的运行。它不仅是存放数据的仓库&#xff0c;还通过一定的…

用HTML和CSS生成炫光动画卡片

这个效果结合了渐变、旋转和悬浮效果的炫酷动画示例&#xff0c;使用HTML和CSS实现。 一、效果 二、实现 代码如下&#xff1a; <!DOCTYPE html> <html lang"en"> <head><meta charset"UTF-8"><meta name"viewport&quo…

蓝桥杯第10届 后缀表达式

题目描述 给定 N 个加号、M 个减号以及 NM1 个整数 A1,A2,⋅⋅⋅,ANM1​&#xff0c;小明想知道在所有由这N 个加号、M 个减号以及 NM1 个整数凑出的合法的 后缀表达式中&#xff0c;结果最大的是哪一个&#xff1f; 请你输出这个最大的结果。 例如使用 1 2 3 -&#xff0c…

常见框架漏洞攻略-ThinkPHP篇

漏洞名称&#xff1a;Thinkphp5x远程命令执行及getshell 第一步&#xff1a;开启靶场 第二步&#xff1a;准备工具 第三步&#xff1a;启动工具&#xff0c;进行漏洞检测 #存在漏洞 1.目标存在tp5_invoke_func_code_exec_1漏洞2.目标存在tp5_dbinfo_leak漏洞payload:http://47…

sql长时间卡在gc current request事件

问题描述 凌晨跑批出现超时。SQL f0ng33agbpzhs业务需要执行160w次左右。现场人员杀掉该sql&#xff0c;重新发起业务&#xff0c;业务批次成功跑完。 问题分析 总体sql分析 分析对比sql的awrsqrpt&#xff0c;对比昨天3月8日的。 总体执行次数没有变化。Cpu时间、物理读等均…

MOSN(Modular Open Smart Network)-04-TLS 安全链路

前言 大家好&#xff0c;我是老马。 sofastack 其实出来很久了&#xff0c;第一次应该是在 2022 年左右开始关注&#xff0c;但是一直没有深入研究。 最近想学习一下 SOFA 对于生态的设计和思考。 sofaboot 系列 SOFAStack-00-sofa 技术栈概览 MOSN&#xff08;Modular O…

使用 Python 开发 MCP Server 及 Inspector 工具详解

使用 Python 开发 MCP Server 及 Inspector 工具详解 前言 模型上下文协议 (Model Context Protocol, MCP) 是一种新兴的协议&#xff0c;旨在让大型语言模型 (LLM) 更容易地与外部工具和服务集成。本文将介绍如何使用 Python 开发一个 MCP Server&#xff0c;并详细讲解如何使…

深入剖析 IS - IS 路由协议的原理、配置及与 OSPF 的对比

目录 ISIS概述 NSAP&#xff08;类似于IP地址&#xff09; NET NET配置举例 IS-IS 和OSPF区域划分的区别 区域和区域的分界点 IS-IS路由器的分类 Level-1路由器 Level-2路由器 Level-1-2路由器 ISIS支持的网络类型 ISIS开销值 IS-IS报文格式 IS-IS报文类型概述…