Unity运行时模型动态加载、编辑与状态持久化全流程实战
2026/8/6 5:39:20 网站建设 项目流程

1. 项目概述与核心价值

最近在做一个工业仿真或者数字孪生类的项目,经常遇到一个头疼的问题:客户或者上游部门给过来的模型文件,比如FBX、OBJ,需要在程序运行时动态加载进来,并且允许用户直接在这个“沙盒”里调整模型的位置、旋转、缩放,甚至还要能编辑碰撞体信息,最后还得把修改后的状态(包括这些变换信息和碰撞体设置)保存下来,下次打开时能原样恢复。Unity编辑器里做这些事当然方便,拖拖拽拽、Inspector里点点就行,但到了运行时,尤其是需要用户自定义配置的场景,就完全不是一回事了。

这个需求的核心,说白了就是实现一个运行时(Runtime)的、轻量级的“模型装配与配置工具”。它不依赖编辑器操作,完全通过C#代码驱动,能够解析模型文件、实例化、提供UI或逻辑接口供交互、并序列化关键数据。这不仅仅是加载一个Prefab那么简单,它涉及到对GameObject底层组件(Transform、Collider)的精确控制,以及对自定义数据的持久化。无论是用于教育培训软件让学员自己摆放设备,还是用于展示系统让用户自定义展厅布局,亦或是游戏里的地图编辑器,这个功能都非常实用。

网上关于运行时加载模型的资料不少,但往往只讲到AssetBundle加载或者Resources.Load为止。关于如何系统性地编辑并保存一个模型的完整状态信息(尤其是包含自定义的碰撞体配置),成体系的、即拿即用的解决方案并不多。很多开发者需要自己摸索,踩不少坑。所以,我把自己在实际项目中打磨出来的一套方案整理出来,包含完整的思路、关键代码和避坑指南,希望能帮你快速搞定这个功能。

2. 整体架构设计与思路拆解

要实现“导入、编辑、保存”这个闭环,我们不能只盯着加载模型那一步。需要从数据流的角度,自上而下地设计整个系统。核心思路可以分解为四个层次:数据层、加载层、编辑层、持久化层

2.1 数据层:定义我们要保存什么

这是最容易忽略但最关键的一步。我们到底要保存模型的哪些信息?仅仅是位置、旋转、缩放吗?对于运行时导入的模型,这远远不够。

首先,位置、旋转、缩放(PRS)这些属于Transform组件的信息,是必须保存的。它们决定了模型在场景中的姿态。

其次,碰撞体信息就复杂了。一个模型上可能有多个碰撞体,类型可能是BoxColliderSphereColliderMeshCollider等。每种碰撞体需要保存的参数完全不同。例如,BoxCollider需要中心点(Center)和大小(Size),MeshCollider则需要知道它是否开启凸包(Convex)、是否是触发器(IsTrigger)以及引用的网格(Mesh)。而网格本身可能又是运行时生成的或者是模型自带的。

因此,我们需要设计一个可扩展的数据结构(ModelData)来封装这些信息。我的做法是使用ScriptableObject或者一个纯粹的C#类([System.Serializable])来定义这个数据结构。它应该包含:

  • 模型文件的唯一标识(如路径、GUID或文件名)。
  • 一个Vector3存储位置,一个Quaternion存储旋转,一个Vector3存储缩放。
  • 一个碰撞体数据列表(List<ColliderData>),其中ColliderData是一个基类或接口,派生出BoxColliderDataSphereColliderDataMeshColliderData等,用[SerializeReference]特性来支持多态序列化(如果你用JsonUtility,需要注意其局限性,后文会详述)。

注意:这里有一个重要的设计抉择。我们保存的“碰撞体信息”,是指碰撞体组件的配置参数,而不是对场景中某个具体Collider组件实例的引用。因为下次运行时,场景是空的,我们需要根据这些参数重新创建出功能完全一致的Collider组件。

2.2 加载层:运行时如何把模型文件变成GameObject

Unity运行时加载外部模型文件,主流且灵活的方式是通过AssetBundle。但对于开发阶段快速测试,或者某些特定平台(如PC独立程序),我们也可以考虑直接读取模型文件(如FBX)并使用第三方库(如AssimpNet)解析,但这非常复杂且性能开销大,通常不推荐。

更实用的方案是预加工:在编辑阶段,将需要用到的模型文件打包成AssetBundle。运行时,通过AssetBundle.LoadFromFile或从服务器下载后加载。加载后得到的是一个GameObject预制体(Prefab),然后我们使用Instantiate方法将其实例化到当前场景。

加载层的关键任务是将实例化后的GameObject与我们定义的ModelData关联起来。我们需要一个管理器(ModelManagerModelEntity)脚本来挂载在实例化的物体上,这个脚本负责:

  1. 持有对该物体对应的ModelData的引用。
  2. 提供接口,将ModelData中的数据(PRS)应用到物体的Transform上。
  3. 根据ModelData中的碰撞体数据列表,动态地为物体添加或配置相应的Collider组件。

2.3 编辑层:如何提供编辑能力

编辑能力可以通过多种方式提供:

  • UI驱动:在UI界面上提供输入框、滑块来修改位置、旋转、缩放的数值。当数值改变时,调用管理器脚本上的方法,同步更新场景中物体的Transform,并更新内存中的ModelData
  • 交互驱动:更直观的方式是让用户直接在场景中通过鼠标拖拽、旋转、缩放物体(类似于编辑器的操作模式)。这需要编写一套简单的运行时Gizmo或手柄(Handle)系统,或者利用一些第三方运行时交互插件。当交互结束时,将物体Transform的当前值写回到ModelData中。
  • 碰撞体编辑:这部分通常通过UI完成。例如,提供一个列表显示当前模型的所有碰撞体,点击后可以修改其类型、尺寸、是否触发器等属性。修改后,需要销毁旧的碰撞体组件,并根据新的数据重新创建和配置。

2.4 持久化层:如何保存与读取

这是闭环的最后一步。我们需要将内存中的ModelData对象(可能是一个列表,保存了场景中所有模型的信息)序列化成字符串(如JSON或二进制),然后保存到硬盘(如Application.persistentDataPath下的一个文件)或上传到服务器。

序列化方案选择

  • JsonUtility:Unity内置,轻量,但功能较弱。最大的问题是默认不支持多态序列化(即前面提到的List<ColliderData>里存放各种派生类)。需要额外的工作,比如使用Type字段配合自定义转换器。
  • Newtonsoft.Json (Json.NET):功能强大,完美支持多态序列化、忽略默认值等,需要通过NuGet或Unity Package Manager安装。是当前更推荐的选择。
  • BinaryFormatter:Unity旧版常用,但存在安全漏洞和版本兼容性问题,官方已不推荐用于长期存储。
  • 自定义二进制格式:性能最优,但开发成本高。

我个人的选择是Json.NET,它在功能性和开发效率上取得了很好的平衡。保存时,将整个ModelData列表序列化成JSON字符串,写入文件。加载时,读取文件字符串,反序列化回ModelData列表,然后交给加载层去重新构建整个场景。

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

接下来,我们深入到代码层面,看看各个核心模块如何实现。我会提供关键代码片段,并解释其背后的逻辑和注意事项。

3.1 数据模型定义

首先,定义核心的数据结构。这里我们使用[System.Serializable]来让它们可被Unity序列化(方便在Inspector中调试),并使用Json.NET的[JsonProperty]特性来精细控制JSON输出。

using System; using System.Collections.Generic; using Newtonsoft.Json; using UnityEngine; // 变换数据 [System.Serializable] public class TransformData { public Vector3 position; public Quaternion rotation; public Vector3 scale; public TransformData() { } public TransformData(Transform transform) { this.position = transform.position; this.rotation = transform.rotation; this.scale = transform.localScale; } public void ApplyTo(Transform transform) { transform.position = position; transform.rotation = rotation; transform.localScale = scale; } } // 碰撞体数据基类 [System.Serializable] [JsonConverter(typeof(ColliderDataConverter))] // 使用自定义转换器处理多态 public abstract class ColliderData { public bool isTrigger; public PhysicMaterial physicMaterial; // 注意:物理材质是Asset,需要特殊处理引用 public abstract void ApplyTo(GameObject targetGameObject); } // 盒子碰撞体数据 [System.Serializable] public class BoxColliderData : ColliderData { public Vector3 center; public Vector3 size; public override void ApplyTo(GameObject targetGameObject) { var collider = targetGameObject.AddComponent<BoxCollider>(); collider.isTrigger = isTrigger; collider.sharedMaterial = physicMaterial; collider.center = center; collider.size = size; } } // 网格碰撞体数据 (这是重点和难点) [System.Serializable] public class MeshColliderData : ColliderData { public bool convex = false; // 是否开启凸包 public Mesh mesh; // 引用的网格!这是关键,如何保存? public override void ApplyTo(GameObject targetGameObject) { var collider = targetGameObject.AddComponent<MeshCollider>(); collider.isTrigger = isTrigger; collider.sharedMaterial = physicMaterial; collider.convex = convex; collider.sharedMesh = mesh; // 这里需要mesh是有效的 } } // 主模型数据 [System.Serializable] public class ModelData { public string assetBundleName; // 标识从哪个AB加载 public string assetName; // 资源在AB中的名称 public TransformData transformData = new TransformData(); public List<ColliderData> colliderDataList = new List<ColliderData>(); }

关键难点与解决方案:MeshColliderData中的Mesh引用这是整个系统最棘手的地方。Mesh是一个Unity引擎对象(UnityEngine.Object),直接序列化到JSON只会保存一个实例ID,运行时这个ID是无效的。我们必须解决网格资产的持久化问题。有几种思路:

  1. 方案A:假设网格来自原始模型。如果碰撞体使用的网格就是模型文件自带的网格(比如MeshFilter.sharedMesh),那么我们只需要记录使用的是哪个子网格的索引。加载模型后,通过GetComponent<MeshFilter>().sharedMesh来获取。这要求模型在导入设置中“Read/Write Enabled”必须打开。
  2. 方案B:运行时生成网格。如果碰撞体是程序化生成的(如一个简化的凸包),那么我们需要将网格数据(顶点、三角形)序列化保存。可以在MeshColliderData中添加List<Vector3> verticesList<int> triangles字段。ApplyTo时,动态创建一个新的Mesh对象并赋值顶点和三角形数组,然后赋给MeshCollider.sharedMesh务必注意:动态创建的Mesh,其verticestriangles数组在赋值后可以调用mesh.UploadMeshData(true)来标记为不再可写以优化性能,但如果你后续还需要修改,就不能这么做。
  3. 方案C:将网格作为独立资产打包。将用于碰撞的网格单独制作成Prefab或Mesh资产,打入AssetBundle。保存时只保存该网格资产的路径或GUID。加载时,先加载网格资产,再赋值。

对于大多数“编辑并保存模型状态”的需求,方案A(引用模型自身网格)是最常见和合理的。因此,我们的MeshColliderData可以改为存储一个int subMeshIndex字段,并在ApplyTo方法中通过targetGameObject.GetComponent<MeshFilter>().sharedMesh(或遍历所有MeshFilter)来获取对应网格。但这里有一个巨大隐患:一个复杂的FBX模型可能包含多个子网格(SubMesh),对应多个材质。MeshCollider默认使用整个模型的合并网格,还是某个子网格?这需要根据你的项目需求明确。为了通用性,我们的实现将支持记录并应用多个碰撞体,每个碰撞体可以指定不同的源网格。

3.2 模型加载与实体管理

我们创建一个ModelEntity类,将其挂载到每个动态加载的模型实例上,负责该模型的数据绑定和状态应用。

using UnityEngine; public class ModelEntity : MonoBehaviour { public ModelData data; // 关联的数据 private void Start() { // 如果启动时已有数据,则应用数据(如从保存文件加载后) if (data != null) { ApplyData(); } } // 从数据应用到当前GameObject public void ApplyData() { if (data == null) return; // 1. 应用变换 data.transformData.ApplyTo(this.transform); // 2. 清除现有碰撞体(可选,根据需求) var existingColliders = GetComponents<Collider>(); foreach (var col in existingColliders) { Destroy(col); } // 3. 应用碰撞体数据 foreach (var colliderData in data.colliderDataList) { colliderData.ApplyTo(this.gameObject); } } // 从当前GameObject状态更新数据(用于保存) public void UpdateData() { if (data == null) data = new ModelData(); // 1. 更新变换数据 data.transformData = new TransformData(this.transform); // 2. 更新碰撞体数据 - 这里需要根据现有Collider组件重新生成ColliderData列表 data.colliderDataList.Clear(); var colliders = GetComponents<Collider>(); foreach (var col in colliders) { ColliderData colData = null; if (col is BoxCollider boxCol) { colData = new BoxColliderData { isTrigger = boxCol.isTrigger, physicMaterial = boxCol.sharedMaterial, center = boxCol.center, size = boxCol.size }; } else if (col is MeshCollider meshCol) { // 注意:这里我们保存的是Mesh引用。实际项目中,可能需要转换为方案A/B/C colData = new MeshColliderData { isTrigger = meshCol.isTrigger, physicMaterial = meshCol.sharedMaterial, convex = meshCol.convex, mesh = meshCol.sharedMesh // 直接保存引用,仅当mesh是持久化资源时有效 }; } // ... 其他Collider类型 if (colData != null) { data.colliderDataList.Add(colData); } } } }

3.3 AssetBundle的运行时加载

这是将模型文件“导入”到运行时的核心步骤。我们通常有一个专门的加载服务。

using System.Collections.Generic; using UnityEngine; public class RuntimeModelLoader : MonoBehaviour { private Dictionary<string, AssetBundle> _loadedBundles = new Dictionary<string, AssetBundle>(); // 加载一个模型并实例化,同时关联数据 public GameObject LoadModelAndCreateEntity(string bundleName, string assetName, ModelData existingData = null) { // 1. 加载或获取AssetBundle if (!_loadedBundles.ContainsKey(bundleName)) { // 假设AssetBundle放在StreamingAssets或PersistentDataPath下 string path = System.IO.Path.Combine(Application.streamingAssetsPath, bundleName); var bundle = AssetBundle.LoadFromFile(path); if (bundle == null) { Debug.LogError($"Failed to load AssetBundle: {bundleName}"); return null; } _loadedBundles[bundleName] = bundle; } // 2. 从Bundle中加载预制体 var prefab = _loadedBundles[bundleName].LoadAsset<GameObject>(assetName); if (prefab == null) { Debug.LogError($"Asset {assetName} not found in bundle {bundleName}"); return null; } // 3. 实例化 GameObject instance = Instantiate(prefab); // 4. 添加并配置ModelEntity ModelEntity entity = instance.AddComponent<ModelEntity>(); if (existingData != null) { // 如果提供了现有数据(如从保存文件读取),则关联并应用 entity.data = existingData; entity.ApplyData(); } else { // 否则,创建新的空数据,并用当前状态初始化它 entity.data = new ModelData { assetBundleName = bundleName, assetName = assetName }; entity.UpdateData(); // 用初始状态填充数据 } return instance; } private void OnDestroy() { // 清理时卸载所有AssetBundle foreach (var bundle in _loadedBundles.Values) { bundle.Unload(true); } _loadedBundles.Clear(); } }

3.4 数据的持久化(保存与加载)

使用Json.NET进行序列化。我们需要处理Mesh这样的特殊对象引用。这里采用一个折中方案:对于MeshColliderData,我们只保存一个meshAssetPath字符串,在应用时尝试从已加载的资源中查找。

using System.IO; using Newtonsoft.Json; using UnityEngine; public class ModelDataManager : MonoBehaviour { public List<ModelEntity> allModelEntities = new List<ModelEntity>(); private string _saveFilePath; void Awake() { _saveFilePath = Path.Combine(Application.persistentDataPath, "SceneLayout.json"); } // 保存所有模型数据 public void SaveAll() { List<ModelData> allData = new List<ModelData>(); foreach (var entity in allModelEntities) { entity.UpdateData(); // 确保数据是最新的 allData.Add(entity.data); } var settings = new JsonSerializerSettings { ReferenceLoopHandling = ReferenceLoopHandling.Ignore, Formatting = Formatting.Indented, // 需要为UnityEngine.Object(如Mesh)编写自定义的JsonConverter Converters = new List<JsonConverter> { new UnityObjectConverter() } }; string json = JsonConvert.SerializeObject(allData, settings); File.WriteAllText(_saveFilePath, json); Debug.Log($"场景布局已保存至: {_saveFilePath}"); } // 加载所有模型数据并重建场景 public void LoadAll() { if (!File.Exists(_saveFilePath)) { Debug.LogWarning("未找到保存文件。"); return; } string json = File.ReadAllText(_saveFilePath); var settings = new JsonSerializerSettings { Converters = new List<JsonConverter> { new UnityObjectConverter() } }; List<ModelData> allData = JsonConvert.DeserializeObject<List<ModelData>>(json, settings); // 先清空当前场景中的动态模型(根据实际情况) foreach (var entity in allModelEntities) { Destroy(entity.gameObject); } allModelEntities.Clear(); // 通过加载器重新创建每个模型 RuntimeModelLoader loader = GetComponent<RuntimeModelLoader>(); foreach (var data in allData) { GameObject go = loader.LoadModelAndCreateEntity(data.assetBundleName, data.assetName, data); if (go != null) { allModelEntities.Add(go.GetComponent<ModelEntity>()); } } } } // 一个简单的Unity对象转换器示例(复杂情况需要更完善的实现) public class UnityObjectConverter : JsonConverter<UnityEngine.Object> { public override UnityEngine.Object ReadJson(JsonReader reader, System.Type objectType, UnityEngine.Object existingValue, bool hasExistingValue, JsonSerializer serializer) { // 简化处理:这里只读回路径,实际加载逻辑应在别处处理 string path = reader.Value as string; return null; // 实际项目中,这里需要根据path去加载资源 } public override void WriteJson(JsonWriter writer, UnityEngine.Object value, JsonSerializer serializer) { // 简化处理:如果是持久化资源,保存其资源路径 if (value != null) { writer.WriteValue(UnityEditor.AssetDatabase.GetAssetPath(value)); // 仅编辑器下有效 // 运行时需要其他方式获取资源标识,如Addressables的Address或自定义ID } else { writer.WriteNull(); } } }

4. 关键难点、避坑指南与性能优化

实现这个功能的过程中,我踩过不少坑。下面把这些经验教训总结出来,希望能帮你绕开这些陷阱。

4.1 碰撞体网格的持久化陷阱

正如前面提到的,MeshCollidersharedMesh引用是最大的难题。如果你的碰撞体使用的是模型自带的网格,请务必在模型导入设置中勾选“Read/Write Enabled”。否则,在运行时尝试获取mesh.vertices或为MeshCollider赋值一个动态创建的网格时会失败。

最佳实践建议

  • 分离碰撞网格:对于复杂的静态模型,不要直接用高模网格做MeshCollider。应该在3D建模软件中创建一个简化的、用于碰撞的低模,并作为独立的网格文件或模型的子对象导入Unity。运行时,加载这个低模网格用于碰撞。这样既保证了碰撞精度,又避免了性能问题和高模网格的“Read/Write”开销。
  • 使用凸包(Convex):对于需要移动的物体(带有Rigidbody),MeshCollider必须开启Convex。但要注意,Unity对凸包网格有三角面数限制(最多255个)。对于复杂物体,需要手动创建简化的凸包代理碰撞体(如多个BoxColliderCapsuleCollider的组合)。
  • Cooking OptionsMeshCollider的烹饪选项对性能和稳定性影响很大。对于运行时动态生成或修改的网格,谨慎使用Cook for Faster SimulationEnable Mesh Cleaning。如果网格数据是“干净”的(无退化三角形、顶点重合等),可以关闭这些选项以获得更快的烹饪速度。如果网格来源不可控,建议开启清理选项以避免物理引擎的诡异行为。

4.2 AssetBundle的管理与内存泄漏

动态加载AssetBundle一定要配套进行卸载管理。AssetBundle.LoadFromFile后,资产数据会留在内存中。如果只Instantiate而不管理引用,当销毁物体时,其对应的网格、材质等资产可能不会被自动卸载。

推荐的内存管理策略

  1. 使用AssetBundle.Unload(false):参数为false时,只卸载AssetBundle文件本身,已经加载出来的资产(如GameObject、Mesh)如果还有被引用,则继续留在内存。这适用于需要频繁加载/卸载同一Bundle内不同资产的场景,但需要你手动管理资产的生命周期。
  2. 使用AssetBundle.Unload(true):参数为true时,会卸载Bundle及其加载出的所有资产,即使它们正在被场景中的物体使用,这会导致“Missing”引用错误。务必确保在调用Unload(true)之前,已经销毁了所有使用该Bundle资产的GameObject。
  3. 采用引用计数:为每个AssetBundle维护一个引用计数器。每次LoadModelAndCreateEntity时计数器+1,每个ModelEntity销毁时计数器-1。当计数器归零时,调用Unload(true)。这是最稳健的方式。

4.3 序列化与反序列化的兼容性

使用JSON保存数据虽然可读性好,但也要注意版本兼容性。如果你的ModelData类结构在未来版本中发生了变化(比如新增了一个字段),旧的保存文件可能无法正确加载。

应对策略

  • ModelData类中使用[JsonProperty]为每个字段指定明确的名称。
  • 考虑在保存文件中加入一个版本号字段(version)。
  • 在加载(反序列化)时,根据版本号执行数据迁移逻辑,将旧格式的数据升级到新格式。

4.4 运行时编辑的交互与性能

如果允许用户直接在3D场景中拖拽、旋转模型,你需要实现或集成一套运行时变换Gizmo。这里有几个要点:

  • 不要每帧更新数据:在拖拽过程中,可以实时更新物体的Transform,但ModelData的更新应该放在拖拽结束(OnMouseUp或类似事件)时进行。频繁的序列化操作(即使是更新内存对象)在模型很多时也可能成为瓶颈。
  • 使用分层撤销/重做:对于编辑类功能,撤销操作是必须的。建议实现一个简单的命令模式(Command Pattern)。每次编辑操作(移动、旋转、修改碰撞体参数)都封装成一个命令对象,压入栈中。撤销时弹出并执行反向操作。
  • 碰撞体编辑的实时预览:当用户在UI上修改碰撞体尺寸时,最好能在场景中实时显示一个线框预览。这可以通过在OnDrawGizmosOnDrawGizmosSelected中绘制Gizmos.DrawWireCube等来实现。注意,这些Gizmo绘制方法只在编辑器下或带有Gizmo组件的相机下生效,纯运行时需要自己用GLDebug.DrawLine来绘制。

4.5 针对MeshCollider的特别优化

从网络资料中我们了解到,MeshCollider的性能开销远大于原始碰撞体。在运行时动态增删MeshCollider更要小心。

  • 避免每帧修改sharedMesh:如果需要动态改变碰撞网格,尽量复用同一个Mesh对象,只更新其顶点数据,并调用mesh.RecalculateBounds()。然后,需要重新设置MeshCollider.sharedMesh = null再重新赋值,以触发物理引擎内部更新。
  • Convex Mesh Collider的顶点数:牢记255个三角形的限制。如果你的模型面数过多,需要在导入时或运行时进行网格简化(Decimation)。
  • Cooking Options的设置:对于运行时生成的静态地形网格,开启Cook for Faster SimulationEnable Mesh Cleaning能获得更好的运行时性能。但对于频繁修改的动态网格,关闭这些选项可以避免重复烹饪的开销。

5. 完整工作流示例与源码整合

让我们串联起整个流程,看看一个典型的“导入->编辑->保存->加载”循环是如何工作的,并提供一些核心源码的整合思路。

5.1 工作流步骤

  1. 准备阶段

    • 将你的3D模型文件(.fbx, .obj等)放入Unity项目的Assets目录。
    • 在Unity编辑器中,创建AssetBundle(在模型文件的Inspector面板底部设置AssetBundle名称)。
    • 构建AssetBundle到StreamingAssets文件夹。
  2. 运行时导入

    • 用户点击“导入模型”按钮,选择模型(实际可能是选择AssetBundle和资产名)。
    • 调用RuntimeModelLoader.LoadModelAndCreateEntity(bundleName, assetName)
    • 加载器加载AB,实例化Prefab,挂载ModelEntity组件,并用初始状态初始化ModelData
    • 将实例化的物体和其ModelEntity加入管理列表。
  3. 运行时编辑

    • 变换编辑:用户通过UI输入或场景Gizmo拖拽物体。交互结束时,调用该物体上ModelEntity.UpdateData(),将最新的Transform值写回其ModelData
    • 碰撞体编辑:用户选中物体,在UI面板上点击“添加碰撞体”,选择类型(如Box),然后调整参数。UI调用一个方法,例如ModelEntity.AddColliderData(new BoxColliderData{...}),该方法会创建数据并立即调用ApplyData()以在场景中生效,同时将数据加入列表。
  4. 保存场景

    • 用户点击保存。ModelDataManager.SaveAll()被调用。
    • 管理器遍历所有ModelEntity,调用其UpdateData()确保数据最新。
    • 将所有实体的ModelData列表序列化为JSON字符串,保存到Application.persistentDataPath下的文件。
  5. 加载场景

    • 下次启动程序,用户点击加载。ModelDataManager.LoadAll()被调用。
    • 从文件读取JSON字符串,反序列化为ModelData列表。
    • 对于列表中的每个ModelData,调用RuntimeModelLoader.LoadModelAndCreateEntity(...),并传入这个ModelData
    • 加载器加载模型后,ModelEntity会利用传入的existingData直接调用ApplyData(),从而将物体恢复到保存时的状态(位置、旋转、缩放、碰撞体)。

5.2 核心源码整合示例

由于篇幅限制,这里无法贴出全部源码,但你可以根据上文提供的类结构进行整合。项目应包含以下核心脚本:

  • TransformData.cs(变换数据)
  • ColliderData.cs,BoxColliderData.cs,MeshColliderData.cs(碰撞体数据类)
  • ModelData.cs(主模型数据)
  • ModelEntity.cs(挂载在模型实例上的组件)
  • RuntimeModelLoader.cs(AssetBundle加载与实例化)
  • ModelDataManager.cs(数据保存与加载总管)
  • RuntimeGizmoController.cs(可选,实现简单的拖拽旋转Gizmo)
  • ColliderEditorUI.cs(可选,处理碰撞体编辑的UI逻辑)

将这些脚本组织好,并确保在场景中有一个全局的管理器GameObject,挂载RuntimeModelLoaderModelDataManager。UI按钮的事件绑定到这两个管理器的方法上。

关于源码的获取:文章开头提到的“含源码”通常意味着作者会提供一个完整的Unity项目包或关键的C#脚本文件。由于我无法直接提供文件下载,但上述代码块已经构成了可工作的核心框架。你只需要创建一个新的Unity项目(建议使用较新版本,如2021 LTS或2022 LTS),将上述代码分别创建为C#脚本,并按照描述进行组装和调试,即可实现基本功能。重点在于理解数据流动和各个模块的职责,然后根据你的具体需求(比如是否需要支持SphereColliderCapsuleCollider,是否需要更复杂的撤销系统)进行扩展。

最后,记住一点:这套系统的设计是模块化的。你可以先从最简单的“保存变换信息”开始,实现并测试通整个流程。然后再逐步加入碰撞体编辑、网格引用处理等更复杂的功能。每一步都做好测试和调试,确保数据能正确地“一圈跑通”。当你看到自己摆放的模型,在关闭应用重新打开后能完美还原时,那种成就感就是对我们开发者最好的回报。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询