本来按3月份的计划,是先把王者荣耀基本模式抄完 ,并以此为基础来抽象出一套基于Lua的通用客户端框架,然后根据需求再慢慢优化。
但是,3月底GAMES系列又出了一个新课GAMES104,《现代游戏引擎:从入门到实践》。
这门课一下子燃爆了我的兴趣,于是我决定暂停客户端框架的开发计划。学完GAMES104之后再回来继续开发客户端框架。
经过这几年的观察。我发现由于算力的缘故,很多高级的技术总是选应用于端游,然后再过很多年。才被用于手游开发(有时甚至还需要各种Trick才能跑得起来)。所以,要想学习和体验最新的引擎技术,最好还是通过端游引擎。
我打算趁着这次GAMES104的课程,写一个自己的引擎。
这个引擎应该使用最新的技术和最新的硬件特性。
这个引擎的业务逻辑语言为Lua。从表现力上讲,Lua要比C和C++强不少,虽然性能会慢一点,但是因为是实验性质的引擎,开发快反而会更重要。
这个引擎应该是跨平台的。虽然我的主要目标是端游,但是我也希望像在手机算力允许的情况下,可以在手机上玩耍。
我花了一周时间把vulkan教程上的例子抄了一遍(画一一个三角形,我竟然抄了3天半 ^_^!)。
然后就开始根据GAMES104的视频课程实现引擎了。
虽然第一版引擎以Vulkan图形API为基础,但是我还是希望能先抽象的个差不多的RHI(Render Hardware Interface), 为未来支持Direct3D和Metal打下基础。
这对我来讲很难,因为我没有任何Direct3D和Metal的基础,连Vulkan也只有一个星期的经验。
我还是想试一下。
一个最容易想到的方案是,为所有图形API设计相同的接口和相同的导出结构,然后使用宏来切换平台,这也正是RHI的表面含义.
伪码如下:
//-----rhi/texture.h------
namespace rhi {
gpu_handle texture_create();
texture_destroy(gpu_handle handle);
}
//-----rhi/texture.cpp-------
#ifdef RHI_VULKAN
#include "vulkan/texture.cpp"
#elseif RHI_DIRECT3D
#include "direct3d/texture.cpp"
#elseif RHI_METAL
#include "metal/texture.cpp"
#endif
//-----render/texture2d.h
namespace render {
class texture2d {
public:
texture2d() {
gpu_texture = texture_create();
}
~texture2d() {
texture_destroy(gpu_texture);
}
private:
gpu_handle gpu_texture;
int width;
int height;
}
}
但是这么做会有一些令人纠结的问题。
以texture2d为例,在Vulkan层面去使用texture时,部分情况是需要用到width和height,write_enable,filter_mode等属性,这时需要如何去获取这些属性。
这时有三种方案:
-
第一种方案:在调用rhi::texture_create()时把所有需要用到的参数都传递过去,然后Vulkan层在内部保存供后面使用。这样做有两个坏处:数据冗余严重、需要额外的代码来将texture2d和gpu_texture之间的属性进行同步。
-
第二种方案:在调用rhi::texture_create()时,直接把texture2d的this指针传递进去,Vulkan层在内部将gpu_texture和this进行绑定。Vulkan层在内部操作gpu_texture时可以通过这种绑定关系,查询到texture2d的指针,并读取相关设置信息。这么做同样也有坏处,首先是会产生循环引用,在render层textuer_2d引用了gpu_texture, 在vulkan层gpu_texture又引用了texture2d,然后是,因为rhi::texture_create的参数有了类型,那么就需要为每一种texture(textur2d, texture3d, cubemap)等添加一个texture_create/texture_destroy接口。
-
第三种方案:在第二种方案的基础上,可以通过去掉gpu_handle的存在,来切断循环引用。
伪码如下:
//-----rhi/texture.h------
namespace rhi {
bool texture_create(texture2d *tex);
void texture_destroy(texture2d *tex);
}
//-----rhi/texture.cpp-------
#ifdef RHI_VULKAN
#include "vulkan/texture.cpp"
#elseif RHI_DIRECT3D
#include "direct3d/texture.cpp"
#elseif RHI_METAL
#include "metal/texture.cpp"
#endif
//-----render/texture2d.h
namespace render {
class texture2d {
public:
texture2d() {
rhi::texture_create(this);
}
~texture2d() {
rhi::texture_destroy(this);
}
private:
int width;
int height;
}
}
当调用rhi::texture_create时,Vulkan层会创建一个texture的GPU资源,并这份GPU资源和texture2d指针进行绑定,但是这种绑定并不导出到外部接口使用。
后续操作某个GPU资源时,直接使用texture2d指针即可。
至于绑定方式,可以有多种多样,最简单直接方式就是使用unordered_map(显然性能并不会太高)。
第三种方案和第二种方案有一个通病,就是一个texture2d资源同时需要至少两个对象来表示,render层的texture2d和vulkan层的gpu_texture2d, 这会造成内存碎片问题。
花了2周时间反复重构了多次,都不太满意。
在重构过程中,我想到了一个全新的思路。
伪码如下:
//-----render/texture2d.h
namespace render {
class texture2d {
pubilc:
static texture2d *create(int width, int height);
static void destroy(texture2d *tex);
protected:
texture2d() {}
~texture2d() {}
protected:
gpu_handle gpu_texture;
int width;
int height;
}
}
//-----vulkan/vk_texture2d.h
namespace vulkan {
class vk_texture2d : public render::texture2d {
public:
vk_texture2d(int width, int height) : texture2d() {
//todo some gpu create
}
~vk_texture2d() {
~texture2d()
//release some gpu resource
}
private:
//some GPU-related resource
}
}
//-----vulkan/vk_factory.h
namespace render {
texture2d *texture2d::create(int width, int height)
{
return new vk_texture2d(width, height);
}
void texture2d::destroy(texture2d *tex)
{
delete (vk_texture2d *)tex;
}
}
在这次抽象中,我彻底去掉了RHI相关的所有中间层。而且几乎解决了上述方案中所有的缺点:内存碎片不存在了,循环引用没有了,GPU和CPU端数据的粘合逻辑消失了。
当然,这套抽象也有他自己的缺点:
- 所有的渲染对象不能再使用new来创建,只能使用create/destroy来创建和销毁。
- vulkan层中的对象之间不能再继承, 比如vk_texture2d本来可以继承自vk_texture的
但是,相比他能解决的问题,我觉得这两个问题都不算是大问题。
业务逻辑是使用Lua来做,所以本来也不会用到new来创建渲染对象。
少使用乃至不使用继承更是我一惯的坚持原则。