Some configs
This commit is contained in:
parent
666b11a477
commit
f205d23179
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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,8 +37,11 @@ export class Agent {
|
|||
this.dir = Vec.randomUnit();
|
||||
}
|
||||
|
||||
update(canvas: Canvas, delta: DOMHighResTimeStamp) {
|
||||
update({input}: Canvas) {
|
||||
const nextPos = this.pos.add(this.dir);
|
||||
if (input.mouseDown) {
|
||||
this.steerTowards(input.mouseWorldPos);
|
||||
} else {
|
||||
const navigations = [];
|
||||
navigations.push(this.stayInBounds(nextPos));
|
||||
navigations.push(this.doNotBumpIntoFlock(nextPos));
|
||||
|
|
@ -52,19 +49,49 @@ export class Agent {
|
|||
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') {
|
||||
render({ctx, input}: Canvas) {
|
||||
if (input.mouseDown) {
|
||||
fillCircle(ctx, this.pos, particleSize, 'gray');
|
||||
} else {
|
||||
if (this.swarm.config.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)));
|
||||
} 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');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
45
src/input.ts
45
src/input.ts
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user