65.9K
CodeProject 正在变化。 阅读更多。
Home

XNA & Beyond: VS 2008 之路

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.91/5 (68投票s)

2007年12月15日

CPOL

22分钟阅读

viewsIcon

300026

downloadIcon

1558

本文展示了如何轻松地将基于 XNA 的游戏嵌入到 WinForms 控件中。此外,它还解释了如何将 XNA GS 项目集成到 VS2008(该 IDE 目前不受 XNA GS 支持),进而能够在基于 XNA 的创作中使用 WPF。

Screenshot -

目录

引言

本文展示了如何轻松地将基于 XNA 的游戏嵌入到 WinForms 控件中。此外,它还解释了如何将 XNA GS 项目集成到 VS2008(该 IDE 目前不受 XNA GS 支持),进而能够在基于 XNA 的创作中使用 WPF。

背景

任何游戏开发者都知道,拥有一个关卡编辑工具来构建游戏世界在当今是“必须的”。根据项目不同,它能帮助你更快、更轻松地构建一切,而不是像使用记事本设计关卡那样,比如,使用

00001110 00010000
00200100 02000000
0*000000***000 0
...........................
00030020000900010

更重要的是,我们大多数人梦想制作一个符合项目需求的关卡编辑器。让我们面对现实:使用第三方工具是一回事,但创建我们自己定制的关卡编辑器工具则完全是另一回事。特别是,如果你打算使用 XNA GS 来创建该工具。

XNA Framework 首次发布时,我说:“这正是我所期待的!”。几天前 XNA GS 的第二个版本发布时,我补充道:“这真是越来越好了!”。作为一名 C# (高级) 程序员,我喜欢使用 XNA GS 来开发我的游戏、原型和“概念验证”。它很有趣,而且——大多数时候——很简单。

不幸的是,如果你想创建自己的关卡编辑器,XNA GS 似乎并没有那么直接,因为乍一看,没有简单的方法可以将 XNA 游戏项目嵌入到 WinForms 控件中。

这个问题原计划在 v2 版本发布时解决,但由于时间问题,XNA 团队将其推迟到以后更新。各位,请理解 XNA 团队为了在圣诞节前发布 XNA GS 2(包含所有新功能)所付出的努力。拜托!事实上,他们值得称赞和休息!

好吧,但是这种情况对我们意味着什么呢?很简单,我们必须找到一种方法来自己解决它。

如果你在 Google 上搜索,或者阅读创建者论坛上的这个帖子,你会发现实现相同目标的各种方法,其中大多数都涉及重新实现图形设备、隐藏 Game.Run() 功能等等。

如果您想利用该功能,并且仍然将基于 XNA 的项目嵌入到 WinForms 控件中怎么办?如果您不想重新实现任何东西,因为您像我一样懒惰,或者您只是不想这样做怎么办?

更糟的是,如果您想使用 VS2008 来处理您的 XNA 项目呢?您知道 XNA GS 的两个版本,无论是 v1(刷新版)还是刚刚发布的 v2,都尚不支持这款出色 IDE 的 2008 版。请阅读此线程以获取更多信息。

总之,有没有一种简单的方法可以将基于 XNA 的项目嵌入到 WinForms 控件中,同时还能使用 VS2008?好吧,让我们一起来探讨。

范围

本文仅适用于 Windows(XP 或 Vista)。

为什么?因为,为了编译 Xbox 360 上的游戏,您只需要任何版本的 VS2005(如上一节所述,VS2008 尚不受支持)。此外,我不认为您可以在 360 上使用 WinForms 控件。否则,编写本文将毫无意义 ;)

此外,如果您正在考虑创建一个关卡编辑工具,请注意,本文的目的不是解释如何创建一个关卡编辑器;因此,特别是,它不会向您展示如何在运行时动态加载自定义内容,例如,通过使用 MSBuild。有大量的文章项目将教您如何实现这一目标。

事实上,本文的主要目的是演示如何使用 VS2008 创建和管理基于 XNA 的项目。

必备组件

为了编译项目,您需要安装

  • VS C# Express 2005(或任何其他 VS2005 版本)
  • VS C# Express 2008(或任何其他 VS2008 版本)
  • 最新版 DirectX SDK
  • 显卡的最新驱动程序
  • 以上所有内容的最新更新(要获取其中大部分,您可以使用“Windows Update”),当然还有
  • XNA GS 2.0(刚刚发布:为此欢呼!)

开始吧

在接下来的章节中,我将尽量保持一切“简单明了”,以确保易于阅读和理解概念及示例代码。

本文附带了两个 zip 文件,其中包含每个部分的源代码。您可以自由使用和修改,遵循“Code Project 开放许可证”(CPOL)。

为了保持文件大小较小,基于 XNA 的项目所做的只是使用 SpriteFonts 在屏幕上显示当前日期和时间。我假设您对 XNA 框架有必要的了解,所以我相信阅读本文后,您将根据自己的需求和梦想扩展示例和模板。

顺便说一句,这是我为“The Code Project”撰写的第一篇文章,所以感谢您的评论和建议……不过请保持友好;)

XNA 和 WinForms:一个简单的方法

虽然这不是本文的主要目的,但我发现了一种我认为优雅而简单的方法,可以将基于 XNA 的游戏嵌入到 WinForms 控件中。

请不要误解我的意思。我仍然认为,当您需要超越 XNA GS 目前在这方面提供的功能时,处理和控制 (a) 图形设备应该如何创建以及 (b) 主循环应该如何表现,才是正确的方向。这一点毫无争议。

然而,正如我在上面几节所说,如果你像我一样懒惰,你可能会对即将介绍的替代方案感兴趣。所以,如果你感兴趣,请继续阅读;如果你不感兴趣,则跳过本节。

首先

  • 创建一个 Windows 游戏项目,
  • 添加一个 Form 控件(IDE 会自动设置对 System.Windows.Forms 的引用),
  • 最后,添加您要渲染的 WinForms 控件,例如 Panel,然后
  • 实现一个返回与您将用于渲染的控件相关联的句柄的属性。

遵循添加 Panel 控件的准则,您的部分类代码应该类似于下面所示

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Text;
using System.Windows.Forms;

namespace WindowsGame1
{
    public partial class Form1 : Form
    {
        public Form1()
        {
            InitializeComponent();
        }

        public IntPtr PanelHandle
        {
            get
            {
                return this.panel1.IsHandleCreated ? 
                       this.panel1.Handle : IntPtr.Zero;
            }
        }
    }
}

现在,让我们转到主游戏文件,默认名为“Game1.cs”,以根据需要修改代码。

在这里,我们将做几件事

  • 游戏窗口首次显示时将其隐藏。
  • 而是显示我们新创建的 Form 控件。
  • 将我们绘制的所有内容复制到我们的 Panel 画布。
  • 当我们的 Panel 被销毁时,退出游戏。

为了完成除第三项任务之外的所有事情,我们需要稍微修改 Initialize 方法

...
...

using SysWinForms = System.Windows.Forms;
// to avoid conflicts with namespaces

...
...

namespace WindowsGame1
{
    /// <summary>
    /// This is the main type for your game
    /// </summary>
    public class Game1 : Microsoft.Xna.Framework.Game
    {
        ...
        ...

        Form1 myForm;

        ...
        ...

        /// <summary>
        /// Allows the game to perform any initialization it needs to before starting to run.
        /// This is where it can query for any required services and load any non-graphic
        /// related content. Calling base.Initialize will enumerate through any components
        /// and initialize them as well.
        /// </summary>
        protected override void Initialize()
        {
            // TODO: Add your initialization logic here

            base.Initialize();

            SysWinForms.Form gameWindowForm = (SysWinForms.Form)SysWinForms.Form.
        FromHandle(this.Window.Handle);
            gameWindowForm.Shown += new EventHandler(gameWindowForm_Shown);

            this.myForm = new Form1();
            myForm.HandleDestroyed += new EventHandler(myForm_HandleDestroyed);
            myForm.Show();
        }

        ...
        ...
    }
}

此外,我们还需要实现如何处理上述两个事件

 namespace WindowsGame1
{
    /// <summary>
    /// This is the main type for your game
    /// </summary>
    public class Game1 : Microsoft.Xna.Framework.Game
    {
        ...
        ...

        void myForm_HandleDestroyed(object sender, EventArgs e)
        {
            this.Exit();
        }

        void gameWindowForm_Shown(object sender, EventArgs e)
        {
            ((SysWinForms.Form)sender).Hide();
        }

        ...
        ...
    }
}

现在是完成第三项任务的时候了:在我们的 Panel 控件中显示所有绘制的内容。为此,我们只需在 Draw 方法的末尾添加一行简单的代码

...
...

{
    /// <summary>
    /// This is the main type for your game 
    /// </summary>
    public class Game1 : Microsoft.Xna.Framework.Game
    {
        ...
        ...

        /// <summary>
        /// This is called when the game should draw itself.
        /// </summary>
        /// <param name="gameTime">Provides a snapshot of timing values.</param>
        protected override void Draw(GameTime gameTime)
        {
            ...
            ...

            base.Draw(gameTime);

            // This one will do the trick we are looking for!
            this.GraphicsDevice.Present(this.myForm.PanelHandle);
        }
    }
}

至此,不需要再做其他事情。您应该能够执行您的项目,并看到此实现运行良好。顺便说一下,现在您知道了——如果您之前没有发现的话——我们为什么要首先获取 Panel 的句柄。

在附加的 zip 文件中,您将找到完整的源代码,其中包含一些补充:我们使用一个简单的 SpriteFont 在屏幕上显示当前日期和时间,并且我们还添加了一个 PropertyGrid 控件——如本文顶部的图片所示——只是为了好玩。

关于此实现最后一点评论:您可能会遇到开销,因为即使您隐藏了“主”游戏窗口,您可能仍在向其绘制。老实说,我没有测试是否是这种情况,因为我正在使用该技术创建我自己的关卡编辑器,所以目前我不太关心性能问题。另外,正如您所注意到的,我很懒惰;)

XNA 和 VS2008:注意事项

XNA 框架背后的“神话”之一是您无法在 VS2008 中创建项目。正如本节将展示的那样,这个假设是错误的。或者至少,不完全正确。

尽管 VS2008 尚未获得官方支持,但您可以找到变通方法在该 IDE 中创建和管理您的 XNA 项目——编辑、编译、运行和调试——因为 XNA 框架程序集可以手动设置为引用,就像我们通常对外部组件、第三方组件以及编译代码所需的所有 .NET Framework 程序集所做的那样。

走这条路你会失去的主要是两件关键的事情:首先,你将无法将你的游戏部署到 Xbox 360,其次,使用内容管道是不可能的。

第一个限制对我们不重要,因为我们只针对 Windows 平台编写代码。但是,第二个限制呢?由于这是一个临时性的变通方法,因此在实践中事情变得有些复杂——尽管解决方案在概念上很简单。

除非我们使用 MSBuild 编译资源,否则我们需要 VS2005 和 VS2008。我们将把内容与代码的其余部分分开,并在 VS2005 中管理前者,在 VS2008 中管理后者。通过这个技巧,您将能够在需要时使用 VS2005 编译内容,然后允许您的 VS2008 游戏项目以二进制输出的形式使用内容管道生成的文件。

请注意,无需手动将输出从一个文件夹复制到另一个文件夹即可编译和执行您的“整个”项目,只需创建两个相关的项目——即 VS2005 和 VS2008 项目——并让它们共享相同的 DebugRelease 文件夹。要更改输出路径,请打开项目属性,选择“Build”选项卡,并在相应的字段中进行更改。

请不要忘记在 VS2008 中将您的构建目标平台设置为“x86”,否则您会收到错误。

让我们总结一下我们刚刚遵循的步骤

  • 在 VS2008 中创建一个“WindowsGame”项目,
  • 设置对 Microsoft.Xna.Framework 程序集的引用,
  • 设置对 Microsoft.Xna.Framework.Game 程序集的引用,
  • 任何编译仅针对“x86”平台,
  • 在 VS2005 中创建一个“WindowsGameLibrary”项目,
  • 将所有您将在“Content”项目中使用的资源添加进去,
  • 让这两个项目共享相同的 DebugRelease 文件夹,
  • 编译 VS2005 项目以生成内容的二进制文件,
  • 编译并运行 VS2008 项目/解决方案,然后
  • 享受表演。

您将在附件的 zip 文件中找到包含源代码的完整示例。

在 VS2008 中创建游戏项目的一个有趣之处在于,您可以利用 .NET Framework 3.5 提供的所有优点;也就是说,匿名类型、Lambda 表达式、LINQ to SQL、LINQ to Objects 等等。但我会把这些留给您作为家庭作业来研究。

在接下来的几节中,事情变得更加有趣。相信我!

WPF,现在就是未来

上一节是关于将 XNA 集成到 VS2008 WinForms 控件中的。

您可能已经知道:Windows Presentation Foundation 允许我们通过使用一种新的声明性 XML 语言来构建应用程序的用户界面元素:XAML(它代表“可扩展应用程序标记语言”)。

现在,是否可以将 XNA 游戏嵌入到 Windows Presentation Foundation 的控件中?更重要的是,我们能否使用 XAML?

这两个问题的简单答案是:

让我们首先设置一个新解决方案,然后

  • 在 VS2008 中创建一个“WPF 应用程序”项目,
  • 设置对 Microsoft.Xna.Framework 程序集的引用,
  • 设置对 Microsoft.Xna.Framework.Game 程序集的引用,
  • 任何编译仅针对“x86”平台,
  • 在 VS2005 中创建一个“WindowsGameLibrary”项目,
  • 将所有您将在“Content”项目中使用的资源添加进去,
  • 相应地配置 IIS,然后
  • 让两个项目共享相同的 DebugRelease 文件夹(如您在上一节中所做)。

现在,与上一节的示例相比,这里出现了差异。

首先,为了将 WinForms 控件嵌入到 WPF 窗口中,您必须使用 WindowsFormsHost,它将作为前者的宿主。在设计视图中打开“Window1”,并按如下方式编辑 XAML 代码

<Window x:Class="WindowsGame1_WPF.Window1"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:wfc="clr-namespace:System.Windows.Forms;assembly=System.Windows.Forms"  
    Title="XNA & Beyond: The Path To VS2008 (Example 3)"
    Height="587" Width="484" Background="SteelBlue">
    <Grid Name="myGrid">
        <Button Height="23" Margin="104,0,99,11" Name="button1" 
            VerticalAlignment="Bottom" Click="button1_Click">Press Me!</Button>
        <WindowsFormsHost Margin="20,20,20,45" Name="windowsFormsHost1">
            <wfc:Panel x:Name="myXnaControl" BackColor="Black" />            
        </WindowsFormsHost>
    </Grid>
</Window>

正如您在上面的代码中看到的,我们直接在 XAML 代码中创建 WinForms Panel,并将其命名为“myXnaControl”。

现在轮到我修改这个窗口后面的代码了,让它看起来像这样

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Shapes;

namespace WindowsGame1_WPF
{
    /// <summary>
    /// Interaction logic for Window1.xaml
    /// </summary>
    public partial class Window1 : Window
    {
        Game1 game;

        public Window1()
        {
            InitializeComponent();

            this.game = new Game1(this.myXnaControl.Handle);
            this.Closing += new System.ComponentModel.CancelEventHandler(Window1_Closing);
        }

        void Window1_Closing(object sender, System.ComponentModel.CancelEventArgs e)
        {
            if(this.game != null)
            {
                this.game.Exit();
            }
        }

        private void button1_Click(object sender, RoutedEventArgs e)
        {
            this.Background = Brushes.Black;
            this.button1.IsEnabled = false;

            this.game.Run();
        }
    }
}

这里有什么新东西?首先,当窗口关闭时,我们退出游戏(如果您还记得以前的示例是在游戏类本身内部处理的)。其次,当我们构建游戏时,我们传递 Panel 的句柄作为参数。第三,游戏在按下按钮之前不会运行(您可以修改此行为,以便在您希望时运行游戏)。

using System;
using System.Collections.Generic;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Audio;
using Microsoft.Xna.Framework.Content;
using Microsoft.Xna.Framework.GamerServices;
using Microsoft.Xna.Framework.Graphics;
using Microsoft.Xna.Framework.Input;
using Microsoft.Xna.Framework.Net;
using Microsoft.Xna.Framework.Storage;
using SysWinForms = System.Windows.Forms;
// to avoid conflicts with namespaces

namespace WindowsGame1_WPF
{
    /// <summary>
    /// This is the main type for your game
    /// </summary>
    public class Game1 : Microsoft.Xna.Framework.Game
    {
        GraphicsDeviceManager graphics;
        SpriteBatch spriteBatch;

        SpriteFont Font1;
        Vector2 FontPos;
        IntPtr myXnaControlHandle;

        public Game1(IntPtr myXnaControlHandle)
        {
            graphics = new GraphicsDeviceManager(this);
            Content.RootDirectory = "Content";

            this.myXnaControlHandle = myXnaControlHandle;
        }

        /// <summary>
        /// Allows the game to perform any initialization
        /// it needs to before starting to run.
        /// This is where it can query for any
        /// required services and load any non-graphic
        /// related content. Calling base.Initialize
        /// will enumerate through any components
        /// and initialize them as well.
        /// </summary>
        protected override void Initialize()
        {
            // TODO: Add your initialization logic here

            base.Initialize();

            SysWinForms.Form gameWindowForm = (SysWinForms.Form)
               SysWinForms.Form.FromHandle(this.Window.Handle);
            gameWindowForm.Shown += new EventHandler(gameWindowForm_Shown);
        }

        void gameWindowForm_Shown(object sender, EventArgs e)
        {
            ((SysWinForms.Form)sender).Hide();
        }

        /// <summary>
        /// LoadContent will be called once per game and is the place to load
        /// all of your content.
        /// </summary>
        protected override void LoadContent()
        {
            // Create a new SpriteBatch, which can be used to draw textures.
            spriteBatch = new SpriteBatch(GraphicsDevice);

            Font1 = Content.Load<SpriteFont>("MyFont");

            // TODO: Load your game content here            
            FontPos = new Vector2(graphics.GraphicsDevice.Viewport.Width / 2,
                graphics.GraphicsDevice.Viewport.Height / 2);
        }

        /// <summary>
        /// UnloadContent will be called once per game and is the place to unload
        /// all content.
        /// </summary>
        protected override void UnloadContent()
        {
            // TODO: Unload any non ContentManager content here
        }

        /// <summary>
        /// Allows the game to run logic such as updating the world,
        /// checking for collisions, gathering input, and playing audio.
        /// </summary>
        /// <param name="gameTime">Provides a snapshot of timing values.</param>
        protected override void Update(GameTime gameTime)
        {
            // Allows the game to exit
            if (GamePad.GetState(PlayerIndex.One).Buttons.Back == ButtonState.Pressed)
                this.Exit();

            // TODO: Add your update logic here

            base.Update(gameTime);
        }

        /// <summary>
        /// This is called when the game should draw itself.
        /// </summary>
        /// <param name="gameTime">Provides a snapshot of timing values.</param>
        protected override void Draw(GameTime gameTime)
        {
            graphics.GraphicsDevice.Clear(Color.CornflowerBlue);

            spriteBatch.Begin();

            // Draw the current date and time
            string output = DateTime.Now.ToString();

            // Find the center of the string
            Vector2 FontOrigin = Font1.MeasureString(output) / 2;
            
            // Draw the string
            spriteBatch.DrawString(Font1, output, FontPos, Color.LightGreen,
                                   0, FontOrigin, 1.0f, SpriteEffects.None, 0.5f);

            spriteBatch.End();

            base.Draw(gameTime);

            // This one will do the trick we are looking for!
            this.GraphicsDevice.Present(this.myXnaControlHandle);
        }
    }
}

上述代码的主要更改是

  • 我们不再关注 HandleDestroyed 事件,并且
  • 我们使用 IntPtr 类型的参数初始化类。

猜猜最后一项陈述的原因是什么?为了避免“互操作”问题。让我提醒您,在所有之前的示例中,我们习惯于在渲染时从各自的 Panel 属性中获取句柄。我们本可以用同样的方式实现所有这些示例,即通过将句柄传递给游戏的构造函数,但我想展示其中的区别。

还有什么需要做的吗?只需编译这两个项目,您将看到类似下图的内容

Sample Image - maximum width is 600 pixels

当您按下窗口底部的按钮时,会发生以下情况

Sample Image - maximum width is 600 pixels

小菜一碟,对吧?现在您可以充分利用这项新技术,享受它的好处。

隧道尽头的“Silverlight”

这是我将来会调查的事情。

原因:也许有一种方法可以使用 Silverlight 技术将基于 XNA 的应用程序嵌入并运行到网页中。在浏览器中加载和玩我们的游戏会很棒。

然而,大声思考之后,我认为应该适用一些限制

  • 客户端必须安装最新的 DirectX 驱动程序(因此任何仅基于 OpenGL 技术的操作系统都无法运行游戏),并且作为推论,
  • 鉴于最终找到的任何解决方案都只适用于 Windows,恐怕不应先验地考虑 Xbox 360 的实现。

也许,我的第二篇文章应该在 Silverlight 2.0 最终发布后重点关注这项调查;据我所知,该版本将允许我们使用 WinForms 控件。

与此同时,在这方面还有另一种变通方法,我将在下面仅出于学习目的进行介绍。

在您继续阅读之前,请务必注意:您绝不应该向任何程序集(以及/或网站)授予完全信任权限,因为这可能会使您的系统面临安全风险,您的机器可能会被第三方远程控制。如果您这样做,无论您认为您知道自己在做什么,还是出于任何其他原因,您都将自行承担授予信任/权限的风险。

当我试图找出一种在浏览器中运行基于 XNA 的游戏的方法时,我记得使用 <Object> HTML 标签和适当的设置,我们可以将 .NET 程序集嵌入到网页中。例如,

<Object id="myControl" name="myControl"
  classid="myWinControl.dll#myNameSpace.myWinControl"
  width="400" height="300" VIEWASTEXT></Object>

尽管上述方法确实有效,但仍需满足某些条件才能避免问题和失望

  • 它可能只适用于 Internet Explorer,
  • 您不能用强名称签署您的程序集,
  • 每次修改和重建对象时,您都应该手动清除 GAC(或手动找到并删除首先缓存的 DLL 文件),
  • 您应该授予您的程序集完全信任权限,
  • 如果您正在构建 ASP.NET 网站,您不应该将您的 DLL 部署到 bin 文件夹——因为后者仅供私有访问,
  • 您应该将您的网站(本地主机或远程网站)添加到信任区域列表,并且
  • 您应该祈祷并等待我们太阳系中所有行星完美对齐。

这一团糟不是我们想要的,不是吗?所以我寻找了第二种方法,这反过来又把我引向了这篇很棒的文章:“在 Visual FoxPro 中托管 .NET ActiveX 控件”。

将程序集设置为 COM 可见为该领域带来了巨大的可能性,因为我们正在将 .NET 程序集公开为 ActiveX 控件。此外,它比以前的替代方案“麻烦更少”地工作。另一方面,正如之前所说,它也可能带来安全风险,因此在处理您的机器或任何客户端机器的安全策略时请务必小心。我重申,此示例仅用于学习目的

好的,我们来看一些代码,好吗?顺便说一下,请记住这只是一个“概念验证”,其唯一目的是表明这个想法在实践中是可以实现的。因此,以下实现设计简单,因此缺少某些理想的功能和控件。

首先,用 Visual Studio(或 VC Express 版)创建一个 WinForms 项目,将其命名为“XnaGame”,然后添加我们在前面示例中使用的 Game1 文件。此外,创建一个 UserControl,并将其命名为“XnaPanel”。我们不需要默认创建的任何 Form 控件,所以直接删除它。

打开项目的属性,并为 COM 互操作注册程序集;此外,通过修改 AssemblyInfo.cs 文件(位于 Properties 文件夹下)使程序集“COM”可见

...
 

// Setting ComVisible to false makes the types in this assembly not visible
// to COM components. If you need to access a type in this assembly from
// COM, set the ComVisible attribute to true on that type.
[assembly: ComVisible(true)]
 
...

完成这些后,我们的 XnaPanel 类的代码应如下所示

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Drawing;
using System.Data;
using System.Text;
using System.Windows.Forms;
using System.Threading;
using System.Runtime.InteropServices;
using System.IO;

namespace XnaGame
{
   /// <summary>
   /// This is the control that will host our XNA-based game.
   /// </summary>
   [Guid("2CD2873E-2A50-4ac9-98CA-B13ACFCC6DFA")]
   [ProgId("XnaGame.XnaPanel")]
   [ComVisible(true)]
   public partial class XnaPanel : UserControl
   {
      static string localPath;

      /// <summary>
      /// Main constructor of the control.
      /// </summary>
      public XnaPanel()
      {
         InitializeComponent();
         this.HandleCreated += new EventHandler(XnaPanel_HandleCreated);
      }

      /// <summary>
      /// Executes when the control's handle is created.
      /// </summary>
      /// <param name="sender">The source of the event.</param>
      /// <param name="e">An System.EventArgs object that contains event data.</param>
      void XnaPanel_HandleCreated(object sender, EventArgs e)
      {
         // Set the path to the local Content folder
         localPath = 
           Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData);
         localPath += @"\Temp\XnaGame\Content";

         // Check whether the folder exists, locally
         while(!Directory.Exists(localPath))
         {
            // If not just wait ...
            // (you can create a timeout control, instead, to avoid
            // running this check for ever!)
         }

         // Run the game in a new thread.
         Thread gameThread = new Thread(RunGame);
         gameThread.Start();
      }

      /// <summary>
      /// Creates and executes the game.
      /// </summary>
      void RunGame()
      { 
         new Game1(this.Handle, localPath).Run();
      }
   }
}

我们在这里所做的与之前看到的没有什么不同,除了

  • XnaPanel_HandleCreated 方法中的循环,
  • 游戏将在其自己的线程中运行(如 DonCroco 所建议),并且
  • 与 COM 相关的属性(您必须为这个类提供一个 GUID)。

正如您从上面的代码中看到的,我们将等到 Content 文件夹在本地创建。

为什么?嗯,我们如何访问服务器上的 Content 文件夹?到目前为止,如果我错了请纠正我,我们无法访问。鉴于 COM 对象在客户端执行,并且内容管道目前不允许我们将 Internet 上的文件夹设置为游戏的 Content 文件夹,我们必须创建 Content 文件夹并在本地复制所有资源。

谁来创建它?什么时候?我们将在下面的几段中看到。

为了给这个项目画龙点睛,让我们修改游戏的构造函数,使其考虑本地 Content 文件夹的路径并正确通知内容管道

...
 
/// <summary>
/// Main constructor of the game.
/// </summary>
/// <param name="myPanelHandle">The handle of the XnaPanel control.</param>
/// <param name="localPath">The path to the local Content folder.</param>
public Game1(IntPtr myPanelHandle, string localPath)
{
   graphics = new GraphicsDeviceManager(this);
   Content.RootDirectory = localPath;
   this.myPanelHandle = myPanelHandle;
}
 
...

我们现在可以构建项目了。首次构建时,VS 将通过在 Windows 注册表中包含以下条目来注册 COM 互操作的程序集

[HKEY_CLASSES_ROOT\CLSID\{2CD2873E-2A50-4AC9-98CA-B13ACFCC6DFA}]
@="XnaGame.XnaPanel"

[HKEY_CLASSES_ROOT\CLSID\{2CD2873E-2A50-4AC9-98CA-B13ACFCC6DFA}\Implemented Categories]

[HKEY_CLASSES_ROOT\CLSID\{2CD2873E-2A50-4AC9-98CA-B13ACFCC6DFA}\Implemented Categories\
{62C8FE65-4EBB-45e7-B440-6E39B2CDBF29}]

[HKEY_CLASSES_ROOT\CLSID\{2CD2873E-2A50-4AC9-98CA-B13ACFCC6DFA}\InprocServer32]
@="mscoree.dll"
"ThreadingModel"="Both"
"Class"="XnaGame.XnaPanel"
"Assembly"="XnaGame, Version=1.0.0.0, Culture=neutral, PublicKeyToken=99c20c94ca29957b"
"RuntimeVersion"="v2.0.50727"
"CodeBase"="<ThePathToTheFolderWhereYourAssemblyIsLocated>/XnaGame.dll"

[HKEY_CLASSES_ROOT\CLSID\{2CD2873E-2A50-4AC9-98CA-B13ACFCC6DFA}\InprocServer32\1.0.0.0]
"Class"="XnaGame.XnaPanel"
"Assembly"="XnaGame, Version=1.0.0.0, Culture=neutral, PublicKeyToken=99c20c94ca29957b"
"RuntimeVersion"="v2.0.50727"
"CodeBase"="<ThePathToTheFolderWhereYourAssemblyIsLocated>/XnaGame.dll"

[HKEY_CLASSES_ROOT\CLSID\{2CD2873E-2A50-4AC9-98CA-B13ACFCC6DFA}\ProgId]
@="XnaGame.XnaPanel"

[HKEY_CLASSES_ROOT\XnaGame.XnaPanel]
@="XnaGame.XnaPanel"

[HKEY_CLASSES_ROOT\XnaGame.XnaPanel\CLSID]
@="{2CD2873E-2A50-4AC9-98CA-B13ACFCC6DFA}"

[HKEY_LOCAL_MACHINE\SOFTWARE\Classes\CLSID\{2CD2873E-2A50-4AC9-98CA-B13ACFCC6DFA}]
@="XnaGame.XnaPanel"

[HKEY_LOCAL_MACHINE\SOFTWARE\Classes\CLSID\
       {2CD2873E-2A50-4AC9-98CA-B13ACFCC6DFA}\Implemented Categories]

[HKEY_LOCAL_MACHINE\SOFTWARE\Classes\CLSID\
       {2CD2873E-2A50-4AC9-98CA-B13ACFCC6DFA}\Implemented Categories\
{62C8FE65-4EBB-45e7-B440-6E39B2CDBF29}]

[HKEY_LOCAL_MACHINE\SOFTWARE\Classes\CLSID\
       {2CD2873E-2A50-4AC9-98CA-B13ACFCC6DFA}\InprocServer32]
@="mscoree.dll"
"ThreadingModel"="Both"
"Class"="XnaGame.XnaPanel"
"Assembly"="XnaGame, Version=1.0.0.0, 
      Culture=neutral, PublicKeyToken=99c20c94ca29957b"
"RuntimeVersion"="v2.0.50727"
"CodeBase"="<ThePathToTheFolderWhereYourAssemblyIsLocated>/XnaGame.dll"

[HKEY_LOCAL_MACHINE\SOFTWARE\Classes\CLSID\
      {2CD2873E-2A50-4AC9-98CA-B13ACFCC6DFA}\InprocServer32\1.0.0.0]
"Class"="XnaGame.XnaPanel"
"Assembly"="XnaGame, Version=1.0.0.0, 
      Culture=neutral, PublicKeyToken=99c20c94ca29957b"
"RuntimeVersion"="v2.0.50727"
"CodeBase"="<ThePathToTheFolderWhereYourAssemblyIsLocated>/XnaGame.dll"

[HKEY_LOCAL_MACHINE\SOFTWARE\Classes\CLSID\{2CD2873E-2A50-4AC9-98CA-B13ACFCC6DFA}\ProgId]
@="XnaGame.XnaPanel"

[HKEY_LOCAL_MACHINE\SOFTWARE\Classes\XnaGame.XnaPanel]
@="XnaGame.XnaPanel"

[HKEY_LOCAL_MACHINE\SOFTWARE\Classes\XnaGame.XnaPanel\CLSID]
@="{2CD2873E-2A50-4AC9-98CA-B13ACFCC6DFA}"

<您的程序集所在文件夹的路径> 并非实际输出,仅为示例目的的占位符。相反,如果您检查注册表,该虚假路径应替换为您的机器上(或远程,如果您已将其上传到 Internet 上的网站)DLL 所在文件夹的实际路径。

此外,这由 VS“自动”完成,但是当您将项目部署到 Internet 时,您必须提供一种注册 COM 对象的方法,如文章“在 Visual FoxPro 中托管 .NET ActiveX 控件”中所述(请参阅该文章中 RegisterClass/UnregisterClass static 方法的示例实现)。

第二个项目:创建一个新的 ASP.NET Web 项目(使用 VS 或 VWD Express),将其命名为“XnaOnWebSite”,并将包含字体文件的 Content 文件夹添加到该项目(如我们在前面章节中看到的)。

首先,我们按如下方式修改内联代码

<%@ Page Language="C#" AutoEventWireup="true" CodeBehind="Default.aspx.cs"
 Inherits="XnaOnWebSite._Default" %>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
 "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" >
   <head runat="server">
      <title>Xna-Based Game On A WebPage</title>
   </head>
   <body bgcolor="Black">
      <form id="form1" runat="server" style="margin-top: 50px;">
         <div style="text-align: center;">
            <object id="myXnaGameControl" name="myXnaGameControl"
             classid="clsid:2CD2873E-2A50-4ac9-98CA-B13ACFCC6DFA"
             width="640" height="480" VIEWASTEXT>
            </object>
         </div>
      </form>
   </body>

</html>

请注意,我们没有使用 classid="XnaGame.dll#XnaGame.XnaPanel",而是使用了我们提供给 COM 可见类的 GUI 类。

然后,我们修改代码隐藏

using System;
using System.Collections;
using System.Configuration;
using System.Data;
using System.Web;
using System.Web.Security;
using System.Web.UI;
using System.Web.UI.HtmlControls;
using System.Web.UI.WebControls;
using System.Web.UI.WebControls.WebParts;
using System.IO;

namespace XnaOnWebSite
{
   /// <summary>
   /// This class is responsible of copying all the content files
   /// from the server to the local Content folder.

   /// </summary>
   public partial class _Default : System.Web.UI.Page
   {
      /// <summary>
      /// Executes when the page loads.
      /// </summary>
      /// <param name="sender">The source of the event.</param>
      /// <param name="e">An System.EventArgs 
      ///           object that contains event data.</param>
      protected void Page_Load(object sender, EventArgs e)
      {
         // Get the physical Path of the Content folder in the server
         string serverPath = Server.MapPath(@"\Content");
         
         // Get the properties of the Content folder 
         DirectoryInfo serverFolder = new DirectoryInfo(serverPath);
         
         // Check whether the folder exists on the server
         if (!serverFolder.Exists)
         {
            throw new DirectoryNotFoundException("The Content" + 
                      " folder is not present on the server.");
         }
          
         // Set the path to the local Content folder
         string localPath = Environment.GetFolderPath(
                             Environment.SpecialFolder.LocalApplicationData);
         localPath += @"\Temp\XnaGame\Content";
 
         // Check whether the folder exists locally
         if (Directory.Exists(localPath))
         {
            DirectoryInfo localFolder = new DirectoryInfo(localPath);
            localFolder.Delete(true);
         }
 
         // Create the folder locally
         Directory.CreateDirectory(localPath);
 
         // Get the properties of the unique content file
         FileInfo serverFile = new FileInfo(serverPath + @"\myFont.xnb");
 
         // Check whether the file exists on the server
         if (!serverFile.Exists)
         {
            throw new FileNotFoundException("The file 'myFont.xnb' is" + 
                      " not present in the server's Content folder.");
         }
 
         // Copy the file to the local Content folder
         serverFile.CopyTo(localPath + @"\myFont.xnb", true);
      }
   }
}

我猜上面的代码是不言自明的,但基本上,我们正在做的是将服务器 Content 文件的内容复制到本地 Content 文件。对于这个示例,没有必要使用递归操作,因为我们知道只有一个文件要复制。

好的。当您构建并执行项目时,您的浏览器将打开新的“本地”网站(您必须将其添加到信任区域列表)。

您会注意到该程序集仍缓存在您的本地机器上(在您的 AppData 文件夹中的某个位置;查找名为“dl2”或“dl3”的文件夹),并且该 DLL 附带一个 ini 文件,默认名为“_AssemblyInfo_.ini”,其内容类似于以下内容

X n a G a m e , 1 . 0 . 0 . 0 , , 9 9 c 2 0 c 9 4 c a 2 9 9 5 7 b  
<ThePathToTheFolderWhereYourAssemblyIsLocated> / X n a G a m e . d l l

一个有趣的注意事项:您可以修改首次构建 COM 可见程序集时由 VS 创建的注册表条目;因此,假设您将 XnaGame.dll 文件托管在 URI“http://yoursite.com/MyBins”下的一个文件夹中;您可以修改注册表,将“旧”路径更改为新路径,下次您检查该文件时,您将得到以下结果

X n a G a m e , 1 . 0 . 0 . 0 , , 9 9 c 2 0 c 9 4 c a 2 9 9 5 7 b 
h t t p : / / y o u r s i t e . c o m / M y B i n s / X n a G a m e . d l l 

什么意思?每次打开网站,文件都会从该远程位置下载并缓存。

终于!好的部分:如果一切顺利,您应该会看到类似下面的截图

Sample Image - maximum width is 600 pixels

如果出现问题,请不要忘记,为了运行示例代码,您必须

  • 用强名称签署程序集,
  • 注册 COM 对象,并且
  • 将您的网站添加到信任区域列表。

根据您的浏览器配置,COM 对象可能会被阻止——因为它无法验证提供者,因为我们没有为我们的程序集附加任何证书——但是如果您完成了上述任务,您会做得很好。

如前所述,这个例子只是一个简单的基础。一个起点。基于此的一个有趣想法是创建一个“XNA Web 播放器”,其中播放器程序集与游戏本身分离。

实现这一点的一种方法是创建一个安装程序,将 COM 可见程序集保存到客户端的“Program Files”目录;该程序集 (a) 包含某种插件系统,并且 (b) 当由浏览器执行时,它会

  • 检查 Content 文件夹是否存在于本地,
  • 检查是否有新文件需要复制到该文件夹,
  • 复制任何新版本的资源,
  • 将游戏下载到内存或“temp”文件夹,
  • 执行游戏,并且显然
  • 在浏览器中充当游戏的宿主。

换句话说,类似于常见的“网页播放器”,或者游戏相关框架提供的那些,您安装播放器和包含游戏引擎 DLL 的 DLL。

你猜怎么着?当客户端首次安装 XNA GS 2 可分发程序集时,我们的基本 DLL 已经存在于客户端机器上。唯一需要处理的是您的游戏使用的第三方 DLL。

然后,只需在网页中插入以下代码即可

<Object id="myControl" name="myControl"
classid="clsid:2CD2873E-2A50-4ac9-98CA-B13ACFCC6DFA"
width="400" height="300" VIEWASTEXT>
<param name="Game" value="/myServerFolder/myGame.dll">
<param name="Content" value="/myServerFolder/Content/">
...
</Object>

我无法想象这样的东西对于展示我们的演示、原型等会有多方便。而且,我想您也有同感……

结论

正如本文所演示的,只需“一点点”努力,我们就可以毫无问题地并排使用 XNA GS 2.0 和 VS2008 来创建和管理我们的 Windows 项目。

所提出的变通方法很简单,并允许我们利用 .NET Framework 3.5 中包含的所有新功能。

本文还演示了如何使用 <Object> 标签和 .NET 程序集的 COM 互操作将基于 XNA 的游戏嵌入到网页中。这方面的示例只是未来研究的“开端”。

希望您觉得本文有用——并且享受阅读的乐趣,就像我享受撰写它的乐趣一样。

关注点

Sample Image - maximum width is 600 pixels

在我们等待 XNA 团队的官方解决方案的同时,这里有一些有趣的创意供您尝试

  • 将所有解释的“技巧”应用于您自己的基于 XNA 的项目(我已能够使用 XNA 团队的“Net Rumble”入门工具包测试我所有的示例代码,所以我可以说代码有效)。充分利用 .NET 3.5 中可用的新功能来处理基于 XNA 的项目;即使用匿名类型、LINQ、lambda 表达式等,
  • 使用代码为每个 WinForms 控件渲染一个视口——如果一个关卡编辑器不能同时提供游戏世界的不同视图(透视图、前视图、顶视图、左视图等),那它就不是一个好的关卡编辑器。
  • 当然,别忘了实现一个漂亮的网格对象。

历史

这是本文的第二版。

自第一版以来的更改

  • 添加了有关如何将基于 XNA 的游戏嵌入网页的内容
  • 更新了示例代码以支持 XNA GS 3.1 和 .NET Framework 3.5,并进行了重大修复(感谢 Don Croco!)
© . All rights reserved.