Origin/new engine features (#2)

* isolate features
This commit is contained in:
ZGrandison 2022-02-15 13:57:47 -05:00 committed by GitHub
parent 8b0f6f34a9
commit a0bace91a0
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
24 changed files with 651 additions and 55 deletions

3
.gitignore vendored
View File

@ -10,6 +10,9 @@ dist/*
# Include the built-in asset folder # Include the built-in asset folder
!dist/builtin/ !dist/builtin/
# Include the hw1 assets
!dist/hw3_assets/
# Include the hw1 assets # Include the hw1 assets
!dist/hw4_assets/ !dist/hw4_assets/

View File

@ -1,6 +1,7 @@
import Actor from "../DataTypes/Interfaces/Actor"; import Actor from "../DataTypes/Interfaces/Actor";
import Updateable from "../DataTypes/Interfaces/Updateable"; import Updateable from "../DataTypes/Interfaces/Updateable";
import AI from "../DataTypes/Interfaces/AI"; import AI from "../DataTypes/Interfaces/AI";
import GoapAI from "../DataTypes/Interfaces/GoapAI"
import Map from "../DataTypes/Map"; import Map from "../DataTypes/Map";
/** /**
@ -39,7 +40,7 @@ export default class AIManager implements Updateable {
* @param name The name of the AI to register * @param name The name of the AI to register
* @param constr The constructor for the AI * @param constr The constructor for the AI
*/ */
registerAI(name: string, constr: new <T extends AI>() => T ): void { registerAI(name: string, constr: new <T extends AI | GoapAI>() => T ): void {
this.registeredAI.add(name, constr); this.registeredAI.add(name, constr);
} }
@ -48,7 +49,7 @@ export default class AIManager implements Updateable {
* @param name The name of the AI to add * @param name The name of the AI to add
* @returns A new AI instance * @returns A new AI instance
*/ */
generateAI(name: string): AI { generateAI(name: string): AI | GoapAI {
if(this.registeredAI.has(name)){ if(this.registeredAI.has(name)){
return new (this.registeredAI.get(name))(); return new (this.registeredAI.get(name))();
} else { } else {

View File

@ -0,0 +1,82 @@
import Graph from "../DataTypes/Graphs/Graph";
import GoapAction from "../DataTypes/Interfaces/GoapAction";
import GoapAI from "../DataTypes/Interfaces/GoapAI";
import Queue from "../DataTypes/Queue";
import Stack from "../DataTypes/Stack";
import GraphUtils from "../Utils/GraphUtils";
export default class GoapActionPlanner {
mapping: Map<number,GoapAction | string>;
graph: Graph;
path: Array<number>;
plan(goal: string, possibleActions: Array<GoapAction>, currentStatus: Array<string>, actor: GoapAI): Stack<GoapAction> {
this.graph = new Graph(true);
this.mapping = new Map();
//0 is our root
this.graph.addNode();
this.mapping.set(0,"Start");
//1 is the goal
this.graph.addNode();
this.mapping.set(1,"Goal");
this.graph.addEdge(1,1,Number.POSITIVE_INFINITY);
//Build tree from 0 to 1
this.buildTree(0, goal, possibleActions, currentStatus);
console.log(this.graph.toString());
//Run djikstra to find shortest path
this.path = GraphUtils.djikstra(this.graph, 0);
//Push all elements of the plan
let plan = new Stack<GoapAction>();
let i = 1;
while(this.path[i] !== -1){
console.log(this.path[i]);
if (this.path[i] !== 0){
plan.push(<GoapAction>this.mapping.get(this.path[i]));
}
i = this.path[i];
}
return plan;
}
buildTree(root: number, goal:string, possibleActions: Array<GoapAction>, currentStatus: Array<string>): void {
//For each possible action
possibleActions.forEach(action => {
console.log("root:" + root + ",action precons:" + action.preconditions.toString()
+ ", action effects:" + action.effects.toString() + ", current Status:" + currentStatus.toString())
//Can it be performed?
if (action.checkPreconditions(currentStatus)){
//This action can be performed
//Add effects to currentStatus
let newStatus = [...currentStatus];
newStatus.push(...action.effects);
//Check if the new node is the goal
if (newStatus.includes(goal)){
console.log("AT GOAL");
let newNode = this.graph.addNode() - 1;
this.mapping.set(newNode, action);
this.graph.addEdge(root, newNode, action.cost);
this.graph.addEdge(newNode, 1, 0);
return;
}
//Add node and edge from root
let newNode = this.graph.addNode() - 1;
this.mapping.set(newNode, action);
this.graph.addEdge(root, newNode, action.cost);
//Recursive call
console.log(possibleActions.indexOf(action))
let newActions = possibleActions.filter(act => act !== action)
this.buildTree(newNode, goal, newActions, action.effects);
}
});
}
}

View File

@ -0,0 +1,40 @@
import GoapAction from "../DataTypes/Interfaces/GoapAction";
import GoapAI from "../DataTypes/Interfaces/GoapAI";
import Queue from "../DataTypes/Queue";
import Stack from "../DataTypes/Stack";
import StateMachine from "../DataTypes/State/StateMachine";
import GameNode from "../Nodes/GameNode";
import GoapActionPlanner from "./GoapActionPlanner";
/**
* A version of a @reference[StateMachine] that is configured to work as an AI controller for a @reference[GameNode]
*/
export default class StateMachineGoapAI extends StateMachine implements GoapAI {
/** The GameNode that uses this StateMachine for its AI */
protected owner: GameNode;
goal: string;
currentStatus: Array<string>;
possibleActions: Array<GoapAction>;
plan: Stack<GoapAction>;
planner: GoapActionPlanner;
// @implemented
initializeAI(owner: GameNode, config: Record<string, any>): void {}
// @implemented
destroy(){
// Get rid of our reference to the owner
delete this.owner;
this.receiver.destroy();
}
// @implemented
activate(options: Record<string, any>): void {}
changeGoal(goal: string): void {}
}

View File

@ -125,7 +125,7 @@ export default class Graph {
for(let i = 0; i < this.numVertices; i++){ for(let i = 0; i < this.numVertices; i++){
let edge = this.edges[i]; let edge = this.edges[i];
let edgeStr = ""; let edgeStr = "";
while(edge !== null){ while(edge !== undefined && edge !== null){
edgeStr += edge.y.toString(); edgeStr += edge.y.toString();
if(this.weighted){ if(this.weighted){
edgeStr += " (" + edge.weight + ")"; edgeStr += " (" + edge.weight + ")";

View File

@ -1,12 +1,13 @@
import NavigationPath from "../../Pathfinding/NavigationPath"; import NavigationPath from "../../Pathfinding/NavigationPath";
import AI from "./AI"; import AI from "./AI";
import GoapAI from "./GoapAI";
/** /**
* A game object that has an AI and can perform its own actions every update cycle * A game object that has an AI and can perform its own actions every update cycle
*/ */
export default interface Actor { export default interface Actor {
/** The AI of the actor */ /** The AI of the actor */
ai: AI; ai: AI | GoapAI;
/** The activity status of the actor */ /** The activity status of the actor */
aiActive: boolean; aiActive: boolean;
@ -21,8 +22,9 @@ export default interface Actor {
* Adds an AI to this Actor. * Adds an AI to this Actor.
* @param ai The name of the AI, or the actual AI, to add to the Actor. * @param ai The name of the AI, or the actual AI, to add to the Actor.
* @param options The options to give to the AI for initialization. * @param options The options to give to the AI for initialization.
* @param type The type of the AI, 0 for AI, 1 for GoapAI, defaults to assume AI
*/ */
addAI<T extends AI>(ai: string | (new () => T), options: Record<string, any>): void; addAI<T extends AI>(ai: string | (new () => T), options: Record<string, any>, type?: number): void;
/** /**
* Sets the AI to start/stop for this Actor. * Sets the AI to start/stop for this Actor.

View File

@ -0,0 +1,42 @@
import GoapActionPlanner from "../../AI/GoapActionPlanner";
import GameEvent from "../../Events/GameEvent";
import GameNode from "../../Nodes/GameNode";
import Queue from "../Queue";
import Stack from "../Stack";
import GoapAction from "./GoapAction";
import Updateable from "./Updateable";
/**
* Defines a controller for a bot or a human. Must be able to update
*/
export default interface GoapAI extends Updateable {
/** Current goal of the AI */
goal: string;
/** All current statuses this AI has */
currentStatus: Array<string>;
/** All possible actions that can be carried out */
possibleActions: Array<GoapAction>;
/** Current actions to be carried out */
plan: Stack<GoapAction>;
/** Once we have no actions, the planner can be called to find a new sequence of actions */
planner: GoapActionPlanner;
/** Clears references from to the owner */
destroy(): void;
/** Activates this AI from a stopped state and allows variables to be passed in */
activate(options: Record<string, any>): void;
/** Handles events from the Actor */
handleEvent(event: GameEvent): void;
/** Initializes the AI with the actor and any additional config */
initializeAI(owner:GameNode, options: Record<string, any>): void
/** Change the goal to a new goal */
changeGoal(goal: string): void
}

View File

@ -0,0 +1,61 @@
import StateMachineGoapAI from "../../AI/StateMachineGoapAI";
export default abstract class GoapAction {
/** Cost it takes to complete this action */
cost: number;
/** Preconditions that have to be satisfied for an action to happen */
preconditions: Array<string>;
/** Resulting statuses after this action completes */
effects: Array<string>;
/** If the action fails, do we keep trying until we succeed */
loopAction: boolean;
/**
* Attempt to perform an action, if successful, it will return an array of the expected effects, otherwise it will return null
* @param statuses Current statuses of the actor
* @param actor GameNode for the actor
* @param deltaT The time sine the last update
* @param target GameNode for a optional target
*/
abstract performAction(statuses: Array<string>, actor: StateMachineGoapAI, deltaT: number, target?: StateMachineGoapAI): Array<string>;
/** Check preconditions with current statuses to see if action can be performed */
checkPreconditions(statuses: Array<string>): boolean {
// Check that every element in the preconditions array is found in the statuses array
return (this.preconditions.every((status) => {
if (!statuses.includes(status)){
return false;
}
return true;
}));
}
/** Add one or more preconditions to this action */
addPrecondition(preconditions: string | string[]): void {
this.preconditions.push(...preconditions);
}
/** Add one or more effects to this action */
addEffect(effects: string | string[]): void {
this.effects.push(...effects);
}
/** Removes an precondition, returns true if successful */
removePrecondition(precondition: string): boolean {
throw new Error("Method not implemented.");
}
/** Removes an precondition, returns true if successful */
removeEffect(effect: string): boolean {
throw new Error("Method not implemented.");
}
/** Update the cost of this action based on options */
abstract updateCost(options: Record<string,number>): void;
abstract toString(): string;
}

View File

@ -7,6 +7,7 @@ export default interface Navigable {
* Gets a new navigation path based on this Navigable object. * Gets a new navigation path based on this Navigable object.
* @param fromPosition The position to start navigation from. * @param fromPosition The position to start navigation from.
* @param toPosition The position to navigate to. * @param toPosition The position to navigate to.
* @param direct If true, move directly from fromPosition to toPosition
*/ */
getNavigationPath(fromPosition: Vec2, toPosition: Vec2): NavigationPath; getNavigationPath(fromPosition: Vec2, toPosition: Vec2, direct?: boolean): NavigationPath;
} }

View File

@ -0,0 +1,38 @@
import Updateable from "../DataTypes/Interfaces/Updateable";
import EventQueue from "./EventQueue";
import Receiver from "./Receiver";
export default abstract class BattleSystem implements Updateable {
units: Map<number,Record<string, number>>;
receiver: Receiver;
eventQueue: EventQueue;
statSystem: Array<string>;
constructor(battleEvents: Array<string>, statSystem: Array<string>){
this.eventQueue = EventQueue.getInstance();
this.eventQueue.subscribe(this.receiver, battleEvents);
this.statSystem = statSystem;
}
getUnitStats(id: number): Record<string,number> {
return this.units.get(id);
}
validateStats(stats: Record<string,number>): Record<string,number> {
this.statSystem.forEach(e => {
if (stats[e] === undefined){
stats[e] = 0;
}
});
return stats;
}
initalizeUnit(id: number, stats: Record<string, number>): void{
this.units.set(id, this.validateStats(stats));
}
abstract update(deltaT: number): void;
}

View File

@ -12,6 +12,7 @@ import { GameEventType } from "../Events/GameEventType";
export default class Input { export default class Input {
private static mousePressed: boolean; private static mousePressed: boolean;
private static mouseJustPressed: boolean; private static mouseJustPressed: boolean;
private static mouseButtonPressed: number;
private static keyJustPressed: Map<boolean>; private static keyJustPressed: Map<boolean>;
private static keyPressed: Map<boolean>; private static keyPressed: Map<boolean>;
@ -80,6 +81,7 @@ export default class Input {
Input.mouseJustPressed = true; Input.mouseJustPressed = true;
Input.mousePressed = true; Input.mousePressed = true;
Input.mousePressPosition = event.data.get("position"); Input.mousePressPosition = event.data.get("position");
Input.mouseButtonPressed = event.data.get("button");
} }
if (event.type === GameEventType.MOUSE_UP) { if (event.type === GameEventType.MOUSE_UP) {
@ -237,20 +239,30 @@ export default class Input {
return false; return false;
} }
} }
/** /**
* Returns whether or not the mouse was newly pressed Input frame *
* Returns whether or not the mouse was newly pressed Input frame.
* @param mouseButton Optionally specify which mouse click you want to know was pressed.
* 0 for left click, 1 for middle click, 2 for right click.
* @returns True if the mouse was just pressed, false otherwise * @returns True if the mouse was just pressed, false otherwise
*/ */
static isMouseJustPressed(): boolean { static isMouseJustPressed(mouseButton?: number): boolean {
if (mouseButton) {
return Input.mouseJustPressed && !Input.mouseDisabled && mouseButton == this.mouseButtonPressed;
}
return Input.mouseJustPressed && !Input.mouseDisabled; return Input.mouseJustPressed && !Input.mouseDisabled;
} }
/** /**
* Returns whether or not the mouse is currently pressed * Returns whether or not the mouse is currently pressed
* @param mouseButton Optionally specify which mouse click you want to know was pressed.
* 0 for left click, 1 for middle click, 2 for right click.
* @returns True if the mouse is currently pressed, false otherwise * @returns True if the mouse is currently pressed, false otherwise
*/ */
static isMousePressed(): boolean { static isMousePressed(mouseButton?: number): boolean {
if (mouseButton) {
return Input.mousePressed && !Input.mouseDisabled && mouseButton == this.mouseButtonPressed;
}
return Input.mousePressed && !Input.mouseDisabled; return Input.mousePressed && !Input.mouseDisabled;
} }

View File

@ -29,7 +29,8 @@ export default class InputHandler {
private handleMouseDown = (event: MouseEvent, canvas: HTMLCanvasElement): void => { private handleMouseDown = (event: MouseEvent, canvas: HTMLCanvasElement): void => {
let pos = this.getMousePosition(event, canvas); let pos = this.getMousePosition(event, canvas);
let gameEvent = new GameEvent(GameEventType.MOUSE_DOWN, {position: pos}); let button = event.button;
let gameEvent = new GameEvent(GameEventType.MOUSE_DOWN, {position: pos, button: button});
this.eventQueue.addEvent(gameEvent); this.eventQueue.addEvent(gameEvent);
} }

View File

@ -18,6 +18,7 @@ import TweenController from "../Rendering/Animations/TweenController";
import Debug from "../Debug/Debug"; import Debug from "../Debug/Debug";
import Color from "../Utils/Color"; import Color from "../Utils/Color";
import Circle from "../DataTypes/Shapes/Circle"; import Circle from "../DataTypes/Shapes/Circle";
import GoapAI from "../DataTypes/Interfaces/GoapAI";
/** /**
* The representation of an object in the game world. * The representation of an object in the game world.
@ -54,7 +55,7 @@ export default abstract class GameNode implements Positioned, Unique, Updateable
isColliding: boolean = false; isColliding: boolean = false;
/*---------- ACTOR ----------*/ /*---------- ACTOR ----------*/
_ai: AI; _ai: AI | GoapAI;
aiActive: boolean; aiActive: boolean;
path: NavigationPath; path: NavigationPath;
pathfinding: boolean = false; pathfinding: boolean = false;
@ -331,11 +332,11 @@ export default abstract class GameNode implements Positioned, Unique, Updateable
} }
/*---------- ACTOR ----------*/ /*---------- ACTOR ----------*/
get ai(): AI { get ai(): AI | GoapAI {
return this._ai; return this._ai;
} }
set ai(ai: AI) { set ai(ai: AI | GoapAI) {
if(!this._ai){ if(!this._ai){
// If we haven't been previously had an ai, register us with the ai manager // If we haven't been previously had an ai, register us with the ai manager
this.scene.getAIManager().registerActor(this); this.scene.getAIManager().registerActor(this);
@ -346,7 +347,7 @@ export default abstract class GameNode implements Positioned, Unique, Updateable
} }
// @implemented // @implemented
addAI<T extends AI>(ai: string | (new () => T), options?: Record<string, any>): void { addAI<T extends AI | GoapAI>(ai: string | (new () => T), options?: Record<string, any>, type?: number): void {
if(!this._ai){ if(!this._ai){
this.scene.getAIManager().registerActor(this); this.scene.getAIManager().registerActor(this);
} }
@ -357,6 +358,7 @@ export default abstract class GameNode implements Positioned, Unique, Updateable
this._ai = new ai(); this._ai = new ai();
} }
// Question, how much do we want different type of AI to be handled the same, i.e. should GoapAI and AI similar methods and signatures for the sake of unity
this._ai.initializeAI(this, options); this._ai.initializeAI(this, options);
this.aiActive = true; this.aiActive = true;

View File

@ -29,4 +29,28 @@ export default abstract class Graphic extends CanvasNode {
setColor(color: Color){ setColor(color: Color){
this.color = color; this.color = color;
} }
set colorR(r: number){
this.color.r = r;
}
get colorR(): number {
return this.color.r;
}
set colorG(g: number){
this.color.g = g;
}
get colorG(): number {
return this.color.g;
}
set colorB(b: number){
this.color.b = b;
}
get colorB(): number {
return this.color.b;
}
} }

View File

@ -2,4 +2,5 @@ export enum GraphicType {
POINT = "POINT", POINT = "POINT",
RECT = "RECT", RECT = "RECT",
LINE = "LINE", LINE = "LINE",
PARTICLE = "PARTICLE"
} }

View File

@ -0,0 +1,57 @@
import Vec2 from "../../DataTypes/Vec2";
import Point from "./Point";
/**
* - Position X
- Velocity (speed and direction) X
- Color X
- Lifetime
- Age can be handled as lifetime
- Shape X
- Size X
- Transparency X
*/
export default class Particle extends Point {
age: number;
inUse: boolean;
vel: Vec2;
mass: number;
constructor(position: Vec2, size: Vec2, mass: number) {
// Are we making this a circle?
super(position, size);
this.inUse = false;
this.mass = mass;
}
setParticleActive(lifetime: number, position: Vec2) {
this.age = lifetime;
this.inUse = true;
this.visible = true;
this.position = position;
}
decrementAge(decay: number) {
this.age -= decay;
}
setParticleInactive(){
this.inUse = false;
this.visible = false;
}
set velY(y: number){
this.vel.y = y;
}
get velY(): number {
return this.vel.y;
}
}

View File

@ -4,9 +4,10 @@ import Vec2 from "../../DataTypes/Vec2";
/** A basic point to be drawn on the screen. */ /** A basic point to be drawn on the screen. */
export default class Point extends Graphic { export default class Point extends Graphic {
constructor(position: Vec2){ constructor(position: Vec2, size: Vec2) {
// Are we making this a circle?
super(); super();
this.position = position; this.position = position;
this.size.set(5, 5); this.size.set(size.x, size.y);
} }
} }

View File

@ -30,10 +30,11 @@ export default class NavigationManager {
* @param navName The name of the registered Navigable object * @param navName The name of the registered Navigable object
* @param fromPosition The starting position of navigation * @param fromPosition The starting position of navigation
* @param toPosition The ending position of Navigation * @param toPosition The ending position of Navigation
* @param direct If true, go direct from fromPosition to toPosition, don't use NavMesh
* @returns A NavigationPath containing the route to take over the Navigable entity to get between the provided positions. * @returns A NavigationPath containing the route to take over the Navigable entity to get between the provided positions.
*/ */
getPath(navName: string, fromPosition: Vec2, toPosition: Vec2): NavigationPath { getPath(navName: string, fromPosition: Vec2, toPosition: Vec2, direct?: boolean): NavigationPath {
let nav = this.navigableEntities.get(navName); let nav = this.navigableEntities.get(navName);
return nav.getNavigationPath(fromPosition.clone(), toPosition.clone()); return nav.getNavigationPath(fromPosition.clone(), toPosition.clone(), direct);
} }
} }

View File

@ -21,18 +21,23 @@ export default class Navmesh implements Navigable {
} }
// @implemented // @implemented
getNavigationPath(fromPosition: Vec2, toPosition: Vec2): NavigationPath { getNavigationPath(fromPosition: Vec2, toPosition: Vec2, direct: boolean): NavigationPath {
let start = this.getClosestNode(fromPosition); let start = this.getClosestNode(fromPosition);
let end = this.getClosestNode(toPosition); let end = this.getClosestNode(toPosition);
let parent = GraphUtils.djikstra(this.graph, start);
let pathStack = new Stack<Vec2>(this.graph.numVertices); let pathStack = new Stack<Vec2>(this.graph.numVertices);
// Push the final position and the final position in the graph // Push the final position and the final position in the graph
pathStack.push(toPosition.clone()); pathStack.push(toPosition.clone());
if (direct) {
return new NavigationPath(pathStack);
}
pathStack.push(this.graph.positions[end]); pathStack.push(this.graph.positions[end]);
let parent = GraphUtils.djikstra(this.graph, start);
// Add all parents along the path // Add all parents along the path
let i = end; let i = end;
while(parent[i] !== -1){ while(parent[i] !== -1){

View File

@ -0,0 +1,163 @@
import Updateable from "../../DataTypes/Interfaces/Updateable";
import Vec2 from "../../DataTypes/Vec2";
import { GraphicType } from "../../Nodes/Graphics/GraphicTypes";
import Particle from "../../Nodes/Graphics/Particle";
import Scene from "../../Scene/Scene";
import Timer from "../../Timing/Timer";
import Color from "../../Utils/Color";
import { EaseFunctionType } from "../../Utils/EaseFunctions";
import RandUtils from "../../Utils/RandUtils";
import ParticleSystemManager from "./ParticleSystemManager";
/*
-Move particle system to HW#4, particle class and particle manager(object pool), source, randomized period of decay,
semi-randomized approach for spawning, should be general purpose
and load some settings from a json (location, states, colors, randomization).
Should be effect when balloon is popped
*/
export default class ParticleSystem implements Updateable {
protected particlePool: Array<Particle>;
protected lifetime: number;
protected liveParticles: number;
protected maxLiveParticles: number;
protected sourcePoint: Vec2;
protected particleSize: Vec2;
protected systemLifetime: Timer;
protected systemRunning: boolean;
protected color: Color = new Color(255, 0, 0);
constructor(poolSize: number, sourcePoint: Vec2, lifetime: number, size: number, maxParticles: number) {
this.particlePool = new Array(poolSize);
this.sourcePoint = sourcePoint;
this.lifetime = lifetime;
this.particleSize = new Vec2(size, size);
this.maxLiveParticles = maxParticles;
this.systemRunning = false;
ParticleSystemManager.getInstance().registerParticleSystem(this);
}
initalizePool(scene: Scene, layer: string, type: ParticleSystemType, mass: number) {
for (let i = 0; i < this.particlePool.length; i++) {
this.particlePool[i] = <Particle>scene.add.graphic(GraphicType.PARTICLE, layer,
{ position: this.sourcePoint.clone(), size: this.particleSize.clone(), mass: mass });
this.particlePool[i].addPhysics();
this.particlePool[i].isCollidable = false;
this.particlePool[i].visible = false;
}
}
startSystem(time: number, startPoint?: Vec2) {
this.systemLifetime = new Timer(time);
this.systemLifetime.start();
this.systemRunning = true;
this.sourcePoint = startPoint;
}
stopSystem() {
console.log(this);
this.systemRunning = false;
for (let particle of this.particlePool) {
if (particle.inUse) {
particle.setParticleInactive();
}
}
}
changeColor(color: Color) {
this.color = color;
}
update(deltaT: number) {
if (!this.systemRunning) {
return;
}
if (this.systemLifetime.isStopped()) {
this.stopSystem();
}
else {
for (let particle of this.particlePool) {
if (particle.inUse) {
particle.decrementAge(deltaT * 1000);
if (particle.age <= 0) {
particle.setParticleInactive();
}
//particle.vel.y += 200*deltaT;
particle.move(particle.vel.scaled(deltaT));
}
else {
particle.setParticleActive(this.lifetime, this.sourcePoint.clone());
particle.color = this.color;
particle.alpha = 1;
//particle.size.set(1)
particle.vel = RandUtils.randVec(-50, 50, -100, 100);
particle.tweens.add("active", {
startDelay: 0,
duration: 2000,
effects: [
{
property: "alpha",
resetOnComplete: true,
start: 1,
end: 0,
ease: EaseFunctionType.IN_OUT_SINE
},
/*{
property: "colorR",
resetOnComplete: true,
start: particle.color.r,
end: 255,
ease: EaseFunctionType.IN_OUT_SINE
},
{
property: "colorG",
resetOnComplete: true,
start: particle.color.g,
end: 255,
ease: EaseFunctionType.IN_OUT_SINE
},
{
property: "colorB",
resetOnComplete: true,
start: particle.color.b,
end: 255,
ease: EaseFunctionType.IN_OUT_SINE
},*/
{
property: "velY",
resetOnComplete: true,
start: particle.vel.y,
end: particle.vel.y + ((this.lifetime * particle.mass)/2),
ease: EaseFunctionType.IN_OUT_SINE
}
]
});
particle.tweens.play("active");
//particle.vel = RandUtils.randVec(-150, 150, -100, 100);
//console.log(particle.vel.toString());
}
}
}
}
}
export enum ParticleSystemType {
emitter = "emitter",
burst = "burst"
}

View File

@ -0,0 +1,40 @@
import Updateable from "../../DataTypes/Interfaces/Updateable";
import ParticleSystem from "./ParticleSystem";
export default class ParticleSystemManager implements Updateable {
private static instance: ParticleSystemManager = null;
protected particleSystems: Array<ParticleSystem>;
private constructor(){
this.particleSystems = new Array();
}
static getInstance(): ParticleSystemManager {
if(ParticleSystemManager.instance === null){
ParticleSystemManager.instance = new ParticleSystemManager();
}
return ParticleSystemManager.instance;
}
registerParticleSystem(system: ParticleSystem){
this.particleSystems.push(system);
}
deregisterParticleSystem(system: ParticleSystem){
let index = this.particleSystems.indexOf(system);
this.particleSystems.splice(index, 1);
}
clearParticleSystems(){
this.particleSystems = new Array();
}
update(deltaT: number): void {
for(let particleSystem of this.particleSystems){
particleSystem.update(deltaT);
}
}
}

View File

@ -14,6 +14,7 @@ import TextInput from "../../Nodes/UIElements/TextInput";
import Rect from "../../Nodes/Graphics/Rect"; import Rect from "../../Nodes/Graphics/Rect";
import ResourceManager from "../../ResourceManager/ResourceManager"; import ResourceManager from "../../ResourceManager/ResourceManager";
import Line from "../../Nodes/Graphics/Line"; import Line from "../../Nodes/Graphics/Line";
import Particle from "../../Nodes/Graphics/Particle";
// @ignorePage // @ignorePage
@ -143,6 +144,9 @@ export default class CanvasNodeFactory {
case GraphicType.RECT: case GraphicType.RECT:
instance = this.buildRect(options); instance = this.buildRect(options);
break; break;
case GraphicType.PARTICLE:
instance = this.buildParticle(options);
break;
default: default:
throw `GraphicType '${type}' does not exist, or is registered incorrectly.` throw `GraphicType '${type}' does not exist, or is registered incorrectly.`
} }
@ -196,8 +200,18 @@ export default class CanvasNodeFactory {
buildPoint(options?: Record<string, any>): Point { buildPoint(options?: Record<string, any>): Point {
this.checkIfPropExists("Point", options, "position", Vec2, "Vec2"); this.checkIfPropExists("Point", options, "position", Vec2, "Vec2");
this.checkIfPropExists("Point", options, "size", Vec2, "Vec2");
return new Point(options.position); return new Point(options.position, options.size);
}
buildParticle(options?: Record<string, any>): Point {
this.checkIfPropExists("Particle", options, "position", Vec2, "Vec2");
this.checkIfPropExists("Particle", options, "size", Vec2, "Vec2");
this.checkIfPropExists("Particle", options, "mass", "number", "number");
//Changed for testing
return new Particle(options.position, options.size, options.mass);
} }
buildLine(options?: Record<string, any>): Point { buildLine(options?: Record<string, any>): Point {

View File

@ -25,6 +25,7 @@ import RenderingManager from "../Rendering/RenderingManager";
import Debug from "../Debug/Debug"; import Debug from "../Debug/Debug";
import TimerManager from "../Timing/TimerManager"; import TimerManager from "../Timing/TimerManager";
import TweenManager from "../Rendering/Animations/TweenManager"; import TweenManager from "../Rendering/Animations/TweenManager";
import ParticleSystemManager from "../Rendering/Animations/ParticleSystemManager";
/** /**
* Scenes are the main container in the game engine. * Scenes are the main container in the game engine.
@ -173,6 +174,9 @@ export default class Scene implements Updateable {
// Update all tweens // Update all tweens
TweenManager.getInstance().update(deltaT); TweenManager.getInstance().update(deltaT);
// Update all particle systems
ParticleSystemManager.getInstance().update(deltaT);
// Update viewport // Update viewport
this.viewport.update(deltaT); this.viewport.update(deltaT);
} }

View File

@ -1,3 +1,4 @@
import Game from "./Wolfie2D/Loop/Game"; import Game from "./Wolfie2D/Loop/Game";
import default_scene from "./default_scene"; import default_scene from "./default_scene";