xna杂记

42
粒粒粒 Particle 粒粒 本本本本 本本本本本 SpriteBatch 本本本本本本本本本本本本本 ,:。 本本本本 http://creators.xna.com/en-US/sample/particle粒粒粒粒 本本本Particle systems 本本本本本本本 )体,、、。, Spacewar 本本本本本本 本本 一。 本 本 本 本 本本本本本本本 本本本本本本本 本本本本本本本本本本本本本本本本 本本本本本本本本本本本本本本本 本本 统一 。,、。。 本本本本本本 ,一一,。 2D alpha 本本本sprites本本本本 本本本本本本本本本本本本本本本本本本本本本本 。一一, 一。 本本 Spacewar 本 本本本本本本本本本本本本本本本本本本本本本本本本本本本 本本本本本本本本 。,。, 本 DrawableGameComponent 本 本 Spacewar 本本本本本本 SceneItem 本 本本本本 粒粒 Shader 粒粒 Vertex Shader Model 1.1

Transcript of xna杂记

Page 1: xna杂记

粒子系统 Particle 示例

本示例介绍了粒子系统的原理,展示了如何 SpriteBatch 使用粒子效果,

包含两个粒子效果:爆炸和烟雾。

原文地址:http://creators.xna.com/en-US/sample/particle。

示例概览

粒子系统(Particle systems)通常用来绘制流体,在游戏中通常用于烟

雾、火焰、火花和水花。例如,在 Spacewar 游戏中的爆炸效果就是一个粒子系

统。

粒子系统是由一定数量的小粒子构成的。每个粒子都有自己的物理性质,通

常包含位置、速度和加速度。更复杂的粒子包含更多的物理属性。粒子的创建和

初始化是由粒子系统决定的,但一旦开始了一个粒子效果,粒子的行为就是独

立的了。粒子通常用 2D alpha 混合的 sprites 进行绘制。大量的独立更新的粒

子一个在另一个之上被绘制,粒子系统就表现出一种混沌或自然的外观。

Page 2: xna杂记

这个示例的粒子系统以 Spacewar 游戏的粒子系统为基础。但是,它还介

绍了如何进行预先设置以避免垃圾回收。而且,本示例的粒子系统从

DrawableGameComponent 继承,而不是像 Spacewar 游戏中那样从

SceneItem 继承,这样我们可以很容易地将粒子系统插入到游戏中去。

最小 Shader 配置

Vertex Shader Model 1.1

Pixel Shader Model 1.1

示例控制

本示例使用以下键盘和手柄控制

动作 键盘控制 手柄控制

切换粒子特效 空格键 A

退出 ESC 或

ALT+F4BACK

工作原理

Page 3: xna杂记

ParticleSampleGame 类是示例的主类。它展示了两种粒子特效:爆炸和

上升的烟雾。爆炸特效使用了两个独立的粒子系统:一个系统绘制火焰,一个系

统绘制烟雾。要使用这些特效,ParticleSampleGame 创建了三个粒子系统:

ExplosionParticleSystem, ExplosionSmokeParticleSystem 和

SmokePlumeParticleSystem。ParticleSampleGame 类有一个 state 对应

现在显示的特效,并拥有一个计时器用于处理何时开始一个新特效。

本示例中最关键的类是 ParticleSystem,它是一个抽象类,包含粒子系

统的核心功能。调用 ParticleSystem 的 AddParticles 方法可以添加一个新的

粒子效果。AddParticles 使用几个常数初始化粒子。这些常数都应该被子类设置

让每个粒子系统都有不同的表现。子类可以重写 ParticleSystem 中的虚拟方法

这样就能更加灵活地控制粒子的创建。

Page 4: xna杂记

要注意的一个关键点是所有粒子都是在程序开始时分配的,然后就可以根

据需要重用,在运行时无需重新实例化。这样可以避免在运行期间的垃圾回收。

但是,这会带来一个问题:有可能在调用 AddParticles 方法时,所有粒子仍

在使用中。在这种情况中,AddParticles 会添加尽量多的粒子,然后停止添加,

这会导致一个期望的特效可能不会被显示。解决方法是在 ParticleSystem 构造

函数中添加一个参数,这个整数类型的参数指定需要的最大特效数量,这样使

用粒子系统的开发者就可以指定特效的最小数量获取最好的效果。

源代码 ParticleSample.rar 下载(注释已翻译成简体中文)。

阴影映射(Shadow Mapping)示例

这个示例展示了如何从一个单向光源实现一个基本的阴影映射,而阴影贴

图的视场和投影可以匹配相机的视锥体。你可以使用这个示例让一个大场景中的

多个动态对象投下动态阴影。

Page 5: xna杂记

原文地址:http://creators.xna.com/en-US/sample/

shadowmapping1。

什么是阴影映射(Shadow Mapping)?

阴影映射是指一种产生阴影的技术,你可以将对象离开光源的距离存储在

一张纹理中。 当绘制场景时,你可以使用这个离开光源的距离判断要绘制的像

素是否在存储在阴影贴图中的值之后。

阴影映射技术需要绘制场景两次。第一次从光源视场中绘制可以产生阴影的

所有对象,这些对象叫做遮蔽体(occluders)。这意味着你需要创建一个位于

场景光源位置和光源方向的视矩阵和投影矩阵。对象的深度信息存储在一个渲染

目标中。渲染目标通常使用的格式是一个 32 位浮点类型的

SurfaceFormat.Single,它可以以 32 位的精度存储对象的深度值。但是有些

显卡不支持这个格式,所以如果你使用的是这种类型的显卡,你必须使用 16

位浮点数的 SurfaceFormat.HalfSingle。更老的显卡完全不支持浮点数格式的

Page 6: xna杂记

渲染目标,需要一个诸如 SurfaceFormat.Rgba32 之类的表面格式,在这种

情况中,要将距离放置在 4 个 8 位的通道中。

第二步是从相机视场中绘制场景。判断每个像素离开光源的距离。然后采样

存储在阴影贴图中的值。如果阴影贴图中的距离小于像素离开光源的距离,你就

知道这个像素被另一个物体遮挡,它会处于阴影之中。

上图表示如何从光源视场和相机视场中绘制场景。红色矩形表示最终的后备

缓冲的内容。蓝色矩形表示存储在阴影贴图中的内容。注意观察位于橙色球体后

面的灰色矩形只有特定部分才位于阴影中。

Page 7: xna杂记

当使用阴影贴图绘制场景时会有一些显示错误。一个错误与存储深度值的表

面格式有关。你使用的这些值限制了判断离开光源距离的精度。这会导致某些距

离接近的地方阴影会不正确。这个问题在脸部的自阴影中最为明显 ,这种错误

叫做 shadow acne(译者:acne 意味痤疮粉刺,很形象)。下图展示了一个

shadow acne 例子。通常可以使用一个 bias偏移量修正这些错误。

Page 8: xna杂记

另一个场景的错误是源自渲染目标中像素的限制和渲染目标覆盖的范围。如

果你将一个很大的区域绘制到一个渲染目标中(就像这个示例中做的那样),

那么一个图素(texel)覆盖的区域就很大。当镜头拉近时,你会注意到阴影边

缘不是平滑的,这种情况叫做锯齿(aliasing),是由覆盖场景的阴影贴图分

辨率不够造成的,下图展示了锯齿的例子。

Page 9: xna杂记

另一个错误只会发生在将一个对象的一部分绘制到阴影贴图的情况中,这

会导致阴影中产生一个洞,这种情况通常发生在当对象位于阴影贴图的边缘时。

本例中这种情况发生在当相机移动到模型后面时,此时相机的视锥体不包含模

型的某一部分,所以,阴影贴图也不包含模型的某一部分,如下图所示。

示例简介

阴影映射算法的第一步是从相机视角绘制场景。要做到这步,你需要视矩阵

和投影矩阵。要最大限度地使用渲染目标的分辨率,这个视矩阵和投影矩阵要对

Page 10: xna杂记

应一个尽量小的视锥体。在本例中,视矩阵和投影矩阵对应相机视锥体的最小包

围盒。这可以让阴影贴图只包含用户可见的部分,同时仍然包含完整的可见部分

上图显示了相机视锥体,相机视锥体的角用来获取匹配光线方向的最小包

围盒。这个技术是用于单向光源的基本阴影映射技术,但它还有一些限制,我们

会在最后加以讨论。

最小 Shader 配置

Vertex Shader Model 2.0

Pixel Shader Model 2.0

示例控制

Page 11: xna杂记

本示例使用以下键盘和手柄控制。

动作 键盘控制 手柄控制

旋转相机UP、DOWN、LEFT 和 RIGHT 方

向键右摇杆

移动相机 W, S, A 和 D 左摇杆

退出 ESC 或 ALT+F4 BACK

工作原理

本示例有 4 个主要部分:

1. 创建渲染目标和深度缓冲

2. 计算光源视矩阵和投影矩阵CreateLightViewProjectionMatrix

3. 创建阴影贴图CreateShadowMap

4. 使用阴影贴图DrawModelsWithShadow 绘制场景

要创建一个纹理储存对象的深度,你需要使用 RenderTarget2D 创建一

个渲染目标。这个示例使用一个长宽都为 2048 的浮点数类型的纹理

SurfaceFormat.Single,这可以提高结果的精度。在写入阴影贴图时还需要使

Page 12: xna杂记

用 DepthStencilBuffer,它存储了场景的 z值,让你可以在阴影贴图中只存储

离开光源最近的点。

要计算光源的视矩阵和投影矩阵,CreateLightViewProjectionMatrix 方

法获取光线方向上最匹配的相机视锥体 BoundingBox。这需要根据光线方向旋

转相机视锥体顶角,将顶角转换到光源空间。然后通过获取包围盒后表面中点的

MinZ值计算光源的位置。

要创建视矩阵,需要使用光源旋转的逆矩阵将光源位置从光源空间转换到

世界空间。然后使用光源位置和方向构建视矩阵。投影矩阵基于前面获得的包围

盒的大小进行计算,其中 X 方向为宽度,Y 方向为高度,Z 方向为近裁平面和

远裁平面间的距离。因为使用的是一个单向光,你需要使用正交矩阵。光线是平

行的,所以阴影也需要平行。将光源的视矩阵和投影矩阵相乘获得光源的 view

projection矩阵。

Page 13: xna杂记

要创建阴影贴图,CreateShadowMap 方法首先调用 SetRenderTarget

方法设置渲染目标。然后保存当前深度模板缓存,设置图形设备的阴影深度模板

缓存。然后将渲染目标清除为白色,因为 1 表示在阴影贴图中距离最远的对象

(即位于远裁平面的对象),任何近于远裁平面的对象在阴影贴图中的值都会

小于 1。shader 中的 CreateShadowMap technique 绘制场景中所有有阴影

的几何体。CreateShadowMap_VertexShader 将顶点转换到光源空间 。

CreateShadowMap_PixelShader 将深度值写到阴影贴图中。

最后,你需要从相机视角绘制场景,这是在 DrawWithShadowMap 方法

中实现的。使用 shader 中的 DrawWithShadowMap technique 绘制场景中

的每个对象。 DrawWithShadowMap_VertexShader 通过世界矩阵变换顶点

并将结果存储在 Output.WorldPos 中。然后在

DrawWithShadowMap_PixelShader 中使用这个值确定像素在光照空间中的

Page 14: xna杂记

位置。像素的位置与存储在阴影贴图中的值作比较,这些值也是存储在光源空间

中的。如果像素的深度大于存储在阴影贴图中的值,你就知道这个像素在某些对

象之后—这个对象是一个遮蔽体(occluder)—需要绘制到阴影贴图。这样,

你就知道了这个像素应该在阴影中。

示例的局限

这个示例展示了基本的技术,而且还有一些视觉错误。因为使用的是相机视

锥体,所以绘制到阴影贴图中的场景的分辨率与此贴图相同。这会导致距离相机

远的对象会采样阴影贴图中大量的像素,但距离相机近的对象需要一个更高的

分辨率以防止出现锯齿。你可以在这个示例中找到几个处理精度和分辨率的技术

如果自己想加以改变,可以有两个选择:你可以提高阴影贴图的大小,或扩展

视锥体的大小。

在视锥体之外的对象不会绘制到阴影贴图中,这会导致屏幕之外的对象不

会在可视区域内投下阴影。一个简单但并不完美的方法是扩展相机投影矩阵创建

Page 15: xna杂记

的包围盒,使它包含更大的范围。还有很多高级的方法可以获取所有可能的遮蔽

体,创建一个正确匹配所有遮蔽体和视锥体的包围盒。

源代码 ShadowMapping.rar 下载,注释已翻译成中文

XNA Game Engine 教程系列 1-Component 和

GameScreen

我经常看到一些人倾向于将大量时间花在学习图形编程上,而没有足够时

间去学习如何组织游戏或游戏引擎的结构。在本教程系列中我要分享关于这一主

题的知识。这并不是唯一正确的方法,但我希望能帮助大家避免常见的错误。

在 XNA Creators Club网站上有一个实例可以作为游戏结构研究的起点,

我设计的这个引擎部分基于此教程。(http://creators.xna.com/en-us/

samples/gamestatemanagement)。那么,让我们开始吧!

引擎的基本结构如下:

游戏引擎的结构

Page 16: xna杂记

Component 类:这个类是基类,游戏引擎管理的所有物体都从这个

类继承。它包含“Update()”和“Draw()”方法更新和绘制自己。它

还包含了一个“Visible”属性用来设置是否可以绘制此组件。

GameScreen 类:这个类包含的一组组件并处理他们的更新和绘制。

它包含“BlocksDraw”, “BlocksUpdate”和“BlocksInput”属性去阻止

绘制,更新,处理 screen。这就使 screen 可以轻松实现如暂停游戏之类的

功能。当然,screens 可以重写这个类使之总能绘制,更新或类似的事情。

Engine 类:这是一个静态类,其中包含 GameScreens 的列表。它管

理 GameScreens 的更新和绘制,同时也包含了一些有用的属性,如

service containter,GraphicsDevice 和 SpriteBatch等等。

设置 Visual Studio

在我们编写代码前,我们需要设置 Visual Studio。在解决方案将有两个项

目,一个是引擎本身,另一个用于利用此引擎的演示游戏。因此,打开 Visual

Page 17: xna杂记

Studio,选择新建项目,并选择XNA Windows Game Liberary,选择一个

名称。我用“InnovationEngine”,但可以是其他任何名称。当项目建立后,打

开解决方案资源管理器,右按一下您的解决方案(顶级节点)。选择添加新项目

并选择型 XNA WindowsGame。选择一个名称(我用

“TestEnvironment”),并按下确定。最后,我们需要从游戏项目中引用引

擎项目。展开 game节点(即“TestEnvironment”),右键点击

“Reference”,并选择“添加引用”。点击“项目”标签,然后选择引擎项目

(即“InnovationEngine”)。在引擎项目新建一个叫做“Component”的文

件夹。现在已全部设置好了。要运行游戏,右键点击项目,并点击“设置为启动

项目”。现在,按下 F5 游戏就可以开始了,目前一个空白蓝屏。

Page 18: xna杂记

这有助于我们避免常见错误#1 -不分离引擎代码和游戏代码。分离代码是

一个好主意,除非游戏是非常简单的。它不仅有助于使解决方案清晰,当以后创

建编辑器时也会变得很容易,也能在不同的游戏中重用代码。

Component 类

我们要编写的第一个类是 Component 类。这是所有物体的基类。通过右击

项目并选择“新建项”添加新的文件,并命名为“Component.cs”。将以下代

Page 19: xna杂记

码添加到下面。注意:请不要将该代码复制并粘贴,自己动手输入能更好地理解

你做的事情。

using Microsoft.Xna.Framework;

namespace Innovation

{

public class Component

{

// The GameScreen object that owns this component

public GameScreen Parent;

// Whether or not this component has been initialized

public bool Initialized = false;

// Whether or not the GameScreen that owns the component

// should draw it

public bool Visible = true;

// This overloaded constructor allows us to specify the parent

public Component(GameScreen Parent)

{

Page 20: xna杂记

InitializeComponent(Parent);

}

// This overload allows will set the parent to the default

// GameScreen

public Component()

{

InitializeComponent(Engine.DefaultScreen);

}

// This is called by the constructor to initialize the

// component. This allows us to only have to override this

// method instead of both constructors

protected virtual void InitializeComponent(GameScreen Parent)

{

// Check if the engine has been initialized before setting

// up the component, or the Engine will crash when the

// component tries to add itself to the list.

if (!Engine.IsInitialized)

throw new Exception("Engine must be initialized

with \"SetupEngine()\"" + "before components can be initialized");

Page 21: xna杂记

Parent.Components.Add(this);

Initialized = true; }

// Updates the component - This is called by the owner

public virtual void Update()

{

} // Draws the component - This is called by the owner

public virtual void Draw() { }

// Unregisters the component with its parent

public virtual void DisableComponent()

{

Parent.Components.Remove(this);

}

}

}

最重要是知道一个组件属于 GameScreen,而 GameScreen 每帧调用组

件的更新和绘制方法。

Page 22: xna杂记

有一个清晰定义的基类可以帮助我们避免常见错误#2 -没有一个类能让引

擎知道如何工作。如果您的引擎是由很多不同格式的类组成的,那么你不用担心

每个组件如何更新和绘制。拥有一个基类意味着我们要做的只是继承它,稍微改

变一下更新和绘制,引擎就能为我没绘制,更新和管理组件而不需要额外的代

码。因为每一个组件都能通过使用基类类型被访问,即使它是一个地形,天空或

其他任何东西,这极大地简化了我们的更新,绘制和管理组件的逻辑。

Component 接口

下一步我们要为组件定义一些接口。接口能确定物体与其他物体是如何发生

作用的,它也让我们区分不同的对象类型。这里我们定义了 I2DComponent 和

I3Dcomponent接口,告知引擎组件使用的是二维还是三维绘制和更新。它还

告知引擎如对象的位置,旋转等一些基本信息,简化了像拾取(检测鼠标是否

点击了目标),在编辑器中改变位置,旋转等属性。

Page 23: xna杂记

我们还将定义一个枚举包含对象组的类型:二维,三维或两者都是。以后我

们可以使用这个枚举用于渲染水面。当绘制水的反射时,我们不希望 HUD也绘

制在反射面上,所以我们可以这样使用这个枚举:

Engine.Draw(ComponentType.Component3D),这将只绘制三维物体,

而 HUD 不会绘制在反射平面上。这可以让我们避免常见错误#3 –有一个基类,

但没有为引擎提供有用的信息。通过提供这些接口,我们可以修改一个对象的位

置,旋转,缩放等,而无需关心是哪种组件。接口的代码和对象类型枚举如下。

将这些代码添加到一个叫做ComponentType.cs 的新文件中。

using Microsoft.Xna.Framework;

namespace Innovation

{

// Represents a 3D object. These objects will be drawn before

// 2D objects, and will have modifiers automatically provided

// in the editor.

Page 24: xna杂记

public interface I3DComponent

{

// Position in the Cartesian system (X, Y, Z)

Vector3 Position { get; set; }

// Rotation represented as a Vector3. This shouldn't

// be used for calculations, it is left in so that

// the rotation can be more easily modified by hand

Vector3 EulerRotation { get; set; }

// Rotation as a Matrix. This will give much smoother

// and cleaner calculations that a

Vector3 Matrix Rotation { get; set; }

// Scale for each axis (X, Y, Z)

Vector3 Scale { get; set; }

// BoundingBox to use for picking and pre-collision

BoundingBox BoundingBox { get; } }

// Represents a 2D object. These objects will be drawn after

// 3D objects, and will have modifiers automatically provided

// in the editor.

Page 25: xna杂记

public interface I2DComponent {

Rectangle Rectangle { get; set; } }

public enum ComponentType

{

// Represents all 2D components (I2DComponent)

Component2D,

// Represents all 3D components (I3DComponent)

Component3D,

// Represents all components that are either 2D or 3D

// components

Both,

// Represents all components regardless of type

All

}

}

GameScreen

GameScreen 是一个含有一组组件的类,并处理组件的更新,绘制和输

入。它拥有 BlocksDraw,BlocksInput 和 BlocksUpdate 属性。这些属性可以

Page 26: xna杂记

被用来控制哪个屏幕在“下面”,然后将他们想象为一个 GameScreens堆栈,

最新的在堆栈顶部。

例如,如果您添加一个新的 GameScreen 并把

BlocksUpdate,BlocksInput 和 BlocksDraw 设置为 true,那么你就创建了

一个暂停菜单。这可以节省大量的时间,因为它非常容易被创建。没有必要建立

一个复杂状态系统来跟踪游戏所处的状态。我们只需添加新的屏幕并根据需要改

变其属性。这会引出常见错误#4-未将代码分隔成明确定义的部分并创建一个

过于复杂的状态管理系统。虽然这可能看起来有些过度,对较小的游戏肯定如此

但系统更具弹性,只需花很少的努力就能扩展它。

虽然状态管理可以创建在最终的游戏中而不是由引擎管理,但使用

GameScreen 能使我们得到一个灵活的设计和框架。下面是 GameScreen 的

代码。它添加到一个叫做GameScreen.cs 新文件中:

Page 27: xna杂记

using System;

using System.Collections.Generic;

using System.Linq; using System.Text;

namespace Innovation

{

public class GameScreen

{

// Keep track of all the components we need to manage

public ComponentCollection Components;

// Whether or not to draw

public bool Visible = true;

// Whether or not this screen should block the update of

// screens below (for pause menus), etc. public

bool BlocksUpdate = false;

// Whether or not this screen can override a blocked update

// from an above screen (for a background screen), etc.

public bool OverrideUpdateBlocked = false;

// Same for drawing public

Page 28: xna杂记

bool BlocksDraw = false;

// Same for drawing public

bool OverrideDrawBlocked = false;

// Same for input public

bool BlocksInput = false;

// Same for input public bool OverrideInputBlocked = false;

// Whether or not we want to block our own input so we can

// do things like loading screens that will want to accept

// input at some point, but not at startup

public bool InputDisabled = false;

// This is set by the engine to tell us whether or not input

// is allowed. We can still get input, but we shouldn't. This

// is useful because a ProcessInput() type of function would

// make it hard to manage input (because we can't utilize //

events, etc.)

public bool IsInputAllowed = true;

// The name of our component, set in the constructor. This

// is used by the Engine, because a GameScreen can be

accessed

Page 29: xna杂记

// by name from Engine.GameScreens[Name].

public string Name;

// Fired when the component's Initialize() is finished. This can

// be hooked for things like asynchronous loading screens public

event EventHandler OnInitialized;

// Whether or not the component is initialized. Handles firing of

// OnInitialized.

bool inititalized = false;

public bool Initialized {

get { return inititalized; }

set { inititalized = value;

if (OnInitialized != null)

{

// Fire the OnInitalized event to let other's know we

// are done initializing OnInitialized(this, new EventArgs());

}

}

}

// Constructor takes the name of the component

Page 30: xna杂记

public GameScreen(string Name)

{ // Setup our component collection

Components = new ComponentCollection(this);

// Register with the engine and set our name

this.Name = Name;

Engine.GameScreens.Add(this);

// Initialize the component

if (!Initialized)

Initialize();

}

// Overridable function to initialize the GameScreen

public virtual void Initialize()

{

this.Initialized = true;

}

// Update the screen and child Components

public virtual void Update()

{

Page 31: xna杂记

// Create a temporary list so we don't crash if

/ a component is added to the collection while

// updating

List updating = new List();

// Populate the temporary list

foreach (Component c in Components)

updating.Add(c);

// Update all components that have been initialized

foreach (Component Component in updating)

if (Component.Initialized)

Component.Update();

}

// Draw the screen and its components. Accepts a

ComponentType

// to tell us what kind of components to draw. Either 2D, 3D, or

// both. (Useful for drawing a reflection into a render target

// without 2D components getting in the way)

public virtual void Draw(ComponentType RenderType)

{

Page 32: xna杂记

// Temporary list

List drawing = new List();

foreach (Component component in Components)

{

if (RenderType == ComponentType.Both)

{

// If the render type is both, we will draw all 2D or

// 3D components

if (component is I2DComponent || component is I3DComponent)

drawing.Add(component);

}

else if (RenderType == ComponentType.Component2D)

{

// If the render type is 2D, we will only draw 2D

// components

if (component is I2DComponent)

drawing.Add(component);

}

Page 33: xna杂记

else if (RenderType == ComponentType.Component3D)

{

// If the render type is 2D, we will only draw 3D

// components

if (component is I3DComponent)

drawing.Add(component);

}

else

{

// Otherwise, we will draw every component regardless of type

drawing.Add(component);

}

}

// Keep a list of components that are 2D so we can draw them

on top

// of the 3D components

List defer2D = new List();

foreach (Component component in drawing)

if (component.Visible && component.Initialized)

Page 34: xna杂记

{

// If the component is visible and is not loading itself..

if (component is I2DComponent) {

// If it is 2D, wait to draw

defer2D.Add(component);

} else

{

// otherwise, draw immediately

component.Draw();

}

} // Draw 2D components

foreach (Component component in defer2D)

component.Draw();

}

// Disables the GameScreen

public virtual void Disable() {

// Clear out our components

Components.Clear();

Page 35: xna杂记

// Unregister from the Engine's list

Engine.GameScreens.Remove(this);

// If the engine happens to have this screen set as the default

// screen, set it to the background screen in the Engine class

if (Engine.DefaultScreen == this)

Engine.DefaultScreen = Engine.BackgroundScreen;

}

// Override ToString() to return our name

public override string ToString()

{

return Name;

}

}

}

还有注意的一件事是输入。我们并不想强制将输入集中到 GameScreen 的

一个功能中而限制输入,因为我们希望能够利用键盘的输入事件。所以,我们有

一个布尔值叫做 IsInputAllowed。这将由引擎在每帧计算,所以当检查输入时

Page 36: xna杂记

我们还应该查看 IsInputAllowed 是否为 true。当然,除非我们想在暂定画面中

忽略相机的移动。

该 GameScreen 类有一个成员称为 Components,这是一个

ComponentCollection。这是一个自定义的集合用来处理 Parent 的管理。集合

的代码如下。它添加到一个名为 ComponentCollection.cs 的文件中:

using System;

using System.Collections.Generic;

using System.Linq;

using System.Text;

using System.Collections.ObjectModel;

namespace Innovation

{

// A custom collection for managing components in a

GameScreen

public class ComponentCollection : Collection

{

Page 37: xna杂记

// The GameScreen to manage components for GameScreen

owner;

public ComponentCollection(GameScreen Owner)

{ owner = Owner; }

// Override InsertItem so we can set the parent of the

// component to the owner

protected override void InsertItem(int index, Component item)

{

if (item.Parent != null && item.Parent != owner)

item.Parent.Components.Remove(item);

item.Parent = owner;

base.InsertItem(index, item);

}

// Override RemoveItem so we can set the paren of

// the component to null (no parent)

protected override void RemoveItem(int index)

{

Items[index].Parent = null;

base.RemoveItem(index);

Page 38: xna杂记

}

}

}

在下篇文章,我们会处理引擎类本身。现在,通读并确保您理解了上述知识

译者注:其实 XNA框架中已经包含了 Component 类和

ComponentColletion 类,照作者的说法,他自己实现这两个类可以获得更大

的自由。