Rapid Roll C#





5.00/5 (5投票s)
C# 中的 Rapid Roll 游戏
引言
我将向您展示一个用C#语言编写的经典诺基亚游戏——Rapid Roll。
游戏玩法
- 玩家使用A键和左箭头键向左移动红色小球,使用D键和右箭头键向右移动。
- 小球必须碰到横杆才能继续前进,否则如果它与顶部的电线碰撞、掉出边界或掉到尖刺上,玩家就会失去一条生命。
- 游戏的目标是获得一定数量的分数才能获胜。
准备背景
在我开始编写代码之前,我修改了窗体(Form),将其BackColor更改为Aqua
,大小调整为400x400,将MaximizeBox设置为false
,Locked设置为true
,并将FormBorderStyle切换为FixedSingle
。之后,我添加了一个pictureBox
,其中包含电线背景图像,位置为0;0,3个名为"Life1
"、"Life2
"和"Life3
"的小pictureBoxes
,一个显示当前分数的标签,以及一个大小为32;32的空pictureBox
作为小球,名为"Ball
"。
Using the Code
首先,我添加了一些全局变量。常量整数width
和height
表示游戏中字段(横杆和尖刺)的宽度和高度,整数值lives
存储玩家的生命数量,而save
和count
将在文章后面解释。Short
类型的值score
将存储玩家获得的分数,布尔值moveLeft
和moveRight
将决定玩家是向左还是向右移动。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
。我没有添加图片,而是使用小球pictureBox
的Paint
事件中的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
事件KeyUp
和KeyDown
,分别名为Pressed
和Released
,来为小球创建控制,同时使用一个名为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
。它有参数w
和h
用于横杆在屏幕上的随机位置,并添加砖块纹理作为背景图像。
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
过程包含w
和h
参数,用于在屏幕上的随机width
和height
位置。
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();
}
屏幕现在应该看起来像这样
小球似乎略高于横杆,但小球和横杆之间的间隙在游戏过程中会减小。
现在我们来讨论横杆(带图层)和尖刺是如何创建的。我使用了两个计时器——Generate
和Detect
。Detect
计时器有一个名为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);
}
}
这个计时器使用上面提到的整数值save
和count
。Save
保存一个随机值,而count
在计时器每次触发时递增。所以假设save
的随机数是4
。当计时器触发时,count
将增加,并检查其值是否等于save
的值。如果不是,则创建横杆。否则,将创建尖刺,并且count
和save
的值都将设置为**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
,则赢得游戏,但您可以随意添加您想要的值。
还有两个最终过程,checkStatus
和StopTimers
。当球与顶部的电线碰撞,掉出边界或掉到尖刺上时,会调用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
函数相当敏感,所以我建议您,如果您在 Observer
和 Detected
事件中将 top
值更改为例如 2,那么将 Settled
中的值 6 更改为 5。为了更好地调整,您可能还需要将小球的移动速度从 6 更改为 4 或 5。
历史
- 2019年12月24日:初始版本