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