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 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);
|
||||||
}
|
}
|
||||||
|
@ -44,14 +41,14 @@ export default class BoidDemo extends Scene {
|
||||||
for(let boid of this.boids){
|
for(let boid of this.boids){
|
||||||
boid.setColor(Color.RED);
|
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){
|
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 {
|
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
|
||||||
|
@ -250,6 +264,21 @@ export default class Vec2 {
|
||||||
clone(): Vec2 {
|
clone(): 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.
|
||||||
|
|
|
@ -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
|
||||||
*/
|
*/
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
|
@ -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 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());
|
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