Swarm behaviour!
This commit is contained in:
@@ -0,0 +1,184 @@
|
||||
import { Canvas } from "@/canvas";
|
||||
import { GameObject } from "@/common/gameobject";
|
||||
import { Rect } from "@/common/rect";
|
||||
import { Vec } from "@/common/vec";
|
||||
import { Particle } from "@/game/fluids/particle";
|
||||
import { strokeCircle } from "@/graphics";
|
||||
|
||||
const gravity = 0.008;
|
||||
const numParticles = 200;
|
||||
const pressureForce = 8000;
|
||||
export const particleSize = 10;
|
||||
export const desiredDensity = 6;
|
||||
const densityRadius = 40;
|
||||
const mass = 1;
|
||||
const viscosityStrength = 0.4;
|
||||
const viscosityRadius = 100;
|
||||
const mouseRadius = 200;
|
||||
const mouseForce = 0.2;
|
||||
const densityKernelVolume = Math.PI * Math.pow(densityRadius, 4) / 6;
|
||||
const derivativeScale = 12 / (Math.PI * Math.pow(densityRadius, 4));
|
||||
const viscosityKernelVolume = Math.PI * Math.pow(viscosityRadius, 8) / 4;
|
||||
|
||||
export class Fluids extends GameObject {
|
||||
particles: Particle[];
|
||||
rect: Rect;
|
||||
constructor(canvas: Canvas) {
|
||||
super();
|
||||
this.particles = [];
|
||||
this.rect = new Rect(0, 0, canvas.width, canvas.height);
|
||||
this.init();
|
||||
// @ts-ignore
|
||||
window.fluids = this;
|
||||
}
|
||||
|
||||
init() {
|
||||
const rectInTheMiddle = this.rect.translate(this.rect.tl.scale(0.25)).scale(0.5);
|
||||
const cols = Math.floor(Math.sqrt(numParticles));
|
||||
const rows = Math.floor(numParticles / 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.particles.push(new Particle(pos));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
update(canvas: Canvas, delta: DOMHighResTimeStamp) {
|
||||
// fluid simulation seems to be breaking when browser goes to sleep
|
||||
delta = Math.min(delta, 10);
|
||||
this.predictParticles(delta);
|
||||
this.applyGravityForce(delta);
|
||||
this.applyPressureForce(delta);
|
||||
this.applyViscosityForce(delta);
|
||||
this.applyMouseForce(canvas);
|
||||
this.particles.forEach(p => p.update(canvas, delta));
|
||||
}
|
||||
|
||||
applyMouseForce({input}: Canvas) {
|
||||
if (input.mouseDown) {
|
||||
for (const p of this.particles) {
|
||||
const diff = input.mousePos.sub(p.pos);
|
||||
const dist = diff.length();
|
||||
if (dist < mouseRadius) {
|
||||
p.vel = p.vel.add(diff.normalize().scale(mouseForce));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
predictParticles(delta: DOMHighResTimeStamp) {
|
||||
for (const p of this.particles) {
|
||||
p.posPrediction = p.pos.add(p.vel.scale(delta));
|
||||
}
|
||||
}
|
||||
|
||||
smoothingKernel(dist: number) {
|
||||
const diff = densityRadius - dist;
|
||||
return diff * diff / densityKernelVolume;
|
||||
}
|
||||
|
||||
smoothingKernelDerivative(dist: number) {
|
||||
if (dist >= densityRadius) return 0;
|
||||
return (dist - densityRadius) * derivativeScale;
|
||||
}
|
||||
|
||||
calculateDensities() {
|
||||
for (const p of this.particles) {
|
||||
p.densitySample = 0;
|
||||
for (const other of this.particles) {
|
||||
if (p === other) continue;
|
||||
|
||||
const dist = p.posPrediction.distance(other.posPrediction);
|
||||
p.densitySample += mass * this.smoothingKernel(dist);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
convertDensityToPressure(density: number) {
|
||||
const densityError = density - desiredDensity;
|
||||
return densityError * pressureForce;
|
||||
}
|
||||
|
||||
calculateSharedPressure(a: Particle, b: Particle) {
|
||||
const pressureA = this.convertDensityToPressure(a.densitySample);
|
||||
const pressureB = this.convertDensityToPressure(b.densitySample);
|
||||
return (pressureA + pressureB) / 2;
|
||||
}
|
||||
|
||||
calculatePressureForce(p: Particle) {
|
||||
let pressureForce = new Vec;
|
||||
for (const other of this.particles) {
|
||||
if (p === other) continue;
|
||||
const diff = p.posPrediction.sub(other.posPrediction);
|
||||
const dist = diff.length();
|
||||
const dir = dist <= 0 ? Vec.randomUnit() : diff.normalize();
|
||||
const slope = this.smoothingKernelDerivative(dist);
|
||||
const density = other.densitySample;
|
||||
const sharedPressure = this.calculateSharedPressure(p, other);
|
||||
pressureForce = pressureForce.sub(
|
||||
dir.scale(
|
||||
sharedPressure * slope * mass / density
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
return pressureForce;
|
||||
}
|
||||
|
||||
viscositySmoothingKernel(dist: number) {
|
||||
const value = Math.max(0, viscosityRadius * viscosityRadius - dist * dist);
|
||||
|
||||
return value * value * value / viscosityKernelVolume;
|
||||
}
|
||||
|
||||
calculateViscosityForce(p: Particle) {
|
||||
let viscosityForce = new Vec;
|
||||
for (const other of this.particles) {
|
||||
if (p === other) continue;
|
||||
const dist = p.posPrediction.distance(other.posPrediction);
|
||||
const influence = this.viscositySmoothingKernel(dist);
|
||||
|
||||
viscosityForce = viscosityForce.add(
|
||||
other.vel.sub(p.vel).scale(influence)
|
||||
);
|
||||
}
|
||||
|
||||
return viscosityForce.scale(viscosityStrength);
|
||||
}
|
||||
|
||||
applyPressureForce(delta: DOMHighResTimeStamp) {
|
||||
this.calculateDensities();
|
||||
for (const p of this.particles) {
|
||||
p.vel = p.vel.add(this.calculatePressureForce(p).scale(delta));
|
||||
}
|
||||
}
|
||||
|
||||
applyViscosityForce(delta: DOMHighResTimeStamp) {
|
||||
for (const p of this.particles) {
|
||||
p.vel = p.vel.add(this.calculateViscosityForce(p).scale(delta));
|
||||
}
|
||||
}
|
||||
|
||||
applyGravityForce(delta: DOMHighResTimeStamp) {
|
||||
for (const p of this.particles) {
|
||||
p.vel = p.vel.add(new Vec(0, gravity * delta));
|
||||
}
|
||||
}
|
||||
|
||||
render(canvas: Canvas) {
|
||||
this.particles.forEach(p => p.render(canvas));
|
||||
if (canvas.input.mouseDown) {
|
||||
strokeCircle(canvas.ctx, canvas.input.mousePos, mouseRadius, "white");
|
||||
}
|
||||
}
|
||||
|
||||
getAverageDensity() {
|
||||
return this.particles.reduce((acc, p) => acc + p.densitySample, 0) / this.particles.length;
|
||||
}
|
||||
|
||||
getLowestDensity() {
|
||||
return this.particles.reduce((acc, p) => Math.min(acc, p.densitySample), Infinity);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
import { Canvas } from "@/canvas";
|
||||
import { clamp } from "@/common/functions";
|
||||
import { Vec } from "@/common/vec";
|
||||
import { desiredDensity, particleSize } from "@/game/fluids/fluids";
|
||||
import { fillCircle } from "@/graphics";
|
||||
|
||||
const padding = 50;
|
||||
export class Particle {
|
||||
vel: Vec;
|
||||
pos: Vec;
|
||||
posPrediction: Vec;
|
||||
densitySample: number;
|
||||
constructor(pos: Vec) {
|
||||
this.pos = pos;
|
||||
this.posPrediction = new Vec;
|
||||
this.vel = new Vec;
|
||||
this.densitySample = 0;
|
||||
}
|
||||
update(canvas: Canvas, delta: DOMHighResTimeStamp) {
|
||||
const nextPos = this.pos.add(this.vel);
|
||||
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);
|
||||
}
|
||||
this.pos = nextPos;
|
||||
}
|
||||
render({ctx}: Canvas) {
|
||||
const densityError = this.densitySample - desiredDensity; // [0, inf)
|
||||
const densityErrorNormalized = Math.min(densityError * densityError / 4, 255); // [0, 255]
|
||||
const inverted = 255 - densityErrorNormalized;
|
||||
fillCircle(ctx, this.pos, particleSize, `rgb(${densityErrorNormalized}, 0, ${inverted})`);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,168 @@
|
||||
import { Canvas } from "@/canvas";
|
||||
import { create2DArray } from "@/common/array";
|
||||
import { GameObject } from "@/common/gameobject";
|
||||
import { GridSize } from "@/common/grid";
|
||||
import { perlinNoise } from "@/common/noise";
|
||||
import { Rect } from "@/common/rect";
|
||||
import { Vec } from "@/common/vec";
|
||||
import { Black, Blue, Color, Green, White } from "@/ui/colors";
|
||||
|
||||
export type TileState = 'land'|'air'|'none';
|
||||
|
||||
const TileColors: Record<TileState, Color> = {
|
||||
land: White,
|
||||
air: Blue,
|
||||
none: Black,
|
||||
};
|
||||
|
||||
export type Tile = {
|
||||
state: TileState,
|
||||
}
|
||||
|
||||
export class Gamemap extends GameObject {
|
||||
public tiles: Tile[][];
|
||||
public cols: number;
|
||||
public rows: number;
|
||||
public rect: Rect;
|
||||
|
||||
constructor(canvas: Canvas) {
|
||||
super();
|
||||
this.rows = Math.floor(canvas.width / GridSize);
|
||||
this.cols = Math.floor(canvas.height / GridSize);
|
||||
this.rect = new Rect(0, 0, this.rows, this.cols);
|
||||
this.tiles = create2DArray(this.rows, this.cols, (x: number, y: number) => ({state: 'air'}));
|
||||
}
|
||||
|
||||
getState(x: number, y: number): TileState;
|
||||
getState(pos: Vec): TileState;
|
||||
getState(x: number|Vec, y?: number): TileState {
|
||||
const pos = x instanceof Vec ? x : new Vec(x, y);
|
||||
|
||||
return this.tiles[pos.x][pos.y].state;
|
||||
}
|
||||
|
||||
setState(x: number, y: number, state: TileState): void;
|
||||
setState(pos: Vec, state: TileState): void;
|
||||
setState(x: number|Vec, y: TileState|number, state?: TileState) {
|
||||
if (x instanceof Vec && "string" === typeof y) {
|
||||
this.tiles[x.x][x.y].state = y;
|
||||
} else if ("number" === typeof x && "number" === typeof y && "string" === typeof state) {
|
||||
this.tiles[x][y].state = state;
|
||||
} else {
|
||||
throw new TypeError("Invalid arguments for setState");
|
||||
}
|
||||
}
|
||||
|
||||
render(canvas: Canvas): void {
|
||||
const {ctx} = canvas;
|
||||
|
||||
|
||||
for (let x = 0; x < this.rows; x++) {
|
||||
for (let y = 0; y < this.cols; y++) {
|
||||
ctx.fillStyle = TileColors[this.getState(x, y)];
|
||||
ctx.fillRect(x * GridSize, y * GridSize, GridSize, GridSize);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
update(canvas: Canvas) {
|
||||
/** noop */
|
||||
}
|
||||
}
|
||||
|
||||
function findNearestTile(map: Gamemap, pos: Vec): Vec|null {
|
||||
let distance = 1;
|
||||
while (distance < Math.max(map.rows, map.cols)) {
|
||||
for (let i = -distance; i <= distance; i++) {
|
||||
const top = pos.add(i, -distance);
|
||||
if (map.rect.contains(top) && map.getState(top) === 'land') {
|
||||
return top;
|
||||
}
|
||||
const bottom = pos.add(i, distance);
|
||||
if (map.rect.contains(bottom) && map.getState(bottom) === 'land') {
|
||||
return bottom;
|
||||
}
|
||||
const left = pos.add(-distance, i);
|
||||
if (map.rect.contains(left) && map.getState(left) === 'land') {
|
||||
return left;
|
||||
}
|
||||
const right = pos.add(distance, i);
|
||||
if (map.rect.contains(right) && map.getState(right) === 'land') {
|
||||
return right;
|
||||
}
|
||||
}
|
||||
distance++;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
function marchToFirstEmptyTile(map: Gamemap, from: Vec, to: Vec): Vec|null {
|
||||
const direction = to.sub(from).normalize();
|
||||
if (from.distance(to) <= 1) {
|
||||
return null;
|
||||
}
|
||||
if (Math.abs(direction.x) > Math.abs(direction.y)) {
|
||||
direction.x = Math.sign(direction.x);
|
||||
direction.y = 0;
|
||||
} else {
|
||||
direction.x = 0;
|
||||
direction.y = Math.sign(direction.y);
|
||||
}
|
||||
const pos = from.add(direction);
|
||||
if (map.getState(pos) !== 'land') {
|
||||
return pos;
|
||||
}
|
||||
return marchToFirstEmptyTile(map, pos, to);
|
||||
}
|
||||
|
||||
function floodFill(map: Gamemap, start: Vec, state: TileState): void {
|
||||
const stack: Vec[] = [start];
|
||||
const removeState = map.getState(start);
|
||||
while (stack.length) {
|
||||
const pos = stack.pop();
|
||||
if (!pos || !map.rect.contains(pos) || map.getState(pos) !== removeState) {
|
||||
continue;
|
||||
}
|
||||
map.setState(pos, state);
|
||||
stack.push(
|
||||
pos.add(1, 0),
|
||||
pos.add(-1, 0),
|
||||
pos.add(0, 1),
|
||||
pos.add(0, -1),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function removeAirPockets(map: Gamemap): void {
|
||||
for (let x = 0; x < map.rows; x++) {
|
||||
for (let y = 0; y < map.cols; y++) {
|
||||
if (map.getState(x, y) === 'air') {
|
||||
map.setState(x, y, 'land');
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function fillMap(map: Gamemap): void {
|
||||
for (let x = 0; x < map.rows; x++) {
|
||||
for (let y = 0; y < map.cols; y++) {
|
||||
const noise =
|
||||
perlinNoise(new Vec(x, y).scale(0.03));
|
||||
if (noise > 0.5) {
|
||||
map.setState(x, y, 'land');
|
||||
}
|
||||
}
|
||||
}
|
||||
for (let i = 0; i < map.rows * map.cols * 0.3; i++) {
|
||||
const pos = Vec.random(map.rows - 1, map.cols - 1);
|
||||
const tile = findNearestTile(map, pos);
|
||||
if (!tile) {
|
||||
throw new Error("No tile found, wtf?!");
|
||||
}
|
||||
const emptyTile = marchToFirstEmptyTile(map, tile, pos) ?? pos;
|
||||
map.setState(emptyTile, 'land');
|
||||
}
|
||||
floodFill(map, new Vec(0, 0), 'none');
|
||||
removeAirPockets(map);
|
||||
}
|
||||
@@ -0,0 +1,195 @@
|
||||
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)));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
import { Canvas } from "@/canvas";
|
||||
import { GameObject } from "@/common/gameobject";
|
||||
import { Rect } from "@/common/rect";
|
||||
import { Agent } from "@/game/swarm/agent";
|
||||
|
||||
const numAgents = 100;
|
||||
|
||||
export class Swarm extends GameObject {
|
||||
rect: Rect;
|
||||
agents: Agent[];
|
||||
constructor(canvas: Canvas) {
|
||||
super();
|
||||
this.rect = new Rect(0, 0, canvas.width, canvas.height);
|
||||
this.agents = [];
|
||||
this.init();
|
||||
// @ts-ignore
|
||||
window.swarm = this;
|
||||
}
|
||||
|
||||
init() {
|
||||
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 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));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user