Ogre是一个跨操作系统平台的开源3D引擎,既支持DirectX,也支持使用OpenGL,支持可替换的场景管理算法(BSP, OCT)。为Ogre提供这些灵活可扩展性的基础之一就是其面向插件的设计。
很多常用的软件大都提供了插件接口,用以扩展应用程序设计者最初未想到的功能,比较常见的譬如PhotoShop的滤镜,After Effect中的各种插件(最有名的比如shine),3dMax的插件譬如渲染器,魔兽世界的辅助插件等等。
通常,插件本身通常也需要实现主应用程序所需要的必要接口,从而使得插件可以被应用程序加载执行。此外,插件的实现也需要由主应用程序提供一些接口api,通过这些接口,插件可以对主应用程序的功能进行调用。
插件可以是动态链接库(win32平台上为DLL文件),也可以是以脚本的形式提供的,比如魔兽世界中的插件就是使用lua编写的,插件也可能是某 种应用程序自定义的文件,只要该应用程序提供了创建该类文件的方法并实现解析、执行功能即可。(不同的实现形式各有利弊,具体需要参考插件及应用程序所处 的运行环境进行取舍)
采用插件的一个巨大的好处,以及很多应用程序中使用插件的主要目的就是,我们可以在不需要改动应用程序本身的情况下扩展应用程序的功能 。
在Ogre中,插件被用来提供渲染子系统(RenderSystem),不同类别的图形API被封装在不同的渲染子系统的视线当中,Ogre默认提 供了DX和OpenGL的实现,甚至,如果我们乐意,甚至可以只用绘点函数实现一套纯软件的渲染子系统提供给Ogre使用。以此为例,用简单的形式来展示 这种实现大概类似于下面这样:
class RenderSystem
{
// … operations that a render system need to support
};
// In DX_RenderSystem.dll (in plugin dx rendersystem )
class DX_RenderSystem : public RenderSystem
{
// … implementation & override of the operations from RenderSystem using DirectX
};
// In GL_RenderSystem.dll (in plugin openGL rendersystem)
class GL_RenderSystem : public RenderSystem
{
// … implementation & override of the operations from RenderSystem using OpenGL
};
// …. We could implement any other rendersystem as we like
在引擎的内部(OgreMain),插件在初始化的时候,将一个渲染子系统的实例创建出来(可能是DX_RenderSystem,也可能是 GL_RenderSystem,也可能是其他),并将之挂接到OgreMain的Root对象当中。此后,OgreMain中的Root所有的渲染操作 也就可以通过该接口访问插件中创建的渲染子系统了。
本文接下来分析Ogre是如何实现插件的,以及插件是如何与OgreMain主引擎进行配合的。
事实上,想编写一个插件是很简单的,我们只需要约定几个接口,插件将这些接口实现了,主应用程序通过某种机制(配置文件/遍历某个目录)加载插件,并查找接口在插件中是否被实现了,如果实现了,则调用之即可。
一个简单的例子如下:
// in xx.dll
__declspec(dllexport) extern “C” // __declspec(dllexport)告知编译器需要将该函数导出
// extern “C” 告知编译器不要对函数做C++名字重整
const char* GetPluginName(void)
{
return “Test Plugin”;
}
// in application
HMODULE hInst = LoadLibrary(”xx.dll”);
if (!hInst) return;
typedef const char* (*GetPluginNameFunc)(void);
GetPluginNameFunc pFunc = GetProcAddress(hInst, “GetPluginName”);
if (!pFunc) return;
const char* szPluginName = pFunc();
// … do other things
事实上,在C语言里,我们就可以通过这种方式,约定一套需要实现的接口,交给插件实现即可。在这里,插件的功能都是在dll中实现的,我们需要加载 dll,保存句柄,并在插件卸载的时候释放相应的资源。在C++中,我们显然可以做的更好一点,比如将dll模块相关的功能以及资源封装起来:
class DynLib
{
public:
bool Load(const char* szPluginPath);
void* GetProcAddress(const char* szProcName);
private:
HMODULE m_hInst;
};
bool DynLib::Load(const char* szPluginPath)
{
m_hInst = LoadLibrary(szPluginPath);
return m_hInst != NULL;
}
void* DynLib::GetProcAddress(const char* szProcName)
{
return ::GetProcAddress(m_hInst, szProcName);
}
这样一来,我们就有了一个DLL的简单封装。
但是一个dll里未必只能有一个插件,假若我们简单的把插件接口设计为一套C导出函数,我们就不得不面对这种局限。经典的做法依旧是抽象,Ogre中定义了一个插件接口,每一个实现了插件接口的实例都代表了一个插件,如下:
class Plugin
{
public:
virtual const String& getName() = 0;
virtual void install() = 0;
virtual void initialize() = 0;
virtual void shutdown() = 0;
virtual void uninstall() = 0;
};
Ogre的每一个插件都需要实现以上接口。该接口提供的功能包括:
install 插件被加载时调用。
initialize 插件被初始化时调用。
shutdown 插件被反初始化时调用。
uninstall 插件被卸载时调用。
不过,单有这个接口,插件还是无法工作。插件中还需要提供两个约定的C导出函数。这个稍后再说。
Ogre在Root对象中统一管理插件,因此Root对象提供了以下与插件相关的接口:
class Root
{
// …
void loadPlugin(const String& pluginName);
void unloadPlugin(const String& pluginName);
void installPlugin(Plugin* plugin);
void uninstallPlugin(Plugin* plugin);
// 以及对应的处理多个插件的接口
// …
};
当loadPlugin函数被调用时,Ogre将加载该插件的dll,从中查找并调用名为dllStartPlugin的导出函数。
对应的,当unloadPlugin函数被调用时,Ogre将从该dll中调用dllStopPlugin函数。并将该dll卸载。
每个插件在实现的时候,都必须提供上述的两个C导出函数。其中,dllStartPlugin负责创建插件实例,并调用 Root::installPlugin,将创建好的插件指针传递给Root对象。在这里,dllStartPlugin实际可以创建多个插件实例,并依 次调用Root::installPlugin,这样我们就可以在一个DLL中包含多个插件了。在dllStopPlugin时,则需要调用 Root::uninstallPlugin,并将插件DLL中创建的plugin实例释放掉。
一个典型的dllStartPlugin的实现像这样:
class D3D9Plugin : public Plugin
{
//…
};
D3D9Plugin* plugin;
__declspec(dllexport) extern “C”
void dllStrartPlugin(void)
{
plugin = new D3D9Plugin();
Root::getSingleton().installPlugin(plugin);
}
__declspec(dllexport) extern “C”
void dllStopPlugin(void)
{
Root::getSingleton().uninstallPlugin(plugin);
delete plugin;
}
Root::getSingleton().installPlugin(plugin)会负责调用插件的install以及initialise操作,并将插件的指针存放起来,以备卸载时使用。
Plugin::install以及Plugin::initialise则分别负责创建OgreMain提供扩展功能接口的实例,以及将创建好的对象挂载到应用程序当中。
一个典型的Plugin的行为像这样:
void BspSceneManagerPlugin::install()
{
mBspFactory = new BspSceneManagerFactory();
}
void BspSceneManagerPlugin::initialise()
{
Root::getSingleton().addSceneManagerFactory(mBspFactory);
}
其中,BspSceneManagerFactory是继承自OgreMain中的SceneManagerFactory的派生类。
( class BspSceneManagerFactory : public SceneManagerFactory {…} )
通过将场景管理器工厂添加到Root对象当中,插件动态的将其功能添加到了OgreMain当中。
PS: SceneManagerFactory是一个用于创建SceneManager的工厂。SceneManager则是被用于场景管理的管理器。具体的 SceneManager也根据算法不同而不同,Ogre自带提供了两种类型的场景管理算法插件:Bsp以及Octree(实现了两个对应的插件,分别是 Plugin_BSPSceneManager以及Plugin_OctreeSceneManager)。
通过上述方式,OgreMain核心引擎并不需要关心场景管理算法的具体实现,不需要关心渲染子系统的具体实现,不需要关心粒子系统的具体实现,等等。一切这些扩展性的功能都通过插件实现,并在加载时动态挂载到OgreMain当中,供OgreMain的引擎核心使用。
使用插件时几个需要注意的地方:
1, 调用插件DLL方法创建的对象需要交由插件DLL释放。(因为不同的链接单元可能具有不同的内存管理上下文环境,此处的new与彼处的new在语义上未必等同)
2, 调用插件DLL方法获取的插件内对象的引用或指针,在插件DLL卸载之后就是无效的,必须保证不再使用。(比较容易引发问题的一个典型例子是从插件中传递 回一个引用计数字符串,当DLL被卸载后,字符串内指向实际数据的指针已经无效,但是在该对象析构时,仍需要访问该指针)