【原文链接】Plugin Architecture – Nuclex Games Blog -- 基于AI
插件架构
本文将指导您设计一个简单而强大的插件架构。这需要您具备一定的C++经验,熟悉动态库(.dll、.so)的使用,并理解面向对象编程的基本概念(如接口和工厂模式)。但在开始之前,让我们先看看插件的优势以及为什么要使用它们:
代码清晰度与一致性提升
插件将第三方库及其他团队成员编写的代码封装为明确定义的接口,使您的代码获得高度一致的交互方式。代码中也不再充斥转换例程(如ErrorCodeToException()
)或特定库的自定义逻辑。
项目模块化增强
代码被清晰地拆分为独立模块,减少项目中需同时处理的文件数量。这种解耦过程创建的组件更易复用,因为它们不会与项目特有的复杂逻辑纠缠。
缩短编译时间
由于实现细节被隐藏,编译器无需解析外部库的头文件来声明内部使用这些库的类,从而显著减少编译时间(您知道windows.h
包含约500KB的代码吗?)。
组件替换与扩展
发布用户补丁时,通常只需更新单个插件而非整个安装包。例如,游戏的新渲染器或扩展包的新单位类型(包括用户制作的模组)可通过简单提供一组插件实现。
闭源项目中使用GPL代码
若在代码中直接使用GPL协议的内容,需公开全部源代码。但若将其封装为插件并作为独立进程运行(参见GPL FAQ),则只需公开插件源码。
个人观点
就个人而言,我使用插件并非因为其“酷炫”,也不是为了频繁向用户推送补丁,甚至不是强制自己编写模块化代码。我认为这是组织大型项目的最佳方式——依赖关系大幅减少,且能专注于替换特定系统,而非因代码库重构而阻塞整个项目或团队进度。
引言
插件系统原理
在常规应用中,若需实现特定功能,通常有两种选择:自行编写代码或寻找现有库。但需求变化时,您不得不重写代码或更换库,这两种选择均可能导致依赖该代码或库的其他部分需要同步修改。
插件系统提供了第三种选择:将项目中不希望绑定具体实现的组件(如基于OpenGL或Direct3D的渲染器)提取到动态库中,并通过主代码库定义的接口解耦。插件则提供这些接口的具体实现。插件的关键特性在于加载方式:应用程序不直接链接这些库,而是通过扫描目录动态加载发现的插件,插件随后以统一方式将自己注册到应用中。
常见误区
许多C++程序员在设计插件系统时,会为每个动态库添加如下函数:
PluginBase *createInstance(const char *);
然后通过名称请求对象,直到某个插件返回实例。更聪明的设计可能让插件自行注册到引擎中:
void dllStartPlugin(PluginManager &pm);
void dllStopPlugin(PluginManager &pm);
但这些设计存在严重问题:
-
需使用
dynamic_cast<>
或reinterpret_cast<>
转换插件返回的对象,类型安全性无法保证。 -
难以支持同一接口的多实现。若插件以不同名称注册(如
Direct3DRenderer
和OpenGLRenderer
),引擎无法自动识别可用选项。 -
若在框架中实现此类系统,可能迫使应用层继承其问题,并要求插件开发者同时依赖引擎和应用的头文件,增加版本冲突风险。
独立工厂模式
引擎定义的接口(如图形输出)应由插件实现。因此,我们让插件向引擎注册其实现类的工厂:
template<typename Interface>
class Factory {
virtual Interface *create() = 0;
};
class Renderer {
virtual void beginScene() = 0;
virtual void endScene() = 0;
};
typedef Factory<Renderer> RendererFactory;
此设计消除了类型转换,且通过工厂模板避免了冗余代码。
方案一:插件管理器
插件通过插件管理器注册工厂,引擎通过管理器使用插件:
class PluginManager {
void registerRenderer(std::auto_ptr<RendererFactory> RF);
void registerSceneManager(std::auto_ptr<SceneManagerFactory> SMF);
};
插件需导出注册函数:
void registerPlugin(PluginManager &PM);
管理器可扫描目录加载所有动态库,或通过XML列表指定插件。
方案二:深度集成
另一种方式是将引擎拆分为多个子系统,由核心管理:
class Kernel {
StorageServer &getStorageServer() const;
GraphicsServer &getGraphicsServer() const;
};
class StorageServer {
void addArchiveReader(std::auto_ptr<ArchiveReader> AL);
std::auto_ptr<Archive> openArchive(const std::string &sFilename);
};
class GraphicsServer {
void addGraphicsDriver(std::auto_ptr<GraphicsDriver> AF);
size_t getDriverCount() const;
GraphicsDriver &getDriver(size_t Index);
};
子系统可内置默认实现(如自定义包格式的ArchiveReader
),同时允许插件扩展功能(如支持.zip/.rar)。
版本控制
插件与主程序版本不匹配可能导致崩溃。解决方案是在核心系统中定义版本常量,插件通过函数返回该值:
// 核心系统
#define MyEngineVersion 1;
// 插件
extern int getExpectedEngineVersion() {
return MyEngineVersion;
}
若引擎版本更新而插件未重新编译,管理器可拒绝加载旧版插件。
总结
本文介绍的类型安全、灵活的插件架构既适用于现有代码库,也适合新项目。
下载
Nuclex.PluginArchitecture.Example.7z (10.1 KiB)
感谢Felipe Bulat贡献的Linux移植版本,使示例支持Windows和Linux平台。