reworked physics system

This commit is contained in:
Joe Weaver 2020-10-25 17:46:43 -04:00
parent c77a947cc0
commit 19028a9a58
60 changed files with 1142 additions and 1130 deletions

View File

@ -35,7 +35,7 @@ export default class BoidDemo extends Scene {
for(let i = 0; i < 150; i++){ 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())); 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.fb = new FlockBehavior(this, boid, this.boids, 75, 50);
boid.setSize(5, 5); boid.size.set(5, 5);
this.boids.push(boid); this.boids.push(boid);
} }
} }

View File

@ -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"; import Vec2 from "../Vec2";
export interface Unique { export interface Unique {
getId: () => number; /** The unique id of this object. */
id: number;
} }
export interface Positioned { export interface Positioned {
/** /** The center of this object. */
* Returns the center of this object position: Vec2;
*/
getPosition: () => Vec2;
} }
export interface Region { export interface Region {
/** /** The size of this object. */
* Returns 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.
*/ */
getSize: () => Vec2; 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<string>;
/** 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 { export interface Updateable {
/** /** Updates this object. */
* Updates this object
*/
update: (deltaT: number) => void; update: (deltaT: number) => void;
} }
export interface Renderable { export interface Renderable {
/** /** Renders this object. */
* Renders this object
*/
render: (ctx: CanvasRenderingContext2D) => void; render: (ctx: CanvasRenderingContext2D) => void;
} }
export interface Debug_Renderable {
/** Renders the debugging infor for this object. */
debug_render: (ctx: CanvasRenderingContext2D) => void;
}

View File

@ -1,6 +1,6 @@
import Vec2 from "./Vec2"; import Vec2 from "./Vec2";
import Collection from "./Collection"; import Collection from "./Collection";
import AABB from "./AABB" import AABB from "./Shapes/AABB"
import { Region, Unique } from "./Interfaces/Descriptors"; import { Region, Unique } from "./Interfaces/Descriptors";
import Map from "./Map"; import Map from "./Map";
import Stats from "../Debug/Stats"; import Stats from "../Debug/Stats";
@ -74,7 +74,7 @@ export default class QuadTree<T extends Region & Unique> implements Collection {
*/ */
insert(item: T): void { insert(item: T): void {
// If the item is inside of the bounds of this quadtree // 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){ if(this.divided){
// Defer to the children // Defer to the children
this.deferInsert(item); this.deferInsert(item);
@ -124,9 +124,9 @@ export default class QuadTree<T extends Region & Unique> implements Collection {
} else { } else {
// Otherwise, return a set of the items // Otherwise, return a set of the items
for(let item of this.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 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 // Add it to our found points
uniqueMap.add(id, item); uniqueMap.add(id, item);
results.push(item); results.push(item);
@ -182,10 +182,10 @@ export default class QuadTree<T extends Region & Unique> implements Collection {
// } // }
// Maybe this is better? Just use a boolean array with no string nonsense? // Maybe this is better? Just use a boolean array with no string nonsense?
if(item.getId() >= uniqueMap.length || !uniqueMap[item.getId()]){ if(item.id >= uniqueMap.length || !uniqueMap[item.id]){
if(item.getBoundary().overlaps(boundary)){ if(item.boundary.overlaps(boundary)){
results.push(item); results.push(item);
uniqueMap[item.getId()] = true; uniqueMap[item.id] = true;
} }
} }
} }

View File

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

View File

@ -1,11 +1,12 @@
import Shape from "./Shape"; import Shape from "./Shape";
import Vec2 from "./Vec2"; import Vec2 from "../Vec2";
import MathUtils from "../Utils/MathUtils"; import MathUtils from "../../Utils/MathUtils";
import Circle from "./Circle";
export default class AABB extends Shape { export default class AABB extends Shape {
protected center: Vec2; center: Vec2;
protected halfSize: Vec2; halfSize: Vec2;
constructor(center?: Vec2, halfSize?: Vec2){ constructor(center?: Vec2, halfSize?: Vec2){
super(); super();
@ -45,16 +46,13 @@ export default class AABB extends Shape {
return this.x + this.hw; return this.x + this.hw;
} }
getCenter(): Vec2 {
return this.center;
}
setCenter(center: Vec2): void {
this.center = center;
}
getBoundingRect(): AABB { 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 { getHalfSize(): Vec2 {
@ -126,10 +124,10 @@ export default class AABB extends Shape {
let signX = MathUtils.sign(scaleX); let signX = MathUtils.sign(scaleX);
let signY = MathUtils.sign(scaleY); let signY = MathUtils.sign(scaleY);
let tnearx = scaleX*(this.center.x - signX*(this.halfSize.x + _paddingX) - point.x); let tnearx = scaleX*(this.x - signX*(this.hw + _paddingX) - point.x);
let tneary = scaleX*(this.center.y - signY*(this.halfSize.y + _paddingY) - point.y); let tneary = scaleX*(this.y - signY*(this.hh + _paddingY) - point.y);
let tfarx = scaleY*(this.center.x + signX*(this.halfSize.x + _paddingX) - point.x); let tfarx = scaleY*(this.x + signX*(this.hw + _paddingX) - point.x);
let tfary = scaleY*(this.center.y + signY*(this.halfSize.y + _paddingY) - point.y); let tfary = scaleY*(this.y + signY*(this.hh + _paddingY) - point.y);
if(tnearx > tfary || tneary > tfarx){ if(tnearx > tfary || tneary > tfarx){
// We aren't colliding - we clear one axis before intersecting another // We aren't colliding - we clear one axis before intersecting another
@ -164,11 +162,18 @@ export default class AABB extends Shape {
return hit; 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 * A simple boolean check of whether this AABB overlaps another
* @param other * @param other
*/ */
overlaps(other: AABB): boolean { overlapsAABB(other: AABB): boolean {
let dx = other.x - this.x; let dx = other.x - this.x;
let px = this.hw + other.hw - Math.abs(dx); let px = this.hw + other.hw - Math.abs(dx);
@ -200,6 +205,35 @@ export default class AABB extends Shape {
return dx*dy; 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 { export class Hit {

View File

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

View File

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

View File

@ -4,11 +4,11 @@ import { Updateable } from "../Interfaces/Descriptors";
import StateMachine from "./StateMachine"; import StateMachine from "./StateMachine";
export default abstract class State implements Updateable { export default abstract class State implements Updateable {
protected parentStateMachine: StateMachine; protected parent: StateMachine;
protected emitter: Emitter; protected emitter: Emitter;
constructor(parent: StateMachine) { constructor(parent: StateMachine) {
this.parentStateMachine = parent; this.parent = parent;
this.emitter = new Emitter(); 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 * @param stateName The name of the state to transition to
*/ */
protected finished(stateName: string): void { protected finished(stateName: string): void {
this.parentStateMachine.changeState(stateName); this.parent.changeState(stateName);
} }
/** /**

View File

@ -46,6 +46,8 @@ export default class Vec2 {
return new Vec2(0, 0); return new Vec2(0, 0);
} }
static readonly ZERO_STATIC = new Vec2(0, 0);
static get INF() { static get INF() {
return new Vec2(Infinity, Infinity); return new Vec2(Infinity, Infinity);
} }

View File

@ -1,6 +1,4 @@
import Scene from "./Scene/Scene"; import Scene from "./Scene/Scene";
import OrthogonalTilemap from "./Nodes/Tilemaps/OrthogonalTilemap";
import Player from "./Player";
import Rect from "./Nodes/Graphics/Rect"; import Rect from "./Nodes/Graphics/Rect";
import Color from "./Utils/Color"; import Color from "./Utils/Color";
import Vec2 from "./DataTypes/Vec2"; import Vec2 from "./DataTypes/Vec2";
@ -10,6 +8,7 @@ import Layer from "./Scene/Layer";
import SecondScene from "./SecondScene"; import SecondScene from "./SecondScene";
import { GameEventType } from "./Events/GameEventType"; import { GameEventType } from "./Events/GameEventType";
import SceneGraphQuadTree from "./SceneGraph/SceneGraphQuadTree"; import SceneGraphQuadTree from "./SceneGraph/SceneGraphQuadTree";
import PlayerController from "./_DemoClasses/Player/PlayerStates/Platformer/PlayerController";
export default class MainScene extends Scene { export default class MainScene extends Scene {
@ -27,7 +26,7 @@ export default class MainScene extends Scene {
bar.setColor(new Color(0, 200, 200)); bar.setColor(new Color(0, 200, 200));
this.load.onLoadProgress = (percentProgress: number) => { this.load.onLoadProgress = (percentProgress: number) => {
bar.setSize(295 * percentProgress, bar.getSize().y); bar.size.x = 295 * percentProgress;
} }
this.load.onLoadComplete = () => { this.load.onLoadComplete = () => {
@ -58,40 +57,38 @@ export default class MainScene extends Scene {
let mainLayer = this.addLayer(); let mainLayer = this.addLayer();
// Add a player // Add a player
let player = this.add.physics(Player, mainLayer, "platformer");
let playerSprite = this.add.sprite("player", mainLayer) let playerSprite = this.add.sprite("player", mainLayer)
player.setSprite(playerSprite); playerSprite.position.set(0, 0);
playerSprite.position = player.position.clone(); playerSprite.size.set(64, 64);
playerSprite.setSize(new Vec2(64, 64));
this.viewport.follow(player); this.viewport.follow(playerSprite);
// Initialize UI // Initialize UI
let uiLayer = this.addLayer(); let uiLayer = this.addLayer();
uiLayer.setParallax(0, 0); uiLayer.setParallax(0, 0);
let recordButton = this.add.uiElement(Button, uiLayer); let recordButton = this.add.uiElement(Button, uiLayer);
recordButton.setSize(100, 50); recordButton.size.set(100, 50);
recordButton.setText("Record"); recordButton.setText("Record");
recordButton.setPosition(400, 30); recordButton.position.set(400, 30);
recordButton.onClickEventId = GameEventType.START_RECORDING; recordButton.onClickEventId = GameEventType.START_RECORDING;
let stopButton = this.add.uiElement(Button, uiLayer); let stopButton = this.add.uiElement(Button, uiLayer);
stopButton.setSize(100, 50); stopButton.size.set(100, 50);
stopButton.setText("Stop"); stopButton.setText("Stop");
stopButton.setPosition(550, 30); stopButton.position.set(550, 30);
stopButton.onClickEventId = GameEventType.STOP_RECORDING; stopButton.onClickEventId = GameEventType.STOP_RECORDING;
let playButton = this.add.uiElement(Button, uiLayer); let playButton = this.add.uiElement(Button, uiLayer);
playButton.setSize(100, 50); playButton.size.set(100, 50);
playButton.setText("Play"); playButton.setText("Play");
playButton.setPosition(700, 30); playButton.position.set(700, 30);
playButton.onClickEventId = GameEventType.PLAY_RECORDING; playButton.onClickEventId = GameEventType.PLAY_RECORDING;
let cycleFramerateButton = this.add.uiElement(Button, uiLayer); let cycleFramerateButton = this.add.uiElement(Button, uiLayer);
cycleFramerateButton.setSize(150, 50); cycleFramerateButton.size.set(150, 50);
cycleFramerateButton.setText("Cycle FPS"); cycleFramerateButton.setText("Cycle FPS");
cycleFramerateButton.setPosition(5, 400); cycleFramerateButton.position.set(5, 400);
let i = 0; let i = 0;
let fps = [15, 30, 60]; let fps = [15, 30, 60];
cycleFramerateButton.onClick = () => { cycleFramerateButton.onClick = () => {
@ -105,32 +102,32 @@ export default class MainScene extends Scene {
pauseLayer.disable(); pauseLayer.disable();
let pauseButton = this.add.uiElement(Button, uiLayer); let pauseButton = this.add.uiElement(Button, uiLayer);
pauseButton.setSize(100, 50); pauseButton.size.set(100, 50);
pauseButton.setText("Pause"); pauseButton.setText("Pause");
pauseButton.setPosition(700, 400); pauseButton.position.set(700, 400);
pauseButton.onClick = () => { pauseButton.onClick = () => {
this.sceneGraph.getLayers().forEach((layer: Layer) => layer.setPaused(true)); this.sceneGraph.getLayers().forEach((layer: Layer) => layer.setPaused(true));
pauseLayer.enable(); pauseLayer.enable();
} }
let modalBackground = this.add.uiElement(UIElement, pauseLayer); 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.setBackgroundColor(new Color(0, 0, 0, 0.4));
modalBackground.setPosition(200, 100); modalBackground.position.set(200, 100);
let resumeButton = this.add.uiElement(Button, pauseLayer); let resumeButton = this.add.uiElement(Button, pauseLayer);
resumeButton.setSize(100, 50); resumeButton.size.set(100, 50);
resumeButton.setText("Resume"); resumeButton.setText("Resume");
resumeButton.setPosition(360, 150); resumeButton.position.set(360, 150);
resumeButton.onClick = () => { resumeButton.onClick = () => {
this.sceneGraph.getLayers().forEach((layer: Layer) => layer.setPaused(false)); this.sceneGraph.getLayers().forEach((layer: Layer) => layer.setPaused(false));
pauseLayer.disable(); pauseLayer.disable();
} }
let switchButton = this.add.uiElement(Button, pauseLayer); let switchButton = this.add.uiElement(Button, pauseLayer);
switchButton.setSize(140, 50); switchButton.size.set(140, 50);
switchButton.setText("Change Scene"); switchButton.setText("Change Scene");
switchButton.setPosition(340, 190); switchButton.position.set(340, 190);
switchButton.onClick = () => { switchButton.onClick = () => {
this.emitter.fireEvent(GameEventType.STOP_SOUND, {key: "level_music"}); this.emitter.fireEvent(GameEventType.STOP_SOUND, {key: "level_music"});
this.sceneManager.changeScene(SecondScene); this.sceneManager.changeScene(SecondScene);

View File

@ -1,7 +1,7 @@
import GameNode from "./GameNode"; import GameNode from "./GameNode";
import Vec2 from "../DataTypes/Vec2"; import Vec2 from "../DataTypes/Vec2";
import { Region } from "../DataTypes/Interfaces/Descriptors"; 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 * 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 { export default abstract class CanvasNode extends GameNode implements Region {
private _size: Vec2; private _size: Vec2;
private _scale: Vec2; private _scale: Vec2;
private boundary: AABB; private _boundary: AABB;
constructor(){ constructor(){
super(); super();
@ -18,7 +18,7 @@ export default abstract class CanvasNode extends GameNode implements Region {
this._size.setOnChange(this.sizeChanged); this._size.setOnChange(this.sizeChanged);
this._scale = new Vec2(1, 1); this._scale = new Vec2(1, 1);
this._scale.setOnChange(this.scaleChanged); this._scale.setOnChange(this.scaleChanged);
this.boundary = new AABB(); this._boundary = new AABB();
this.updateBoundary(); this.updateBoundary();
} }
@ -42,34 +42,11 @@ export default abstract class CanvasNode extends GameNode implements Region {
this.scaleChanged(); 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 => { protected positionChanged = (): void => {
if(this.hasPhysics){
this.collisionShape.center = this.position;
}
this.updateBoundary(); this.updateBoundary();
} }
@ -82,12 +59,12 @@ export default abstract class CanvasNode extends GameNode implements Region {
} }
private updateBoundary(): void { private updateBoundary(): void {
this.boundary.setCenter(this.position.clone()); this._boundary.center.set(this.position.x, this.position.y);
this.boundary.setHalfSize(this.size.clone().mult(this.scale).scale(1/2)); this._boundary.halfSize.set(this.size.x*this.scale.x/2, this.size.y*this.scale.y/2);
} }
getBoundary(): AABB { get boundary(): AABB {
return this.boundary; return this._boundary;
} }
/** /**
@ -96,7 +73,7 @@ export default abstract class CanvasNode extends GameNode implements Region {
* @param y * @param y
*/ */
contains(x: number, y: number): boolean { 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; abstract render(ctx: CanvasRenderingContext2D): void;

View File

@ -4,19 +4,44 @@ import Receiver from "../Events/Receiver";
import Emitter from "../Events/Emitter"; import Emitter from "../Events/Emitter";
import Scene from "../Scene/Scene"; import Scene from "../Scene/Scene";
import Layer from "../Scene/Layer"; 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 * The representation of an object in the game world
*/ */
export default abstract class GameNode implements Positioned, Unique, Updateable { export default abstract class GameNode implements Positioned, Unique, Updateable, Physical {
protected input: InputReceiver; /*---------- POSITIONED ----------*/
private _position: Vec2; 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<string>;
_velocity: Vec2;
sweptRect: AABB;
protected input: InputReceiver;
protected receiver: Receiver; protected receiver: Receiver;
protected emitter: Emitter; protected emitter: Emitter;
protected scene: Scene; protected scene: Scene;
protected layer: Layer; protected layer: Layer;
private id: number;
constructor(){ constructor(){
this.input = InputReceiver.getInstance(); this.input = InputReceiver.getInstance();
@ -26,22 +51,7 @@ export default abstract class GameNode implements Positioned, Unique, Updateable
this.emitter = new Emitter(); this.emitter = new Emitter();
} }
setScene(scene: Scene): void { /*---------- POSITIONED ----------*/
this.scene = scene;
}
getScene(): Scene {
return this.scene;
}
setLayer(layer: Layer): void {
this.layer = layer;
}
getLayer(): Layer {
return this.layer;
}
get position(): Vec2 { get position(): Vec2 {
return this._position; return this._position;
} }
@ -52,30 +62,116 @@ export default abstract class GameNode implements Positioned, Unique, Updateable
this.positionChanged(); this.positionChanged();
} }
getPosition(): Vec2 { /*---------- UNIQUE ----------*/
return this._position.clone(); get id(): number {
return this._id;
} }
setPosition(vecOrX: Vec2 | number, y: number = null): void { set id(id: number) {
if(vecOrX instanceof Vec2){ // id can only be set once
this.position.set(vecOrX.x, vecOrX.y); if(this._id === undefined){
this._id = id;
} else { } else {
this.position.set(vecOrX, y); throw "Attempted to assign id to object that already has id."
} }
} }
setId(id: number): void { /*---------- PHYSICAL ----------*/
this.id = id; /**
* @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 = (<any>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 * 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? // TODO - This doesn't seem ideal. Is there a better way to do this?
getViewportOriginWithParallax(): Vec2 { getViewportOriginWithParallax(): Vec2 {
@ -86,6 +182,5 @@ export default abstract class GameNode implements Positioned, Unique, Updateable
return this.scene.getViewport().getZoomLevel(); return this.scene.getViewport().getZoomLevel();
} }
abstract update(deltaT: number): void; abstract update(deltaT: number): void;
} }

View File

@ -6,7 +6,7 @@ export default class Point extends Graphic {
constructor(position: Vec2){ constructor(position: Vec2){
super(); super();
this.position = position; this.position = position;
this.setSize(5, 5); this.size.set(5, 5);
} }
update(deltaT: number): void {} update(deltaT: number): void {}

View File

@ -39,7 +39,7 @@ export default class Sprite extends CanvasNode {
ctx.lineWidth = 4; ctx.lineWidth = 4;
ctx.strokeStyle = "#00FF00" 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); ctx.strokeRect(b.x - b.hw - origin.x, b.y - b.hh - origin.y, b.hw*2*zoom, b.hh*2*zoom);
} }
} }

View File

@ -13,7 +13,6 @@ export default abstract class Tilemap extends GameNode {
protected tileSize: Vec2; protected tileSize: Vec2;
protected scale: Vec2; protected scale: Vec2;
public data: Array<number>; public data: Array<number>;
public collidable: boolean;
public visible: boolean; public visible: boolean;
// TODO: Make this no longer be specific to Tiled // TODO: Make this no longer be specific to Tiled
@ -48,14 +47,15 @@ export default abstract class Tilemap extends GameNode {
this.scale = scale; this.scale = scale;
} }
isCollidable(): boolean {
return this.collidable;
}
isVisible(): boolean { isVisible(): boolean {
return this.visible; return this.visible;
} }
/** Adds this tilemaps to the physics system */
addPhysics = (): void => {
this.scene.getPhysicsManager().registerTilemap(this);
}
abstract getTileAt(worldCoords: Vec2): number; abstract getTileAt(worldCoords: Vec2): number;
/** /**

View File

@ -18,11 +18,11 @@ export default class OrthogonalTilemap extends Tilemap {
this.tileSize.set(tilemapData.tilewidth, tilemapData.tileheight); this.tileSize.set(tilemapData.tilewidth, tilemapData.tileheight);
this.data = layer.data; this.data = layer.data;
this.visible = layer.visible; this.visible = layer.visible;
this.collidable = false; this.isCollidable = false;
if(layer.properties){ if(layer.properties){
for(let item of layer.properties){ for(let item of layer.properties){
if(item.name === "Collidable"){ 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 // TODO - Currently, all tiles in a collidable layer are collidable
return this.data[index] !== 0 && this.collidable; return this.data[index] !== 0 && this.isCollidable;
} }
/** /**

View File

@ -201,7 +201,7 @@ export default class UIElement extends CanvasNode {
ctx.lineWidth = 4; ctx.lineWidth = 4;
ctx.strokeStyle = "#00FF00" 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); ctx.strokeRect(b.x - b.hw - origin.x, b.y - b.hh - origin.y, b.hw*2, b.hh*2);
} }
} }

View File

@ -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<Physical>;
/** The array of dynamic nodes */
protected dynamicNodes: Array<Physical>;
/** The array of tilemaps */
protected tilemaps: Array<Tilemap>;
/** 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<TileCollisionData>();
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;
}
}

View File

@ -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<Physical[]>;
abstract addNode(node: GameNode): void;
}

View File

@ -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<Physical>;
protected yList: Array<Physical>;
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<Physical[]> {
// 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;
}
}

View File

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

View File

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

View File

@ -1,302 +1,24 @@
import PhysicsNode from "./PhysicsNode"; import GameNode from "../Nodes/GameNode";
import Vec2 from "../DataTypes/Vec2"; import Vec2 from "../DataTypes/Vec2";
import StaticBody from "./StaticBody"; import { Debug_Renderable, Updateable } from "../DataTypes/Interfaces/Descriptors";
import Debug from "../Debug/Debug";
import MathUtils from "../Utils/MathUtils";
import Tilemap from "../Nodes/Tilemap"; import Tilemap from "../Nodes/Tilemap";
import OrthogonalTilemap from "../Nodes/Tilemaps/OrthogonalTilemap"; import Receiver from "../Events/Receiver";
import AABB from "../DataTypes/AABB"; import Emitter from "../Events/Emitter";
import { getTimeOfCollision } from "./Colliders/Collisions";
import Collider from "./Colliders/Collider";
export default class PhysicsManager { export default abstract class PhysicsManager implements Updateable, Debug_Renderable {
protected receiver: Receiver;
private physicsNodes: Array<PhysicsNode>; protected emitter: Emitter;
private tilemaps: Array<Tilemap>;
private movements: Array<MovementData>;
private tcols: Array<TileCollisionData> = [];
constructor(){ constructor(){
this.physicsNodes = new Array(); this.receiver = new Receiver();
this.tilemaps = new Array(); this.emitter = new Emitter();
this.movements = new Array();
} }
/** abstract registerObject(object: GameNode): void;
* Adds a PhysicsNode to the manager to be handled in case of collisions
* @param node
*/
add(node: PhysicsNode): void {
this.physicsNodes.push(node);
}
/** abstract registerTilemap(tilemap: Tilemap): void;
* Adds a tilemap node to the manager to be handled for collisions
* @param tilemap
*/
addTilemap(tilemap: Tilemap): void {
this.tilemaps.push(tilemap);
}
/** abstract update(deltaT: number): 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 debug_render(ctx: CanvasRenderingContext2D): 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<TileCollisionData>();
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<PhysicsNode>();
let dynamicSet = new Array<PhysicsNode>();
// 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 = (<AABB>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;
}
} }

View File

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

View File

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

View File

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

View File

@ -1,5 +1,4 @@
import Scene from "./Scene/Scene"; import Scene from "./Scene/Scene";
import { GameEventType } from "./Events/GameEventType"
import Point from "./Nodes/Graphics/Point"; import Point from "./Nodes/Graphics/Point";
import Rect from "./Nodes/Graphics/Rect"; import Rect from "./Nodes/Graphics/Rect";
import Layer from "./Scene/Layer"; import Layer from "./Scene/Layer";
@ -7,9 +6,6 @@ import SceneGraphQuadTree from "./SceneGraph/SceneGraphQuadTree"
import Vec2 from "./DataTypes/Vec2"; import Vec2 from "./DataTypes/Vec2";
import InputReceiver from "./Input/InputReceiver"; import InputReceiver from "./Input/InputReceiver";
import Color from "./Utils/Color"; 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 { export default class QuadTreeScene extends Scene {
@ -41,14 +37,14 @@ export default class QuadTreeScene extends Scene {
} }
updateScene(deltaT: number): void { updateScene(deltaT: number): void {
this.view.setPosition(InputReceiver.getInstance().getGlobalMousePosition()); this.view.position.copy(InputReceiver.getInstance().getGlobalMousePosition());
for(let point of this.points){ for(let point of this.points){
point.setColor(Color.RED); 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))); 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){ for(let result of results){
if(result instanceof Point){ if(result instanceof Point){

View File

@ -23,7 +23,7 @@ export default class CanvasNodeFactory {
// Add instance to scene // Add instance to scene
instance.setScene(this.scene); instance.setScene(this.scene);
instance.setId(this.scene.generateId()); instance.id = this.scene.generateId();
this.scene.getSceneGraph().addNode(instance); this.scene.getSceneGraph().addNode(instance);
// Add instance to layer // Add instance to layer
@ -42,7 +42,7 @@ export default class CanvasNodeFactory {
// Add instance to scene // Add instance to scene
instance.setScene(this.scene); instance.setScene(this.scene);
instance.setId(this.scene.generateId()); instance.id = this.scene.generateId();
this.scene.getSceneGraph().addNode(instance); this.scene.getSceneGraph().addNode(instance);
// Add instance to layer // Add instance to layer
@ -62,7 +62,7 @@ export default class CanvasNodeFactory {
// Add instance to scene // Add instance to scene
instance.setScene(this.scene); instance.setScene(this.scene);
instance.setId(this.scene.generateId()); instance.id = this.scene.generateId();
this.scene.getSceneGraph().addNode(instance); this.scene.getSceneGraph().addNode(instance);
// Add instance to layer // Add instance to layer

View File

@ -1,21 +1,17 @@
import Scene from "../Scene"; import Scene from "../Scene";
import PhysicsNodeFactory from "./PhysicsNodeFactory";
import CanvasNodeFactory from "./CanvasNodeFactory"; import CanvasNodeFactory from "./CanvasNodeFactory";
import TilemapFactory from "./TilemapFactory"; import TilemapFactory from "./TilemapFactory";
import PhysicsManager from "../../Physics/PhysicsManager"; import PhysicsManager from "../../Physics/PhysicsManager";
import SceneGraph from "../../SceneGraph/SceneGraph";
import Tilemap from "../../Nodes/Tilemap"; import Tilemap from "../../Nodes/Tilemap";
export default class FactoryManager { export default class FactoryManager {
// Constructors are called here to allow assignment of their functions to functions in this class // Constructors are called here to allow assignment of their functions to functions in this class
private canvasNodeFactory: CanvasNodeFactory = new CanvasNodeFactory(); private canvasNodeFactory: CanvasNodeFactory = new CanvasNodeFactory();
private physicsNodeFactory: PhysicsNodeFactory = new PhysicsNodeFactory();
private tilemapFactory: TilemapFactory = new TilemapFactory(); private tilemapFactory: TilemapFactory = new TilemapFactory();
constructor(scene: Scene, physicsManager: PhysicsManager, tilemaps: Array<Tilemap>){ constructor(scene: Scene, physicsManager: PhysicsManager, tilemaps: Array<Tilemap>){
this.canvasNodeFactory.init(scene); this.canvasNodeFactory.init(scene);
this.physicsNodeFactory.init(scene, physicsManager);
this.tilemapFactory.init(scene, tilemaps, physicsManager); this.tilemapFactory.init(scene, tilemaps, physicsManager);
} }
@ -23,6 +19,5 @@ export default class FactoryManager {
uiElement = this.canvasNodeFactory.addUIElement; uiElement = this.canvasNodeFactory.addUIElement;
sprite = this.canvasNodeFactory.addSprite; sprite = this.canvasNodeFactory.addSprite;
graphic = this.canvasNodeFactory.addGraphic; graphic = this.canvasNodeFactory.addGraphic;
physics = this.physicsNodeFactory.add;
tilemap = this.tilemapFactory.add; tilemap = this.tilemapFactory.add;
} }

View File

@ -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 = <T extends PhysicsNode>(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;
}
}

View File

@ -8,7 +8,6 @@ import Tileset from "../../DataTypes/Tilesets/Tileset";
import Vec2 from "../../DataTypes/Vec2"; import Vec2 from "../../DataTypes/Vec2";
import { TiledCollectionTile } from "../../DataTypes/Tilesets/TiledData"; import { TiledCollectionTile } from "../../DataTypes/Tilesets/TiledData";
import Sprite from "../../Nodes/Sprites/Sprite"; import Sprite from "../../Nodes/Sprites/Sprite";
import StaticBody from "../../Physics/StaticBody";
export default class TilemapFactory { export default class TilemapFactory {
private scene: Scene; private scene: Scene;
@ -71,7 +70,7 @@ export default class TilemapFactory {
if(layer.type === "tilelayer"){ if(layer.type === "tilelayer"){
// Create a new tilemap object for the layer // Create a new tilemap object for the layer
let tilemap = new constr(tilemapData, layer, tilesets, scale); let tilemap = new constr(tilemapData, layer, tilesets, scale);
tilemap.setId(this.scene.generateId()); tilemap.id = this.scene.generateId();
tilemap.setScene(this.scene); tilemap.setScene(this.scene);
// Add tilemap to scene // Add tilemap to scene
@ -80,8 +79,8 @@ export default class TilemapFactory {
sceneLayer.addNode(tilemap); sceneLayer.addNode(tilemap);
// Register tilemap with physics if it's collidable // Register tilemap with physics if it's collidable
if(tilemap.isCollidable()){ if(tilemap.isCollidable){
this.physicsManager.addTilemap(tilemap); tilemap.addPhysics();
} }
} else { } else {
// Layer is an object layer, so add each object as a sprite to a new layer // 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); let offset = tileset.getImageOffsetForTile(obj.gid);
sprite = this.scene.add.sprite(imageKey, sceneLayer); sprite = this.scene.add.sprite(imageKey, sceneLayer);
let size = tileset.getTileSize().clone(); 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.setImageOffset(offset);
sprite.setSize(size); sprite.size.copy(size);
sprite.setScale(new Vec2(scale.x, scale.y)); sprite.scale.set(scale.x, scale.y);
} }
} }
@ -120,22 +119,16 @@ export default class TilemapFactory {
if(obj.gid === tile.id){ if(obj.gid === tile.id){
let imageKey = tile.image; let imageKey = tile.image;
sprite = this.scene.add.sprite(imageKey, sceneLayer); sprite = this.scene.add.sprite(imageKey, sceneLayer);
sprite.setPosition((obj.x + tile.imagewidth/2)*scale.x, (obj.y - tile.imageheight/2)*scale.y); sprite.position.set((obj.x + tile.imagewidth/2)*scale.x, (obj.y - tile.imageheight/2)*scale.y);
sprite.setScale(new Vec2(scale.x, scale.y)); sprite.scale.set(scale.x, scale.y);
} }
} }
} }
// Now we have sprite. Associate it with our physics object if there is one // Now we have sprite. Associate it with our physics object if there is one
if(collidable){ if(collidable){
let pos = sprite.getPosition().clone(); sprite.addPhysics();
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);
} }
} }
} }

View File

@ -1,9 +1,9 @@
import Stack from "../DataTypes/Stack";
import Layer from "./Layer"; import Layer from "./Layer";
import Viewport from "../SceneGraph/Viewport"; import Viewport from "../SceneGraph/Viewport";
import Vec2 from "../DataTypes/Vec2"; import Vec2 from "../DataTypes/Vec2";
import SceneGraph from "../SceneGraph/SceneGraph"; import SceneGraph from "../SceneGraph/SceneGraph";
import PhysicsManager from "../Physics/PhysicsManager"; import PhysicsManager from "../Physics/PhysicsManager";
import BasicPhysicsManager from "../Physics/BasicPhysicsManager";
import SceneGraphArray from "../SceneGraph/SceneGraphArray"; import SceneGraphArray from "../SceneGraph/SceneGraphArray";
import FactoryManager from "./Factories/FactoryManager"; import FactoryManager from "./Factories/FactoryManager";
import Tilemap from "../Nodes/Tilemap"; import Tilemap from "../Nodes/Tilemap";
@ -12,32 +12,41 @@ import GameLoop from "../Loop/GameLoop";
import SceneManager from "./SceneManager"; import SceneManager from "./SceneManager";
import Receiver from "../Events/Receiver"; import Receiver from "../Events/Receiver";
import Emitter from "../Events/Emitter"; 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; protected worldSize: Vec2;
/** The viewport. */
protected viewport: Viewport; protected viewport: Viewport;
/** A flag that represents whether this scene is running or not. */
protected running: boolean; protected running: boolean;
/** The overall game loop. */
protected game: GameLoop; protected game: GameLoop;
/** The manager of this scene. */
protected sceneManager: SceneManager; protected sceneManager: SceneManager;
/** The receiver for this scene. */
protected receiver: Receiver; protected receiver: Receiver;
/** The emitter for this scene. */
protected emitter: Emitter; protected emitter: Emitter;
/** This list of tilemaps in this scene. */
protected tilemaps: Array<Tilemap>; protected tilemaps: Array<Tilemap>;
/** /** The scene graph of the Scene*/
* The scene graph of the Scene - can be exchanged with other SceneGraphs for more variation
*/
protected sceneGraph: SceneGraph; protected sceneGraph: SceneGraph;
protected physicsManager: PhysicsManager; 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; 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; public load: ResourceManager;
constructor(viewport: Viewport, sceneManager: SceneManager, game: GameLoop){ constructor(viewport: Viewport, sceneManager: SceneManager, game: GameLoop){
@ -52,40 +61,28 @@ export default class Scene{
this.tilemaps = new Array(); this.tilemaps = new Array();
this.sceneGraph = new SceneGraphArray(this.viewport, this); 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.add = new FactoryManager(this, this.physicsManager, this.tilemaps);
this.load = ResourceManager.getInstance(); this.load = ResourceManager.getInstance();
} }
/** /** A lifecycle method that gets called when a new scene is created. Load all files you wish to access in the scene here. */
* A function that gets called when a new scene is created. Load all files you wish to access in the scene here.
*/
loadScene(): void {} loadScene(): void {}
/** /** A lifecycle method that gets called on scene destruction. Specify which files you no longer need for garbage collection. */
* A function that gets called on scene destruction. Specify which files you no longer need for garbage collection.
*/
unloadScene(): void {} unloadScene(): void {}
/** /** A lifecycle method called strictly after loadScene(). Create any game objects you wish to use in the scene here. */
* Called strictly after loadScene() is called. Create any game objects you wish to use in the scene here.
*/
startScene(): void {} 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 * @param delta
*/ */
updateScene(deltaT: number): void {} updateScene(deltaT: number): void {}
/**
* Updates all scene elements
* @param deltaT
*/
update(deltaT: number): void { update(deltaT: number): void {
this.updateScene(deltaT); this.updateScene(deltaT);
@ -106,10 +103,6 @@ export default class Scene{
this.viewport.update(deltaT); this.viewport.update(deltaT);
} }
/**
* Render all CanvasNodes and Tilemaps in the Scene
* @param ctx
*/
render(ctx: CanvasRenderingContext2D): void { render(ctx: CanvasRenderingContext2D): void {
// For webGL, pass a visible set to the renderer // For webGL, pass a visible set to the renderer
// We need to keep track of the order of things. // We need to keep track of the order of things.
@ -127,7 +120,7 @@ export default class Scene{
visibleSet.forEach(node => node.render(ctx)); visibleSet.forEach(node => node.render(ctx));
// Debug render the physicsManager // Debug render the physicsManager
this.physicsManager.render(ctx); this.physicsManager.debug_render(ctx);
} }
setRunning(running: boolean): void { setRunning(running: boolean): void {
@ -145,9 +138,7 @@ export default class Scene{
return this.sceneGraph.addLayer(); return this.sceneGraph.addLayer();
} }
/** /** Returns the viewport associated with this scene */
* Returns the viewport associated with this scene
*/
getViewport(): Viewport { getViewport(): Viewport {
return this.viewport; return this.viewport;
} }
@ -160,6 +151,10 @@ export default class Scene{
return this.sceneGraph; return this.sceneGraph;
} }
getPhysicsManager(): PhysicsManager {
return this.physicsManager;
}
generateId(): number { generateId(): number {
return this.sceneManager.generateId(); return this.sceneManager.generateId();
} }

View File

@ -5,7 +5,7 @@ import Vec2 from "../DataTypes/Vec2";
import Scene from "../Scene/Scene"; import Scene from "../Scene/Scene";
import Layer from "../Scene/Layer"; import Layer from "../Scene/Layer";
import Stack from "../DataTypes/Stack"; 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. * An abstract interface of a SceneGraph. Exposes methods for use by other code, but leaves the implementation up to the subclasses.

View File

@ -4,7 +4,7 @@ import Viewport from "./Viewport";
import Scene from "../Scene/Scene"; import Scene from "../Scene/Scene";
import Stack from "../DataTypes/Stack"; import Stack from "../DataTypes/Stack";
import Layer from "../Scene/Layer" import Layer from "../Scene/Layer"
import AABB from "../DataTypes/AABB"; import AABB from "../DataTypes/Shapes/AABB";
import Stats from "../Debug/Stats"; import Stats from "../Debug/Stats";
export default class SceneGraphArray extends SceneGraph{ export default class SceneGraphArray extends SceneGraph{
@ -50,7 +50,7 @@ export default class SceneGraphArray extends SceneGraph{
let results = []; let results = [];
for(let node of this.nodeList){ for(let node of this.nodeList){
if(boundary.overlaps(node.getBoundary())){ if(boundary.overlaps(node.boundary)){
results.push(node); results.push(node);
} }
} }
@ -94,8 +94,7 @@ export default class SceneGraphArray extends SceneGraph{
// Sort by depth, then by visible set by y-value // Sort by depth, then by visible set by y-value
visibleSet.sort((a, b) => { visibleSet.sort((a, b) => {
if(a.getLayer().getDepth() === b.getLayer().getDepth()){ if(a.getLayer().getDepth() === b.getLayer().getDepth()){
return (a.getPosition().y + a.getSize().y*a.getScale().y) return (a.boundary.bottom) - (b.boundary.bottom);
- (b.getPosition().y + b.getSize().y*b.getScale().y);
} else { } else {
return a.getLayer().getDepth() - b.getLayer().getDepth(); return a.getLayer().getDepth() - b.getLayer().getDepth();
} }

View File

@ -4,7 +4,7 @@ import Viewport from "./Viewport";
import Scene from "../Scene/Scene"; import Scene from "../Scene/Scene";
import RegionQuadTree from "../DataTypes/RegionQuadTree"; import RegionQuadTree from "../DataTypes/RegionQuadTree";
import Vec2 from "../DataTypes/Vec2"; import Vec2 from "../DataTypes/Vec2";
import AABB from "../DataTypes/AABB"; import AABB from "../DataTypes/Shapes/AABB";
import Stats from "../Debug/Stats"; import Stats from "../Debug/Stats";
export default class SceneGraphQuadTree extends SceneGraph { 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 // Sort by depth, then by visible set by y-value
visibleSet.sort((a, b) => { visibleSet.sort((a, b) => {
if(a.getLayer().getDepth() === b.getLayer().getDepth()){ if(a.getLayer().getDepth() === b.getLayer().getDepth()){
return (a.getPosition().y + a.getSize().y*a.getScale().y) return (a.boundary.bottom) - (b.boundary.bottom);
- (b.getPosition().y + b.getSize().y*b.getScale().y);
} else { } else {
return a.getLayer().getDepth() - b.getLayer().getDepth(); return a.getLayer().getDepth() - b.getLayer().getDepth();
} }

View File

@ -3,7 +3,7 @@ import GameNode from "../Nodes/GameNode";
import CanvasNode from "../Nodes/CanvasNode"; import CanvasNode from "../Nodes/CanvasNode";
import MathUtils from "../Utils/MathUtils"; import MathUtils from "../Utils/MathUtils";
import Queue from "../DataTypes/Queue"; import Queue from "../DataTypes/Queue";
import AABB from "../DataTypes/AABB"; import AABB from "../DataTypes/Shapes/AABB";
import Debug from "../Debug/Debug"; import Debug from "../Debug/Debug";
import InputReceiver from "../Input/InputReceiver"; import InputReceiver from "../Input/InputReceiver";
@ -43,11 +43,11 @@ export default class Viewport {
* Returns the position of the viewport as a Vec2 * Returns the position of the viewport as a Vec2
*/ */
getCenter(): Vec2 { getCenter(): Vec2 {
return this.view.getCenter(); return this.view.center;
} }
getOrigin(): Vec2 { 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 { includes(node: CanvasNode): boolean {
let parallax = node.getLayer().getParallax(); let parallax = node.getLayer().getParallax();
let center = this.view.getCenter().clone(); let center = this.view.center.clone();
this.view.getCenter().mult(parallax); this.view.center.mult(parallax);
let overlaps = this.view.overlaps(node.getBoundary()); let overlaps = this.view.overlaps(node.boundary);
this.view.setCenter(center); this.view.center = center
return overlaps; return overlaps;
} }
@ -155,8 +155,8 @@ export default class Viewport {
let hheight = (upperY - lowerY)/2; let hheight = (upperY - lowerY)/2;
let x = lowerX + hwidth; let x = lowerX + hwidth;
let y = lowerY + hheight; let y = lowerY + hheight;
this.boundary.setCenter(new Vec2(x, y)); this.boundary.center.set(x, y);
this.boundary.setHalfSize(new Vec2(hwidth, hheight)); this.boundary.halfSize.set(hwidth, hheight);
} }
/** /**
@ -202,7 +202,7 @@ export default class Viewport {
// If viewport is following an object // If viewport is following an object
if(this.following){ if(this.following){
// Update our list of previous positions // 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){ if(this.lastPositions.getSize() > this.smoothingFactor){
this.lastPositions.dequeue(); this.lastPositions.dequeue();
} }
@ -222,7 +222,7 @@ export default class Viewport {
Debug.log("vp", "Viewport pos: " + pos.toString()) Debug.log("vp", "Viewport pos: " + pos.toString())
this.view.setCenter(pos); this.view.center.copy(pos);
} else { } else {
if(this.lastPositions.getSize() > this.smoothingFactor){ if(this.lastPositions.getSize() > this.smoothingFactor){
this.lastPositions.dequeue(); this.lastPositions.dequeue();
@ -241,7 +241,7 @@ export default class Viewport {
pos.y = Math.floor(pos.y); pos.y = Math.floor(pos.y);
Debug.log("vp", "Viewport pos: " + pos.toString()) Debug.log("vp", "Viewport pos: " + pos.toString())
this.view.setCenter(pos); this.view.center.copy(pos);
} }
} }
} }

View File

@ -1,6 +1,4 @@
import Scene from "./Scene/Scene"; import Scene from "./Scene/Scene";
import OrthogonalTilemap from "./Nodes/Tilemaps/OrthogonalTilemap";
import Player from "./Player";
import Rect from "./Nodes/Graphics/Rect"; import Rect from "./Nodes/Graphics/Rect";
import Color from "./Utils/Color"; import Color from "./Utils/Color";
import Vec2 from "./DataTypes/Vec2"; import Vec2 from "./DataTypes/Vec2";
@ -23,7 +21,7 @@ export default class SecondScene extends Scene {
bar.setColor(new Color(255, 100, 0)); bar.setColor(new Color(255, 100, 0));
this.load.onLoadProgress = (percentProgress: number) => { this.load.onLoadProgress = (percentProgress: number) => {
bar.setSize(295 * percentProgress, bar.getSize().y); //bar.setSize(295 * percentProgress, bar.getSize().y);
} }
this.load.onLoadComplete = () => { this.load.onLoadComplete = () => {
@ -32,76 +30,76 @@ export default class SecondScene extends Scene {
} }
startScene(){ startScene(){
// Add the tilemap // // Add the tilemap
let mainLayer = this.add.tilemap("level2")[1]; // let mainLayer = this.add.tilemap("level2")[1];
mainLayer.setYSort(true); // mainLayer.setYSort(true);
// Add a player // // Add a player
let player = this.add.physics(Player, mainLayer, "topdown"); // let player = this.add.physics(Player, mainLayer, "topdown");
let playerSprite = this.add.sprite("player", mainLayer); // let playerSprite = this.add.sprite("player", mainLayer);
player.setSprite(playerSprite); // player.setSprite(playerSprite);
this.viewport.follow(player); // this.viewport.follow(player);
// Initialize UI // // Initialize UI
let uiLayer = this.addLayer(); // let uiLayer = this.addLayer();
uiLayer.setParallax(0, 0); // uiLayer.setParallax(0, 0);
let recordButton = this.add.uiElement(Button, uiLayer); // let recordButton = this.add.uiElement(Button, uiLayer);
recordButton.setSize(100, 50); // recordButton.setSize(100, 50);
recordButton.setText("Record"); // recordButton.setText("Record");
recordButton.setPosition(400, 30); // recordButton.setPosition(400, 30);
recordButton.onClickEventId = GameEventType.START_RECORDING; // recordButton.onClickEventId = GameEventType.START_RECORDING;
let stopButton = this.add.uiElement(Button, uiLayer); // let stopButton = this.add.uiElement(Button, uiLayer);
stopButton.setSize(100, 50); // stopButton.setSize(100, 50);
stopButton.setText("Stop"); // stopButton.setText("Stop");
stopButton.setPosition(550, 30); // stopButton.setPosition(550, 30);
stopButton.onClickEventId = GameEventType.STOP_RECORDING; // stopButton.onClickEventId = GameEventType.STOP_RECORDING;
let playButton = this.add.uiElement(Button, uiLayer); // let playButton = this.add.uiElement(Button, uiLayer);
playButton.setSize(100, 50); // playButton.setSize(100, 50);
playButton.setText("Play"); // playButton.setText("Play");
playButton.setPosition(700, 30); // playButton.setPosition(700, 30);
playButton.onClickEventId = GameEventType.PLAY_RECORDING; // playButton.onClickEventId = GameEventType.PLAY_RECORDING;
let cycleFramerateButton = this.add.uiElement(Button, uiLayer); // let cycleFramerateButton = this.add.uiElement(Button, uiLayer);
cycleFramerateButton.setSize(150, 50); // cycleFramerateButton.setSize(150, 50);
cycleFramerateButton.setText("Cycle FPS"); // cycleFramerateButton.setText("Cycle FPS");
cycleFramerateButton.setPosition(5, 400); // cycleFramerateButton.setPosition(5, 400);
let i = 0; // let i = 0;
let fps = [15, 30, 60]; // let fps = [15, 30, 60];
cycleFramerateButton.onClick = () => { // cycleFramerateButton.onClick = () => {
this.game.setMaxUpdateFPS(fps[i]); // this.game.setMaxUpdateFPS(fps[i]);
i = (i + 1) % 3; // i = (i + 1) % 3;
} // }
// Pause Menu // // Pause Menu
let pauseLayer = this.addLayer(); // let pauseLayer = this.addLayer();
pauseLayer.setParallax(0, 0); // pauseLayer.setParallax(0, 0);
pauseLayer.disable(); // pauseLayer.disable();
let pauseButton = this.add.uiElement(Button, uiLayer); // let pauseButton = this.add.uiElement(Button, uiLayer);
pauseButton.setSize(100, 50); // pauseButton.setSize(100, 50);
pauseButton.setText("Pause"); // pauseButton.setText("Pause");
pauseButton.setPosition(700, 400); // pauseButton.setPosition(700, 400);
pauseButton.onClick = () => { // pauseButton.onClick = () => {
this.sceneGraph.getLayers().forEach((layer: Layer) => layer.setPaused(true)); // this.sceneGraph.getLayers().forEach((layer: Layer) => layer.setPaused(true));
pauseLayer.enable(); // pauseLayer.enable();
} // }
let modalBackground = this.add.uiElement(UIElement, pauseLayer); // let modalBackground = this.add.uiElement(UIElement, pauseLayer);
modalBackground.setSize(400, 200); // modalBackground.setSize(400, 200);
modalBackground.setBackgroundColor(new Color(0, 0, 0, 0.4)); // modalBackground.setBackgroundColor(new Color(0, 0, 0, 0.4));
modalBackground.setPosition(200, 100); // modalBackground.setPosition(200, 100);
let resumeButton = this.add.uiElement(Button, pauseLayer); // let resumeButton = this.add.uiElement(Button, pauseLayer);
resumeButton.setSize(100, 50); // resumeButton.setSize(100, 50);
resumeButton.setText("Resume"); // resumeButton.setText("Resume");
resumeButton.setPosition(400, 200); // resumeButton.setPosition(400, 200);
resumeButton.onClick = () => { // resumeButton.onClick = () => {
this.sceneGraph.getLayers().forEach((layer: Layer) => layer.setPaused(false)); // this.sceneGraph.getLayers().forEach((layer: Layer) => layer.setPaused(false));
pauseLayer.disable(); // pauseLayer.disable();
} // }
} }
} }

View File

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

24
src/Utils/SortingUtils.ts Normal file
View File

@ -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<T>(arr: Array<T>, 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<T>(arr: Array<T>, i: number, j: number){
let temp = arr[i];
arr[i] = arr[j];
arr[j] = temp;
}
}

View File

@ -69,7 +69,7 @@ export default class BoidBehavior extends State {
speed = MathUtils.clamp(speed, BoidBehavior.MIN_SPEED, BoidBehavior.MAX_SPEED); speed = MathUtils.clamp(speed, BoidBehavior.MIN_SPEED, BoidBehavior.MAX_SPEED);
this.actor.velocity.scale(speed); this.actor.velocity.scale(speed);
if(this.actor.getId() < 1){ if(this.actor.id < 1){
Debug.log("BoidSep", "Separation: " + separationForce.toString()); Debug.log("BoidSep", "Separation: " + separationForce.toString());
Debug.log("BoidAl", "Alignment: " + alignmentForce.toString()); Debug.log("BoidAl", "Alignment: " + alignmentForce.toString());
Debug.log("BoidCo", "Cohesion: " + cohesionForce.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()); Debug.log("BoidDir", "Velocity: " + this.actor.velocity.toString());
} }

View File

@ -1,4 +1,4 @@
import AABB from "../../DataTypes/AABB"; import AABB from "../../DataTypes/Shapes/AABB";
import Vec2 from "../../DataTypes/Vec2"; import Vec2 from "../../DataTypes/Vec2";
import Point from "../../Nodes/Graphics/Point"; import Point from "../../Nodes/Graphics/Point";
import Scene from "../../Scene/Scene"; import Scene from "../../Scene/Scene";
@ -21,14 +21,14 @@ export default class FlockBehavior {
this.actor = actor; this.actor = actor;
this.flock = flock; 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; this.avoidRadius = avoidRadius;
} }
update(): void { update(): void {
// Update the visible region // 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); let neighbors = this.scene.getSceneGraph().getNodesInRegion(this.visibleRegion);
@ -46,7 +46,7 @@ export default class FlockBehavior {
} }
// Draw a group // Draw a group
if(this.actor.getId() < 1){ if(this.actor.id < 1){
this.actor.setColor(Color.GREEN); this.actor.setColor(Color.GREEN);
for(let neighbor of neighbors){ for(let neighbor of neighbors){
if(neighbor === this.actor) continue; if(neighbor === this.actor) continue;

View File

@ -1,10 +1,11 @@
import StateMachine from "../../DataTypes/State/StateMachine"; import StateMachine from "../../DataTypes/State/StateMachine";
import { CustomGameEventType } from "../CustomGameEventType"; import { CustomGameEventType } from "../CustomGameEventType";
import Goomba from "../MarioClone/Goomba";
import Idle from "../Enemies/Idle"; import Idle from "../Enemies/Idle";
import Jump from "../Enemies/Jump"; import Jump from "../Enemies/Jump";
import Walk from "../Enemies/Walk"; import Walk from "../Enemies/Walk";
import Debug from "../../Debug/Debug"; import Debug from "../../Debug/Debug";
import GameNode from "../../Nodes/GameNode";
import Vec2 from "../../DataTypes/Vec2";
export enum GoombaStates { export enum GoombaStates {
IDLE = "idle", IDLE = "idle",
@ -14,10 +15,13 @@ export enum GoombaStates {
} }
export default class GoombaController extends StateMachine { export default class GoombaController extends StateMachine {
owner: Goomba; owner: GameNode;
jumpy: boolean; jumpy: boolean;
direction: Vec2 = Vec2.ZERO;
velocity: Vec2 = Vec2.ZERO;
speed: number = 200;
constructor(owner: Goomba, jumpy: boolean){ constructor(owner: GameNode, jumpy: boolean){
super(); super();
this.owner = owner; this.owner = owner;

View File

@ -1,19 +1,21 @@
import State from "../../DataTypes/State/State"; import State from "../../DataTypes/State/State";
import StateMachine from "../../DataTypes/State/StateMachine"; 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 { export default abstract class GoombaState extends State {
owner: Goomba; owner: GameNode;
gravity: number = 7000; gravity: number = 7000;
parent: GoombaController
constructor(parent: StateMachine, owner: Goomba){ constructor(parent: StateMachine, owner: GameNode){
super(parent); super(parent);
this.owner = owner; this.owner = owner;
} }
update(deltaT: number): void { update(deltaT: number): void {
// Do gravity; // Do gravity
this.owner.velocity.y += this.gravity*deltaT; this.parent.velocity.y += this.gravity*deltaT;
} }
} }

View File

@ -6,7 +6,7 @@ import OnGround from "./OnGround";
export default class Idle extends OnGround { export default class Idle extends OnGround {
onEnter(): void { onEnter(): void {
this.owner.speed = this.owner.speed; this.parent.speed = this.parent.speed;
} }
handleInput(event: GameEvent) { handleInput(event: GameEvent) {
@ -22,8 +22,8 @@ export default class Idle extends OnGround {
update(deltaT: number): void { update(deltaT: number): void {
super.update(deltaT); 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));
} }
} }

View File

@ -11,17 +11,17 @@ export default class Jump extends GoombaState {
update(deltaT: number): void { update(deltaT: number): void {
super.update(deltaT); super.update(deltaT);
if(this.owner.isGrounded()){ if(this.owner.onGround){
this.finished(GoombaStates.PREVIOUS); this.finished(GoombaStates.PREVIOUS);
} }
if(this.owner.isOnCeiling()){ if(this.owner.onCeiling){
this.owner.velocity.y = 0; 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 {} onExit(): void {}

View File

@ -7,19 +7,19 @@ export default class OnGround extends GoombaState {
onEnter(): void {} onEnter(): void {}
handleInput(event: GameEvent): void { handleInput(event: GameEvent): void {
if(event.type === CustomGameEventType.PLAYER_JUMP && (<GoombaController>this.parentStateMachine).jumpy){ if(event.type === CustomGameEventType.PLAYER_JUMP && (<GoombaController>this.parent).jumpy){
this.finished(GoombaStates.JUMP); this.finished(GoombaStates.JUMP);
this.owner.velocity.y = -2000; this.parent.velocity.y = -2000;
} }
} }
update(deltaT: number): void { update(deltaT: number): void {
if(this.owner.velocity.y > 0){ if(this.parent.velocity.y > 0){
this.owner.velocity.y = 0; this.parent.velocity.y = 0;
} }
super.update(deltaT); super.update(deltaT);
if(!this.owner.isGrounded()){ if(!this.owner.onGround){
this.finished(GoombaStates.JUMP); this.finished(GoombaStates.JUMP);
} }
} }

View File

@ -3,21 +3,21 @@ import OnGround from "./OnGround";
export default class Walk extends OnGround { export default class Walk extends OnGround {
onEnter(): void { onEnter(): void {
if(this.owner.direction.isZero()){ if(this.parent.direction.isZero()){
this.owner.direction = new Vec2(-1, 0); this.parent.direction = new Vec2(-1, 0);
} }
} }
update(deltaT: number): void { update(deltaT: number): void {
super.update(deltaT); super.update(deltaT);
if(this.owner.isOnWall()){ if(this.owner.onWall){
// Flip around // 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));
} }
} }

View File

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

View File

@ -1,9 +1,10 @@
import Scene from "../../Scene/Scene"; import Scene from "../../Scene/Scene";
import Rect from "../../Nodes/Graphics/Rect"; import Rect from "../../Nodes/Graphics/Rect";
import Vec2 from "../../DataTypes/Vec2"; import Vec2 from "../../DataTypes/Vec2";
import Player from "./Player";
import Color from "../../Utils/Color"; 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 { export default class MarioClone extends Scene {
@ -16,19 +17,25 @@ export default class MarioClone extends Scene {
let layer = this.addLayer(); let layer = this.addLayer();
this.add.tilemap("level", new Vec2(2, 2)); this.add.tilemap("level", new Vec2(2, 2));
let player = this.add.physics(Player, layer, new Vec2(0, 0)); let player = this.add.graphic(Rect, layer, new Vec2(0, 0), new Vec2(64, 64));
let playerSprite = this.add.graphic(Rect, layer, new Vec2(0, 0), new Vec2(64, 64)); player.setColor(Color.BLUE);
playerSprite.setColor(Color.BLUE); player.addPhysics();
player.addChild(playerSprite);
this.viewport.follow(playerSprite); this.viewport.follow(player);
this.viewport.setBounds(0, 0, 5120, 1280); 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]){ 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 goomba = this.add.sprite("goomba", layer);
let goombaSprite = this.add.sprite("goomba", layer); let ai = new GoombaController(goomba, false);
goombaSprite.setPosition(64*xPos, 0); ai.initialize("idle");
goombaSprite.setScale(new Vec2(2, 2)); goomba.update = (deltaT: number) => {ai.update(deltaT)};
goomba.addChild(goombaSprite); goomba.position.set(64*xPos, 0);
goomba.scale.set(2, 2);
goomba.addPhysics();
} }
} }

View File

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

View File

@ -39,7 +39,7 @@ export default class MoveTopDown extends State {
// Otherwise, we are still moving, so update position // Otherwise, we are still moving, so update position
let velocity = this.direction.normalize().scale(this.speed); 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 // Emit an event to tell the world we are moving
this.emitter.fireEvent(CustomGameEventType.PLAYER_MOVE, {position: this.owner.position.clone()}); this.emitter.fireEvent(CustomGameEventType.PLAYER_MOVE, {position: this.owner.position.clone()});

View File

@ -4,7 +4,7 @@ import PlayerState from "./PlayerState";
export default class Idle extends OnGround { export default class Idle extends OnGround {
onEnter(): void { onEnter(): void {
this.owner.speed = this.owner.MIN_SPEED; this.parent.speed = this.parent.MIN_SPEED;
} }
update(deltaT: number): void { 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));
} }
} }

View File

@ -14,20 +14,20 @@ export default class Jump extends PlayerState {
update(deltaT: number): void { update(deltaT: number): void {
super.update(deltaT); super.update(deltaT);
if(this.owner.isGrounded()){ if(this.owner.onGround){
this.finished(PlayerStates.PREVIOUS); this.finished(PlayerStates.PREVIOUS);
} }
if(this.owner.isOnCeiling()){ if(this.owner.onCeiling){
this.owner.velocity.y = 0; this.parent.velocity.y = 0;
} }
let dir = this.getInputDirection(); 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.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 {} onExit(): void {}

View File

@ -8,16 +8,16 @@ export default class OnGround extends PlayerState {
handleInput(event: GameEvent): void {} handleInput(event: GameEvent): void {}
update(deltaT: number): void { update(deltaT: number): void {
if(this.owner.velocity.y > 0){ if(this.parent.velocity.y > 0){
this.owner.velocity.y = 0; this.parent.velocity.y = 0;
} }
super.update(deltaT); super.update(deltaT);
if(this.input.isJustPressed("w") || this.input.isJustPressed("space")){ if(this.input.isJustPressed("w") || this.input.isJustPressed("space")){
this.finished("jump"); this.finished("jump");
this.owner.velocity.y = -2000; this.parent.velocity.y = -2000;
this.emitter.fireEvent(CustomGameEventType.PLAYER_JUMP) this.emitter.fireEvent(CustomGameEventType.PLAYER_JUMP)
} else if(!this.owner.isGrounded()){ } else if(!this.owner.onGround){
this.finished("jump"); this.finished("jump");
} }
} }

View File

@ -1,10 +1,11 @@
import StateMachine from "../../../../DataTypes/State/StateMachine"; import StateMachine from "../../../../DataTypes/State/StateMachine";
import Debug from "../../../../Debug/Debug"; import Debug from "../../../../Debug/Debug";
import Player from "../../../MarioClone/Player";
import Idle from "./Idle"; import Idle from "./Idle";
import Jump from "./Jump"; import Jump from "./Jump";
import Walk from "./Walk"; import Walk from "./Walk";
import Run from "./Run"; import Run from "./Run";
import GameNode from "../../../../Nodes/GameNode";
import Vec2 from "../../../../DataTypes/Vec2";
export enum PlayerStates { export enum PlayerStates {
WALK = "walk", WALK = "walk",
@ -15,9 +16,13 @@ export enum PlayerStates {
} }
export default class PlayerController extends StateMachine { 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(); super();
this.owner = owner; this.owner = owner;

View File

@ -2,15 +2,17 @@ import State from "../../../../DataTypes/State/State";
import StateMachine from "../../../../DataTypes/State/StateMachine"; import StateMachine from "../../../../DataTypes/State/StateMachine";
import Vec2 from "../../../../DataTypes/Vec2"; import Vec2 from "../../../../DataTypes/Vec2";
import InputReceiver from "../../../../Input/InputReceiver"; import InputReceiver from "../../../../Input/InputReceiver";
import CanvasNode from "../../../../Nodes/CanvasNode"; import GameNode from "../../../../Nodes/GameNode";
import Player from "../../../MarioClone/Player"; import PlayerController from "./PlayerController";
export default abstract class PlayerState extends State { export default abstract class PlayerState extends State {
input: InputReceiver = InputReceiver.getInstance(); input: InputReceiver = InputReceiver.getInstance();
owner: Player; owner: GameNode;
gravity: number = 7000; gravity: number = 7000;
parent: PlayerController;
constructor(parent: StateMachine, owner: Player){ constructor(parent: StateMachine, owner: GameNode){
super(parent); super(parent);
this.owner = owner; this.owner = owner;
} }
@ -27,7 +29,7 @@ export default abstract class PlayerState extends State {
} }
update(deltaT: number): void { update(deltaT: number): void {
// Do gravity; // Do gravity
this.owner.velocity.y += this.gravity*deltaT; this.parent.velocity.y += this.gravity*deltaT;
} }
} }

View File

@ -4,7 +4,7 @@ import { PlayerStates } from "./PlayerController";
export default class Run extends OnGround { export default class Run extends OnGround {
onEnter(): void { onEnter(): void {
this.owner.speed = this.owner.MAX_SPEED; this.parent.speed = this.parent.MAX_SPEED;
} }
update(deltaT: number): void { 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.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));
} }
} }

View File

@ -4,7 +4,7 @@ import { PlayerStates } from "./PlayerController";
export default class Walk extends OnGround { export default class Walk extends OnGround {
onEnter(): void { onEnter(): void {
this.owner.speed = this.owner.MAX_SPEED/2; this.parent.speed = this.parent.MAX_SPEED/2;
} }
update(deltaT: number): void { 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.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));
} }
} }

View File

@ -10,7 +10,7 @@ function main(){
let game = new GameLoop({canvasSize: {x: 800, y: 600}}); let game = new GameLoop({canvasSize: {x: 800, y: 600}});
game.start(); game.start();
let sm = game.getSceneManager(); 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 { CanvasRenderingContext2D.prototype.roundedRect = function(x: number, y: number, w: number, h: number, r: number): void {

View File

@ -39,7 +39,7 @@
"src/Physics/Colliders/AABB.ts", "src/Physics/Colliders/AABB.ts",
"src/Physics/Colliders/Collider.ts", "src/Physics/Colliders/Collider.ts",
"src/Physics/PhysicsManager.ts", "src/Physics/PhysicsManager_Old.ts",
"src/Physics/PhysicsNode.ts", "src/Physics/PhysicsNode.ts",
"src/Playback/Recorder.ts", "src/Playback/Recorder.ts",