|
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 EnumWe 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 SubShould 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 SelectNow 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
|
|