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

青蛙跳

starIconstarIcon
emptyStarIcon
starIcon
emptyStarIconemptyStarIcon

2.70/5 (14投票s)

2004年5月25日

10分钟阅读

viewsIcon

84705

downloadIcon

1479

用 C# 编写的 Frogger 克隆游戏。使用了双缓冲定时器和其他技术。

Sample Image - FrogGoHop.gif

引言

这是 Atari 原版 Frogger 的克隆。这是我第一个用 C# 制作的游戏。我希望,在您阅读完这篇文章后,您能获得一些制作自己有趣游戏的技能。剪贴画被认为是来自免费剪贴画网站的公共领域作品。我使用定时器和 OnPaint 事件使用 GDI 双缓冲绘制图像以减少闪烁。我使用了 Quartz.dll 和我自己的类(tonysound.cs,我写了一篇关于它的文章),来播放声音事件和背景音乐。点击我的文章链接找到这篇文章。

变量

速度变量

我设置了几个速度变量,如您在标题为 variables 的代码 #region 中所见。速度变量用于控制正在绘制的 Image 对象的增量。例如,如果我想让对象慢速移动,您将需要一个低的增量数字,这样它就不会那么快地穿过屏幕。对于游戏的每个级别,我都会在当前速度上加一。这将增加对象移动的增量,并使对象看起来移动得更快。

X 坐标Lane1var1 等 X 坐标是用于游戏区域的 X 坐标。每个对象都有其 X,Y 坐标的位置。所以如果我们移动 x,它会明显地向左或向右移动。这就是青蛙和汽车移动的方式。我在定时器内为 Image 对象增量 x,例如

if (long1varx<672)
    long1varx+=logset1speed;
else
    long1varx=-71;
if (long2varx<672)
    long2varx+=logset1speed;
else 
    long2varx=-71;
if (long3varx<672)
    long3varx+=logset1speed;
else
    long3varx=-71;

正如您在此处看到的,我检查第一行的第一个原木是否小于 672。如果是,那么我将 logset1speed 添加到它,使其改变其 x 位置。否则,我将其重置为其起始位置 -71。我对其他原木也做了同样的处理。

现在,青蛙我通过 KeyEvent 获取其 x 值,然后在 timer1 调用 drawstuff(g) 函数时,它将获取青蛙的更新后的 x 值并相应地绘制图形。

public void OnKeyPress(object sender,System.Windows.Forms.KeyEventArgs e)
{
    ....
    
    if (e.KeyCode == System.Windows.Forms.Keys.Left)
    {        
                
        Frogx-=30;
        Sound.Play(hopsound, 
                   PlaySoundFlags.SND_FILENAME | PlaySoundFlags.SND_ASYNC | 
                   PlaySoundFlags.SND_NOSTOP);
        
    }
    .......

上面发生的事情是,每次按下按键时都会触发 keypress 事件。然后我检查左箭头、右箭头等是否被按下。然后我根据箭头移动青蛙的坐标。如果是向左,那么我从其原始 x 位置减去其移动增量。如果是向右,那么我添加。

Target Taken 和 Snake Taken 的布尔值。 我有 bool 值,如 bool Target1takenbool Snake1Taken。这会设置一个 truefalse 来确定青蛙是否可以进入家园。如果目标已被占领,这意味着青蛙已经到达这里并且不能再次进入。现在如果那里有蛇,它会杀死青蛙,所以我们测试它是否在那里,所以如果青蛙跳入该空间,它就会死亡。

得分和原因的静态变量。 如果您想知道为什么我将 score 变量设为 static,原因如下。如果您在另一个使用相同命名空间的窗体上,您将无法使用来自另一个窗体的变量,而无需创建对象的实例。所以我稍微作弊,将变量类型设为 static,然后我只需要输入 formname.variablename;例如,Form1.Score,而无需在,例如,Form4 中创建窗体实例。

那是什么声音? 好了,就像几乎任何游戏一样,您有声音事件和背景音乐。好了,这里也不例外。如果您阅读了我关于如何为 C# 添加声音的文章,您就会对我的做法有所了解。我使用了我的类 tonysound.cs,它使用 winmm.dll 来播放我的声音事件,例如青蛙跳跃和死亡。现在,背景音乐有点不同。您看,winmm.dll 不支持并行播放声音。所以,我不得不为背景音乐想出一些稍微不同的东西。我听到您在那里询问如何做到,那么让我们深入探讨一下。

Quartz.dll

为了使用 Quartz.dll,我们必须将其添加到项目的引用中。要做到这一点,我们进入 Source Explorer 中的主项目名称,然后点击 Add reference。当对话框弹出时,我们点击 Browse 按钮,然后进入 windows\system32 目录并找到 quartz.dll。添加后,然后点击 OK。您将看到 QuartzTypeLib 已被添加。现在我们可以开始使用这个很酷的 DLL 来播放声音了。好了,和几乎所有其他事情一样,我们需要一个对象,所以让我们创建一个。

public QuartzTypeLib.FilgraphManagerClass mc;
//quartz object needed to play background

现在我们有了对象,在我们实际让声音播放之前,还需要做几件事。

QuartzTypeLib.FilgraphManager graphManager = 
                       new QuartzTypeLib.FilgraphManager();
            
// QueryInterface for the IMediaControl interface:
            
mc =(QuartzTypeLib.FilgraphManagerClass)graphManager;

我们上面所做的是创建了一个 Graphmanager,用于播放声音。您会注意到,我将 mc 赋值给 graphManager 的值,该值已被转换为 filgraphManagerClass 类型。现在完成了这个,我们就可以开始使用 mc 对象来播放我们的声音了。所以我们必须创建一个调用 soundbackground 来播放我们声音的线程。

backgroundplay = new Thread (new ThreadStart(soundbackground));
// start thread to play music

在调用此线程之前,我们希望它在后台运行。所以,当我们关闭程序时,我们不会出现进程未关闭的情况。所以,要做到这一点,我们在启动线程之前执行以下操作

backgroundplay.IsBackground=true;    // make the thread a background thread
backgroundplay.Start(); //start the music playing thread
void soundbackground()
{
    
    while(true)
    {
                    
        // Call some methods on a COM interface
        // Pass in file to RenderFile method on COM object.
                    
        mc.RenderFile("backmusic.mp3"); // loads the file in
        mc.Run(); // starts the playing
        if (mc.CurrentPosition==mc.Duration) // checks if ended
        {                            
            mc.CurrentPosition=0; // if we have ended we wanna start over
        }
                                    
         // -1 blocks this event infinately and the soundvar is an 
         // eventcode that gets triggered after time out                
         mc.WaitForCompletion(-1, out soundvar); 
    }            
}

图像对象及其边界检查

如果您查看标题为 Images#region 部分,您将看到我创建了一些图像对象。这些是您在屏幕上看到的图像。要测试边界,我们必须将图像制成矩形。查看 #region Bounds,您会看到声明。

public System.Drawing.Rectangle RectangleFrog;
public System.Drawing.Rectangle RectangleCar3;
public System.Drawing.Rectangle RectangleCar2;
public System.Drawing.Rectangle RectangleCar1;
public System.Drawing.Rectangle RectangleFastCar;
public System.Drawing.Rectangle RectangleTractor;
public System.Drawing.Rectangle RectangleCow;

现在我们已经声明了对象,我们需要定义它。所以,我们执行以下操作。您将在 tickme 部分看到这个。我本可以把它放在别处,但我没有。

RectangleCar1 = new Rectangle( Lane1var1, 312, Jeep1_Lane1.Width, 
                               Jeep1_Lane1.Height );
RectangleCar2 = new Rectangle( Lane1var2, 312, Jeep2_Lane1.Width, 
                               Jeep2_Lane1.Height );
RectangleCar3 = new Rectangle( Lane1var3, 312, Jeep2_Lane1.Width, 
                               Jeep2_Lane1.Height );
RectangleFastCar = new Rectangle(Lane2var,350,FastCar.Width,FastCar.Height);
RectangleTractor = new Rectangle(Lane3var,370,Tracter.Width,Tracter.Height);
RectangleCow = new Rectangle(Lane3var2,370,Cow.Width-10,Cow.Height-10);

现在我们已经为我们的对象定义了矩形边界,我们可以测试它们是否与其他对象相交。这很有用,因为,比如说,如果青蛙与汽车相交,那么我们显然希望青蛙死掉。所以 C# 通过允许我们使用名为 IntersectsWith 的函数使这变得容易。如果您查看 tickme 函数,您会看到我在那里检查青蛙是否被原木上的汽车等击中。

if (RectangleFrog.IntersectsWith(RectangleCar2)||
    RectangleFrog.IntersectsWith(RectangleCar1)||
    RectangleFrog.IntersectsWith(RectangleCar3)||
    RectangleFrog.IntersectsWith(RectangleFastCar)|| 
    RectangleFrog.IntersectsWith(RectangleTractor)||
    RectangleFrog.IntersectsWith(RectangleCow))
{
    lives--;
    livesfunction();
}

上面的代码的作用是检查青蛙图像是否碰到汽车、牛或拖拉机之一。如果碰到,那么我们将减少一次生命值,然后调用 lives 函数来查看我们是否生命耗尽。

图形的双缓冲

我不会详细介绍这个,因为我已经写了一篇关于它的文章。请在此处查看 AntiFlicker Graphics 文章。阅读上述文章后,您会看到您必须启用双缓冲,因为它默认是关闭的。此外,您必须创建一个 onpaint 事件,然后从定时器调用 DrawStuff 函数,以使图形无闪烁和延迟地移动。

将分数保存在文本文件中:读取方法

高分是逐行从一个简单的文本文件中读取的。我想过使用数据库,但因为觉得没必要而放弃了这个想法。我还想过将文件设为二进制,这可能在未来的版本中仍然会这样做。是的,我知道您在说别啰嗦了。我想看看这是如何完成的,所以我们开始吧……

try
{
    StreamReader s = File.OpenText("Score.txt");            
            
    line1=s.ReadLine();
    line2=s.ReadLine();
    scorecompare= Convert.ToInt32(line2);
    label4.Text=line1;
    label5.Text=line2;
            
    s.Close();
}
catch
{
    MessageBox.Show("Can't find Score.txt");
}

所以,让我们看看这里发生了什么。首先,我们创建我们的 trycatch,所以如果 score.txt 文件丢失了,那么我们想告诉用户程序崩溃的原因。然后我们确保在顶部有 using System.IO;,所以我们可以使用 StreamReader。所以,我们创建一个 StreamReader 类型的变量,并将其链接到 score.txt 的路径。一旦完成,我们就将 line1 赋值为第一次读取的值,即第一行。然后对 line2 做同样的事情。然后我将包含分数的 line2 转换为整数并存储在 scorecompair 中,以便之后我们可以将高分与当前分数进行比较。然后,我只是将高分名称和高分分数分别放在它们的标签文本中。

将分数保存在文本文件中:写入方法

嗯,您可能想知道我们是如何创建这个 score.txt 文件的?好的,如果您仔细查看代码,您会注意到,如果 scorecompare 小于当前分数,那么当前玩家得分最高,所以我们只需显示一个获取 score 的窗体;记住,我们将其设为 static,以便我们可以使用它?好了,在所有这些都到位后,我们有一个提交按钮,它执行以下操作

private void button1_Click(object sender, System.EventArgs e)
{
    FileInfo f = new FileInfo("Score.txt");
    StreamWriter w = f.CreateText();
    w.WriteLine(textBox1.Text.ToString());
    w.WriteLine(Form1.score.ToString());
    w.Close();
    Close();
}

作者注

Quartz.dll 很有趣。我意识到,有了它,我就可以轻松制作一个媒体播放器。我曾想过让我的狗随着音乐播放而摇尾巴。这个项目对我来说很有趣,也是一次很棒的学习经历。我一直想学习 C# 并制作一款游戏。在 Code Project 成员的帮助下,您知道自己是谁,我能够将这个梦想变为现实。有一件事仍然让我感到困惑是 winmm.dll。我听说过它可以在 IRC 上与个人并行播放两种声音,但我从未见过任何可用的代码。因为,从我的看法来看,它没有阻塞方法,所以当另一个事件触发时,最后调用的事件会优先。另一件事让我抓狂。我想做 DirectX,而且仍然会。不过,我可能需要获取最新版本的 Visual Studio,但是,这里的交易是这样的。我已经尝试了我所知道的每种方法来正确安装 DirectX。它根本不愿意这样做。我遵循了几种方法,比如注册 DLL、复制 DLL 等等。所以,当我稍微不忙了之后,我将卸载 VS 并重新安装所有东西,看看是否有效。还有一件事。我现在是定时器的忠实粉丝。它们让我的生活轻松了很多。哦,我确实学到了一件有趣的事情。如果您包含 using System.Diagnostics;,那么您就可以调用 process.Start("a url");,它会让您的用户给您发电子邮件或访问您的网站或其他任何东西。

摘要

希望在阅读完这篇文章后,您能从中获得一些想法,为您的家人甚至您自己制作一些精彩的游戏。我一直想要老式的 Frogger,当然我本可以买到它,但我想为什么不学习如何制作它呢,所以这就是我的 Frogger 版本。我敢肯定此软件中有错误,并且一旦发现它们就会得到纠正。如果您喜欢此代码,请帮我一个忙。请给我发送电子邮件至 Junkmail4tony@comcast.net,让我知道您有多喜欢它。

© . All rights reserved.