From 177bc3d36b9a3dc1d8cee246bb943baaa6b2d83f Mon Sep 17 00:00:00 2001 From: Sergey Ufocoder Date: Thu, 9 May 2024 09:58:26 +0300 Subject: [PATCH] chore: update --- src/global.d.ts | 4 +- src/levels/base.ts | 6 +- src/lib/ecs/components/SpriteComponent.ts | 2 - src/lib/ecs/lib/PositionMap.ts | 6 +- src/lib/ecs/systems/CameraSystem.ts | 371 ++++++++++------------ src/lib/ecs/systems/MoveSystem.ts | 15 +- src/scenes/LevelScene.ts | 35 +- 7 files changed, 202 insertions(+), 237 deletions(-) diff --git a/src/global.d.ts b/src/global.d.ts index 1311343..2d0639f 100644 --- a/src/global.d.ts +++ b/src/global.d.ts @@ -57,4 +57,6 @@ interface Texture { width: number; height: number; colors: Color[][]; -} \ No newline at end of file +} + +type Sprite = Texture \ No newline at end of file diff --git a/src/levels/base.ts b/src/levels/base.ts index 9bcf930..a8b5252 100644 --- a/src/levels/base.ts +++ b/src/levels/base.ts @@ -25,9 +25,9 @@ const level: Level = { 2: "exit", }, player: { - x: 9.7, - y: 2.01, - angle: 180, + x: 0, + y: 5.5, + angle: 0, health: 100, }, enemies: [ diff --git a/src/lib/ecs/components/SpriteComponent.ts b/src/lib/ecs/components/SpriteComponent.ts index 2c8f069..3a08eee 100644 --- a/src/lib/ecs/components/SpriteComponent.ts +++ b/src/lib/ecs/components/SpriteComponent.ts @@ -1,7 +1,5 @@ import { Component } from "src/lib/ecs/Component"; -type Sprite = Texture; - export default class SpriteComponent implements Component { sprite: Sprite; diff --git a/src/lib/ecs/lib/PositionMap.ts b/src/lib/ecs/lib/PositionMap.ts index f925eae..382c057 100644 --- a/src/lib/ecs/lib/PositionMap.ts +++ b/src/lib/ecs/lib/PositionMap.ts @@ -12,15 +12,15 @@ export default class PositionMap { } public set(x: number, y: number, entity: T) { - this.map.set(y * this.rows + x, entity); + this.map.set(y * this.cols + x, entity); } public get(x: number, y: number) { - return this.map.get(y * this.rows + x); + return this.map.get(y * this.cols + x); } public has(x: number, y: number) { - return this.map.has(y * this.rows + x); + return this.map.has(y * this.cols + x); } public clear() { diff --git a/src/lib/ecs/systems/CameraSystem.ts b/src/lib/ecs/systems/CameraSystem.ts index fdb1272..09c989c 100644 --- a/src/lib/ecs/systems/CameraSystem.ts +++ b/src/lib/ecs/systems/CameraSystem.ts @@ -1,7 +1,10 @@ import Canvas from "src/lib/Canvas/BufferCanvas"; import System from "src/lib/ecs/System"; import Entity from "src/lib/ecs/Entity"; -import { calculateAngle, degreeToRadians, radiansToDegrees } from "src/lib/utils"; +import { + degreeToRadians, + radiansToDegrees, +} from "src/lib/utils"; // import BoxComponent from "src/lib/ecs/components/BoxComponent"; import PositionComponent from "src/lib/ecs/components/PositionComponent"; import AngleComponent from "src/lib/ecs/components/AngleComponent"; @@ -10,13 +13,12 @@ import CameraComponent from "src/lib/ecs/components/CameraComponent"; import PositionMap from "../lib/PositionMap"; import QuerySystem from "../lib/QuerySystem"; import SpriteComponent from "../components/SpriteComponent"; -import BoxComponent from "../components/BoxComponent"; +import TextureManager from "src/managers/TextureManager"; export default class CameraSystem extends System { requiredComponents = [CameraComponent, PositionComponent]; - protected textureMap: PositionMap; - protected spriteMap: PositionMap; + protected walls: PositionMap; protected readonly width: number = 640; protected readonly height: number = 480; @@ -26,8 +28,9 @@ export default class CameraSystem extends System { protected readonly level: Level; protected readonly canvas: Canvas; protected readonly container: HTMLElement; + protected readonly textureManager: TextureManager; - constructor(querySystem: QuerySystem, container: HTMLElement, level: Level) { + constructor(querySystem: QuerySystem, container: HTMLElement, level: Level, textureManager: TextureManager) { super(querySystem); const cols = level.map[0].length; @@ -41,31 +44,16 @@ export default class CameraSystem extends System { width: this.width, }); - this.textureMap = new PositionMap(cols, rows); - this.spriteMap = new PositionMap(cols, rows); + this.textureManager = textureManager; - this.querySystem - .query([PositionComponent, TextureComponent]) - .forEach((entity) => { - const position = entity.getComponent(PositionComponent); - - this.textureMap.set( - Math.floor(position.x), - Math.floor(position.y), - entity - ); - }); + this.walls = new PositionMap(cols, rows); this.querySystem - .query([PositionComponent, SpriteComponent]) + .query([PositionComponent, TextureComponent]) .forEach((entity) => { const position = entity.getComponent(PositionComponent); - this.spriteMap.set( - Math.floor(position.x), - Math.floor(position.y), - entity - ); + this.walls.set(Math.floor(position.x), Math.floor(position.y), entity); }); } @@ -74,281 +62,244 @@ export default class CameraSystem extends System { } update(_: number, entities: Entity[]) { - this.canvas.clear(); - const camera = entities.find((entity) => entity.hasComponent(CameraComponent) ); - if (camera) { - this.canvas.createBufferSnapshot(); - this._rayCasting(camera); - - this.canvas.commitBufferSnapshot(); + if (!camera) { + return; } + + this.canvas.createBufferSnapshot(); + this.render(camera); + this.canvas.commitBufferSnapshot(); } destroy(): void { this.canvas.element.remove(); } - _rayCasting(camera: Entity) { - const cameraPosition = camera.getComponent(PositionComponent); + render(camera: Entity) { const cameraFov = camera.getComponent(CameraComponent); const cameraAngle = camera.getComponent(AngleComponent); + const cameraPosition = camera.getComponent(PositionComponent); - const width = this.width; - const halfHeight = this.height / 2; - - const initialRayX = cameraPosition.x; - const initialRayY = cameraPosition.y; - const incrementAngle = cameraFov.fov / width; + const spriteEntities: Entity[] = []; + const incrementAngle = cameraFov.fov / this.width; let rayAngle = cameraAngle.angle - cameraFov.fov / 2; - const spriteEntities: Entity[] = []; - for (let rayCount = 0; rayCount < width; rayCount++) { + for (let x = 0; x < this.width; x++) { + const incrementRayX = + Math.cos(degreeToRadians(rayAngle)) / this.rayPrecision; + const incrementRayY = + Math.sin(degreeToRadians(rayAngle)) / this.rayPrecision; - const rayCollidedSpriteEntities: Set = new Set(); - const incrementRayX = Math.cos(degreeToRadians(rayAngle)) / this.rayPrecision; - const incrementRayY = Math.sin(degreeToRadians(rayAngle)) / this.rayPrecision; + let rayX = cameraPosition.x; + let rayY = cameraPosition.y; - let currentRayX = initialRayX; - let currentRayY = initialRayY; let distanceRay = 0; - let rayCollidedTextureEntity: Entity | undefined; - let rayCollidedSpriteEntity: Entity | undefined; + let wallEntity: Entity | undefined; let isPropogating = true; while (isPropogating) { - currentRayX += incrementRayX; - currentRayY += incrementRayY; + rayX += incrementRayX; + rayY += incrementRayY; + + if (rayX < 0 || rayX > this.walls.cols) { + isPropogating = false; + continue; + } - const x = Math.floor(currentRayX); - const y = Math.floor(currentRayY); + if (rayY < 0 || rayY > this.walls.rows) { + isPropogating = false; + continue; + } - rayCollidedTextureEntity = this.textureMap.get(x, y); - rayCollidedSpriteEntity = this.spriteMap.get(x, y); + wallEntity = this.walls.get(Math.floor(rayX), Math.floor(rayY)); - if (rayCollidedTextureEntity) { + if (wallEntity) { isPropogating = false; } distanceRay = Math.sqrt( - Math.pow(cameraPosition.x - currentRayX, 2) + - Math.pow(cameraPosition.y - currentRayY, 2) + Math.pow(cameraPosition.x - rayX, 2) + + Math.pow(cameraPosition.y - rayY, 2) ); if (distanceRay >= this.rayMaxDistanceRay) { isPropogating = false; } - - if (isPropogating && rayCollidedSpriteEntity && !rayCollidedSpriteEntities.has(rayCollidedSpriteEntity)) { - rayCollidedSpriteEntities.add(rayCollidedSpriteEntity); - } } - rayCollidedSpriteEntities.forEach(entity => { - const entityPosition = entity.getComponent(PositionComponent); - const entityAngle = calculateAngle(cameraPosition.x, cameraPosition.y, entityPosition.x, entityPosition.y); + distanceRay = + distanceRay * Math.cos(degreeToRadians(rayAngle - cameraAngle.angle)); - if (entityAngle >= rayAngle && entityAngle <= rayAngle + incrementAngle) { - spriteEntities.unshift(entity); - } - }) - - // Fish eye fix - distanceRay = distanceRay * Math.cos(degreeToRadians(rayAngle - cameraAngle.angle)); - - // Wall height - const wallHeight = rayCollidedTextureEntity - ? Math.floor(halfHeight / distanceRay) - : 0; - - // Draw top - this.canvas.drawVerticalLine({ - x: rayCount, - y1: 0, - y2: halfHeight - wallHeight, - color: this.level.world.colors.top, - }); - - // Draw bottom - this.canvas.drawVerticalLine({ - x: rayCount, - y1: halfHeight + wallHeight, - y2: this.height, - color: this.level.world.colors.bottom, - }); + if (wallEntity) { + const wallHeight = Math.floor(this.height / 2 / distanceRay); - if (rayCollidedTextureEntity) { - this._drawTexture( - rayCount, - currentRayX, - currentRayY, - rayCollidedTextureEntity, - wallHeight - ); + this._drawHorizon(x, wallHeight); + this._drawFloor(x, wallHeight, rayAngle, camera); + this._drawWall(x, rayX, rayY, wallEntity, wallHeight); + } else { + this._drawHorizon(x, 0); + this._drawFloor(x, 0, rayAngle, camera); } rayAngle += incrementAngle; } - spriteEntities.forEach(sprite => { + spriteEntities.forEach((sprite) => { this._drawSprite(camera, sprite); - }) + }); + } + _drawHorizon(x: number, wallHeight: number) { + this.canvas.drawVerticalLine({ + x, + y1: 0, + y2: this.height / 2 - wallHeight, + color: this.level.world.colors.top, + }); + + this.canvas.drawVerticalLine({ + x, + y1: this.height / 2 + wallHeight, + y2: this.height, + color: this.level.world.colors.bottom, + }); } - _drawTexture( + _drawWall( x: number, rayX: number, rayY: number, - entity: Entity, + wallEntity: Entity, wallHeight: number ) { - - const texture = entity.getComponent(TextureComponent).texture; - const box = entity.getComponent(BoxComponent); + const texture = wallEntity.getComponent(TextureComponent).texture; const texturePositionX = Math.floor( (texture.width * (rayX + rayY)) % texture.width ); + const yIncrementer = (wallHeight * 2) / texture.height; let y = this.height / 2 - wallHeight; for (let i = 0; i < texture.height; i++) { - const y1 = y; - const y2 = y + (yIncrementer + box.size / 2) + 1; - + if (y > -yIncrementer && y < this.height) { + this.canvas.drawVerticalLine({ + x, + y1: y, + y2: Math.floor(y + yIncrementer), + color: texture.colors[i][texturePositionX], + }); + } y += yIncrementer; + } + } - if (y1 < 0 && y2 < 0 || y1 > this.height && y2 > this.height) { - continue; - } + _drawFloor( + x: number, + wallHeight: number, + rayAngle: number, + camera: Entity, + ) { + const cameraPosition = camera.getComponent(PositionComponent); + const cameraAngle = camera.getComponent(AngleComponent); + const texture = this.textureManager.get('floor'); - this.canvas.drawVerticalLine({ - x: x, - y1, - y2, - color: texture.colors[i][texturePositionX], - }); + const halfHeight = this.height / 2; + const start = halfHeight + wallHeight + 1; + + const directionCos = Math.cos(degreeToRadians(rayAngle)); + const directionSin = Math.sin(degreeToRadians(rayAngle)); + + for (let y = start; y < this.height; y++) { + let distance = this.height / (2 * y - this.height); + + // Inverse fisheye fix + + distance = distance / Math.cos(degreeToRadians(cameraAngle.angle) - degreeToRadians(rayAngle)); + + // Get the tile position + let tileX = distance * directionCos; + let tileY = distance * directionSin; + tileX += cameraPosition.x; + tileY += cameraPosition.y; + + // Define texture coords + const textureX = Math.abs(Math.floor(tileX * texture.width)) % texture.width; + const textureY = Math.abs(Math.floor(tileY * texture.height)) % texture.height; + + // Get pixel color + const color = texture.colors[textureX][textureY]; + + this.canvas.drawPixel({ x, y, color }); } } - - _calculateSpriteProjection( - camera: Entity, - sprite: Entity - ) { + + _calculateEntityProjection(camera: Entity, entity: Entity) { const cameraPosition = camera.getComponent(PositionComponent); const cameraAngle = camera.getComponent(AngleComponent); const cameraFov = camera.getComponent(CameraComponent); - const spritePosition = sprite.getComponent(PositionComponent); + const entityPosition = entity.getComponent(PositionComponent); - // Get X and Y coords in relation of the player coords - const spriteXRelative = spritePosition.x - cameraPosition.x; - const spriteYRelative = spritePosition.y - cameraPosition.y; + const dx = entityPosition.x - cameraPosition.x; + const dy = entityPosition.y - cameraPosition.y; - // Get angle of the sprite in relation of the player angle - const spriteAngleRadians = Math.atan2(spriteYRelative, spriteXRelative); - const spriteAngle = radiansToDegrees(spriteAngleRadians) - Math.floor(cameraAngle.angle - cameraFov.fov / 2); + const entityAngleRadians = Math.atan2(dx, dy); + const entityAngle = + radiansToDegrees(entityAngleRadians) - + Math.floor(cameraAngle.angle - cameraFov.fov / 2); - // Three rule to discover the x position of the script - const spriteX = Math.floor(spriteAngle * this.width / cameraFov.fov); - - // Get the distance of the sprite (Pythagoras theorem) - const distance = Math.sqrt(Math.pow(cameraPosition.x - spritePosition.x, 2) + Math.pow(cameraPosition.y - spritePosition.y, 2)); + const x = Math.floor((entityAngle * this.width) / cameraFov.fov); + const distance = Math.sqrt(dx ** 2 + dy ** 2); - // Calc sprite width and height - const spriteHeight = Math.floor(this.height / 2 / distance); - const spriteWidth = Math.floor(this.width / 2 / distance); + const height = Math.floor(this.height / 2 / distance); + const width = Math.floor(this.width / 2 / distance); return { + x, distance, - x: spriteX, - height: spriteHeight, - width: spriteWidth, + height, + width, }; } - _drawSprite( - camera: Entity, - sprite: Entity - ) { - const spriteProjection = this._calculateSpriteProjection(camera, sprite); + _drawSprite(camera: Entity, sprite: Entity) { + const spriteProjection = this._calculateEntityProjection(camera, sprite); const spriteTexture = sprite.getComponent(SpriteComponent).sprite; const wallHeight = Math.floor(this.height / spriteProjection.distance); - const xIncrementer = (spriteProjection.width) / spriteTexture.width; - const yIncrementer = (spriteProjection.height) / spriteTexture.height; + const xIncrementer = spriteProjection.width / spriteTexture.width; + const yIncrementer = spriteProjection.height / spriteTexture.height; let xProjection = spriteProjection.x - spriteProjection.width / 2; - for(let spriteX = 0 ; spriteX < spriteTexture.width; spriteX++) { - let yProjection = this.height / 2 - spriteProjection.height / 2 - (spriteProjection.height /2 - wallHeight / 2) / 2; - - for(let spriteY = 0; spriteY < spriteTexture.height; spriteY++) { - const color = spriteTexture.colors[spriteY][spriteX]; - - if (color.a !== 0) { - this.canvas.drawRect({ - x: xProjection, - y: yProjection, - width: xIncrementer, - height: yIncrementer, - color - }); - } + for (let spriteX = 0; spriteX < spriteTexture.width; spriteX++) { + let yProjection = + this.height / 2 - + spriteProjection.height / 2 - + (spriteProjection.height / 2 - wallHeight / 2) / 2; + + for (let spriteY = 0; spriteY < spriteTexture.height; spriteY++) { + const color = spriteTexture.colors[spriteY][spriteX]; + + if (color.a !== 0) { + this.canvas.drawRect({ + x: xProjection, + y: yProjection, + width: xIncrementer, + height: yIncrementer, + color, + }); + } - yProjection += yIncrementer; + yProjection += yIncrementer; } xProjection += xIncrementer; } } - _drawFloor({ - x, - texture, - position, - wallHeight, - angle, - rayAngle, - }: { - x: number; - texture: Texture; - position: PositionComponent; - wallHeight: number; - angle: number; - rayAngle: number; - }) { - const halfHeight = this.height / 2; - - const start = halfHeight + wallHeight + 1; - const directionCos = Math.cos(degreeToRadians(rayAngle)); - const directionSin = Math.sin(degreeToRadians(rayAngle)); - - for (let y = start; y < this.height; y++) { - let distance = this.height / (2 * y - this.height); - - // Inverse fisheye fix - distance = - distance / Math.cos(degreeToRadians(angle) - degreeToRadians(rayAngle)); - - // Get the tile position - let tileX = distance * directionCos; - let tileY = distance * directionSin; - tileX += position.x; - tileY += position.y; - - // Define texture coords - const textureX = - Math.abs(Math.floor(tileX * texture.width)) % texture.width; - const textureY = - Math.abs(Math.floor(tileY * texture.height)) % texture.height; - - // Get pixel color - const color = texture.colors[textureX][textureY]; - this.canvas.drawPixel({ x, y, color }); - } - } } diff --git a/src/lib/ecs/systems/MoveSystem.ts b/src/lib/ecs/systems/MoveSystem.ts index 4e79cae..60ac0ae 100644 --- a/src/lib/ecs/systems/MoveSystem.ts +++ b/src/lib/ecs/systems/MoveSystem.ts @@ -13,7 +13,9 @@ import CameraComponent from "../components/CameraComponent"; export default class MoveSystem extends System { requiredComponents = [PositionComponent, AngleComponent, RotateComponent, MoveComponent]; - protected positionMap: PositionMap; + protected positionMap: PositionMap; + protected cols: number; + protected rows: number; constructor(querySystem: QuerySystem, level: Level) { super(querySystem); @@ -23,6 +25,9 @@ export default class MoveSystem extends System { this.positionMap = new PositionMap(cols, rows); + this.cols = cols; + this.rows = rows; + this.querySystem.query([PositionComponent, CollisionComponent]).forEach(entity => { if (entity.hasComponent(CameraComponent)) { return; @@ -96,6 +101,14 @@ export default class MoveSystem extends System { const newX = positionComponent.x + k * playerCos * moveComponent.moveSpeed * dt; const newY = positionComponent.y + k * playerSin * moveComponent.moveSpeed * dt; + if (newX < 0 || newX > this.cols) { + return + } + + if (newY < 0 || newY > this.rows) { + return + } + if (!this.positionMap.has(Math.floor(newX), Math.floor(newY))) { positionComponent.y = newY; positionComponent.x = newX; diff --git a/src/scenes/LevelScene.ts b/src/scenes/LevelScene.ts index ab5c80d..9c00aec 100644 --- a/src/scenes/LevelScene.ts +++ b/src/scenes/LevelScene.ts @@ -25,7 +25,7 @@ export default class LevelScene implements BaseScene { protected readonly level: Level; protected readonly loop: Loop; protected onCompleteCallback?: () => void; - + protected readonly querySystem: QuerySystem; protected readonly systems: System[]; protected readonly entities: Entity[]; @@ -37,30 +37,31 @@ export default class LevelScene implements BaseScene { const querySystem = new QuerySystem(entities); this.systems = [ - new ControlSystem(querySystem), - new AISystem(querySystem), - new MoveSystem(querySystem, level), - new CameraSystem(querySystem, container, level), - new MinimapSystem(querySystem, container, level), + new ControlSystem(querySystem), + new AISystem(querySystem), + new MoveSystem(querySystem, level), + new CameraSystem(querySystem, container, level, textureManager), + new MinimapSystem(querySystem, container, level), ]; - + this.entities = entities; this.querySystem = querySystem; this.loop = createLoop(this.onTick); } onTick = (dt: number) => { - this.systems.forEach(system => { - const entities = this.querySystem.query(system.requiredComponents); - system.update(dt, entities); + this.systems.forEach((system) => { + system.update(dt, this.querySystem.query(system.requiredComponents)); }); const [camera] = this.querySystem.query([CameraComponent]); if ( camera && - Math.floor(camera.getComponent(PositionComponent).x) === this.level.exit.x && - Math.floor(camera.getComponent(PositionComponent).y) === this.level.exit.y && + Math.floor(camera.getComponent(PositionComponent).x) === + this.level.exit.x && + Math.floor(camera.getComponent(PositionComponent).y) === + this.level.exit.y && this.onCompleteCallback ) { window.requestAnimationFrame(this.onCompleteCallback); @@ -72,7 +73,7 @@ export default class LevelScene implements BaseScene { }; start() { - this.systems.forEach(system => { + this.systems.forEach((system) => { system.start(); }); this.loop.play(); @@ -80,12 +81,12 @@ export default class LevelScene implements BaseScene { destroy() { // this.destroyListeners(); - this.systems.forEach(system => { - system.destroy(); - }) + this.systems.forEach((system) => { + system.destroy(); + }); } -/* + /* createListeners() { document.addEventListener("pointerdown", this.handleDocumentPointerdown); window.addEventListener("blur", this.handleWindowBlur);