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

SilverStunts -Silverlight 中的数据驱动游戏

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.85/5 (10投票s)

2007 年 12 月 31 日

BSD

16分钟阅读

viewsIcon

72262

downloadIcon

789

本文讨论了数据驱动网页游戏的理念。并以一个名为“SilverStunts”的示例游戏为例,进行了技术细节的阐述。

SilverStunts powered by Siliverlight

目录

引言

制作游戏既是一项挑战,也是一件趣事。制作网页游戏对我来说更具挑战性。我一直在寻找网页上的新技术和游戏概念。2007 年 4 月,在 MIX'07 大会上发布的 Silverlight 1.1 Alpha 是一个绝佳的机会,可以测试这个新平台,并(可能)推动网页游戏开发向前发展。当时微软也发布了 Silverlight 的开发工具:Visual Studio 2008 代号“Orcas”Beta 1 及其 Silverlight 工具。我下载了这些工具,并花了几个周末的时间开发了一个 Silverlight 游戏原型,以在新平台上尝试一些概念。

本文分为两部分

  • 第一部分从游戏开发者的角度讨论了 Silverlight 平台。还介绍了一些适合网页游戏的通用概念。
  • 第二部分将更详细地介绍 SilverStunts 游戏项目。SilverStunts 项目是一款 2D 物理驱动游戏,运行在 Silverlight 中,可以使用 Python 进行脚本编写,并包含游戏内关卡编辑器。

第一部分可能对对 Siliverlight、游戏和网络趋势感兴趣的普通读者有所帮助。不需要任何 Siliverlight 或编程背景。第二部分可能对将要在 Siliverlight 中实现游戏的程序员有所帮助。代码可以作为新游戏项目的骨架应用程序。它包含了一种嵌入 IronPython 脚本并将其绑定到游戏对象的新颖方法。第二部分要求具备 C#、HTML 和 JavaScript 背景,并且读者需要熟悉 Siliverlight 的概念和工具。一篇很好的入门文章可能是 Silverlight 1.1 Fun and Games 或 CodeProject 网站 Silverlight 部分中的其他文章。

为了清楚起见,在此说明:所有关于 Silverlight 的说明都指的是 1.1 版本(最近已升级到 2.0),该版本包含一些对游戏开发者非常有用的功能。

网页游戏

浏览器正日益成为一个强大的应用程序平台,许多桌面应用程序正逐渐迁移到网页上,因为网页技术日趋成熟,并且宽带连接成本越来越低。近年来,Web2.0 的关注点在于桌面应用程序的迁移以及网页上出现的新可能性(如共享、协作、Web 服务、数据云和可访问性)。那么游戏呢?它们是否准备好迁移到网页上了?

我认为,任何使网页成为更丰富平台的技朮都可能导致游戏开发迁移。至少某些类型的游戏可以受益于新功能。让我们看看 Silverlight 为游戏开发者提供了哪些功能……

为游戏开发者提供的 Silverlight

Silverlight 的优点

  • 强大的平台 == .NET 是一个严肃的框架,支持多种语言,拥有海量的文档和一个庞大的社区,这些都是高质量开发者的来源。
  • 出色的工具 == 程序员使用的 Visual Studio 和设计师使用的 Expression Studio
  • WPF/E (XAML) == 加速的 2D 渲染引擎,适用于游戏(矢量图形、图像、动画、声音、字体等)
  • 与 XNA 相似 == 这为构建 Web、桌面和 Xbox 游戏提供了绝佳的可能性,可以共享代码库和内容
  • 动态语言运行时 (DLR) == 使程序员能够将脚本嵌入到他们的游戏中(Python、Ruby、JScript 等)
  • 高清视频流 == 可用于过场动画和电影般的游戏
  • 全屏支持 == 提供出色的游戏体验

Silverlight 的缺点

  • 无低级 TCP/IP 网络 == 套接字对于创建快速网络游戏(点对点)至关重要。Silverlight 通过 HTTP 提供经典的 AJAX 式网络。
  • 无帧缓冲访问(逐像素) == 这在矢量图形引擎中有意义,但游戏开发者需要某种方式来实现后期处理效果和图像转换。我希望新的 Siliverlight 版本能引入一些与游戏相关的(全屏)效果。
  • 用户采用率 == 目前,由于产品处于 Alpha 阶段,用户采用率缓慢。我很想知道微软将如何设法将 Siliverlight 推送到用户机器上。

数据驱动游戏

电脑游戏通常包含

  • 游戏内容 - 游戏数据,如图像、声音、游戏地图、游戏内文本等……
  • 实现游戏逻辑的代码 - 游戏规则、AI、玩家移动、故事情节……
  • 实现给定平台上的游戏托管的代码 - 键盘和鼠标输入、渲染、播放声音、读取文件、操作系统互操作等……

第一项显而易见,每个人都理解它是一块数据(即带有图像的压缩文件夹)。第二项更棘手。游戏逻辑是代码,但也可以视为数据。例如,如果游戏逻辑作为脚本实现,并通过某些脚本引擎动态运行。脚本源只是文本,可以像数据一样下载和操作。因此,我们将脚本计入游戏数据。第三项通常称为“游戏引擎”。游戏引擎负责运行游戏数据的代码。换句话说,游戏引擎驱动游戏数据(包括脚本)。

数据驱动游戏是一种将游戏数据与游戏引擎清晰分离的游戏。这是一个非常强大的概念,但通常很难实现。在接下来的章节中,我将介绍其动机以及它与 Siliverlight 的关系。

动机

稍微思考一下,您可能会认识到数据驱动设计的某些好处

  • 游戏引擎可重用性
  • 游戏工具可重用性
  • 游戏数据可以用不同的工具制作 - 游戏工具可以独立开发周期
  • 游戏数据更易于移植 - 我们可以重写引擎并可能应用一些数据转换(例如,减小图像尺寸)
  • 游戏数据更易于流式传输

但是也可能存在一些缺点

  • 数据驱动设计更难(成本更高)- 完成第一个游戏可能需要更长时间,基础设施维护成本可能过高
  • 性能 - 数据驱动引擎可能较慢,并且可能消耗更多资源
  • 平台限制 - 例如,在非常受限的平台上不可行
  • 数据驱动设计在大型项目(有后续可能性)中才能发挥作用

在数据驱动设计中,游戏质量在很大程度上取决于数据质量。这就是为什么在这个“数据驱动的世界”中,工具比引擎更重要。

数据驱动的 Siliverlight

微软最初推出 Siliverlight 并非是为游戏开发者准备的。微软将 Siliverlight 作为富网络应用程序的通用平台。幸运的是,当今的 Web 应用程序用户对桌面世界的各种“花哨”功能有着强烈的需求。微软在新平台上不得不满足这些需求。这对游戏有利;“丰富”意味着一个闪亮、更快速、更复杂的游戏平台。实际上,这意味着数据驱动设计在 Siliverlight 中变得更加可行,这是一个向前发展并创造令人惊叹事物的机会。

我坚信可以在 Siliverlight 中实现强大的数据驱动 2D 游戏引擎。Silverlight 包含两项关键技术

  • WPF/E 渲染引擎 - 加速的 2D 矢量图形,数据驱动(XAML 是数据)
  • 动态语言运行时 (DLR) - 脚本引擎,数据驱动(脚本是数据)

这样还可以帮助解决工具和可移植性问题。 Microsoft Expression Studio 为 XAML 创作工具提供了坚实的基础。在过去的几年里,Microsoft Visual Studio 已经是许多游戏开发者的日常工具。新的是,Visual Studio 可以通过 Visual Studio Extensibility (Shell) 变成一个自定义的游戏编辑器。这些工具是基于 Windows 的,但它们的目标平台可以是桌面 Windows (WPF + .NET)、Web (Siliverlight) 或 Xbox (XNA)。这可能为创建跨平台游戏提供有趣的机会。

拥抱网页的精彩

网页在可访问性和协作方面非常强大。创建游戏数据不必完全依赖微软的基础设施。我们可以直接在 Siliverlight 中构建游戏编辑器,并在 Web 上托管,以便进行分布式内容创建和由社区进行定制。网络社区富有创造力,可能会渴望参与“云端”的游戏数据。这使得内容创建和游戏生命周期管理有了新的模式。

这也可以实现新的游戏营销和盈利模式。网页游戏和娱乐是网络趋势的重要组成部分。例如,这个领域的一个新颖方法是 Kongregate,我可以想象他们很快就会为游戏创作者和玩家开设 Silverlight 部分。但这超出了本文的范围。让我们来看一个 Siliverlight 中数据驱动游戏的示例实现……


SilverStunts 游戏 - 一个概念验证

SilverStunts 是一款简单的 2D 物理驱动游戏原型,类似于 Elastomania。我在 2007 年 5 月的几个周末期间进行的开发。我的目标是学习 Siliverlight 并通过创建一个简单的网页游戏来评估它。

我决定使用 Chris Cavanagh 的 2D 物理引擎,并于 5 月份将其移植到 Siliverlight。本文不讨论 Siliverlight 的最佳物理引擎。我没有更新它,尽管 Chris 在 5 月份 晚些时候将他的引擎移植到了 Siliverlight。截至今天(2007 年 12 月),我可能会在新项目中考虑使用 Farseer Physics Engine。物理引擎如今竞争激烈,随着最终 Silverlight 2.0 的发布,我们可以期待更多引擎出现。

功能

  • 物理驱动的 2D 游戏
  • 滚动
  • 按需加载关卡
  • 游戏内编辑器
  • 客户端实时 Python 脚本(使用 HTML 脚本编辑器)
  • 使用 XAML 模板实现游戏对象皮肤

在线演示

您可以在 http://silverstunts.com/cp1/index.html 观看在线演示。该网站需要 Silverlight 1.1 (Alpha Refresh),并且在 Firefox 下效果最佳(IE 会吞噬一些键盘快捷键)。如果遇到任何问题,请遵循页面上的说明。

入门

假设您已在计算机上安装了 Silverlight 1.1 Alpha Refresh (September) 运行时,要在调试模式下运行游戏,请下载源代码并将其解压缩到本地磁盘的某个位置(例如 C:\SilverStunts)。Visual Studio 项目(C:\SilverStunts\game\SilverStunts.sln)适用于 Visual Studio 2008 Professional(或 Beta2)。它也应该适用于 Visual Studio 2008 的 Express 版本(未经测试)。您还应在计算机上安装 Microsoft Visual Studio 2008 的 Microsoft Silverlight 1.1 Tools Alpha

项目配置为 ASP.NET 开发服务器。我们需要一个真正的 Web 服务器,因为最新版本的 Siliverlight 对下载器对象下载本地文件有限制。在首次调试模式下运行网站之前,请务必在解决方案资源管理器中右键单击 C:\SilverStunts\site 并选择“属性页”来设置网站设置。

注意,您可以在某处设置断点(例如,在 Page.xaml.csGameTick 方法中),然后按 F5 进行调试。页面将在您的默认 Web 浏览器中打开。

项目结构

源代码分为三个项目

  • SilverStunts - 游戏源代码和 Siliverlight 入口点(生成 SilverStunts.dll
  • Physics - 物理引擎的源代码(生成 Physics.dll
  • Website - 网页文件,网站“链接”到 SilverStunts 项目,因此每次生成都会将 Page.xaml 复制到网站根目录,将 DLL 复制到 ClientBin

注意:由于 Python 脚本,我需要 DLL 位于网站的根目录。问题是 Visual Studio 构建系统总是将它们放在 ClientBin 中,而我找不到重新配置的方法。我最终使用了一个在站点 Web.config 中正确配置的 URL 重写器。请参阅注释了解更多详情。

有趣的技术细节

在这里,我们将讨论项目中我认为有趣的一些部分。

嵌入 IronPython 脚本

这是我在此项目中颇为自豪的部分。在当前版本中嵌入 IronPython 并不十分直接。您需要创建一个自定义的平台适配层 (PAL),如 本文所述。早在 5 月份,当我进行研究时,网上还没有关于这些问题的讨论。

我找到了另一种方法。非常棘手,但有效。其思想是通过从默认 XAML 页面启动一个 Python 脚本来引导 Python 子系统。然后,在 Python 脚本的“Loaded”事件中,我们可以公开 .NET 功能并动态加载 SilverStunts 程序集。然后,像 Siliverlight 运行时从指向 C# 程序集的 XAML 页面调用一样,创建并初始化主页面。

Page.xaml 包含两行用于以“Python 模式”运行

<x:Code Source="page.py" Type="text/ironpython" />
<Canvas x:Name="loader" Loaded="onLoaded" />

您可以在 page.py 中查看 Python 引导程序

# this is python code driving our silverlight control
# it acts as a scripting DLR bootstrapper (I was unable
# to intialize python scripting engine from managed code)
# we load managed assembly and route all relevant actions to it

import sys, clr

SilverStunts = clr.LoadAssemblyByName("SilverStunts, Version=1.0.0.0")

####################################################################
# class suitable for output redirect
class Redirect:
    def __init__(self, kind):
        self.method = SilverStunts.SilverStunts.Page.Current.PrintConsole
        self.kind = kind

    def write(self, s):
        self.method(s, self.kind)

####################################################################

def onLoaded(sender, args):
    # bootstrap page
    global page
    page = SilverStunts.CreateInstance("SilverStunts.Page")
    page.Init(sender.Parent)
    # redirect standard outputs
    sys.stdout = Redirect(SilverStunts.SilverStunts.Page.ConsoleOutputKind.Output)
    sys.stderr = Redirect(SilverStunts.SilverStunts.Page.ConsoleOutputKind.Error)

注意:重定向魔术在这里是为了将 Python 脚本引擎的输出重定向到我的基于 Web 的控制台。

我在 Shell.cs 中实现了一个围绕 Microsoft.Scripting.Hosting.ScriptEngine 的小包装器。您可以在其中看到初始化脚本模块和执行 Python 表达式的具体细节。

例如,调用关卡脚本中的 tick 事件归结为

private delegate bool TickDelegate(int tick, int elapsed);
private TickDelegate tickDelegate;
// initialization 
public void InitLevel(...)
{
    ...
    // wire dynamically loaded python script with tickDelegate
    tickDelegate = shell.Engine.EvaluateAs<TickDelegate>("tick", shell.Module);
    ...
}

// tick event
public void Tick(int tick, int elapsed)
{
    tickDelegate(tick, elapsed); // call into dynamically loaded python script
}

游戏循环

在 Siliverlight 中,您不会获得一个独占线程来运行代码。执行线程是从浏览器借用的。这意味着当您执行长时间运行的任务时,浏览器可能会变得无响应。尽快将控制权返回给浏览器非常重要。

您的代码通过浏览器或 Siliverlight 运行时的一些事件被调用。订阅事件的第一个机会是 XAML 页面初始化事件。在那里,您可以订阅计时器。您可以订阅 HTML Timer 或 XAML Storyboard。对于游戏循环,XAML Storyboard 是首选方式。我使用的方法与 A Better Game Loop 文章中描述的方法类似。我的实现位于 Timer 类中。

我以 60 FPS 的速度运行 GameTick,但渲染速度减半。

public void GameTick(TimeSpan timeElapsed)
{
    // this game loop should run at 60FPS
    tick++;

    game.ProcessInputs(keyboard.keys);
    game.Simulate(); // here is simulated physics
    level.Tick(tick, (int)timeElapsed.TotalMilliseconds);
    // here is executed level script

    // throttle rendering to half speed (30FPS is OK for visuals)
    if (tick % 2 == 0)
    {
        level.UpdateVisuals();
        game.UpdateScrolling();
        renderTick++;
    }

    // this is ugly, but who cares ...
    HandleContinueMessage();
}

实体、视觉元素和普通游戏对象

在实现脚本子系统时,我希望脚本成为系统中的一流公民。脚本系统在跟踪脚本内部发生的情况并将游戏状态相应更新方面存在问题。例如:在 SilverStunts 中,您有一个交互式控制台,可以在游戏会话期间输入并执行 margaret = Circle(185, 155, 35)。这将在 Python 子系统中创建一个名为 margaret 的新实体。好的,这是一个 Python 变量,但它必须在游戏引擎和渲染器中有一个影子才能在屏幕上可见。托管的 C# 游戏核心必须知道已创建新实体,并有机会将其注册到系统中。这就是为什么我想区分实体、视觉元素和普通对象。

  • 普通对象 - 是一个原始数据结构(例如,物理对象)
  • 视觉元素 - 是 C# 中的一个数据结构,充当普通对象的视觉表示(它包含 Canvas、一部分 XAML,以及更新 Canvas 的规则)
  • 实体 - 是 C# 中的一个数据结构,但在脚本上下文中可见;它保存对视觉元素和普通对象的引用

因此,实体是存在于 C# 中并从脚本可见的顶级对象。当从脚本创建新实体时,我们会通过进入实体构造函数的代码收到通知。因此,可能的情况是

  • 实体从脚本创建 - 调用构造函数并创建相应的视觉元素和普通对象
  • 实体从脚本删除 - 由于垃圾回收器,析构函数可能会延迟调用;我们可以从脚本调用 Die() 方法来立即销毁实体
  • 实体由游戏创建 - 游戏拥有完全控制权,并且知道如何更新其状态
  • 实体由游戏删除 - 游戏拥有完全控制权,并且知道如何清理数据

您可以检查 SilverStunts 项目的 Entities 子文件夹中的实体实现。

那么视觉元素呢?我实现了一个简单的视觉元素模板系统。视觉模板位于 Visuals.xml 中,一个模板可能看起来像这样

...
<Visual Type="CircleParticle">
    <Canvas.RenderTransform>
        <TransformGroup>
            <TranslateTransform X="{CenterOffset.X}" Y="{CenterOffset.Y}"/>
            <TranslateTransform X="{Curr.X}" Y="{Curr.Y}"/>
        </TransformGroup>
    </Canvas.RenderTransform>
    <Ellipse Width="{Diameter}" Height="{Diameter}" 
            Fill="Blue" RenderTransformOrigin="0.5, 0.5"/>
</Visual>
...

这是一段 XAML,通过绑定属性(在花括号中)进行扩展。这些是绑定到普通对象属性的绑定。此 XML 在启动时作为数据加载并被解析。花括号被转换为绑定表(请参阅 Bindings 类)。当创建新实体时,会为其分配一个模板,并基于该模板创建一个新的视觉元素。在游戏过程中,普通对象的属性将被动画化(例如,物理引擎会改变对象的位置)。视觉元素能够使用普通对象属性的新值更新 Canvas。这是使用 .NET 反射实现的,代码可以在 Visual.cs 中找到。

// binding between plain object (source) and visual's content (target)
class Binding
{
    int id;
    string attribute;
    string field;

    public Binding(int id, string attribute, string field)
    {
        this.id = id;
        this.attribute = attribute;
        this.field = field;
    }

    public object GetValue(Object source)
    {
        BindingFlags f = BindingFlags.IgnoreCase | 
          BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public;
        string[] names = field.Split('.');
        Object value = source;
        for (int i = 0; i < names.Length; i++)
        {
            Type st = value.GetType();
            value = st.InvokeMember("get_" + names[i], f | 
              BindingFlags.InvokeMethod, null, value, new object[] { });
        }
        return value;
    }

    public void SetValue(Object target, Object value)
    {
        BindingFlags f = BindingFlags.IgnoreCase | BindingFlags.Instance | 
          BindingFlags.NonPublic | BindingFlags.Public;
        Type tt = target.GetType();
        tt.InvokeMember("set_" + attribute, 
          f | BindingFlags.InvokeMethod, null, target, new object[] { value });
    }

    public void Update(Canvas content, Object source)
    {
        Object target = content.FindName(id.ToString());
        Object value = GetValue(source);
        SetValue(target, value);
    }
}

滚动

滚动是通过剪辑画布实现的。剪辑画布会隐藏其边界矩形之外的所有内容。实现位于 ClipCanvas 类中。以下是定义游戏滚动器布局的 Game.xaml,它使用了 ClipCanvas

<ss:ClipCanvas x:Name="viewport"
    xmlns="http://schemas.microsoft.com/client/2007"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:ss="clr-namespace:SilverStunts;assembly=SilverStunts.dll"
    Width="1000" Height="600"
    Background="#EEEEEE"
>

    <Canvas x:Name="gui" >
        <!-- here will be inserted game UI elements -->
    </Canvas>
    
    <!-- scrolling canvas for game and editor -->
    <Canvas x:Name="scroller">

        <Canvas x:Name="background">
            <!-- here will be inserted background xaml -->
        </Canvas>

        <Canvas x:Name="world">
            <!-- here will be inserted game entities -->
        </Canvas>

        <Canvas x:Name="foreground">
            <!-- here will be inserted foreground xaml -->
        </Canvas>

        <Canvas x:Name="workspace">
            <!-- here will be inserted editor UI elements (gizmos, etc) -->
        </Canvas>

    </Canvas>

    <Canvas Visibility="Collapsed" x:Name="grid" 
            Opacity="0.1" Width="1000" Height="600">
        <Canvas.Background>
            <!-- Siliverlight 1.1ALPHA does not support TileMode property 
                 on ImageBrush => need to improvise using image -->
            <ImageBrush ImageSource="images/grid.png" />
        </Canvas.Background>
    </Canvas>

</ss:ClipCanvas>

滚动是通过对名为“scroller”的 Canvas 应用平移来实现的。您可以在 Page.xaml.csUpdateScrolling 方法中看到代码。

TranslateTransform tt = new TranslateTransform();
tt.X = -(cameraX - 500);
tt.Y = -(cameraY - 400);
scroller.RenderTransform = tt;

注意:我们需要应用负(反向)相机变换,因为我们是在移动世界而不是视口。

游戏内编辑器

游戏内编辑器非常巧妙。您可以按空格键进入编辑模式。您可以单击一个对象,然后会显示其编辑控件(一个灰色矩形)。控件有一些手柄,您可以通过手柄调整对象属性。当然,您可以进行多选并移动对象、复制粘贴或删除它们。在关卡中进行的视觉更改会反映在脚本编辑器(请参阅实体选项卡)中。脚本编辑器中的更改会反映在游戏中。

编辑器实现在 Editor.cs 中。控件实现在 Gizmo.xaml.cs 中。每个控件都必须实现此接口

public interface IGizmo
{
    void Destroy();
    bool HitTest(System.Windows.Input.MouseEventArgs e);
    void HandleMouseLeftButtonDown(object sender, 
         System.Windows.Input.MouseEventArgs e);
    void HandleMouseLeftButtonUp(object sender, 
         System.Windows.Input.MouseEventArgs e);
    void HandleMouseMove(object sender, System.Windows.Input.MouseEventArgs e);
    void Update();
}

编辑器通过此接口将鼠标事件路由到控件,控件负责影响对象的属性。

遇到的问题

我在实现过程中遇到了一些问题。我假设这些问题将在最终版本中得到修复。

  • 键盘 - 某些浏览器会吞噬按键或对许多按键做出反应(在 IE 中我无法使用 F 键、箭头键或 TAB 键)
  • 鼠标捕获 - 当您激活鼠标捕获后离开浏览器窗口时,捕获永远不会被释放(这是 Siliverlight 中的一个 bug)
  • IronPython 嵌入 - 在最终版本中,IronPython 的嵌入应该会更直接

结论

Silverlight 1.1 仍处于 Alpha 阶段,所以我不想进行严格的评判。制作游戏和学习 Siliverlight 对我来说是一次很棒的经历。从这次经验来看,我可以自信地说,Siliverlight 是一个功能丰富的运行时平台,绝对适合制作休闲游戏。在本文中,我想介绍 Siliverlight 的独特功能,如 DRL 和 WPF,它们在实现数据驱动游戏引擎时非常有帮助。我相信 Siliverlight 开发的未来以及用户用户会快速接受它。我是一个 Siliverlight 和其工程团队的粉丝。

随意将我的代码用作您游戏项目的启动骨架。您可以立即开始。

鸣谢

历史

  • 2007 年 12 月 31 日 - 第一版公开发布。
© . All rights reserved.