UNPKG

blaze-2d

Version:

A fast and simple WebGL 2 2D game engine written in TypeScript

277 lines 9.36 kB
import { vec2 } from "gl-matrix"; import Camera from "./camera/camera"; import BatchRenderer from "./renderer/batchRenderer"; import Renderer from "./renderer/renderer"; import Viewport from "./camera/viewport"; /** * Represents the 2D world. */ export default class World { constructor(cellSize, cameraViewport) { this.useBatchRenderer = false; this.entities = []; this.fluids = []; /** * Callbacks which are fired whenever an entity is added or removed. */ this.entityListeners = []; this.cellSize = cellSize; if (!cameraViewport) { const canvas = Renderer.getGL().canvas; this.camera = new Camera(vec2.fromValues(0, 0), canvas.width, canvas.height); // setup resize observer const observer = new ResizeObserver(() => { if (canvas.clientWidth === this.camera.viewport.getWidth() && canvas.clientHeight === this.camera.viewport.getHeight()) return; this.camera.viewport = new Viewport(this.camera.getPosition(), canvas.clientWidth, canvas.clientHeight); this.camera.setZoom(this.camera.getZoom()); }); observer.observe(canvas); } else { this.camera = new Camera(vec2.fromValues(0, 0), cameraViewport[0], cameraViewport[1]); } } /** * Updates the world's camera, terrain and entities. * * Also calls the render function. * * @param ts The {@link TimeStep} for this update */ update(ts) { this.camera.update(); // update entities for (const e of this.entities) { e.update(ts.dt); // if (this.camera.viewport.containsRectCollider(e.collider as RectCollider, this.getWorldToPixelSpace())) { // } } // rendering this.renderEntities(); this.renderFluids(); } /** * Renders the entities in the world using the world's current camera. */ renderEntities() { const worldToClipSpace = this.getWorldtoClipSpace(); // Renderer.setMode("TRIANGLES"); Renderer.useCamera(this.camera); Renderer.setScale(worldToClipSpace); if (this.useBatchRenderer) { for (const e of this.entities) { BatchRenderer.queueEntity(e, e.getZIndex()); } } else { for (const e of this.entities) { e.render(); } } } /** * Renders the fluids in the world using the world's current camera. */ renderFluids() { const worldToClipSpace = this.getWorldtoClipSpace(); // Renderer.setMode("TRIANGLES"); Renderer.useCamera(this.camera); Renderer.setScale(worldToClipSpace); for (const fluid of this.fluids) { fluid.render(); } } /** * Calculates the world space to clip space scale. * * @returns The number that multiplying a world space coordinate by provides the equivalent clip space coordinate. */ getWorldtoClipSpace() { const width = this.camera.viewport.getWidth(); const height = this.camera.viewport.getHeight(); return vec2.fromValues((this.cellSize[0] * 2) / width, (this.cellSize[1] * 2) / height); } /** * Calculates the world space to pixel space scale. * * @returns The number that multiplying a world space coordinate by provides the equivalent pixel space coordinate. */ getWorldToPixelSpace() { const v = vec2.clone(this.cellSize); return v; } /** * Calculates the pixel space to world space scale. * * @returns The number that multiplying a pixel space coordinate by provides the equivalent world space coordinate. */ getPixelToWorldSpace() { const zoom = this.camera.getZoom(); const cellSize = vec2.scale(vec2.create(), this.cellSize, zoom); const v = vec2.fromValues(1 / cellSize[0], 1 / cellSize[1]); return v; } /** * Gets the world location of a pixel on the screen. * * This is calculated using the viewport of the world camera. * * @param pixel A pixel position on the screen */ getWorldFromPixel(pixel) { const view = this.camera.viewport; const vw = view.getOriginalWidth(); const vh = view.getOriginalHeight(); const p = vec2.fromValues(pixel[0] - vw / 2, vh / 2 - pixel[1]); const pixelToWorld = this.getPixelToWorldSpace(); const world = vec2.fromValues(p[0] * pixelToWorld[0], p[1] * pixelToWorld[1]); // get world position inside current view const centre = this.camera.getPosition(); vec2.add(world, world, centre); vec2.rotate(world, world, centre, -this.camera.getRotation()); return world; } /** * Gets the pixel coordinate of a world location. * * This is calculated using the viewport of the world camera. * * @param world A world position */ getPixelFromWorld(world) { const centre = this.camera.getPosition(); const pos = vec2.rotate(vec2.create(), world, centre, this.camera.getRotation()); vec2.sub(pos, pos, centre); const worldToPixel = this.getWorldToPixelSpace(); const pixel = vec2.fromValues(pos[0] * worldToPixel[0], pos[1] * worldToPixel[1]); const view = this.camera.viewport; const vw = view.getOriginalWidth(); const vh = view.getOriginalHeight(); return vec2.fromValues(pixel[0] + vw / 2, vh / 2 - pixel[1]); } /** * Gets all entities currently in the world. * * @returns All entites in the world */ getEntities() { return this.entities; } /** * Gets all entities with the given name. * * @param name The name of the entity */ getEntitiesByName(name) { return this.getEntities().filter((entity) => entity.name === name); } /** * Adds an entity to the world. * * @param entity The entity to add * @param fireListeners Wether or not to execute the world's entity listeners */ addEntity(entity, fireListeners = true) { this.entities.push(entity); if (fireListeners) this.callEntityListeners("add", entity, this.entities.length - 1); } /** * Adds the given entities to the world. * * By default entity listeners are fired for each entity added however, if the last argument * passed to this function is a boolean then it will be used to decide wether or not to fire entity listeners. * * @param entities The entities to add */ addEntities(...entities) { let fireListeners = true; if (typeof entities[entities.length - 1] === "boolean") { fireListeners = entities[entities.length - 1]; entities.pop(); } entities.forEach((entity) => { if (typeof entity === "boolean") return; this.addEntity(entity, fireListeners); }); } /** * Removes an entity from the world. * * @param entity The entity to remove * @param fireListeners Wether or not to execute the world's entity listeners * @returns Wether or not the entity was removed */ removeEntity(entity, fireListeners = true) { const i = this.entities.findIndex((e) => e === entity); if (i === -1) return false; this.entities.splice(i, 1); if (fireListeners) this.callEntityListeners("remove", entity, i); return true; } callEntityListeners(event, entity, index) { for (const l of this.entityListeners) { l(event, entity, index, this.entities); } } /** * Adds a callback which will be called whenever an entity is added or removed from the world. * * @param listener The listener to add */ addEntityListener(listener) { this.entityListeners.push(listener); } /** * Removes an {@link EntityListener} from the world. * * @param listener The listener to remove */ removeEntityListener(listener) { const index = this.entityListeners.findIndex((l) => l === listener); if (index === -1) return; this.entityListeners.splice(index, 1); } /** * Adds a fluid to the world. * * @param fluid The fluid to add */ addFluid(fluid) { this.fluids.push(fluid); } /** * Removes a fluid from the world. * * @param fluid The fluid to remove */ removeFluid(fluid) { const index = this.fluids.findIndex((f) => f === fluid); if (index === -1) return; this.fluids.splice(index, 1); } /** * Sets the camera to use for rendering. * * @param camera The camera to use for rendering */ useCamera(camera) { this.camera = camera; } /** * Gets the camera that is currently being used for rendering. * * @returns The camera that is currently being used for rendering */ getCamera() { return this.camera; } } //# sourceMappingURL=world.js.map