added quadtree scene graph and gave CanvasNodes boundaries
This commit is contained in:
parent
cf3c801bdb
commit
a32066468f
103
src/DataTypes/AABB.ts
Normal file
103
src/DataTypes/AABB.ts
Normal file
|
@ -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;
|
||||
}
|
||||
}
|
44
src/DataTypes/Interfaces/Descriptors.ts
Normal file
44
src/DataTypes/Interfaces/Descriptors.ts
Normal file
|
@ -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;
|
||||
}
|
154
src/DataTypes/QuadTree.ts
Normal file
154
src/DataTypes/QuadTree.ts
Normal file
|
@ -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<T extends Positioned> 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<T>;
|
||||
|
||||
// The child quadtrees of this one
|
||||
protected nw: QuadTree<T>;
|
||||
protected ne: QuadTree<T>;
|
||||
protected sw: QuadTree<T>;
|
||||
protected se: QuadTree<T>;
|
||||
|
||||
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.");
|
||||
}
|
||||
|
||||
}
|
257
src/DataTypes/RegionQuadTree.ts
Normal file
257
src/DataTypes/RegionQuadTree.ts
Normal file
|
@ -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<T extends Region & Unique> 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<T>;
|
||||
|
||||
// The child quadtrees of this one
|
||||
protected nw: QuadTree<T>;
|
||||
protected ne: QuadTree<T>;
|
||||
protected sw: QuadTree<T>;
|
||||
protected se: QuadTree<T>;
|
||||
|
||||
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<T> {
|
||||
// A matrix to keep track of our results
|
||||
let results = new Array<T>();
|
||||
|
||||
// A map to keep track of the items we've already found
|
||||
let uniqueMap = new Map<T>();
|
||||
|
||||
// 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<T>, uniqueMap: Map<T>): 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<T> {
|
||||
// A matrix to keep track of our results
|
||||
let results = new Array<T>();
|
||||
|
||||
// A map to keep track of the items we've already found
|
||||
let uniqueMap = new Map<T>();
|
||||
|
||||
// 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<T>, uniqueMap: Map<T>): 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;
|
||||
|
||||
}
|
||||
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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 ? <GameConfig>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
|
||||
|
@ -212,3 +216,7 @@ export default class GameLoop{
|
|||
Debug.render(this.ctx);
|
||||
}
|
||||
}
|
||||
|
||||
class GameConfig {
|
||||
viewportSize: {x: number, y: number}
|
||||
}
|
|
@ -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;
|
||||
|
|
|
@ -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());
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
22
src/Nodes/Graphics/Point.ts
Normal file
22
src/Nodes/Graphics/Point.ts
Normal file
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
|
@ -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);
|
||||
|
|
@ -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 {}
|
||||
|
|
|
@ -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"){
|
||||
|
|
67
src/QuadTreeScene.ts
Normal file
67
src/QuadTreeScene.ts
Normal file
|
@ -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<Point>;
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
|
|
|
@ -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<Tilemap>){
|
||||
this.canvasNodeFactory.init(scene, sceneGraph);
|
||||
constructor(scene: Scene, physicsManager: PhysicsManager, tilemaps: Array<Tilemap>){
|
||||
this.canvasNodeFactory.init(scene);
|
||||
this.physicsNodeFactory.init(scene, physicsManager);
|
||||
this.tilemapFactory.init(scene, tilemaps, physicsManager);
|
||||
}
|
||||
|
|
|
@ -21,7 +21,8 @@ export default class PhysicsNodeFactory {
|
|||
*/
|
||||
add = <T extends PhysicsNode>(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();
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
}
|
|
@ -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<T extends Scene>(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);
|
||||
}
|
||||
|
|
|
@ -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<CanvasNode> {
|
||||
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<CanvasNode>;
|
||||
|
||||
/**
|
||||
* 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<CanvasNode>;
|
||||
|
||||
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
|
||||
|
|
|
@ -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<CanvasNode>;
|
||||
|
@ -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<CanvasNode> {
|
||||
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<CanvasNode> {
|
||||
return [];
|
||||
}
|
||||
|
||||
update(deltaT: number): void {
|
||||
|
@ -49,6 +56,8 @@ export default class SceneGraphArray extends SceneGraph{
|
|||
}
|
||||
}
|
||||
|
||||
render(ctx: CanvasRenderingContext2D): void {}
|
||||
|
||||
getVisibleSet(): Array<CanvasNode> {
|
||||
// If viewport culling is turned off for demonstration
|
||||
if(this.turnOffViewportCulling_demoTool){
|
||||
|
|
80
src/SceneGraph/SceneGraphQuadTree.ts
Normal file
80
src/SceneGraph/SceneGraphQuadTree.ts
Normal file
|
@ -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<CanvasNode>;
|
||||
private nodes: Array<CanvasNode>;
|
||||
|
||||
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<CanvasNode> {
|
||||
return this.qt.queryPoint(new Vec2(x, y));
|
||||
}
|
||||
|
||||
getNodesInRegion(boundary: AABB): Array<CanvasNode> {
|
||||
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<CanvasNode> {
|
||||
let visibleSet = new Array<CanvasNode>();
|
||||
|
||||
// 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;
|
||||
}
|
||||
}
|
|
@ -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() +")"
|
||||
|
|
|
@ -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
|
||||
|
|
122
src/Utils/Rand/Perlin.ts
Normal file
122
src/Utils/Rand/Perlin.ts
Normal file
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
|
||||
}
|
|
@ -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 {
|
||||
|
|
Loading…
Reference in New Issue
Block a user