Posted: 30th Dec 2021 20:57
The title says it all.
After a discussion on discord I knocked this quick example together.
+ Code Snippet
 
// Project: Path_Following_Demo 
// Created: 2021-12-30

// show all errors
SetErrorMode(2)

// set window properties
SetWindowTitle( "Path_Following_Demo" )
SetWindowSize( 1200, 800, 0 )
SetWindowAllowResize( 1 ) // allow the user to resize the window

// set display properties
SetVirtualResolution( 1200, 800 ) // doesn't have to match the window
SetOrientationAllowed( 1, 1, 1, 1 ) // allow both portrait and landscape on mobile devices
SetSyncRate( 0, 0 ) // 30fps instead of 60 to save battery
SetScissor( 0,0,0,0 ) // use the maximum available screen space, no black borders
UseNewDefaultFonts( 1 ) // since version 2.0.22 we can use nicer default fonts


type tWayPoint
	col as integer
	txt as integer
	x as float
	y as float
endtype


type tBlob
	spr as integer
	wp as integer
	turnspeed as integer
	speed as float
	angle as float
	desired as float
endtype


global path as tWayPoint[5]
global blob as tBlob[]
global collide as integer = 1


SetPhysicsGravity(0, 0)
SetPhysicsDebugOn()
SetViewOffset(0, 0) //Short-cut to remove physics walls


//Create Path
NewPath()

//Create Blobs
CreateBlobs(10)


do
    Print("Press P for a new random path")
    Print("Press Up Arrow for more blobs (Currently: " + str(blob.length+1)+")")
    Print("Press C to toggle collision on/off (Currently: "+GetOnOff(collide))
    
	if GetRawKeyPressed(80)
		NewPath()
	endif
	if GetRawKeyPressed(38)
		CreateBlobs(10)
	endif
	if GetRawKeyPressed(67)
		ToggleCollision()
	endif
	
	UpdateBlobs()
	DrawPath()
	
    Print("FPS: " + str(ScreenFPS(), 0))
    Sync()
loop



function UpdateBlobs()
	for c = 0 to blob.length
		nextWP = mod((blob[c].wp+1), path.length+1)
		
		if GetSpriteCollision(blob[c].spr, path[nextWp].col)
			inc blob[c].wp
		endif
		
		x1# = GetSpriteXByOffset(blob[c].spr)
		y1# = GetSpriteYByOffset(blob[c].spr)
		x2# = path[nextWP].x
		y2# = path[nextWP].y
		blob[c].desired = ATanFull(x2#-x1#, y2#-y1#)
		blob[c].angle = CurveAngle(blob[c].desired, blob[c].angle, blob[c].turnspeed) 
		SetSpritePhysicsVelocity(blob[c].spr, sin(blob[c].angle)*blob[c].speed, -cos(blob[c].angle)*blob[c].speed)
	next c
endfunction


	
function NewPath()
	DeleteAllText()
	//Create Random Path
	for k = 0 to path.length
		if GetSpriteExists(path[k].col) then DeleteSprite(path[k].col)
		path[k].x = Random2(GetScreenBoundsLeft()+50, GetScreenBoundsRight()-50)
		path[k].y = Random2(GetScreenBoundsTop()+50, GetScreenBoundsBottom()-50)
		path[k].col = CreateDummySprite()
		SetSpritePositionByOffset(path[k].col, path[k].x, path[k].y)
		SetSpritePhysicsOn(path[k].col, 1)
		SetSpriteSize(path[k].col, 100, 100)
		SetSpriteShape(path[k].col, 1)
		SetSpritePhysicsIsSensor(path[k].col, 1)
		path[k].txt = CreateText(str(k))
		SetTextSize(path[k].txt, 20)
		SetTextPosition(path[k].txt, path[k].x, path[k].y)
	next k
endfunction


function DrawPath()
	c = MakeColor(0, 255, 0, 255)
	for p = 0 to path.length-1
		DrawLine(path[p].x, path[p].y, path[p+1].x, path[p+1].y, c, c)
	next p
	DrawLine(path[p].x, path[p].y, path[0].x, path[0].y, c, c)
endfunction


function CreateBlobs(qty)
	b as tBlob
	for c = 1 to qty
		b.spr = CreateSprite(0)
		SetSpriteColor(b.spr, Random2(0, 255), Random2(0, 255), Random2(0, 255), 255)
		b.wp = 0
		b.speed = Random2(50, 150)
		b.turnspeed = Random2(30, 150)
		SetSpritePhysicsOn(b.spr, 2)
		SetSpriteSize(b.spr, 10, -1)
		SetSpritePhysicsCanRotate(b.spr, 0)
		SetSpriteCollideBit(b.spr, 1, collide)
		SetSpriteShape(b.spr, 1)
		SetSpritePositionByOffset(b.spr, path[0].x, path[0].y)
		blob.insert(b)
	next c
endfunction


function ToggleCollision()
	collide = not(collide)
	for b = 0 to blob.length
		SetSpriteCollideBit(blob[b].spr, 1, collide)
	next b
endfunction


function GetOnOff(value)
	if value then exitfunction "On"
endfunction "Off"


//**********************************
//	Wrap Angle
//**********************************
//Returns a value that does not exceed the range of 0 to 360.
function WrapAngle(angle as float)
	angle = fmod(angle, 360.0)
	if angle <0 then angle=angle+360
endfunction angle



//**********************************
//	Curve Angle
//**********************************
//This command will return an auto-interpolated angle based on a given speed.
function CurveAngle( destination as float, current as float,  speed as float)
	local diff as float
	if speed < 1.0 then speed = 1.0
	destination = WrapAngle( destination )
	current = WrapAngle( current )
	diff = destination - current
	if diff <- 180.0 then diff = ( destination + 360.0 ) - current
	if diff > 180.0 then diff = destination - ( current + 360.0 )
	current = current + ( diff / speed )
	current = WrapAngle( current )
endfunction current


I should credit Virtual Nomad with the idea for this. I didn't realise how much advantage could be gained by misusing Box2D like this.

[Edit] Minor update. You can now press the up arrow to increase the number of 'blobs'
[Edit2] Another small update to toggle collision on/off
Posted: 30th Dec 2021 23:07
That's pretty good.
They all do the same thing but seem to have a mind of their own.
Posted: 31st Dec 2021 14:11
It's pretty cool isn't it?
In the update above I've given them all a different turn rate a well as a different speed so the 'emergent behaviour' is even more pronounced
Posted: 31st Dec 2021 21:11
I was staring at your program for a while and so many ideas come to mind.

At first it looked like an AI race.
Then it looked like a crowd behaviour sim. Like when there is a big sale on with the faster ones pushing the slower ones out the way.

Then I thought it could be herbivores running from a carnivore with the slower one getting caught.
You could even throw in a final waypoint for one or two of them to go and hide behind something.

This could be used in so many ways limited only by imagination. I like it!
Posted: 1st Jan 2022 9:10
I held at a steady 350 fps until I got to about 500 blobs. But it's more than just path following, each blob is aware of the others