制作Overlay
- 先准备好Overlay用到的贴图,每个Overlay单独一个文件夹,把贴图放在文件夹下面。
- 菜单UMA->Core->Overlay Asset,设置好名字,然后设置Material(是UMA专用的Material),一般选择UMA_Diffuse_Normal_Metallic,选择对应的贴图数量,这个材质是3个贴图。
- 把创建好的Overlay放入到UMA Global Library中。
- 创建对应的Wardrobe Recipe,菜单UMA->DCS->Wardrobe Recipe,选择对应的race,选择Wardrobe Slot,选择对应的Base Slot,最后Add Overlay。
- 把创建好的Wardrobe Recipe放入到UMA Global Library中。
PS:如果发现做好的Overlay分辨率比原始素材要低,那是因为UMA在生成最终贴图的时候,会把分辨率减半。这个是可以设置的,在UMA_DCS->UMAGenerator->Initial Scale Factor,默认是2,就是贴图尺寸减半,如果改成1,就是保持原来的贴图大小。这样分辨率就高了,当然会占用更多的资源。
优化Overlay
- 如果制作的Overlay是一张很大的图,但是只用到了其中很小的一部分,其他部分都是空白,那么会浪费很多最后游戏包的空间(runtime内存是不浪费的,因为UMA最终会合成一张贴图)。
- 到ps中,找到一个小的长方形区域(2的幂次方),可以完全包含需要的那部分贴图,记住这个区域和整张图的左上角的偏移量。
- 然后把原图切小成小的长方形区域大小,并保存放回unity中覆盖原图。
- 回到Overlay的设置界面,设置其中的Rect参数。因为ps中偏移量是从原图左上角到小区域的左上角,而unity需要的是原图左下角,到小区域的左下角,所以这里的偏移量需要做计算的,如下图所示。最后还要填上小图的尺寸大小。
然后要更新一下对应的Wardrobe Recipe,在其中删除老的Overlay,重新添加修改的Overlay,就大功告成了。
把其他的模型适配到UMA第一节
- 把fbx导入到blender中,注意导入的选项,保证骨骼的方向是正确的。
- 通过骨骼的旋转和缩放等,把导入的模型和UMA模型基本重叠起来。
- 把旋转和缩放都bake到模型的顶点上,确保骨骼和模型没有旋转和缩放。
把模型和骨骼解除绑定,并删除其中的权重信息。
UMA 101视频教程备忘录
UMA_DCS prefab 加上 UMA Dynamic Character Avatar, 选择一个race,然后设置好默认的animator controller就可以运行了。
base recipe –> 确定一个裸体的人物模型。
wardrobe recipe –> 确定人物的衣服,鞋子,饰品等;这里可以设置A物体覆盖B物体。Dynamic Character Avatar中的Default Recipes可以为这个人物添加默认的衣服裤子等。每个Wardrobe recipe都只能和相对应的race一起才能正常工作。Wardrobe slot是设置这个物体是放在人身上的哪个部位的。还可以设置需要覆盖哪个部位的其他物件和base物件。
Dynamic Character Avatar中的Character Colors可以重载recipes中的shared colors,然后在slot中可以用shared color来影响overlay的颜色。这样就可以通过设置Character Colors来方便的设置人物的整体颜色。
所有的recipes和他们所用到的components都需要到UMA Global Library中去注册才能使用。制作DCS->wardrobe recipe
Misc->Mesh Hide Asset,可以用来隐藏某个slot上的某些三角形,已解决衣服和身体部分重叠的情况下的穿模问题。UMA内建工具可以编辑需要去掉那些三角形。在编辑的时候还可以把衣服覆盖在身体上,方便查看需要隐藏哪些三角形。
Math Magician - Lerp, Slerp, and Nlerp
Math Magician – Lerp, Slerp, and Nlerp
Man, I wish I had this blog going through school, because this place has become my online notebook. Game development is like riding a bike – but math, for me, can have a hard time stickin’. I’ve been working in databases so long that I have a lot of bit-shifting down, but my matrix and vector math is starting to lack. So, a creative way for me to remember all these algorithms is to try to explain them. Today, I’m going through Linear Interpolation, Spherical Linear Interpolation, and Normalized Linear Interpolation.
KBEngine源码解读五--转载二
此笔记是本人在开发及研读的过程中记录下来的,由于没整理,会看得有些吃力,请读者视能力而读,个人理解,如有问题,悉心接受。
部分引用KBEngine官网的一些句段,感谢kbe。
每个cell在被创建时当被观察者观察到时(也就是进入到别人的AOI里)那么会被调用kbe底层回调python层onWitnessed方法,
当然只在增加观察者时第一次被观察到或者删除观察者时观察者个数为0时才回调,通过参数1或者0进行区别,看以下代码
controlledBy:kbe底层提供了可以把一个非客户端持有的cell上的entity给客户端控制,由客户端来进行可以控制的改变权限是改变方向和位置,
但是这个entity必须是在控制者的entity(demo里的avatar的entity)的Aoi范围内,而且这个控制者是必须被客户端控制的一个entity,
具有aoi也就是还必须是具有cell的,操作如下:
在cell上self.controlledBy = base(操作者的base)
KBEngine源码解读四--转载一
此笔记是本人在开发及研读的过程中记录下来的,由于没整理,会看得有些吃力,请读者视能力而读,个人理解,如有问题,悉心接受。部分引用KBEngine官网的一些句段,感谢kbe。
下列符号解释 =:继承 ==:等同 +:与上具有子关系
CellAppMgr:管理多个CellApp
+CellApp:管理多个区域
+ Cell:一个区域
==Space:等同Cell
+此区域上的玩家entity:代表玩家
+Witness(对象):监视周围的玩家entity,将发生的事件消息同步给客户端
+AOI(兴趣范围):默认500M
+GhostEntitys(list):存取此区域边沿外界一定距离内的玩家entity的列表
+GhostEntity:从邻近的Cell的对应的entity的部分数据的拷贝的实体
+属性数据:只读,如果某个属性对于客户端是可见的,那么该属性必须是可以存在
Ghost的,例如:当前的武器、等级、名称
+范围:默认500M,可配置,大于等于玩家的AOI
+负载平衡:CellApp会告诉它们的Cell的边界应该在哪里
+新建的玩家Entity加入到正确的Cell上
+一个服务器组一个Mgr实例
DBMgr:管理Entity数据的数据库存储
- +存数据:在BaseApp间轮流调度处理,BaseApp向CellApp要entity的cell部分的数据再定时转给DBMgr存储
Machine:监视服务器进程信息,每个服务器机器上有一个machine
+作用:启动/停止服务器进程,通知服务器群组各个进程的存活状态,监视机器的使用状态:cpu/内存/带宽
+machine不会tcp连接的
ObjectPool:对象池,一些对象频繁的被创建,例如:moneystream,bundle,tcppacket等等,这个对象池通过服务端峰值有效的预估提前创建一些对象缓存起来,在用到的时候直接从对象池中提取
SmartPoolObject:智能池对象,创建一个智能池对象,与相应的对象池绑定,只要此对象析构时,那么会调用绑定的对象池进行回收
Components:组件类,记录当前组件信息
ServerApp:App基类,每个app都需继承这个类,基本的C++框架模块 都会存在这个类,其中有继承通道超时操作,继承通道取消注册操作,
Resmgr:资源类,存有一切环境信息,例如bin或者资源目录,和一些已打开文件
ThreadPool:线程池类
KBEngine源码解读三
BaseApp::createEntityAnywhere
1 | // 把Entity的类型和参数全部序列化,然后发送给Baseappmgr,由Baseappmgr::reqCreateEntityAnywhere负责创建。 |
Baseapp::createEntityAnywhereFromDBID
1 | Baseapp::createEntityAnywhereFromDBID |
Baseapp::createCellEntityInNewSpace
1 | // 在cellapp上创建一个空间(space)并且将该实体的cell创建到这个新的空间中,它请求通过cellappmgr来完成。 |
Baseapp::createCellEntity
1 | // 请求在一个cell里面创建一个关联的实体。 |
KBEngine源码解读二组件互联
server/Components
组件在互相发现的时候用的是UDP广播,找到对应组件知道其IP和Port后,用TCP来建立并保持连接。
machine组件会监听20086端口,然后其他组件都会往这个端口广播自己的信息(RPC调用MachineInterface::onBroadcastInterface
函数)。然后machine收到信息后会判断有效性(有没有和其他组件用了相同的设置),如果无效会发回一个无效的通知给该组件,然后该组件就会退出。如果有效的则,machine会记录和自己同一台物理机上的组件到自己的组件列表中(不同物理机的组件会被忽略掉)。
然后组件会有一个需要找的组件的类型的列表,对于每种类型,它都会广播MachineInterface::onFindInterfaceAddr
这个消息到machine。machine收到这个消息后,会把已经注册过的本地同类型组件发回给该组件。如果没有找到对应的类型,会定时到下一个循环继续找,直到找到自己感兴趣的所有类型的组件为止。
external port和telnet的port都是通过配置文件指定好的,internal port传的零,就是让系统决定一个可用的随机端口,bind成功后在用getsockname()
得到具体的端口。
下面是每个组件感兴趣的列表:
组件类型 | 感兴趣的类型 |
---|---|
Cell app | logger, dbmgr, cellAppMgr, baseAppMgr |
Base app | logger, dbmgr, baseAppMgr, cellAppMgr |
Base app mgr | logger, dbmgr, cellAppMgr |
Cell app mgr | logger, dbmgr, baseAppMgr |
db mgr | logger |
在连接其他组件时,会调用被连接组件的XXX::onRegisterNewApp
函数。当baseApp或者cellApp连接Dbmgr
时,同样会调用Dbmgr::onRegisterNewApp
。在这个函数中,如果连接者是baseApp或者cellApp,那么就会将自己注册到所有其他baseapp和cellapp中。主要是通过遍历已经注册在Dbmgr中的其他baseapp和cellapp,然后RPC调用相应的onGetEntityAppFromDbmgr
。在被调用的onGetEntityAppFromDbmgr
中,被调用者会去连接当前组件。这样就能让所有的baseApp和cellApp互相连接了。
KBEngine源码解读一
一. libs/common部分
MemoryStream:
将常用数据类型二进制序列化与反序列化,内部封装了一个std::vector<uint8>
。使用方法:1
2
3
4
5
6
7
8
9
10
11
12
13
14MemoryStream stream;
stream << (int64)100000000;
stream << (uint8)1;
stream << (uint8)32;
stream << "kbe";
stream.print_storage();
uint8 n, n1;
int64 x;
std::string a;
stream >> x;
stream >> n;
stream >> n1;
stream >> a;
printf("还原: %lld, %d, %d, %s", x, n, n1, a.c_str());
Tasks:
任务Task
的管理类,内部封装了std::vector<Task *>
,通过调用process()
来遍历所有Tash
的process()
虚函数。
TimersT<T>:
定时器管理类,内部用小顶堆管理所有的定时器。通过1
2TimerHandle add(TimeStamp startTime, TimeStamp interval,
TimerHandler* pHandler, void * pUser);
来新增一个定时器,返回的TimerHandle
是新增定时器的句柄,可以控制相应的定时器。用户需要继承TimerHandler
类,并重载1
virtual void handleTimeout(TimerHandle handle, void * pUser)
虚函数来实现定时器的回调。
内部预定义了两个TimersT<T>
类1
2typedef TimersT<uint32> Timers;
typedef TimersT<uint64> Timers64;
教你从头写游戏服务器框架三
协程
使用异步非阻塞编程,确实能获得很好的性能。但是在代码上,确非常不直观。因为任何一个可能阻塞的操作,都必须要要通过“回调”函数来链接。比如一个玩家登录,你需要先读数据库,然后读一个远程缓冲服务器(如 redis),然后返回登录结果:用户名、等级……在这个过程里,有两个可能阻塞的操作,你就必须把这个登录的程序,分成三个函数来编写:一个是收到客户端数据包的回调,第二个是读取数据库后的回调,第三个是读取缓冲服务器后的回调。
这种情况下,代码被放在三个函数里,对于阅读代码的人来说,是一种负担。因为我们阅读代码,比如通过日志、coredump 去查问题,往往会直接切入到某一个函数里。这个被切入阅读的函数,很可能就是一个回调函数,对于这个函数为什么会被调用,属于什么流程,单从这个函数的代码是很难理解的。
另外一个负担,是关于开发过程的。我们知道回调函数的代码,是需要“上下文”的,也就是发起回调时的数据状态的。为了让回调函数能获得发起函数的一个变量内容,我们就必须把这个变量内容放到某个“上下文”的变量中,然后传给回调函数。由于业务逻辑的变化,这种需要传递的上下文变量会不停的变化,反复的编写“放入”“取出”上下文的代码,也是一种重复的编码劳动。而且上下文本身的设置可能也不够安全,因为你无法预计,哪个回调函数会怎么样的修改这个上下文对象,这也是很多难以调试的 BUG 的来源。
为了解决这个问题,出现了所谓的协程技术。我们可以认为,协程技术提供给我们一种特殊的 return 语句:yield。这个语句会类似 return 一样从函数中返回,但你可以用另外一个特殊的语句 resume(id) 来从新从 yield 语句下方开始运行代码。更重要的是,在 resume 之后,之前整个函数中的所有临时变量,都是可以继续访问的。
当然,做 resume(id) 的时候,肯定是在进程的所谓“主循环”中,而这个 id 参数,则代表了被中断了的函数。这种可以被中断的函数调用过程,就叫协程。而这个 id ,则是代表了协程的一个数字。异步调用的上下文变量,就被自动的以这个协程函数的“栈”所取代,也就是说,协程函数中的所有局部变量,都自动的成为了上下文的内容。这样就再也不用反复的编写“放入”“取出”上下文内容的代码了。
我使用了 https://github.com/Tencent/Pebble/tree/master/src/common 项目下的 coroutine.cpp/.h 作为协程的实现者。
游戏开发中,协程确实能大大的提高开发效率。因此我认为协程也应该是 Game Server 所应该具备的能力。特别是在处理业务逻辑的 Handler 的 Process() 函数,本身就应该是一个协程函数。所以我设计了一个 CoroutineProcessor 的类,为普通的 Processor 添加上协程的能力。——基于装饰器模式。这样任何的 Processor::Process() 函数,就自然的在一个协程之中。
因为有了协程的支持,那些可能产生阻塞而要求编写回调的功能,就可以统一的变成以协程使用的 API 了:
DataStore -> CoroutineDataStore
Cache -> CoroutineCache
Client -> CoroutineClient
使用协程的 API,就完全不需要各种 Callback 类型的参数了,完全提供一个返回结果用的输出参数即可。
1 | /** |
教你从头写游戏服务器框架二
对象序列化
现代编程技术中,面向对象是一个最常见的思想。因此不管是 C++ Java C#,还是 Python JS,都有对象的概念。虽然说面向对象并不是软件开发的“银弹”,但也不失为一种解决复杂逻辑的优秀工具。回到游戏服务器端程序来说,自然我会希望能有一定面向对象方面的支持。所以,从游戏服务器端的整个处理过程来看,我认为,有以下几个地方,是可以用对象来抽象业务数据的:
数据传输:我们可以把通过网络传输的数据,看成是一个对象。这样我们可以简单的构造一个对象,然后直接通过网络收、发它。
数据缓存:一批在内存中的,可以用对象进行“抽象”。而 key-value 的模型也是最常见的数据容器,因此我们可以起码把 key-value 中的 value 作为对象处理。
数据持久化:长久以来,我们使用 SQL 的二维表结构来持久化数据。但是 ORM (对象关系映射)的库一直非常流行,就是想在二维表和对象之间搭起桥梁。现在 NoSQL 的使用越来越常见,其实也是一种 key-value 模型。所以也是可以视为一个存放“对象”的工具。
对象序列化的标准模式也很常见,因此我定义成:
1 | class Serializable { |