diff --git a/.gitignore b/.gitignore index c1721ba..07b29ed 100644 --- a/.gitignore +++ b/.gitignore @@ -7,6 +7,12 @@ dist/* # Include the demo_assets folder !dist/demo_assets/ +# Include the built-in asset folder +!dist/builtin/ + +# Include the hw1 assets +!dist/hw1_assets/ + ### IF YOU ARE MAKING A PROJECT, YOU MAY WANT TO UNCOMMENT THIS LINE ### # !dist/assets/ diff --git a/dist/builtin/shaders/point.fshader b/dist/builtin/shaders/point.fshader new file mode 100644 index 0000000..08dd687 --- /dev/null +++ b/dist/builtin/shaders/point.fshader @@ -0,0 +1,7 @@ +precision mediump float; + +uniform vec4 u_Color; + +void main(){ + gl_FragColor = u_Color; +} \ No newline at end of file diff --git a/dist/builtin/shaders/point.vshader b/dist/builtin/shaders/point.vshader new file mode 100644 index 0000000..bd147d4 --- /dev/null +++ b/dist/builtin/shaders/point.vshader @@ -0,0 +1,8 @@ +attribute vec4 a_Position; + +uniform float u_PointSize; + +void main(){ + gl_Position = a_Position; + gl_PointSize = u_PointSize; +} \ No newline at end of file diff --git a/dist/builtin/shaders/rect.fshader b/dist/builtin/shaders/rect.fshader new file mode 100644 index 0000000..08dd687 --- /dev/null +++ b/dist/builtin/shaders/rect.fshader @@ -0,0 +1,7 @@ +precision mediump float; + +uniform vec4 u_Color; + +void main(){ + gl_FragColor = u_Color; +} \ No newline at end of file diff --git a/dist/builtin/shaders/rect.vshader b/dist/builtin/shaders/rect.vshader new file mode 100644 index 0000000..24c7378 --- /dev/null +++ b/dist/builtin/shaders/rect.vshader @@ -0,0 +1,7 @@ +attribute vec4 a_Position; + +uniform mat4 u_Transform; + +void main(){ + gl_Position = u_Transform * a_Position; +} \ No newline at end of file diff --git a/dist/builtin/shaders/sprite.fshader b/dist/builtin/shaders/sprite.fshader new file mode 100644 index 0000000..a4fb440 --- /dev/null +++ b/dist/builtin/shaders/sprite.fshader @@ -0,0 +1,9 @@ +precision mediump float; + +uniform sampler2D u_Sampler; + +varying vec2 v_TexCoord; + +void main(){ + gl_FragColor = texture2D(u_Sampler, v_TexCoord); +} \ No newline at end of file diff --git a/dist/builtin/shaders/sprite.vshader b/dist/builtin/shaders/sprite.vshader new file mode 100644 index 0000000..f04fc3c --- /dev/null +++ b/dist/builtin/shaders/sprite.vshader @@ -0,0 +1,13 @@ +attribute vec4 a_Position; +attribute vec2 a_TexCoord; + +uniform mat4 u_Transform; +uniform vec2 u_texShift; +uniform vec2 u_texScale; + +varying vec2 v_TexCoord; + +void main(){ + gl_Position = u_Transform * a_Position; + v_TexCoord = a_TexCoord*u_texScale + u_texShift; +} \ No newline at end of file diff --git a/dist/hw1_assets/shaders/gradient_circle.fshader b/dist/hw1_assets/shaders/gradient_circle.fshader new file mode 100644 index 0000000..3cd2e62 --- /dev/null +++ b/dist/hw1_assets/shaders/gradient_circle.fshader @@ -0,0 +1,24 @@ +precision mediump float; + +uniform vec4 u_Color; + +varying vec4 v_Position; + +void main(){ + // Default alpha is 0 + float alpha = 0.0; + + // Radius is 0.5, since the diameter of our quad is 1 + float radius = 0.5; + + // Get the distance squared of from (0, 0) + float dist_sq = v_Position.x*v_Position.x + v_Position.y*v_Position.y; + + if(dist_sq < radius*radius){ + // Multiply by 4, since distance squared is at most 0.25 + alpha = 4.0*dist_sq; + } + + // Use the alpha value in our color + gl_FragColor = vec4(u_Color.rgb, alpha); +} \ No newline at end of file diff --git a/dist/hw1_assets/shaders/gradient_circle.vshader b/dist/hw1_assets/shaders/gradient_circle.vshader new file mode 100644 index 0000000..9cb98c8 --- /dev/null +++ b/dist/hw1_assets/shaders/gradient_circle.vshader @@ -0,0 +1,11 @@ +attribute vec4 a_Position; + +uniform mat4 u_Transform; + +varying vec4 v_Position; + +void main(){ + gl_Position = u_Transform * a_Position; + + v_Position = a_Position; +} \ No newline at end of file diff --git a/dist/hw1_assets/spritesheets/player_spaceship.json b/dist/hw1_assets/spritesheets/player_spaceship.json new file mode 100644 index 0000000..9ae8470 --- /dev/null +++ b/dist/hw1_assets/spritesheets/player_spaceship.json @@ -0,0 +1,145 @@ +{ + "name": "player_spaceship", + "spriteSheetImage": "player_spaceship.png", + "spriteWidth": 256, + "spriteHeight": 256, + "leftBuffer": 0, + "rightBuffer": 0, + "topBuffer": 0, + "bottomBuffer": 0, + "columns": 5, + "rows": 5, + "animations": [ + { + "name": "idle", + "repeat": true, + "frames": [ + { + "index": 0, + "duration": 10 + }, + { + "index": 1, + "duration": 10 + }, + { + "index": 2, + "duration": 10 + } + ] + }, + { + "name": "boost", + "repeat": true, + "frames": [ + { + "index": 3, + "duration": 10 + }, + { + "index": 4, + "duration": 10 + }, + { + "index": 5, + "duration": 10 + } + ] + }, + { + "name": "shield", + "repeat": false, + "frames": [ + { + "index": 6, + "duration": 10 + }, + { + "index": 7, + "duration": 10 + }, + { + "index": 8, + "duration": 10 + }, + { + "index": 9, + "duration": 10 + }, + { + "index": 10, + "duration": 10 + }, + { + "index": 11, + "duration": 10 + }, + { + "index": 12, + "duration": 10 + } + ] + }, + { + "name": "explode", + "repeat": false, + "frames": [ + { + "index": 13, + "duration": 10 + }, + { + "index": 14, + "duration": 10 + }, + { + "index": 15, + "duration": 10 + }, + { + "index": 16, + "duration": 10 + }, + { + "index": 17, + "duration": 10 + }, + { + "index": 18, + "duration": 10 + }, + { + "index": 19, + "duration": 10 + }, + { + "index": 20, + "duration": 10 + }, + { + "index": 21, + "duration": 10 + }, + { + "index": 22, + "duration": 10 + }, + { + "index": 23, + "duration": 10 + } + ] + }, + { + "name": "explode", + "repeat": false, + "onEnd": "dead", + "frames": [ + { + "index": 24, + "duration": 1 + } + ] + } + ] +} \ No newline at end of file diff --git a/dist/hw1_assets/spritesheets/player_spaceship.png b/dist/hw1_assets/spritesheets/player_spaceship.png new file mode 100644 index 0000000..4b0f482 Binary files /dev/null and b/dist/hw1_assets/spritesheets/player_spaceship.png differ diff --git a/src/WebGLScene.ts b/src/WebGLScene.ts new file mode 100644 index 0000000..d95cb02 --- /dev/null +++ b/src/WebGLScene.ts @@ -0,0 +1,48 @@ +import Vec2 from "./Wolfie2D/DataTypes/Vec2"; +import { GraphicType } from "./Wolfie2D/Nodes/Graphics/GraphicTypes"; +import Point from "./Wolfie2D/Nodes/Graphics/Point"; +import Rect from "./Wolfie2D/Nodes/Graphics/Rect"; +import AnimatedSprite from "./Wolfie2D/Nodes/Sprites/AnimatedSprite"; +import Sprite from "./Wolfie2D/Nodes/Sprites/Sprite"; +import Scene from "./Wolfie2D/Scene/Scene"; +import Color from "./Wolfie2D/Utils/Color"; + +export default class WebGLScene extends Scene { + + private point: Point; + private rect: Rect; + private player: AnimatedSprite; + private t: number = 0; + + loadScene() { + this.load.spritesheet("player", "hw1_assets/spritesheets/player_spaceship.json"); + } + + startScene() { + this.addLayer("primary"); + + this.point = this.add.graphic(GraphicType.POINT, "primary", {position: new Vec2(100, 100), size: new Vec2(10, 10)}) + this.point.color = Color.CYAN; + console.log(this.point.color.toStringRGBA()); + + this.rect = this.add.graphic(GraphicType.RECT, "primary", {position: new Vec2(300, 100), size: new Vec2(100, 50)}); + this.rect.color = Color.ORANGE; + + this.player = this.add.animatedSprite("player", "primary"); + this.player.position.set(800, 500); + this.player.scale.set(0.5, 0.5); + this.player.animation.play("idle"); + } + + updateScene(deltaT: number) { + this.t += deltaT; + + let s = Math.sin(this.t); + let c = Math.cos(this.t); + + this.point.position.x = 100 + 100*c; + this.point.position.y = 100 + 100*s; + + this.rect.rotation = this.t; + } +} \ No newline at end of file diff --git a/src/Wolfie2D/DataTypes/Mat4x4.ts b/src/Wolfie2D/DataTypes/Mat4x4.ts new file mode 100644 index 0000000..c0765bb --- /dev/null +++ b/src/Wolfie2D/DataTypes/Mat4x4.ts @@ -0,0 +1,167 @@ +import Vec2 from "./Vec2"; + +/** A 4x4 matrix0 */ +export default class Mat4x4 { + private mat: Float32Array; + + constructor(){ + this.mat = new Float32Array([ + 0, 0, 0, 0, + 0, 0, 0, 0, + 0, 0, 0, 0, + 0, 0, 0, 0 + ]); + } + + // Static members + static get IDENTITY(): Mat4x4 { + return new Mat4x4().identity(); + } + + static get ZERO(): Mat4x4 { + return new Mat4x4().zero(); + } + + // Accessors + set _00(x: number) { + this.mat[0] = x; + } + + set(col: number, row: number, value: number): Mat4x4 { + if(col < 0 || col > 3 || row < 0 || row > 3){ + throw `Error - index (${col}, ${row}) is out of bounds for Mat4x4` + } + this.mat[row*4 + col] = value; + + return this; + } + + get(col: number, row: number): number { + return this.mat[row*4 + col]; + } + + setAll(...items: Array): Mat4x4 { + this.mat.set(items); + return this; + } + + identity(): Mat4x4 { + return this.setAll( + 1, 0, 0, 0, + 0, 1, 0, 0, + 0, 0, 1, 0, + 0, 0, 0, 1 + ) + } + + zero(): Mat4x4 { + return this.setAll( + 0, 0, 0, 0, + 0, 0, 0, 0, + 0, 0, 0, 0, + 0, 0, 0, 0 + ); + } + + /** + * Makes this Mat4x4 a rotation matrix of the specified number of radians ccw + * @param zRadians The number of radians to rotate + * @returns this Mat4x4 + */ + rotate(zRadians: number): Mat4x4 { + return this.setAll( + Math.cos(zRadians), -Math.sin(zRadians), 0, 0, + Math.sin(zRadians), Math.cos(zRadians), 0, 0, + 0, 0, 1, 0, + 0, 0, 0, 1 + ); + } + + /** + * Turns this Mat4x4 into a translation matrix of the specified translation + * @param translation The translation in x and y + * @returns this Mat4x4 + */ + translate(translation: Vec2 | Float32Array): Mat4x4 { + // If translation is a vec, get its array + if(translation instanceof Vec2){ + translation = translation.toArray(); + } + + return this.setAll( + 1, 0, 0, translation[0], + 0, 1, 0, translation[1], + 0, 0, 1, 0, + 0, 0, 0, 1 + ); + } + + scale(scale: Vec2 | Float32Array | number): Mat4x4 { + // Make sure scale is a float32Array + if(scale instanceof Vec2){ + scale = scale.toArray(); + } else if(!(scale instanceof Float32Array)){ + scale = new Float32Array([scale, scale]); + } + + return this.setAll( + scale[0], 0, 0, 0, + 0, scale[1], 0, 0, + 0, 0, 1, 0, + 0, 0, 0, 1 + ); + } + + /** + * Returns a new Mat4x4 that represents the right side multiplication THIS x OTHER + * @param other The other Mat4x4 to multiply by + * @returns a new Mat4x4 containing the product of these two Mat4x4s + */ + mult(other: Mat4x4, out?: Mat4x4): Mat4x4 { + let temp = new Float32Array(16); + + for(let i = 0; i < 4; i++){ + for(let j = 0; j < 4; j++){ + let value = 0; + for(let k = 0; k < 4; k++){ + value += this.get(k, i) * other.get(j, k); + } + temp[j*4 + i] = value; + } + } + + if(out !== undefined){ + return out.setAll(...temp); + } else { + return new Mat4x4().setAll(...temp); + } + } + + /** + * Multiplies all given matricies in order. e.g. MULT(A, B, C) -> A*B*C + * @param mats A list of Mat4x4s to multiply in order + * @returns A new Mat4x4 holding the result of the operation + */ + static MULT(...mats: Array): Mat4x4 { + // Create a new array + let temp = Mat4x4.IDENTITY; + + // Multiply by every array in order, in place + for(let i = 0; i < mats.length; i++){ + temp.mult(mats[i], temp); + } + + return temp; + } + + toArray(): Float32Array { + return this.mat; + } + + toString(): string { + return `|${this.mat[0].toFixed(2)}, ${this.mat[1].toFixed(2)}, ${this.mat[2].toFixed(2)}, ${this.mat[3].toFixed(2)}|\n` + + `|${this.mat[4].toFixed(2)}, ${this.mat[5].toFixed(2)}, ${this.mat[6].toFixed(2)}, ${this.mat[7].toFixed(2)}|\n` + + `|${this.mat[8].toFixed(2)}, ${this.mat[9].toFixed(2)}, ${this.mat[10].toFixed(2)}, ${this.mat[11].toFixed(2)}|\n` + + `|${this.mat[12].toFixed(2)}, ${this.mat[13].toFixed(2)}, ${this.mat[14].toFixed(2)}, ${this.mat[15].toFixed(2)}|`; + } +} \ No newline at end of file diff --git a/src/Wolfie2D/DataTypes/Rendering/WebGLGameTexture.ts b/src/Wolfie2D/DataTypes/Rendering/WebGLGameTexture.ts new file mode 100644 index 0000000..c111c66 --- /dev/null +++ b/src/Wolfie2D/DataTypes/Rendering/WebGLGameTexture.ts @@ -0,0 +1,5 @@ +export default class WebGLGameTexture { + webGLTextureId: number; + webGLTexture: WebGLTexture; + imageKey: string; +} \ No newline at end of file diff --git a/src/Wolfie2D/DataTypes/Rendering/WebGLProgramType.ts b/src/Wolfie2D/DataTypes/Rendering/WebGLProgramType.ts new file mode 100644 index 0000000..6452dfa --- /dev/null +++ b/src/Wolfie2D/DataTypes/Rendering/WebGLProgramType.ts @@ -0,0 +1,29 @@ +/** A container for info about a webGL shader program */ +export default class WebGLProgramType { + /** A webGL program */ + program: WebGLProgram; + + /** A vertex shader */ + vertexShader: WebGLShader; + + /** A fragment shader */ + fragmentShader: WebGLShader; + + /** + * Deletes this shader program + */ + delete(gl: WebGLRenderingContext): void { + // Clean up all aspects of this program + if(this.program){ + gl.deleteProgram(this.program); + } + + if(this.vertexShader){ + gl.deleteShader(this.vertexShader); + } + + if(this.fragmentShader){ + gl.deleteShader(this.fragmentShader); + } + } +} \ No newline at end of file diff --git a/src/Wolfie2D/DataTypes/Vec2.ts b/src/Wolfie2D/DataTypes/Vec2.ts index b618781..10783a9 100644 --- a/src/Wolfie2D/DataTypes/Vec2.ts +++ b/src/Wolfie2D/DataTypes/Vec2.ts @@ -383,6 +383,10 @@ export default class Vec2 { this.onChange = f; } + toArray(): Float32Array { + return this.vec; + } + /** * Performs linear interpolation between two vectors * @param a The first vector diff --git a/src/Wolfie2D/Loop/Game.ts b/src/Wolfie2D/Loop/Game.ts index b5bc04b..47b174c 100644 --- a/src/Wolfie2D/Loop/Game.ts +++ b/src/Wolfie2D/Loop/Game.ts @@ -16,6 +16,8 @@ import GameLoop from "./GameLoop"; import FixedUpdateGameLoop from "./FixedUpdateGameLoop"; import EnvironmentInitializer from "./EnvironmentInitializer"; import Vec2 from "../DataTypes/Vec2"; +import Registry from "../Registry/Registry"; +import WebGLRenderer from "../Rendering/WebGLRenderer"; /** * The main loop of the game engine. @@ -36,7 +38,7 @@ export default class Game { readonly WIDTH: number; readonly HEIGHT: number; private viewport: Viewport; - private ctx: CanvasRenderingContext2D; + private ctx: CanvasRenderingContext2D | WebGLRenderingContext; private clearColor: Color; // All of the necessary subsystems that need to run here @@ -73,8 +75,12 @@ export default class Game { this.WIDTH = this.gameOptions.canvasSize.x; this.HEIGHT = this.gameOptions.canvasSize.y; - // For now, just hard code a canvas renderer. We can do this with options later - this.renderingManager = new CanvasRenderer(); + // This step MUST happen before the resource manager does anything + if(this.gameOptions.useWebGL){ + this.renderingManager = new WebGLRenderer(); + } else { + this.renderingManager = new CanvasRenderer(); + } this.initializeGameWindow(); this.ctx = this.renderingManager.initializeCanvas(this.GAME_CANVAS, this.WIDTH, this.HEIGHT); this.clearColor = new Color(this.gameOptions.clearColor.r, this.gameOptions.clearColor.g, this.gameOptions.clearColor.b); @@ -131,8 +137,14 @@ export default class Game { // Set the render function of the loop this.loop.doRender = () => this.render(); - // Start the loop - this.loop.start(); + // Preload registry items + Registry.preload(); + + // Load the items with the resource manager + this.resourceManager.loadResourcesFromQueue(() => { + // When we're dont loading, start the loop + this.loop.start(); + }); } /** @@ -164,12 +176,17 @@ export default class Game { */ render(): void { // Clear the canvases - this.ctx.clearRect(0, 0, this.WIDTH, this.HEIGHT); Debug.clearCanvas(); - // Game Canvas - this.ctx.fillStyle = this.clearColor.toString(); - this.ctx.fillRect(0, 0, this.WIDTH, this.HEIGHT); + if(this.gameOptions.useWebGL){ + (this.ctx).clearColor(this.clearColor.r, this.clearColor.g, this.clearColor.b, this.clearColor.a); + (this.ctx).clear((this.ctx).COLOR_BUFFER_BIT | (this.ctx).DEPTH_BUFFER_BIT); + } else { + (this.ctx).clearRect(0, 0, this.WIDTH, this.HEIGHT); + (this.ctx).fillStyle = this.clearColor.toString(); + (this.ctx).fillRect(0, 0, this.WIDTH, this.HEIGHT); + } + this.sceneManager.render(); // Debug render diff --git a/src/Wolfie2D/Loop/GameOptions.ts b/src/Wolfie2D/Loop/GameOptions.ts index 50bd213..a9112fb 100644 --- a/src/Wolfie2D/Loop/GameOptions.ts +++ b/src/Wolfie2D/Loop/GameOptions.ts @@ -20,6 +20,9 @@ export default class GameOptions { /* Whether or not the stats rendering should occur */ showStats: boolean; + /* Whether or not to use webGL */ + useWebGL: boolean; + /** * Parses the data in the raw options object * @param options The game options as a Record @@ -34,6 +37,7 @@ export default class GameOptions { gOpt.inputs = options.inputs ? options.inputs : []; gOpt.showDebug = !!options.showDebug; gOpt.showStats = !!options.showStats; + gOpt.useWebGL = !!options.useWebGL; return gOpt; } diff --git a/src/Wolfie2D/Nodes/CanvasNode.ts b/src/Wolfie2D/Nodes/CanvasNode.ts index b4d4f30..59eb0a3 100644 --- a/src/Wolfie2D/Nodes/CanvasNode.ts +++ b/src/Wolfie2D/Nodes/CanvasNode.ts @@ -12,6 +12,8 @@ export default abstract class CanvasNode extends GameNode implements Region { private _size: Vec2; private _scale: Vec2; private _boundary: AABB; + private _hasCustomShader: boolean; + private _customShaderKey: string; /** A flag for whether or not the CanvasNode is visible */ visible: boolean = true; @@ -24,6 +26,8 @@ export default abstract class CanvasNode extends GameNode implements Region { this._scale.setOnChange(() => this.scaleChanged()); this._boundary = new AABB(); this.updateBoundary(); + + this._hasCustomShader = false; } get size(): Vec2 { @@ -56,6 +60,14 @@ export default abstract class CanvasNode extends GameNode implements Region { this.scale.y = value; } + get hasCustomShader(): boolean { + return this._hasCustomShader; + } + + get customShaderKey(): string { + return this._customShaderKey; + } + // @override protected positionChanged(): void { super.positionChanged(); @@ -89,6 +101,15 @@ export default abstract class CanvasNode extends GameNode implements Region { return this.boundary.halfSize.clone().scaled(zoom, zoom); } + /** + * Adds a custom shader to this CanvasNode + * @param key The registry key of the ShaderType + */ + useCustomShader(key: string): void { + this._hasCustomShader = true; + this._customShaderKey = key; + } + /** * Returns true if the point (x, y) is inside of this canvas object * @param x The x position of the point diff --git a/src/Wolfie2D/Nodes/GameNode.ts b/src/Wolfie2D/Nodes/GameNode.ts index 51854b7..260a687 100644 --- a/src/Wolfie2D/Nodes/GameNode.ts +++ b/src/Wolfie2D/Nodes/GameNode.ts @@ -176,6 +176,7 @@ export default abstract class GameNode implements Positioned, Unique, Updateable // Set the collision shape if provided, or simply use the the region if there is one. if(collisionShape){ this.collisionShape = collisionShape; + this.collisionShape.center = this.position; } else if (isRegion(this)) { // If the gamenode has a region and no other is specified, use that this.collisionShape = (this).boundary.clone(); diff --git a/src/Wolfie2D/Nodes/Sprites/AnimatedSprite.ts b/src/Wolfie2D/Nodes/Sprites/AnimatedSprite.ts index ae4ffef..bdd909f 100644 --- a/src/Wolfie2D/Nodes/Sprites/AnimatedSprite.ts +++ b/src/Wolfie2D/Nodes/Sprites/AnimatedSprite.ts @@ -8,9 +8,17 @@ export default class AnimatedSprite extends Sprite { /** The number of columns in this sprite sheet */ protected numCols: number; + get cols(): number { + return this.numCols; + } + /** The number of rows in this sprite sheet */ protected numRows: number; + get rows(): number { + return this.numRows; + } + /** The animationManager for this sprite */ animation: AnimationManager; diff --git a/src/Wolfie2D/Registry/Registries/ShaderRegistry.ts b/src/Wolfie2D/Registry/Registries/ShaderRegistry.ts new file mode 100644 index 0000000..33d4636 --- /dev/null +++ b/src/Wolfie2D/Registry/Registries/ShaderRegistry.ts @@ -0,0 +1,98 @@ +import Map from "../../DataTypes/Map"; +import ShaderType from "../../Rendering/WebGLRendering/ShaderType"; +import PointShaderType from "../../Rendering/WebGLRendering/ShaderTypes/PointShaderType"; +import RectShaderType from "../../Rendering/WebGLRendering/ShaderTypes/RectShaderType"; +import SpriteShaderType from "../../Rendering/WebGLRendering/ShaderTypes/SpriteShaderType"; +import ResourceManager from "../../ResourceManager/ResourceManager"; + +/** + * A registry that handles shaders + */ +export default class ShaderRegistry extends Map { + + // Shader names + public static POINT_SHADER = "point"; + public static RECT_SHADER = "rect"; + public static SPRITE_SHADER = "sprite"; + + private registryItems: Array = new Array(); + + /** + * Preloads all built-in shaders + */ + public preload(){ + console.log("Preloading"); + + // Get the resourceManager and queue all built-in shaders for preloading + const rm = ResourceManager.getInstance(); + + // Queue a load for the point shader + this.registerAndPreloadItem(ShaderRegistry.POINT_SHADER, PointShaderType, "builtin/shaders/point.vshader", "builtin/shaders/point.fshader"); + + // Queue a load for the rect shader + this.registerAndPreloadItem(ShaderRegistry.RECT_SHADER, RectShaderType, "builtin/shaders/rect.vshader", "builtin/shaders/rect.fshader"); + + // Queue a load for the sprite shader + this.registerAndPreloadItem(ShaderRegistry.SPRITE_SHADER, SpriteShaderType, "builtin/shaders/sprite.vshader", "builtin/shaders/sprite.fshader"); + + // Queue a load for any preloaded items + for(let item of this.registryItems){ + const shader = new item.constr(item.key); + shader.initBufferObject(); + this.add(item.key, shader); + + console.log("Added", item.key); + + // Load if desired + if(item.preload !== undefined){ + console.log("Preloading", item.key); + rm.shader(item.key, item.preload.vshaderLocation, item.preload.fshaderLocation); + } + } + } + + /** + * Registers a shader in the registry and loads it before the game begins + * @param key The key you wish to assign to the shader + * @param constr The constructor of the ShaderType + * @param vshaderLocation The location of the vertex shader + * @param fshaderLocation the location of the fragment shader + */ + public registerAndPreloadItem(key: string, constr: new (programKey: string) => ShaderType, vshaderLocation: string, fshaderLocation: string): void { + let shaderPreload = new ShaderPreload(); + shaderPreload.vshaderLocation = vshaderLocation; + shaderPreload.fshaderLocation = fshaderLocation; + + let registryItem = new ShaderRegistryItem(); + registryItem.key = key; + registryItem.constr = constr; + registryItem.preload = shaderPreload; + + this.registryItems.push(registryItem); + } + + /** + * Registers a shader in the registry. NOTE: If you use this, you MUST load the shader before use. + * If you wish to preload the shader, use registerAndPreloadItem() + * @param key The key you wish to assign to the shader + * @param constr The constructor of the ShaderType + */ + public registerItem(key: string, constr: new (programKey: string) => ShaderType): void { + let registryItem = new ShaderRegistryItem(); + registryItem.key = key; + registryItem.constr = constr; + + this.registryItems.push(registryItem); + } +} + +class ShaderRegistryItem { + key: string; + constr: new (programKey: string) => ShaderType; + preload: ShaderPreload; +} + +class ShaderPreload { + vshaderLocation: string; + fshaderLocation: string; +} \ No newline at end of file diff --git a/src/Wolfie2D/Registry/Registry.ts b/src/Wolfie2D/Registry/Registry.ts new file mode 100644 index 0000000..113fb51 --- /dev/null +++ b/src/Wolfie2D/Registry/Registry.ts @@ -0,0 +1,16 @@ +import ShaderRegistry from "./Registries/ShaderRegistry"; + +/** + * The Registry is the system's way of converting classes and types into string + * representations for use elsewhere in the application. + * It allows classes to be accessed without explicitly using constructors in code, + * and for resources to be loaded at Game creation time. + */ +export default class Registry { + + public static shaders = new ShaderRegistry(); + + static preload(){ + this.shaders.preload(); + } +} \ No newline at end of file diff --git a/src/Wolfie2D/Rendering/Animations/AnimationManager.ts b/src/Wolfie2D/Rendering/Animations/AnimationManager.ts index 05dfc6b..18dc2f4 100644 --- a/src/Wolfie2D/Rendering/Animations/AnimationManager.ts +++ b/src/Wolfie2D/Rendering/Animations/AnimationManager.ts @@ -85,6 +85,15 @@ export default class AnimationManager { } } + /** + * Determines whether the specified animation is currently playing + * @param key The key of the animation to check + * @returns true if the specified animation is playing, false otherwise + */ + isPlaying(key: string): boolean { + return this.currentAnimation === key && this.animationState === AnimationState.PLAYING; + } + /** * Retrieves the current animation index and advances the animation frame * @returns The index of the animation frame @@ -147,7 +156,7 @@ export default class AnimationManager { * @param loop Whether or not to loop the animation. False by default * @param onEnd The name of an event to send when this animation naturally stops playing. This only matters if loop is false. */ - playIfNotAlready(animation: string, loop: boolean = false, onEnd?: string): void { + playIfNotAlready(animation: string, loop?: boolean, onEnd?: string): void { if(this.currentAnimation !== animation){ this.play(animation, loop, onEnd); } @@ -159,18 +168,27 @@ export default class AnimationManager { * @param loop Whether or not to loop the animation. False by default * @param onEnd The name of an event to send when this animation naturally stops playing. This only matters if loop is false. */ - play(animation: string, loop: boolean = false, onEnd?: string): void { + play(animation: string, loop?: boolean, onEnd?: string): void { this.currentAnimation = animation; this.currentFrame = 0; this.frameProgress = 0; - this.loop = loop; this.animationState = AnimationState.PLAYING; + + // If loop arg was provided, use that + if(loop !== undefined){ + this.loop = loop; + } else { + // Otherwise, use what the json file specified + this.loop = this.animations.get(animation).repeat; + } + if(onEnd !== undefined){ this.onEndEvent = onEnd; } else { this.onEndEvent = null; } + // Reset pending animation this.pendingAnimation = null; } diff --git a/src/Wolfie2D/Rendering/Animations/AnimationTypes.ts b/src/Wolfie2D/Rendering/Animations/AnimationTypes.ts index e96f7bd..72f0ac7 100644 --- a/src/Wolfie2D/Rendering/Animations/AnimationTypes.ts +++ b/src/Wolfie2D/Rendering/Animations/AnimationTypes.ts @@ -12,6 +12,7 @@ export enum AnimationState { export class AnimationData { name: string; frames: Array<{index: number, duration: number}>; + repeat: boolean = false; } export class TweenData { diff --git a/src/Wolfie2D/Rendering/CanvasRenderer.ts b/src/Wolfie2D/Rendering/CanvasRenderer.ts index caa1cb5..6a72b69 100644 --- a/src/Wolfie2D/Rendering/CanvasRenderer.ts +++ b/src/Wolfie2D/Rendering/CanvasRenderer.ts @@ -131,7 +131,7 @@ export default class CanvasRenderer extends RenderingManager { } this.ctx.setTransform(xScale, 0, 0, yScale, (node.position.x - this.origin.x)*this.zoom, (node.position.y - this.origin.y)*this.zoom); - this.ctx.rotate(node.rotation); + this.ctx.rotate(-node.rotation); let globalAlpha = this.ctx.globalAlpha; this.ctx.globalAlpha = node.alpha; diff --git a/src/Wolfie2D/Rendering/WebGLRenderer.ts b/src/Wolfie2D/Rendering/WebGLRenderer.ts new file mode 100644 index 0000000..1e91d09 --- /dev/null +++ b/src/Wolfie2D/Rendering/WebGLRenderer.ts @@ -0,0 +1,136 @@ +import Graph from "../DataTypes/Graphs/Graph"; +import Map from "../DataTypes/Map"; +import Vec2 from "../DataTypes/Vec2"; +import CanvasNode from "../Nodes/CanvasNode"; +import Graphic from "../Nodes/Graphic"; +import { GraphicType } from "../Nodes/Graphics/GraphicTypes"; +import Point from "../Nodes/Graphics/Point"; +import Rect from "../Nodes/Graphics/Rect"; +import AnimatedSprite from "../Nodes/Sprites/AnimatedSprite"; +import Sprite from "../Nodes/Sprites/Sprite"; +import Tilemap from "../Nodes/Tilemap"; +import UIElement from "../Nodes/UIElement"; +import ShaderRegistry from "../Registry/Registries/ShaderRegistry"; +import Registry from "../Registry/Registry"; +import ResourceManager from "../ResourceManager/ResourceManager"; +import UILayer from "../Scene/Layers/UILayer"; +import RenderingUtils from "../Utils/RenderingUtils"; +import RenderingManager from "./RenderingManager"; +import ShaderType from "./WebGLRendering/ShaderType"; + +export default class WebGLRenderer extends RenderingManager { + + protected origin: Vec2; + protected zoom: number; + protected worldSize: Vec2; + + protected gl: WebGLRenderingContext; + + initializeCanvas(canvas: HTMLCanvasElement, width: number, height: number): WebGLRenderingContext { + canvas.width = width; + canvas.height = height; + + this.worldSize = Vec2.ZERO; + this.worldSize.x = width; + this.worldSize.y = height; + + // Get the WebGL context + this.gl = canvas.getContext("webgl"); + + this.gl.viewport(0, 0, canvas.width, canvas.height); + + this.gl.disable(this.gl.DEPTH_TEST); + this.gl.enable(this.gl.BLEND); + this.gl.blendFunc(this.gl.SRC_ALPHA, this.gl.ONE_MINUS_SRC_ALPHA); + this.gl.enable(this.gl.CULL_FACE); + + // Tell the resource manager we're using WebGL + ResourceManager.getInstance().useWebGL(true, this.gl); + + return this.gl; + } + + render(visibleSet: CanvasNode[], tilemaps: Tilemap[], uiLayers: Map): void { + for(let node of visibleSet){ + this.renderNode(node); + } + } + + protected renderNode(node: CanvasNode): void { + // Calculate the origin of the viewport according to this sprite + this.origin = this.scene.getViewTranslation(node); + + // Get the zoom level of the scene + this.zoom = this.scene.getViewScale(); + + if(node.hasCustomShader){ + // If the node has a custom shader, render using that + this.renderCustom(node); + } else if(node instanceof Graphic){ + this.renderGraphic(node); + } else if(node instanceof Sprite){ + if(node instanceof AnimatedSprite){ + this.renderAnimatedSprite(node); + } else { + this.renderSprite(node); + } + } + } + + protected renderSprite(sprite: Sprite): void { + let shader = Registry.shaders.get(ShaderRegistry.SPRITE_SHADER); + + let options = shader.getOptions(sprite); + options.worldSize = this.worldSize; + options.origin = this.origin; + + shader.render(this.gl, options); + } + + protected renderAnimatedSprite(sprite: AnimatedSprite): void { + let shader = Registry.shaders.get(ShaderRegistry.SPRITE_SHADER); + + let options = shader.getOptions(sprite); + options.worldSize = this.worldSize; + options.origin = this.origin; + + Registry.shaders.get(ShaderRegistry.SPRITE_SHADER).render(this.gl, options); + } + + protected renderGraphic(graphic: Graphic): void { + + if(graphic instanceof Point){ + let shader = Registry.shaders.get(ShaderRegistry.POINT_SHADER); + let options = shader.getOptions(graphic); + options.worldSize = this.worldSize; + options.origin = this.origin; + + shader.render(this.gl, options); + } else if(graphic instanceof Rect) { + let shader = Registry.shaders.get(ShaderRegistry.RECT_SHADER); + let options = shader.getOptions(graphic); + options.worldSize = this.worldSize; + options.origin = this.origin; + + shader.render(this.gl, options); + } + } + + protected renderTilemap(tilemap: Tilemap): void { + throw new Error("Method not implemented."); + } + + protected renderUIElement(uiElement: UIElement): void { + throw new Error("Method not implemented."); + } + + protected renderCustom(node: CanvasNode): void { + let shader = Registry.shaders.get(node.customShaderKey); + let options = shader.getOptions(node); + options.worldSize = this.worldSize; + options.origin = this.origin; + + shader.render(this.gl, options); + } + +} \ No newline at end of file diff --git a/src/Wolfie2D/Rendering/WebGLRendering/ShaderType.ts b/src/Wolfie2D/Rendering/WebGLRendering/ShaderType.ts new file mode 100644 index 0000000..897dc09 --- /dev/null +++ b/src/Wolfie2D/Rendering/WebGLRendering/ShaderType.ts @@ -0,0 +1,44 @@ +import Map from "../../DataTypes/Map"; +import CanvasNode from "../../Nodes/CanvasNode"; +import ResourceManager from "../../ResourceManager/ResourceManager"; + +/** + * A wrapper class for WebGL shaders. + * This class is a singleton, and there is only one for each shader type. + * All objects that use this shader type will refer to and modify this same type. + */ +export default abstract class ShaderType { + /** The name of this shader */ + protected name: string; + + /** The key to the WebGLProgram in the ResourceManager */ + protected programKey: string; + + /** A reference to the resource manager */ + protected resourceManager: ResourceManager; + + constructor(programKey: string){ + this.programKey = programKey; + this.resourceManager = ResourceManager.getInstance(); + } + + /** + * Initializes any buffer objects associated with this shader type. + * @param gl The WebGL rendering context + */ + abstract initBufferObject(): void; + + /** + * Loads any uniforms + * @param gl The WebGL rendering context + * @param options Information about the object we're currently rendering + */ + abstract render(gl: WebGLRenderingContext, options: Record): void; + + /** + * Extracts the options from the CanvasNode and gives them to the render function + * @param node The node to get options from + * @returns An object containing the options that should be passed to the render function + */ + getOptions(node: CanvasNode): Record {return {};} +} \ No newline at end of file diff --git a/src/Wolfie2D/Rendering/WebGLRendering/ShaderTypes/PointShaderType.ts b/src/Wolfie2D/Rendering/WebGLRendering/ShaderTypes/PointShaderType.ts new file mode 100644 index 0000000..e7ae466 --- /dev/null +++ b/src/Wolfie2D/Rendering/WebGLRendering/ShaderTypes/PointShaderType.ts @@ -0,0 +1,61 @@ +import Debug from "../../../Debug/Debug"; +import Point from "../../../Nodes/Graphics/Point"; +import ResourceManager from "../../../ResourceManager/ResourceManager"; +import RenderingUtils from "../../../Utils/RenderingUtils"; +import ShaderType from "../ShaderType"; + +export default class PointShaderType extends ShaderType { + + protected bufferObjectKey: string; + + constructor(programKey: string){ + super(programKey); + } + + initBufferObject(): void { + this.bufferObjectKey = "point"; + this.resourceManager.createBuffer(this.bufferObjectKey); + } + + render(gl: WebGLRenderingContext, options: Record): void { + let position = RenderingUtils.toWebGLCoords(options.position, options.origin, options.worldSize); + let color = RenderingUtils.toWebGLColor(options.color); + + const program = this.resourceManager.getShaderProgram(this.programKey); + const buffer = this.resourceManager.getBuffer(this.bufferObjectKey); + + gl.useProgram(program); + + const vertexData = position; + + const FSIZE = vertexData.BYTES_PER_ELEMENT; + + // Bind the buffer + gl.bindBuffer(gl.ARRAY_BUFFER, buffer); + gl.bufferData(gl.ARRAY_BUFFER, vertexData, gl.STATIC_DRAW); + + // Attributes + const a_Position = gl.getAttribLocation(program, "a_Position"); + gl.vertexAttribPointer(a_Position, 2, gl.FLOAT, false, 2 * FSIZE, 0 * FSIZE); + gl.enableVertexAttribArray(a_Position); + + // Uniforms + const u_Color = gl.getUniformLocation(program, "u_Color"); + gl.uniform4fv(u_Color, color); + + const u_PointSize = gl.getUniformLocation(program, "u_PointSize"); + gl.uniform1f(u_PointSize, options.pointSize); + + gl.drawArrays(gl.POINTS, 0, 1); + } + + getOptions(point: Point): Record { + let options: Record = { + position: point.position, + color: point.color, + pointSize: point.size, + } + + return options; + } +} \ No newline at end of file diff --git a/src/Wolfie2D/Rendering/WebGLRendering/ShaderTypes/QuadShaderType.ts b/src/Wolfie2D/Rendering/WebGLRendering/ShaderTypes/QuadShaderType.ts new file mode 100644 index 0000000..fa524f1 --- /dev/null +++ b/src/Wolfie2D/Rendering/WebGLRendering/ShaderTypes/QuadShaderType.ts @@ -0,0 +1,25 @@ +import Mat4x4 from "../../../DataTypes/Mat4x4"; +import ShaderType from "../ShaderType"; + +/** Represents any WebGL objects that have a quad mesh (i.e. a rectangular game object composed of only two triangles) */ +export default abstract class QuadShaderType extends ShaderType { + /** The key to the buffer object for this shader */ + protected bufferObjectKey: string; + + /** The scale matric */ + protected scale: Mat4x4; + + /** The rotation matrix */ + protected rotation: Mat4x4; + + /** The translation matrix */ + protected translation: Mat4x4; + + constructor(programKey: string){ + super(programKey); + + this.scale = Mat4x4.IDENTITY; + this.rotation = Mat4x4.IDENTITY; + this.translation = Mat4x4.IDENTITY; + } +} \ No newline at end of file diff --git a/src/Wolfie2D/Rendering/WebGLRendering/ShaderTypes/RectShaderType.ts b/src/Wolfie2D/Rendering/WebGLRendering/ShaderTypes/RectShaderType.ts new file mode 100644 index 0000000..e977434 --- /dev/null +++ b/src/Wolfie2D/Rendering/WebGLRendering/ShaderTypes/RectShaderType.ts @@ -0,0 +1,133 @@ +import Mat4x4 from "../../../DataTypes/Mat4x4"; +import Vec2 from "../../../DataTypes/Vec2"; +import Rect from "../../../Nodes/Graphics/Rect"; +import ResourceManager from "../../../ResourceManager/ResourceManager"; +import QuadShaderType from "./QuadShaderType"; + +export default class RectShaderType extends QuadShaderType { + + constructor(programKey: string){ + super(programKey); + this.resourceManager = ResourceManager.getInstance(); + } + + initBufferObject(): void { + this.bufferObjectKey = "rect"; + this.resourceManager.createBuffer(this.bufferObjectKey); + } + + render(gl: WebGLRenderingContext, options: Record): void { + const color = options.color.toWebGL(); + + const program = this.resourceManager.getShaderProgram(this.programKey); + const buffer = this.resourceManager.getBuffer(this.bufferObjectKey); + + gl.useProgram(program); + + const vertexData = this.getVertices(options.size.x, options.size.y); + + const FSIZE = vertexData.BYTES_PER_ELEMENT; + + // Bind the buffer + gl.bindBuffer(gl.ARRAY_BUFFER, buffer); + gl.bufferData(gl.ARRAY_BUFFER, vertexData, gl.STATIC_DRAW); + + // Attributes + const a_Position = gl.getAttribLocation(program, "a_Position"); + gl.vertexAttribPointer(a_Position, 2, gl.FLOAT, false, 2 * FSIZE, 0 * FSIZE); + gl.enableVertexAttribArray(a_Position); + + // Uniforms + const u_Color = gl.getUniformLocation(program, "u_Color"); + gl.uniform4fv(u_Color, color); + + // Get transformation matrix + // We want a square for our rendering space, so get the maximum dimension of our quad + let maxDimension = Math.max(options.size.x, options.size.y); + + // The size of the rendering space will be a square with this maximum dimension + let size = new Vec2(maxDimension, maxDimension).scale(2/options.worldSize.x, 2/options.worldSize.y); + + // Center our translations around (0, 0) + const translateX = (options.position.x - options.origin.x - options.worldSize.x/2)/maxDimension; + const translateY = -(options.position.y - options.origin.y - options.worldSize.y/2)/maxDimension; + + // Create our transformation matrix + this.translation.translate(new Float32Array([translateX, translateY])); + this.scale.scale(size); + this.rotation.rotate(options.rotation); + let transformation = Mat4x4.MULT(this.translation, this.scale, this.rotation); + + // Pass the translation matrix to our shader + const u_Transform = gl.getUniformLocation(program, "u_Transform"); + gl.uniformMatrix4fv(u_Transform, false, transformation.toArray()); + + // Draw the quad + gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4); + } + + + /* + So as it turns out, WebGL has an issue with non-square quads. + It doesn't like when you don't have a 1-1 scale, and rotations are entirely messed up if this is not the case. + To solve this, I used the scale of the LARGEST dimension of the quad to make a square, then adjusted the vertex coordinates inside of that. + A diagram of the solution follows. + + There is a bounding square for the quad with dimensions hxh (in this case, since height is the largest dimension). + The offset in the vertical direction is therefore 0.5, as it is normally. + However, the offset in the horizontal direction is not so straightforward, but isn't conceptually hard. + All we really have to do is a range change from [0, height/2] to [0, 0.5], where our value is t = width/2, and 0 <= t <= height/2. + + So now we have our rect, in a space scaled with respect to the largest dimension. + Rotations work as you would expect, even for long rectangles. + + 0.5 + __ __ __ __ __ __ __ + | |88888888888| | + | |88888888888| | + | |88888888888| | + -0.5|_ _|88888888888|_ _|0.5 + | |88888888888| | + | |88888888888| | + | |88888888888| | + |___|88888888888|___| + -0.5 + + The getVertices function below does as described, and converts the range + */ + /** + * The rendering space always has to be a square, so make sure its square w.r.t to the largest dimension + * @param w The width of the quad in pixels + * @param h The height of the quad in pixels + * @returns An array of the vertices of the quad + */ + getVertices(w: number, h: number): Float32Array { + let x, y; + + if(h > w){ + y = 0.5; + x = w/(2*h); + } else { + x = 0.5; + y = h/(2*w); + } + + return new Float32Array([ + -x, y, + -x, -y, + x, y, + x, -y + ]); + } + + getOptions(rect: Rect): Record { + let options: Record = { + position: rect.position, + color: rect.color, + size: rect.size, + rotation: rect.rotation + } + + return options; + } +} \ No newline at end of file diff --git a/src/Wolfie2D/Rendering/WebGLRendering/ShaderTypes/SpriteShaderType.ts b/src/Wolfie2D/Rendering/WebGLRendering/ShaderTypes/SpriteShaderType.ts new file mode 100644 index 0000000..3753745 --- /dev/null +++ b/src/Wolfie2D/Rendering/WebGLRendering/ShaderTypes/SpriteShaderType.ts @@ -0,0 +1,153 @@ +import Mat4x4 from "../../../DataTypes/Mat4x4"; +import Vec2 from "../../../DataTypes/Vec2"; +import Debug from "../../../Debug/Debug"; +import AnimatedSprite from "../../../Nodes/Sprites/AnimatedSprite"; +import Sprite from "../../../Nodes/Sprites/Sprite"; +import ResourceManager from "../../../ResourceManager/ResourceManager"; +import QuadShaderType from "./QuadShaderType"; + +/** A shader for sprites and animated sprites */ +export default class SpriteShaderType extends QuadShaderType { + constructor(programKey: string){ + super(programKey); + this.resourceManager = ResourceManager.getInstance(); + } + + initBufferObject(): void { + this.bufferObjectKey = "sprite"; + this.resourceManager.createBuffer(this.bufferObjectKey); + } + + render(gl: WebGLRenderingContext, options: Record): void { + const program = this.resourceManager.getShaderProgram(this.programKey); + const buffer = this.resourceManager.getBuffer(this.bufferObjectKey); + const texture = this.resourceManager.getTexture(options.imageKey); + const image = this.resourceManager.getImage(options.imageKey); + + gl.useProgram(program); + + // Enable texture0 + gl.activeTexture(gl.TEXTURE0); + + // Bind our texture to texture 0 + gl.bindTexture(gl.TEXTURE_2D, texture); + + // Set the texture parameters + gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR); + gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE); + gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE); + + // Set the texture image + gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, image); + + const vertexData = this.getVertices(options.size.x, options.size.y, options.scale); + + const FSIZE = vertexData.BYTES_PER_ELEMENT; + + // Bind the buffer + gl.bindBuffer(gl.ARRAY_BUFFER, buffer); + gl.bufferData(gl.ARRAY_BUFFER, vertexData, gl.STATIC_DRAW); + + // Attributes + const a_Position = gl.getAttribLocation(program, "a_Position"); + gl.vertexAttribPointer(a_Position, 2, gl.FLOAT, false, 4 * FSIZE, 0 * FSIZE); + gl.enableVertexAttribArray(a_Position); + + const a_TexCoord = gl.getAttribLocation(program, "a_TexCoord"); + gl.vertexAttribPointer(a_TexCoord, 2, gl.FLOAT, false, 4 * FSIZE, 2*FSIZE); + gl.enableVertexAttribArray(a_TexCoord); + + // Uniforms + // Get transformation matrix + // We want a square for our rendering space, so get the maximum dimension of our quad + let maxDimension = Math.max(options.size.x, options.size.y); + + // The size of the rendering space will be a square with this maximum dimension + let size = new Vec2(maxDimension, maxDimension).scale(2/options.worldSize.x, 2/options.worldSize.y); + + // Center our translations around (0, 0) + const translateX = (options.position.x - options.origin.x - options.worldSize.x/2)/maxDimension; + const translateY = -(options.position.y - options.origin.y - options.worldSize.y/2)/maxDimension; + + // Create our transformation matrix + this.translation.translate(new Float32Array([translateX, translateY])); + this.scale.scale(size); + this.rotation.rotate(options.rotation); + let transformation = Mat4x4.MULT(this.translation, this.scale, this.rotation); + + // Pass the translation matrix to our shader + const u_Transform = gl.getUniformLocation(program, "u_Transform"); + gl.uniformMatrix4fv(u_Transform, false, transformation.toArray()); + + // Set texture unit 0 to the sampler + const u_Sampler = gl.getUniformLocation(program, "u_Sampler"); + gl.uniform1i(u_Sampler, 0); + + // Pass in texShift + const u_texShift = gl.getUniformLocation(program, "u_texShift"); + gl.uniform2fv(u_texShift, options.texShift); + + // Pass in texScale + const u_texScale = gl.getUniformLocation(program, "u_texScale"); + gl.uniform2fv(u_texScale, options.texScale); + + // Draw the quad + gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4 ); + } + + /** + * The rendering space always has to be a square, so make sure its square w.r.t to the largest dimension + * @param w The width of the quad in pixels + * @param h The height of the quad in pixels + * @returns An array of the vertices of the quad + */ + getVertices(w: number, h: number, scale: Float32Array): Float32Array { + let x, y; + + if(h > w){ + y = 0.5; + x = w/(2*h); + } else { + x = 0.5; + y = h/(2*w); + } + + // Scale the rendering space if needed + x *= scale[0]; + y *= scale[1]; + + return new Float32Array([ + -x, y, 0.0, 0.0, + -x, -y, 0.0, 1.0, + x, y, 1.0, 0.0, + x, -y, 1.0, 1.0 + ]); + } + + getOptions(sprite: Sprite): Record { + let texShift; + let texScale; + + if(sprite instanceof AnimatedSprite){ + let animationIndex = sprite.animation.getIndexAndAdvanceAnimation(); + let offset = sprite.getAnimationOffset(animationIndex); + texShift = new Float32Array([offset.x / (sprite.cols * sprite.size.x), offset.y / (sprite.rows * sprite.size.y)]); + texScale = new Float32Array([1/(sprite.cols), 1/(sprite.rows)]); + } else { + texShift = new Float32Array([0, 0]); + texScale = new Float32Array([1, 1]); + } + + let options: Record = { + position: sprite.position, + rotation: sprite.rotation, + size: sprite.size, + scale: sprite.scale.toArray(), + imageKey: sprite.imageId, + texShift, + texScale + } + + return options; + } +} \ No newline at end of file diff --git a/src/Wolfie2D/ResourceManager/ResourceManager.ts b/src/Wolfie2D/ResourceManager/ResourceManager.ts index 3d00d47..6532496 100644 --- a/src/Wolfie2D/ResourceManager/ResourceManager.ts +++ b/src/Wolfie2D/ResourceManager/ResourceManager.ts @@ -4,6 +4,8 @@ import { TiledTilemapData } from "../DataTypes/Tilesets/TiledData"; import StringUtils from "../Utils/StringUtils"; import AudioManager from "../Sound/AudioManager"; import Spritesheet from "../DataTypes/Spritesheet"; +import WebGLProgramType from "../DataTypes/Rendering/WebGLProgramType"; +import PhysicsManager from "../Physics/PhysicsManager"; /** * The resource manager for the game engine. @@ -68,6 +70,21 @@ export default class ResourceManager { /** The total number of "types" of things that need to be loaded (i.e. images and tilemaps) */ private loadonly_typesToLoad: number; + /* ########## INFORMATION SPECIAL TO WEBGL ########## */ + private gl_WebGLActive: boolean; + + private loadonly_gl_ShaderProgramsLoaded: number; + private loadonly_gl_ShaderProgramsToLoad: number; + private loadonly_gl_ShaderLoadingQueue: Queue; + + private gl_DefaultShaderPrograms: Map; + private gl_ShaderPrograms: Map; + + private gl_Textures: Map; + private gl_Buffers: Map; + + private gl: WebGLRenderingContext; + private constructor(){ this.loading = false; this.justLoaded = false; @@ -91,6 +108,16 @@ export default class ResourceManager { this.loadonly_audioToLoad = 0; this.loadonly_audioLoadingQueue = new Queue(); this.audioBuffers = new Map(); + + this.loadonly_gl_ShaderProgramsLoaded = 0; + this.loadonly_gl_ShaderProgramsToLoad = 0; + this.loadonly_gl_ShaderLoadingQueue = new Queue(); + + this.gl_DefaultShaderPrograms = new Map(); + this.gl_ShaderPrograms = new Map(); + + this.gl_Textures = new Map(); + this.gl_Buffers = new Map(); }; /** @@ -105,6 +132,19 @@ export default class ResourceManager { return this.instance; } + /** + * Activates or deactivates the use of WebGL + * @param flag True if WebGL should be used, false otherwise + * @param gl The instance of the graphics context, if applicable + */ + public useWebGL(flag: boolean, gl: WebGLRenderingContext): void { + this.gl_WebGLActive = flag; + + if(this.gl_WebGLActive){ + this.gl = gl; + } + } + /** * Loads an image from file * @param key The key to associate the loaded image with @@ -199,15 +239,26 @@ export default class ResourceManager { console.log("Loaded Images"); this.loadAudioFromQueue(() => { console.log("Loaded Audio"); - // Done loading - this.loading = false; - this.justLoaded = true; - callback(); + + if(this.gl_WebGLActive){ + this.gl_LoadShadersFromQueue(() => { + console.log("Loaded Shaders"); + this.finishLoading(callback); + }); + } else { + this.finishLoading(callback); + } }); }); }); }); + } + private finishLoading(callback: Function): void { + // Done loading + this.loading = false; + this.justLoaded = true; + callback(); } /** @@ -232,6 +283,12 @@ export default class ResourceManager { this.loadonly_audioLoaded = 0; this.loadonly_audioToLoad = 0; this.audioBuffers.clear(); + + // WebGL + // Delete all programs through webGL + this.gl_ShaderPrograms.forEach(key => this.gl_ShaderPrograms.get(key).delete(this.gl)); + this.gl_ShaderPrograms.clear(); + this.gl_Textures.clear(); } /** @@ -385,6 +442,9 @@ export default class ResourceManager { // Add to loaded images this.images.add(key, image); + // If WebGL is active, create a texture + this.createWebGLTexture(key); + // Finish image load this.finishLoadingImage(callbackIfLast); } @@ -464,6 +524,192 @@ export default class ResourceManager { } } + /* ########## WEBGL SPECIFIC FUNCTIONS ########## */ + + public getTexture(key: string): WebGLTexture { + return this.gl_Textures.get(key); + } + + public getShaderProgram(key: string): WebGLProgram { + return this.gl_ShaderPrograms.get(key).program; + } + + public getBuffer(key: string): WebGLBuffer { + return this.gl_Buffers.get(key); + } + + private createWebGLTexture(key:string): void { + if(this.gl_WebGLActive){ + const texture = this.gl.createTexture(); + this.gl_Textures.add(key, texture); + } + } + + public createBuffer(key: string): void { + if(this.gl_WebGLActive){ + let buffer = this.gl.createBuffer(); + + this.gl_Buffers.add(key, buffer); + } + } + + /** + * Enqueues loading of a new shader program + * @param key The key of the shader program + * @param vShaderFilepath + * @param fShaderFilepath + */ + public shader(key: string, vShaderFilepath: string, fShaderFilepath: string): void { + let splitPath = vShaderFilepath.split("."); + let end = splitPath[splitPath.length - 1]; + + if(end !== "vshader"){ + throw `${vShaderFilepath} is not a valid vertex shader - must end in ".vshader`; + } + + splitPath = fShaderFilepath.split("."); + end = splitPath[splitPath.length - 1]; + + if(end !== "fshader"){ + throw `${fShaderFilepath} is not a valid vertex shader - must end in ".fshader`; + } + + let paths = new KeyPath_Shader(); + paths.key = key; + paths.vpath = vShaderFilepath; + paths.fpath = fShaderFilepath; + + this.loadonly_gl_ShaderLoadingQueue.enqueue(paths); + } + + private gl_LoadShadersFromQueue(onFinishLoading: Function): void { + this.loadonly_gl_ShaderProgramsToLoad = this.loadonly_gl_ShaderLoadingQueue.getSize(); + this.loadonly_gl_ShaderProgramsLoaded = 0; + + // If webGL isn'active or there are no items to load, we're finished + if(!this.gl_WebGLActive || this.loadonly_gl_ShaderProgramsToLoad === 0){ + onFinishLoading(); + } + + while(this.loadonly_gl_ShaderLoadingQueue.hasItems()){ + let shader = this.loadonly_gl_ShaderLoadingQueue.dequeue(); + this.gl_LoadShader(shader.key, shader.vpath, shader.fpath, onFinishLoading); + } + } + + private gl_LoadShader(key: string, vpath: string, fpath: string, callbackIfLast: Function): void { + this.loadTextFile(vpath, (vFileText: string) => { + const vShader = vFileText; + + this.loadTextFile(fpath, (fFileText: string) => { + const fShader = fFileText + + // Extract the program and shaders + const [shaderProgram, vertexShader, fragmentShader] = this.createShaderProgram(vShader, fShader); + + // Create a wrapper type + const programWrapper = new WebGLProgramType(); + programWrapper.program = shaderProgram; + programWrapper.vertexShader = vertexShader; + programWrapper.fragmentShader = fragmentShader; + + // Add to our map + this.gl_ShaderPrograms.add(key, programWrapper); + + // Finish loading + this.gl_FinishLoadingShader(callbackIfLast); + }); + }); + } + + private gl_FinishLoadingShader(callback: Function): void { + this.loadonly_gl_ShaderProgramsLoaded += 1; + + if(this.loadonly_gl_ShaderProgramsLoaded === this.loadonly_gl_ShaderProgramsToLoad){ + // We're done loading shaders + callback(); + } + } + + private createShaderProgram(vShaderSource: string, fShaderSource: string){ + const vertexShader = this.loadVertexShader(vShaderSource); + const fragmentShader = this.loadFragmentShader(fShaderSource); + + if(vertexShader === null || fragmentShader === null){ + // We had a problem intializing - error + return null; + } + + // Create a shader program + const program = this.gl.createProgram(); + if(!program) { + // Error creating + console.log("Failed to create program"); + return null; + } + + // Attach our vertex and fragment shader + this.gl.attachShader(program, vertexShader); + this.gl.attachShader(program, fragmentShader); + + // Link + this.gl.linkProgram(program); + if(!this.gl.getProgramParameter(program, this.gl.LINK_STATUS)){ + // Error linking + const error = this.gl.getProgramInfoLog(program); + console.log("Failed to link program: " + error); + + // Clean up + this.gl.deleteProgram(program); + this.gl.deleteShader(vertexShader); + this.gl.deleteShader(fragmentShader); + return null; + } + + // We successfully create a program + return [program, vertexShader, fragmentShader]; + } + + private loadVertexShader(shaderSource: string): WebGLShader{ + // Create a new vertex shader + return this.loadShader(this.gl.VERTEX_SHADER, shaderSource); + } + + private loadFragmentShader(shaderSource: string): WebGLShader{ + // Create a new fragment shader + return this.loadShader(this.gl.FRAGMENT_SHADER, shaderSource); + } + + private loadShader(type: number, shaderSource: string): WebGLShader{ + const shader = this.gl.createShader(type); + + // If we couldn't create the shader, error + if(shader === null){ + console.log("Unable to create shader"); + return null; + } + + // Add the source to the shader and compile + this.gl.shaderSource(shader, shaderSource); + this.gl.compileShader(shader); + + // Make sure there were no errors during this process + if(!this.gl.getShaderParameter(shader, this.gl.COMPILE_STATUS)){ + // Not compiled - error + const error = this.gl.getShaderInfoLog(shader); + console.log("Failed to compile shader: " + error); + + // Clean up + this.gl.deleteShader(shader); + return null; + } + + // Sucess, so return the shader + return shader; + } + + /* ########## GENERAL LOADING FUNCTIONS ########## */ + private loadTextFile(textFilePath: string, callback: Function): void { let xobj: XMLHttpRequest = new XMLHttpRequest(); xobj.overrideMimeType("application/json"); @@ -476,6 +722,8 @@ export default class ResourceManager { xobj.send(null); } + /* ########## LOADING BAR INFO ########## */ + private getLoadPercent(): number { return (this.loadonly_tilemapsLoaded/this.loadonly_tilemapsToLoad + this.loadonly_spritesheetsLoaded/this.loadonly_spritesheetsToLoad @@ -499,6 +747,12 @@ export default class ResourceManager { } class KeyPathPair { - key: string - path: string + key: string; + path: string; +} + +class KeyPath_Shader { + key: string; + vpath: string; + fpath: string; } \ No newline at end of file diff --git a/src/Wolfie2D/Scene/SceneManager.ts b/src/Wolfie2D/Scene/SceneManager.ts index e8bccc7..331c07c 100644 --- a/src/Wolfie2D/Scene/SceneManager.ts +++ b/src/Wolfie2D/Scene/SceneManager.ts @@ -88,7 +88,9 @@ export default class SceneManager { * Renders the current Scene */ public render(): void { - this.currentScene.render(); + if(this.currentScene.isRunning()){ + this.currentScene.render(); + } } /** diff --git a/src/Wolfie2D/SceneGraph/SceneGraphArray.ts b/src/Wolfie2D/SceneGraph/SceneGraphArray.ts index c47b4b9..c9fa6e3 100644 --- a/src/Wolfie2D/SceneGraph/SceneGraphArray.ts +++ b/src/Wolfie2D/SceneGraph/SceneGraphArray.ts @@ -83,7 +83,7 @@ export default class SceneGraphArray extends SceneGraph { let visibleSet = new Array(); for(let node of this.nodeList){ - if(!node.getLayer().isHidden() && this.viewport.includes(node)){ + if(!node.getLayer().isHidden() && node.visible && this.viewport.includes(node)){ visibleSet.push(node); } } diff --git a/src/Wolfie2D/Utils/Color.ts b/src/Wolfie2D/Utils/Color.ts index 55096a5..8f936b0 100644 --- a/src/Wolfie2D/Utils/Color.ts +++ b/src/Wolfie2D/Utils/Color.ts @@ -138,6 +138,14 @@ export default class Color { return new Color(MathUtils.clamp(this.r - 40, 0, 255), MathUtils.clamp(this.g - 40, 0, 255), MathUtils.clamp(this.b - 40, 0, 255), this.a); } + /** + * Returns this color as an array + * @returns [r, g, b, a] + */ + toArray(): [number, number, number, number] { + return [this.r, this.g, this.b, this.a]; + } + /** * Returns the color as a string of the form #RRGGBB * @returns #RRGGBB @@ -164,4 +172,17 @@ export default class Color { } return "rgba(" + this.r.toString() + ", " + this.g.toString() + ", " + this.b.toString() + ", " + this.a.toString() +")" } + + /** + * Turns this color into a float32Array and changes color range to [0.0, 1.0] + * @returns a Float32Array containing the color + */ + toWebGL(): Float32Array { + return new Float32Array([ + this.r/255, + this.g/255, + this.b/255, + this.a + ]); + } } \ No newline at end of file diff --git a/src/Wolfie2D/Utils/MathUtils.ts b/src/Wolfie2D/Utils/MathUtils.ts index a910b18..1b4c0c8 100644 --- a/src/Wolfie2D/Utils/MathUtils.ts +++ b/src/Wolfie2D/Utils/MathUtils.ts @@ -57,6 +57,10 @@ export default class MathUtils { } } + static changeRange(x: number, min: number, max: number, newMin: number, newMax: number): number { + return this.lerp(newMin, newMax, this.invLerp(min, max, x)); + } + /** * Linear Interpolation * @param a The first value for the interpolation bound diff --git a/src/Wolfie2D/Utils/RenderingUtils.ts b/src/Wolfie2D/Utils/RenderingUtils.ts new file mode 100644 index 0000000..f822227 --- /dev/null +++ b/src/Wolfie2D/Utils/RenderingUtils.ts @@ -0,0 +1,28 @@ +import Vec2 from "../DataTypes/Vec2"; +import Color from "./Color"; +import MathUtils from "./MathUtils"; + +export default class RenderingUtils { + static toWebGLCoords(point: Vec2, origin: Vec2, worldSize: Vec2): Float32Array { + return new Float32Array([ + MathUtils.changeRange(point.x, origin.x, origin.x + worldSize.x, -1, 1), + MathUtils.changeRange(point.y, origin.y, origin.y + worldSize.y, 1, -1) + ]); + } + + static toWebGLScale(size: Vec2, worldSize: Vec2): Float32Array { + return new Float32Array([ + 2*size.x/worldSize.x, + 2*size.y/worldSize.y, + ]); + } + + static toWebGLColor(color: Color): Float32Array { + return new Float32Array([ + MathUtils.changeRange(color.r, 0, 255, 0, 1), + MathUtils.changeRange(color.g, 0, 255, 0, 1), + MathUtils.changeRange(color.b, 0, 255, 0, 1), + color.a + ]); + } +} \ No newline at end of file diff --git a/src/hw1/GradientCircleShaderType.ts b/src/hw1/GradientCircleShaderType.ts new file mode 100644 index 0000000..adb6db0 --- /dev/null +++ b/src/hw1/GradientCircleShaderType.ts @@ -0,0 +1,68 @@ +import Map from "../Wolfie2D/DataTypes/Map"; +import Mat4x4 from "../Wolfie2D/DataTypes/Mat4x4"; +import Vec2 from "../Wolfie2D/DataTypes/Vec2"; +import RectShaderType from "../Wolfie2D/Rendering/WebGLRendering/ShaderTypes/RectShaderType"; + +/** + * The gradient circle is technically rendered on a quad, and is similar to a rect, so we'll extend the RectShaderType + */ +export default class GradientCircleShaderType extends RectShaderType { + + initBufferObject(): void { + this.bufferObjectKey = "gradient_circle"; + this.resourceManager.createBuffer(this.bufferObjectKey); + } + + render(gl: WebGLRenderingContext, options: Record): void { + // Get our program and buffer object + const program = this.resourceManager.getShaderProgram(this.programKey); + const buffer = this.resourceManager.getBuffer(this.bufferObjectKey); + + // Let WebGL know we're using our shader program + gl.useProgram(program); + + // Get our vertex data + const vertexData = this.getVertices(options.size.x, options.size.y); + const FSIZE = vertexData.BYTES_PER_ELEMENT; + + // Bind the buffer + gl.bindBuffer(gl.ARRAY_BUFFER, buffer); + gl.bufferData(gl.ARRAY_BUFFER, vertexData, gl.STATIC_DRAW); + + /* ##### ATTRIBUTES ##### */ + // No texture, the only thing we care about is vertex position + const a_Position = gl.getAttribLocation(program, "a_Position"); + gl.vertexAttribPointer(a_Position, 2, gl.FLOAT, false, 2 * FSIZE, 0 * FSIZE); + gl.enableVertexAttribArray(a_Position); + + /* ##### UNIFORMS ##### */ + // Send the color to the gradient circle + const color = options.color.toWebGL(); + const u_Color = gl.getUniformLocation(program, "u_Color"); + gl.uniform4fv(u_Color, color); + + // Get transformation matrix + // We have a square for our rendering space, so get the maximum dimension of our quad + let maxDimension = Math.max(options.size.x, options.size.y); + + // The size of the rendering space will be a square with this maximum dimension + let size = new Vec2(maxDimension, maxDimension).scale(2/options.worldSize.x, 2/options.worldSize.y); + + // Center our translations around (0, 0) + const translateX = (options.position.x - options.origin.x - options.worldSize.x/2)/maxDimension; + const translateY = -(options.position.y - options.origin.y - options.worldSize.y/2)/maxDimension; + + // Create our transformation matrix + this.translation.translate(new Float32Array([translateX, translateY])); + this.scale.scale(size); + this.rotation.rotate(options.rotation); + let transformation = Mat4x4.MULT(this.translation, this.scale, this.rotation); + + // Pass the translation matrix to our shader + const u_Transform = gl.getUniformLocation(program, "u_Transform"); + gl.uniformMatrix4fv(u_Transform, false, transformation.toArray()); + + // Draw the quad + gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4); + } +} \ No newline at end of file diff --git a/src/hw1/HW1_Enums.ts b/src/hw1/HW1_Enums.ts new file mode 100644 index 0000000..d380546 --- /dev/null +++ b/src/hw1/HW1_Enums.ts @@ -0,0 +1,8 @@ +export enum Homework1Event { + PLAYER_DAMAGE = "PLAYER_DAMAGE", + SPAWN_FLEET = "SPAWN_FLEET" +} + +export enum Homework1Shaders { + GRADIENT_CIRCLE = "GRADIENT_CIRCLE" +} \ No newline at end of file diff --git a/src/hw1/HW1_Scene.ts b/src/hw1/HW1_Scene.ts new file mode 100644 index 0000000..abba8e3 --- /dev/null +++ b/src/hw1/HW1_Scene.ts @@ -0,0 +1,107 @@ +import AABB from "../Wolfie2D/DataTypes/Shapes/AABB"; +import Vec2 from "../Wolfie2D/DataTypes/Vec2"; +import Graphic from "../Wolfie2D/Nodes/Graphic"; +import { GraphicType } from "../Wolfie2D/Nodes/Graphics/GraphicTypes"; +import AnimatedSprite from "../Wolfie2D/Nodes/Sprites/AnimatedSprite"; +import Scene from "../Wolfie2D/Scene/Scene"; +import { Homework1Event, Homework1Shaders } from "./HW1_Enums"; +import SpaceshipPlayerController from "./SpaceshipPlayerController"; + +/** + * In Wolfie2D, custom scenes extend the original scene class. + * This gives us access to lifecycle methods to control our game. + */ +export default class Homework1_Scene extends Scene { + // Here we define member variables of our game, and object pools for adding in game objects + private player: AnimatedSprite; + + // Create an object pool for our fleet + private MAX_FLEET_SIZE = 20; + private fleet: Array = new Array(this.MAX_FLEET_SIZE); + + // Create an object pool for our fleet + private MAX_NUM_ASTEROIDS = 6; + private asteroids: Array = new Array(this.MAX_NUM_ASTEROIDS); + + // Create an object pool for our fleet + private MAX_NUM_MINERALS = 20; + private minerals: Array = new Array(this.MAX_NUM_MINERALS); + + /* + * loadScene() overrides the parent class method. It allows us to load in custom assets for + * use in our scene. + */ + loadScene(){ + /* ##### DO NOT MODIFY ##### */ + // Load in the player spaceship spritesheet + this.load.spritesheet("player", "hw1_assets/spritesheets/player_spaceship.json"); + + /* ##### YOUR CODE GOES BELOW THIS LINE ##### */ + } + + /* + * startScene() allows us to add in the assets we loaded in loadScene() as game objects. + * Everything here happens strictly before update + */ + startScene(){ + /* ##### DO NOT MODIFY ##### */ + // Create a layer to serve as our main game - Feel free to use this for your own assets + // It is given a depth of 5 to be above our background + this.addLayer("primary", 5); + + // Add in the player as an animated sprite + // We give it the key specified in our load function and the name of the layer + this.player = this.add.animatedSprite("player", "primary"); + + // Set the player's position to the middle of the screen, and scale it down + this.player.position.set(this.viewport.getCenter().x, this.viewport.getCenter().y); + this.player.scale.set(0.5, 0.5); + + // Play the idle animation by default + this.player.animation.play("idle"); + + // Add physics to the player + let playerCollider = new AABB(Vec2.ZERO, new Vec2(64, 64)); + + // We'll specify a smaller collider centered on the player. + // Also, we don't need collision handling, so disable it. + this.player.addPhysics(playerCollider, Vec2.ZERO, false); + + // Add a a playerController to the player + this.player.addAI(SpaceshipPlayerController, {owner: this.player, spawnFleetEventKey: "spawnFleet"}); + + /* ##### YOUR CODE GOES BELOW THIS LINE ##### */ + // Initialize the fleet object pool + + // Initialize the mineral object pool + for(let i = 0; i < this.minerals.length; i++){ + this.minerals[i] = this.add.graphic(GraphicType.RECT, "primary", {position: new Vec2(0, 0), size: new Vec2(32, 32)}); + this.minerals[i].visible = false; + } + + // Initialize the asteroid object pool + let gc = this.add.graphic(GraphicType.RECT, "primary", {position: new Vec2(400, 400), size: new Vec2(100, 100)}); + gc.useCustomShader(Homework1Shaders.GRADIENT_CIRCLE); + + // Subscribe to events + this.receiver.subscribe(Homework1Event.PLAYER_DAMAGE); + this.receiver.subscribe(Homework1Event.SPAWN_FLEET); + } + + /* + * updateScene() is where the real work is done. This is where any custom behavior goes. + */ + updateScene(){ + // Handle events we care about + while(this.receiver.hasNextEvent()){ + let event = this.receiver.getNextEvent(); + } + + // Check for collisions + for(let i = 0; i < this.minerals.length; i++){ + if(this.player.collisionShape.overlaps(this.minerals[i].boundary)){ + console.log(true); + } + } + } +} \ No newline at end of file diff --git a/src/hw1/SpaceshipPlayerController.ts b/src/hw1/SpaceshipPlayerController.ts new file mode 100644 index 0000000..e4a3ec0 --- /dev/null +++ b/src/hw1/SpaceshipPlayerController.ts @@ -0,0 +1,77 @@ +import AI from "../Wolfie2D/DataTypes/Interfaces/AI"; +import Vec2 from "../Wolfie2D/DataTypes/Vec2"; +import Debug from "../Wolfie2D/Debug/Debug"; +import Emitter from "../Wolfie2D/Events/Emitter"; +import GameEvent from "../Wolfie2D/Events/GameEvent"; +import Input from "../Wolfie2D/Input/Input"; +import AnimatedSprite from "../Wolfie2D/Nodes/Sprites/AnimatedSprite"; +import MathUtils from "../Wolfie2D/Utils/MathUtils"; +import { Homework1Event } from "./HW1_Enums"; + +export default class SpaceshipPlayerController implements AI { + // We want to be able to control our owner, so keep track of them + private owner: AnimatedSprite; + + // The direction the spaceship is moving + private direction: Vec2; + private MIN_SPEED: number = 0; + private MAX_SPEED: number = 300; + private speed: number; + private ACCELERATION: number = 4; + private rotationSpeed: number; + + // An emitter to hook into the event queue + private emitter: Emitter; + + initializeAI(owner: AnimatedSprite, options: Record): void { + this.owner = owner; + + // Start facing up + this.direction = new Vec2(0, 1); + this.speed = 0; + this.rotationSpeed = 2; + + this.emitter = new Emitter(); + } + + handleEvent(event: GameEvent): void { + // We need to handle animations when we get hurt + if(event.type === Homework1Event.PLAYER_DAMAGE){ + this.owner.animation.play("shield"); + } + } + + update(deltaT: number): void { + // We need to handle player input + let forwardAxis = (Input.isPressed('forward') ? 1 : 0) + (Input.isPressed('backward') ? -1 : 0); + let turnDirection = (Input.isPressed('turn_ccw') ? -1 : 0) + (Input.isPressed('turn_cw') ? 1 : 0); + + // Space controls - speed stays the same if nothing happens + // Forward to speed up, backward to slow down + this.speed += this.ACCELERATION * forwardAxis; + this.speed = MathUtils.clamp(this.speed, this.MIN_SPEED, this.MAX_SPEED); + + // Rotate the player + this.direction.rotateCCW(turnDirection * this.rotationSpeed * deltaT); + + // Update the visual direction of the player + this.owner.rotation = -(Math.atan2(this.direction.y, this.direction.x) - Math.PI/2); + + // Move the player with physics + this.owner.move(this.direction.scaled(-this.speed * deltaT)); + + // If the player clicked, we need to spawn in a fleet member + if(Input.isMouseJustPressed()){ + this.emitter.fireEvent(Homework1Event.SPAWN_FLEET, {position: Input.getGlobalMousePosition()}); + } + + // Animations + if(!this.owner.animation.isPlaying("shield")){ + if(this.speed > 0){ + this.owner.animation.playIfNotAlready("boost"); + } else { + this.owner.animation.playIfNotAlready("idle"); + } + } + } +} \ No newline at end of file diff --git a/src/main.ts b/src/main.ts index d5c2cc3..6acbaa2 100644 --- a/src/main.ts +++ b/src/main.ts @@ -1,30 +1,36 @@ import Game from "./Wolfie2D/Loop/Game"; -import Platformer from "./Platformer"; +import Homework1_Scene from "./hw1/HW1_Scene"; +import Registry from "./Wolfie2D/Registry/Registry"; +import { Homework1Shaders } from "./hw1/HW1_Enums"; +import GradientCircleShaderType from "./hw1/GradientCircleShaderType"; // The main function is your entrypoint into Wolfie2D. Specify your first scene and any options here. (function main(){ - // These are options for initializing the game - // Here, we'll set the size of the viewport, color the background, and set up key bindings. + // Set up options let options = { - canvasSize: {x: 800, y: 600}, - zoomLevel: 4, - clearColor: {r: 34, g: 32, b: 52}, + canvasSize: {x: 1200, y: 800}, + clearColor: {r: 0.1, g: 0.1, b: 0.1}, inputs: [ - { name: "left", keys: ["a"] }, - { name: "right", keys: ["d"] }, - { name: "jump", keys: ["space", "w"]} - ] + { name: "forward", keys: ["w"] }, + { name: "backward", keys: ["s"] }, + { name: "turn_ccw", keys: ["a"] }, + { name: "turn_cw", keys: ["d"] }, + ], + useWebGL: true, + showDebug: false } - // Create our game. This will create all of the systems. - const demoGame = new Game(options); + // We have a custom shader, so lets add it to the registry and preload it + Registry.shaders.registerAndPreloadItem( + Homework1Shaders.GRADIENT_CIRCLE, // The key of the shader program + GradientCircleShaderType, // The constructor of the shader program + "hw1_assets/shaders/gradient_circle.vshader", // The path to the vertex shader + "hw1_assets/shaders/gradient_circle.fshader"); // the path to the fragment shader - // Run our game. This will start the game loop and get the updates and renders running. - demoGame.start(); + // Create a game with the options specified + const game = new Game(options); - // For now, we won't specify any scene options. - let sceneOptions = {}; - - // Add our first scene. This will load this scene into the game world. - demoGame.getSceneManager().addScene(Platformer, sceneOptions); + // Start our game + game.start(); + game.getSceneManager().addScene(Homework1_Scene, {}); })(); \ No newline at end of file