diff --git a/src/BoidDemo.ts b/src/BoidDemo.ts index 558959b..22d57e3 100644 --- a/src/BoidDemo.ts +++ b/src/BoidDemo.ts @@ -35,7 +35,7 @@ export default class BoidDemo extends Scene { for(let i = 0; i < 150; i++){ let boid = this.add.graphic(Boid, layer, new Vec2(this.worldSize.x*Math.random(), this.worldSize.y*Math.random())); boid.fb = new FlockBehavior(this, boid, this.boids, 75, 50); - boid.setSize(5, 5); + boid.size.set(5, 5); this.boids.push(boid); } } diff --git a/src/DataTypes/Interfaces/Descriptors.ts b/src/DataTypes/Interfaces/Descriptors.ts index 2e05d44..4a5f119 100644 --- a/src/DataTypes/Interfaces/Descriptors.ts +++ b/src/DataTypes/Interfaces/Descriptors.ts @@ -1,44 +1,121 @@ -import AABB from "../AABB"; +import GameEvent from "../../Events/GameEvent"; +import Map from "../Map"; +import AABB from "../Shapes/AABB"; +import Shape from "../Shapes/Shape"; import Vec2 from "../Vec2"; export interface Unique { - getId: () => number; + /** The unique id of this object. */ + id: number; } export interface Positioned { - /** - * Returns the center of this object - */ - getPosition: () => Vec2; + /** The center of this object. */ + position: Vec2; } export interface Region { - /** - * Returns the size of this object - */ - getSize: () => Vec2; + /** The size of this object. */ + size: Vec2; + + /** The scale of this object. */ + scale: Vec2; + + /** The bounding box of this object. */ + boundary: AABB; +} + +export function isRegion(arg: any): boolean { + return arg && arg.size && arg.scale && arg.boundary; +} + +/** + * Describes an object that can opt into physics. + */ +export interface Physical { + /** A flag for whether or not this object has initialized game physics. */ + hasPhysics: boolean; + + /** Represents whether the object is moving or not. */ + moving: boolean; + + /** Represents whether the object is on the ground or not. */ + onGround: boolean; + + /** Reprsents whether the object is on the wall or not. */ + onWall: boolean; + + /** Reprsents whether the object is on the ceiling or not. */ + onCeiling: boolean; + + /** Represnts whether this object has active physics or not. */ + active: boolean; + + /** The shape of the collider for this physics object. */ + collisionShape: Shape; + + /** Represents whether this object can move or not. */ + isStatic: boolean; + + /** Represents whether this object is collidable (solid) or not. */ + isCollidable: boolean; + + /** Represnts whether this object is a trigger or not. */ + isTrigger: boolean; + + /** The physics group of this object. Used for triggers and for selective collisions. */ + group: string; + + /** Associates different groups with trigger events. */ + triggers: Map; + + /** A vector that allows velocity to be passed to the physics engine */ + _velocity: Vec2; + + /** The rectangle swept by the movement of this object, if dynamic */ + sweptRect: AABB; + + /*---------- FUNCTIONS ----------*/ /** - * Returns the scale of this object + * Tells the physics engine to handle a move by this object. + * @param velocity The velocity with which to move the object. */ - getScale: () => Vec2; + move: (velocity: Vec2) => void; /** - * Returns the bounding box of this object + * The move actually done by the physics engine after collision checks are done. + * @param velocity The velocity with which the object will move. */ - getBoundary: () => AABB; + finishMove: () => void; + + /** + * Adds physics to this object + * @param collisionShape The shape of this collider for this object + * @param isCollidable Whether this object will be able to collide with other objects + * @param isStatic Whether this object will be static or not + */ + addPhysics: (collisionShape?: Shape, isCollidable?: boolean, isStatic?: boolean) => void; + + /** + * Adds a trigger to this object for a specific group + * @param group The name of the group that activates the trigger + * @param eventType The name of the event to send when this trigger is activated + */ + addTrigger: (group: string, eventType: string) => void; } export interface Updateable { - /** - * Updates this object - */ + /** Updates this object. */ update: (deltaT: number) => void; } export interface Renderable { - /** - * Renders this object - */ + /** Renders this object. */ render: (ctx: CanvasRenderingContext2D) => void; } + +export interface Debug_Renderable { + /** Renders the debugging infor for this object. */ + debug_render: (ctx: CanvasRenderingContext2D) => void; +} diff --git a/src/DataTypes/RegionQuadTree.ts b/src/DataTypes/RegionQuadTree.ts index bdf8451..66560cf 100644 --- a/src/DataTypes/RegionQuadTree.ts +++ b/src/DataTypes/RegionQuadTree.ts @@ -1,6 +1,6 @@ import Vec2 from "./Vec2"; import Collection from "./Collection"; -import AABB from "./AABB" +import AABB from "./Shapes/AABB" import { Region, Unique } from "./Interfaces/Descriptors"; import Map from "./Map"; import Stats from "../Debug/Stats"; @@ -74,7 +74,7 @@ export default class QuadTree implements Collection { */ insert(item: T): void { // If the item is inside of the bounds of this quadtree - if(this.boundary.overlaps(item.getBoundary())){ + if(this.boundary.overlaps(item.boundary)){ if(this.divided){ // Defer to the children this.deferInsert(item); @@ -124,9 +124,9 @@ export default class QuadTree implements Collection { } else { // Otherwise, return a set of the items for(let item of this.items){ - let id = item.getId().toString(); + let id = item.id.toString(); // If the item hasn't been found yet and it contains the point - if(!uniqueMap.has(id) && item.getBoundary().containsPoint(point)){ + if(!uniqueMap.has(id) && item.boundary.containsPoint(point)){ // Add it to our found points uniqueMap.add(id, item); results.push(item); @@ -182,10 +182,10 @@ export default class QuadTree implements Collection { // } // Maybe this is better? Just use a boolean array with no string nonsense? - if(item.getId() >= uniqueMap.length || !uniqueMap[item.getId()]){ - if(item.getBoundary().overlaps(boundary)){ + if(item.id >= uniqueMap.length || !uniqueMap[item.id]){ + if(item.boundary.overlaps(boundary)){ results.push(item); - uniqueMap[item.getId()] = true; + uniqueMap[item.id] = true; } } } diff --git a/src/DataTypes/Shape.ts b/src/DataTypes/Shape.ts deleted file mode 100644 index 610a876..0000000 --- a/src/DataTypes/Shape.ts +++ /dev/null @@ -1,8 +0,0 @@ -import AABB from "./AABB"; -import Vec2 from "./Vec2"; - -export default abstract class Shape { - abstract setCenter(center: Vec2): void; - abstract getCenter(): Vec2; - abstract getBoundingRect(): AABB; -} \ No newline at end of file diff --git a/src/DataTypes/AABB.ts b/src/DataTypes/Shapes/AABB.ts similarity index 72% rename from src/DataTypes/AABB.ts rename to src/DataTypes/Shapes/AABB.ts index 4114f4c..d34c5c2 100644 --- a/src/DataTypes/AABB.ts +++ b/src/DataTypes/Shapes/AABB.ts @@ -1,11 +1,12 @@ import Shape from "./Shape"; -import Vec2 from "./Vec2"; -import MathUtils from "../Utils/MathUtils"; +import Vec2 from "../Vec2"; +import MathUtils from "../../Utils/MathUtils"; +import Circle from "./Circle"; export default class AABB extends Shape { - protected center: Vec2; - protected halfSize: Vec2; + center: Vec2; + halfSize: Vec2; constructor(center?: Vec2, halfSize?: Vec2){ super(); @@ -45,16 +46,13 @@ export default class AABB extends Shape { return this.x + this.hw; } - getCenter(): Vec2 { - return this.center; - } - - setCenter(center: Vec2): void { - this.center = center; - } - getBoundingRect(): AABB { - return this; + return this.clone(); + } + + getBoundingCircle(): Circle { + let r = Math.max(this.hw, this.hh) + return new Circle(this.center.clone(), r); } getHalfSize(): Vec2 { @@ -126,10 +124,10 @@ export default class AABB extends Shape { let signX = MathUtils.sign(scaleX); let signY = MathUtils.sign(scaleY); - let tnearx = scaleX*(this.center.x - signX*(this.halfSize.x + _paddingX) - point.x); - let tneary = scaleX*(this.center.y - signY*(this.halfSize.y + _paddingY) - point.y); - let tfarx = scaleY*(this.center.x + signX*(this.halfSize.x + _paddingX) - point.x); - let tfary = scaleY*(this.center.y + signY*(this.halfSize.y + _paddingY) - point.y); + let tnearx = scaleX*(this.x - signX*(this.hw + _paddingX) - point.x); + let tneary = scaleX*(this.y - signY*(this.hh + _paddingY) - point.y); + let tfarx = scaleY*(this.x + signX*(this.hw + _paddingX) - point.x); + let tfary = scaleY*(this.y + signY*(this.hh + _paddingY) - point.y); if(tnearx > tfary || tneary > tfarx){ // We aren't colliding - we clear one axis before intersecting another @@ -164,11 +162,18 @@ export default class AABB extends Shape { return hit; } + overlaps(other: Shape): boolean { + if(other instanceof AABB){ + return this.overlapsAABB(other); + } + throw "Overlap not defined between these shapes." + } + /** * A simple boolean check of whether this AABB overlaps another * @param other */ - overlaps(other: AABB): boolean { + overlapsAABB(other: AABB): boolean { let dx = other.x - this.x; let px = this.hw + other.hw - Math.abs(dx); @@ -200,6 +205,35 @@ export default class AABB extends Shape { return dx*dy; } + + /** + * Moves and resizes this rect from its current position to the position specified + * @param velocity The movement of the rect from its position + * @param fromPosition A position specified to be the starting point of sweeping + * @param halfSize The halfSize of the sweeping rect + */ + sweep(velocity: Vec2, fromPosition?: Vec2, halfSize?: Vec2): void { + if(!fromPosition){ + fromPosition = this.center; + } + + if(!halfSize){ + halfSize = this.halfSize; + } + + let centerX = fromPosition.x + velocity.x/2; + let centerY = fromPosition.y + velocity.y/2; + + let minX = Math.min(fromPosition.x - halfSize.x, fromPosition.x + velocity.x - halfSize.x); + let minY = Math.min(fromPosition.y - halfSize.y, fromPosition.y + velocity.y - halfSize.y); + + this.center.set(centerX, centerY); + this.halfSize.set(centerX - minX, centerY - minY); + } + + clone(): AABB { + return new AABB(this.center.clone(), this.halfSize.clone()); + } } export class Hit { diff --git a/src/DataTypes/Shapes/Circle.ts b/src/DataTypes/Shapes/Circle.ts new file mode 100644 index 0000000..c6e6f17 --- /dev/null +++ b/src/DataTypes/Shapes/Circle.ts @@ -0,0 +1,42 @@ +import Vec2 from "../Vec2"; +import AABB from "./AABB"; +import Shape from "./Shape"; + +export default class Circle extends Shape { + private _center: Vec2; + private radius: number; + + constructor(center: Vec2, radius: number) { + super(); + this._center = center ? center : new Vec2(0, 0); + this.radius = radius ? radius : 0; + } + + get center(): Vec2 { + return this._center; + } + + set center(center: Vec2) { + this._center = center; + } + + get halfSize(): Vec2 { + return new Vec2(this.radius, this.radius); + } + + getBoundingRect(): AABB { + return new AABB(this._center.clone(), new Vec2(this.radius, this.radius)); + } + + getBoundingCircle(): Circle { + return this.clone(); + } + + overlaps(other: Shape): boolean { + throw new Error("Method not implemented."); + } + + clone(): Circle { + return new Circle(this._center.clone(), this.radius); + } +} \ No newline at end of file diff --git a/src/DataTypes/Shapes/Shape.ts b/src/DataTypes/Shapes/Shape.ts new file mode 100644 index 0000000..ec2ecf8 --- /dev/null +++ b/src/DataTypes/Shapes/Shape.ts @@ -0,0 +1,111 @@ +import Vec2 from "../Vec2"; +import AABB from "./AABB"; +import Circle from "./Circle"; + +export default abstract class Shape { + abstract get center(): Vec2; + + abstract set center(center: Vec2); + + abstract get halfSize(): Vec2; + + /** Gets a bounding rectangle for this shape */ + abstract getBoundingRect(): AABB; + + /** Gets a bounding circle for this shape */ + abstract getBoundingCircle(): Circle; + + /** Returns a copy of this Shape */ + abstract clone(): Shape; + + /** Checks if this shape overlaps another */ + abstract overlaps(other: Shape): boolean; + + static getTimeOfCollision(A: Shape, velA: Vec2, B: Shape, velB: Vec2): [Vec2, Vec2, boolean, boolean] { + if(A instanceof AABB && B instanceof AABB){ + return Shape.getTimeOfCollision_AABB_AABB(A, velA, B, velB); + } + } + + private static getTimeOfCollision_AABB_AABB(A: AABB, velA: Vec2, B: Shape, velB: Vec2): [Vec2, Vec2, boolean, boolean] { + let posSmaller = A.center; + let posLarger = B.center; + let sizeSmaller = A.halfSize; + let sizeLarger = B.halfSize; + + let firstContact = new Vec2(0, 0); + let lastContact = new Vec2(0, 0); + + let collidingX = false; + let collidingY = false; + + // Sort by position + if(posLarger.x < posSmaller.x){ + // Swap, because smaller is further right than larger + let temp: Vec2; + temp = sizeSmaller; + sizeSmaller = sizeLarger; + sizeLarger = temp; + + temp = posSmaller; + posSmaller = posLarger; + posLarger = temp; + + temp = velA; + velA = velB; + velB = temp; + } + + // A is left, B is right + firstContact.x = Infinity; + lastContact.x = Infinity; + + if (posLarger.x - sizeLarger.x >= posSmaller.x + sizeSmaller.x){ + // If we aren't currently colliding + let relVel = velA.x - velB.x; + + if(relVel > 0){ + // If they are moving towards each other + firstContact.x = ((posLarger.x - sizeLarger.x) - (posSmaller.x + sizeSmaller.x))/(relVel); + lastContact.x = ((posLarger.x + sizeLarger.x) - (posSmaller.x - sizeSmaller.x))/(relVel); + } + } else { + collidingX = true; + } + + if(posLarger.y < posSmaller.y){ + // Swap, because smaller is further up than larger + let temp: Vec2; + temp = sizeSmaller; + sizeSmaller = sizeLarger; + sizeLarger = temp; + + temp = posSmaller; + posSmaller = posLarger; + posLarger = temp; + + temp = velA; + velA = velB; + velB = temp; + } + + // A is top, B is bottom + firstContact.y = Infinity; + lastContact.y = Infinity; + + if (posLarger.y - sizeLarger.y >= posSmaller.y + sizeSmaller.y){ + // If we aren't currently colliding + let relVel = velA.y - velB.y; + + if(relVel > 0){ + // If they are moving towards each other + firstContact.y = ((posLarger.y - sizeLarger.y) - (posSmaller.y + sizeSmaller.y))/(relVel); + lastContact.y = ((posLarger.y + sizeLarger.y) - (posSmaller.y - sizeSmaller.y))/(relVel); + } + } else { + collidingY = true; + } + + return [firstContact, lastContact, collidingX, collidingY]; + } +} \ No newline at end of file diff --git a/src/DataTypes/State/State.ts b/src/DataTypes/State/State.ts index 08055fa..969ab26 100644 --- a/src/DataTypes/State/State.ts +++ b/src/DataTypes/State/State.ts @@ -4,11 +4,11 @@ import { Updateable } from "../Interfaces/Descriptors"; import StateMachine from "./StateMachine"; export default abstract class State implements Updateable { - protected parentStateMachine: StateMachine; + protected parent: StateMachine; protected emitter: Emitter; constructor(parent: StateMachine) { - this.parentStateMachine = parent; + this.parent = parent; this.emitter = new Emitter(); } @@ -30,7 +30,7 @@ export default abstract class State implements Updateable { * @param stateName The name of the state to transition to */ protected finished(stateName: string): void { - this.parentStateMachine.changeState(stateName); + this.parent.changeState(stateName); } /** diff --git a/src/DataTypes/Vec2.ts b/src/DataTypes/Vec2.ts index 313b4fd..08c49e0 100644 --- a/src/DataTypes/Vec2.ts +++ b/src/DataTypes/Vec2.ts @@ -46,6 +46,8 @@ export default class Vec2 { return new Vec2(0, 0); } + static readonly ZERO_STATIC = new Vec2(0, 0); + static get INF() { return new Vec2(Infinity, Infinity); } diff --git a/src/MainScene.ts b/src/MainScene.ts index 88d68d5..982a7cb 100644 --- a/src/MainScene.ts +++ b/src/MainScene.ts @@ -1,6 +1,4 @@ import Scene from "./Scene/Scene"; -import OrthogonalTilemap from "./Nodes/Tilemaps/OrthogonalTilemap"; -import Player from "./Player"; import Rect from "./Nodes/Graphics/Rect"; import Color from "./Utils/Color"; import Vec2 from "./DataTypes/Vec2"; @@ -10,6 +8,7 @@ import Layer from "./Scene/Layer"; import SecondScene from "./SecondScene"; import { GameEventType } from "./Events/GameEventType"; import SceneGraphQuadTree from "./SceneGraph/SceneGraphQuadTree"; +import PlayerController from "./_DemoClasses/Player/PlayerStates/Platformer/PlayerController"; export default class MainScene extends Scene { @@ -27,7 +26,7 @@ export default class MainScene extends Scene { bar.setColor(new Color(0, 200, 200)); this.load.onLoadProgress = (percentProgress: number) => { - bar.setSize(295 * percentProgress, bar.getSize().y); + bar.size.x = 295 * percentProgress; } this.load.onLoadComplete = () => { @@ -58,40 +57,38 @@ export default class MainScene extends Scene { let mainLayer = this.addLayer(); // Add a player - let player = this.add.physics(Player, mainLayer, "platformer"); let playerSprite = this.add.sprite("player", mainLayer) - player.setSprite(playerSprite); - playerSprite.position = player.position.clone(); - playerSprite.setSize(new Vec2(64, 64)); + playerSprite.position.set(0, 0); + playerSprite.size.set(64, 64); - this.viewport.follow(player); + this.viewport.follow(playerSprite); // Initialize UI let uiLayer = this.addLayer(); uiLayer.setParallax(0, 0); let recordButton = this.add.uiElement(Button, uiLayer); - recordButton.setSize(100, 50); + recordButton.size.set(100, 50); recordButton.setText("Record"); - recordButton.setPosition(400, 30); + recordButton.position.set(400, 30); recordButton.onClickEventId = GameEventType.START_RECORDING; let stopButton = this.add.uiElement(Button, uiLayer); - stopButton.setSize(100, 50); + stopButton.size.set(100, 50); stopButton.setText("Stop"); - stopButton.setPosition(550, 30); + stopButton.position.set(550, 30); stopButton.onClickEventId = GameEventType.STOP_RECORDING; let playButton = this.add.uiElement(Button, uiLayer); - playButton.setSize(100, 50); + playButton.size.set(100, 50); playButton.setText("Play"); - playButton.setPosition(700, 30); + playButton.position.set(700, 30); playButton.onClickEventId = GameEventType.PLAY_RECORDING; let cycleFramerateButton = this.add.uiElement(Button, uiLayer); - cycleFramerateButton.setSize(150, 50); + cycleFramerateButton.size.set(150, 50); cycleFramerateButton.setText("Cycle FPS"); - cycleFramerateButton.setPosition(5, 400); + cycleFramerateButton.position.set(5, 400); let i = 0; let fps = [15, 30, 60]; cycleFramerateButton.onClick = () => { @@ -105,32 +102,32 @@ export default class MainScene extends Scene { pauseLayer.disable(); let pauseButton = this.add.uiElement(Button, uiLayer); - pauseButton.setSize(100, 50); + pauseButton.size.set(100, 50); pauseButton.setText("Pause"); - pauseButton.setPosition(700, 400); + pauseButton.position.set(700, 400); pauseButton.onClick = () => { this.sceneGraph.getLayers().forEach((layer: Layer) => layer.setPaused(true)); pauseLayer.enable(); } let modalBackground = this.add.uiElement(UIElement, pauseLayer); - modalBackground.setSize(400, 200); + modalBackground.size.set(400, 200); modalBackground.setBackgroundColor(new Color(0, 0, 0, 0.4)); - modalBackground.setPosition(200, 100); + modalBackground.position.set(200, 100); let resumeButton = this.add.uiElement(Button, pauseLayer); - resumeButton.setSize(100, 50); + resumeButton.size.set(100, 50); resumeButton.setText("Resume"); - resumeButton.setPosition(360, 150); + resumeButton.position.set(360, 150); resumeButton.onClick = () => { this.sceneGraph.getLayers().forEach((layer: Layer) => layer.setPaused(false)); pauseLayer.disable(); } let switchButton = this.add.uiElement(Button, pauseLayer); - switchButton.setSize(140, 50); + switchButton.size.set(140, 50); switchButton.setText("Change Scene"); - switchButton.setPosition(340, 190); + switchButton.position.set(340, 190); switchButton.onClick = () => { this.emitter.fireEvent(GameEventType.STOP_SOUND, {key: "level_music"}); this.sceneManager.changeScene(SecondScene); diff --git a/src/Nodes/CanvasNode.ts b/src/Nodes/CanvasNode.ts index 60993ef..f44cc1a 100644 --- a/src/Nodes/CanvasNode.ts +++ b/src/Nodes/CanvasNode.ts @@ -1,7 +1,7 @@ import GameNode from "./GameNode"; import Vec2 from "../DataTypes/Vec2"; import { Region } from "../DataTypes/Interfaces/Descriptors"; -import AABB from "../DataTypes/AABB"; +import AABB from "../DataTypes/Shapes/AABB"; /** * The representation of an object in the game world that can be drawn to the screen @@ -9,7 +9,7 @@ import AABB from "../DataTypes/AABB"; export default abstract class CanvasNode extends GameNode implements Region { private _size: Vec2; private _scale: Vec2; - private boundary: AABB; + private _boundary: AABB; constructor(){ super(); @@ -18,7 +18,7 @@ export default abstract class CanvasNode extends GameNode implements Region { this._size.setOnChange(this.sizeChanged); this._scale = new Vec2(1, 1); this._scale.setOnChange(this.scaleChanged); - this.boundary = new AABB(); + this._boundary = new AABB(); this.updateBoundary(); } @@ -42,34 +42,11 @@ export default abstract class CanvasNode extends GameNode implements Region { this.scaleChanged(); } - getSize(): Vec2 { - return this.size.clone(); - } - - setSize(vecOrX: Vec2 | number, y: number = null): void { - if(vecOrX instanceof Vec2){ - this.size.set(vecOrX.x, vecOrX.y); - } else { - this.size.set(vecOrX, y); - } - } - - /** - * 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; - } protected positionChanged = (): void => { + if(this.hasPhysics){ + this.collisionShape.center = this.position; + } this.updateBoundary(); } @@ -82,12 +59,12 @@ export default abstract class CanvasNode extends GameNode implements Region { } private updateBoundary(): void { - this.boundary.setCenter(this.position.clone()); - this.boundary.setHalfSize(this.size.clone().mult(this.scale).scale(1/2)); + this._boundary.center.set(this.position.x, this.position.y); + this._boundary.halfSize.set(this.size.x*this.scale.x/2, this.size.y*this.scale.y/2); } - getBoundary(): AABB { - return this.boundary; + get boundary(): AABB { + return this._boundary; } /** @@ -96,7 +73,7 @@ export default abstract class CanvasNode extends GameNode implements Region { * @param y */ contains(x: number, y: number): boolean { - return this.boundary.containsPoint(new Vec2(x, y)); + return this._boundary.containsPoint(new Vec2(x, y)); } abstract render(ctx: CanvasRenderingContext2D): void; diff --git a/src/Nodes/GameNode.ts b/src/Nodes/GameNode.ts index 915342f..f8e4d11 100644 --- a/src/Nodes/GameNode.ts +++ b/src/Nodes/GameNode.ts @@ -4,19 +4,44 @@ import Receiver from "../Events/Receiver"; import Emitter from "../Events/Emitter"; import Scene from "../Scene/Scene"; import Layer from "../Scene/Layer"; -import { Positioned, Unique, Updateable } from "../DataTypes/Interfaces/Descriptors" +import { Physical, Positioned, isRegion, Unique, Updateable, Region } from "../DataTypes/Interfaces/Descriptors" +import Shape from "../DataTypes/Shapes/Shape"; +import GameEvent from "../Events/GameEvent"; +import Map from "../DataTypes/Map"; +import AABB from "../DataTypes/Shapes/AABB"; /** * The representation of an object in the game world */ -export default abstract class GameNode implements Positioned, Unique, Updateable { - protected input: InputReceiver; +export default abstract class GameNode implements Positioned, Unique, Updateable, Physical { + /*---------- POSITIONED ----------*/ private _position: Vec2; + + /*---------- UNIQUE ----------*/ + private _id: number; + + /*---------- PHYSICAL ----------*/ + hasPhysics: boolean; + moving: boolean; + onGround: boolean; + onWall: boolean; + onCeiling: boolean; + active: boolean; + collisionShape: Shape; + isStatic: boolean; + isCollidable: boolean; + isTrigger: boolean; + group: string; + triggers: Map; + _velocity: Vec2; + sweptRect: AABB; + + protected input: InputReceiver; protected receiver: Receiver; protected emitter: Emitter; protected scene: Scene; protected layer: Layer; - private id: number; + constructor(){ this.input = InputReceiver.getInstance(); @@ -26,22 +51,7 @@ export default abstract class GameNode implements Positioned, Unique, Updateable this.emitter = new Emitter(); } - setScene(scene: Scene): void { - this.scene = scene; - } - - getScene(): Scene { - return this.scene; - } - - setLayer(layer: Layer): void { - this.layer = layer; - } - - getLayer(): Layer { - return this.layer; - } - + /*---------- POSITIONED ----------*/ get position(): Vec2 { return this._position; } @@ -52,30 +62,116 @@ export default abstract class GameNode implements Positioned, Unique, Updateable this.positionChanged(); } - getPosition(): Vec2 { - return this._position.clone(); + /*---------- UNIQUE ----------*/ + get id(): number { + return this._id; } - setPosition(vecOrX: Vec2 | number, y: number = null): void { - if(vecOrX instanceof Vec2){ - this.position.set(vecOrX.x, vecOrX.y); + set id(id: number) { + // id can only be set once + if(this._id === undefined){ + this._id = id; } else { - this.position.set(vecOrX, y); + throw "Attempted to assign id to object that already has id." } } - setId(id: number): void { - this.id = id; + /*---------- PHYSICAL ----------*/ + /** + * @param velocity The velocity with which to move the object. + */ + move = (velocity: Vec2): void => { + this.moving = true; + this._velocity = velocity; + }; + + /** + * @param velocity The velocity with which the object will move. + */ + finishMove = (): void => { + this.moving = false; + this.position.add(this._velocity); } - getId(): number { - return this.id; + /** + * @param collisionShape The collider for this object. If this has a region (implements Region), + * it will be used when no collision shape is specified (or if collision shape is null). + * @param isCollidable Whether this is collidable or not. True by default. + * @param isStatic Whether this is static or not. False by default + */ + addPhysics = (collisionShape?: Shape, isCollidable: boolean = true, isStatic: boolean = false): void => { + this.hasPhysics = true; + this.moving = false; + this.onGround = false; + this.onWall = false; + this.onCeiling= false; + this.active = true; + this.isCollidable = isCollidable; + this.isStatic = isStatic; + this.isTrigger = false; + this.group = ""; + this.triggers = new Map(); + this._velocity = Vec2.ZERO; + this.sweptRect = new AABB(); + + if(collisionShape){ + this.collisionShape = collisionShape; + } else if (isRegion(this)) { + // If the gamenode has a region and no other is specified, use that + this.collisionShape = (this).boundary.clone(); + } else { + throw "No collision shape specified for physics object." + } + + this.sweptRect = this.collisionShape.getBoundingRect(); + this.scene.getPhysicsManager().registerObject(this); + } + + /** + * @param group The name of the group that will activate the trigger + * @param eventType The type of this event to send when this trigger is activated + */ + addTrigger = (group: string, eventType: string): void => { + this.isTrigger = true; + this.triggers.add(group, eventType); + }; + + /*---------- GAME NODE ----------*/ + /** + * Sets the scene for this object. + * @param scene The scene this object belongs to. + */ + setScene(scene: Scene): void { + this.scene = scene; + } + + /** Gets the scene this object is in. */ + getScene(): Scene { + return this.scene; + } + + /** + * Sets the layer of this object. + * @param layer The layer this object will be on. + */ + setLayer(layer: Layer): void { + this.layer = layer; + } + + /** Returns the layer this object is on. */ + getLayer(): Layer { + return this.layer; } /** * Called if the position vector is modified or replaced */ - protected positionChanged = (): void => {}; + // TODO - For some reason this isn't recognized in the child class + protected positionChanged = (): void => { + if(this.hasPhysics){ + this.collisionShape.center = this.position; + } + }; // TODO - This doesn't seem ideal. Is there a better way to do this? getViewportOriginWithParallax(): Vec2 { @@ -86,6 +182,5 @@ export default abstract class GameNode implements Positioned, Unique, Updateable return this.scene.getViewport().getZoomLevel(); } - abstract update(deltaT: number): void; } \ No newline at end of file diff --git a/src/Nodes/Graphics/Point.ts b/src/Nodes/Graphics/Point.ts index 180c01d..e9366f8 100644 --- a/src/Nodes/Graphics/Point.ts +++ b/src/Nodes/Graphics/Point.ts @@ -6,7 +6,7 @@ export default class Point extends Graphic { constructor(position: Vec2){ super(); this.position = position; - this.setSize(5, 5); + this.size.set(5, 5); } update(deltaT: number): void {} diff --git a/src/Nodes/Sprites/Sprite.ts b/src/Nodes/Sprites/Sprite.ts index 374650e..98d6c6b 100644 --- a/src/Nodes/Sprites/Sprite.ts +++ b/src/Nodes/Sprites/Sprite.ts @@ -39,7 +39,7 @@ export default class Sprite extends CanvasNode { ctx.lineWidth = 4; ctx.strokeStyle = "#00FF00" - let b = this.getBoundary(); + let b = this.boundary; ctx.strokeRect(b.x - b.hw - origin.x, b.y - b.hh - origin.y, b.hw*2*zoom, b.hh*2*zoom); } } \ No newline at end of file diff --git a/src/Nodes/Tilemap.ts b/src/Nodes/Tilemap.ts index a31f2cc..62683ee 100644 --- a/src/Nodes/Tilemap.ts +++ b/src/Nodes/Tilemap.ts @@ -13,7 +13,6 @@ export default abstract class Tilemap extends GameNode { protected tileSize: Vec2; protected scale: Vec2; public data: Array; - public collidable: boolean; public visible: boolean; // TODO: Make this no longer be specific to Tiled @@ -48,14 +47,15 @@ export default abstract class Tilemap extends GameNode { this.scale = scale; } - isCollidable(): boolean { - return this.collidable; - } - isVisible(): boolean { return this.visible; } + /** Adds this tilemaps to the physics system */ + addPhysics = (): void => { + this.scene.getPhysicsManager().registerTilemap(this); + } + abstract getTileAt(worldCoords: Vec2): number; /** diff --git a/src/Nodes/Tilemaps/OrthogonalTilemap.ts b/src/Nodes/Tilemaps/OrthogonalTilemap.ts index 41d7e61..59f75b1 100644 --- a/src/Nodes/Tilemaps/OrthogonalTilemap.ts +++ b/src/Nodes/Tilemaps/OrthogonalTilemap.ts @@ -18,11 +18,11 @@ export default class OrthogonalTilemap extends Tilemap { this.tileSize.set(tilemapData.tilewidth, tilemapData.tileheight); this.data = layer.data; this.visible = layer.visible; - this.collidable = false; + this.isCollidable = false; if(layer.properties){ for(let item of layer.properties){ if(item.name === "Collidable"){ - this.collidable = item.value; + this.isCollidable = item.value; } } } @@ -64,7 +64,7 @@ export default class OrthogonalTilemap extends Tilemap { } // TODO - Currently, all tiles in a collidable layer are collidable - return this.data[index] !== 0 && this.collidable; + return this.data[index] !== 0 && this.isCollidable; } /** diff --git a/src/Nodes/UIElement.ts b/src/Nodes/UIElement.ts index 78ac054..c3c4bd1 100644 --- a/src/Nodes/UIElement.ts +++ b/src/Nodes/UIElement.ts @@ -201,7 +201,7 @@ export default class UIElement extends CanvasNode { ctx.lineWidth = 4; ctx.strokeStyle = "#00FF00" - let b = this.getBoundary(); + let b = this.boundary; ctx.strokeRect(b.x - b.hw - origin.x, b.y - b.hh - origin.y, b.hw*2, b.hh*2); } } \ No newline at end of file diff --git a/src/Physics/BasicPhysicsManager.ts b/src/Physics/BasicPhysicsManager.ts new file mode 100644 index 0000000..b7fe7b2 --- /dev/null +++ b/src/Physics/BasicPhysicsManager.ts @@ -0,0 +1,312 @@ +import { Physical } from "../DataTypes/Interfaces/Descriptors"; +import Vec2 from "../DataTypes/Vec2"; +import GameNode from "../Nodes/GameNode"; +import Tilemap from "../Nodes/Tilemap"; +import PhysicsManager from "./PhysicsManager"; +import BroadPhase from "./BroadPhaseAlgorithms/BroadPhase"; +import SweepAndPrune from "./BroadPhaseAlgorithms/SweepAndPrune"; +import Shape from "../DataTypes/Shapes/Shape"; +import MathUtils from "../Utils/MathUtils"; +import OrthogonalTilemap from "../Nodes/Tilemaps/OrthogonalTilemap"; +import Debug from "../Debug/Debug"; +import AABB from "../DataTypes/Shapes/AABB"; + +export default class BasicPhysicsManager extends PhysicsManager { + + /** The array of static nodes */ + protected staticNodes: Array; + + /** The array of dynamic nodes */ + protected dynamicNodes: Array; + + /** The array of tilemaps */ + protected tilemaps: Array; + + /** The broad phase collision detection algorithm used by this physics system */ + protected broadPhase: BroadPhase; + + constructor(){ + super(); + this.staticNodes = new Array(); + this.dynamicNodes = new Array(); + this.tilemaps = new Array(); + this.broadPhase = new SweepAndPrune(); + } + + /** + * Add a new physics object to be updated with the physics system + * @param node The node to be added to the physics system + */ + registerObject(node: GameNode): void { + if(node.isStatic){ + // Static and not collidable + this.staticNodes.push(node); + } else { + // Dynamic and not collidable + this.dynamicNodes.push(node); + } + this.broadPhase.addNode(node); + } + + /** + * Add a new tilemap to be updated with the physics system + * @param tilemap The tilemap to be added to the physics system + */ + registerTilemap(tilemap: Tilemap): void { + this.tilemaps.push(tilemap); + } + + /** + * Resolves a collision between two nodes, adjusting their velocities accordingly. + * @param node1 + * @param node2 + * @param firstContact + * @param lastContact + * @param collidingX + * @param collidingY + */ + resolveCollision(node1: Physical, node2: Physical, firstContact: Vec2, lastContact: Vec2, collidingX: boolean, collidingY: boolean): void { + // Handle collision + if( (firstContact.x < 1 || collidingX) && (firstContact.y < 1 || collidingY)){ + // We are colliding. Check for any triggers + let group1 = node1.group; + let group2 = node2.group; + + // TODO - This is problematic if a collision happens, but it is later learned that another collision happens before it + if(node1.triggers.has(group2)){ + // Node1 should send an event + let eventType = node1.triggers.get(group2); + this.emitter.fireEvent(eventType, {node: node1, other: node2}); + } + + if(node2.triggers.has(group1)){ + // Node2 should send an event + let eventType = node2.triggers.get(group1); + this.emitter.fireEvent(eventType, {node: node2, other: node1}); + } + + if(collidingX && collidingY){ + // If we're already intersecting, freak out I guess? Probably should handle this in some way for if nodes get spawned inside of tiles + } else if(node1.isCollidable && node2.isCollidable) { + // We aren't already colliding, and both nodes can collide, so this is a new collision. + + // Get the amount to scale x and y based on their initial collision times + let xScale = MathUtils.clamp(firstContact.x, 0, 1); + let yScale = MathUtils.clamp(firstContact.y, 0, 1); + + // Handle special case of stickiness on perfect corner to corner collisions + if(xScale === yScale){ + xScale = 1; + } + + // Handle being stopped moving in the y-direction + if(yScale !== 1){ + // Figure out which node is on top + let node1onTop = node1.collisionShape.center.y < node2.collisionShape.center.y; + + // If either is moving, set their onFloor and onCeiling appropriately + if(!node1.isStatic && node1.moving){ + node1.onGround = node1onTop; + node1.onCeiling = !node1onTop; + } + + if(!node2.isStatic && node2.moving){ + node1.onGround = !node1onTop; + node1.onCeiling = node1onTop; + } + } + + // Handle being stopped moving in the x-direction + if(xScale !== 1){ + // If either node is non-static and moving, set its onWall to true + if(!node1.isStatic && node1.moving){ + node1.onWall = true; + } + + if(!node2.isStatic && node2.moving){ + node2.onWall = true; + } + } + + // Scale velocity for either node if it is moving + if(!node1.isStatic && node1.moving){ + node1._velocity.scale(xScale, yScale); + } + + if(!node2.isStatic && node2.moving){ + node2._velocity.scale(xScale, yScale); + } + } + } + } + + collideWithTilemap(node: Physical, tilemap: Tilemap, velocity: Vec2): void { + if(tilemap instanceof OrthogonalTilemap){ + this.collideWithOrthogonalTilemap(node, tilemap, velocity); + } + } + + collideWithOrthogonalTilemap(node: Physical, tilemap: OrthogonalTilemap, velocity: Vec2): void { + // Get the starting position, ending position, and size of the node + let startPos = node.collisionShape.center; + let endPos = startPos.clone().add(velocity); + let size = node.collisionShape.halfSize; + + // Get the min and max x and y coordinates of the moving node + let min = new Vec2(Math.min(startPos.x - size.x, endPos.x - size.x), Math.min(startPos.y - size.y, endPos.y - size.y)); + let max = new Vec2(Math.max(startPos.x + size.x, endPos.x + size.x), Math.max(startPos.y + size.y, endPos.y + size.y)); + + // Convert the min/max x/y to the min and max row/col in the tilemap array + let minIndex = tilemap.getColRowAt(min); + let maxIndex = tilemap.getColRowAt(max); + + // Create an empty set of tilemap collisions (We'll handle all of them at the end) + let tilemapCollisions = new Array(); + let tileSize = tilemap.getTileSize(); + + Debug.log("tilemapCollision", ""); + + // Loop over all possible tiles (which isn't many in the scope of the velocity per frame) + for(let col = minIndex.x; col <= maxIndex.x; col++){ + for(let row = minIndex.y; row <= maxIndex.y; row++){ + if(tilemap.isTileCollidable(col, row)){ + Debug.log("tilemapCollision", "Colliding with Tile"); + + // Get the position of this tile + let tilePos = new Vec2(col * tileSize.x + tileSize.x/2, row * tileSize.y + tileSize.y/2); + + // Create a new collider for this tile + let collider = new AABB(tilePos, tileSize.scaled(1/2)); + + // Calculate collision area between the node and the tile + let dx = Math.min(startPos.x, tilePos.x) - Math.max(startPos.x + size.x, tilePos.x + size.x); + let dy = Math.min(startPos.y, tilePos.y) - Math.max(startPos.y + size.y, tilePos.y + size.y); + + // If we overlap, how much do we overlap by? + let overlap = 0; + if(dx * dy > 0){ + overlap = dx * dy; + } + + tilemapCollisions.push(new TileCollisionData(collider, overlap)); + } + } + } + + // Now that we have all collisions, sort by collision area highest to lowest + tilemapCollisions = tilemapCollisions.sort((a, b) => a.overlapArea - b.overlapArea); + + let areas = ""; + tilemapCollisions.forEach(col => areas += col.overlapArea + ", ") + Debug.log("cols", areas) + + // Resolve the collisions in order of collision area (i.e. "closest" tiles are collided with first, so we can slide along a surface of tiles) + tilemapCollisions.forEach(collision => { + let [firstContact, _, collidingX, collidingY] = Shape.getTimeOfCollision(node.collisionShape, velocity, collision.collider, Vec2.ZERO); + + // Handle collision + if( (firstContact.x < 1 || collidingX) && (firstContact.y < 1 || collidingY)){ + if(collidingX && collidingY){ + // If we're already intersecting, freak out I guess? Probably should handle this in some way for if nodes get spawned inside of tiles + } else { + // Get the amount to scale x and y based on their initial collision times + let xScale = MathUtils.clamp(firstContact.x, 0, 1); + let yScale = MathUtils.clamp(firstContact.y, 0, 1); + + // Handle special case of stickiness on perfect corner to corner collisions + if(xScale === yScale){ + xScale = 1; + } + + if(yScale !== 1){ + // If the tile is below us + if(collision.collider.y > node.collisionShape.center.y){ + node.onGround = true; + } else { + node.onCeiling = true; + } + } + + if(xScale !== 1){ + node.onWall = true; + } + + // Scale the velocity of the node + velocity.scale(xScale, yScale); + } + } + }) + } + + + update(deltaT: number): void { + /*---------- INITIALIZATION PHASE ----------*/ + for(let node of this.dynamicNodes){ + // Clear frame dependent boolean values for each node + node.onGround = false; + node.onCeiling = false; + node.onWall = false; + + // Update the swept shapes of each node + if(node.moving){ + // Round Velocity + node._velocity.x = Math.round(node._velocity.x*1000)/1000; + node._velocity.y = Math.round(node._velocity.y*1000)/1000; + + // If moving, reflect that in the swept shape + node.sweptRect.sweep(node._velocity, node.collisionShape.center, node.collisionShape.halfSize); + } else { + node.sweptRect.sweep(Vec2.ZERO_STATIC, node.collisionShape.center, node.collisionShape.halfSize); + } + } + + /*---------- BROAD PHASE ----------*/ + // Get a potentially colliding set + let potentialCollidingPairs = this.broadPhase.runAlgorithm(); + + + // TODO - Should I be getting all collisions first, sorting by the time they happen, the resolving them? + /*---------- NARROW PHASE ----------*/ + for(let pair of potentialCollidingPairs){ + let node1 = pair[0]; + let node2 = pair[1]; + + // Get Collision (which may or may not happen) + let [firstContact, lastContact, collidingX, collidingY] = Shape.getTimeOfCollision(node1.collisionShape, node1._velocity, node2.collisionShape, node2._velocity); + + this.resolveCollision(node1, node2, firstContact, lastContact, collidingX, collidingY); + } + + /*---------- TILEMAP PHASE ----------*/ + for(let node of this.dynamicNodes){ + if(node.moving && node.isCollidable){ + // If a node is moving and can collide, check it against every tilemap + for(let tilemap of this.tilemaps){ + this.collideWithTilemap(node, tilemap, node._velocity); + } + } + } + + /*---------- ENDING PHASE ----------*/ + for(let node of this.dynamicNodes){ + if(node.moving){ + node.finishMove(); + } + } + } + + debug_render(ctx: CanvasRenderingContext2D): void { + + } +} + +// Collision data objects for tilemaps +class TileCollisionData { + collider: AABB; + overlapArea: number; + + constructor(collider: AABB, overlapArea: number){ + this.collider = collider; + this.overlapArea = overlapArea; + } +} \ No newline at end of file diff --git a/src/Physics/BroadPhaseAlgorithms/BroadPhase.ts b/src/Physics/BroadPhaseAlgorithms/BroadPhase.ts new file mode 100644 index 0000000..de0bbd9 --- /dev/null +++ b/src/Physics/BroadPhaseAlgorithms/BroadPhase.ts @@ -0,0 +1,11 @@ +import { Physical } from "../../DataTypes/Interfaces/Descriptors"; +import GameNode from "../../Nodes/GameNode"; + +export default abstract class BroadPhase { + /** + * Runs the algorithm and returns an array of possible collision pairs. + */ + abstract runAlgorithm(): Array; + + abstract addNode(node: GameNode): void; +} \ No newline at end of file diff --git a/src/Physics/BroadPhaseAlgorithms/SweepAndPrune.ts b/src/Physics/BroadPhaseAlgorithms/SweepAndPrune.ts new file mode 100644 index 0000000..24fe704 --- /dev/null +++ b/src/Physics/BroadPhaseAlgorithms/SweepAndPrune.ts @@ -0,0 +1,69 @@ +import { Physical } from "../../DataTypes/Interfaces/Descriptors"; +import PhysicsUtils from "../../Utils/PhysicsUtils"; +import SortingUtils from "../../Utils/SortingUtils"; +import BroadPhase from "./BroadPhase"; + +export default class SweepAndPrune extends BroadPhase { + protected xList: Array; + protected yList: Array; + + constructor(){ + super(); + this.xList = new Array(); + this.yList = new Array(); + } + + addNode(node: Physical): void { + this.xList.push(node); + this.yList.push(node); + } + + // TODO - Can optimize further by doing a callback whenever a swap occurs + // TODO - And by using better pair management + runAlgorithm(): Array { + // Sort the xList + SortingUtils.insertionSort(this.xList, (a, b) => (a.sweptRect.left - b.sweptRect.left) ); + + let xCollisions = []; + for(let i = 0; i < this.xList.length; i++){ + let node = this.xList[i]; + + let index = 1; + while(i + index < this.xList.length && node.sweptRect.right > this.xList[i + index].sweptRect.left){ + // Colliding pair in x-axis + xCollisions.push([node, this.xList[i + index]]); + index++; + } + } + + // Sort the y-list + SortingUtils.insertionSort(this.yList, (a, b) => (a.sweptRect.top - b.sweptRect.top) ); + + let yCollisions = []; + for(let i = 0; i < this.yList.length; i++){ + let node = this.yList[i]; + + let index = 1; + while(i + index < this.yList.length && node.sweptRect.bottom > this.yList[i + index].sweptRect.top){ + // Colliding pair in y-axis + yCollisions.push([node, this.yList[i + index]]); + index++; + } + } + + // Check the pairs + let collisions = [] + for(let xPair of xCollisions){ + for(let yPair of yCollisions){ + if((xPair[0] === yPair[0] && xPair[1] === yPair[1]) + ||(xPair[0] === yPair[1] && xPair[1] === yPair[0])){ + // Colliding in both axes, add to set + collisions.push(xPair); + } + } + } + + return collisions; + } + +} \ No newline at end of file diff --git a/src/Physics/Colliders/Collider.ts b/src/Physics/Colliders/Collider.ts deleted file mode 100644 index b2030f9..0000000 --- a/src/Physics/Colliders/Collider.ts +++ /dev/null @@ -1,39 +0,0 @@ -import AABB from "../../DataTypes/AABB"; -import { Positioned } from "../../DataTypes/Interfaces/Descriptors"; -import Shape from "../../DataTypes/Shape"; -import Vec2 from "../../DataTypes/Vec2"; - -export default class Collider implements Positioned { - protected shape: Shape; - - constructor(shape: Shape){ - this.shape = shape; - } - - setPosition(position: Vec2): void { - this.shape.setCenter(position); - } - - getPosition(): Vec2 { - return this.shape.getCenter(); - } - - getBoundingRect(): AABB { - return this.shape.getBoundingRect(); - } - - /** - * Sets the collision shape for this collider. - * @param shape - */ - setCollisionShape(shape: Shape): void { - this.shape = shape; - } - - /** - * Returns the collision shape this collider has - */ - getCollisionShape(): Shape { - return this.shape; - } -} \ No newline at end of file diff --git a/src/Physics/Colliders/Collisions.ts b/src/Physics/Colliders/Collisions.ts deleted file mode 100644 index e0d8221..0000000 --- a/src/Physics/Colliders/Collisions.ts +++ /dev/null @@ -1,97 +0,0 @@ -import Shape from "../../DataTypes/Shape"; -import AABB from "../../DataTypes/AABB"; -import Vec2 from "../../DataTypes/Vec2"; -import Collider from "./Collider"; -import Debug from "../../Debug/Debug"; - -export function getTimeOfCollision(A: Collider, velA: Vec2, B: Collider, velB: Vec2): [Vec2, Vec2, boolean, boolean] { - let shapeA = A.getCollisionShape(); - let shapeB = B.getCollisionShape(); - - if(shapeA instanceof AABB && shapeB instanceof AABB){ - return getTimeOfCollision_AABB_AABB(shapeA, velA, shapeB, velB); - } -} - -// TODO - Make this work with centered points to avoid this initial calculation -function getTimeOfCollision_AABB_AABB(A: AABB, velA: Vec2, B: AABB, velB: Vec2): [Vec2, Vec2, boolean, boolean] { - let posA = A.getCenter().clone(); - let posB = B.getCenter().clone(); - let sizeA = A.getHalfSize(); - let sizeB = B.getHalfSize(); - - let firstContact = new Vec2(0, 0); - let lastContact = new Vec2(0, 0); - - let collidingX = false; - let collidingY = false; - - // Sort by position - if(posB.x < posA.x){ - // Swap, because B is to the left of A - let temp: Vec2; - temp = sizeA; - sizeA = sizeB; - sizeB = temp; - - temp = posA; - posA = posB; - posB = temp; - - temp = velA; - velA = velB; - velB = temp; - } - - // A is left, B is right - firstContact.x = Infinity; - lastContact.x = Infinity; - - if (posB.x - sizeB.x >= posA.x + sizeA.x){ - // If we aren't currently colliding - let relVel = velA.x - velB.x; - - if(relVel > 0){ - // If they are moving towards each other - firstContact.x = ((posB.x - sizeB.x) - (posA.x + sizeA.x))/(relVel); - lastContact.x = ((posB.x + sizeB.x) - (posA.x - sizeA.x))/(relVel); - } - } else { - collidingX = true; - } - - if(posB.y < posA.y){ - // Swap, because B is above A - let temp: Vec2; - temp = sizeA; - sizeA = sizeB; - sizeB = temp; - - temp = posA; - posA = posB; - posB = temp; - - temp = velA; - velA = velB; - velB = temp; - } - - // A is top, B is bottom - firstContact.y = Infinity; - lastContact.y = Infinity; - - if (posB.y - sizeB.y >= posA.y + sizeA.y){ - // If we aren't currently colliding - let relVel = velA.y - velB.y; - - if(relVel > 0){ - // If they are moving towards each other - firstContact.y = ((posB.y - sizeB.y) - (posA.y + sizeA.y))/(relVel); - lastContact.y = ((posB.y + sizeB.y) - (posA.y - sizeA.y))/(relVel); - } - } else { - collidingY = true; - } - - return [firstContact, lastContact, collidingX, collidingY]; -} \ No newline at end of file diff --git a/src/Physics/PhysicsManager.ts b/src/Physics/PhysicsManager.ts index 3a80909..af42473 100644 --- a/src/Physics/PhysicsManager.ts +++ b/src/Physics/PhysicsManager.ts @@ -1,302 +1,24 @@ -import PhysicsNode from "./PhysicsNode"; +import GameNode from "../Nodes/GameNode"; import Vec2 from "../DataTypes/Vec2"; -import StaticBody from "./StaticBody"; -import Debug from "../Debug/Debug"; -import MathUtils from "../Utils/MathUtils"; +import { Debug_Renderable, Updateable } from "../DataTypes/Interfaces/Descriptors"; import Tilemap from "../Nodes/Tilemap"; -import OrthogonalTilemap from "../Nodes/Tilemaps/OrthogonalTilemap"; -import AABB from "../DataTypes/AABB"; -import { getTimeOfCollision } from "./Colliders/Collisions"; -import Collider from "./Colliders/Collider"; +import Receiver from "../Events/Receiver"; +import Emitter from "../Events/Emitter"; -export default class PhysicsManager { +export default abstract class PhysicsManager implements Updateable, Debug_Renderable { + protected receiver: Receiver; + protected emitter: Emitter; - private physicsNodes: Array; - private tilemaps: Array; - private movements: Array; - private tcols: Array = []; - - constructor(){ - this.physicsNodes = new Array(); - this.tilemaps = new Array(); - this.movements = new Array(); - } + constructor(){ + this.receiver = new Receiver(); + this.emitter = new Emitter(); + } - /** - * Adds a PhysicsNode to the manager to be handled in case of collisions - * @param node - */ - add(node: PhysicsNode): void { - this.physicsNodes.push(node); - } + abstract registerObject(object: GameNode): void; - /** - * Adds a tilemap node to the manager to be handled for collisions - * @param tilemap - */ - addTilemap(tilemap: Tilemap): void { - this.tilemaps.push(tilemap); - } + abstract registerTilemap(tilemap: Tilemap): void; - /** - * Adds a movement to this frame. All movements are handled at the end of the frame - * @param node - * @param velocity - */ - addMovement(node: PhysicsNode, velocity: Vec2): void { - this.movements.push(new MovementData(node, velocity)); - } + abstract update(deltaT: number): void; - /** - * Handles a collision between a physics node and a tilemap - * @param node - * @param tilemap - * @param velocity - */ - private collideWithTilemap(node: PhysicsNode, tilemap: Tilemap, velocity: Vec2): void { - if(tilemap instanceof OrthogonalTilemap){ - this.collideWithOrthogonalTilemap(node, tilemap, velocity); - } - } - - /** - * Specifically handles a collision for orthogonal tilemaps - * @param node - * @param tilemap - * @param velocity - */ - private collideWithOrthogonalTilemap(node: PhysicsNode, tilemap: OrthogonalTilemap, velocity: Vec2): void { - // Get the starting position of the moving node - let startPos = node.getCollider().getPosition(); - - // Get the end position of the moving node - let endPos = startPos.clone().add(velocity); - let size = node.getCollider().getBoundingRect().getHalfSize(); - - // Get the min and max x and y coordinates of the moving node - let min = new Vec2(Math.min(startPos.x - size.x, endPos.x - size.x), Math.min(startPos.y - size.y, endPos.y - size.y)); - let max = new Vec2(Math.max(startPos.x + size.x, endPos.x + size.x), Math.max(startPos.y + size.y, endPos.y + size.y)); - - // Convert the min/max x/y to the min and max row/col in the tilemap array - let minIndex = tilemap.getColRowAt(min); - let maxIndex = tilemap.getColRowAt(max); - - // Create an empty set of tilemap collisions (We'll handle all of them at the end) - let tilemapCollisions = new Array(); - this.tcols = []; - let tileSize = tilemap.getTileSize(); - - Debug.log("tilemapCollision", ""); - - // Loop over all possible tiles - for(let col = minIndex.x; col <= maxIndex.x; col++){ - for(let row = minIndex.y; row <= maxIndex.y; row++){ - if(tilemap.isTileCollidable(col, row)){ - Debug.log("tilemapCollision", "Colliding with Tile"); - - // Get the position of this tile - let tilePos = new Vec2(col * tileSize.x + tileSize.x/2, row * tileSize.y + tileSize.y/2); - - // Create a new collider for this tile - let collider = new Collider(new AABB(tilePos, tileSize.scaled(1/2))); - - // Calculate collision area between the node and the tile - let dx = Math.min(startPos.x, tilePos.x) - Math.max(startPos.x + size.x, tilePos.x + size.x); - let dy = Math.min(startPos.y, tilePos.y) - Math.max(startPos.y + size.y, tilePos.y + size.y); - - // If we overlap, how much do we overlap by? - let overlap = 0; - if(dx * dy > 0){ - overlap = dx * dy; - } - - this.tcols.push(new TileCollisionData(collider, overlap)) - tilemapCollisions.push(new TileCollisionData(collider, overlap)); - } - } - } - - // Now that we have all collisions, sort by collision area highest to lowest - tilemapCollisions = tilemapCollisions.sort((a, b) => a.overlapArea - b.overlapArea); - let areas = ""; - tilemapCollisions.forEach(col => areas += col.overlapArea + ", ") - Debug.log("cols", areas) - - // Resolve the collisions in order of collision area (i.e. "closest" tiles are collided with first, so we can slide along a surface of tiles) - tilemapCollisions.forEach(collision => { - let [firstContact, _, collidingX, collidingY] = getTimeOfCollision(node.getCollider(), velocity, collision.collider, Vec2.ZERO); - - // Handle collision - if( (firstContact.x < 1 || collidingX) && (firstContact.y < 1 || collidingY)){ - if(collidingX && collidingY){ - // If we're already intersecting, freak out I guess? Probably should handle this in some way for if nodes get spawned inside of tiles - } else { - // Get the amount to scale x and y based on their initial collision times - let xScale = MathUtils.clamp(firstContact.x, 0, 1); - let yScale = MathUtils.clamp(firstContact.y, 0, 1); - - // Handle special case of stickiness on perfect corner to corner collisions - if(xScale === yScale){ - xScale = 1; - } - - // If we are scaling y, we're on the ground, so tell the node it's grounded - // TODO - This is a bug, check to make sure our velocity is going downwards - // Maybe feed in a downward direction to check to be sure - if(yScale !== 1){ - // If the collider is below us - if(collision.collider.getPosition().y > node.position.y){ - node.setGrounded(true); - } else { - console.log("On ceiling") - node.setOnCeiling(true); - } - } - - if(xScale !== 1){ - node.setOnWall(true); - } - - // Scale the velocity of the node - velocity.scale(xScale, yScale); - } - } - }) - } - - private collideWithStaticNode(movingNode: PhysicsNode, staticNode: PhysicsNode, velocity: Vec2){ - let [firstContact, _, collidingX, collidingY] = getTimeOfCollision(movingNode.getCollider(), velocity, staticNode.getCollider(), Vec2.ZERO); - - if( (firstContact.x < 1 || collidingX) && (firstContact.y < 1 || collidingY)){ - if(collidingX && collidingY){ - // If we're already intersecting, freak out I guess? - } else { - // let contactTime = Math.min(firstContact.x, firstContact.y); - // velocity.scale(contactTime); - let xScale = MathUtils.clamp(firstContact.x, 0, 1); - let yScale = MathUtils.clamp(firstContact.y, 0, 1); - - // Handle special case of stickiness on perfect corner to corner collisions - if(xScale === yScale){ - xScale = 1; - } - - // If we are scaling y, we're on the ground, so tell the node it's grounded - // TODO - This is a bug, check to make sure our velocity is going downwards - // Maybe feed in a downward direction to check to be sure - if(yScale !== 1){ - // If the collider is below us - if(staticNode.position.y > movingNode.position.y){ - movingNode.setGrounded(true); - } else { - movingNode.setOnCeiling(true); - } - } - - if(xScale !== 1){ - movingNode.setOnWall(true); - } - - // Scale the velocity of the node - velocity.scale(xScale, yScale); - } - } - } - - update(deltaT: number): void { - for(let node of this.physicsNodes){ - if(!node.getLayer().isPaused()){ - node.update(deltaT); - } - } - - let staticSet = new Array(); - let dynamicSet = new Array(); - - // TODO: REALLY bad, the physics system has to be improved, but that isn't the focus for now - for(let node of this.physicsNodes){ - if(node.isMoving()){ - dynamicSet.push(node); - node.setMoving(false); - } else { - staticSet.push(node); - } - } - - // For now, we will only have the moving player, don't bother checking for collisions with other moving things - for(let movingNode of dynamicSet){ - movingNode.setGrounded(false); - movingNode.setOnCeiling(false); - movingNode.setOnWall(false); - // Get velocity of node - let velocity = null; - for(let data of this.movements){ - if(data.node === movingNode){ - velocity = new Vec2(data.velocity.x, data.velocity.y); - } - } - - // TODO handle collisions between dynamic nodes - // We probably want to sort them by their left edges - - for(let staticNode of staticSet){ - this.collideWithStaticNode(movingNode, staticNode, velocity); - } - - // Handle Collisions with the tilemaps - for(let tilemap of this.tilemaps){ - this.collideWithTilemap(movingNode, tilemap, velocity); - } - - movingNode.finishMove(velocity); - } - - // Reset movements - this.movements = new Array(); - } - - render(ctx: CanvasRenderingContext2D): void { - let vpo; - for(let node of this.physicsNodes){ - vpo = node.getViewportOriginWithParallax(); - let pos = node.getPosition().sub(node.getViewportOriginWithParallax()); - let size = (node.getCollider().getCollisionShape()).getHalfSize(); - - ctx.lineWidth = 2; - ctx.strokeStyle = "#FF0000"; - ctx.strokeRect(pos.x - size.x, pos.y-size.y, size.x*2, size.y*2); - } - - for(let node of this.tcols){ - let pos = node.collider.getPosition().sub(vpo); - let size = node.collider.getBoundingRect().getHalfSize(); - - ctx.lineWidth = 2; - ctx.strokeStyle = "#FF0000"; - ctx.strokeRect(pos.x - size.x, pos.y-size.y, size.x*2, size.y*2); - } - } -} - -// Helper classes for internal data -// TODO: Move these to data -// When an object moves, store it's data as MovementData so all movements can be processed at the same time at the end of the frame -class MovementData { - node: PhysicsNode; - velocity: Vec2; - constructor(node: PhysicsNode, velocity: Vec2){ - this.node = node; - this.velocity = velocity; - } -} - -// Collision data objects for tilemaps -class TileCollisionData { - collider: Collider; - overlapArea: number; - - constructor(collider: Collider, overlapArea: number){ - this.collider = collider; - this.overlapArea = overlapArea; - } + abstract debug_render(ctx: CanvasRenderingContext2D): void; } \ No newline at end of file diff --git a/src/Physics/PhysicsNode.ts b/src/Physics/PhysicsNode.ts deleted file mode 100644 index 94fd456..0000000 --- a/src/Physics/PhysicsNode.ts +++ /dev/null @@ -1,99 +0,0 @@ -import Collider from "./Colliders/Collider"; -import GameNode from "../Nodes/GameNode"; -import PhysicsManager from "./PhysicsManager"; -import Vec2 from "../DataTypes/Vec2"; - -/** - * The representation of a physic-affected object in the game world. Sprites and other game nodes can be associated with - * a physics node to move them around as well. - */ -export default abstract class PhysicsNode extends GameNode { - - protected collider: Collider = null; - protected children: Array; - private manager: PhysicsManager; - protected moving: boolean; - protected grounded: boolean; - protected onCeiling: boolean; - protected onWall: boolean; - - constructor(){ - super(); - this.children = new Array(); - this.grounded = false; - this.onCeiling = false; - this.onWall = false; - this.moving = false; - } - - setGrounded(grounded: boolean): void { - this.grounded = grounded; - } - - isGrounded(): boolean { - return this.grounded; - } - - setOnCeiling(onCeiling: boolean): void { - this.onCeiling = onCeiling; - } - - isOnCeiling(): boolean { - return this.onCeiling; - } - - setOnWall(onWall: boolean): void { - this.onWall = onWall; - } - - isOnWall(): boolean { - return this.onWall; - } - - addManager(manager: PhysicsManager): void { - this.manager = manager; - } - - addChild(child: GameNode): void { - this.children.push(child); - } - - isCollidable(): boolean { - return this.collider !== null; - } - - getCollider(): Collider { - return this.collider; - } - - setMoving(moving: boolean): void { - this.moving = moving; - } - - isMoving(): boolean { - return this.moving; - } - - /** - * Register a movement to the physics manager that can be handled at the end of the frame - * @param velocity - */ - move(velocity: Vec2): void { - this.moving = true; - this.manager.addMovement(this, velocity); - } - - /** - * Called by the physics manager to finish the movement and actually move the physics object and its children - * @param velocity - */ - finishMove(velocity: Vec2): void { - this.position.add(velocity); - this.collider.getPosition().add(velocity); - for(let child of this.children){ - child.position.add(velocity); - } - } - - abstract create(): void; -} \ No newline at end of file diff --git a/src/Physics/StaticBody.ts b/src/Physics/StaticBody.ts deleted file mode 100644 index 95d20e8..0000000 --- a/src/Physics/StaticBody.ts +++ /dev/null @@ -1,20 +0,0 @@ -import PhysicsNode from "./PhysicsNode"; -import Vec2 from "../DataTypes/Vec2"; -import Collider from "./Colliders/Collider"; -import AABB from "../DataTypes/AABB"; - -export default class StaticBody extends PhysicsNode { - - constructor(position: Vec2, size: Vec2){ - super(); - this.setPosition(position.x, position.y); - let aabb = new AABB(position.clone(), size.scaled(1/2)); - this.collider = new Collider(aabb); - this.moving = false; - } - - create(): void {} - - update(deltaT: number): void {} - -} \ No newline at end of file diff --git a/src/Player.ts b/src/Player.ts deleted file mode 100644 index a25592d..0000000 --- a/src/Player.ts +++ /dev/null @@ -1,103 +0,0 @@ -import PhysicsNode from "./Physics/PhysicsNode"; -import Vec2 from "./DataTypes/Vec2"; -import Debug from "./Debug/Debug"; -import CanvasNode from "./Nodes/CanvasNode"; -import { GameEventType } from "./Events/GameEventType"; -import AABB from "./DataTypes/AABB"; -import Collider from "./Physics/Colliders/Collider"; - -export default class Player extends PhysicsNode { - velocity: Vec2; - speed: number; - debug: Debug; - size: Vec2; - gravity: number = 7000; - type: string; - - constructor(type: string){ - super(); - this.type = type; - this.velocity = new Vec2(0, 0); - this.speed = 600; - this.size = new Vec2(50, 50); - this.position = new Vec2(0, 0); - if(this.type === "topdown"){ - this.position = new Vec2(100, 100); - } - - this.collider = new Collider(new AABB(this.position.clone(), this.size.scaled(1/2))); - } - - create(): void {}; - - sprite: CanvasNode; - setSprite(sprite: CanvasNode): void { - this.sprite = sprite; - sprite.position = this.position.clone(); - sprite.setSize(this.size); - this.children.push(sprite); - } - - update(deltaT: number): void { - if(this.type === "topdown"){ - let dir = this.topdown_computeDirection(); - this.velocity = this.topdown_computeVelocity(dir, deltaT); - } else { - let dir = this.platformer_computeDirection(); - this.velocity = this.platformer_computeVelocity(dir, deltaT); - } - - this.move(new Vec2(this.velocity.x * deltaT, this.velocity.y * deltaT)); - - Debug.log("player", "Pos: " + this.sprite.getPosition() + ", Size: " + this.sprite.getSize()); - Debug.log("playerbound", "Pos: " + this.sprite.getBoundary().getCenter() + ", Size: " + this.sprite.getBoundary().getHalfSize()); - } - - topdown_computeDirection(): Vec2 { - let dir = new Vec2(0, 0); - dir.x += this.input.isPressed('a') ? -1 : 0; - dir.x += this.input.isPressed('d') ? 1 : 0; - dir.y += this.input.isPressed('w') ? -1 : 0; - dir.y += this.input.isPressed('s') ? 1 : 0; - - dir.normalize(); - - return dir; - } - - topdown_computeVelocity(dir: Vec2, deltaT: number): Vec2 { - let vel = new Vec2(dir.x * this.speed, dir.y * this.speed); - return vel - } - - platformer_computeDirection(): Vec2 { - let dir = new Vec2(0, 0); - dir.x += this.input.isPressed('a') ? -1 : 0; - dir.x += this.input.isPressed('d') ? 1 : 0; - - if(this.grounded){ - dir.y += this.input.isJustPressed('w') ? -1 : 0; - } - - return dir; - } - - platformer_computeVelocity(dir: Vec2, deltaT: number): Vec2 { - let vel = new Vec2(0, this.velocity.y); - - if(this.grounded){ - if(dir.y === -1){ - // Jumping - this.emitter.fireEvent(GameEventType.PLAY_SOUND, {key: "player_jump"}); - } - vel.y = dir.y*1800; - } - - vel.y += this.gravity * deltaT; - - vel.x = dir.x * this.speed; - - return vel - } - -} \ No newline at end of file diff --git a/src/QuadTreeScene.ts b/src/QuadTreeScene.ts index f5e26be..4ffcc41 100644 --- a/src/QuadTreeScene.ts +++ b/src/QuadTreeScene.ts @@ -1,5 +1,4 @@ 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"; @@ -7,9 +6,6 @@ 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 { @@ -41,14 +37,14 @@ export default class QuadTreeScene extends Scene { } updateScene(deltaT: number): void { - this.view.setPosition(InputReceiver.getInstance().getGlobalMousePosition()); + this.view.position.copy(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()); + let results = this.sceneGraph.getNodesInRegion(this.view.boundary); for(let result of results){ if(result instanceof Point){ diff --git a/src/Scene/Factories/CanvasNodeFactory.ts b/src/Scene/Factories/CanvasNodeFactory.ts index 537eaaf..baaa374 100644 --- a/src/Scene/Factories/CanvasNodeFactory.ts +++ b/src/Scene/Factories/CanvasNodeFactory.ts @@ -23,7 +23,7 @@ export default class CanvasNodeFactory { // Add instance to scene instance.setScene(this.scene); - instance.setId(this.scene.generateId()); + instance.id = this.scene.generateId(); this.scene.getSceneGraph().addNode(instance); // Add instance to layer @@ -42,7 +42,7 @@ export default class CanvasNodeFactory { // Add instance to scene instance.setScene(this.scene); - instance.setId(this.scene.generateId()); + instance.id = this.scene.generateId(); this.scene.getSceneGraph().addNode(instance); // Add instance to layer @@ -62,7 +62,7 @@ export default class CanvasNodeFactory { // Add instance to scene instance.setScene(this.scene); - instance.setId(this.scene.generateId()); + instance.id = this.scene.generateId(); this.scene.getSceneGraph().addNode(instance); // Add instance to layer diff --git a/src/Scene/Factories/FactoryManager.ts b/src/Scene/Factories/FactoryManager.ts index 86b57bf..212cdbd 100644 --- a/src/Scene/Factories/FactoryManager.ts +++ b/src/Scene/Factories/FactoryManager.ts @@ -1,21 +1,17 @@ import Scene from "../Scene"; -import PhysicsNodeFactory from "./PhysicsNodeFactory"; import CanvasNodeFactory from "./CanvasNodeFactory"; import TilemapFactory from "./TilemapFactory"; import PhysicsManager from "../../Physics/PhysicsManager"; -import SceneGraph from "../../SceneGraph/SceneGraph"; import Tilemap from "../../Nodes/Tilemap"; export default class FactoryManager { // Constructors are called here to allow assignment of their functions to functions in this class private canvasNodeFactory: CanvasNodeFactory = new CanvasNodeFactory(); - private physicsNodeFactory: PhysicsNodeFactory = new PhysicsNodeFactory(); private tilemapFactory: TilemapFactory = new TilemapFactory(); constructor(scene: Scene, physicsManager: PhysicsManager, tilemaps: Array){ this.canvasNodeFactory.init(scene); - this.physicsNodeFactory.init(scene, physicsManager); this.tilemapFactory.init(scene, tilemaps, physicsManager); } @@ -23,6 +19,5 @@ export default class FactoryManager { uiElement = this.canvasNodeFactory.addUIElement; sprite = this.canvasNodeFactory.addSprite; graphic = this.canvasNodeFactory.addGraphic; - physics = this.physicsNodeFactory.add; tilemap = this.tilemapFactory.add; } \ No newline at end of file diff --git a/src/Scene/Factories/PhysicsNodeFactory.ts b/src/Scene/Factories/PhysicsNodeFactory.ts deleted file mode 100644 index 21c2108..0000000 --- a/src/Scene/Factories/PhysicsNodeFactory.ts +++ /dev/null @@ -1,34 +0,0 @@ -import Scene from "../Scene"; -import PhysicsNode from "../../Physics/PhysicsNode"; -import PhysicsManager from "../../Physics/PhysicsManager"; -import Layer from "../Layer"; - -export default class PhysicsNodeFactory { - private scene: Scene; - private physicsManager: PhysicsManager; - - init(scene: Scene, physicsManager: PhysicsManager): void { - this.scene = scene; - this.physicsManager = physicsManager; - } - - // TODO: Currently this doesn't care about layers - /** - * Adds a new PhysicsNode to the scene on the specified Layer - * @param constr The constructor of the PhysicsNode to be added to the scene - * @param layer The layer on which to add the PhysicsNode - * @param args Any additional arguments to send to the PhysicsNode constructor - */ - add = (constr: new (...a: any) => T, layer: Layer, ...args: any): T => { - let instance = new constr(...args); - instance.setScene(this.scene); - instance.setId(this.scene.generateId()); - instance.addManager(this.physicsManager); - instance.create(); - - layer.addNode(instance); - - this.physicsManager.add(instance); - return instance; - } -} \ No newline at end of file diff --git a/src/Scene/Factories/TilemapFactory.ts b/src/Scene/Factories/TilemapFactory.ts index 1ed0aa3..71243c6 100644 --- a/src/Scene/Factories/TilemapFactory.ts +++ b/src/Scene/Factories/TilemapFactory.ts @@ -8,7 +8,6 @@ import Tileset from "../../DataTypes/Tilesets/Tileset"; import Vec2 from "../../DataTypes/Vec2"; import { TiledCollectionTile } from "../../DataTypes/Tilesets/TiledData"; import Sprite from "../../Nodes/Sprites/Sprite"; -import StaticBody from "../../Physics/StaticBody"; export default class TilemapFactory { private scene: Scene; @@ -71,7 +70,7 @@ export default class TilemapFactory { if(layer.type === "tilelayer"){ // Create a new tilemap object for the layer let tilemap = new constr(tilemapData, layer, tilesets, scale); - tilemap.setId(this.scene.generateId()); + tilemap.id = this.scene.generateId(); tilemap.setScene(this.scene); // Add tilemap to scene @@ -80,8 +79,8 @@ export default class TilemapFactory { sceneLayer.addNode(tilemap); // Register tilemap with physics if it's collidable - if(tilemap.isCollidable()){ - this.physicsManager.addTilemap(tilemap); + if(tilemap.isCollidable){ + tilemap.addPhysics(); } } else { // Layer is an object layer, so add each object as a sprite to a new layer @@ -107,10 +106,10 @@ export default class TilemapFactory { let offset = tileset.getImageOffsetForTile(obj.gid); sprite = this.scene.add.sprite(imageKey, sceneLayer); let size = tileset.getTileSize().clone(); - sprite.setPosition((obj.x + size.x/2)*scale.x, (obj.y - size.y/2)*scale.y); + sprite.position.set((obj.x + size.x/2)*scale.x, (obj.y - size.y/2)*scale.y); sprite.setImageOffset(offset); - sprite.setSize(size); - sprite.setScale(new Vec2(scale.x, scale.y)); + sprite.size.copy(size); + sprite.scale.set(scale.x, scale.y); } } @@ -120,22 +119,16 @@ export default class TilemapFactory { if(obj.gid === tile.id){ let imageKey = tile.image; sprite = this.scene.add.sprite(imageKey, sceneLayer); - sprite.setPosition((obj.x + tile.imagewidth/2)*scale.x, (obj.y - tile.imageheight/2)*scale.y); - sprite.setScale(new Vec2(scale.x, scale.y)); + sprite.position.set((obj.x + tile.imagewidth/2)*scale.x, (obj.y - tile.imageheight/2)*scale.y); + sprite.scale.set(scale.x, scale.y); } } } // Now we have sprite. Associate it with our physics object if there is one if(collidable){ - let pos = sprite.getPosition().clone(); - let size = sprite.getSize().clone().mult(sprite.getScale()); - pos.x = Math.floor(pos.x); - pos.y = Math.floor(pos.y); - let staticBody = this.scene.add.physics(StaticBody, sceneLayer, pos, size); - staticBody.addChild(sprite); + sprite.addPhysics(); } - } } diff --git a/src/Scene/Scene.ts b/src/Scene/Scene.ts index 5a5ff06..0728adb 100644 --- a/src/Scene/Scene.ts +++ b/src/Scene/Scene.ts @@ -1,9 +1,9 @@ -import Stack from "../DataTypes/Stack"; import Layer from "./Layer"; import Viewport from "../SceneGraph/Viewport"; import Vec2 from "../DataTypes/Vec2"; import SceneGraph from "../SceneGraph/SceneGraph"; import PhysicsManager from "../Physics/PhysicsManager"; +import BasicPhysicsManager from "../Physics/BasicPhysicsManager"; import SceneGraphArray from "../SceneGraph/SceneGraphArray"; import FactoryManager from "./Factories/FactoryManager"; import Tilemap from "../Nodes/Tilemap"; @@ -12,32 +12,41 @@ import GameLoop from "../Loop/GameLoop"; import SceneManager from "./SceneManager"; import Receiver from "../Events/Receiver"; import Emitter from "../Events/Emitter"; +import { Renderable, Updateable } from "../DataTypes/Interfaces/Descriptors"; -export default class Scene{ +export default class Scene implements Updateable, Renderable { + /** The size of the game world. */ protected worldSize: Vec2; + + /** The viewport. */ protected viewport: Viewport; + + /** A flag that represents whether this scene is running or not. */ protected running: boolean; + + /** The overall game loop. */ protected game: GameLoop; + + /** The manager of this scene. */ protected sceneManager: SceneManager; + + /** The receiver for this scene. */ protected receiver: Receiver; + + /** The emitter for this scene. */ protected emitter: Emitter; + /** This list of tilemaps in this scene. */ protected tilemaps: Array; - /** - * The scene graph of the Scene - can be exchanged with other SceneGraphs for more variation - */ + /** The scene graph of the Scene*/ protected sceneGraph: SceneGraph; protected physicsManager: PhysicsManager; - /** - * An interface that allows the adding of different nodes to the scene - */ + /** An interface that allows the adding of different nodes to the scene */ public add: FactoryManager; - /** - * An interface that allows the loading of different files for use in the scene - */ + /** An interface that allows the loading of different files for use in the scene */ public load: ResourceManager; constructor(viewport: Viewport, sceneManager: SceneManager, game: GameLoop){ @@ -52,40 +61,28 @@ export default class Scene{ this.tilemaps = new Array(); this.sceneGraph = new SceneGraphArray(this.viewport, this); - this.physicsManager = new PhysicsManager(); - + this.physicsManager = new BasicPhysicsManager(); this.add = new FactoryManager(this, this.physicsManager, this.tilemaps); - this.load = ResourceManager.getInstance(); } - /** - * A function that gets called when a new scene is created. Load all files you wish to access in the scene here. - */ + /** A lifecycle method that gets called when a new scene is created. Load all files you wish to access in the scene here. */ loadScene(): void {} - /** - * A function that gets called on scene destruction. Specify which files you no longer need for garbage collection. - */ + /** A lifecycle method that gets called on scene destruction. Specify which files you no longer need for garbage collection. */ unloadScene(): void {} - /** - * Called strictly after loadScene() is called. Create any game objects you wish to use in the scene here. - */ + /** A lifecycle method called strictly after loadScene(). Create any game objects you wish to use in the scene here. */ startScene(): void {} /** - * Called every frame of the game. This is where you can dynamically do things like add in new enemies + * A lifecycle method called every frame of the game. This is where you can dynamically do things like add in new enemies * @param delta */ updateScene(deltaT: number): void {} - /** - * Updates all scene elements - * @param deltaT - */ update(deltaT: number): void { this.updateScene(deltaT); @@ -106,10 +103,6 @@ export default class Scene{ this.viewport.update(deltaT); } - /** - * Render all CanvasNodes and Tilemaps in the Scene - * @param ctx - */ render(ctx: CanvasRenderingContext2D): void { // For webGL, pass a visible set to the renderer // We need to keep track of the order of things. @@ -127,7 +120,7 @@ export default class Scene{ visibleSet.forEach(node => node.render(ctx)); // Debug render the physicsManager - this.physicsManager.render(ctx); + this.physicsManager.debug_render(ctx); } setRunning(running: boolean): void { @@ -145,9 +138,7 @@ export default class Scene{ return this.sceneGraph.addLayer(); } - /** - * Returns the viewport associated with this scene - */ + /** Returns the viewport associated with this scene */ getViewport(): Viewport { return this.viewport; } @@ -160,6 +151,10 @@ export default class Scene{ return this.sceneGraph; } + getPhysicsManager(): PhysicsManager { + return this.physicsManager; + } + generateId(): number { return this.sceneManager.generateId(); } diff --git a/src/SceneGraph/SceneGraph.ts b/src/SceneGraph/SceneGraph.ts index 40203e9..7b18342 100644 --- a/src/SceneGraph/SceneGraph.ts +++ b/src/SceneGraph/SceneGraph.ts @@ -5,7 +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"; +import AABB from "../DataTypes/Shapes/AABB"; /** * An abstract interface of a SceneGraph. Exposes methods for use by other code, but leaves the implementation up to the subclasses. diff --git a/src/SceneGraph/SceneGraphArray.ts b/src/SceneGraph/SceneGraphArray.ts index 5b71f03..c4eab36 100644 --- a/src/SceneGraph/SceneGraphArray.ts +++ b/src/SceneGraph/SceneGraphArray.ts @@ -4,7 +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"; +import AABB from "../DataTypes/Shapes/AABB"; import Stats from "../Debug/Stats"; export default class SceneGraphArray extends SceneGraph{ @@ -50,7 +50,7 @@ export default class SceneGraphArray extends SceneGraph{ let results = []; for(let node of this.nodeList){ - if(boundary.overlaps(node.getBoundary())){ + if(boundary.overlaps(node.boundary)){ results.push(node); } } @@ -94,8 +94,7 @@ export default class SceneGraphArray extends SceneGraph{ // 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); + return (a.boundary.bottom) - (b.boundary.bottom); } else { return a.getLayer().getDepth() - b.getLayer().getDepth(); } diff --git a/src/SceneGraph/SceneGraphQuadTree.ts b/src/SceneGraph/SceneGraphQuadTree.ts index 577d5b8..5956bab 100644 --- a/src/SceneGraph/SceneGraphQuadTree.ts +++ b/src/SceneGraph/SceneGraphQuadTree.ts @@ -4,7 +4,7 @@ import Viewport from "./Viewport"; import Scene from "../Scene/Scene"; import RegionQuadTree from "../DataTypes/RegionQuadTree"; import Vec2 from "../DataTypes/Vec2"; -import AABB from "../DataTypes/AABB"; +import AABB from "../DataTypes/Shapes/AABB"; import Stats from "../Debug/Stats"; export default class SceneGraphQuadTree extends SceneGraph { @@ -80,8 +80,7 @@ export default class SceneGraphQuadTree extends SceneGraph { // 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); + return (a.boundary.bottom) - (b.boundary.bottom); } else { return a.getLayer().getDepth() - b.getLayer().getDepth(); } diff --git a/src/SceneGraph/Viewport.ts b/src/SceneGraph/Viewport.ts index 4b98c86..fa6bd9f 100644 --- a/src/SceneGraph/Viewport.ts +++ b/src/SceneGraph/Viewport.ts @@ -3,7 +3,7 @@ import GameNode from "../Nodes/GameNode"; import CanvasNode from "../Nodes/CanvasNode"; import MathUtils from "../Utils/MathUtils"; import Queue from "../DataTypes/Queue"; -import AABB from "../DataTypes/AABB"; +import AABB from "../DataTypes/Shapes/AABB"; import Debug from "../Debug/Debug"; import InputReceiver from "../Input/InputReceiver"; @@ -43,11 +43,11 @@ export default class Viewport { * Returns the position of the viewport as a Vec2 */ getCenter(): Vec2 { - return this.view.getCenter(); + return this.view.center; } getOrigin(): Vec2 { - return this.view.getCenter().clone().sub(this.view.getHalfSize()) + return this.view.center.clone().sub(this.view.halfSize) } /** @@ -134,10 +134,10 @@ export default class Viewport { */ includes(node: CanvasNode): boolean { let parallax = node.getLayer().getParallax(); - let center = this.view.getCenter().clone(); - this.view.getCenter().mult(parallax); - let overlaps = this.view.overlaps(node.getBoundary()); - this.view.setCenter(center); + let center = this.view.center.clone(); + this.view.center.mult(parallax); + let overlaps = this.view.overlaps(node.boundary); + this.view.center = center return overlaps; } @@ -155,8 +155,8 @@ export default class Viewport { let hheight = (upperY - lowerY)/2; let x = lowerX + hwidth; let y = lowerY + hheight; - this.boundary.setCenter(new Vec2(x, y)); - this.boundary.setHalfSize(new Vec2(hwidth, hheight)); + this.boundary.center.set(x, y); + this.boundary.halfSize.set(hwidth, hheight); } /** @@ -202,7 +202,7 @@ export default class Viewport { // If viewport is following an object if(this.following){ // Update our list of previous positions - this.lastPositions.enqueue(this.following.getPosition().clone()); + this.lastPositions.enqueue(this.following.position.clone()); if(this.lastPositions.getSize() > this.smoothingFactor){ this.lastPositions.dequeue(); } @@ -222,7 +222,7 @@ export default class Viewport { Debug.log("vp", "Viewport pos: " + pos.toString()) - this.view.setCenter(pos); + this.view.center.copy(pos); } else { if(this.lastPositions.getSize() > this.smoothingFactor){ this.lastPositions.dequeue(); @@ -241,7 +241,7 @@ export default class Viewport { pos.y = Math.floor(pos.y); Debug.log("vp", "Viewport pos: " + pos.toString()) - this.view.setCenter(pos); + this.view.center.copy(pos); } } } \ No newline at end of file diff --git a/src/SecondScene.ts b/src/SecondScene.ts index 601e09f..f7a16f2 100644 --- a/src/SecondScene.ts +++ b/src/SecondScene.ts @@ -1,6 +1,4 @@ import Scene from "./Scene/Scene"; -import OrthogonalTilemap from "./Nodes/Tilemaps/OrthogonalTilemap"; -import Player from "./Player"; import Rect from "./Nodes/Graphics/Rect"; import Color from "./Utils/Color"; import Vec2 from "./DataTypes/Vec2"; @@ -23,7 +21,7 @@ export default class SecondScene extends Scene { bar.setColor(new Color(255, 100, 0)); this.load.onLoadProgress = (percentProgress: number) => { - bar.setSize(295 * percentProgress, bar.getSize().y); + //bar.setSize(295 * percentProgress, bar.getSize().y); } this.load.onLoadComplete = () => { @@ -32,76 +30,76 @@ export default class SecondScene extends Scene { } startScene(){ - // Add the tilemap - let mainLayer = this.add.tilemap("level2")[1]; - mainLayer.setYSort(true); + // // Add the tilemap + // let mainLayer = this.add.tilemap("level2")[1]; + // mainLayer.setYSort(true); - // Add a player - let player = this.add.physics(Player, mainLayer, "topdown"); - let playerSprite = this.add.sprite("player", mainLayer); - player.setSprite(playerSprite); + // // Add a player + // let player = this.add.physics(Player, mainLayer, "topdown"); + // let playerSprite = this.add.sprite("player", mainLayer); + // player.setSprite(playerSprite); - this.viewport.follow(player); + // this.viewport.follow(player); - // Initialize UI - let uiLayer = this.addLayer(); - uiLayer.setParallax(0, 0); + // // Initialize UI + // let uiLayer = this.addLayer(); + // uiLayer.setParallax(0, 0); - let recordButton = this.add.uiElement(Button, uiLayer); - recordButton.setSize(100, 50); - recordButton.setText("Record"); - recordButton.setPosition(400, 30); - recordButton.onClickEventId = GameEventType.START_RECORDING; + // let recordButton = this.add.uiElement(Button, uiLayer); + // recordButton.setSize(100, 50); + // recordButton.setText("Record"); + // recordButton.setPosition(400, 30); + // recordButton.onClickEventId = GameEventType.START_RECORDING; - let stopButton = this.add.uiElement(Button, uiLayer); - stopButton.setSize(100, 50); - stopButton.setText("Stop"); - stopButton.setPosition(550, 30); - stopButton.onClickEventId = GameEventType.STOP_RECORDING; + // let stopButton = this.add.uiElement(Button, uiLayer); + // stopButton.setSize(100, 50); + // stopButton.setText("Stop"); + // stopButton.setPosition(550, 30); + // stopButton.onClickEventId = GameEventType.STOP_RECORDING; - let playButton = this.add.uiElement(Button, uiLayer); - playButton.setSize(100, 50); - playButton.setText("Play"); - playButton.setPosition(700, 30); - playButton.onClickEventId = GameEventType.PLAY_RECORDING; + // let playButton = this.add.uiElement(Button, uiLayer); + // playButton.setSize(100, 50); + // playButton.setText("Play"); + // playButton.setPosition(700, 30); + // playButton.onClickEventId = GameEventType.PLAY_RECORDING; - let cycleFramerateButton = this.add.uiElement(Button, uiLayer); - cycleFramerateButton.setSize(150, 50); - cycleFramerateButton.setText("Cycle FPS"); - cycleFramerateButton.setPosition(5, 400); - let i = 0; - let fps = [15, 30, 60]; - cycleFramerateButton.onClick = () => { - this.game.setMaxUpdateFPS(fps[i]); - i = (i + 1) % 3; - } + // let cycleFramerateButton = this.add.uiElement(Button, uiLayer); + // cycleFramerateButton.setSize(150, 50); + // cycleFramerateButton.setText("Cycle FPS"); + // cycleFramerateButton.setPosition(5, 400); + // let i = 0; + // let fps = [15, 30, 60]; + // cycleFramerateButton.onClick = () => { + // this.game.setMaxUpdateFPS(fps[i]); + // i = (i + 1) % 3; + // } - // Pause Menu - let pauseLayer = this.addLayer(); - pauseLayer.setParallax(0, 0); - pauseLayer.disable(); + // // Pause Menu + // let pauseLayer = this.addLayer(); + // pauseLayer.setParallax(0, 0); + // pauseLayer.disable(); - let pauseButton = this.add.uiElement(Button, uiLayer); - pauseButton.setSize(100, 50); - pauseButton.setText("Pause"); - pauseButton.setPosition(700, 400); - pauseButton.onClick = () => { - this.sceneGraph.getLayers().forEach((layer: Layer) => layer.setPaused(true)); - pauseLayer.enable(); - } + // let pauseButton = this.add.uiElement(Button, uiLayer); + // pauseButton.setSize(100, 50); + // pauseButton.setText("Pause"); + // pauseButton.setPosition(700, 400); + // pauseButton.onClick = () => { + // this.sceneGraph.getLayers().forEach((layer: Layer) => layer.setPaused(true)); + // pauseLayer.enable(); + // } - let modalBackground = this.add.uiElement(UIElement, pauseLayer); - modalBackground.setSize(400, 200); - modalBackground.setBackgroundColor(new Color(0, 0, 0, 0.4)); - modalBackground.setPosition(200, 100); + // let modalBackground = this.add.uiElement(UIElement, pauseLayer); + // modalBackground.setSize(400, 200); + // modalBackground.setBackgroundColor(new Color(0, 0, 0, 0.4)); + // modalBackground.setPosition(200, 100); - let resumeButton = this.add.uiElement(Button, pauseLayer); - resumeButton.setSize(100, 50); - resumeButton.setText("Resume"); - resumeButton.setPosition(400, 200); - resumeButton.onClick = () => { - this.sceneGraph.getLayers().forEach((layer: Layer) => layer.setPaused(false)); - pauseLayer.disable(); - } + // let resumeButton = this.add.uiElement(Button, pauseLayer); + // resumeButton.setSize(100, 50); + // resumeButton.setText("Resume"); + // resumeButton.setPosition(400, 200); + // resumeButton.onClick = () => { + // this.sceneGraph.getLayers().forEach((layer: Layer) => layer.setPaused(false)); + // pauseLayer.disable(); + // } } } \ No newline at end of file diff --git a/src/Utils/PhysicsUtils.ts b/src/Utils/PhysicsUtils.ts new file mode 100644 index 0000000..2084d7b --- /dev/null +++ b/src/Utils/PhysicsUtils.ts @@ -0,0 +1,7 @@ +import { Physical } from "../DataTypes/Interfaces/Descriptors"; + +export default class PhysicsUtils { + static sweepAndPrune(nodes: Array){ + // Sort + } +} \ No newline at end of file diff --git a/src/Utils/SortingUtils.ts b/src/Utils/SortingUtils.ts new file mode 100644 index 0000000..a218deb --- /dev/null +++ b/src/Utils/SortingUtils.ts @@ -0,0 +1,24 @@ +export default class SortingUtils { + /** + * + * @param arr + * @param comparator Compares element a and b in the array. Returns -1 if a < b, 0 if a = b, and 1 if a > b + */ + static insertionSort(arr: Array, comparator: (a: T, b: T) => number): void { + let i = 1; + let j; + while(i < arr.length){ + j = i; + while(j > 0 && comparator(arr[j-1], arr[j]) > 0){ + SortingUtils.swap(arr, j-1, j); + } + i += 1; + } + } + + static swap(arr: Array, i: number, j: number){ + let temp = arr[i]; + arr[i] = arr[j]; + arr[j] = temp; + } +} \ No newline at end of file diff --git a/src/_DemoClasses/Boids/BoidStates/BoidBehavior.ts b/src/_DemoClasses/Boids/BoidStates/BoidBehavior.ts index 27e30c7..0337a71 100644 --- a/src/_DemoClasses/Boids/BoidStates/BoidBehavior.ts +++ b/src/_DemoClasses/Boids/BoidStates/BoidBehavior.ts @@ -69,7 +69,7 @@ export default class BoidBehavior extends State { speed = MathUtils.clamp(speed, BoidBehavior.MIN_SPEED, BoidBehavior.MAX_SPEED); this.actor.velocity.scale(speed); - if(this.actor.getId() < 1){ + if(this.actor.id < 1){ Debug.log("BoidSep", "Separation: " + separationForce.toString()); Debug.log("BoidAl", "Alignment: " + alignmentForce.toString()); Debug.log("BoidCo", "Cohesion: " + cohesionForce.toString()); @@ -77,7 +77,7 @@ export default class BoidBehavior extends State { } } - if(this.actor.getId() < 1){ + if(this.actor.id < 1){ Debug.log("BoidDir", "Velocity: " + this.actor.velocity.toString()); } diff --git a/src/_DemoClasses/Boids/FlockBehavior.ts b/src/_DemoClasses/Boids/FlockBehavior.ts index f3bb84d..58fbf21 100644 --- a/src/_DemoClasses/Boids/FlockBehavior.ts +++ b/src/_DemoClasses/Boids/FlockBehavior.ts @@ -1,4 +1,4 @@ -import AABB from "../../DataTypes/AABB"; +import AABB from "../../DataTypes/Shapes/AABB"; import Vec2 from "../../DataTypes/Vec2"; import Point from "../../Nodes/Graphics/Point"; import Scene from "../../Scene/Scene"; @@ -21,14 +21,14 @@ export default class FlockBehavior { this.actor = actor; this.flock = flock; - this.visibleRegion = new AABB(this.actor.getPosition().clone(), new Vec2(visionRange, visionRange)); + this.visibleRegion = new AABB(this.actor.position.clone(), new Vec2(visionRange, visionRange)); this.avoidRadius = avoidRadius; } update(): void { // Update the visible region - this.visibleRegion.setCenter(this.actor.getPosition().clone()); + this.visibleRegion.center.copy(this.actor.position); let neighbors = this.scene.getSceneGraph().getNodesInRegion(this.visibleRegion); @@ -46,7 +46,7 @@ export default class FlockBehavior { } // Draw a group - if(this.actor.getId() < 1){ + if(this.actor.id < 1){ this.actor.setColor(Color.GREEN); for(let neighbor of neighbors){ if(neighbor === this.actor) continue; diff --git a/src/_DemoClasses/Enemies/GoombaController.ts b/src/_DemoClasses/Enemies/GoombaController.ts index 6bc1fdb..12bf25d 100644 --- a/src/_DemoClasses/Enemies/GoombaController.ts +++ b/src/_DemoClasses/Enemies/GoombaController.ts @@ -1,10 +1,11 @@ import StateMachine from "../../DataTypes/State/StateMachine"; import { CustomGameEventType } from "../CustomGameEventType"; -import Goomba from "../MarioClone/Goomba"; import Idle from "../Enemies/Idle"; import Jump from "../Enemies/Jump"; import Walk from "../Enemies/Walk"; import Debug from "../../Debug/Debug"; +import GameNode from "../../Nodes/GameNode"; +import Vec2 from "../../DataTypes/Vec2"; export enum GoombaStates { IDLE = "idle", @@ -14,10 +15,13 @@ export enum GoombaStates { } export default class GoombaController extends StateMachine { - owner: Goomba; + owner: GameNode; jumpy: boolean; + direction: Vec2 = Vec2.ZERO; + velocity: Vec2 = Vec2.ZERO; + speed: number = 200; - constructor(owner: Goomba, jumpy: boolean){ + constructor(owner: GameNode, jumpy: boolean){ super(); this.owner = owner; diff --git a/src/_DemoClasses/Enemies/GoombaState.ts b/src/_DemoClasses/Enemies/GoombaState.ts index 876b760..5144acc 100644 --- a/src/_DemoClasses/Enemies/GoombaState.ts +++ b/src/_DemoClasses/Enemies/GoombaState.ts @@ -1,19 +1,21 @@ import State from "../../DataTypes/State/State"; import StateMachine from "../../DataTypes/State/StateMachine"; -import Goomba from "../MarioClone/Goomba"; +import GameNode from "../../Nodes/GameNode"; +import GoombaController from "./GoombaController"; export default abstract class GoombaState extends State { - owner: Goomba; + owner: GameNode; gravity: number = 7000; + parent: GoombaController - constructor(parent: StateMachine, owner: Goomba){ + constructor(parent: StateMachine, owner: GameNode){ super(parent); this.owner = owner; } update(deltaT: number): void { - // Do gravity; - this.owner.velocity.y += this.gravity*deltaT; + // Do gravity + this.parent.velocity.y += this.gravity*deltaT; } } \ No newline at end of file diff --git a/src/_DemoClasses/Enemies/Idle.ts b/src/_DemoClasses/Enemies/Idle.ts index 6dc8923..a3172f6 100644 --- a/src/_DemoClasses/Enemies/Idle.ts +++ b/src/_DemoClasses/Enemies/Idle.ts @@ -6,7 +6,7 @@ import OnGround from "./OnGround"; export default class Idle extends OnGround { onEnter(): void { - this.owner.speed = this.owner.speed; + this.parent.speed = this.parent.speed; } handleInput(event: GameEvent) { @@ -22,8 +22,8 @@ export default class Idle extends OnGround { update(deltaT: number): void { super.update(deltaT); - this.owner.velocity.x = 0; + this.parent.velocity.x = 0; - this.owner.move(this.owner.velocity.scaled(deltaT)); + this.owner.move(this.parent.velocity.scaled(deltaT)); } } \ No newline at end of file diff --git a/src/_DemoClasses/Enemies/Jump.ts b/src/_DemoClasses/Enemies/Jump.ts index a6cf237..dfae729 100644 --- a/src/_DemoClasses/Enemies/Jump.ts +++ b/src/_DemoClasses/Enemies/Jump.ts @@ -11,17 +11,17 @@ export default class Jump extends GoombaState { update(deltaT: number): void { super.update(deltaT); - if(this.owner.isGrounded()){ + if(this.owner.onGround){ this.finished(GoombaStates.PREVIOUS); } - if(this.owner.isOnCeiling()){ - this.owner.velocity.y = 0; + if(this.owner.onCeiling){ + this.parent.velocity.y = 0; } - this.owner.velocity.x += this.owner.direction.x * this.owner.speed/3.5 - 0.3*this.owner.velocity.x; + this.parent.velocity.x += this.parent.direction.x * this.parent.speed/3.5 - 0.3*this.parent.velocity.x; - this.owner.move(this.owner.velocity.scaled(deltaT)); + this.owner.move(this.parent.velocity.scaled(deltaT)); } onExit(): void {} diff --git a/src/_DemoClasses/Enemies/OnGround.ts b/src/_DemoClasses/Enemies/OnGround.ts index d8ec629..f529a77 100644 --- a/src/_DemoClasses/Enemies/OnGround.ts +++ b/src/_DemoClasses/Enemies/OnGround.ts @@ -7,19 +7,19 @@ export default class OnGround extends GoombaState { onEnter(): void {} handleInput(event: GameEvent): void { - if(event.type === CustomGameEventType.PLAYER_JUMP && (this.parentStateMachine).jumpy){ + if(event.type === CustomGameEventType.PLAYER_JUMP && (this.parent).jumpy){ this.finished(GoombaStates.JUMP); - this.owner.velocity.y = -2000; + this.parent.velocity.y = -2000; } } update(deltaT: number): void { - if(this.owner.velocity.y > 0){ - this.owner.velocity.y = 0; + if(this.parent.velocity.y > 0){ + this.parent.velocity.y = 0; } super.update(deltaT); - if(!this.owner.isGrounded()){ + if(!this.owner.onGround){ this.finished(GoombaStates.JUMP); } } diff --git a/src/_DemoClasses/Enemies/Walk.ts b/src/_DemoClasses/Enemies/Walk.ts index ffcc4ce..86667a5 100644 --- a/src/_DemoClasses/Enemies/Walk.ts +++ b/src/_DemoClasses/Enemies/Walk.ts @@ -3,21 +3,21 @@ import OnGround from "./OnGround"; export default class Walk extends OnGround { onEnter(): void { - if(this.owner.direction.isZero()){ - this.owner.direction = new Vec2(-1, 0); + if(this.parent.direction.isZero()){ + this.parent.direction = new Vec2(-1, 0); } } update(deltaT: number): void { super.update(deltaT); - if(this.owner.isOnWall()){ + if(this.owner.onWall){ // Flip around - this.owner.direction.x *= -1; + this.parent.direction.x *= -1; } - this.owner.velocity.x = this.owner.direction.x * this.owner.speed; + this.parent.velocity.x = this.parent.direction.x * this.parent.speed; - this.owner.move(this.owner.velocity.scaled(deltaT)); + this.owner.move(this.parent.velocity.scaled(deltaT)); } } \ No newline at end of file diff --git a/src/_DemoClasses/MarioClone/Goomba.ts b/src/_DemoClasses/MarioClone/Goomba.ts deleted file mode 100644 index 6574bc2..0000000 --- a/src/_DemoClasses/MarioClone/Goomba.ts +++ /dev/null @@ -1,30 +0,0 @@ -import AABB from "../../DataTypes/AABB"; -import Vec2 from "../../DataTypes/Vec2"; -import Sprite from "../../Nodes/Sprites/Sprite"; -import Collider from "../../Physics/Colliders/Collider"; -import PhysicsNode from "../../Physics/PhysicsNode"; -import GoombaController, { GoombaStates } from "../Enemies/GoombaController"; - -export default class Goomba extends PhysicsNode { - controller: GoombaController; - velocity: Vec2 = Vec2.ZERO; - speed: number = 200; - direction: Vec2 = Vec2.ZERO; - - constructor(position: Vec2, canJump: boolean){ - super(); - this.position.copy(position); - this.velocity = Vec2.ZERO; - this.controller = new GoombaController(this, canJump); - this.controller.initialize(GoombaStates.IDLE); - this.collider = new Collider(new AABB(position, new Vec2(32, 32))) - } - - create(): void { - - } - - update(deltaT: number): void { - this.controller.update(deltaT); - } -} \ No newline at end of file diff --git a/src/_DemoClasses/MarioClone/MarioClone.ts b/src/_DemoClasses/MarioClone/MarioClone.ts index 9c8d725..5fa9e44 100644 --- a/src/_DemoClasses/MarioClone/MarioClone.ts +++ b/src/_DemoClasses/MarioClone/MarioClone.ts @@ -1,9 +1,10 @@ import Scene from "../../Scene/Scene"; import Rect from "../../Nodes/Graphics/Rect"; import Vec2 from "../../DataTypes/Vec2"; -import Player from "./Player"; import Color from "../../Utils/Color"; -import Goomba from "./Goomba"; +import PlayerController from "../Player/PlayerStates/Platformer/PlayerController"; +import { PlayerStates } from "../Player/PlayerStates/Platformer/PlayerController"; +import GoombaController from "../Enemies/GoombaController"; export default class MarioClone extends Scene { @@ -16,19 +17,25 @@ export default class MarioClone extends Scene { let layer = this.addLayer(); this.add.tilemap("level", new Vec2(2, 2)); - let player = this.add.physics(Player, layer, new Vec2(0, 0)); - let playerSprite = this.add.graphic(Rect, layer, new Vec2(0, 0), new Vec2(64, 64)); - playerSprite.setColor(Color.BLUE); - player.addChild(playerSprite); - this.viewport.follow(playerSprite); + let player = this.add.graphic(Rect, layer, new Vec2(0, 0), new Vec2(64, 64)); + player.setColor(Color.BLUE); + player.addPhysics(); + + this.viewport.follow(player); this.viewport.setBounds(0, 0, 5120, 1280); + let ai = new PlayerController(player); + ai.initialize(PlayerStates.IDLE); + player.update = (deltaT: number) => {ai.update(deltaT)}; + for(let xPos of [14, 20, 25, 30, 33, 37, 49, 55, 58, 70, 74]){ - let goomba = this.add.physics(Goomba, layer, new Vec2(64*xPos, 0), true); - let goombaSprite = this.add.sprite("goomba", layer); - goombaSprite.setPosition(64*xPos, 0); - goombaSprite.setScale(new Vec2(2, 2)); - goomba.addChild(goombaSprite); + let goomba = this.add.sprite("goomba", layer); + let ai = new GoombaController(goomba, false); + ai.initialize("idle"); + goomba.update = (deltaT: number) => {ai.update(deltaT)}; + goomba.position.set(64*xPos, 0); + goomba.scale.set(2, 2); + goomba.addPhysics(); } } diff --git a/src/_DemoClasses/MarioClone/Player.ts b/src/_DemoClasses/MarioClone/Player.ts deleted file mode 100644 index 7d09a93..0000000 --- a/src/_DemoClasses/MarioClone/Player.ts +++ /dev/null @@ -1,33 +0,0 @@ -import AABB from "../../DataTypes/AABB"; -import Vec2 from "../../DataTypes/Vec2"; -import Debug from "../../Debug/Debug"; -import Collider from "../../Physics/Colliders/Collider"; -import PhysicsNode from "../../Physics/PhysicsNode"; -import PlayerController from "../Player/PlayerStates/Platformer/PlayerController"; -import { PlayerStates } from "../Player/PlayerStates/Platformer/PlayerController"; - -export default class Player extends PhysicsNode { - protected controller: PlayerController - velocity: Vec2; - speed: number = 400; - MIN_SPEED: number = 400; - MAX_SPEED: number = 1000; - - constructor(position: Vec2){ - super(); - this.position.copy(position); - this.velocity = Vec2.ZERO; - this.controller = new PlayerController(this); - this.controller.initialize(PlayerStates.IDLE); - this.collider = new Collider(new AABB(Vec2.ZERO, new Vec2(32, 32))) - } - - create(): void { - - } - - update(deltaT: number): void { - this.controller.update(deltaT); - Debug.log("playerVel", "Pos: " + this.position.toString() + ", Vel: " + this.velocity.toString()) - } -} \ No newline at end of file diff --git a/src/_DemoClasses/Player/PlayerStates/MoveTopDown.ts b/src/_DemoClasses/Player/PlayerStates/MoveTopDown.ts index 9392f05..72a3156 100644 --- a/src/_DemoClasses/Player/PlayerStates/MoveTopDown.ts +++ b/src/_DemoClasses/Player/PlayerStates/MoveTopDown.ts @@ -39,7 +39,7 @@ export default class MoveTopDown extends State { // Otherwise, we are still moving, so update position let velocity = this.direction.normalize().scale(this.speed); - this.owner.position.add(velocity.scale(deltaT)); + this.owner.move(velocity.scale(deltaT)); // Emit an event to tell the world we are moving this.emitter.fireEvent(CustomGameEventType.PLAYER_MOVE, {position: this.owner.position.clone()}); diff --git a/src/_DemoClasses/Player/PlayerStates/Platformer/Idle.ts b/src/_DemoClasses/Player/PlayerStates/Platformer/Idle.ts index 26f0b73..d278084 100644 --- a/src/_DemoClasses/Player/PlayerStates/Platformer/Idle.ts +++ b/src/_DemoClasses/Player/PlayerStates/Platformer/Idle.ts @@ -4,7 +4,7 @@ import PlayerState from "./PlayerState"; export default class Idle extends OnGround { onEnter(): void { - this.owner.speed = this.owner.MIN_SPEED; + this.parent.speed = this.parent.MIN_SPEED; } update(deltaT: number): void { @@ -20,8 +20,8 @@ export default class Idle extends OnGround { } } - this.owner.velocity.x = 0; + this.parent.velocity.x = 0; - this.owner.move(this.owner.velocity.scaled(deltaT)); + this.owner.move(this.parent.velocity.scaled(deltaT)); } } \ No newline at end of file diff --git a/src/_DemoClasses/Player/PlayerStates/Platformer/Jump.ts b/src/_DemoClasses/Player/PlayerStates/Platformer/Jump.ts index b0ee81c..cb9932e 100644 --- a/src/_DemoClasses/Player/PlayerStates/Platformer/Jump.ts +++ b/src/_DemoClasses/Player/PlayerStates/Platformer/Jump.ts @@ -14,20 +14,20 @@ export default class Jump extends PlayerState { update(deltaT: number): void { super.update(deltaT); - if(this.owner.isGrounded()){ + if(this.owner.onGround){ this.finished(PlayerStates.PREVIOUS); } - if(this.owner.isOnCeiling()){ - this.owner.velocity.y = 0; + if(this.owner.onCeiling){ + this.parent.velocity.y = 0; } let dir = this.getInputDirection(); - this.owner.velocity.x += dir.x * this.owner.speed/3.5 - 0.3*this.owner.velocity.x; + this.parent.velocity.x += dir.x * this.parent.speed/3.5 - 0.3*this.parent.velocity.x; this.emitter.fireEvent(CustomGameEventType.PLAYER_MOVE, {position: this.owner.position.clone()}); - this.owner.move(this.owner.velocity.scaled(deltaT)); + this.owner.move(this.parent.velocity.scaled(deltaT)); } onExit(): void {} diff --git a/src/_DemoClasses/Player/PlayerStates/Platformer/OnGround.ts b/src/_DemoClasses/Player/PlayerStates/Platformer/OnGround.ts index 2bbf4a5..9197514 100644 --- a/src/_DemoClasses/Player/PlayerStates/Platformer/OnGround.ts +++ b/src/_DemoClasses/Player/PlayerStates/Platformer/OnGround.ts @@ -8,16 +8,16 @@ export default class OnGround extends PlayerState { handleInput(event: GameEvent): void {} update(deltaT: number): void { - if(this.owner.velocity.y > 0){ - this.owner.velocity.y = 0; + if(this.parent.velocity.y > 0){ + this.parent.velocity.y = 0; } super.update(deltaT); if(this.input.isJustPressed("w") || this.input.isJustPressed("space")){ this.finished("jump"); - this.owner.velocity.y = -2000; + this.parent.velocity.y = -2000; this.emitter.fireEvent(CustomGameEventType.PLAYER_JUMP) - } else if(!this.owner.isGrounded()){ + } else if(!this.owner.onGround){ this.finished("jump"); } } diff --git a/src/_DemoClasses/Player/PlayerStates/Platformer/PlayerController.ts b/src/_DemoClasses/Player/PlayerStates/Platformer/PlayerController.ts index c75dbe2..2f1fb96 100644 --- a/src/_DemoClasses/Player/PlayerStates/Platformer/PlayerController.ts +++ b/src/_DemoClasses/Player/PlayerStates/Platformer/PlayerController.ts @@ -1,10 +1,11 @@ import StateMachine from "../../../../DataTypes/State/StateMachine"; import Debug from "../../../../Debug/Debug"; -import Player from "../../../MarioClone/Player"; import Idle from "./Idle"; import Jump from "./Jump"; import Walk from "./Walk"; import Run from "./Run"; +import GameNode from "../../../../Nodes/GameNode"; +import Vec2 from "../../../../DataTypes/Vec2"; export enum PlayerStates { WALK = "walk", @@ -15,9 +16,13 @@ export enum PlayerStates { } export default class PlayerController extends StateMachine { - protected owner: Player; + protected owner: GameNode; + velocity: Vec2 = Vec2.ZERO; + speed: number = 400; + MIN_SPEED: number = 400; + MAX_SPEED: number = 1000; - constructor(owner: Player){ + constructor(owner: GameNode){ super(); this.owner = owner; diff --git a/src/_DemoClasses/Player/PlayerStates/Platformer/PlayerState.ts b/src/_DemoClasses/Player/PlayerStates/Platformer/PlayerState.ts index 1ee2ae8..4c64bdf 100644 --- a/src/_DemoClasses/Player/PlayerStates/Platformer/PlayerState.ts +++ b/src/_DemoClasses/Player/PlayerStates/Platformer/PlayerState.ts @@ -2,15 +2,17 @@ import State from "../../../../DataTypes/State/State"; import StateMachine from "../../../../DataTypes/State/StateMachine"; import Vec2 from "../../../../DataTypes/Vec2"; import InputReceiver from "../../../../Input/InputReceiver"; -import CanvasNode from "../../../../Nodes/CanvasNode"; -import Player from "../../../MarioClone/Player"; +import GameNode from "../../../../Nodes/GameNode"; +import PlayerController from "./PlayerController"; + export default abstract class PlayerState extends State { input: InputReceiver = InputReceiver.getInstance(); - owner: Player; + owner: GameNode; gravity: number = 7000; + parent: PlayerController; - constructor(parent: StateMachine, owner: Player){ + constructor(parent: StateMachine, owner: GameNode){ super(parent); this.owner = owner; } @@ -27,7 +29,7 @@ export default abstract class PlayerState extends State { } update(deltaT: number): void { - // Do gravity; - this.owner.velocity.y += this.gravity*deltaT; + // Do gravity + this.parent.velocity.y += this.gravity*deltaT; } } \ No newline at end of file diff --git a/src/_DemoClasses/Player/PlayerStates/Platformer/Run.ts b/src/_DemoClasses/Player/PlayerStates/Platformer/Run.ts index 3e320a3..022f0d1 100644 --- a/src/_DemoClasses/Player/PlayerStates/Platformer/Run.ts +++ b/src/_DemoClasses/Player/PlayerStates/Platformer/Run.ts @@ -4,7 +4,7 @@ import { PlayerStates } from "./PlayerController"; export default class Run extends OnGround { onEnter(): void { - this.owner.speed = this.owner.MAX_SPEED; + this.parent.speed = this.parent.MAX_SPEED; } update(deltaT: number): void { @@ -20,9 +20,9 @@ export default class Run extends OnGround { } } - this.owner.velocity.x = dir.x * this.owner.speed + this.parent.velocity.x = dir.x * this.parent.speed this.emitter.fireEvent(CustomGameEventType.PLAYER_MOVE, {position: this.owner.position.clone()}); - this.owner.move(this.owner.velocity.scaled(deltaT)); + this.owner.move(this.parent.velocity.scaled(deltaT)); } } \ No newline at end of file diff --git a/src/_DemoClasses/Player/PlayerStates/Platformer/Walk.ts b/src/_DemoClasses/Player/PlayerStates/Platformer/Walk.ts index f69b33d..b0ffa29 100644 --- a/src/_DemoClasses/Player/PlayerStates/Platformer/Walk.ts +++ b/src/_DemoClasses/Player/PlayerStates/Platformer/Walk.ts @@ -4,7 +4,7 @@ import { PlayerStates } from "./PlayerController"; export default class Walk extends OnGround { onEnter(): void { - this.owner.speed = this.owner.MAX_SPEED/2; + this.parent.speed = this.parent.MAX_SPEED/2; } update(deltaT: number): void { @@ -20,9 +20,9 @@ export default class Walk extends OnGround { } } - this.owner.velocity.x = dir.x * this.owner.speed + this.parent.velocity.x = dir.x * this.parent.speed this.emitter.fireEvent(CustomGameEventType.PLAYER_MOVE, {position: this.owner.position.clone()}); - this.owner.move(this.owner.velocity.scaled(deltaT)); + this.owner.move(this.parent.velocity.scaled(deltaT)); } } \ No newline at end of file diff --git a/src/main.ts b/src/main.ts index 8eae268..7a97e93 100644 --- a/src/main.ts +++ b/src/main.ts @@ -10,7 +10,7 @@ function main(){ let game = new GameLoop({canvasSize: {x: 800, y: 600}}); game.start(); let sm = game.getSceneManager(); - sm.addScene(BoidDemo); + sm.addScene(MarioClone); } CanvasRenderingContext2D.prototype.roundedRect = function(x: number, y: number, w: number, h: number, r: number): void { diff --git a/tsconfig.json b/tsconfig.json index dcc62a5..092b94a 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -39,7 +39,7 @@ "src/Physics/Colliders/AABB.ts", "src/Physics/Colliders/Collider.ts", - "src/Physics/PhysicsManager.ts", + "src/Physics/PhysicsManager_Old.ts", "src/Physics/PhysicsNode.ts", "src/Playback/Recorder.ts",