Unity-Dots框架下GameObject转Entity的方法

前言

        最近自己也在研究Unity-Dots框架,在此期间我就遇到了资源加载后将其转为Entity的问题。因此我写了这篇文章记录我现在的想法。我也想为新来研究Unity-Dots框架的朋友提供一些思路。Unity资源加载的方式有很多,但是这里我只说如何从Resources文件夹中加载GameObject。其实其他的加载方式也是一样,我们主要还是了解如何将加载出来的GameObject转为Unity-Dots框架下的Entity。

电脑环境信息:

    Windows10版本

    Unity2022.3.7f1c1

    Dots框架版本1.0.16

全加载

        全加载就是暴力的将Resources文件夹或是AB包中的所有GameObject对象转为Entity并保留下来。这个适合一些小型项目的开发。这个想法的确限制过多,但还是有坑点。所以我还是想记录一下。如果你看了描述也觉得这个方法不好,可以直接跳过这部分。

        我们先做一些准备的工作,在项目的Assets文件下创建Resources文件(如果你已经有了,就不用了)。然后在Resources文件下再创建一个独立的文件,这个文件的名字随意。这里我把其命名为DotObjs。当然再次创建独立文件的这个步骤并不是一定要的,这里我考虑到在做项目的时候Resources文件中也会存放一些不需要转Entity的GameObject对象所以我才再次创建新的文件夹。之后我们找需要转为转Entity的GameObject对象就只要去这个文件中查找。接下来我们创建cs脚本命名为GenerateResInfo,其内容如下:

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
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
using System.Collections.Generic;

using UnityEngine;

using UnityEditor;

using System.IO;

using System.Text;

using System.Text.RegularExpressions;



namespace ESCLearn.Editor

{

    public class GenerateResInfo

    {

        [MenuItem("SelfTool/GenerateResInfo")]

        public static void CallFun()

        {

            string url = Path.Combine(Application.dataPath,Path.Combine("Resources", "DotObjs"));

            // 读取“Resources/DotObjs”文件下的prefab文件

            if (Directory.Exists(url))

            {

                var allfiles = Directory.GetFiles(url, "*.prefab");

                HashSet<string> fileSet = new HashSet<string>();

                StringBuilder enumInfo = new StringBuilder();

                enumInfo.Append("namespace ESCLearn\n{\n\tpublic enum ResObjTable\n\t{");

                Debug.Log("一共有" + allfiles.Length + "需要加载");

                Regex regex = new Regex("^[0-9a-zA-Z]*$");

                bool isCorrect = true;

                for(int i = 0;i < allfiles.Length; ++i)

                {

                    var fileName = Path.GetFileNameWithoutExtension(allfiles[i]);

                    // 保证不出现重名,否则生成的枚举会出现问题

                    if(!fileSet.Contains(fileName))

                    {

                        // 保证文件名只有字母和数字组成

                        if(regex.IsMatch(fileName))

                        {

                            fileSet.Add(fileName);

                            if (i != 0)

                            {

                                enumInfo.Append(',');

                            }

                            enumInfo.Append("\n\t\t");

                            enumInfo.Append(fileName);

                        }

                        else

                        {

                            Debug.LogError(fileName + " 不只有字母和数字构成");

                            isCorrect = false;

                            break;

                        }

                    }

                    else

                    {

                        Debug.LogError(fileName + " 存在了至少两个,这是不被允许的");

                        isCorrect = false;

                        break;

                    }

                }

                enumInfo.Append("\n\t}\n}");

                if(isCorrect)

                {

                    Debug.Log(enumInfo.ToString());

                    string saveUrl = Path.Combine(Application.dataPath, Path.Combine("Script", "DotsLoadGameObjectFromFile"));

                    saveUrl = Path.Combine(saveUrl, "ResObjTable.cs");

                    Debug.Log("存储路径为: " + saveUrl);

                    File.WriteAllText(saveUrl, enumInfo.ToString());

                    AssetDatabase.Refresh();

                }

            }

            else Debug.Log("未能找到 " + url + "路径");

        }

    }

}

通过这个脚本,我生成了一个名为ResObjTable的枚举。在非Dots框架下,我们可以使用资源的名称(即字符串)来做加载。但是在Dots框架下使用字符串很麻烦,所以我想着使用枚举来做这样的事情。因为枚举同样能存储文件的名称,而且它可以值类型在Dots框架下使用也十分方便。再加上资源名并不会在运行的过程中进行改变,所以我就想着使用枚举来做。这里我们需要注意的是,枚举中枚举命名的方式并没有文件命名那么自由。而且你也能发现,我并没处理DotObjs文件下其他的文件,即我的代码仅处理DotObjs文件下的模型而没有处理其子路径下的。这里要修改也不困难,但是你们要注意子路径中枚举的命名和后面的加载。我这里偷懒一下(主要是我懒病犯了,懒得再深入了)。GenerateResInfo脚本弄好后,在Unity编辑器工具栏中就会出现SelfTool的选项。你点击SelfTool后再次点击GenerateResInfo就成功运行了GenerateResInfo脚本。这时候在你设定的存储路径中应该就会存在一个名为ResObjTable的cs文件。至此我们的准备工作就做好了。接下来我们就开始加载文件中的对象并将其转换为Entity。

        我们创建一个名为AllLoadComponent的cs脚本。其内容如下:

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



namespace ESCLearn

{

    public struct AllLoadConfigTag : IComponentData { }



    public struct AllLoadConfigBuffer : IBufferElementData

    {

        public Entity entity;

        public int index;

    }

}

然后我们再创建一个名为AllLoadInit的cs脚本。其内容如下:

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
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
using System;

using System.IO;

using Unity.Entities;

using UnityEngine;



namespace ESCLearn

{

    public class AllLoadInit : MonoBehaviour { }



    public class AllLoadInitBaker : Baker<AllLoadInit>

    {

        public override void Bake(AllLoadInit authoring)

        {

            var entity = GetEntity(TransformUsageFlags.None);

            AddComponent<AllLoadConfigTag>(entity);

            var buffer = AddBuffer<AllLoadConfigBuffer>(entity);

            foreach (var item in Enum.GetNames(typeof(ResObjTable)))

            {

                GameObject obj = Resources.Load<GameObject>(Path.Combine("DotObjs", item));

                if (obj != null)

                {

                    buffer.Add(new AllLoadConfigBuffer

                    {

                        entity = GetEntity(obj, TransformUsageFlags.Dynamic),

                        index = (int)Enum.Parse(typeof(ResObjTable), item)

                    });

                }

            }

        }

    }

}

之后我们在SubScene(Dots框架中需要创建的Scene)中创建一个子物体并将AllLoadInit挂载在上面。最后我们创建一个名为SpawnSystem的cs脚本,此脚本用来显示我们转换出来的实体。其内容如下:

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
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
using Unity.Burst;

using Unity.Entities;



namespace ESCLearn

{

    [BurstCompile]

    public partial struct SpawnSystem : ISystem

    {

        [BurstCompile]

        private void OnCreate(ref SystemState state)

        {

            // 只有存在AllLoadConfigTag后系统才会运行

            state.RequireForUpdate<AllLoadConfigTag>();

        }



        private void OnUpdate(ref SystemState state)

        {

            state.Enabled = false;

            var buffer = SystemAPI.GetSingletonBuffer<AllLoadConfigBuffer>(true);

            var ecbSingleton = SystemAPI.GetSingleton<EndSimulationEntityCommandBufferSystem.Singleton>();

            var ecb = ecbSingleton.CreateCommandBuffer(state.WorldUnmanaged);

            foreach (var i in buffer)

            {

                ecb.Instantiate(i.entity);

            }

        }

    }

}

运行游戏后,你就会看到文件中的物体被加载好了。这里我只是简单做了全部加载,后面我会再给出使用ResObjTable的加载。这里我将转换成Entity的对象存储在了IBufferElementData中。这里是因为IComponentData是不允许有类似NativeArray等结构体,而如果你想使用BurstCompile则List等类也不能使用了。当然这里还有一个办法,那就是在一个静态类中声明一个类似NativeArray的变量,并在Baker时将Entity信息存储下来。从逻辑上来说,这个方法都没有问题。只是我在实践过程中就会出现一个的问题。在Baker时,我们使用GetEntity得到的Entity信息和之后在SpawnSystem中从buffer中获取到的信息不一致。这个你只需要打日志(如果要打日志,记得注释BurstCompile否则会报错)就可以发现这个问题。我现在是没有找到为什么会出现这样的情况,我只能想Dots框架内部一定是做了特殊处理进行了数据变化。而我们如果在Baker中使用静态类进行存储则只能存储变化前的信息,这往往是错误的。

        既然Baker时候获取的数据是错误,那么我们只要在SpawnSystem中使用静态类存储就好了。那么接下来我就显示如何使用静态类存储并使用ResObjTable加载。我们创建一个新的cs文件名为GlobalData,其内容如下:

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
26
27
28
29
30
31
32
33
34
35
using Unity.Collections;

using Unity.Entities;



namespace ESCLearn

{

    public static class GlobalData

    {

        public static NativeArray<Entity> entities;



        public static void Release()

        {

            if(entities.IsCreated)

            {

                entities.Dispose();

            }

        }

    }

}

然后我们再创建一个名为AllLoadTest的cs文件,其内容如下:

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
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
using Unity.Entities;

using UnityEngine;



namespace ESCLearn

{

    public class AllLoadTest : MonoBehaviour

    {

        public ResObjTable spawnObj;

        public int num;



        private void OnDestroy()

        {

            GlobalData.Release();

        }

    }



    public struct AllLoadSpawnInfo : IBufferElementData

    {

        public ResObjTable spawnObj;

        public int num;

    }



    public class AllLoadTestBaker : Baker<AllLoadTest>

    {

        public override void Bake(AllLoadTest authoring)

        {

            var entity = GetEntity(TransformUsageFlags.None);

            var buffer = AddBuffer<AllLoadSpawnInfo>(entity);

            buffer.Add(new AllLoadSpawnInfo()

            {

                spawnObj = authoring.spawnObj,

                num = authoring.num

            });

        }

    }

}

接下来我们在SubScene中创建一个对象,或是在之前挂载AllLoadInit的对象挂载AllLoadTest。最后我们修改一下SpawnSystem,其内容如下:

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
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
using Unity.Burst;

using Unity.Collections;

using Unity.Entities;



namespace ESCLearn

{

    [BurstCompile]

    public partial struct SpawnSystem : ISystem

    {

        public bool init;



        [BurstCompile]

        private void OnCreate(ref SystemState state)

        {

            state.RequireForUpdate<AllLoadConfigTag>();

            init = false;

        }



        //[BurstCompile]

        private void OnUpdate(ref SystemState state)

        {

            if(init)

            {

                var buffer = SystemAPI.GetSingletonBuffer<AllLoadSpawnInfo>();

                var ecbSingleton = SystemAPI.GetSingleton<EndSimulationEntityCommandBufferSystem.Singleton>();

                var ecb = ecbSingleton.CreateCommandBuffer(state.WorldUnmanaged);

                if(GlobalData.entities.IsCreated)

                {

                    foreach (var info in buffer)

                    {

                        for (int i = 0; i < info.num; ++i)

                        {

                            ecb.Instantiate(GlobalData.entities[(int)info.spawnObj]);

                        }

                    }

                }

                buffer.Clear();

            }

            else

            {

                var buffer = SystemAPI.GetSingletonBuffer<AllLoadConfigBuffer>();

                GlobalData.Release();

                GlobalData.entities = new NativeArray<Entity>(buffer.Length, Allocator.Persistent);

                foreach (var i in buffer)

                {

                    GlobalData.entities[i.index] = i.entity;

                }

                init = true;

                buffer.Clear();

            }

        }

    }

}

我们在编辑器中设置好AllLoadTest的参数后运行游戏就可以看到结果了。

        虽然我给出的这个例子仍然是只能生成一次,但是如果你还想生成其他的,那么你只需要在运行的过程中对AllLoadSpawnInfo的buffer添加数据就好了。这里使用静态类有些限制。你或许已经注意到了,我将BurstCompile注释掉了。因为我们使用BurstCompile + 静态类会在Unity编辑器中报错。除此之外我们使用静态类其实很容易出现内存泄漏的情况。并且静态类Unity在每次运行结束后是不会主动释放其中的静态变量,所以我在AllLoadTest的OnDestroy函数中强制释放内存。以我个人来看,其实这里完全没有必要使用静态类。如果我们将生成Entity的职责全部给SpawnSystem(按照面对对象的设计来说本来就应该这样),那么我们完全就可以将这个设定在SpawnSystem内部。则我们就可以让NativeArray随着ISystem生命周期结束来释放内存。且我们也不会受静态类的限制。只是我个人参与的项目中解决的方案就是静态类,所以我就顺着写下来了。我也不知道静态类到底有什么好处,这里我就当记录一个想法好了。在我们不使用静态类的情况下,除了删除AllLoadTest的OnDestroy函数,还要将SpawnSystem的内容改为下面的样子:

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
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
using Unity.Burst;

using Unity.Collections;

using Unity.Entities;



namespace ESCLearn

{

    [BurstCompile]

    public partial struct SpawnSystem : ISystem

    {

        public bool init;

        private NativeArray<Entity> entities;



        [BurstCompile]

        private void OnCreate(ref SystemState state)

        {

            state.RequireForUpdate<AllLoadConfigTag>();

            init = false;

        }



        [BurstCompile]

        private void OnUpdate(ref SystemState state)

        {

            if(init)

            {

                var buffer = SystemAPI.GetSingletonBuffer<AllLoadSpawnInfo>();

                var ecbSingleton = SystemAPI.GetSingleton<EndSimulationEntityCommandBufferSystem.Singleton>();

                var ecb = ecbSingleton.CreateCommandBuffer(state.WorldUnmanaged);

                if(entities.IsCreated)

                {

                    foreach (var info in buffer)

                    {

                        for (int i = 0; i < info.num; ++i)

                        {

                            ecb.Instantiate(entities[(int)info.spawnObj]);

                        }

                    }

                }

                buffer.Clear();

            }

            else

            {

                var buffer = SystemAPI.GetSingletonBuffer<AllLoadConfigBuffer>();

                entities = new NativeArray<Entity>(buffer.Length, Allocator.Persistent);

                foreach (var i in buffer)

                {

                    entities[i.index] = i.entity;

                }

                init = true;

                buffer.Clear();

            }

        }

    }

}

        这里再说一些细节,因为我们一次性加载了Resources文件中所有需要转换为Entity的GameObject对象。所以我们其实是知道总长度的,这里就直接使用NativeArray。在Baker时,我们还进行了Enum转int的操作。因为Enum默认是从0开始增长的,所以我们可以直接使用这个做下标。之所以进行这个转换操作还有另外一个原因,我并不清楚Buffer是否每次获取的信息都是按照我们添加的顺序。我并没有在官方文档中找到一个明确的说明,为了准确性我额外多记录了一个下标去保证下标和Enum的一一对应。其实我自己在做单线程测试时得到的结果就是一一对应的,但这也可能是因为我测试案例很简单。

按需增长

        通常来说大家更喜欢按需增长,其实这也能做到。只是我们必须要先去加载GameObject之后才能将其转换为Entity。因为我们必须要操作GameObject所以我们只能在托管类中进行这些转换操作。但是生成的逻辑,我们仍然可以放在在非托管类中。这里就存在一个托管类和非托管类之间,Mono和Dot之间消息传递的问题。接下来我会一一解决这些问题。(如果你有看全加载,那么记得将全加载的在SubScene下挂载的脚本删除。)

        按需增长其实也是我看视频后想到的,但是视频我也找不到(主要是因为那时候没时间记录,后面就找不到了)。Dots框架下,所有的物体都是Entity。而其拥有的功能是由其身上的组件来决定的。那么我们完全可以创建一个空的实体,然后将Baker中会给的组件由我们自己创建并添加到空实体上。那么这不就等于一次转换吗?我有了这样的思路后就写了下面的实现。(重要的地方就只有一个 IncreasingTranslateSystem。其他就是用来做消息传递和显示示例。)

        我们先解决消息传递的问题,我们创建cs文件名为IncreasingLoad,其内容如下:

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
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
using UnityEngine;

using Unity.Entities;



namespace ESCLearn

{

    public class IncreasingLoad : MonoBehaviour

    {



    }



    public class IncreasingLoadBaker : Baker<IncreasingLoad>

    {

        public override void Bake(IncreasingLoad authoring)

        {

            var e = GetEntity(authoring.gameObject, TransformUsageFlags.None);

            AddComponent<EventMessageTag>(e);

            AddBuffer<IncreasingLoadMessage>(e);

        }

    }



    public struct EventMessageTag : IComponentData { }



    public struct IncreasingLoadMessage : IBufferElementData

    {

        public Entity entity;

        public int num;

    }

}

这里我偷懒了一下,我将接下来要用到的Dots组件也在这里进行声明。具体该在什么地方进行声明,大家按照自己项目的要求来。这里我使用了一个EventMessageTag进行标记,我们烘焙出来的实体。之后这个实体仅仅用来进行消息传输,具体要传递什么消息由之后大家赋给其的IBufferElementData类型决定。而EventMessageTag只起到一个标记的作用,所以它内部不需要任何的内容。且拥有EventMessageTag的实体,在整个项目运行的过程中请确保只有一个。这样会方便我们获取到这个实体。之所以我使用IBufferElementData类型,是因为同一时间内可能存在发多条信息的可能。如果你能保证一帧内有且只有可能出现一条信息,那么你也可以使用IComponentData来进行具体消息的声明。我这里IncreasingLoadMessage的定义是想告诉生成系统我要生成哪个实体,要生成多少个。做完IncreasingLoad文件的内容后,我们在SubScene下创建一个空对象并将脚本挂载给它。

        接下来我们创建新的cs文件名为IncreasingLoadMonoInfo,其内容如下:

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
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
using UnityEngine;



namespace ESCLearn

{

    public class IncreasingLoadMonoInfo : MonoBehaviour

    {

        private static IncreasingLoadMonoInfo _instance;



        public static IncreasingLoadMonoInfo Instance

        {

            get

            {

                if(_instance == null)

                {

                    _instance = new GameObject().AddComponent<IncreasingLoadMonoInfo>();

                }

                return _instance;

            }

        }



        [SerializeField]

        private string resPath;

        [SerializeField]

        private int num;



        public string ResPath

        {

            get { return resPath; }

        }



        public int Num

        {

            get {

                int curNum = num;

                num = 0;

                return curNum;

            }

        }    

        private void Start()

        {

            if (_instance != null)

            {

                Destroy(gameObject);

            }

            else _instance = this;

        }

    }

}

我这里写了一个单例,我将使用这个单例作为Mono与Dots之间传递消息的桥梁。在我自己写项目的时候,因为开发进度的原因。网络消息的逻辑还是在Mono层面上的,而且项目中还存在使用UI进行某些响应的问题。所以这是我想到这样的方法进行消息交互的方法。除此之外,你可以使用静态类进行传递消息。一些全局通用的数据,我就是使用静态类型来做消息传递。(这些方法不一定好,但是这是我现在能想到的最好的方式了)我们将其挂载在一个物体下。(这个物体最好不要在SubScene下,当然这是我个人的建议。因为其还算是在Mono下的,所以我觉得这还是不要放在SubScene下)。

        之后我们再创建一个cs文件名为IncreasingSpawnSystem,我们使用这个来进行物体的生成。其内容如下:

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
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
using Unity.Entities;



namespace ESCLearn

{

    public partial struct IncreasingSpawnSystem : ISystem

    {

        private bool isFirstLoad;

        private Entity msgEntity;



        private void OnCreate(ref SystemState state)

        {

            isFirstLoad = true;

            state.RequireForUpdate<EventMessageTag>();

        }



        private void OnUpdate(ref SystemState state)

        {

            if(isFirstLoad)

            {

                msgEntity = SystemAPI.GetSingletonEntity<EventMessageTag>();

                isFirstLoad = false;

            }



            var spawnInfo = state.EntityManager.GetBuffer<IncreasingLoadMessage>(msgEntity);

            var ecbSingleton = SystemAPI.GetSingleton<EndSimulationEntityCommandBufferSystem.Singleton>();

            var ecb = ecbSingleton.CreateCommandBuffer(state.WorldUnmanaged);

            foreach (var info in spawnInfo)

            {

                for (int i = 0; i < info.num; ++i)

                {

                    var e = ecb.Instantiate(info.entity);

                    ecb.SetEnabled(e, true);

                }

            }

            spawnInfo.Clear();

        }

    }

}

这其实就是一个简单的生成逻辑,唯一要注意的是我并没有对生成的东西做太多的处理。所以相同的物体它们一定会生成在同一个位置下。不过加处理的代码并不是本文的重点,所以我就不做处理了(主要是我想偷懒一下)。

        最后也是最重要的一部分————转换代码。我们创建一个名为IncreasingTranslateSystem的cs文件,其内容如下:

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
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
using System.Collections.Generic;

using Unity.Entities;

using Unity.Entities.Graphics;

using Unity.Mathematics;

using Unity.Rendering;

using Unity.Transforms;

using UnityEngine;

using UnityEngine.Rendering;



namespace ESCLearn

{

    public partial class IncreasingTranslateSystem : SystemBase

    {

        private Dictionary<string, Entity> entities;

        private Entity msgEntity;

        private SceneSection sceneSection;

        private SceneTag sceneTag;

        private bool isFirstLoad;



        protected override void OnCreate()

        {

            base.OnCreate();

            isFirstLoad = true;

            entities = new Dictionary<string, Entity>();

            RequireForUpdate<EventMessageTag>();

        }



        protected override void OnUpdate()

        {

            var entityManager = World.DefaultGameObjectInjectionWorld.EntityManager;

            if (isFirstLoad)

            {

                msgEntity = SystemAPI.GetSingletonEntity<EventMessageTag>();

                sceneSection = entityManager.GetSharedComponent<SceneSection>(msgEntity);

                sceneTag = entityManager.GetSharedComponent<SceneTag>(msgEntity);

                isFirstLoad = false;

            }

            int num = IncreasingLoadMonoInfo.Instance.Num;

            if (num > 0)

            {

                string resPath = IncreasingLoadMonoInfo.Instance.ResPath;

                Entity entity;

                if (!entities.TryGetValue(resPath,out entity))

                {

                    GameObject obj = Resources.Load<GameObject>(resPath);

                    if (obj != null)

                    {

                        entity = TranslateGameObjToEntity(ref entityManager, ref sceneSection, ref sceneTag, obj);

                    }

                    else

                    {

                        Debug.LogError("Resources下面没有这个资源");

                        return;

                    }

                }

                var buffer = entityManager.GetBuffer<IncreasingLoadMessage>(msgEntity);

                buffer.Add(new IncreasingLoadMessage()

                {

                    entity = entity,

                    num = num

                });

            }

        }



        public Entity TranslateGameObjToEntity(ref EntityManager entityManager, ref SceneSection sceneSection, ref SceneTag sceneTag, GameObject obj)

        {

            Entity curEntity = entityManager.CreateEntity();

            MeshRenderer render;

            MeshFilter mesh;

            // 将GameObject下的渲染组件转化为Dots框架下的渲染组件

            if (obj.TryGetComponent(out render) && obj.TryGetComponent(out mesh))

            {

                var renderMeshArray = new RenderMeshArray(render.sharedMaterials, new[] { mesh.sharedMesh });



                var renderMeshDescription = new RenderMeshDescription

                {

                    FilterSettings = RenderFilterSettings.Default,

                    LightProbeUsage = LightProbeUsage.Off,

                };



                RenderMeshUtility.AddComponents(

                    curEntity,

                    entityManager,

                    renderMeshDescription,

                    renderMeshArray,

                    MaterialMeshInfo.FromRenderMeshArrayIndices(0, 0));

                entityManager.AddComponentData(curEntity, new BlendProbeTag());

            }

            var localToWorld = new LocalToWorld { Value = obj.transform.localToWorldMatrix };

            var localTrans = LocalTransform.FromPosition(localToWorld.Position);

            localTrans.Rotation = localToWorld.Rotation;

            float3 scale = localToWorld.Value.Scale();

            // 判断缩放是否为xyz一致的缩放

            if (math.abs(scale.x - scale.y) < 1e-6 && math.abs(scale.z - scale.y) < 1e-6)

                localTrans.Scale = localToWorld.Value.Scale().x;

            else

            {

                entityManager.AddComponentData(curEntity, new PostTransformMatrix

                {

                    Value = localToWorld.Value

                });

            }

            entityManager.AddComponentData(curEntity, localTrans);

            entityManager.AddComponentData(curEntity, localToWorld);

            entityManager.AddBuffer<LinkedEntityGroup>(curEntity).Add(curEntity);

            entityManager.AddSharedComponentManaged(curEntity, sceneTag);

            entityManager.AddSharedComponentManaged(curEntity, sceneSection);

            CreateChildrenEntity(obj.transform, ref entityManager, ref curEntity);

            entityManager.SetEnabled(curEntity, false);

            return curEntity;

        }



        private void CreateChildrenEntity(Transform curTran, ref EntityManager entityManager, ref Entity curEntity)

        {

            for (int i = 0; i < curTran.childCount; ++i)

            {

                var child = curTran.GetChild(i);

                var childEntity = entityManager.CreateEntity();

                var localToWorld = new LocalToWorld { Value = child.localToWorldMatrix };

                var localTrans = LocalTransform.FromPosition(localToWorld.Position);

                localTrans.Rotation = localToWorld.Rotation;

                float3 scale = localToWorld.Value.Scale();

                if (math.abs(scale.x - scale.y) < 1e-6 && math.abs(scale.z - scale.y) < 1e-6)

                    localTrans.Scale = localToWorld.Value.Scale().x;

                else

                {

                    entityManager.AddComponentData(childEntity, new PostTransformMatrix

                    {

                        Value = localToWorld.Value

                    });

                }

                entityManager.AddComponentData(childEntity, localTrans);

                entityManager.AddComponentData(childEntity, localToWorld);

                entityManager.AddComponentData(childEntity, new Parent { Value = curEntity });

                entityManager.AddBuffer<LinkedEntityGroup>(childEntity).Add(childEntity);

                entityManager.GetBuffer<LinkedEntityGroup>(curEntity).Add(childEntity);

                entityManager.AddSharedComponentManaged(childEntity, entityManager.GetSharedComponent<SceneTag>(curEntity));

                entityManager.AddSharedComponentManaged(curEntity, entityManager.GetSharedComponent<SceneSection>(curEntity));

                MeshRenderer render;

                MeshFilter mesh;

                // 将GameObject下的渲染组件转化为Dots框架下的渲染组件

                if (child.TryGetComponent(out render) && child.TryGetComponent(out mesh))

                {

                    var renderMeshArray = new RenderMeshArray(render.sharedMaterials, new[] { mesh.sharedMesh });



                    var renderMeshDescription = new RenderMeshDescription

                    {

                        FilterSettings = RenderFilterSettings.Default,

                        LightProbeUsage = LightProbeUsage.Off,

                    };



                    RenderMeshUtility.AddComponents(

                        childEntity,

                        entityManager,

                        renderMeshDescription,

                        renderMeshArray,

                        MaterialMeshInfo.FromRenderMeshArrayIndices(0, 0));

                    entityManager.AddComponentData(childEntity, new BlendProbeTag());

                }



                CreateChildrenEntity(child, ref entityManager, ref childEntity);

            }

        }

    }

}

上面手动添加的组件都是我照着烘焙出来的实体一个个加上去的。SceneSection和SceneTag,其实是不一定要加上去的。现在我只能想到拿其他实体上的SceneSection和SceneTag来做赋值处理。首先是因为这两个组件是IShareComponentData,所以同一个SubScene下的实体这两个就是一样。对应的官方文档为:SceneSection APi, SceneSection overview, SceneTag API, ISharedComponentData。如果你不加这两个组件,实体仍然会显示,但是其不会在SubScene下。我觉得这样可能有问题(我自己在测试的时候一点问题都没有),所以我还是加了上去。LinkedEntityGroup组件是用来表达实体间父子关系的组件之一。比如你想要父物体隐藏时子物体也隐藏,那么你就要加入这个组件。并且在我们实例化父物体的时候,有LinkedEntityGroup组件才会将子物体一起实例化。请注意直接加Parent组件的达不到这样的效果,Parent只负责位置的修改和在Hierarchy显示父子关系。具体官方文档为:LinkedEntityGroup, Parent

碎碎念

       我本来想多研究一下Dots框架,结果项目做到一半我换到另外一个项目去了。这个项目并不使用Dots框架,大概率没有什么需求我也不会碰Dots框架了吧。我自己感觉也有些水文章了,但是我也怕要是不记录我后面又遇到Dots项目那又会忘,所以我还是水完了。我本来是有挺多想法的。因为在我写完全加载后,我的拖延症犯了导致我拖了两周才再次去写这篇文章,之前我想写什么早都忘了。希望这篇水文能帮到各位。