# main.lua -rw-r--r-- 5.8 KiB View raw
                                                                                
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
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
-- Boids
-- Kartik Agaram, 2025-04-26
-- Requires LÖVE; tested on v11.5

Boids = {}
Personal_space = 12
Perceptual_distance = 70

function love.load()
	Window_side = 800
	love.window.setMode(Window_side, Window_side)
	love.graphics.setBackgroundColor(0.8,0.8,0.8)
	for _ = 1,80 do
		table.insert(Boids, make_boid(math.random(Window_side), math.random(Window_side)))
	end
end

function make_boid(x, y)
	return {pos={x=x, y=y}, velocity={x=0, y=0}}
end

function love.draw()
	love.graphics.setColor(0,0,0)
	draw_boids(Boids)
	draw_hud_stats()
end

function draw_boids(boids)
	local boid_width, boid_length = Personal_space/2, Personal_space
	for _, boid in ipairs(boids) do
		local pos, velocity = boid.pos, boid.velocity
		local dir = vnorm(velocity) or {x=0, y=-1}
		local p1 = vadd(pos, vscale(dir, boid_length/2))
		local p2 = vadd(
				pos,
				vscale(dir, -boid_length/2),
				vscale(vperpendicular_anticlockwise(dir),
						boid_width/2))
		local p3 = vadd(
				pos,
				vscale(dir, -boid_length/2),
				vscale(vperpendicular_clockwise(dir),
						boid_width/2))
		love.graphics.polygon('fill', p1.x, p1.y, p2.x, p2.y, p3.x, p3.y)
	end
end

function draw_hud_stats()
	local count = 0
	for _, boid in ipairs(Boids) do
		local p = boid.pos
		if p.x >= 0 and p.x <= Window_side
			and p.y >= 0 and p.y <= Window_side
		then
			count = count+1
		end
	end
	love.graphics.setColor(1,1,1)
	love.graphics.print(('visible: %d'):format(count), 100, 100)
end

function love.update(dt)
	-- velocities and accelerations are per frame
	local max_speed = 0.5
	local max_accel = 20
	local nan = tostring(0/0)
	for _,boid in ipairs(Boids) do
		boid.pos = vadd(boid.pos, boid.velocity)
		assert(tostring(boid.pos.x) ~= nan)
		assert(tostring(boid.pos.y) ~= nan)
	end
	for i,boid in ipairs(Boids) do
		local accel = {x=0, y=0}
		-- each rule returns a normalized vector
		-- all weighting occurs here
		accel = vadd(accel,
			vscale(avoid_others(boid, Boids), 20*dt))
		accel = vadd(accel,
			vscale(seek_others(boid, Boids), 10*dt))
		accel = vadd(accel,
			vscale(align_with_others(boid, Boids), 10*dt))
		accel = vadd(accel,
			vscale(remain_within_viewport(boid), 40*dt))
		local curr_heading = vnorm(boid.velocity)  -- could be nil
		accel = vclamp2(accel, max_accel*dt, curr_heading, math.pi/6*dt)
		boid.velocity = vadd(boid.velocity, accel)
		boid.velocity = vclamp(boid.velocity, max_speed)
	end
end

function avoid_others(boid, boids)
	local result = {x=0, y=0}
	local dr = 0
	for i, boid2 in ipairs(boids) do
		if boid2 ~= boid then
			local delta = vsub(boid.pos, boid2.pos)
			local dist = vlength(delta)
			if dist < Personal_space then
				result = vadd(result, vnorm(delta))
				dr = dr + 1
			end
		end
	end
	if dr == 0 then
		return result
	end
	return vnorm(result)
end

function seek_others(boid, boids)
	-- fly towards the center of neighbors
	local center = {x=0, y=0}
	local dr = 0
	for _, boid2 in ipairs(boids) do
		if boid2 ~= boid and vdist(boid.pos, boid2.pos) < Perceptual_distance then
			center = vadd(center, boid2.pos)
			dr = dr + 1
		end
	end
	if dr == 0 then
		return {x=0, y=0}
	end
	center = vscale(center, 1/dr)
	return vnorm(vsub(center, boid.pos)) or {x=0, y=0}
end

function align_with_others(boid, boids)
	-- aim in the average heading of neighbors
	local result = {x=0, y=0}
	for _, boid2 in ipairs(boids) do
		if boid2 ~= boid and vdist(boid.pos, boid2.pos) < Perceptual_distance then
			result = vadd(result, boid2.velocity)
		end
	end
	return vnorm(result) or {x=0, y=0}
end

function remain_within_viewport(boid)
	local result = {x=0, y=0}
	local margin = 50
	local x, y = boid.pos.x, boid.pos.y
	if x < margin then
		result.x = result.x + (margin-x)/margin
	end
	if x > Window_side - margin then
		result.x = result.x + (Window_side-margin-x)/margin
	end
	if y < margin then
		result.y = result.y + (margin-y)/margin
	end
	if y > Window_side - margin then
		result.y = result.y + (Window_side-margin-y)/margin
	end
	return vnorm(result) or result
end

-- make sure magnitude of v is max or less
function vclamp(v, max)
	local l = vlength(v)
	if l <= max then return v end
	return vscale(v, max/l)
end

-- make sure:
--  * magnitude of v is mag or less, and
--  * angle of v is no more than `ang` away from vector `heading`
function vclamp2(v, mag, heading, ang)
	assert(ang >= 0)
	if heading == nil then  -- we have no basis for preferring a direction
		return v
	end
	local r = vlength(v)
	if r == 0 then return v end  -- we have no direction
	local theta = math.atan2(v.y, v.x)
	local base = math.atan2(heading.y, heading.x)
	local dtheta = canonicalize_angle(theta - base)
	dtheta = math.max(-ang, math.min(dtheta, ang))
	theta = base + dtheta
	return {x=r*math.cos(theta), y=r*math.sin(theta)}
end

function canonicalize_angle(theta)
	-- fix angle between -pi and +pi
	if theta > math.pi then
		return - (2*math.pi - theta)  -- must be negative
	end
	if theta <= -math.pi then
		return 2*math.pi + theta  -- must be positive
	end
	return theta
end

-- length of a vector
function vlength(v)
	return (v.x^2 + v.y^2)^0.5
end

-- scalar distance between two vectors
function vdist(v1, v2)
	return vlength(vsub(v1, v2))
end

-- add multiple vectors together
function vadd(...)
	local args = {...}
	local x, y = 0, 0
	for _, v in ipairs(args) do
		x = x+v.x
		y = y+v.y
	end
	return {x=x, y=y}
end

-- subtract v2 from v1
function vsub(v1, v2)
	return {x=v1.x-v2.x, y=v1.y-v2.y}
end

-- scale vector to magnitude 1
-- can return nil
function vnorm(v)
	local l = vlength(v)
	if l == 0 then return end
	return vscale(v, 1/l)
end

-- scale vector by factor of k
function vscale(v, k)
	return {x=k*v.x, y=k*v.y}
end

-- turn v 90° counter-clockwise
function vperpendicular_anticlockwise(v)
	return {x=-v.y, y=v.x}
end

-- turn v 90° clockwise
function vperpendicular_clockwise(v)
	return {x=v.y, y=-v.x}
end

-- print a vector to terminal
function vprint(v)
	print('vector:', v.x, v.y)
end