Thanks for @Vald and @Egor's comments. Seems the problem is caused by "too-long coroutines" to finish in a PICO-8 cycle. So the solution is that I store unfinished coroutines in a table and resume them if not finished. But somehow the movement is changed, kinda like "lost frame".
Here's my edited code:
function _init()
-- code
cors={}
end
function _update()
for i=1,#boids do
local co=cocreate(move_boid)
local c=boids[i]
add(cors,co)
coresume(co,c)
end
for co in all(cors) do
if (co and costatus(co)!="dead") then
coresume(co)
else
del(cors,co)
end
end
end
And also modify the calculation function, adding a new line in the middle:
function move_boid(c)
-- code
yield()
-- code
end
Just to yield before it's completed.
Update: another way to do it is reusing coroutines.
function _init()
-- code
-- create coroutines
cors={}
for i=1,#boids do
local co=cocreate(move_boid)
local c=boids[i]
add(cors,co)
coresume(co,c)
end
end
function _update()
foreach(cors,coresume)
end
-- and wrap the move function with a loop
function move_boid(c)
while true do
-- code
yield()
-- code
yield()
end
end