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

vbProgramming | Tutorials | Games | RPG Programming I : Setting it up
    vbProgramming 
Tutorials -
RPG Programming Series:
Stage II. Scrolling and Basic Physics
 
     

vbProgramming Home :: vbProgramming Forums :: Tutorials :: Contact :: Links 

 

 

This is the second tutorial in the RPG Programming Series. Besides learning how to scroll and apply basic physics (collision detection) you will also learn how to change the screen resolution.

Intro
Welcome to the second tutorial in the RPG Programming Series. Please be sure you have a good understanding of the Scrolling tutorial as well as the Tile Based Collision Detection tutorial. In those tutorials, you either did one or the other, this tutorial will combine the two (scrolling and physics) and will also teach you how to change screen resolution.

In this tutorial, you will create another text file for the tile set (we used "Basis"). A tile set (or chipset) is simply a set of tiles which make up a certain environment. For example, we could have a chipset for dungeon, overworld, outer space (!)..etc. It's just a set of bitmaps. So, we'll create a textfile for each chipset which contains collision detection information. Not too difficult.

Changing Screen Resolution
See Changing Screen Resolution tutorial in the Miscellaneous section.  Please do what it says, and make sure your resolution is 640x480. Run the project - slight problem: you see the borders of the form.

Now, in the form designer, set form1.FormBorderstyle to None.  Run it, to close the form you have to use Alt+F4.
We'll now add support for Escape, in form1_keydown, add support for the escape event by adding this case:
Case Keys.Escape
    Me.Close()


Easy enough, now we'll make a box that says "Do you really want to close?"

Go to the form1_closing (note, this is different than form1_close) and type in the following code:
Dim result As MsgBoxResult
result = MessageBox.Show("Do you really want to quit?", "Quit?", MessageBoxButtons.YesNo, MessageBoxIcon.Question)

If result = MsgBoxResult.No Then
   e.Cancel = True
End If


Very self explanatory huh? Now (yay) your app is in fullscreen.
Difference between form1_Closing and form1_Close:
Closing happens before Close, and in the closing event you can cancel out, ex: don't allow the form to close.

If you wanted to make an app that never closed then in form1_closing you'd say e.cancel = true. Hehe, obvioulsy control alt delete would be the only way out of that one ;).

Scrolling - Concept & Code
I won't go through a lot of detail here on the static scrolling because everything has already been covered in the Scrolling tutorial. As a quick refresher - remember that to scroll you simply by moving all other objects the in the opposite direction of the character. For example, if you press right - the map moves left to achieve scrolling effect.

Enough about concepts! Let's get coding.

Very simple.

Go to the paint event, and change the line which renders the map to:
'Scroll in the direction opposite to movement
 e.Graphics.DrawImage(Game.map.Tiles(x, y), New Point(x * 30 - Game.alex.Position.X, y * 30 - Game.alex.Position.Y))

And change the line which renders the character to:
'Keep the character centered on the screen. The *real* center is (640/2,480/2) which is (320, 240). Since 320 doesn't divide evenly into 30 (meaning he won't be on a tile 'if he's placed at an X of 320), I simply rounded down to 300.
 e.Graphics.DrawImage(Game.alex.SpriteImage(Game.alex.Direction, Game.alex.Frame), New Point(300, 240))

Easy, now run it and you'll see that it works perfectly.

Collision Detection - Setting up the Map class
Aye, this will probably be one of the most memory-consuming parts of our program (besides the rendering and the animation of course).

First, we're going to need to make changes to our map. In NotePad, open up bin/Maps/lightworld.map and change it to the following:
22,16
3
#LEGEND
1=TRUE
2=TRUE
3=FALSE
#END LEGEND
2,1,1,1,1,1,1,1,1,1,1,1,1,1,1,3,1,1,1,1,1,1
3,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,2
1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,2
1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,2
1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1
1,1,1,1,2,2,2,2,2,2,2,2,2,1,1,1,1,1,1,1,1,2
1,1,1,1,2,2,2,2,2,2,2,2,2,1,1,1,1,1,1,1,1,2
1,1,1,1,2,2,2,2,2,2,2,2,2,2,1,1,1,1,1,1,1,1
1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,2
1,1,1,1,2,1,1,1,1,1,1,1,1,3,1,1,1,1,1,1,1,2
1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1
3,1,1,1,1,1,1,3,1,1,1,1,1,1,1,3,1,1,1,1,1,1
1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1
1,1,1,1,2,1,1,1,1,1,1,1,1,3,1,1,1,1,1,1,1,1
1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1
3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3

The 3 which I added (after "22,16") signifies that there are 3 tiles. In between #LEGEND and #ENDLEGEND contains the information on whether or not the tile is passable or not. The first tile is grass, so that tile is passable. The second tile is tall grass, and yes it is passable. The third tile is a mountain, and no you can't pass through it (thus, 3=FALSE).

Now go back to your clsMap class and declare the following variables:
'Stores whether or not the tile is passable
Public Passable(,) As Boolean

What we're going to do (in our physics class which we'll create soon) is check whether certain tiles are passable. For example, you could say something like
If Passable(0,1) Then LetTheDudeMove() or something. So this is just a boolean array which stores whether each tile is passable.

Next we're going to have to make the program understand the things we just added to the map class.

Go to the ReadMap sub, and find the line where you ReDim Tiles(Width,Height), and add the following:
Redim Passable(Width,Height)

Dim TypesOfTiles As Integer
'Store the next line (which contains the number of types of tiles) into an integer
TypesOfTiles = SR.ReadLine()
Dim Walkable(TypesOfTiles) As Boolean

Walkable is an array of Booleans which represents each tile's state. For example, we know that 0=TRUE (because you can pass over grass).
Note that there is a slight difference between Walkable() and Passable(,). Sorry for naming these two very similarly. But, Walkable tells you what kind of tile (ex: grass/mountain..etc) can be walked over and Passable tells you what tile on the map specifically can be walked over (ex: tile(0,0) is a "2" so yes, it can be walked over).

Now we're going to have to split the lines according to the equal sign (just like we split the width and the height into two on the first reading.

Dim theLine() As String

Here's the confusing part:
While (Not ln = "#END LEGEND")
     ln = SR.ReadLine
     If Not ln = "#LEGEND" And Not ln = "#END LEGEND" Then
          theLine = ln.Split("=")
          Walkable(theLine(0)) = CBool(theLine(1))
     End If
End While

Don't get freaked out yet! The first line tells you "Loop until you see a #END LEGEND". The second line stores the current line being read into ln.
The third line (the If statement) makes sure that the current line being read isn't the "#LEGEND" or the "#END LEGEND" line.

If it isn't one of those two lines then:

The fourth line tells ln to be split according to the equal signs. Such that the first part will be stored in theLine(0) and the second part will be stored in theLine(1).
For example, in our textfile we have:
1=TRUE. It would split it in such a way that theLine(0) = "1" and theLine(1) = "TRUE".

The fifth line has strange syntax, it basically says "The Current Tile being read will be stored as passable or not". Let's go back to our
1=TRUE example. Let's plug things in:
Walkable(1)=CBool("TRUE")
CBool means convert to Boolean, so the string "TRUE" will be converted into the Boolean value True.

End If
End While

Now it's time to go down a bit, and start storing things into our Passable(,) array. Go to the nested For loop (This one: For CurrentColumn = 0 To Width).
And after the line where you assign the Tiles(,) variable, add the following:
Passable(CurrentColumn, CurrentRow) = Walkable(Line(CurrentColumn))

Again, don't freak out. Plug in numbers
Passable(0,0) = Walkable(0)

Keep in mind the Line(CurrentColumn) gives the current tile being read. We know that Walkable(0) = True (meaning that you can walk over tile 0 (grass), so:
Passable(0,0) = True

It all ties in together if you think about it.

Collision Detection - Setting up the Sprite class
We're going to need a variable which returns the current tile (TilePos) that the character is on. Remember, TilePos = (XPosition / TileWidth, YPosition / TileHeight).
Easy, we know that if he's on position (30,30) then he's on tile (1,1). By the way, this has already been covered before (I believe it was the Tile Based Collision Detection tutorial).
Quick note: If he's on position (15,15), then he's on tile (0,0). Remember - the tile (0,0) goes from position (0,0) to (30,30), so a position of (15,15) would mean that he's on tile (0,0). In other words - when we divide tile width and the tile height to get the TilePos , we need to round it down. The point (29,29) is still on tile (0,0). So to do this (round down), we use Math.Floor.

Go back to clsSprite and add a ReadOnly Property:
Public ReadOnly Property TilePos() As Point
     Get
         Return New Point(Math.Floor(Position.X / 30), Math.Floor(Position.Y / 30))
     End Get
End Property

Simple enough. Now we want to make our classes reusable, remember, and not all games will use a tile size of 30x30. Go back to gameclass:
Public Const TileWidth As Integer = 30
Public Const TileHeight As Integer = 30

Now, go back to clsSprite and replace the 30's with the corresponding GameClass.TileHeight and GameClass.TileWidth
Tutorial edit: I just realized that you have a similar property which does pretty much the same thing, called CurrentTile (from the previous tutorial). Please delete CurrentTile.

Alright, you're going to have to replace all the 30's that you see with "GameClass.TileWidth" or "GameClass.TileHeight." It's not going to change how your code operates, and it's not that important.  But - here's a list of places that you can replace 30 with these constants (I recommend you change it):

List of changes
*clsSprite
      moveUp() and moveDown(). You see
For x = 1 To 30 * Multiplier  replace 30 with GameClass.TileHeight
      moveLeft() and moveRight(). You see
For x = 1 To 30 * Multiplier replace 30 with GameClass.TileWidth.
*Form1
      Form1_Paint. Change the line which renders the map to:
      e.Graphics.DrawImage(Game.map.Tiles(x, y), New Point(x * Game.TileWidth - Game.alex.Position.X, y * Game.TileHeight- Game.alex.Position.Y))

Now note, change the TileWidth and TileHeight to 60. See what happens? You see all these spaces. Why? Because the images are only 30x30. Move around, you'll see clearly that the tile based movement is working perfectly ;). Change the TileWidth and the TileHeight back to 30.

Collision Detection - The class itself
Create a new class called clsCollision. And add the following code to it:

'***NOTE***: Paste into .NET to see the formatting
'The main purpose of this class is to simply check for collision!
Public Class clsCollision
Public Function Allow(ByVal direction As clsSprite.Dir) As Boolean
Select Case direction
Case clsSprite.Dir.Up
'First make sure the tile above him is not the outside of the map
If GameClass.alex.TilePos.Y = 0 Then Return False
'Then make sure that the tile above him is passable
If GameClass.map.Passable(GameClass.alex.TilePos.X, GameClass.alex.TilePos.Y - 1) Then
Return True
Else
Return False
End If
Case clsSprite.Dir.Down
'Be sure that he's not on the bottom tile and heading down
If GameClass.alex.TilePos.Y = GameClass.map.Height / 30 Then Return False
'Then make sure that the tile below him is passable
If GameClass.map.Passable(GameClass.alex.TilePos.X, GameClass.alex.TilePos.Y + 1) Then
Return True
Else
Return False
End If
Case clsSprite.Dir.Left
'Be sure that he's not on the leftmost tile and heading left
If GameClass.alex.TilePos.X = 0 Then Return False
'Then make sure that the tile to the left of him is passable
If GameClass.map.Passable(GameClass.alex.TilePos.X - 1, GameClass.alex.TilePos.Y) Then
Return True
Else
Return False
End If
Case clsSprite.Dir.Right
'Be sure that he's not on the rightmost tile and heading right
If GameClass.alex.TilePos.X = GameClass.map.Width / 30 Then Return False
'Then make sure that the tile to the right of him is passable
If GameClass.map.Passable(GameClass.alex.TilePos.X + 1, GameClass.alex.TilePos.Y) Then
Return True
Else
Return False
End If
End Select
End Function
End Class

Alright, it's pretty self explanatory.  It basically checks if the tile he's heading towards is passable or not. First, it also checks if he's on a "border" tile. For example, if he's on one of the top tiles, you shouldn't allow him to go up, so I added support for that too.

In GameClass:
 'Declare the physics class
Public Shared Physics As New clsCollision()


Now go to clsSprite. In each Move sub (MoveUp, MoveDown, MoveLeft, and MoveRight), wrap the entire sub around an If statement which checks if the physics class allows to move in that direction.

For example, in the beginning of the MoveUp sub:
 If GameClass.Physics.Allow(Dir.Up) Then
and at the very end, of course, you need End If

Do this for all 4 "move" subs.

Now, go and run the program.
...
...

Not working! Oh No!  Solution to problem.
Oh - crap what went wrong? Go up, for example, even though the tile above you is grass, you can't go up! Don't worry ;).

In the world of game programming, programmers run into some difficulties with their code - thinking that their code works, but it doesn't. Once they run their app some thing goes wrong. These problems will crop up everywhere and usually it's just a result of a stupid mistake. In my opinion, if you have the patience enough to fix these problems (I'm not just talking about this game), then you're a great programmer. If you try to make a game on your own, you'll run into a crapload of these things which come back to bite you in the back in the end (unless you're truly a wonderful coder!)

Guess what our mistake was? It started back in the scrolling section. In form1, set the formborderstyle property to sizable and in the keydown event, set me.text to gameclass.alex.tilepos.tostring() <-- that won't fix the problem, but it'll just give us why that happened

Now run it, and you'll see his position (it all depends on what you set his default position to), it should be (0,0) or (0,2) or something, it's user defined (see clsSprite.Position)
 
Let's go back and fix this problem. Here's what happened:
-We originally rendered the character and the map at (0,0) so that the character was on the top left of the map.
-We then proceeded to scroll, shifting the character's position to the center of the screen to (300,240).
-His position was still recorded as (0,0), but we always rendered him in 300,240.
That was the problem.

Render him at 0,0 (Change his position in the Paint event):
 e.Graphics.DrawImage(Game.alex.SpriteImage(Game.alex.Direction, Game.alex.Frame), New Point(0, 0))
and you'll see that it works fine, but (obviously) he remains on the top left of the screen and scrolls... which is a bit odd.

To fix this problem, it's pretty simple. First of all, revert all the changes you made ( [1] - Change the formborderstyle to None and [2] render him at 300,240 again instead of 0,0). To fix the problem, all you need to do is account for the position shifted while placing him in the center, changing the line which renders the map to:
e.Graphics.DrawImage(Game.map.Tiles(x, y), New Point(x * Game.TileWidth - Game.alex.Position.X + 300, y * Game.TileHeight - Game.alex.Position.Y + 240))

Simple right?

Finishing touches
Let's add some finishing touches to our tutorial (Don't worry. The RPG Programming Series isn't over yet!).

Walk around until a mountain is directly below you. Push down. The character won't look down! He won't even try to go down (by animating). This is because we've excluded all frame changing and animation unless there is no object (because we wrapped the entire Move sub with an If statement).

To fix this, follow the model below:
Public Sub MoveUp()
   'Set his direction
   Direction = Dir.Up
   'Animate the sprite!
   Me.Animate()
   GameClass.GameForm.Invalidate()


   If GameClass.Physics.Allow(Dir.Up) Then
   'If the sprite is currently not moving Then move him up
       If Not InMotion Then
           'Now he's in motion.
           InMotion = True
           'Loop Variable
           Dim x As Single
           'Animate the sprite!
           Me.Animate()
           'Loop 30 times controlled by the multiplier
           For x = 1 To GameClass.TileHeight * Multiplier
                  'Move him one unit up, 30 times controlled by the multiplier
                   Position.Y -= 1 / Multiplier
                  'Let your application do other events (ex: Check for keypress and refresh the form)  in this loop.
                  'Animate the sprite!
                  Me.Animate()
                 'DoEvents
                  Application.DoEvents()
                 'Refresh the form
                  GameClass.GameForm.Invalidate()
          Next
         'Round his decimal place
         Position.Y = Math.Round(Position.Y, 0)
         'Now he's done moving.
         InMotion = False
     End If
   End If
End Sub

All I did was, before the If statement which checks for direction, I added these changes
a) Set the Direction
b) Animate
c) Invalidate

Those are the only changes that I made.  Now you should see him "Attempting" to move down at the mountain.

Guys - ever get the feeling that he's shuffling his feet too fast? Obviously this means that he's changing frames too fast.
I find it better if he animates 10 times slower (instead of stepping 30 times from tile to tile, he steps 3 times).

At the top of clsSprite:
 'Secondary frames
Private SFrame As Integer

 Now, change the Animate sub to:
Public Sub Animate()
     SFrame += 1
     If SFrame = 9 Then
         SFrame = 0
         ' Make him move
         Frame += 1
         ' Don't let it go too high!
         If Frame = 3 Then Frame = 0
     End If
End Sub

Basic and self explanatory. It basically means, for every 10 sFrames, you move one frame. Since this Animate sub gets called 30 times per frame, SFrame gets incremented 30 times, meaning that frame gets incremented 3 times.

Play around with the numbers if you wish. 9 works best for me.

Hope you've enjoyed this tutorial. If you ever got lost (and understandably so), you always have the source code to look at.

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