diff --git a/package-lock.json b/package-lock.json index 3bd67f2..8180253 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,6 +9,7 @@ "version": "1.0.0", "license": "UNLICENSED", "dependencies": { + "quickhull": "^1.0.3", "sat": "^0.9.0", "three": "^0.171.0", "websocket-express": "^3.1.2" @@ -745,6 +746,12 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/quickhull": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/quickhull/-/quickhull-1.0.3.tgz", + "integrity": "sha512-AQbLaXdzGDJdO9Mu3qY/NY5JWlDqIutCLW8vJbsQTq+/bydIZeltnMVRKCElp81Y5/uRm4Yw/RsMdcltFYsS6w==", + "license": "MIT" + }, "node_modules/range-parser": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", diff --git a/package.json b/package.json index ce98d2d..fdb664b 100644 --- a/package.json +++ b/package.json @@ -10,6 +10,7 @@ "license": "UNLICENSED", "description": "", "dependencies": { + "quickhull": "^1.0.3", "sat": "^0.9.0", "three": "^0.171.0", "websocket-express": "^3.1.2" diff --git a/public/client.js b/public/client.js index 9d295f9..c75d2a5 100644 --- a/public/client.js +++ b/public/client.js @@ -43,6 +43,7 @@ global.renderer = renderer global.camera = camera global.scene = scene + const keysDown = {} function render() { @@ -54,8 +55,52 @@ function minimapRender() { minimapRenderer.render(scene, minimapCamera) } -const cameraSpeed = 0.1 +var cameraLocked = true +function followCamera() { + // entity = entities.filter((e) => e.id = '1').at(0) + const entity = entities['1'] + if (entity == null) { return } + + const offsetX = 0 + const offsetY = -15 + + const distanceX = Math.abs((entity.position.x + offsetX) - camera.position.x) + const distanceY = Math.abs((entity.position.y + offsetY) - camera.position.y) + + if (distanceX > 0.01) { + if (entity.position.x + offsetX > camera.position.x) { + camera.position.x += cameraSpeed * distanceX + } + if (entity.position.x + offsetX < camera.position.x) { + camera.position.x -= cameraSpeed * distanceX + } + } + else if (distanceX != 0) { + camera.position.x = entity.position.x + offsetX + } + + if (distanceY > 0.01) { + if (entity.position.y + offsetY > camera.position.y) { + camera.position.y += cameraSpeed * distanceY + } + if (entity.position.y + offsetY < camera.position.y) { + camera.position.y -= cameraSpeed * distanceY + } + } + else if (distanceY != 0) { + camera.position.y = entity.position.y + offsetY + } + + +} + +const cameraSpeed = 0.03 function cameraMovement() { + if (cameraLocked) { + followCamera() + return + } + if (keysDown.ArrowLeft) { camera.position.x -= cameraSpeed } else if (keysDown.ArrowRight) { camera.position.x += cameraSpeed } diff --git a/src/entity.js b/src/entity.js index e1458e3..244101e 100644 --- a/src/entity.js +++ b/src/entity.js @@ -64,23 +64,20 @@ export default class Entity { return entityColliders.concat(terrainColliders) } + // DEPRECATED: hulls were a failed concept for position fixing + collidableHulls() { + const entityColliders = (this.game?.entities ?? []).filter((e) => e.id != this.id).map((e) => e.collider) + const terrainColliders = (this.game?.terrains ?? []).map((t) => t.hull) + + return entityColliders.concat(terrainColliders) + } + isColliding(...colliders) { return SATX.collideObjects(this.collider, colliders) } - // TODO: calculate convex hulls of collidables because tris handle collisions sequentially moveAction(x, y) { - const temp = SATX.fixCollisions(new Vector2(x, y), this.collidables(), this.radius) - // const entity = new Entity() - // entity.teleport(temp.x, temp.y) - // entity.radius = this.radius - // this.game.spawn_entity(entity) - this.#dest = SATX.clamp( - temp, - this.game?.width, - this.game?.height, - this.radius, - ) + this.#dest = SATX.fixCollisions(new Vector2(x, y), this.collidables(), this.radius, this.game?.width, this.game?.height) } state() { @@ -97,7 +94,6 @@ export default class Entity { this.position.set(x, y) } - // TODO: pathfinding stops if wall is clicked (did you forget to fix the destination?) takeStep(distanceTraveled = 0) { const speed = (this.speed / (this.game?.tickBudget ?? 1000)) - distanceTraveled const collidables = this.collidables() @@ -105,14 +101,14 @@ export default class Entity { const fixedDest = SATX.clamp(SATX.fixCollisions(this.#dest, collidables, this.radius), this.game?.width, this.game?.height, this.radius) if (this.#path.length < 1 || !this.#path.at(-1).equals(fixedDest)) { - console.time('pathfinding') + // console.time('pathfinding') const start = SATX.vectorToFloat32Array(this.position) const goal = SATX.vectorToFloat32Array(fixedDest) const nonUniqueWaypoints = this.waypoints().map((w) => SATX.vectorToFloat32Array(w)).concat([start, goal]) const waypoints = Pathfind.uniqueWaypoints(nonUniqueWaypoints) const graph = Pathfind.buildGraph(waypoints, collidables, this.radius) this.#path = Pathfind.shortestPath(graph, start, goal).map((waypoint) => new Vector2(waypoint[0], waypoint[1])) - console.timeEnd('pathfinding') + // console.timeEnd('pathfinding') } if (this.#path.length > 0) { diff --git a/src/game.js b/src/game.js index 79128ef..515fa12 100644 --- a/src/game.js +++ b/src/game.js @@ -34,6 +34,10 @@ export default class Game { this.#terrains.push(terrain) } + remove_terrain(terrain) { + this.#terrains = this.#terrains.filter((t) => t.id != terrain.id) + } + state() { return { ...this, diff --git a/src/index.js b/src/index.js index 97fbbbb..ac72423 100644 --- a/src/index.js +++ b/src/index.js @@ -25,6 +25,10 @@ app.ws('/ws', async (req, res) => { websocket.on('message', (rawData) => { const message = JSON.parse(rawData) const entity = message.id != null ? game.entities.find((e) => e.id == message.id) : null + if (entity == null) { + console.log({ error: { reason: 'Invalid ID', message } }) + return + } console.log(message) if (message.action == 'teleport') { @@ -46,11 +50,11 @@ app.listen(port, () => { entity1.radius = 50 game.spawn_entity(entity1) - const entity2 = new Entity() - entity2.id = '2' - entity2.teleport(110, 110) - entity2.radius = 50 - game.spawn_entity(entity2) + // const entity2 = new Entity() + // entity2.id = '2' + // entity2.teleport(110, 110) + // entity2.radius = 50 + // game.spawn_entity(entity2) // const triangle = new Terrain([ // { x: 400, y: 200 }, @@ -115,8 +119,7 @@ app.listen(port, () => { pole.id = 'pole' game.add_terrain(pole) - // entity1.moveAction(1000, 500) - + // entity1.moveAction(750, 950) // setTimeout(() => entity1.moveAction(100, 400), 10) game.start() diff --git a/src/satx.js b/src/satx.js index d940df8..94a06f6 100644 --- a/src/satx.js +++ b/src/satx.js @@ -69,18 +69,32 @@ export default class SATX { ]) } - static fixCollisions(entityPosition, colliders, radius = 0) { - const position = entityPosition.clone() - let collider = Entity.collider(position.x, position.y, radius) - colliders.forEach((c) => { - let result = new SAT.Response() - if (this.collideObject(collider, c, result)) { - position.sub(new Vector2(result.overlapV.x, result.overlapV.y)) - collider = Entity.collider(position.x, position.y, radius) - } - }) + static fixCollisions(entityPosition, colliders, radius = 0, maxX = Infinity, maxY = Infinity) { + if (!this.collideObjects(Entity.collider(entityPosition.x, entityPosition.y, radius), colliders)) { + return entityPosition + } + // console.time('fixCollisions') - return position + let direction = new Vector2(0, 5) + let multiplier = 1 + const rotationSlices = 16 + + for (let limit = 1; limit <= 10000; limit++) { + const rads = (limit % rotationSlices) * 2 * Math.PI / rotationSlices + const offset = direction.clone().rotateAround(new Vector2(), rads).multiplyScalar(multiplier) + const position = SATX.clamp(entityPosition.clone().add(offset), maxX, maxY, radius) + if (!this.collideObjects(Entity.collider(position.x, position.y, radius), colliders)) { + // console.timeEnd('fixCollisions') + return position + } + + if (limit % rotationSlices == 0) { + multiplier++ + } + } + + // console.timeEnd('fixCollisions') + console.error('ERROR: can\'t fix collision') } static line(fromX, fromY, toX, toY) { diff --git a/src/terrain.js b/src/terrain.js index a257480..d6a8a53 100644 --- a/src/terrain.js +++ b/src/terrain.js @@ -1,3 +1,4 @@ +import QuickHull from 'quickhull' // DEPRECATED: hulls were a failed concept for position fixing import SAT from 'sat' import { Shape, ShapeUtils, Vector2 } from 'three' @@ -8,6 +9,7 @@ export default class Terrain { relativeVertices = [] #colliders = [] + #hull = null #vertices = [] #unadjustedWaypoints = [] @@ -24,6 +26,7 @@ export default class Terrain { } get colliders() { return this.#colliders } + get hull() { return this.#hull } // DEPRECATED: hulls were a failed concept for position fixing get unadjustedWaypoints() { return this.#unadjustedWaypoints } get vertices() { return this.#vertices } @@ -73,6 +76,16 @@ export default class Terrain { } this.#colliders = ShapeUtils.triangulateShape(points.shape, points.holes).map(indicesToPolygon) + this.#calculateHull() + } + + // DEPRECATED: hulls were a failed concept for position fixing + #calculateHull() { + const vertices = QuickHull(this.#vertices.map((v) => ({ x: v.x, y: v.y }))).map((v) => new Vector2(v.x, v.y)) + const first = vertices.at(0) + const satPoints = [new SAT.Vector(...first.toArray()), ...vertices.slice(1).map((v) => new SAT.Vector(...v.clone(first).sub(first).toArray()))] + + this.#hull = new SAT.Polygon(satPoints[0], [new SAT.Vector(), ...satPoints.slice(1)]) } #calculatePosition() {