Click here to Skip to main content
15,881,173 members
Articles / Programming Languages / C#

Space Invaders in C# WinForm

Rate me:
Please Sign up or sign in to vote.
4.61/5 (17 votes)
5 Dec 2019CPOL7 min read 28.6K   1.8K   23   9
Simple Space Invaders

Introduction

Simple Space Invaders game made in C# WinForm. The sprites are being taken from:

How the Game Works

  • Player moves left using the A and Left arrow buttons, or right using the D and Right arrow buttons or space to fire toward aliens.
  • Aliens are moving from right to left and vice-versa lowering down when they hit the edge, firing at player while they are moving.
  • Player wins if he eliminate all the aliens before they reach him and loses if he gets hit 3 times by a laser, or gets in collision with some of aliens meaning that he failed to kill them in time.

Using the Code

First, we're going to add a Player moving controls. In the Form Event grid, I used KeyDown and KeyUp events called Pressed and Released. There are also 3 global boolean values called moveRight and moveLeft which are going to be set to true depending on which buttons the 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;
    }
    else if (e.KeyCode == Keys.Space && game && !fired)
    {
        Missile();
        fired = 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;
    }
    else if (e.KeyCode == Keys.Space)
    {
        fired = false;
    }
}
private void PlayerMove(object sender, EventArgs e)
{
    if (moveLeft && Player.Location.X >= 0)
    {
        Player.Left--;
    }
    else if (moveRight && Player.Location.X <= limit)
    {
        Player.Left++;
    }
}

If the player pressed space to fire, the value fired is going to get set to true to prevent continuous firing when the button is pressed, and there is additional check if the value game is set to true checking if the game is still active. If everything is ok, the procedure which creates bullet sprite called Missile() is called:

C#
private void Missile()
{
 PictureBox bullet = new PictureBox();
 bullet.Location = new Point(Player.Location.X + Player.Width / 2, Player.Location.Y - 20);
 bullet.Size = new Size(5, 20);
 bullet.BackgroundImage = Properties.Resources.bullet;
 bullet.BackgroundImageLayout = ImageLayout.Stretch;
 bullet.Name = "Bullet";
 this.Controls.Add(bullet);
}

Now let's add some aliens to the form. For the purpose of better readability, I created a small class called Enemies which sets alien sprite parameters (size, images, quantity):

  • Width and height presents the size of the alien sprite (40)
  • Rows and columns are the total number of aliens in order 5x10
  • X and Y presents the starting coordinates of the sprites in form, and the space is the distance between them
C#
class Enemies
{
    private int width, height;
    private int columns, rows;
    private int x, y, space; 

    public Enemies()
    {
        width = 40;
        height = 40;
        columns = 10;
        rows = 5;
        space = 10;
        x = 150;
        y = 0; 
    }
    private void CreateControl(Form p)
    {
        PictureBox pb = new PictureBox();
        pb.Location = new Point(x, y);
        pb.Size = new Size(width, height);
        pb.BackgroundImage = Properties.Resources.invader;
        pb.BackgroundImageLayout = ImageLayout.Stretch;
        pb.Name = "Alien";
        p.Controls.Add(pb); 
    }
    public void CreateSprites(Form p)
    {
        for(int i = 0; i < rows; i++)
        {
          for(int j = 0; j < columns; j++)
          {
              CreateControl(p);
              x += width + space; 
          }
              y += height + space;
              x = 150; 
        }
    }
}

To add them once the program is started, we need to create a class within the Form constructor:

C#
public Form1()
{
    InitializeComponent();
    new Enemies().CreateSprites(this);
    InsertAliens();
}   

And when this is done and you start the program, it should appear like this:

Image 1

Now we're moving to the alien movement part. But before that, I'm going to isolate all the sprites (pictureBox-es) named "Alien" in one public list called aliens:

C#
private void InsertAliens()
{
    foreach(Control c in this.Controls)
    {
        if (c is PictureBox && c.Name == "Alien")
        {
            PictureBox alien = (PictureBox)c;
            aliens.Add(alien); 
         }
    }
}

And we can go further now. As the aliens are moving from right to left and vice versa, when they touch one of the edges of the screen, they will go down and change direction so I had to create a certain logic for that purpose. First, I'm going to make a boolean function named Touched to check if the aliens touched the edge of the screen:

C#
private bool Touched(PictureBox a) 
{
    return a.Location.X <= 0 || a.Location.X >= limit; 
}

And then the SetDirection procedure. The values I'm using here are top and left for the direction of the aliens, cnt that counts how low aliens went, and speed which is going to switch the direction of the aliens at a certain point.

First, it will check if one of the aliens touched the edge of the screen, and if it did, the variable cnt will increment. When the cnt reaches the height size of a single alien, it will stop going down and change direction. The same thing will happen when the cnt reaches double size of the previous value meaning that the aliens collided with the edge from the other side and the direction will be changed again. After that, the cnt value will be set to 0 in order to do the same procedure in the next row:

C#
private void SetDirection(PictureBox a)
{
    int size = a.Height;

    if (Touched(a))
    {
        top = 1; left = 0; cnt++;

        if (cnt == size)
        {
            top = 0; left = speed * (-1); Observer.Start();
        }
        else if (cnt == size * 2)
        {
            top = 0; left = speed; cnt = 0; Observer.Start();
        }
    }
}

The function of the timer called Observer will be explained later.

Now I will post the code for procedure called AlienMoves which loops through the list of aliens (called aliens) and moves the sprites by the coordinates that are being set in the SetDirection procedure. The AlienMoves procedure also contains another procedure which checks if the aliens collided with player in which case the game is over:

C#
private void AlienMove()
{            
    foreach(PictureBox alien in aliens)
    {
        alien.Location = new Point(alien.Location.X + left, alien.Location.Y + top);
        SetDirection(alien);
        Collided(alien);                
    }
}

Collided procedure:

C#
private void Collided(PictureBox a)
{
    if (a.Bounds.IntersectsWith(Player.Bounds))
    {
        gameOver();
    }
}

And the timer MoveAliens that calls the AlienMoves procedure to run things:

C#
private void MoveAliens(object sender, EventArgs e)
{
    AlienMove();
}

Now we arrived at the part of the aliens firing towards player. Just like in case of player, first we need to write a procedure which creates a sprite of the laser. It's called Beam:

C#
private void Beam(PictureBox a)
{
 PictureBox laser = new PictureBox();
 laser.Location = new Point(a.Location.X + a.Width / 3, a.Location.Y + 20);
 laser.Size = new Size(5, 20);
 laser.BackgroundImage = Properties.Resources.laser;
 laser.BackgroundImageLayout = ImageLayout.Stretch;
 laser.Name = "Laser";
 this.Controls.Add(laser);
}

Before I post the code, I will explain how the firing works. We have two timers where one presents the time span at which the lasers are going to be fired called StrikeSpan and its time interval is set to 1000 (1s) meaning that after each second, a laser is going to be fired. The other one called DetectLaser finds the laser that is created and hurls it down the ground by setting its Top property to 5. That timer is set to interval of 1ms.

StrikeSpan:

C#
private void StrikeSpan(object sender, EventArgs e)
{
    Random r = new Random();
    int pick; 

    if (aliens.Count > 0)
    {
        pick = r.Next(aliens.Count);
        Beam(aliens[pick]);
    }
}

DetectLaser:

C#
private void DetectLaser(object sender, EventArgs e)
{
    foreach(Control c in this.Controls)
    {
        if (c is PictureBox && c.Name == "Laser")
        {
            PictureBox laser = (PictureBox)c;
            laser.Top += 5; 

            if (laser.Location.Y >= limit)
            {
                this.Controls.Remove(laser); 
            }
            if (laser.Bounds.IntersectsWith(Player.Bounds))
            {
                this.Controls.Remove(laser); 
                LoseLife(); 
            }                    
         }
     }
}  

As you can see, the code checks if the laser left the field in which case it's going to be removed, or if laser collided with player when the player loses life.

That was a part of the aliens striking. Now it's time for the player.

We have a single timer called FireBullet, but things are a bit more complicated as we have to perform multiple different checks. I'll separate the one important part:

C#
if (bullet.Bounds.IntersectsWith(alien.Bounds) && !Touched(alien))
{
    this.Controls.Remove(bullet);
    this.Controls.Remove(alien);
    aliens.Remove(alien);
    pts += 5;
    Score(pts);
    CheckForWinner();
}
else if (bullet.Bounds.IntersectsWith(alien.Bounds) && Touched(alien))
{
    this.Controls.Remove(bullet);
    this.Controls.Remove(alien);
    delay.Add(alien);
    pts += 5;
    Score(pts);
    CheckForWinner();
}

While testing the code, I figured out that when the aliens touch the edge of the screen and I destroy them, the condition from procedure SetDirection returns false and the procedure fails to switch direction properly, as there are no more aliens touching the edge, so they would just keep moving down which presents the logical issue. I solved that problem by creating an additional global list called delay and a timer called Observer, so when I destroy aliens when they touched the edge, the bullet and alien controls are being removed, and instead of deleting the picture from the list aliens (from which they are moving on the screen), I add all destroyed pictures to delay list first so they still exist in aliens list to be able to switch direction. Once they switched direction, I call Observer timer which then removes all the aliens from the aliens list and clear the delay list. So when they reach the edge of the same side I destroyed the aliens from before, there won't be an empty space between aliens and the screen, and the sprites will change the direction correctly. Timer also checks if the bullet left the screen in which case it gets removed, or if it collided with some of the lasers. The full code is as given below:

C#
private void FireBullet(object sender, EventArgs e)
{
    foreach (Control c in this.Controls)
    {
        if (c is PictureBox && c.Name == "Bullet")
        {
            PictureBox bullet = (PictureBox)c;
            bullet.Top -= 5;

            if (bullet.Location.Y <= 0)
            {
                this.Controls.Remove(bullet); 
            }
            foreach(Control ct in this.Controls)
            {
                if (ct is PictureBox && ct.Name == "Laser")
                {
                    PictureBox laser = (PictureBox)ct;

                    if (bullet.Bounds.IntersectsWith(laser.Bounds))
                    {
                        this.Controls.Remove(bullet);
                        this.Controls.Remove(laser);
                        pts++;
                        Score(pts);
                    }
                 }
             }
             foreach(Control ctrl in this.Controls)
             {
                 if (ctrl is PictureBox && ctrl.Name == "Alien")
                 {
                      PictureBox alien = (PictureBox)ctrl;

                      if (bullet.Bounds.IntersectsWith(alien.Bounds) && !Touched(alien))
                      {
                          this.Controls.Remove(bullet);
                          this.Controls.Remove(alien);
                          aliens.Remove(alien);
                          pts += 5;
                          Score(pts);
                          CheckForWinner();
                      }
                      else if (bullet.Bounds.IntersectsWith(alien.Bounds) && Touched(alien))
                      {
                          this.Controls.Remove(bullet);
                          this.Controls.Remove(alien);
                          delay.Add(alien);
                          pts += 5;
                          Score(pts);
                          CheckForWinner();
                      }
                  }
              }
        }
    }
}

Observer timer:

C#
private void Observe(object sender, EventArgs e)
{
    Observer.Stop();

    foreach (PictureBox delayed in delay)
    {
        aliens.Remove(delayed);
    }
    delay.Clear();
}

And for the end, I left procedures which check whether the player has won, lost life, if the game is over and add a score in certain situations.

If the bullet collides with a laser, the player gets 1 point, and if the alien is destroyed, he gets 5. To write the result:

C#
private void Score(int pts)
{
    label2.Text = "Score: " + pts.ToString();
}

When the player gets hit by a laser, he loses life, and at that point, the small picture of tank in the lower left screen gets removed and the player is centered at his starting position in the form. It also checks if the game is over:

C#
private void LoseLife()
{
    Player.Location = new Point(x, y);

    foreach(Control c in this.Controls)
    {
        if (c is PictureBox && c.Name.Contains("Life") && c.Visible == true)
        {
            PictureBox player = (PictureBox)c;
            player.Visible = false;
            return;
        }
    }
    gameOver(); 
}

And if the game is over, we stop all of the timers, loop through the form finding the label with the name "Finished". Once it's been found, we're writing the text "Game Over" and all the other controls visibility is set to false:

C#
private void gameOver()
{
 timer1.Stop(); timer2.Stop(); timer3.Stop(); timer4.Stop(); timer5.Stop(); Observer.Stop();

 foreach (Control c in this.Controls)
 {
     if (c is Label && c.Name == "Finish")
     {
         Label lbl = (Label)c;
         lbl.Text = "Game Over!";
         game = false;
     }
     else
     {
         c.Visible = false;
     }
 }
}

Finding a winner contains two small procedures. The first one called CheckForWinner() counts the number of the pictureBox-es with the name "Alien". if the count is 0, it calls YouWon() procedures that again finds the control named "Finish" writing the text "You Won" and presenting the score player achieved. It also sets game value to false preventing the player from creating sprites of the bullet when the space is hit:

C#
private void CheckForWinner()
{
    int count = 0; 

    foreach(Control c in this.Controls)
    {
        if (c is PictureBox && c.Name == "Alien") count++; 
    }

    if (count == 0) YouWon(); 
}

YouWon procedure:

C#
private void YouWon()
{
    game = false; 

    foreach(Control c in this.Controls)
    {
        if (c is Label && c.Name == "Finish")
        {
            Label lbl = (Label)c;
            lbl.Text = "You Won!" + "\n"
                       + "Score: " + pts.ToString(); 
        }
        else
        {
            c.Visible = false; 
        }
    }
}

Points of Interest

Next time, there could be a lot of potential updates to the game.

History

  • 6th December, 2019: Initial version

License

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


Written By
Employed (other) G4S
Serbia Serbia
My name is Aleksandar,I'm 27. I'm currently working in G4S secure solutions. I went to college,studied IT but quit after short time. Still my love for programming remained. Now I'm writing small-trivial projects in C# and posting them as a Tips.

Comments and Discussions

 
Questiongreat game! Pin
Southmountain17-Dec-22 13:14
Southmountain17-Dec-22 13:14 
GeneralWay too many casts Pin
Paulo Zemek4-Jan-20 23:15
mvaPaulo Zemek4-Jan-20 23:15 
GeneralRe: Way too many casts Pin
NickNs19915-Jan-20 12:09
NickNs19915-Jan-20 12:09 
GeneralRe: Way too many casts Pin
Paulo Zemek5-Jan-20 12:19
mvaPaulo Zemek5-Jan-20 12:19 
The answer for this question can be quite complex... so, for this particular case, just avoiding the casts and putting the objects in appropriate lists should do the job.

But if we are to talk about "system design" you probably will want to have different classes for the different elements of the game (for example the PlayerCharacter, the Shot and the Alien class)... and if needed, these classes would create the atual controls (it is possible to do something similar to WPF's DataTemplates even in windows forms, but I wouldn't go that far).

The thing is, each class would know how to create the appropriate control, and how to deal with its animation... and then you could have lists of the appropriate types (a List<Alien> or List<Shot>).

Edit: Also, you shouldn't create a new Random instance at every StrikeSpan... you should create only one and reuse it all the time. Random is only "random" enough if you keep calling next in the same instance. If you create 10 Random instances at the same millisecond they are probably all going to return the same values (they will all have the same "seed").

modified 5-Jan-20 18:32pm.

PraiseThank you for posting Space Invaders Pin
Garry Lowther9-Dec-19 4:39
Garry Lowther9-Dec-19 4:39 
PraiseAwesomeness! Pin
Ravi Bhavnani6-Dec-19 10:46
professionalRavi Bhavnani6-Dec-19 10:46 
Questionmy internet security detected invader.exe as trojin horse Pin
Southmountain6-Dec-19 7:47
Southmountain6-Dec-19 7:47 
AnswerRe: my internet security detected invader.exe as trojin horse Pin
NickNs19917-Dec-19 13:08
NickNs19917-Dec-19 13:08 
PraiseHow adorable! You get my 5 vote Pin
honey the codewitch6-Dec-19 2:28
mvahoney the codewitch6-Dec-19 2:28 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.