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

Collision - A C# Game, Part 1: Parallax Scrolling

starIconstarIconstarIcon
emptyStarIcon
starIcon
emptyStarIcon

3.86/5 (4投票s)

2002年4月17日

5分钟阅读

viewsIcon

108798

downloadIcon

1485

我尝试用C#编写一个简单的游戏。

Sample Image - Collision.jpg

概述

作为我 C# 学习之旅的一部分,我决定写一个游戏,很大程度上是因为人们似乎喜欢游戏,而且我已经用 C++/DirectX 写了一个《Asteroids》游戏。我认为这意味着我已经有了程序逻辑和图形,我只需要学习我想要的部分,比如处理键盘输入、资源等等。不过有人比我抢先一步,所以我决定做一个更简单的——一个横向卷轴游戏,目标是避开迎面而来的小行星。

视差滚动

对于那些不记得的人来说,视差滚动是在 3D 时代让事物看起来很酷的一种方法。基本上,它涉及以不同的速率滚动多个不同的位图,这给人一种 3D 的错觉,就像两个以相同速率移动的物体,如果它们离你的距离不同,它们会以不同的速度行驶一样。

时序

动作游戏的第一步是让它在任何计算机上都能以相同的速度运行。使用 C++,我会通过捕获 WM_IDLE 来实现这一点,并在处理器空闲时绘制我的对象,但根据上次移动它们以来经过的时间来移动它们。这样,速度始终相同,并且我们能获得处理器可能达到的最高帧速率。较慢的机器会降低帧速率,而不是游戏速度。

嗯,我不知道 C# 是否有 OnIdle 消息,尽管我知道通过一些巧妙的技巧(如 Petzold 书中所述)可以捕获该消息。但是,为了练习的目的,我决定改用计时器。在 C# 中,我们这样设置计时器:

private Timer timer = new Timer();
timer.Tick += new EventHandler(OnTimer);
timer.Enabled = true;
timer.Interval = 1000/60;

该变量被设置为类成员,其余的在初始化时完成。我们为计时器定义了一个事件处理程序,将其打开并设置为每秒触发 60 次。只有在系统不忙时计时器才会触发,所以我们不能保证每秒精确触发 60 次。为了获得更好的精度,我们将使用 DateTime 对象来计时。我们创建一个名为 m_DateTime 的成员 DateTime 并使用 DateTime.Now 进行设置。然后,在我们的计时器函数开始时,我们这样做:

TimeSpan ts = DateTime.Now - m_DateTime;

if (ts.Milliseconds > 1000/m_nFPS)
{
	m_DateTime = DateTime.Now;
	m_DateTime.AddMilliseconds(ts.Milliseconds - (1000/m_nFPS));

换句话说,如果经过的时间超过我们期望的每秒帧数之间的时间,我们就采取行动,进行绘制,并再次将变量重置为 Now(),再加上任何剩余的偏移量。正如我们稍后将看到的,虽然我使用简单的绘图测试了这个代码,但一旦我们滚动位图,它就变得无关紧要了,因为在关闭计时器后,它会全速运行,我每秒只能获得大约 2-3 帧。C# 的速度不足以进行动作游戏,或者至少它没有提供我能看到的方法来快速执行位图滚动。

资源

.NET 平台有一种有趣的处理资源的方式。基本上,通过选择**Project | Add Existing Item** 来添加资源,然后通过在 Solution Explorer 中点击该项,可以将其构建操作更改为“Embedded Resource”,即它成为您的*.exe* 的一部分。要加载位图资源,我使用这一行:

m_bmPlanets = new Bitmap(GetType(), "Planets.jpg");

现在我的位图是从资源中加载的。根据您的默认命名空间,您可能需要指定一个命名空间名称,例如本例中的 Collision,以便加载资源。我必须承认,我花了一个小时才让它开始工作,而且我不确定我做了什么……

平铺

我的最初策略是创建一个背景不断变化的系统,所以我构建了两个资源位图,一个包含相同分辨率的星图行,另一个包含每块一个行星,所有块的大小都相同。然后,我构建了一个比屏幕宽一个块宽的位图,并在每次屏幕滚动一个块宽时填充它,在屏幕外绘制新图像,让它们滚动到视图中。我很快就发现这非常慢。我发现的唯一一种不编写图像滤镜就能滚动位图的方法是创建一个克隆,并将其以一定的像素偏移量复制回自身。为了提高速度,我现在有一个重复的系统,我将两个位图都绘制两次,并使用 TranslateTransform 在绘制两个位图之前移动 Graphics 对象。可惜,这也很慢,让我得出结论,没有快速的方法可以做到我正在做的事情。前面提到的计时代码在演示中已关闭,因为它没有被使用。

透明位图

我发现的另一件事是,为了在星星上绘制带透明度的行星,我需要使用 ImageAttributes 对象。位图有一个 MakeTransparent 方法,它接受一个要使其透明的颜色,但由于我使用的资源是以 JPEG 格式保存的,我发现这不起作用。JPEG 是一种有损压缩,这意味着您得到的结果不是您输入的结果。具体来说,我的透明区域不是全黑,而是在一个黑色范围内,所以我需要过滤 0,0,0 到 35,35,35 来获得我需要的遮罩效果。

ImageAttributes iattrib = new ImageAttributes();
iattrib.SetColorKey(Color.FromArgb(255, 0, 0, 0), 
                    Color.FromArgb(255, 35, 35, 35));
iattrib.SetWrapMode(System.Drawing.Drawing2D.WrapMode.Tile,
                    Color.Black, false);

gr.DrawImage(m_bmPlanetLayer, new Rectangle(0, 0, 640, 320), 
             m_nPlanetPos, 0, 640, 320, GraphicsUnit.Pixel, iattrib);

接下来去哪里?

发现 GDI+ 速度太慢无法满足我的需求后,我决定简化。下一部分将包含我将在屏幕上跟踪的行星,以及一个我可以移动的飞船。它将不再滚动星星,希望能提供足够的加速,为最后一篇做好准备,在最后一篇中,我将进行每像素的命中测试,以知道我何时撞入了行星。

历史

  • 2002 年 4 月 17 日:初始版本

许可证

本文没有明确的许可证,但可能包含文章文本或下载文件本身的使用条款。如有疑问,请通过下面的讨论区联系作者。作者可能使用的许可证列表可以在这里找到。

© . All rights reserved.