Using design patterns in game engines
By Rasmus Christian Kaae
kaae@daimi.au.dk
Student of computer science and programmer at TietoEnator Consulting A/S
August 2001
All rights reserved
译:Room3rd@hotmail.com
2005年4月5日
1.0 简介
关于模式设计,我们并不陌生,在项目设计初始阶段,能给予我们很大帮助,提供如何分段的方针及如何通过分化为若干函数成为更小的段。本文中我将利用模式设计就如何构造游戏引擎说明我的观点。这些认识主要基于个人经验及Aarhus综合大学计算机科学学院中的学习。出于不同目的及需要,设计模式也不同,这里我通过构造游戏的Model-view-control来说明基本的模式设计,以及3D引擎的内核构造。
阅读之前,建议您对C++的多态性先有所了解。
2.0 引擎内核的Model-view-control
引擎内核应该能处理游戏中涉及的所有任务,这就意味着处理包括用户输入、其他用户的可能输入(网络游戏)、游戏事件,当然还应该包括音频、视频输出。为了达到该目的,所以我引出了Model-view-control设计模式,该模式把引擎划分为三个重要部分。
Model-view-control对于GUI程序设计而言非常有用,它可以提供某种程度的模块性,使得程序可以利用已有的图形输出设备实现。例如,利用最新的DirectX SDK,游戏引擎可以实现创建一个执行的DirectX程序,如果换成OpenGL也不成问题。由于关于游戏引擎及DirectX的对话框的实现都隐藏在view类中,所以移植就太简单了—应该只有view类需要变更。
关于model-view-control结构的详细描述可以到http://ootips.org/mvcpattern.html ;及http://compsci.about.com/cs/mvcpattern/index.htm浏览察看。
2.1 模型(model)
结构模型应包含所有游戏结构,例如3D-引擎结构、行为模式(behaviour pattern,针对用户输入如何处理)。
模型部分与结构的view及control部分无关,这就意味着在另外的环境中模型部分可单独使用。
2.2 View
View部分包含所有对用户的输出,所以应该包含视频音频传达。例如,这里需要实现OpenGL及DirectX驱动。
该部分与模型部分有关系,也就是说view部分能够在用户与模型当前状态间传递信息。
2.3 Control
Control部分是结构中最基本的部分。初始化后,它分配一个view实例及模型类实例,并等待用户输入。然后用户的输入经分析交模型部分处理,之后view被请求映射更新当前模型状态。
2.4 示例代码
以下代码为伪-C++代码,所以不能被编译。
/* InputBlock 包含用户的当前输入 */
class InputBlock
{
// todo : 添加一些有意思的变量及函数来显示用户输入
};
/* 游戏模型 */
class Model
{
public:
Engine3D *m_engine;
Sound *m_sound;
Model();
UpdateControl(InputBlock *input)
{
// todo : 反作用于当前输入
}
};
/* ViewVisual是一个接口类,用来实现视频驱动 */
class ViewVisual
{
public:
ViewVisual()
{
}
// 纯虚函数用来处理当前模型中的变化
virtual void Update(Model *model)=0;
}
/* ViewVisualOpenGL是ViewVisual接口的一个实现 */
class ViewVisualOpenGL
{
public:
ViewVisualOpenGL()
{
// todo : 初始化代码
}
// 显示模型的当前状态
void Update(Model *model)
{
// todo : 绘制model->m_engine
}
};
/* ViewAudio为音频驱动实现的接口类 */
class ViewAudio
{
public:
ViewAudio()
{
// todo: 初始化代码
}
// 纯虚函数包含当前模型变化的处理
virtual void Update(Model *model)=0;
};
/* ViewAudioDirectSound 为ViewAudio接口的DirectSound实现 */
class ViewAudioDirectSound
{
public:
ViewAudioDirectSound()
{
// todo : 初始化代码
}
void Update(Model *model)
{
// todo : 从model->m_sound播放当前声音
}
};
/* View为连接了视频、音频的类 */
class View
{
protected:
ViewVisual *m_visual;
ViewAudio *m_audio;
public:
View(Model *model, ViewVisual *visual, ViewAudio *audio)
{
m_model=model;
m_visual=visual;
m_audio=audio;
}
void Update()
{
m_visual->Update(m_model);
m_audio->Update(m_model);
}
};
/* Control类处理用户输入并启动输出*/
class Control
{
enum
{
ESCAPE_IS_PRESSED,
USER_INPUT
};
private:
View *m_view;
Model *m_model;
public:
Control()
{
m_model = new Model();
m_view = new View(m_model, new ViewVisualOpenGL(),
new ViewAudioDirectSound());
}
~Control()
{
delete m_model;
delete m_view;
}
InputBlock Input(int which)
{
switch (which)
{
case USER_EXIT :
if (escape_is_pressed) return newInputBlock(ESCAPE_IS_PRESSED);
break;
case USER_INPUT :
if(user_has_made_an_input) return new InputBlock(the_new_input_info);
break;
}
}
void Run()
{
while (!Input(USER_EXIT))
{
if (Input(USER_INPUT))
m_model->UpdateControl(Input(USER_INPUT));
m_view->Update();
}
}
};
/* main函数分配我们的结构,并开始运行*/
void Main(int argc, char **argv)
{
Control *control = new Control();
control->Run();
delete control;
}
2.4.1 关于代码的说明
以上代码将首先初始化分配一个控制器(controller),然后由控制器分配了一个OpenGL输出设备、一个DirectSound输出设备及一个模型,完成分配后,主程序调用了控制器的Run()函数,该函数在其内部某个位置实现了一个循环知道用户按下了退出键。程序将按照给出的输入一直更新模型及view直到用户退出。如果给出了输入,输入经过分析传递给模型模块进行处理。当输入(或缺少输入)已被处理时,控制器请求view进行更新。退出时,以上也很好的示范了清除代码。
2.5 一种图形表示
上图示意了当前model-view-control结构之间的联系,Control向model及view两者传递消息,而view从model检索数据。在实际实现时,这些关系应该变更以适应实际需要。I我喜欢以上方式,因为各类间有充分的独立性。然而,可能添加一个model及view到controller之间的通讯线路比较好,这样允许在发生类似突然错误时发送通知。
3.0 3D引擎的层模式
组织3D引擎内核时使用层模式的初衷是为了尽可能简便的访问代码。作为一个示例,它应该能够“说”scene->m_activeCamera->LookAt(pos,target),然后整个场景就会按此改变;同样应该在“隐藏”层中进行3D-model载入,因此任何事情都将在更高级别处理(因而绘制在引擎内核的view部分实现)。整个引擎将被分化为至少3层,每层都是上层的扩展,而且越来越低级。
3.1 顶层
顶层提供了修改更新当前场景状态的基本接口。上层把输入传递给结构的低层,例如,玩家移动到一个新位置,顶层就会确认特定的玩家对象会得到一个新的平移点,下次重画时才能保证玩家在正确的位置上。灯光及摄像机也会如此(在第一人称的设计游戏中,摄像机就是玩家视角)。
3.2 第二层
第二层更为深入,包含所有类及将来扩展的接口类,例如不同类型的光源(聚光灯、泛光灯、发光点等等)。
3.3 第三层及更低的层次
这些层将包含具体实现,如灯光、hiearchy-objects等等。
3.4 示例代码
为了说明(不明确的)3D-引擎层的定义,这里给出了一些示例代码。
/* 第二层 — 灯光,包含要实现的各种不同类型光源接口 */
class Light
{
public:
enum
{
OMNI
};
int m_light_type;
virtual void* GetData()=0;
};
/* 第三层- lightomni – 灯光的直接实现*/
class LightOmni : public Light
{
public:
LightOmniData *m_light_omni_data;
LightOmni() { m_light_type=OMNI; }
void *GetData() { return m_light_omni_data; }
};
/* 第二层 — 对象,包含要实现的对象接口 */
class Object
{
public:
Vector *m_vertices;
Face *m_faces;
// 设置指定对象的当前平移点
virtual void SetTranslation(Vector &v)=0;
// 以大量数据填充指针
virtual void GetObjectData(Vector *out_vertices, Face *out_faces,
int *out_num_faces)=0;
};
/* 第三层—Objecthierachy – 对象的直接实现 */
class ObjectHierachy : public Object
{
public:
ObjectHiearchy *m_child;
Vector m_translation;
// 设置当前平移点
void SetTranslation(Vector v) { translation=v; }
// 以大量数据填充指针
void GetObjectData(Vector *out_vertices, Face *out_faces,
int *out_num_faces)
{
ObjectHiearchy *o = this;
while (o->child!=NULL)
{
// 从"o"添加数据到out_vertices, out_faces及out_num_faces
}
}
};
/* 包含分层结构的上层
注意:建议在场景类中使用链接列表(linkedlists)而不是静态数组,由于这里仅作范例,故不作详细描述*/
class Scene
{
public:
Camera *m_currentCamera;
Camera **m_cameras;
Light **m_lights;
Object **m_objects;
};
3.5 图示
顶层
SCENE
第二层
Objects
Light
Camera
第三层
HIEARCHY
Other
Spot
Other
上图示意了各层间类的关系。应用以上设计,你可以摆脱繁琐迂腐的细节,而对每个任意的对象类型进行抽象,并编写通用代码以完成Object类(还有Light、Camera)具体实现。
4.0 结束语
由于需要,我写下了该文—我已经查找类似文章有段时间了(而且没那么幸运)。该文更应该被视为实现游戏引擎的一个启发(它也许不是完美的解决方案)。作为一名程序员,依我的经验,(开发中)使用设计模式非常好,since it allows you to lean back and use trusted structures,instead of reinventing the wheel(不知怎么翻译较好J)。
Chapter 3.0 and 4.0 might be subject to discussions since there are many individual elements to beconsidered. For the 3D-engine the implementation may differ from one 3D-editor to another.
Have fun with the programming,
Rasmus Christian Kaae