-
Notifications
You must be signed in to change notification settings - Fork 13
/
pynboids2.py
executable file
·142 lines (132 loc) · 7 KB
/
pynboids2.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
#!/usr/bin/env python3
from random import randint
import pygame as pg
import numpy as np
'''
PyNBoids - a Boids simulation - github.com/Nikorasu/PyNBoids
Uses numpy array math instead of math lib, more efficient.
Copyright (c) 2021 Nikolaus Stromberg nikorasu85@gmail.com
'''
FLLSCRN = True # True for Fullscreen, or False for Window
BOIDZ = 150 # How many boids to spawn, too many may slow fps
WRAP = False # False avoids edges, True wraps to other side
FISH = False # True to turn boids into fish
SPEED = 170 # Movement speed
WIDTH = 1200 # Window Width (1200)
HEIGHT = 800 # Window Height (800)
BGCOLOR = (0, 0, 0) # Background color in RGB
FPS = 60 # 30-90
SHOWFPS = False # show frame rate
class Boid(pg.sprite.Sprite):
def __init__(self, boidNum, data, drawSurf, isFish=False, cHSV=None):
super().__init__()
self.data = data
self.bnum = boidNum
self.drawSurf = drawSurf
self.image = pg.Surface((15, 15)).convert()
self.image.set_colorkey(0)
self.color = pg.Color(0) # preps color so we can use hsva
self.color.hsva = (randint(0,360), 90, 90) if cHSV is None else cHSV # randint(5,55) #4goldfish
if isFish: # (randint(120,300) + 180) % 360 #4noblues
pg.draw.polygon(self.image, self.color, ((7,0),(12,5),(3,14),(11,14),(2,5),(7,0)), width=3)
self.image = pg.transform.scale(self.image, (16, 24))
else : pg.draw.polygon(self.image, self.color, ((7,0), (13,14), (7,11), (1,14), (7,0)))
self.bSize = 22 if isFish else 17
self.orig_image = pg.transform.rotate(self.image.copy(), -90)
self.dir = pg.Vector2(1, 0) # sets up forward direction
maxW, maxH = self.drawSurf.get_size()
self.rect = self.image.get_rect(center=(randint(50, maxW - 50), randint(50, maxH - 50)))
self.ang = randint(0, 360) # random start angle, & position ^
self.pos = pg.Vector2(self.rect.center)
def update(self, dt, speed, ejWrap=False):
maxW, maxH = self.drawSurf.get_size()
turnDir = xvt = yvt = yat = xat = 0
turnRate = 120 * dt # about 120 seems ok
margin = 42
# Make list of nearby boids, sorted by distance
otherBoids = np.delete(self.data.array, self.bnum, 0)
array_dists = (self.pos.x - otherBoids[:,0])**2 + (self.pos.y - otherBoids[:,1])**2
closeBoidIs = np.argsort(array_dists)[:7]
neiboids = otherBoids[closeBoidIs]
neiboids[:,3] = np.sqrt(array_dists[closeBoidIs])
neiboids = neiboids[neiboids[:,3] < self.bSize*12]
if neiboids.size > 1: # if has neighborS, do math and sim rules
yat = np.sum(np.sin(np.deg2rad(neiboids[:,2])))
xat = np.sum(np.cos(np.deg2rad(neiboids[:,2])))
# averages the positions and angles of neighbors
tAvejAng = np.rad2deg(np.arctan2(yat, xat))
targetV = (np.mean(neiboids[:,0]), np.mean(neiboids[:,1]))
# if too close, move away from closest neighbor
if neiboids[0,3] < self.bSize : targetV = (neiboids[0,0], neiboids[0,1])
# get angle differences for steering
tDiff = pg.Vector2(targetV) - self.pos
tDistance, tAngle = pg.math.Vector2.as_polar(tDiff)
# if boid is close enough to neighbors, match their average angle
if tDistance < self.bSize*6 : tAngle = tAvejAng
# computes the difference to reach target angle, for smooth steering
angleDiff = (tAngle - self.ang) + 180
if abs(tAngle - self.ang) > 1.2: turnDir = (angleDiff / 360 - (angleDiff // 360)) * 360 - 180
# if boid gets too close to target, steer away
if tDistance < self.bSize and targetV == (neiboids[0,0], neiboids[0,1]) : turnDir = -turnDir
# Avoid edges of screen by turning toward the edge normal-angle
if not ejWrap and min(self.pos.x, self.pos.y, maxW - self.pos.x, maxH - self.pos.y) < margin:
if self.pos.x < margin : tAngle = 0
elif self.pos.x > maxW - margin : tAngle = 180
if self.pos.y < margin : tAngle = 90
elif self.pos.y > maxH - margin : tAngle = 270
angleDiff = (tAngle - self.ang) + 180 # if in margin, increase turnRate to ensure stays on screen
turnDir = (angleDiff / 360 - (angleDiff // 360)) * 360 - 180
edgeDist = min(self.pos.x, self.pos.y, maxW - self.pos.x, maxH - self.pos.y)
turnRate = turnRate + (1 - edgeDist / margin) * (20 - turnRate) #minRate+(1-dist/margin)*(maxRate-minRate)
if turnDir != 0: # steers based on turnDir, handles left or right
self.ang += turnRate * abs(turnDir) / turnDir
self.ang %= 360 # ensures that the angle stays within 0-360
# Adjusts angle of boid image to match heading
self.image = pg.transform.rotate(self.orig_image, -self.ang)
self.rect = self.image.get_rect(center=self.rect.center) # recentering fix
self.dir = pg.Vector2(1, 0).rotate(self.ang).normalize()
self.pos += self.dir * dt * (speed + (7 - neiboids.size) * 2) # movement speed
# Optional screen wrap
if ejWrap and not self.drawSurf.get_rect().contains(self.rect):
if self.rect.bottom < 0 : self.pos.y = maxH
elif self.rect.top > maxH : self.pos.y = 0
if self.rect.right < 0 : self.pos.x = maxW
elif self.rect.left > maxW : self.pos.x = 0
# Actually update position of boid
self.rect.center = self.pos
# Finally, output pos/ang to array
self.data.array[self.bnum,:3] = [self.pos[0], self.pos[1], self.ang]
class BoidArray(): # Holds array to store positions and angles
def __init__(self):
self.array = np.zeros((BOIDZ, 4), dtype=float)
def main():
pg.init() # prepare window
pg.display.set_caption("PyNBoids")
try: pg.display.set_icon(pg.image.load("nboids.png"))
except: print("FYI: nboids.png icon not found, skipping..")
# setup fullscreen or window mode
if FLLSCRN:
currentRez = (pg.display.Info().current_w, pg.display.Info().current_h)
screen = pg.display.set_mode(currentRez, pg.SCALED)
pg.mouse.set_visible(False)
else: screen = pg.display.set_mode((WIDTH, HEIGHT), pg.RESIZABLE)
nBoids = pg.sprite.Group()
dataArray = BoidArray()
for n in range(BOIDZ):
nBoids.add(Boid(n, dataArray, screen, FISH)) # spawns desired # of boidz
clock = pg.time.Clock()
if SHOWFPS : font = pg.font.Font(None, 30)
# main loop
while True:
for e in pg.event.get():
if e.type == pg.QUIT or e.type == pg.KEYDOWN and e.key == pg.K_ESCAPE:
return
dt = clock.tick(FPS) / 1000
screen.fill(BGCOLOR)
nBoids.update(dt, SPEED, WRAP)
nBoids.draw(screen)
if SHOWFPS : screen.blit(font.render(str(int(clock.get_fps())), True, [0,200,0]), (8, 8))
pg.display.update()
if __name__ == '__main__':
main() # by Nik
pg.quit()