Unity3D Entitas ECS 框架

一个使用ECS框架,制定Agent移动, 最后使用倒播功能还原路径的功能演示。

20180502 Unity 2018.1 正式版出来了, 带来了内置的ECS和Job System。 在UnityECS中, Entity对应的是GameObject, Component对应的是MonoBehavior, 而System对应的是ComponentSystem。
不过在写这篇文章的测试demo时使用的还是GitHub上的版本, 两者之间仅有API的名称上的区别。
GitHub地址:https://github.com/sschmid/Entitas-CSharp

安装:
版本 1.52
https://github.com/sschmid/Entitas-CSharp/releases

1.使用已编译的版本 https://github.com/sschmid/Entitas-CSharp/releases/download/1.5.2/Entitas.zip
直接将文件解压到Assets目录下即可

2.使用源码安装 git clone https://github.com/sschmid/Entitas-CSharp.git
1.将Libraries/Dependencies 里的文件全部拷贝到 Plugins 目录下
2.将Entitas 和 Addones 目录拷贝到Assets 目录下
3.随便打开一个C#文件,让Unity生成项目文件后,就可以在工具栏启动 Tools/Entitas/Perfererces
4.如果在第三步出错了,那是因为源码文件中 EntitasResources.cs 取资源目录里的文件Version.txt 失败了, 这是由于Unity生成的dll文件不会包含资源,因此要么修改这个函数直接返回版本号(不取Version),要么直接使用官方已经编译好的Entitas.dll

ECS架构概述

ECS架构看起来就是这样子的。先有个World,它是系统(译注,这里的系统指的是ECS中的S,不是一般意义上的系统,为了方便阅读,下文统称System)和实体(Entity)的集合。而实体就是一个ID,这个ID对应了组件(Component)的集合。组件用来存储游戏状态并且没有任何的行为(Behavior)。System有行为但是没有状态。

这听起来可能挺让人惊讶的,因为组件没有函数而System没有任何字段。

from:http://gad.qq.com/article/detail/28682

ECS框架其实在许多年前就已经诞生了,这几年名声大噪源于GDC2017守望先锋的一次技术分享,ECS的理念是组合模式优于面向对象模式,ECS解决的问题是之前OOP框架开发时状态和行为混合,在对象功能非常庞大时维护的成本很高。
ECS的全称是 Entity , Component, System. 其中Entity是对象实体, Component维护了对象的所有状态, 而System则是利用对象身上的Compoent实现各种行为, 在实际编程发现这种编程方式非常类似行为树的构架,开发过行为树逻辑的同学非常熟悉开发模式就是讲各种逻辑进行拆分,分散一个个处理细节的函数,使这些函数可以被行为树任意组合复用。

C#版的ECS利用了许多C#的特性,比如分散类 partical. ECS中Component 实际上是把OOP中的成员属性变成一个个单独的partical class进行拆分,但是本质上组合后还是属于他的成员。
举个例子, 在OOP中一个玩家包含了生命和移动速度的属性 Like this

1
2
3
4
public class Player {
    public uint HP;
    public uint MoveSpeed;
}

而在ECS构架中这个类会变成这样

1
2
3
4
5
6
7
8
9
10
HPComponent.cs

public partial class Player{
    public HPComponent hpComponent;
}

MoveSpeedComponent.cs
public partial class Player{
    public MoveSpeedComponent moveSpeedComponent
}

这里我为什么要把类名标识出来, 这是因为对于程序员开发来说,当你要修改Player的某个属性时,只要关注属性类即可, 不需要去Player.cs中大海捞针找属性在哪,这是一种开发上的快捷。 而对于编辑器来说,这些类都都是partial 聚合类, 本质上都是Player.cs 的一部分与OOP无差。

继续阅读Unity3D Entitas ECS 框架

XLua 与 ILRuntime 性能测试

首先。。今天是个好日子,因为可以

好了,进入正题
现在很多项目都使用xlua来开发整个项目,但是实际上使用的并不是xlua标榜的“热修复”,毕竟国内游戏还是要要求可以热更新新功能的,因此如果采用热修复的方案,则需要小版本使用lua写功能,大版本又要把lua版本转换为C#代码重写一次,不太现实,因此现实中的许多公司都是使用lua来写大部分的逻辑。

但是u3d是个C#语言为编程语言的引擎(当然还有JS…),一般lua项目中,我们会把逻辑运算量大,复杂度高,对性能有要求的代码写在C#代码,或者C++ DLL库中,比如加载,更新,框架,战斗等, 这就需要程序要经常同时使用C#和lua写代码,容易人格分裂。

在Ilruntime 1.3版本之后,有稳定的调试插件(通过tcp 连接,因此可以真机调试),值绑定等功能后 也成为一个不错选择,程序员不需要更换语言来编写项目。至于它的局限性,对比lua来说都是半斤八两,比如主工程的泛型类无法导出,常用值类型需要生成wrap等(否则会有严重性能问题)。

目前很多人对Ilruntime 的看法有2点。 1,使用的项目比较少,未预见的坑比较多。 2,性能比较差,毕竟lua 有Jit, 在支持Jit的设备上是接近c的性能,大部分的性能损耗在接口交互上,而Ilruntime 是自己实现了一套解释器,是C#编写的,原生性能较差。 因此我打算做一个性能测试,看看真实的情况是什么。

使用的ILruntime库地址
https://github.com/Ourpalm/ILRuntime
使用的Xlua库地址
https://github.com/Tencent/xLua

注:Ilruntime 已经设置全局宏 DISABLE_ILRUNTIME_DEBUG, 并且hotfix项目为Release, 生成了Vecto3_Binding
Xlua 生成了 Vector3_Wrap
.Net 3.5版本下

测试3种情况下的性能情况

Test1 测试U3d内部值计算

ILRuntime:

1
2
3
4
5
6
7
8
9
10
11
12
13
public void Test1()
        {
            Stopwatch sw = new Stopwatch();
            sw.Start();
            for(int i = 0; i < 1000000; i++)
            {
                Vector3 a = new Vector3(1, 2, 3);
                Vector3 b = new Vector3(4, 5, 6);
                Vector3 c = a + b;
            }
            sw.Stop();
            UnityEngine.Debug.Log("il test1:" + sw.ElapsedMilliseconds);
        }

Xlua:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
 void LuaTest1()
    {
        System.Diagnostics.Stopwatch sw = new System.Diagnostics.Stopwatch();
        sw.Start();

        env.DoString(@"

        for i = 0, 1000000, 1 do

        local a = CS.UnityEngine.Vector3(1,2,3)

        local b = CS.UnityEngine.Vector3(4,5,6)

        local c = a + b

        end

        "
);
        sw.Stop();

        Debug.Log("lua test1:" + sw.ElapsedMilliseconds);
    }

继续阅读XLua 与 ILRuntime 性能测试

使用xlua 进行Unity3D 热更新-2

一接触到新的东西,总想看看背后的原理是怎样的,xlua也不例外。于是试着写了一下,算是了解底层的实现原理,以后不用xlua也能有借鉴的地方。

xlua的热修复原理实际上是在 C# 编译成中间语言的时候,进行代码的插入这部分用到了 Mono.Ceil 库来操作,当然还有其他很多的库也可以实现。 因为是在IL的部分插入,因此直接支持IL2CPP

直接进入主题

已知有一个类

1
2
3
4
5
6
7
8
9
10
public class InputTest{

    void Start(){
        Hello();
    }
    private void Hello(){
        Debug.Log("hello");
        Debug.Log("666"):
    }
}

这个类在被Unity调用的时候会输出 “Hello”
那么如果我们想修改Hello函数该怎么做呢

1
2
3
4
string injectPath = @"./Library\ScriptAssemblies\Assembly-CSharp.dll";
AssemblyDefinition assemblyDefinition = null;
var readerParameters = new ReaderParameters { ReadSymbols = true };
assemblyDefinition = AssemblyDefinition.ReadAssembly(injectPath, readerParameters);

第一步 是要将当前代码的 Assembly 读出来, U3d有3个Assembly。 一个是项目代码叫 Assembly-CSharp.dll 一个是编辑器代码 Assembly-Editor-CSharp.dll.
还有一个是插件 Assembly-Plugin-CSharp.dll. 因为 InputTest是项目代码部分,所以读取 Assembly-CSharp.dll即可

读取成功后,所有的数据都在 assemblyDefinition 中,只需要遍历一下找到要修改的类即可

1
2
3
4
5
6
7
8
9
10
 foreach (Mono.Cecil.TypeDefinition item in assemblyDefinition.MainModule.Types) {

                if(item.FullName == "InputTest") {
                    foreach (MethodDefinition method in item.Methods) {

                        if (method.Name.Equals("Hello")) {
                        }
                    }
                }
}

第二步 通过遍历类型定义找到我们的类 “InputTest” 然后在 类定义中遍历所有的函数定义,找到我们要修改的 “Hello”函数
找到函数后,就可以正式做函数修改了。

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
var ins = method.Body.Instructions.First();
var worker = method.Body.GetILProcessor();
var logRef = assemblyDefinition.MainModule.Import(typeof(Debug).GetMethod("Log", new Type[] { typeof(string) }));

worker.InsertBefore(ins, worker.Create(OpCodes.Ldstr, "Fuck Off"));
worker.InsertBefore(ins, worker.Create(OpCodes.Call, logRef));

worker.InsertBefore(ins, worker.Create(OpCodes.Ldstr, "Fuck On"));
worker.InsertBefore(ins, worker.Create(OpCodes.Call, logRef));

Type type = typeof(InjectTest);

 if (null != type) {
     MethodInfo subMethod = type.GetMethod("SayFuck");

     if (null != subMethod) {
          Debug.Log("Find Method: " + subMethod);

          var sayRef = assemblyDefinition.MainModule.Import(subMethod);

          worker.InsertBefore(ins, worker.Create(OpCodes.Call, sayRef));
      }
}

var writerParameters = new WriterParameters { WriteSymbols = true };
assemblyDefinition.Write(injectPath, new WriterParameters());

第三步 做了3件事情, 绑定了2个UnityEngine的Log函数,打印了 “Fuck Off”, “Fuck On” 之后再绑定一个类 “InjectTest”中的静态函数 SayFuck()
这样原本的 Hello()函数就会在 打印”Hello”之前先打印 “Fuck Off”, “Fuck On” 调用 InjectTest.SayFuck().

最后就是将执行的修改进行保存 assemblyDefinition.Write

最后的最后用C#反编译软件打开 Assembly-CSharp.dll 看看修改后的Hello()函数

可以看到已经成功的修改啦。