From a32066468fdb1600d8dd8dc7da8cf6970ec6a81e Mon Sep 17 00:00:00 2001 From: Joe Weaver Date: Mon, 28 Sep 2020 18:57:02 -0400 Subject: [PATCH] added quadtree scene graph and gave CanvasNodes boundaries --- src/DataTypes/AABB.ts | 103 +++++++ src/DataTypes/Interfaces/Descriptors.ts | 44 +++ src/DataTypes/QuadTree.ts | 154 +++++++++++ src/DataTypes/RegionQuadTree.ts | 257 ++++++++++++++++++ src/DataTypes/Vec2.ts | 42 +++ src/Loop/GameLoop.ts | 16 +- src/Nodes/CanvasNode.ts | 93 +++++-- src/Nodes/GameNode.ts | 34 ++- src/Nodes/Graphic.ts | 5 + src/Nodes/Graphics/Point.ts | 22 ++ src/Nodes/Graphics/Rect.ts | 24 +- .../Colliders/{AABB.ts => AABBCollider.ts} | 6 +- src/Physics/StaticBody.ts | 9 +- src/Player.ts | 4 +- src/QuadTreeScene.ts | 67 +++++ src/ResourceManager/ResourceManager.ts | 23 +- src/Scene/Factories/CanvasNodeFactory.ts | 13 +- src/Scene/Factories/FactoryManager.ts | 4 +- src/Scene/Factories/PhysicsNodeFactory.ts | 3 +- src/Scene/Factories/TilemapFactory.ts | 1 + src/Scene/Scene.ts | 22 +- src/Scene/SceneManager.ts | 9 + src/SceneGraph/SceneGraph.ts | 15 +- src/SceneGraph/SceneGraphArray.ts | 17 +- src/SceneGraph/SceneGraphQuadTree.ts | 80 ++++++ src/Utils/Color.ts | 43 ++- src/Utils/MathUtils.ts | 10 + src/Utils/Rand/Perlin.ts | 122 +++++++++ src/Utils/RandUtils.ts | 12 + src/main.ts | 6 +- 30 files changed, 1183 insertions(+), 77 deletions(-) create mode 100644 src/DataTypes/AABB.ts create mode 100644 src/DataTypes/Interfaces/Descriptors.ts create mode 100644 src/DataTypes/QuadTree.ts create mode 100644 src/DataTypes/RegionQuadTree.ts create mode 100644 src/Nodes/Graphics/Point.ts rename src/Physics/Colliders/{AABB.ts => AABBCollider.ts} (86%) create mode 100644 src/QuadTreeScene.ts create mode 100644 src/SceneGraph/SceneGraphQuadTree.ts create mode 100644 src/Utils/Rand/Perlin.ts diff --git a/src/DataTypes/AABB.ts b/src/DataTypes/AABB.ts new file mode 100644 index 0000000..736ed5b --- /dev/null +++ b/src/DataTypes/AABB.ts @@ -0,0 +1,103 @@ +import Vec2 from "./Vec2"; + +export default class AABB { + + protected center: Vec2; + protected halfSize: Vec2; + + constructor(center?: Vec2, halfSize?: Vec2){ + 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; + } + + getCenter(): Vec2 { + return this.center; + } + + setCenter(center: Vec2): void { + this.center = center; + } + + getHalfSize(): Vec2 { + return this.halfSize; + } + + setHalfSize(halfSize: Vec2): void { + this.halfSize = halfSize; + } + + /** + * A simple boolean check of whether this AABB contains a point + * @param 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 + } + + intersectPoint(point: Vec2): boolean { + let dx = point.x - this.x; + let px = this.hw - Math.abs(dx); + + if(px <= 0){ + return false; + } + + let dy = point.y - this.y; + let py = this.hh - Math.abs(dy); + + if(py <= 0){ + return false; + } + + return true; + } + + /** + * 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 + */ + containsPointSoft(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 overlaps another + * @param other + */ + overlaps(other: AABB): boolean { + let dx = other.x - this.x; + let px = this.hw + other.hw - Math.abs(dx); + + if(px <= 0){ + return false; + } + + let dy = other.y - this.y; + let py = this.hh + other.hh - Math.abs(dy); + + if(py <= 0){ + return false; + } + + return true; + } +} \ No newline at end of file diff --git a/src/DataTypes/Interfaces/Descriptors.ts b/src/DataTypes/Interfaces/Descriptors.ts new file mode 100644 index 0000000..2e05d44 --- /dev/null +++ b/src/DataTypes/Interfaces/Descriptors.ts @@ -0,0 +1,44 @@ +import AABB from "../AABB"; +import Vec2 from "../Vec2"; + +export interface Unique { + getId: () => number; +} + +export interface Positioned { + /** + * Returns the center of this object + */ + getPosition: () => Vec2; +} + +export interface Region { + /** + * Returns the size of this object + */ + getSize: () => Vec2; + + /** + * Returns the scale of this object + */ + getScale: () => Vec2; + + /** + * Returns the bounding box of this object + */ + getBoundary: () => AABB; +} + +export interface Updateable { + /** + * Updates this object + */ + update: (deltaT: number) => void; +} + +export interface Renderable { + /** + * Renders this object + */ + render: (ctx: CanvasRenderingContext2D) => void; +} diff --git a/src/DataTypes/QuadTree.ts b/src/DataTypes/QuadTree.ts new file mode 100644 index 0000000..1d7f78f --- /dev/null +++ b/src/DataTypes/QuadTree.ts @@ -0,0 +1,154 @@ +import Vec2 from "./Vec2"; +import Collection from "./Collection"; +import AABB from "./AABB" +import { Positioned } from "./Interfaces/Descriptors"; + +// TODO - Make max depth + +/** + * Primarily used to organize the scene graph + */ +export default class QuadTree implements Collection { + /** + * The center of this quadtree + */ + protected boundary: AABB; + + /** + * The number of elements this quadtree root can hold before splitting + */ + protected capacity: number; + + /** + * The maximum height of the quadtree from this root + */ + protected maxDepth: number; + + /** + * Represents whether the quadtree is a root or a leaf + */ + protected divided: boolean; + + /** + * The array of the items in this quadtree + */ + protected items: Array; + + // The child quadtrees of this one + protected nw: QuadTree; + protected ne: QuadTree; + protected sw: QuadTree; + protected se: QuadTree; + + constructor(center: Vec2, size: Vec2, maxDepth?: number, capacity?: number){ + this.boundary = new AABB(center, size); + this.maxDepth = maxDepth !== undefined ? maxDepth : 10 + this.capacity = capacity ? capacity : 10; + + // If we're at the bottom of the tree, don't set a max size + if(this.maxDepth === 0){ + this.capacity = Infinity; + } + + this.divided = false; + this.items = new Array(); + } + + /** + * Inserts a new item into this quadtree. Defers to children if this quadtree is divided + * or divides the quadtree if capacity is exceeded with this add. + * @param item The item to add to the quadtree + */ + insert(item: T){ + // If the item is inside of the bounds of this quadtree + if(this.boundary.containsPointSoft(item.getPosition())){ + if(this.divided){ + // Defer to the children + this.deferInsert(item); + } else if (this.items.length < this.capacity){ + // Add to this items list + this.items.push(item); + } else { + // We aren't divided, but are at capacity - divide + this.subdivide(); + this.deferInsert(item); + this.divided = true; + } + } + } + + /** + * Divides this quadtree up into 4 smaller ones - called through insert. + */ + protected subdivide(): void { + let x = this.boundary.x; + let y = this.boundary.y; + let hw = this.boundary.hw; + let hh = this.boundary.hh; + + this.nw = new QuadTree(new Vec2(x-hw/2, y-hh/2), new Vec2(hw/2, hh/2), this.maxDepth - 1); + this.ne = new QuadTree(new Vec2(x+hw/2, y-hh/2), new Vec2(hw/2, hh/2), this.maxDepth - 1) + this.sw = new QuadTree(new Vec2(x-hw/2, y+hh/2), new Vec2(hw/2, hh/2), this.maxDepth - 1) + this.se = new QuadTree(new Vec2(x+hw/2, y+hh/2), new Vec2(hw/2, hh/2), this.maxDepth - 1) + + this.distributeItems(); + } + + /** + * Distributes the items of this quadtree into its children. + */ + protected distributeItems(): void { + this.items.forEach(item => this.deferInsert(item)); + + // Delete the items from this array + this.items.forEach((item, index) => delete this.items[index]); + this.items.length = 0; + } + + /** + * Defers this insertion to the children of this quadtree + * @param item + */ + protected deferInsert(item: T): void { + this.nw.insert(item); + this.ne.insert(item); + this.sw.insert(item); + this.se.insert(item); + } + + /** + * Renders the quadtree for demo purposes. + * @param ctx + */ + public render_demo(ctx: CanvasRenderingContext2D): void { + ctx.strokeStyle = "#FF0000"; + ctx.strokeRect(this.boundary.x - this.boundary.hw, this.boundary.y - this.boundary.hh, 2*this.boundary.hw, 2*this.boundary.hh); + + if(this.divided){ + this.nw.render_demo(ctx); + this.ne.render_demo(ctx); + this.sw.render_demo(ctx); + this.se.render_demo(ctx); + } + } + + forEach(func: Function): void { + // If divided, send the call down + if(this.divided){ + this.nw.forEach(func); + this.ne.forEach(func); + this.sw.forEach(func); + this.se.forEach(func); + } else { + // Otherwise, iterate over items + for(let i = 0; i < this.items.length; i++){ + func(this.items[i]); + } + } + } + + clear(): void { + throw new Error("Method not implemented."); + } + +} \ No newline at end of file diff --git a/src/DataTypes/RegionQuadTree.ts b/src/DataTypes/RegionQuadTree.ts new file mode 100644 index 0000000..0afd642 --- /dev/null +++ b/src/DataTypes/RegionQuadTree.ts @@ -0,0 +1,257 @@ +import Vec2 from "./Vec2"; +import Collection from "./Collection"; +import AABB from "./AABB" +import { Region, Unique } from "./Interfaces/Descriptors"; +import Map from "./Map"; + +/** + * Primarily used to organize the scene graph + */ +export default class QuadTree implements Collection { + /** + * The center of this quadtree + */ + protected boundary: AABB; + + /** + * The number of elements this quadtree root can hold before splitting + */ + protected capacity: number; + + /** + * The maximum height of the quadtree from this root + */ + protected maxDepth: number; + + /** + * Represents whether the quadtree is a root or a leaf + */ + protected divided: boolean; + + /** + * The array of the items in this quadtree + */ + protected items: Array; + + // The child quadtrees of this one + protected nw: QuadTree; + protected ne: QuadTree; + protected sw: QuadTree; + protected se: QuadTree; + + constructor(center: Vec2, size: Vec2, maxDepth?: number, capacity?: number){ + this.boundary = new AABB(center, size); + this.maxDepth = maxDepth !== undefined ? maxDepth : 10 + this.capacity = capacity ? capacity : 10; + + // If we're at the bottom of the tree, don't set a max size + if(this.maxDepth === 0){ + this.capacity = Infinity; + } + + this.divided = false; + this.items = new Array(); + } + + /** + * Inserts a new item into this quadtree. Defers to children if this quadtree is divided + * or divides the quadtree if capacity is exceeded with this add. + * @param item The item to add to the quadtree + */ + insert(item: T): void { + // If the item is inside of the bounds of this quadtree + if(this.boundary.overlaps(item.getBoundary())){ + if(this.divided){ + // Defer to the children + this.deferInsert(item); + } else if (this.items.length < this.capacity){ + // Add to this items list + this.items.push(item); + } else { + // We aren't divided, but are at capacity - divide + this.subdivide(); + this.deferInsert(item); + this.divided = true; + } + } + } + + /** + * Returns all items at this point. + * @param point The point to query at + */ + queryPoint(point: Vec2): Array { + // A matrix to keep track of our results + let results = new Array(); + + // A map to keep track of the items we've already found + let uniqueMap = new Map(); + + // Query and return + this._queryPoint(point, results, uniqueMap); + return results; + } + + /** + * A recursive function called by queryPoint + * @param point The point being queried + * @param results The results matrix + * @param uniqueMap A map that stores the unique ids of the results so we know what was already found + */ + protected _queryPoint(point: Vec2, results: Array, uniqueMap: Map): void { + // Does this quadtree even contain the point? + if(!this.boundary.containsPointSoft(point)) return; + + // If the matrix is divided, ask its children for results + if(this.divided){ + this.nw._queryPoint(point, results, uniqueMap); + this.ne._queryPoint(point, results, uniqueMap); + this.sw._queryPoint(point, results, uniqueMap); + this.se._queryPoint(point, results, uniqueMap); + } else { + // Otherwise, return a set of the items + for(let item of this.items){ + let id = item.getId().toString(); + // If the item hasn't been found yet and it contains the point + if(!uniqueMap.has(id) && item.getBoundary().containsPoint(point)){ + // Add it to our found points + uniqueMap.add(id, item); + results.push(item); + } + } + } + } + +/** + * Returns all items at this point. + * @param point The point to query at + */ + queryRegion(boundary: AABB): Array { + // A matrix to keep track of our results + let results = new Array(); + + // A map to keep track of the items we've already found + let uniqueMap = new Map(); + + // Query and return + this._queryRegion(boundary, results, uniqueMap); + return results; + } + + /** + * A recursive function called by queryPoint + * @param point The point being queried + * @param results The results matrix + * @param uniqueMap A map that stores the unique ids of the results so we know what was already found + */ + protected _queryRegion(boundary: AABB, results: Array, uniqueMap: Map): void { + // Does this quadtree even contain the point? + if(!this.boundary.overlaps(boundary)) return; + + // If the matrix is divided, ask its children for results + if(this.divided){ + this.nw._queryRegion(boundary, results, uniqueMap); + this.ne._queryRegion(boundary, results, uniqueMap); + this.sw._queryRegion(boundary, results, uniqueMap); + this.se._queryRegion(boundary, results, uniqueMap); + } else { + // Otherwise, return a set of the items + for(let item of this.items){ + let id = item.getId().toString(); + // If the item hasn't been found yet and it contains the point + if(!uniqueMap.has(id) && item.getBoundary().overlaps(boundary)){ + // Add it to our found points + uniqueMap.add(id, item); + results.push(item); + } + } + } + } + + /** + * Divides this quadtree up into 4 smaller ones - called through insert. + */ + protected subdivide(): void { + let x = this.boundary.x; + let y = this.boundary.y; + let hw = this.boundary.hw; + let hh = this.boundary.hh; + + this.nw = new QuadTree(new Vec2(x-hw/2, y-hh/2), new Vec2(hw/2, hh/2), this.maxDepth - 1); + this.ne = new QuadTree(new Vec2(x+hw/2, y-hh/2), new Vec2(hw/2, hh/2), this.maxDepth - 1) + this.sw = new QuadTree(new Vec2(x-hw/2, y+hh/2), new Vec2(hw/2, hh/2), this.maxDepth - 1) + this.se = new QuadTree(new Vec2(x+hw/2, y+hh/2), new Vec2(hw/2, hh/2), this.maxDepth - 1) + + this.distributeItems(); + } + + /** + * Distributes the items of this quadtree into its children. + */ + protected distributeItems(): void { + this.items.forEach(item => this.deferInsert(item)); + + // Delete the items from this array + this.items.forEach((item, index) => delete this.items[index]); + this.items.length = 0; + } + + /** + * Defers this insertion to the children of this quadtree + * @param item + */ + protected deferInsert(item: T): void { + this.nw.insert(item); + this.ne.insert(item); + this.sw.insert(item); + this.se.insert(item); + } + + /** + * Renders the quadtree for demo purposes. + * @param ctx + */ + public render_demo(ctx: CanvasRenderingContext2D): void { + ctx.strokeStyle = "#0000FF"; + ctx.strokeRect(this.boundary.x - this.boundary.hw, this.boundary.y - this.boundary.hh, 2*this.boundary.hw, 2*this.boundary.hh); + + if(this.divided){ + this.nw.render_demo(ctx); + this.ne.render_demo(ctx); + this.sw.render_demo(ctx); + this.se.render_demo(ctx); + } + } + + forEach(func: Function): void { + // If divided, send the call down + if(this.divided){ + this.nw.forEach(func); + this.ne.forEach(func); + this.sw.forEach(func); + this.se.forEach(func); + } else { + // Otherwise, iterate over items + for(let i = 0; i < this.items.length; i++){ + func(this.items[i]); + } + } + } + + clear(): void { + delete this.nw; + delete this.ne; + delete this.sw; + delete this.se; + + for(let item in this.items){ + delete this.items[item]; + } + + this.items.length = 0; + + this.divided = false; + + } + +} \ No newline at end of file diff --git a/src/DataTypes/Vec2.ts b/src/DataTypes/Vec2.ts index 60ffe9f..751b311 100644 --- a/src/DataTypes/Vec2.ts +++ b/src/DataTypes/Vec2.ts @@ -6,6 +6,11 @@ export default class Vec2 { // Store x and y in an array private vec: Float32Array; + /** + * When this vector changes its value, do something + */ + private onChange: Function; + constructor(x: number = 0, y: number = 0) { this.vec = new Float32Array(2); this.vec[0] = x; @@ -19,6 +24,10 @@ export default class Vec2 { set x(x: number) { this.vec[0] = x; + + if(this.onChange){ + this.onChange(); + } } get y() { @@ -27,12 +36,20 @@ export default class Vec2 { set y(y: number) { this.vec[1] = y; + + if(this.onChange){ + this.onChange(); + } } static get ZERO() { return new Vec2(0, 0); } + static get UP() { + return new Vec2(0, -1); + } + /** * The squared magnitude of the vector */ @@ -68,6 +85,14 @@ export default class Vec2 { return this; } + /** + * Returns a vector that point from this vector to another one + * @param other + */ + vecTo(other: Vec2): Vec2 { + return new Vec2(other.x - this.x, other.y - this.y); + } + /** * Keeps the vector's direction, but sets its magnitude to be the provided magnitude * @param magnitude @@ -92,6 +117,15 @@ export default class Vec2 { return this; } + /** + * Returns a scaled version of this vector without modifying it. + * @param factor + * @param yFactor + */ + scaled(factor: number, yFactor: number = null): Vec2 { + return this.clone().scale(factor, yFactor); + } + /** * Rotates the vector counter-clockwise by the angle amount specified * @param angle The angle to rotate by in radians @@ -176,4 +210,12 @@ export default class Vec2 { clone(): Vec2 { return new Vec2(this.x, this.y); } + + /** + * Sets the function that is called whenever this vector is changed. + * @param f The function to be called + */ + setOnChange(f: Function): void { + this.onChange = f; + } } \ No newline at end of file diff --git a/src/Loop/GameLoop.ts b/src/Loop/GameLoop.ts index e8931fb..ee05000 100644 --- a/src/Loop/GameLoop.ts +++ b/src/Loop/GameLoop.ts @@ -8,7 +8,7 @@ import Viewport from "../SceneGraph/Viewport"; import SceneManager from "../Scene/SceneManager"; import AudioManager from "../Sound/AudioManager"; -export default class GameLoop{ +export default class GameLoop { // The amount of time to spend on a physics step private maxFPS: number; private simulationTimestep: number; @@ -45,7 +45,11 @@ export default class GameLoop{ private sceneManager: SceneManager; private audioManager: AudioManager; - constructor(){ + constructor(config?: object){ + // Typecast the config object to a GameConfig object + let gameConfig = config ? config : new GameConfig(); + console.log(gameConfig) + this.maxFPS = 60; this.simulationTimestep = Math.floor(1000/this.maxFPS); this.frame = 0; @@ -62,8 +66,8 @@ export default class GameLoop{ this.GAME_CANVAS.style.setProperty("background-color", "whitesmoke"); // Give the canvas a size and get the rendering context - this.WIDTH = 800; - this.HEIGHT = 500; + this.WIDTH = gameConfig.viewportSize ? gameConfig.viewportSize.x : 800; + this.HEIGHT = gameConfig.viewportSize ? gameConfig.viewportSize.y : 500; this.ctx = this.initializeCanvas(this.GAME_CANVAS, this.WIDTH, this.HEIGHT); // Size the viewport to the game canvas @@ -211,4 +215,8 @@ export default class GameLoop{ this.sceneManager.render(this.ctx); Debug.render(this.ctx); } +} + +class GameConfig { + viewportSize: {x: number, y: number} } \ No newline at end of file diff --git a/src/Nodes/CanvasNode.ts b/src/Nodes/CanvasNode.ts index f1caf8d..48e55c3 100644 --- a/src/Nodes/CanvasNode.ts +++ b/src/Nodes/CanvasNode.ts @@ -1,36 +1,48 @@ import GameNode from "./GameNode"; import Vec2 from "../DataTypes/Vec2"; +import { Region } from "../DataTypes/Interfaces/Descriptors"; +import AABB from "../DataTypes/AABB"; /** * The representation of an object in the game world that can be drawn to the screen */ -export default abstract class CanvasNode extends GameNode{ - protected size: Vec2; - protected scale: Vec2; +export default abstract class CanvasNode extends GameNode implements Region { + private _size: Vec2; + private _scale: Vec2; + private boundary: AABB; constructor(){ super(); - this.size = new Vec2(0, 0); - this.scale = new Vec2(1, 1); + this._size = new Vec2(0, 0); + this._size.setOnChange(this.sizeChanged); + this._scale = new Vec2(1, 1); + this._scale.setOnChange(this.scaleChanged); + this.boundary = new AABB(); + this.updateBoundary(); } - /** - * Returns the scale of the sprite - */ - getScale(): Vec2 { - return this.scale; - } + get size(): Vec2 { + return this._size; + } - /** - * Sets the scale of the sprite to the value provided - * @param scale - */ - setScale(scale: Vec2): void { - this.scale = scale; - } + set size(size: Vec2){ + this._size = size; + this._size.setOnChange(this.sizeChanged); + this.sizeChanged(); + } + + get scale(): Vec2 { + return this._scale; + } + + set scale(scale: Vec2){ + this._scale = scale; + this._scale.setOnChange(this.sizeChanged); + this.scaleChanged(); + } getSize(): Vec2 { - return this.size; + return this.size.clone(); } setSize(vecOrX: Vec2 | number, y: number = null): void { @@ -41,18 +53,49 @@ export default abstract class CanvasNode extends GameNode{ } } + /** + * Returns the scale of the sprite + */ + getScale(): Vec2 { + return this.scale.clone(); + } + + /** + * Sets the scale of the sprite to the value provided + * @param scale + */ + setScale(scale: Vec2): void { + this.scale = scale; + } + + positionChanged = (): void => { + this.updateBoundary(); + } + + sizeChanged = (): void => { + this.updateBoundary(); + } + + scaleChanged = (): void => { + this.updateBoundary(); + } + + private updateBoundary(): void { + this.boundary.setCenter(this.position.clone()); + this.boundary.setHalfSize(this.size.clone().mult(this.scale).scale(1/2)); + } + + getBoundary(): AABB { + return this.boundary; + } + /** * Returns true if the point (x, y) is inside of this canvas object * @param x * @param y */ contains(x: number, y: number): boolean { - if(this.position.x < x && this.position.x + this.size.x > x){ - if(this.position.y < y && this.position.y + this.size.y > y){ - return true; - } - } - return false; + return this.boundary.containsPoint(new Vec2(x, y)); } abstract render(ctx: CanvasRenderingContext2D): void; diff --git a/src/Nodes/GameNode.ts b/src/Nodes/GameNode.ts index 4864a1d..7647d75 100644 --- a/src/Nodes/GameNode.ts +++ b/src/Nodes/GameNode.ts @@ -5,21 +5,24 @@ import Receiver from "../Events/Receiver"; import Emitter from "../Events/Emitter"; import Scene from "../Scene/Scene"; import Layer from "../Scene/Layer"; +import { Positioned, Unique } from "../DataTypes/Interfaces/Descriptors" /** * The representation of an object in the game world */ -export default abstract class GameNode { +export default abstract class GameNode implements Positioned, Unique { protected input: InputReceiver; - protected position: Vec2; + private _position: Vec2; protected receiver: Receiver; protected emitter: Emitter; protected scene: Scene; protected layer: Layer; + private id: number; constructor(){ this.input = InputReceiver.getInstance(); - this.position = new Vec2(0, 0); + this._position = new Vec2(0, 0); + this._position.setOnChange(this.positionChanged); this.receiver = new Receiver(); this.emitter = new Emitter(); } @@ -40,8 +43,18 @@ export default abstract class GameNode { return this.layer; } + get position(): Vec2 { + return this._position; + } + + set position(pos: Vec2) { + this._position = pos; + this._position.setOnChange(this.positionChanged); + this.positionChanged(); + } + getPosition(): Vec2 { - return this.position; + return this._position.clone(); } setPosition(vecOrX: Vec2 | number, y: number = null): void { @@ -52,6 +65,19 @@ export default abstract class GameNode { } } + setId(id: number): void { + this.id = id; + } + + getId(): number { + return this.id; + } + + /** + * Called if the position vector is modified or replaced + */ + protected positionChanged(){} + // TODO - This doesn't seem ideal. Is there a better way to do this? protected getViewportOriginWithParallax(): Vec2 { return this.scene.getViewport().getPosition().clone().mult(this.layer.getParallax()); diff --git a/src/Nodes/Graphic.ts b/src/Nodes/Graphic.ts index 5182ef1..f5902e1 100644 --- a/src/Nodes/Graphic.ts +++ b/src/Nodes/Graphic.ts @@ -8,6 +8,11 @@ export default abstract class Graphic extends CanvasNode { protected color: Color; + constructor(){ + super(); + this.color = Color.RED; + } + setColor(color: Color){ this.color = color; } diff --git a/src/Nodes/Graphics/Point.ts b/src/Nodes/Graphics/Point.ts new file mode 100644 index 0000000..180c01d --- /dev/null +++ b/src/Nodes/Graphics/Point.ts @@ -0,0 +1,22 @@ +import Graphic from "../Graphic"; +import Vec2 from "../../DataTypes/Vec2"; + +export default class Point extends Graphic { + + constructor(position: Vec2){ + super(); + this.position = position; + this.setSize(5, 5); + } + + update(deltaT: number): void {} + + render(ctx: CanvasRenderingContext2D): void { + let origin = this.getViewportOriginWithParallax(); + + ctx.fillStyle = this.color.toStringRGBA(); + ctx.fillRect(this.position.x - origin.x - this.size.x/2, this.position.y - origin.y - this.size.y/2, + this.size.x, this.size.y); + } + +} \ No newline at end of file diff --git a/src/Nodes/Graphics/Rect.ts b/src/Nodes/Graphics/Rect.ts index b1534a4..17ff459 100644 --- a/src/Nodes/Graphics/Rect.ts +++ b/src/Nodes/Graphics/Rect.ts @@ -1,12 +1,26 @@ import Graphic from "../Graphic"; import Vec2 from "../../DataTypes/Vec2"; +import Color from "../../Utils/Color"; export default class Rect extends Graphic { + protected borderColor: Color; + protected borderWidth: number; + constructor(position: Vec2, size: Vec2){ super(); this.position = position; this.size = size; + this.borderColor = this.color; + this.borderWidth = 0; + } + + setBorderColor(color: Color){ + this.borderColor = color; + } + + setBorderWidth(width: number){ + this.borderWidth = width; } update(deltaT: number): void {} @@ -14,10 +28,14 @@ export default class Rect extends Graphic { render(ctx: CanvasRenderingContext2D): void { let origin = this.getViewportOriginWithParallax(); - console.log(origin.toFixed()); + if(this.color.a !== 0){ + ctx.fillStyle = this.color.toStringRGB(); + ctx.fillRect(this.position.x - this.size.x/2 - origin.x, this.position.y - this.size.y/2 - origin.y, this.size.x, this.size.y); + } - ctx.fillStyle = this.color.toStringRGBA(); - ctx.fillRect(this.position.x - origin.x, this.position.y - origin.y, this.size.x, this.size.y); + ctx.strokeStyle = this.borderColor.toStringRGB(); + ctx.lineWidth = this.borderWidth; + ctx.strokeRect(this.position.x - this.size.x/2 - origin.x, this.position.y - this.size.y/2 - origin.y, this.size.x, this.size.y); } } \ No newline at end of file diff --git a/src/Physics/Colliders/AABB.ts b/src/Physics/Colliders/AABBCollider.ts similarity index 86% rename from src/Physics/Colliders/AABB.ts rename to src/Physics/Colliders/AABBCollider.ts index d494264..b97829d 100644 --- a/src/Physics/Colliders/AABB.ts +++ b/src/Physics/Colliders/AABBCollider.ts @@ -1,10 +1,10 @@ import Collider from "./Collider"; import Vec2 from "../../DataTypes/Vec2"; -export default class AABB extends Collider { +export default class AABBCollider extends Collider { isCollidingWith(other: Collider): boolean { - if(other instanceof AABB){ + if(other instanceof AABBCollider){ if(other.position.x > this.position.x && other.position.x < this.position.x + this.size.x){ return other.position.y > this.position.y && other.position.y < this.position.y + this.size.y; } @@ -13,7 +13,7 @@ export default class AABB extends Collider { } willCollideWith(other: Collider, thisVel: Vec2, otherVel: Vec2): boolean { - if(other instanceof AABB){ + if(other instanceof AABBCollider){ let thisPos = new Vec2(this.position.x + thisVel.x, this.position.y + thisVel.y); let otherPos = new Vec2(other.position.x + otherVel.x, other.position.y + otherVel.y); diff --git a/src/Physics/StaticBody.ts b/src/Physics/StaticBody.ts index 1e06487..d2b3ab9 100644 --- a/src/Physics/StaticBody.ts +++ b/src/Physics/StaticBody.ts @@ -1,21 +1,16 @@ import PhysicsNode from "./PhysicsNode"; import Vec2 from "../DataTypes/Vec2"; -import AABB from "./Colliders/AABB"; +import AABBCollider from "./Colliders/AABBCollider"; export default class StaticBody extends PhysicsNode { - id: string; - static numCreated: number = 0; - constructor(position: Vec2, size: Vec2){ super(); this.setPosition(position.x, position.y); - this.collider = new AABB(); + this.collider = new AABBCollider(); this.collider.setPosition(position.x, position.y); this.collider.setSize(new Vec2(size.x, size.y)); - this.id = StaticBody.numCreated.toString(); this.moving = false; - StaticBody.numCreated += 1; } create(): void {} diff --git a/src/Player.ts b/src/Player.ts index 7f599f4..d29001b 100644 --- a/src/Player.ts +++ b/src/Player.ts @@ -1,7 +1,7 @@ import PhysicsNode from "./Physics/PhysicsNode"; import Vec2 from "./DataTypes/Vec2"; import Debug from "./Debug/Debug"; -import AABB from "./Physics/Colliders/AABB"; +import AABBCollider from "./Physics/Colliders/AABBCollider"; import CanvasNode from "./Nodes/CanvasNode"; import { GameEventType } from "./Events/GameEventType"; @@ -19,7 +19,7 @@ export default class Player extends PhysicsNode { this.velocity = new Vec2(0, 0); this.speed = 600; this.size = new Vec2(50, 50); - this.collider = new AABB(); + this.collider = new AABBCollider(); this.collider.setSize(this.size); this.position = new Vec2(0, 0); if(this.type === "topdown"){ diff --git a/src/QuadTreeScene.ts b/src/QuadTreeScene.ts new file mode 100644 index 0000000..f5e26be --- /dev/null +++ b/src/QuadTreeScene.ts @@ -0,0 +1,67 @@ +import Scene from "./Scene/Scene"; +import { GameEventType } from "./Events/GameEventType" +import Point from "./Nodes/Graphics/Point"; +import Rect from "./Nodes/Graphics/Rect"; +import Layer from "./Scene/Layer"; +import SceneGraphQuadTree from "./SceneGraph/SceneGraphQuadTree" +import Vec2 from "./DataTypes/Vec2"; +import InputReceiver from "./Input/InputReceiver"; +import Color from "./Utils/Color"; +import CanvasNode from "./Nodes/CanvasNode"; +import Graphic from "./Nodes/Graphic"; +import RandUtils from "./Utils/RandUtils"; + +export default class QuadTreeScene extends Scene { + + mainLayer: Layer; + view: Rect; + points: Array; + + loadScene(){} + + startScene(){ + // Make the scene graph a quadtree scenegraph + this.sceneGraph = new SceneGraphQuadTree(this.viewport, this); + + // Make a main layer + this.mainLayer = this.sceneGraph.addLayer(); + + // Generate a bunch of random points + this.points = []; + for(let i = 0; i < 1000; i++){ + let pos = new Vec2(500/3*(Math.random() + Math.random() + Math.random()), 500/3*(Math.random() + Math.random() + Math.random())); + let point = this.add.graphic(Point, this.mainLayer, pos); + point.setColor(Color.RED); + this.points.push(point); + } + + this.view = this.add.graphic(Rect, this.mainLayer, Vec2.ZERO, new Vec2(150, 100)); + this.view.setColor(Color.TRANSPARENT); + this.view.setBorderColor(Color.ORANGE); + } + + updateScene(deltaT: number): void { + this.view.setPosition(InputReceiver.getInstance().getGlobalMousePosition()); + for(let point of this.points){ + point.setColor(Color.RED); + + point.position.add(Vec2.UP.rotateCCW(Math.random()*2*Math.PI).add(point.position.vecTo(this.view.position).normalize().scale(0.1))); + } + + let results = this.sceneGraph.getNodesInRegion(this.view.getBoundary()); + + for(let result of results){ + if(result instanceof Point){ + result.setColor(Color.GREEN); + } + } + + results = this.sceneGraph.getNodesAt(this.view.position); + + for(let result of results){ + if(result instanceof Point){ + result.setColor(Color.YELLOW); + } + } + } +} \ No newline at end of file diff --git a/src/ResourceManager/ResourceManager.ts b/src/ResourceManager/ResourceManager.ts index 4aae2ed..47dd2b8 100644 --- a/src/ResourceManager/ResourceManager.ts +++ b/src/ResourceManager/ResourceManager.ts @@ -215,6 +215,11 @@ export default class ResourceManager { this.loadonly_tilemapsToLoad = this.loadonly_tilemapLoadingQueue.getSize(); this.loadonly_tilemapsLoaded = 0; + // If no items to load, we're finished + if(this.loadonly_tilemapsToLoad === 0){ + onFinishLoading(); + } + while(this.loadonly_tilemapLoadingQueue.hasItems()){ let tilemap = this.loadonly_tilemapLoadingQueue.dequeue(); this.loadTilemap(tilemap.key, tilemap.path, onFinishLoading); @@ -276,6 +281,11 @@ export default class ResourceManager { this.loadonly_imagesToLoad = this.loadonly_imageLoadingQueue.getSize(); this.loadonly_imagesLoaded = 0; + // If no items to load, we're finished + if(this.loadonly_imagesToLoad === 0){ + onFinishLoading(); + } + while(this.loadonly_imageLoadingQueue.hasItems()){ let image = this.loadonly_imageLoadingQueue.dequeue(); this.loadImage(image.key, image.path, onFinishLoading); @@ -323,6 +333,11 @@ export default class ResourceManager { this.loadonly_audioToLoad = this.loadonly_audioLoadingQueue.getSize(); this.loadonly_audioLoaded = 0; + // If no items to load, we're finished + if(this.loadonly_audioToLoad === 0){ + onFinishLoading(); + } + while(this.loadonly_audioLoadingQueue.hasItems()){ let audio = this.loadonly_audioLoadingQueue.dequeue(); this.loadAudio(audio.key, audio.path, onFinishLoading); @@ -390,10 +405,14 @@ export default class ResourceManager { public update(deltaT: number): void { if(this.loading){ - this.onLoadProgress(this.getLoadPercent()); + if(this.onLoadProgress){ + this.onLoadProgress(this.getLoadPercent()); + } } else if(this.justLoaded){ this.justLoaded = false; - this.onLoadComplete(); + if(this.onLoadComplete){ + this.onLoadComplete(); + } } } } \ No newline at end of file diff --git a/src/Scene/Factories/CanvasNodeFactory.ts b/src/Scene/Factories/CanvasNodeFactory.ts index 649c3f3..537eaaf 100644 --- a/src/Scene/Factories/CanvasNodeFactory.ts +++ b/src/Scene/Factories/CanvasNodeFactory.ts @@ -7,11 +7,9 @@ import Sprite from "../../Nodes/Sprites/Sprite"; export default class CanvasNodeFactory { private scene: Scene; - private sceneGraph: SceneGraph; - init(scene: Scene, sceneGraph: SceneGraph): void { + init(scene: Scene): void { this.scene = scene; - this.sceneGraph = sceneGraph; } /** @@ -25,7 +23,8 @@ export default class CanvasNodeFactory { // Add instance to scene instance.setScene(this.scene); - this.sceneGraph.addNode(instance); + instance.setId(this.scene.generateId()); + this.scene.getSceneGraph().addNode(instance); // Add instance to layer layer.addNode(instance); @@ -43,7 +42,8 @@ export default class CanvasNodeFactory { // Add instance to scene instance.setScene(this.scene); - this.sceneGraph.addNode(instance); + instance.setId(this.scene.generateId()); + this.scene.getSceneGraph().addNode(instance); // Add instance to layer layer.addNode(instance); @@ -62,7 +62,8 @@ export default class CanvasNodeFactory { // Add instance to scene instance.setScene(this.scene); - this.sceneGraph.addNode(instance); + instance.setId(this.scene.generateId()); + this.scene.getSceneGraph().addNode(instance); // Add instance to layer layer.addNode(instance); diff --git a/src/Scene/Factories/FactoryManager.ts b/src/Scene/Factories/FactoryManager.ts index 1f42d2f..86b57bf 100644 --- a/src/Scene/Factories/FactoryManager.ts +++ b/src/Scene/Factories/FactoryManager.ts @@ -13,8 +13,8 @@ export default class FactoryManager { private physicsNodeFactory: PhysicsNodeFactory = new PhysicsNodeFactory(); private tilemapFactory: TilemapFactory = new TilemapFactory(); - constructor(scene: Scene, sceneGraph: SceneGraph, physicsManager: PhysicsManager, tilemaps: Array){ - this.canvasNodeFactory.init(scene, sceneGraph); + constructor(scene: Scene, physicsManager: PhysicsManager, tilemaps: Array){ + this.canvasNodeFactory.init(scene); this.physicsNodeFactory.init(scene, physicsManager); this.tilemapFactory.init(scene, tilemaps, physicsManager); } diff --git a/src/Scene/Factories/PhysicsNodeFactory.ts b/src/Scene/Factories/PhysicsNodeFactory.ts index e15b7e2..21c2108 100644 --- a/src/Scene/Factories/PhysicsNodeFactory.ts +++ b/src/Scene/Factories/PhysicsNodeFactory.ts @@ -21,7 +21,8 @@ export default class PhysicsNodeFactory { */ add = (constr: new (...a: any) => T, layer: Layer, ...args: any): T => { let instance = new constr(...args); - instance.setScene(this.scene); + instance.setScene(this.scene); + instance.setId(this.scene.generateId()); instance.addManager(this.physicsManager); instance.create(); diff --git a/src/Scene/Factories/TilemapFactory.ts b/src/Scene/Factories/TilemapFactory.ts index 3cd5bcb..85d2b9c 100644 --- a/src/Scene/Factories/TilemapFactory.ts +++ b/src/Scene/Factories/TilemapFactory.ts @@ -71,6 +71,7 @@ export default class TilemapFactory { if(layer.type === "tilelayer"){ // Create a new tilemap object for the layer let tilemap = new constr(tilemapData, layer, tilesets); + tilemap.setId(this.scene.generateId()); tilemap.setScene(this.scene); // Add tilemap to scene diff --git a/src/Scene/Scene.ts b/src/Scene/Scene.ts index bf190ac..ce75d1b 100644 --- a/src/Scene/Scene.ts +++ b/src/Scene/Scene.ts @@ -41,8 +41,7 @@ export default class Scene{ public load: ResourceManager; constructor(viewport: Viewport, sceneManager: SceneManager, game: GameLoop){ - - this.worldSize = new Vec2(1600, 1000); + this.worldSize = new Vec2(500, 500); this.viewport = viewport; this.viewport.setBounds(0, 0, 2560, 1280); this.running = false; @@ -56,7 +55,7 @@ export default class Scene{ this.physicsManager = new PhysicsManager(); - this.add = new FactoryManager(this, this.sceneGraph, this.physicsManager, this.tilemaps); + this.add = new FactoryManager(this, this.physicsManager, this.tilemaps); this.load = ResourceManager.getInstance(); @@ -81,7 +80,7 @@ export default class Scene{ * Called every frame of the game. This is where you can dynamically do things like add in new enemies * @param delta */ - updateScene(delta: number): void {} + updateScene(deltaT: number): void {} /** * Updates all scene elements @@ -116,6 +115,9 @@ export default class Scene{ // We need to keep track of the order of things. let visibleSet = this.sceneGraph.getVisibleSet(); + // Render scene graph for demo + this.sceneGraph.render(ctx); + // Render tilemaps this.tilemaps.forEach(tilemap => { tilemap.render(ctx); @@ -146,4 +148,16 @@ export default class Scene{ getViewport(): Viewport { return this.viewport; } + + getWorldSize(): Vec2 { + return this.worldSize; + } + + getSceneGraph(): SceneGraph { + return this.sceneGraph; + } + + generateId(): number { + return this.sceneManager.generateId(); + } } \ No newline at end of file diff --git a/src/Scene/SceneManager.ts b/src/Scene/SceneManager.ts index aa3847b..c0c4c39 100644 --- a/src/Scene/SceneManager.ts +++ b/src/Scene/SceneManager.ts @@ -9,11 +9,13 @@ export default class SceneManager { private viewport: Viewport; private resourceManager: ResourceManager; private game: GameLoop; + private idCounter: number; constructor(viewport: Viewport, game: GameLoop){ this.resourceManager = ResourceManager.getInstance(); this.viewport = viewport; this.game = game; + this.idCounter = 0; } /** @@ -21,6 +23,7 @@ export default class SceneManager { * @param constr The constructor of the scene to add */ public addScene(constr: new (...args: any) => T): void { + console.log("Adding Scene"); let scene = new constr(this.viewport, this, this.game); this.currentScene = scene; @@ -28,7 +31,9 @@ export default class SceneManager { scene.loadScene(); // Load all assets + console.log("Starting Scene Load"); this.resourceManager.loadResourcesFromQueue(() => { + console.log("Starting Scene"); scene.startScene(); scene.setRunning(true); }); @@ -49,6 +54,10 @@ export default class SceneManager { this.addScene(constr); } + public generateId(): number { + return this.idCounter++; + } + public render(ctx: CanvasRenderingContext2D){ this.currentScene.render(ctx); } diff --git a/src/SceneGraph/SceneGraph.ts b/src/SceneGraph/SceneGraph.ts index 59e6580..40203e9 100644 --- a/src/SceneGraph/SceneGraph.ts +++ b/src/SceneGraph/SceneGraph.ts @@ -5,6 +5,7 @@ import Vec2 from "../DataTypes/Vec2"; import Scene from "../Scene/Scene"; import Layer from "../Scene/Layer"; import Stack from "../DataTypes/Stack"; +import AABB from "../DataTypes/AABB"; /** * An abstract interface of a SceneGraph. Exposes methods for use by other code, but leaves the implementation up to the subclasses. @@ -76,20 +77,22 @@ export default abstract class SceneGraph { * @param vecOrX * @param y */ - getNodeAt(vecOrX: Vec2 | number, y: number = null): CanvasNode { + getNodesAt(vecOrX: Vec2 | number, y: number = null): Array { if(vecOrX instanceof Vec2){ - return this.getNodeAtCoords(vecOrX.x, vecOrX.y); + return this.getNodesAtCoords(vecOrX.x, vecOrX.y); } else { - return this.getNodeAtCoords(vecOrX, y); + return this.getNodesAtCoords(vecOrX, y); } } + + abstract getNodesInRegion(boundary: AABB): Array; /** * The specific implementation of getting a node at certain coordinates * @param x * @param y */ - protected abstract getNodeAtCoords(x: number, y: number): CanvasNode; + protected abstract getNodesAtCoords(x: number, y: number): Array; addLayer(): Layer { let layer = new Layer(this.scene); @@ -103,7 +106,9 @@ export default abstract class SceneGraph { return this.layers; } - abstract update(deltaT: number): void; + abstract update(deltaT: number): void; + + abstract render(ctx: CanvasRenderingContext2D): void; /** * Gets the visible set of CanvasNodes based on the viewport diff --git a/src/SceneGraph/SceneGraphArray.ts b/src/SceneGraph/SceneGraphArray.ts index 9feb36e..ca05f82 100644 --- a/src/SceneGraph/SceneGraphArray.ts +++ b/src/SceneGraph/SceneGraphArray.ts @@ -4,6 +4,7 @@ import Viewport from "./Viewport"; import Scene from "../Scene/Scene"; import Stack from "../DataTypes/Stack"; import Layer from "../Scene/Layer" +import AABB from "../DataTypes/AABB"; export default class SceneGraphArray extends SceneGraph{ private nodeList: Array; @@ -31,14 +32,20 @@ export default class SceneGraphArray extends SceneGraph{ } } - getNodeAtCoords(x: number, y: number): CanvasNode { - // TODO: This only returns the first node found. There is no notion of z coordinates + getNodesAtCoords(x: number, y: number): Array { + let results = []; + for(let node of this.nodeList){ if(node.contains(x, y)){ - return node; + results.push(node); } } - return null; + + return results; + } + + getNodesInRegion(boundary: AABB): Array { + return []; } update(deltaT: number): void { @@ -49,6 +56,8 @@ export default class SceneGraphArray extends SceneGraph{ } } + render(ctx: CanvasRenderingContext2D): void {} + getVisibleSet(): Array { // If viewport culling is turned off for demonstration if(this.turnOffViewportCulling_demoTool){ diff --git a/src/SceneGraph/SceneGraphQuadTree.ts b/src/SceneGraph/SceneGraphQuadTree.ts new file mode 100644 index 0000000..2b6fcb1 --- /dev/null +++ b/src/SceneGraph/SceneGraphQuadTree.ts @@ -0,0 +1,80 @@ +import SceneGraph from "./SceneGraph"; +import CanvasNode from "../Nodes/CanvasNode"; +import Viewport from "./Viewport"; +import Scene from "../Scene/Scene"; +import RegionQuadTree from "../DataTypes/RegionQuadTree"; +import Vec2 from "../DataTypes/Vec2"; +import AABB from "../DataTypes/AABB"; + +export default class SceneGraphQuadTree extends SceneGraph { + private qt: RegionQuadTree; + private nodes: Array; + + constructor(viewport: Viewport, scene: Scene){ + super(viewport, scene); + + let size = this.scene.getWorldSize(); + this.qt = new RegionQuadTree(size.clone().scale(1/2), size.clone().scale(1/2), 5); + this.nodes = new Array(); + } + + addNodeSpecific(node: CanvasNode, id: string): void { + this.nodes.push(node); + } + + removeNodeSpecific(node: CanvasNode, id: string): void { + let index = this.nodes.indexOf(node); + if(index >= 0){ + this.nodes.splice(index, 1); + } + } + + getNodesAtCoords(x: number, y: number): Array { + return this.qt.queryPoint(new Vec2(x, y)); + } + + getNodesInRegion(boundary: AABB): Array { + return this.qt.queryRegion(boundary); + } + + update(deltaT: number): void { + this.qt.clear(); + + for(let node of this.nodes){ + this.qt.insert(node); + } + + this.qt.forEach((node: CanvasNode) => { + if(!node.getLayer().isPaused()){ + node.update(deltaT); + } + }); + } + + render(ctx: CanvasRenderingContext2D): void { + this.qt.render_demo(ctx); + } + + getVisibleSet(): Array { + let visibleSet = new Array(); + + // TODO - Currently just gets all of them + this.qt.forEach((node: CanvasNode) => { + if(!node.getLayer().isHidden() && this.viewport.includes(node)){ + visibleSet.push(node); + } + }); + + // Sort by depth, then by visible set by y-value + visibleSet.sort((a, b) => { + if(a.getLayer().getDepth() === b.getLayer().getDepth()){ + return (a.getPosition().y + a.getSize().y*a.getScale().y) + - (b.getPosition().y + b.getSize().y*b.getScale().y); + } else { + return a.getLayer().getDepth() - b.getLayer().getDepth(); + } + }); + + return visibleSet; + } +} \ No newline at end of file diff --git a/src/Utils/Color.ts b/src/Utils/Color.ts index 0c59cf7..4b7f0db 100644 --- a/src/Utils/Color.ts +++ b/src/Utils/Color.ts @@ -7,13 +7,52 @@ export default class Color { public b: number; public a: number; - constructor(r: number = 0, g: number = 0, b: number = 0, a: number = null){ + constructor(r: number = 0, g: number = 0, b: number = 0, a: number = 1){ this.r = r; this.g = g; this.b = b; this.a = a; } + static get TRANSPARENT(): Color { + return new Color(0, 0, 0, 0); + } + + static get RED(): Color { + return new Color(255, 0, 0, 1); + } + + static get GREEN(): Color { + return new Color(0, 255, 0, 1); + } + + static get BLUE(): Color { + return new Color(0, 0, 255, 1); + } + + static get YELLOW(): Color { + return new Color(255, 255, 0, 1); + } + + static get PURPLE(): Color { + return new Color(255, 0, 255, 1); + } + static get CYAN(): Color { + return new Color(0, 255, 255, 1); + } + + static get WHITE(): Color { + return new Color(255, 255, 255, 1); + } + + static get BLACK(): Color { + return new Color(0, 0, 0, 1); + } + + static get ORANGE(): Color { + return new Color(255, 100, 0, 1); + } + /** * Returns a new color slightly lighter than the current color */ @@ -46,7 +85,7 @@ export default class Color { * Returns the color as a string of the form rgba(r, g, b, a) */ toStringRGBA(): string { - if(this.a === null){ + if(this.a === 0){ return this.toStringRGB(); } return "rgba(" + this.r.toString() + ", " + this.g.toString() + ", " + this.b.toString() + ", " + this.a.toString() +")" diff --git a/src/Utils/MathUtils.ts b/src/Utils/MathUtils.ts index 551293c..aa1d27a 100644 --- a/src/Utils/MathUtils.ts +++ b/src/Utils/MathUtils.ts @@ -11,6 +11,16 @@ export default class MathUtils { return x; } + /** + * Linear Interpolation + * @param a The first value for the interpolation bound + * @param b The second value for the interpolation bound + * @param x The value we are interpolating + */ + static lerp(a: number, b: number, x: number){ + return a + x * (b - a); + } + /** * Returns the number as a hexadecimal * @param num The number to convert to hex diff --git a/src/Utils/Rand/Perlin.ts b/src/Utils/Rand/Perlin.ts new file mode 100644 index 0000000..69bb95d --- /dev/null +++ b/src/Utils/Rand/Perlin.ts @@ -0,0 +1,122 @@ +import MathUtils from "../MathUtils"; + +const permutation = [ 151,160,137,91,90,15, + 131,13,201,95,96,53,194,233,7,225,140,36,103,30,69,142,8,99,37,240,21,10,23, + 190, 6,148,247,120,234,75,0,26,197,62,94,252,219,203,117,35,11,32,57,177,33, + 88,237,149,56,87,174,20,125,136,171,168, 68,175,74,165,71,134,139,48,27,166, + 77,146,158,231,83,111,229,122,60,211,133,230,220,105,92,41,55,46,245,40,244, + 102,143,54, 65,25,63,161, 1,216,80,73,209,76,132,187,208, 89,18,169,200,196, + 135,130,116,188,159,86,164,100,109,198,173,186, 3,64,52,217,226,250,124,123, + 5,202,38,147,118,126,255,82,85,212,207,206,59,227,47,16,58,17,182,189,28,42, + 223,183,170,213,119,248,152, 2,44,154,163, 70,221,153,101,155,167, 43,172,9, + 129,22,39,253, 19,98,108,110,79,113,224,232,178,185, 112,104,218,246,97,228, + 251,34,242,193,238,210,144,12,191,179,162,241, 81,51,145,235,249,14,239,107, + 49,192,214, 31,181,199,106,157,184, 84,204,176,115,121,50,45,127, 4,150,254, + 138,236,205,93,222,114,67,29,24,72,243,141,128,195,78,66,215,61,156,180 +]; + +export default class Perlin { + + private p: Int16Array; + private repeat: number; + + constructor(){ + this.p = new Int16Array(512); + for(let i = 0; i < 512; i++){ + this.p[i] = permutation[i%256]; + } + this.repeat = -1; + } + + /** + * Returns a random perlin noise value + * @param x + * @param y + * @param z + */ + perlin(x: number, y: number, z: number = 0){ + if(this.repeat > 0) { + x = x%this.repeat; + y = y%this.repeat; + z = z%this.repeat; + } + + // Get the position of the unit cube of (x, y, z) + let xi = Math.floor(x) & 255; + let yi = Math.floor(y) & 255; + let zi = Math.floor(z) & 255; + // Get the position of (x, y, z) in that unit cube + let xf = x - Math.floor(x); + let yf = y - Math.floor(y); + let zf = z - Math.floor(z); + + // Use the fade function to relax the coordinates towards a whole value + let u = this.fade(xf); + let v = this.fade(yf); + let w = this.fade(zf); + + // Perlin noise hash function + let aaa = this.p[this.p[this.p[ xi ]+ yi ]+ zi ]; + let aba = this.p[this.p[this.p[ xi ]+this.inc(yi)]+ zi ]; + let aab = this.p[this.p[this.p[ xi ]+ yi ]+this.inc(zi)]; + let abb = this.p[this.p[this.p[ xi ]+this.inc(yi)]+this.inc(zi)]; + let baa = this.p[this.p[this.p[this.inc(xi)]+ yi ]+ zi ]; + let bba = this.p[this.p[this.p[this.inc(xi)]+this.inc(yi)]+ zi ]; + let bab = this.p[this.p[this.p[this.inc(xi)]+ yi ]+this.inc(zi)]; + let bbb = this.p[this.p[this.p[this.inc(xi)]+this.inc(yi)]+this.inc(zi)]; + + // Calculate the value of the perlin noies + let x1 = MathUtils.lerp(this.grad (aaa, xf , yf , zf), this.grad (baa, xf-1, yf , zf), u); + let x2 = MathUtils.lerp(this.grad (aba, xf , yf-1, zf), this.grad (bba, xf-1, yf-1, zf), u); + let y1 = MathUtils.lerp(x1, x2, v); + + x1 = MathUtils.lerp(this.grad (aab, xf , yf , zf-1), this.grad (bab, xf-1, yf , zf-1), u); + x2 = MathUtils.lerp(this.grad (abb, xf , yf-1, zf-1), this.grad (bbb, xf-1, yf-1, zf-1), u); + let y2 = MathUtils.lerp (x1, x2, v); + + return (MathUtils.lerp(y1, y2, w) + 1)/2; + } + + grad(hash: number, x: number, y: number, z: number){ + switch(hash & 0xF) + { + case 0x0: return x + y; + case 0x1: return -x + y; + case 0x2: return x - y; + case 0x3: return -x - y; + case 0x4: return x + z; + case 0x5: return -x + z; + case 0x6: return x - z; + case 0x7: return -x - z; + case 0x8: return y + z; + case 0x9: return -y + z; + case 0xA: return y - z; + case 0xB: return -y - z; + case 0xC: return y + x; + case 0xD: return -y + z; + case 0xE: return y - x; + case 0xF: return -y - z; + default: return 0; // never happens + } + } + + /** + * Safe increment that doesn't go beyond the repeat value + * @param num The number to increment + */ + inc(num: number){ + num++; + if(this.repeat > 0){ + num %= this.repeat; + } + return num; + } + + /** + * The fade function 6t^5 - 15t^4 + 10t^3 + * @param t The value we are applying the fade to + */ + fade(t: number){ + return t*t*t*(t*(t*6 - 15) + 10); + } +} \ No newline at end of file diff --git a/src/Utils/RandUtils.ts b/src/Utils/RandUtils.ts index d5642c7..3c1bebb 100644 --- a/src/Utils/RandUtils.ts +++ b/src/Utils/RandUtils.ts @@ -1,5 +1,14 @@ import MathUtils from "./MathUtils"; import Color from "./Color"; +import Perlin from "./Rand/Perlin"; + +class Noise { + p: Perlin = new Perlin(); + + perlin(x: number, y: number, z?: number): number { + return this.p.perlin(x, y, z); + } +} export default class RandUtils { /** @@ -29,4 +38,7 @@ export default class RandUtils { let b = RandUtils.randInt(0, 256); return new Color(r, g, b); } + + static noise: Noise = new Noise(); + } \ No newline at end of file diff --git a/src/main.ts b/src/main.ts index 83608a6..f699249 100644 --- a/src/main.ts +++ b/src/main.ts @@ -1,13 +1,13 @@ import GameLoop from "./Loop/GameLoop"; import {} from "./index"; -import MainScene from "./MainScene"; +import QuadTreeScene from "./QuadTreeScene"; function main(){ // Create the game object - let game = new GameLoop(); + let game = new GameLoop({viewportSize: {x: 500, y: 500}}); game.start(); let sm = game.getSceneManager(); - sm.addScene(MainScene); + sm.addScene(QuadTreeScene); } CanvasRenderingContext2D.prototype.roundedRect = function(x: number, y: number, w: number, h: number, r: number): void {