从零开始做3D地图编辑器 基于QT与OGRE
第一章 基础知识
注:文章里面有不少个人见解,欢迎大家一起互相讨论。希望高人能给予相应理解与意见建议。
在实际3D游戏开发中,编辑器是极其重要的一个部分,一个优秀健壮的编辑器,可以使项目事半功倍,而相反,一款BUG超多(随时会挂)又不注重操作习惯(完全基于快捷键,又没有详细的使用说明)的编辑器,不仅会使项目事倍功半,而且会削弱开发人员的积极性,甚至让开发人员对项目产生排斥情绪。
编辑器在游戏里面应用很广泛,一般都有地图编辑器(关卡、世界)、粒子编辑器、动画编辑器、字体编辑器(单机里面较多)、UI编辑器、材质编辑器、脚本编辑器等等,编辑器设计制作方法也大致可分为两个趋势,一种是倾向于做大而全的世界编辑器,一种是做小而精的功能编辑器,在这里我不想讨论这两者的利与弊,我只能说,只要这个解决方案可以解决我们当前的问题,那么它就是一个适合现阶段的解决方案,但并不一定是最好的解决方案。
一、工具
现在制作编辑器,流行以下几种方式:
1、 使用C#制作基于WinForm的编辑器。
2、 制作基于MFC的编辑器。
3、 制作基于WxWidgets的编辑器。
4、 制作基于QT的编辑器。
基于C#来制作编辑器,在制作一些小工具上面很有利,比如说打包工具,加密器等等和图形关系不大的工具,它的优势在于它的简单易用,但是当你涉及到图形这一块的时候,如果引擎支持不C#,那么使用XNA、Manage DX 都不是很好的一种解决方案(除非你的游戏就是基于两者),导入动态链接库的方法又会比较麻烦,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源码外,你需要额外下载Microsoft Visual C++ Dependencies Package,并把它解压到OGRE目录(你自己的OGRE目录)后编译。
2、你需要下载CMAKE,官方网站是www.cmake.org。下载一个最新版本就行。
3、你机器需要安装DX的SDK,不然OIS和DX的渲染系统插件无法编译。
4、使用Cmake生成Ogre VS解决方案的时候要记得指定Dependencies目录(在Cmake提醒你的时候指定)。此过程可以参考
用VS打开生成的解决方案,
然后直接编译就可以获得dll和lib.
2、QT下载
QT官方网站:http://qt.nokia.com/products
下载QT也有两种方式,一种是纯SDK(Qt SDK for Windows* (287 MB)),另外一种是针对VS2008的库(Qt libraries 4.6.2 for Windows (VS 2008, 194 MB)),这两者有一定的区别,前者带有更多的工具(IDE等)。我推荐下载针对VS2008的库,下载安装完之后,还需要下载一个Addin,这个Addin比较难找,在Other downloads里面下载Visual Studio Add-in (44 MB)。
安装完Add-In之后,打开VS2008应该就可以找到QT的模板了。QT4 projects下面有一些选项,选择新建一个QT Application。新建完编译通过,运行发现这是一个基本窗口。
如果编译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目录里面的工程文件。
# Visual Studio 2008
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中新建QT Application,QT头文件与库的相关配置会自动帮你设置好。你只需要在它的基础上把其他库添加好就行了。
四、QT基本知识
回到QT,先在VS中新建一个QT Application,项目里面有几个目录:
1、 Form Files目录,它里面放的是使用QT designer制作的基于XML的布局文件,双击它就会自动进入QT designer。
2、 Generated Files目录,它里面放的是一些临时生成的文件,这些文件用来处理QT的信号和槽等机制。
3、 Resource Files目录,它里面放的是基于XML的资源文件,你可以在窗体里面使用它们。
4、 Header Files和Source Files这两个和VS默认是一样的。
理解了目录结构之后,先来试着写一个Hello World,先把除了main.cpp之外的所有文件移除(使用QT designer会提高制作效率,但是会让QT入门门槛变高)!打开main.cpp,仅保留以下代码:
view plaincopy to clipboardprint?
#include <QtGui/QApplication>
int main(int argc, char *argv[])
{
QApplication a(argc, argv);
return a.exec();
}
#include <QtGui/QApplication>
int main(int argc, char *argv[])
{
QApplication a(argc, argv);
return a.exec();
}
编译通过。运行没有任何反应,因为还没有往里面增加任何东西。
在代码中,Main函数是C语言的入口,之后申请的QApplication用来管理控制流和主要设置,这是核心,一定要保留。
按钮是GUI中最基本的一个控件,先看看怎么增加一个按钮。使用按钮控件必须先包含头文件:
view plaincopy to clipboardprint?
#include <QtGui/QPushButton>
#include <QtGui/QPushButton>
然后在QApplication a(argc, argv);与return a.exec();中间插入下面代码:
view plaincopy to clipboardprint?
QPushButton button("HELLO");
button.setGeometry(100,100,300,300);
button.show();
QPushButton button("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。函数原型是:
view plaincopy to clipboardprint?
Bool connect ( const QObject * sender, const char * signal, const QObject * receiver, const char * method, Qt::ConnectionType type = Qt::AutoConnection )
Bool connect ( const QObject * sender, const char * signal, const QObject * receiver, const char * method, Qt::ConnectionType type = Qt::AutoConnection )
其中sender是发送者,而receiver是接收者,signal是信号,而method就是slot,type里面提供了几种绑定方式,可以详细查看助手。
先看一个例子,在上面代码中加入点击按钮后关闭应用程序的效果。很简单,只需要在
view plaincopy to clipboardprint?
button. setGeometry(100,100,300,300);
button. setGeometry(100,100,300,300);
后面加入
view plaincopy to clipboardprint?
QObject::connect(&button, SIGNAL(clicked()), &a, SLOT(quit()));
QObject::connect(&button, SIGNAL(clicked()), &a, SLOT(quit()));
编译运行,点击后窗体关闭。
这是使用默认槽的例子,有时候需要点击按钮之后执行自定义的效果,那么就需要使用自定义槽了。
下面是一个使用自定义Slot的例子,鼠标点击按钮之后,文本框文字会改变。
先加入一个QLabel控件,你先加入头文件:
view plaincopy to clipboardprint?
#include <QtGui/QLabel>
#include <QtGui/QLabel>
然后在connect前加入
view plaincopy to clipboardprint?
QLabel label("World");
label.setGeometry(50,50,300,300);
QLabel label("World");
label.setGeometry(50,50,300,300);
先尝试编译一下,结果label没有出现在窗体里面!它当然不会出现在窗体里面,因为我们只是对Button使用了Show()函数,尝试加入label.show(),结果出现了两个窗体,一个里面有按钮,另一个里面有一个label。那么怎么把它们放在一起呢?
通过上面的测试发现,调用一次show就会产生一个窗口,那么是不是只调用一次show就行了?把函数里面代码改为:
view plaincopy to clipboardprint?
QApplication a(argc, argv);
QWidget window;
QPushButton button("HELLO");
button.setGeometry(100,100,300,300);
QLabel label("World");
label.setGeometry(50,50,300,300);
QHBoxLayout layout;
Layout.addWidget(&button);
Layout.addWidget(&label);
QObject::connect(&button, SIGNAL(clicked()), &window, SLOT(close()));
window.setLayout(&layout);
window.show();
return a.exec();
QApplication a(argc, argv);
QWidget window;
QPushButton button("HELLO");
button.setGeometry(100,100,300,300);
QLabel label("World");
label.setGeometry(50,50,300,300);
QHBoxLayout layout;
Layout.addWidget(&button);
Layout.addWidget(&label);
QObject::connect(&button, SIGNAL(clicked()), &window, SLOT(close()));
window.setLayout(&layout);
window.show();
return a.exec();
附上此时的头文件列表:
view plaincopy to clipboardprint?
#include <QtGui/QPushButton>
#include <QtGui/QApplication>
#include <QtGui/QLabel>
#include <QtGui/QHBoxLayout>
#include <QtGui/QWidget>
#include <QtGui/QPushButton>
#include <QtGui/QApplication>
#include <QtGui/QLabel>
#include <QtGui/QHBoxLayout>
#include <QtGui/QWidget>
一开始,我就申请了一个QWidget,QWidget类是QT中所有用户界面对象的基类,它本身并没有什么实际意义,在这里你可以把它看成一个窗体容器,然后又添加了一个
QHBoxLayout layout; QHBoxLayout这是个可以对子widget进行特定布局的控件,通过它可以把按钮和label并排,之后把窗体的layout设为指定的layout,然后调用show()。
调试运行,终于两个控件都出现了。
回到之前的话题,自定义槽。在QT中所有自定义槽都需要先编译成moc,才可以被使用。不过你放心,这个过程由QT自动完成,当然你也可以手动进行编译,QT的Bin目录里面有moc.exe,参照说明进行使用。
你应该可以看到我已经偷偷把按钮的点击信号转向了窗体的close槽。为什么要这样做呢,因为我们需要把自定义槽函数定义放在头文件里。
第一步,先把window封装起来,我新建一个MainWidget类,继承自QWidget类,类的头文件如下:
view plaincopy to clipboardprint?
#ifndef _MAIN_WIDGET_H_
#define _MAIN_WIDGET_H_
#include <QtGui/QLabel>
#include <QtGui/QHBoxLayout>
#include <QtGui/QWidget>
#include <QtGui/QPushButton>
class MainWidget: public QWidget
{
public:
MainWidget();
~MainWidget();
protected:
private:
QLabel* m_pLabel;
QPushButton* m_pButton;
QHBoxLayout* m_pLayout;
};
#endif
CPP如下:
#include "MainWidget.h"
MainWidget::MainWidget()
{
m_pLabel = new QLabel("World");
m_pLabel ->setGeometry(50,50,300,300);
m_pButton = new QPushButton ("HELLO");
m_pButton ->setGeometry(100,100,300,300);
m_pLayout = new QHBoxLayout();
m_pLayout -> addWidget(m_pButton);
m_pLayout -> addWidget(m_pLabel);
connect(m_pButton, SIGNAL(clicked()), this, SLOT(close()));
setLayout(m_pLayout);
}
MainWidget::~MainWidget()
{
}
#ifndef _MAIN_WIDGET_H_
#define _MAIN_WIDGET_H_
#include <QtGui/QLabel>
#include <QtGui/QHBoxLayout>
#include <QtGui/QWidget>
#include <QtGui/QPushButton>
class MainWidget: public QWidget
{
public:
MainWidget();
~MainWidget();
protected:
private:
QLabel* m_pLabel;
QPushButton* m_pButton;
QHBoxLayout* m_pLayout;
};
#endif
CPP如下:
#include "MainWidget.h"
MainWidget::MainWidget()
{
m_pLabel = new QLabel("World");
m_pLabel ->setGeometry(50,50,300,300);
m_pButton = new QPushButton ("HELLO");
m_pButton ->setGeometry(100,100,300,300);
m_pLayout = new QHBoxLayout();
m_pLayout -> addWidget(m_pButton);
m_pLayout -> addWidget(m_pLabel);
connect(m_pButton, SIGNAL(clicked()), this, SLOT(close()));
setLayout(m_pLayout);
}
MainWidget::~MainWidget()
{
}
Main.cpp改为:
view plaincopy to clipboardprint?
#include <QtGui/QApplication>
#include "MainWidget.h"
int main(int argc, char *argv[])
{
QApplication a(argc, argv);
MainWidget window;
window.show();
return a.exec();
}
#include <QtGui/QApplication>
#include "MainWidget.h"
int main(int argc, char *argv[])
{
QApplication a(argc, argv);
MainWidget window;
window.show();
return a.exec();
}
编译运行,结果和上次一样。
接下来申请一个自定义Slot,首先在头文件public:前加入宏
Q_OBJECT;
只有加入了Q_OBJECT,你才能使用QT中的signal和slot机制。这点很重要,不然你编译的时候会报“找不到slot”的错误。
然后在protected: 前加入:
view plaincopy to clipboardprint?
private slots:
void SetText ();
private slots:
void SetText ();
slot同样也分private、public、protected,意义和c++一样。
CPP中加入相应执行:
view plaincopy to clipboardprint?
void MainWidget:: SetText ()
{
m_pLabel -> setText("Test");
}
void MainWidget:: SetText ()
{
m_pLabel -> setText("Test");
}
把connect改成:
view plaincopy to clipboardprint?
connect(m_pButton, SIGNAL(clicked()), this, SLOT(SetText ()));
connect(m_pButton, SIGNAL(clicked()), this, SLOT(SetText ()));
编译运行,这时点击按钮就会改变文字了。就这么简单。
信号也可以自定义,不过信号自定义相对来说用武之地稍微小一点,定义的方式和slot定义差不多,都得在头文件中定义,举个例子:点击按钮后文本改变,触发一个新信号,这个信号会把文字又变回来。
在头文件中加入:
view plaincopy to clipboardprint?
signals:
void TextChanged ();
signals:
void TextChanged ();
再增加一个Slot用来对这个信号进行反馈。在private slots:后加入
view plaincopy to clipboardprint?
void RecoverText ();
void RecoverText ();
CPP中加入执行:
view plaincopy to clipboardprint?
void MainWidget::RecoverText()
{
m_pLabel -> setText("Hello");
}
void MainWidget::RecoverText()
{
m_pLabel -> setText("Hello");
}
注意信号是不需要加执行代码。
然后修改SetText()函数加入触发新信号的代码:
view plaincopy to clipboardprint?
emit TextChanged();
emit TextChanged();
最后加入新的connect:
view plaincopy to clipboardprint?
connect(this, SIGNAL(TextChanged()), this, SLOT(RecoverText()));
connect(this, SIGNAL(TextChanged()), this, SLOT(RecoverText()));
编译运行,结果和我们想要的一样。
注:信号和槽都是可以有参数的。
有关QT的基础知识就介绍到这里,具体控件的使用方法,请自行参考助手。
五、OGRE基础知识
友善提醒:如果你对OGRE比较了解,请自觉跳过此节。
本节并不打算提供详细的入门教程,只是对OGRE的简单介绍,如果需要OGRE的详细资料,请自行使用网络功能。
1、OGRE是什么
Ogre是一款开源的图形渲染引擎,它的全名叫(Object-oriented Graphics Rendering Engine),目前在开源图形渲染引擎这一块排名第一,由于它功能齐全(跨平台,支持DX和OPENGL)、知名度高,而且不断更新,所以国内学习资料也比较多,在网络游戏在一块已有不少游戏公司已经使用过或者正在使用OGRE(《天龙八部》、《成吉思汗》等),部分公司招聘要求里面也明确表示熟悉OGRE者优先,所以说学习OGRE是前景可观的。
2、OGRE可以做什么
首先OGRE只是一个图形渲染引擎,连输入输出都使用第三方的OIS,目前大部分应用都在游戏、VR。但是如果你需要用它来做网络游戏,你还需要网络库、UI库、音频音效库等。
3、如何学习OGRE
OGRE自1.7以来,抛弃了它的ExampleApplication的框架,开始使用SampleBrower加dll的方式来表示例子,我个人认为虽然看起来更专业了,但是对于新人入门来说,难度比 ExampleApplication还高,尽管ExampleApplication就已经让新人晕头转向了!
那么怎么去学习OGRE呢,有一本书是必备的,名字叫《PRO OGRE 3D PROGRAMMING》(现在已经有爱好者翻译的中文版了),这本书是OGRE入门的圣典,推荐方法是先仔细地看一遍,然后再重头开始码例子,为什么推荐这样做,因为我发现有些人在学习Opengl的时候,看完glbegin,glend就不看了,甚至还动手写引擎,人家红宝书后面明确地表明尽量不要使用glbegin,glend!
官方手册也是必看的,里面对一些模块进行了详细的讲解,材质脚本说得挺细。
个人推荐OGRE入门掌握顺序:
A、 渲染窗体管理(初级:初始化,销毁)
B、 OIS输入输出(初级:两种模式(回调、缓冲)、按键处理)
C、场景管理(初级:管理器选择,节点管理、实体管理)
D、材质(初级:材质使用、材质脚本)
E、资源管理(初级:资源组、资源)
D、动画(初级:骨骼动画)
E、面片相关(初级:表层、公告板、粒子)
基本掌握这些就可以做些简单的游戏了,然后在这基础之上再慢慢探索OGRE的庞大的世界。
几个学习的地方:
1、OGRE官方网站:www.ogre3d.org、官方论坛、Addon论坛、wiki是学习OGRE的好地方。
2、中文社区:www.ogre3d.cn也聚集了不少OGRE的爱好者。
3、游戏资源网也是一个学习游戏开发的不错的网站。
请充分利用你手头上面的搜索工具,百度适合搜索国内中文资料,google适合搜索英文资料。
第二章 编辑器的基本框架
一、几个问题
前面说了很多编辑器之外的东西,真正要动手做编辑器了,也不能一股脑地就开始了,这之前必须要问自己几个问题:
1、这个地图编辑器有什么基本功能?
2、导入导出文件格式?
A、3D地图编辑器的基本功能
正如开篇所说,编辑器制作有两种趋势,其中一种是大而全的世界编辑器,这种方式可以带给极大的成就感,正合很多新人的意,但是我觉得一开始给自己(特别是新人)设定一个庞大的计划是件空洞而不现实的事情,一个编辑器越是大而全它的应用方向就越窄,越不利用拓展,使用就越费劲,问题BUG也就越集中,维护成本也就越高。
其实可以从小做起,先来分析基本需求:
所谓地图编辑器,地图编辑是其基础功能,一般地图都是在地形(平面)上面放置演员(把它叫作演员是不希望和OGRE的实体概念冲突),那么我们就确定了我们两个需求:地形编辑、演员管理。
那么这两个需求又引申出新的需求,地形不能是光模吧,演员不能永远是编辑器预设的几个模型吧,所以我们又需要实体、纹理加载与删除的功能。加载之后的纹理和实体总应该有个地方可以浏览吧,不然怎么选择使用?
好了,因为我们的目标暂时是做一个基本框架。所以我们暂时确定以下基本需求:
1、 添加删除浏览实体、纹理
2、 地形编辑
3、 演员管理
除了基本需求外,我们还有另外一些编辑器本身的一些需求:
1、 菜单、工具栏、状态栏。
2、 日志管理。
日志管理是一个很重要的东西,它得支持两种方式,一种是导成文本,另一种是在编辑器里面实时看到,为什么要提供这两种东西呢,如果没有文本,有时候挂的时候你看不到为什么挂,如果没有实时地看到每次去看文本又很麻烦。
B、文件格式
导入导出文件格式是一个很纠结的问题,现在一般流行几种方式:
1、 纯二进制数据,优点是读取速度非常快,几乎无浪费数据,缺点是不易被修改,如果没有工具基本上几乎不可能被改动(当然你要约定某些字符串也是可以的),这种方法还有不少应用。
2、 自定义格式,类似于INI,优点是终于可能手动修改了,缺点是得花不少时间去写解析模块,应该是一种过渡解决方案,这种方案和上面那种有模糊的界定,区别在于这个拥有一个解析器。
3、 XML,现在应该是主流,优点是编辑修改很方式,手改也行,工具也很多,还不用写解析器,TinyXML,RapidXML等都是不错的解析器,缺点是效率低,在特定环境下会出现偶尔读不出文件的情况(可能是解析器的问题)。
现在不少游戏使用两种1和3两种方式结合的方法,在编辑时使用XML,结果用工具导成纯二进制加密文件,我也打算使用这种方法:
编辑器配置文件(需要对窗体的开关状态进行存储)和生成的地图使用XML。导入纹理、实体使用OGRE默认支持的格式。
二、基本框架布局
根据上一节的一些基本需求,做出一个简单的布局如下图:
窗体大小:1024*768,太大了有点显示器放不下!支持最大化,最小化,关闭按钮,支持手动拖大拖小。
拥有菜单项(支持快捷键,图标):
File : New, Open, Save, Save As, Exit
Edit: Undo, Redo,Copy, Paste(这个功能暂时保留)
Window:Entity List, Texture List ,Node List, property,Log,(支持图标check)
Help:About
工具栏拥有按钮:
New, Open, Save, Save As, Undo, Redo,Copy, Paste, Entity List, Texture List , Scene, property,Log,(部分支持check)
Dock窗口(支持自由拖动,重新排列,叠加,关闭,打开):
EntityList, Texture List, Node List, property,(这四个都可以是左右两侧),log窗口(只能在下侧)
视口:OGRE的显示窗口,大小可以改变。我们这里暂时只提供一个窗口,通过状态机加多个摄影机来管理浏览。
各个Dock窗口都拥有自己的小工具栏,Entity List/Texture List/Node list都是树型结构,栏目里面拥有相应添加删除的子功能。
属性栏使用了QT的第三方库,这个库得另行下载,后面会有详细介绍。
日志栏使用QListWidget,提供SystemLog方法,添加日志后自动跳转到最新。
状态栏显示鼠标移上控件时的一些详细说明。
三、窗体、菜单栏、工具栏、状态栏
A、创建窗体
我还是按照QT基础那节的内容创建一个窗体类,不同的是这个窗体类现在继承于QMainWindow,它会使得封装中央部件、菜单条和工具条以及窗口状态变得更容易。
接下去,设置标题和窗口大小:
view plaincopy to clipboardprint?
setWindowTitle("Editor"); //设置窗口标题
resize( WINDOW_WIDTH, WINDOW_HEIGHT);// 设置窗口大小
setWindowTitle("Editor"); //设置窗口标题
resize( WINDOW_WIDTH, WINDOW_HEIGHT);// 设置窗口大小
其中WINDOW_WIDTH和WINDOW_HEIGHT被定义在头文件中:
view plaincopy to clipboardprint?
static const int WINDOW_WIDTH = 1024;
static const int WINDOW_HEIGHT = 768;
static const int WINDOW_WIDTH = 1024;
static const int WINDOW_HEIGHT = 768;
在C++里尽量减少使用宏。
设置中央部件:
view plaincopy to clipboardprint?
setCentralWidget(m_pButton);//暂时把按钮设置为中央部件
setCentralWidget(m_pButton);//暂时把按钮设置为中央部件
为什么要设置中央部件呢?因为接下去我要使用Dock Widget,如果没有中央部件,左侧,右侧,下部就没有参照,也没意义了。
针对我们的基本框架,目前也仅仅需要这些简单的功能。
B、创建菜单栏和工具栏
在QT创建菜单、工具栏前,必须先创建QAction,然后把这个QAction添加给菜单或者工具栏。
QAction是什么,它是用户的UI动作,在一列菜单中,比如说File下面的new 就是一个QAction,这个QAction包括包括图标,名字,快捷方式,状态栏信息等。
我通过以下方法来设置QAction:
头文件中加入:
view plaincopy to clipboardprint?
QAction* m_pFileNew;
QAction* m_pFileNew;
因为工具栏和菜单都共用一个QAction,所以我把它用为类成员放在头文件中。
Cpp中加入:
view plaincopy to clipboardprint?
m_pFileNew = new QAction(QIcon(":/images/new.png"), tr("&New"), this);
m_pFileNew -> setShortcuts(QKeySequence::New);
m_pFileNew -> setStatusTip(tr("Create a new map"));
connect(m_pFileNew, SIGNAL(triggered()), this, SLOT(newFile()));
m_pFileNew = new QAction(QIcon(":/images/new.png"), tr("&New"), this);
m_pFileNew -> setShortcuts(QKeySequence::New);
m_pFileNew -> setStatusTip(tr("Create a new map"));
connect(m_pFileNew, SIGNAL(triggered()), this, SLOT(newFile()));
第一行是申请一个QAction,第一个参数是指定一个图标QIcon,待会我才来讨论图片的路径。第二个参数就是QAction显示的文字内容,tr()是将来用作本地化的,你只需要记得在你文本前加上tr()就行,this是父部件指针。
第二行是设置一个快捷键,在这里使用的是QT定义的NEW快捷键,你也可以使用QKeySequence(Qt::ALT + Qt::Key_E)的方式来取得QKeySequence。
第三行就是设置状态栏要显示的文本。
第四行是设置信号与槽,这里使用的是自定义槽,不熟悉的话回过头去看QT基础知识那节。
依照以上方法分别设定好(New,Open,Open,Open As,Exit),我想你这时候应该用一个函数管理了这些QAction的生成,这是一种好的习惯,不要把一大堆的函数都挤在构造函数里面,原则上超过50行的函数就得考虑增加一个新函数。
接下去把QAction添加进菜单和工具栏里面去。在QT4.6里面菜单使用的是QMenu类(以前是使用QPopupMenu,如果你看到一些教程上面写的是这个,那么你最好换一个教程),工具栏使用的是QToolBar类。
因为当前窗体继承于QMainWidow,所以可以通过menuBar()函数来获得窗体菜单条指针(菜单条和菜单不是同一个东西,菜单条指的是那一行可以放菜单的长条,而菜单只是File那一列),把菜单添加到菜单条里面去,就可以在菜单条上看到了。
注:菜单有两种方式,一种是添加进菜单条后变成固定菜单,另一种是弹出式菜单,两者区别不大,这个在后面会详细说明。
头文件中加入:
view plaincopy to clipboardprint?
QMenu* m_pFile;
QMenu* m_pFile;
Cpp中加入:
view plaincopy to clipboardprint?
m_pFile = menuBar()->addMenu(tr("&File"));
m_pFile -> addAction(m_pFileNew);
m_pFile -> addSeparator();
m_pFile = menuBar()->addMenu(tr("&File"));
m_pFile -> addAction(m_pFileNew);
m_pFile -> addSeparator();
第一行首先是获得菜单条的指针,然后添加一个File的新菜单,并把返回指针。
第二行是把QAction增加进菜单。
第三行是在QAction中间增加一个分隔条(横条)。
添加工具栏相对来说比添加菜单还更简单,你甚至还不用menuBar()取得菜单条,看代码:
头文件中加入:
view plaincopy to clipboardprint?
QToolBar* m_FileToolBar;
QToolBar* m_FileToolBar;
Cpp中加入:
view plaincopy to clipboardprint?
m_FileToolBar = addToolBar(tr("File"));
m_FileToolBar->addAction(m_pFileNew);
m_FileToolBar -> addSeparator();
m_FileToolBar = addToolBar(tr("File"));
m_FileToolBar->addAction(m_pFileNew);
m_FileToolBar -> addSeparator();
第一行增加一个名字叫File的工具栏,File这个文字不显示,它会生成一个特别的分隔条:两条竖杠,如果这个工具栏不是第一个工具栏的话,它可以被左右拖动。每增加一个工具栏都会产生这个分隔条。这是使用起来很简单也很有效果的东西。
第二行是把QAction增加进工具栏。
第三行是在QAction中间增加一个分隔条(单行竖条)。
上面都是最基本的函数,还有两个函数经常使用。
view plaincopy to clipboardprint?
m_ FileToolBar ->setIconSize(QSize(20,20));
m_ FileToolBar ->setToolButtonStyle(Qt::ToolButtonIconOnly);
m_ FileToolBar ->setIconSize(QSize(20,20));
m_ FileToolBar ->setToolButtonStyle(Qt::ToolButtonIconOnly);
第一个是设置显示图标大小。
第二个是设置按钮风格,现在是只显示图标,它有好几种文字和图标显示风格,可以在助手查看详细说明。
A、 状态栏
状态栏就非常简单了,工具栏与菜单栏都自动更改状态栏信息,如果你要手动更改的话,就直接加上这句话:
view plaincopy to clipboardprint?
statusBar()->showMessage(tr("Ready"));
statusBar()->showMessage(tr("Ready"));
这是在窗体创建时状态栏显示的内容。
本文来自CSDN博客,转载请标明出处:http://blog.csdn.net/vickylh/archive/2010/05/19/5608329.aspx
再分享一下我老师大神的人工智能教程吧。零基础!通俗易懂!风趣幽默!还带黄段子!希望你也加入到我们人工智能的队伍中来!https://blog.csdn.net/jiangjunshow