XNA Game Engine教程系列1-Component和GameScreen
我经常看到一些人倾向于将大量时间花在学习图形编程上,而没有足够时间去学习如何组织游戏或游戏引擎的结构。在本教程系列中我要分享关于这一主题的知识。这并不是唯一正确的方法,但我希望能帮助大家避免常见的错误。
在XNA Creators Club网站上有一个实例可以作为游戏结构研究的起点,我设计的这个引擎部分基于此教程。(http://creators.xna.com/en-us/samples/gamestatemanagement)。那么,让我们开始吧!
引擎的基本结构如下:
游戏引擎的结构
- Component类:这个类是基类,游戏引擎管理的所有物体都从这个类继承。它包含“Update()”和“Draw()”方法更新和绘制自己。它还包含了一个“Visible”属性用来设置是否可以绘制此组件。
- GameScreen类:这个类包含的一组组件并处理他们的更新和绘制。它包含“BlocksDraw”, “BlocksUpdate”和“BlocksInput”属性去阻止绘制,更新,处理screen。这就使screen可以轻松实现如暂停游戏之类的功能。当然,screens可以重写这个类使之总能绘制,更新或类似的事情。
- Engine类:这是一个静态类,其中包含GameScreens的列表。它管理GameScreens的更新和绘制,同时也包含了一些有用的属性,如service containter,GraphicsDevice和SpriteBatch等等。
设置Visual Studio
在我们编写代码前,我们需要设置Visual Studio。在解决方案将有两个项目,一个是引擎本身,另一个用于利用此引擎的演示游戏。因此,打开Visual Studio,选择新建项目,并选择XNA Windows Game Liberary,选择一个名称。我用“InnovationEngine”,但可以是其他任何名称。当项目建立后,打开解决方案资源管理器,右按一下您的解决方案(顶级节点)。选择添加新项目,并选择型XNA WindowsGame。选择一个名称(我用“TestEnvironment”),并按下确定。最后,我们需要从游戏项目中引用引擎项目。展开game节点(即“TestEnvironment”),右键点击“Reference”,并选择“添加引用”。点击“项目”标签,然后选择引擎项目(即“InnovationEngine”)。在引擎项目新建一个叫做“Component”的文件夹。现在已全部设置好了。要运行游戏,右键点击项目,并点击“设置为启动项目”。现在,按下F5游戏就可以开始了,目前一个空白蓝屏。
这有助于我们避免常见错误#1 -不分离引擎代码和游戏代码。分离代码是一个好主意,除非游戏是非常简单的。它不仅有助于使解决方案清晰,当以后创建编辑器时也会变得很容易,也能在不同的游戏中重用代码。
Component类
我们要编写的第一个类是Component类。这是所有物体的基类。通过右击项目并选择“新建项”添加新的文件,并命名为“Component.cs”。将以下代码添加到下面。注意:请不要将该代码复制并粘贴,自己动手输入能更好地理解你做的事情。
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)
{
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");
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每帧调用组件的更新和绘制方法。
有一个清晰定义的基类可以帮助我们避免常见错误#2 -没有一个类能让引擎知道如何工作。如果您的引擎是由很多不同格式的类组成的,那么你不用担心每个组件如何更新和绘制。拥有一个基类意味着我们要做的只是继承它,稍微改变一下更新和绘制,引擎就能为我没绘制,更新和管理组件而不需要额外的代码。因为每一个组件都能通过使用基类类型被访问,即使它是一个地形,天空或其他任何东西,这极大地简化了我们的更新,绘制和管理组件的逻辑。
Component接口
下一步我们要为组件定义一些接口。接口能确定物体与其他物体是如何发生作用的,它也让我们区分不同的对象类型。这里我们定义了I2DComponent和I3Dcomponent接口,告知引擎组件使用的是二维还是三维绘制和更新。它还告知引擎如对象的位置,旋转等一些基本信息,简化了像拾取(检测鼠标是否点击了目标),在编辑器中改变位置,旋转等属性。
我们还将定义一个枚举包含对象组的类型:二维,三维或两者都是。以后我们可以使用这个枚举用于渲染水面。当绘制水的反射时,我们不希望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.
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.
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属性。这些属性可以被用来控制哪个屏幕在“下面”,然后将他们想象为一个GameScreens堆栈,最新的在堆栈顶部。
例如,如果您添加一个新的GameScreen并把BlocksUpdate,BlocksInput和BlocksDraw设置为true,那么你就创建了一个暂停菜单。这可以节省大量的时间,因为它非常容易被创建。没有必要建立一个复杂状态系统来跟踪游戏所处的状态。我们只需添加新的屏幕并根据需要改变其属性。这会引出常见错误#4-未将代码分隔成明确定义的部分并创建一个过于复杂的状态管理系统。虽然这可能看起来有些过度,对较小的游戏肯定如此,但系统更具弹性,只需花很少的努力就能扩展它。
虽然状态管理可以创建在最终的游戏中而不是由引擎管理,但使用GameScreen能使我们得到一个灵活的设计和框架。下面是GameScreen的代码。它添加到一个叫做GameScreen.cs新文件中:
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
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
// 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
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()
{
// Create a temporary list so we don't crash if
/ a component is added to the collection while
// updating
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)
{
// Temporary list
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);
}
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
foreach (Component component in drawing)
if (component.Visible && component.Initialized)
{
// 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();
// 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。这将由引擎在每帧计算,所以当检查输入时我们还应该查看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
{
// 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);
}
}
}
在下篇文章,我们会处理引擎类本身。现在,通读并确保您理解了上述知识。
译者注:其实XNA框架中已经包含了Component类和ComponentColletion类,照作者的说法,他自己实现这两个类可以获得更大的自由。
发布时间:2008/12/24 上午10:43:53 阅读次数:7820