Chapter 15. Dialogue: Talking with NPCs

The purpose of this chapter is to build a dialogue system for the game so that the player’s character can talk with non-player characters (NPCs). This is important, because a dialogue system will allow the player to trade (buying and selling items), as well as allow the player to acquire and turn in quests to gain experience. The dialogue system developed in this chapter will be used to communicate with the player in many more ways beyond the single purpose of focus in this chapter.

Here’s what we’ll cover in this chapter:

Talking with NPCs

For a dialogue system to work well in the game, the first step is to allow the player to get the attention of an NPC, who will either be walking around or staying in a certain location, usually in a town, in a shop, or on the street. Talking with NPCs usually involves walking up close to them and hitting a “talk” button. In console games, that is usually one of the non-combat buttons (like the X or Y button on an Xbox 360 controller, for instance). Until you are in range of the NPC, you cannot talk with them. So, we’ll need to use the distance function to find out if we’re close enough to an NPC. Interestingly enough, this distance code will be used for combat in the next chapter as well—you know, so that you can only hit an enemy if you’re close enough to it.

Creating the “Vendor” NPC

Our example in this chapter has just one NPC—the vendor character. Normally, we would see many NPCs in a level file with a town in it, not just one character. But, this demo will be on the simple side so you can study the code and understand how this one NPC works. Adding more characters will then be no problem. We need to treat the NPCs like any other object in the game, and render it appropriately. Do you recall the Tree Demo from way back in Chapter 12, “Adding Objects to the World”? The same sort of code will be used now to draw the NPC, although we won’t use an array or list this time because there’s only one object. If you want to see a full-blown NPC system with many characters, scenery objects, and monsters, see the finished game in the last chapter!

We will use the character editor again to create the vendor character, which is shown in Figure 15.1. You’ll note in the figure that the vendor has no real attributes worthy of note. That is because the vendor cannot fight, and cannot even be attacked. Note also that only one image is used for the vendor’s so-called “animation” sheets, and the columns property is set to 1 for all three. This character doesn’t need to animate or move, which keeps things simple in the art requirement!

The Vendor character in the editor.

Figure 15.1. The Vendor character in the editor.

Starting a Conversation

Since both the player and the NPC are based on the Character class, we know that they both have the same properties, including a Position property that returns a PointF (which is like a regular Point structure but containing Singles instead of Integers). So, how do we use the position of the two characters to find the distance between them? First, we treat the position of each character as the endpoint of a line between them, as shown in Figure 15.2.

The center point of two sprites is used to calculate the distance between them.

Figure 15.2. The center point of two sprites is used to calculate the distance between them.

Hint

The properties were left out of the Character class in the previous chapter to save space. Some of the properties are mentioned in this chapter, which may be a surprise. Please open the project to see the complete source code (www.courseptr.com/downloads).

When using distance to determine whether two sprites are colliding, what we must do is calculate the center point of each sprite, calculate the radius of the sprite (from the center point to the edge), and then check the distance between the two center points. If the distance is less than the two radii combined, then you know the sprites are overlapping. Why? The radius of each sprite, when added together, should be less than the distance between the two sprites.

To calculate the distance between any two points, we can use the classic distance formula. Any two points can be converted into a right triangle by treating them as the end points of the two sides of that triangle, as shown in Figure 15.3. Take the delta value of the X and Y of each point, square each delta value, add them together, then take the square root, and you have the distance between the two points.

A triangle is used to calculate the distance between two points. Image courtesy of Wikipedia.

Figure 15.3. A triangle is used to calculate the distance between two points. Image courtesy of Wikipedia.

delta_x = x2 – x1
delta_y = y2 – y1

delta_x_squared = delta_x * delta_x
delta_y_squared = delta_y * delta_y
distance = square root ( delta_x_squared + delta_y_squared)

Here is a function that will meet our needs for calculating distance. This Distance() function would be most helpful if added to the Game class along with some overloaded parameters. We may also need a more specific function suited for the player’s standing position, so it might be helpful to add a helper function to the Character class that also calculates distance.

Function Distance(ByVal first As PointF, ByVal second As PointF) As Single
    Dim deltaX As Single = second.X - first.X
    Dim deltaY As Single = second.Y - first.Y
    Dim dist = Math.Sqrt(deltaX * deltaX + deltaY * deltaY)
    Return dist
End Function

But, we don’t want to just use the player’s raw X,Y position for the comparison. Remember back in Chapter 13, “Using Portals to Expand the World,” we had to compare the player’s foot position to see if he’s walking on a portal or not? The raw position gives the upper-left corner of the player sprite’s collision box. We need to use the same HeroFeet() function that returns an adjusted Point containing the coordinates of the player’s feet, as if the sprite is really walking on the tiled ground.

Private Function HeroFeet() As Point
    Return New Point(hero.X + 32, hero.Y + 32 + 16)
End Function

After deciding whether the player is close enough to an NPC to talk with it, the next step is to trigger a new dialogue mode in the game. We will want the rest of the game to pause while talking so nothing happens that the player would be unable to respond to in the game (like being attacked). This pause mode can be handled with a flag that causes some parts of the game to stop updating, but we will want them to be drawn. While that is happening, we do want to allow dialogue to happen, so this probably calls for a Dialogue class. But what should the class do?

The first example for this chapter, Dialogue Demo 1, shows how to calculate the distance between the hero and an NPC, displays the distance, and draws a “talk radius” circle around the NPC so you can see when the character is in range. By pressing the Space key when in range, a “talking” flag is triggered. Figure 15.4 shows the example. (The code to load the vendor and player characters will be shown in the second example later in this chapter.)

If the NPC is in range, then the player can begin a dialogue.

Figure 15.4. If the NPC is in range, then the player can begin a dialogue.

Without getting too deep into the complete source code listing, here is the key code from the first demo (there are three for this chapter). If the player is in range then the circle is drawn in blue to show that the player is in range. A line connecting both characters shows visually what the distance looks like from the precise locations from which it is calculated.

Private Sub doVendor()
    Dim relativePos As PointF
    REM draw the vendor sprite
    If vendor.X > level.ScrollPos.X _

    And vendor.X < level.ScrollPos.X + 23 * 32 _
    And vendor.Y > level.ScrollPos.Y _
    And vendor.Y < level.ScrollPos.Y + 17 * 32 Then
        relativePos.X = Math.Abs(level.ScrollPos.X - vendor.X)
        relativePos.Y = Math.Abs(level.ScrollPos.Y - vendor.Y)
        vendor.GetSprite.Draw(relativePos.X, relativePos.Y)
    End If

    Dim talkRadius As Integer = 70

    REM get center of hero sprite
    Dim heroCenter As PointF = HeroFeet()
    heroCenter.X += 16
    heroCenter.Y += 16
    game.Device.DrawRectangle(Pens.Red, heroCenter.X - 2, heroCenter.Y - 2, 4, 4)

    REM get center of NPC
    Dim vendorCenter As PointF = relativePos
    vendorCenter.X += vendor.GetSprite.Width / 2
    vendorCenter.Y += vendor.GetSprite.Height / 2
    game.Device.DrawRectangle(Pens.Red, vendorCenter.X - 2, vendorCenter.Y - 2, 4, 4)

    REM draw line connecting player to vendor
    Dim dist As Single = Distance(heroCenter, vendorCenter)
    Dim color As Pen
    If dist < talkRadius Then
        color = New Pen(Brushes.Blue, 2.0)
    Else
        color = New Pen(Brushes.Red, 2.0)
    End If
    game.Device.DrawLine(color, heroCenter, vendorCenter)

    REM print distance
    game.Print(relativePos.X, relativePos.Y, _
        "D = " + dist.ToString("N0"), Brushes.White)

    REM draw circle around vendor to show talk radius
    Dim spriteSize As Single = vendor.GetSprite.Width / 2
    Dim centerx As Single = relativePos.X + spriteSize

    Dim centery As Single = relativePos.Y + spriteSize
    Dim circleRect As New RectangleF( _
        centerx - talkRadius, _
        centery - talkRadius, _
        talkRadius * 2, talkRadius * 2)
    game.Device.DrawEllipse(color, circleRect)

    REM is playing trying to talk to this vendor?
    If dist < talkRadius Then
        If talkFlag Then
            talking = True
        End If
    Else
        talking = False
    End If
End Sub

Dialogue Choices

If our game had a mystery plot that required the player to interview dozens or perhaps hundreds of NPCs to find out “who dunnit,” then we would absolutely need another game editor to handle all of the complex interactions with these NPCs, with branching dialogue trees and variables so that the NPCs remember past conversations—or at least seem to remember them. We don’t want this level of complexity for Celtic Crusader. Basically, on each major area of the game, we want to have several NPCs that help the player: buying drop items, selling better gear, healing the player, and so on. These are pretty simple interactions. Since the inventory system and item editor won’t be coming along for a couple more chapters, we can’t offer up actual gear for the player to use, nor can we let the player sell drop items to the town vendors (yet). But we can get the framework in place so that these things are possible. In other words, we need a generic dialogue system with options that the player can select.

Continuing to think through the design considerations of our dialogue system, one assumption I’ll make now is that we will not have any complex graphical controls like scrolling lists, drop-down lists, or even anything like a scrollbar. The level of complexity for our GUI will end with buttons, and there will be a limited number of them. However, with the use of a state variable, we can create multiple levels for the dialogue system. Let’s say, first of all, there are two dialogue choices for a vendor:

  • BUY

  • SELL

If you choose the “BUY” option, then a variable is set so that a list of items for sale is displayed next. Then we just recycle the same dialogue with a different set of options (a limited list of items for sale).

  • Dagger

  • Short Sword

  • Barbarian Hammer

  • Leather Armor

  • Chain Armor

  • Plate Armor

  • More...

There are some limitations to this system, but with creative use of state variables you could offer an unlimited number of items by making the last button a More button that brings up a second page, and so on. One design consideration that you might want to consider is abandoning any sort of Back button in the dialogue system. I know it seems reasonable to let the player go back one level or page, but that tends to complicate things. It is easy enough to just end the dialogue and start it up again with the character, and I have seen many games take this approach.

Creating the Dialogue System

Now that we can determine whether the player is close enough to an NPC to talk with it, the next step is to bring up a dialog window and let the user interact with the NPC. First, we’ll incorporate the new dialogue helper functions and properties into the classes to make more effective use of them.

Making Eye Contact

We can still use the distance function to find out when the player is close to an NPC, but Distance() is obviously so reusable that it must be moved into the Game class. I’ll go a step further by adding an overload. Feel free to add any other variations of the core Game functions or properties that you would find useful. This new version works with individual coordinate values for the two points passed as parameters.

Public Function Distance(ByVal x1 As Single, ByVal y1 As Single, _
        ByVal x2 As Single, ByVal y2 As Single) As Single
    Dim first As New PointF(x1, y1)
    Dim second As New PointF(x2, y2)
    Return Distance(first, second)
End Function

The HeroFeet() function will work again, but it is becoming tiresome. Simple, yes, but it is not very reusable. I want a more generic version of this code actually built into the Character class. We have to make some more assumptions, about the tile size used in the game, but at this point we’re set on 32 × 32 so I won’t be concerned with that now. The HeroFeet() function has become the Character. FootPos property. This property is also now part of the Character class.

Public ReadOnly Property FootPos() As Point
    Get
        Return New Point(Me.X + 32, Me.Y + 32 + 16)
    End Get
End Property

Another helpful property that would greatly help to simplify our code is a CenterPos property for the Character class. The center of a character is its X,Y position plus its width and height, each divided by two.

Public ReadOnly Property CenterPos() As PointF
    Get
         Dim pos As PointF = Me.Position
         pos.X += Me.GetSprite.Width / 2
         pos.Y += Me.GetSprite.Height / 2
         Return pos
    End Get
End Property

Next, we’ll incorporate the distance calculations right inside the Character class while taking into account both the foot position and the center position of the character.

Public Function FootDistance(ByRef other As Character) As Single
    Return p_game.Distance(Me.FootPos, other.FootPos)
End Function
Public Function FootDistance(ByVal pos As PointF) As Single
    Return p_game.Distance(Me.FootPos, pos)
End Function
Public Function CenterDistance(ByRef other As Character) As Single
    Return p_game.Distance(CenterPos, other.CenterPos)
End Function
Public Function CenterDistance(ByVal pos As PointF) As Single
    Return p_game.Distance(Me.CenterPos, pos)
End Function

I’m going to use the CenterPos property this time, rather than FootPos, to simplify the example code a bit and show that both techniques work equally well when you just want to know if the player is close enough to an NPC to talk to it. When the player character is within range of the NPC and the talk radius circle turns blue, then press the Space key to begin talking.

Dialogue GUI

What do we want this dialogue system to look like? It needs to be simple and positioned in such a way that it doesn’t block out a large portion of the screen, but at the same time, it would be helpful to draw the dialogue interface as a window at a certain location every time. What about drawing the window at one of the four corners of the game window, depending on where the player character is located? It would be unfortunate to draw the dialogue window over the top of the player! That affects the user’s suspension of disbelief. The player wants to see his character while talking with an NPC, not interact in some sort of disembodied way. Let’s start by figuring out where the player is located and then drawing a box in an opposing corner—whichever corner is farthest from the player.

The Dialogue class will be reusable and serve many roles beyond just talking with NPCs. This will be our de facto way to communicate with the player, with either just simple messages requiring the click of an OK button to a more complex message with many choices. The Dialogue class will be self-contained, requiring very little from other classes except for Game.Device, which is needed to draw. We’re going to need to use a smaller font than the default font in the Game class. Although we have Game.SetFont() for changing that font, it will be too much of a pain to change the font back and forth after showing the dialogue text, so the dialogue system will use its own font. The dialogue window will be set up using properties and then drawn with a Draw() function, which will pause the game until the player chooses one of the options. Figure 15.5 shows a dialogue window positioned at the lower left with a size of one-quarter the screen (400 × 300).

A possible dialogue window position.

Figure 15.5. A possible dialogue window position.

Positioning the Dialogue Window

In my opinion, this window size is a bit too large, even if we give it some transparency with an alpha channel. While I would enjoy working on a resizable dialogue window, I’m not willing to get into the complexity of drawing button controls onto a variable-sized window—no, we need to keep this simple and enhance it as needed for the needs of each game. Let’s try a slightly smaller window with that alpha channel, shown in Figure 15.6. This screen mock-up shows the slightly smaller dialogue window (360 × 280) in the four corners. A border and shadow would certainly improve its appearance, but it already looks usable.

Positioning the dialogue window at any of the four corners.

Figure 15.6. Positioning the dialogue window at any of the four corners.

Hint

To create your own dialogue window with whatever level of transparency you want, use a graphic editor like GIMP, create a window with the resolution you want, and then use the Opacity slider or Layer, Mask menu option to change the alpha level of the image. An alpha level of 60% looks pretty good. However, we can also just draw a filled rectangle with whatever alpha level we want at runtime so that’s probably the best solution (although it’s a bit slower).

To automatically move the dialogue to one of the corners based on the player’s position, we’ll use an enumeration:

Public Enum Positions
   UpperLeft
   LowerLeft
   UpperRight
   LowerRight
End Enum

Now, based on the player’s current position, the dialogue window will automatically reposition itself to one of the four corners farthest away from the player.

Select Case p_corner
    Case Positions.UpperLeft
        p_position = New PointF(10, 10)
    Case Positions.LowerLeft
        p_position = New PointF(10, 600 - p_size.Height - 10)
    Case Positions.UpperRight
        p_position = New PointF(800 - p_size.Width - 10, 10)
    Case Positions.LowerRight
        p_position = New PointF(800 - p_size.Width - 10, _
            600 - p_size.Height - 10)
End Select

In our main game code, the automatic positioning of the dialogue window is handled. This could easily be moved inside the Dialogue class itself if you prefer.

If hero.CenterPos.X < 400 Then
    If hero.CenterPos.Y < 300 Then
        dialogue.setCorner(Dialogue.Positions.LowerRight)
    Else
        dialogue.setCorner(Dialogue.Positions.UpperRight)
    End If
Else
    If hero.CenterPos.Y < 300 Then
        dialogue.setCorner(Dialogue.Positions.LowerLeft)
    Else
        dialogue.setCorner(Dialogue.Positions.UpperLeft)
    End If
End If

Drawing the window is done with a call to Graphics.FillRectangle(). The trick here is to create a color that contains an alpha channel at the percentage of transparency that we want. Since the color values fall in the range of 0 to 255, one easy way to calculate the alpha level is to just multiply 255 by the desired percentage like so:

Dim pen As New Pen(Color.FromArgb(255 * 0.6, 255, 255, 255))
p_game.Device.FillRectangle(pen.Brush, _
    p_position.X, p_position.Y, p_size.Width, p_size.Height)

The color manipulation code is a bit tricky, because FillRectangle() doesn’t accept just a normal Color parameter, it must be a Brush. Since Pen can convert to a Brush, we can use a Pen with the desired Color components to arrive at a white rectangle with 60% alpha. The result is shown in Figure 15.7. See the example Dialogue Demo 2 to see the next step.

The dialogue automatically moves based on the player’s location.

Figure 15.7. The dialogue automatically moves based on the player’s location.

Hint

If the window size is just way too small for your needs, you could make a much taller window and just cause it to stay in place or allow it to automatically move from left to right instead of jumping among the four corners. A window of this type could be used for the player’s inventory system, for instance. For the dialogue system, I was thinking about keeping it smaller and using a small font.

Drawing the Dialogue Window

I think that’s all we need to build the dialogue window at this point. All of the items on the window will be positioned relative to the window’s position so that everything gets drawn in the right place even when the window moves. As for the interface, there will be a title, message text, and ten buttons, as shown in the design mock-up in Figure 15.8.

A mock-up of the dialogue user interface.

Figure 15.8. A mock-up of the dialogue user interface.

Drawing the Title

Let’s begin with the title. It should display the name of the character with whom you’re talking. The Title property, p_title, will be displayed in the Draw() function. To center the title on the dialogue window, we use a function called Graphics.MeasureString(), which returns the width and height of text according to the specified string and font. Using this information, we can get the width of the text and center it on the window.

Dim size As SizeF
size = p_game.Device.MeasureString(p_title, p_fontTitle)

Dim tx As Integer = p_position.X + p_size.Width / 2 - size.Width / 2
Dim ty As Integer = p_position.Y + 6
Print(tx, ty, p_title, Brushes.Gold)

Drawing the Message with Word Wrapping

Next up is the message text. This is the information an NPC wants to communicate to the player, be it for a quest or an introduction or any other purpose. We have quite a bit of room here for a lengthy message given the Arial-12 font. If you want to change the font for the title or the message, that could be done via properties. We again use Graphics.MesaureString(), but this time it is used to position multi-line text within a bounded region specified in the SizeF property layoutArea. Using the supplied dimensions, MeasureString() provides the minimum width and height needed to render the message with the specified font. It’s a very cool function!

Dim layoutArea As New SizeF(p_size.Width, 80)
size = p_game.Device.MeasureString(p_message, p_fontMessage, _
     layoutArea, Nothing, p_message.Length(), 4)
Dim layoutRect As New RectangleF(p_position.X + 4, _
    p_position.Y + 34, size.Width, size.Height)
p_game.Device.DrawString(p_message, p_fontMessage, Brushes.White, _
layoutRect)

Drawing the Buttons

Now we come to the buttons, and the most difficult aspect of the user interface. However, by making some assumptions we can keep the problem under control. First of all, let me state that there is a huge amount of variation in what you could potentially do with the dialogue buttons. With the right options in the form of enumeration values and some creative code, this could be an even more versatile dialogue system than planned. But, I don’t want to go all out with it at this point—keep it functional and simple, with the knowledge that it can handle more at a later time if needed.

A helper structure is needed to manage and draw the buttons. We don’t need to be concerned with the positionof each button, because they are simply enumerated and drawn in order, based on the dialogue’s properties. (A future enhancement to the user interface might require a position property for the buttons, though.) In fact, the Dialogue.Button structure doesn’t really resemble a button at all! There is no positional or dimensional information in the structure, just a text property. What gives?!

The structure is in place for future needs (such as the aforementioned features). We don’t need anything more than the text property, but putting it in a structure allows for much easier changes later.

For the first time we are actually going to use the mouse in our game code! That’s quite a statement now that we’re so heavily invested into 15 chapters, but until now we have not needed the mouse. The main form is covered up by the PictureBox that is created by the Game class and attached to the form, so we have to modify the PictureBox control in the Game class to support mouse input, oddly enough.

Private WithEvents p_pb As PictureBox
Private p_mousePos As Point
Private p_mouseBtn As MouseButtons

A new event handler is needed to support mouse input. This new function is added to the Game class and handles the events for MouseMove and MouseDown, which are both needed to get mouse movement and button clicks.

Private Sub p_pb_MouseInput(ByVal sender As Object, _
        ByVal e As System.Windows.Forms.MouseEventArgs) _
        Handles p_pb.MouseMove, p_pb.MouseDown
    p_mousePos.X = e.X
    p_mousePos.Y = e.Y
    p_mouseBtn = e.Button
End Sub

In support of the new mouse handler are these two properties in the Game class.

Public Property MousePos() As Point
    Get
         Return p_mousePos
    End Get
    Set(ByVal value As Point)
        p_mousePos = value
    End Set
End Property
Public Property MouseButton() As MouseButtons
    Get
         Return p_mouseBtn

    End Get
    Set(ByVal value As MouseButtons)
        p_mouseBtn = value
    End Set
End Property

Three private variables are needed to handle the buttons on the dialogue window.

Private p_buttons(10) As Dialogue.Button
Private p_numButtons As Integer
Private p_selection As Integer

The following code will cause the buttons to come to life when the mouse hovers over each button, and causes the Dialogue class to report which button was clicked.

Public Property NumButtons() As Integer
    Get
         Return p_numButtons
    End Get
    Set(ByVal value As Integer)
        p_numButtons = value
    End Set
End Property

Public Sub setButtonText(ByVal index As Integer, ByVal value As String)
    p_buttons(index).Text = value
End Sub

Public Function getButtonText(ByVal index) As String
    Return p_buttons(index).Text
End Function

Public Function getButtonRect(ByVal index As Integer) As Rectangle
    Dim i As Integer = index - 1
    Dim rect As New Rectangle(p_position.X, p_position.Y, 0, 0)
    rect.Width = p_size.Width / 2 - 4
    rect.Height = (p_size.Height * 0.4) / 5
    rect.Y += p_size.Height * 0.6 - 4
    Select Case index
        Case 1, 3, 5, 7, 9

            rect.X += 4
            rect.Y += Math.Floor(i / 2) * rect.Height
        Case 2, 4, 6, 8, 10
            rect.X += 4 + rect.Width
            rect.Y += Math.Floor(i / 2) * rect.Height
    End Select
    Return rect
End Function

Public Property Selection() As Integer
    Get
        Return p_selection
    End Get
    Set(ByVal value As Integer)
        p_selection = value
    End Set
End Property

Now we come to the Draw() function again. Previously, we have already added the code to draw the title and message onto the dialogue window. Now we need to write the code that draws the buttons and detects mouse movement and selection. This code is primarily based around the getButtonRect() function, which returns a Rectangle that represents the position and dimensions of the virtual button. This is then used to both draw the button and to look for mouse activity within its region.

REM draw the buttons
For n = 1 To p_numButtons
    Dim rect As Rectangle = getButtonRect(n)
    REM draw button background
    Dim color As Color
    If rect.Contains(p_mousePos) Then
        REM clicked on this button?
        If p_mouseBtn = MouseButtons.Left Then
            p_selection = n
        Else
            p_selection = 0
        End If
        color = color.FromArgb(200, 80, 100, 120)
        p_game.Device.FillRectangle(New Pen(color).Brush, rect)
    End If

   REM draw button border
   p_game.Device.DrawRectangle(Pens.Gray, rect)
   REM print button label
   size = p_game.Device.MeasureString(p_buttons(n).Text, p_fontButton)
   tx = rect.X + rect.Width / 2 - size.Width / 2
   ty = rect.Y + 2
   p_game.Device.DrawString(p_buttons(n).Text, p_fontButton, _
       Brushes.White, tx, ty)
Next

Final Example

I promised to go over the source code for a complete example before ending this chapter, so we’ll do that now. There are three Dialogue Demo programs in this chapter’s resource files (found at www.courseptr.com/downloads) that you can open and study one step at a time, from the initial code to calculate distance between the characters to the opening and positioning of the dialogue window to the full user interface. Figure 15.9 shows the final GUI for the dialogue window with all of the buttons filled with sample items for purchase, while Figure 15.10 shows that the game responds to the selection after the dialogue window is closed.

Public Class Form1
    Public Structure keyStates
        Public up, down, left, right As Boolean
    End Structure
    Private game As Game
    Private level As Level
    Private keyState As keyStates
    Private gameover As Boolean = False
    Private hero As Character
    Private vendor As Character
    Private talkFlag As Boolean = False
    Private talking As Boolean = False
    Private dialogue As Dialogue
    Private purchase As String = ""

    Private Sub Form1_Load(ByVal sender As System.Object, _
            ByVal e As System.EventArgs) Handles MyBase.Load
       Me.Text = "NPC Dialogue Demo 3"
The final Dialogue Demo program responds to user choices.

Figure 15.9. The final Dialogue Demo program responds to user choices.

Displaying the item selected in the dialogue.

Figure 15.10. Displaying the item selected in the dialogue.

REM create game object
game = New Game(Me, 800, 600)
REM create tilemap
level = New Level(game, 25, 19, 32)
level.loadTilemap("sample.level")

    level.loadPalette("palette.bmp", 5)
    REM load hero
    hero = New Character(game)
    hero.Load("paladin.char")
    hero.Position = New Point(300, 200)
    REM load vendor
    vendor = New Character(game)
    vendor.Load("vendor.char")
    vendor.Position = New Point(350, 250)

    REM create dialogue window
    dialogue = New Dialogue(game)

    While Not gameover
         doUpdate()
    End While
End Sub

Private Sub Form1_KeyDown(ByVal sender As Object, _
    ByVal e As System.Windows.Forms.KeyEventArgs) Handles Me.KeyDown
    Select Case (e.KeyCode)
       Case Keys.Up, Keys.W : keyState.up = True
       Case Keys.Down, Keys.S : keyState.down = True
       Case Keys.Left, Keys.A : keyState.left = True
       Case Keys.Right, Keys.D : keyState.right = True
       Case Keys.Space : talkFlag = True
    End Select
End Sub

Private Sub Form1_KeyUp(ByVal sender As System.Object, _
    ByVal e As System.Windows.Forms.KeyEventArgs) Handles MyBase.KeyUp
    Select Case (e.KeyCode)
       Case Keys.Escape : End
       Case Keys.Up, Keys.W : keyState.up = False
       Case Keys.Down, Keys.S : keyState.down = False
       Case Keys.Left, Keys.A : keyState.left = False
       Case Keys.Right, Keys.D : keyState.right = False
       Case Keys.Space : talkFlag = False
    End Select
End Sub

Private Sub doUpdate()
    Dim frameRate As Integer = game.FrameRate()
    Dim ticks As Integer = Environment.TickCount()
    Static drawLast As Integer = 0
    If ticks > drawLast + 16 Then
        drawLast = ticks
        doScrolling()
        doHero()
        doVendor()
        doDialogue()
        If purchase <> "" Then
            game.Print(hero.Position.X, hero.Position.Y, purchase, _
                Brushes.White)
        End If
        game.Update()
        Application.DoEvents()
    Else
        Threading.Thread.Sleep(1)
    End If
End Sub

Private Sub doScrolling()
    REM move the tilemap scroll position
    Dim steps As Integer = 4
    Dim pos As PointF = level.ScrollPos
    REM up key movement
    If keyState.up Then
        If hero.Y > 300 - 48 Then
            hero.Y -= steps
        Else
            pos.Y -= steps
            If pos.Y <= 0 Then
                hero.Y -= steps
            End If
        End If
        REM down key movement
    ElseIf keyState.down Then
        If hero.Y < 300 - 48 Then
            hero.Y += steps
        Else

            pos.Y += steps
            If pos.Y >= (127 - 19) * 32 Then
                hero.Y += steps
            End If
        End If
    End If
    REM left key movement
    If keyState.left Then
        If hero.X > 400 - 48 Then
            hero.X -= steps
        Else
            pos.X -= steps
            If pos.X <= 0 Then
                 hero.X -= steps
            End If
        End If
        REM right key movement
    ElseIf keyState.right Then
        If hero.X < 400 - 48 Then
           hero.X += steps
        Else
            pos.X += steps
            If pos.X >= (127 - 25) * 32 Then
                 hero.X += steps
            End If
        End If
    End If
    REM update scroller position
    level.ScrollPos = pos
    level.Update()
    REM draw the tilemap
    level.Draw(0, 0, 800, 600)
End Sub

Private Sub doHero()
    REM limit player sprite to the screen boundary
    If hero.X < -32 Then
        hero.X = -32
    ElseIf hero.X > 800 - 65 Then
        hero.X = 800 - 65

    End If
    If hero.Y < -48 Then
        hero.Y = -48
    ElseIf hero.Y > 600 - 81 Then
        hero.Y = 600 - 81
    End If
    REM orient the player in the right direction
    If keyState.up And keyState.right Then
        hero.Direction = 1
    ElseIf keyState.right And keyState.down Then
        hero.Direction = 3
    ElseIf keyState.down And keyState.left Then
        hero.Direction = 5
    ElseIf keyState.left And keyState.up Then
        hero.Direction = 7
    ElseIf keyState.up Then
        hero.Direction = 0
    ElseIf keyState.right Then
        hero.Direction = 2
    ElseIf keyState.down Then
        hero.Direction = 4
    ElseIf keyState.left Then
        hero.Direction = 6
    Else
        hero.Direction = -1
    End If
    REM draw the hero
    hero.Draw()
End Sub

Private Sub doVendor()
    Dim relativePos As PointF
    REM draw the vendor sprite
    If vendor.X > level.ScrollPos.X _
    And vendor.X < level.ScrollPos.X + 23 * 32 _
    And vendor.Y > level.ScrollPos.Y _
    And vendor.Y < level.ScrollPos.Y + 17 * 32 Then
         relativePos.X = Math.Abs(level.ScrollPos.X - vendor.X)
         relativePos.Y = Math.Abs(level.ScrollPos.Y - vendor.Y)
         vendor.GetSprite.Draw(relativePos.X, relativePos.Y)

    End If
    Dim talkRadius As Integer = 70

    REM get center of hero sprite
    Dim heroCenter As PointF = hero.CenterPos

    REM get center of NPC
    Dim vendorCenter As PointF = relativePos
    vendorCenter.X += vendor.GetSprite.Width / 2
    vendorCenter.Y += vendor.GetSprite.Height / 2

    REM get distance to the NPC
    Dim dist As Single = hero.CenterDistance(vendorCenter)
    Dim color As Pen
    If dist < talkRadius Then
        color = New Pen(Brushes.Blue, 2.0)
    Else
        color = New Pen(Brushes.Red, 2.0)
    End If

    REM draw circle around vendor to show talk radius
    Dim spriteSize As Single = vendor.GetSprite.Width / 2
    Dim centerx As Single = relativePos.X + spriteSize
    Dim centery As Single = relativePos.Y + spriteSize
    Dim circleRect As New RectangleF( _
        centerx - talkRadius, _
        centery - talkRadius, _
        talkRadius * 2, talkRadius * 2)
    game.Device.DrawEllipse(color, circleRect)

    REM is playing trying to talk to this vendor?
    If dist < talkRadius Then
        If talkFlag Then
            talking = True
        End If
    Else
        talking = False
    End If
End Sub

Private Sub doDialogue()
    If Not talking Then Return
    REM prepare the dialogue
    dialogue.Title = "Bartholomu The Cheapskate"
    dialogue.Message = "Greetings visitor. Oh my goodness, " + _
        "you look like you've seen a bit of action. I ain't got " + _
        "much, y'know, but ye'll be get'n a fair price! Better'n " + _
        "you'll ever get over at Nathan's, I tell ye that much!"
    dialogue.setButtonText(1, "2s Rusty Dagger (1-4 dmg)")
    dialogue.setButtonText(2, "3s Wool Clothes (1 AC)")
    dialogue.setButtonText(3, "10s Short Sword (2-8 dmg)")
    dialogue.setButtonText(4, "15s Leather Armor (4 AC)")
    dialogue.setButtonText(5, "25s Long Sword (6-12 dmg)")
    dialogue.setButtonText(6, "50s Chain Armor (10 AC)")
    dialogue.setButtonText(7, "30s Long Bow (4-10 dmg)")
    dialogue.setButtonText(8, "90s Plate Armor (18 AC)")
    dialogue.setButtonText(9, "80s Doom Ring (+2 STR)")
    dialogue.setButtonText(10, "45s Bone Necklace (+2 STA)")

    REM reposition dialogue window
    If hero.CenterPos.X < 400 Then
        If hero.CenterPos.Y < 300 Then
            dialogue.setCorner(Dialogue.Positions.LowerRight)
        Else
           dialogue.setCorner(Dialogue.Positions.UpperRight)
        End If
    Else
        If hero.CenterPos.Y < 300 Then
            dialogue.setCorner(Dialogue.Positions.LowerLeft)
        Else
            dialogue.setCorner(Dialogue.Positions.UpperLeft)
        End If
    End If

    REM draw dialogue and look for selection
    dialogue.updateMouse(game.MousePos, game.MouseButton)
    dialogue.Draw()
    If dialogue.Selection > 0 Then
         talking = False
         purchase = "You bought " + _

               dialogue.getButtonText(dialogue.Selection)
           dialogue.Selection = 0
        Else
            purchase = ""
        End If
    End Sub
End Class

Level Up!

This was a heavy-hitting chapter that covered some tough new material, but it was absolutely necessary and we ended up with a powerful new user interface class that can be used for many different parts of the Celtic Crusader game. Speaking of which, in the very next chapter, it will be time to learn about fighting, gaining experience, and leveling! We have a few minor things to fix by the time we get to the final game in the last chapter, such as character sprites jumping a bit as we near the edge of the level, but it’s nothing that a little extra code won’t fix.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
18.189.31.26