从零开始做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提醒你的时候指定)。此过程可以参考

http://wiki.ogre3d.cn/wiki/index.php?title=CMake_%E5%BF%AB%E9%80%9F%E5%85%A5%E9%97%A8%E6%89%8B%E5%86%8C

用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。新建完编译通过,运行发现这是一个基本窗口。

从零开始做3D地图编辑器 基于QT与OGRE
如果编译OIS没有成功,请在项目属性里面填入DX的include和lib路径。

 

三、开始之前的配置

我看到过很多同志在做项目时,直接新建项目后立马就直接开始编程,使用的是VS默认目录,结果在Debug的时候老是找不到dll,找不到资源,然后又花了一堆的时间去查找问题,白白地浪费了不少时间,更有甚者就在此时便失去了继续向下的动力,觉得这个东西太难理解了(一遇挫折就跑)。所以我觉得在每次开始项目前都应该好好地把解决方案配置一下。

我做项目的时候喜欢这种方式,项目目录下面存在以下几个目录。

 从零开始做3D地图编辑器 基于QT与OGRE

 

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默认支持的格式。

 

二、基本框架布局

         根据上一节的一些基本需求,做出一个简单的布局如下图:

 从零开始做3D地图编辑器 基于QT与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

上一篇:力扣860、柠檬水找零


下一篇:ogre 学习笔记 - Day 0