这篇小文是我读“Building an Engine Plugin System”(需翻墙)随手留下的一些笔记。

开始正文前,先无责任评论两句吧。bitsquid是一个强调动态(各种reload),极简设计(接口知识最小化),轻量级(核心不到20万行)的引擎,跟俺口味很贴近,所以俺一向比较关注他们的动向。这是个小团队,虽然网站上没啥信息,引擎也未公开放出,可是blog的水分很少,质量比较高,是俺的菜。

按照传统套路,先啰嗦一番“非插件化设计”的弊端:

  • 想扩展现有行为,就得修改代码,重编引擎。(隐含工作量太大——获取全部代码和依赖的库,架设所有build环境)
  • 一旦开始有了本地改动就得维护之,一旦pull了新版本就得merge,这个merge的工作量,随着本地改动的增加和时间的推移,不断上涨。
  • 本地的改动是基于当时那个snapshot的代码逻辑,有很多隐含的假设,随着时间的推移和引擎的重构,这些假设会被破坏,导致各种莫名的问题。(最常见的是本地行为依赖了一个内部数据结构,结果那个结构被重构甚至被去掉了)
  • 所有本地改动很难与其他人共享。实践上,最多就是打个patch发一下,难以整包发布。

插件的好处就不细说了,基本上因为双方依赖的是显式的API,这种紧密关系就被解耦了,各种好处。

先说下naive的做法——插件定义和导出一些标准的函数入口,引擎在特定时间调用。
看上去基本上是下面这样的:

__declspec(dllexport) void init();
__declspec(dllexport) void update(float dt);
__declspec(dllexport) void shutdown();

 

当插件要用引擎的功能时,通过SharedDLL来实现。

Image Title

一些传统引擎(比如Ogre)就是这么实现的,某种意义上讲,Windows本身也是如此。

这么设计的问题是引擎的设计者很被动——插件要啥功能,就得把啥放到SharedDLL里去。搞着搞着发现所有东西都进去了。暴露出去的接口变得臃肿,很多小秘密也被插件知道了。这样一来重走老路,修改的破坏性风险又急剧增加。

改良的办法是通过lua等脚本来作为天然的api屏障,这也是各种MMO的典型做法。但这种也有局限性,就是插件需要取得一些底层的东西时比较难办(给还是不给呢?)。对游戏引擎的设计来讲,大部分情况下写插件的人是程序,两边都是C++,中间用lua会很啰嗦和低效,有点儿没事找抽的感觉。

本文的方案是基于一种C的接口查询的方法(C++的ABI,唉,不提也罢)。
与让插件链接到一个提供引擎API的DLL,不如直接在插件初始化的时候把引擎API传过去,如下所示:
(小声说一句,这思路其实是抄Linux的)

"plugin_api.h"

typedef struct EngineApi
{
 void (*spawn_unit)(World *world, const char *name, float pos[3]);
 ...
} EngineApi;

一个头文件搞定,插件只要包含这个文件就行,不用链接任何dll。插件只需要在入口处获得(引擎提供的)这个结构的指针,然后想要什么功能就可以直接调进去了。

嗯,为了方便演化和兼容性,加个版本控制。为免牵一发而动全身,可把这些接口根据逻辑上的相关性,拆成多个结构,各自独立演化,如下所示:

"plugin_api.h"


#define WORLD_API_ID    0
#define LUA_API_ID      1

typedef struct World World;

typedef struct WorldApi_v0 {
 void (*spawn_unit)(World *world, const char *name, float pos[3]);
 ...
} WorldApi_v0;

typedef struct WorldApi_v1 {
 void (*spawn_unit)(World *world, const char *name, float pos[3], float rot[4]);
 ...
} WorldApi_v1;

typedef struct lua_State lua_State;
typedef int (*lua_CFunction) (lua_State *L);

typedef struct LuaApi_v0 {
 void (*add_module_function)(const char *module, const char *name, lua_CFunction f);
 ...
} LuaApi_v0;

typedef void *(*GetApiFunction)(unsigned api, unsigned version);

现在插件只要提供模块ID和版本号,就可以用 GetApiFunction 来获取对应的引擎功能了,具体代码可看原文,比较长就不贴了。

反过来插件暴露给引擎的API,也是如法炮制。各种小好处和细节,略去不提。

以上就是 基于C的API协议定义技术,在插件设计里的应用。

这个结构本质上很简单,A也不依赖B,B也不依赖A,咱俩都依赖协议,私下里就可以没负担地随便改了——只要别破坏协议就行。
这种方式比所谓的 DLL 显式链接(GetProcAddress + "FunctionName")依赖更少,因为显式链接除了签名一致以外,还依赖函数名一致。
这样也有动力保持协议最小化了——协议越简洁,双方的发展自由度也就越大,将来潜在的矛盾和冲突也就越小。(小一点,再小一点。各种这一类的梗可见 The old new thing,微软历史上各种被坑的经历,不要重演,呵呵)

(完)
Gu Lu
2014-05-16

Comments
Write a Comment

Tags

随笔   游戏开发   Bitcoin   Programming   C/C++   优化   Unity   C++   区块链   知乎   BSV   游戏设计   中国文化   比特币   Unity3D   软件开发   引擎设计   系统架构   Production   idtech   Bitcoin SV   加密货币   项目管理   游戏评论   资源管理   资源流水线   效率   道德经   网络   方法论   模板编程   Blockchain   Lua   Blockchain Computing   Oculus   GDC   渲染   VR   PerfAssist   BitcoinSV   Unity MemoryProfiler   BCH   读书笔记   经济学   信息过载   行业报告   字体   Productivity   图形   网络编程   Dice   协程   EMC   Premake   万物理论   测试   中间件   SatoPlay   Game Engine   新手引导   区块链游戏   Methodology   CI   命令行解析   Science   goroutine   ndk   Ethereum   nanomsg   自动化   Scripting   摘录   Debugging   同步技术   cppcon   C++模板   数据上链   DOOM3   技术评估   Unity GC   C++11   学习方法   Surface Pro 3   Engine Evaluation   CRT   文化   笔记   golang   图形编程   多线程   ETH   Bitcoin Cash   cppcon14   Visual Studio   Unity Coroutine   跨语言可变参数列表   团队协作   货币   Deployment   Visual Assist   工程改进   Michael Abrash   exp   开放世界   权利   量子计算   域名   虚拟现实   系统重构   slua   遮挡剔除   完美转发   协作式调度   Modern C++   Money   类型推导   Memory Debugging   个人成长   小故事   BTC   暴雪   产品   历史   错误处理   Unity Profiler   MOD  

知识共享许可协议
本作品由Gu Lu创作,采用知识共享Attribution-NonCommercial-NoDerivatives 4.0 国际许可协议进行许可。