跳转到内容

使用 XNA 创建游戏/编程/框架

来自 Wikibooks,开放世界中的开放书籍

框架的数量和失败的游戏开发者一样多。每次有人无法完成他们的游戏,或者游戏结果证明是失败的时候,开发者就会将剩余的源代码变成一个“框架”。幸运的是,有一些实际上有用的框架,在这章中,我们想向您展示一些可以轻松快速地用于创建出色游戏的框架。您应该担心的一件事是框架发布的许可证。

LTrees 允许您创建随机生成的树木,包括树干、树枝和树叶。它还具有树木的迎风动画。有几种不同的树木可用,例如桦树、松树和柳树。您可以在右侧看到一个示例。

LTrees 示例

将 LTrees 添加到您的项目需要一些工作,但是添加一些具有预定义迎风动画的简单树木的代码非常简短。

首先,您需要 LTrees 源代码。您可以从 这里 下载它们。解压缩文件,并将项目 “LTreesLibrary” 和 “LTreesPipeline” 添加到您的解决方案中(见下文)。接下来的步骤都在 Visual Studio 中的 *解决方案资源管理器* 中进行。右键单击您的解决方案,然后选择 *添加 -> 现有项目*。现在,导航到解压缩的项目,并选择相应的 *.csproj 文件以将其添加到您的项目中。您可能需要重新生成您的解决方案(CTRL+Shift+B)。然后,您必须将引用添加到您的主游戏项目中。右键单击 *引用 -> 添加引用*。选择 *项目*,然后添加 LTreesLibrary。现在,展开 *内容* 项目,右键单击 *引用* 子菜单,并添加 LTreesPipeline 引用(与上面相同的步骤)。LTreesLibrary 的引用现在应该位于项目主文件夹中的 *引用* 部分,而 LTreesPipeline 的引用应该位于项目 *内容* 子文件夹的 *引用* 部分。现在,您需要将树模型和纹理添加到您的项目中。只需在资源管理器中打开从下载的源代码包中解压缩的 LTreeDemo 项目(此时是普通的 Windows 资源管理器),导航到 *内容* 子文件夹,然后将 *字体*、*纹理* 和 *树* 文件夹拖放到 Visual Studio 中解决方案资源管理器中游戏项目的 *内容* 文件夹中。

现在我们可以继续进行相关代码。以下示例部分取自源代码包中提供的 LTrees 演示应用程序 [1]。首先要添加的是 LTrees 库

using LTreesLibrary.Pipeline;
using LTreesLibrary.Trees;
using LTreesLibrary.Trees.Wind;


我们需要一些全局变量来加载和创建树木和动画。配置文件变量包含有关不同树木的信息。我们还需要一个 TreeLineMesh、一些 SimpleTree 对象、一个 WindStrengthSin(这定义了迎风动画的模式)和一个 TreeWindAnimator 对象。

public class MyGame : Microsoft.Xna.Framework.Game
{

//...
String profileAssetFormat = "Trees/{0}";

        String[] profileNames = new String[]
        {
            "Birch",
            "Pine",
            "Gardenwood",
            "Graywood",
            "Rug",
            "Willow",
        };
TreeProfile[] profiles;

TreeLineMesh linemesh;

int currentTree = 0;

SimpleTree tree, tree2, tree3;

WindStrengthSin wind;
TreeWindAnimator animator;
//...
}


需要两个新方法。LoadTreeGenerators() 将有关树木的信息加载到内容管理器中,NewTree() 生成一棵简单的树木,包括树干、树枝和树叶。

        void LoadTreeGenerators()
        {
                      
            profiles = new TreeProfile[profileNames.Length];
            for (int i = 0; i < profiles.Length; i++)
            {
                profiles[i] = Content.Load<TreeProfile>(String.Format(profileAssetFormat, profileNames[i]));
            }
        }

        void NewTree()
        {
            // Generates a new tree using the currently selected tree profile
            // We call TreeProfile.GenerateSimpleTree() which does three things for us:
            // 1. Generates a tree skeleton
            // 2. Creates a mesh for the branches
            // 3. Creates a particle cloud (TreeLeafCloud) for the leaves
            // The line mesh is just for testing and debugging
			
						
	    //Each tree was loaded into the profiles[]-filed and can be accessed with the numbers 0 to 5. They are chosen randomly here.
            Random num = new Random(); 
            tree = profiles[num.Next(0, 5)].GenerateSimpleTree();
            tree2 = profiles[num.Next(0, 5)].GenerateSimpleTree();
            tree3 = profiles[num.Next(0, 5)].GenerateSimpleTree();
            linemesh = new TreeLineMesh(GraphicsDevice, tree.Skeleton);
        }


上面的方法在 LoadContent() 方法中调用。此外,还会加载迎风动画。

protected override void LoadContent()
        {
            // ...

            wind = new WindStrengthSin();
            animator = new TreeWindAnimator(wind);

            LoadTreeGenerators();            
            NewTree();

            // ...
        }


最后,需要绘制树木。这发生在 Draw(GameTime) 方法中。树木需要适当缩放和平移。此外,我们需要一个 StateBlock 来捕获并重新应用渲染状态,因为 LTrees 不会为我们这样做。如果您省略了它,您很可能会遇到图形故障。

protected override void Draw(GameTime gameTime)
        {
            //..

            Matrix world = Matrix.Identity;
            Matrix scale = Matrix.CreateScale(0.0015f);
            Matrix translation = Matrix.CreateTranslation(3.0f, 0.0f, 0.0f);
            Matrix translation2 = Matrix.CreateTranslation(-3.0f, 0.0f, 0.0f);
            StateBlock sb = new StateBlock(GraphicsDevice);

            sb.Capture();
            tree.DrawTrunk(world * scale, cam.viewMatrix, cam.projectionMatrix);
            tree.DrawLeaves(world * scale, cam.viewMatrix, cam.projectionMatrix);
            animator.Animate(tree.Skeleton, tree.AnimationState, gameTime);
            sb.Apply();

            sb.Capture();
            tree2.DrawTrunk(world * scale * translation, cam.viewMatrix, cam.projectionMatrix);
            tree2.DrawLeaves(world * scale * translation, cam.viewMatrix, cam.projectionMatrix);
            animator.Animate(tree2.Skeleton, tree2.AnimationState, gameTime);
            sb.Apply();

            sb.Capture();
            tree3.DrawTrunk(world * scale * translation2, cam.viewMatrix, cam.projectionMatrix);
            tree3.DrawLeaves(world * scale * translation2, cam.viewMatrix, cam.projectionMatrix);
            animator.Animate(tree3.Skeleton, tree3.AnimationState, gameTime);
            sb.Apply();
			
	    //..
        }

现在编译并启动您的项目,享受在风中摇曳的树木吧!

Nuclex 框架

[编辑 | 编辑源代码]
Nuclex 框架的组装和层
来源:nuclexframework.codeplex.com

Nuclex 是一个框架,它实际上包含多个功能。它专门为 XNA 和其他用 .NET 编写的平台构建。Nuclex 的优势在于不同可用模块的独立性。模块只是指组件,例如 3D 文本渲染或游戏状态管理器。它们可以互换,也可以调整。程序员可以混合它们,只取其中一些元素。事实上,大多数模块对于游戏来说都是必不可少的,因此,只使用一个组件可能已经可以帮助缩短完成时间,或者专注于游戏的其他部分。这些组件是程序员 “不重复造轮子” 的有效帮助,并提供了一种可以稍后自定义的解决方案。如果一个游戏应该包含 GUI、GamePad 输入、矢量字体或其他与游戏相关的功能 - Nuclex 框架是寻找解决方案的正确地方。

有趣的是,Nuclex 框架是微软创建的开源社区 www.codeplex.com 的一部分。即使代码和组件不是微软的财产。

所有类和库都使用完整的测试覆盖率进行编码,其中包括对垃圾回收器或内存管理的测试。正如该项目所说,所有类都具有 Nuclex 是开源的,因此它可以用于任何类型的项目。使用条款明确说明,这些库可以在任何游戏中实现,只要它对其他用户保持开放。此外,每个游戏创建者都欢迎加入平台,与其他 Nuclex 编码者合作。注册一个帐户并成为社区的一部分非常简单。根据 Nuclex 社区的意见,使用框架组件的唯一要求是对编程语言有充分的理解。除此之外,以下所有组件都可以使程序员的生活更愉快。 [2]

Nuclex 框架的功能

  • 3D 文本渲染
  • 任意原始批处理
  • 自动顶点声明
  • 特殊集合
  • 文本输入和标准 PC 游戏手柄支持
  • 核心仿射线程池
  • 调试叠加
  • 游戏状态管理
  • LZMA 内容压缩
  • 多线程粒子系统
  • 矩形填充
  • 带蒙皮的图形用户界面

有关每个模块的更多信息可以在 http://nuclexframework.codeplex.com/ 上找到。由于框架中有很多有用的类,它们的操作可以在网页上轻松跟踪,因此本文只介绍一些解决方案。在接下来的部分中,将解释 Nuclex 的三个主要组件。

框架的组装看起来很复杂,但它实际上只是不同库的集合,这些库可以单独使用。框架的核心包含用于数学、网络和 Windows 窗体的基本类。

矢量字体

[编辑 | 编辑源代码]
使用 Nuclex 框架的 VectorFont

Nuclex 框架中最棒的组件之一是矢量字体创建。它实际上从 .ttf 文件中获取字符,并对每个字符的边缘进行插值。插值完成后,所有信息都存储在一个 .xnb 文件中,然后可以由 Nuclex.Fonts 库使用。即使在尺寸很小的情况下,文本看起来也不如大型花哨的标题好,但它是一个很棒的功能。

这些字体可以在 PC 或 Xbox 上无缝使用,甚至比 XNA 中的 SpriteBatch 类更快。

有三种显示字体的方式。一种是带轮廓的文本。它基本上从字体中获取字母,并计算每个字符的边缘以进行描边查看。显示矢量字体的另一种方式是填充方式。技术与之前相同,但它填充了字符。最后,可以使用字符的挤出版本。

首先,重要的是导入 Nuclex.Fonts 和 Nuclex.Graphics,并提供一个加载 ttf 的 VectorFont 对象。在 LoadContent() 方法中,您现在可以加载字体

using Nuclex.Fonts;
using Nuclex.Graphics;

private VectorFont arialVectorFont;
private Text helloWorldText;
protected override void LoadContent() {
  this.arialVectorFont = this.content.Load<VectorFont>("Content/Fonts/Arial");

  this.helloWorldText = this.arial24vector.Extrude("Hello IMIs!");

//.....

除了 VectorFont 之外,我们还需要一个类似于 SpriteBatch 的类,名为 TextBatch。使用它的实例,我们实际上可以绘制文本。我们仍然在 LoadContent() 方法中。

///....

  this.spriteBatch = new SpriteBatch(this.graphics.GraphicsDevice);
  this.textBatch = new TextBatch(this.graphics.GraphicsDevice);
}

private TextBatch textBatch;

最后,我们需要将所有部分连接在一起并绘制文本。当然,选择不同的填充类型会产生不同的结果。

///....

   this.textBatch.ViewProjection = this.camera.View * this.camera.Projection;
  this.textBatch.Begin();
  this.textBatch.DrawText(
    this.helloWorldText, // text mesh to render
    textTransform, // transformation matrix (scale + position)
    Color.White // text color
  );
  this.textBatch.End();

Nuclex.UserInterface [3]

[编辑 | 编辑源代码]

Nuclex 的这一部分是一个库,它提供了所有用于游戏或应用程序的交互式视觉界面的工具。无论如何,图形对象都可以通过缩放或定位进行调整,它们易于控制(例如状态更改),并且渲染系统没有连接。因此,不会对游戏造成干扰。

为什么要使用 UserInterface?

[编辑 | 编辑源代码]
  • 直观且简单的设计
  • 跨平台工作(Xbox 360 和 Windows)
  • 特殊的控制台 UI 控件
  • 支持不同的键盘布局
  • 统一缩放
  • 与渲染器无关的设计
  • 在默认渲染器中的蒙皮(使用 XML 文件蒙皮元素)
  • 完全测试覆盖率
使用 Nuclex 框架的简单窗口

用于在游戏中快速轻松地创建 GUI 的组件。它不是用于复杂设置的 GUI 管理器,而是涵盖了典型游戏 GUI 的所有方面。它会根据屏幕自动更改大小,并提供默认视图/蒙皮,除非选择自定义规范。


就像在任何其他 GUI 框架中一样,您可以创建按钮、窗口以及几乎所有其他现代界面功能。


在真正开始之前,我们需要一个基本的界面。这个界面需要非常直观,可以通过 Screen 类来创建。创建一个实例并将其添加到 GuiManager 对象中。GuiManager 负责窗口,因此您需要在前端创建它,可能是在类的构造函数中。

然后,正如之前所述,您可以添加 Screen 对象,并为真正的界面工作做好准备。注意:Viewport 用于将窗口调整到合适的大小。

最后几行确定了窗口的边界。如果您省略它们,窗口仍然会显示,但外观不会很好。

      this.graphics = new GraphicsDeviceManager(this);
      this.input = new InputManager(Services, Window.Handle);
      this.gui = new GuiManager(Services);

      Viewport viewport = GraphicsDevice.Viewport;
      Screen mainScreen = new Screen(viewport.Width, viewport.Height);
      this.gui.Screen = mainScreen;

   mainScreen.Desktop.Bounds = new UniRectangle(
        new UniScalar(0.1f, 0.0f), new UniScalar(0.1f, 0.0f), // x and y = 10%
        new UniScalar(0.8f, 0.0f), new UniScalar(0.8f, 0.0f) // width and height = 80%
      );


现在让我们从一个普通的按钮开始。首先,您需要一个 ButtonControl 实例,然后添加文本,最后设置边界。

 ButtonControl newGameButton = new ButtonControl();
      newGameButton.Text = "Neu...";
      newGameButton.Bounds = new UniRectangle(
        new UniScalar(1.0f, -190.0f), new UniScalar(1.0f, -32.0f), 100, 32
}

放置按钮后,我们可以为其添加一个委托,使其可点击。之后,我们需要将按钮添加到我们的 mainScreen。这与其他 GUI 管理器非常类似,因为您只需将所有对象添加到不同的组件中。在本例中,我们希望将按钮放在桌面上(基本上是屏幕的最底层)。

注意:在以下代码中,按钮的委托会打开一个新窗口。DialogWin 扩展了 WindowControl 类,稍后会详细介绍。

 
      newGameButton.Pressed += delegate(object sender, EventArgs arguments) {
        this.gui.Screen.Desktop.Children.Insert(0, new DialogWin());
      };


现在我们有了按钮,并使其可点击,我们可能想要一个新窗口。我们可以通过扩展 WindowControl 类并向其添加自己的组件来轻松实现这一点。添加意味着我们将它附加到当前窗口,更确切地说是附加到它的“子级”。“子级”是一个由 WindowControl 基类默认实例化的对象。

 public partial class DialogWin : WindowControl {

   //Initializes a new GUI demonstration dialog
    public DialogWin() {
      
            this.nameEntryLabel.Text = "Deine Martikelnummer bitte:";
      this.nameEntryLabel.Bounds = new UniRectangle(10.0f, 30.0f, 110.0f, 24.0f);

          Children.Add(this.nameEntryLabel);
    }

最后,我们想将 GUI 添加到我们的框架中,并使鼠标可见。使用 Nuclex 框架创建界面就是这么简单。

      Components.Add(this.gui);
      this.gui.DrawOrder = 1000;

      IsMouseVisible = true;

游戏状态管理 [4]

[编辑 | 编辑源代码]

顾名思义,它是一个协调不同状态的管理器。一次只能激活一个状态,但可以将一个状态放在另一个状态之上。例如,主菜单可以将正在进行的游戏放在旁边,并在退出主菜单后返回。

管理器的接口

//  Manages the game states and updates the active game state</summary>
public class GameStateManager {

  // Snapshot of the game's timing values
  void Update(GameTime gameTime) { /* ... */ }

  //  Draws the active game state
  void Draw(GameTime gameTime) { /* ... */ }

  //  Pushes the specified state onto the state stack
  void Push(GameState state) { /* ... */ }

  //  Takes the currently active game state from the stack</summary>
  void Pop() { /* ... */ }

  //   This replaces the running game state in the stack with the specified state.
  void Switch(GameState state) { /* ... */ }

  //  The currently active game state. Can be null.</summary>
  GameState ActiveState { get { /* ... */ } }
}

Lennart Brüggemann, mglaeser

参考文献

[编辑 | 编辑源代码]
  1. LTrees Demo Application, Change Set 22316. ltrees.codeplex.com,于 2011 年 5 月 28 日获取
  2. http://nuclexframework.codeplex.com/
  3. http://nuclexframework.codeplex.com/wikipage?title=Nuclex.UserInterface&referringTitle=Home
  4. http://nuclexframework.codeplex.com/wikipage?title=Game%20State%20Management&referringTitle=Home
华夏公益教科书