fiddle/src/game/swarm/agent.ts
2024-05-04 14:17:06 +02:00

196 lines
6.8 KiB
TypeScript

import { Canvas } from "@/canvas";
import { clamp } from "@/common/functions";
import { Vec } from "@/common/vec";
import { desiredDensity, particleSize } from "@/game/fluids/fluids";
import { Swarm } from "@/game/swarm/swarm";
import { fillCircle, strokeLine } from "@/graphics";
const padding = 50;
const allowedSteer = 2 / 180;
const idlingUrgency = 0.001;
const idlingDistance = 20;
const desiredDistanceToEveryone = 30;
const flockPerceptionDistance = 150;
const behaveLikeFlockDoesUrgency = 0.8;
const epsilon = 1e-5;
export type NavigationType = 'stayInBounds' | 'doNotBumpIntoFlock' | 'keepFlockClose' | 'behaveLikeFlockDoes' | 'idling';
export type NavigatingDesire = {
pos: Vec;
urgency: number; // [0,1]
type: NavigationType;
}
const navigationColorMap = {
stayInBounds: 'red',
doNotBumpIntoFlock: 'blue',
keepFlockClose: 'green',
behaveLikeFlockDoes: 'yellow',
idling: 'purple'
};
export class Agent {
dir: Vec;
pos: Vec;
swarm: Swarm;
idlingGoal: Vec|null = null;
lastNavigation: NavigatingDesire;
constructor(pos: Vec, swarm: Swarm) {
this.pos = pos;
this.swarm = swarm;
this.lastNavigation = {
pos: pos,
urgency: 0,
type: 'idling'
}
this.dir = Vec.randomUnit();
}
update(canvas: Canvas, delta: DOMHighResTimeStamp) {
const nextPos = this.pos.add(this.dir);
const navigations = [];
/**if (nextPos.y >= canvas.height - padding || nextPos.y < padding) {
this.vel.y *= -1;
nextPos.y = clamp(nextPos.y, padding, canvas.height - padding);
}
if (nextPos.x >= canvas.width - padding || nextPos.x < padding) {
this.vel.x *= -1;
nextPos.x = clamp(nextPos.x, padding, canvas.width - padding);
}*/
navigations.push(this.stayInBounds(nextPos));
navigations.push(this.doNotBumpIntoFlock(nextPos));
navigations.push(this.keepFlockClose(nextPos));
navigations.push(this.behaveLikeFlockDoes(nextPos));
navigations.push(this.idling(nextPos));
const navigation = this.mostUrgentNavigation(navigations);
this.steerTowards(navigation.pos);
this.lastNavigation = navigation;
this.pos = nextPos;
}
steerTowards(target: Vec) {
const targetDir = target.sub(this.pos);
// no division by zero!
if (targetDir.length() < epsilon) {
return;
}
const alpha = this.dir.clockwiseAngleBetween(targetDir.normalize());
this.dir = this.dir.rotate(Math.min(allowedSteer, Math.abs(alpha)) * Math.sign(alpha)).normalize();
}
mostUrgentNavigation(navigations: NavigatingDesire[]) {
return navigations
.reduce((a, b) => a.urgency > b.urgency ? a : b);
}
stayInBounds(nextPos: Vec): NavigatingDesire {
const {rect} = this.swarm;
if (rect.contains(this.pos.add(this.dir.scale(padding)))) {
return {
pos: nextPos,
urgency: 0,
type: 'stayInBounds'
};
}
return {
pos: rect.center,
urgency: 1,
type: 'stayInBounds'
};
}
doNotBumpIntoFlock(nextPos: Vec): NavigatingDesire {
const {agents} = this.swarm;
const visibleAgentsNotMe = agents
.filter(a => a.pos.distance(this.pos) < flockPerceptionDistance && a !== this);
const visibleAgentsInBounds = visibleAgentsNotMe
.filter(a => this.swarm.rect.contains(a.pos));
let closestAgent: Agent|null = null;
for (const a of visibleAgentsInBounds) {
if (!closestAgent || a.pos.distance(this.pos) < closestAgent.pos.distance(this.pos)) {
closestAgent = a;
}
}
if (!closestAgent) {
return {
pos: nextPos,
urgency: 0,
type: 'doNotBumpIntoFlock'
};
}
const awayFromClosestAgentDir = this.pos.sub(closestAgent.pos).normalize();
return {
pos: this.pos.add(awayFromClosestAgentDir),
urgency: (desiredDistanceToEveryone - closestAgent.pos.distance(this.pos)) / desiredDistanceToEveryone,
type: 'doNotBumpIntoFlock'
};
}
keepFlockClose(nextPos: Vec): NavigatingDesire {
const {agents} = this.swarm;
const visibleAgents = agents
.filter(a => a.pos.distance(this.pos) < flockPerceptionDistance);
const visibleAgentsInBounds = visibleAgents
.filter(a => this.swarm.rect.contains(a.pos));
const center = visibleAgentsInBounds
.reduce((sum, a) => sum.add(a.pos), new Vec)
.scale(1 / visibleAgentsInBounds.length);
const distanceToCenter = this.pos.distance(center);
return {
pos: center,
urgency: distanceToCenter / flockPerceptionDistance,
type: 'keepFlockClose'
};
}
behaveLikeFlockDoes(nextPos: Vec): NavigatingDesire {
const {agents} = this.swarm;
const visibleAgents = agents
.filter(a => a.pos.distance(this.pos) < flockPerceptionDistance);
const visibleAgentsInBounds = visibleAgents
.filter(a => this.swarm.rect.contains(a.pos));
const averageDir = visibleAgentsInBounds
.reduce((sum, a) => sum.add(a.dir), new Vec)
.scale(1 / visibleAgentsInBounds.length);
const angleDiff = this.dir.clockwiseAngleBetween(averageDir);
return {
pos: nextPos.add(averageDir),
urgency: behaveLikeFlockDoesUrgency * Math.abs(angleDiff) / Math.PI,
type: 'behaveLikeFlockDoes'
};
}
idling(nextPos: Vec): NavigatingDesire {
if (this.idlingGoal && this.idlingGoal.distance(nextPos) > idlingDistance) {
return {
pos: this.idlingGoal,
urgency: idlingUrgency,
type: 'idling'
};
}
this.idlingGoal = Vec.random(this.swarm.rect);
return {
pos: this.idlingGoal,
urgency: idlingUrgency,
type: 'idling'
}
}
/**
* @returns {Number} between 0 and 255
*/
satisfaction(): number {
return 255 - this.lastNavigation.urgency * 255;
}
/**render({ctx}: Canvas) {
const satisfaction = this.satisfaction();
const dissatisfaction = 255 - satisfaction;
fillCircle(ctx, this.pos, particleSize, `rgb(${dissatisfaction}, 0, ${satisfaction})`);
strokeLine(ctx, this.pos, this.pos.add(this.dir.scale(30)));
}*/
render({ctx}: Canvas) {
const color = navigationColorMap[this.lastNavigation.type];
fillCircle(ctx, this.pos, particleSize, color);
strokeLine(ctx, this.pos, this.pos.add(this.dir.scale(30)));
}
}