使用C++和DirectX开发游戏GUI(二)

欢迎您继续阅读"使用C++和Directx开发GUI"的第二部分.这里是第一部分.接着我们的主题(讲解在我未来的游戏如何使用GUI(图形用户界面)),本文将解释窗体的许多神秘之处.我们将关注窗体树如何工作,为我们使用GUI制订计划,以及创建窗体类的细节,包括绘制,消息机制,坐标系统和其他所有的麻烦事儿. 在此我们将着重使用C++.如果你对纯虚函数,dynamic_cast'ing等等已经生疏了,那么赶快翻翻C++书再继续吧.不开玩笑了,让我们开始. 

  在涉及代码之前,明确我们的目标是很重要的.在我们的游戏已完成的GUI里,我们将使用一个树来跟踪显示在屏幕上的每个窗体.窗体树是个简单的N节点树.树的根部是视窗桌面(windows desktop).桌面窗体(Desktop window)的子窗体通常是应用程序的主窗体;主窗体的子窗体是对话框,对话框的子窗体是独立的对话控件(按钮,文本框等).重要的区别在于--窗体的外观并不取决于它在树中的位置.例如,许多游戏把按钮直接放在他们的桌面窗体上,就如同对话框一样. 是的,按钮也是窗体.意识到这一点是很重要的.一个按钮只是一个有着有趣外观的窗体.实际上,所有的GUI控件都是有着不同外观的简单窗体.这体现了C++的能力.如果我们创建一个继承的窗体类,给它几条虚函数,我们就能通过重载基类的函数轻易地创建我们的控件.如此应用多态性简直称得上优雅;实际上,许多C++书将它作为范例(在第三部分我将详述此点). 这是我们的基本设计,下面让我们想想应用方法. 

计划 

  当我应用我的GUI时,我做了如下几步: 

  1.首先我写了些基本的窗体管理代码.这些代码负责窗体树,增加/删除窗体,显示/隐藏窗体,把它们移动到Z坐标的顶端(即在最前显示),等等.我通过在窗体应处的位置绘制矩形完成了窗体的绘制过程,然后根据窗体的Z坐标在左上角绘制一个数字. 如果你购买或编写一个优秀可靠的指针阵列的模版类,那你的生活将会变得非常轻松.STL(标准模版库Standard Template Library)得到许多C++版本的支持,它有很多好的模板性的指针阵列类,但是如果你想使用你自己的模板类,在你应用于你的窗体管理之前要进行完整彻底的测试.现在你要注意的问题是由错误的阵列类所引起的不易察觉的内存泄漏或空指针引用. 

  2.一旦我有了基础的窗体管理函数,我花了一些时间思考我的坐标系统.写了一些坐标管理函数. 

  3.下一步,我处理窗体绘制代码.我继承一个"奇异窗体"类,并显示它如何使用一套九个精灵程序绘制自身的--其中四个精灵程序绘制角落,四个绘边,一个绘制背景. 使用这九个窗体精灵程序,使创建既有独特的艺术外观又可动态改变大小(ala StarDock's WindowBlinds)的窗体成为可能.这样做的基础是你需要有一个相当智能的绘图库,一个能处理封存精灵程序,弹性精灵程序以及集中精灵程序的库,并且它是一个非常复杂的窗体生成程序(一些艺术家可以用以创建他们的窗体的代码),这使这种方法可以实际的实现.当然,你也要注意窗体绘制速度. 

  4.一旦普通窗体的绘制代码完成,我开始实现控制部分.代码控制是简单的,但还是需要非常彻底的测试.我由简单的控制:静态,图标等开始像在前面解释的那样来回反复我的工作. 

  5.最后,完成我的控制部分后,我开始编写一个简单的资源编辑器,一个允许用户可视的放置控件,布局对话框的程序.这个资源编辑器用了我整整一个月的时间,但我强烈建议这样做(而不是用文本文件去决定位置)--图形化对话框的建立非常容易,并且这也是一个好的练习:在完善中我在我的控制部分的代码中没有发现几个bug,在实际的程序中被证明是很难解决的. 

  我被编写一个可以转换MSVC++的资源(.RC)文件为我的GUI可使用的资源文件的程序的这个想法困扰了好久.最后,我发现这样一个程序远比它的价值麻烦.我写这个GUI的目的就是要摆脱Windows的限制,为了正真的做到这一点,我要由自己的编辑器,使用我自己的资源文件格式,按自己的形式做事情.我决定用MFC由底层实现一个所见即所得(WYSIWYG)的资源编辑器.我的需求,我决定;你的需求也许不同.如果某人想要写一个转化器,我将很乐于听到这样的消息. 现在到哪了?这篇文章剩下的部分将探究开始的两步.这一系列的第三部分将进入令人麻木的控制代码细节.第四部分将讨论一点资源编辑器的实现和序列化窗体. 因此...让我们来开始第一步:基本的窗体管理代码. 

实现 

  我们开始.这是为我们基本窗体类定义的好的开始: 

class gui_window 

public: 
gui_window(); // boring 
~gui_window(); // boring 
virtual void init(void); // boring 
gui_window *getparent(void) { return(m_pParent); } 

///////////// 
// section I: window management controls 
///////////// 

int addwindow(gui_window *w); 
int removewindow(gui_window *w); 

void show(void) { m_bIsShown = true; } 
void hide(void) { m_bIsShown = false; } 
bool isshown(void) { return(m_bIsShown); } 
void bringtotop(void); 
bool isactive(void); 

///////////// 
// Section II: coordinates 
///////////// 

void setpos(coord x1, coord y1); // boring 
void setsize(coord width, coord height); // boring 

void screentoclient(coord &x, coord &y); 

int virtxtopixels(coord virtx); // convert GUI units to actual pixels 
int virtytopixels(coord virty); // ditto 

virtual gui_window *findchildatcoord(coord x, coord y, int flags = 0); 

///////////// 
// Section III: Drawing Code 
///////////// 

// renders this window + all children recursively 
int renderall(coord x, coord y, int drawme = 1); 

gui_wincolor &getcurrentcolorset(void) 
{ return(isactive() ? m_activecolors : m_inactivecolors); } 

///////////// 
// Messaging stuff to be discussed in later Parts 
///////////// 

int calcall(void); 

virtual int wm_paint(coord x, coord y); 
virtual int wm_rendermouse(coord x, coord y); 
virtual int wm_lbuttondown(coord x, coord y); 
virtual int wm_lbuttonup(coord x, coord y); 
virtual int wm_ldrag(coord x, coord y); 
virtual int wm_lclick(coord x, coord y); 
virtual int wm_keydown(int key); 
virtual int wm_command(gui_window *win, int cmd, int param) { return(0); }; 
virtual int wm_cansize(coord x, coord y); 
virtual int wm_size(coord x, coord y, int cansize); 
virtual int wm_sizechanged(void) { return(0); } 
virtual int wm_update(int msdelta) { return(0); } 

protected: 

virtual void copy(gui_window &r); // deep copies one window to another 

gui_window *m_pParent; 
uti_pointerarray m_subwins; 
uti_rectangle m_position; 

// active and inactive colorsets 
gui_wincolor m_activecolor; 
gui_wincolor m_inactivecolor; 

// window caption 
uti_string m_caption; 
}; 

  当你细读我们讨论的函数,你将会发现递归到处可见.比如,我们的程序将通过调用一个源窗体的方法renderall()来绘制整个GUI系统,这个方法又将回调它的子窗体的renderall()方法,这些子窗体的renderall()方法还要调它们的子窗体的renderall()方法,以此类推.大部分的函数都遵循这种递归模式. 整个GUI系统有一个全局的静态变量--源窗体.出于安全性的考虑,我把它封装在一个全局的函数GetDesktop()中.现在,我们开始,我们来完成一些函数,由窗体管理代码开始,如何? 

窗体管理 

/**************************************************************************** 
addwindow: adds a window to this window's subwin array 
****************************************************************************/ 
int gui_window::addwindow(gui_window *w) 

if (!w) return(-1); 
// only add it if it isn't already in our window list. 
if (m_subwins.find(w) == -1) m_subwins.add(w); 
w->setparent(this); 
return(0); 

/**************************************************************************** 
removewindow: removes a window from this window's subwin array 
****************************************************************************/ 
int gui_window::removewindow(gui_window *w) 

w->setparent(NULL); 
return(m_subwins.findandremove(w)); 

/**************************************************************************** 
bringtotop: bring this window to the top of the z-order. the top of the 
z-order is the HIGHEST index in the subwin array. 
****************************************************************************/ 
void gui_window::bringtotop(void) 

if (m_parent) { 
// we gotta save the old parent so we know who to add back to 
gui_window *p = m_parent; 
p->removewindow(this); 
p->addwindow(this); 


/**************************************************************************** 

isactive: returns true if this window is the active one (the one with input focus). 
****************************************************************************/ 
bool gui_window::isactive(void) 

if (!m_parent) return(1); 
if (!m_parent->isactive()) return(0); 
return(this == m_parent->m_subwins.getat(m_parent->m_subwins.getsize()-1)); 

  这一系列函数是处理我所说的窗体管理:新建窗体,删除窗体,显示/隐藏窗体,改变它们Z坐标.所有的这些都是完全的列阵操作:在这里你的列阵类得到测试. 在增加/删除窗体函数中唯一感兴趣的问题是:"谁来对窗体指针负责?"在C++中,这总是一个问自己得很好的问题.Addwindow和removewindow都要获得窗体类的指针.这就意味这创建一个新的窗体你的代码新建一个指针并通过addwindow()把指针传到父(桌面)窗体.那么,谁来负责删除你新建的指针呢? 

  我的回答是"GUI不拥有窗体指针;游戏本身负责增加指针".这与C++的笨拙规则"谁创建谁删除"是一致的. 

我选择的可行的方法是"父窗体为它的所有子窗体指针负责".这就意味着为了防治内存泄漏,每个窗体必须在它的(虚拟)析构函数(记住,有继承类)中搜寻它的子窗体列阵并且删除所有的包括在其中的窗体. 

如果你决定实现一个拥有指针系统的GUI,注意一个重要的原则--所有的窗体必须动态的分配.这样的系统崩溃最快的方法是把一个变量的地址传到堆栈中,如调用"addwindow(&mywindow)",其中mywindow被定义为堆栈中的局部变量.系统将好好工作直到mywindow超出它的有效区,或其父窗体的析构函数被调用,此时系统将试图删除给地址,这样系统即崩溃.所以说"对待指针一定要特别的小心". 

这就是为什么我的GUI不拥有窗体指针的主要原因.如果你在你的GUI中处理大量复杂的窗体指针(也就是说,比如你要处理属性表),你将更想要这样一个系统,它不必跟踪每一个指针比且删除只意味着"这个指针现在为我所控制:只从你的列阵中移走它但并不删除它".这样只要你能保证在指针超出有效区前removewindow(),你也可以使用(小心)在堆栈中的局部变量地址. 

继续?显示和隐藏窗体通过一个布尔型变量来完成.Showwindow()和hindewindow()只是简单的设置或清除这个变量:窗体绘制程序和消息处理程序在它们处理任何之前先检查这个"窗体可见"标志位.非常简单吧! 

Z坐标顺序也是相当的简单.不熟悉这种说法,可把z坐标顺序比为窗体"堆栈"一个重叠一个.一开始,你也许想像DirectDraw处理覆盖那样实现z坐标顺序,你也许决定给每个窗体一个整数来描述它在z坐标的绝对位置,也就是说,可能0表示屏幕的顶端,则-1000代表最后.我想了一下这种Z坐标顺序实现方法,但我不赞成--Z坐标绝对位置不是我所关心的;我更关心的是他们的相对位置.也就是说,我不需要准确的知道一个窗体在另一个的多后,我只要简单的知道这个给定的窗体在另一个的后面还是前面. 

所以,我决定实现Z坐标顺序如下:在列阵中有最大的索引值,m_subwins,的窗体在"最前".拥有[size-1]的窗体紧跟其后,紧接着是[size-2],依次类推.位置为[0]的窗体将在最底.用这种方法Z坐标顺序实现变得非常容易.而且,一举两得,我将把最前的窗体视为活动窗体,或更技术的说法,它将被视为拥有输入焦点的窗体.尽管我的GUI使用的这种"始终最前"窗体是有限制的(比如,在Windows NT中的任务管理器不管输入焦点始终在所有的窗体之前),我觉得这样有利于使代码尽可能的简单. 

当然,我用数列表示Z坐标顺序在我移动窗体到最前时处理数列付出了一些小的代价.比如,我要在50个窗体中将第二个窗体移到最前;我将为了移动二号窗体而移动48个窗体.但信运的是,移动窗体到Z坐标最前不是最耗时的函数,即使是,也有很多好的快的方法可以处理,比如链表即可. 

看看我在bringtotop()函数中的小技巧.因为我知道窗体不拥有指针,我就删除这个窗体又马上创建一个,非常有效率的将它重定位在数列最前.我这样做是因为我的指针类,uti_pointerarray,已经被编写好了一旦删除一个元素,所有的更高的元素将向后移动. 

时间: 2025-01-29 08:11:38

使用C++和DirectX开发游戏GUI(二)的相关文章

使用C++和Directx开发游戏GUI(四)

欢迎回到"使用C++和DX开发GUI"的第三部分.接着我们的主题(描述我如何为我未来的游戏构建GUI),本文将探讨建造GUI所需的一些通用控件.我们将详细描述几种不同的控件形式,包括按钮,列表框,文本框等等.    这一节并不像其他章节那样有很多的代码--这主要是因为我们程序员对于GUI的外观是很挑剔的.我们喜欢把我们的按钮,文本框和GUI做的看起来独一无二,并且符合我们自己的审美标准.这样的结果是,每个人的控件代码都很不同,而且不会想要我的特殊的绘制代码.此外,写绘制GUI元素的代码

使用C++和DirectX开发游戏GUI(六)

保存窗口    窗口序列化(存储和载入窗口)对你的工程而言或许不重要.如果你的游戏GUI很简单,你可以全靠程序在游戏中实现窗口.但如果你的GUI相对复杂,或者随着开发的过程经常会改变,那么你会想写程序以把一个窗口(和所有它的子窗口)存到文件里,然后再装载它.对初学者,窗口序列化代码允许你改变游戏的GUI而不用重新编译,而且它对于多人协调工作也是有益的.    我的计划是从主对话窗口开始,然后遍历它的所有子窗口,把每一个存到磁盘上.如果我用c语言写程序的话,我告诉自己的第一句话是"好的,如果我必须

使用C++和DirectX开发游戏GUI(五)

// represents a column in our listbox class gui_listbox_column { public: gui_listbox_column() { } virtual ~gui_listbox_column() { }  virtual void draw(uti_rectangle &where);  void setname(const char *name) { m_name = name; } uti_string getname(void)

c++-C++如何开发游戏服务端?

问题描述 C++如何开发游戏服务端? 对于动作类.格斗类游戏,有一个很大的问题,就是需要做碰撞检查和寻路. 如果是单机游戏,客户端有非常非常多的引擎,例如unity3d等. 但如果是网游对战游戏,如果都是客户端判断的话,容易不同步,也容易作弊. 我想请问,服务端做碰撞检查的话,有没有类似的引擎? 如果没有,一般该如何处理? 解决方案 你没有了解过游戏开发.根本不需要将这种表现同步. 动作类游戏,每个目标在服务端就是一个点,二点之间有攻击距离,在攻击距离内就可以攻击,否则不能,客户端发起攻击请求然

html5能开发游戏,不需要其它语言吗?

问题描述 html5能开发游戏,不需要其它语言吗? 权html5就能开发游戏吗?不需要其它语言?不需要js等其它辅助语言就能开发游戏? 解决方案 广义的html5包括css3和js.怎么能不用js呢?另外,如果游戏要实现一些复杂的功能,还是需要服务器端的. 解决方案二: jChartFX,Html5,Css3,SVG 解决方案三: HTML和css是连体的般一起用,再加上js就可以实现一些很炫酷的功能,无论你用他们来开发网页还是干其他的.光用HTML就像盖楼房光用砖不用钢筋一样 解决方案四: j

世界级的Android测试开发流程(二)

本文讲的是世界级的Android测试开发流程(二), 在我们的上一篇博客文章,"世界级的Android测试开发流程(一)",我们开始讨论一个Android的测试开发流程.我们讨论了一个软件工程师从开始写测试到找到测试开发的一些问题的演化过程.我们获得了以下结论,概括如下: 自动化测试是成功的软件开发的关键. 为了写特定类型的测试,可测试的代码是必须的. 一些开发者对测什么与怎么测一无所知,就开始写测试. 我们的测试的质量与可读性并不总是能达到预期. 一个测试开发流程对定义测什么与怎么测

请问AS开发游戏是用的Adobe flash professinal吗?

问题描述 请问AS开发游戏是用的Adobe flash professinal吗? 请问AS开发游戏是用的Adobe flash professinal吗? 一般流行的是用什么呢? 解决方案 一般用flexbuilder 解决方案二: Flash as3空袭游戏Flash as3空袭游戏Flash AS游戏引擎原理

开发游戏怎么入门呀

问题描述 开发游戏该学什么呀?和3D动漫设计是什么关系?做游戏不用学动漫那些么?好难呀.可以分为那几个方向和怎样入门呢?请高手回答. 解决方案 策划:全能的,数值策划.任务脚本策划.功能策划程序:服务器.客户端.工具美术:角色原画,场景原画,特效.场景3D建模,角色3D建模,动作,图标绘制测试员:什么都测试.这个是职责分配的,如果你做程序的话,那些什么3d动漫是美术做的......解决方案二:怎样入门呢? 那得看你以上那个放下感兴趣啊..然后学习相关基础知识,再找相关工作,然后再在工作中深入啊.

c++ 发号系统-C++开发游戏推广平台的发号系统

问题描述 C++开发游戏推广平台的发号系统 用C++可以开发发号系统吗,可以的话使用什么知识进行开发的,谢谢 解决方案 不玩游戏-所以发号平台是做什么?生成不重复的号码? 解决方案二: 对,生成激活码,可以用来换游戏英雄皮肤,你知道怎么做吗 解决方案三: 对,生成激活码,可以用来换游戏英雄皮肤,你知道怎么做吗