@leviyuan
2017-08-04T09:48:20.000000Z
字数 14850
阅读 326

字典中的key是要排序的数据
value是排序的依据,类型为table数组,每项值为number类型
-- 创建一个排序器,排序依据的table有最多4个值-- 依次按照升序、升序、降序、降序的方式排列local compare = utils.table.compare.makearr(true, true, false, false)-- 使用这个排序器创建一个orderdictlocal dict = utils.table.createorderdict(compare)-- 添加一些数据dict["chiuan"] = {28, 15, 236, 0}dict["levi"] = {27, 0, 0, 0}dict["test"] = {28, 15, 0, 0}-- 调用接口测试一下-- 数据的数量print(dict:getlen()) -- 3-- 获取排序在第2位的数据print(dict:getat(2)) -- test-- 完整遍历一下for k,v in dict:ipairs() doprint(k, v) -- 1,levi 2,test 3,chiuanend-- 删除一项dict["test"] = nil-- 下面这样会导致error “orderdict do not support same order value”dict["test_error"] = {27, 0, 0, 0}
与原生table基本一样,唯一差别就是保证pairs的遍历顺序永远与添加进去的顺序一致
-- 创建一个有序字段local dict = utils.table.createsortdict()-- 添加一些数据dict["levi"] = 27dict["chiuan"] = {}dict["test"] = true-- 使用接口测试一下print(dict:getfirst()) -- 27print(dict.count) -- 3-- 完整遍历for k,v in dict:pairs() doprint(k, v) -- levi,27 chiuan,table0x0000 test,trueend-- 删除或更新会让这一项移动到最末位dict["levi"] = 1print(dict:getfirst()) -- table0x0000dict["chiuan"] = {}print(dict:getfirst()) -- true

e Entity 实体
类似于Unity的GameObject,仅仅只是一个东西,没有任何直接用处的一个东西
可以理解为载体、容器
c Component 组件
组件是放在在Entity里面的东西,可以拥有各种属性
与Unity中的Component类似,但只可以有属性,不能有方法
s System 系统
ecs世界里真正执行逻辑的部分,一般不可以存储任何属性
ecs提供了一套便捷的API让每一个system都可以快速获取到要处理的数据
各种不同的system分工合作,达到最终目的
pool 池
pool是entity的容器,某些情况下需要多个ecs世界并行,pool是最好的分隔方案
matcher 匹配器
matcher是system取数据的依据,作用对象是entity
matcher之于ecs,相当于sql之于数据库
group 组
符合相同matcher的一组entity称为group
与group相关有一些事件可以使用,例如:有新的entity加入指定group
在group之内,可以预设排序、索引等高级查询功能
有一个流水线,要做一种罐头
创建一个pool,名字叫罐头制作流水线
罐头由瓶子、盖子、水、糖浆、水果组成
我们最终要创建出的entity包含瓶子、盖子、水、糖浆、水果这五种component
流水线上有5道工序
需要5个system来实现这个过程
第一道工序,在合理的情况下(下游没有累积过多、总需求量没超),向流水线上放空瓶子
第一个system,在合适的时候,创建空entity,并添加瓶子组件
第二、三、四道工序,分别对应水、糖浆、水果,检查瓶子里没有自己要加的东西时,添加进去
第二、三、四个system,在entity上没有自己对应组件时,添加给entity
第五道工序,检查其他的都已经齐全,把盖子盖上
第五个system,当其他四个component都已经添加时,为entity添加盖子component
流水线把产品制作分成不同模块,方便分别改进优化
流水线为每个工人提供工作环境,单个工人不需要在意整体流程
产品出现问题时,可以在流水线上查找产品定位问题到单一工人
承担相同工作的工人可以在流水线上随意替换
ecs把流水线的好处带给coding
使用ecs最大的难点是要转变设计思路,它与面向对象的程序设计思路相差甚远
下面从一个实际的需求开始讲述如何使用ecs的思路来设计解决方案
需求:服务器列表功能
1、服务器有 编号、名称、分组、排序、状态属性需要显示
2、需要自动选择玩家上次登录的服务器
3、玩家可以手动刷新服务器状态
4、服务器列表中要显示玩家在该服务器拥有的角色基本信息
这一步需要设计出功能需要数据的最精简结构
这部分数据将是功能的核心数据,其他的数据都可以通过计算或者数据查询得到
在这个示例需求中,最核心的数据就是
服务器固定信息:{编号、名称、分组、序号}
服务器动态信息:{编号、状态}
玩家信息:{上次登录的服务器编号}
角色信息:{编号、名称、等级、所属服务器}
这样一个数据结构
PS:设计基础数据时要保证不同时变化的数据需要分开,示例中的服务器固定信息和动态信息
分开的好处是方便做数据匹配,可以让系统设计更加容易
在完整的数据结构里,将会数据的 整合、分组 设计
然后得出模块的Component设计
整合
整合的过程与面向对象设计思路类似,把相关数据合在一起形成对象形式,给Entity赋予实际含义
服务器固定信息、服务器动态信息都是服务器的数据,实际的逻辑是把这两个数据在一起使用
玩家上次登录的服务器实际上是服务器的一个标签
在需要的功能里,每个角色要跟随服务器来展现
所以我们有如下的设计:
ServerEntity { // 一个服务器 ServerInfoComponent { // 服务器的基本信息 id、name、group、order } ServerStateComponent {// 服务器状态信息 state } isLastLogin // 是否是上一次登录的服务器 ServerRoleListComponent {// 包含的角色id列表 list } } RoleInfoEntity { // 一个角色 RoleInfoComponent { // 角色信息 id、name、level、serverid } }
分组
把同类的Entity通过Component标识分组,然后加上索引或排序规则以便进行实际逻辑处理
整合过程与面向对象思路很像,但这里绝不是简单的按照“对象类型”分组
举一个简单的例子:
人、汽车、空气 都可以移动
人、汽车可以被看见,空气不行
用面向对象思路来归类这三个东西已然GG
在ecs的思路里,设计两个分组即可,分别是:可以移动的东西、可以被看见的东西
这个分组概念与GO语言的interface更接近,可以参考来理解
索引:参考sql数据的索引设计概念来理解,为分组内的数据设计快速查询方式(而不是遍历)
排序:按照实际需求做排序规则,以便具体逻辑直接使用
Group(ServerInfoComponent) { // 服务器数据组,ServerInfoComponent是分组标识 index : ServerInfoComponent.id // 通过id索引一个服务器 order : ServerInfoComponent.order // 通过order排序服务器 } Group(RoleInfoComponent) { // 角色数据组,RoleInfoComponent是分组标识 index : RoleInfoComponent.id // 通过id索引一个角色 index : RoleInfoComponent.serverid // 通过id索引一个角色 }
这一步将会得到一些System的设计结论,从以下两个方向来思考
基础数据获得
完整数据结构组织
汇总一下前面的结论,所有的Component如下:
RoleInfoComponent
所有的System如下(做了适当整合):
system:setreactiveexecute(func(self, entities, count))
设置系统为触发式,每次被轮询,如果有收集到触发的entity,就回调接口,并传入所有触发了的entity
因性能缘故,entities并不是一般的luatable,只能使用如下方式遍历:
for i = 1, count dolocal entity = entities[i]-- do some thing to entityend
ps:如果一个entity触发了条件,但是在执行前被删除,或者不满足触发执行的确认条件,entities中将不包含此entity;这种情况下,count值可能为0
system:addtrigger(triggerEvt)
为触发式system添加触发侦听条件
matcher代表指定的group,evtType有三种值
简单的matcher有缩写方式来指定triggerEvt
ecs.matcher.[componentname].[added|removed|addedorremoved]
ecs.matcher.[componentname].any.[added|removed|addedorremoved]
ecs.matcher.[componentname].none.[added|removed|addedorremoved]
ecs.component.regist("A", "a")ecs.component.regist("B", "b")local p = ecs.pool.create("test")local group = p:getgroup(ecs.matcher.A)-- 用B组件的属性来做索引计算,但是group的声明中并没有涉及到B-- ecs内部更新时,不会认为B组件的变化与这个group有任何关系-- 所以索引值完全不正确,没法办法去使用group:addindex("b", function(entity)return entity.hasB and entity.B.b or 0end)-- 实际的逻辑中,如果用A+B作为group的特征描述也ok,那么可以这样声明groupgroup = p:getgroup(ecs.matcher.A, eca.matcher.B)-- 如果只有A可以作为特征描述,B不与特征有直接关系,那么可以这样做-- 本质与getgroup(ecs.matcher.A)相同,但是向ecs系统说明了这个group与B有关group = p:getgroup(ecs.matcher.A, eca.matcher.A.any, eca.matcher.B.any)
geoup:addorder(ordername, filter(entity), compare, ...)
为group中的entity添加一个名为ordername排序
filter:过滤函数,nil表示不过滤,全部参与排序;函数的返回值表示是否参与排序
compare:比较函数,排序的内部实际上就是orderdict,compare等价orderdict的参数
{...}:排序计算使用的index,性能限制,只能使用索引值进行排序
group:ipairs(ordername)
按照指定的排序方式,遍历entity
for i, entity in group:ipairs("ordername") doprint(i, entity)end
{-- using UnityEngine;"A.B.*","C.*",-- using C = UnityEngine.Camera;c = "D.EE",f = "D.XXXX"}
没有声明shorttable的情况下,ecs系统会在全局搜索以'.componentname'结尾的组件,如果只有一个,那就当做是这个组件来处理,如果不止一个,将会抛出error,"componet name XXXXX is not explicited"
以示例的shorttable来说明组件搜索过程:
1、查找“c = "D.EE"”这样的格式,如果匹配,那么就是等号右边就是组件的fullname
2、依次把componentname代入"A.B.*"这样格式,查找是否存在这样fullname的组件,如果有,查找结束
3、无视shorttable,去全局查找,有可能出现"componet name XXXXX is not explicited"的报错
EntityEditor
pool-GameObject的children是Entity-GameObject,有
标识

对于每一项component

SystemsEditor
在Hierarchy中,DonotDestroyOnLoad下,有
标注的是System-GameObject

使用FairyGUI引擎 http://www.fairygui.com/
-- 定义游戏中界面的层级,数值越小越靠底部local layer = {scene = 3,hud = 4,default = 5,guide = 10,tips = 11,loading = 12}-- 为ui的layer设置默认值ui.defaultlayer = layer.default-- 添加资源包ui.addpackage("主界面") -- 声明ui资源中有一个包叫"主界面":dep("通用按钮") -- "主界面"资源包引用了"通用按钮"资源包,被依赖的资源包不必在引用之前声明:add("home", -- 必填字段,ui的名称"主面板", -- 可选字段,ui素材在资源包中的名称;默认值为空--使用默认值则不能创建显示对象,只能使用ui.apply接口打开ui"pkm.ui.home.main", -- 可选字段,ui脚本的加载路径,默认值为空true, -- 可选字段,界面是否是单例的,默认值为truelayer.default, -- 可选字段,界面的层级,默认值为ui.defaultlayer{"home.*"} -- 可选字段,ui脚本中的ecs接口缩写说明,默认为空)
界面脚本被闭包括起来,可以读取全局环境,但不可以直接修改全局环境,所有的global写操作都会卸载ui逻辑对象上
-- 初始化界面function initcom()herobtn = getchild("pannel/herobtn") --mainview:GetChild("pannel"):GetChild("herobtn")herobtn.onClick:Add(function() -- 为按钮添加监听ui.open("hero", 1) -- 打开hero界面,并且传一个参数 1end)pvebtn = mainview:GetChild("pvebtn")end-- 界面显示出来时候调用,会接到透穿参数function show(p1, p2)print("open params", p1, p2)end-- 被关闭时调用,可选,mainview已经被释放function onclose()print("home ui closed")end-- 自定义方法function customfunc(...)end
使用开源日志工具 TTConsole https://github.com/chiuan/TTConsole
for i = 0,param.Length do print(param[i]) end 来遍历 ~按键打开console,移动端使用三根手机按屏幕的上半部分打开?,可以列出所有可用命令列表 2u,可以把指定类型的日志输出到unity的consolerun,可以在命令行中交互式地执行lua脚本,包括远程consoleTTConsole_Remote_Unity.exe就是RemoteConsole的PC端