Posted on 2009-07-01 13:54
RichardHe 阅读(1781)
评论(0) 编辑 收藏 引用 所属分类:
OGRE
Ogre源码剖析3–可扩展性&插件机制
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被卸载后,字符串内指向实际数据的指针已经无效,但是在该对象析构时,仍需要访问该指针)