|
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
|