transferred game engine code to typescript
This commit is contained in:
parent
e5d0678b5d
commit
40a05fdbdb
3
src/DataTypes/Collection.ts
Normal file
3
src/DataTypes/Collection.ts
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
export default interface Collection{
|
||||||
|
forEach(func: Function): void;
|
||||||
|
}
|
33
src/DataTypes/Map.ts
Normal file
33
src/DataTypes/Map.ts
Normal file
|
@ -0,0 +1,33 @@
|
||||||
|
import Collection from "./Collection";
|
||||||
|
|
||||||
|
export default class Map<T> implements Collection{
|
||||||
|
private map: Record<string, T>;
|
||||||
|
|
||||||
|
constructor(){
|
||||||
|
this.map = {};
|
||||||
|
}
|
||||||
|
|
||||||
|
add(key: string, value: T): void {
|
||||||
|
this.map[key] = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
get(key: string): T {
|
||||||
|
return this.map[key];
|
||||||
|
}
|
||||||
|
|
||||||
|
set(key: string, value: T): void {
|
||||||
|
this.add(key, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
has(key: string): boolean {
|
||||||
|
return this.map[key] !== undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
keys(): Array<string> {
|
||||||
|
return Object.keys(this.map);
|
||||||
|
}
|
||||||
|
|
||||||
|
forEach(func: Function): void {
|
||||||
|
Object.keys(this.map).forEach(key => func(key));
|
||||||
|
}
|
||||||
|
}
|
61
src/DataTypes/Queue.ts
Normal file
61
src/DataTypes/Queue.ts
Normal file
|
@ -0,0 +1,61 @@
|
||||||
|
import Collection from "./Collection";
|
||||||
|
|
||||||
|
export default class Queue<T> implements Collection{
|
||||||
|
readonly MAX_ELEMENTS: number;
|
||||||
|
private q: Array<T>;
|
||||||
|
private head: number;
|
||||||
|
private tail: number;
|
||||||
|
|
||||||
|
constructor(maxElements: number = 100){
|
||||||
|
this.MAX_ELEMENTS = maxElements;
|
||||||
|
this.q = new Array(this.MAX_ELEMENTS);
|
||||||
|
this.head = 0;
|
||||||
|
this.tail = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
enqueue(item: T): void{
|
||||||
|
if((this.tail + 1) % this.MAX_ELEMENTS === this.head){
|
||||||
|
throw "Queue full - cannot add element"
|
||||||
|
}
|
||||||
|
|
||||||
|
this.q[this.tail] = item;
|
||||||
|
this.tail = (this.tail + 1) % this.MAX_ELEMENTS;
|
||||||
|
}
|
||||||
|
|
||||||
|
dequeue(): T {
|
||||||
|
if(this.head === this.tail){
|
||||||
|
throw "Queue empty - cannot remove element"
|
||||||
|
}
|
||||||
|
|
||||||
|
let item = this.q[this.head];
|
||||||
|
this.head = (this.head + 1) % this.MAX_ELEMENTS;
|
||||||
|
|
||||||
|
return item;
|
||||||
|
}
|
||||||
|
|
||||||
|
peekNext(): T {
|
||||||
|
if(this.head === this.tail){
|
||||||
|
throw "Queue empty - cannot get element"
|
||||||
|
}
|
||||||
|
|
||||||
|
let item = this.q[this.head];
|
||||||
|
|
||||||
|
return item;
|
||||||
|
}
|
||||||
|
|
||||||
|
hasItems(): boolean {
|
||||||
|
return this.head !== this.tail;
|
||||||
|
}
|
||||||
|
|
||||||
|
clear(): void {
|
||||||
|
this.head = this.tail;
|
||||||
|
}
|
||||||
|
|
||||||
|
forEach(func: Function): void {
|
||||||
|
let i = this.head;
|
||||||
|
while(i !== this.tail){
|
||||||
|
func(this.q[i]);
|
||||||
|
i = (i + 1) % this.MAX_ELEMENTS;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
68
src/DataTypes/Stack.ts
Normal file
68
src/DataTypes/Stack.ts
Normal file
|
@ -0,0 +1,68 @@
|
||||||
|
import Collection from "./Collection";
|
||||||
|
|
||||||
|
export default class Stack<T> implements Collection{
|
||||||
|
readonly MAX_ELEMENTS: number;
|
||||||
|
private stack: Array<T>;
|
||||||
|
private head: number;
|
||||||
|
|
||||||
|
constructor(maxElements: number = 100){
|
||||||
|
this.MAX_ELEMENTS = maxElements;
|
||||||
|
this.stack = new Array<T>(this.MAX_ELEMENTS);
|
||||||
|
this.head = -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adds an item to the top of the stack
|
||||||
|
* @param {*} item The new item to add to the stack
|
||||||
|
*/
|
||||||
|
push(item: T): void {
|
||||||
|
if(this.head + 1 === this.MAX_ELEMENTS){
|
||||||
|
throw "Stack full - cannot add element";
|
||||||
|
}
|
||||||
|
this.head += 1;
|
||||||
|
this.stack[this.head] = item;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Removes an item from the top of the stack
|
||||||
|
*/
|
||||||
|
pop(): T{
|
||||||
|
if(this.head === -1){
|
||||||
|
throw "Stack empty - cannot remove element";
|
||||||
|
}
|
||||||
|
this.head -= 1;
|
||||||
|
return this.stack[this.head + 1];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Removes all elements from the stack
|
||||||
|
*/
|
||||||
|
clear(): void{
|
||||||
|
this.head = -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the element currently at the top of the stack
|
||||||
|
*/
|
||||||
|
peek(): T {
|
||||||
|
if(this.head === -1){
|
||||||
|
throw "Stack empty - cannot get element";
|
||||||
|
}
|
||||||
|
return this.stack[this.head];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the number of items currently in the stack
|
||||||
|
*/
|
||||||
|
size(): number {
|
||||||
|
return this.head + 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
forEach(func: Function): void{
|
||||||
|
let i = 0;
|
||||||
|
while(i <= this.head){
|
||||||
|
func(this.stack[i]);
|
||||||
|
i += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
79
src/DataTypes/Vec2.ts
Normal file
79
src/DataTypes/Vec2.ts
Normal file
|
@ -0,0 +1,79 @@
|
||||||
|
export default class Vec2{
|
||||||
|
|
||||||
|
public x : number;
|
||||||
|
public y : number;
|
||||||
|
|
||||||
|
constructor(x : number = 0, y : number = 0){
|
||||||
|
this.x = x;
|
||||||
|
this.y = y;
|
||||||
|
}
|
||||||
|
|
||||||
|
magSq() : number{
|
||||||
|
return this.x*this.x + this.y*this.y;
|
||||||
|
}
|
||||||
|
|
||||||
|
mag() : number {
|
||||||
|
return Math.sqrt(this.magSq());
|
||||||
|
}
|
||||||
|
|
||||||
|
normalize() : Vec2{
|
||||||
|
if(this.x === 0 && this.y === 0) return this;
|
||||||
|
let mag = this.mag();
|
||||||
|
this.x /= mag;
|
||||||
|
this.y /= mag;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
setToAngle(angle : number) : Vec2{
|
||||||
|
this.x = Math.cos(angle);
|
||||||
|
this.y = Math.sin(angle);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
scaleTo(magnitude : number) : Vec2{
|
||||||
|
return this.normalize().scale(magnitude);
|
||||||
|
}
|
||||||
|
|
||||||
|
scale(factor : number, yFactor : number = null) : Vec2{
|
||||||
|
if(yFactor !== null){
|
||||||
|
this.x *= factor;
|
||||||
|
this.y *= yFactor;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
this.x *= factor;
|
||||||
|
this.y *= factor;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
rotate(angle : number) : Vec2{
|
||||||
|
let cs = Math.cos(angle);
|
||||||
|
let sn = Math.sin(angle);
|
||||||
|
let tempX = this.x*cs - this.y*sn;
|
||||||
|
let tempY = this.x*sn + this.y*cs;
|
||||||
|
this.x = tempX;
|
||||||
|
this.y = tempY;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
set(x : number, y : number) : Vec2{
|
||||||
|
this.x = x;
|
||||||
|
this.y = y;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
add(other : Vec2) : Vec2{
|
||||||
|
this.x += other.x;
|
||||||
|
this.y += other.y;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
sub(other : Vec2) : Vec2{
|
||||||
|
this.x -= other.x;
|
||||||
|
this.y -= other.y;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
toString() : string{
|
||||||
|
return "(" + this.x + ", " + this.y + ")";
|
||||||
|
}
|
||||||
|
}
|
20
src/DataTypes/Vec4.ts
Normal file
20
src/DataTypes/Vec4.ts
Normal file
|
@ -0,0 +1,20 @@
|
||||||
|
import Vec2 from "./Vec2";
|
||||||
|
|
||||||
|
export default class Vec4{
|
||||||
|
|
||||||
|
public x : number;
|
||||||
|
public y : number;
|
||||||
|
public z : number;
|
||||||
|
public w : number;
|
||||||
|
|
||||||
|
constructor(x : number = 0, y : number = 0, z : number = 0, w : number = 0){
|
||||||
|
this.x = x;
|
||||||
|
this.y = y;
|
||||||
|
this.z = z;
|
||||||
|
this.w = w;
|
||||||
|
}
|
||||||
|
|
||||||
|
split() : [Vec2, Vec2]{
|
||||||
|
return [new Vec2(this.x, this.y), new Vec2(this.z, this.w)];
|
||||||
|
}
|
||||||
|
}
|
66
src/Events/EventQueue.ts
Normal file
66
src/Events/EventQueue.ts
Normal file
|
@ -0,0 +1,66 @@
|
||||||
|
import Queue from "../DataTypes/Queue";
|
||||||
|
import Map from "../DataTypes/Map";
|
||||||
|
import GameEvent from "./GameEvent";
|
||||||
|
import Receiver from "./Receiver";
|
||||||
|
|
||||||
|
export default class EventQueue {
|
||||||
|
private static instance: EventQueue = null;
|
||||||
|
private readonly MAX_SIZE: number;
|
||||||
|
private q: Queue<GameEvent>;
|
||||||
|
private receivers: Map<Array<Receiver>>
|
||||||
|
|
||||||
|
private constructor(){
|
||||||
|
this.MAX_SIZE = 100;
|
||||||
|
this.q = new Queue<GameEvent>(this.MAX_SIZE);
|
||||||
|
this.receivers = new Map<Array<Receiver>>();
|
||||||
|
}
|
||||||
|
|
||||||
|
static getInstance(): EventQueue {
|
||||||
|
if(this.instance === null){
|
||||||
|
this.instance = new EventQueue();
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.instance;
|
||||||
|
}
|
||||||
|
|
||||||
|
addEvent(event: GameEvent): void {
|
||||||
|
this.q.enqueue(event);
|
||||||
|
}
|
||||||
|
|
||||||
|
subscribe(receiver: Receiver, type: string | Array<string>): void {
|
||||||
|
if(type instanceof Array){
|
||||||
|
// If it is an array, subscribe to all event types
|
||||||
|
for(let t of type){
|
||||||
|
this.addListener(receiver, t);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
this.addListener(receiver, type);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private addListener(receiver: Receiver, type: string): void {
|
||||||
|
if(this.receivers.has(type)){
|
||||||
|
this.receivers.get(type).push(receiver);
|
||||||
|
} else {
|
||||||
|
this.receivers.add(type, [receiver]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
update(deltaT: number): void{
|
||||||
|
while(this.q.hasItems()){
|
||||||
|
let event = this.q.dequeue();
|
||||||
|
|
||||||
|
if(this.receivers.has(event.type)){
|
||||||
|
for(let receiver of this.receivers.get(event.type)){
|
||||||
|
receiver.receive(event);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if(this.receivers.has("all")){
|
||||||
|
for(let receiver of this.receivers.get("all")){
|
||||||
|
receiver.receive(event);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
28
src/Events/GameEvent.ts
Normal file
28
src/Events/GameEvent.ts
Normal file
|
@ -0,0 +1,28 @@
|
||||||
|
import Map from "../DataTypes/Map"
|
||||||
|
|
||||||
|
export default class GameEvent{
|
||||||
|
public type: string;
|
||||||
|
public data: Map<any>;
|
||||||
|
public time: number;
|
||||||
|
|
||||||
|
constructor(type: string, data: Map<any> | Record<string, any> = null){
|
||||||
|
if (data === null) {
|
||||||
|
this.data = new Map<any>();
|
||||||
|
} else if (!(data instanceof Map)){
|
||||||
|
// data is a raw object, unpack
|
||||||
|
this.data = new Map<any>();
|
||||||
|
for(let key in data){
|
||||||
|
this.data.add(key, data[key]);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
this.data = data;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.type = type;
|
||||||
|
this.time = Date.now();
|
||||||
|
}
|
||||||
|
|
||||||
|
toString(): string {
|
||||||
|
return this.type + ": @" + this.time;
|
||||||
|
}
|
||||||
|
}
|
32
src/Events/Receiver.ts
Normal file
32
src/Events/Receiver.ts
Normal file
|
@ -0,0 +1,32 @@
|
||||||
|
import Queue from "../DataTypes/Queue";
|
||||||
|
import GameEvent from "./GameEvent";
|
||||||
|
|
||||||
|
export default class Receiver{
|
||||||
|
readonly MAX_SIZE: number;
|
||||||
|
private q: Queue<GameEvent>;
|
||||||
|
|
||||||
|
constructor(){
|
||||||
|
this.MAX_SIZE = 100;
|
||||||
|
this.q = new Queue(this.MAX_SIZE);
|
||||||
|
}
|
||||||
|
|
||||||
|
receive(event: GameEvent): void {
|
||||||
|
this.q.enqueue(event);
|
||||||
|
}
|
||||||
|
|
||||||
|
getNextEvent(): GameEvent {
|
||||||
|
return this.q.dequeue();
|
||||||
|
}
|
||||||
|
|
||||||
|
peekNextEvent(): GameEvent {
|
||||||
|
return this.q.peekNext()
|
||||||
|
}
|
||||||
|
|
||||||
|
hasNextEvent(): boolean {
|
||||||
|
return this.q.hasItems();
|
||||||
|
}
|
||||||
|
|
||||||
|
ignoreEvents(): void {
|
||||||
|
this.q.clear();
|
||||||
|
}
|
||||||
|
}
|
33
src/GameState/GameState.ts
Normal file
33
src/GameState/GameState.ts
Normal file
|
@ -0,0 +1,33 @@
|
||||||
|
import Stack from "../DataTypes/Stack";
|
||||||
|
import Scene from "./Scene";
|
||||||
|
|
||||||
|
export default class GameState{
|
||||||
|
private sceneStack: Stack<Scene>;
|
||||||
|
|
||||||
|
constructor(){
|
||||||
|
this.sceneStack = new Stack(10);
|
||||||
|
}
|
||||||
|
|
||||||
|
addScene(scene: Scene, pauseScenesBelow: boolean = true): void {
|
||||||
|
this.sceneStack.forEach((scene: Scene) => scene.setPaused(pauseScenesBelow));
|
||||||
|
this.sceneStack.push(scene);
|
||||||
|
}
|
||||||
|
|
||||||
|
removeScene(startNewTopScene: boolean = true): void {
|
||||||
|
this.sceneStack.pop();
|
||||||
|
this.sceneStack.peek().setPaused(!startNewTopScene);
|
||||||
|
}
|
||||||
|
|
||||||
|
changeScene(scene: Scene): void {
|
||||||
|
this.sceneStack.clear();
|
||||||
|
this.sceneStack.push(scene);
|
||||||
|
}
|
||||||
|
|
||||||
|
update(deltaT: number): void {
|
||||||
|
this.sceneStack.forEach((scene: Scene) => scene.update(deltaT));
|
||||||
|
}
|
||||||
|
|
||||||
|
render(ctx: CanvasRenderingContext2D): void {
|
||||||
|
this.sceneStack.forEach((scene: Scene) => scene.render(ctx));
|
||||||
|
}
|
||||||
|
}
|
56
src/GameState/Scene.ts
Normal file
56
src/GameState/Scene.ts
Normal file
|
@ -0,0 +1,56 @@
|
||||||
|
import Vec2 from "../DataTypes/Vec2";
|
||||||
|
import Viewport from "../SceneGraph/Viewport";
|
||||||
|
import SceneGraph from "../SceneGraph/SceneGraph";
|
||||||
|
import SceneGraphArray from "../SceneGraph/SceneGraphArray";
|
||||||
|
import GameNode from "../Nodes/GameNode";
|
||||||
|
|
||||||
|
export default class Scene{
|
||||||
|
private viewport: Viewport
|
||||||
|
private worldSize: Vec2;
|
||||||
|
private sceneGraph: SceneGraph;
|
||||||
|
private paused: boolean;
|
||||||
|
|
||||||
|
constructor(){
|
||||||
|
this.viewport = new Viewport();
|
||||||
|
this.viewport.setSize(800, 500);
|
||||||
|
// TODO: Find a way to make this not a hard-coded value
|
||||||
|
this.worldSize = new Vec2(1600, 1000);
|
||||||
|
this.viewport.setBounds(0, 0, 1600, 1000);
|
||||||
|
this.sceneGraph = new SceneGraphArray(this.viewport);
|
||||||
|
this.paused = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
setPaused(pauseValue: boolean): void {
|
||||||
|
this.paused = pauseValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
isPaused(): boolean {
|
||||||
|
return this.paused;
|
||||||
|
}
|
||||||
|
|
||||||
|
getViewport(): Viewport {
|
||||||
|
return this.viewport;
|
||||||
|
}
|
||||||
|
|
||||||
|
add(children: Array<GameNode> | GameNode): void {
|
||||||
|
if(children instanceof Array){
|
||||||
|
for(let child of children){
|
||||||
|
this.sceneGraph.addNode(child);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
this.sceneGraph.addNode(children);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
update(deltaT: number): void {
|
||||||
|
if(!this.paused){
|
||||||
|
this.viewport.update(deltaT);
|
||||||
|
this.sceneGraph.update(deltaT);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
render(ctx: CanvasRenderingContext2D): void {
|
||||||
|
let visibleSet = this.sceneGraph.getVisibleSet();
|
||||||
|
visibleSet.forEach(node => node.render(ctx, this.viewport.getPosition(), this.viewport.getSize()));
|
||||||
|
}
|
||||||
|
}
|
51
src/Input/InputHandler.ts
Normal file
51
src/Input/InputHandler.ts
Normal file
|
@ -0,0 +1,51 @@
|
||||||
|
import EventQueue from "../Events/EventQueue";
|
||||||
|
import Vec2 from "../DataTypes/Vec2";
|
||||||
|
import GameEvent from "../Events/GameEvent";
|
||||||
|
|
||||||
|
export default class InputHandler{
|
||||||
|
private eventQueue: EventQueue;
|
||||||
|
|
||||||
|
constructor(canvas: HTMLCanvasElement){
|
||||||
|
this.eventQueue = EventQueue.getInstance();
|
||||||
|
|
||||||
|
canvas.onmousedown = (event) => this.handleMouseDown(event, canvas);
|
||||||
|
canvas.onmouseup = (event) => this.handleMouseUp(event, canvas);
|
||||||
|
document.onkeydown = this.handleKeyDown;
|
||||||
|
document.onkeyup = this.handleKeyUp;
|
||||||
|
}
|
||||||
|
|
||||||
|
private handleMouseDown = (event: MouseEvent, canvas: HTMLCanvasElement): void => {
|
||||||
|
let pos = this.getMousePosition(event, canvas);
|
||||||
|
let gameEvent = new GameEvent("mouse_down", {position: pos});
|
||||||
|
this.eventQueue.addEvent(gameEvent);
|
||||||
|
}
|
||||||
|
|
||||||
|
private handleMouseUp = (event: MouseEvent, canvas: HTMLCanvasElement): void => {
|
||||||
|
let pos = this.getMousePosition(event, canvas);
|
||||||
|
let gameEvent = new GameEvent("mouse_up", {position: pos});
|
||||||
|
this.eventQueue.addEvent(gameEvent);
|
||||||
|
}
|
||||||
|
|
||||||
|
private handleKeyDown = (event: KeyboardEvent): void => {
|
||||||
|
let key = this.getKey(event);
|
||||||
|
let gameEvent = new GameEvent("key_down", {key: key});
|
||||||
|
this.eventQueue.addEvent(gameEvent);
|
||||||
|
}
|
||||||
|
|
||||||
|
private handleKeyUp = (event: KeyboardEvent): void => {
|
||||||
|
let key = this.getKey(event);
|
||||||
|
let gameEvent = new GameEvent("key_up", {key: key});
|
||||||
|
this.eventQueue.addEvent(gameEvent);
|
||||||
|
}
|
||||||
|
|
||||||
|
private getKey(keyEvent: KeyboardEvent){
|
||||||
|
return keyEvent.key.toLowerCase();
|
||||||
|
}
|
||||||
|
|
||||||
|
private getMousePosition(mouseEvent: MouseEvent, canvas: HTMLCanvasElement): Vec2 {
|
||||||
|
let rect = canvas.getBoundingClientRect();
|
||||||
|
let x = mouseEvent.clientX - rect.left;
|
||||||
|
let y = mouseEvent.clientY - rect.top;
|
||||||
|
return new Vec2(x, y);
|
||||||
|
}
|
||||||
|
}
|
93
src/Input/InputReceiver.ts
Normal file
93
src/Input/InputReceiver.ts
Normal file
|
@ -0,0 +1,93 @@
|
||||||
|
import Receiver from "../Events/Receiver";
|
||||||
|
import Map from "../DataTypes/Map";
|
||||||
|
import Vec2 from "../DataTypes/Vec2";
|
||||||
|
import EventQueue from "../Events/EventQueue";
|
||||||
|
|
||||||
|
export default class InputReceiver{
|
||||||
|
private static instance: InputReceiver = null;
|
||||||
|
|
||||||
|
private mousePressed: boolean;
|
||||||
|
private mouseJustPressed: boolean;
|
||||||
|
private keyJustPressed: Map<boolean>;
|
||||||
|
private keyPressed: Map<boolean>;
|
||||||
|
private mousePressPosition: Vec2;
|
||||||
|
private eventQueue: EventQueue;
|
||||||
|
private receiver: Receiver;
|
||||||
|
|
||||||
|
private constructor(){
|
||||||
|
this.mousePressed = false;
|
||||||
|
this.mouseJustPressed = false;
|
||||||
|
this.receiver = new Receiver();
|
||||||
|
this.keyJustPressed = new Map<boolean>();
|
||||||
|
this.keyPressed = new Map<boolean>();
|
||||||
|
this.mousePressPosition = null;
|
||||||
|
|
||||||
|
this.eventQueue = EventQueue.getInstance();
|
||||||
|
this.eventQueue.subscribe(this.receiver, ["mouse_down", "mouse_up", "key_down", "key_up"]);
|
||||||
|
}
|
||||||
|
|
||||||
|
static getInstance(): InputReceiver{
|
||||||
|
if(this.instance === null){
|
||||||
|
this.instance = new InputReceiver();
|
||||||
|
}
|
||||||
|
return this.instance;
|
||||||
|
}
|
||||||
|
|
||||||
|
update(deltaT: number): void {
|
||||||
|
// Reset the justPressed values to false
|
||||||
|
this.mouseJustPressed = false;
|
||||||
|
this.keyJustPressed.forEach((key: string) => this.keyJustPressed.set(key, false));
|
||||||
|
|
||||||
|
while(this.receiver.hasNextEvent()){
|
||||||
|
let event = this.receiver.getNextEvent();
|
||||||
|
if(event.type === "mouse_down"){
|
||||||
|
this.mouseJustPressed = true;
|
||||||
|
this.mousePressed = true;
|
||||||
|
this.mousePressPosition = event.data.get("position");
|
||||||
|
}
|
||||||
|
|
||||||
|
if(event.type === "mouse_up"){
|
||||||
|
this.mousePressed = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if(event.type === "key_down"){
|
||||||
|
let key = event.data.get("key")
|
||||||
|
this.keyJustPressed.set(key, true);
|
||||||
|
this.keyPressed.set(key, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
if(event.type === "key_up"){
|
||||||
|
let key = event.data.get("key")
|
||||||
|
this.keyPressed.set(key, false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
isJustPressed(key: string): boolean {
|
||||||
|
if(this.keyJustPressed.has(key)){
|
||||||
|
return this.keyJustPressed.get(key)
|
||||||
|
} else {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
isPressed(key: string): boolean {
|
||||||
|
if(this.keyPressed.has(key)){
|
||||||
|
return this.keyPressed.get(key)
|
||||||
|
} else {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
isMouseJustPressed(): boolean {
|
||||||
|
return this.mouseJustPressed;
|
||||||
|
}
|
||||||
|
|
||||||
|
isMousePressed(): boolean {
|
||||||
|
return this.mousePressed;
|
||||||
|
}
|
||||||
|
|
||||||
|
getMousePressPosition(): Vec2 {
|
||||||
|
return this.mousePressPosition;
|
||||||
|
}
|
||||||
|
}
|
153
src/Loop/GameLoop.ts
Normal file
153
src/Loop/GameLoop.ts
Normal file
|
@ -0,0 +1,153 @@
|
||||||
|
import EventQueue from "../Events/EventQueue";
|
||||||
|
import InputReceiver from "../Input/InputReceiver";
|
||||||
|
import InputHandler from "../Input/InputHandler";
|
||||||
|
import Recorder from "../Playback/Recorder";
|
||||||
|
import GameState from "../GameState/GameState";
|
||||||
|
|
||||||
|
export default class GameLoop{
|
||||||
|
// The amount of time to spend on a physics step
|
||||||
|
private maxFPS: number;
|
||||||
|
private simulationTimestep: number;
|
||||||
|
|
||||||
|
// The time when the last frame was drawn
|
||||||
|
private lastFrameTime: number;
|
||||||
|
|
||||||
|
// The current frame of the game
|
||||||
|
private frame: number;
|
||||||
|
|
||||||
|
// Keeping track of the fps
|
||||||
|
private runningFrameSum: number;
|
||||||
|
private numFramesInSum: number;
|
||||||
|
private maxFramesInSum: number;
|
||||||
|
private fps: number;
|
||||||
|
|
||||||
|
private started: boolean;
|
||||||
|
private running: boolean;
|
||||||
|
private frameDelta: number;
|
||||||
|
|
||||||
|
readonly GAME_CANVAS: HTMLCanvasElement;
|
||||||
|
readonly WIDTH: number;
|
||||||
|
readonly HEIGHT: number;
|
||||||
|
private ctx: CanvasRenderingContext2D;
|
||||||
|
private eventQueue: EventQueue;
|
||||||
|
private inputHandler: InputHandler;
|
||||||
|
private inputReceiver: InputReceiver;
|
||||||
|
private recorder: Recorder;
|
||||||
|
private gameState: GameState;
|
||||||
|
|
||||||
|
constructor(){
|
||||||
|
this.maxFPS = 60;
|
||||||
|
this.simulationTimestep = Math.floor(1000/this.maxFPS);
|
||||||
|
this.frame = 0;
|
||||||
|
this.runningFrameSum = 0;
|
||||||
|
this.numFramesInSum = 0;
|
||||||
|
this.maxFramesInSum = 30;
|
||||||
|
this.fps = this.maxFPS;
|
||||||
|
|
||||||
|
this.started = false;
|
||||||
|
this.running = false;
|
||||||
|
|
||||||
|
this.GAME_CANVAS = document.getElementById("game-canvas") as HTMLCanvasElement;
|
||||||
|
this.GAME_CANVAS.style.setProperty("background-color", "whitesmoke");
|
||||||
|
|
||||||
|
this.WIDTH = 800;
|
||||||
|
this.HEIGHT = 500;
|
||||||
|
this.ctx = this.initializeCanvas(this.GAME_CANVAS, this.WIDTH, this.HEIGHT);
|
||||||
|
|
||||||
|
this.eventQueue = EventQueue.getInstance();
|
||||||
|
this.inputHandler = new InputHandler(this.GAME_CANVAS);
|
||||||
|
this.inputReceiver = InputReceiver.getInstance();
|
||||||
|
this.recorder = new Recorder();
|
||||||
|
this.gameState = new GameState();
|
||||||
|
}
|
||||||
|
|
||||||
|
private initializeCanvas(canvas: HTMLCanvasElement, width: number, height: number): CanvasRenderingContext2D {
|
||||||
|
canvas.width = width;
|
||||||
|
canvas.height = height;
|
||||||
|
return canvas.getContext("2d");
|
||||||
|
}
|
||||||
|
|
||||||
|
setMaxFPS(initMax: number): void {
|
||||||
|
this.maxFPS = initMax;
|
||||||
|
this.simulationTimestep = Math.floor(1000/this.maxFPS);
|
||||||
|
}
|
||||||
|
|
||||||
|
getGameState(): GameState {
|
||||||
|
return this.gameState;
|
||||||
|
}
|
||||||
|
|
||||||
|
private renderFPS(ctx: CanvasRenderingContext2D): void {
|
||||||
|
ctx.fillStyle = "black";
|
||||||
|
ctx.font = "30px Arial"
|
||||||
|
ctx.fillText(this.fps.toFixed(1), 5, 5 + 30);
|
||||||
|
}
|
||||||
|
|
||||||
|
private updateFrameCount(timestep: number): void {
|
||||||
|
this.frame += 1;
|
||||||
|
this.numFramesInSum += 1;
|
||||||
|
this.runningFrameSum += timestep;
|
||||||
|
if(this.numFramesInSum >= this.maxFramesInSum){
|
||||||
|
this.fps = 1000 * this.numFramesInSum / this.runningFrameSum;
|
||||||
|
this.numFramesInSum = 0;
|
||||||
|
this.runningFrameSum = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
start(): void {
|
||||||
|
if(!this.started){
|
||||||
|
this.started = true;
|
||||||
|
|
||||||
|
window.requestAnimationFrame(this.startFrame);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
startFrame = (timestamp: number): void => {
|
||||||
|
this.running = true;
|
||||||
|
|
||||||
|
this.render();
|
||||||
|
|
||||||
|
this.lastFrameTime = timestamp;
|
||||||
|
|
||||||
|
window.requestAnimationFrame(this.doFrame);
|
||||||
|
}
|
||||||
|
|
||||||
|
doFrame = (timestamp: number): void => {
|
||||||
|
// Request animation frame to prepare for another update or render
|
||||||
|
window.requestAnimationFrame(this.doFrame);
|
||||||
|
|
||||||
|
// If we are trying to update too soon, return and do nothing
|
||||||
|
if(timestamp < this.lastFrameTime + this.simulationTimestep){
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Currently, update and draw are synced - eventually it would probably be good to desync these
|
||||||
|
this.frameDelta = timestamp - this.lastFrameTime;
|
||||||
|
this.lastFrameTime = timestamp;
|
||||||
|
|
||||||
|
// Update while we can (This will present problems if we leave the window)
|
||||||
|
let i = 0;
|
||||||
|
while(this.frameDelta >= this.simulationTimestep){
|
||||||
|
this.update(this.simulationTimestep/1000);
|
||||||
|
this.frameDelta -= this.simulationTimestep;
|
||||||
|
|
||||||
|
// Update the frame of the game
|
||||||
|
this.updateFrameCount(this.simulationTimestep);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Updates are done, draw
|
||||||
|
this.render();
|
||||||
|
}
|
||||||
|
|
||||||
|
update(deltaT: number): void {
|
||||||
|
this.eventQueue.update(deltaT);
|
||||||
|
this.inputReceiver.update(deltaT);
|
||||||
|
this.recorder.update(deltaT);
|
||||||
|
this.gameState.update(deltaT);
|
||||||
|
}
|
||||||
|
|
||||||
|
render(): void {
|
||||||
|
this.ctx.clearRect(0, 0, this.WIDTH, this.HEIGHT);
|
||||||
|
this.gameState.render(this.ctx);
|
||||||
|
this.renderFPS(this.ctx);
|
||||||
|
}
|
||||||
|
}
|
26
src/Nodes/ColoredCircle.ts
Normal file
26
src/Nodes/ColoredCircle.ts
Normal file
|
@ -0,0 +1,26 @@
|
||||||
|
import GameNode from "./GameNode";
|
||||||
|
import Color from "../Utils/Color";
|
||||||
|
import Vec2 from "../DataTypes/Vec2";
|
||||||
|
import RandUtils from "../Utils/RandUtils";
|
||||||
|
|
||||||
|
export default class ColoredCircle extends GameNode{
|
||||||
|
private color: Color;
|
||||||
|
|
||||||
|
constructor(){
|
||||||
|
super();
|
||||||
|
this.position = new Vec2(RandUtils.randInt(0, 1000), RandUtils.randInt(0, 1000));
|
||||||
|
this.color = RandUtils.randColor();
|
||||||
|
console.log(this.color.toStringRGB());
|
||||||
|
this.size = new Vec2(50, 50);
|
||||||
|
}
|
||||||
|
|
||||||
|
update(deltaT: number): void {}
|
||||||
|
|
||||||
|
render(ctx: CanvasRenderingContext2D, viewportOrigin: Vec2, viewportSize: Vec2){
|
||||||
|
ctx.fillStyle = this.color.toStringRGB();
|
||||||
|
ctx.beginPath();
|
||||||
|
ctx.arc(this.position.x + this.size.x/2 - viewportOrigin.x, this.position.y + this.size.y/2 - viewportOrigin.y, this.size.x/2, 0, Math.PI*2, false);
|
||||||
|
ctx.fill();
|
||||||
|
ctx.closePath();
|
||||||
|
}
|
||||||
|
}
|
38
src/Nodes/GameNode.ts
Normal file
38
src/Nodes/GameNode.ts
Normal file
|
@ -0,0 +1,38 @@
|
||||||
|
import EventQueue from "../Events/EventQueue";
|
||||||
|
import InputReceiver from "../Input/InputReceiver";
|
||||||
|
import Vec2 from "../DataTypes/Vec2";
|
||||||
|
|
||||||
|
export default abstract class GameNode{
|
||||||
|
protected eventQueue: EventQueue;
|
||||||
|
protected input: InputReceiver;
|
||||||
|
protected position: Vec2;
|
||||||
|
protected size: Vec2;
|
||||||
|
|
||||||
|
constructor(){
|
||||||
|
this.eventQueue = EventQueue.getInstance();
|
||||||
|
this.input = InputReceiver.getInstance();
|
||||||
|
this.position = new Vec2(0, 0);
|
||||||
|
this.size = new Vec2(0, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
getPosition(): Vec2 {
|
||||||
|
return this.position;
|
||||||
|
}
|
||||||
|
|
||||||
|
getSize(): Vec2 {
|
||||||
|
return this.size;
|
||||||
|
}
|
||||||
|
|
||||||
|
contains(x: number, y: number): boolean {
|
||||||
|
if(x > this.position.x && x < this.position.x + this.size.x){
|
||||||
|
if(y > this.position.y && y < this.position.y + this.size.y){
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
abstract update(deltaT: number): void;
|
||||||
|
|
||||||
|
abstract render(ctx: CanvasRenderingContext2D, viewportOrigin: Vec2, viewportSize: Vec2): void;
|
||||||
|
}
|
32
src/Nodes/Player.ts
Normal file
32
src/Nodes/Player.ts
Normal file
|
@ -0,0 +1,32 @@
|
||||||
|
import GameNode from "./GameNode";
|
||||||
|
import Vec2 from "../DataTypes/Vec2";
|
||||||
|
|
||||||
|
export default class Player extends GameNode{
|
||||||
|
velocity: Vec2;
|
||||||
|
speed: number;
|
||||||
|
|
||||||
|
constructor(){
|
||||||
|
super();
|
||||||
|
this.velocity = new Vec2(0, 0);
|
||||||
|
this.speed = 300;
|
||||||
|
this.size = new Vec2(50, 50);
|
||||||
|
};
|
||||||
|
|
||||||
|
update(deltaT: number): void {
|
||||||
|
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('s') ? 1 : 0;
|
||||||
|
dir.y += this.input.isPressed('w') ? -1 : 0;
|
||||||
|
|
||||||
|
dir.normalize();
|
||||||
|
|
||||||
|
this.velocity = dir.scale(this.speed);
|
||||||
|
this.position = this.position.add(this.velocity.scale(deltaT));
|
||||||
|
}
|
||||||
|
|
||||||
|
render(ctx: CanvasRenderingContext2D, viewportOrigin: Vec2, viewportSize: Vec2){
|
||||||
|
ctx.fillStyle = "#FF0000";
|
||||||
|
ctx.fillRect(this.position.x - viewportOrigin.x, this.position.y - viewportOrigin.y, this.size.x, this.size.y);
|
||||||
|
}
|
||||||
|
}
|
89
src/Nodes/UIElement.ts
Normal file
89
src/Nodes/UIElement.ts
Normal file
|
@ -0,0 +1,89 @@
|
||||||
|
import GameNode from "./GameNode";
|
||||||
|
import Color from "../Utils/Color";
|
||||||
|
import Vec2 from "../DataTypes/Vec2";
|
||||||
|
import GameEvent from "../Events/GameEvent";
|
||||||
|
|
||||||
|
export default class UIElement extends GameNode{
|
||||||
|
parent: GameNode;
|
||||||
|
children: Array<GameNode>;
|
||||||
|
text: string;
|
||||||
|
backgroundColor: Color;
|
||||||
|
textColor: Color;
|
||||||
|
onPress: Function;
|
||||||
|
onPressSignal: string;
|
||||||
|
onHover: Function;
|
||||||
|
|
||||||
|
constructor(){
|
||||||
|
super();
|
||||||
|
|
||||||
|
this.parent = null;
|
||||||
|
this.children = [];
|
||||||
|
this.text = "";
|
||||||
|
this.backgroundColor = new Color(0, 0, 0, 0);
|
||||||
|
this.textColor = new Color(0, 0, 0, 1);
|
||||||
|
|
||||||
|
this.onPress = null;
|
||||||
|
this.onPressSignal = null;
|
||||||
|
|
||||||
|
this.onHover = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
setPosition(vecOrX: Vec2 | number, y: number = null): void {
|
||||||
|
if(vecOrX instanceof Vec2){
|
||||||
|
this.position.set(vecOrX.x, vecOrX.y);
|
||||||
|
} else {
|
||||||
|
this.position.set(vecOrX, y);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
setText(text: string): void {
|
||||||
|
this.text = text;
|
||||||
|
}
|
||||||
|
|
||||||
|
setBackgroundColor(color: Color): void {
|
||||||
|
this.backgroundColor = color;
|
||||||
|
}
|
||||||
|
|
||||||
|
setTextColor(color: Color): void {
|
||||||
|
this.textColor = color;
|
||||||
|
}
|
||||||
|
|
||||||
|
update(deltaT: number): void {
|
||||||
|
if(this.input.isMouseJustPressed()){
|
||||||
|
let mousePos = this.input.getMousePressPosition();
|
||||||
|
if(mousePos.x >= this.position.x && mousePos.x <= this.position.x + this.size.x){
|
||||||
|
// Inside x bounds
|
||||||
|
if(mousePos.y >= this.position.y && mousePos.y <= this.position.y + this.size.y){
|
||||||
|
// Inside element
|
||||||
|
if(this.onHover !== null){
|
||||||
|
this.onHover();
|
||||||
|
}
|
||||||
|
|
||||||
|
if(this.onPress !== null){
|
||||||
|
this.onPress();
|
||||||
|
}
|
||||||
|
if(this.onPressSignal !== null){
|
||||||
|
let event = new GameEvent(this.onPressSignal, {});
|
||||||
|
this.eventQueue.addEvent(event);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
render(ctx: CanvasRenderingContext2D, viewportOrigin: Vec2, viewportSize: Vec2){
|
||||||
|
ctx.fillStyle = this.backgroundColor.toStringRGBA();
|
||||||
|
ctx.fillRect(this.position.x - viewportOrigin.x, this.position.y - viewportOrigin.y, this.size.x, this.size.y);
|
||||||
|
ctx.fillStyle = this.textColor.toStringRGBA();
|
||||||
|
ctx.font = "30px Arial"
|
||||||
|
ctx.fillText(this.text, this.position.x - viewportOrigin.x, this.position.y - viewportOrigin.y + 30);
|
||||||
|
}
|
||||||
|
}
|
90
src/Playback/Recorder.ts
Normal file
90
src/Playback/Recorder.ts
Normal file
|
@ -0,0 +1,90 @@
|
||||||
|
import Queue from "../DataTypes/Queue";
|
||||||
|
import Receiver from "../Events/Receiver";
|
||||||
|
import GameEvent from "../Events/GameEvent";
|
||||||
|
import EventQueue from "../Events/EventQueue";
|
||||||
|
|
||||||
|
export default class Recorder{
|
||||||
|
private receiver: Receiver;
|
||||||
|
private log: Queue<LogItem>;
|
||||||
|
private recording: boolean;
|
||||||
|
private eventQueue: EventQueue;
|
||||||
|
private frame: number;
|
||||||
|
private playing: boolean;
|
||||||
|
|
||||||
|
constructor(){
|
||||||
|
this.receiver = new Receiver();
|
||||||
|
this.log = new Queue(1000);
|
||||||
|
this.recording = false;
|
||||||
|
this.playing = false;
|
||||||
|
this.frame = 0;
|
||||||
|
|
||||||
|
this.eventQueue = EventQueue.getInstance();
|
||||||
|
this.eventQueue.subscribe(this.receiver, "all");
|
||||||
|
}
|
||||||
|
|
||||||
|
update(deltaT: number): void {
|
||||||
|
if(this.recording){
|
||||||
|
this.frame += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
if(this.playing){
|
||||||
|
// If playing, ignore events, just feed the record to the event queue
|
||||||
|
this.receiver.ignoreEvents();
|
||||||
|
|
||||||
|
/*
|
||||||
|
While there is a next item, and while it should occur in this frame,
|
||||||
|
send the event. i.e., while current_frame * current_delta_t is greater
|
||||||
|
than recorded_frame * recorded_delta_t
|
||||||
|
*/
|
||||||
|
while(this.log.hasItems()
|
||||||
|
&& this.log.peekNext().frame * this.log.peekNext().delta < this.frame * deltaT){
|
||||||
|
let event = this.log.dequeue().event;
|
||||||
|
console.log(event);
|
||||||
|
this.eventQueue.addEvent(event);
|
||||||
|
}
|
||||||
|
|
||||||
|
if(!this.log.hasItems()){
|
||||||
|
this.playing = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.frame += 1;
|
||||||
|
} else {
|
||||||
|
// If not playing, handle events
|
||||||
|
while(this.receiver.hasNextEvent()){
|
||||||
|
let event = this.receiver.getNextEvent();
|
||||||
|
|
||||||
|
if(event.type === "stop_button_press"){
|
||||||
|
this.recording = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if(this.recording){
|
||||||
|
this.log.enqueue(new LogItem(this.frame, deltaT, event));
|
||||||
|
}
|
||||||
|
|
||||||
|
if(event.type === "record_button_press"){
|
||||||
|
this.log.clear();
|
||||||
|
this.recording = true;
|
||||||
|
this.frame = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
if(event.type === "play_button_press"){
|
||||||
|
this.frame = 0;
|
||||||
|
this.recording = false;
|
||||||
|
this.playing = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class LogItem {
|
||||||
|
frame: number;
|
||||||
|
delta: number;
|
||||||
|
event: GameEvent;
|
||||||
|
|
||||||
|
constructor(frame: number, deltaT: number, event: GameEvent){
|
||||||
|
this.frame = frame;
|
||||||
|
this.delta = deltaT;
|
||||||
|
this.event = event;
|
||||||
|
}
|
||||||
|
}
|
55
src/SceneGraph/SceneGraph.ts
Normal file
55
src/SceneGraph/SceneGraph.ts
Normal file
|
@ -0,0 +1,55 @@
|
||||||
|
import Viewport from "./Viewport";
|
||||||
|
import GameNode from "../Nodes/GameNode";
|
||||||
|
import Map from "../DataTypes/Map";
|
||||||
|
import Vec2 from "../DataTypes/Vec2";
|
||||||
|
|
||||||
|
export default abstract class SceneGraph{
|
||||||
|
protected viewport: Viewport;
|
||||||
|
protected nodeMap: Map<GameNode>;
|
||||||
|
protected idCounter: number;
|
||||||
|
|
||||||
|
constructor(viewport: Viewport){
|
||||||
|
this.viewport = viewport;
|
||||||
|
this.nodeMap = new Map<GameNode>();
|
||||||
|
this.idCounter = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
addNode(node: GameNode): number {
|
||||||
|
this.nodeMap.add(this.idCounter.toString(), node);
|
||||||
|
this.addNodeSpecific(node, this.idCounter.toString());
|
||||||
|
this.idCounter += 1;
|
||||||
|
return this.idCounter - 1;
|
||||||
|
};
|
||||||
|
|
||||||
|
protected abstract addNodeSpecific(node: GameNode, id: string): void;
|
||||||
|
|
||||||
|
removeNode(node: GameNode): void {
|
||||||
|
// Find and remove node in O(n)
|
||||||
|
// TODO: Can this be better?
|
||||||
|
let id = this.nodeMap.keys().filter((key: string) => this.nodeMap.get(key) === node)[0];
|
||||||
|
if(id !== undefined){
|
||||||
|
this.nodeMap.set(id, undefined);
|
||||||
|
this.removeNodeSpecific(node, id);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
protected abstract removeNodeSpecific(node: GameNode, id: string): void;
|
||||||
|
|
||||||
|
getNode(id: string): GameNode{
|
||||||
|
return this.nodeMap.get(id);
|
||||||
|
};
|
||||||
|
|
||||||
|
getNodeAt(vecOrX: Vec2 | number, y: number = null): GameNode{
|
||||||
|
if(vecOrX instanceof Vec2){
|
||||||
|
return this.getNodeAtCoords(vecOrX.x, vecOrX.y);
|
||||||
|
} else {
|
||||||
|
return this.getNodeAtCoords(vecOrX, y);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected abstract getNodeAtCoords(x: number, y: number): GameNode;
|
||||||
|
|
||||||
|
abstract update(deltaT: number): void;
|
||||||
|
|
||||||
|
abstract getVisibleSet(): Array<GameNode>;
|
||||||
|
}
|
67
src/SceneGraph/SceneGraphArray.ts
Normal file
67
src/SceneGraph/SceneGraphArray.ts
Normal file
|
@ -0,0 +1,67 @@
|
||||||
|
import SceneGraph from "./SceneGraph";
|
||||||
|
import GameNode from "../Nodes/GameNode";
|
||||||
|
import Viewport from "./Viewport";
|
||||||
|
|
||||||
|
export default class SceneGraphArray extends SceneGraph{
|
||||||
|
private nodeList: Array<GameNode>;
|
||||||
|
private turnOffViewportCulling_demoTool: boolean;
|
||||||
|
|
||||||
|
constructor(viewport: Viewport){
|
||||||
|
super(viewport);
|
||||||
|
|
||||||
|
this.nodeList = new Array<GameNode>();
|
||||||
|
this.turnOffViewportCulling_demoTool = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
setViewportCulling_demoTool(bool: boolean): void {
|
||||||
|
this.turnOffViewportCulling_demoTool = bool;
|
||||||
|
}
|
||||||
|
|
||||||
|
addNodeSpecific(node: GameNode, id: string): void {
|
||||||
|
this.nodeList.push(node);
|
||||||
|
}
|
||||||
|
|
||||||
|
removeNodeSpecific(node: GameNode, id: string): void {
|
||||||
|
let index = this.nodeList.indexOf(node);
|
||||||
|
if(index > -1){
|
||||||
|
this.nodeList.splice(index, 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
getNodeAtCoords(x: number, y: number): GameNode {
|
||||||
|
// TODO: This only returns the first node found. There is no notion of z coordinates
|
||||||
|
for(let node of this.nodeList){
|
||||||
|
if(node.contains(x, y)){
|
||||||
|
return node;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
update(deltaT: number): void {
|
||||||
|
for(let node of this.nodeList){
|
||||||
|
node.update(deltaT);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
getVisibleSet(): Array<GameNode> {
|
||||||
|
// If viewport culling is turned off for demonstration
|
||||||
|
if(this.turnOffViewportCulling_demoTool){
|
||||||
|
let visibleSet = new Array<GameNode>();
|
||||||
|
for(let node of this.nodeList){
|
||||||
|
visibleSet.push(node);
|
||||||
|
}
|
||||||
|
return visibleSet;
|
||||||
|
}
|
||||||
|
|
||||||
|
let visibleSet = new Array<GameNode>();
|
||||||
|
|
||||||
|
for(let node of this.nodeList){
|
||||||
|
if(this.viewport.includes(node)){
|
||||||
|
visibleSet.push(node);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return visibleSet;
|
||||||
|
}
|
||||||
|
}
|
73
src/SceneGraph/Viewport.ts
Normal file
73
src/SceneGraph/Viewport.ts
Normal file
|
@ -0,0 +1,73 @@
|
||||||
|
import Vec2 from "../DataTypes/Vec2";
|
||||||
|
import Vec4 from "../DataTypes/Vec4";
|
||||||
|
import GameNode from "../Nodes/GameNode";
|
||||||
|
import MathUtils from "../Utils/MathUtils";
|
||||||
|
|
||||||
|
export default class Viewport{
|
||||||
|
private position: Vec2;
|
||||||
|
private size: Vec2;
|
||||||
|
private bounds: Vec4;
|
||||||
|
private following: GameNode;
|
||||||
|
|
||||||
|
constructor(){
|
||||||
|
this.position = new Vec2(0, 0);
|
||||||
|
this.size = new Vec2(0, 0);
|
||||||
|
this.bounds = new Vec4(0, 0, 0, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
getPosition(): Vec2 {
|
||||||
|
return this.position;
|
||||||
|
}
|
||||||
|
|
||||||
|
setPosition(vecOrX: Vec2 | number, y: number = null): void {
|
||||||
|
if(vecOrX instanceof Vec2){
|
||||||
|
this.position.set(vecOrX.x, vecOrX.y);
|
||||||
|
} else {
|
||||||
|
this.position.set(vecOrX, y);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
getSize(): Vec2{
|
||||||
|
return this.size;
|
||||||
|
}
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
includes(node: GameNode): boolean {
|
||||||
|
let nodePos = node.getPosition();
|
||||||
|
let nodeSize = node.getSize();
|
||||||
|
if(nodePos.x + nodeSize.x > this.position.x && nodePos.x < this.position.x + this.size.x){
|
||||||
|
if(nodePos.y + nodeSize.y > this.position.y && nodePos.y < this.position.y + this.size.y){
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: Put some error handling on this for trying to make the bounds too small for the viewport
|
||||||
|
// TODO: This should probably be done automatically, or should consider the aspect ratio or something
|
||||||
|
setBounds(lowerX: number, lowerY: number, upperX: number, upperY: number): void {
|
||||||
|
this.bounds = new Vec4(lowerX, lowerY, upperX, upperY);
|
||||||
|
}
|
||||||
|
|
||||||
|
follow(node: GameNode): void {
|
||||||
|
this.following = node;
|
||||||
|
}
|
||||||
|
|
||||||
|
update(deltaT: number): void {
|
||||||
|
if(this.following){
|
||||||
|
this.position.x = this.following.getPosition().x - this.size.x/2;
|
||||||
|
this.position.y = this.following.getPosition().y - this.size.y/2;
|
||||||
|
let [min, max] = this.bounds.split();
|
||||||
|
this.position.x = MathUtils.clamp(this.position.x, min.x, max.x - this.size.x/2);
|
||||||
|
this.position.y = MathUtils.clamp(this.position.y, min.y, max.y - this.size.y/2);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
30
src/Utils/Color.ts
Normal file
30
src/Utils/Color.ts
Normal file
|
@ -0,0 +1,30 @@
|
||||||
|
import MathUtils from "./MathUtils";
|
||||||
|
|
||||||
|
export default class Color{
|
||||||
|
public r: number;
|
||||||
|
public g: number;
|
||||||
|
public b: number;
|
||||||
|
public a: number;
|
||||||
|
|
||||||
|
constructor(r: number = 0, g: number = 0, b: number = 0, a: number = null){
|
||||||
|
this.r = r;
|
||||||
|
this.g = g;
|
||||||
|
this.b = b;
|
||||||
|
this.a = a;
|
||||||
|
}
|
||||||
|
|
||||||
|
toString(): string{
|
||||||
|
return "#" + MathUtils.toHex(this.r, 2) + MathUtils.toHex(this.g, 2) + MathUtils.toHex(this.b, 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
toStringRGB(){
|
||||||
|
return "rgb(" + this.r.toString() + ", " + this.g.toString() + ", " + this.b.toString() + ")";
|
||||||
|
}
|
||||||
|
|
||||||
|
toStringRGBA(){
|
||||||
|
if(this.a === null){
|
||||||
|
throw "No alpha value assigned to color";
|
||||||
|
}
|
||||||
|
return "rgb(" + this.r.toString() + ", " + this.g.toString() + ", " + this.b.toString() + ", " + this.a.toString() +")"
|
||||||
|
}
|
||||||
|
}
|
37
src/Utils/MathUtils.ts
Normal file
37
src/Utils/MathUtils.ts
Normal file
|
@ -0,0 +1,37 @@
|
||||||
|
export default class MathUtils{
|
||||||
|
static clamp(x: number, min: number, max: number): number{
|
||||||
|
if(x < min) return min;
|
||||||
|
if(x > max) return max;
|
||||||
|
return x;
|
||||||
|
}
|
||||||
|
|
||||||
|
static toHex(num: number, minLength: number = null): string{
|
||||||
|
let factor = 1;
|
||||||
|
while(factor*16 < num){
|
||||||
|
factor *= 16;
|
||||||
|
}
|
||||||
|
let hexStr = "";
|
||||||
|
while(num > 0){
|
||||||
|
let digit = Math.floor(num/factor);
|
||||||
|
hexStr += MathUtils.toHexDigit(digit);
|
||||||
|
num -= digit * factor;
|
||||||
|
factor /= 16;
|
||||||
|
}
|
||||||
|
|
||||||
|
if(minLength !== null){
|
||||||
|
while(hexStr.length < minLength){
|
||||||
|
hexStr = "0" + hexStr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return hexStr;
|
||||||
|
}
|
||||||
|
|
||||||
|
static toHexDigit(num: number): string{
|
||||||
|
if(num < 10){
|
||||||
|
return "" + num;
|
||||||
|
} else {
|
||||||
|
return String.fromCharCode(65 + num - 10);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
19
src/Utils/RandUtils.ts
Normal file
19
src/Utils/RandUtils.ts
Normal file
|
@ -0,0 +1,19 @@
|
||||||
|
import MathUtils from "./MathUtils";
|
||||||
|
import Color from "./Color";
|
||||||
|
|
||||||
|
export default class RandUtils{
|
||||||
|
static randInt(min: number, max: number): number{
|
||||||
|
return Math.floor(Math.random()*(max - min) + min);
|
||||||
|
}
|
||||||
|
|
||||||
|
static randHex(min: number, max: number): string{
|
||||||
|
return MathUtils.toHex(RandUtils.randInt(min, max));
|
||||||
|
}
|
||||||
|
|
||||||
|
static randColor(): Color{
|
||||||
|
let r = RandUtils.randInt(0, 256);
|
||||||
|
let g = RandUtils.randInt(0, 256);
|
||||||
|
let b = RandUtils.randInt(0, 256);
|
||||||
|
return new Color(r, g, b);
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,3 +0,0 @@
|
||||||
export function sayHello(name: string){
|
|
||||||
return `Hello from ${name}`;
|
|
||||||
}
|
|
|
@ -1,11 +1,11 @@
|
||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html>
|
<html>
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
<title>Hello World!</title>
|
<title>Hello World!</title>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<p id="greeting">Loading ...</p>
|
<canvas id="game-canvas"></canvas>
|
||||||
<script src="bundle.js"></script>
|
<script src="bundle.js"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
91
src/main.ts
91
src/main.ts
|
@ -1,8 +1,89 @@
|
||||||
import { sayHello } from './greet';
|
import GameLoop from "./Loop/GameLoop";
|
||||||
|
import Scene from "./GameState/Scene";
|
||||||
|
import Player from "./Nodes/Player";
|
||||||
|
import UIElement from "./Nodes/UIElement";
|
||||||
|
import ColoredCircle from "./Nodes/ColoredCircle";
|
||||||
|
import Color from "./Utils/Color";
|
||||||
|
|
||||||
function showHello(divName: string, name: string) {
|
function main(){
|
||||||
const elt = document.getElementById(divName);
|
// Create the game object
|
||||||
elt.innerText = sayHello(name);
|
let game = new GameLoop();
|
||||||
|
|
||||||
|
let mainScene = new Scene();
|
||||||
|
let pauseMenu = new Scene();
|
||||||
|
|
||||||
|
// Initialize GameObjects
|
||||||
|
let player = new Player();
|
||||||
|
|
||||||
|
let recordButton = new UIElement();
|
||||||
|
recordButton.setSize(100, 50);
|
||||||
|
recordButton.setText("Record");
|
||||||
|
recordButton.setBackgroundColor(new Color(200, 100, 0, 0.3));
|
||||||
|
recordButton.setPosition(400, 30);
|
||||||
|
recordButton.onPressSignal = "record_button_press";
|
||||||
|
|
||||||
|
let stopButton = new UIElement();
|
||||||
|
stopButton.setSize(100, 50);
|
||||||
|
stopButton.setText("Stop");
|
||||||
|
stopButton.setBackgroundColor(new Color(200, 0, 0, 0.3));
|
||||||
|
stopButton.setPosition(550, 30);
|
||||||
|
stopButton.onPressSignal = "stop_button_press";
|
||||||
|
|
||||||
|
let playButton = new UIElement();
|
||||||
|
playButton.setSize(100, 50);
|
||||||
|
playButton.setText("Play");
|
||||||
|
playButton.setBackgroundColor(new Color(0, 200, 0, 0.3));
|
||||||
|
playButton.setPosition(700, 30);
|
||||||
|
playButton.onPressSignal = "play_button_press";
|
||||||
|
|
||||||
|
let cycleFramerateButton = new UIElement();
|
||||||
|
cycleFramerateButton.setSize(150, 50);
|
||||||
|
cycleFramerateButton.setText("Cycle FPS");
|
||||||
|
cycleFramerateButton.setBackgroundColor(new Color(200, 0, 200, 0.3));
|
||||||
|
cycleFramerateButton.setPosition(5, 400);
|
||||||
|
let i = 0;
|
||||||
|
let fps = [15, 30, 60];
|
||||||
|
cycleFramerateButton.onPress = () => {
|
||||||
|
game.setMaxFPS(fps[i]);
|
||||||
|
i = (i + 1) % 3;
|
||||||
|
}
|
||||||
|
|
||||||
|
let pauseButton = new UIElement();
|
||||||
|
pauseButton.setSize(100, 50);
|
||||||
|
pauseButton.setText("Pause");
|
||||||
|
pauseButton.setBackgroundColor(new Color(200, 0, 200, 1));
|
||||||
|
pauseButton.setPosition(700, 400);
|
||||||
|
pauseButton.onPress = () => {
|
||||||
|
game.getGameState().addScene(pauseMenu);
|
||||||
|
}
|
||||||
|
|
||||||
|
let modalBackground = new UIElement();
|
||||||
|
modalBackground.setSize(400, 200);
|
||||||
|
modalBackground.setBackgroundColor(new Color(0, 0, 0, 0.4));
|
||||||
|
modalBackground.setPosition(200, 100);
|
||||||
|
|
||||||
|
let resumeButton = new UIElement();
|
||||||
|
resumeButton.setSize(100, 50);
|
||||||
|
resumeButton.setText("Resume");
|
||||||
|
resumeButton.setBackgroundColor(new Color(200, 0, 200, 1));
|
||||||
|
resumeButton.setPosition(400, 200);
|
||||||
|
resumeButton.onPress = () => {
|
||||||
|
game.getGameState().removeScene();
|
||||||
|
}
|
||||||
|
|
||||||
|
let lotsOfCircs = [];
|
||||||
|
for(let i = 0; i < 10; i++){
|
||||||
|
lotsOfCircs.push(new ColoredCircle());
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
mainScene.add([...lotsOfCircs, player, recordButton, stopButton, playButton, cycleFramerateButton, pauseButton]);
|
||||||
|
mainScene.getViewport().follow(player);
|
||||||
|
pauseMenu.add([modalBackground, resumeButton]);
|
||||||
|
|
||||||
|
game.getGameState().changeScene(mainScene);
|
||||||
|
|
||||||
|
game.start();
|
||||||
}
|
}
|
||||||
|
|
||||||
showHello('greeting', 'TypeScript');
|
main();
|
|
@ -1,7 +1,40 @@
|
||||||
{
|
{
|
||||||
"files": [
|
"files": [
|
||||||
"src/main.ts",
|
"src/main.ts",
|
||||||
"src/greet.ts"
|
|
||||||
|
"src/DataTypes/Collection.ts",
|
||||||
|
"src/DataTypes/Map.ts",
|
||||||
|
"src/DataTypes/Queue.ts",
|
||||||
|
"src/DataTypes/Stack.ts",
|
||||||
|
"src/DataTypes/Vec2.ts",
|
||||||
|
"src/DataTypes/Vec4.ts",
|
||||||
|
|
||||||
|
"src/Events/EventQueue.ts",
|
||||||
|
"src/Events/GameEvent.ts",
|
||||||
|
"src/Events/Receiver.ts",
|
||||||
|
|
||||||
|
"src/GameState/GameState.ts",
|
||||||
|
"src/GameState/Scene.ts",
|
||||||
|
|
||||||
|
"src/Input/InputHandler.ts",
|
||||||
|
"src/Input/InputReceiver.ts",
|
||||||
|
|
||||||
|
"src/Loop/GameLoop.ts",
|
||||||
|
|
||||||
|
"src/Nodes/ColoredCircle.ts",
|
||||||
|
"src/Nodes/GameNode.ts",
|
||||||
|
"src/Nodes/Player.ts",
|
||||||
|
"src/Nodes/UIElement.ts",
|
||||||
|
|
||||||
|
"src/Playback/Recorder.ts",
|
||||||
|
|
||||||
|
"src/SceneGraph/SceneGraph.ts",
|
||||||
|
"src/SceneGraph/SceneGraphArray.ts",
|
||||||
|
"src/SceneGraph/Viewport.ts",
|
||||||
|
|
||||||
|
"src/Utils/Color.ts",
|
||||||
|
"src/Utils/MathUtils.ts",
|
||||||
|
"src/Utils/RandUtils.ts"
|
||||||
],
|
],
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"noImplicitAny": true,
|
"noImplicitAny": true,
|
||||||
|
|
Loading…
Reference in New Issue
Block a user