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


    如果编译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

  • 相关阅读:
    让你一次性搞定堆、栈、值类型、引用类型…… (Part 2) (zt)
    延时加载图片(原创)
    Bambook 简介
    无框架Ajax分页(原创)
    windows系统下Python环境的搭建
    将制定内容输出为压缩后的xls文件
    (原创)对DateTime进行强制约束
    VS 2010中JS代码折叠插件
    延时加载图片(终极版,解决一行多张图片无法显示的问题)
    javascript this详解(转)
  • 原文地址:https://www.cnblogs.com/skiwnchiwns/p/10343966.html
Copyright © 2020-2023  润新知