首页 > 其他分享 >【文档翻译】构建一个引擎插件系统

【文档翻译】构建一个引擎插件系统

时间:2023-11-08 14:44:50浏览次数:36  
标签:API 引擎 插件 plugin lua api 文档

本文档译自 bitsquid 引擎开发博客文章"Building an Engine Plugin System",作者 Niklas Frykholm,原文参见此处


概述 - Overview

插件系统是开发者扩展引擎能力的一个好方法。当然,引擎也可以直接通过修改源代码来进行扩展,但是这种方法有几个缺点:

  • 更改代码需要重新编译引擎。任何想要修改引擎的人都必须拥有完整的源代码,能够访问所有库并正确设置构建环境。
  • 每次从上游提取更改时,都必须将更改与传入的补丁合并。随着时间的推移,这将成为一项重量级的工作。
  • 由于直接在源代码中工作,而不是针对已发布的 API,因此重构引擎系统可能会导致从头开始重写代码。
  • 没有简单的方法可以与他人分享你所做的修改。

插件系统解决了上述难题。插件可以作为编译后的 DLL 分发。它们很容易共享,你可以通过把它们放在引擎的插件文件夹中来安装它们。由于插件使用显式 API,它们将继续与新版本的引擎一起工作(除非向后兼容性被显式破坏)。

当然,插件 API 不可能什么都能做,所以总有一些事情你需要通过修改引擎来完成。不过,这仍是一个很好的补充。


两个 API 的故事 - A Tale of Two APIs

当创造一个插件系统时,有两个 API 你需要考虑。

第一个,也是最明显的,就是插件向引擎所公开的 API:一组导出的函数,引擎将在预定义的时间调用这些函数。对于一个简单的系统,它看起来是这样的:

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

另一个 API 通常要麻烦一些,它是引擎向插件公开的 API

为了产生效果,插件通常需要调用引擎来做一些事情。这可以是生成一个单位,播放声音,渲染一些网格等。引擎需要为插件提供一些方法来供它调用这些服务。

有很多方法可以做到这一点。一种常见的解决方案是将所有共享功能放在一个公共 DLL 中,然后将引擎应用程序和插件链接到这个 DLL

image

这种方法的缺点是,插件需要访问的功能越多,共享 DLL 中必须包含的功能就越多。最终,你不得不在共享 DLL 中放置大部分引擎功能,这与我们所追求的干净和简单的 API 相去甚远。

这在引擎和插件之间创建了一个非常强的耦合。每次我们想要修改引擎中的某些内容时,我们可能不得不修改共享 DLL,从而可能破坏所有插件。

任何读过我之前文章的人都知道,我不喜欢这种强耦合。它们是重写和重构系统的强大阻力,最终导致代码停滞不前。

另一种方法是让引擎的脚本语言(在我们的例子中是 Lua)作为引擎的 API。通过这种方法,任何时候插件想要引擎做一些事情,它可以调用 Lua

对于许多应用程序,我认为这可能是一个非常好的解决方案,但在我们的情况下,它似乎不是一个完美的选择。首先,这些插件可能需要访问许多“低层级”的东西,而无法从 Lua 中访问。而且我并不热衷于将引擎的所有内部都暴露给 Lua。其次,由于插件和引擎都是用 C++ 编写的,因此通过 Lua 编组它们之间的所有调用似乎过于复杂且效率低下。

我更喜欢一个简约的、面向数据的、基于 C 语言的接口(因为 C++ABI 兼容性问题,也因为……嗯……C++)。


接口查询 - Interface Querying

我们可以在初始化插件时将引擎 API 发送给它,而不是将插件链接到提供引擎 APIDLL。像这样(一个简化的例子):

// plugin_api.h
typedef struct EngineApi
{
 void (*spawn_unit)(World *world, const char *name, float pos[3]);
 ...
} EngineApi;
// plugin.h
#include "plugin_api.h"
__declspec(dllexport) void init(EngineApi *api);
__declspec(dllexport) void update(float dt);
__declspec(dllexport) void shutdown();

非常不错。插件开发者不需要再链接任何东西,只需要包含 plugin_api.h 文件即可,之后就可以调用 EngineApi 结构体中的函数来告诉引擎去做一些事情。

唯一不足的是版本支持。

在将来的某个时候,我们可能想要修改 EngineApi。也许我们发现我们想要在 spawn_unit() 或其他东西中添加一个旋转参数。我们可以通过在系统中引入版本控制来实现这一点。我们没有直接向插件发送引擎 API,而是向插件发送一个函数,让它查询特定版本的引擎 API

通过这种方法,我们还可以将 API 分解为可以单独查询的更小的子模块。这给了我们一个更干净的结构组织。

// 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);

当引擎实例化插件时,它会传递 get_engine_api(),插件可以使用它来获取不同的引擎 API

插件通常会在 init() 函数中设置 API:

static WorldApi_v1 *_world_api = nullptr;
static LuaApi_v0 *_lua_api = nullptr;

void init(GetApiFunction get_engine_api)
{
  _world_api = (WorldApi_v1*)get_engine_api(WORLD_API, 1);
  _lua_api = (LuaApi_v0*)get_engine_api(LUA_API, 0);
}

之后,插件将使用这些 API

_world_api->spawn_unit(world, "player", pos);

如果我们需要对 API 进行重大更改,我们可以引入该 API 的新版本。只要 get_engine_api()在被请求时仍然可以返回旧的 API 版本,所有现有的插件都将继续工作。

有了这个引擎的查询系统,对插件也使用相同的方法是有意义的。也就是说,与暴露单独的函数 init()update() 等不同,这个插件可以只暴露一个函数 get_plugin_api(),引擎可以用同样的方式从插件中查询 API

// plugin_api.h
#define PLUGIN_API_ID 2
typedef struct PluginApi_v0
{
  void (*init)(GetApiFunction get_engine_api);
  ...
} PluginApi_v0;
// plugin.c
__declspec(dllexport) void *get_plugin_api(unsigned api, unsigned version);

由于我们现在对插件 API 也有版本控制,这意味着我们可以在不破坏现有插件的情况下修改它(添加新的所需功能等)。


组合起来 - Putting It All Together

综上所述,这是一个插件的完整(但非常小)示例,它向引擎的 Lua 层暴露了一个新功能:

// plugin_api.h
#define PLUGIN_API_ID       0
#define LUA_API_ID          1

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

typedef struct PluginApi_v0
{
  void (*init)(GetApiFunction get_engine_api);
} PluginApi_v0;

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);
  double (*to_number)(lua_State *L, int idx);
  void (*push_number)(lua_State *L, double number);
} LuaApi_v0;
// plugin.c
#include "plugin_api.h"

LuaApi_v0* _lua;

static int test(lua_State* L)
{
  double a = _lua->to_number(L, 1);
  double b = _lua->to_number(L, 2);
  _lua->push_number(L, a + b);
  return 1;
}

static void init(GetApiFunction get_engine_api)
{
  _lua = get_engine_api(LUA_API_ID, 0);

  if (_lua)
    _lua->add_module_function("Plugin", "test", test);
}

__declspec(dllexport) void* get_plugin_api(unsigned api_id, unsigned version)
{
  if (api_id == PLUGIN_API_ID && version == 0) {
    static PluginApi_v0 api;
    api.init = init;
    // Do Some Init...
    return &api;
  }
  return 0;
}
// engine.c
// Initialized elsewhere.
LuaEnvironment* _env = 0;

void add_module_function(const char* module, const char* name, lua_CFunction f)
{
  _env->add_module_function(module, name, f);
}

void *get_engine_api(unsigned api, unsigned version)
{
  if (api == LUA_API_ID && version == 0 && _env) {
    static LuaApi_v0 lua;
    lua.add_module_function = add_module_function;
    lua.to_number = lua_tonumber;
    lua.push_number = lua_pushnumber;
    return &lua;
  }
  return 0;
}

void load_plugin(const char* path)
{
  HMODULE plugin_module = LoadLibrary(path);
  if (!plugin_module)
    return;
  GetApiFunction get_plugin_api = (GetApiFunction)GetProcAddress(plugin_module, "get_plugin_api");
  if (!get_plugin_api)
    return;
  PluginApi_v0* plugin = (PluginApi_v0*)get_plugin_api(PLUGIN_API_ID, 0);
  if (!plugin)
    return;
  plugin->init(get_engine_api);
}

标签:API,引擎,插件,plugin,lua,api,文档
From: https://www.cnblogs.com/Code-For-What/p/17817382.html

相关文章

  • 【文档翻译】面向数据设计的现在和未来
    本文档译自gamesfromwithin.com的文章"Data-OrientedDesignNowAndInTheFuture",作者Noel,原文参见此处概述-Overview最近有很多关于面向数据设计的讨论(和批评)。我想解决一些已经提出的问题,但在此之前,我将从我最近的《GameDeveloperMagazine》发表开始。如果你有任何......
  • 【文档翻译】面向数据设计(以及为啥用OOP可能会搬起石头砸自己的脚)
    本文档译自gamesfromwithin.com的文章"Data-OrientedDesign(OrWhyYouMightBeShootingYourselfInTheFootWithOOP)",作者Noel,原文参见此处概述-Overview想象一下:在开发周期的末尾,你的游戏卡的像乌龟在爬,但是你却没有在profiler发现任何明显的性能热点。真正的......
  • MySQL的存储引擎、事务补充、MySQL的锁机制、MySQL的日志
    MySQL的存储引擎概述数据库存储引擎是数据库底层软件组织,数据库管理系统(DBMS)使用数据引擎进行创建、查询、更新和删除数据。不同的存储引擎提供不同的存储机制、索引技巧、锁定水平等功能。现在许多不同的数据库管理系统都支持多种不同的数据引擎。MySQL的核心就是存储引擎。用户可......
  • CAST电子部单片机方向授课——串口通信 预习文档
    CAST电子部单片机方向授课——串口通信预习文档课前小准备安装串口调试助手第一步:进入MicrosoftStore第二步:在MicrosoftStore中搜索“串口调试助手”第三步:点击获取,按要求安装即可下载完成后,桌面上可能没有快捷方式,需要在win里搜索一下,然后拖到桌面上。注:其他安装方......
  • Burp联动Sqlmap插件进行sql注入扫描
    一、插件介绍sqlmap4burp++是一款兼容Windows,mac,linux多个系统平台的Burp与sqlmap联动插件这个插件嘎嘎好用,大大提升了sqlmap的效率项目地址https://github.com/c0ny1/sqlmap4burp-plus-plusgithub中间有空格,把空格去掉在进行访问即可进入burp拓展模块点击添加上传文件......
  • docker日志收集docker插件+loki+grafna
    实现收集docker容器日志方式:dokcer安装插件,将日志发送到loki,grafna展示日志。1、安装lokiwgethttps://raw.githubusercontent.com/grafana/loki/v2.9.1/cmd/loki/loki-local-config.yaml-Oloki-config.yamldockerrun--nameloki-d-v$(pwd):/mnt/config-p3100:3100......
  • IDEA插件分享:代码零入侵,后端神器
    今天给大家介绍一款好用的IDEA插件:Apipost-Helper-2.0。非常好用!主要包含以下功能:1、无侵入生成API文档编写完代码后,只需右键upload同步接口即可快速将源码中包含的API以及注解自动生成API文档,并生成可以访问的链接。无需任何额外操作。 2、快速调式(类似Postman)编写完代码......
  • 超好用的IDEA插件推荐
    写完代码还得重复打字编写接口文档?代码量大定位接口定义方法太难找?麻烦!写完代码还得复制粘贴到postman进行调试?这三点太麻烦?今天给大家推荐一款IDEA插件,写完代码IDEA内一键生成API文档,无需安装、打开任何其他软件;写完代码IDEA内一键调试,无需安装、打开任何其他软件;生成API目录树,......
  • 软件开发项目文档系列之十如何撰写测试用例
    测试用例的重要性和意义在于它们是软件开发和维护过程中的关键工具,用于确保软件产品的质量、稳定性和可靠性。通过详细描述了如何测试不同方面的功能和性能,测试用例可以帮助团队发现潜在问题、验证功能是否按照规格要求正常运行,并确保软件在各种使用情境下表现出色。它们也有助于......
  • 学生毕业管理系统-计算机毕业设计源码+LW文档
    摘 要对学生毕业管理的流程进行科学整理、归纳和功能的精简,通过软件工程的研究方法,结合当下流行的互联网技术,最终设计并实现了一个简单、易操作的学生毕业管理小程序。内容包括系统的设计思路、系统模块和实现方法。系统使用过程主要涉及到管理员,教师和学生三种角色,主要包含系统......