added quadtree scene graph and gave CanvasNodes boundaries

This commit is contained in:
Joe Weaver 2020-09-28 18:57:02 -04:00
parent cf3c801bdb
commit a32066468f
30 changed files with 1183 additions and 77 deletions

103
src/DataTypes/AABB.ts Normal file
View 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;
}
}

View 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
View 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.");
}
}

View 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;
}
}

View File

@ -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;
}
}

View File

@ -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
@ -211,4 +215,8 @@ export default class GameLoop{
this.sceneManager.render(this.ctx);
Debug.render(this.ctx);
}
}
class GameConfig {
viewportSize: {x: number, y: number}
}

View File

@ -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;

View File

@ -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());

View File

@ -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;
}

View 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);
}
}

View File

@ -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);
}
}

View File

@ -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);

View File

@ -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 {}

View File

@ -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
View 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);
}
}
}
}

View File

@ -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();
}
}
}
}

View File

@ -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);

View File

@ -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);
}

View File

@ -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();

View File

@ -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

View File

@ -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();
}
}

View File

@ -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);
}

View File

@ -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

View File

@ -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){

View 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;
}
}

View File

@ -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() +")"

View File

@ -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
View 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);
}
}

View File

@ -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();
}

View File

@ -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 {