Free Web Hosting Provider - Web Hosting - E-commerce - High Speed Internet - Free Web Page
Search the Web

   

vbProgramming
Tutorials - Tile-Based Collision Detection
*Updated Version*

 
       
vbProgramming Home :: vbProgramming Forums :: Tutorials :: Contact :: Links 
This tutorial will teach you TIle-Based Collision Detection, commonly used
in Role Playing Games(RPGs).
Please Note : This is the updated version of the tutorial. The previous version
(created before 6.21.04 was an erroneous version)

 

 

 

Intro
Tile Based Collision Detetection is pretty simple. What is Tile Based Collision you say?
Each tile is a fixed size. The player is the size of one tile. When the player pushes right, he walks right until he gets to the next tile (meaning "no in-between tiles"). For example, say each tile is 30x30. Pretend the player is at 0,0 (the top left of the screen). If he pushes right, he'll end up at 30,0. Now im not saying he's going to "jump" from 0 to 30. In most games, the program will animate the player in such a way that he'll walk smoothly from 0,0 to 30,0. Hhe will never end up in uncanny places like (16,0). His X and Y positions will always be a multiple of 30.

How do we figure out what tile he's on? Really simple: [(PlayerPos.X / TileWidth), (PlayerPos.Y / TileHeight)]. Woah, that looks freaky - its not. For example in our example above, he went from (0,0) to (30,0). Thus, he went from tile (0,0) to tile (1,0). How is it (1,0)? Well PlayerPos.X in our case is 30. The tile width is 30, so (30/30, 0/30) = (1,0).


Now let's get coding (we'll talk about collision once we've got the basic movements done)! We'll use a blue rectangle as the character for this tutorial.

Creating the Classes clsSprite and clsLevel
Create a new class called clsSprite. This will hold our character's data. Create the following variable:

    Public Pos As Point 'The position he's in; ex. - (30,30) 
Create another class called clsLevel. This will hold the level data. Dim this variable:
    Public Shared Tile As Size = New Size(30, 30)

"Newing" the size in one line like that is the same as saying: Public Tile as Size, Tile.Width = 30, and Tile.Height = 30. I just like the shortcut method better :). Ok - so why is it shared? It means, no matter what level we're in, the size will
always be the same. We do this becuase we want the game to look 'uniform' and 'neat'.
--
For example:

Dim level1 as new clsLevel
messagebox.show(level1.tile.width) 'you'll get 30
Dim level2 as new clsLevel
Messagebox.show(level2.tile.width) 'you'll obviously get 30

clsLevel.tile.width = 80 'this will change the width for ALL instances of clsLevel
messagebox.show(level1.tile.width) 'you'll get 80
messagebox.show(level2.tile.width) 'you'll get 80 too!
--

Now go back to clsSprite. Type this in:

   Public Level As New clsLevel()


    Public Enum Dir
        UP
        DOWN
        LEFT
        RIGHT
    End Enum
We have Level as New clsLevel to store what level the sprite is in. This will be useful for collision detection purposes.
Since each level has different tiles, you want to check whether the sprite can pass throuugh a certain point depending
on the level.
We use the enums to store the character's Direction (Im pretty sure I explained it before in some other
tutorial).

Add the following property (explanation below)
    Public ReadOnly Property TilePos() As Point
        Get
            'The Tile that a player is on is (X coordinate/Width of tiles, Y coordinate / height of tiles)
            Return New Point((Pos.X / Level.Tile.Width), (Pos.Y / Level.Tile.Height)) 
        End Get
    End Property

First of all, you can see that the property has a Return value: a Point. Why couldn't we use a function? Well functions are
usually meant to *do something*, Properties are meant to Get or Set a value. We make it ReadOnly becuase we dont want to say something like "ourSprite.TilePos = new Point(30,30). Nah, we want to control his position through the Pos variable. Ok, what's
"Get" ? Get is what happens when you try to ACCESS the property. For example
MessageBox.show(TilePos). You tried to access the property, and it returned a point saying its TilePos.
The Return syntax is a little tricky. Im trying to return a Point, and in order to return both at once, i'd have to return a NEW point The property uses the level that the player is on (using its tile Width and Height)
to figure out his position(explained above).

One more thing before we get him to move. We have to find out what level he's on!
    Public Sub New(ByVal TheLevel As clsLevel)
        Level = TheLevel 'Find out what level the character is on when you initialize him and store it
    End Sub

Self explanatory isn't it? Now let's get him to move with GDI+!

Getting him to move Go back to form1's code. Add the following variables:
    Dim Level1 As New clsLevel()
    Dim Dude As New clsSprite(Level1) 'He's on Level1 (see Sub New in clsSprite)
Now go to form1_Paint. For this tutorial, we're just gonna draw a rectangle. In order to draw a rectangle, you need the following arguments
Pen, X, Y, Width, and Height.
We can create the Pen object just like we created the Bitmap object earlier when we displayed an image with GDI+ in a different tutorial.
     e.Graphics.DrawRectangle(New Pen(Color.Blue), Dude.Pos.X, Dude.Pos.Y, Level1.Tile.Width, Level1.Tile.Height)
Well, that's how to create a rectangle in GDI+. Now let's move him in form1_keydown

Select Case e.KeyCode 
Case Keys.Down Dude.Pos.Y += clsSprite.Tile.Height Case Keys.Up Dude.Pos.Y -= clsSprite.Tile.Height Case Keys.Right Dude.Pos.X += clsSprite.Tile.Width Case Keys.Left Dude.Pos.X -= clsSprite.Tile.Height
End Select Me.Invalidate()
Nothing new. Run the program, he'll "jump" from 1 tile to another. Dim x as integer in form1_keydown (in the very beginning)

Lets smoothen out the animation with a Loop:


01        While x < 30 'Loop 30 times
02
03            x = x + 1
04            Select Case e.KeyCode
05                Case Keys.Down
06                    Dude.Pos.Y += Level1.Tile.Height / 30
07                Case Keys.Up
08                    Dude.Pos.Y -= Level1.Tile.Height / 30
09                Case Keys.Right
10                    Dude.Pos.X += Level1.Tile.Width / 30
11                Case Keys.Left
12                    Dude.Pos.X -= Level1.Tile.Height / 30
13                    'loop 30 times, moving him 1 pixel each time = 30 pixels = moved 1 tile 
14            End Select
15
16            Me.Text = "Position: " & Dude.Pos.ToString & "Tile Number: " & Dude.TilePos.ToString 
17            Me.Invalidate()
18
19        End While


(like the line numbers, eh? - Thanks to www.SquishyWeb.com).
All this does is this:
Loop 30 times, moving him 1 pixel each time (30/30), that's 30 pixels. which means he moved 1 tile! Then the next thing i did was display his position on the titlebar of the form.
Run it, it'll run very weird. it'll "lag" while you're holding down a key. Here's why: When you're in a loop, the system ignores certain events, such as keypresses, mousedowns, and invalidation events(the
cause of the lag feeling). In order to avoid this, .net has a handy function called Application.DoEvents. I purposely left a gap at
line 02, becuase that's where you're gonna type in
    Application.DoEvents
Now run it. It'll work :).
Wait! Run it again.. this time, push random arrow keys quickly for an extended period of time (in various directions). Let go, and watch
your character go crazy. Why is it doing this? Well, the system is reading all your arrow keys and 'remembering them'. But you're
pushing the arrow keys so fast that it cant execute the loop and move him quick enough to match your hand speed! Well we're just going to have to limit that and make sure he can only press a key after the guy is done animating.
Dim this variable in form1's gloabals:
    Dim CanWalk As Boolean = True 'Set it to true becuase you want him to walk in the beginning!
Ok, now we want to be able to press a key only after he's done animating. After "dim x as integer", type in
    If CanWalk Then
and after End While type in End If.
Right after "while x < 30", type in CanWalk = False. This just means "don't let him walk now, he's walking already!" - after all, he is in the animation 'loop'

In between End While and End If, type in CanWalk = True, becuase he is done walking, so you want to let him walk some more!

Run the program now, his walking should be smooth!
Now its time for collision detection!


Collision Detection
Our example for collision detection will be a very simple one. The map will be 600x600. Meaning there are 400 tiles(the map is 20x20 tiles) Our tiles will be 30x30 pixels (what we've been doing throughout this tutorial).
We're going to set up our tiles. Go back to clsLevel.

    Public Passable(20, 20) As Boolean 'make the map 20x20

    Public Sub SetUpTiles()
        Dim x, y As Integer

        For x = 0 To 20
            For y = 0 To 20
                Passable(x, y) = True 'Set every element in Passable to True
            Next
        Next

        Passable(10, 10) = False 'and then make the middle tile not passable
    End Sub
Should be a little self explanatory :). There is one point I ought to make: this example program isn't very flexible at all :). At the end of
this 2nd section, the game we're going to make is an RPG (with an Engine). We are going to use a similar SetUpTiles function - except
we are going to read the mapsize and the 'passable' data from a Text File. Also, the engine will (quite obviously) support displaying graphic
and reading *that* data from the text file. But since this is just an example, we're going to make less flexible (for example: by not worrying
about displaying tiles as graphics, and 'selecting' which tile is passable or not. - That's for the last tutorial in this section (i think) OK, so we're not really dealing much with tile graphics here. We know that you can't walk through tile (10,10) which is located in position
(300,300). Just go to the form designer, and draw a picturebox there :). Make sure you place the picturebox at (300,300) and make sure its
size is 30x30. By the way, set its BackColor to something different so you can see it :p. Now, go back to clsSprite. Let's add an "AllowedToWalk" function.

Public Function AllowedToWalk(ByVal Direction As Dir) As Boolean
        Select Case Direction
            Case Dir.DOWN


                If Level.Passable(TilePos.X, TilePos.Y + 1) = True Then 'The Tile below him
                    Return True
                Else
                    Return False
                End If

            Case Dir.LEFT

                If Level.Passable(TilePos.X - 1, TilePos.Y) = True Then 'The Tile to his Left
                    Return True
                Else
                    Return False
                End If

            Case Dir.RIGHT

                If Level.Passable(TilePos.X + 1, TilePos.Y) = True Then 'The Tile to his right
                    Return True
                Else
                    Return False
                End If


            Case Dir.UP

                If Level.Passable(TilePos.X, TilePos.Y - 1) = True Then 'The Tile above him
                    Return True
                Else
                    Return False
                End If

        End Select
    End Function

Sort of self explanatory.  It just inputs his direction, and checks whether the tile he's heading towards is passable or not.  Now go back to
form1_load.  Type in level1.SetupTiles(). Now go back to form1_keydown. Change the following lines by adding If statements around them
checking whether he can move there in the first place .

                Select Case e.KeyCode
                    Case Keys.Down

                        If Dude.CanWalk(clsSprite.Dir.DOWN) Then 'Just check whether he can go down or not
                            Dude.Pos.Y += Level1.Tile.Height / 30
                        End If


                    Case Keys.Up
                        If Dude.CanWalk(clsSprite.Dir.UP) Then
                            Dude.Pos.Y -= Level1.Tile.Height / 30
                        End If

                    Case Keys.Right
                        If Dude.CanWalk(clsSprite.Dir.RIGHT) Then
                            Dude.Pos.X += Level1.Tile.Width / 30
                        End If


                    Case Keys.Left
                        If Dude.CanWalk(clsSprite.Dir.LEFT) Then
                            Dude.Pos.X -= Level1.Tile.Height / 30
                        End If

                        'loop 30 times, moving him 1 pixel each time = 30 pixels = moved 1 tile 
                End Select
Now run it, walking up to tile 10,10. You'll see that he cant go through it!
Run it again. We have one more problem to take care of before wrapping this up. Go right, and then come back left.
The guy's position will be 15,0 and the program will give an error. (The same happens when you go down and come back up). The error
says 'index out of bounds.'

Here's why its hapening - look carefully at what TilePos in clsSprite returns:
Return New Point((Pos.X / Level.Tile.Width), (Pos.Y / Level.Tile.Height))
Substitute in an X position of 15 (beucase that's what our position was when we got the error :p).
15 / 30 is actually 0!! (we're using INTEGERS, so it rounds the numbers. .5 rounds down to 0, anything greater than .5, like .51 would
round to 1) Well, there's a simple solution: Round Up! How? Math.Ceiling

Return New Point(Math.Ceiling(Pos.X / Level.Tile.Width), Math.Ceiling(Pos.Y / Level.Tile.Height))
'Math.Ceiling rounds UP. 15/30 rounds up to 1
That's it for this tut! Next up: Bit String Flicking ;)

Optimizations
I just realized one thing. There's another small bug in this tutorial. When you approach the tile from the left or from the top, his position gets a little messed up. (If you approach it from the left, the collision detection stops him from moving to the next tile)
Darc, from vbProgramming forums gave me a quick fix to this problem. He suggested that I don't check for collision as I walk, but rather check for collision once, and let him walk to the next tile without checking for collision. The updated source code is as follows:

If CanWalk Then

            Dim Walking As Boolean = False
            Dim Direction As Dir

            CanWalk = False 'Don't let him walk again, he's walking now!

            Select Case e.KeyCode
                Case Keys.Down

                    If Dude.AllowedToWalk(clsSprite.Dir.DOWN) Then 
                        Direction = Dir.DOWN
                        Walking = True
                    End If


                Case Keys.Up
                    If Dude.AllowedToWalk(clsSprite.Dir.UP) Then
                        Direction = Dir.UP
                        Walking = True
                    End If

                Case Keys.Right
                    If Dude.AllowedToWalk(clsSprite.Dir.RIGHT) Then
                        Direction = Dir.RIGHT
                        Walking = True
                    End If


                Case Keys.Left
                    If Dude.AllowedToWalk(clsSprite.Dir.LEFT) Then
                        Direction = Dir.LEFT
                        Walking = True
                    End If

                Case Else
                    Walking = False

           End Select

            If Walking Then
                Dim x As Integer
                For x = 1 To 30
                    Select Case Direction
                        Case Dir.DOWN
                            Dude.Pos.Y += level1.tile.height / 30
Case Dir.UP Dude.Pos.Y -= level1.tile.height / 30 Case Dir.LEFT Dude.Pos.X -= level1.tile.height / 30 Case Dir.RIGHT Dude.Pos.X += level1.tile.height / 30 End Select Application.DoEvents() Me.Text = "Position: " & Dude.Pos.ToString & "Tile Number: " & Dude.TilePos.ToString Me.Invalidate()
'loop 30 times, moving him 1 pixel each time = 30 pixels = moved 1 tile Next End If CanWalk = True 'he's done walking, let him walk some more! End If
Once again, all credits go to Darc for this bug fix.
The Source Code for this tutorial is located here.
You can also locate this by logging in to vbProgramming Forums and going to:
Tutorials > Tutorial Source Code > Source Code