Share via


VB.NET: Making a Space Invaders game using a DataTable and DataGridView

Using a DataGridView (dgv) control alone in a project to both store and show data on the screen is not really good practice. One should instead use a DataTable (dt) to store and manipulate the data and then show the data on the screen with a dgv. This is especially true if you need multiple tables.

 

Here is a simple example that uses a dt to store and manipulate data. Then the data is displayed on the computer screen using a dgv. All the dgv does in this example is show the data to the user on the computer screen. Everything else is done using the dt.

All you have to do to show dt information is set the dgv DataSource to the dt and whaalla, the data is shown in the grid. The dgv rows and columns are all defined automatically to match the dt:

    dgv1.DataSource = Board

The example code for the "Train Robbers" Space Invaders game was designed to show just how easy it is to use a dt and dgv together.

How the Game Works Most of you are familiar with the old Space Invaders game. It basically consists of a game board that is just a  grid of squares where characters that represent the Invaders Space Ships are moved back and forth while the user fires missiles at the ships. 

With a little creative thinking one can see the dgv is a perfect surface for our game board. After all, a dgv is just a grid of squares exactly like we need. Then if we use a DataTable to hold and manipulate the data for the squares on our board grid we can easily move the game pieces on the board and show the data in the dgv. After all, a dt is just another grid of data squares in memory made of the dt rows and columns.

It is not hard to visualize that the way the game program works is to move all the existing rows of space ship characters down one row as the characters move back and forth left and right on the screen. All the programmer must do is remove a row from the bottom of the dt and add a new row to the top of the dt with each loop of the characters from left to right. First we create DataTables to hold the positions of the characters and then move the characters within the dt with each game "tick" of the Timer.

In our example we have separate DataTables for the ship positions, the missile positions, and the game board. The game board is filled with the ships and missile data and then shown on the screen using the dgv. 

The game tables are first declared at the form class level so they are available to all the sub routines in the form:

Private Ships As New  DataTable("Ships")
Private Missiles As New  DataTable("Missiles")
Private Board As New  DataTable("Board")

  

Next we define the Gridrows and Gridcols variables for the size of the game tables at class level. In our example we use 25 rows and columns.

In the program code we first fill the game DataTables with the CreateBoardTable sub routine in the form load event. By calling the routine with the dt name we define the size of the dt using the global variables gridrows and gridcols. Each cell in the table is assigned the string character "space" for the initial value. We initialize the Ship, Missile, and Board DataTables this way.

In CreateBoardTable we initialize all the game tables to be the same size by creating a for loop to iterate from 0 to GridCols and add columns to the dt:

'create the datatable rows and cols, set all cells to empty
For c = 0 To GridCols - 1
    dt.Columns.Add(c.ToString)
Next

Once the columns are setup we create the rows in the table using another for loop. A data row compatible with the dt is made by declaring a DataRow (dr) and setting it equal to a dt row using NewRow:

Dim dr As DataRow
For r = 0 To GridRows - 1
    dr = dt.NewRow
    For c = 0 To GridCols - 1
        dr(c) = SymbolSpace
    Next
    dt.Rows.Add(dr)
Next

For each iteration of the for loop we create a new dt row named dr and fill the columns with SymbolSpace (a text string space character " "). Then we add the new row to the dt.

After creating the game tables filled with empty spaces we need to add the Space Ship characters using the sub routine MakeShipFleet which inserts the SymbolShip character (an "X") into the Ships dt in the formation of a fleet of ships.

Once the game tables have been initialized we start the game by starting the timer1 in the form load event. Then, in the timer tick event, we move the ships left and right:

              'update the fleet position by moving left or right
            For r = 0 To GridRows - 1
                If direction = 1 Then
                    'shift all GridCols to right
                    For c = GridCols - 2 To 1 Step -1
                        CheckCollision(r, c)
                        Ships.Rows(r)(c + 1) = Ships.Rows(r)(c)
                        Ships.Rows(r)(c) = SymbolSpace
                    Next
                Else
                    'shift all GridCols to left
                    For c = 2 To GridCols - 1
                        CheckCollision(r, c)
                        Ships.Rows(r)(c - 1) = Ships.Rows(r)(c)
                        Ships.Rows(r)(c) = SymbolSpace
                    Next
                End If
            Next

If the ship fleet has reached the left or right edge of the game board grid we reverse the ship direction by changing the sign of the direction variable and move all the rows in the Ship table down one row using the UpdateShips sub routine. The UpdateShips routine also deletes one row from the bottom of the board table and adds a new row to the top of the table using InsertAt. In this way we achieve the game movement from top to bottom of the screen:

Dim dr As Data.DataRow
FleetRowCount += 1
 
'move ships down one row
Ships.Rows(GridRows - 1).Delete()
dr = Ships.NewRow()
For c = 0 To GridCols - 1
    dr(c) = False
Next
Ships.Rows.InsertAt(dr, 0)

Playing the Game

To play the game the user fires missiles at the Space Invader's Ships by pressing the spacebar on the keyboard. The missile cannon location at the bottom of the board is moved by the player when pressing the left and right arrow keys. These keyboard events are processed in the form's KeyDown event:

Private Sub  Form1KeyDown(sender As Object, e As  KeyEventArgs) Handles Me.KeyDown
    Select Case  e.KeyCode
        Case Keys.Left                                      'move cannon
            If MissileCol > 1 Then MissileCol -= 1
        Case Keys.Right                                     'move cannon
            If MissileCol < GridCols - 2 Then MissileCol += 1
        Case Keys.Space, Keys.F                             'fire a missile
            If Not  KeyDownActive Or e.KeyCode = Keys.F Then
                KeyDownActive = True         'KeyDownActive prevents contiuous fire when holding down shift key
                Missiles.Rows(GridRows - 2)(MissileCol) = SymbolMissile
                Missiles.Rows(GridRows - 1)(MissileCol) = SymbolMissile
            End If
    End Select
End Sub

In the program code, the Missiles dt stores the positions of the missiles. In the timer event the missiles are moved on the screen with each tick of the game clock. 

As we move the ships across the screen in the timer tick event we check to see if any missiles in the missile dt occupy the same space as a ship in the ship dt by using the CheckCollision sub routine. If the missile hits a ship the cell is given an empty space character.

Finally, we combine the missile dt and the ship dt into the board dt for display:

'combine the ships and Missiles tables into the board table
For r = 0 To GridRows - 1
    For c = 0 To GridCols - 1
        If Ships.Rows(r)(c) Is SymbolShip Then
            Board.Rows(r)(c) = Ships.Rows(r)(c)
        ElseIf Missiles.Rows(r)(c) Is SymbolMissile Then
            Board.Rows(r)(c) = Missiles.Rows(r)(c)
        Else
            Board.Rows(r)(c) = SymbolSpace
        End If
    Next
Next

Once the ships and missiles have been combined into the board, we add the cannon characters to the bottom of the game.

Now we play the game. If the player shoots all the ships before they reach the bottom of the board the player wins!

Drawing the Game Board

In our example we use the Dgv_CellPainting event to draw the characters on the screen. Drawing the text characters manually for each cell in this way is not required. We used it so we can either draw the grid lines or not. Furthermore, we could draw other details if desired including using a bitmap image for the characters instead of text symbols. For this example we have just show simple text characters.

The Example Code

To create the example in Visual Studio, make a new Windows Forms Project and copy and paste the code into an empty Form1 and run it. That's all. The code creates the dgv control.

Option Strict On
 
Public Class  Form1
    'train robber invasion game version 1
    'DataGridView display of datatable example all character based
    Private WithEvents  timer1 As  New Windows.Forms.Timer With {.Interval = 100}
    Private WithEvents  dgv1 As  New DataGridView With {.Parent = Me}
    Private Ships As New  DataTable("Ships")
    Private Missiles As New  DataTable("Missiles")
    Private Board As New  DataTable("Board")
    Private GridRows As Integer  = 25       'use a 25 x 25 grid for the game board
    Private GridCols As Integer  = 25
    Private FleetCols As Integer  = 5       'number of ship columns in a fleet
    Private KeyDownActive As Boolean
    Private MissileCol As Integer  = CInt((GridCols - 1) / 2)
    Private TotalShips, FleetRowCount As Integer
    Private FleetColCount As Integer  = 2
    Private SymbolShip As String  = "X"
    Private SymbolMissile As String  = "o"
    Private SymbolSpace As String  = " "
 
    Private Sub  Form1_Load(sender As Object, e As  EventArgs) Handles MyBase.Load
        DoubleBuffered = True
        KeyPreview = True
        Text = "Train Robbers Invasion"
        ClientSize = New  Size(400, 400)
 
        'fill the game data tables
        CreateBoardTable(Ships)
        MakeShipFleet()
        CreateBoardTable(Missiles)
        CreateBoardTable(Board)
 
        dgv1.Dock = DockStyle.Fill
        dgv1.Font = New  Font("Arial", 10, FontStyle.Bold)
        dgv1.ReadOnly = True   'dont allow editing
        dgv1.RowTemplate.Height = CInt(0.95 * ClientSize.Height / GridRows)
        dgv1.RowHeadersVisible = False
        dgv1.ColumnHeadersVisible = False
        dgv1.AutoSizeColumnsMode = DataGridViewAutoSizeColumnsMode.Fill
        dgv1.DataSource = Board
 
        'start the game
        timer1.Start()
    End Sub
 
    Private Sub  Form1KeyDown(sender As Object, e As  KeyEventArgs) Handles Me.KeyDown
        Select Case  e.KeyCode
            Case Keys.Left                                      'move cannon
                If MissileCol > 1 Then MissileCol -= 1
            Case Keys.Right                                     'move cannon
                If MissileCol < GridCols - 2 Then MissileCol += 1
            Case Keys.Space, Keys.F                             'fire a missile
                If Not  KeyDownActive Or e.KeyCode = Keys.F Then
                    KeyDownActive = True         'KeyDownActive prevents continuous fire when holding down shift key
                    Missiles.Rows(GridRows - 2)(MissileCol) = SymbolMissile
                    Missiles.Rows(GridRows - 1)(MissileCol) = SymbolMissile
                End If
        End Select
    End Sub
 
    Private Sub  Form1_KeyUp(sender As Object, e As  KeyEventArgs) Handles Me.KeyUp
        KeyDownActive = False
    End Sub
 
    Private Sub  Dgv_CellPainting(sender As Object, e As  DataGridViewCellPaintingEventArgs) Handles dgv1.CellPainting
        Dim newRect As New  Rectangle(e.CellBounds.X + 1, e.CellBounds.Y + 1, e.CellBounds.Width - 4, e.CellBounds.Height - 4)
        Dim backColorBrush As New  SolidBrush(e.CellStyle.BackColor)
        Dim gridBrush As New  SolidBrush(dgv1.GridColor)
        Dim gridLinePen As New  Pen(gridBrush)
 
        ' Erase the cell.
        e.Graphics.FillRectangle(Brushes.White, e.CellBounds)   'backColorBrush
 
        'draw the cell border
        'e.Graphics.DrawLine(gridLinePen, e.CellBounds.Left, e.CellBounds.Bottom - 1, e.CellBounds.Right - 1, e.CellBounds.Bottom - 1)
        'e.Graphics.DrawLine(gridLinePen, e.CellBounds.Right - 1, e.CellBounds.Top, e.CellBounds.Right - 1, e.CellBounds.Bottom)
 
        ' Draw the text content of the cell, ignoring alignment. 
        If (e.Value IsNot Nothing) Then
            Using br1 As  SolidBrush = New SolidBrush(Color.SkyBlue), _
                p2 As  Pen = New  Pen(Color.CadetBlue, 3)
                Select Case  e.Value.ToString
                    Case SymbolShip
                        e.Graphics.DrawString(e.Value.ToString, e.CellStyle.Font, Brushes.Blue, e.CellBounds.X + 2, e.CellBounds.Y + 2, StringFormat.GenericDefault)
                    Case SymbolMissile
                        e.Graphics.DrawString(e.Value.ToString, e.CellStyle.Font, Brushes.Red, e.CellBounds.X + 2, e.CellBounds.Y + 2, StringFormat.GenericDefault)
                    Case Else
                        If e.Value IsNot SymbolSpace Then e.Graphics.DrawString(e.Value.ToString, e.CellStyle.Font, Brushes.Green, e.CellBounds.X + 2, e.CellBounds.Y + 2, StringFormat.GenericDefault)
                End Select
            End Using
        End If
 
        e.Handled = True
    End Sub
 
    Private Sub  timer1_Tick(sender As Object, e As  EventArgs) Handles timer1.Tick
        Static direction As Integer  = 1
        FleetColCount += direction
 
        dgv1.SuspendLayout()
 
        If FleetColCount < 2 Or FleetColCount > GridCols - (1 + FleetCols) Then
            direction *= -1
            UpdateShips()
        Else
            'update the fleet position by moving left or right
            For r = 0 To GridRows - 1
                If direction = 1 Then
                    'shift all GridCols to right
                    For c = GridCols - 2 To 1 Step -1
                        CheckCollision(r, c)
                        Ships.Rows(r)(c + 1) = Ships.Rows(r)(c)
                        Ships.Rows(r)(c) = SymbolSpace
                    Next
                Else
                    'shift all GridCols to left
                    For c = 2 To GridCols - 1
                        CheckCollision(r, c)
                        Ships.Rows(r)(c - 1) = Ships.Rows(r)(c)
                        Ships.Rows(r)(c) = SymbolSpace
                    Next
                End If
            Next
        End If
 
        'combine the ships and Missiles tables into the board table
        For r = 0 To GridRows - 1
            For c = 0 To GridCols - 1
                If Ships.Rows(r)(c) Is SymbolShip Then
                    Board.Rows(r)(c) = Ships.Rows(r)(c)
                ElseIf Missiles.Rows(r)(c) Is SymbolMissile Then
                    Board.Rows(r)(c) = Missiles.Rows(r)(c)
                Else
                    Board.Rows(r)(c) = SymbolSpace
                End If
            Next
        Next
 
        UpdateMissiles()
 
        'add the canon
        Board.Rows(GridRows - 2)(MissileCol) = "[]"
        dgvWrite("[ ]", GridRows - 1, MissileCol - 2)
 
        If TotalShips < 1 Then
            'all ships destroyed you win
            timer1.Stop()
            dgvWrite("YOU WIN!!!", 10, 5)
        End If
 
        For c = 0 To GridCols - 1
            If Ships.Rows(GridRows - 2)(c) Is SymbolShip Then
                'ship at bottom game over
                dgvWrite("GAME OVER", 10, 5)
                timer1.Stop()
            End If
        Next
 
        dgv1.ResumeLayout()
    End Sub
 
    Private Sub  Form1_Resize(sender As Object, e As  EventArgs) Handles Me.Resize
        Me.Invalidate()
    End Sub
 
    Private Sub  dgvWrite(theText As String, row As  Integer, col As Integer)
        For c = 1 To theText.Length
            Board.Rows(row)(c + col) = Mid(theText, c, 1)
        Next
    End Sub
 
    Private Sub  UpdateShips()
        Dim dr As Data.DataRow
        FleetRowCount += 1
 
        'move ships down one row
        Ships.Rows(GridRows - 1).Delete()
        dr = Ships.NewRow()
        For c = 0 To GridCols - 1
            dr(c) = False
        Next
        Ships.Rows.InsertAt(dr, 0)
 
        Select Case  FleetRowCount
            Case CInt(GridRows / 4)
                MakeShipFleet()         'add more ships
            Case CInt(GridRows / 3)
                timer1.Interval = 60    'go faster
            Case CInt(GridRows / 1.5)
                timer1.Interval = 40
        End Select
    End Sub
 
    Private Sub  UpdateMissiles()
        Dim dr As Data.DataRow
        'move Missiles up 3 GridRows for each ship move down 
        For i = 1 To 3
            Missiles.Rows(0).Delete()
            dr = Missiles.NewRow()
            For c = 0 To GridCols - 1
                dr(c) = False
            Next
            Missiles.Rows.InsertAt(dr, GridRows - 1)
        Next
    End Sub
 
    Private Sub  CheckCollision(r As Integer, c As  Integer)
        'if there is a missile here then destroy the ship
        If Missiles.Rows(r)(c) Is SymbolMissile And Ships.Rows(r)(c) Is SymbolShip Then
            Ships.Rows(r)(c) = SymbolSpace
            Missiles.Rows(r)(c) = SymbolSpace
            TotalShips -= 1
        End If
    End Sub
 
    Private Sub  CreateBoardTable(dt As DataTable)
        'create the datatable rows and cols, set all cells to empty
        For c = 0 To GridCols - 1
            dt.Columns.Add(c.ToString)
        Next
        Dim dr As DataRow
        For r = 0 To GridRows - 1
            dr = dt.NewRow
            For c = 0 To GridCols - 1
                dr(c) = SymbolSpace
            Next
            dt.Rows.Add(dr)
        Next
    End Sub
 
    Private Sub  MakeShipFleet()
        'add a new fleet of ships to the ships datatable
        For r = 0 To 2
            For c = 0 To FleetCols - 1
                Ships.Rows(r)(c + FleetColCount) = SymbolShip
            Next
        Next
        TotalShips += 3 * FleetCols
    End Sub
 
End Class

References

  • DataGridViewDataTable
  • DataSource 
  • Add columns
  • NewRow
  • Delete rows 
  • InsertAt
  • KeyDown 
  • Timer