It took a few prompts for ChatGPT 3.5 to create it, but I had to fix some of it's mistakes. From Wikipedia, for those who don't know what a boids simulation is, it's an artificial life program, developed by Craig Reynolds in 1986, which simulates the flocking behaviour of birds, and related group motion.
In this boids simulation project you can change the number of boids in the simulation and pause their movement. 4 boids is the smallest size a flock can be. The boids avoid the mouse cursor, so you can observe the new counts of flocks, trios, pairs and lone boids. It runs fine when you resize the form.
The capture above is from the 1st version of the boids simulation project. The updated winforms and Wpf versions have filled-in triangles, and can handle up to 1000 boids.
The code below is also from the 1st version. It needs 1 panel docked at the bottom and a PictureBox to fill the remaining space in the Form. It also needs 2 timers named "SimulationTimer" and "CountingTimer", 7 labels, a textbox, and two buttons. Button2 is the pause button.
The code has enough comments to explain everything. All required classes will be in the next two comments.
Main class:
Code:
Option Strict On
Public Class Form1
' Maximum speed of boids
Private MaxSpeed As Integer = 7
' Range within which boids perceive and interact with each other
Public VisualRange As Integer = 40
' Range within which boids avoid collisions
Private ProtectedRange As Integer = 20
' List to store individual boid instances representing the flock.
Private BoidsList As List(Of Boid)
' Buffered bitmap for off-screen rendering
Private buffer As Bitmap
' Variables for mouse cursor avoidance.
Private BoidsMousePosition As Point
Private MouseAvoidanceThreshold As Double = 25
' Instances for alignment, cohesion, and separation rules for boid behavior.
' MaxSpeed parameter is set to the maximum speed allowed for boids.
Private alignmentRule As New Alignment(MaxSpeed)
Private cohesionRule As New Cohesion()
Private separationRule As New Separation()
' The initial number of boids to be created and initialized
Private Const InitialBoidCount As Integer = 400
' Flag to track pause state
Private IsPaused As Boolean = False
' BoidCounterInstance represents an instance of the BoidCounter class,
' which is responsible for counting various configurations of boids within a specified visual range.
Public BoidCounterInstance As New BoidCounter()
Private Sub Form1_Load(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles MyBase.Load
' Set the DoubleBuffered property to True to enable double buffering
Me.DoubleBuffered = True
' Initialize the buffered bitmap
buffer = New Bitmap(PictureBox1.ClientSize.Width, PictureBox1.ClientSize.Height)
' Set VisualRange for BoidCounterInstance
BoidCounterInstance.VisualRange = VisualRange
' Initialize the list of boids
BoidsList = New List(Of Boid)()
InitializeBoids(InitialBoidCount)
' Start the simulation timer
SimulationTimer.Start()
CountingTimer.Start()
End Sub
' Initializes a specified number of boids with random positions and velocities.
Private Sub InitializeBoids(ByVal count As Integer)
' Create a random number generator.
Dim random As Random = New Random()
' Generate boids with random positions and velocities.
For i As Integer = 0 To count - 1
Dim boid As Boid = New Boid()
boid.Location = New Point(random.Next(0, PictureBox1.ClientSize.Width), random.Next(0, PictureBox1.ClientSize.Height))
boid.Velocity = New Point(random.Next(-2, 3), random.Next(-2, 3))
BoidsList.Add(boid)
Next
End Sub
Private Sub SimulationTimer_Tick(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles SimulationTimer.Tick
If Not IsPaused Then
' Update boids position
For Each boid As Boid In BoidsList
UpdateBoid(boid)
Next
' Draw boids to the buffer
DrawBoidsToBuffer()
End If
End Sub
' Updates the position and behavior of a given boid based on its interactions with other boids and the environment.
Private Sub UpdateBoid(ByVal boid As Boid)
' Variables to store averages, counts, and distances for boid interactions
Dim averageXposition, averageYposition, averageXvelocity, averageYvelocity, neighboringBoids, closeDx, closeDy As Double
' Loop through every other boid in the flock
For Each otherBoid As Boid In BoidsList
' Check if the boid is not itself
If Not boid.Equals(otherBoid) Then
Dim dx As Double = boid.Location.X - otherBoid.Location.X
Dim dy As Double = boid.Location.Y - otherBoid.Location.Y
Dim squaredDistance As Double = dx * dx + dy * dy
' Check if the boids are within the protected range
If squaredDistance < ProtectedRange * ProtectedRange Then
closeDx += boid.Location.X - otherBoid.Location.X
closeDy += boid.Location.Y - otherBoid.Location.Y
ElseIf squaredDistance < VisualRange * VisualRange Then
' Update averages for boids within the visual range
averageXposition += otherBoid.Location.X
averageYposition += otherBoid.Location.Y
averageXvelocity += otherBoid.Velocity.X
averageYvelocity += otherBoid.Velocity.Y
neighboringBoids += 1
End If
End If
Next
' Check if there are neighboring boids
If neighboringBoids > 0 Then
' Calculate average velocity and position
Dim avgVelocity As New Point(CInt(averageXvelocity / neighboringBoids), CInt(averageYvelocity / neighboringBoids))
Dim avgPosition As New Point(CInt(averageXposition / neighboringBoids), CInt(averageYposition / neighboringBoids))
' Calculate distance to the mouse
Dim mouseDistance As Double = Math.Sqrt((boid.Location.X - BoidsMousePosition.X) ^ 2 + (boid.Location.Y - BoidsMousePosition.Y) ^ 2)
' Skip alignment and cohesion when boid is near the mouse
If mouseDistance > VisualRange Then
' Apply alignment rule
alignmentRule.ApplyAlignmentRule(boid, avgVelocity)
' Apply cohesion rule
cohesionRule.ApplyCohesionRule(boid, avgPosition)
End If
End If
' Apply separation rule
separationRule.ApplySeparationRule(boid, closeDx, closeDy)
' Move boid
boid.Location = New Point(boid.Location.X + boid.Velocity.X, boid.Location.Y + boid.Velocity.Y)
' Apply mouse avoidance
Dim distanceToMouse As Double = Math.Sqrt((boid.Location.X - BoidsMousePosition.X) ^ 2 + (boid.Location.Y - BoidsMousePosition.Y) ^ 2)
If distanceToMouse < MouseAvoidanceThreshold Then
' Adjust velocity to avoid the mouse more strongly
Dim angle As Double = Math.Atan2(boid.Location.Y - BoidsMousePosition.Y, boid.Location.X - BoidsMousePosition.X)
boid.Velocity = New Point(CInt(boid.Velocity.X + Math.Cos(angle) * 4), CInt(boid.Velocity.Y + Math.Sin(angle) * 4))
End If
' Keep boids within the PictureBox boundaries
If boid.Location.X < 0 Then
boid.Location = New Point(0, boid.Location.Y)
boid.Velocity = New Point(-boid.Velocity.X, boid.Velocity.Y)
ElseIf boid.Location.X > PictureBox1.ClientSize.Width Then
boid.Location = New Point(PictureBox1.ClientSize.Width, boid.Location.Y)
boid.Velocity = New Point(-boid.Velocity.X, boid.Velocity.Y)
End If
If boid.Location.Y < 0 Then
boid.Location = New Point(boid.Location.X, 0)
boid.Velocity = New Point(boid.Velocity.X, -boid.Velocity.Y)
ElseIf boid.Location.Y > PictureBox1.ClientSize.Height Then
boid.Location = New Point(boid.Location.X, PictureBox1.ClientSize.Height)
boid.Velocity = New Point(boid.Velocity.X, -boid.Velocity.Y)
End If
' Normalize velocity
Dim length As Double = Math.Sqrt(boid.Velocity.X ^ 2 + boid.Velocity.Y ^ 2)
If length > MaxSpeed Then
' Scale velocity back to the maximum speed
boid.Velocity = New Point(CInt(MaxSpeed * boid.Velocity.X / length), CInt(MaxSpeed * boid.Velocity.Y / length))
End If
End Sub
Private Sub CountingTimer_Tick(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles CountingTimer.Tick
' Set the BoidsList property before counting
BoidCounterInstance.BoidsList = BoidsList
' Counts flocks, trios, pairs, and lone boids
BoidCounterInstance.CountFlocksTriosPairsLoners(BoidsList)
' Update labels outside the loop with the calculated counts.
Label1.Text = "Flocks: " & BoidCounterInstance.FlockCount
Label2.Text = "Trios: " & BoidCounterInstance.TrioCount
Label3.Text = "Pairs: " & BoidCounterInstance.PairCount
Label4.Text = "Loners: " & BoidCounterInstance.LoneCount
End Sub
Private Sub PictureBox1_MouseMove(ByVal sender As Object, ByVal e As System.Windows.Forms.MouseEventArgs) Handles PictureBox1.MouseMove
' Update mouse position
BoidsMousePosition = e.Location
End Sub
Private Sub DrawBoidsToBuffer()
' Draw boids as small narrow triangles on the off-screen buffer
Using g As Graphics = Graphics.FromImage(buffer)
g.Clear(Color.White) ' You can choose a different background color if needed
For Each boid As Boid In BoidsList
' Calculate the angle of the velocity vector
Dim angle As Double = Math.Atan2(boid.Velocity.Y, boid.Velocity.X) ' The angle now directly represents the direction of the top of the triangle
' Calculate the points of the triangle
Dim points As Point() = {
New Point(boid.Location.X + CInt(7 * Math.Cos(angle)), boid.Location.Y + CInt(8 * Math.Sin(angle))),
New Point(boid.Location.X + CInt(3 * Math.Cos(angle - 2 * Math.PI / 3)), boid.Location.Y + CInt(3 * Math.Sin(angle - 2 * Math.PI / 3))),
New Point(boid.Location.X + CInt(3 * Math.Cos(angle + 2 * Math.PI / 3)), boid.Location.Y + CInt(3 * Math.Sin(angle + 2 * Math.PI / 3)))
}
' Draw the triangle (body) on the off-screen buffer
g.DrawPolygon(Pens.Blue, points)
Next
End Using
' Trigger the Paint event to display the buffered bitmap on the PictureBox
PictureBox1.Invalidate()
End Sub
Private Sub PictureBox1_Paint(ByVal sender As Object, ByVal e As System.Windows.Forms.PaintEventArgs) Handles PictureBox1.Paint
' Draw the buffered image onto the PictureBox
e.Graphics.DrawImage(buffer, 0, 0)
End Sub
Private Sub PictureBox1_SizeChanged(ByVal sender As Object, ByVal e As System.EventArgs) Handles PictureBox1.SizeChanged
' Resize the buffered bitmap when the PictureBox size changes
buffer = New Bitmap(PictureBox1.ClientSize.Width, PictureBox1.ClientSize.Height)
' Update the label displaying the display area size
Label7.Text = "Display Area: " & PictureBox1.ClientSize.Width & " x " & PictureBox1.ClientSize.Height
' Invalidate the form to trigger a redraw
Me.Invalidate()
End Sub
Private Sub Panel1_Paint(ByVal sender As System.Object, ByVal e As System.Windows.Forms.PaintEventArgs) Handles Panel1.Paint
' Draw a black line at the top of the panel
Dim topBorderPen As New Pen(Color.Black)
e.Graphics.DrawLine(topBorderPen, 0, 0, Panel1.Width, 0)
End Sub
Private Sub Button1_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles Button1.Click
' Attempt to parse the input as an integer
Dim newBoidCount As Integer
If Integer.TryParse(TextBox1.Text, newBoidCount) Then
If CInt(TextBox1.Text) > 1000 Then
' Input integer value is too high, display an error message
MessageBox.Show("The integer value is too high")
Else
' Input is a valid integer, reset the list and initialize new boids
BoidsList = New List(Of Boid)()
InitializeBoids(newBoidCount)
' Check if the simulation is paused and unpause if needed
If IsPaused Then
IsPaused = False
' Update button text when resumed
PauseButton.Text = "Pause"
End If
End If
Else
' Input is not a valid integer, display an error message
MessageBox.Show("Invalid input for boid count." & vbCrLf & vbCrLf & "Please enter a valid integer value.")
' clears invalid input
TextBox1.Text = ""
End If
End Sub
Private Sub PauseButton_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles PauseButton.Click
' Toggle the pause state
IsPaused = Not IsPaused
' Update button text based on the pause state
If IsPaused Then
' Update button text when paused
PauseButton.Text = "Resume"
Else
' Update button text when resumed
PauseButton.Text = "Pause"
End If
End Sub
End Class
The boids simulation was coded in Visual Basic 2010, Framework 4, but the code provided above and in the next two comments should work with any Framework.
The "ChatGPT Boid Simulation Update.zip" project's and "Wpf Boids Simulation.zip" project's performance is the same. The only thing, when you minimize the ""ChatGPT Boid Simulation Update.zip" version to the taskbar, it crashes. I can't figure out why it does that.
The capture above was taken soon after I changed the boid count. The boids eventually unite into two or 3 flocks. Large flocks sometimes split-up.
Update, Dec 8th:
I updated the WPF boids simulation project zip below. It no longer goes by a previous canvas size when changing the boid count, but the actual canvas size after resizing the window. It also displays the actual canvas size while resizing.
Last edited by Peter Porter; Dec 8th, 2023 at 09:22 AM.
' Represents a boid with properties for its location and velocity.
Public Class Boid
Public Property Location As Point
Public Property Velocity As Point
End Class
Alignment class:
Code:
' For applying the alignment rule to boids.
Public Class Alignment
Private MaxSpeed As Integer
' Constructor to initialize the Alignment class with the maximum speed.
Public Sub New(ByVal maxSpeed As Integer)
Me.MaxSpeed = maxSpeed
End Sub
' Applies the alignment rule to adjust the boid's velocity based on the average velocity of neighboring boids.
Public Sub ApplyAlignmentRule(ByVal boid As Boid, ByVal avgVelocity As Point)
' Calculate the angle of the new velocity based on the average velocity.
Dim newVelocityAngle As Double = Math.Atan2(avgVelocity.Y, avgVelocity.X)
' Set the boid's velocity with the adjusted speed in the direction of the new velocity angle.
boid.Velocity = New Point(CInt(MaxSpeed * Math.Cos(newVelocityAngle)), CInt(MaxSpeed * Math.Sin(newVelocityAngle)))
End Sub
End Class
Separation class:
Code:
' For applying the separation rule to boids.
Public Class Separation
' Applies the separation rule to adjust the boid's velocity based on the distances to close neighbors.
Public Sub ApplySeparationRule(ByVal boid As Boid, ByVal closeDx As Double, ByVal closeDy As Double)
' Separation rule logic
' The separation factor determines the strength of the separation effect. Adjust as needed.
Dim separationFactor As Double = 0.1
' Adjust the boid's velocity based on the distances to close neighbors and the separation factor.
boid.Velocity = New Point(CInt(boid.Velocity.X + closeDx * separationFactor), CInt(boid.Velocity.Y + closeDy * separationFactor))
End Sub
End Class
Cohesion class:
Code:
' For applying the cohesion rule to boids.
Public Class Cohesion
' The factor determining the strength of the cohesion effect. Adjust as needed.
Public Property CenteringFactor As Double = 0.1
' Applies the cohesion rule to adjust the boid's velocity based on the average position of neighboring boids.
Public Sub ApplyCohesionRule(ByVal boid As Boid, ByVal averagePosition As Point)
' Cohesion rule logic
' The cohesion factor is a combination of the centering factor and a scaling factor. Adjust as needed.
Dim cohesionFactor As Double = CenteringFactor * 0.4
' Adjust the boid's velocity based on the average position of neighboring boids and the cohesion factor.
boid.Velocity = New Point(CInt(boid.Velocity.X + (averagePosition.X - boid.Location.X) * cohesionFactor),
CInt(boid.Velocity.Y + (averagePosition.Y - boid.Location.Y) * cohesionFactor))
End Sub
End Class
Last edited by Peter Porter; Dec 2nd, 2023 at 05:48 PM.
' BoidCounter class is responsible for counting various configurations of boids within a specified visual range.
Public Class BoidCounter
' Property to set or get the list of boids from the Form1 class.
Public Property BoidsList As List(Of Boid)
' Property to set or get the visual range within which boids are considered neighbors.
Public Property VisualRange As Integer
Public FlockCount As Integer = 0 ' Number of flocks
Public TrioCount As Integer = 0 ' Number of trios
Public PairCount As Integer = 0 ' Number of pairs
Public LoneCount As Integer = 0 ' Number of loners
' Counts the number of flocks, trios, pairs, and lone boids based on the specified visual range.
' Requires the BoidsList property to be set before calling this method.
Public Sub CountFlocksTriosPairsLoners(ByVal BoidsList As List(Of Boid))
' Initialize a set to keep track of visited boids during counting.
Dim visitedBoids As New HashSet(Of Boid)
' Reset count variables to zero.
FlockCount = 0
TrioCount = 0
PairCount = 0
LoneCount = 0
' Iterate through each boid in the BoidsList.
For Each boid As Boid In BoidsList
' Check if the boid has not been visited before.
If Not visitedBoids.Contains(boid) Then
' Find the boids to which the boid belongs and mark the visited boids.
Dim boids As List(Of Boid) = FindBoidsGroup(boid, visitedBoids)
visitedBoids.UnionWith(boids)
' Update counts based on boids group size.
If boids.Count > 3 Then
FlockCount += 1
End If
' Check if it's a trio.
If boids.Count = 3 Then
TrioCount += 1
End If
' Check if it's a pair.
If boids.Count = 2 Then
PairCount += 1
End If
' Check if it's a lone boid.
If boids.Count = 1 Then
LoneCount += 1
End If
End If
Next
End Sub
' Finds and returns the group of boids to which a given boid belongs.
' Uses depth-first search to traverse the boid groups.
' Parameters:
' - boid: The boid for which to find the group.
' - visitedBoids: A set to keep track of visited boids during the traversal.
' Returns a List(Of Boid) representing the group of boids.
Private Function FindBoidsGroup(ByVal boid As Boid, ByVal visitedBoids As HashSet(Of Boid)) As List(Of Boid)
' Initialize a list to store the boids in the group.
Dim group As New List(Of Boid)
' Use a stack to perform depth-first search to find the boids group.
Dim stack As New Stack(Of Boid)
stack.Push(boid)
' Iterate until the stack is empty.
While stack.Count > 0
' Pop the current boid from the stack.
Dim currentBoid As Boid = stack.Pop()
' If the current boid has not been visited, add it to the group and visit its neighbors.
If Not visitedBoids.Contains(currentBoid) Then
visitedBoids.Add(currentBoid)
group.Add(currentBoid)
' Visit neighbors and add them to the stack if they are not already visited.
For Each neighbor As Boid In BoidsList
If Not visitedBoids.Contains(neighbor) AndAlso IsNeighbor(currentBoid, neighbor, VisualRange) Then
stack.Push(neighbor)
End If
Next
End If
End While
' Return the final group of boids.
Return group
End Function
' Determines if two boids are neighbors based on distance.
Private Function IsNeighbor(ByVal boid1 As Boid, ByVal boid2 As Boid, ByVal visualRange As Double) As Boolean
' Calculate the Euclidean distance between the locations of the two boids.
Dim distance As Double = Math.Sqrt((boid1.Location.X - boid2.Location.X) ^ 2 + (boid1.Location.Y - boid2.Location.Y) ^ 2)
' Return true if the distance is less than the visual range.
Return distance < visualRange ' Adjust the distance as needed
End Function
End Class
Last edited by Peter Porter; Dec 2nd, 2023 at 05:48 PM.