|
Intro
Welcome to the fourth tutorial in the series, probably one of the
most important tutorials in this series. There is a new tutorial in
Section II called Alternative to Arrays, please read that tutorial
before continuing. You will use the knowledge of Hashtables to
program this tutorial.
Basically in this tutorial, we're
going to let the application run itself. Heh. We'll just feed the
program a few text files and BAM, you're done. Actually, most of
what we've been doing is event-driven (or self-driven, or
scripted...I prefer the term event-driven). For example, take the
clsMap class. We're not doing anything manually... we just feed the
class a .map file and it creates a map for us. We don't
manually say "Tiles(0,0) = new bitmap("0.bmp").
Basically, most of the stuff we've been doing is event-driven, you
probably haven't realized it. This tutorial takes it a step forward,
using the aid of HashTables to help us out.
Adding another NPC
Let's first add another NPC.
This way, you'll find out why event-driven programming is useful.
In GameClass:
Public
Shared doom
As New
clsNPC("poom",
New Point(10, 10), clsSprite.Dir.Up, "poom.dlg")
Just note that we say "poom" because I don't have a set of images
for doom, so I'll just use poom. If you want, feel free to make your
own character set for doom and use that. But this is just an
adjustment.
In GameClass.Render, right after rendering poom:
e.Graphics.DrawImage(Me.doom.CurrentImage,
New
Point((Me.doom.Position.X
* 30) - Me.alex.Position.X
+ 300, (Me.doom.Position.Y
* 30) - Me.alex.Position.Y
+ 240))
In another tutorial (the Scrolling
and Physics one, I think), we already discussed why we need the +300
and the +240, so this should be pretty familiar to you by now. Easy
enough, we're rendering another NPC. Run it and it should work.
Now, imagine what we would have to do if we wanted to "talk" to this
guy, we would have to update the clsEvent sub and add a LOT of If
statements and checks just to get this guy to talk.
This is the problem we need to avoid.
I am
proud to introduce you to Event-Driven Programming....
Event-Driven Programming
As I've said before, you've
done this all along. As a matter of fact, Visual Basic itself is
designed to be event-driven. Everything in Visual Basic. NET is
event driven, for example: the program 'waits' for you to press a
key before it gets to the form1_keydown event, when the form loads,
the form1_load is called....etc. Visual Basic .NET is designed
to be an event-driven language. It passively waits for events and
actively responds to them when they are called. Most of what we've
been doing is event-driven, whether you realize it or not.
<If anyone is experiencing a problem with the website being fully
italicized, please Contact
me. The code formatting (as you see
above) might mess things up a bit>
However, the manual coding in of NPCs is NOT event-driven. As a
matter of fact, what we're going to do with these NPCs borderlines
on Scripting, so what you're seeing right now is basically an
introduction to Scripting. Anyways, let's get started.
A prerequisite for this tutorial
is the "Alternative to Arrays" tutorial in Section II, so be sure
you've read that.
Let's make a collection (Hashtable) of clsNPCs:
Public Shared NPCs As New
Collections.Hashtable()
'This will hold all the items in our NPCs collection. See Alternative to
Arrays tutorial for more information.
Dim Entry As Collections.DictionaryEntry
Replace the NPC rendering code (both poom and doom) with the
following (It's a bit long and confusing):
For Each Entry In NPCs
e.Graphics.DrawImage(DirectCast(NPCs(Entry.Key),
clsNPC).CurrentImage, New Point((DirectCast(NPCs(Entry.Key),
clsNPC).Position.X * 30) - Me.alex.Position.X + 300,
DirectCast(NPCs(Entry.Key), clsNPC).Position.Y * 30 -
Me.alex.Position.Y + 240))
Next
Uhh. Yeah. It seems very confusing, but if you compare the
code to the previous code for rendering the NPC without Hashtables,
you'll notice that it's the exact same. Here's a comparison,
argument by argument (With and without Hashtables). The previous
code is in green, and the new code is in black:
1.
Me.doom.CurrentImage
DirectCast(NPCs(Entry.Key),
clsNPC).CurrentImage,
2.
New Point((Me.doom.Position.X * 30) - Me.alex.Position.X + 300
New Point((DirectCast(NPCs(Entry.Key),
clsNPC).Position.X * 30) - Me.alex.Position.X + 300
3.
(Me.doom.Position.Y * 30) - Me.alex.Position.Y + 240)) <-- this is
the Y argument of New Point
DirectCast(NPCs(Entry.Key), clsNPC).Position.Y
* 30 - Me.alex.Position.Y + 240))
So you'll notice that it's the exact same, except we've
modified it to let it work with Hashtables.
Now in form1_load, just do this (at the end)
GameClass.NPCs.Add("poom", New clsNPC("poom",
New Point(3, 3), clsSprite.Dir.Down, "poom.dlg"))
GameClass.NPCs.Add("doom", New clsNPC("poom", New Point(4, 3),
clsSprite.Dir.Down, "poom.dlg"))
GameClass.NPCs.Add("boom", New clsNPC("poom", New Point(5, 3),
clsSprite.Dir.Down, "poom.dlg"))
GameClass.NPCs.Add("room", New clsNPC("poom", New Point(6, 3),
clsSprite.Dir.Down, "poom.dlg"))
As you can tell, I have a great imagination and creativity for
names....... .... no. This is why I do graphics and not concept
design in a game.
Run it, it should work (Don't talk the NPCs yet).
Now it's time for the clsEvent class. Instead of doing an "Argument
by argument" comparison like before, I'll just give you the code.
The code is the exact same as before, except I've adjusted it
for use with HashTables. There's a little too much code to
syntax highlight, just pretend anything green is black. Also you'll
have to ignore the code that "sticks out", it's because of the
limited margins on the page.
'This class has a very basic purpose: Checking for events
'Examples of events are: Talking to NPCs, Picking up items... that
sort of thing.
Public
Class clsEvent
Dim i
As Integer
Dim Entry
As DictionaryEntry
Public
Sub CheckForEvents()
If
Not GameClass.alex.InConversation
Then
For
Each Entry
In GameClass.NPCs
Select
Case GameClass.alex.Direction
Case
clsSprite.Dir.Up
If GameClass.alex.TilePos.X = DirectCast(GameClass.NPCs(Entry.Key),
clsNPC).Position.X And
GameClass.alex.TilePos.Y - 1 = DirectCast(GameClass.NPCs(Entry.Key),
clsNPC).Position.Y Then
DirectCast(GameClass.NPCs(Entry.Key), clsNPC).Direction =
clsSprite.Dir.Down
clsText.Text = "Hi, I'm the guy at " &
DirectCast(GameClass.NPCs(Entry.Key),
clsNPC).Position.ToString()
GameClass.GameForm.Invalidate()
GameClass.alex.InConversation =
True
End If
Case
clsSprite.Dir.Down
If GameClass.alex.TilePos.X = DirectCast(GameClass.NPCs(Entry.Key),
clsNPC).Position.X And
GameClass.alex.TilePos.Y + 1 = DirectCast(GameClass.NPCs(Entry.Key),
clsNPC).Position.Y Then
DirectCast(GameClass.NPCs(Entry.Key),
clsNPC).Direction = clsSprite.Dir.Up
clsText.Text = "Hi, I'm the guy at " &
DirectCast(GameClass.NPCs(Entry.Key),
clsNPC).Position.ToString()
GameClass.GameForm.Invalidate()
GameClass.alex.InConversation =
True
End If
Case
clsSprite.Dir.Left
If GameClass.alex.TilePos.X - 1 =
DirectCast(GameClass.NPCs(Entry.Key), clsNPC).Position.X
And GameClass.alex.TilePos.Y =
DirectCast(GameClass.NPCs(Entry.Key),
clsNPC).Position.Y Then
DirectCast(GameClass.NPCs(Entry.Key), clsNPC).Direction =
clsSprite.Dir.Right
clsText.Text = "Hi, I'm the guy at " &
DirectCast(GameClass.NPCs(Entry.Key),
clsNPC).Position.ToString()
GameClass.GameForm.Invalidate()
GameClass.alex.InConversation =
True
End If
Case
clsSprite.Dir.Right
If GameClass.alex.TilePos.X + 1 =
DirectCast(GameClass.NPCs(Entry.Key), clsNPC).Position.X
And GameClass.alex.TilePos.Y =
DirectCast(GameClass.NPCs(Entry.Key),
clsNPC).Position.Y Then
DirectCast(GameClass.NPCs(Entry.Key), clsNPC).Direction =
clsSprite.Dir.Left
clsText.Text = "Hi, I'm the guy at " &
DirectCast(GameClass.NPCs(Entry.Key),
clsNPC).Position.ToString()
GameClass.GameForm.Invalidate()
GameClass.alex.InConversation =
True
End If
End
Select
Next
ElseIf
GameClass.alex.InConversation Then
'Since in this
tutorial, we're not going to do much with text,
'We'll just say that
text is nothing
clsText.Text = Nothing
GameClass.GameForm.Invalidate()
GameClass.alex.InConversation =
False
'In a future tutorial,
we'll say something like
' "If there's more
conversation remaining then advance the conversation "
' "Else, end
conversation
End
If
End
Sub
End
Class
Remember, we'll do the actual
dialogue class later. Just walk up to the guy and he'll give you his
position.
Now it works.
Making it more Event-Driven
The purpose of making our
program Event-Driven (or Self-Driven), is to eliminate any "manual"
code. The program should really run itself. We want to avoid what we
did earlier:
GameClass.NPCs.Add("poom", New clsNPC("poom",
New Point(3, 3), clsSprite.Dir.Down, "poom.dlg"))
GameClass.NPCs.Add("doom", New clsNPC("poom", New Point(4, 3),
clsSprite.Dir.Down, "poom.dlg"))
GameClass.NPCs.Add("boom", New clsNPC("poom", New Point(5, 3),
clsSprite.Dir.Down, "poom.dlg"))
GameClass.NPCs.Add("room", New clsNPC("poom", New Point(6, 3),
clsSprite.Dir.Down, "poom.dlg"))
We'll fix this, and much more, in the clsLevel class.
Public
Class clsLevel
Dim Reader
As
System.IO.StreamReader
Public
Sub LoadLevel(ByVal
Level As
String)
Reader =
New
System.IO.StreamReader("Levels\" & Level)
End Sub
End
Class
There's your basic class (It doesn't
do anything yet). As you can tell, we'll be creating a "level" file
and the program will read it and create the level.
Paste this into notepad (making sure you leave no extra spaces
- sometimes they appear at the end of each line for some random
reason, delete those)
lightworld.map
basis
5,5
NPCs:4
poom,poom,3,3,Down,poom.dlg
doom,poom,4,3,Up,doom.dlg
boom,poom,5,3,Left,boom.dlg
room,poom,6,3,Right,room.dlg
Create a folder in your
bin folder called Levels, and in it, save it as level1.lvl
Now for an explanation:
1) Map file
2) Tileset (our game only has one.... heh)
3) Default Character Position
4) Number of NPCs
5)
Name of NPC (aka the "key"), folder of NPC images, x, y, direction,
dialogue location (not used yet, we'll use this in another
tutorial)
6) Same as above
7) Same as above
8) Same as above
'A level file will look like:
'--------------------
'mapfile.map
'TileSet Name
'x,y <-- where x and y represent the character's tilepos
'NPCs:4 // The number of NPCs can be anything
'name of npc, folder of NPC images, x,y, direction, dialogue
location
'name of npc, folder of NPC images, x,y, direction, dialogue
location
'name of npc, folder of NPC images, x,y, direction, dialogue
location
'name of npc, folder of NPC images, x,y, direction, dialogue
location
Public
Class clsLevel
Dim Reader
As
System.IO.StreamReader
Public
Sub LoadLevel(ByVal
Level As
String)
Reader =
New
System.IO.StreamReader("Levels/" & Level)
Dim
map, tileset As
String
'Read the map
map = Reader.ReadLine
'Read the tileset
tileset = Reader.ReadLine
'Now create the map
GameClass.map =
New clsMap(map, tileset)
Dim
s() As
String
'Now read the position (the
line is written as "x,y")
s = Reader.ReadLine.Split(",")
'Since it is the tilepos, we
multiply by 30
GameClass.alex.Position.X
= CInt(s(0))
* 30
GameClass.alex.Position.Y =
CInt(s(1)) * 30
'Read the line that says "NPCs:4"
(where 4 is any number)
'and get that number
s = Reader.ReadLine.Split(":")
Dim
NumberOfNPCs As
Integer
NumberOfNPCs =
CInt(s(1))
Dim x
As Integer
Dim
dir As
String
Dim
direction As
clsSprite.Dir
'Loop through and add each and every NPC
For x
= 0 To
(NumberOfNPCs - 1)
'Now split all the NPC data.
For your reference:
's(0) = name
's(1) =
folder
's(2) = x
position
's(3) = y
position
's(4) =
direction
's(5) =
dialogue location
s = Reader.ReadLine.Split(",")
'Get the direction (5th item
when string is split by the comma)
dir = s(4)
'Now "translate" the string
direction into a specific direction (clssprite.dir)
Select
Case dir
Case
"Down"
direction = clsSprite.Dir.Down
Case
"Up"
direction = clsSprite.Dir.Up
Case
"Left"
direction = clsSprite.Dir.Left
Case
"Right"
direction = clsSprite.Dir.Right
End
Select
'Now create the NPC (this is a
very LONG/CONFUSING line!)
GameClass.NPCs.Add(s(0),
New
clsNPC(s(1), New
Point(CInt(s(2)),
CInt(s(3))),
direction, s(5)))
Next
'The
NPC will be rendered shortly after (since you added it to the NPC
list), so invalidate
GameClass.GameForm.Invalidate()
'Close the file
Reader.Close()
End Sub
End
Class
Well, it
might seem confusing. I highly suggest you take a look at the text
file and "pretend to be the compiler" as you do this. It's really
not at all that difficult.
Alright, now in Gameclass, replace:
Public Shared map As New clsMap("lightworld.map", "basis")
with
Public Shared map As clsMap
since our clsLevel class will
load the map for us.
In form1_Load, delete all the Game.NPCs.Add lines, and replace it
with one line:
First in gameclass:
'Here's our level class
Public Shared Level As New clsLevel()
And in form1_load (after you say Game = New GameClass(Me))
'Load level1
Game.Level.LoadLevel("level1.lvl")
Run it, the game should work as normal.
If you noticed, all of the game handling is done in GameClass
except for keypress (and “Do you really want to quit” but that’s not
important). You know the drill, let's keep code out of form1:
Delete the code out of form1_keydown, and move it into gameclass,
here's the code:
Public
Sub Keydown(ByVal sender
As Object,
ByVal e As
System.Windows.Forms.KeyEventArgs)
'Select case is just another way
of writing If statements.
If
Not GameClass.alex.InConversation
Then
Select
Case e.KeyCode
Case Keys.Up
alex.MoveUp()
Case Keys.Down
alex.MoveDown()
Case Keys.Left
alex.MoveLeft()
Case Keys.Right
alex.MoveRight()
Case Keys.Delete
GameForm.Invalidate()
End
Select
End
If
Select Case
e.KeyCode
Case Keys.Space
Events.CheckForEvents()
Case Keys.Escape
GameForm.Close()
End
Select
'Display his position.
Commented out because this is fullscreen and this isn't needed.
' GameClass.GameForm.Text
= "Current Position: " & alex.TilePos.ToString
End
Sub
Now in the constructor of Gameclass
(after you AddHandler the Paint sub):
AddHandler GameForm.KeyDown, AddressOf
Me.Keydown
Should work as normal.
Now let's take the screensettings class and move it over to form1.
In GameClass:
Private SetRes As ScreenSettings
In the constructor of GameClass(at the end):
SetRes = New ScreenSettings(640, 480)
In gameclass:
Public Sub Terminate()
SetRes.Dispose()
End Sub
Now back in form1, delete any traces of SetRes. In the
form1_closed event, say Game.Terminate().
I wish we could move
Me.SetStyle(ControlStyles.DoubleBuffer, True)
Me.SetStyle(ControlStyles.AllPaintingInWmPaint, True)
Me.SetStyle(ControlStyles.UserPaint, True)
to GameClass, but it's protected. So I guess we'll live with
that.
Conclusion
So basically, your entire app runs itself now. All you're doing is
feeding it text files and it reads them. Any non-programmer can make
these things (Text files, graphics...etc) for you easily. This
allows for group-projects to become easier.
You can take these set of classes to any project, and if you
have the graphics and the text files, it will run. Now as you've
noticed, there are some VERY "Strict" rules for using these classes.
For example, the resolution HAS to be 640x480, or positions will
mess up..... another example: the character HAS to be alex... the .lvl
files HAVE to be in the Levels directory, the tileheight and
tilewidths HAVE to be 30x30. This might get annoying for those of
you who are trying to use this (very small) engine as a base for
your project. This is why we'll have an Expandability tutorial, or I
might call it Flexibility or something.
One more thing, since the final outcome of this series is an RPG
Engine (an engine is a codebase that can be reused), I'll show you
how to make the Engine into a .dll file so anyone can use it. For
those of you with VS.NET Standard edition, you cannot create DLLs...
BUT ... there's a way around it. You'll have to thank a guy by the
name of Divil for that (I believe it's www.divil.co.uk).
This tutorial is borderlining on scripting, but you'll see [a lot]
more of that on the clsText class tutorial (the one that reads and
outputs dialogues, and can parse choicse in dialogues....etc!)
We'll have an optimization tutorial later.
I hope you've enjoyed this tutorial. I'm not sure what the next one
will be. I will definitely have to start working on the DirectX
tutorials and start getting out some APIs. The next tutorial
might be Playing Music, I'm not sure. I've been working on the
clsText class (I've never done anything like this before), and it
might be a while before I make a tutorial on it and perfect it.
Oh, of course, I forgot - we need to have a Map Editor
tutorial, creating these maps in a text file must be a REAL pain.
The map editor will create a .lvl file (which can store the tileset,
default character position, and <when we get to it> "warps" (meaning
when you go to the edge of a map it will take you somewhere), and
more).
The Battle Engine tutorial will be a BEAST. Battles are the
heart and the core of every RPG
I think I'm giving too much away. This series will be longer than I
thought. You might see as many as 15-20 tutorials instead of the 5-8
which I said earlier. The more, the better. Actually, I don't think
there's a stopping point :).
I hope you've enjoyed this tutorial.
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
|