Fart Sniffer

Created by: X
Copyright Apr 18, 2006

URL: http://www.vbgamer.com/tutorial.asp?ndx=70


Page 1
Introduction

Author: Created by: X
Website: www.createdbyx.com

This tutorial will walk you through creating a number of flies that will fallow a scent trail, that you draw on the screen using the mouse. It will be designed to show how easy it is to code AI to achieve simple pathfinding/fallowing "fly like" behavior.

What you will need

For this tutorial we will be using vb.net 2005 and the .NET framework 2.0. But if you do not have Visual Studio 2005 you can download it for free on Microsoft’s website. This tutorial assumes that you are familiar with the visual studio IDE, as well as the graphics objects under the System.Drawing namespace.

Getting started

To begin you must first create a new windows application, under visual studio. Second you will need to add a new code file and insert the fallowing code into it.

  1. Public Module General
  2. Public Function RestrictValue(ByVal V As Single, ByVal Min As Single, ByVal Max As Single) As Single
  3.     If V < Min Then V = Min
  4.     If V > Max Then V = Max
  5.     Return V
  6. End Function
  7. Public Function RestrictValue(ByVal V As Integer, ByVal Min As Integer, ByVal Max As Integer) As Integer
  8.     If V < Min Then V = Min
  9.     If V > Max Then V = Max
  10.     Return V
  11. End Function
  12. Public Sub Displacement(ByVal X As Single, ByVal Y As Single, ByVal Distance As Single, ByVal AngleInRadians As Single, _
  13.                              ByRef NewX As Single, ByRef NewY As Single)
  14.     NewX = CSng(X + (System.Math.Cos(AngleInRadians) * Distance))
  15.     NewY = CSng(Y + (System.Math.Sin(AngleInRadians) * Distance))
  16. End Sub
  17. Public Function CircleCircle(ByVal Center1X As Single, ByVal Center1Y As Single, ByVal R1 As Single, _
  18.                            ByVal Center2X As Single, ByVal Center2Y As Single, ByVal R2 As Single,_
  19.                            Optional ByRef Distance As Single = 0) As Boolean
  20.      Distance = CSng(Math.Sqrt((Math.Abs(Center1X - Center2X) ^ 2) + (Math.Abs(Center1Y - Center2Y) ^ 2)))
  21.     Return Distance <= R1 + R2
  22. End Function
  23. End Module


The code provided above will be used by the application and provides simple helper functions. This code will no be covered in this tutorial as it is not relevant to the overall goal of this tutorial.


Part 1 - Base Types

First we will need to declare some class types to store the information we will need like Flies, Fly receptacles, and a Scent object.

Because the flies and the player we see on screen could be considered actors that share similar qualities we will declare an Actor class and then create a Fly class that inherits from the Actor class. We will not create a player class because the player will not possess any unique qualities other then what is already provided by the actor class.

  1. Public Class Actor
  2.    Public Position As Point
  3.    Public Direction As Single = 0
  4.    Public Speed As Single = 1
  5.    Public Size As Single = 6
  6. End Class


  1. Public Class Fly
  2.    Inherits Actor
  3.    Public Receptors As New Generic.List(Of Receptor)
  4.    Private mlngLastDirectionChange As Long
  5.    Public Sub Update()
  6.       If Now.Ticks > Me.mlngLastDirectionChange + (TimeSpan.TicksPerSecond \ 2) Then
  7.          Randomize(Now.Ticks)
  8.          Me.Direction = CSng(Rnd() * (Math.PI * 2))
  9.          Me.mlngLastDirectionChange = Now.Ticks
  10.       End If
  11.    End Sub
  12.    Public Sub New(ByVal Position As Point)
  13.       Me.Position = Position
  14.       Me.Speed = 6
  15.       mlngLastDirectionChange = CLng(Rnd() * TimeSpan.TicksPerSecond)
  16.    End Sub
  17.    Public Sub New(ByVal Position As Point, ByVal R As Generic.List(Of Receptor))
  18.       Me.New(Position)
  19.       Me.Receptors = R
  20.    End Sub
  21. End Class


You will notice that the Fly class contains a generic collection of Receptor objects. Receptors are like little antenna that we will use for detecting any scent that the Receptor may come in contact with. The flies we will be using for this tutorial will only be using 2 receptors.

  1. Public Class Fly
  2.    Inherits Actor
  3.    Public Receptors As New Generic.List(Of Receptor)
  4.    Private mlngLastDirectionChange As Long
  5.    Public Sub Update()
  6.       If Now.Ticks > Me.mlngLastDirectionChange + (TimeSpan.TicksPerSecond \ 2) Then
  7.          Randomize(Now.Ticks)
  8.          Me.Direction = CSng(Rnd() * (Math.PI * 2))
  9.          Me.mlngLastDirectionChange = Now.Ticks
  10.       End If
  11.    End Sub
  12.    Public Sub New(ByVal Position As Point)
  13.       Me.Position = Position
  14.       Me.Speed = 6
  15.       mlngLastDirectionChange = CLng(Rnd() * TimeSpan.TicksPerSecond)
  16.    End Sub
  17.    Public Sub New(ByVal Position As Point, ByVal R As Generic.List(Of Receptor))
  18.       Me.New(Position)
  19.       Me.Receptors = R
  20.    End Sub
  21. End Class

 
Next we will define a Scent class that will represent a scent in our application. The scent class contains properties like Strength which we will use to determine how large we should draw the scent on screen. It also contains two other properties DecayRate and DecaySpeed. DecayRate specifies how much the scent strength will be reduced. And DecaySpeed will be used to determine how fast to apply the DecayRate. The scent class also contains a field called Owner. Owner is not required in this tutorial, but it will be set to reference the player varible we will define later.

  1. Public Class Scent
  2.    Public Strength As Single = 25
  3.    Public DecayRate As Single = 8
  4.    Public DecaySpeed As Single = 1
  5.    Public Position As Point
  6.    Public Owner As Actor
  7.    Public Sub New(ByVal Owner As Actor)
  8.       Me.Owner = Owner
  9.       Me.position = Me.Owner.Position
  10.    End Sub
  11.    Public Sub Decay()
  12.       Static LastDecayTime As Long
  13.       Dim TheTime As Long = Now.Ticks
  14.       If TheTime > LastDecayTime + (TimeSpan.TicksPerSecond \ CLng(DecaySpeed)) Then
  15.          Me.Strength = RestrictValue(Me.Strength - Me.DecayRate, 0, Single.MaxValue)
  16.          LastDecayTime = TheTime
  17.       End If
  18.    End Sub   
  19. End Class



Part 2 - Declaring Variables

Now that we have all of our classes defined we can proceed to declare some variables. Open up the Code View for Form1 and copy and paste the fallowing code.

  1. Private Const NumberOfFlies As Integer = 25
  2. Private Const SpawnStinkyInterval As Integer = 1000 \ 30 ' 1000 ms div 30 fps
  3. Private Const UpdateFliesInterval As Integer = 1000 \ 60 ' 1000 ms div 60 fps
  4. Private mobjPlayer As Actor
  5. Private mobjFlies As Generic.List(Of Fly)
  6. Private mobjScents As Generic.List(Of Scent)
  7. Private WithEvents mobjTimer As Timers.Timer
  8. Private WithEvents mobjFlyUpdater As Timers.Timer
  9. Private WithEvents mobjStinkySpawner As Timers.Timer
  10. Private mobjGraphics As BufferedGraphics


The first 3 constants are as fallows..

  1. NumberOfFlies - Specifies how many flies our app will use.
  2. SpawnStinkyInterval - Specifies how many times per second the app will update the scent objects in the scene. The scent objects will be updated 30 times per second.
  3. UpdateFliesInterval - Specifies how many timer per second the app will update the flies in the scene. Flies will be updated 60 times per second.
After the constants is the player object, which is just defined as an actor. The next two variables are collections to store the flies and scent objects.
The next three variables after that are timers that will be used to update the flies and scent objects as well as draw them on screen at specified intervals.

The last variable is a BufferedGraphics object that is new in .NET 2.0 and we will use it to draw our graphics on screen. The BufferedGraphics object will help prevent any flickering on the screen when we draw our flies and scent objects.
 

Part 3 - Draw Methods

In order to see what the flies and scent objects are doing we will need to draw them. Copy and paste the fallowing code into Form1

  1. Public Sub DrawFlies()
  2.    For Each F As Fly In mobjFlies
  3.       DrawActor(F)
  4.    Next
  5. End Sub
  6. Public Sub DrawReceptors()
  7.    For Each F As Fly In mobjFlies
  8.       For Each R As Receptor In F.Receptors
  9.             Dim NX, NY As Single
  10.            Displacement(F.Position.X, F.Position.Y, R.Distance, R.Direction +F.Direction, NX, NY)
  11.             Dim Half As Single
  12.             Half = R.Size / 2.0F
  13.            mobjGraphics.Graphics.DrawLine(Pens.Yellow, F.Position.X, F.Position.Y,NX, NY)
  14.            mobjGraphics.Graphics.DrawEllipse(Pens.Yellow, NX - Half, NY - Half,R.Size, R.Size)
  15.       Next
  16.    Next
  17. End Sub
  18. Public Sub DrawScents()
  19.    For Each s As Scent In mobjScents
  20.       Dim Half As Single
  21.       Half = s.Strength / 2.0F
  22.       mobjGraphics.Graphics.DrawEllipse(Pens.Green, _
  23.                  s.Position.X - Half, s.Position.Y - Half, s.Strength, s.Strength)
  24.    Next
  25. End Sub
  26. Public Sub DrawActor(ByVal A As Actor)
  27.    Dim Half As Single
  28.    Half = A.Size / 2.0F
  29.    mobjGraphics.Graphics.DrawEllipse(Pens.Red, _
  30.               A.Position.X - Half, A.Position.Y - Half, A.Size, A.Size)
  31. End Sub


The methods for drawing our flies and scent objects are pretty straight forward, and should be easy enough to understand by looking at the code.
 

Part 4 - Form Events

Next we will need to handle some form events. When the user clicks the mouse on the form or presses a key it will cause the application to quit so copy and paste the fallowing code into Form1

  1. Private Sub Form_Click(ByVal sender As Object, ByVal e As System.EventArgs) Handles Me.Click
  2.    Me.Close()
  3. End Sub
  4. Private Sub Form_KeyDown(ByVal sender As Object, _
  5.                  ByVal e As System.Windows.Forms.KeyEventArgs) Handles Me.KeyDown
  6.    Me.Close()
  7. End Sub


We will also want the ability to drag a scent trail on the scene by using the mouse. To do this, set the player position to the position of the mouse as it moves across the form. Copy and paste the code below into Form1

  1. Private Sub Form_MouseMove(ByVal sender As Object, ByVal e As MouseEventArgs) Handles Me.MouseMove
  2.    mobjPlayer.Position = New Point(e.X, e.Y)
  3. End Sub


Next we will need to perform a check to see if the form is closing so we can dispose of the variables we have declared. We do this using the FormClosing event. If the form is not being canceled we can clean up our variables by calling the DoCleanUp method. Copy and paste the code below into Form1

  1. Private Sub Form1_FormClosing(ByVal sender As Object, _
  2.    ByVal e As System.Windows.Forms.FormClosingEventArgs) Handles Me.FormClosing
  3.    If Not e.Cancel Then Me.DoCleanUp()
  4. End Sub
  5. Private Sub DoCleanUp()
  6.    mobjTimer.Stop()
  7.    mobjTimer = Nothing
  8.    mobjStinkySpawner.Stop()
  9.    mobjStinkySpawner = Nothing
  10.    mobjFlyUpdater.Stop()
  11.    mobjFlyUpdater = Nothing
  12.    mobjScents.Clear()
  13.    mobjScents = Nothing
  14.    mobjPlayer = Nothing
  15.    mobjFlies.Clear()
  16.    mobjFlies = Nothing
  17.    mobjGraphics.Dispose()
  18.    mobjGraphics = Nothing
  19. End Sub


Finally we can add code to the forms Load event. Copy and paste the code below into Form1.

  1. Private Sub Form1_Load(ByVal sender As Object, ByVal e As System.EventArgs) Handles Me.Load
  2.    ' Resize form so that client size is 512x512
  3.    Me.Size = New Size(512, 512) + (Me.Size - Me.ClientSize)
  4.    ' create player object
  5.    mobjPlayer = New Actor
  6.    mobjPlayer.Position = New Point(Me.ClientSize.Width \ 2, Me.ClientSize.Height \ 2)
  7.    mobjPlayer.Size = 10
  8.    ' create flies and scent collections
  9.    mobjFlies = New Generic.List(Of Fly)
  10.    mobjScents = New Generic.List(Of Scent)
  11.    ' add flies and receptors (Changing receptor values of one fly will change receptor of all flies)
  12.    Dim R As New Generic.List(Of Receptor)
  13.    R.Add(New Receptor(10, -(Math.PI / 4), 5))
  14.    R.Add(New Receptor(10, Math.PI / 4, 5))
  15.    Randomize(Now.Ticks)
  16.    For idx As Integer = 0 To NumberOfFlies - 1
  17.       Dim F As Fly
  18.       F = New Fly(New Point(CInt(Rnd() * Me.ClientSize.Width), CInt(Rnd() * Me.ClientSize.Height)), R)
  19.       F.Direction = CSng(Rnd() * (Math.PI * 2))
  20.       mobjFlies.Add(F)
  21.    Next
  22.    ' Create graphics
  23.    Me.SetStyle(ControlStyles.AllPaintingInWmPaint Or ControlStyles.UserPaint, True)
  24.    Drawing.BufferedGraphicsManager.Current.MaximumBuffer = New Size(1, 1) + Me.ClientSize
  25.    mobjGraphics = Drawing.BufferedGraphicsManager.Current.Allocate(Me.CreateGraphics, Me.ClientRectangle)
  26.    ' setup timers
  27.    mobjTimer = New Timers.Timer
  28.    mobjTimer.Interval = 1
  29.    mobjTimer.AutoReset = False
  30.    mobjTimer.Start()
  31.    mobjStinkySpawner = New Timers.Timer
  32.    mobjStinkySpawner.Interval = SpawnStinkyInterval
  33.    mobjStinkySpawner.AutoReset = False
  34.    mobjStinkySpawner.Start()
  35.    mobjFlyUpdater = New Timers.Timer
  36.    mobjFlyUpdater.Interval = UpdateFliesInterval
  37.    mobjFlyUpdater.AutoReset = False
  38.    mobjFlyUpdater.Start()
  39. End Sub


The first thing we do is resize the form so the it’s client area is 512 wide by 512 high. Second we create the player and position it in the center of the form, as well as set it’s size to 10. The next thing is to create the flies and scent collections.

Next we create our flies. But before we do that we create a new collection that will contain the flies Receptor’s. The Receptor collection will have two receptors added to it. The first receptor will be a distance of 10 from the position of the fly, and facing -45 degrees from the direction the fly is facing. We also specify that the receptor has a size value of 5. The second receptor is the same as the first except that it will be +45 Degrees to the right from the direction the fly is facing. Keep in mind that every fly in the scene will be referencing these same 2 receptors that were declared. Each fly does not hold it’s own unique collection of receptors.

Now that we have a collection of receptors we can begin creating the flies. Each new fly we create will be randomly placed on the form and given a random starting direction.

After the flies have been created we specify that we want to control the painting on the form by calling the SetStyle method. Second we need to specify the maximum size of the buffer we will be drawing to. And third we allocate a new BufferedGraphics object by calling the allocate method and passing in a new graphics object that was created by the form, as well as the area on the form we will be drawing to.

The next thing to do is create the timer objects. Each timer has been setup to begin running, and after the interval of time has elapsed will raise it’s Elapsed event once.


Part 5 - Timer events
Copy and paste the fallowing code into Form1.

  1. Private Sub mobjTimer_Elapsed(ByVal sender As Object, ByVal e As System.Timers.ElapsedEventArgs) Handles mobjTimer.Elapsed
  2.    If mobjGraphics Is Nothing Then Exit Sub
  3.    Try
  4.       mobjGraphics.Graphics.Clear(Color.Black)
  5.       DrawFlies()
  6.       DrawReceptors()
  7.       DrawScents()
  8.       DrawActor(mobjPlayer)
  9.       KeepInBounds()
  10.       DecayFarts()
  11.       mobjGraphics.Graphics.DrawString("Clickor press a key to exit...", Me.Font, Brushes.White, 50, 50)
  12.       mobjGraphics.Render()
  13.       mobjTimer.Start()
  14.    Catch
  15.    End Try
  16. End Sub


The first thing we do is to check if the graphics variable has been set to nothing. If it has we can exit. We do this check because the timer objects we are using are running in the background and even if we were to close the form and set all variables to nothing the next Elapsed event will still be raised. Next we call the Clear method on the graphics variable to clear the scene of what was drawn previously. Then it proceeds to draw the flies, receptors, player, and scent objects onto the buffered graphics variable we setup earlier.

The next two methods being called are KeepInBounds and DecayFarts. These methods will be covered later in the tutorial.

Next we draw a message on the screen for the user and then render out what we have drawn out to the form.

The timer that is used to draw the graphics on screen has been setup to raise the Elapsed event only once. So we must call mobjTimer.Start again to receive another Elapsed event.

The fly updater and stinky spawner timers are simply setup to call the MoveFiles and MakeStinky methods. After that they call there Start methods so that there Elapsed events will fire again.

Copy and paste the fallowing code into Form1

  1. Private Sub mobjStinkySpawner_Elapsed(ByVal sender As Object, ByVal eAs System.Timers.ElapsedEventArgs) Handles mobjStinkySpawner.Elapsed
  2.    If mobjGraphics Is Nothing Then Exit Sub
  3.    Try
  4.       MakeStinky()
  5.       mobjStinkySpawner.Start()
  6.    Catch
  7.    End Try
  8. End Sub
  9. Private Sub mobjFlyUpdater_Elapsed(ByVal sender As Object, ByVal e AsSystem.Timers.ElapsedEventArgs) Handles mobjFlyUpdater.Elapsed
  10.    If mobjGraphics Is Nothing Then Exit Sub
  11.    Try
  12.       MoveFlies()
  13.       mobjFlyUpdater.Start()
  14.    Catch
  15.    End Try
  16. End Sub



Part 6 - Keeping things in view
The KeepInBounds method checks to see if the player is within the bounds of the forms client area and if not prevents it from moving outside that area. It then perform the same checking for each fly in the flies collection.

Copy and paste the fallowing code into Form1

  1. Private Sub KeepInBounds()
  2.    ' keep the player within the visible area of the form
  3.    mobjPlayer.Position.X = RestrictValue(mobjPlayer.Position.X, 0, Me.ClientSize.Width - 1)
  4.    mobjPlayer.Position.Y = RestrictValue(mobjPlayer.Position.Y, 0, Me.ClientSize.Height - 1)
  5.    ' keep all flies within the visible area of the form
  6.    For Each F As Fly In mobjFlies
  7.       F.Position.X = RestrictValue(F.Position.X, 0, Me.ClientSize.Width - 1)
  8.       F.Position.Y = RestrictValue(F.Position.Y, 0, Me.ClientSize.Height - 1)
  9.    Next
  10. End Sub



Part 7 - Fart Decay
The DecayFarts method process each scent in the scene and call’s it’s Decay method. It then checks to see if the strength of the scent is less or equal to zero if it is then it removes it from the collection, otherwise it moves on to check the next scent in the collection.
The Decay method checks to see if it is time for the scent to decay and if so reduces the scent strength by the DecayRate.

Copy and paste the fallowing code into Form1

  1. Public Sub DecayFarts()
  2.    Dim idx As Integer
  3.    While idx <= mobjScents.Count - 1
  4.       mobjScents(idx).Decay()
  5.       If mobjScents(idx).Strength <= 0 Then
  6.             mobjScents.RemoveAt(idx)
  7.       Else
  8.             idx += 1
  9.       End If
  10.    End While
  11. End Sub



Part 8 - Methane production
The only thing the MakeStinky method does is add a new scent to the scent collection. Because the Stinky Spawner timer will raise it's event 30 times per second, 30 scent objects will be created every second that passes. Scent objects that are created here are being removed by the DecayFarts method discussed earlier.

Copy and paste the code below into Form1

  1. Private Sub MakeStinky()
  2.     mobjScents.Add(New Scent(mobjPlayer))
  3. End Sub



Part 9 - Incoming!
Copy and paste the code below into Form1

  1. Private Sub MoveFlies()
  2.    Randomize(Now.Ticks)
  3.    For Each F As Fly In mobjFlies
  4.       Dim NX, NY As Single
  5.       Displacement(F.Position.X, F.Position.Y, F.Speed, F.Direction, NX, NY)
  6.       If NX > Me.ClientSize.Width - 1 Then F.Direction += CSng(Rnd() * Math.PI)
  7.       If NX < 0 Then F.Direction += CSng(Rnd() * Math.PI)
  8.       If NY > Me.ClientSize.Height - 1 Then F.Direction += CSng(Rnd() * Math.PI)
  9.       If NY < 0 Then F.Direction += CSng(Rnd() * Math.PI)
  10.       F.Position.X = CInt(NX)
  11.       F.Position.Y = CInt(NY)
  12.       Dim FoundScent As Boolean = False
  13.       For Each R As Receptor In F.Receptors
  14.            Displacement(F.Position.X, F.Position.Y, R.Distance, R.Direction +F.Direction, NX, NY)
  15.             For Each S As Scent In mobjScents
  16.               If CircleCircle(S.Position.X, S.Position.Y, S.Strength, NX, NY,R.Distance) Then
  17.                  F.Direction += R.Direction
  18.                  ' Try using this line instead
  19.                  'F.Direction += ((R.Direction * 0.9F) + (Rnd() * (R.Direction * 0.2F)))
  20.                   FoundScent = True
  21.                   Exit For
  22.                End If
  23.             Next
  24.       Next
  25.       If Not FoundScent Then F.Update()
  26.    Next
  27. End Sub


Finally after all that setup we can begin to code some AI. The MoveFlies method is at the heart of this application. First it re-seeds the random number generator, and then begins to process each of the flies in the flies collection.

The NX and NY variables are used to store the next location that the fly will be moving to. A call to the Displacement method is made and stores the new fly position in the NX and NY variables. If the new position is outside of the bounds of the form then a new random direction is given to the fly so that it does not try to constantly escape from view.

The FoundScent variable will store weather or not a scent was detected by a receptor. It then proceeds to process each receptor. To determine the location of the receptor on screen a call is made to the Displacement method again. Now that the location of the receptor is known it begins to check each scent in an effort to determine if the receptor collides with it. To determine if a collision takes place a call to the CircleCircle method is made.

The next line of code is how the fly knows what direction to move to, and how it is able to fallow a trail of scent objects.

  1. F.Direction += R.Direction


It takes the direction the fly is currently facing and adds the direction that the receptor is facing. Because the direction of the receptors are relative the fly will then be facing in the general direction of the scent.

Now that a scent has been detected we can set the FoundScent variable to true and exit the scent checking loop.

After all receptors have been processed it checks if a scent was found and if so calls the flies update method. The Fly.Update method checks to see if the fly has changed direction within the last half second, and if not changes the flies direction to a new random direction.

Conclusion

You should now be able to run the application! Admittedly the name Fart Sniffer is meant to be somewhat amusing. But the technique used could be applied to other kinds of AI path finding/fallowing such as a game where a scent trail is left behind by the player and a pack of wolves or demons use it to track down the players location.

As you can see from running the app the flies are not perfect, they sometimes travel backwards away from a stronger scent to a weaker scent, but with some minor tweaks you could direct the flies to always fallow a more stronger scent. One thing that could be done is to use the alternative way of setting the flies direction provided in the code in part 9.

  1. ' Try using this line instead
  2. 'F.Direction += ((R.Direction * 0.9F) + (Rnd() * (R.Direction * 0.2F)))


What this code will do is instead of simply adding the direction of the receptor to the fly's direction it takes 90 percent of the receptors direction and adds a random of 0 to 20 percent of the receptors direction. For example if the fly was facing 0 degrees and the receptors direction is 45 degrees then the new fly direction would be set to anywhere from 40 degrees to 50 degrees or so. This would add a little more detail to the fly's behavior instead of always making hard 45 degree turns all the time.

I hope you have found this tutorial to be useful. The full source code can be downloaded from the Created by X website here FartSniffer.zip If you are unable to download the file contact Created by: X using the contact info provided on the http://www.createdbyx.com/ website and ask for a copy of the file.