transferred game engine code to typescript

This commit is contained in:
Joe Weaver 2020-08-07 12:21:15 -04:00
parent e5d0678b5d
commit 40a05fdbdb
29 changed files with 1460 additions and 17 deletions

View File

@ -0,0 +1,3 @@
export default interface Collection{
forEach(func: Function): void;
}

33
src/DataTypes/Map.ts Normal file
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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();
}
}

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

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

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

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

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

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

View File

@ -1,3 +0,0 @@
export function sayHello(name: string){
return `Hello from ${name}`;
}

View File

@ -5,7 +5,7 @@
<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>

View File

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

View File

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