├── README.md
├── Flocking.sln
├── LICENSE
├── Flocking
├── Flocking.pyproj
└── Flocking.py
├── .gitattributes
└── .gitignore
/README.md:
--------------------------------------------------------------------------------
1 | # Flocking
2 |
3 | Python implementation of boids logic to demonstrate 2D flocking behaviour of birds
4 |
5 | # Installation
6 |
7 | Clone the repo and simply run the flocking.py file; alternatively clone into Visual Studio with Python Tools and simply run from there
8 |
9 | Designed for Python 3.5
10 |
--------------------------------------------------------------------------------
/Flocking.sln:
--------------------------------------------------------------------------------
1 |
2 | Microsoft Visual Studio Solution File, Format Version 12.00
3 | # Visual Studio 14
4 | VisualStudioVersion = 14.0.25420.1
5 | MinimumVisualStudioVersion = 10.0.40219.1
6 | Project("{888888A0-9F3D-457C-B088-3A5042F75D52}") = "Flocking", "Flocking\Flocking.pyproj", "{3876FE17-508B-4999-82D2-9AB5FCF5D0C3}"
7 | EndProject
8 | Global
9 | GlobalSection(SolutionConfigurationPlatforms) = preSolution
10 | Debug|Any CPU = Debug|Any CPU
11 | Release|Any CPU = Release|Any CPU
12 | EndGlobalSection
13 | GlobalSection(ProjectConfigurationPlatforms) = postSolution
14 | {3876FE17-508B-4999-82D2-9AB5FCF5D0C3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
15 | {3876FE17-508B-4999-82D2-9AB5FCF5D0C3}.Release|Any CPU.ActiveCfg = Release|Any CPU
16 | EndGlobalSection
17 | GlobalSection(SolutionProperties) = preSolution
18 | HideSolutionNode = FALSE
19 | EndGlobalSection
20 | EndGlobal
21 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2017 DKarandikar
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/Flocking/Flocking.pyproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Debug
5 | 2.0
6 | 3876fe17-508b-4999-82d2-9ab5fcf5d0c3
7 | .
8 | Flocking.py
9 |
10 |
11 | .
12 | .
13 | Flocking
14 | Flocking
15 |
16 |
17 | true
18 | false
19 |
20 |
21 | true
22 | false
23 |
24 |
25 |
26 |
27 |
28 | 10.0
29 | $(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion)\Python Tools\Microsoft.PythonTools.targets
30 |
31 |
32 |
33 |
36 |
37 |
38 |
39 |
40 |
41 |
--------------------------------------------------------------------------------
/.gitattributes:
--------------------------------------------------------------------------------
1 | ###############################################################################
2 | # Set default behavior to automatically normalize line endings.
3 | ###############################################################################
4 | * text=auto
5 |
6 | ###############################################################################
7 | # Set default behavior for command prompt diff.
8 | #
9 | # This is need for earlier builds of msysgit that does not have it on by
10 | # default for csharp files.
11 | # Note: This is only used by command line
12 | ###############################################################################
13 | #*.cs diff=csharp
14 |
15 | ###############################################################################
16 | # Set the merge driver for project and solution files
17 | #
18 | # Merging from the command prompt will add diff markers to the files if there
19 | # are conflicts (Merging from VS is not affected by the settings below, in VS
20 | # the diff markers are never inserted). Diff markers may cause the following
21 | # file extensions to fail to load in VS. An alternative would be to treat
22 | # these files as binary and thus will always conflict and require user
23 | # intervention with every merge. To do so, just uncomment the entries below
24 | ###############################################################################
25 | #*.sln merge=binary
26 | #*.csproj merge=binary
27 | #*.vbproj merge=binary
28 | #*.vcxproj merge=binary
29 | #*.vcproj merge=binary
30 | #*.dbproj merge=binary
31 | #*.fsproj merge=binary
32 | #*.lsproj merge=binary
33 | #*.wixproj merge=binary
34 | #*.modelproj merge=binary
35 | #*.sqlproj merge=binary
36 | #*.wwaproj merge=binary
37 |
38 | ###############################################################################
39 | # behavior for image files
40 | #
41 | # image files are treated as binary by default.
42 | ###############################################################################
43 | #*.jpg binary
44 | #*.png binary
45 | #*.gif binary
46 |
47 | ###############################################################################
48 | # diff behavior for common document formats
49 | #
50 | # Convert binary document formats to text before diffing them. This feature
51 | # is only available from the command line. Turn it on by uncommenting the
52 | # entries below.
53 | ###############################################################################
54 | #*.doc diff=astextplain
55 | #*.DOC diff=astextplain
56 | #*.docx diff=astextplain
57 | #*.DOCX diff=astextplain
58 | #*.dot diff=astextplain
59 | #*.DOT diff=astextplain
60 | #*.pdf diff=astextplain
61 | #*.PDF diff=astextplain
62 | #*.rtf diff=astextplain
63 | #*.RTF diff=astextplain
64 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | ## Ignore Visual Studio temporary files, build results, and
2 | ## files generated by popular Visual Studio add-ons.
3 |
4 | # User-specific files
5 | *.suo
6 | *.user
7 | *.userosscache
8 | *.sln.docstates
9 |
10 | # User-specific files (MonoDevelop/Xamarin Studio)
11 | *.userprefs
12 |
13 | # Build results
14 | [Dd]ebug/
15 | [Dd]ebugPublic/
16 | [Rr]elease/
17 | [Rr]eleases/
18 | [Xx]64/
19 | [Xx]86/
20 | [Bb]uild/
21 | bld/
22 | [Bb]in/
23 | [Oo]bj/
24 |
25 | # Visual Studio 2015 cache/options directory
26 | .vs/
27 | # Uncomment if you have tasks that create the project's static files in wwwroot
28 | #wwwroot/
29 |
30 | # MSTest test Results
31 | [Tt]est[Rr]esult*/
32 | [Bb]uild[Ll]og.*
33 |
34 | # NUNIT
35 | *.VisualState.xml
36 | TestResult.xml
37 |
38 | # Build Results of an ATL Project
39 | [Dd]ebugPS/
40 | [Rr]eleasePS/
41 | dlldata.c
42 |
43 | # DNX
44 | project.lock.json
45 | artifacts/
46 |
47 | *_i.c
48 | *_p.c
49 | *_i.h
50 | *.ilk
51 | *.meta
52 | *.obj
53 | *.pch
54 | *.pdb
55 | *.pgc
56 | *.pgd
57 | *.rsp
58 | *.sbr
59 | *.tlb
60 | *.tli
61 | *.tlh
62 | *.tmp
63 | *.tmp_proj
64 | *.log
65 | *.vspscc
66 | *.vssscc
67 | .builds
68 | *.pidb
69 | *.svclog
70 | *.scc
71 |
72 | # Chutzpah Test files
73 | _Chutzpah*
74 |
75 | # Visual C++ cache files
76 | ipch/
77 | *.aps
78 | *.ncb
79 | *.opendb
80 | *.opensdf
81 | *.sdf
82 | *.cachefile
83 | *.VC.db
84 |
85 | # Visual Studio profiler
86 | *.psess
87 | *.vsp
88 | *.vspx
89 | *.sap
90 |
91 | # TFS 2012 Local Workspace
92 | $tf/
93 |
94 | # Guidance Automation Toolkit
95 | *.gpState
96 |
97 | # ReSharper is a .NET coding add-in
98 | _ReSharper*/
99 | *.[Rr]e[Ss]harper
100 | *.DotSettings.user
101 |
102 | # JustCode is a .NET coding add-in
103 | .JustCode
104 |
105 | # TeamCity is a build add-in
106 | _TeamCity*
107 |
108 | # DotCover is a Code Coverage Tool
109 | *.dotCover
110 |
111 | # NCrunch
112 | _NCrunch_*
113 | .*crunch*.local.xml
114 | nCrunchTemp_*
115 |
116 | # MightyMoose
117 | *.mm.*
118 | AutoTest.Net/
119 |
120 | # Web workbench (sass)
121 | .sass-cache/
122 |
123 | # Installshield output folder
124 | [Ee]xpress/
125 |
126 | # DocProject is a documentation generator add-in
127 | DocProject/buildhelp/
128 | DocProject/Help/*.HxT
129 | DocProject/Help/*.HxC
130 | DocProject/Help/*.hhc
131 | DocProject/Help/*.hhk
132 | DocProject/Help/*.hhp
133 | DocProject/Help/Html2
134 | DocProject/Help/html
135 |
136 | # Click-Once directory
137 | publish/
138 |
139 | # Publish Web Output
140 | *.[Pp]ublish.xml
141 | *.azurePubxml
142 |
143 | # TODO: Un-comment the next line if you do not want to checkin
144 | # your web deploy settings because they may include unencrypted
145 | # passwords
146 | #*.pubxml
147 | *.publishproj
148 |
149 | # NuGet Packages
150 | *.nupkg
151 | # The packages folder can be ignored because of Package Restore
152 | **/packages/*
153 | # except build/, which is used as an MSBuild target.
154 | !**/packages/build/
155 | # Uncomment if necessary however generally it will be regenerated when needed
156 | #!**/packages/repositories.config
157 | # NuGet v3's project.json files produces more ignoreable files
158 | *.nuget.props
159 | *.nuget.targets
160 |
161 | # Microsoft Azure Build Output
162 | csx/
163 | *.build.csdef
164 |
165 | # Microsoft Azure Emulator
166 | ecf/
167 | rcf/
168 |
169 | # Windows Store app package directory
170 | AppPackages/
171 | BundleArtifacts/
172 |
173 | # Visual Studio cache files
174 | # files ending in .cache can be ignored
175 | *.[Cc]ache
176 | # but keep track of directories ending in .cache
177 | !*.[Cc]ache/
178 |
179 | # Others
180 | ClientBin/
181 | [Ss]tyle[Cc]op.*
182 | ~$*
183 | *~
184 | *.dbmdl
185 | *.dbproj.schemaview
186 | *.pfx
187 | *.publishsettings
188 | node_modules/
189 | orleans.codegen.cs
190 |
191 | # RIA/Silverlight projects
192 | Generated_Code/
193 |
194 | # Backup & report files from converting an old project file
195 | # to a newer Visual Studio version. Backup files are not needed,
196 | # because we have git ;-)
197 | _UpgradeReport_Files/
198 | Backup*/
199 | UpgradeLog*.XML
200 | UpgradeLog*.htm
201 |
202 | # SQL Server files
203 | *.mdf
204 | *.ldf
205 |
206 | # Business Intelligence projects
207 | *.rdl.data
208 | *.bim.layout
209 | *.bim_*.settings
210 |
211 | # Microsoft Fakes
212 | FakesAssemblies/
213 |
214 | # GhostDoc plugin setting file
215 | *.GhostDoc.xml
216 |
217 | # Node.js Tools for Visual Studio
218 | .ntvs_analysis.dat
219 |
220 | # Visual Studio 6 build log
221 | *.plg
222 |
223 | # Visual Studio 6 workspace options file
224 | *.opt
225 |
226 | # Visual Studio LightSwitch build output
227 | **/*.HTMLClient/GeneratedArtifacts
228 | **/*.DesktopClient/GeneratedArtifacts
229 | **/*.DesktopClient/ModelManifest.xml
230 | **/*.Server/GeneratedArtifacts
231 | **/*.Server/ModelManifest.xml
232 | _Pvt_Extensions
233 |
234 | # LightSwitch generated files
235 | GeneratedArtifacts/
236 | ModelManifest.xml
237 |
238 | # Paket dependency manager
239 | .paket/paket.exe
240 |
241 | # FAKE - F# Make
242 | .fake/
243 |
--------------------------------------------------------------------------------
/Flocking/Flocking.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | """
3 | Created on Sat Jan 14 23:29:06 2017
4 |
5 | @author: Daniel
6 | """
7 |
8 | import random
9 | import time
10 | import math
11 |
12 | import tkinter as tk
13 | import numpy as np
14 |
15 |
16 | BOIDS = 50
17 | CHOOSE_FOCUS = False
18 | OBS_TYPE = "line"
19 | OBS_TYPE = "circle"
20 | OBS_TYPE = "pass"
21 |
22 |
23 | class Quadtree():
24 | '''Quadtree class which is designed to contain objects with a .rect attribute
25 |
26 | Attributes:
27 | max_objects: Maximum number of objects in a quad
28 | max_level: Maximum depth to recur the quads
29 | level: current level of the tree
30 | objects: actual objects in that quad
31 | bounds: the rectangle that bounds the top-most Quadtree
32 | nodes: all sub-Quadtrees
33 | '''
34 | def __init__(self, level, bounds):
35 | self.max_objects = 8
36 | self.max_level = 8
37 | self.level = level
38 | self.objects = []
39 | self.bounds = bounds
40 | self.nodes = []
41 |
42 | def clear2(self):
43 | '''Clears all objects recursively in the Quadtree'''
44 | self.objects.clear()
45 | for i in range(0, len(self.nodes)):
46 | if self.nodes[i]:
47 | self.nodes[i].clear2()
48 | self.nodes.clear()
49 |
50 | def split(self):
51 | '''Splits the quadtree into four sub trees'''
52 | subwidth = int((self.bounds[0]+self.bounds[2])/2)
53 | subheight = int((self.bounds[1]+self.bounds[3])/2)
54 |
55 | self.nodes.append(Quadtree(self.level+1,
56 | (subwidth, self.bounds[1],
57 | subwidth*2-self.bounds[0], subheight)))
58 | self.nodes.append(Quadtree(self.level+1,
59 | (self.bounds[0], self.bounds[1],
60 | subwidth, subheight)))
61 | self.nodes.append(Quadtree(self.level+1,
62 | (self.bounds[0], subheight,
63 | subwidth, 2*subheight-self.bounds[1])))
64 | self.nodes.append(Quadtree(self.level+1,
65 | (subwidth, subheight,
66 | 2*subwidth - self.bounds[0], 2*subheight - self.bounds[1])))
67 |
68 | def getindex(self, rect):
69 | '''Returns an integer corresponding to where rect fits'''
70 | index = -1
71 | verticalmid = int((self.bounds[0]+self.bounds[2])/2)
72 | horizontalmid = int((self.bounds[1]+self.bounds[3])/2)
73 |
74 | topquad = (rect[3] < horizontalmid)
75 | botquad = (rect[1] > horizontalmid)
76 |
77 | if rect[2] < verticalmid:
78 | if topquad:
79 | index = 1
80 | elif botquad:
81 | index = 2
82 | elif rect[0] > verticalmid:
83 | if topquad:
84 | index = 0
85 | elif botquad:
86 | index = 3
87 |
88 | return index
89 |
90 | def getindex2(self, rect):
91 | '''Returns a list corresponding to where rect fits'''
92 | index = [0, 1, 2, 3]
93 | verticalmid = int((self.bounds[0]+self.bounds[2])/2)
94 | horizontalmid = int((self.bounds[1]+self.bounds[3])/2)
95 |
96 | topquad = (rect[3] < horizontalmid)
97 | botquad = (rect[1] > horizontalmid)
98 |
99 | if topquad:
100 | index.remove(2)
101 | index.remove(3)
102 | elif botquad:
103 | index.remove(1)
104 | index.remove(0)
105 |
106 | if rect[2] < verticalmid:
107 | try:
108 | index.remove(1)
109 | index.remove(2)
110 | except ValueError:
111 | pass
112 | elif rect[0] > verticalmid:
113 | try:
114 | index.remove(0)
115 | index.remove(3)
116 | except ValueError:
117 | pass
118 |
119 | return index
120 |
121 | def insert(self, boid):
122 | '''Puts an object into the right place in the tree, and splits as necessary'''
123 | if len(self. nodes) != 0:
124 | index = self.getindex(boid.rect)
125 |
126 | if index != -1:
127 | self.nodes[index].insert(boid)
128 | return()
129 |
130 | self.objects.append(boid)
131 |
132 | if len(self.objects) > self.max_objects and self.level < self.max_level:
133 | if len(self.nodes) == 0:
134 | self.split()
135 |
136 | i = 0
137 | while i < len(self.objects):
138 | index = self.getindex(self.objects[i].rect)
139 | if index != -1:
140 | self.nodes[index].insert(self.objects.pop(i))
141 | else:
142 | i += 1
143 |
144 | def retreive(self, returnobjects, boid):
145 | '''Retreives all objects that could collide with boid'''
146 | index = self.getindex(boid.rect)
147 | if index != -1 and len(self.nodes) != 0:
148 | self.nodes[index].retreive(returnobjects, boid)
149 |
150 | if index == -1:
151 | index_list = self.getindex2(boid.rect)
152 | to_test = []
153 | for i in index_list:
154 | try:
155 | to_test.append(self.nodes[i])
156 | except IndexError:
157 | pass
158 | for k in to_test:
159 | k.retreive(returnobjects, boid)
160 |
161 |
162 | for obj in self.objects:
163 | returnobjects.append(obj)
164 |
165 | return returnobjects
166 |
167 | class World():
168 | '''A world containing Boids and Obstacles
169 |
170 | Attributes:
171 | width and height: width and height of the World
172 | boids and obstacles: lists of each type
173 | quad: a Quadtree that contains all the objects
174 | '''
175 | def __init__(self):
176 | self.width = 1000
177 | self.height = 1000
178 | self.boids = []
179 | self.obstacles = []
180 | self.quad = Quadtree(0, (0, 0, self.width, self.height))
181 |
182 | def setup_boids(self, num):
183 | '''Randomly generates n boids within 150 of the world bounds'''
184 | self.boids.clear()
185 | for i in range(num):
186 | k = Boid()
187 | k.rand_start(self.height, self.width)
188 | self.boids.append(k)
189 |
190 | if i == 0 and CHOOSE_FOCUS:
191 | k.foc()
192 |
193 | def setup_obstacles(self, obs_type):
194 | '''Sets up obstacles in different configurations'''
195 | self.obstacles.clear()
196 |
197 | if obs_type == "line": #Sets two vertical lines on the edges
198 | for i in range(int(self.height/50)+1):
199 | k = Obstacle()
200 | k.setxy(25, 50*i)
201 | j = Obstacle()
202 | j.setxy(self.width-25, 50*i)
203 | self.obstacles.append(k)
204 | self.obstacles.append(j)
205 |
206 | elif obs_type == "circle": #Sets a circle of obstacles
207 | for i in range(45):
208 | k = Obstacle()
209 | k_x = self.width/2 + 375* math.cos((i+1)*2*math.pi / 45)
210 | k_y = self.height/2 + 375* math.sin((i+1)*2*math.pi / 45)
211 | k.setxy(k_x, k_y)
212 | self.obstacles.append(k)
213 |
214 | def addboid(self, pos_x, pos_y):
215 | '''Adds a new Boid at x,y '''
216 | new_boid = Boid()
217 | new_boid.setxy(pos_x, pos_y)
218 | self.boids.append(new_boid)
219 |
220 |
221 | def step(self):
222 | '''Performs a deiscrete update step on all boids'''
223 | self.quad.clear2()
224 |
225 | for boid in self.boids:
226 | self.quad.insert(boid)
227 |
228 | for obstacle in self.obstacles:
229 | self.quad.insert(obstacle)
230 |
231 | returned = []
232 | for boid in self.boids:
233 | returned.clear()
234 | returned = self.quad.retreive(returned, boid)
235 | returned.remove(boid)
236 |
237 | boid.update(returned, self.height, self.width)
238 |
239 |
240 | class Boid():
241 | '''The main object, a Boid
242 |
243 | Attributes:
244 | vel_x, vel_y, pos_x, pos_y: Position and speed components of the Boid
245 | theta: The angular direction it travels in
246 | rep, ali and att: Square of the distance to repulse, align and attract (resp)
247 | focus: Whether to focus on the boid or not
248 | speed, turn_speed: distance to travel or turn in one time step
249 | aware: list of all objects that the Boid is aware of
250 | obstacle: False if it is a Boid, true for an obstacle
251 | obstacles: Whether or not a boid sees an obstacle; allows for priority of movement
252 | rect: The rectangle which describes its attraction range
253 | '''
254 |
255 | # pylint: disable=too-many-instance-attributes
256 | # Boids just have a lot going on, ok?!
257 |
258 | def __init__(self):
259 | self.vel_x = 0
260 | self.vel_y = 0
261 | self.pos_x = 0
262 | self.pos_y = 0
263 | self.theta = 0
264 |
265 | self.rep = 900
266 | self.ali = 2200
267 | self.att = 3000
268 | self.rect = (0, 0, 0, 0)
269 |
270 | self.focus = False
271 |
272 | self.speed = 0.75
273 | self.turn_speed = 0.05
274 | self.aware = [] #all things it is aware of, colour purposes only
275 |
276 | self.obstacle = False
277 | self.obstacles = False
278 |
279 | def setxy(self, pos_x, pos_y):
280 | '''Set the x and y variables of the Boid, and calculate rect appropriately'''
281 | self.pos_x = pos_x
282 | self.pos_y = pos_y
283 | self.rect = (self.pos_x-int(self.att**(1/2)),
284 | self.pos_y-int(self.att**(1/2)),
285 | self.pos_x+int(self.att**(1/2)),
286 | self.pos_y+int(self.att**(1/2)))
287 |
288 | def rand_start(self, height, width):
289 | '''Randomly generates a position to start the boid within the bounds of height and width'''
290 | self.pos_x = random.randint(150, width-150)
291 | self.pos_y = random.randint(150, height-150)
292 |
293 | self.theta = random.random()*2*math.pi
294 | self.vel_x = math.cos(self.theta)
295 | self.vel_y = math.sin(self.theta)
296 |
297 | self.rect = (self.pos_x-int(self.att**(1/2)),
298 | self.pos_y-int(self.att**(1/2)),
299 | self.pos_x+int(self.att**(1/2)),
300 | self.pos_y+int(self.att**(1/2)))
301 |
302 | def foc(self):
303 | '''Change whether the boid is focussed or not'''
304 | self.focus = not self.focus
305 |
306 | def update(self, objects, height, width):
307 | '''The main method of a boid, includes all the logic for following the three rules of boids
308 |
309 | Repulse: Avoids nearby boids and preferentially avoids obstacless
310 | Align: Aligns to the direction of boids in a certain range
311 | Attract: Aligns to the average location of boids in the neighbourhood
312 |
313 | '''
314 | repulse = []
315 | align = []
316 | attract = []
317 | self.obstacles = False #This allows boids to prioritise avoiding obstacles over all else
318 |
319 | velocity = np.array([self.vel_x, self.vel_y])
320 | for boid in objects:
321 |
322 | distance = (self.pos_x - boid.pos_x)**2 + (self.pos_y - boid.pos_y)**2
323 |
324 | if boid.obstacle and distance < self.att:
325 | repulse.append(boid)
326 | self.obstacles = True
327 |
328 | else:
329 |
330 | disp = np.array([boid.pos_x - self.pos_x, boid.pos_y-self.pos_y])
331 | dotted = np.dot(velocity, disp)
332 | vel_mag = np.sqrt(np.dot(velocity, velocity))
333 | disp_mag = np.sqrt(np.dot(disp, disp))
334 | if vel_mag > 0 and disp_mag > 0:
335 | dotted /= (vel_mag * disp_mag)
336 |
337 | if dotted > -0.5:
338 | if distance < self.rep:
339 | repulse.append(boid)
340 | elif distance < self.ali:
341 | align.append(boid)
342 | elif distance < self.att:
343 | attract.append(boid)
344 |
345 | aim_theta = self.aim(repulse, align, attract)
346 |
347 | self.aware = repulse + align + attract
348 |
349 | self.steer(aim_theta)
350 |
351 | self.move(height, width)
352 |
353 | def aim(self, repulse, align, attract):
354 | ''' Takes in the three types of object and returns the new aiming angle'''
355 | aim_theta = self.theta
356 | if repulse and not self.obstacles:
357 | ave_x = sum(boid.pos_x-self.pos_x for boid in repulse)/len(repulse)
358 | ave_y = sum(boid.pos_y-self.pos_y for boid in repulse)/len(repulse)
359 | aim_theta = math.atan2(ave_y, ave_x) + math.pi
360 | elif repulse and self.obstacles:
361 | obs = []
362 | for obj in repulse:
363 | if obj.obstacle:
364 | obs.append(obj)
365 | ave_x = sum(boid.pos_x-self.pos_x for boid in obs)/len(obs)
366 | ave_y = sum(boid.pos_y-self.pos_y for boid in obs)/len(obs)
367 | aim_theta = math.atan2(ave_y, ave_x) + math.pi
368 | else:
369 |
370 | if align and not self.obstacles:
371 | #new_theta = aim_theta
372 | new_theta = 0
373 | for boid in align:
374 | new_theta += boid.theta
375 | #aim_theta = (new_theta/(len(align)+1))
376 | aim_theta = (new_theta/(len(align)))
377 |
378 | if attract and not self.obstacles:
379 | ave_x = sum(boid.pos_x-self.pos_x for boid in attract)/len(attract)
380 | ave_y = sum(boid.pos_y-self.pos_y for boid in attract)/len(attract)
381 | if align:
382 | aim_theta = 0.5*(aim_theta + math.atan2(ave_y, ave_x))
383 | else:
384 | aim_theta = math.atan2(ave_y, ave_x)
385 |
386 | if random.random() > 0.5 and not self.obstacles:
387 | if aim_theta > 0 and aim_theta < math.pi/2:
388 | aim_theta += random.uniform(-0.05, 0)
389 | elif math.pi/2 < aim_theta and aim_theta < math.pi:
390 | aim_theta += random.uniform(0, 0.05)
391 |
392 | if repulse and self.obstacles:
393 | closest = 1000000000
394 | for obj in repulse:
395 | if obj.obstacle:
396 | dist = (obj.pos_y-self.pos_y)**2 + (obj.pos_x-self.pos_x)**2
397 | if dist < closest:
398 | closest = dist
399 | aim_theta = math.atan2(obj.pos_y-self.pos_y, obj.pos_x-self.pos_x) + math.pi
400 |
401 | return aim_theta
402 |
403 | def steer(self, aim_theta):
404 | '''Steers towards the aimed for angle'''
405 |
406 | option1 = (self.theta - aim_theta)%(math.pi*2) < self.turn_speed
407 | option2 = (self.theta - aim_theta)%(math.pi*2) > 2*math.pi - self.turn_speed
408 |
409 | if option1 or option2:
410 | self.theta = aim_theta
411 | elif (aim_theta - self.theta)%(math.pi*2) < math.pi:
412 | self.theta = (self.theta + self.turn_speed)%(math.pi * 2)
413 | else:
414 | self.theta = (self.theta - self.turn_speed)%(math.pi * 2)
415 |
416 | self.vel_x = math.cos(self.theta)
417 | self.vel_y = math.sin(self.theta)
418 |
419 | def move(self, height, width):
420 | '''Moves the Boid, keeping it within the bounds and updates its rect'''
421 | self.pos_x = (self.pos_x + self.speed*self.vel_x)%width
422 | self.pos_y = (self.pos_y + self.speed*self.vel_y)%height
423 | self.rect = (self.pos_x-int(self.att**(1/2)),
424 | self.pos_y-int(self.att**(1/2)),
425 | self.pos_x+int(self.att**(1/2)),
426 | self.pos_y+int(self.att**(1/2)))
427 |
428 |
429 | class Obstacle(Boid):
430 | '''Extends the Boid class to make an Obstacle
431 |
432 | Attributes:
433 | x and y: The position of the Obstacle
434 | obstacle: True for Obstacles
435 | diameter: The width of the circle to be drawn
436 | oval: Rect for the circle to be drawn
437 | rect: The area that it could affect with att
438 | '''
439 | def __init__(self, *args, **kwargs):
440 | Boid.__init__(self, *args, **kwargs)
441 | self.obstacle = True
442 | self.diameter = 20
443 | self.oval = (0, 0, 0, 0)
444 | self.pos_x = 0
445 | self.pos_y = 0
446 |
447 | def setxy(self, x, y):
448 | '''Set the position of the Obstacle'''
449 | self.pos_x = x
450 | self.pos_y = y
451 | self.oval = (self.pos_x-int(0.5*self.diameter),
452 | self.pos_y-int(0.5*self.diameter),
453 | self.pos_x+int(0.5*self.diameter),
454 | self.pos_y+int(0.5*self.diameter))
455 |
456 | self.rect = (self.pos_x-int(self.att**(1/2)),
457 | self.pos_y-int(self.att**(1/2)),
458 | self.pos_x+int(self.att**(1/2)),
459 | self.pos_y+int(self.att**(1/2)))
460 |
461 |
462 | class Flocking(tk.Tk):
463 | '''The main App class for the Flocking simulation
464 |
465 | Attributes:
466 | world: A world of boids and obstacles
467 | height and width: Inhereted from the World
468 | start: Used to time, in order to calculate FPS
469 | canvas: A tk canvas object
470 | '''
471 | def __init__(self, *args, **kwargs):
472 | tk.Tk.__init__(self, *args, **kwargs)
473 |
474 | self.world = World()
475 | self.height = self.world.height
476 | self.width = self.world.width
477 | self.start = time.time()
478 | self.draw_quad = tk.BooleanVar()
479 | self.draw_quad.set(False)
480 | self.running = tk.BooleanVar()
481 | self.running.set(True)
482 |
483 | self.canvas = tk.Canvas(self, width=self.width, height=self.height,
484 | borderwidth=0, highlightthickness=0)
485 |
486 | self.canvas.bind("", self.addboid)
487 |
488 | self.canvas.pack(side="left", fill="both", expand="true")
489 |
490 | self.buttons = tk.Frame(self)
491 | self.create_buttons()
492 | self.buttons.pack(side="right", expand="true", fill="both")
493 |
494 | self.world.setup_boids(BOIDS)
495 | self.world.setup_obstacles(OBS_TYPE)
496 | self.update()
497 |
498 | def create_buttons(self):
499 | '''Creates all the buttons'''
500 | self.clear_button = tk.Button(self.buttons,
501 | text="Clear",
502 | command=self.kill_boids,
503 | width=18)
504 | self.clear_button.pack(side="top", pady=(10, 10))
505 |
506 | self.reset_button = tk.Button(self.buttons,
507 | text="Reset",
508 | command=lambda: self.world.setup_boids(BOIDS),
509 | width=18)
510 | self.reset_button.pack(side="top", pady=(10, 10))
511 |
512 | self.average_button = tk.Button(self.buttons,
513 | text="Average",
514 | command=self.averages,
515 | width=18)
516 | self.average_button.pack(side="top", pady=(10, 10))
517 |
518 | self.quad_button = tk.Checkbutton(self.buttons,
519 | text="Draw Quadtree",
520 | variable=self.draw_quad,
521 | width=18, onvalue=True, offvalue=False)
522 | self.quad_button.pack(side="top", pady=(10, 10))
523 |
524 | self.run_button = tk.Checkbutton(self.buttons,
525 | text="Pause",
526 | variable=self.running,
527 | width=18, onvalue=False, offvalue=True)
528 | self.run_button.pack(side="top", pady=(10, 10))
529 |
530 |
531 | def addboid(self, event):
532 | '''Adds a boid at the position of the mouse'''
533 | self.world.addboid(event.x, event.y)
534 |
535 | def kill_boids(self, event=None):
536 | '''Removes all boids'''
537 | self.world.boids.clear()
538 |
539 | def averages(self):
540 | '''Prints the mean angle of boids'''
541 | mean = sum(boid.theta for boid in self.world.boids)/len(self.world.boids)
542 | print(mean)
543 |
544 | def get_quads(self):
545 | '''Recursively gets all bounds of quads '''
546 |
547 | quad = self.world.quad
548 | new_nodes = set(quad.nodes)
549 | nodes = {quad}
550 |
551 | while new_nodes:
552 | nodes |= set(new_nodes)
553 | new_nodes.clear()
554 | for node in nodes:
555 | new_nodes |= set(node.nodes)
556 | new_nodes -= nodes
557 |
558 | bound_list = []
559 | for node in nodes:
560 | bound_list.append(node.bounds)
561 |
562 | return bound_list
563 |
564 |
565 | def update(self):
566 | '''Updates the world and draws everything per tick'''
567 |
568 | tick = time.time()-self.start
569 | self.start = time.time()
570 | if tick != 0:
571 | #print("\r", str(1/tick), end="")
572 | pass
573 |
574 | if self.running.get():
575 | self.world.step()
576 | self.canvas.delete("all")
577 | self.draw_boids()
578 | self.draw_obstacles()
579 |
580 | self.canvas.delete("quad")
581 | if self.draw_quad.get():
582 | self.draw_quadtree()
583 |
584 | self.canvas.pack(side="left", fill="both", expand="true")
585 |
586 | self.after(13, self.update)
587 |
588 | def draw_boids(self):
589 | '''Draws boids onto canvas'''
590 |
591 | for boid in self.world.boids:
592 | tip_x = boid.pos_x + (20*boid.vel_x)
593 | tip_y = boid.pos_y+ (20*boid.vel_y)
594 |
595 | if boid.focus:
596 | colour = "red"
597 | else:
598 | if boid.obstacles:
599 | colour = "green"
600 | elif len(boid.aware) > 0:
601 | colour = "blue"
602 | else:
603 | colour = "orange"
604 |
605 | self.canvas.create_polygon((tip_x, tip_y,
606 | int(boid.pos_x-(5*boid.vel_y)),
607 | int(boid.pos_y+(5*boid.vel_x)),
608 | int(boid.pos_x+(5*boid.vel_y)),
609 | int(boid.pos_y-(5*boid.vel_x))),
610 | fill=colour, outline="black", tags="boid")
611 |
612 | def draw_obstacles(self):
613 | '''Draws the obstacles'''
614 | for obs in self.world.obstacles:
615 | self.canvas.create_oval(obs.oval, fill="red", outline="", tags="obstacle")
616 | #self.canvas.create_oval((obs.x-100,obs.y-100,obs.x+100,obs.y+100))
617 |
618 | def draw_quadtree(self):
619 | '''Draws the quadtree'''
620 | bound_list = self.get_quads()
621 | for bound in bound_list:
622 | self.canvas.create_rectangle(bound, tags="quad")
623 |
624 |
625 | if __name__ == "__main__":
626 | APP = Flocking()
627 | APP.mainloop()
628 | APP.quit()
629 |
--------------------------------------------------------------------------------