├── 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 | --------------------------------------------------------------------------------