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

vbProgramming | Tutorials | Games | RPG Programming III : Loading NPCs
    vbProgramming 
Tutorials -
RPG Programming Series:
Stage IV. Event-Driven Programming
 
     

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

 

 

This is the fourth tutorial in the RPG Programming Series. This will teach you how to make your application more event-driven (or self-driven).

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