From 25e0b8a39ea817bc85f640c8a1b33397e5061bc7 Mon Sep 17 00:00:00 2001 From: Joe Weaver Date: Wed, 14 Oct 2020 14:55:22 -0400 Subject: [PATCH] added states and state machines for ai behaviors --- src/Behaviors/Behavior.ts | 14 -- src/BoidDemo.ts | 33 ++--- src/DataTypes/State/State.ts | 40 ++++++ src/DataTypes/State/StateMachine.ts | 128 ++++++++++++++++++ src/DataTypes/Vec2.ts | 55 ++++++-- src/Nodes/GameNode.ts | 36 +---- src/Nodes/Graphics/Rect.ts | 8 ++ src/_DemoClasses/Boid.ts | 43 ------ src/_DemoClasses/BoidBehavior.ts | 82 ----------- src/_DemoClasses/Boids/Boid.ts | 41 ++++++ src/_DemoClasses/Boids/BoidController.ts | 32 +++++ .../Boids/BoidStates/BoidBehavior.ts | 95 +++++++++++++ .../Boids/BoidStates/RunAwayFromPlayer.ts | 82 +++++++++++ src/_DemoClasses/{ => Boids}/FlockBehavior.ts | 17 +-- src/_DemoClasses/CustomGameEventType.ts | 3 + src/_DemoClasses/Player/Player.ts | 17 +++ src/_DemoClasses/Player/PlayerController.ts | 50 +++++++ .../Player/PlayerStates/IdleTopDown.ts | 32 +++++ .../Player/PlayerStates/MoveTopDown.ts | 51 +++++++ 19 files changed, 645 insertions(+), 214 deletions(-) delete mode 100644 src/Behaviors/Behavior.ts create mode 100644 src/DataTypes/State/State.ts create mode 100644 src/DataTypes/State/StateMachine.ts delete mode 100644 src/_DemoClasses/Boid.ts delete mode 100644 src/_DemoClasses/BoidBehavior.ts create mode 100644 src/_DemoClasses/Boids/Boid.ts create mode 100644 src/_DemoClasses/Boids/BoidController.ts create mode 100644 src/_DemoClasses/Boids/BoidStates/BoidBehavior.ts create mode 100644 src/_DemoClasses/Boids/BoidStates/RunAwayFromPlayer.ts rename src/_DemoClasses/{ => Boids}/FlockBehavior.ts (85%) create mode 100644 src/_DemoClasses/CustomGameEventType.ts create mode 100644 src/_DemoClasses/Player/Player.ts create mode 100644 src/_DemoClasses/Player/PlayerController.ts create mode 100644 src/_DemoClasses/Player/PlayerStates/IdleTopDown.ts create mode 100644 src/_DemoClasses/Player/PlayerStates/MoveTopDown.ts diff --git a/src/Behaviors/Behavior.ts b/src/Behaviors/Behavior.ts deleted file mode 100644 index d31df77..0000000 --- a/src/Behaviors/Behavior.ts +++ /dev/null @@ -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; -} \ No newline at end of file diff --git a/src/BoidDemo.ts b/src/BoidDemo.ts index 6464a6d..2bbb239 100644 --- a/src/BoidDemo.ts +++ b/src/BoidDemo.ts @@ -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); } @@ -44,14 +41,14 @@ export default class BoidDemo extends Scene { for(let boid of this.boids){ 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(); } } + } \ No newline at end of file diff --git a/src/DataTypes/State/State.ts b/src/DataTypes/State/State.ts new file mode 100644 index 0000000..08055fa --- /dev/null +++ b/src/DataTypes/State/State.ts @@ -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; +} \ No newline at end of file diff --git a/src/DataTypes/State/StateMachine.ts b/src/DataTypes/State/StateMachine.ts new file mode 100644 index 0000000..f221e0f --- /dev/null +++ b/src/DataTypes/State/StateMachine.ts @@ -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; + protected stateMap: Map; + 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); + } +} \ No newline at end of file diff --git a/src/DataTypes/Vec2.ts b/src/DataTypes/Vec2.ts index cbf08be..313b4fd 100644 --- a/src/DataTypes/Vec2.ts +++ b/src/DataTypes/Vec2.ts @@ -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 @@ -250,6 +264,21 @@ export default class Vec2 { clone(): 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. diff --git a/src/Nodes/GameNode.ts b/src/Nodes/GameNode.ts index a3796cb..c92d1b1 100644 --- a/src/Nodes/GameNode.ts +++ b/src/Nodes/GameNode.ts @@ -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; 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(constr: new (...args: any) => T): T { - let query = null; - - for(let behavior of this.behaviors){ - if(behavior instanceof constr){ - query = behavior; - } - } - - return query; - } - /** * Called if the position vector is modified or replaced */ diff --git a/src/Nodes/Graphics/Rect.ts b/src/Nodes/Graphics/Rect.ts index 17ff459..a2133dd 100644 --- a/src/Nodes/Graphics/Rect.ts +++ b/src/Nodes/Graphics/Rect.ts @@ -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; } diff --git a/src/_DemoClasses/Boid.ts b/src/_DemoClasses/Boid.ts deleted file mode 100644 index 7aa007d..0000000 --- a/src/_DemoClasses/Boid.ts +++ /dev/null @@ -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); - } -} \ No newline at end of file diff --git a/src/_DemoClasses/BoidBehavior.ts b/src/_DemoClasses/BoidBehavior.ts deleted file mode 100644 index 3fda155..0000000 --- a/src/_DemoClasses/BoidBehavior.ts +++ /dev/null @@ -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); - } - -} \ No newline at end of file diff --git a/src/_DemoClasses/Boids/Boid.ts b/src/_DemoClasses/Boids/Boid.ts new file mode 100644 index 0000000..1b7a746 --- /dev/null +++ b/src/_DemoClasses/Boids/Boid.ts @@ -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(); + } +} \ No newline at end of file diff --git a/src/_DemoClasses/Boids/BoidController.ts b/src/_DemoClasses/Boids/BoidController.ts new file mode 100644 index 0000000..b5d1c45 --- /dev/null +++ b/src/_DemoClasses/Boids/BoidController.ts @@ -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); + } +} \ No newline at end of file diff --git a/src/_DemoClasses/Boids/BoidStates/BoidBehavior.ts b/src/_DemoClasses/Boids/BoidStates/BoidBehavior.ts new file mode 100644 index 0000000..27e30c7 --- /dev/null +++ b/src/_DemoClasses/Boids/BoidStates/BoidBehavior.ts @@ -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); + } + +} \ No newline at end of file diff --git a/src/_DemoClasses/Boids/BoidStates/RunAwayFromPlayer.ts b/src/_DemoClasses/Boids/BoidStates/RunAwayFromPlayer.ts new file mode 100644 index 0000000..b166bce --- /dev/null +++ b/src/_DemoClasses/Boids/BoidStates/RunAwayFromPlayer.ts @@ -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); + } + +} \ No newline at end of file diff --git a/src/_DemoClasses/FlockBehavior.ts b/src/_DemoClasses/Boids/FlockBehavior.ts similarity index 85% rename from src/_DemoClasses/FlockBehavior.ts rename to src/_DemoClasses/Boids/FlockBehavior.ts index a365e3a..f3bb84d 100644 --- a/src/_DemoClasses/FlockBehavior.ts +++ b/src/_DemoClasses/Boids/FlockBehavior.ts @@ -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; @@ -19,7 +17,6 @@ export default class FlockBehavior extends Behavior { separationHeading: Vec2; constructor(scene: Scene, actor: Boid, flock: Array, 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()); diff --git a/src/_DemoClasses/CustomGameEventType.ts b/src/_DemoClasses/CustomGameEventType.ts new file mode 100644 index 0000000..f7bd102 --- /dev/null +++ b/src/_DemoClasses/CustomGameEventType.ts @@ -0,0 +1,3 @@ +export enum CustomGameEventType { + PLAYER_MOVE = "player_move", +} \ No newline at end of file diff --git a/src/_DemoClasses/Player/Player.ts b/src/_DemoClasses/Player/Player.ts new file mode 100644 index 0000000..a010fd5 --- /dev/null +++ b/src/_DemoClasses/Player/Player.ts @@ -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); + } +} \ No newline at end of file diff --git a/src/_DemoClasses/Player/PlayerController.ts b/src/_DemoClasses/Player/PlayerController.ts new file mode 100644 index 0000000..46d46b5 --- /dev/null +++ b/src/_DemoClasses/Player/PlayerController.ts @@ -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); + } +} \ No newline at end of file diff --git a/src/_DemoClasses/Player/PlayerStates/IdleTopDown.ts b/src/_DemoClasses/Player/PlayerStates/IdleTopDown.ts new file mode 100644 index 0000000..22a76d9 --- /dev/null +++ b/src/_DemoClasses/Player/PlayerStates/IdleTopDown.ts @@ -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 {} + +} \ No newline at end of file diff --git a/src/_DemoClasses/Player/PlayerStates/MoveTopDown.ts b/src/_DemoClasses/Player/PlayerStates/MoveTopDown.ts new file mode 100644 index 0000000..9392f05 --- /dev/null +++ b/src/_DemoClasses/Player/PlayerStates/MoveTopDown.ts @@ -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 + } +} \ No newline at end of file