added states and state machines for ai behaviors

This commit is contained in:
Joe Weaver 2020-10-14 14:55:22 -04:00
parent f3449c1526
commit 25e0b8a39e
19 changed files with 645 additions and 214 deletions

View File

@ -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;
}

View File

@ -1,12 +1,10 @@
import Vec2 from "./DataTypes/Vec2"; import Vec2 from "./DataTypes/Vec2";
import Debug from "./Debug/Debug";
import Point from "./Nodes/Graphics/Point";
import Scene from "./Scene/Scene"; import Scene from "./Scene/Scene";
import SceneGraphQuadTree from "./SceneGraph/SceneGraphQuadTree"; import SceneGraphQuadTree from "./SceneGraph/SceneGraphQuadTree";
import Color from "./Utils/Color"; import Color from "./Utils/Color";
import Boid from "./_DemoClasses/Boid"; import Boid from "./_DemoClasses/Boids/Boid";
import BoidBehavior from "./_DemoClasses/BoidBehavior"; import FlockBehavior from "./_DemoClasses/Boids/FlockBehavior";
import FlockBehavior from "./_DemoClasses/FlockBehavior"; import Player from "./_DemoClasses/Player/Player";
/** /**
* This demo emphasizes an ai system for the game engine with component architecture * 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.setBounds(0, 0, 800, 600)
this.viewport.setCenter(400, 300); this.viewport.setCenter(400, 300);
let layer = this.addLayer() let layer = this.addLayer();
this.boids = new Array(); this.boids = new Array();
// Add the player
this.add.graphic(Player, layer, new Vec2(0, 0));
// Create a bunch of boids // 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 boid = this.add.graphic(Boid, layer, new Vec2(this.worldSize.x*Math.random(), this.worldSize.y*Math.random()));
let separation = 3; boid.fb = new FlockBehavior(this, boid, this.boids, 75, 50);
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); boid.setSize(5, 5);
this.boids.push(boid); this.boids.push(boid);
} }
@ -45,13 +42,13 @@ export default class BoidDemo extends Scene {
boid.setColor(Color.RED); boid.setColor(Color.RED);
} }
for(let boid of this.boids){ this.updateFlock();
boid.getBehavior(FlockBehavior).doBehavior(deltaT);
} }
updateFlock(): void {
for(let boid of this.boids){ for(let boid of this.boids){
boid.getBehavior(BoidBehavior).doBehavior(deltaT); boid.fb.update();
} }
} }
} }

View 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;
}

View 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);
}
}

View File

@ -4,10 +4,7 @@
export default class Vec2 { export default class Vec2 {
// Store x and y in an array // Store x and y in an array
//private vec: Float32Array; private vec: Float32Array;
protected _x: number;
protected _y: number;
/** /**
* When this vector changes its value, do something * When this vector changes its value, do something
@ -15,20 +12,18 @@ export default class Vec2 {
private onChange: Function = () => {}; private onChange: Function = () => {};
constructor(x: number = 0, y: number = 0) { constructor(x: number = 0, y: number = 0) {
// this.vec = new Float32Array(2); this.vec = new Float32Array(2);
// this.vec[0] = x; this.vec[0] = x;
// this.vec[1] = y; this.vec[1] = y;
this._x = x;
this._y = y;
} }
// Expose x and y with getters and setters // Expose x and y with getters and setters
get x() { get x() {
return this._x; //this.vec[0]; return this.vec[0];
} }
set x(x: number) { set x(x: number) {
this._x = x;//this.vec[0] = x; this.vec[0] = x;
if(this.onChange){ if(this.onChange){
this.onChange(); this.onChange();
@ -36,11 +31,11 @@ export default class Vec2 {
} }
get y() { get y() {
return this._y;//this.vec[1]; return this.vec[1];
} }
set y(y: number) { set y(y: number) {
this._y = y;//this.vec[1] = y; this.vec[1] = y;
if(this.onChange){ if(this.onChange){
this.onChange(); this.onChange();
@ -51,6 +46,10 @@ export default class Vec2 {
return new Vec2(0, 0); return new Vec2(0, 0);
} }
static get INF() {
return new Vec2(Infinity, Infinity);
}
static get UP() { static get UP() {
return new Vec2(0, -1); return new Vec2(0, -1);
} }
@ -88,6 +87,13 @@ export default class Vec2 {
return new Vec2(this.x/mag, this.y/mag); 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. * 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
@ -164,6 +170,14 @@ export default class Vec2 {
return this; 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 * Adds this vector the another vector
* @param other * @param other
@ -251,6 +265,21 @@ export default class Vec2 {
return new Vec2(this.x, this.y); 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. * Sets the function that is called whenever this vector is changed.
* @param f The function to be called * @param f The function to be called

View File

@ -1,18 +1,15 @@
import EventQueue from "../Events/EventQueue";
import InputReceiver from "../Input/InputReceiver"; import InputReceiver from "../Input/InputReceiver";
import Vec2 from "../DataTypes/Vec2"; import Vec2 from "../DataTypes/Vec2";
import Receiver from "../Events/Receiver"; import Receiver from "../Events/Receiver";
import Emitter from "../Events/Emitter"; import Emitter from "../Events/Emitter";
import Scene from "../Scene/Scene"; 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, Updateable } from "../DataTypes/Interfaces/Descriptors"
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
*/ */
export default abstract class GameNode implements Positioned, Unique { export default abstract class GameNode implements Positioned, Unique, Updateable {
protected input: InputReceiver; protected input: InputReceiver;
private _position: Vec2; private _position: Vec2;
protected receiver: Receiver; protected receiver: Receiver;
@ -20,7 +17,6 @@ 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();
@ -28,7 +24,6 @@ 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 {
@ -77,33 +72,6 @@ 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
*/ */

View File

@ -15,10 +15,18 @@ export default class Rect extends Graphic {
this.borderWidth = 0; this.borderWidth = 0;
} }
/**
* Sets the border color of this rectangle
* @param color The border color
*/
setBorderColor(color: Color){ setBorderColor(color: Color){
this.borderColor = color; this.borderColor = color;
} }
/**Sets the border width of this rectangle
*
* @param width The width of the rectangle in pixels
*/
setBorderWidth(width: number){ setBorderWidth(width: number){
this.borderWidth = width; this.borderWidth = width;
} }

View File

@ -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);
}
}

View File

@ -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);
}
}

View 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();
}
}

View 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);
}
}

View 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);
}
}

View 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);
}
}

View File

@ -1,13 +1,11 @@
import Behavior from "../Behaviors/Behavior"; import AABB from "../../DataTypes/AABB";
import AABB from "../DataTypes/AABB"; import Vec2 from "../../DataTypes/Vec2";
import Vec2 from "../DataTypes/Vec2"; import Point from "../../Nodes/Graphics/Point";
import Point from "../Nodes/Graphics/Point"; import Scene from "../../Scene/Scene";
import Scene from "../Scene/Scene"; import Color from "../../Utils/Color";
import Color from "../Utils/Color";
import Boid from "./Boid"; import Boid from "./Boid";
import BoidBehavior from "./BoidBehavior";
export default class FlockBehavior extends Behavior { export default class FlockBehavior {
scene: Scene; scene: Scene;
actor: Boid; actor: Boid;
flock: Array<Boid>; flock: Array<Boid>;
@ -19,7 +17,6 @@ export default class FlockBehavior extends Behavior {
separationHeading: Vec2; separationHeading: Vec2;
constructor(scene: Scene, actor: Boid, flock: Array<Boid>, visionRange: number, avoidRadius: number) { constructor(scene: Scene, actor: Boid, flock: Array<Boid>, visionRange: number, avoidRadius: number) {
super();
this.scene = scene; this.scene = scene;
this.actor = actor; this.actor = actor;
this.flock = flock; this.flock = flock;
@ -28,7 +25,7 @@ export default class FlockBehavior extends Behavior {
this.avoidRadius = avoidRadius; this.avoidRadius = avoidRadius;
} }
doBehavior(deltaT: number): void { update(): void {
// Update the visible region // Update the visible region
this.visibleRegion.setCenter(this.actor.getPosition().clone()); this.visibleRegion.setCenter(this.actor.getPosition().clone());

View File

@ -0,0 +1,3 @@
export enum CustomGameEventType {
PLAYER_MOVE = "player_move",
}

View 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);
}
}

View 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);
}
}

View 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 {}
}

View 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
}
}