Lumieru的知识库

  • 首页

  • 标签

  • 分类

  • 归档

  • 搜索

UMA 201视频教程备忘录

发表于 2019-11-14 | 分类于 Unity
  1. 制作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,就是保持原来的贴图大小。这样分辨率就高了,当然会占用更多的资源。

      UMA 201 - Part 2a Skin Overlays

  2. 优化Overlay

    • 如果制作的Overlay是一张很大的图,但是只用到了其中很小的一部分,其他部分都是空白,那么会浪费很多最后游戏包的空间(runtime内存是不浪费的,因为UMA最终会合成一张贴图)。
    • 到ps中,找到一个小的长方形区域(2的幂次方),可以完全包含需要的那部分贴图,记住这个区域和整张图的左上角的偏移量。
    • 然后把原图切小成小的长方形区域大小,并保存放回unity中覆盖原图。
    • 回到Overlay的设置界面,设置其中的Rect参数。因为ps中偏移量是从原图左上角到小区域的左上角,而unity需要的是原图左下角,到小区域的左下角,所以这里的偏移量需要做计算的,如下图所示。最后还要填上小图的尺寸大小。
      OverlayOffset.png
    • 然后要更新一下对应的Wardrobe Recipe,在其中删除老的Overlay,重新添加修改的Overlay,就大功告成了。

      UMA 201 - Part 2b Optimising Overlays

  3. 把其他的模型适配到UMA第一节

    • 把fbx导入到blender中,注意导入的选项,保证骨骼的方向是正确的。
    • 通过骨骼的旋转和缩放等,把导入的模型和UMA模型基本重叠起来。
    • 把旋转和缩放都bake到模型的顶点上,确保骨骼和模型没有旋转和缩放。
    • 把模型和骨骼解除绑定,并删除其中的权重信息。

      UMA 201 - Part 3a Preparing Pre-Rigged Clothing

阅读全文 »

UMA 101视频教程备忘录

发表于 2019-11-14 | 更新于 2019-11-15 | 分类于 Unity
  1. UMA_DCS prefab 加上 UMA Dynamic Character Avatar, 选择一个race,然后设置好默认的animator controller就可以运行了。

    UMA 101 - Part 2 Up and Running

  2. base recipe –> 确定一个裸体的人物模型。
    wardrobe recipe –> 确定人物的衣服,鞋子,饰品等;这里可以设置A物体覆盖B物体。

    UMA 101 - Part 3 Introducing Recipes

  3. Dynamic Character Avatar中的Default Recipes可以为这个人物添加默认的衣服裤子等。每个Wardrobe recipe都只能和相对应的race一起才能正常工作。Wardrobe slot是设置这个物体是放在人身上的哪个部位的。还可以设置需要覆盖哪个部位的其他物件和base物件。

    UMA 101 - Part 4 Default Recipes

  4. Dynamic Character Avatar中的Character Colors可以重载recipes中的shared colors,然后在slot中可以用shared color来影响overlay的颜色。这样就可以通过设置Character Colors来方便的设置人物的整体颜色。

    UMA 101 - Part 5 Shared Colours

  5. 所有的recipes和他们所用到的components都需要到UMA Global Library中去注册才能使用。制作DCS->wardrobe recipe

    UMA 101 - Part 7 Creating Custom Recipes

  6. Misc->Mesh Hide Asset,可以用来隐藏某个slot上的某些三角形,已解决衣服和身体部分重叠的情况下的穿模问题。UMA内建工具可以编辑需要去掉那些三角形。在编辑的时候还可以把衣服覆盖在身体上,方便查看需要隐藏哪些三角形。

    UMA 101 - Part 8 Advanced Occlusion

阅读全文 »

Math Magician - Lerp, Slerp, and Nlerp

发表于 2019-07-23 | 分类于 GameEngine

Math Magician – Lerp, Slerp, and Nlerp

原文出处

Illustration of linear interpolation on a data...

Illustration of linear interpolation on a data set. The same data set is used for other interpolation methods in the interpolation article. (Photo credit: Wikipedia)

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源码解读五--转载二

发表于 2019-06-28 | 分类于 GameServer

原文地址

此笔记是本人在开发及研读的过程中记录下来的,由于没整理,会看得有些吃力,请读者视能力而读,个人理解,如有问题,悉心接受。

部分引用KBEngine官网的一些句段,感谢kbe。

每个cell在被创建时当被观察者观察到时(也就是进入到别人的AOI里)那么会被调用kbe底层回调python层onWitnessed方法,

当然只在增加观察者时第一次被观察到或者删除观察者时观察者个数为0时才回调,通过参数1或者0进行区别,看以下代码

KBE2_1.png
KBE2_2.png

controlledBy:kbe底层提供了可以把一个非客户端持有的cell上的entity给客户端控制,由客户端来进行可以控制的改变权限是改变方向和位置,

但是这个entity必须是在控制者的entity(demo里的avatar的entity)的Aoi范围内,而且这个控制者是必须被客户端控制的一个entity,

具有aoi也就是还必须是具有cell的,操作如下:

在cell上self.controlledBy = base(操作者的base)

阅读全文 »

KBEngine源码解读四--转载一

发表于 2019-06-28 | 分类于 GameServer

原文地址

此笔记是本人在开发及研读的过程中记录下来的,由于没整理,会看得有些吃力,请读者视能力而读,个人理解,如有问题,悉心接受。部分引用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源码解读三

发表于 2019-06-28 | 更新于 2019-07-24 | 分类于 GameServer

BaseApp::createEntityAnywhere

1
2
3
4
5
6
7
8
9
10
11
12
// 把Entity的类型和参数全部序列化,然后发送给Baseappmgr,由Baseappmgr::reqCreateEntityAnywhere负责创建。
BaseApp::createEntityAnywhere
// 首先会找一个没有一个实体的baseapp或者是所有baseapp中负载最小的baseapp。
// 然后再调用这个baseapp的onCreateEntityAnywhere函数
--> Baseappmgr::reqCreateEntityAnywhere
// 把数据反序列化,然后调用createEntity函数创建。创建完成后,如果发起方和创建方是相同的,
// 则直接调用_onCreateEntityAnywhereCallback返回创建结果。如果不相同,则发送消息给发起方,
// 调用发起方的onCreateEntityAnywhereCallback函数返回结果。
--> BaseApp::onCreateEntityAnywhere
--> BaseApp::onCreateEntityAnywhereCallback //调用_onCreateEntityAnywhereCallback
// 如果有设置python的callback,就调用python的callback。如果是其他baseapp创建的,则创建并保存成EntityCall。
-> BaseApp::_onCreateEntityAnywhereCallback

Baseapp::createEntityAnywhereFromDBID

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
Baseapp::createEntityAnywhereFromDBID
// 首先会找一个没有一个实体的baseapp或者是所有baseapp中负载最小的baseapp。
// 然后再调用这个baseapp的onGetCreateEntityAnywhereFromDBIDBestBaseappID函数
--> Baseappmgr::reqCreateEntityAnywhereFromDBIDQueryBestBaseappID
--> Baseapp::onGetCreateEntityAnywhereFromDBIDBestBaseappID
// 常见DBTaskQueryEntity,添加到bufferedDBTasksMaps_中,然后有task完成任务
--> Dbmgr::queryEntity
// 从数据库中查询相关Entity的信息
-> DBTaskQueryEntity::db_thread_process
// 回到主线程,因为query mode是1,所以调用onCreateEntityAnywhereFromDBIDCallback
-> DBTaskQueryEntity::presentMainThread
--> Baseapp::onCreateEntityAnywhereFromDBIDCallback
// 消息中包含了要在那个Baseapp上创建,就是前面决定的那个Baseapp。
--> Baseappmgr::reqCreateEntityAnywhereFromDBID
// 创建Entity,并调用回调函数onCreateEntityAnywhereFromDBIDOtherBaseappCallback,
// 如果是同一个Baseapp就直接回调,否则就是RPC回调
--> Baseapp::createEntityAnywhereFromDBIDOtherBaseapp
// 如果有python的callback id,就调用python的callback函数。
--> Baseapp::onCreateEntityAnywhereFromDBIDOtherBaseappCallback

Baseapp::createCellEntityInNewSpace

1
2
3
4
5
6
7
8
9
10
11
12
// 在cellapp上创建一个空间(space)并且将该实体的cell创建到这个新的空间中,它请求通过cellappmgr来完成。
Baseapp::createCellEntityInNewSpace
// 如果CellappIndex > 0,那么用这个index模总的cellapp数量,得到目标cellapp。
// 如果CellappIndex == 0,那么找到负载最小的cellapp作为目标cellapp。
--> Cellappmgr::reqCreateCellEntityInNewSpace
// 通过SpaceMemorys::createNewSpace(spaceID, entityType)创建一个新的SpaceMemory(就是一个space)。
// 并且创建Cellapp上的Entity,设置Baseapp的EntityCall,如果有Client,则还设置Client的EntityCall,并创建Witness。
// 把新创建的Entity加入到SpaceMemory中。
// 在SpaceMemory的构造函数/析构函数中,还会调用Cellappmgr::updateSpaceData。Cellappmgr好像也维护了
// 每个Cellapp中的Space信息,所有需要更新一下。
--> Cellapp::onCreateCellEntityInNewSpaceFromBaseapp
--> Baseapp::onEntityGetCell

Baseapp::createCellEntity

1
2
3
4
5
6
7
8
// 请求在一个cell里面创建一个关联的实体。
Baseapp::createCellEntity
--> Cellapp::onCreateCellEntityFromBaseapp
// 根据spaceID找到对应的SpaceMemory,然后在该SpaceMemory中创建Entity,
// 设置Baseapp的EntityCall,如果有Client,则还设置Client的EntityCall,并创建Witness。
// 把新创建的Entity加入到SpaceMemory中。
-> Cellapp::_onCreateCellEntityFromBaseapp
--> Baseapp::onEntityGetCell
阅读全文 »

KBEngine源码解读二组件互联

发表于 2019-06-22 | 更新于 2019-06-23 | 分类于 GameServer

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源码解读一

发表于 2019-06-14 | 更新于 2019-06-22 | 分类于 GameServer

一. libs/common部分

MemoryStream:

将常用数据类型二进制序列化与反序列化,内部封装了一个std::vector<uint8>。使用方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
MemoryStream 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
2
TimerHandle add(TimeStamp startTime, TimeStamp interval,
TimerHandler* pHandler, void * pUser);

来新增一个定时器,返回的TimerHandle是新增定时器的句柄,可以控制相应的定时器。用户需要继承TimerHandler类,并重载

1
virtual void handleTimeout(TimerHandle handle, void * pUser)

虚函数来实现定时器的回调。

内部预定义了两个TimersT<T>类

1
2
typedef TimersT<uint32> Timers;
typedef TimersT<uint64> Timers64;

阅读全文 »

教你从头写游戏服务器框架三

发表于 2019-06-04 | 更新于 2019-06-22 | 分类于 GameServer

原文出处

协程

使用异步非阻塞编程,确实能获得很好的性能。但是在代码上,确非常不直观。因为任何一个可能阻塞的操作,都必须要要通过“回调”函数来链接。比如一个玩家登录,你需要先读数据库,然后读一个远程缓冲服务器(如 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 了:

  1. DataStore -> CoroutineDataStore

  2. Cache -> CoroutineCache

  3. Client -> CoroutineClient

使用协程的 API,就完全不需要各种 Callback 类型的参数了,完全提供一个返回结果用的输出参数即可。

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
/**
* @brief DataStore 的具备协程能力的装饰器类型。
* @attention 除了定义变量语句和 Update() 以外,其他的操作都需要在协程中调用。
*/
class CoroutineDataStore : public Updateable{
public:
CoroutineDataStore(DataStore* data_store, CoroutineSchedule* schedule);
virtual ~CoroutineDataStore();

int Init(Config* cfg, std::string* err_msg);

/**
* 读取一个数据对象,通过 key ,把数据放入到输出参数 value。
* 此函数会在调用过程中使用协程的 yield 出去。
*/
int Get(const std::string&key, Serializable* value);

/**
* 写入一个数据对象,写入 key ,value
* 写入结果从返回值获得,返回 0 表示成功,其他值表示失败。
* 此函数会在调用过程中使用协程的 yield 出去。
*/
int Put(const std::string&key, const Serializable& value);

/**
* 删除一个数据对象,通过 key
* 写入结果从返回值获得,返回 0 表示成功,其他值表示失败。
* 此函数会在调用过程中使用协程的 yield 出去
*/
int Remove(const std::string& key);

int Update();

private:
DataStore* data_store_;
CoroutineSchedule* schedule_;
};
阅读全文 »

教你从头写游戏服务器框架二

发表于 2019-06-04 | 更新于 2019-06-22 | 分类于 GameServer

原文出处

对象序列化

现代编程技术中,面向对象是一个最常见的思想。因此不管是 C++ Java C#,还是 Python JS,都有对象的概念。虽然说面向对象并不是软件开发的“银弹”,但也不失为一种解决复杂逻辑的优秀工具。回到游戏服务器端程序来说,自然我会希望能有一定面向对象方面的支持。所以,从游戏服务器端的整个处理过程来看,我认为,有以下几个地方,是可以用对象来抽象业务数据的:

  1. 数据传输:我们可以把通过网络传输的数据,看成是一个对象。这样我们可以简单的构造一个对象,然后直接通过网络收、发它。

  2. 数据缓存:一批在内存中的,可以用对象进行“抽象”。而 key-value 的模型也是最常见的数据容器,因此我们可以起码把 key-value 中的 value 作为对象处理。

  3. 数据持久化:长久以来,我们使用 SQL 的二维表结构来持久化数据。但是 ORM (对象关系映射)的库一直非常流行,就是想在二维表和对象之间搭起桥梁。现在 NoSQL 的使用越来越常见,其实也是一种 key-value 模型。所以也是可以视为一个存放“对象”的工具。

对象序列化的标准模式也很常见,因此我定义成:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class Serializable {
public:

   /**
    * 序列化到一个数组中
    * @param buffer 目地缓冲区数组
    * @param buffer_length 缓冲区长度
    * @return 返回写入了 buffer 的数据长度。如果返回 -1 表示出错,比如 buffer_length 不够。
    */
   virtual ssize_t SerializeTo(char* buffer, int buffer_length) const = 0;

   /**
    * @brief 从一个 buffer 中读取 length 个字节,反序列化到本对象。
    *@return  返回 0 表示成功,其他值表示出错。
    */
   virtual int SerializeFrom(const char* buffer, int length) = 0;

   virtual ~Serializable(){}

};
阅读全文 »
123…5

Lumieru

46 日志
8 分类
17 标签
© 2022 Lumieru
由 Hexo 强力驱动 v3.9.0
|
主题 – NexT.Pisces v7.1.1