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

C#版的Asteroids(小行星)无需DirectX

starIconstarIconstarIcon
emptyStarIcon
starIcon
emptyStarIcon

3.10/5 (10投票s)

2002年3月28日

11分钟阅读

viewsIcon

202073

downloadIcon

2323

使用2D绘图类实现的经典小行星游戏。

Sample Image - asteroids.gif

引言

各位好消息!我发明了一个稍有不同的贪吃蛇游戏;它不依赖于DirectX编码。

现在的zip文件包含了所有必需的文件,对于下载了不完整版本的朋友表示歉意。

这是我的第一篇文章,请原谅任何不整洁之处。首先,简单介绍一下我为什么写这段代码和这篇文章,以及我的编码风格。我一直很喜欢游戏,很久以前,我决定写一个经典的太空侵略者游戏的版本,这将是教我的Visual Foxpro同事关于面向对象设计和编码的好方法。我很久以前也得出一个结论,学习新编程语言的最佳方法是分开学习过程。学习新语言的典型路径包含大约3个部分:

  1. 学习IDE
  2. 学习语法
  3. 为某个问题编写代码。

如果你在使用一个包编写代码时,能记住这三个元素中的哪个,学习就会更容易。为了让事情变得更简单,如果你不必解决一个问题,你可以最大化第1点和第2点。因此,我决定为了学习C#及其IDE,最好重新发明太空侵略者这个轮子,因为我已经知道至少一个解决方案。

嗯,我已经搞定了了一个初步的太空侵略者,所以在中途,我打破了自己的规则,把它改成了贪吃蛇。有时候,你就是无法抗拒改变范围 ;-)

这个项目的目的是为了很好地入门C#——目标是*不是*写出完美的代码或OO设计。这是我的免责声明。我的生产代码(赚钱的)总是比这个格式更好、结构更清晰,真的。不,真的。嘿嘿。

因此,我这篇文章的目的是:向世界展示我的孩子。我对此非常满意,尤其是这次我搞定了游戏中的键盘处理(感谢Lutz Roeder和他移植的旧游戏Digger),而且除了“Hello World”之外,这是我第一次真正用C#写代码。其次,分享是好的。第三,人们可以从中学习,最后,我希望其他人能对这段代码中不足之处提出建议/进行修改。我非常感谢有建设性的反馈——最好附带代码示例。别手下留情,伙计们,我想学习。

对于那些细心的人来说,上面提到的稍有不同的地方是:游戏有1/10的几率在所有贪吃蛇都被摧毁后改变游戏玩法——贪吃蛇可能会从屏幕边缘反弹,而不是绕到另一边。我称之为弹球或碰碰车模式。接下来,还有1/10的几率贪吃蛇可以相互摧毁(自杀模式),所以玩家需要快速积累分数。

以下是待解决的问题和为在学习过程中改进游戏而提出的建议列表。

  • 使用区域(regions)进行碰撞检测。
  • 类层次结构有点乱(或者用我们殖民地同胞的话说“shonky”)。需要一些重构,把部分功能向上或向下移动到层次结构中。
  • 将类成员从主要是公共声明改为大部分私有范围。
  • 重构playingField,将其拆分成三个新类:canvasobserver(包括碰撞检测)和sound system。
  • 文本消息类,继承自Player类,用于处理分数和其他消息。
  • XML高分榜
  • 数据库高分榜
  • 可序列化的高分榜类
  • 更好地理解ActiveX包装类和Multimedia控件。
  • 实现更好的移动物理模型,形式为y=mx+c,以便更好地控制游戏元素的移动速度。
  • 重写现有的导弹物理模型,使用实数而不是整数(从而提高精度)。
  • 将player类重命名为类似gameCharacter的名称,以避免与人类玩家混淆。
  • 我可能在使用静态方法/字段的概念上存在错误。这是我第一次遇到它们(Visual Foxpro没有它们)。
  • 随机UFO角色,可以摧毁玩家/贪吃蛇。
  • 网络版双人游戏(合作或轮流)。
  • 可配置的键盘控制
  • 游戏结束的代码非常“gadgy”(粗糙)。
  • 摆脱陈词滥调的三角形飞船(嘿嘿)。

运行代码

在构建和运行代码之前,您需要提供以下.wav文件:
  • bigasteroid.wav // 用于大贪吃蛇死亡和导弹爆炸
  • mediumasteroid.wav // 用于中等贪吃蛇死亡
  • smallasteroid.wav // 猜猜?
  • player_missile.wav // 用于发射导弹
  • acid bass drum.wav // 用于开始每个新关卡(make_asteroids)

并修改player类中的以下字段:

protected string soundPath = "d:\\my documents\\Visual Studio Projects\\asteroids\\Asteroids\\"; // sound files 的根路径

我发现从操作系统的弹球版本借用wav文件作为音效是相当令人满意的。

游戏控制

  • z - 左旋转
  • x - 右旋转
  • j - 加速
  • k - 开火
  • L - 紧急停止
  • p - 暂停游戏

概述

一个窗体包含一个playing field和一个timer。playing field渲染并观察所有游戏角色。timer触发移动和动画。

类描述

PlayingField

这个类至少实现了2个目标:

  1. 承担观察者角色(观察者模式,来自《设计模式》,Gamma、Helm、Johnson & Vlissides)并处理碰撞检测。
  2. 将角色渲染到屏幕上。

回想起来,将这些功能分开会是个好主意,因为它们基本上没有关联。

碰撞检测

总的来说,这看起来比实际应该的要复杂。由于我使用了数组而不是ArrayList,代码中有许多空值检查,我一开始并不知道这一点。ArrayList在添加/删除元素时会自动调整大小和重新索引,因此这将有助于减少空值检查的需要,但可能会增加确定哪些控件已被检查碰撞和渲染的复杂性,因为元素索引会由ArrayList类透明地更改。

碰撞检测已经得到了相当的优化,并且有一个标志可以设置为减少这种优化以用于实验目的。在顶层检测中,会检查每个角色的边界矩形是否相交(这是可以关闭的层,当有很多贪吃蛇时,它会产生显著差异)。下一层检测使用点数组和GraphicsPaths(graphicsPath.isvisible)来判断targetA中的一个点是否与targetBgraphicsPath相交。传统上,我猜这通常是通过Regions来实现的,然而,由于无知和缺乏帮助,我无法让C#的beta版本在Region特定的代码上正常工作。因此,对于任何可碰撞的元素,都必须有一个GraphicsPath成员和一个点数组,该数组界定了对象的轮廓。

绘制角色

我的最初想法是让游戏中的每个角色自己绘制。当我尝试这样做时,我发现无法去除角色的背景区域,因此当一个角色与其他角色重叠时,一部分就会被遮挡。这看起来太原始了,所以我尝试了各种方法来去除背景,包括将背景颜色设置为透明等等。我失败了,所以决定一个可能的解决方案是让一个单一的控件负责绘制所有角色,因此,所有角色都会在一个大的、单一的背景上绘制。

在代码中,我将该类命名为PlayingField,但Canvas会更合适。PlayingField有一个名为PlayerArray的数组。它用于存储所有需要绘制/动画的游戏元素(除了分数)。对于timer的每一次滴答,这个数组都会被迭代,并且它的元素会被绘制到屏幕上。每个需要绘制的元素都必须有一个动画数组——这个数组用于存储GraphicsPaths以及用于渲染它们的Pens或Brushes。

一个简单的角色,比如贪吃蛇,通常在其数组中只有2个元素:一个由多边形构成的GraphicsPath,以及一个用于填充它的Brush或Pen。一个复杂的角色,比如导弹爆炸,可能需要2个以上的元素。在这种情况下,爆炸由一个用Pen(仅轮廓)绘制的圆和一个用Brush(填充)绘制的小圆组成。

进一步的编码可以创建一个文本消息类,它可以沿着相同的思路工作。然后,它就可以用于在屏幕上显示分数或任何其他需要编写/动画的消息,例如,在自杀模式或弹球/碰碰车模式下。

Player

任何需要移动、动画或碰撞检测的游戏元素父类。如前所述,这需要进行修改,以便将一些方法移到层次结构的下层(例如,贪吃蛇有一个launch_missile方法是没有意义的,只有人类玩家需要关心lastKeyPressed)。

大多数字段都是不言自明的,和/或在代码中有详细注释,所以我总体上不会再进行说明。那些更复杂的字段如下:

public object[,] animationArray; 这将存储GraphicsPath和Pen或Brush用于渲染。每个实例可能有不止一个GraphicsPath和Pen/Brush对。这样就可以为动画叠加各种颜色和形状。例如,导弹爆炸由一个用扩展圆形轮廓填充的扩展圆形组成,且颜色不同,因此有一个[2,2]的数组(即两个对,而贪吃蛇是[1,2]的数组)。

public Point[] shapeArray; 一个点数组,用于两个目的:定义用于填充GraphicsPath的形状,参与碰撞检测。当有人提出用于碰撞的Region相交代码时,这个数组可能会变得多余。

make_move()animate():这两个定义之间界限很小,但将它们分成了两个方法,因为这似乎是值得的。make_move()处理移动的物理原理(物品是否反弹、屏幕环绕、速度和方向)。animate()处理颜色、大小和形状的变化。其他系统可能在这里使用预加载的位图数组。

声音系统

这个系统有两个版本:一个支持C#的beta2版本,另一个支持C#的最终发布版本。

beta版本在代码中被注释掉了。不幸的是,我找不到我使用的Microsoft多媒体控件最终版本的文档,所以这又是一个实验,但至少它能工作。感谢**Jerzy Peter和Peter Stephens**,他们帮助我完成了最初使用winmm.dll播放声音的系统。我在源文件中留下了一些代码供参考。不幸的是,我无法让它同时播放多个声音(但这确实迫使我尝试使用线程来尝试让它工作),所以我想到了使用一个ActiveX多媒体播放器数组来实现这一点。

为了制作声音播放器类,我只是将一个多媒体播放器放在一个窗体上,并查看IDE生成的代码。我并不完全理解C#使用的ActiveX包装器在做什么,但同样,它能工作,所以目前足够了。

播放音效

您可以设置声音播放器数组以获得更多并发声音。代码本身很好理解。我将其设置为6个。

当一个多媒体播放器播放声音时,它不允许共享正在使用的wav文件。为了解决这个问题,我创建了一个传入文件的副本,并使用该副本。然后,当播放器发送其done_event时,该文件将被删除。我最初认为复制文件效率不高,所以我写了一些代码来使用FileStream而不是静态的File.Copy方法来复制文件。我的测试表明,差别不大,只要文件足够小,系统就能工作。

为了删除临时声音文件,我不得不强制播放器释放它们,我通过要求播放器加载一个空文件名来将其置于错误状态。我知道这是“gadgy”编码,但仍请将您的“kludge”(蹩脚)奖章发送到上面的电子邮件地址。不幸的是,这个错误似乎导致音效与游戏略微不同步。

结论

最后,我希望您喜欢我的代码和文章,我希望它是初学者级别的(循序渐进)但这是一个*巨大的*工程,考虑到我目前的状况,这会花费太长时间。向那些编写此类文章的人致敬,它们非常耗时。

我的下一篇文章将是关于一个我写过的树模拟器,它能生成一个简单的类似树的图像,并带有闪烁的色彩。它非常漂亮。

非常感谢Codeproject的所有人,特别是那些为移除背景和声音系统提出建议的人。

© . All rights reserved.