An Introduction to Game Engine and OO Design Patterns
You can’t connect the dots looking forward; you can only connect them looking backwards
— Steve Jobs, Stanford Report, June 14, 2005
预计时间:3-4 * 45 min
A game engine is a software-development environment designed for people to build video games.
根据这个定义,从 Construct2 到 Unreal Engine 都是游戏引擎。之所以 Unreal Engine 才是你眼中引擎是因为 EPIC Game 能够提供电影艺术级别效果,一下就吸引了你的眼球。与指相比而 Construct2 等引擎尽管默默无闻,但也是无数游戏设计师、开发者的最爱。
【注】游戏引擎产生了如此难以置信的精美而“真实”的场景画面,几乎每个人都梦想能开发这样的引擎!
简而言之,游戏引擎 是一组游戏运行部件以及软件工具的集合。随着技术进步,多数现代游戏引擎都包含以下部件,游戏引擎架构如图所示:
如上图所示,游戏引擎分为两个层次:
游戏内容层:一组工具管理游戏需要的数据
游戏引擎层:一组游戏运行部件,支撑游戏的运行与人机交互
尽管不同厂家的引擎性能差别巨大,每个部件功能也不同,其基本原理和使用方法基本一致。特别的,现代游戏都是数据驱动的架构,即游戏代码工作量一般不太大,游戏的行为、规则主要由数据决定。
早期游戏引擎是在游戏开发过程产生的。例如:id TECH 制作了游戏 《德军总部3D》、《Doom 3》(毁灭战士)、《Quake》(雷神之锤)等大卖的 3D 游戏,同时也把 3D 游戏的核心部件以及相关工具卖给其他游戏公司或电影制作企业。比较著名的就是 Quake engine,它的作者约翰·卡马克* 是开源运动的支持者,你可以下载源代码与各种资源。
早期游戏引擎都是以动画与渲染为核心,并没有使用现代显卡技术。
【注】 约翰·卡马克,id TECH 联合创始人(John Carmack)。现在已经加入Oculus Rift 团队,并且担任首席技术官一职。
PC与游戏机专业游戏引擎
以下是一些商业引擎与代表作:
引擎 | 代表作 |
---|---|
虚幻/Unreal | 《战争机器》 |
Cry Engine* | 《Crysis》 |
寒霜/Frostbite | 《战地》 |
Infinity Ward | 《使命召唤》 |
EGO | 《尘埃2》 |
id TECH | 《DOOM3》 |
Source | 《半条命2》 |
X-Ray* | 《潜行者》 |
Havok Vision | 《哥特王朝》 |
Quake/idTECH | 《雷神之锤》 |
Chrome4* | 《狂野西部2》 |
MT framework | 《生化危机5》 |
Gamebryo | 《上古卷轴IV》 |
Jupiter EX | 《 F.E.A.R》 |
* 顶级特效引擎,需要强大的 CPU 和 GPU(甚至超级计算)支持。
因为游戏引擎开源,具有实力的游戏公司一般都对外宣称拥有自己的游戏引擎。因此,要学好游戏开发,要点是强化游戏引擎知识,而不是简单的追随大厂如EPIC(Unreal)、EA(Frostbite)这些产品。
游戏引擎核心部件几乎 100% 由 c 和 c++ 实现。因此要进入游戏引擎开发的核心,c语言、数学、算法、计算机图形学等是基础。 如果要深入 AR/VR,SLAM 、计算机视觉与理解等技术是重要内容。因此游戏编程技术仅是游戏开发的一个方面,学好相关课程很重要。
移动端游戏引擎
手机端3D游戏引擎几乎是 Unity3D 一家独大。Unity Technologies 在PC、游戏机平台的游戏大厂比,难以竞争。就借助一款 mono 跨平台 .net 实现框架软件(类似java虚拟机),把它的游戏引擎部署到几乎任意的操作系统上,特别在手机平台上获得成功!
众多的开发者倒逼 Unity 成为一家专业提供游戏引擎与资源服务的公司。与传统游戏引擎比 Unity 3D 有着强大的开发工具和比较完善的服务社区,不仅是游戏入门学习的首选,也是 3D 手游开发的最佳工具之一。
Unity 的成功吸引了其他企业进入手游市场,其他包括:
面向游戏设计师的游戏引擎
简单一些,就是几乎不用写代码(交互编程,可视化编程)的游戏引擎。常用于非计算机专业人员游戏入门、做游戏 demo 和 testing、编写 html5 小游戏等
网页平台(HTML5)游戏引擎
具体说应该是 WebGL 开发的副产物,为展示新一代互联网图形基础设施而开发,并逐步走向流行。
开源游戏引擎
很多公司游戏引擎都是基于开源引擎而建,因此有必要了解它们
一些公司,开源了部分基础代码以获得同行信任,如:
游戏就是模拟世界或构建虚拟世界。用计算机技术呈现现实或虚拟世界的动态场景,统称“离散仿真系统”
这是一个简单的游戏世界,飞机打坦克的场景,如图所示:
为了呈现炮弹打击坦克的过程,
需要不断计算炮弹的位置,并在屏幕上画出炮弹。当游戏的引擎每 1/60 秒计算出所有游戏对象的位置、形态,并在屏幕上画出来,我们就看到了如电影一般飞机打坦克的动态场景。
游戏循环
先看仿真系统底层运作的伪代码,在游戏引擎中称为游戏循环(Game Loop):
Initialize()
LoadContent()
WHILE not end of game DO {
UpdateGameObjects(t) //创建、删除、修改游戏对象
DrawGameObjects(t) //绘制游戏对象
}
UnloadContent()
这么简单(难以置信)。微软 XNA 游戏引擎的基本框架就是这样,如图所示:
所有,XNA 游戏编程的模板如下:
public class Game1 : Microsoft.Xna.Framework.Game {
GraphicsDeviceManager graphics;
SpriteBatch spriteBatch;
public Game1() {
graphics = new GraphicsDeviceManager(this);
Content.RootDirectory = "Content";
}
protected override void Initialize() {
base.Initialize();
}
protected override void LoadContent() {
spriteBatch = new SpriteBatch(GraphicsDevice);
}
protected override void UnloadContent(){
}
protected override void Update(GameTime gameTime)
{
if (GamePad.GetState(PlayerIndex.One).Buttons.Back == ButtonState.Pressed)
this.Exit();
base.Update(gameTime);
}
protected override void Draw(GameTime gameTime) {
GraphicsDevice.Clear(Color.CornflowerBlue);
base.Draw(gameTime);
}
}
既然游戏执行过程是固定的,但每步骤的具体内容是用户定义的,这就是“设计模式”教材上典型的模板方法模式设计!
尽管现代游戏引擎的游戏循环非常复杂,但作为开发者必须明白,所有复杂的代码均建立在这样简单的基础代码之上。
无论引擎怎么强大,其游戏循环一定是单线程的。即有仅有一个线程渲染画面,由于渲染过程中计算线程不能修改游戏对象状态,所以过多 CPU 很难被利用。
“Discrete”是离散,为了研究一个系统的动态,计算机必须通过一个时间点来就系统状态,比如研究对象进入系统和离开系统的时间点,进入队列和离开队列的时间点,开始加工和完成加工的时间点等等。这些时间点在时间轴上是离散而非连续的序列,而系统状态仅在离散的时间点上发生变化。
离散仿真
为了研究系统动态,时间被分成为若干小的时间片,系统状态被这段时间内发生的系列活动而改变。称为基于活动的仿真(activity-based simulation)
离散仿真存在一些显而易见的问题:
这里仅给出游戏离散仿真中两个典型问题。你必须明白,无论游戏编程或引擎开发都需要认真学习相关知识,避免 “too young too naive”
离散事件仿真
早期游戏引擎最大的问题是硬件性能不足,游戏优化能力决定了游戏的成功。在硬件性能冗余、面向对象的时代,用 XNA 这样的引擎从头开始编写管理成百上千游戏对象的游戏是什么感觉?
程序员做2D小游戏时 “一切控制在手中” 的好感觉将荡然无存。因此,需要对游戏离散引擎进行改造,既要合适面向对象的编程,也要将游戏设计与优化工作自动化与工具化,在游戏执行性能可接受的条件下,使得游戏开发难度减低到普通程序员可以接受的程度。Unity 3d 在这方面工作使得游戏开发得到普及,EPIC(Unreal) 等大厂也积极跟进,谁也不乐意被开发者抛弃!
离散事件仿真(Discrete Event Simulation,缩写为DES):为了研究系统动态,系统中对象处理在内部(如对象状态改变产生事件)、外部事件,并在事件处理过程中进一步引发系统状态改变产生系列事件。称为基于事件的仿真(Events-based simulation)。与离散仿真不同,我们是在特定事件(条件)中观察并改变系统状态。
现代游戏引擎一般都是离散仿真与离散事件仿真混合模型。先给出一个更接近现代游戏引擎的伪代码:
initialize()
loadContent()
WHILE not end of game DO {
FOREACH GameObject o In game DO {
IF (o.fristUpdated) o.Start();
}
FOREACH GameObject o In game DO {
o.Update();
}
FOREACH GameObject o In game DO {
o.LastUpdate();
}
drawGameObjects(t)
}
unloadContent()
尽管这个代码与实际代码差别很大,它体现了以下一些事实:
现代游戏引擎由于要管理许多游戏对象,空间管理与性能优化无疑是巨大的挑战。所幸的是程序员编写游戏正在一步步变得更简单!
Unity 的使用与操作细节请移步 Unity 用户手册,这里仅关注相关内容
1、了解 Unity 3D 基本界面
如果你是新手,先阅读 Getting Started
安装完成后,创建一个 3D 项目。Unity 主界面如图所示:
2、初识游戏对象与资源
任务是在游戏场景中放置一个物体(如 Cube)并赋予红色,运行游戏。
操作 02-01 ,GameObject 练习:
1、游戏对象的表示
Unity 游戏对象主要涉及三种类:
它们之间的关系如图所示:
直观上,游戏对象继承非常直观,例如:96A主站坦克继承抽象坦克,抽象坦克继承游戏对象基类,似乎是天经地义的设计。然而,游戏引擎能仅能与游戏对象基类打交道。游戏引擎做的事越多,游戏对象基类必然要承担许多职责,导致基类过于庞大。
这里,游戏对象用一组部件来表达不同的方面的要求,能更好满足游戏世界的复杂性,提升游戏对象的灵活性,便于与引擎协作。例如:游戏对象位置等由 Transform 管理,形态网格由 Mesh 管理, 绘制由 Render 等部件协作完成,行为则由 MonoBehaviour 的子类管理。这些部件,仅需要时才加入游戏对象的定义。
组合优于继承
为什么要这样设计?在设计模式的装饰模式器描述了这样的设计场景:“装饰器模式(Decorator Pattern)允许向一个现有的对象添加新的功能,同时又不改变其结构。” 动态地给一个对象添加一些额外的职责。就增加功能来说,装饰器模式相比生成子类更为灵活。
这个设计充分体现了这条软件设计原则“组合优于继承”。然而,众多的部件对象也带来管理复杂性与性能优化问题。
【注】装饰器模式的案例多数是该设计场景的特例,“Wrapper”模式。
现在,做一些任务验证上图设计:
操作 02-02 ,GameObject 与 Component 关系练习:
2、赋予游戏对象行为
游戏对象行为是游戏对象的一个部件,都是脚本部件 MonoBehaviour 的子类。
下面的任务就是创建一个简单的脚本,并挂载到 Cube 对象。
操作 02-03 ,c# Script 编写练习:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class FisrtBeh : MonoBehaviour {
// Use this for initialization
void Start () {
Debug.Log("This Start!");
}
// Update is called once per frame
void Update () {
// Debug.Log("This Update!");
}
void OnEnable () {
Debug.Log("This Enabled!");
}
void OnDisable () {
Debug.Log("This Disabled!");
}
}
你可能注意到 FisrtBeh 是 MonoBehaviour 的子类。我们怎么知道引擎调用了哪些方法和事件呢? MonoBehaviour 基类就是一个编程模板,Unity API 的 Messages 一节给出了它可以处理的引擎回调(callback)与事件句柄(OnXXX)。
由于 Update 在每个游戏循环都会被调用,为了避免大量输出,所以暂时注释了。
修改代码,让 update 中语句执行, 重复上述过程。
脚本文件名称必须与类名一致,否则 …
3、游戏脚本对象方法与事件执行顺序
你可能想知道这些消息在游戏循环的什么时候发生,它们之间的顺序,Unity 官方手册这样描述了游戏循环、事件、引擎部件之间的关系:
这是一张可怕的大图,对入门者极其不友好。知道以下事件就够用了:
事件名称 | 执行条件或时机 |
---|---|
Awake | 当一个脚本实例被载入时Awake被调用。或者脚本构造时调用 |
Start | 第一次进入游戏循环时调用 |
FixUpdate | 每个游戏循环,由物理引擎调用 |
Update | 所有 Start 调用完后,被游戏循环调用 |
LastUpdate | 所有 Update 调用完后,被游戏循环调用 |
OnGUI | 游戏循环在渲染过程中,场景渲染之后调用 |
由于游戏对象与部件之间是组合关系,Compnonent 对象子类的构建、释放必须由对应 GameObject 完成。程序员不能创建它们
为什么不能让程序员用 new 创建部件?
1、用脚本创建 Primitive 游戏对象
操作 02-04 ,创建 Primitive 游戏对象练习:
菜单能作的工作,利用 API 编程也能做到!
2、游戏对象组合与预制
如果我们每次都从基础游戏对象构建游戏,这需要多少代码,而且不易于修改。我们可以把基本的游戏对象组合起来,制作成 预制,以后把预制当作一个游戏对象使用。
如果说基本游戏对象是原材料,预制就是半成品。因此,预制的概念在 Unity 中及其重要,也是游戏制作最基础的知识!
下图就是本节的任务,制作一个座椅的预制
操作 02-05 ,创建 Prefabs 练习:
在层次视图中,预制的颜色与普通游戏对象不同!!!
父对象坐标与子对象坐标的关系(世界坐标、相对坐标)?
事实上,游戏都是一些预制好的对象进一步组合而成,我们的代码不过是胶水,控制作这些事物的变化。
思考题:从对象设计角度,称为“组合模式(Composite Pattern)”。例如,行政区是一个抽象概念,国家、省、市、县都是行政区。这些行政区对象按树形结构组合,每个高级别的行政区都由几个低级别的行政区组合构成。
许多同学(包括网上绝大多数博客)都从编程特征来理解设计模式,而不像设计模式作者们那样从现实社会设计问题中去理解,忽视具体问题的业务场景与上下问。23 种面向对象设计模式的强大,就是这么多年来大家都觉得足够用了!
3、游戏场景、预制与资源
游戏场景的保存与恢复
与戏剧一样,一个游戏由一个与多个场景(Scenes)。场景中包含背景、静态游戏对象与动态游戏对象。Unity 场景视图就是场景中所有事物的可视化设计器。层次视图则是从对象的角度,描述了游戏对象树林这种数据结构。
操作 02-06 ,创建与恢复 Scene 练习:
资源、预制与场景
到开游戏项目资源所在的目录,例如: D:\mywork\unity\New Unity Project 2\Assets
发现每个游戏资源都对应了响应的文件。因此,资源是存在的硬盘的文件。
对于 Unity 预知和场景都是一样的文件。预制是游戏对象及其树上所有对象的文本定义(可以翻译成任何文本,如 YAML,XML,JSON]);场景是场景中所有游戏对象的文本定义。默认它们以压缩格式保存。
【高级话题】文本化资源文件 Text-Based Scene Files
游戏代码的基本任务是根据资源动态加载游戏对象,并控制它们。本次的任务是掌握用代码创建游戏对象的基本技巧。
首先创建如下代码资源 LoadBeh
:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class LoadBeh : MonoBehaviour {
public Transform res;
// Use this for initialization
void Start () {
// Load Resources
GameObject newobj = Instantiate<Transform> (res, this.transform).gameObject;
newobj.transform.position = new Vector3 (0, Random.Range (-5, 5), 0);
}
}
随着版本更新,Unity 越来越喜欢使用模板,可能导致部分版本不兼容。例如:Instantiate 方法的定义 public static T Instantiate(T original, Transform parent);
操作 02-07 ,从预制创建游戏对象 练习:
这是,我们将观察到座椅随机出现在 Game 视图中。
编程练习 02-08,使用砖块构建一面5*10 的墙
编程要求与提示:
Unity 常用资源
作业内容
1、简答题【建议做】
2、 编程实践,小游戏
3、思考题【选做】
作业提交要求