【游戏技术群】959392658  【游戏出海群】12067810
游戏蛮牛 手机端
开启辅助访问
 找回密码
 注册帐号

扫一扫,访问微社区

开发者专栏

关注:2375

当前位置:游戏蛮牛 技术专区 开发者专栏

__________________________________________________________________________________
开发者干货区版块规则:

  1、文章必须是图文形式。(至少2幅图)
      2、文章字数必须保持在1500字节以上。(编辑器右下角有字数检查)
      3、本版块只支持在游戏蛮牛原创首发,不支持转载。
      4、本版块回复不得无意义,如:顶、呵呵、不错......【真的会扣分的哦】
      5、......
__________________________________________________________________________________
查看: 4451|回复: 60

[士郎] Unity 实体组件系统(ECS)——预览与体验

[复制链接]  [移动端链接]
排名
3
昨日变化

7088

主题

7615

帖子

2万

积分

Rank: 16

UID
1231
好友
185
蛮牛币
11121
威望
30
注册时间
2013-7-29
在线时间
3646 小时
最后登录
2018-12-17

社区QQ达人活力之星原创精华达人突出贡献奖财富之证游戏蛮牛QQ群会员蛮牛妹VIP

发表于 2018-11-12 12:19:10 | 显示全部楼层 |阅读模式

马上注册,结交更多好友,享用更多功能,让你轻松玩转社区。

您需要 登录 才可以下载或查看,没有帐号?注册帐号

x
1.jpg

Hi,大家好。吃饱喝足,该写点东西了。
3.jpg


这次给大家带来一期"新技术"的介绍。没错,主角就是Unity官方正在推行的ECS框架(Entity-Component-System)。
相信大家多少听说过ECS(实体组件系统),或者在网络上查找过相关资料,甚至动手实现过一个自己的简易ECS框架。如果没有听说过也没有关系,可以通过实践可以更好地理解它。


简单介绍一下ECS的核心概念:
Entity(实体):由一个唯一ID所标识的一系列组件的集合。
Component(组件):一系列数据的集合,本身没有任何方法。只能用于存储状态。
System(系统):只有方法,没有状态的工具,类似静态类。
这种设计看上去很新颖且奇特,具体到游戏开发的环节中是什么样的呢?





简单来说,Entity相当于一个只有唯一ID的GameObject,Component就是一个只有字段的Struct,System只有方法没有任何字段。Entity通过不同Component的组合可以被不同的System关注。

如下图所示:
4.jpg



Player(Entity)拥有Position,MoveSpeed,Velocity,Player这些Component。那么他就会被PlayerInputSystem,MoveSystem所关注,这些系统在Update时会对该实体的组件进行读写操作。



系统的调用顺序也可以打乱。PlayerInputSystem跟AIInputSystem由于写的是不同的实体的Velocity所以可以并行,可以把他们归到一个Group里面。MoveSystem由于需要读取Velocity,所以得等待Group中所有写入操作都完成后才能Update。




至于为什么要使用ECS,相信很多熟悉OOP(面向对象编程)的同学开发稍微复杂点的游戏时都遇到过:一大堆类不知道继承哪一个,为了解耦写一大堆管理器等。



ECS这种反直觉的设计理念在游戏开发中比起OOP有这些显而易见的优点:

  • 没有大量的管理器或者中间件,简单地说就是避免了OOP中常见的过度抽象。
  • 比起继承,组合的方式更容易塑造新的实体类型。
  • 数据驱动,因为Component没有方法且被统一管理,方便利用Excel配置数据。
  • 可以利用Utils(工具类)抽出System的共有方法,加上SingletonComponent(单例组件)提供全局访问进行解耦。

ECS在1998年就已经被应用在一款叫做:Thief : The Dark Project 的游戏中。直到2017年在 GDC 2017上的演讲:Overwatch Gameplay Architecture and Netcode





守望先锋团队向大家分享了在守望先锋中使用的ECS以及一系列实现上的细节。这下才被广大开发者熟知。



由于ECS架构的一些特点,他可以很容易利用多个CPU实现逻辑并行,紧凑且连续的内存布局,比起OOP可以更方便地获得更大的性能提升。



在Unity2018中,伴随Unity ECS推出的还有Burst编译器与C# Job System。下面列出了一部分Unity ECS的愿景:



  • 我们相信我们可以快速编写高性能代码,就像MonoBehaviour.Update一样简单。
  • 我们相信,在基础层面,这将使Unity比现在更加灵活。
  • 我们会立即为您提供有关任何竞态条件的错误信息。
  • 对于小内容,我们希望Unity在不到1秒的时间内加载。
  • 在大型项目中更改单个.cs文件时。组合编译和热重载时间应小于500毫秒。

Unity ECS现阶段并不推荐直接用于生产,但是了解他的使用方法还是很有用处的,因为ECS不仅可以提高性能,还可以帮助你编写更清晰,更易于维护的代码。
看到这里有没有很想体验一下Unity的ECS?


1.1.gif




下面我们就来写一些Unity ECS-Style风格的代码。

首先我们下载一个Unity 2018.X,新建一个工程在Window -> Package Manager 中选择Advanced -> Show Preview Packages,然后选择Entities并点击Install。


(在2018.1中点击All可以看到Entities)
5.jpg

看到Jobs出现就说明Entities已经安装完毕

准备就绪后,我们直接创建一个脚本并命名Bootstrap。


[AppleScript] 纯文本查看 复制代码
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using Unity.Entities;

public class Bootstrap
{
    [RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.BeforeSceneLoad)]
    public static void Awake()
    {

    }

    [RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.AfterSceneLoad)]
    public static void Start()
    {

    }
}



ECS不同于以往的Monobehavior,他有一套自己的生命周期。


代码中的Awake跟Start方法都是我自己编写的,只要给他们打上一个特性就会被Unity在场景加载的前后时机进行调用。(方法必须为静态方法)
我们在场景中创建一个空物体并命名Player,加上Mesh Instance Renderer Component(渲染组件)。


6.jpg



Mesh选择球形,新建一个材质球Red并且放在Material中,再把Cast Shadows(投影)设置为开启。





打开Bootstrap脚本,在Awake中创建出EntityManager与EntityArchetype:


   
[AppleScript] 纯文本查看 复制代码
 private static EntityManager entityManager;     //所有实体的管理器, 提供操作Entity的API

    private static EntityArchetype playerArchetype; //Entity原型, 可以看成由组件组成的数组

    [RuntimeInitializeOnLoadMethod(loadType: RuntimeInitializeLoadType.BeforeSceneLoad)]
    public static void Awake()
    {
        entityManager = World.Active.GetOrCreateManager<EntityManager>();

        //下面的的Position类型需要引入Unity.Transforms命名空间
        playerArchetype = entityManager.CreateArchetype(typeof(Position));
    }

通过World创建出EntityManager,EntityManager的对象提供了创建实体,给实体添加组件,获取组件,移除组件,实例化与销毁实体等功能。

按照Unity的说法,默认情况下会在进入播放模式时创建好World,因此我们直接在Awake使用World创建EntityManager就好了。
所以EntityManager就是一个实体的管理器。



上个版本的Unity ECS还是静态类,现在已经该为由World创建的实例了。
等待场景加载完成之后会调用Start方法:


[AppleScript] 纯文本查看 复制代码
 [RuntimeInitializeOnLoadMethod(loadType: RuntimeInitializeLoadType.AfterSceneLoad)]
    public static void Start()
    {
        //把GameObect.Find放在这里因为场景加载完成前无法获取游戏物体。
        GameObject playerGo = GameObject.Find("Player");

        //下面的类型是一个Struct, 需要引入Unity.Rendering命名空间
        MeshInstanceRenderer playerRenderer = 
            playerGo.GetComponent<MeshInstanceRendererComponent>().Value;

        //获取到渲染数据后可以销毁空物体
        Object.Destroy(playerGo); 

        Entity player = entityManager.CreateEntity(playerArchetype);

        //修改实体的Position组件
        entityManager.SetComponentData(player, new Position 
            { Value = new Unity.Mathematics.float3(0, 2, 0) });

        // 向实体添加共享数据组件
        entityManager.AddSharedComponentData(player, playerRenderer);
    }

float3 是Unity新推出的数学库(Unity.Mathematics)中的类型,用法跟Vector3基本一致。
Unity建议在ECS中使用该数学库。
通过entityManager对象创建的实体都会被管理起来,在创建Entity时我们可以在CreateEntity方法的参数中填上之前创建好的playerArchetype(玩家原型),即按照原型中包含的组件依次添加到实体上。

在Update方法中获取了之前在场景中设置的渲染组件,并且作为AddSharedComponentData的参数。

这时我们的player实体已经拥有了两个组件:Position跟MeshInstanceRenderer。

关于ISharedComponentData接口:他的作用是当实体拥有属性时,比如:球形的实体共享同样的Mesh网格数据时。这些数据会储存在一个Chuck中并非每个实体上,因此在每个实体上可以实现0内存开销。
值得注意的是:如果Entity不包含Position组件,这个实体是不会被Unity的渲染系统关注的。因此想在屏幕上看见这个实体,必须确保MeshInstanceRenderer跟Position都添加到了实体上。


我们运行游戏就会看到我们创建的Player被显示出来了:
7.png


细心的你也发现了,在Hierarchy中并没有这个实体的信息。

8.jpg

原因是现在Unity编辑器还没有与ECS整合,因此我们需要打开Window -> Analysis -> Entity Debugger面板查看我们的系统与实体。

9.jpg


在EntityManager中可以看到Entity 0,那就是我们创建的player实体。此时他的Inspector菜单也会有数据填充:

11.jpg

每个Value对应一个Component及其具体的值。可以看到他的坐标,Mesh与Material都被更改了。


现在我们搭建一个简易场景,首先创建一个Plane并命名为Ground。然后创建一个灰色的材质球挂上去:

12.png

同时保持他的默认组件就好了:

13.jpg



运行游戏看一下效果:


14.png



我们只需要修改摄像机的Transform就能让游戏画面呈现出俯视角的效果:

15.jpg

视角调的还不错:


16.jpg


这时如果我们想在ECS框架中控制这个小球(Player)的移动该怎么实现呢?


我们之前在Bootstrap脚本中已经实现了Awake跟Start了,其实每一个System都会实现Update方法。并且会在Start调用后开始调用。
player现在只包含两个组件:Position与MeshInstanceRenderer,显然缺乏一个标识组件,我们创建一个脚本并命名为PlayerComponent:
[AppleScript] 纯文本查看 复制代码
using Unity.Entities;

//组件必须是struct并且得继承IComponentData接口
public struct PlayerComponent : IComponentData
{
}

在Bootstrap.Start方法中,在player创建出来后加上一句:
[AppleScript] 纯文本查看 复制代码
entityManager.AddComponentData(player, new PlayerComponent()); //添加PlayerComponent组件




现在我们创建一个脚本命名为MovementSystem,继承自ComponentSystem:
类似继承Monobehavior,我们的系统只要继承了这个基类就会被Unity识别,并且每一帧都调用OnUpdate。
[AppleScript] 纯文本查看 复制代码
using UnityEngine;
using Unity.Entities;
using Unity.Transforms;
using Unity.Mathematics;

public class MovementSystem : ComponentSystem
{
    protected override void OnUpdate()
    {
    }
}

Unity ECS帮我们简化了获取实体再获取组件的过程,现在可以直接获取不同的组件。也就是说我们可以获取被EntityManager管理的实体上我们想要的组件组成的集合。


利用一个特性:[Inject]:

   
[AppleScript] 纯文本查看 复制代码
//这里声明一个结构, 其中包含我们定义的过滤条件, 也就是必须拥有CameraComponent组件才会被注入。
    public struct Group
    {
        public readonly int Length;

        public ComponentDataArray<Position> Positions;
    }

    //然后声明结构类型的字段, 并且加上[Inject]
    [Inject] Group data;

    protected override void OnUpdate()
    {
        float deltaTime = Time.deltaTime;

        for (int i = 0; i < data.Length; i++)
        {
            float3 up = new float3(0, 1, 0);

            float3 pos = data.Positions.Value; //Read

            pos += up * deltaTime;

            data.Positions = new Position { Value = pos }; //Write
        }
    }




声明了Length属性后,Length会被自动注入,它代表结构中每个数组的总元素数量,方便进行for循环迭代。
[Inject]会从所有Entity中寻找同时拥有PlayerComponent与Position组件的实体,接着获取他们的这些组件,注入我们声明的不同数组中。
我们只需要在结构中声明好筛选的条件与我们需要的组件,ECS就会在背后帮我们处理,给我们想要的结果。



运行后player果然升天了:
1.2.gif


趁热打铁,现在我们想自己通过输入控制小球在平面上移动。
先声明一个组件InputComponent作为一个标识:


[AppleScript] 纯文本查看 复制代码
using Unity.Entities;

public struct InputComponent : IComponentData
{
}




然后再声明一个组件VelocityComponent保存我们的输入向量:

[AppleScript] 纯文本查看 复制代码
using Unity.Entities;
using Unity.Mathematics;

public struct VelocityComponent : IComponentData
{
    public float3 moveDir;   
}

我们默认player的速度为1就不单独声明速度值了。
接着创建InputSystem来更改VelocityComponent的值,接下来的工作就是照猫画虎了:
[AppleScript] 纯文本查看 复制代码
using UnityEngine;
using Unity.Entities;
using Unity.Mathematics;

public class InputSystem : ComponentSystem
{
    public struct Group
    {
        public readonly int Length;

        public ComponentDataArray<PlayerComponent> Players;

        public ComponentDataArray<InputComponent> Inputs;

        public ComponentDataArray<VelocityComponent> Velocities;
    }

    [Inject] Group data;

    protected override void OnUpdate()
    {
        for (int i = 0; i < data.Length; i++)
        {
            float x = Input.GetAxisRaw("Horizontal");
            float z = Input.GetAxisRaw("Vertical");
            float3 normalized = new float3();
            if(x != 0 || y != 0)
                normalized = math.normalize(new float3(x, 0, z));
                
            data.Velocities = new VelocityComponent { moveDir = normalized }; //Write
        }
    }
}

比较麻烦的一点就是,在游戏初期没有确立基础的组件与系统时需要频繁修改。
移动系统也需要修改:


  
[AppleScript] 纯文本查看 复制代码
  //这里声明一个结构, 其中包含我们定义的过滤条件, 也就是必须拥有CameraComponent组件才会被注入。
    public struct Group
    {
        public readonly int Length;

        public ComponentDataArray<VelocityComponent> Velocities;

        public ComponentDataArray<Position> Positions;
    }

    //然后声明结构类型的字段, 并且加上[Inject]
    [Inject] Group data;

    protected override void OnUpdate()
    {
        float deltaTime = Time.deltaTime;

        for (int i = 0; i < data.Length; i++)
        {
            float3 pos = data.Positions.Value;             //Read
            float3 vector = data.Velocities.moveDir;       //Read

            pos += vector * deltaTime; //Move

            data.Positions = new Position { Value = pos }; //Write
        }
    }


还要回到Bootstrap.Start中,向我们的player继续添加这两个组件:

      
[AppleScript] 纯文本查看 复制代码
 Entity player = entityManager.CreateEntity(playerArchetype);    // Position
        //添加PlayerComponent组件
        entityManager.AddComponentData(player, new PlayerComponent());  // PlayerComponent
        entityManager.AddComponentData(player, new VelocityComponent());// VelocityComponent
        entityManager.AddComponentData(player, new InputComponent());   // InputComponent
        // 向实体添加共享的数据
        entityManager.AddSharedComponentData(player, playerRenderer);   // MeshInstanceRenderer

        //修改实体的Position组件
        entityManager.SetComponentData(player, new Position 
            { Value = new Unity.Mathematics.float3(0, 0.5f, 0) });

Duang:
1.3.gif
可以WASD操控player移动了


Unity ECS只提供了渲染系统并没有提供物理系统,如果要跟以前的项目结合,我们还需要能够访问场景中的游戏物体,比如一个经典的Cube。

17.jpg

我们的目的是让这个立方体升天




在Bootstrap.Start中获取我们的Cube,并且加上GameObjectEntity组件。
GameObjectEntity 确实叫这个名, 是Unity提供的组件。
添加上这个组件后Cube就可以被entityManager关注,并且可以获取Cube上的任意组件:

      
[AppleScript] 纯文本查看 复制代码
 //获取Cube        
        GameObjectEntity cubeEntity = GameObject.Find("Cube").AddComponent<GameObjectEntity>();

        //添加Velocity组件
        entityManager.AddComponentData(cubeEntity.Entity, new VelocityComponent
            { moveDir = new Unity.Mathematics.float3(0, 1, 0) });




我们向Cube添加了一个VelocityComponent组件,在MovementSystem加上这些代码:


   
[AppleScript] 纯文本查看 复制代码
 public struct GameObject
    {
        public readonly int Length;

        public ComponentArray<Transform> Transforms; //该数组可以获取传统的Component

        public ComponentDataArray<VelocityComponent> Velocities;//该数组获取继承IComponentData的
    }

    [Inject] GameObject go;

在OnUpdate中加上这些代码,针对Transform进行操作:
      
[AppleScript] 纯文本查看 复制代码
  for (int i = 0; i < go.Length; i++)
        {
            float3 pos = go.Transforms.position; //Read
            float3 vector = go.Velocities.moveDir; //Read

            pos += vector * deltaTime; //Move

            go.Transforms.position = pos; //Write
        }


运行游戏后,我们可以看到:
1.4.gif
Cube果然上天了





Cube跟player的移动其实是被不同的系统实现的,player是因为被默认存在的渲染系统关注了所以实现了移动,而Cube是我们自己的MovementSystem实现的。


如果想在ECS中用到之前的物理系统最好是自己写一个单独的系统并关注Rigidbody,BoxCollider这些传统组件,然后在OnUpdate中使用它们。





看到这里你应该已经明白了ECS特点与Unity ECS的用法了,希望可以勾起你们对于ECS的兴趣,在以后针对多核开发的时代,相信ECS会成为高性能的代表。
介于篇幅原因,JobComponentSystem,NativeArray,System并行,组件的先后顺序,读写权限这些跟性能优化相关的点就没有介绍了,感兴趣的话可以去Unity ECS官网了解。



附上项目下载地址:


密码:altt

等以后Unity ECS更完善时再出一期。这期文章就到这里了,拜拜咯。
知乎@ProcessCA



点评

ecs  发表于 2018-11-12 15:44
ecs  发表于 2018-11-12 14:43
ecs  发表于 2018-11-12 12:47

本帖被以下淘专辑推荐:

  • · I Like|主题: 86, 订阅: 8

跟我念“站长妹纸萌萌哒!”我说站长,你说YO!爱你们么么哒~
回复

使用道具 举报

排名
28297
昨日变化
10

0

主题

26

帖子

42

积分

Rank: 1

UID
302398
好友
1
蛮牛币
70
威望
0
注册时间
2018-11-4
在线时间
4 小时
最后登录
2018-11-23
发表于 2018-11-12 12:43:21 | 显示全部楼层
ECS不同于以往的Monobehavior,他有一套自己的生命周期

回复 支持 反对

使用道具 举报

排名
21644
昨日变化
4

0

主题

47

帖子

76

积分

Rank: 2Rank: 2

UID
302409
好友
2
蛮牛币
145
威望
0
注册时间
2018-11-4
在线时间
7 小时
最后登录
2018-12-5
发表于 2018-11-12 14:37:11 | 显示全部楼层
Entity相当于一个只有唯一ID的GameObject,Component就是一个只有字段的Struct,System只有方法没有任何字段。

回复 支持 反对

使用道具 举报

排名
26388
昨日变化
6

0

主题

32

帖子

52

积分

Rank: 2Rank: 2

UID
302396
好友
1
蛮牛币
169
威望
0
注册时间
2018-11-4
在线时间
6 小时
最后登录
2018-12-6
发表于 2018-11-12 14:59:54 | 显示全部楼层
ECS在1998年就已经被应用在一款叫做:Thief : The Dark Project 的游戏中。

回复 支持 反对

使用道具 举报

5熟悉之中
621/1000
排名
3894
昨日变化
1

1

主题

62

帖子

621

积分

Rank: 5Rank: 5

UID
9545
好友
0
蛮牛币
2026
威望
0
注册时间
2013-12-4
在线时间
190 小时
最后登录
2018-12-3
发表于 2018-11-12 15:55:36 | 显示全部楼层
6666666666666666666666

回复 支持 反对

使用道具 举报

5熟悉之中
959/1000
排名
2811
昨日变化

1

主题

143

帖子

959

积分

Rank: 5Rank: 5

UID
236305
好友
1
蛮牛币
1355
威望
0
注册时间
2017-8-7
在线时间
311 小时
最后登录
2018-12-17
发表于 2018-11-12 16:32:33 | 显示全部楼层
学习一下 不知道会不会成为未来主流

回复 支持 反对

使用道具 举报

4四处流浪
352/500
排名
7645
昨日变化
1

0

主题

102

帖子

352

积分

Rank: 4

UID
218300
好友
0
蛮牛币
326
威望
0
注册时间
2017-4-18
在线时间
92 小时
最后登录
2018-12-16
发表于 2018-11-12 17:10:00 | 显示全部楼层
够详细 谢谢楼主 学习了

回复 支持 反对

使用道具 举报

5熟悉之中
900/1000
排名
3419
昨日变化
3

0

主题

212

帖子

900

积分

Rank: 5Rank: 5

UID
2623
好友
0
蛮牛币
1576
威望
0
注册时间
2013-8-26
在线时间
266 小时
最后登录
2018-12-17
发表于 2018-11-12 17:13:51 | 显示全部楼层
[发帖际遇]: zuoyamin 发帖时在路边捡到 1 蛮牛币,偷偷放进了口袋. 幸运榜 / 衰神榜

回复

使用道具 举报

3偶尔光临
197/300
排名
11375
昨日变化

2

主题

52

帖子

197

积分

Rank: 3Rank: 3Rank: 3

UID
299243
好友
0
蛮牛币
196
威望
0
注册时间
2018-10-8
在线时间
57 小时
最后登录
2018-12-17
发表于 2018-11-12 19:35:32 | 显示全部楼层
不能使用“使用静态”特性,因为它不是C 4语言规范的一部分 QQ截图20181112193148.png 这是什么啥问题

回复 支持 反对

使用道具 举报

7日久生情
1597/5000
排名
1828
昨日变化
1

0

主题

447

帖子

1597

积分

Rank: 7Rank: 7Rank: 7Rank: 7

UID
135463
好友
0
蛮牛币
58
威望
0
注册时间
2016-1-23
在线时间
440 小时
最后登录
2018-12-17
发表于 2018-11-12 21:07:04 | 显示全部楼层
ghkghk vmgbmnb
[发帖际遇]: wasdml123 乐于助人,奖励 1 蛮牛币. 幸运榜 / 衰神榜

回复

使用道具 举报

3偶尔光临
174/300
排名
63302
昨日变化
52

0

主题

80

帖子

174

积分

Rank: 3Rank: 3Rank: 3

UID
198139
好友
0
蛮牛币
23
威望
0
注册时间
2017-1-2
在线时间
92 小时
最后登录
2018-12-16
发表于 2018-11-12 23:22:06 | 显示全部楼层
这就是我想要的!非常感谢!

回复 支持 反对

使用道具 举报

4四处流浪
322/500
排名
16534
昨日变化

0

主题

214

帖子

322

积分

Rank: 4

UID
289569
好友
0
蛮牛币
168
威望
0
注册时间
2018-7-12
在线时间
66 小时
最后登录
2018-12-17
发表于 2018-11-13 08:25:47 | 显示全部楼层
够详细 谢谢楼主

回复

使用道具 举报

7日久生情
1780/5000
排名
2257
昨日变化

12

主题

830

帖子

1780

积分

Rank: 7Rank: 7Rank: 7Rank: 7

UID
218409
好友
3
蛮牛币
5260
威望
0
注册时间
2017-4-19
在线时间
348 小时
最后登录
2018-12-14
发表于 2018-11-13 09:16:40 | 显示全部楼层
66666666666666666666

回复 支持 反对

使用道具 举报

6蛮牛粉丝
1375/1500
排名
3785
昨日变化

0

主题

810

帖子

1375

积分

Rank: 6Rank: 6Rank: 6

UID
210390
好友
0
蛮牛币
1658
威望
0
注册时间
2017-3-7
在线时间
183 小时
最后登录
2018-12-17
发表于 2018-11-13 09:26:29 | 显示全部楼层

回复

使用道具 举报

6蛮牛粉丝
1224/1500
排名
2450
昨日变化
1

0

主题

305

帖子

1224

积分

Rank: 6Rank: 6Rank: 6

UID
119648
好友
3
蛮牛币
1730
威望
0
注册时间
2015-8-25
在线时间
357 小时
最后登录
2018-12-14
QQ
发表于 2018-11-13 09:28:11 | 显示全部楼层
够详细 谢谢楼主 学习了

回复 支持 反对

使用道具 举报

您需要登录后才可以回帖 登录 | 注册帐号

本版积分规则

快速回复 返回顶部 返回列表