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 Shape from "./Shape";
|
||||||
import Vec2 from "./Vec2";
|
import Vec2 from "./Vec2";
|
||||||
|
import MathUtils from "../Utils/MathUtils";
|
||||||
|
|
||||||
export default class AABB extends Shape {
|
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
|
&& 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
|
* A simple boolean check of whether this AABB overlaps another
|
||||||
* @param other
|
* @param other
|
||||||
|
@ -137,4 +200,11 @@ export default class AABB extends Shape {
|
||||||
|
|
||||||
return dx*dy;
|
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 hw = this.boundary.hw;
|
||||||
let hh = this.boundary.hh;
|
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.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.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.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.se = new QuadTree(new Vec2(x+hw/2, y+hh/2), new Vec2(hw/2, hh/2), this.maxDepth - 1, this.capacity);
|
||||||
|
|
||||||
this.distributeItems();
|
this.distributeItems();
|
||||||
}
|
}
|
||||||
|
@ -213,7 +213,6 @@ export default class QuadTree<T extends Region & Unique> implements Collection {
|
||||||
* @param ctx
|
* @param ctx
|
||||||
*/
|
*/
|
||||||
public render_demo(ctx: CanvasRenderingContext2D): void {
|
public render_demo(ctx: CanvasRenderingContext2D): void {
|
||||||
return;
|
|
||||||
ctx.strokeStyle = "#0000FF";
|
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);
|
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;
|
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.
|
* Sets the vector's x and y based on the angle provided. Goes counter clockwise.
|
||||||
* @param angle The angle in radians
|
* @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);
|
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
|
* Returns a string representation of this vector rounded to 1 decimal point
|
||||||
*/
|
*/
|
||||||
|
|
|
@ -27,7 +27,9 @@ export default class GameLoop {
|
||||||
|
|
||||||
private started: boolean;
|
private started: boolean;
|
||||||
private running: boolean;
|
private running: boolean;
|
||||||
private frameDelta: number;
|
private frameDelta: number;
|
||||||
|
private panic: boolean;
|
||||||
|
private numUpdateSteps: number;
|
||||||
|
|
||||||
// Game canvas and its width and height
|
// Game canvas and its width and height
|
||||||
readonly GAME_CANVAS: HTMLCanvasElement;
|
readonly GAME_CANVAS: HTMLCanvasElement;
|
||||||
|
@ -61,7 +63,7 @@ export default class GameLoop {
|
||||||
this.running = false;
|
this.running = false;
|
||||||
|
|
||||||
// Get the game canvas and give it a background color
|
// 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");
|
this.GAME_CANVAS.style.setProperty("background-color", "whitesmoke");
|
||||||
|
|
||||||
// Give the canvas a size and get the rendering context
|
// Give the canvas a size and get the rendering context
|
||||||
|
@ -169,17 +171,40 @@ export default class GameLoop {
|
||||||
this.lastFrameTime = timestamp;
|
this.lastFrameTime = timestamp;
|
||||||
|
|
||||||
// Update while we can (This will present problems if we leave the window)
|
// Update while we can (This will present problems if we leave the window)
|
||||||
let i = 0;
|
this.numUpdateSteps = 0;
|
||||||
while(this.frameDelta >= this.simulationTimestep){
|
while(this.frameDelta >= this.simulationTimestep){
|
||||||
this.update(this.simulationTimestep/1000);
|
this.update(this.simulationTimestep/1000);
|
||||||
this.frameDelta -= this.simulationTimestep;
|
this.frameDelta -= this.simulationTimestep;
|
||||||
|
|
||||||
|
this.numUpdateSteps++;
|
||||||
|
if(this.numUpdateSteps > 100){
|
||||||
|
this.panic = true;
|
||||||
|
}
|
||||||
|
|
||||||
// Update the frame of the game
|
// Update the frame of the game
|
||||||
this.updateFrameCount(this.simulationTimestep);
|
this.updateFrameCount(this.simulationTimestep);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Updates are done, draw
|
// Updates are done, draw
|
||||||
this.render();
|
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 Layer from "../Scene/Layer";
|
||||||
import { Positioned, Unique } from "../DataTypes/Interfaces/Descriptors"
|
import { Positioned, Unique } from "../DataTypes/Interfaces/Descriptors"
|
||||||
import UIElement from "./UIElement";
|
import UIElement from "./UIElement";
|
||||||
|
import Behavior from "../Behaviors/Behavior";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The representation of an object in the game world
|
* 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 scene: Scene;
|
||||||
protected layer: Layer;
|
protected layer: Layer;
|
||||||
private id: number;
|
private id: number;
|
||||||
|
protected behaviors: Array<Behavior>;
|
||||||
|
|
||||||
constructor(){
|
constructor(){
|
||||||
this.input = InputReceiver.getInstance();
|
this.input = InputReceiver.getInstance();
|
||||||
|
@ -26,6 +28,7 @@ export default abstract class GameNode implements Positioned, Unique {
|
||||||
this._position.setOnChange(this.positionChanged);
|
this._position.setOnChange(this.positionChanged);
|
||||||
this.receiver = new Receiver();
|
this.receiver = new Receiver();
|
||||||
this.emitter = new Emitter();
|
this.emitter = new Emitter();
|
||||||
|
this.behaviors = new Array();
|
||||||
}
|
}
|
||||||
|
|
||||||
setScene(scene: Scene): void {
|
setScene(scene: Scene): void {
|
||||||
|
@ -74,6 +77,33 @@ export default abstract class GameNode implements Positioned, Unique {
|
||||||
return this.id;
|
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
|
* Called if the position vector is modified or replaced
|
||||||
*/
|
*/
|
||||||
|
|
|
@ -45,7 +45,15 @@ export default class SceneGraphArray extends SceneGraph{
|
||||||
}
|
}
|
||||||
|
|
||||||
getNodesInRegion(boundary: AABB): Array<CanvasNode> {
|
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 {
|
update(deltaT: number): void {
|
||||||
|
|
|
@ -14,7 +14,7 @@ export default class SceneGraphQuadTree extends SceneGraph {
|
||||||
super(viewport, scene);
|
super(viewport, scene);
|
||||||
|
|
||||||
let size = this.scene.getWorldSize();
|
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();
|
this.nodes = new Array();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -44,11 +44,13 @@ export default class SceneGraphQuadTree extends SceneGraph {
|
||||||
this.qt.insert(node);
|
this.qt.insert(node);
|
||||||
}
|
}
|
||||||
|
|
||||||
this.qt.forEach((node: CanvasNode) => {
|
this.nodes.forEach((node: CanvasNode) => node.update(deltaT));
|
||||||
if(!node.getLayer().isPaused()){
|
// TODO: forEach is buggy, some nodes are update multiple times
|
||||||
node.update(deltaT);
|
// this.qt.forEach((node: CanvasNode) => {
|
||||||
}
|
// if(!node.getLayer().isPaused()){
|
||||||
});
|
// node.update(deltaT);
|
||||||
|
// }
|
||||||
|
// });
|
||||||
}
|
}
|
||||||
|
|
||||||
render(ctx: CanvasRenderingContext2D): void {
|
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.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);
|
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);
|
this.view.setCenter(pos);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,4 +1,14 @@
|
||||||
|
import Vec2 from "../DataTypes/Vec2";
|
||||||
|
|
||||||
export default class MathUtils {
|
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
|
* Clamps the value x to the range [min, max], rounding up or down if needed
|
||||||
* @param x The value to be clamped
|
* @param x The value to be clamped
|
||||||
|
@ -11,7 +21,23 @@ export default class MathUtils {
|
||||||
return x;
|
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
|
* Linear Interpolation
|
||||||
* @param a The first value for the interpolation bound
|
* @param a The first value for the interpolation bound
|
||||||
* @param b The second value for the interpolation bound
|
* @param b The second 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 {} from "./index";
|
||||||
import MainScene from "./MainScene"
|
import MainScene from "./MainScene"
|
||||||
import QuadTreeScene from "./QuadTreeScene";
|
import QuadTreeScene from "./QuadTreeScene";
|
||||||
|
import BoidDemo from "./BoidDemo";
|
||||||
|
|
||||||
function main(){
|
function main(){
|
||||||
// Create the game object
|
// Create the game object
|
||||||
let game = new GameLoop({viewportSize: {x: 800, y: 600}});
|
let game = new GameLoop({viewportSize: {x: 800, y: 600}});
|
||||||
game.start();
|
game.start();
|
||||||
let sm = game.getSceneManager();
|
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 {
|
CanvasRenderingContext2D.prototype.roundedRect = function(x: number, y: number, w: number, h: number, r: number): void {
|
||||||
|
|
Loading…
Reference in New Issue
Block a user