Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / game

Rapid Roll C#

5.00/5 (5 votes)
24 Dec 2019CPOL7 min read 14.3K  
Rapid Roll game in C#

Introduction

I'm presenting you an old Nokia game called Rapid Roll in C# language.

How the Game Works

  • Player moves the red ball using A and Left arrow keys for left,and D and Right arrow keys for right.
  • The ball must hit the bar in order to move on, otherwise if it collides with wire on top, falls out of bounds or falls on the spike, player loses life.
  • The goal of the game is to score a certain amount of points in order to win.

Preparing the Background

Before I started writing the code, I modified the Form by changing its BackColor to Aqua, resizing it to 400x400, setting MaximizeBox to false, Locked to true and switching FormBorderStyle to FixedSingle. After that, I added a pictureBox with the wire background image at position 0;0, 3 small pictureBoxes named "Life1","Life2" and "Life3", a label that presents the current score, and an empty pictureBox of size 32;32 for ball, named "Ball":

Using the Code

For starters, I added some global variables. Constant integers width and height presents the width and height of the fields in game (bars and spikes), integer value lives stores the number of lives of the player, while save and count will be explained later in article. Short type value score will store the amount of points player achieved, and bools moveLeft and moveRight will determine if the player moves left or right. The r instance of Random class will be used in multiple procedures in the program:

C#
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(); 

The first thing I needed was the ball. Instead of adding a picture, I used GDI in Paint event of the ball pictureBox to draw the ball in the control. The control is slightly larger than the image within:

C#
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);
}

Then, when the ball is created, I used Form events KeyUp and KeyDown called Pressed and Released to make controls for the ball, alongside timer named Controller with event called Controlling that will direct the ball based on the keys that player pressed:

C#
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;
    }
} 

The game contains three type of fields: Bars, Spikes and Layers.

  • Bars are regular fields where the ball shall fall in order to move on. But due to the fact that the IntersectsWith() method will connect the ball with the bar at any point, we have the next field - layer.
  • Layers are thin pictureBoxes added 5px above the bars and their use is to diminish (for the most part) the impact of the ball on the bar.
  • Spikes are fields that shall be avoided by the player because colliding with them will cause player to lose life.

Bar procedure will create its Layer after the field is added on the form. It has arguments w and h reserved for the random position of the bar on the screen, and adds the brick looking texture as a background image:

C#
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 procedure holds argument b to get placed at the same X position as its bar, and 5px above it:

C#
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);
}

Just like Bar, Spikes procedure holds w and h arguments for random width and height position on the screen:

C#
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);
}

Now we have the ball with its controls, procedures for creating bars, layers and spikes, but no interface. Instead of adding pictureBoxes manually, I wrote two separated procedures - Interface which will create pictureBoxes (bars and layers) repeatedly to the certain point of the screen, and SetBall that will relocate the ball to the lowest bar created, 5px above the bar.

SetBall:

C#
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 will keep creating the bars until the position of the bar is lesser than the (about) screen height minus ball height. Still, I had to use conditional break for logical reasons:

C#
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();
}

The screen should now look like this:

The ball seems to be slightly above the bar, but the space between ball and bar diminishes during the game.

Now we came to the part of how the bars (with layers) and spikes are created. I used two timers - Generate and Detect. Detect timer holds event called Detected:

C#
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);
    }
}

This timer uses the above mentioned integer values save and count. Save holds a random value, while count increments every time timer fires. So let's say that the random number of save was 4. When the timer is fired, count will increase, and check if its value is equal to the value of save. If it's not, the bar is created. Otherwise,the spike will be created, and both values count and save will be set to 0 and the cycle runs over and over. So at that point, timer will create 3 bars and 1 spike. The timer interval is set to 350ms.

Detect timers holds event called Detector and loops through the controls searching for pictureBoxes with AccessibleName property set to "Field". I forgot to mention that I added that property to all fields - bars, layers and spikes in order to simplify the code. Once they're found, they've been launched above, and if they collided with the wire at the top, they will be removed. That decreases memory usage of the program and enhances performance:

C#
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);
            }
        }
    }
}

As the ball falls, when it intersects with bar, it shall stay on it and move above, but that part might not be that easy. As I mentioned somewhere above, the bare IntersectsWith() will return true whenever (and wherever) one control collides with another control. That's another reason why I created layers above the bars. But now, we need to find a safe range of where the ball can collide with layer and stay on it, without falling through. After testing, I came to the idea to calculate the difference between Y position of the layer and Y position of the ball minus "safe number" which will make sure that the ball stays on the layer and intersects at the same time:

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

Let's move to the Observer now:

C#
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();
}

First, we have local variable top that is set to 3. Before we approach looping through the controls, we will check if the ball collided with wire on the top in which case, player loses life. After it, we're looking for pictureBox controls with names "Layer" and "Spike". If layer is found, the program will check if the ball collided with it at specific range, and if it did, top gets multiplied by -1 meaning that it will change direction and will start moving up. If the spike is found, program checks if the ball collided with it, and if it did, player loses life, or ends the game if he has no more lives. Once the loop is done, it uses value of top variable to determine where the ball should go, adds a score to the user if he moves down (if the ball is standing on the bar\layer, the score won't increase) and checking if player has won by reaching a certain amount of points.

MoveBall:

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

WriteScore:

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

CheckForWinner:

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

Player wins the game if he reaches maximal value of short data type which is 32767, but feel free to add a number by your will.

There are two final procedures, checkStatus and StopTimers. checkStatus is called when the ball collided with the wire on top, fell out of the bounds on fell on the spike. It will decrement value lives, exit from the loop and check if the lives value is 0 meaning that the game is over. If the player has no more lives, StopTimers will be called, and the message of game over will appear, along with the score that player achieved. Otherwise, the SetBall procedure is called so the ball will be relocated to the another active bar and the game keeps on:

C#
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();
    }
}

And at the end, whether the player won or lost, all timers will be disposed:

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

Note

The Settled function is quite sensitive, so I would suggest you that if you're changing the top value in Observer and Detected events for example to 2, then change the value 6 from Settled to 5. You might also want to change the movement speed of the ball from 6 to 4 or 5 for the sake of better adjustment.

History

  • 24th December, 2019: Initial version

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)