added annotations to all files

This commit is contained in:
Joe Weaver 2021-01-13 13:30:45 -05:00
parent 3661ee3ada
commit 9e0ebae63c
82 changed files with 1693 additions and 475 deletions

View File

@ -1,10 +1,15 @@
import { Actor, AI, Updateable } from "../DataTypes/Interfaces/Descriptors";
import Map from "../DataTypes/Map";
/**
* A manager class for all of the AI in a scene.
* Keeps a list of registered actors and handles AI generation for actors.
*/
export default class AIManager implements Updateable {
/** The array of registered actors */
actors: Array<Actor>;
registeredAI: Map<new <T extends AI>() => T>;
/** Maps AI names to their constructors */
registeredAI: Map<AIConstructor>;
constructor(){
this.actors = new Array();
@ -20,10 +25,20 @@ export default class AIManager implements Updateable {
this.actors.push(actor);
}
/**
* Registers an AI with the AIManager for use later on
* @param name The name of the AI to register
* @param constr The constructor for the AI
*/
registerAI(name: string, constr: new <T extends AI>() => T ): void {
this.registeredAI.add(name, constr);
}
/**
* Generates an AI instance from its name
* @param name The name of the AI to add
* @returns A new AI instance
*/
generateAI(name: string): AI {
if(this.registeredAI.has(name)){
return new (this.registeredAI.get(name))();
@ -37,3 +52,5 @@ export default class AIManager implements Updateable {
this.actors.forEach(actor => { if(actor.aiActive) actor.ai.update(deltaT) });
}
}
type AIConstructor = new <T extends AI>() => T;

View File

@ -2,8 +2,13 @@ import { AI } from "../DataTypes/Interfaces/Descriptors";
import StateMachine from "../DataTypes/State/StateMachine";
import GameNode from "../Nodes/GameNode";
/**
* A version of a @reference[StateMachine] that is configured to work as an AI controller for a @reference[GameNode]
*/
export default class StateMachineAI extends StateMachine implements AI {
/** The GameNode that uses this StateMachine for its AI */
protected owner: GameNode;
// @implemented
initializeAI(owner: GameNode, config: Record<string, any>): void {}
}

View File

@ -1,12 +1,12 @@
// @ignorePage
// TODO - Is there already a way to do this in js/ts?
/**
* An interface for all iterable data custom data structures
*/
export default interface Collection {
/**
* Iterates through all of the items in this data structure.
* @param func
* @param func The function to evaluate of every item in the collection
*/
forEach(func: Function): void;

View File

@ -0,0 +1,22 @@
/**
* A linked-list for the edges in a @reference[Graph].
*/
export default class EdgeNode {
/** The node in the Graph this edge connects to */
y: number;
/** The weight of this EdgeNode */
weight: number;
/** The next EdgeNode in the linked-list */
next: EdgeNode;
/**
* Creates a new EdgeNode
* @param index The index of the node this edge connects to
* @param weight The weight of this edge
*/
constructor(index: number, weight?: number){
this.y = index;
this.next = null;
this.weight = weight ? weight : 1;
}
}

View File

@ -1,13 +1,28 @@
import EdgeNode from "./EdgeNode";
export const MAX_V = 100;
/**
* An implementation of a graph data structure using edge lists. Inspired by The Algorithm Design Manual.
*/
export default class Graph {
/** An array of edges at the node specified by the index */
edges: Array<EdgeNode>;
/** An array representing the degree of the node specified by the index */
degree: Array<number>;
/** The number of vertices in the graph */
numVertices: number;
/** The number of edges in the graph */
numEdges: number;
/** Whether or not the graph is directed */
directed: boolean;
/** Whether or not the graph is weighted */
weighted: boolean;
/**
* Constructs a new graph
* @param directed Whether or not this graph is directed
*/
constructor(directed: boolean = false){
this.directed = directed;
this.weighted = false;
@ -19,12 +34,20 @@ export default class Graph {
this.degree = new Array(MAX_V);
}
/** Adds a node to this graph and returns the index of it
* @returns The index of the new node
*/
addNode(): number {
this.numVertices++;
return this.numVertices;
}
addEdge(x: number, y: number, weight?: number){
/** Adds an edge between node x and y, with an optional weight
* @param x The index of the start of the edge
* @param y The index of the end of the edge
* @param weight The optional weight of the new edge
*/
addEdge(x: number, y: number, weight?: number): void {
let edge = new EdgeNode(y, weight);
if(this.edges[x]){
@ -46,18 +69,36 @@ export default class Graph {
this.numEdges += 1;
}
/**
* Gets the edge list associated with node x
* @param x The index of the node
* @returns The head of a linked-list of edges
*/
getEdges(x: number): EdgeNode {
return this.edges[x];
}
/**
* Gets the degree associated with node x
* @param x The index of the node
*/
getDegree(x: number): number {
return this.degree[x];
}
/**
* Converts the specifed node into a string
* @param index The index of the node to convert to a string
* @returns The string representation of the node: "Node x"
*/
protected nodeToString(index: number): string {
return "Node " + index;
}
/**
* Converts the Graph into a string format
* @returns The graph as a string
*/
toString(): string {
let retval = "";
@ -82,15 +123,3 @@ export default class Graph {
return retval;
}
}
export class EdgeNode {
y: number;
next: EdgeNode;
weight: number;
constructor(index: number, weight?: number){
this.y = index;
this.next = null;
this.weight = weight ? weight : 1;
}
}

View File

@ -2,27 +2,74 @@ import Graph, { MAX_V } from "./Graph";
import Vec2 from "../Vec2";
import { DebugRenderable } from "../Interfaces/Descriptors";
/**
* An extension of Graph that has nodes with positions in 2D space.
* This is a weighted graph (though not inherently directd)
*/
export default class PositionGraph extends Graph implements DebugRenderable {
/** An array of the positions of the nodes in this graph */
positions: Array<Vec2>;
/**
* Createes a new PositionGraph
* @param directed Whether or not this graph is directed
*/
constructor(directed: boolean = false){
super(directed);
this.positions = new Array(MAX_V);
}
addPositionedNode(position: Vec2){
/**
* Adds a positioned node to this graph
* @param position The position of the node to add
* @returns The index of the added node
*/
addPositionedNode(position: Vec2): number {
this.positions[this.numVertices] = position;
this.addNode();
return this.addNode();
}
/**
* Changes the position of a node.
* Automatically adjusts the weights of the graph tied to this node.
* As such, be warned that this function has an O(n + m) running time, and use it sparingly.
* @param index The index of the node
* @param position The new position of the node
*/
setNodePosition(index: number, position: Vec2): void {
this.positions[index] = position;
// Recalculate all weights associated with this index
for(let i = 0; i < this.numEdges; i++){
let edge = this.edges[i];
while(edge !== null){
// If this node is on either side of the edge, recalculate weight
if(i === index || edge.y === index){
edge.weight = this.positions[i].distanceTo(this.positions[edge.y]);
}
edge = edge.next;
}
}
}
/**
* Gets the position of a node
* @param index The index of the node
* @returns The position of the node
*/
getNodePosition(index: number): Vec2 {
return this.positions[index];
}
/**
* Adds an edge to this graph between node x and y.
* Automatically calculates the weight of the edge as the distance between the nodes.
* @param x The beginning of the edge
* @param y The end of the edge
*/
addEdge(x: number, y: number): void {
if(!this.positions[x] || !this.positions[y]){
throw "Can't add edge to un-positioned node!";
@ -34,6 +81,7 @@ export default class PositionGraph extends Graph implements DebugRenderable{
super.addEdge(x, y, weight);
}
// @override
protected nodeToString(index: number): string {
return "Node " + index + " - " + this.positions[index].toString();
}

View File

@ -5,6 +5,8 @@ import Vec2 from "../Vec2";
import NavigationPath from "../../Pathfinding/NavigationPath";
import GameNode from "../../Nodes/GameNode";
// @ignorePage
export interface Unique {
/** The unique id of this object. */
id: number;

View File

@ -6,14 +6,15 @@ import Collection from "./Collection";
export default class Map<T> implements Collection {
private map: Record<string, T>;
/** Creates a new map */
constructor(){
this.map = {};
}
/**
* Adds a value T stored at a key.
* @param key
* @param value
* @param key The key of the item to be stored
* @param value The item to be stored
*/
add(key: string, value: T): void {
this.map[key] = value;
@ -21,16 +22,17 @@ export default class Map<T> implements Collection {
/**
* Get the value associated with a key.
* @param key
* @param key The key of the item
* @returns The item at the key or undefined
*/
get(key: string): T {
return this.map[key];
}
/**
* Sets the value stored at key to the new specified value
* @param key
* @param value
* An alias of add. Sets the value stored at key to the new specified value
* @param key The key of the item to be stored
* @param value The item to be stored
*/
set(key: string, value: T): void {
this.add(key, value);
@ -38,7 +40,8 @@ export default class Map<T> implements Collection {
/**
* Returns true if there is a value stored at the specified key, false otherwise.
* @param key
* @param key The key to check
* @returns A boolean representing whether or not there is an item at the given key.
*/
has(key: string): boolean {
return this.map[key] !== undefined;
@ -46,11 +49,13 @@ export default class Map<T> implements Collection {
/**
* Returns an array of all of the keys in this map.
* @returns An array containing all keys in the map.
*/
keys(): Array<string> {
return Object.keys(this.map);
}
// @implemented
forEach(func: (key: string) => void): void {
Object.keys(this.map).forEach(key => func(key));
}
@ -63,10 +68,15 @@ export default class Map<T> implements Collection {
delete this.map[key];
}
// @implemented
clear(): void {
this.forEach(key => delete this.map[key]);
}
/**
* Converts this map to a string representation.
* @returns The string representation of this map.
*/
toString(): string {
let str = "";

View File

@ -0,0 +1,21 @@
import AABB from "../Shapes/AABB";
/**
* A class that contains the area of overlap of two colliding objects to allow for sorting by the physics system.
*/
export default class AreaCollision {
/** The area of the overlap for the colliding objects */
area: number;
/** The AABB of the other collider in this collision */
collider: AABB;
/**
* Creates a new AreaCollision object
* @param area The area of the collision
* @param collider The other collider
*/
constructor(area: number, collider: AABB){
this.area = area;
this.collider = collider;
}
}

View File

@ -0,0 +1,18 @@
import Vec2 from "../Vec2";
/**
* An object representing the data collected from a physics hit between two geometric objects.
* Inspired by the helpful collision documentation @link(here)(https://noonat.github.io/intersect/).
*/
export default class Hit {
/** The time of the collision. Only numbers 0 through 1 happen in this frame. */
time: number;
/** The near times of the collision */
nearTimes: Vec2 = Vec2.ZERO;
/** The position of the collision */
pos: Vec2 = Vec2.ZERO;
/** The overlap distance of the hit */
delta: Vec2 = Vec2.ZERO;
/** The normal vector of the hit */
normal: Vec2 = Vec2.ZERO;
}

View File

@ -5,6 +5,8 @@ import { Positioned } from "./Interfaces/Descriptors";
// TODO - Make max depth
// @ignorePage
/**
* Primarily used to organize the scene graph
*/

View File

@ -4,12 +4,25 @@ import Collection from "./Collection";
* A FIFO queue with elements of type T
*/
export default class Queue<T> implements Collection {
/** The maximum number of elements in the Queue */
private readonly MAX_ELEMENTS: number;
/** The internal representation of the queue */
private q: Array<T>;
/** The head of the queue */
private head: number;
/** The tail of the queue */
private tail: number;
/** The current number of items in the queue */
private size: number;
/**
* Constructs a new queue
* @param maxElements The maximum size of the stack
*/
constructor(maxElements: number = 100){
this.MAX_ELEMENTS = maxElements;
this.q = new Array(this.MAX_ELEMENTS);
@ -20,7 +33,7 @@ export default class Queue<T> implements Collection {
/**
* Adds an item to the back of the queue
* @param item
* @param item The item to add to the back of the queue
*/
enqueue(item: T): void{
if((this.tail + 1) % this.MAX_ELEMENTS === this.head){
@ -34,6 +47,7 @@ export default class Queue<T> implements Collection {
/**
* Retrieves an item from the front of the queue
* @returns The item at the front of the queue
*/
dequeue(): T {
if(this.head === this.tail){
@ -51,7 +65,8 @@ export default class Queue<T> implements Collection {
}
/**
* Returns the item at the front of the queue, but does not return it
* Returns the item at the front of the queue, but does not remove it
* @returns The item at the front of the queue
*/
peekNext(): T {
if(this.head === this.tail){
@ -65,6 +80,7 @@ export default class Queue<T> implements Collection {
/**
* Returns true if the queue has items in it, false otherwise
* @returns A boolean representing whether or not this queue has items
*/
hasItems(): boolean {
return this.head !== this.tail;
@ -72,17 +88,20 @@ export default class Queue<T> implements Collection {
/**
* Returns the number of elements in the queue.
* @returns The size of the queue
*/
getSize(): number {
return this.size;
}
// @implemented
clear(): void {
this.forEach((item, index) => delete this.q[index]);
this.size = 0;
this.head = this.tail;
}
// @implemented
forEach(func: (item: T, index?: number) => void): void {
let i = this.head;
while(i !== this.tail){

View File

@ -6,38 +6,34 @@ import Map from "./Map";
import Stats from "../Debug/Stats";
/**
* Primarily used to organize the scene graph
* A quadtree data structure implemented to work with regions rather than points.
* Elements in this quadtree have a position and an area, and thus can span multiple
* quadtree branches.
*/
export default class QuadTree<T extends Region & Unique> implements Collection {
/**
* The center of this quadtree
*/
/** The center of this quadtree */
protected boundary: AABB;
/**
* The number of elements this quadtree root can hold before splitting
*/
/** The number of elements this quadtree root can hold before splitting */
protected capacity: number;
/**
* The maximum height of the quadtree from this root
*/
/** The maximum height of the quadtree from this root */
protected maxDepth: number;
/**
* Represents whether the quadtree is a root or a leaf
*/
/** Represents whether the quadtree is a root or a leaf */
protected divided: boolean;
/**
* The array of the items in this quadtree
*/
/** The array of the items in this quadtree */
protected items: Array<T>;
// The child quadtrees of this one
/** The top left child */
protected nw: QuadTree<T>;
/** The top right child */
protected ne: QuadTree<T>;
/** The bottom left child */
protected sw: QuadTree<T>;
/** The bottom right child */
protected se: QuadTree<T>;
constructor(center: Vec2, size: Vec2, maxDepth?: number, capacity?: number){
@ -92,6 +88,7 @@ export default class QuadTree<T extends Region & Unique> implements Collection {
/**
* Returns all items at this point.
* @param point The point to query at
* @returns A list of all elements in the quadtree that contain the specified point
*/
queryPoint(point: Vec2): Array<T> {
// A matrix to keep track of our results
@ -105,6 +102,7 @@ export default class QuadTree<T extends Region & Unique> implements Collection {
return results;
}
// @ignoreFunction
/**
* A recursive function called by queryPoint
* @param point The point being queried
@ -139,6 +137,7 @@ export default class QuadTree<T extends Region & Unique> implements Collection {
* Returns all items in this region
* @param boundary The region to check
* @param inclusionCheck Allows for additional inclusion checks to further refine searches
* @returns A list of all elements in the specified region
*/
queryRegion(boundary: AABB): Array<T> {
// A matrix to keep track of our results
@ -152,6 +151,7 @@ export default class QuadTree<T extends Region & Unique> implements Collection {
return results;
}
// @ignoreFunction
/**
* A recursive function called by queryPoint
* @param point The point being queried
@ -213,7 +213,7 @@ export default class QuadTree<T extends Region & Unique> implements Collection {
/**
* Defers this insertion to the children of this quadtree
* @param item
* @param item The item to insert
*/
protected deferInsert(item: T): void {
this.nw.insert(item);
@ -222,10 +222,6 @@ export default class QuadTree<T extends Region & Unique> implements Collection {
this.se.insert(item);
}
/**
* Renders the quadtree for demo purposes.
* @param ctx
*/
public render_demo(ctx: CanvasRenderingContext2D, origin: Vec2, zoom: number): void {
ctx.strokeStyle = "#0000FF";
ctx.strokeRect((this.boundary.x - this.boundary.hw - origin.x)*zoom, (this.boundary.y - this.boundary.hh - origin.y)*zoom, 2*this.boundary.hw*zoom, 2*this.boundary.hh*zoom);
@ -238,6 +234,7 @@ export default class QuadTree<T extends Region & Unique> implements Collection {
}
}
// @implemented
forEach(func: Function): void {
// If divided, send the call down
if(this.divided){
@ -253,9 +250,7 @@ export default class QuadTree<T extends Region & Unique> implements Collection {
}
}
/**
* Clear the items in this quadtree
*/
// @implemented
clear(): void {
if(this.nw){
this.nw.clear();
@ -271,7 +266,6 @@ export default class QuadTree<T extends Region & Unique> implements Collection {
this.items.length = 0;
this.divided = false;
}
}

View File

@ -2,77 +2,64 @@ import Shape from "./Shape";
import Vec2 from "../Vec2";
import MathUtils from "../../Utils/MathUtils";
import Circle from "./Circle";
import Debug from "../../Debug/Debug";
import Hit from "../Physics/Hit";
/**
* An Axis-Aligned Bounding Box. In other words, a rectangle that is always aligned to the x-y grid.
* Inspired by the helpful collision documentation @link(here)(https://noonat.github.io/intersect/).
*/
export default class AABB extends Shape {
center: Vec2;
halfSize: Vec2;
/**
* Creates a new AABB
* @param center The center of the AABB
* @param halfSize The half size of the AABB - The distance from the center to an edge in x and y
*/
constructor(center?: Vec2, halfSize?: Vec2){
super();
this.center = center ? center : new Vec2(0, 0);
this.halfSize = halfSize ? halfSize : new Vec2(0, 0);
}
get x(): number {
return this.center.x;
}
get y(): number {
return this.center.y;
}
get hw(): number {
return this.halfSize.x;
}
get hh(): number {
return this.halfSize.y;
}
get top(): number {
return this.y - this.hh;
}
get bottom(): number {
return this.y + this.hh;
}
get left(): number {
return this.x - this.hw;
}
get right(): number {
return this.x + this.hw;
}
// @override
getBoundingRect(): AABB {
return this.clone();
}
// @override
getBoundingCircle(): Circle {
let r = Math.max(this.hw, this.hh)
return new Circle(this.center.clone(), r);
}
// @deprecated
getHalfSize(): Vec2 {
return this.halfSize;
}
// @deprecated
setHalfSize(halfSize: Vec2): void {
this.halfSize = halfSize;
}
// TODO - move these all to the Shape class
/**
* A simple boolean check of whether this AABB contains a point
* @param point
* @param point The point to check
* @returns A boolean representing whether this AABB contains the specified point
*/
containsPoint(point: Vec2): boolean {
return point.x >= this.x - this.hw && point.x <= this.x + this.hw
&& point.y >= this.y - this.hh && point.y <= this.y + this.hh
}
/**
* A simple boolean check of whether this AABB contains a point
* @param point The point to check
* @returns A boolean representing whether this AABB contains the specified point
*/
intersectPoint(point: Vec2): boolean {
let dx = point.x - this.x;
let px = this.hw - Math.abs(dx);
@ -94,7 +81,8 @@ export default class AABB extends Shape {
/**
* A boolean check of whether this AABB contains a point with soft left and top boundaries.
* In other words, if the top left is (0, 0), the point (0, 0) is not in the AABB
* @param point
* @param point The point to check
* @returns A boolean representing whether this AABB contains the specified point
*/
containsPointSoft(point: Vec2): boolean {
return point.x > this.x - this.hw && point.x <= this.x + this.hw
@ -105,10 +93,9 @@ export default class AABB extends Shape {
/**
* Returns the data from the intersection of this AABB with a line segment from a point in a direction
* @param point The point that the line segment starts from
* @param direction The direction the point will go
* @param distance The length of the line segment, if the direction is a unit vector
* @param paddingX Pads the AABB in the x axis
* @param paddingY Pads the AABB in the y axis
* @param delta The direction and distance of the segment
* @param padding Pads the AABB to make it wider for the intersection test
* @returns The Hit object representing the intersection, or null if there was no intersection
*/
intersectSegment(point: Vec2, delta: Vec2, padding?: Vec2): Hit {
let paddingX = padding ? padding.x : 0;
@ -172,6 +159,7 @@ export default class AABB extends Shape {
return hit;
}
// @override
overlaps(other: Shape): boolean {
if(other instanceof AABB){
return this.overlapsAABB(other);
@ -181,9 +169,10 @@ export default class AABB extends Shape {
/**
* A simple boolean check of whether this AABB overlaps another
* @param other
* @param other The other AABB to check against
* @returns True if this AABB overlaps the other, false otherwise
*/
overlapsAABB(other: AABB): boolean {
protected overlapsAABB(other: AABB): boolean {
let dx = other.x - this.x;
let px = this.hw + other.hw - Math.abs(dx);
@ -201,7 +190,11 @@ export default class AABB extends Shape {
return true;
}
// TODO - Implement this generally and use it in the tilemap
/**
* Calculates the area of the overlap between this AABB and another
* @param other The other AABB
* @returns The area of the overlap between the AABBs
*/
overlapArea(other: AABB): number {
let leftx = Math.max(this.x - this.hw, other.x - other.hw);
let rightx = Math.min(this.x + this.hw, other.x + other.hw);
@ -241,19 +234,16 @@ export default class AABB extends Shape {
this.halfSize.set(centerX - minX, centerY - minY);
}
// @override
clone(): AABB {
return new AABB(this.center.clone(), this.halfSize.clone());
}
/**
* Converts this AABB to a string format
* @returns (center: (x, y), halfSize: (x, y))
*/
toString(): string {
return "(center: " + this.center.toString() + ", half-size: " + this.halfSize.toString() + ")"
}
}
export class Hit {
time: number;
nearTimes: Vec2 = Vec2.ZERO;
pos: Vec2 = Vec2.ZERO;
delta: Vec2 = Vec2.ZERO;
normal: Vec2 = Vec2.ZERO;
}

View File

@ -2,10 +2,18 @@ import Vec2 from "../Vec2";
import AABB from "./AABB";
import Shape from "./Shape";
/**
* A Circle
*/
export default class Circle extends Shape {
private _center: Vec2;
private radius: number;
/**
* Creates a new Circle
* @param center The center of the circle
* @param radius The radius of the circle
*/
constructor(center: Vec2, radius: number) {
super();
this._center = center ? center : new Vec2(0, 0);
@ -24,18 +32,22 @@ export default class Circle extends Shape {
return new Vec2(this.radius, this.radius);
}
// @override
getBoundingRect(): AABB {
return new AABB(this._center.clone(), new Vec2(this.radius, this.radius));
}
// @override
getBoundingCircle(): Circle {
return this.clone();
}
// @override
overlaps(other: Shape): boolean {
throw new Error("Method not implemented.");
}
// @override
clone(): Circle {
return new Circle(this._center.clone(), this.radius);
}

View File

@ -2,6 +2,9 @@ import Vec2 from "../Vec2";
import AABB from "./AABB";
import Circle from "./Circle";
/**
* An abstract Shape class that acts as an interface for better interactions with subclasses.
*/
export default abstract class Shape {
abstract get center(): Vec2;
@ -9,16 +12,63 @@ export default abstract class Shape {
abstract get halfSize(): Vec2;
/** Gets a bounding rectangle for this shape */
get x(): number {
return this.center.x;
}
get y(): number {
return this.center.y;
}
get hw(): number {
return this.halfSize.x;
}
get hh(): number {
return this.halfSize.y;
}
get top(): number {
return this.y - this.hh;
}
get bottom(): number {
return this.y + this.hh;
}
get left(): number {
return this.x - this.hw;
}
get right(): number {
return this.x + this.hw;
}
/**
* Gets a bounding rectangle for this shape. Warning - may be the same as this Shape.
* For instance, the bounding circle of an AABB is itself. Use clone() if you need a new shape.
* @returns An AABB that bounds this shape
*/
abstract getBoundingRect(): AABB;
/** Gets a bounding circle for this shape */
/**
* Gets a bounding circle for this shape. Warning - may be the same as this Shape.
* For instance, the bounding circle of a Circle is itself. Use clone() if you need a new shape.
* @returns A Circle that bounds this shape
*/
abstract getBoundingCircle(): Circle;
/** Returns a copy of this Shape */
/**
* Returns a copy of this Shape
* @returns A new copy of this shape
*/
abstract clone(): Shape;
/** Checks if this shape overlaps another */
/**
* Checks if this shape overlaps another
* @param other The other shape to check against
* @returns a boolean that represents whether this Shape overlaps the other one
*/
abstract overlaps(other: Shape): boolean;
static getTimeOfCollision(A: Shape, velA: Vec2, B: Shape, velB: Vec2): [Vec2, Vec2, boolean, boolean] {

View File

@ -1,11 +1,22 @@
import { AnimationData } from "../Rendering/Animations/AnimationTypes";
/** A class representing data contained in a spritesheet.
* Spritesheets are the images associated with sprites, and contain images indexed in a grid, which
* correspond to animations.
*/
export default class Spritesheet {
/** The name of the spritesheet */
name: string;
/** The image key of the spritesheet */
spriteSheetImage: string;
/** The width of the sprite */
spriteWidth: number;
/** The height of the sprite */
spriteHeight: number;
/** The number of columns in the spritesheet */
columns: number;
/** The number of rows in the spritesheet */
rows: number;
/** An array of the animations associated with this spritesheet */
animations: Array<AnimationData>;
}

View File

@ -4,10 +4,19 @@ import Collection from "./Collection";
* A LIFO stack with items of type T
*/
export default class Stack<T> implements Collection {
readonly MAX_ELEMENTS: number;
/** The maximum number of elements in the Stack */
private readonly MAX_ELEMENTS: number;
/** The internal representation of the stack */
private stack: Array<T>;
/** The head of the stack */
private head: number;
/**
* Constructs a new stack
* @param maxElements The maximum size of the stack
*/
constructor(maxElements: number = 100){
this.MAX_ELEMENTS = maxElements;
this.stack = new Array<T>(this.MAX_ELEMENTS);
@ -28,6 +37,7 @@ export default class Stack<T> implements Collection {
/**
* Removes an item from the top of the stack
* @returns The item at the top of the stack
*/
pop(): T {
if(this.head === -1){
@ -39,6 +49,7 @@ export default class Stack<T> implements Collection {
/**
* Returns the element currently at the top of the stack
* @returns The item at the top of the stack
*/
peek(): T {
if(this.head === -1){
@ -47,11 +58,14 @@ export default class Stack<T> implements Collection {
return this.stack[this.head];
}
/** Returns true if this stack is empty */
/** Returns true if this stack is empty
* @returns A boolean that represents whether or not the stack is empty
*/
isEmpty(): boolean {
return this.head === -1;
}
// @implemented
clear(): void {
this.forEach((item, index) => delete this.stack[index]);
this.head = -1;
@ -59,11 +73,13 @@ export default class Stack<T> implements Collection {
/**
* Returns the number of items currently in the stack
* @returns The number of items in the stack
*/
size(): number {
return this.head + 1;
}
// @implemented
forEach(func: (item: T, index?: number) => void): void{
let i = 0;
while(i <= this.head){
@ -72,6 +88,10 @@ export default class Stack<T> implements Collection {
}
}
/**
* Converts this stack into a string format
* @returns A string representing this stack
*/
toString(): string {
let retval = "";

View File

@ -3,10 +3,21 @@ import GameEvent from "../../Events/GameEvent";
import { Updateable } from "../Interfaces/Descriptors";
import StateMachine from "./StateMachine";
/**
* An abstract implementation of a state for a @reference[StateMachine].
* This class should be extended to allow for custom state behaviors.
*/
export default abstract class State implements Updateable {
/** The StateMachine that uses this State */
protected parent: StateMachine;
/** An event emitter */
protected emitter: Emitter;
/**
* Constructs a new State
* @param parent The parent StateMachine of this state
*/
constructor(parent: StateMachine) {
this.parent = parent;
this.emitter = new Emitter();
@ -18,11 +29,12 @@ export default abstract class State implements Updateable {
abstract onEnter(): void;
/**
* Handles an input event, such as taking damage.
* @param event
* A lifecycle method that handles an input event, such as taking damage.
* @param event The GameEvent to process
*/
abstract handleInput(event: GameEvent): void;
// @implemented
abstract update(deltaT: number): void;
/**
@ -34,7 +46,7 @@ export default abstract class State implements Updateable {
}
/**
* This is called when the state is ending.
* A lifecycle method is called when the state is ending.
*/
abstract onExit(): void;
}

View File

@ -8,18 +8,29 @@ 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.
* for more flexibility, as described in @link(Game Programming Patterns)(https://gameprogrammingpatterns.com/state.html).
*/
export default class StateMachine implements Updateable {
/** A stack of the current states */
protected stack: Stack<State>;
/** A mape of state keys to actual state instances */
protected stateMap: Map<State>;
/** The current state */
protected currentState: State;
/** An event receiver */
protected receiver: Receiver;
/** An event emitter */
protected emitter: Emitter;
/** A boolean representing whether or not this StateMachine is currently active */
protected active: boolean;
/** A boolean representing whether or not this StateMachine should emit an event on state change */
protected emitEventOnStateChange: boolean;
/** The name of the event to be emitted on state change */
protected stateChangeEventName: string;
/**
* Creates a new StateMachine
*/
constructor(){
this.stack = new Stack();
this.stateMap = new Map();
@ -56,7 +67,7 @@ export default class StateMachine implements Updateable {
* 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){
initialize(initialState: string): void {
this.stack.push(this.stateMap.get(initialState));
this.currentState = this.stack.peek();
this.setActive(true);
@ -109,6 +120,7 @@ export default class StateMachine implements Updateable {
this.currentState.handleInput(event);
}
// @implemented
update(deltaT: number): void {
// If the state machine isn't currently active, ignore all events and don't update
if(!this.active){

View File

@ -1,3 +1,4 @@
// @ignorePage
/**
* a representation of Tiled's tilemap data
*/

View File

@ -7,12 +7,19 @@ import { TiledTilesetData } from "./TiledData";
* with a startIndex if required (as it is with Tiled using two images in one tilset).
*/
export default class Tileset {
/** The key of the image used by this tileset */
protected imageKey: string;
/** The size of the tileset image */
protected imageSize: Vec2;
/** The index of 0th image of this tileset */
protected startIndex: number;
/** The index of the last image of this tilset */
protected endIndex: number;
/** The size of the tiles in this tileset */
protected tileSize: Vec2;
/** The number of rows in this tileset */
protected numRows: number;
/** The number of columns in this tileset */
protected numCols: number;
// TODO: Change this to be more general and work with other tileset formats
@ -35,6 +42,10 @@ export default class Tileset {
this.imageSize = new Vec2(tiledData.imagewidth, tiledData.imageheight);
}
/**
* Gets the image key associated with this tilemap
* @returns The image key of this tilemap
*/
getImageKey(): string {
return this.imageKey;
}
@ -42,6 +53,7 @@ export default class Tileset {
/**
* Returns a Vec2 containing the left and top offset from the image origin for this tile.
* @param tileIndex The index of the tile from startIndex to endIndex of this tileset
* @returns A Vec2 containing the offset for the specified tile.
*/
getImageOffsetForTile(tileIndex: number): Vec2 {
// Get the true index
@ -58,18 +70,34 @@ export default class Tileset {
return new Vec2(left, top);
}
/**
* Gets the start index
* @returns The start index
*/
getStartIndex(): number {
return this.startIndex;
}
/**
* Gets the tile set
* @returns A Vec2 containing the tile size
*/
getTileSize(): Vec2 {
return this.tileSize;
}
/**
* Gets the number of rows in the tileset
* @returns The number of rows
*/
getNumRows(): number {
return this.numRows;
}
/**
* Gets the number of columns in the tilset
* @returns The number of columns
*/
getNumCols(): number {
return this.numCols;
}
@ -78,6 +106,11 @@ export default class Tileset {
return this.endIndex - this.startIndex + 1;
}
/**
* Checks whether or not this tilset contains the specified tile index. This is used for rendering.
* @param tileIndex The index of the tile to check
* @returns A boolean representing whether or not this tilset uses the specified index
*/
hasTile(tileIndex: number): boolean {
return tileIndex >= this.startIndex && tileIndex <= this.endIndex;
}

View File

@ -6,7 +6,7 @@ import MathUtils from "../Utils/MathUtils";
export default class Vec2 {
// Store x and y in an array
/** The array that stores the actual vector values */
/** The array that stores the actual vector values x and y */
private vec: Float32Array;
/**
@ -77,21 +77,25 @@ export default class Vec2 {
}
/**
* The squared magnitude of the vector
* The squared magnitude of the vector. This tends to be faster, so use it in situations where taking the
* square root doesn't matter, like for comparing distances.
* @returns The squared magnitude of the vector
*/
magSq(): number {
return this.x*this.x + this.y*this.y;
}
/**
* The magnitude of the vector
* The magnitude of the vector.
* @returns The magnitude of the vector.
*/
mag(): number {
return Math.sqrt(this.magSq());
}
/**
* Returns this vector as a unit vector - Equivalent to dividing x and y by the magnitude
* Divdes x and y by the magnitude to obtain the unit vector in the direction of this vector.
* @returns This vector as a unit vector.
*/
normalize(): Vec2 {
if(this.x === 0 && this.y === 0) return this;
@ -102,7 +106,8 @@ export default class Vec2 {
}
/**
* Returns a new vector that is the normalized version of this one
* Works like normalize(), but returns a new Vec2
* @returns A new vector that is the unit vector for this one
*/
normalized(): Vec2 {
let mag = this.mag();
@ -110,7 +115,8 @@ export default class Vec2 {
}
/**
* Sets the x and y elements of this vector to zero
* Sets the x and y elements of this vector to zero.
* @returns This vector, with x and y set to zero.
*/
zero(): Vec2 {
return this.set(0, 0);
@ -120,6 +126,7 @@ export default class Vec2 {
* Sets the vector's x and y based on the angle provided. Goes counter clockwise.
* @param angle The angle in radians
* @param radius The magnitude of the vector at the specified angle
* @returns This vector.
*/
setToAngle(angle: number, radius: number = 1): Vec2 {
this.x = MathUtils.floorToPlace(Math.cos(angle)*radius, 5);
@ -129,7 +136,8 @@ export default class Vec2 {
/**
* Returns a vector that point from this vector to another one
* @param other
* @param other The vector to point to
* @returns A new Vec2 that points from this vector to the one provided
*/
vecTo(other: Vec2): Vec2 {
return new Vec2(other.x - this.x, other.y - this.y);
@ -137,7 +145,8 @@ export default class Vec2 {
/**
* Returns a vector containing the direction from this vector to another
* @param other
* @param other The vector to point to
* @returns A new Vec2 that points from this vector to the one provided. This new Vec2 will be a unit vector.
*/
dirTo(other: Vec2): Vec2 {
return this.vecTo(other).normalize();
@ -145,7 +154,8 @@ export default class Vec2 {
/**
* Keeps the vector's direction, but sets its magnitude to be the provided magnitude
* @param magnitude
* @param magnitude The magnitude the vector should be
* @returns This vector with its magnitude set to the new magnitude
*/
scaleTo(magnitude: number): Vec2 {
return this.normalize().scale(magnitude);
@ -153,8 +163,9 @@ export default class Vec2 {
/**
* Scales x and y by the number provided, or if two number are provided, scales them individually.
* @param factor
* @param yFactor
* @param factor The scaling factor for the vector, or for only the x-component if yFactor is provided
* @param yFactor The scaling factor for the y-component of the vector
* @returns This vector after scaling
*/
scale(factor: number, yFactor: number = null): Vec2 {
if(yFactor !== null){
@ -169,8 +180,9 @@ export default class Vec2 {
/**
* Returns a scaled version of this vector without modifying it.
* @param factor
* @param yFactor
* @param factor The scaling factor for the vector, or for only the x-component if yFactor is provided
* @param yFactor The scaling factor for the y-component of the vector
* @returns A new vector that has the values of this vector after scaling
*/
scaled(factor: number, yFactor: number = null): Vec2 {
return this.clone().scale(factor, yFactor);
@ -179,6 +191,7 @@ export default class Vec2 {
/**
* Rotates the vector counter-clockwise by the angle amount specified
* @param angle The angle to rotate by in radians
* @returns This vector after rotation.
*/
rotateCCW(angle: number): Vec2 {
let cs = Math.cos(angle);
@ -192,8 +205,9 @@ export default class Vec2 {
/**
* Sets the vectors coordinates to be the ones provided
* @param x
* @param y
* @param x The new x value for this vector
* @param y The new y value for this vector
* @returns This vector
*/
set(x: number, y: number): Vec2 {
this.x = x;
@ -204,6 +218,7 @@ export default class Vec2 {
/**
* Copies the values of the other Vec2 into this one.
* @param other The Vec2 to copy
* @returns This vector with its values set to the vector provided
*/
copy(other: Vec2): Vec2 {
return this.set(other.x, other.y);
@ -211,7 +226,8 @@ export default class Vec2 {
/**
* Adds this vector the another vector
* @param other
* @param other The Vec2 to add to this one
* @returns This vector after adding the one provided
*/
add(other: Vec2): Vec2 {
this.x += other.x;
@ -221,7 +237,8 @@ export default class Vec2 {
/**
* Subtracts another vector from this vector
* @param other
* @param other The Vec2 to subtract from this one
* @returns This vector after subtracting the one provided
*/
sub(other: Vec2): Vec2 {
this.x -= other.x;
@ -230,8 +247,9 @@ export default class Vec2 {
}
/**
* Multiplies this vector with another vector element-wise
* @param other
* Multiplies this vector with another vector element-wise. In other words, this.x *= other.x and this.y *= other.y
* @param other The Vec2 to multiply this one by
* @returns This vector after multiplying its components by this one
*/
mult(other: Vec2): Vec2 {
this.x *= other.x;
@ -240,8 +258,9 @@ export default class Vec2 {
}
/**
* Divides this vector with another vector element-wise
* @param other
* Divides this vector with another vector element-wise. In other words, this.x /= other.x and this.y /= other.y
* @param other The vector to divide this one by
* @returns This vector after division
*/
div(other: Vec2): Vec2 {
if(other.x === 0 || other.y === 0) throw "Divide by zero error";
@ -252,7 +271,8 @@ export default class Vec2 {
/**
* Returns the squared distance between this vector and another vector
* @param other
* @param other The vector to compute distance squared to
* @returns The squared distance between this vector and the one provided
*/
distanceSqTo(other: Vec2): number {
return (this.x - other.x)*(this.x - other.x) + (this.y - other.y)*(this.y - other.y);
@ -260,7 +280,8 @@ export default class Vec2 {
/**
* Returns the distance between this vector and another vector
* @param other
* @param other The vector to compute distance to
* @returns The distance between this vector and the one provided
*/
distanceTo(other: Vec2): number {
return Math.sqrt(this.distanceSqTo(other));
@ -268,7 +289,8 @@ export default class Vec2 {
/**
* Returns the dot product of this vector and another
* @param other
* @param other The vector to compute the dot product with
* @returns The dot product of this vector and the one provided.
*/
dot(other: Vec2): number {
return this.x*other.x + this.y*other.y;
@ -276,7 +298,8 @@ export default class Vec2 {
/**
* Returns the angle counter-clockwise in radians from this vector to another vector
* @param other
* @param other The vector to compute the angle to
* @returns The angle, rotating CCW, from this vector to the other vector
*/
angleToCCW(other: Vec2): number {
let dot = this.dot(other);
@ -292,6 +315,7 @@ export default class Vec2 {
/**
* Returns a string representation of this vector rounded to 1 decimal point
* @returns This vector as a string
*/
toString(): string {
return this.toFixed();
@ -299,7 +323,8 @@ export default class Vec2 {
/**
* Returns a string representation of this vector rounded to the specified number of decimal points
* @param numDecimalPoints
* @param numDecimalPoints The number of decimal points to create a string to
* @returns This vector as a string
*/
toFixed(numDecimalPoints: number = 1): string {
return "(" + this.x.toFixed(numDecimalPoints) + ", " + this.y.toFixed(numDecimalPoints) + ")";
@ -307,6 +332,7 @@ export default class Vec2 {
/**
* Returns a new vector with the same coordinates as this one.
* @returns A new Vec2 with the same values as this one
*/
clone(): Vec2 {
return new Vec2(this.x, this.y);
@ -315,6 +341,7 @@ export default class Vec2 {
/**
* Returns true if this vector and other have the EXACT same x and y (not assured to be safe for floats)
* @param other The vector to check against
* @returns A boolean representing the equality of the two vectors
*/
strictEquals(other: Vec2): boolean {
return this.x === other.x && this.y === other.y;
@ -323,6 +350,7 @@ export default class Vec2 {
/**
* Returns true if this vector and other have the same x and y
* @param other The vector to check against
* @returns A boolean representing the equality of the two vectors
*/
equals(other: Vec2): boolean {
let xEq = Math.abs(this.x - other.x) < 0.0000001;
@ -333,6 +361,7 @@ export default class Vec2 {
/**
* Returns true if this vector is the zero vector exactly (not assured to be safe for floats).
* @returns A boolean representing the equality of this vector and the zero vector
*/
strictIsZero(): boolean {
return this.x === 0 && this.y === 0;
@ -340,6 +369,7 @@ export default class Vec2 {
/**
* Returns true if this x and y for this vector are both zero.
* @returns A boolean representing the equality of this vector and the zero vector
*/
isZero(): boolean {
return Math.abs(this.x) < 0.0000001 && Math.abs(this.y) < 0.0000001;
@ -353,18 +383,12 @@ export default class Vec2 {
this.onChange = f;
}
/**
* Gets the function that is called whenever this vector is changed
*/
getOnChange(): string {
return this.onChange.toString();
}
/**
* Performs linear interpolation between two vectors
* @param a The first vector
* @param b The second vector
* @param t The time of the lerp, with 0 being vector A, and 1 being vector B
* @returns A new Vec2 representing the lerp between vector a and b.
*/
static lerp(a: Vec2, b: Vec2, t: number): Vec2 {
return new Vec2(MathUtils.lerp(a.x, b.x, t), MathUtils.lerp(a.y, b.y, t));

View File

@ -1,5 +1,6 @@
import Vec2 from "./Vec2";
// @ignorePage
export default class Vec4 {
public vec: Float32Array;

View File

@ -1,11 +1,11 @@
import Map from "../DataTypes/Map";
import Vec2 from "../DataTypes/Vec2";
import InputHandler from "../Input/InputHandler";
import GameNode from "../Nodes/GameNode";
import Color from "../Utils/Color";
type DebugRenderFunction = (ctx: CanvasRenderingContext2D) => void;
/**
* A util class for rendering Debug messages to the canvas.
*/
export default class Debug {
/** A map of log messages to display on the screen */
@ -40,7 +40,7 @@ export default class Debug {
/**
* Deletes a a key from the log and stops it from keeping up space on the screen
* @param id
* @param id The id of the log item to clear
*/
static clearLogItem(id: string): void {
this.logMessages.delete(id);
@ -59,6 +59,7 @@ export default class Debug {
* @param center The center of the box
* @param halfSize The dimensions of the box
* @param filled A boolean for whether or not the box is filled
* @param color The color of the box to draw
*/
static drawBox(center: Vec2, halfSize: Vec2, filled: boolean, color: Color): void {
if(filled){
@ -72,6 +73,12 @@ export default class Debug {
}
}
/**
* Draws a ray at the specified position
* @param from The starting position of the ray
* @param to The ending position of the ray
* @param color The color of the ray
*/
static drawRay(from: Vec2, to: Vec2, color: Color): void {
this.debugRenderingContext.lineWidth = 2;
this.debugRenderingContext.strokeStyle = color.toString();
@ -83,16 +90,32 @@ export default class Debug {
this.debugRenderingContext.stroke();
}
/**
* Draws a point at the specified position
* @param pos The position of the point
* @param color The color of the point
*/
static drawPoint(pos: Vec2, color: Color): void {
let pointSize = 6;
this.debugRenderingContext.fillStyle = color.toString();
this.debugRenderingContext.fillRect(pos.x - pointSize/2, pos.y - pointSize/2, pointSize, pointSize);
}
/**
* Sets the default rendering color for text for the debugger
* @param color The color to render the text
*/
static setDefaultTextColor(color: Color): void {
this.defaultTextColor = color;
}
/**
* Performs any necessary setup operations on the Debug canvas
* @param canvas The debug canvas
* @param width The desired width of the canvas
* @param height The desired height of the canvas
* @returns The rendering context extracted from the canvas
*/
static initializeDebugCanvas(canvas: HTMLCanvasElement, width: number, height: number): CanvasRenderingContext2D {
canvas.width = width;
canvas.height = height;
@ -104,15 +127,18 @@ export default class Debug {
return this.debugRenderingContext;
}
/** Clears the debug canvas */
static clearCanvas(): void {
this.debugRenderingContext.clearRect(0, 0, this.debugCanvasSize.x, this.debugCanvasSize.y);
}
/** Renders the text and nodes sent to the Debug system */
static render(): void {
this.renderText();
this.renderNodes();
}
/** Renders the text sent to the Debug canvas */
static renderText(): void {
let y = 20;
this.debugRenderingContext.font = "20px Arial";
@ -125,6 +151,7 @@ export default class Debug {
});
}
/** Renders the nodes registered with the debug canvas */
static renderNodes(): void {
if(this.nodes){
this.nodes.forEach(node => {

View File

@ -1,7 +1,8 @@
import Color from "../Utils/Color";
// @ignorePage
export default class Stats extends Object {
/** The fps of the game. */
// The fps of the game.
private static prevfps: Array<number>;
private static readonly NUM_POINTS: number = 60;
private static ctx: CanvasRenderingContext2D;

View File

@ -2,17 +2,23 @@ import Map from "../DataTypes/Map";
import EventQueue from "./EventQueue";
import GameEvent from "./GameEvent";
/**
* An event emitter object other systems can use to hook into the EventQueue.
* Provides an easy interface for firing off events.
*/
export default class Emitter {
/** A reference to the EventQueue */
private eventQueue: EventQueue;
/** Creates a new Emitter */
constructor(){
this.eventQueue = EventQueue.getInstance();
}
/**
* Emit and event of type eventType with the data packet data
* @param eventType
* @param data
* @param eventType The name of the event to fire off
* @param data A @reference[Map] or record containing any data about the event
*/
fireEvent(eventType: string, data: Map<any> | Record<string, any> = null): void {
this.eventQueue.addEvent(new GameEvent(eventType, data));

View File

@ -4,11 +4,35 @@ import GameEvent from "./GameEvent";
import Receiver from "./Receiver";
import { GameEventType } from "./GameEventType";
/**
* The main event system of the game engine.
* Events are sent to the EventQueue, which handles distribution to any systems that are listening for those events.
* This allows for handling of input without having classes directly hook into javascript event handles,
* and allows otherwise separate classes to communicate with each other cleanly, such as a Player object
* requesting a sound be played by the audio system.
*
* The distribution of @reference[GameEvent]s happens as follows:
*
* Events are recieved throughout a frame and are queued up by the EventQueue.
* At the beginning of the next frame, events are sent out to any receivers that are hooked into the event type.
* @reference[Receiver]s are then free to process events as they see fit.
*
* Overall, the EventQueue can be considered as something similar to an email server,
* and the @reference[Receiver]s can be considered as the client inboxes.
*
* See @link(Game Programming Patterns)(https://gameprogrammingpatterns.com/event-queue.html) for more discussion on EventQueues
*/
export default class EventQueue {
private static instance: EventQueue = null;
/** The maximum number of events visible */
private readonly MAX_SIZE: number;
/** The actual queue of events */
private q: Queue<GameEvent>;
private receivers: Map<Array<Receiver>>
/** The map of receivers registered for an event name */
private receivers: Map<Array<Receiver>>;
private constructor(){
this.MAX_SIZE = 100;
@ -16,6 +40,7 @@ export default class EventQueue {
this.receivers = new Map<Array<Receiver>>();
}
/** Retrieves the instance of the Singleton EventQueue */
static getInstance(): EventQueue {
if(this.instance === null){
this.instance = new EventQueue();
@ -24,14 +49,18 @@ export default class EventQueue {
return this.instance;
}
/** Adds an event to the EventQueue.
* This is exposed to the rest of the game engine through the @reference[Emitter] class */
addEvent(event: GameEvent): void {
this.q.enqueue(event);
}
/**
* Associates a receiver with a type of event. Every time this event appears in the future, it will be given to the receiver (and any others watching that type)
* @param receiver
* @param type
* Associates a receiver with a type of event. Every time this event appears in the future,
* it will be given to the receiver (and any others watching that type).
* This is exposed to the rest of the game engine through the @reference[Receiver] class
* @param receiver The event receiver
* @param type The type or types of events to subscribe to
*/
subscribe(receiver: Receiver, type: string | Array<string>): void {
if(type instanceof Array){

View File

@ -1,13 +1,22 @@
import Map from "../DataTypes/Map"
/**
* A representation of an in-game event
* A representation of an in-game event that is passed through the @reference[EventQueue]
*/
export default class GameEvent {
/** The type of the event */
public type: string;
/** The data contained by the event */
public data: Map<any>;
/** The time of the event in ms */
public time: number;
/**
* Creates a new GameEvent.
* This is handled implicitly through the @reference[Emitter] class
* @param type The type of the GameEvent
* @param data The data contained by the GameEvent
*/
constructor(type: string, data: Map<any> | Record<string, any> = null) {
// Parse the game event data
if (data === null) {
@ -26,10 +35,19 @@ export default class GameEvent {
this.time = Date.now();
}
/**
* Checks the type of the GameEvent
* @param type The type to check
* @returns True if the GameEvent is the specified type, false otherwise.
*/
isType(type: string): boolean {
return this.type === type;
}
/**
* Returns this GameEvent as a string
* @returns The string representation of the GameEvent
*/
toString(): string {
return this.type + ": @" + this.time;
}

View File

@ -1,3 +1,5 @@
// @ignorePage
export enum GameEventType {
/**
* Mouse Down event. Has data: {position: Vec2 - Mouse Position}

View File

@ -3,12 +3,16 @@ import EventQueue from "./EventQueue";
import GameEvent from "./GameEvent";
/**
* Receives subscribed events from the EventQueue
* Receives subscribed events from the EventQueue.
*/
export default class Receiver {
/** The maximum number of events this Receiver can hold at one time */
readonly MAX_SIZE: number;
/** The inbox of the Receiver */
private q: Queue<GameEvent>;
/** Creates a new Receiver */
constructor(){
this.MAX_SIZE = 100;
this.q = new Queue(this.MAX_SIZE);
@ -23,7 +27,8 @@ export default class Receiver{
}
/**
* Adds an event to the queue of this reciever
* Adds an event to the queue of this reciever. This is used by the @reference[EventQueue] to distribute events
* @param event The event to receive
*/
receive(event: GameEvent): void {
this.q.enqueue(event);
@ -31,13 +36,15 @@ export default class Receiver{
/**
* Retrieves the next event from the receiver's queue
* @returns The next GameEvent
*/
getNextEvent(): GameEvent {
return this.q.dequeue();
}
/**
* Looks at the next event in the receiver's queue
* Looks at the next event in the receiver's queue, but doesn't remove it from the queue
* @returns The next GameEvent
*/
peekNextEvent(): GameEvent {
return this.q.peekNext()
@ -45,6 +52,7 @@ export default class Receiver{
/**
* Returns true if the receiver has any events in its queue
* @returns True if the receiver has another event, false otherwise
*/
hasNextEvent(): boolean {
return this.q.hasItems();

View File

@ -4,11 +4,15 @@ import GameEvent from "../Events/GameEvent";
import { GameEventType } from "../Events/GameEventType";
/**
* Handles communication with the web browser to receive asynchronous events and send them to the event queue
* Handles communication with the web browser to receive asynchronous events and send them to the @reference[EventQueue]
*/
export default class InputHandler {
private eventQueue: EventQueue;
/**
* Creates a new InputHandler
* @param canvas The game canvas
*/
constructor(canvas: HTMLCanvasElement){
this.eventQueue = EventQueue.getInstance();

View File

@ -7,7 +7,7 @@ import GameEvent from "../Events/GameEvent";
import { GameEventType } from "../Events/GameEventType";
/**
* Receives input events from the event queue and allows for easy access of information about input
* Receives input events from the @reference[EventQueue] and allows for easy access of information about input by other systems
*/
export default class InputReceiver{
private static instance: InputReceiver = null;
@ -45,6 +45,10 @@ export default class InputReceiver{
GameEventType.KEY_DOWN, GameEventType.KEY_UP, GameEventType.CANVAS_BLUR, GameEventType.WHEEL_UP, GameEventType.WHEEL_DOWN]);
}
/**
* Gets the statc instance of the Singleton InputReceiver
* @returns The InputReceiver instance
*/
static getInstance(): InputReceiver{
if(this.instance === null){
this.instance = new InputReceiver();
@ -117,6 +121,12 @@ export default class InputReceiver{
this.keyPressed.forEach((key: string) => this.keyPressed.set(key, false));
}
/**
* Returns whether or not a key was newly pressed this frame.
* If the key is still pressed from last frame and wasn't re-pressed, this will return false.
* @param key The key
* @returns True if the key was just pressed, false otherwise
*/
isJustPressed(key: string): boolean {
if(this.keyJustPressed.has(key)){
return this.keyJustPressed.get(key)
@ -125,6 +135,11 @@ export default class InputReceiver{
}
}
/**
* Returns an array of all of the keys that are newly pressed this frame.
* If a key is still pressed from last frame and wasn't re-pressed, it will not be in this list.
* @returns An array of all of the newly pressed keys.
*/
getKeysJustPressed(): Array<string> {
let keys = Array<string>();
this.keyJustPressed.forEach(key => {
@ -135,6 +150,11 @@ export default class InputReceiver{
return keys;
}
/**
* Returns whether or not a key is being pressed.
* @param key The key
* @returns True if the key is currently pressed, false otherwise
*/
isPressed(key: string): boolean {
if(this.keyPressed.has(key)){
return this.keyPressed.get(key)
@ -143,38 +163,76 @@ export default class InputReceiver{
}
}
/**
* Returns whether or not the mouse was newly pressed this frame
* @returns True if the mouse was just pressed, false otherwise
*/
isMouseJustPressed(): boolean {
return this.mouseJustPressed;
}
/**
* Returns whether or not the mouse is currently pressed
* @returns True if the mouse is currently pressed, false otherwise
*/
isMousePressed(): boolean {
return this.mousePressed;
}
/**
* Returns whether the user scrolled or not
* @returns True if the user just scrolled this frame, false otherwise
*/
didJustScroll(): boolean {
return this.justScrolled;
}
/**
* Gets the direction of the scroll
* @returns -1 if the user scrolled up, 1 if they scrolled down
*/
getScrollDirection(): number {
return this.scrollDirection;
}
/**
* Gets the position of the player's mouse
* @returns The mouse position stored as a Vec2
*/
getMousePosition(): Vec2 {
return this.mousePosition;
}
/**
* Gets the position of the player's mouse in the game world,
* taking into consideration the scrolling of the viewport
* @returns The mouse position stored as a Vec2
*/
getGlobalMousePosition(): Vec2 {
return this.mousePosition.clone().add(this.viewport.getOrigin());
}
/**
* Gets the position of the last mouse press
* @returns The mouse position stored as a Vec2
*/
getMousePressPosition(): Vec2 {
return this.mousePressPosition;
}
/**
* Gets the position of the last mouse press in the game world,
* taking into consideration the scrolling of the viewport
* @returns The mouse position stored as a Vec2
*/
getGlobalMousePressPosition(): Vec2 {
return this.mousePressPosition.clone().add(this.viewport.getOrigin());
}
/**
* Gives the input receiver a reference to the viewport
* @param viewport The viewport
*/
setViewport(viewport: Viewport): void {
this.viewport = viewport;
}

View File

@ -8,11 +8,16 @@ import Viewport from "../SceneGraph/Viewport";
import SceneManager from "../Scene/SceneManager";
import AudioManager from "../Sound/AudioManager";
import Stats from "../Debug/Stats";
import ArrayUtils from "../Utils/ArrayUtils";
import RenderingManager from "../Rendering/RenderingManager";
import CanvasRenderer from "../Rendering/CanvasRenderer";
import Color from "../Utils/Color";
import GameOptions from "./GameOptions";
/**
* The main loop of the game engine.
* Handles the update order, and initializes all subsystems.
* The GameLoop manages the update cycle, and requests animation frames to render to the browser.
*/
export default class GameLoop {
gameOptions: GameOptions;
@ -77,6 +82,10 @@ export default class GameLoop {
private audioManager: AudioManager;
private renderingManager: RenderingManager;
/**
* Creates a new GameLoop
* @param options The options for GameLoop initialization
*/
constructor(options?: Record<string, any>){
// Typecast the config object to a GameConfig object
this.gameOptions = GameOptions.parse(options);
@ -148,24 +157,32 @@ export default class GameLoop {
/**
* Changes the maximum allowed physics framerate of the game
* @param initMax
* @param initMax The max framerate
*/
setMaxUpdateFPS(initMax: number): void {
this.maxUpdateFPS = initMax;
this.simulationTimestep = Math.floor(1000/this.maxUpdateFPS);
}
/**
* Sets the maximum rendering framerate
* @param maxFPS The max framerate
*/
setMaxFPS(maxFPS: number): void {
this.minFrameDelay = 1000/maxFPS;
}
/**
* Retreives the SceneManager from the GameLoop
* @returns The SceneManager
*/
getSceneManager(): SceneManager {
return this.sceneManager;
}
/**
* Updates the frame count and sum of time for the framerate of the game
* @param timestep
* @param timestep The current time in ms
*/
private updateFPS(timestamp: number): void {
this.fps = 0.9 * this.framesSinceLastFpsUpdate * 1000 / (timestamp - this.lastFpsUpdate) +(1 - 0.9) * this.fps;
@ -189,9 +206,9 @@ export default class GameLoop {
/**
* The first game frame - initializes the first frame time and begins the render
* @param timestamp
* @param timestamp The current time in ms
*/
startFrame = (timestamp: number): void => {
startFrame(timestamp: number): void {
this.running = true;
this.render();
@ -207,7 +224,7 @@ export default class GameLoop {
* The main loop of the game. Updates and renders every frame
* @param timestamp
*/
doFrame = (timestamp: number): void => {
doFrame(timestamp: number): void {
// Request animation frame to prepare for another update or render
window.requestAnimationFrame(this.doFrame);
@ -250,6 +267,9 @@ export default class GameLoop {
this.panic = false;
}
/**
* Ends the game loop
*/
end(){
if(this.panic) {
var discardedTime = Math.round(this.resetFrameDelta());
@ -265,7 +285,7 @@ export default class GameLoop {
/**
* Updates all necessary subsystems of the game. Defers scene updates to the sceneManager
* @param deltaT
* @param deltaT The time sine the last update
*/
update(deltaT: number): void {
// Handle all events that happened since the start of the last loop
@ -288,7 +308,7 @@ export default class GameLoop {
}
/**
* Clears the canvas and defers scene rendering to the sceneManager. Renders the debug
* Clears the canvas and defers scene rendering to the sceneManager. Renders the debug canvas
*/
render(): void {
// Clear the canvases
@ -305,17 +325,3 @@ export default class GameLoop {
Stats.render();
}
}
class GameOptions {
viewportSize: {x: number, y: number};
clearColor: {r: number, g: number, b: number}
static parse(options: Record<string, any>): GameOptions {
let gOpt = new GameOptions();
gOpt.viewportSize = options.viewportSize ? options.viewportSize : {x: 800, y: 600};
gOpt.clearColor = options.clearColor ? options.clearColor : {r: 255, g: 255, b: 255};
return gOpt;
}
}

24
src/Loop/GameOptions.ts Normal file
View File

@ -0,0 +1,24 @@
// @ignorePage
/** The options for initializing the @reference[GameLoop] */
export default class GameOptions {
/** The size of the viewport */
viewportSize: {x: number, y: number};
/** The color to clear the canvas to each frame */
clearColor: {r: number, g: number, b: number}
/**
* Parses the data in the raw options object
* @param options The game options as a Record
* @returns A version of the options converted to a GameOptions object
*/
static parse(options: Record<string, any>): GameOptions {
let gOpt = new GameOptions();
gOpt.viewportSize = options.viewportSize ? options.viewportSize : {x: 800, y: 600};
gOpt.clearColor = options.clearColor ? options.clearColor : {r: 255, g: 255, b: 255};
return gOpt;
}
}

View File

@ -73,7 +73,7 @@ export default abstract class Tilemap extends CanvasNode {
/**
* Adds this tilemap to the physics system
*/
addPhysics = (): void => {
addPhysics(): void {
this.scene.getPhysicsManager().registerTilemap(this);
}
@ -99,9 +99,9 @@ export default abstract class Tilemap extends CanvasNode {
abstract getTile(index: number): number;
/**
* Sets the value of the tile at the specified index
* @param index
* @param type
* Sets the tile at the specified index
* @param index The index of the tile
* @param type The new data value of the tile
*/
abstract setTile(index: number, type: number): void;

View File

@ -101,11 +101,7 @@ export default class OrthogonalTilemap extends Tilemap {
return this.data[index];
}
/**
* Sets the tile at the specified index
* @param index The index of the tile
* @param type The new data value of the tile
*/
// @override
setTile(index: number, type: number): void {
this.data[index] = type;
}

View File

@ -46,7 +46,7 @@ export default class Label extends UIElement{
* Gets a string containg the font details for rendering
* @returns A string containing the font details
*/
protected getFontString(): string {
getFontString(): string {
return this.fontSize + "px " + this.font;
}

View File

@ -12,7 +12,7 @@ export default class Slider extends UIElement {
/** The color of the slider track */
public sliderColor: Color;
/** The reaction of this UIElement to a value change */
public onValueChange: (value: number) => void;
public onValueChange: Function;
/** The event propagated by this UIElement when value changes */
public onValueChangeEventId: string;

View File

@ -3,18 +3,35 @@ import Map from "../DataTypes/Map";
import Vec2 from "../DataTypes/Vec2";
import NavigationPath from "./NavigationPath";
/**
* The manager class for navigation.
* Handles all navigable entities, such and allows them to be accessed by outside systems by requesting a path
* from one position to another.
*/
export default class NavigationManager {
/** The list of all navigable entities */
protected navigableEntities: Map<Navigable>;
constructor(){
this.navigableEntities = new Map();
}
addNavigableEntity(navName: string, nav: Navigable){
/**
* Adds a navigable entity to the NavigationManager
* @param navName The name of the navigable entitry
* @param nav The actual Navigable instance
*/
addNavigableEntity(navName: string, nav: Navigable): void {
this.navigableEntities.add(navName, nav);
}
/**
* Gets a path frome one point to another using a specified Navigable object
* @param navName The name of the registered Navigable object
* @param fromPosition The starting position of navigation
* @param toPosition The ending position of Navigation
* @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 {
let nav = this.navigableEntities.get(navName);
return nav.getNavigationPath(fromPosition.clone(), toPosition.clone());

View File

@ -6,10 +6,17 @@ import GameNode from "../Nodes/GameNode";
* A path that AIs can follow. Uses finishMove() in Physical to determine progress on the route
*/
export default class NavigationPath {
protected path: Stack<Vec2>
/** The navigation path, stored as a stack of next positions */
protected path: Stack<Vec2>;
/** The current direction of movement */
protected currentMoveDirection: Vec2;
/** The distance a node must be to a point to consider it as having arrived */
protected distanceThreshold: number;
/**
* Constructs a new NavigationPath
* @param path The path of nodes to take
*/
constructor(path: Stack<Vec2>){
this.path = path;
console.log(path.toString())
@ -17,15 +24,28 @@ export default class NavigationPath {
this.distanceThreshold = 20;
}
/**
* Returns the status of navigation along this NavigationPath
* @returns True if the node has reached the end of the path, false otherwise
*/
isDone(): boolean {
return this.path.isEmpty();
}
/**
* Gets the movement direction in the current position along the path
* @param node The node to move along the path
* @returns The movement direction as a Vec2
*/
getMoveDirection(node: GameNode): Vec2 {
// Return direction to next point in the nav
return node.position.dirTo(this.path.peek());
}
/**
* Updates this NavigationPath to the current state of the GameNode
* @param node The node moving along the path
*/
handlePathProgress(node: GameNode): void {
if(node.position.distanceSqTo(this.path.peek()) < this.distanceThreshold*this.distanceThreshold){
// We've reached our node, move on to the next destination

View File

@ -5,13 +5,22 @@ import Vec2 from "../DataTypes/Vec2";
import GraphUtils from "../Utils/GraphUtils";
import NavigationPath from "./NavigationPath";
/**
* An implementation of a Navmesh. Navmeshes are graphs in the game world along which nodes can move.
*/
export default class Navmesh implements Navigable {
/** The graph of points in the NavMesh */
protected graph: PositionGraph;
/**
* Creates a new Navmesh from the points in the speecified graph
* @param graph The graph to construct a navmesh from
*/
constructor(graph: PositionGraph){
this.graph = graph;
}
// @implemented
getNavigationPath(fromPosition: Vec2, toPosition: Vec2): NavigationPath {
let start = this.getClosestNode(fromPosition);
let end = this.getClosestNode(toPosition);
@ -34,7 +43,12 @@ export default class Navmesh implements Navigable {
return new NavigationPath(pathStack);
}
getClosestNode(position: Vec2): number {
/**
* Gets the closest node in this Navmesh to the specified position
* @param position The position to query
* @returns The index of the closest node in the Navmesh to the position
*/
protected getClosestNode(position: Vec2): number {
let n = this.graph.numVertices;
let i = 1;
let index = 0;

View File

@ -11,6 +11,8 @@ import OrthogonalTilemap from "../Nodes/Tilemaps/OrthogonalTilemap";
import AABB from "../DataTypes/Shapes/AABB";
import Debug from "../Debug/Debug";
// @ignorePage
export default class BasicPhysicsManager extends PhysicsManager {
/** The array of static nodes */

View File

@ -1,6 +1,8 @@
import { Physical } from "../../DataTypes/Interfaces/Descriptors";
import GameNode from "../../Nodes/GameNode";
// @ignorePage
export default abstract class BroadPhase {
/**
* Runs the algorithm and returns an array of possible collision pairs.

View File

@ -3,6 +3,8 @@ import PhysicsUtils from "../../Utils/PhysicsUtils";
import SortingUtils from "../../Utils/SortingUtils";
import BroadPhase from "./BroadPhase";
// @ignorePage
export default class SweepAndPrune extends BroadPhase {
protected xList: Array<Physical>;
protected yList: Array<Physical>;

View File

@ -1,6 +1,8 @@
import { Physical } from "../DataTypes/Interfaces/Descriptors";
import Vec2 from "../DataTypes/Vec2";
// @ignorePage
export class Collision {
firstContact: Vec2;
lastContact: Vec2;

View File

@ -6,14 +6,20 @@ import Receiver from "../Events/Receiver";
import Emitter from "../Events/Emitter";
import Map from "../DataTypes/Map";
/**
* An abstract physics manager.
* This class exposes functions for subclasses to implement that should allow for a working physics system to be created.
*/
export default abstract class PhysicsManager implements Updateable {
/** The event receiver for the physics system */
protected receiver: Receiver;
/** The event emitter for the physics system */
protected emitter: Emitter;
/** Layer names to numbers */
/** Maps layer names to numbers */
protected layerMap: Map<number>;
/** Layer numbers to names */
/** Maps layer numbers to names */
protected layerNames: Array<string>;
constructor(){
@ -27,22 +33,23 @@ export default abstract class PhysicsManager implements Updateable {
/**
* Registers a gamenode with this physics manager
* @param object
* @param object The object to register
*/
abstract registerObject(object: GameNode): void;
/**
* Registers a tilemap with this physics manager
* @param tilemap
* @param tilemap The tilemap to register
*/
abstract registerTilemap(tilemap: Tilemap): void;
/**
* Updates the physics
* @param deltaT
*/
abstract update(deltaT: number): void;
/**
* Sets the physics layer of the GameNode
* @param node The GameNode
* @param layer The layer that the GameNode should be on
*/
setLayer(node: GameNode, layer: string): void {
node.physicsLayer = this.layerMap.get(layer);
}

View File

@ -1,13 +1,46 @@
import GameNode from "../Nodes/GameNode";
import { Physical, Updateable } from "../DataTypes/Interfaces/Descriptors";
import { Physical } from "../DataTypes/Interfaces/Descriptors";
import Tilemap from "../Nodes/Tilemap";
import PhysicsManager from "./PhysicsManager";
import Vec2 from "../DataTypes/Vec2";
import Debug from "../Debug/Debug";
import Color from "../Utils/Color";
import AABB from "../DataTypes/Shapes/AABB";
import OrthogonalTilemap from "../Nodes/Tilemaps/OrthogonalTilemap";
import AreaCollision from "../DataTypes/Physics/AreaCollision";
/**
* ALGORITHM:
* In an effort to keep things simple and working effectively, each dynamic node will resolve its
* collisions considering the rest of the world as static.
*
* Collision detecting will happen first. This can be considered a broad phase, but it is not especially
* efficient, as it does not need to be for this game engine. Every dynamic node is checked against every
* other node for collision area. If collision area is non-zero (meaning the current node sweeps into another),
* it is added to a list of hits.
*
* INITIALIZATION:
* - Physics constants are reset
* - Swept shapes are recalculated. If a node isn't moving, it is skipped.
*
* COLLISION DETECTION:
* - For a node, collision area will be calculated using the swept AABB of the node against every other AABB in a static state
* - These collisions will be sorted by area in descending order
*
* COLLISION RESOLUTION:
* - For each hit, time of collision is calculated using a swept line through the AABB of the static node expanded
* with minkowski sums (discretely, but the concept is there)
* - The collision is resolved based on the near time of the collision (from method of separated axes)
* - X is resolved by near x, Y by near y.
* - There is some fudging to allow for sliding along walls of separate colliders. Sorting by area also helps with this.
* - Corner to corner collisions are resolve to favor x-movement. This is in consideration of platformers, to give
* the player some help with jumps
*
* Pros:
* - Everything happens with a consistent time. There is a distinct before and after for each resolution.
* - No back-tracking needs to be done. Once we resolve a node, it is definitively resolved.
*
* Cons:
* - Nodes that are processed early have movement priority over other nodes. This can lead to some undesirable interactions.
*/
export default class TestPhysicsManager extends PhysicsManager {
/** The array of static nodes */
@ -26,10 +59,7 @@ export default class TestPhysicsManager extends PhysicsManager {
this.tilemaps = new Array();
}
/**
* Add a new physics object to be updated with the physics system
* @param node The node to be added to the physics system
*/
// @override
registerObject(node: GameNode): void {
if(node.isStatic){
// Static and not collidable
@ -40,56 +70,18 @@ export default class TestPhysicsManager extends PhysicsManager {
}
}
/**
* Registers a tilemap with this physics manager
* @param tilemap
*/
// @override
registerTilemap(tilemap: Tilemap): void {
this.tilemaps.push(tilemap);
}
// @override
setLayer(node: GameNode, layer: string): void {
node.physicsLayer = this.layerMap.get(layer);
}
/**
* Updates the physics
* @param deltaT
*/
// @override
update(deltaT: number): void {
/* ALGORITHM:
In an effort to keep things simple and working effectively, each dynamic node will resolve its
collisions considering the rest of the world as static.
Collision detecting will happen first. This can be considered a broad phase, but it is not especially
efficient, as it does not need to be for this game engine. Every dynamic node is checked against every
other node for collision area. If collision area is non-zero (meaning the current node sweeps into another),
it is added to a list of hits.
INITIALIZATION:
- Physics constants are reset
- Swept shapes are recalculated. If a node isn't moving, it is skipped.
COLLISION DETECTION:
- For a node, collision area will be calculated using the swept AABB of the node against every other AABB in a static state
- These collisions will be sorted by area in descending order
COLLISION RESOLUTION:
- For each hit, time of collision is calculated using a swept line through the AABB of the static node expanded
with minkowski sums (discretely, but the concept is there)
- The collision is resolved based on the near time of the collision (from method of separated axes)
- X is resolved by near x, Y by near y.
- There is some fudging to allow for sliding along walls of separate colliders. Sorting by area also helps with this.
- Corner to corner collisions are resolve to favor x-movement. This is in consideration of platformers, to give
the player some help with jumps
Pros:
- Everything happens with a consistent time. There is a distinct before and after for each resolution.
- No back-tracking needs to be done. Once we resolve a node, it is definitively resolved.
Cons:
- Nodes that are processed early have movement priority over other nodes. This can lead to some undesirable interactions.
*/
for(let node of this.dynamicNodes){
/*---------- INITIALIZATION PHASE ----------*/
// Clear frame dependent boolean values for each node
@ -186,7 +178,13 @@ export default class TestPhysicsManager extends PhysicsManager {
}
}
collideWithOrthogonalTilemap(node: Physical, tilemap: OrthogonalTilemap, overlaps: Array<AreaCollision>): void {
/**
* Handles a collision between this node and an orthogonal tilemap
* @param node The node
* @param tilemap The tilemap the node may be colliding with
* @param overlaps The list of overlaps
*/
protected collideWithOrthogonalTilemap(node: Physical, tilemap: OrthogonalTilemap, overlaps: Array<AreaCollision>): void {
// Get the min and max x and y coordinates of the moving node
let min = new Vec2(node.sweptRect.left, node.sweptRect.top);
let max = new Vec2(node.sweptRect.right, node.sweptRect.bottom);
@ -218,12 +216,3 @@ export default class TestPhysicsManager extends PhysicsManager {
}
}
}
class AreaCollision {
area: number;
collider: AABB;
constructor(area: number, collider: AABB){
this.area = area;
this.collider = collider;
}
}

View File

@ -4,6 +4,8 @@ import GameEvent from "../Events/GameEvent";
import EventQueue from "../Events/EventQueue";
import { GameEventType } from "../Events/GameEventType";
// @ignorePage
export default class Recorder {
private receiver: Receiver;
private log: Queue<LogItem>;

View File

@ -3,6 +3,12 @@ import Emitter from "../../Events/Emitter";
import CanvasNode from "../../Nodes/CanvasNode";
import { AnimationData, AnimationState } from "./AnimationTypes";
/**
* An animation manager class for an animated CanvasNode.
* This class keeps track of the possible animations, as well as the current animation state,
* and abstracts all interactions with playing, pausing, and stopping animations as well as
* creating new animations from the CanvasNode.
*/
export default class AnimationManager {
/** The owner of this animation manager */
protected owner: CanvasNode;
@ -40,6 +46,10 @@ export default class AnimationManager {
/** The onEnd event of a pending animation */
protected pendingOnEnd: string;
/**
* Creates a new AnimationManager
* @param owner The owner of the AnimationManager
*/
constructor(owner: CanvasNode){
this.owner = owner;
this.animationState = AnimationState.STOPPED;
@ -61,7 +71,10 @@ export default class AnimationManager {
this.animations.add(key, animation);
}
/** Gets the index specified by the current animation and current frame */
/**
* Gets the index specified by the current animation and current frame
* @returns The index in the current animation
*/
getIndex(): number {
if(this.animations.has(this.currentAnimation)){
return this.animations.get(this.currentAnimation).frames[this.currentFrame].index;
@ -72,6 +85,10 @@ export default class AnimationManager {
}
}
/**
* Retrieves the current animation index and advances the animation frame
* @returns The index of the animation frame
*/
getIndexAndAdvanceAnimation(): number {
// If we aren't playing, we won't be advancing the animation
if(!(this.animationState === AnimationState.PLAYING)){
@ -109,6 +126,7 @@ export default class AnimationManager {
}
}
/** Ends the current animation and fires any necessary events, as well as starting any new animations */
protected endCurrentAnimation(): void {
this.currentFrame = 0;
this.animationState = AnimationState.STOPPED;
@ -144,7 +162,13 @@ export default class AnimationManager {
this.pendingAnimation = null;
}
/** Queues a single animation to be played after the current one. Does NOT stack */
/**
* Queues a single animation to be played after the current one. Does NOT stack.
* Queueing additional animations past 1 will just replace the queued animation
* @param animation The animation to queue
* @param loop Whether or not the loop the queued animation
* @param onEnd The event to fire when the queued animation ends
*/
queue(animation: string, loop: boolean = false, onEnd?: string): void {
this.pendingAnimation = animation;
this.pendingLoop = loop;

View File

@ -1,6 +1,8 @@
import { TweenableProperties } from "../../Nodes/GameNode";
import { EaseFunctionType } from "../../Utils/EaseFunctions";
// @ignorePage
export enum AnimationState {
STOPPED = 0,
PAUSED = 1,

View File

@ -4,11 +4,24 @@ import { AnimationState, TweenData } from "./AnimationTypes";
import EaseFunctions from "../../Utils/EaseFunctions";
import MathUtils from "../../Utils/MathUtils";
/**
* A manager for the tweens of a GameNode.
* Tweens are short animations played by interpolating between two properties using an easing function.
* For a good visual representation of easing functions, check out @link(https://easings.net/)(https://easings.net/).
* Multiple tween can be played at the same time, as long as they don't change the same property.
* This allows for some interesting polishes or animations that may be very difficult to do with sprite work alone
* - especially pixel art (such as rotations or scaling).
*/
export default class TweenManager {
/** The GameNode this TweenManager acts upon */
protected owner: GameNode;
/** The list of created tweens */
protected tweens: Map<TweenData>;
/**
* Creates a new TweenManager
* @param owner The owner of the TweenManager
*/
constructor(owner: GameNode){
this.owner = owner;
this.tweens = new Map();
@ -83,7 +96,7 @@ export default class TweenManager {
/**
* Stops a currently playing tween
* @param key
* @param key The key of the tween
*/
stop(key: string): void {
if(this.tweens.has(key)){

View File

@ -20,6 +20,9 @@ import TextInput from "../Nodes/UIElements/TextInput";
import AnimatedSprite from "../Nodes/Sprites/AnimatedSprite";
import Vec2 from "../DataTypes/Vec2";
/**
* An implementation of the RenderingManager class using CanvasRenderingContext2D.
*/
export default class CanvasRenderer extends RenderingManager {
protected ctx: CanvasRenderingContext2D;
protected graphicRenderer: GraphicRenderer;
@ -33,6 +36,7 @@ export default class CanvasRenderer extends RenderingManager {
super();
}
// @override
setScene(scene: Scene){
this.scene = scene;
this.graphicRenderer.setScene(scene);
@ -40,6 +44,7 @@ export default class CanvasRenderer extends RenderingManager {
this.uiElementRenderer.setScene(scene);
}
// @override
initializeCanvas(canvas: HTMLCanvasElement, width: number, height: number): CanvasRenderingContext2D {
canvas.width = width;
canvas.height = height;
@ -56,6 +61,7 @@ export default class CanvasRenderer extends RenderingManager {
return this.ctx;
}
// @override
render(visibleSet: CanvasNode[], tilemaps: Tilemap[], uiLayers: Map<UILayer>): void {
// Sort by depth, then by visible set by y-value
visibleSet.sort((a, b) => {
@ -104,6 +110,10 @@ export default class CanvasRenderer extends RenderingManager {
uiLayers.forEach(key => uiLayers.get(key).getItems().forEach(node => this.renderNode(<CanvasNode>node)));
}
/**
* Renders a specified CanvasNode
* @param node The CanvasNode to render
*/
protected renderNode(node: CanvasNode): void {
// Calculate the origin of the viewport according to this sprite
this.origin = this.scene.getViewTranslation(node);
@ -139,6 +149,7 @@ export default class CanvasRenderer extends RenderingManager {
this.ctx.setTransform(1, 0, 0, 1, 0, 0);
}
// @override
protected renderSprite(sprite: Sprite): void {
// Get the image from the resource manager
let image = this.resourceManager.getImage(sprite.imageId);
@ -156,16 +167,9 @@ export default class CanvasRenderer extends RenderingManager {
sprite.size.x, sprite.size.y,
(-sprite.size.x*sprite.scale.x/2)*this.zoom, (-sprite.size.y*sprite.scale.y/2)*this.zoom,
sprite.size.x * sprite.scale.x*this.zoom, sprite.size.y * sprite.scale.y*this.zoom);
// Debug mode
if(this.debug){
this.ctx.lineWidth = 4;
this.ctx.strokeStyle = "#00FF00"
let b = sprite.boundary;
this.ctx.strokeRect(-b.hw*this.zoom, -b.hh*this.zoom, b.hw*2*this.zoom, b.hh*2*this.zoom);
}
}
// @override
protected renderAnimatedSprite(sprite: AnimatedSprite): void {
// Get the image from the resource manager
let image = this.resourceManager.getImage(sprite.imageId);
@ -187,16 +191,9 @@ export default class CanvasRenderer extends RenderingManager {
sprite.size.x, sprite.size.y,
(-sprite.size.x*sprite.scale.x/2)*this.zoom, (-sprite.size.y*sprite.scale.y/2)*this.zoom,
sprite.size.x * sprite.scale.x*this.zoom, sprite.size.y * sprite.scale.y*this.zoom);
// Debug mode
if(this.debug){
this.ctx.lineWidth = 4;
this.ctx.strokeStyle = "#00FF00"
let b = sprite.boundary;
this.ctx.strokeRect(-b.hw*this.zoom, -b.hh*this.zoom, b.hw*2*this.zoom, b.hh*2*this.zoom);
}
}
// @override
protected renderGraphic(graphic: Graphic): void {
if(graphic instanceof Point){
this.graphicRenderer.renderPoint(<Point>graphic, this.zoom);
@ -205,12 +202,14 @@ export default class CanvasRenderer extends RenderingManager {
}
}
// @override
protected renderTilemap(tilemap: Tilemap): void {
if(tilemap instanceof OrthogonalTilemap){
this.tilemapRenderer.renderOrthogonalTilemap(<OrthogonalTilemap>tilemap);
}
}
// @override
protected renderUIElement(uiElement: UIElement): void {
if(uiElement instanceof Label){
this.uiElementRenderer.renderLabel(uiElement);

View File

@ -1,12 +1,17 @@
import Vec2 from "../../DataTypes/Vec2";
import Point from "../../Nodes/Graphics/Point";
import Rect from "../../Nodes/Graphics/Rect";
import ResourceManager from "../../ResourceManager/ResourceManager";
import Scene from "../../Scene/Scene";
/**
* A utility class to help the @reference[CanvasRenderer] render @reference[Graphic]s
*/
export default class GraphicRenderer {
/** The resource manager of the game engine */
protected resourceManager: ResourceManager;
/** The current scene */
protected scene: Scene;
/** The rendering context */
protected ctx: CanvasRenderingContext2D;
constructor(ctx: CanvasRenderingContext2D){
@ -14,16 +19,30 @@ export default class GraphicRenderer {
this.ctx = ctx;
}
/**
* Sets the scene of this GraphicRenderer
* @param scene The current scene
*/
setScene(scene: Scene): void {
this.scene = scene;
}
/**
* Renders a point
* @param point The point to render
* @param zoom The zoom level
*/
renderPoint(point: Point, zoom: number): void {
this.ctx.fillStyle = point.color.toStringRGBA();
this.ctx.fillRect((-point.size.x/2)*zoom, (-point.size.y/2)*zoom,
point.size.x*zoom, point.size.y*zoom);
}
/**
* Renders a rect
* @param rect The rect to render
* @param zoom The zoom level
*/
renderRect(rect: Rect, zoom: number): void {
// Draw the interior of the rect
if(rect.color.a !== 0){

View File

@ -4,6 +4,9 @@ import OrthogonalTilemap from "../../Nodes/Tilemaps/OrthogonalTilemap";
import Vec2 from "../../DataTypes/Vec2";
import Tileset from "../../DataTypes/Tilesets/Tileset";
/**
* A utility class for the @reference[CanvasRenderer] to render @reference[Tilemap]s
*/
export default class TilemapRenderer {
protected resourceManager: ResourceManager;
protected scene: Scene;
@ -14,10 +17,18 @@ export default class TilemapRenderer {
this.ctx = ctx;
}
/**
* Sets the scene of this TilemapRenderer
* @param scene The current scene
*/
setScene(scene: Scene): void {
this.scene = scene;
}
/**
* Renders an orthogonal tilemap
* @param tilemap The tilemap to render
*/
renderOrthogonalTilemap(tilemap: OrthogonalTilemap): void {
let previousAlpha = this.ctx.globalAlpha;
this.ctx.globalAlpha = tilemap.getLayer().getAlpha();
@ -49,6 +60,16 @@ export default class TilemapRenderer {
this.ctx.globalAlpha = previousAlpha;
}
/**
* Renders a tile
* @param tileset The tileset this tile belongs to
* @param tileIndex The index of the tile
* @param tilemapRow The row of the tile in the tilemap
* @param tilemapCol The column of the tile in the tilemap
* @param origin The origin of the viewport
* @param scale The scale of the tilemap
* @param zoom The zoom level of the viewport
*/
protected renderTile(tileset: Tileset, tileIndex: number, tilemapRow: number, tilemapCol: number, origin: Vec2, scale: Vec2, zoom: number): void {
let image = this.resourceManager.getImage(tileset.getImageKey());

View File

@ -7,6 +7,9 @@ import ResourceManager from "../../ResourceManager/ResourceManager";
import Scene from "../../Scene/Scene";
import MathUtils from "../../Utils/MathUtils";
/**
* A utility class to help the @reference[CanvasRenderer] render @reference[UIElement]s
*/
export default class UIElementRenderer {
protected resourceManager: ResourceManager;
protected scene: Scene;
@ -17,10 +20,18 @@ export default class UIElementRenderer {
this.ctx = ctx;
}
/**
* Sets the scene of this UIElementRenderer
* @param scene The current scene
*/
setScene(scene: Scene): void {
this.scene = scene;
}
/**
* Renders a label
* @param label The label to render
*/
renderLabel(label: Label): void {
// If the size is unassigned (by the user or automatically) assign it
label.handleInitialSizing(this.ctx);
@ -51,10 +62,18 @@ export default class UIElementRenderer {
this.ctx.globalAlpha = previousAlpha;
}
/**
* Renders a button
* @param button The button to render
*/
renderButton(button: Button): void {
this.renderLabel(button);
}
/**
* Renders a slider
* @param slider The slider to render
*/
renderSlider(slider: Slider): void {
// Grab the global alpha so we can adjust it for this render
let previousAlpha = this.ctx.globalAlpha;
@ -82,6 +101,10 @@ export default class UIElementRenderer {
this.ctx.globalAlpha = previousAlpha;
}
/**
* Renders a textInput
* @param textInput The textInput to render
*/
renderTextInput(textInput: TextInput): void {
// Show a cursor sometimes
if(textInput.focused && textInput.cursorCounter % 60 > 30){

View File

@ -9,32 +9,73 @@ import ResourceManager from "../ResourceManager/ResourceManager";
import UILayer from "../Scene/Layers/UILayer";
import Scene from "../Scene/Scene";
/**
* An abstract framework to put all rendering in once place in the application
*/
export default abstract class RenderingManager {
// Give the renderer access to the resource manager
/** The ResourceManager */
protected resourceManager: ResourceManager;
/** The scene currently being rendered */
protected scene: Scene;
debug: boolean;
constructor(){
this.resourceManager = ResourceManager.getInstance();
this.debug = false;
}
/**
* Sets the scene currently being rendered
* @param scene The current Scene
*/
setScene(scene: Scene): void {
this.scene = scene;
}
/**
* Initialize the canvas for the game
* @param canvas The canvas element
* @param width The desired width of the canvas
* @param height The desired height of the canvas
* @returns The rendering context of the canvas
*/
abstract initializeCanvas(canvas: HTMLCanvasElement, width: number, height: number): any;
/**
* Renders the visible set of CanvasNodes and visible portions of tilemaps, as well as any UIElement in UILayers
* @param visibleSet The visible set of CanvasNodes
* @param tilemaps The tilemaps used in the application
* @param uiLayers The user interface layers
*/
abstract render(visibleSet: Array<CanvasNode>, tilemaps: Array<Tilemap>, uiLayers: Map<UILayer>): void;
/**
* Renders a sprite
* @param sprite The sprite to render
*/
protected abstract renderSprite(sprite: Sprite): void;
/**
* Renders an animated sprite
* @param sprite The animated sprite to render
*/
protected abstract renderAnimatedSprite(sprite: AnimatedSprite): void;
/**
* Renders a graphic
* @param graphic The graphic to render
*/
protected abstract renderGraphic(graphic: Graphic): void;
/**
* Renders a tilemap
* @param tilemap The tilemap to render
*/
protected abstract renderTilemap(tilemap: Tilemap): void;
/**
* Renders a UIElement
* @param uiElement The UIElement to render
*/
protected abstract renderUIElement(uiElement: UIElement): void;
}

View File

@ -5,16 +5,27 @@ import StringUtils from "../Utils/StringUtils";
import AudioManager from "../Sound/AudioManager";
import Spritesheet from "../DataTypes/Spritesheet";
/**
* The resource manager for the game engine.
* The resource manager interfaces with the loadable assets of a game such as images, data files,
* and sounds, which are all found in the dist folder.
* This class controls loading and updates the @reference[Scene] with the loading progress, so that the scene does
* not start before all necessary assets are loaded.
*/
export default class ResourceManager {
// Instance for the singleton class
private static instance: ResourceManager;
// Booleans to keep track of whether or not the ResourceManager is currently loading something
/** Whether or not any resources are loading */
private loading: boolean;
/** A boolean to indicate that the assets just finished loading */
private justLoaded: boolean;
// Functions to do something when loading progresses or is completed such as render a loading screen
/** A function that is called when loading progresses */
public onLoadProgress: Function;
/** A function that is called when loading completes */
public onLoadComplete: Function;
@ -23,7 +34,7 @@ export default class ResourceManager {
/** Number to keep track of how many images are loaded */
private loadonly_imagesToLoad: number;
/** The queue of images we must load */
private loadonly_imageLoadingQueue: Queue<{key: string, path: string}>;
private loadonly_imageLoadingQueue: Queue<KeyPathPair>;
/** A map of the images that are currently loaded and (presumably) being used by the scene */
private images: Map<HTMLImageElement>;
@ -32,7 +43,7 @@ export default class ResourceManager {
/** Number to keep track of how many tilemaps are loaded */
private loadonly_spritesheetsToLoad: number;
/** The queue of tilemaps we must load */
private loadonly_spritesheetLoadingQueue: Queue<{key: string, path: string}>;
private loadonly_spritesheetLoadingQueue: Queue<KeyPathPair>;
/** A map of the tilemaps that are currently loaded and (presumably) being used by the scene */
private spritesheets: Map<Spritesheet>;
@ -41,7 +52,7 @@ export default class ResourceManager {
/** Number to keep track of how many tilemaps are loaded */
private loadonly_tilemapsToLoad: number;
/** The queue of tilemaps we must load */
private loadonly_tilemapLoadingQueue: Queue<{key: string, path: string}>;
private loadonly_tilemapLoadingQueue: Queue<KeyPathPair>;
/** A map of the tilemaps that are currently loaded and (presumably) being used by the scene */
private tilemaps: Map<TiledTilemapData>;
@ -50,7 +61,7 @@ export default class ResourceManager {
/** Number to keep track of how many sounds are loaded */
private loadonly_audioToLoad: number;
/** The queue of sounds we must load */
private loadonly_audioLoadingQueue: Queue<{key: string, path: string}>;
private loadonly_audioLoadingQueue: Queue<KeyPathPair>;
/** A map of the sounds that are currently loaded and (presumably) being used by the scene */
private audioBuffers: Map<AudioBuffer>;
@ -84,6 +95,7 @@ export default class ResourceManager {
/**
* Returns the current instance of this class or a new instance if none exist
* @returns The resource manager
*/
static getInstance(): ResourceManager {
if(!this.instance){
@ -105,6 +117,7 @@ export default class ResourceManager {
/**
* Retrieves a loaded image
* @param key The key of the loaded image
* @returns The image element associated with this key
*/
public getImage(key: string): HTMLImageElement {
let image = this.images.get(key);
@ -114,18 +127,28 @@ export default class ResourceManager {
return image;
}
/**
* Loads a spritesheet from file
* @param key The key to associate the loaded spritesheet with
* @param path The path to the spritesheet to load
*/
public spritesheet(key: string, path: string): void {
this.loadonly_spritesheetLoadingQueue.enqueue({key: key, path: path});
}
/**
* Retrieves a loaded spritesheet
* @param key The key of the spritesheet to load
* @returns The loaded Spritesheet
*/
public getSpritesheet(key: string): Spritesheet {
return this.spritesheets.get(key);
}
/**
* Load an audio file
* @param key
* @param path
* Loads an audio file
* @param key The key to associate with the loaded audio file
* @param path The path to the audio file to load
*/
public audio(key: string, path: string): void {
this.loadonly_audioLoadingQueue.enqueue({key: key, path: path});
@ -133,7 +156,8 @@ export default class ResourceManager {
/**
* Retrieves a loaded audio file
* @param key
* @param key The key of the audio file to load
* @returns The AudioBuffer created from the loaded audio fle
*/
public getAudio(key: string): AudioBuffer {
return this.audioBuffers.get(key);
@ -141,8 +165,8 @@ export default class ResourceManager {
/**
* Load a tilemap from a json file. Automatically loads related images
* @param key
* @param path
* @param key The key to associate with the loaded tilemap
* @param path The path to the tilemap to load
*/
public tilemap(key: string, path: string): void {
this.loadonly_tilemapLoadingQueue.enqueue({key: key, path: path});
@ -150,7 +174,8 @@ export default class ResourceManager {
/**
* Retreives a loaded tilemap
* @param key
* @param key The key of the loaded tilemap
* @returns The tilemap data associated with the key
*/
public getTilemap(key: string): TiledTilemapData {
return this.tilemaps.get(key);
@ -158,7 +183,7 @@ export default class ResourceManager {
/**
* Loads all resources currently in the queue
* @param callback
* @param callback The function to cal when the resources are finished loading
*/
loadResourcesFromQueue(callback: Function): void {
this.loadonly_typesToLoad = 3;
@ -211,7 +236,7 @@ export default class ResourceManager {
/**
* Loads all tilemaps currently in the tilemap loading queue
* @param onFinishLoading
* @param onFinishLoading The function to call when loading is complete
*/
private loadTilemapsFromQueue(onFinishLoading: Function): void {
this.loadonly_tilemapsToLoad = this.loadonly_tilemapLoadingQueue.getSize();
@ -230,9 +255,9 @@ export default class ResourceManager {
/**
* Loads a singular tilemap
* @param key
* @param pathToTilemapJSON
* @param callbackIfLast
* @param key The key of the tilemap
* @param pathToTilemapJSON The path to the tilemap JSON file
* @param callbackIfLast The function to call if this is the last tilemap to load
*/
private loadTilemap(key: string, pathToTilemapJSON: string, callbackIfLast: Function): void {
this.loadTextFile(pathToTilemapJSON, (fileText: string) => {
@ -263,7 +288,7 @@ export default class ResourceManager {
/**
* Finish loading a tilemap. Calls the callback function if this is the last tilemap being loaded
* @param callback
* @param callback The function to call if this is the last tilemap to load
*/
private finishLoadingTilemap(callback: Function): void {
this.loadonly_tilemapsLoaded += 1;
@ -276,7 +301,7 @@ export default class ResourceManager {
/**
* Loads all spritesheets currently in the spritesheet loading queue
* @param onFinishLoading
* @param onFinishLoading The function to call when the spritesheets are done loading
*/
private loadSpritesheetsFromQueue(onFinishLoading: Function): void {
this.loadonly_spritesheetsToLoad = this.loadonly_spritesheetLoadingQueue.getSize();
@ -295,9 +320,9 @@ export default class ResourceManager {
/**
* Loads a singular spritesheet
* @param key
* @param pathToSpritesheetJSON
* @param callbackIfLast
* @param key The key of the spritesheet to load
* @param pathToSpritesheetJSON The path to the spritesheet JSON file
* @param callbackIfLast The function to call if this is the last spritesheet
*/
private loadSpritesheet(key: string, pathToSpritesheetJSON: string, callbackIfLast: Function): void {
this.loadTextFile(pathToSpritesheetJSON, (fileText: string) => {
@ -317,7 +342,7 @@ export default class ResourceManager {
/**
* Finish loading a spritesheet. Calls the callback function if this is the last spritesheet being loaded
* @param callback
* @param callback The function to call if this is the last spritesheet to load
*/
private finishLoadingSpritesheet(callback: Function): void {
this.loadonly_spritesheetsLoaded += 1;
@ -330,7 +355,7 @@ export default class ResourceManager {
/**
* Loads all images currently in the image loading queue
* @param onFinishLoading
* @param onFinishLoading The function to call when there are no more images to load
*/
private loadImagesFromQueue(onFinishLoading: Function): void {
this.loadonly_imagesToLoad = this.loadonly_imageLoadingQueue.getSize();
@ -349,9 +374,9 @@ export default class ResourceManager {
/**
* Loads a singular image
* @param key
* @param path
* @param callbackIfLast
* @param key The key of the image to load
* @param path The path to the image to load
* @param callbackIfLast The function to call if this is the last image
*/
public loadImage(key: string, path: string, callbackIfLast: Function): void {
var image = new Image();
@ -369,7 +394,7 @@ export default class ResourceManager {
/**
* Finish loading an image. If this is the last image, it calls the callback function
* @param callback
* @param callback The function to call if this is the last image
*/
private finishLoadingImage(callback: Function): void {
this.loadonly_imagesLoaded += 1;
@ -382,7 +407,7 @@ export default class ResourceManager {
/**
* Loads all audio currently in the tilemap loading queue
* @param onFinishLoading
* @param onFinishLoading The function to call when tilemaps are done loading
*/
private loadAudioFromQueue(onFinishLoading: Function){
this.loadonly_audioToLoad = this.loadonly_audioLoadingQueue.getSize();
@ -401,9 +426,9 @@ export default class ResourceManager {
/**
* Load a singular audio file
* @param key
* @param path
* @param callbackIfLast
* @param key The key to the audio file to load
* @param path The path to the audio file to load
* @param callbackIfLast The function to call if this is the last audio file to load
*/
private loadAudio(key: string, path: string, callbackIfLast: Function): void {
let audioCtx = AudioManager.getInstance().getAudioContext();
@ -428,7 +453,7 @@ export default class ResourceManager {
/**
* Finish loading an audio file. Calls the callback functon if this is the last audio sample being loaded.
* @param callback
* @param callback The function to call if this is the last audio file to load
*/
private finishLoadingAudio(callback: Function): void {
this.loadonly_audioLoaded += 1;
@ -459,7 +484,7 @@ export default class ResourceManager {
/ this.loadonly_typesToLoad;
}
public update(deltaT: number): void {
update(deltaT: number): void {
if(this.loading){
if(this.onLoadProgress){
this.onLoadProgress(this.getLoadPercent());
@ -472,3 +497,8 @@ export default class ResourceManager {
}
}
}
class KeyPathPair {
key: string
path: string
}

View File

@ -13,9 +13,13 @@ import Slider from "../../Nodes/UIElements/Slider";
import TextInput from "../../Nodes/UIElements/TextInput";
import Rect from "../../Nodes/Graphics/Rect";
import ResourceManager from "../../ResourceManager/ResourceManager";
import UILayer from "../Layers/UILayer";
import ParallaxLayer from "../Layers/ParallaxLayer";
// @ignorePage
/**
* A factory that abstracts adding @reference[CanvasNode]s to the @reference[Scene].
* Access methods in this factory through Scene.add.[methodName]().
*/
export default class CanvasNodeFactory {
protected scene: Scene;
protected resourceManager: ResourceManager;
@ -30,6 +34,7 @@ export default class CanvasNodeFactory {
* @param type The type of UIElement to add
* @param layerName The layer to add the UIElement to
* @param options Any additional arguments to feed to the constructor
* @returns A new UIElement
*/
addUIElement = (type: string | UIElementType, layerName: string, options?: Record<string, any>): UIElement => {
// Get the layer
@ -68,6 +73,7 @@ export default class CanvasNodeFactory {
* Adds a sprite to the current scene
* @param key The key of the image the sprite will represent
* @param layerName The layer on which to add the sprite
* @returns A new Sprite
*/
addSprite = (key: string, layerName: string): Sprite => {
let layer = this.scene.getLayer(layerName);
@ -88,6 +94,12 @@ export default class CanvasNodeFactory {
return instance;
}
/**
* Adds an AnimatedSprite to the current scene
* @param key The key of the image the sprite will represent
* @param layerName The layer on which to add the sprite
* @returns A new AnimatedSprite
*/
addAnimatedSprite = (key: string, layerName: string): AnimatedSprite => {
let layer = this.scene.getLayer(layerName);
let spritesheet = this.resourceManager.getSpritesheet(key);
@ -112,6 +124,7 @@ export default class CanvasNodeFactory {
* @param type The type of graphic to add
* @param layerName The layer on which to add the graphic
* @param options Any additional arguments to send to the graphic constructor
* @returns A new Graphic
*/
addGraphic = (type: GraphicType | string, layerName: string, options?: Record<string, any>): Graphic => {
// Get the layer

View File

@ -2,7 +2,18 @@ import Scene from "../Scene";
import CanvasNodeFactory from "./CanvasNodeFactory";
import TilemapFactory from "./TilemapFactory";
import Tilemap from "../../Nodes/Tilemap";
import { UIElementType } from "../../Nodes/UIElements/UIElementTypes";
import UIElement from "../../Nodes/UIElement";
import Sprite from "../../Nodes/Sprites/Sprite";
import { GraphicType } from "../../Nodes/Graphics/GraphicTypes";
import Graphic from "../../Nodes/Graphic";
import AnimatedSprite from "../../Nodes/Sprites/AnimatedSprite";
import Vec2 from "../../DataTypes/Vec2";
import Layer from "../Layer";
/**
* The manager of all factories used for adding @reference[GameNode]s to the @reference[Scene].
*/
export default class FactoryManager {
// Constructors are called here to allow assignment of their functions to functions in this class
@ -15,9 +26,56 @@ export default class FactoryManager {
}
// Expose all of the factories through the factory manager
uiElement = this.canvasNodeFactory.addUIElement;
sprite = this.canvasNodeFactory.addSprite;
animatedSprite = this.canvasNodeFactory.addAnimatedSprite;
graphic = this.canvasNodeFactory.addGraphic;
tilemap = this.tilemapFactory.add;
/**
* Adds an instance of a UIElement to the current scene - i.e. any class that extends UIElement
* @param type The type of UIElement to add
* @param layerName The layer to add the UIElement to
* @param options Any additional arguments to feed to the constructor
* @returns A new UIElement
*/
uiElement(type: string | UIElementType, layerName: string, options?: Record<string, any>): UIElement {
return this.canvasNodeFactory.addUIElement(type, layerName, options);
}
/**
* Adds a sprite to the current scene
* @param key The key of the image the sprite will represent
* @param layerName The layer on which to add the sprite
* @returns A new Sprite
*/
sprite(key: string, layerName: string): Sprite {
return this.canvasNodeFactory.addSprite(key, layerName);
}
/**
* Adds an AnimatedSprite to the current scene
* @param key The key of the image the sprite will represent
* @param layerName The layer on which to add the sprite
* @returns A new AnimatedSprite
*/
animatedSprite(key: string, layerName: string): AnimatedSprite {
return this.canvasNodeFactory.addAnimatedSprite(key, layerName);
}
/**
* Adds a new graphic element to the current Scene
* @param type The type of graphic to add
* @param layerName The layer on which to add the graphic
* @param options Any additional arguments to send to the graphic constructor
* @returns A new Graphic
*/
graphic(type: GraphicType | string, layerName: string, options?: Record<string, any>): Graphic {
return this.canvasNodeFactory.addGraphic(type, layerName, options);
}
/**
* Adds a tilemap to the scene
* @param key The key of the loaded tilemap to load
* @param constr The constructor of the desired tilemap
* @param args Additional arguments to send to the tilemap constructor
* @returns An array of Layers, each of which contains a layer of the tilemap as its own Tilemap instance.
*/
tilemap(key: string, scale?: Vec2): Array<Layer> {
return this.tilemapFactory.add(key, scale);
}
}

View File

@ -10,6 +10,12 @@ import Sprite from "../../Nodes/Sprites/Sprite";
import PositionGraph from "../../DataTypes/Graphs/PositionGraph";
import Navmesh from "../../Pathfinding/Navmesh";
// @ignorePage
/**
* A factory that abstracts adding @reference[Tilemap]s to the @reference[Scene].
* Access methods in this factory through Scene.add.[methodName]().
*/
export default class TilemapFactory {
private scene: Scene;
private tilemaps: Array<Tilemap>;
@ -30,6 +36,7 @@ export default class TilemapFactory {
* @param key The key of the loaded tilemap to load
* @param constr The constructor of the desired tilemap
* @param args Additional arguments to send to the tilemap constructor
* @returns An array of Layers, each of which contains a layer of the tilemap as its own Tilemap instance.
*/
add = (key: string, scale: Vec2 = new Vec2(1, 1)): Array<Layer> => {
// Get Tilemap Data

View File

@ -4,7 +4,7 @@ import GameNode from "../Nodes/GameNode";
/**
* A layer in the scene. Has its own alpha value and parallax.
* A layer in the scene. Layers are used for sorting @reference[GameNode]s by depth.
*/
export default class Layer {
/** The scene this layer belongs to */
@ -31,6 +31,11 @@ export default class Layer {
/** The depth of this layer compared to other layers */
protected depth: number;
/**
* Creates a new layer. To do this in a game, use the addLayer() method in @refrence[Scene]
* @param scene The scene to add the layer to
* @param name The name of the layer
*/
constructor(scene: Scene, name: string){
this.scene = scene;
this.name = name;
@ -42,30 +47,57 @@ export default class Layer {
this.depth = 0;
}
/**
* Retreives the name of the layer
* @returns The name of the layer
*/
getName(): string {
return this.name;
}
/**
* Pauses/Unpauses the layer. Affects all elements in this layer
* @param pauseValue True if the layer should be paused, false if not
*/
setPaused(pauseValue: boolean): void {
this.paused = pauseValue;
}
/**
* Returns whether or not the layer is paused
*/
isPaused(): boolean {
return this.paused;
}
/**
* Sets the opacity of the layer
* @param alpha The new opacity value in the range [0, 1]
*/
setAlpha(alpha: number): void {
this.alpha = MathUtils.clamp(alpha, 0, 1);
}
/**
* Gets the opacity of the layer
* @returns The opacity
*/
getAlpha(): number {
return this.alpha;
}
/**
* Sets the layer's hidden value. If hidden, a layer will not be rendered, but will still update
* @param hidden The hidden value of the layer
*/
setHidden(hidden: boolean): void {
this.hidden = hidden;
}
/**
* Returns the hideen value of the lyaer
* @returns True if the scene is hidden, false otherwise
*/
isHidden(): boolean {
return this.hidden;
}
@ -82,27 +114,55 @@ export default class Layer {
this.hidden = false;
}
/**
* Sets whether or not the scene will ySort automatically.
* ySorting means that CanvasNodes on this layer will have their depth sorted depending on their y-value.
* This means that if an object is "higher" in the scene, it will sort behind objects that are "lower".
* This is useful for 3/4 view games, or similar situations, where you sometimes want to be in front of objects,
* and other times want to be behind the same objects.
* @param ySort True if ySorting should be active, false if not
*/
setYSort(ySort: boolean): void {
this.ySort = ySort;
}
/**
* Gets the ySort status of the scene
* @returns True if ySorting is occurring, false otherwise
*/
getYSort(): boolean {
return this.ySort;
}
/**
* Sets the depth of the layer compared to other layers. A larger number means the layer will be closer to the screen.
* @param depth The depth of the layer.
*/
setDepth(depth: number): void {
this.depth = depth;
}
/**
* Retrieves the depth of the layer.
* @returns The depth
*/
getDepth(): number {
return this.depth;
}
/**
* Adds a node to this layer
* @param node The node to add to this layer.
*/
addNode(node: GameNode): void {
this.items.push(node);
node.setLayer(this);
}
/**
* Retreives all GameNodes from this layer
* @returns an Array that contains all of the GameNodes in this layer.
*/
getItems(): Array<GameNode> {
return this.items;
}

View File

@ -2,9 +2,20 @@ import Layer from "../Layer";
import Vec2 from "../../DataTypes/Vec2";
import Scene from "../Scene";
/**
* An extension of a Layer that has a parallax value.
*/
export default class ParallaxLayer extends Layer {
/** The value of the parallax of the Layer */
parallax: Vec2;
/**
* Creates a new ParallaxLayer.
* Use addParallaxLayer() in @reference[Scene] to add a layer of this type to your game.
* @param scene The Scene to add this ParallaxLayer to
* @param name The name of the ParallaxLayer
* @param parallax The parallax level
*/
constructor(scene: Scene, name: string, parallax: Vec2){
super(scene, name);
this.parallax = parallax;

View File

@ -2,7 +2,18 @@ import Vec2 from "../../DataTypes/Vec2";
import Scene from "../Scene";
import ParallaxLayer from "./ParallaxLayer";
/**
* A Layer strictly to be used for managing UIElements.
* This is intended to be a Layer that always stays in the same place,
* and thus renders things like a HUD or an inventory without taking into consideration the \reference[Viewport] scroll.
*/
export default class UILayer extends ParallaxLayer {
/**
* Creates a new UILayer.
* Use addUILayer() in @reference[Scene] to add a layer of this type to your game.
* @param scene The Scene to add this UILayer to
* @param name The name of the UILayer
*/
constructor(scene: Scene, name: string){
super(scene, name, Vec2.ZERO);
}

View File

@ -20,10 +20,16 @@ import ParallaxLayer from "./Layers/ParallaxLayer";
import UILayer from "./Layers/UILayer";
import CanvasNode from "../Nodes/CanvasNode";
import GameNode from "../Nodes/GameNode";
import ArrayUtils from "../Utils/ArrayUtils";
import SceneOptions from "./SceneOptions";
import RenderingManager from "../Rendering/RenderingManager";
import Debug from "../Debug/Debug";
/**
* Scenes are the main container in the game engine.
* Your main scene is the current level or menu of the game, and will contain all of the GameNodes needed.
* Scenes provide an easy way to load assets, add assets to the game world, and unload assets,
* and have lifecycle methods exposed for these functions.
*/
export default class Scene implements Updateable {
/** The size of the game world. */
protected worldSize: Vec2;
@ -82,6 +88,14 @@ export default class Scene implements Updateable {
/** The configuration options for this scene */
public sceneOptions: SceneOptions;
/**
* Creates a new Scene. To add a new Scene in your game, use addScene() in @reference[SceneManager]
* @param viewport The viewport of the game
* @param sceneManager The SceneManager that owns this Scene
* @param renderingManager The RenderingManager that will handle this Scene's rendering
* @param game The instance of the GameLoop
* @param options The options for Scene initialization
*/
constructor(viewport: Viewport, sceneManager: SceneManager, renderingManager: RenderingManager, game: GameLoop, options: Record<string, any>){
this.sceneOptions = SceneOptions.parse(options);
@ -122,7 +136,7 @@ export default class Scene implements Updateable {
/**
* A lifecycle method called every frame of the game. This is where you can dynamically do things like add in new enemies
* @param delta
* @param delta The time this frame represents
*/
updateScene(deltaT: number): void {}
@ -149,6 +163,9 @@ export default class Scene implements Updateable {
this.viewport.update(deltaT);
}
/**
* Collects renderable sets and coordinates with the RenderingManager to draw the Scene
*/
render(): void {
// Get the visible set of nodes
let visibleSet = this.sceneGraph.getVisibleSet();
@ -171,10 +188,18 @@ export default class Scene implements Updateable {
Debug.setNodes(nodes);
}
/**
* Sets the scene as running or not
* @param running True if the Scene should be running, false if not
*/
setRunning(running: boolean): void {
this.running = running;
}
/**
* Returns whether or not the Scene is running
* @returns True if the scene is running, false otherwise
*/
isRunning(): boolean {
return this.running;
}
@ -183,6 +208,7 @@ export default class Scene implements Updateable {
* Adds a new layer to the scene and returns it
* @param name The name of the new layer
* @param depth The depth of the layer
* @returns The newly created Layer
*/
addLayer(name: string, depth?: number): Layer {
if(this.layers.has(name) || this.parallaxLayers.has(name) || this.uiLayers.has(name)){
@ -205,6 +231,7 @@ export default class Scene implements Updateable {
* @param name The name of the parallax layer
* @param parallax The parallax level
* @param depth The depth of the layer
* @returns The newly created ParallaxLayer
*/
addParallaxLayer(name: string, parallax: Vec2, depth?: number): ParallaxLayer {
if(this.layers.has(name) || this.parallaxLayers.has(name) || this.uiLayers.has(name)){
@ -225,6 +252,7 @@ export default class Scene implements Updateable {
/**
* Adds a new UILayer to the scene
* @param name The name of the new UIlayer
* @returns The newly created UILayer
*/
addUILayer(name: string): UILayer {
if(this.layers.has(name) || this.parallaxLayers.has(name) || this.uiLayers.has(name)){
@ -239,8 +267,10 @@ export default class Scene implements Updateable {
}
/**
* Gets a layer from the scene by name if it exists
* Gets a layer from the scene by name if it exists.
* This can be a Layer or any of its subclasses
* @param name The name of the layer
* @returns The Layer found with that name
*/
getLayer(name: string): Layer {
if(this.layers.has(name)){
@ -256,7 +286,8 @@ export default class Scene implements Updateable {
/**
* Returns true if this layer is a ParallaxLayer
* @param name
* @param name The name of the layer
* @returns True if this layer is a ParallaxLayer
*/
isParallaxLayer(name: string): boolean {
return this.parallaxLayers.has(name);
@ -264,15 +295,18 @@ export default class Scene implements Updateable {
/**
* Returns true if this layer is a UILayer
* @param name
* @param name The name of the layer
* @returns True if this layer is ParallaxLayer
*/
isUILayer(name: string): boolean {
return this.uiLayers.has(name);
}
/**
* Returns the translation of this node with respect to camera space (due to the viewport moving);
* @param node
* Returns the translation of this node with respect to camera space (due to the viewport moving).
* This value is affected by the parallax level of the @reference[Layer] the node is on.
* @param node The node to check the viewport with respect to
* @returns A Vec2 containing the translation of viewport with respect to this node.
*/
getViewTranslation(node: GameNode): Vec2 {
let layer = node.getLayer();
@ -284,40 +318,75 @@ export default class Scene implements Updateable {
}
}
/** Returns the scale level of the view */
/**
* Returns the scale level of the view
* @returns The zoom level of the viewport
*/
getViewScale(): number {
return this.viewport.getZoomLevel();
}
/** Returns the viewport associated with this scene */
/**
* Returns the Viewport associated with this scene
* @returns The current Viewport
*/
getViewport(): Viewport {
return this.viewport;
}
/**
* Gets the world size of this Scene
* @returns The world size in a Vec2
*/
getWorldSize(): Vec2 {
return this.worldSize;
}
/**
* Gets the SceneGraph associated with this Scene
* @returns The SceneGraph
*/
getSceneGraph(): SceneGraph {
return this.sceneGraph;
}
/**
* Gets the PhysicsManager associated with this Scene
* @returns The PhysicsManager
*/
getPhysicsManager(): PhysicsManager {
return this.physicsManager;
}
/**
* Gets the NavigationManager associated with this Scene
* @returns The NavigationManager
*/
getNavigationManager(): NavigationManager {
return this.navManager;
}
/**
* Gets the AIManager associated with this Scene
* @returns The AIManager
*/
getAIManager(): AIManager {
return this.aiManager;
}
/**
* Generates an ID for a GameNode
* @returns The new ID
*/
generateId(): number {
return this.sceneManager.generateId();
}
/**
* Retrieves a Tilemap in this Scene
* @param name The name of the Tilemap
* @returns The Tilemap, if one this name exists, otherwise null
*/
getTilemap(name: string): Tilemap {
for(let tilemap of this .tilemaps){
if(tilemap.name === name){
@ -328,29 +397,3 @@ export default class Scene implements Updateable {
return null;
}
}
class SceneOptions {
physics: {
numPhysicsLayers: number,
physicsLayerNames: Array<string>,
physicsLayerCollisions: Array<Array<number>>;
}
static parse(options: Record<string, any>): SceneOptions{
let sOpt = new SceneOptions();
sOpt.physics = {
numPhysicsLayers: 10,
physicsLayerNames: null,
physicsLayerCollisions: ArrayUtils.ones2d(10, 10)
};
if(options.physics){
if(options.physics.numPhysicsLayers) sOpt.physics.numPhysicsLayers = options.physics.numPhysicsLayers;
if(options.physics.physicsLayerNames) sOpt.physics.physicsLayerNames = options.physics.physicsLayerNames;
if(options.physics.physicsLayerCollisions) sOpt.physics.physicsLayerCollisions = options.physics.physicsLayerCollisions;
}
return sOpt;
}
}

View File

@ -4,14 +4,31 @@ import Viewport from "../SceneGraph/Viewport";
import GameLoop from "../Loop/GameLoop";
import RenderingManager from "../Rendering/RenderingManager";
/**
* The SceneManager of the game engine. There is only one of theses.
* The SceneManager acts as an interface to create Scenes, and handles the lifecycle methods of Scenes.
* The Scene manager keeps track of systems that are constant across scene, such as the @reference[ResourceManager]
*/
export default class SceneManager {
/** The current Scene of the game */
protected currentScene: Scene;
/** The Viewport of the game */
protected viewport: Viewport;
/** A reference to the ResourceManager */
protected resourceManager: ResourceManager;
/** The GameLoop this SceneManager belongs to */
protected game: GameLoop;
/** A counter to keep track of game ids */
protected idCounter: number;
/** The RenderingManager of the game */
protected renderingManager: RenderingManager;
/**
* Creates a new SceneManager
* @param viewport The Viewport of the game
* @param game The GameLoop instance
* @param renderingManager The RenderingManager of the game
*/
constructor(viewport: Viewport, game: GameLoop, renderingManager: RenderingManager){
this.resourceManager = ResourceManager.getInstance();
this.viewport = viewport;
@ -21,7 +38,8 @@ export default class SceneManager {
}
/**
* Add a scene as the main scene
* Add a scene as the main scene.
* Use this method if you've created a subclass of Scene, and you want to add it as the main Scene.
* @param constr The constructor of the scene to add
*/
public addScene<T extends Scene>(constr: new (...args: any) => T, options: Record<string, any>): void {
@ -43,7 +61,8 @@ export default class SceneManager {
}
/**
* Change from the current scene to this new scene
* Change from the current scene to this new scene.
* Use this method if you've created a subclass of Scene, and you want to add it as the main Scene.
* @param constr The constructor of the scene to change to
*/
public changeScene<T extends Scene>(constr: new (...args: any) => T, options: Record<string, any>): void {
@ -57,14 +76,25 @@ export default class SceneManager {
this.addScene(constr, options);
}
/**
* Generates a unique ID
* @returns A new ID
*/
public generateId(): number {
return this.idCounter++;
}
public render(){
/**
* Renders the current Scene
*/
public render(): void {
this.currentScene.render();
}
/**
* Updates the current Scene
* @param deltaT The timestep of the Scene
*/
public update(deltaT: number){
if(this.currentScene.isRunning()){
this.currentScene.update(deltaT);

32
src/Scene/SceneOptions.ts Normal file
View File

@ -0,0 +1,32 @@
import ArrayUtils from "../Utils/ArrayUtils";
// @ignorePage
/**
* The options to give a @reference[Scene] for initialization
*/
export default class SceneOptions {
physics: {
numPhysicsLayers: number,
physicsLayerNames: Array<string>,
physicsLayerCollisions: Array<Array<number>>;
}
static parse(options: Record<string, any>): SceneOptions{
let sOpt = new SceneOptions();
sOpt.physics = {
numPhysicsLayers: 10,
physicsLayerNames: null,
physicsLayerCollisions: ArrayUtils.ones2d(10, 10)
};
if(options.physics){
if(options.physics.numPhysicsLayers) sOpt.physics.numPhysicsLayers = options.physics.numPhysicsLayers;
if(options.physics.physicsLayerNames) sOpt.physics.physicsLayerNames = options.physics.physicsLayerNames;
if(options.physics.physicsLayerCollisions) sOpt.physics.physicsLayerCollisions = options.physics.physicsLayerCollisions;
}
return sOpt;
}
}

View File

@ -6,14 +6,25 @@ import Scene from "../Scene/Scene";
import AABB from "../DataTypes/Shapes/AABB";
/**
* An abstract interface of a SceneGraph. Exposes methods for use by other code, but leaves the implementation up to the subclasses.
* An abstract interface of a SceneGraph.
* Exposes methods for use by other code, but leaves the implementation up to the subclasses.
* The SceneGraph manages the positions of all GameNodes, and can easily prune a visible set for rendering.
*/
export default abstract class SceneGraph {
/** A reference to the viewport */
protected viewport: Viewport;
/** A map of CanvasNodes in this SceneGraph */
protected nodeMap: Map<CanvasNode>;
/** A counter of IDs for nodes in this SceneGraph */
protected idCounter: number;
/** A reference to the Scene this SceneGraph belongs to */
protected scene: Scene;
/**
* Creates a new SceneGraph
* @param viewport The viewport
* @param scene The Scene this SceneGraph belongs to
*/
constructor(viewport: Viewport, scene: Scene){
this.viewport = viewport;
this.scene = scene;
@ -24,6 +35,7 @@ export default abstract class SceneGraph {
/**
* Add a node to the SceneGraph
* @param node The CanvasNode to add to the SceneGraph
* @returns The SceneGraph ID of this newly added CanvasNode
*/
addNode(node: CanvasNode): number {
this.nodeMap.add(this.idCounter.toString(), node);
@ -63,15 +75,17 @@ export default abstract class SceneGraph {
/**
* Get a specific node using its id
* @param id The id of the CanvasNode to retrieve
* @returns The node with this ID
*/
getNode(id: string): CanvasNode {
return this.nodeMap.get(id);
}
/**
* Returns the node at specific coordinates
* @param vecOrX
* @param y
* Returns the nodes at specific coordinates
* @param vecOrX The x-coordinate of the position, or the coordinates in a Vec2
* @param y The y-coordinate of the position
* @returns An array of nodes found at the position provided
*/
getNodesAt(vecOrX: Vec2 | number, y: number = null): Array<CanvasNode> {
if(vecOrX instanceof Vec2){
@ -81,8 +95,17 @@ export default abstract class SceneGraph {
}
}
/**
* Returns the nodes that overlap a specific boundary
* @param boundary The region to check
* @returns An array of nodes found overlapping the provided boundary
*/
abstract getNodesInRegion(boundary: AABB): Array<CanvasNode>;
/**
* Returns all nodes in the SceneGraph
* @returns An Array containing all nodes in the SceneGraph
*/
getAllNodes(): Array<CanvasNode> {
let arr = new Array<CanvasNode>();
this.nodeMap.forEach(key => arr.push(this.nodeMap.get(key)));
@ -91,8 +114,8 @@ export default abstract class SceneGraph {
/**
* The specific implementation of getting a node at certain coordinates
* @param x
* @param y
* @param x The x-coordinates of the node
* @param y The y-coordinates of the node
*/
protected abstract getNodesAtCoords(x: number, y: number): Array<CanvasNode>;
@ -101,7 +124,8 @@ export default abstract class SceneGraph {
abstract render(ctx: CanvasRenderingContext2D): void;
/**
* Gets the visible set of CanvasNodes based on the viewport
* Gets the visible set of CanvasNodes based on the @reference[Viewport]
* @returns An array containing all visible nodes in the SceneGraph
*/
abstract getVisibleSet(): Array<CanvasNode>;
}

View File

@ -2,37 +2,41 @@ import SceneGraph from "./SceneGraph";
import CanvasNode from "../Nodes/CanvasNode";
import Viewport from "./Viewport";
import Scene from "../Scene/Scene";
import Stack from "../DataTypes/Stack";
import Layer from "../Scene/Layer"
import AABB from "../DataTypes/Shapes/AABB";
import Stats from "../Debug/Stats";
/**
* An implementation of a SceneGraph that simply stored CanvasNodes in an array.
*/
export default class SceneGraphArray extends SceneGraph {
/** The list of CanvasNodes in this SceneGraph */
private nodeList: Array<CanvasNode>;
private turnOffViewportCulling_demoTool: boolean;
/**
* Creates a new SceneGraphArray
* @param viewport The Viewport
* @param scene The Scene this SceneGraph belongs to
*/
constructor(viewport: Viewport, scene: Scene){
super(viewport, scene);
this.nodeList = new Array<CanvasNode>();
this.turnOffViewportCulling_demoTool = false;
}
setViewportCulling_demoTool(bool: boolean): void {
this.turnOffViewportCulling_demoTool = bool;
}
addNodeSpecific(node: CanvasNode, id: string): void {
// @override
protected addNodeSpecific(node: CanvasNode, id: string): void {
this.nodeList.push(node);
}
removeNodeSpecific(node: CanvasNode, id: string): void {
// @override
protected removeNodeSpecific(node: CanvasNode, id: string): void {
let index = this.nodeList.indexOf(node);
if(index > -1){
this.nodeList.splice(index, 1);
}
}
// @override
getNodesAtCoords(x: number, y: number): Array<CanvasNode> {
let results = [];
@ -45,6 +49,7 @@ export default class SceneGraphArray extends SceneGraph{
return results;
}
// @override
getNodesInRegion(boundary: AABB): Array<CanvasNode> {
let t0 = performance.now();
let results = [];
@ -73,16 +78,8 @@ export default class SceneGraphArray extends SceneGraph{
render(ctx: CanvasRenderingContext2D): void {}
// @override
getVisibleSet(): Array<CanvasNode> {
// If viewport culling is turned off for demonstration
if(this.turnOffViewportCulling_demoTool){
let visibleSet = new Array<CanvasNode>();
for(let node of this.nodeList){
visibleSet.push(node);
}
return visibleSet;
}
let visibleSet = new Array<CanvasNode>();
for(let node of this.nodeList){

View File

@ -7,10 +7,21 @@ import Vec2 from "../DataTypes/Vec2";
import AABB from "../DataTypes/Shapes/AABB";
import Stats from "../Debug/Stats";
/**
* An implementation of a SceneGraph that uses a @reference[RegionQuadTree] to store @reference[CanvasNode]s.
*/
export default class SceneGraphQuadTree extends SceneGraph {
/** The QuadTree used to store the CanvasNodes */
private qt: RegionQuadTree<CanvasNode>;
/** A list of nodes to help out the QuadTree */
private nodes: Array<CanvasNode>;
/**
* Creates a new SceneGraphQuadTree
* @param viewport The Viewport
* @param scene The Scene this SceneGraph belongs to
*/
constructor(viewport: Viewport, scene: Scene){
super(viewport, scene);
@ -19,21 +30,25 @@ export default class SceneGraphQuadTree extends SceneGraph {
this.nodes = new Array();
}
addNodeSpecific(node: CanvasNode, id: string): void {
// @override
protected addNodeSpecific(node: CanvasNode, id: string): void {
this.nodes.push(node);
}
removeNodeSpecific(node: CanvasNode, id: string): void {
// @override
protected removeNodeSpecific(node: CanvasNode, id: string): void {
let index = this.nodes.indexOf(node);
if(index >= 0){
this.nodes.splice(index, 1);
}
}
// @override
getNodesAtCoords(x: number, y: number): Array<CanvasNode> {
return this.qt.queryPoint(new Vec2(x, y));
}
// @override
getNodesInRegion(boundary: AABB): Array<CanvasNode> {
let t0 = performance.now();
let res = this.qt.queryRegion(boundary);
@ -72,6 +87,7 @@ export default class SceneGraphQuadTree extends SceneGraph {
this.qt.render_demo(ctx, origin, zoom);
}
// @override
getVisibleSet(): Array<CanvasNode> {
let visibleSet = this.qt.queryRegion(this.viewport.getView());

View File

@ -4,29 +4,37 @@ import CanvasNode from "../Nodes/CanvasNode";
import MathUtils from "../Utils/MathUtils";
import Queue from "../DataTypes/Queue";
import AABB from "../DataTypes/Shapes/AABB";
import Debug from "../Debug/Debug";
import InputReceiver from "../Input/InputReceiver";
import ParallaxLayer from "../Scene/Layers/ParallaxLayer";
import UILayer from "../Scene/Layers/UILayer";
/**
* The viewport of the game. Corresponds to the visible window displayed in the browser.
* The viewport keeps track of its position in the game world, and can act as a camera to follow objects.
*/
export default class Viewport {
/** The AABB that contains the position and size of the viewport view */
private view: AABB;
/** The boundary for the viewport. This represents the limits to where the viewport can go */
private boundary: AABB;
/** The GameNode the Viewport is following */
private following: GameNode;
/** The position the GameNode is focusing on. This is overridden if "following" is set. */
private focus: Vec2;
/**
* A queue of previous positions of what this viewport is following. Used for smoothing viewport movement
*/
/** A queue of previous positions of what this viewport is following. Used for smoothing viewport movement */
private lastPositions: Queue<Vec2>;
/**
* The number of previous positions this viewport tracks
*/
/** The number of previous positions this viewport tracks */
private smoothingFactor: number;
/** A boolean tha represents whether the player can zoom by scrolling with the mouse wheel */
private scrollZoomEnabled: boolean;
/** The amount that is zoomed in or out. */
private ZOOM_FACTOR: number = 1.2;
/** The size of the canvas */
private canvasSize: Vec2;
constructor(){
@ -39,24 +47,30 @@ export default class Viewport {
this.focus = Vec2.ZERO;
}
/** Enables the viewport to zoom in and out */
enableZoom(): void {
this.scrollZoomEnabled = true;
}
/**
* Returns the position of the viewport as a Vec2
* Returns the position of the viewport
* @returns The center of the viewport as a Vec2
*/
getCenter(): Vec2 {
return this.view.center;
}
/** Returns a new Vec2 with the origin of the viewport */
/**
* Returns a new Vec2 with the origin of the viewport
* @returns The top left cornder of the Vieport as a Vec2
*/
getOrigin(): Vec2 {
return new Vec2(this.view.left, this.view.top);
}
/**
* Returns the region visible to this viewport
* @returns The AABB containing the region visible to the viewport
*/
getView(): AABB {
return this.view;
@ -64,8 +78,8 @@ export default class Viewport {
/**
* Set the position of the viewport
* @param vecOrX
* @param y
* @param vecOrX The new position or the x-coordinate of the new position
* @param y The y-coordinate of the new position
*/
setCenter(vecOrX: Vec2 | number, y: number = null): void {
let pos: Vec2;
@ -81,6 +95,7 @@ export default class Viewport {
/**
* Returns the size of the viewport as a Vec2
* @returns The half-size of the viewport as a Vec2
*/
getHalfSize(): Vec2 {
return this.view.getHalfSize();
@ -88,8 +103,8 @@ export default class Viewport {
/**
* Sets the size of the viewport
* @param vecOrX
* @param y
* @param vecOrX The new width of the viewport or the new size as a Vec2
* @param y The new height of the viewport
*/
setSize(vecOrX: Vec2 | number, y: number = null): void {
if(vecOrX instanceof Vec2){
@ -99,6 +114,11 @@ export default class Viewport {
}
}
/**
* Sets the half-size of the viewport
* @param vecOrX The new half-width of the viewport or the new half-size as a Vec2
* @param y The new height of the viewport
*/
setHalfSize(vecOrX: Vec2 | number, y: number = null): void {
if(vecOrX instanceof Vec2){
this.view.setHalfSize(vecOrX.clone());
@ -108,9 +128,9 @@ export default class Viewport {
}
/**
* Sets the size of the canvas that the viewport is projecting to.
* @param vecOrX
* @param y
* Updates the viewport with the size of the current Canvas
* @param vecOrX The width of the canvas, or the canvas size as a Vec2
* @param y The height of the canvas
*/
setCanvasSize(vecOrX: Vec2 | number, y: number = null): void {
if(vecOrX instanceof Vec2){
@ -120,10 +140,18 @@ export default class Viewport {
}
}
/**
* Sets the zoom level of the viewport
* @param zoom The zoom level
*/
setZoomLevel(zoom: number): void {
this.view.halfSize.scale(1/zoom);
}
/**
* Gets the zoom level of the viewport
* @returns The zoom level
*/
getZoomLevel(): number {
return this.canvasSize.x/this.view.hw/2
}
@ -139,7 +167,7 @@ export default class Viewport {
/**
* Tells the viewport to focus on a point. Overidden by "following".
* @param focus
* @param focus The point the viewport should focus on
*/
setFocus(focus: Vec2): void {
this.focus.copy(focus);
@ -147,7 +175,8 @@ export default class Viewport {
/**
* Returns true if the CanvasNode is inside of the viewport
* @param node
* @param node The node to check
* @returns True if the node is currently visible in the viewport, false if not
*/
includes(node: CanvasNode): boolean {
let parallax = node.getLayer() instanceof ParallaxLayer || node.getLayer() instanceof UILayer ? (<ParallaxLayer>node.getLayer()).parallax : new Vec2(1, 1);
@ -162,10 +191,10 @@ export default class Viewport {
// TODO: This should probably be done automatically, or should consider the aspect ratio or something
/**
* Sets the bounds of the viewport
* @param lowerX
* @param lowerY
* @param upperX
* @param upperY
* @param lowerX The left edge of the viewport
* @param lowerY The top edge of the viewport
* @param upperX The right edge of the viewport
* @param upperY The bottom edge of the viewport
*/
setBounds(lowerX: number, lowerY: number, upperX: number, upperY: number): void {
let hwidth = (upperX - lowerX)/2;

View File

@ -3,9 +3,18 @@ import Receiver from "../Events/Receiver";
import ResourceManager from "../ResourceManager/ResourceManager";
import { GameEventType } from "../Events/GameEventType";
/**
* Manages any sounds or music needed for the game.
* Through the EventQueue, exposes interface to play sounds so GameNodes can activate sounds without
* needing direct references to the audio system
*/
export default class AudioManager {
private static instance: AudioManager;
/** The event receiver of this AudioManager */
private receiver: Receiver;
/** A Map of the names of currently playing (or paused) sounds to their AudioBuffers */
private currentSounds: Map<AudioBufferSourceNode>;
private audioCtx: AudioContext;
@ -19,6 +28,7 @@ export default class AudioManager {
/**
* Get the instance of the AudioManager class or create a new one if none exists
* @returns The AudioManager
*/
public static getInstance(): AudioManager {
if(!this.instance){
@ -42,15 +52,12 @@ export default class AudioManager {
/**
* Returns the current audio context
* @returns The AudioContext
*/
public getAudioContext(): AudioContext {
return this.audioCtx;
}
/**
* Creates a new sound from the key of a loaded audio file
* @param key The key of the loaded audio file to create a new sound for
*/
/*
According to the MDN, create a new sound for every call:
@ -61,6 +68,11 @@ export default class AudioManager {
hold a reference to it. It will automatically be garbage-collected at an appropriate time, which won't be
until sometime after the sound has finished playing.
*/
/**
* Creates a new sound from the key of a loaded audio file
* @param key The key of the loaded audio file to create a new sound for
* @returns The newly created AudioBuffer
*/
protected createSound(key: string): AudioBufferSourceNode {
// Get audio buffer
let buffer = ResourceManager.getInstance().getAudio(key);
@ -109,10 +121,6 @@ export default class AudioManager {
}
}
/**
* Updates the AudioManager
* @param deltaT
*/
update(deltaT: number): void {
// Play each audio clip requested
// TODO - Add logic to merge sounds if there are multiple of the same key

View File

@ -1,8 +1,10 @@
/** A class containing some utility functions for Arrays */
export default class ArrayUtils {
/**
* Returns a 2d array of dim1 x dim2 filled with 1s
* @param dim1
* @param dim2
* @param dim1 The first dimension of the array to create
* @param dim2 The second dimension of the array to create
* @returns A dim1 x dim2 Array filled with 1s
*/
static ones2d(dim1: number, dim2: number): number[][] {
let arr = new Array<Array<number>>(dim1);
@ -18,6 +20,13 @@ export default class ArrayUtils {
return arr;
}
/**
* Returns a 2d array of dim1 x dim2 filled with true or false
* @param dim1 The first dimension of the array to create
* @param dim2 The second dimension of the array to create
* @param flag The boolean to fill the array with
* @returns A dim1 x dim2 Array filled with flag
*/
static bool2d(dim1: number, dim2: number, flag: boolean): boolean[][] {
let arr = new Array<Array<boolean>>(dim1);

View File

@ -1,12 +1,26 @@
import MathUtils from "./MathUtils";
// TODO: This should be moved to the datatypes folder
/**
* A Color util class that keeps track of colors like a vector, but can be converted into a string format
*/
export default class Color {
/** The red value */
public r: number;
/** The green value */
public g: number;
/** The blue value */
public b: number;
/** The alpha value */
public a: number;
/**
* Creates a new color
* @param r Red
* @param g Green
* @param b Blue
* @param a Alpha
*/
constructor(r: number = 0, g: number = 0, b: number = 0, a: number = 1){
this.r = r;
this.g = g;
@ -14,45 +28,93 @@ export default class Color {
this.a = a;
}
/**
* Transparent color
* @returns rgba(0, 0, 0, 0)
*/
static get TRANSPARENT(): Color {
return new Color(0, 0, 0, 0);
}
/**
* Red color
* @returns rgb(255, 0, 0)
*/
static get RED(): Color {
return new Color(255, 0, 0, 1);
}
/**
* Green color
* @returns rgb(0, 255, 0)
*/
static get GREEN(): Color {
return new Color(0, 255, 0, 1);
}
/**
* Blue color
* @returns rgb(0, 0, 255)
*/
static get BLUE(): Color {
return new Color(0, 0, 255, 1);
}
/**
* Yellow color
* @returns rgb(255, 255, 0)
*/
static get YELLOW(): Color {
return new Color(255, 255, 0, 1);
}
/**
* Purple color
* @returns rgb(255, 0, 255)
*/
static get PURPLE(): Color {
return new Color(255, 0, 255, 1);
}
/**
* Cyan color
* @returns rgb(0, 255, 255)
*/
static get CYAN(): Color {
return new Color(0, 255, 255, 1);
}
/**
* White color
* @returns rgb(255, 255, 255)
*/
static get WHITE(): Color {
return new Color(255, 255, 255, 1);
}
/**
* Black color
* @returns rgb(0, 0, 0)
*/
static get BLACK(): Color {
return new Color(0, 0, 0, 1);
}
/**
* Orange color
* @returns rgb(255, 100, 0)
*/
static get ORANGE(): Color {
return new Color(255, 100, 0, 1);
}
/**
* Sets the color to the values provided
* @param r Red
* @param g Green
* @param b Blue
* @param a Alpha
*/
set(r: number, g: number, b: number, a: number = 1): void {
this.r = r;
this.g = g;
@ -62,6 +124,7 @@ export default class Color {
/**
* Returns a new color slightly lighter than the current color
* @returns A new lighter Color
*/
lighten(): Color {
return new Color(MathUtils.clamp(this.r + 40, 0, 255), MathUtils.clamp(this.g + 40, 0, 255), MathUtils.clamp(this.b + 40, 0, 255), this.a);
@ -69,6 +132,7 @@ export default class Color {
/**
* Returns a new color slightly darker than the current color
* @returns A new darker Color
*/
darken(): Color {
return new Color(MathUtils.clamp(this.r - 40, 0, 255), MathUtils.clamp(this.g - 40, 0, 255), MathUtils.clamp(this.b - 40, 0, 255), this.a);
@ -76,6 +140,7 @@ export default class Color {
/**
* Returns the color as a string of the form #RRGGBB
* @returns #RRGGBB
*/
toString(): string {
return "#" + MathUtils.toHex(this.r, 2) + MathUtils.toHex(this.g, 2) + MathUtils.toHex(this.b, 2);
@ -83,6 +148,7 @@ export default class Color {
/**
* Returns the color as a string of the form rgb(r, g, b)
* @returns rgb(r, g, b)
*/
toStringRGB(): string {
return "rgb(" + this.r.toString() + ", " + this.g.toString() + ", " + this.b.toString() + ")";
@ -90,6 +156,7 @@ export default class Color {
/**
* Returns the color as a string of the form rgba(r, g, b, a)
* @returns rgba(r, g, b, a)
*/
toStringRGBA(): string {
if(this.a === 0){

View File

@ -1,3 +1,5 @@
// @ignorePage
export default class EaseFunctions {
static easeInOutSine(x: number): number {

View File

@ -1,7 +1,15 @@
import Graph, { EdgeNode } from "../DataTypes/Graphs/Graph";
import Graph from "../DataTypes/Graphs/Graph";
import EdgeNode from "../DataTypes/Graphs/EdgeNode";
/** A class to provides some utility functions for graphs */
export default class GraphUtils {
/**
* An implementation of Djikstra's shortest path algorithm based on the one described in The Algorithm Design Manual.
* @param g The graph
* @param start The number to start the shortest path from
* @returns An array containing the parent of each node of the Graph in the shortest path.
*/
static djikstra(g: Graph, start: number): Array<number> {
let i: number; // Counter
let p: EdgeNode; // Pointer to edgenode

View File

@ -1,9 +1,11 @@
import Vec2 from "../DataTypes/Vec2";
/** A class containing some utility functions for math operations */
export default class MathUtils {
/**
* Returns the sign of the value provided
* @param x The value to extract the sign from
* @returns -1 if the number is less than 0, 1 otherwise
*/
static sign(x: number): number {
return x < 0 ? -1 : 1;
@ -14,6 +16,7 @@ export default class MathUtils {
* @param x The value to be clamped
* @param min The min of the range
* @param max The max of the range
* @returns x, if it is between min and max, or min/max if it exceeds their bounds
*/
static clamp(x: number, min: number, max: number): number {
if(x < min) return min;
@ -24,6 +27,7 @@ export default class MathUtils {
/**
* Clamps the value x to the range between 0 and 1
* @param x The value to be clamped
* @returns x, if it is between 0 and 1, or 0/1 if it exceeds their bounds
*/
static clamp01(x: number): number {
return MathUtils.clamp(x, 0, 1);
@ -42,11 +46,19 @@ export default class MathUtils {
* @param a The first value for the interpolation bound
* @param b The second value for the interpolation bound
* @param t The time we are interpolating to
* @returns The value between a and b at time t
*/
static lerp(a: number, b: number, t: number): number {
return a + t * (b - a);
}
/**
* Inverse Linear Interpolation. Finds the time at which a value between a and b would occur
* @param a The first value for the interpolation bound
* @param b The second value for the interpolation bound
* @param value The current value
* @returns The time at which the current value occurs between a and b
*/
static invLerp(a: number, b: number, value: number){
return (value - a)/(b - a);
}
@ -55,6 +67,7 @@ export default class MathUtils {
* Cuts off decimal points of a number after a specified place
* @param num The number to floor
* @param place The last decimal place of the new number
* @returns The floored number
*/
static floorToPlace(num: number, place: number): number {
if(place === 0){
@ -75,6 +88,7 @@ export default class MathUtils {
* Returns the number as a hexadecimal
* @param num The number to convert to hex
* @param minLength The length of the returned hex string (adds zero padding if needed)
* @returns The hex representation of the number as a string
*/
static toHex(num: number, minLength: number = null): string {
let factor = 1;
@ -99,8 +113,9 @@ export default class MathUtils {
}
/**
* Converts the number to hexadecimal
* @param num The number to convert to hexadecimal
* Converts a digit to hexadecimal. In this case, a digit is between 0 and 15 inclusive
* @param num The digit to convert to hexadecimal
* @returns The hex representation of the digit as a string
*/
static toHexDigit(num: number): string {
if(num < 10){

View File

@ -1,7 +0,0 @@
import { Physical } from "../DataTypes/Interfaces/Descriptors";
export default class PhysicsUtils {
static sweepAndPrune(nodes: Array<Physical>){
// Sort
}
}

View File

@ -15,6 +15,9 @@ const permutation = [ 151,160,137,91,90,15,
138,236,205,93,222,114,67,29,24,72,243,141,128,195,78,66,215,61,156,180
];
/**
* A noise generator
*/
export default class Perlin {
private p: Int16Array;
@ -30,11 +33,12 @@ export default class Perlin {
/**
* Returns a random perlin noise value
* @param x
* @param y
* @param z
* @param x An input value
* @param y An input value
* @param z An input value
* @returns A noise value
*/
perlin(x: number, y: number, z: number = 0){
perlin(x: number, y: number, z: number = 0): number {
if(this.repeat > 0) {
x = x%this.repeat;
y = y%this.repeat;

View File

@ -10,11 +10,13 @@ class Noise {
}
}
/** A class that has some random generator utils */
export default class RandUtils {
/**
* Generates a random integer in the specified range
* @param min The min of the range (inclusive)
* @param max The max of the range (exclusive)
* @returns A random int in the range [min, max)
*/
static randInt(min: number, max: number): number {
return Math.floor(Math.random()*(max - min) + min);
@ -24,6 +26,7 @@ export default class RandUtils {
* Generates a random hexadecimal number in the specified range
* @param min The min of the range (inclusive)
* @param max The max of the range (exclusive)
* @returns a random hex number in the range [min, max) as a string
*/
static randHex(min: number, max: number): string {
return MathUtils.toHex(RandUtils.randInt(min, max));
@ -31,6 +34,7 @@ export default class RandUtils {
/**
* Generates a random color
* @returns A random Color
*/
static randColor(): Color {
let r = RandUtils.randInt(0, 256);
@ -39,6 +43,7 @@ export default class RandUtils {
return new Color(r, g, b);
}
/** A noise generator */
static noise: Noise = new Noise();
}

View File

@ -1,7 +1,14 @@
/** Some utility functions for sorting arrays */
export default class SortingUtils {
/**
*
* @param arr
* An implementation of insertion sort.
* In game engines, this is particularly useful to sort node positions because of temporal coherence -
* the idea that nodes are almost in the same place as last frame, and thus, in a frame-to-frame comparison,
* nodes essentially do not change position.
* This means we have a nearly sorted array of nodes if we keep track of this,
* so something like insertion sort actually becomes essentailly O(n),
* as it performs very well on nearly sorted arrays.
* @param arr The array to sort in place
* @param comparator Compares element a and b in the array. Returns -1 if a < b, 0 if a = b, and 1 if a > b
*/
static insertionSort<T>(arr: Array<T>, comparator: (a: T, b: T) => number): void {
@ -16,7 +23,13 @@ export default class SortingUtils {
}
}
static swap<T>(arr: Array<T>, i: number, j: number){
/**
* Swaps two elements in the provided array
* @param arr The array to perform the swap on in place
* @param i The first index
* @param j The second index
*/
static swap<T>(arr: Array<T>, i: number, j: number): void {
let temp = arr[i];
arr[i] = arr[j];
arr[j] = temp;

View File

@ -1,7 +1,9 @@
/** Some utility functions for dealing with strings */
export default class StringUtils {
/**
* Extracts the path from a filepath that includes the file
* @param filePath the filepath to extract the path form
* @param filePath the filepath to extract the path from
* @returns The path portion of the filepath provided
*/
static getPathFromFilePath(filePath: string): string {
let splitPath = filePath.split("/");