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

Rapid Roll C#

starIconstarIconstarIconstarIconstarIcon

5.00/5 (5投票s)

2019年12月24日

CPOL

7分钟阅读

viewsIcon

14992

downloadIcon

147

C# 中的 Rapid Roll 游戏

引言

我将向您展示一个用C#语言编写的经典诺基亚游戏——Rapid Roll。

游戏玩法

  • 玩家使用A键和左箭头键向左移动红色小球,使用D键和右箭头键向右移动。
  • 小球必须碰到横杆才能继续前进,否则如果它与顶部的电线碰撞、掉出边界或掉到尖刺上,玩家就会失去一条生命。
  • 游戏的目标是获得一定数量的分数才能获胜。

准备背景

在我开始编写代码之前,我修改了窗体(Form),将其BackColor更改为Aqua,大小调整为400x400,将MaximizeBox设置为falseLocked设置为true,并将FormBorderStyle切换为FixedSingle。之后,我添加了一个pictureBox,其中包含电线背景图像,位置为0;0,3个名为"Life1"、"Life2"和"Life3"的小pictureBoxes,一个显示当前分数的标签,以及一个大小为32;32的空pictureBox作为小球,名为"Ball"。

Using the Code

首先,我添加了一些全局变量。常量整数widthheight表示游戏中字段(横杆和尖刺)的宽度和高度,整数值lives存储玩家的生命数量,而savecount将在文章后面解释。Short类型的值score将存储玩家获得的分数,布尔值moveLeftmoveRight将决定玩家是向左还是向右移动。Random类的r实例将在程序的多个过程中使用。

const int width = 90;
const int height = 15;

int save = 0;
int count = 0;
int lives = 3;

short score = 0;

bool moveLeft = false;
bool moveRight = false;

Random r = new Random(); 

我首先需要的是ball。我没有添加图片,而是使用小球pictureBoxPaint事件中的GDI来在控件中绘制小球。该控件略大于其中的图像。

private void DrawBall(object sender, PaintEventArgs e)
{
    Rectangle ball = new Rectangle(0, 0, 30, 30);
    Pen pn = new Pen(Color.Black);
    SolidBrush brush = new SolidBrush(Color.Red);
    e.Graphics.DrawEllipse(pn, ball);
    e.Graphics.FillEllipse(brush, ball);
}

然后,当小球被创建后,我使用Form事件KeyUpKeyDown,分别名为PressedReleased,来为小球创建控制,同时使用一个名为Controller的计时器,其事件名为Controlling,它将根据玩家按下的按键来引导小球。

private void Pressed(object sender, KeyEventArgs e)
{
    if (e.KeyCode == Keys.A || e.KeyCode == Keys.Left)
    {
        moveLeft = true;
    }
    else if (e.KeyCode == Keys.D || e.KeyCode == Keys.Right)
    {
        moveRight = true;
    }
}
private void Released(object sender, KeyEventArgs e)
{
    if (e.KeyCode == Keys.A || e.KeyCode == Keys.Left)
    {
        moveLeft = false;
    }
    else if (e.KeyCode == Keys.D || e.KeyCode == Keys.Right)
    {
        moveRight = false;
    }
}
private void Controlling(object sender, EventArgs e)
{
    if (moveLeft && Ball.Location.X >= 0)
    {
        Ball.Left -= 6;
    }
    else if (moveRight && Ball.Location.X <= ClientSize.Width - Ball.Width)
    {
        Ball.Left += 6;
    }
} 

游戏包含三种类型的字段:Bars(横杆)、Spikes(尖刺)和Layers(图层)。

  • Bars是普通字段,小球必须落到上面才能继续前进。但由于IntersectsWith()方法会在任何时候将小球与横杆连接,所以我们有下一个字段——图层。
  • Layers是薄的pictureBoxes,添加到横杆上方5px处,它们的作用是(大部分)减弱小球对横杆的冲击。
  • Spikes是玩家必须避开的字段,因为与它们碰撞会导致玩家失去生命。

Bar过程将在字段添加到窗体后创建其Layer。它有参数wh用于横杆在屏幕上的随机位置,并添加砖块纹理作为背景图像。

private void Bar(int w, int h)
{
    PictureBox bar = new PictureBox();
    bar.Location = new Point(w, h);
    bar.Size = new Size(width, height);
    bar.BackgroundImage = Properties.Resources.brick;
    bar.BackgroundImageLayout = ImageLayout.Stretch;
    bar.AccessibleName = "Field";
    bar.Name = "Bar";
    this.Controls.Add(bar);
    Layer(bar);
}

Layer过程包含参数b,以便将其放置在与其横杆相同的X位置,并比横杆高出5px。

private void Layer(PictureBox b)
{
    PictureBox layer = new PictureBox();
    layer.Location = new Point(b.Location.X, b.Location.Y - 5);
    layer.Size = new Size(width, 1);
    layer.AccessibleName = "Field";
    layer.Name = "Layer";
    this.Controls.Add(layer);
}

Bar类似,Spikes过程包含wh参数,用于在屏幕上的随机widthheight位置。

private void Spike(int w, int h)
{
    PictureBox spike = new PictureBox();
    spike.Location = new Point(w, h);
    spike.Size = new Size(width, height);
    spike.BackgroundImage = Properties.Resources.spikes;
    spike.BackgroundImageLayout = ImageLayout.Stretch;
    spike.AccessibleName = "Field";
    spike.Name = "Spike";
    this.Controls.Add(spike);
}

现在我们有了带有控制的小球,创建横杆、图层和尖刺的过程,但没有界面。我没有手动添加pictureBoxes,而是编写了两个独立的过程——Interface,它将重复创建pictureBoxes(横杆和图层)直到屏幕上的某个点,以及SetBall,它将把小球重新定位到创建的最低横杆上方5px处。

SetBall:

private void SetBall()
{
 foreach (Control c in this.Controls)
 {
     if (c is PictureBox && c.Name == "Layer")
     {
         PictureBox layer = (PictureBox)c;
         Ball.Location = new Point(layer.Location.X, layer.Location.Y - Ball.Height - 5);
     }
 }
}

Interface将持续创建横杆,直到横杆的位置小于(大约)屏幕高度减去球的高度。尽管如此,我仍然出于逻辑原因使用了条件**break**。

private void Interface()
{
    int x, c, y = 0;

    while (y < ClientSize.Height - Ball.Height)
    {
        x = r.Next(0, ClientSize.Width - width);
        c = r.Next(60, 120);
        y += c;
        if (y > ClientSize.Height - Ball.Height) break;
        Bar(x, y);
    }

    SetBall();
}

屏幕现在应该看起来像这样

小球似乎略高于横杆,但小球和横杆之间的间隙在游戏过程中会减小。

现在我们来讨论横杆(带图层)和尖刺是如何创建的。我使用了两个计时器——GenerateDetectDetect计时器有一个名为Detected的事件。

private void Generator(object sender, EventArgs e)
{
    if (save == 0)
    {
        int pick = r.Next(1, 5) + 1;
        save = pick;
    }

    count++;

    if (count == save)
    {
        Spike(r.Next(ClientSize.Width - width), ClientSize.Height);
        count = 0; save = 0;
    }
    else
    {
        Bar(r.Next(ClientSize.Width - width), ClientSize.Height);
    }
}

这个计时器使用上面提到的整数值savecountSave保存一个随机值,而count在计时器每次触发时递增。所以假设save的随机数是4。当计时器触发时,count将增加,并检查其值是否等于save的值。如果不是,则创建横杆。否则,将创建尖刺,并且countsave的值都将设置为**0**,然后循环一遍又一遍地运行。所以此时,计时器将创建**3**个横杆和**1**个尖刺。计时器间隔设置为**350ms**。

Detect计时器包含一个名为Detector的事件,并遍历控件,搜索AccessibleName属性设置为"Field"的pictureBoxes。我忘了提到,我将该属性添加到了所有字段——横杆、图层和尖刺中,以简化代码。一旦找到它们,它们就会被向上发射,如果它们与顶部的电线碰撞,它们将被移除。这减少了程序的内存使用并提高了性能。

private void Detector(object sender, EventArgs e)
{
    foreach (Control c in this.Controls)
    {
        if (c is PictureBox && c.AccessibleName == "Field")
        {
            PictureBox field = (PictureBox)c;
            field.Top -= 3;

            if (field.Bounds.IntersectsWith(Wire.Bounds))
            {
                this.Controls.Remove(field);
            }
        }
    }
}

当球下落时,当它与横杆相交时,它应该停留在上面并向上移动,但这部分可能并不那么容易。正如我上面某处提到的,单纯的IntersectsWith()只要一个控件与另一个控件碰撞(无论在哪里),就会返回true。这是我在横杆上方创建图层的另一个原因。但是现在,我们需要找到一个安全范围,在这个范围内球可以与图层碰撞并停留在上面,而不会掉落。经过测试,我想到了计算图层的Y位置和球的Y位置减去一个“安全数字”的差值,这将确保球停留在图层上并同时相交。

private bool Settled(PictureBox surface)
{
    return surface.Location.Y - Ball.Location.Y >= Ball.Height - 6;
}

现在让我们转向Observer

private void Observer(object sender, EventArgs e)
{
    int top = 3;

    if (Ball.Bounds.IntersectsWith(Wire.Bounds) || Ball.Location.Y >= ClientSize.Height)
    {
        checkStatus();
        return;
    }
    foreach (Control c in this.Controls)
    {
        if (c is PictureBox && c.Name == "Layer")
        {
            PictureBox layer = (PictureBox)c; 

            if (Ball.Bounds.IntersectsWith(layer.Bounds) && Settled(layer))
            {
                top = -3; 
            }
        }
        else if (c is PictureBox && c.Name == "Spike")
        {
            PictureBox spike = (PictureBox)c; 

            if (Ball.Bounds.IntersectsWith(spike.Bounds))
            {
                checkStatus();
                return;
            }
        }
    }         
    MoveBall(top); 
    WriteScore(top); 
    CheckForWinner();
}

首先,我们有一个局部变量top,它被设置为3。在我们循环遍历控件之前,我们将检查小球是否与顶部的电线碰撞,在这种情况下,玩家会失去一条生命。之后,我们寻找名为"Layer"和"Spike"的pictureBox控件。如果找到layer,程序将检查小球是否在特定范围内与它碰撞,如果碰撞了,top将乘以-1,这意味着它将改变方向并开始向上移动。如果找到spike,程序会检查小球是否与它碰撞,如果碰撞了,玩家会失去生命,如果他没有更多生命,游戏就会结束。循环结束后,它使用top变量的值来确定小球应该去哪里,如果玩家向下移动(如果小球站在横杆/图层上,分数不会增加),则为用户添加score,并检查玩家是否通过达到一定分数而获胜。

MoveBall:

private void MoveBall(int num)
{
    Ball.Location = new Point(Ball.Location.X, Ball.Location.Y + num);
}

WriteScore:

private void WriteScore(int value)
{
    if (value > 0)
    {
        score++;
        label1.Text = "Score: " + score.ToString();
    }
}

CheckForWinner:

private void CheckForWinner()
{
    if (score == short.MaxValue)
    {
        DisposeTimers();
        MessageBox.Show("Congratulations, you won!");
    }
}

如果玩家达到短数据类型的最大值,即32767,则赢得游戏,但您可以随意添加您想要的值。

还有两个最终过程,checkStatusStopTimers。当球与顶部的电线碰撞,掉出边界或掉到尖刺上时,会调用checkStatus。它将递减lives值,退出循环并检查lives值是否为0,这意味着游戏结束。如果玩家没有更多生命,将调用StopTimers,并出现游戏结束消息,以及玩家获得的分数。否则,将调用SetBall过程,以便球将重新定位到另一个活动横杆上,游戏继续进行。

private void checkStatus()
{
    foreach (Control c in this.Controls)
    {
        if (c is PictureBox && c.Name.Contains("Life"))
        {
            this.Controls.Remove(c);
            lives--;
            break;
        }
    }

    if (lives == 0)
    {
        DisposeTimers();
        MessageBox.Show("Game Over!" + "\n"
                        + "Score: " + score.ToString()
                       );
    }
    else
    {
        SetBall();
    }
}

最后,无论玩家赢了还是输了,所有计时器都将被释放。

private void DisposeTimers()
{
    Controller.Dispose();
    Generate.Dispose();
    Detect.Dispose();
    Observe.Dispose();
}

注意

Settled 函数相当敏感,所以我建议您,如果您在 ObserverDetected 事件中将 top 值更改为例如 2,那么将 Settled 中的值 6 更改为 5。为了更好地调整,您可能还需要将小球的移动速度从 6 更改为 45

历史

  • 2019年12月24日:初始版本
© . All rights reserved.