added states and state machines for ai behaviors
This commit is contained in:
parent
f3449c1526
commit
25e0b8a39e
|
@ -1,14 +0,0 @@
|
|||
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;
|
||||
}
|
|
@ -1,12 +1,10 @@
|
|||
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";
|
||||
import Boid from "./_DemoClasses/Boids/Boid";
|
||||
import FlockBehavior from "./_DemoClasses/Boids/FlockBehavior";
|
||||
import Player from "./_DemoClasses/Player/Player";
|
||||
|
||||
/**
|
||||
* This demo emphasizes an ai system for the game engine with component architecture
|
||||
|
@ -24,17 +22,16 @@ export default class BoidDemo extends Scene {
|
|||
this.viewport.setBounds(0, 0, 800, 600)
|
||||
this.viewport.setCenter(400, 300);
|
||||
|
||||
let layer = this.addLayer()
|
||||
let layer = this.addLayer();
|
||||
this.boids = new Array();
|
||||
|
||||
// Add the player
|
||||
this.add.graphic(Player, layer, new Vec2(0, 0));
|
||||
|
||||
// Create a bunch of boids
|
||||
for(let i = 0; i < 200; i++){
|
||||
for(let i = 0; i < 100; 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.fb = new FlockBehavior(this, boid, this.boids, 75, 50);
|
||||
boid.setSize(5, 5);
|
||||
this.boids.push(boid);
|
||||
}
|
||||
|
@ -45,13 +42,13 @@ export default class BoidDemo extends Scene {
|
|||
boid.setColor(Color.RED);
|
||||
}
|
||||
|
||||
for(let boid of this.boids){
|
||||
boid.getBehavior(FlockBehavior).doBehavior(deltaT);
|
||||
this.updateFlock();
|
||||
}
|
||||
|
||||
|
||||
updateFlock(): void {
|
||||
for(let boid of this.boids){
|
||||
boid.getBehavior(BoidBehavior).doBehavior(deltaT);
|
||||
boid.fb.update();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
40
src/DataTypes/State/State.ts
Normal file
40
src/DataTypes/State/State.ts
Normal file
|
@ -0,0 +1,40 @@
|
|||
import Emitter from "../../Events/Emitter";
|
||||
import GameEvent from "../../Events/GameEvent";
|
||||
import { Updateable } from "../Interfaces/Descriptors";
|
||||
import StateMachine from "./StateMachine";
|
||||
|
||||
export default abstract class State implements Updateable {
|
||||
protected parentStateMachine: StateMachine;
|
||||
protected emitter: Emitter;
|
||||
|
||||
constructor(parent: StateMachine) {
|
||||
this.parentStateMachine = parent;
|
||||
this.emitter = new Emitter();
|
||||
}
|
||||
|
||||
/**
|
||||
* A method that is called when this state is entered. Use this to initialize any variables before updates occur.
|
||||
*/
|
||||
abstract onEnter(): void;
|
||||
|
||||
/**
|
||||
* Handles an input event, such as taking damage.
|
||||
* @param event
|
||||
*/
|
||||
abstract handleInput(event: GameEvent): void;
|
||||
|
||||
abstract update(deltaT: number): void;
|
||||
|
||||
/**
|
||||
* Tells the state machine that this state has ended, and makes it transition to the new state specified
|
||||
* @param stateName The name of the state to transition to
|
||||
*/
|
||||
protected finished(stateName: string): void {
|
||||
this.parentStateMachine.changeState(stateName);
|
||||
}
|
||||
|
||||
/**
|
||||
* This is called when the state is ending.
|
||||
*/
|
||||
abstract onExit(): void;
|
||||
}
|
128
src/DataTypes/State/StateMachine.ts
Normal file
128
src/DataTypes/State/StateMachine.ts
Normal file
|
@ -0,0 +1,128 @@
|
|||
import Stack from "../Stack";
|
||||
import State from "./State";
|
||||
import Map from "../Map";
|
||||
import GameEvent from "../../Events/GameEvent";
|
||||
import Receiver from "../../Events/Receiver";
|
||||
import Emitter from "../../Events/Emitter";
|
||||
import { Updateable } from "../Interfaces/Descriptors";
|
||||
|
||||
/**
|
||||
* An implementation of a Push Down Automata State machine. States can also be hierarchical
|
||||
* for more flexibility, as described in Game Programming Principles.
|
||||
*/
|
||||
export default class StateMachine implements Updateable {
|
||||
protected stack: Stack<State>;
|
||||
protected stateMap: Map<State>;
|
||||
protected currentState: State;
|
||||
protected receiver: Receiver;
|
||||
protected emitter: Emitter;
|
||||
protected active: boolean;
|
||||
protected emitEventOnStateChange: boolean;
|
||||
protected stateChangeEventName: string;
|
||||
|
||||
constructor(){
|
||||
this.stack = new Stack();
|
||||
this.stateMap = new Map();
|
||||
this.receiver = new Receiver();
|
||||
this.emitter = new Emitter();
|
||||
this.emitEventOnStateChange = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the activity state of this state machine
|
||||
* @param flag True if you want to set this machine running, false otherwise
|
||||
*/
|
||||
setActive(flag: boolean): void {
|
||||
this.active = flag;
|
||||
}
|
||||
|
||||
/**
|
||||
* Makes this state machine emit an event any time its state changes
|
||||
* @param stateChangeEventName The name of the event to emit
|
||||
*/
|
||||
setEmitEventOnStateChange(stateChangeEventName: string): void {
|
||||
this.emitEventOnStateChange = true;
|
||||
this.stateChangeEventName = stateChangeEventName;
|
||||
}
|
||||
|
||||
/**
|
||||
* Stops this state machine from emitting events on state change.
|
||||
*/
|
||||
cancelEmitEventOnStateChange(): void {
|
||||
this.emitEventOnStateChange = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Initializes this state machine with an initial state and sets it running
|
||||
* @param initialState The name of initial state of the state machine
|
||||
*/
|
||||
initialize(initialState: string){
|
||||
this.stack.push(this.stateMap.get(initialState));
|
||||
this.currentState = this.stack.peek();
|
||||
this.setActive(true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a state to this state machine
|
||||
* @param stateName The name of the state to add
|
||||
* @param state The state to add
|
||||
*/
|
||||
addState(stateName: string, state: State): void {
|
||||
this.stateMap.add(stateName, state);
|
||||
}
|
||||
|
||||
/**
|
||||
* Changes the state of this state machine to the provided string
|
||||
* @param state The string name of the state to change to
|
||||
*/
|
||||
changeState(state: string): void {
|
||||
// Exit the current state
|
||||
this.currentState.onExit();
|
||||
|
||||
// Make sure the correct state is at the top of the stack
|
||||
if(state === "previous"){
|
||||
// Pop the current state off the stack
|
||||
this.stack.pop();
|
||||
} else {
|
||||
// Retrieve the new state from the statemap and put it at the top of the stack
|
||||
this.stack.pop();
|
||||
this.stack.push(this.stateMap.get(state));
|
||||
}
|
||||
|
||||
// Retreive the new state from the stack
|
||||
this.currentState = this.stack.peek();
|
||||
|
||||
// Emit an event if turned on
|
||||
if(this.emitEventOnStateChange){
|
||||
this.emitter.fireEvent(this.stateChangeEventName, {state: this.currentState});
|
||||
}
|
||||
|
||||
// Enter the new state
|
||||
this.currentState.onEnter();
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles input. This happens at the very beginning of this state machine's update cycle.
|
||||
* @param event The game event to process
|
||||
*/
|
||||
handleInput(event: GameEvent): void {
|
||||
this.currentState.handleInput(event);
|
||||
}
|
||||
|
||||
update(deltaT: number): void {
|
||||
// If the state machine isn't currently active, ignore all events and don't update
|
||||
if(!this.active){
|
||||
this.receiver.ignoreEvents();
|
||||
return;
|
||||
}
|
||||
|
||||
// Handle input from all events
|
||||
while(this.receiver.hasNextEvent()){
|
||||
let event = this.receiver.getNextEvent();
|
||||
this.handleInput(event);
|
||||
}
|
||||
|
||||
// Delegate the update to the current state
|
||||
this.currentState.update(deltaT);
|
||||
}
|
||||
}
|
|
@ -4,10 +4,7 @@
|
|||
export default class Vec2 {
|
||||
|
||||
// Store x and y in an array
|
||||
//private vec: Float32Array;
|
||||
|
||||
protected _x: number;
|
||||
protected _y: number;
|
||||
private vec: Float32Array;
|
||||
|
||||
/**
|
||||
* When this vector changes its value, do something
|
||||
|
@ -15,20 +12,18 @@ export default class Vec2 {
|
|||
private onChange: Function = () => {};
|
||||
|
||||
constructor(x: number = 0, y: number = 0) {
|
||||
// this.vec = new Float32Array(2);
|
||||
// this.vec[0] = x;
|
||||
// this.vec[1] = y;
|
||||
this._x = x;
|
||||
this._y = y;
|
||||
this.vec = new Float32Array(2);
|
||||
this.vec[0] = x;
|
||||
this.vec[1] = y;
|
||||
}
|
||||
|
||||
// Expose x and y with getters and setters
|
||||
get x() {
|
||||
return this._x; //this.vec[0];
|
||||
return this.vec[0];
|
||||
}
|
||||
|
||||
set x(x: number) {
|
||||
this._x = x;//this.vec[0] = x;
|
||||
this.vec[0] = x;
|
||||
|
||||
if(this.onChange){
|
||||
this.onChange();
|
||||
|
@ -36,11 +31,11 @@ export default class Vec2 {
|
|||
}
|
||||
|
||||
get y() {
|
||||
return this._y;//this.vec[1];
|
||||
return this.vec[1];
|
||||
}
|
||||
|
||||
set y(y: number) {
|
||||
this._y = y;//this.vec[1] = y;
|
||||
this.vec[1] = y;
|
||||
|
||||
if(this.onChange){
|
||||
this.onChange();
|
||||
|
@ -51,6 +46,10 @@ export default class Vec2 {
|
|||
return new Vec2(0, 0);
|
||||
}
|
||||
|
||||
static get INF() {
|
||||
return new Vec2(Infinity, Infinity);
|
||||
}
|
||||
|
||||
static get UP() {
|
||||
return new Vec2(0, -1);
|
||||
}
|
||||
|
@ -88,6 +87,13 @@ export default class Vec2 {
|
|||
return new Vec2(this.x/mag, this.y/mag);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the x and y elements of this vector to zero
|
||||
*/
|
||||
zero(){
|
||||
return this.set(0, 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the vector's x and y based on the angle provided. Goes counter clockwise.
|
||||
* @param angle The angle in radians
|
||||
|
@ -164,6 +170,14 @@ export default class Vec2 {
|
|||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Copies the values of the other Vec2 into this one.
|
||||
* @param other The Vec2 to copy
|
||||
*/
|
||||
copy(other: Vec2): Vec2 {
|
||||
return this.set(other.x, other.y);
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds this vector the another vector
|
||||
* @param other
|
||||
|
@ -251,6 +265,21 @@ export default class Vec2 {
|
|||
return new Vec2(this.x, this.y);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if this vector and other have the same x and y
|
||||
* @param other The vector to check against
|
||||
*/
|
||||
equals(other: Vec2): boolean {
|
||||
return this.x === other.x && this.y === other.y;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if this vector is the zero vector
|
||||
*/
|
||||
isZero(): boolean {
|
||||
return this.x === 0 && this.y === 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the function that is called whenever this vector is changed.
|
||||
* @param f The function to be called
|
||||
|
|
|
@ -1,18 +1,15 @@
|
|||
import EventQueue from "../Events/EventQueue";
|
||||
import InputReceiver from "../Input/InputReceiver";
|
||||
import Vec2 from "../DataTypes/Vec2";
|
||||
import Receiver from "../Events/Receiver";
|
||||
import Emitter from "../Events/Emitter";
|
||||
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";
|
||||
import { Positioned, Unique, Updateable } from "../DataTypes/Interfaces/Descriptors"
|
||||
|
||||
/**
|
||||
* The representation of an object in the game world
|
||||
*/
|
||||
export default abstract class GameNode implements Positioned, Unique {
|
||||
export default abstract class GameNode implements Positioned, Unique, Updateable {
|
||||
protected input: InputReceiver;
|
||||
private _position: Vec2;
|
||||
protected receiver: Receiver;
|
||||
|
@ -20,7 +17,6 @@ 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();
|
||||
|
@ -28,7 +24,6 @@ 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 {
|
||||
|
@ -77,33 +72,6 @@ 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
|
||||
*/
|
||||
|
|
|
@ -15,10 +15,18 @@ export default class Rect extends Graphic {
|
|||
this.borderWidth = 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the border color of this rectangle
|
||||
* @param color The border color
|
||||
*/
|
||||
setBorderColor(color: Color){
|
||||
this.borderColor = color;
|
||||
}
|
||||
|
||||
/**Sets the border width of this rectangle
|
||||
*
|
||||
* @param width The width of the rectangle in pixels
|
||||
*/
|
||||
setBorderWidth(width: number){
|
||||
this.borderWidth = width;
|
||||
}
|
||||
|
|
|
@ -1,43 +0,0 @@
|
|||
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);
|
||||
}
|
||||
}
|
|
@ -1,82 +0,0 @@
|
|||
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);
|
||||
}
|
||||
|
||||
}
|
41
src/_DemoClasses/Boids/Boid.ts
Normal file
41
src/_DemoClasses/Boids/Boid.ts
Normal file
|
@ -0,0 +1,41 @@
|
|||
import Vec2 from "../../DataTypes/Vec2";
|
||||
import Graphic from "../../Nodes/Graphic";
|
||||
import BoidController from "./BoidController";
|
||||
import FlockBehavior from "./FlockBehavior";
|
||||
|
||||
export default class Boid extends Graphic {
|
||||
direction: Vec2 = Vec2.UP.rotateCCW(Math.random()*2*Math.PI);
|
||||
acceleration: Vec2 = Vec2.ZERO;
|
||||
velocity: Vec2 = Vec2.ZERO;
|
||||
|
||||
ai: BoidController;
|
||||
fb: FlockBehavior;
|
||||
|
||||
constructor(position: Vec2){
|
||||
super();
|
||||
this.position = position;
|
||||
this.ai = new BoidController(this);
|
||||
}
|
||||
|
||||
update(deltaT: number){
|
||||
this.ai.update(deltaT);
|
||||
}
|
||||
|
||||
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 - origin.x + dirVec.x, this.position.y - origin.y + dirVec.y);
|
||||
ctx.lineTo(this.position.x - origin.x + finVec1.x, this.position.y - origin.y + finVec1.y);
|
||||
ctx.lineTo(this.position.x - origin.x - dirVec.x/3, this.position.y - origin.y - dirVec.y/3);
|
||||
ctx.lineTo(this.position.x - origin.x + finVec2.x, this.position.y - origin.y + finVec2.y);
|
||||
ctx.lineTo(this.position.x - origin.x + dirVec.x, this.position.y - origin.y + dirVec.y);
|
||||
ctx.fill();
|
||||
}
|
||||
}
|
32
src/_DemoClasses/Boids/BoidController.ts
Normal file
32
src/_DemoClasses/Boids/BoidController.ts
Normal file
|
@ -0,0 +1,32 @@
|
|||
import StateMachine from "../../DataTypes/State/StateMachine";
|
||||
import { CustomGameEventType } from "../CustomGameEventType";
|
||||
import Boid from "./Boid";
|
||||
import BoidBehavior from "./BoidStates/BoidBehavior";
|
||||
import RunAwayFromPlayer from "./BoidStates/RunAwayFromPlayer";
|
||||
|
||||
export default class BoidController extends StateMachine {
|
||||
constructor(boid: Boid){
|
||||
super();
|
||||
|
||||
// Normal Boid Behavior
|
||||
let normalBehavior = new BoidBehavior(this, boid, 3, 1, 3);
|
||||
this.addState("normal", normalBehavior);
|
||||
|
||||
// Run away from player behavior
|
||||
let runAway = new RunAwayFromPlayer(this, boid);
|
||||
this.addState("runAway", runAway);
|
||||
|
||||
// Sign up to be warned of player movement
|
||||
this.receiver.subscribe(CustomGameEventType.PLAYER_MOVE);
|
||||
|
||||
this.initialize("normal");
|
||||
}
|
||||
|
||||
changeState(stateName: string): void {
|
||||
if(stateName === "runAway"){
|
||||
this.stack.push(this.stateMap.get(stateName));
|
||||
}
|
||||
|
||||
super.changeState(stateName);
|
||||
}
|
||||
}
|
95
src/_DemoClasses/Boids/BoidStates/BoidBehavior.ts
Normal file
95
src/_DemoClasses/Boids/BoidStates/BoidBehavior.ts
Normal file
|
@ -0,0 +1,95 @@
|
|||
import State from "../../../DataTypes/State/State";
|
||||
import StateMachine from "../../../DataTypes/State/StateMachine";
|
||||
import Vec2 from "../../../DataTypes/Vec2";
|
||||
import Debug from "../../../Debug/Debug";
|
||||
import GameEvent from "../../../Events/GameEvent";
|
||||
import MathUtils from "../../../Utils/MathUtils";
|
||||
import { CustomGameEventType } from "../../CustomGameEventType";
|
||||
import Boid from "../Boid";
|
||||
|
||||
export default class BoidBehavior extends State {
|
||||
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(parent: StateMachine, actor: Boid, separationFactor: number, alignmentFactor: number, cohesionFactor: number){
|
||||
super(parent);
|
||||
this.actor = actor;
|
||||
this.separationFactor = separationFactor;
|
||||
this.alignmentFactor = alignmentFactor;
|
||||
this.cohesionFactor = cohesionFactor;
|
||||
}
|
||||
|
||||
onEnter(): void {
|
||||
// Do nothing special
|
||||
}
|
||||
|
||||
handleInput(event: GameEvent): void {
|
||||
if(event.type === CustomGameEventType.PLAYER_MOVE){
|
||||
if(this.actor.position.distanceSqTo(event.data.get("position")) < 50*50){
|
||||
// If player moved and we're close, change state
|
||||
this.finished("runAway");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
onExit(): void {
|
||||
// Do nothing special
|
||||
}
|
||||
|
||||
update(deltaT: number): void {
|
||||
if(this.actor.velocity.x === 0 && this.actor.velocity.y === 0){
|
||||
this.actor.velocity = this.actor.direction.scaled(BoidBehavior.START_SPEED);
|
||||
}
|
||||
|
||||
// Only update as boid if it has neighbors
|
||||
if(this.actor.fb.hasNeighbors){
|
||||
let flockCenter = this.actor.fb.flockCenter;
|
||||
let flockHeading = this.actor.fb.flockHeading;
|
||||
let separationHeading = this.actor.fb.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("BoidSep", "Separation: " + separationForce.toString());
|
||||
Debug.log("BoidAl", "Alignment: " + alignmentForce.toString());
|
||||
Debug.log("BoidCo", "Cohesion: " + cohesionForce.toString());
|
||||
Debug.log("BoidSpd", "Speed: " + speed);
|
||||
}
|
||||
}
|
||||
|
||||
if(this.actor.getId() < 1){
|
||||
Debug.log("BoidDir", "Velocity: " + this.actor.velocity.toString());
|
||||
}
|
||||
|
||||
// Update the position
|
||||
this.actor.position.add(this.actor.velocity.scaled(deltaT));
|
||||
this.actor.position.x = (this.actor.position.x + this.actor.getScene().getWorldSize().x)%this.actor.getScene().getWorldSize().x;
|
||||
this.actor.position.y = (this.actor.position.y + this.actor.getScene().getWorldSize().y)%this.actor.getScene().getWorldSize().y;
|
||||
}
|
||||
|
||||
steerTowards(vec: Vec2){
|
||||
let v = vec.normalize().scale(BoidBehavior.MAX_SPEED).sub(this.actor.velocity);
|
||||
return MathUtils.clampMagnitude(v, BoidBehavior.MAX_STEER_FORCE);
|
||||
}
|
||||
|
||||
}
|
82
src/_DemoClasses/Boids/BoidStates/RunAwayFromPlayer.ts
Normal file
82
src/_DemoClasses/Boids/BoidStates/RunAwayFromPlayer.ts
Normal file
|
@ -0,0 +1,82 @@
|
|||
import State from "../../../DataTypes/State/State";
|
||||
import StateMachine from "../../../DataTypes/State/StateMachine";
|
||||
import Vec2 from "../../../DataTypes/Vec2";
|
||||
import GameEvent from "../../../Events/GameEvent";
|
||||
import MathUtils from "../../../Utils/MathUtils";
|
||||
import { CustomGameEventType } from "../../CustomGameEventType";
|
||||
import Boid from "../Boid";
|
||||
|
||||
export default class RunAwayFromPlayer extends State {
|
||||
actor: Boid;
|
||||
runAwayDirection: Vec2;
|
||||
lastPlayerPosition: Vec2;
|
||||
|
||||
timeElapsed: number;
|
||||
|
||||
static RUN_AWAY_SPEED: number = 120;
|
||||
static MAX_STEER_FORCE: number = 300;
|
||||
static FEAR_RADIUS: number = 75;
|
||||
|
||||
constructor(parent: StateMachine, actor: Boid){
|
||||
super(parent);
|
||||
this.actor = actor;
|
||||
}
|
||||
|
||||
onEnter(): void {
|
||||
console.log("Entered Running away")
|
||||
this.runAwayDirection = Vec2.ZERO;
|
||||
this.lastPlayerPosition = Vec2.INF;
|
||||
this.timeElapsed = 0;
|
||||
}
|
||||
|
||||
handleInput(event: GameEvent): void {
|
||||
if(event.type === CustomGameEventType.PLAYER_MOVE){
|
||||
this.lastPlayerPosition.copy(event.data.get("position"));
|
||||
|
||||
if(this.actor.position.distanceSqTo(this.lastPlayerPosition)
|
||||
< RunAwayFromPlayer.FEAR_RADIUS*RunAwayFromPlayer.FEAR_RADIUS){
|
||||
// Reset our run away timer
|
||||
this.timeElapsed = 0;
|
||||
|
||||
// Update the run away direction
|
||||
this.runAwayDirection.copy(this.actor.position).sub(event.data.get("position")).normalize();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
update(deltaT: number): void {
|
||||
this.timeElapsed += deltaT;
|
||||
|
||||
// Run away for at least 500 ms
|
||||
if(this.timeElapsed > 0.5){
|
||||
// If it's been long enough, go back to what we were doing before
|
||||
this.finished("previous");
|
||||
}
|
||||
|
||||
// Move away from the player
|
||||
let force = this.steerTowards(this.runAwayDirection.clone()).scaled(10);
|
||||
|
||||
this.actor.acceleration = force;
|
||||
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, RunAwayFromPlayer.RUN_AWAY_SPEED, RunAwayFromPlayer.RUN_AWAY_SPEED);
|
||||
this.actor.velocity.scale(speed);
|
||||
|
||||
// Update the position
|
||||
this.actor.position.add(this.actor.velocity.scaled(deltaT));
|
||||
this.actor.position.x = (this.actor.position.x + this.actor.getScene().getWorldSize().x)%this.actor.getScene().getWorldSize().x;
|
||||
this.actor.position.y = (this.actor.position.y + this.actor.getScene().getWorldSize().y)%this.actor.getScene().getWorldSize().y;
|
||||
}
|
||||
|
||||
onExit(): void {
|
||||
|
||||
}
|
||||
|
||||
steerTowards(vec: Vec2){
|
||||
let v = vec.normalize().scale(RunAwayFromPlayer.RUN_AWAY_SPEED).sub(this.actor.velocity);
|
||||
return MathUtils.clampMagnitude(v, RunAwayFromPlayer.MAX_STEER_FORCE);
|
||||
}
|
||||
|
||||
}
|
|
@ -1,13 +1,11 @@
|
|||
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 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 {
|
||||
export default class FlockBehavior {
|
||||
scene: Scene;
|
||||
actor: Boid;
|
||||
flock: Array<Boid>;
|
||||
|
@ -19,7 +17,6 @@ export default class FlockBehavior extends Behavior {
|
|||
separationHeading: Vec2;
|
||||
|
||||
constructor(scene: Scene, actor: Boid, flock: Array<Boid>, visionRange: number, avoidRadius: number) {
|
||||
super();
|
||||
this.scene = scene;
|
||||
this.actor = actor;
|
||||
this.flock = flock;
|
||||
|
@ -28,7 +25,7 @@ export default class FlockBehavior extends Behavior {
|
|||
this.avoidRadius = avoidRadius;
|
||||
}
|
||||
|
||||
doBehavior(deltaT: number): void {
|
||||
update(): void {
|
||||
|
||||
// Update the visible region
|
||||
this.visibleRegion.setCenter(this.actor.getPosition().clone());
|
3
src/_DemoClasses/CustomGameEventType.ts
Normal file
3
src/_DemoClasses/CustomGameEventType.ts
Normal file
|
@ -0,0 +1,3 @@
|
|||
export enum CustomGameEventType {
|
||||
PLAYER_MOVE = "player_move",
|
||||
}
|
17
src/_DemoClasses/Player/Player.ts
Normal file
17
src/_DemoClasses/Player/Player.ts
Normal file
|
@ -0,0 +1,17 @@
|
|||
import Vec2 from "../../DataTypes/Vec2";
|
||||
import Rect from "../../Nodes/Graphics/Rect";
|
||||
import PlayerController, { PlayerType } from "./PlayerController";
|
||||
|
||||
export default class Player extends Rect {
|
||||
controller: PlayerController;
|
||||
|
||||
constructor(position: Vec2){
|
||||
super(position, new Vec2(20, 20));
|
||||
|
||||
this.controller = new PlayerController(this, PlayerType.TOPDOWN);
|
||||
}
|
||||
|
||||
update(deltaT: number): void {
|
||||
this.controller.update(deltaT);
|
||||
}
|
||||
}
|
50
src/_DemoClasses/Player/PlayerController.ts
Normal file
50
src/_DemoClasses/Player/PlayerController.ts
Normal file
|
@ -0,0 +1,50 @@
|
|||
import StateMachine from "../../DataTypes/State/StateMachine";
|
||||
import CanvasNode from "../../Nodes/CanvasNode";
|
||||
import IdleTopDown from "./PlayerStates/IdleTopDown";
|
||||
import MoveTopDown from "./PlayerStates/MoveTopDown";
|
||||
|
||||
export enum PlayerType {
|
||||
PLATFORMER = "platformer",
|
||||
TOPDOWN = "topdown"
|
||||
}
|
||||
|
||||
export enum PlayerStates {
|
||||
MOVE = "move",
|
||||
IDLE = "idle"
|
||||
}
|
||||
|
||||
export default class PlayerController extends StateMachine {
|
||||
protected owner: CanvasNode;
|
||||
|
||||
constructor(owner: CanvasNode, playerType: string){
|
||||
super();
|
||||
|
||||
this.owner = owner;
|
||||
|
||||
if(playerType === PlayerType.TOPDOWN){
|
||||
this.initializeTopDown();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Initializes the player controller for a top down player
|
||||
*/
|
||||
initializeTopDown(): void {
|
||||
let idle = new IdleTopDown(this);
|
||||
let move = new MoveTopDown(this, this.owner);
|
||||
|
||||
this.addState(PlayerStates.IDLE, idle);
|
||||
this.addState(PlayerStates.MOVE, move);
|
||||
|
||||
this.initialize(PlayerStates.IDLE);
|
||||
}
|
||||
|
||||
changeState(stateName: string): void {
|
||||
if(stateName === PlayerStates.MOVE){
|
||||
// If move, push to the stack
|
||||
this.stack.push(this.stateMap.get(stateName));
|
||||
}
|
||||
|
||||
super.changeState(stateName);
|
||||
}
|
||||
}
|
32
src/_DemoClasses/Player/PlayerStates/IdleTopDown.ts
Normal file
32
src/_DemoClasses/Player/PlayerStates/IdleTopDown.ts
Normal file
|
@ -0,0 +1,32 @@
|
|||
import State from "../../../DataTypes/State/State";
|
||||
import Vec2 from "../../../DataTypes/Vec2";
|
||||
import GameEvent from "../../../Events/GameEvent";
|
||||
import InputReceiver from "../../../Input/InputReceiver";
|
||||
import { PlayerStates } from "../PlayerController";
|
||||
|
||||
export default class IdleTopDown extends State {
|
||||
direction: Vec2 = Vec2.ZERO;
|
||||
input: InputReceiver = InputReceiver.getInstance();
|
||||
|
||||
onEnter(): void {
|
||||
this.direction.zero();
|
||||
}
|
||||
|
||||
handleInput(event: GameEvent): void {
|
||||
// Ignore inputs
|
||||
}
|
||||
|
||||
update(deltaT: number): void {
|
||||
// If we're starting to move, change states
|
||||
this.direction.x = (this.input.isPressed("a") ? -1 : 0) + (this.input.isPressed("d") ? 1 : 0);
|
||||
this.direction.y = (this.input.isPressed("w") ? -1 : 0) + (this.input.isPressed("s") ? 1 : 0);
|
||||
|
||||
if(!this.direction.isZero()){
|
||||
this.finished(PlayerStates.MOVE);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
onExit(): void {}
|
||||
|
||||
}
|
51
src/_DemoClasses/Player/PlayerStates/MoveTopDown.ts
Normal file
51
src/_DemoClasses/Player/PlayerStates/MoveTopDown.ts
Normal file
|
@ -0,0 +1,51 @@
|
|||
import State from "../../../DataTypes/State/State";
|
||||
import StateMachine from "../../../DataTypes/State/StateMachine";
|
||||
import Vec2 from "../../../DataTypes/Vec2";
|
||||
import GameEvent from "../../../Events/GameEvent";
|
||||
import InputReceiver from "../../../Input/InputReceiver";
|
||||
import CanvasNode from "../../../Nodes/CanvasNode";
|
||||
import { CustomGameEventType } from "../../CustomGameEventType";
|
||||
|
||||
export default class MoveTopDown extends State {
|
||||
direction: Vec2 = Vec2.ZERO;
|
||||
speed: number = 0;
|
||||
input: InputReceiver = InputReceiver.getInstance();
|
||||
owner: CanvasNode;
|
||||
|
||||
constructor(parent: StateMachine, owner: CanvasNode) {
|
||||
super(parent);
|
||||
this.owner = owner;
|
||||
}
|
||||
|
||||
onEnter(): void {
|
||||
// Initialize or reset the direction and speed
|
||||
this.direction.zero();
|
||||
this.speed = 100;
|
||||
}
|
||||
|
||||
handleInput(event: GameEvent): void {
|
||||
// Ignore input for now
|
||||
}
|
||||
|
||||
update(deltaT: number): void {
|
||||
// Get direction
|
||||
this.direction.x = (this.input.isPressed("a") ? -1 : 0) + (this.input.isPressed("d") ? 1 : 0);
|
||||
this.direction.y = (this.input.isPressed("w") ? -1 : 0) + (this.input.isPressed("s") ? 1 : 0);
|
||||
|
||||
if(this.direction.isZero()){
|
||||
this.finished("previous");
|
||||
return;
|
||||
}
|
||||
|
||||
// Otherwise, we are still moving, so update position
|
||||
let velocity = this.direction.normalize().scale(this.speed);
|
||||
this.owner.position.add(velocity.scale(deltaT));
|
||||
|
||||
// Emit an event to tell the world we are moving
|
||||
this.emitter.fireEvent(CustomGameEventType.PLAYER_MOVE, {position: this.owner.position.clone()});
|
||||
}
|
||||
|
||||
onExit(): void {
|
||||
// Nothing special to do here
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user