Note: This is Part 4 of a CodeProject Article Series entitled WestWorld
. To get more background information for this article and for Parts 1-3, follow this link for WestWorld.
Introduction
A practical example of how to create agents that utilize finite state machines. In this article, I will attempt to explain how to implement FSM code using WinForms and GDI+. We are going to look at a game environment where agents inhabit an Old West-style gold mining town named West World. There are two inhabitants of WestWorld — a gold miner named Miner Bob and his Wife, Amanda.
WestWorld1
- Basics of FSM WestWorld2
- FSM with two agents (Miner & Wife) WestWorld3
- Includes FSM with Messaging between Agents WestWorld4
(this article) - Introduces WindowsForms GDI+ using RogueSharp
Background
The aim of this tutorial is to show you how to create a simple game, from scratch, without the help of higher level APIs like XNA or DirectX which automate half the process for you. All we will be using is a Windows Form and GDI+ to do some basic drawing, and for a little extra convenience, a few of the Form's events.
Map Generation
WestWorld
uses a modified VB.NET version of RogueSharp. Map Generation generates a randomly created unique map. Your map may appear different than the map shown.
RogueSharp is a free library written in C# to help roguelike developers get a head start on their game. RogueSharp provides many utility functions for dealing with map generation, field-of-view calculations, path finding, random number generation and more. It also contains strategies for making room and cave-like maps.
Most interactions with RogueSharp is based around the concept of a Map
which is a rectangular grid of Cells
.
Each Cell
in a Map
has the following properties:
IsTransparent
: true
if visibility extends through the Cell
IsWalkable
: true
if a the Cell
may be traversed by the player IsExplored
: true
if the player has ever had line-of-sight to the Cell
IsInFov
: true
if the Cell
is currently in the player's field-of-view
To instantiate a new Map
, you can use its constructor which takes a width and height and will create a new map of those dimensions with the properties of all Cells
set to false
.
Notice that the ToString()
operator is overridden on the Map
class to provide a simple visual representation of the map. An optional bool
parameter can be provided to ToString()
to indicate if you want to use the field-of-view or not. If the parameter is not given, it defaults to false
.
The symbols used are as follows:
%
: Cell
is not in field-of-view .
: Cell
is transparent, walkable, and in field-of-view s
: Cell
is walkable and in field-of-view (but not transparent) o
: Cell
is transparent and in field-of-view (but not walkable) #
: Cell
is in field-of-view (but not transparent or walkable)
A more interesting way to create a map is to use the Map
class' static
method Create
which takes an IMapCreationStrategy
. Some simple classes implementing IMapCreationStrategy
are provided with RogueSharp but this is easily extended by creating your own class that implements the strategy.
For the original version of RogueSharp, a NuGet package is available at https://www.nuget.org/packages/RogueSharp.
Sprite Rendering
To animate WestWorld
, we simply draw an image, and then move to the next frame, then we draw the next image in the sequence. So, we need to keep track of what frame of the animation we're on, how many frames are in the animation, the width and height of an individual sprite, and the direction in which to turn. To animate our images, we draw the image at (0, 32)
, and then on the next frame, draw the image at (33, 65)
, and repeat.
Dim sizeOfSprites As Integer = 32
Dim scale As Double = 0.5F
Dim rectPosition As New System.Drawing.Rectangle_
(New Point(X * sizeOfSprites, Y * sizeOfSprites)...
buffer.DrawImage(_Image, rectPosition, imageX, imageY, _Width, _Height, GraphicsUnit.Pixel)
The image is too large to fit on the screen. Therefore, let's just scale all of our sprites down to 1/2 size, 16 x 16 pixels. Using the DrawImage
method on the Bitmap
class allows us to set the scale of the sprite to draw based upon its current position.
Map Drawing
To actually see our map, we’re going to need to add some code to the Draw()
method in World.vb. We set a variable corresponding to the size of the sprites we are using for our floor and wall Cells. If you used the graphics provided, these are 64 pixels square. This value will be used to calculate where to draw each sprite. Next, we call the GetAllCells
() method on our newly created _map
. This returns an IEnumerable
<cell> of all Cells in the map that we can iterate through using a For Each
loop. As we iterate through each Cell
, we check the IsWalkable
property. This will determine if we should draw a floor. We calculate the position of where we should draw the sprite by looking at the corresponding Cell’s X
and Y
properties and multiplying it by the variable we set. This is where we make the actual calls to the Draw
method on our Bitmap
buffer
variable and provide either the floor or wall texture and the position we calculated earlier.
Public Sub Draw(ByVal g As Graphics)
Dim sizeOfSprites As Integer = 64
Dim scale As Double = 0.5F
Dim multiplier As Integer = CInt(sizeOfSprites * scale)
Dim myBrush As Brush = New SolidBrush(Color.Yellow)
For Each Cell As Cell In _map.GetAllCells
Dim Text As String = String.Format("X:{0}, Y:{1}", Cell.X, Cell.Y)
If Cell.IsWalkable And Cell.IsTransparent Then
Dim position As New System.Drawing.Point(CInt(Cell.X * multiplier), CInt(Cell.Y * multiplier))
g.DrawImage(_Grass, position)
Else
Dim position As New System.Drawing.Point(CInt(Cell.X * multiplier), CInt(Cell.Y * multiplier))
g.DrawImage(_Wall, position)
End If
Next
History
Credits