Some configs

This commit is contained in:
Schmop 2024-05-04 22:47:37 +02:00
parent 666b11a477
commit f205d23179
4 changed files with 190 additions and 80 deletions

View File

@ -94,13 +94,20 @@ export class Camera extends GameObject {
lockToObj(object: GameObject) {
this.lockedObject = object;
console.log("Watching", object);
console.info("Watching", object);
}
unlock() {
this.lockedObject = null;
}
reset() {
this.pos = new Vec(0, 0);
this.zoom = 1;
this.rotateAngle = 0;
this.applyTransformation();
}
screenToScene(pos: Vec) {
return pos
.scale(1 / this.zoom)

View File

@ -1,18 +1,11 @@
import { Canvas } from "@/canvas";
import { clamp } from "@/common/functions";
import { GameObject } from "@/common/gameobject";
import { Vec } from "@/common/vec";
import { desiredDensity, particleSize } from "@/game/fluids/fluids";
import { particleSize } from "@/game/fluids/fluids";
import { Swarm } from "@/game/swarm/swarm";
import { fillCircle, strokeLine } from "@/graphics";
import { fillCircle, strokeCircle, 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;
@ -26,13 +19,14 @@ const navigationColorMap = {
behaveLikeFlockDoes: 'yellow',
idling: 'purple'
};
export class Agent {
export class Agent extends GameObject {
dir: Vec;
pos: Vec;
swarm: Swarm;
idlingGoal: Vec|null = null;
lastNavigation: NavigatingDesire;
lastFlockCenter: Vec = new Vec;
constructor(pos: Vec, swarm: Swarm) {
super();
this.pos = pos;
this.swarm = swarm;
this.lastNavigation = {
@ -43,28 +37,61 @@ export class Agent {
this.dir = Vec.randomUnit();
}
update(canvas: Canvas, delta: DOMHighResTimeStamp) {
update({input}: Canvas) {
const nextPos = this.pos.add(this.dir);
const navigations = [];
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;
if (input.mouseDown) {
this.steerTowards(input.mouseWorldPos);
} else {
const navigations = [];
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);
//const navigation = this.combinedNavigation(navigations, nextPos);
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) {
if (targetDir.length() < this.swarm.config.epsilon) {
return;
}
const alpha = this.dir.clockwiseAngleBetween(targetDir.normalize());
this.dir = this.dir.rotate(Math.min(allowedSteer, Math.abs(alpha)) * Math.sign(alpha)).normalize();
this.dir = this.dir.rotate(Math.min(this.swarm.config.allowedSteer, Math.abs(alpha)) * Math.sign(alpha)).normalize();
}
combinedNavigation(navigations: NavigatingDesire[], nextPos: Vec) {
const mostUrgent = this.mostUrgentNavigation(navigations);
if (mostUrgent.urgency === 1) {
return mostUrgent;
}
const weightedDir = navigations
.reduce((sum, n) => {
const diff = n.pos.sub(this.pos);
if (diff.length() < this.swarm.config.epsilon) {
return sum;
}
const dir = diff.normalize();
return sum.add(dir.scale(n.urgency));
}, new Vec);
let pos = this.pos.add(weightedDir.normalize());
if (pos.sub(this.pos).length() < this.swarm.config.epsilon) {
pos = nextPos;
}
const urgency = navigations
.reduce((sum, n) => sum + n.urgency, 0) / navigations.length;
return {
pos,
urgency,
type: mostUrgent.type,
};
}
mostUrgentNavigation(navigations: NavigatingDesire[]) {
@ -74,7 +101,7 @@ export class Agent {
stayInBounds(nextPos: Vec): NavigatingDesire {
const {rect} = this.swarm;
if (rect.contains(this.pos.add(this.dir.scale(padding)))) {
if (rect.contains(this.pos.add(this.dir.scale(this.swarm.config.padding)))) {
return {
pos: nextPos,
urgency: 0,
@ -91,7 +118,7 @@ export class Agent {
doNotBumpIntoFlock(nextPos: Vec): NavigatingDesire {
const {agents} = this.swarm;
const visibleAgentsNotMe = agents
.filter(a => a.pos.distance(this.pos) < flockPerceptionDistance && a !== this);
.filter(a => a.pos.distance(this.pos) < this.swarm.config.flockPerceptionDistance && a !== this);
const visibleAgentsInBounds = visibleAgentsNotMe
.filter(a => this.swarm.rect.contains(a.pos));
let closestAgent: Agent|null = null;
@ -108,9 +135,10 @@ export class Agent {
};
}
const awayFromClosestAgentDir = this.pos.sub(closestAgent.pos).normalize();
const urgency = Math.max(0, (this.swarm.config.desiredDistanceToEveryone - closestAgent.pos.distance(this.pos)) / this.swarm.config.desiredDistanceToEveryone);
return {
pos: this.pos.add(awayFromClosestAgentDir),
urgency: (desiredDistanceToEveryone - closestAgent.pos.distance(this.pos)) / desiredDistanceToEveryone,
urgency: urgency * urgency,
type: 'doNotBumpIntoFlock'
};
}
@ -118,16 +146,18 @@ export class Agent {
keepFlockClose(nextPos: Vec): NavigatingDesire {
const {agents} = this.swarm;
const visibleAgents = agents
.filter(a => a.pos.distance(this.pos) < flockPerceptionDistance);
.filter(a => a.pos.distance(this.pos) < this.swarm.config.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);
this.lastFlockCenter = center;
let urgency = Math.max(0, (distanceToCenter - this.swarm.config.flockIsCloseEnoughDistance) / this.swarm.config.flockPerceptionDistance);
return {
pos: center,
urgency: distanceToCenter / flockPerceptionDistance,
urgency: urgency * urgency,
type: 'keepFlockClose'
};
}
@ -135,7 +165,7 @@ export class Agent {
behaveLikeFlockDoes(nextPos: Vec): NavigatingDesire {
const {agents} = this.swarm;
const visibleAgents = agents
.filter(a => a.pos.distance(this.pos) < flockPerceptionDistance);
.filter(a => a.pos.distance(this.pos) < this.swarm.config.flockPerceptionDistance);
const visibleAgentsInBounds = visibleAgents
.filter(a => this.swarm.rect.contains(a.pos));
const averageDir = visibleAgentsInBounds
@ -144,23 +174,23 @@ export class Agent {
const angleDiff = this.dir.clockwiseAngleBetween(averageDir);
return {
pos: nextPos.add(averageDir),
urgency: behaveLikeFlockDoesUrgency * Math.abs(angleDiff) / Math.PI,
urgency: this.swarm.config.behaveLikeFlockDoesUrgency * Math.abs(angleDiff) / Math.PI,
type: 'behaveLikeFlockDoes'
};
}
idling(nextPos: Vec): NavigatingDesire {
if (this.idlingGoal && this.idlingGoal.distance(nextPos) > idlingDistance) {
if (this.idlingGoal && this.idlingGoal.distance(nextPos) > this.swarm.config.idlingDistance) {
return {
pos: this.idlingGoal,
urgency: idlingUrgency,
urgency: this.swarm.config.idlingUrgency,
type: 'idling'
};
}
this.idlingGoal = Vec.random(this.swarm.rect);
return {
pos: this.idlingGoal,
urgency: idlingUrgency,
urgency: this.swarm.config.idlingUrgency,
type: 'idling'
}
}
@ -172,21 +202,24 @@ export class Agent {
return 255 - this.lastNavigation.urgency * 255;
}
/**render({ctx}: Canvas) {
}*/
render({ctx}: Canvas) {
if (this.swarm.renderStyle === 'satisfaction') {
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, input}: Canvas) {
if (input.mouseDown) {
fillCircle(ctx, this.pos, particleSize, 'gray');
} else {
const color = navigationColorMap[this.lastNavigation.type];
fillCircle(ctx, this.pos, particleSize, color);
if (this.swarm.config.renderStyle === 'satisfaction') {
const satisfaction = this.satisfaction();
const dissatisfaction = 255 - satisfaction;
fillCircle(ctx, this.pos, particleSize, `rgb(${dissatisfaction}, 0, ${satisfaction})`);
} else {
const color = navigationColorMap[this.lastNavigation.type];
fillCircle(ctx, this.pos, particleSize, color);
}
}
if (this.swarm.config.showDirections) {
strokeLine(ctx, this.pos, this.pos.add(this.dir.scale(30)));
}
if (this.swarm.config.showFlockCenter) {
strokeCircle(ctx, this.lastFlockCenter, particleSize, 'orange');
}
}
}

View File

@ -3,34 +3,58 @@ import { GameObject } from "@/common/gameobject";
import { Rect } from "@/common/rect";
import { Vec } from "@/common/vec";
import { Agent } from "@/game/swarm/agent";
import { strokeCircle } from "@/graphics";
import Button from "@/ui/button";
let numAgents = 20;
export type RenderStyle = 'satisfaction'|'navigationMode';
export class Config {
numAgents: number = 20;
padding: number = 50;
allowedSteer: number = 5 / 180;
idlingUrgency: number = 0.001;
idlingDistance: number = 20;
desiredDistanceToEveryone: number = 50;
renderStyle: RenderStyle = 'satisfaction';
showDirections: boolean = false;
showFlockCenter: boolean = false;
flockPerceptionDistance: number = 100;
flockIsCloseEnoughDistance: number = 20;
behaveLikeFlockDoesUrgency: number = 0.8;
epsilon: number = 1e-5;
}
export class Swarm extends GameObject {
rect: Rect;
agents: Agent[];
renderStyle: 'satisfaction'|'navigationMode' = 'navigationMode';
selectedAgent: Agent | null = null;
config: Config;
constructor(canvas: Canvas) {
super();
this.rect = new Rect(0, 0, canvas.width, canvas.height);
this.agents = [];
this.init();
this.config = new Config;
this.init(canvas);
this.initUI(canvas);
// @ts-ignore
window.swarm = this;
}
init() {
init(canvas: Canvas) {
canvas.camera.reset();
this.agents.forEach(a => canvas.remove(a));
this.agents = [];
this.selectedAgent = null;
const rectInTheMiddle = this.rect.translate(this.rect.tl.scale(0.25)).scale(0.5);
const cols = Math.floor(Math.sqrt(numAgents));
const rows = Math.floor(numAgents / cols);
const cols = Math.floor(Math.sqrt(this.config.numAgents));
const rows = Math.floor(this.config.numAgents / cols);
const spacing = rectInTheMiddle.width / cols;
for (let x = 0; x < cols; x++) {
for (let y = 0; y < rows; y++) {
const pos = rectInTheMiddle.tl.add(x * spacing, y * spacing);
this.agents.push(new Agent(pos, this));
const agent = new Agent(pos, this);
this.agents.push(agent);
canvas.add(agent);
}
}
}
@ -38,29 +62,56 @@ export class Swarm extends GameObject {
initUI(canvas: Canvas) {
const buttonSize = new Vec(100, 40);
const buttons = [
new Button("Reset", Rect.bySize(new Vec(10, 10), buttonSize), () => this.init()),
new Button("Reset", Rect.bySize(new Vec(10, 10), buttonSize), () => this.init(canvas)),
new Button("Toggle Colors", Rect.bySize(new Vec(110, 10), buttonSize), () => {
this.renderStyle = this.renderStyle === 'satisfaction' ? 'navigationMode' : 'satisfaction';
this.config.renderStyle = this.config.renderStyle === 'satisfaction' ? 'navigationMode' : 'satisfaction';
}),
new Button("Less Agents", Rect.bySize(new Vec(10, 60), buttonSize), () => {
numAgents -= Math.max(1, numAgents / 10);
this.init();
this.config.numAgents -= Math.max(1, this.config.numAgents / 10);
this.init(canvas);
}),
new Button("More Agents", Rect.bySize(new Vec(110, 60), buttonSize), () => {
numAgents += Math.max(1, numAgents / 10);
this.init();
this.config.numAgents += Math.max(1, this.config.numAgents / 10);
this.init(canvas);
}),
new Button("toggle Directions", Rect.bySize(new Vec(10, 110), buttonSize), () => {
this.config.showDirections = !this.config.showDirections;
}),
new Button("toggle Flock Center", Rect.bySize(new Vec(110, 110), buttonSize), () => {
this.config.showFlockCenter = !this.config.showFlockCenter;
}),
]
buttons.forEach(b => canvas.add(b, Canvas.LAYER_UI));
canvas.input.onRightClick((event ) => {
const mousePos = canvas.input.mouseWorldPos;
this.selectedAgent = this.agents.reduce((closest: Agent|null, a) => {
if (!closest) {
return a;
}
const dist = a.pos.distance(mousePos);
if (dist < closest.pos.distance(mousePos)) {
return a;
}
return closest;
}, null);
if (this.selectedAgent) {
canvas.camera.lockToObj(this.selectedAgent);
}
});
}
update(canvas: Canvas, delta: DOMHighResTimeStamp) {
// simulation seems to be breaking when browser goes to sleep
delta = Math.min(delta, 10);
this.agents.forEach(p => p.update(canvas, delta));
}
render(canvas: Canvas) {
this.agents.forEach(p => p.render(canvas));
const {input, ctx} = canvas;
if (input.mouseDown) {
strokeCircle(ctx, input.mouseWorldPos, 40, 'white');
}
if (this.selectedAgent) {
canvas.ctx.strokeStyle = "darkgrey"
canvas.ctx.strokeRect(this.rect.x, this.rect.y, this.rect.width, this.rect.height);
strokeCircle(ctx, this.selectedAgent.pos, 20, 'purple');
}
}
}

View File

@ -34,33 +34,48 @@ export class Input {
document.addEventListener('keyup', event => {
this.keydowns.delete(event.key);
});
document.addEventListener('mousemove', event => {
this._mousePos = new Vec(event.clientX, event.clientY);
});
document.addEventListener('mouseup', event => {
if (event.button === 0) {
const mouseUp = (event: MouseEvent|TouchEvent) => {
const button = event instanceof MouseEvent ? event.button : 0;
if (button === 0) {
this._mouseDown = false;
} else if (event.button === 1) {
} else if (button === 1) {
this._middleMouseDown = false;
this._middleMousePressed = false;
} else if (event.button === 2) {
} else if (button === 2) {
this._rightMouseDown = false;
}
});
document.addEventListener('mousedown', event => {
if (event.button === 0) {
}
const mouseMove = (event: MouseEvent|TouchEvent) => {
if (event instanceof MouseEvent) {
this._mousePos = new Vec(event.clientX, event.clientY);
} else {
this._mousePos = new Vec(event.touches[0].clientX, event.touches[0].clientY);
}
}
const mouseDown = (event: MouseEvent|TouchEvent) => {
mouseMove(event);
const button = event instanceof MouseEvent ? event.button : 0;
if (button === 0) {
this._mouseDown = true;
this._mousePressed = true;
} else if (event.button === 1) {
} else if (button === 1) {
this._middleMouseDown = true;
this._middleMousePressed = true;
event.preventDefault();
} else if (event.button === 2) {
} else if (button === 2) {
this._rightMouseDown = true;
this._rightMousePressed = true;
event.preventDefault();
}
});
}
document.addEventListener('mousedown', mouseDown);
document.addEventListener('mousemove', mouseMove);
document.addEventListener('mouseup', mouseUp);
document.addEventListener('touchstart', mouseDown);
document.addEventListener('touchmove', mouseMove);
document.addEventListener('touchend', mouseUp);
document.addEventListener('touchcancel', mouseUp);
document.addEventListener('contextmenu', event => {
event.preventDefault();
});
@ -117,6 +132,10 @@ export class Input {
document.addEventListener('click', callback);
}
onRightClick(callback: (event: MouseEvent) => void) {
document.addEventListener('contextmenu', callback);
}
onWheel(callback: (event: WheelEvent) => void) {
document.addEventListener('wheel', callback);
}