从零开始做3D地图编辑器基于QT与OGRE.docx
《从零开始做3D地图编辑器基于QT与OGRE.docx》由会员分享,可在线阅读,更多相关《从零开始做3D地图编辑器基于QT与OGRE.docx(29页珍藏版)》请在冰豆网上搜索。
从零开始做3D地图编辑器基于QT与OGRE
第一章基础知识
注:
文章里面有不少个人见解,欢迎大家一起互相讨论。
希望高人能给予相应理解与意见建议。
在实际3D游戏开发中,编辑器是极其重要的一个部分,一个优秀健壮的编辑器,可以使项目事半功倍,而相反,一款BUG超多(随时会挂)又不注重操作习惯(完全基于快捷键,又没有详细的使用说明)的编辑器,不仅会使项目事倍功半,而且会削弱开发人员的积极性,甚至让开发人员对项目产生排斥情绪。
编辑器在游戏里面应用很广泛,一般都有地图编辑器(关卡、世界)、粒子编辑器、动画编辑器、字体编辑器(单机里面较多)、UI编辑器、材质编辑器、脚本编辑器等等,编辑器设计制作方法也大致可分为两个趋势,一种是倾向于做大而全的世界编辑器,一种是做小而精的功能编辑器,在这里我不想讨论这两者的利与弊,我只能说,只要这个解决方案可以解决我们当前的问题,那么它就是一个适合现阶段的解决方案,但并不一定是最好的解决方案。
一、工具
现在制作编辑器,流行以下几种方式:
1、 使用C#制作基于WinForm的编辑器。
2、 制作基于MFC的编辑器。
3、 制作基于WxWidgets的编辑器。
4、 制作基于QT的编辑器。
基于C#来制作编辑器,在制作一些小工具上面很有利,比如说打包工具,加密器等等和图形关系不大的工具,它的优势在于它的简单易用,但是当你涉及到图形这一块的时候,如果引擎支持不C#,那么使用XNA、ManageDX都不是很好的一种解决方案(除非你的游戏就是基于两者),导入动态链接库的方法又会比较麻烦,C#和C++之间还是有不小的区别。
基于MFC做编辑器,在以前基本是首选,它的优势在于文档应用特别多,你遇到问题的时候,基本上网络上都能找到解决方案,但是它相对门槛高,一个初学者经常会被它折磨得兴趣殆尽,应用也很麻烦,特别是在多窗口应用上面,所以以前我用MFC做编辑器都是基于Dialog来做。
WxWidgets和QT都是跨平台的GUI库,目前来说还算主流,我个人倾向于QT,WxWidgets了解不多,QT目前由诺基亚负责,有自己的IDE、设计工具、详细的例子、比较充实的文档、与VS的结合还算完美,还有一些第三方的库支持,网络上的资料也还多,是个发展潜力不错的GUI库。
因为我将要做一个3D地图编辑器,在图形这一块也有不少选择,OGRE与Irrlicht等,我选择使用OGRE,当然你也可以选择自己的引擎。
OGRE是一个开源的图形渲染引擎,它的材质脚本还是很强大的,简单易用、目的性明确,让你的Shader容易应用与修改。
早期的版本在地形这一块做得不够,所以早期做OGRE的地形编辑时一般会选择ETM,PLSM等库,新的1.7版本对地形这一块增强不少,而我也会在编辑器里面应用它地形编辑的功能。
二、工具安装指南
1、OGRE下载与编译
OGRE官方网站:
http:
//www.ogre3d.org
下载最高版本的OGRE(1.7.1),有两种方式:
第一种方式是直接下载SDK,下载的SDK可以直接使用,但是由于编译环境不同,可能会缺少一些DX的DLL,你得在网络下另外下载缺少的DLL,下载方法是从网站左侧的DownLoad里面选择SDK,然后选择相应VS的版本,我们推荐使用VS2008,因为QT针对2008做了一个AddIn。
第二种方式是下载源代码进行编译。
个人觉得使用OGRE应该使用自己编译的库,毕竟有什么需要的时候还可以自己修改,自己编译需要注意几点:
1、除了OGRE源码外,你需要额外下载MicrosoftVisualC++DependenciesPackage,并把它解压到OGRE目录(你自己的OGRE目录)后编译。
2、你需要下载CMAKE,官方网站是www.cmake.org。
下载一个最新版本就行。
3、你机器需要安装DX的SDK,不然OIS和DX的渲染系统插件无法编译。
4、使用Cmake生成OgreVS解决方案的时候要记得指定Dependencies目录(在Cmake提醒你的时候指定)。
此过程可以参考
用VS打开生成的解决方案,
然后直接编译就可以获得dll和lib.
2、QT下载
QT官方网站:
下载QT也有两种方式,一种是纯SDK(QtSDKforWindows*(287MB)),另外一种是针对VS2008的库(Qtlibraries4.6.2forWindows(VS2008,194MB)),这两者有一定的区别,前者带有更多的工具(IDE等)。
我推荐下载针对VS2008的库,下载安装完之后,还需要下载一个Addin,这个Addin比较难找,在Otherdownloads里面下载VisualStudioAdd-in(44MB)。
安装完Add-In之后,打开VS2008应该就可以找到QT的模板了。
QT4projects下面有一些选项,选择新建一个QTApplication。
新建完编译通过,运行发现这是一个基本窗口。
如果编译OIS没有成功,请在项目属性里面填入DX的include和lib路径。
三、开始之前的配置
我看到过很多同志在做项目时,直接新建项目后立马就直接开始编程,使用的是VS默认目录,结果在Debug的时候老是找不到dll,找不到资源,然后又花了一堆的时间去查找问题,白白地浪费了不少时间,更有甚者就在此时便失去了继续向下的动力,觉得这个东西太难理解了(一遇挫折就跑)。
所以我觉得在每次开始项目前都应该好好地把解决方案配置一下。
我做项目的时候喜欢这种方式,项目目录下面存在以下几个目录。
Bin目录不难理解,里面放的是生成的可执行文件,下面又分了Debug、Release、Data(Media)等目录,Debug、Release里面放的是执行文件和dll,命名的时候Debug要命名为_d.exe.因为资源文件是共用的,所以资源不应该放在Debug或Release下面,直接放Bin下面就行了。
Docs目录里面放的是相关文档。
Objs目录里面存放编译过程中的中间文件,临时文件。
Scripts目录里面存放解决方案,Sln或其他格式。
SDKS目录存放第三方库,比如OGRE,Boost,Lua等。
Tools目录存放着制作时的一些工具。
剩下那个目录一般改名为Src或source.
为什么目录要这样分?
Bin文件夹分出来有利于你程序的发布,调试。
把Objs从source分出来,有利于你的源代码版本控制,备份。
把解决方案单独拿出来,有利于你的跨平台或换IDE,SDKS拿出来很重要,因为有可能两年后你的引擎或者底层更新或者大改动过,但是你又需要把两年前的游戏重新编译,如果没有备份好,结果自然不难想像。
同样,工具也是这样,比如说加密器算法经常改动,你不备份好你的东西以后都没有办法修改了。
接下来要调整VS来适应这一套目录结构。
第一件事,用文本工具打开修改sln,把它指向source目录里面的工程文件。
#VisualStudio2008
Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}")="Test","..\Source\Test.vcproj","{83E01383-8BC1-404F-9C25-A9AFFCDBB210}"
EndProject
像上面那样修改为你的工程名.
之后用新的解决方案打开项目,在VS里项目名上右键打开属性。
之后的第一件事情就是修改工作目录,很多同志就是因为没有设定这个目录导致找不到DLL,它在配置属性中调试一栏里面,修改成你当前的Bin所在目录,最好设置为相对目录,Debug模式下是../Bin/Debug,对应的Release下面是../Bin/Release。
接着在常规里面修改中间目录和输出目录,我们都修改成../Objs/Debug和../Objs/Release。
之后在链接器>常规里面修改输出文件,修改成..\bin\Debug\$(ProjectName)_d.exe和../Bin/Release/$(ProjectName).exe。
然后在C/C++>常规中把你要的include添加进去,在链接器>附加库中把你要的lib目录添加进去。
完成这些我们就配置完了。
附:
Ogre1.7.1的配置要注意:
由于Ogre使用了boost,所以一定要把Ogre自带的Boost目录放进SDKs中,如果要使用OIS,还得包含OIS的头文件路径,库文件和OGRE放在一起,所以不用再设置。
另:
如果是在IDE中新建QTApplication,QT头文件与库的相关配置会自动帮你设置好。
你只需要在它的基础上把其他库添加好就行了。
四、QT基本知识
回到QT,先在VS中新建一个QTApplication,项目里面有几个目录:
1、 FormFiles目录,它里面放的是使用QTdesigner制作的基于XML的布局文件,双击它就会自动进入QTdesigner。
2、 GeneratedFiles目录,它里面放的是一些临时生成的文件,这些文件用来处理QT的信号和槽等机制。
3、 ResourceFiles目录,它里面放的是基于XML的资源文件,你可以在窗体里面使用它们。
4、 HeaderFiles和SourceFiles这两个和VS默认是一样的。
理解了目录结构之后,先来试着写一个HelloWorld,先把除了main.cpp之外的所有文件移除(使用QTdesigner会提高制作效率,但是会让QT入门门槛变高)!
打开main.cpp,仅保留以下代码:
viewplaincopytoclipboardprint?
#include
intmain(intargc,char*argv[])
{
QApplicationa(argc,argv);
returna.exec();
}
#include
intmain(intargc,char*argv[])
{
QApplicationa(argc,argv);
returna.exec();
}
编译通过。
运行没有任何反应,因为还没有往里面增加任何东西。
在代码中,Main函数是C语言的入口,之后申请的QApplication用来管理控制流和主要设置,这是核心,一定要保留。
按钮是GUI中最基本的一个控件,先看看怎么增加一个按钮。
使用按钮控件必须先包含头文件:
viewplaincopytoclipboardprint?
#include
#include
然后在QApplicationa(argc,argv);与returna.exec();中间插入下面代码:
viewplaincopytoclipboardprint?
QPushButtonbutton("HELLO");
button.setGeometry(100,100,300,300);
button.show();
QPushButtonbutton("HELLO");
button.setGeometry(100,100,300,300);
button.show();
代码第一行是申请一个按钮,并把按钮的Caption标题设为HELLO,第二行表示这个按钮出现在屏幕坐标(100,100)的位置,宽高为(300,300),最后一行是显示这个按钮,你可以尝试把它去掉看看效果(官方助手里有QPushButton的更多资料,请自行查看)。
编译出来,发现屏幕上出现一个框,框里面有一个按钮,按钮可以点击,但是没有任何反应,因为还没有为这个按钮增加任何的槽(Slot)。
在MFC对控件的处理一般是通过事件机制,而在QT中是使用信号(Signal)和槽(Slot)机制,其实你也可以把它理解为事件机制。
简单理解信号其实就是输入,而槽就是输出,拿按钮打比方,在一次点击中,这个点击,就是一个信号,而点击后的反馈,就是槽。
每一个控件都拥有一些默认的Signal和Slot,这些都可以在官方提供的助手中查看。
绑定Signal和slot是使用静态函数connect。
函数原型是:
viewplaincopytoclipboardprint?
Boolconnect(constQObject*sender,constchar*signal,constQObject*receiver,constchar*method,Qt:
:
ConnectionTypetype=Qt:
:
AutoConnection)
Boolconnect(constQObject*sender,constchar*signal,constQObject*receiver,constchar*method,Qt:
:
ConnectionTypetype=Qt:
:
AutoConnection)
其中sender是发送者,而receiver是接收者,signal是信号,而method就是slot,type里面提供了几种绑定方式,可以详细查看助手。
先看一个例子,在上面代码中加入点击按钮后关闭应用程序的效果。
很简单,只需要在
viewplaincopytoclipboardprint?
button.setGeometry(100,100,300,300);
button.setGeometry(100,100,300,300);
后面加入
viewplaincopytoclipboardprint?
QObject:
:
connect(&button,SIGNAL(clicked()),&a,SLOT(quit()));
QObject:
:
connect(&button,SIGNAL(clicked()),&a,SLOT(quit()));
编译运行,点击后窗体关闭。
这是使用默认槽的例子,有时候需要点击按钮之后执行自定义的效果,那么就需要使用自定义槽了。
下面是一个使用自定义Slot的例子,鼠标点击按钮之后,文本框文字会改变。
先加入一个QLabel控件,你先加入头文件:
viewplaincopytoclipboardprint?
#include
#include
然后在connect前加入
viewplaincopytoclipboardprint?
QLabellabel("World");
label.setGeometry(50,50,300,300);
QLabellabel("World");
label.setGeometry(50,50,300,300);
先尝试编译一下,结果label没有出现在窗体里面!
它当然不会出现在窗体里面,因为我们只是对Button使用了Show()函数,尝试加入label.show(),结果出现了两个窗体,一个里面有按钮,另一个里面有一个label。
那么怎么把它们放在一起呢?
通过上面的测试发现,调用一次show就会产生一个窗口,那么是不是只调用一次show就行了?
把函数里面代码改为:
viewplaincopytoclipboardprint?
QApplicationa(argc,argv);
QWidgetwindow;
QPushButtonbutton("HELLO");
button.setGeometry(100,100,300,300);
QLabellabel("World");
label.setGeometry(50,50,300,300);
QHBoxLayoutlayout;
Layout.addWidget(&button);
Layout.addWidget(&label);
QObject:
:
connect(&button,SIGNAL(clicked()),&window,SLOT(close()));
window.setLayout(&layout);
window.show();
returna.exec();
QApplicationa(argc,argv);
QWidgetwindow;
QPushButtonbutton("HELLO");
button.setGeometry(100,100,300,300);
QLabellabel("World");
label.setGeometry(50,50,300,300);
QHBoxLayoutlayout;
Layout.addWidget(&button);
Layout.addWidget(&label);
QObject:
:
connect(&button,SIGNAL(clicked()),&window,SLOT(close()));
window.setLayout(&layout);
window.show();
returna.exec();
附上此时的头文件列表:
viewplaincopytoclipboardprint?
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
一开始,我就申请了一个QWidget,QWidget类是QT中所有用户界面对象的基类,它本身并没有什么实际意义,在这里你可以把它看成一个窗体容器,然后又添加了一个
QHBoxLayoutlayout;QHBoxLayout这是个可以对子widget进行特定布局的控件,通过它可以把按钮和label并排,之后把窗体的layout设为指定的layout,然后调用show()。
调试运行,终于两个控件都出现了。
回到之前的话题,自定义槽。
在QT中所有自定义槽都需要先编译成moc,才可以被使用。
不过你放心,这个过程由QT自动完成,当然你也可以手动进行编译,QT的Bin目录里面有moc.exe,参照说明进行使用。
你应该可以看到我已经偷偷把按钮的点击信号转向了窗体的close槽。
为什么要这样做呢,因为我们需要把自定义槽函数定义放在头文件里。
第一步,先把window封装起来,我新建一个MainWidget类,继承自QWidget类,类的头文件如下:
viewplaincopytoclipboardprint?
#ifndef_MAIN_WIDGET_H_
#define_MAIN_WIDGET_H_
#include
#include
#include
#include
classMainWidget:
publicQWidget
{
public:
MainWidget();
~MainWidget();
protected:
private:
QLabel* m_pLabel;
QPushButton* m_pButton;
QHBoxLayout* m_pLayout;
};
#endif
CPP如下:
#include"MainWidget.h"
MainWidget:
:
MainWidget()
{
m_pLabel=newQLabel("World");
m_pLabel->setGeometry(50,50,300,300);
m_pButton=newQPushButton("HELLO");
m_pButton->setGeometry(100,100,300,300);
m_pLayout=newQHBoxLayout();
m_pLayout->addWidget(m_pButton);
m_pLayout->addWidget(m_pLabel);
connect(m_pButton,SIGNAL(clicked()),t