From f3449c15269d26a891b1506d57be6b03c1cfe4ca Mon Sep 17 00:00:00 2001 From: Joe Weaver Date: Wed, 7 Oct 2020 15:00:28 -0400 Subject: [PATCH] added AI support --- src/Behaviors/Behavior.ts | 14 +++++ src/BoidDemo.ts | 57 +++++++++++++++++++ src/DataTypes/AABB.ts | 70 +++++++++++++++++++++++ src/DataTypes/RegionQuadTree.ts | 9 ++- src/DataTypes/Vec2.ts | 24 ++++++++ src/Loop/GameLoop.ts | 31 ++++++++++- src/Nodes/GameNode.ts | 30 ++++++++++ src/SceneGraph/SceneGraphArray.ts | 10 +++- src/SceneGraph/SceneGraphQuadTree.ts | 14 +++-- src/SceneGraph/Viewport.ts | 14 +++++ src/Utils/MathUtils.ts | 28 +++++++++- src/_DemoClasses/Boid.ts | 43 ++++++++++++++ src/_DemoClasses/BoidBehavior.ts | 82 +++++++++++++++++++++++++++ src/_DemoClasses/FlockBehavior.ts | 83 ++++++++++++++++++++++++++++ src/main.ts | 3 +- 15 files changed, 495 insertions(+), 17 deletions(-) create mode 100644 src/Behaviors/Behavior.ts create mode 100644 src/BoidDemo.ts create mode 100644 src/_DemoClasses/Boid.ts create mode 100644 src/_DemoClasses/BoidBehavior.ts create mode 100644 src/_DemoClasses/FlockBehavior.ts diff --git a/src/Behaviors/Behavior.ts b/src/Behaviors/Behavior.ts new file mode 100644 index 0000000..d31df77 --- /dev/null +++ b/src/Behaviors/Behavior.ts @@ -0,0 +1,14 @@ +import Emitter from "../Events/Emitter"; +import Receiver from "../Events/Receiver"; + +export default abstract class Behavior { + protected receiver: Receiver; + protected emitter: Emitter; + + constructor(){ + this.receiver = new Receiver(); + this.emitter = new Emitter(); + } + + abstract doBehavior(deltaT: number): void; +} \ No newline at end of file diff --git a/src/BoidDemo.ts b/src/BoidDemo.ts new file mode 100644 index 0000000..6464a6d --- /dev/null +++ b/src/BoidDemo.ts @@ -0,0 +1,57 @@ +import Vec2 from "./DataTypes/Vec2"; +import Debug from "./Debug/Debug"; +import Point from "./Nodes/Graphics/Point"; +import Scene from "./Scene/Scene"; +import SceneGraphQuadTree from "./SceneGraph/SceneGraphQuadTree"; +import Color from "./Utils/Color"; +import Boid from "./_DemoClasses/Boid"; +import BoidBehavior from "./_DemoClasses/BoidBehavior"; +import FlockBehavior from "./_DemoClasses/FlockBehavior"; + +/** + * This demo emphasizes an ai system for the game engine with component architecture + * Boids move around with components + * Boids have randomized affects (maybe?) + * Boids respond to player movement + */ +export default class BoidDemo extends Scene { + boids: Array; + + startScene(){ + // Set the world size + this.worldSize = new Vec2(800, 600); + this.sceneGraph = new SceneGraphQuadTree(this.viewport, this); + this.viewport.setBounds(0, 0, 800, 600) + this.viewport.setCenter(400, 300); + + let layer = this.addLayer() + this.boids = new Array(); + + // Create a bunch of boids + for(let i = 0; i < 200; i++){ + let boid = this.add.graphic(Boid, layer, new Vec2(this.worldSize.x*Math.random(), this.worldSize.y*Math.random())); + let separation = 3; + let alignment = 1; + let cohesion = 3; + boid.addBehavior(new BoidBehavior(this, boid, separation, alignment, cohesion)); + boid.addBehavior(new FlockBehavior(this, boid, this.boids, 75, 50)); + boid.setSize(5, 5); + this.boids.push(boid); + } + } + + updateScene(deltaT: number): void { + for(let boid of this.boids){ + boid.setColor(Color.RED); + } + + for(let boid of this.boids){ + boid.getBehavior(FlockBehavior).doBehavior(deltaT); + } + + + for(let boid of this.boids){ + boid.getBehavior(BoidBehavior).doBehavior(deltaT); + } + } +} \ No newline at end of file diff --git a/src/DataTypes/AABB.ts b/src/DataTypes/AABB.ts index 10b71e8..4114f4c 100644 --- a/src/DataTypes/AABB.ts +++ b/src/DataTypes/AABB.ts @@ -1,5 +1,6 @@ import Shape from "./Shape"; import Vec2 from "./Vec2"; +import MathUtils from "../Utils/MathUtils"; export default class AABB extends Shape { @@ -101,6 +102,68 @@ export default class AABB extends Shape { && point.y > this.y - this.hh && point.y <= this.y + this.hh } + + /** + * Returns the data from the intersection of this AABB with a line segment from a point in a direction + * @param point The point that the line segment starts from + * @param direction The direction the point will go + * @param distance The length of the line segment, if the direction is a unit vector + * @param paddingX Pads the AABB in the x axis + * @param paddingY Pads the AABB in the y axis + */ + intersectSegment(point: Vec2, direction: Vec2, distance?: number, paddingX?: number, paddingY?: number): Hit { + // Scale by the distance if it has been provided + if(distance){ + direction = direction.scaled(distance); + } + + let _paddingX = paddingX ? paddingX : 0; + let _paddingY = paddingY ? paddingY : 0; + + let scaleX = 1/direction.x; + let scaleY = 1/direction.y; + + let signX = MathUtils.sign(scaleX); + let signY = MathUtils.sign(scaleY); + + let tnearx = scaleX*(this.center.x - signX*(this.halfSize.x + _paddingX) - point.x); + let tneary = scaleX*(this.center.y - signY*(this.halfSize.y + _paddingY) - point.y); + let tfarx = scaleY*(this.center.x + signX*(this.halfSize.x + _paddingX) - point.x); + let tfary = scaleY*(this.center.y + signY*(this.halfSize.y + _paddingY) - point.y); + + if(tnearx > tfary || tneary > tfarx){ + // We aren't colliding - we clear one axis before intersecting another + return null; + } + + let tnear = Math.max(tnearx, tneary); + let tfar = Math.min(tfarx, tfary); + + if(tnear >= 1 || tfar <= 0){ + return null; + } + + // We are colliding + let hit = new Hit(); + hit.t = MathUtils.clamp01(tnear); + + if(tnearx > tneary){ + // We hit on the left or right size + hit.normal.x = -signX; + hit.normal.y = 0; + } else { + hit.normal.x = 0; + hit.normal.y = -signY; + } + + hit.delta.x = (1.0 - hit.t) * -direction.x; + hit.delta.y = (1.0 - hit.t) * -direction.y; + hit.pos.x = point.x + direction.x * hit.t; + hit.pos.y = point.y + direction.y * hit.t; + + return hit; + } + /** * A simple boolean check of whether this AABB overlaps another * @param other @@ -137,4 +200,11 @@ export default class AABB extends Shape { return dx*dy; } +} + +export class Hit { + t: number; + pos: Vec2 = Vec2.ZERO; + delta: Vec2 = Vec2.ZERO; + normal: Vec2 = Vec2.ZERO; } \ No newline at end of file diff --git a/src/DataTypes/RegionQuadTree.ts b/src/DataTypes/RegionQuadTree.ts index 8e4fbed..4a82c33 100644 --- a/src/DataTypes/RegionQuadTree.ts +++ b/src/DataTypes/RegionQuadTree.ts @@ -178,10 +178,10 @@ export default class QuadTree implements Collection { let hw = this.boundary.hw; let hh = this.boundary.hh; - this.nw = new QuadTree(new Vec2(x-hw/2, y-hh/2), new Vec2(hw/2, hh/2), this.maxDepth - 1); - this.ne = new QuadTree(new Vec2(x+hw/2, y-hh/2), new Vec2(hw/2, hh/2), this.maxDepth - 1) - this.sw = new QuadTree(new Vec2(x-hw/2, y+hh/2), new Vec2(hw/2, hh/2), this.maxDepth - 1) - this.se = new QuadTree(new Vec2(x+hw/2, y+hh/2), new Vec2(hw/2, hh/2), this.maxDepth - 1) + this.nw = new QuadTree(new Vec2(x-hw/2, y-hh/2), new Vec2(hw/2, hh/2), this.maxDepth - 1, this.capacity); + this.ne = new QuadTree(new Vec2(x+hw/2, y-hh/2), new Vec2(hw/2, hh/2), this.maxDepth - 1, this.capacity); + this.sw = new QuadTree(new Vec2(x-hw/2, y+hh/2), new Vec2(hw/2, hh/2), this.maxDepth - 1, this.capacity); + this.se = new QuadTree(new Vec2(x+hw/2, y+hh/2), new Vec2(hw/2, hh/2), this.maxDepth - 1, this.capacity); this.distributeItems(); } @@ -213,7 +213,6 @@ export default class QuadTree implements Collection { * @param ctx */ public render_demo(ctx: CanvasRenderingContext2D): void { - return; ctx.strokeStyle = "#0000FF"; ctx.strokeRect(this.boundary.x - this.boundary.hw, this.boundary.y - this.boundary.hh, 2*this.boundary.hw, 2*this.boundary.hh); diff --git a/src/DataTypes/Vec2.ts b/src/DataTypes/Vec2.ts index f3e1dca..cbf08be 100644 --- a/src/DataTypes/Vec2.ts +++ b/src/DataTypes/Vec2.ts @@ -80,6 +80,14 @@ export default class Vec2 { return this; } + /** + * Returns a new vector that is the normalized version of this one + */ + normalized(){ + let mag = this.mag(); + return new Vec2(this.x/mag, this.y/mag); + } + /** * Sets the vector's x and y based on the angle provided. Goes counter clockwise. * @param angle The angle in radians @@ -205,6 +213,22 @@ export default class Vec2 { return (this.x - other.x)*(this.x - other.x) + (this.y - other.y)*(this.y - other.y); } + /** + * Returns the distance between this vector and another vector + * @param other + */ + distanceTo(other: Vec2): number { + return Math.sqrt(this.distanceSqTo(other)); + } + + /** + * Returns the dot product of this vector and another + * @param other + */ + dot(other: Vec2): number { + return this.x*other.x + this.y*other.y; + } + /** * Returns a string representation of this vector rounded to 1 decimal point */ diff --git a/src/Loop/GameLoop.ts b/src/Loop/GameLoop.ts index 253b6e5..032e78c 100644 --- a/src/Loop/GameLoop.ts +++ b/src/Loop/GameLoop.ts @@ -27,7 +27,9 @@ export default class GameLoop { private started: boolean; private running: boolean; - private frameDelta: number; + private frameDelta: number; + private panic: boolean; + private numUpdateSteps: number; // Game canvas and its width and height readonly GAME_CANVAS: HTMLCanvasElement; @@ -61,7 +63,7 @@ export default class GameLoop { this.running = false; // Get the game canvas and give it a background color - this.GAME_CANVAS = document.getElementById("game-canvas") as HTMLCanvasElement; + this.GAME_CANVAS = document.getElementById("game-canvas"); this.GAME_CANVAS.style.setProperty("background-color", "whitesmoke"); // Give the canvas a size and get the rendering context @@ -169,17 +171,40 @@ export default class GameLoop { this.lastFrameTime = timestamp; // Update while we can (This will present problems if we leave the window) - let i = 0; + this.numUpdateSteps = 0; while(this.frameDelta >= this.simulationTimestep){ this.update(this.simulationTimestep/1000); this.frameDelta -= this.simulationTimestep; + this.numUpdateSteps++; + if(this.numUpdateSteps > 100){ + this.panic = true; + } + // Update the frame of the game this.updateFrameCount(this.simulationTimestep); } // Updates are done, draw this.render(); + + // End the frame + this.end(); + + this.panic = false; + } + + end(){ + if(this.panic) { + var discardedTime = Math.round(this.resetFrameDelta()); + console.warn('Main loop panicked, probably because the browser tab was put in the background. Discarding ' + discardedTime + 'ms'); + } + } + + resetFrameDelta() : number { + var oldFrameDelta = this.frameDelta; + this.frameDelta = 0; + return oldFrameDelta; } /** diff --git a/src/Nodes/GameNode.ts b/src/Nodes/GameNode.ts index 8b97f49..a3796cb 100644 --- a/src/Nodes/GameNode.ts +++ b/src/Nodes/GameNode.ts @@ -7,6 +7,7 @@ import Scene from "../Scene/Scene"; import Layer from "../Scene/Layer"; import { Positioned, Unique } from "../DataTypes/Interfaces/Descriptors" import UIElement from "./UIElement"; +import Behavior from "../Behaviors/Behavior"; /** * The representation of an object in the game world @@ -19,6 +20,7 @@ export default abstract class GameNode implements Positioned, Unique { protected scene: Scene; protected layer: Layer; private id: number; + protected behaviors: Array; constructor(){ this.input = InputReceiver.getInstance(); @@ -26,6 +28,7 @@ export default abstract class GameNode implements Positioned, Unique { this._position.setOnChange(this.positionChanged); this.receiver = new Receiver(); this.emitter = new Emitter(); + this.behaviors = new Array(); } setScene(scene: Scene): void { @@ -74,6 +77,33 @@ export default abstract class GameNode implements Positioned, Unique { return this.id; } + /** + * Adds a behavior to the list of behaviors in this GameNode + * @param behavior The behavior to add to this GameNode + */ + addBehavior(behavior: Behavior): void { + this.behaviors.push(behavior); + } + + /** + * Does all of the behaviors of this GameNode + */ + doBehaviors(deltaT: number): void { + this.behaviors.forEach(behavior => behavior.doBehavior(deltaT)); + } + + getBehavior(constr: new (...args: any) => T): T { + let query = null; + + for(let behavior of this.behaviors){ + if(behavior instanceof constr){ + query = behavior; + } + } + + return query; + } + /** * Called if the position vector is modified or replaced */ diff --git a/src/SceneGraph/SceneGraphArray.ts b/src/SceneGraph/SceneGraphArray.ts index ca05f82..d52a274 100644 --- a/src/SceneGraph/SceneGraphArray.ts +++ b/src/SceneGraph/SceneGraphArray.ts @@ -45,7 +45,15 @@ export default class SceneGraphArray extends SceneGraph{ } getNodesInRegion(boundary: AABB): Array { - return []; + let results = []; + + for(let node of this.nodeList){ + if(boundary.overlapArea(node.getBoundary())){ + results.push(node); + } + } + + return results; } update(deltaT: number): void { diff --git a/src/SceneGraph/SceneGraphQuadTree.ts b/src/SceneGraph/SceneGraphQuadTree.ts index 74fb552..83da21d 100644 --- a/src/SceneGraph/SceneGraphQuadTree.ts +++ b/src/SceneGraph/SceneGraphQuadTree.ts @@ -14,7 +14,7 @@ export default class SceneGraphQuadTree extends SceneGraph { super(viewport, scene); let size = this.scene.getWorldSize(); - this.qt = new RegionQuadTree(size.clone().scale(1/2), size.clone().scale(1/2), 5); + this.qt = new RegionQuadTree(size.clone().scale(1/2), size.clone().scale(1/2), 5, 30); this.nodes = new Array(); } @@ -44,11 +44,13 @@ export default class SceneGraphQuadTree extends SceneGraph { this.qt.insert(node); } - this.qt.forEach((node: CanvasNode) => { - if(!node.getLayer().isPaused()){ - node.update(deltaT); - } - }); + this.nodes.forEach((node: CanvasNode) => node.update(deltaT)); + // TODO: forEach is buggy, some nodes are update multiple times + // this.qt.forEach((node: CanvasNode) => { + // if(!node.getLayer().isPaused()){ + // node.update(deltaT); + // } + // }); } render(ctx: CanvasRenderingContext2D): void { diff --git a/src/SceneGraph/Viewport.ts b/src/SceneGraph/Viewport.ts index 6c28955..af9f43f 100644 --- a/src/SceneGraph/Viewport.ts +++ b/src/SceneGraph/Viewport.ts @@ -158,6 +158,20 @@ export default class Viewport { pos.x = MathUtils.clamp(pos.x, this.boundary.left + this.view.hw, this.boundary.right - this.view.hw); pos.y = MathUtils.clamp(pos.y, this.boundary.top + this.view.hh, this.boundary.bottom - this.view.hh); + this.view.setCenter(pos); + } else { + if(this.lastPositions.getSize() > this.smoothingFactor){ + this.lastPositions.dequeue(); + } + + let pos = Vec2.ZERO; + this.lastPositions.forEach(position => pos.add(position)); + pos.scale(1/this.lastPositions.getSize()); + + // Set this position either to the object or to its bounds + pos.x = MathUtils.clamp(pos.x, this.boundary.left + this.view.hw, this.boundary.right - this.view.hw); + pos.y = MathUtils.clamp(pos.y, this.boundary.top + this.view.hh, this.boundary.bottom - this.view.hh); + this.view.setCenter(pos); } } diff --git a/src/Utils/MathUtils.ts b/src/Utils/MathUtils.ts index aa1d27a..95c1c96 100644 --- a/src/Utils/MathUtils.ts +++ b/src/Utils/MathUtils.ts @@ -1,4 +1,14 @@ +import Vec2 from "../DataTypes/Vec2"; + export default class MathUtils { + /** + * Returns the sign of the value provided + * @param x The value to extract the sign from + */ + static sign(x: number): number { + return x < 0 ? -1 : 1; + } + /** * Clamps the value x to the range [min, max], rounding up or down if needed * @param x The value to be clamped @@ -11,7 +21,23 @@ export default class MathUtils { return x; } - /** + /** + * Clamps the value x to the range between 0 and 1 + * @param x The value to be clamped + */ + static clamp01(x: number): number { + return MathUtils.clamp(x, 0, 1); + } + + static clampMagnitude(v: Vec2, m: number): Vec2 { + if(v.magSq() > m*m){ + return v.scaleTo(m); + } else{ + return v; + } + } + + /** * Linear Interpolation * @param a The first value for the interpolation bound * @param b The second value for the interpolation bound diff --git a/src/_DemoClasses/Boid.ts b/src/_DemoClasses/Boid.ts new file mode 100644 index 0000000..7aa007d --- /dev/null +++ b/src/_DemoClasses/Boid.ts @@ -0,0 +1,43 @@ +import Vec2 from "../DataTypes/Vec2"; +import Graphic from "../Nodes/Graphic"; +import BoidBehavior from "./BoidBehavior"; + +export default class Boid extends Graphic { + direction: Vec2 = Vec2.UP.rotateCCW(Math.random()*2*Math.PI); + acceleration: Vec2 = Vec2.ZERO; + velocity: Vec2 = Vec2.ZERO; + + constructor(position: Vec2){ + super(); + this.position = position; + } + + update(deltaT: number){ + this.position.add(this.velocity.scaled(deltaT)); + + this.position.x = (this.position.x + this.scene.getWorldSize().x)%this.scene.getWorldSize().x; + this.position.y = (this.position.y + this.scene.getWorldSize().y)%this.scene.getWorldSize().y; + } + + render(ctx: CanvasRenderingContext2D): void { + let origin = this.getViewportOriginWithParallax(); + + let dirVec = this.direction.scaled(this.size.x, this.size.y); + let finVec1 = this.direction.clone().rotateCCW(Math.PI/2).scale(this.size.x/2, this.size.y/2).sub(this.direction.scaled(this.size.x/1.5, this.size.y/1.5)); + let finVec2 = this.direction.clone().rotateCCW(-Math.PI/2).scale(this.size.x/2, this.size.y/2).sub(this.direction.scaled(this.size.x/1.5, this.size.y/1.5)); + + ctx.lineWidth = 1; + ctx.fillStyle = this.color.toString(); + ctx.beginPath(); + ctx.moveTo(this.position.x + dirVec.x, this.position.y + dirVec.y); + ctx.lineTo(this.position.x + finVec1.x, this.position.y + finVec1.y); + ctx.lineTo(this.position.x - dirVec.x/3, this.position.y - dirVec.y/3); + ctx.lineTo(this.position.x + finVec2.x,this.position.y + finVec2.y); + ctx.lineTo(this.position.x + dirVec.x, this.position.y + dirVec.y); + ctx.fill(); + + // ctx.fillStyle = this.color.toStringRGBA(); + // ctx.fillRect(this.position.x - origin.x - this.size.x/2, this.position.y - origin.y - this.size.y/2, + // this.size.x, this.size.y); + } +} \ No newline at end of file diff --git a/src/_DemoClasses/BoidBehavior.ts b/src/_DemoClasses/BoidBehavior.ts new file mode 100644 index 0000000..3fda155 --- /dev/null +++ b/src/_DemoClasses/BoidBehavior.ts @@ -0,0 +1,82 @@ +import Behavior from "../Behaviors/Behavior"; +import AABB from "../DataTypes/AABB"; +import Vec2 from "../DataTypes/Vec2"; +import Debug from "../Debug/Debug"; +import Point from "../Nodes/Graphics/Point"; +import Scene from "../Scene/Scene"; +import Color from "../Utils/Color"; +import MathUtils from "../Utils/MathUtils"; +import Boid from "./Boid"; +import FlockBehavior from "./FlockBehavior"; + +export default class BoidBehavior extends Behavior { + scene: Scene; + actor: Boid; + separationFactor: number; + alignmentFactor: number; + cohesionFactor: number; + + static MIN_SPEED: number = 80; + static START_SPEED: number = 90; + static MAX_SPEED: number = 100; + static MAX_STEER_FORCE: number = 300; + + constructor(scene: Scene, actor: Boid, separationFactor: number, alignmentFactor: number, cohesionFactor: number){ + super(); + this.scene = scene; + this.actor = actor; + this.separationFactor = separationFactor; + this.alignmentFactor = alignmentFactor; + this.cohesionFactor = cohesionFactor; + } + + doBehavior(deltaT: number): void { + if(this.actor.getId() < 1){ + this.actor.setColor(Color.GREEN); + } + + if(this.actor.velocity.x === 0 && this.actor.velocity.y === 0){ + this.actor.velocity = this.actor.direction.scaled(BoidBehavior.START_SPEED * deltaT); + } + + let flock = this.actor.getBehavior(FlockBehavior); + + if(!flock.hasNeighbors){ + // No neighbors, don't change velocity; + return; + } + + let flockCenter = flock.flockCenter; + let flockHeading = flock.flockHeading; + let separationHeading = flock.separationHeading; + + let offsetToFlockmateCenter = flockCenter.sub(this.actor.position); + + let separationForce = this.steerTowards(separationHeading).scale(this.separationFactor); + let alignmentForce = this.steerTowards(flockHeading).scale(this.alignmentFactor); + let cohesionForce = this.steerTowards(offsetToFlockmateCenter).scale(this.cohesionFactor); + + this.actor.acceleration = Vec2.ZERO; + this.actor.acceleration.add(separationForce).add(alignmentForce).add(cohesionForce); + this.actor.velocity.add(this.actor.acceleration.scaled(deltaT)); + let speed = this.actor.velocity.mag(); + this.actor.velocity.normalize(); + this.actor.direction = this.actor.velocity.clone(); + speed = MathUtils.clamp(speed, BoidBehavior.MIN_SPEED, BoidBehavior.MAX_SPEED); + this.actor.velocity.scale(speed); + + if(this.actor.getId() < 1){ + Debug.log("BoidDir", "Velocity: " + this.actor.velocity.toString()); + Debug.log("BoidSep", "Separation: " + separationForce.toString()); + Debug.log("BoidAl", "Alignment: " + alignmentForce.toString()); + Debug.log("BoidCo", "Cohesion: " + cohesionForce.toString()); + Debug.log("BoidSpd", "Speed: " + speed); + } + } + + steerTowards(vec: Vec2){ + let v = vec.normalize().scale(BoidBehavior.MAX_SPEED).sub(this.actor.velocity); + return MathUtils.clampMagnitude(v, BoidBehavior.MAX_STEER_FORCE); + } + +} \ No newline at end of file diff --git a/src/_DemoClasses/FlockBehavior.ts b/src/_DemoClasses/FlockBehavior.ts new file mode 100644 index 0000000..a365e3a --- /dev/null +++ b/src/_DemoClasses/FlockBehavior.ts @@ -0,0 +1,83 @@ +import Behavior from "../Behaviors/Behavior"; +import AABB from "../DataTypes/AABB"; +import Vec2 from "../DataTypes/Vec2"; +import Point from "../Nodes/Graphics/Point"; +import Scene from "../Scene/Scene"; +import Color from "../Utils/Color"; +import Boid from "./Boid"; +import BoidBehavior from "./BoidBehavior"; + +export default class FlockBehavior extends Behavior { + scene: Scene; + actor: Boid; + flock: Array; + visibleRegion: AABB; + avoidRadius: number; + hasNeighbors: boolean; + flockCenter: Vec2; + flockHeading: Vec2; + separationHeading: Vec2; + + constructor(scene: Scene, actor: Boid, flock: Array, visionRange: number, avoidRadius: number) { + super(); + this.scene = scene; + this.actor = actor; + this.flock = flock; + + this.visibleRegion = new AABB(this.actor.getPosition().clone(), new Vec2(visionRange, visionRange)); + this.avoidRadius = avoidRadius; + } + + doBehavior(deltaT: number): void { + + // Update the visible region + this.visibleRegion.setCenter(this.actor.getPosition().clone()); + + let neighbors = this.scene.getSceneGraph().getNodesInRegion(this.visibleRegion); + + neighbors = neighbors.filter(neighbor => { + return (neighbor instanceof Boid) + && (neighbor !== this.actor) + && this.actor.direction.dot(neighbor.position.clone().sub(this.actor.position).normalize()) > -0.866; + }); + + if(neighbors.length <= 0){ + this.hasNeighbors = false; + return; + } else { + this.hasNeighbors = true; + } + + // Draw a group + if(this.actor.getId() < 1){ + this.actor.setColor(Color.GREEN); + for(let neighbor of neighbors){ + if(neighbor === this.actor) continue; + (neighbor).setColor(Color.BLUE) + } + } + + let flockCenter = Vec2.ZERO; + let flockHeading = Vec2.ZERO; + let separationHeading = Vec2.ZERO; + + for(let neighbor of neighbors){ + let neighborPos = neighbor.position; + flockCenter.add(neighborPos); + + flockHeading.add((neighbor).direction); + + let dist = this.actor.position.distanceSqTo(neighborPos); + if(dist < this.avoidRadius*this.avoidRadius){ + separationHeading.add(this.actor.position.clone().sub(neighborPos).scale(1/dist)); + } + } + + flockCenter.scale(1/neighbors.length); + + this.flockCenter = flockCenter; + this.flockHeading = flockHeading; + this.separationHeading = separationHeading; + } + +} \ No newline at end of file diff --git a/src/main.ts b/src/main.ts index 17e434c..0b06493 100644 --- a/src/main.ts +++ b/src/main.ts @@ -2,13 +2,14 @@ import GameLoop from "./Loop/GameLoop"; import {} from "./index"; import MainScene from "./MainScene" import QuadTreeScene from "./QuadTreeScene"; +import BoidDemo from "./BoidDemo"; function main(){ // Create the game object let game = new GameLoop({viewportSize: {x: 800, y: 600}}); game.start(); let sm = game.getSceneManager(); - sm.addScene(MainScene); + sm.addScene(BoidDemo); } CanvasRenderingContext2D.prototype.roundedRect = function(x: number, y: number, w: number, h: number, r: number): void {