added AI support
This commit is contained in:
parent
254462993a
commit
f3449c1526
14
src/Behaviors/Behavior.ts
Normal file
14
src/Behaviors/Behavior.ts
Normal file
|
@ -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;
|
||||
}
|
57
src/BoidDemo.ts
Normal file
57
src/BoidDemo.ts
Normal file
|
@ -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<Boid>;
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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
|
||||
|
@ -138,3 +201,10 @@ 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;
|
||||
}
|
|
@ -178,10 +178,10 @@ export default class QuadTree<T extends Region & Unique> 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<T extends Region & Unique> 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);
|
||||
|
||||
|
|
|
@ -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
|
||||
*/
|
||||
|
|
|
@ -28,6 +28,8 @@ export default class GameLoop {
|
|||
private started: boolean;
|
||||
private running: boolean;
|
||||
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 = <HTMLCanvasElement>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;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -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<Behavior>;
|
||||
|
||||
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<T extends Behavior>(constr: new (...args: any) => T): T {
|
||||
let query = null;
|
||||
|
||||
for(let behavior of this.behaviors){
|
||||
if(behavior instanceof constr){
|
||||
query = <T>behavior;
|
||||
}
|
||||
}
|
||||
|
||||
return query;
|
||||
}
|
||||
|
||||
/**
|
||||
* Called if the position vector is modified or replaced
|
||||
*/
|
||||
|
|
|
@ -45,7 +45,15 @@ export default class SceneGraphArray extends SceneGraph{
|
|||
}
|
||||
|
||||
getNodesInRegion(boundary: AABB): Array<CanvasNode> {
|
||||
return [];
|
||||
let results = [];
|
||||
|
||||
for(let node of this.nodeList){
|
||||
if(boundary.overlapArea(node.getBoundary())){
|
||||
results.push(node);
|
||||
}
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
update(deltaT: number): void {
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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,6 +21,22 @@ 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
|
||||
|
|
43
src/_DemoClasses/Boid.ts
Normal file
43
src/_DemoClasses/Boid.ts
Normal file
|
@ -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);
|
||||
}
|
||||
}
|
82
src/_DemoClasses/BoidBehavior.ts
Normal file
82
src/_DemoClasses/BoidBehavior.ts
Normal file
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
83
src/_DemoClasses/FlockBehavior.ts
Normal file
83
src/_DemoClasses/FlockBehavior.ts
Normal file
|
@ -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<Boid>;
|
||||
visibleRegion: AABB;
|
||||
avoidRadius: number;
|
||||
hasNeighbors: boolean;
|
||||
flockCenter: Vec2;
|
||||
flockHeading: Vec2;
|
||||
separationHeading: Vec2;
|
||||
|
||||
constructor(scene: Scene, actor: Boid, flock: Array<Boid>, 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;
|
||||
(<Point>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((<Boid>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;
|
||||
}
|
||||
|
||||
}
|
|
@ -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 {
|
||||
|
|
Loading…
Reference in New Issue
Block a user