From ba0d8f606a3a874c376f7780129d9326c743776c Mon Sep 17 00:00:00 2001 From: Thayol Date: Mon, 23 Dec 2024 11:57:36 +0900 Subject: [PATCH] add client side terrain without collision --- .gitignore | 2 ++ src/entity.js | 8 +++---- src/game.js | 7 ++++++ src/index.js | 26 ++++++++++++++++----- src/satx.js | 12 ++++++++++ src/terrain.js | 61 ++++++++++++++++++++++++++++++++++++++++++++++++++ 6 files changed, 107 insertions(+), 9 deletions(-) create mode 100644 src/terrain.js diff --git a/.gitignore b/.gitignore index c6bba59..ea01008 100644 --- a/.gitignore +++ b/.gitignore @@ -128,3 +128,5 @@ dist .yarn/build-state.yml .yarn/install-state.gz .pnp.* + +public/temp diff --git a/src/entity.js b/src/entity.js index f01e7b9..720eee4 100644 --- a/src/entity.js +++ b/src/entity.js @@ -1,4 +1,4 @@ -import * as THREE from 'three' +import { Vector2 } from 'three' import SAT from 'sat' import SATX from './satx.js' @@ -7,7 +7,7 @@ export default class Entity { speed = 400 radius = 0 - #position = new THREE.Vector2(0, 0) + #position = new Vector2() #dest = null #game = null @@ -40,7 +40,7 @@ export default class Entity { } moveAction(x, y) { - this.#dest = new THREE.Vector3(x, y, 0) + this.#dest = new Vector2(x, y) } state() { @@ -60,7 +60,7 @@ export default class Entity { takeStep() { const speed = this.speed / (this.game?.tickBudget ?? 1000) if (this.#dest != null) { - const fixedDest = new THREE.Vector2( + const fixedDest = new Vector2( Math.min(Math.max(this.radius, this.#dest.x), (this.game?.width ?? Infinity) - this.radius), Math.min(Math.max(this.radius, this.#dest.y), (this.game?.height ?? Infinity) - this.radius), ) diff --git a/src/game.js b/src/game.js index ddf19c6..b794d59 100644 --- a/src/game.js +++ b/src/game.js @@ -8,10 +8,12 @@ export default class Game { #entities = [] #eventEmitter = new EventEmitter() + #terrains = [] #tickBudget = Math.floor(1000 / this.tickRate) get entities() { return this.#entities } get eventEmitter() { return this.#eventEmitter } + get terrains() { return this.#terrains } get tickBudget() { return this.#tickBudget } spawn_entity(entity) { @@ -19,10 +21,15 @@ export default class Game { entity.game = this } + add_terrain(terrain) { + this.#terrains.push(terrain) + } + state() { return { ...this, entities: this.#entities.map((e) => e.state()), + terrains: this.#terrains.map((t) => t.state()), } } diff --git a/src/index.js b/src/index.js index 8362dcb..4d3053c 100644 --- a/src/index.js +++ b/src/index.js @@ -2,6 +2,7 @@ import express from 'express' import { WebSocketExpress } from 'websocket-express' import Game from './game.js' import Entity from './entity.js' +import Terrain from './terrain.js' const app = new WebSocketExpress() const port = 1280 @@ -39,11 +40,11 @@ app.ws('/ws', async (req, res) => { app.listen(port, () => { console.log(`Server started! Visit http://localhost:${port}`) - const entity = new Entity() - entity.id = '1' - entity.teleport(100, 100) - entity.radius = 35 - game.spawn_entity(entity) + const entity1 = new Entity() + entity1.id = '1' + entity1.teleport(100, 100) + entity1.radius = 35 + game.spawn_entity(entity1) const entity2 = new Entity() entity2.id = '2' @@ -51,5 +52,20 @@ app.listen(port, () => { entity2.radius = 35 game.spawn_entity(entity2) + const vertices = [ + { x: 0, y: 0 }, + { x: 20, y: 0 }, + { x: 20, y: 20 }, + { x: 10, y: 20 }, + { x: 10, y: 5 }, + { x: 5, y: 5 }, + { x: 5, y: 20 }, + { x: 0, y: 20 }, + ] + + const terrain1 = new Terrain(vertices) + terrain1.id = 'a' + game.add_terrain(terrain1) + game.start() }) diff --git a/src/satx.js b/src/satx.js index ca74c2f..76e4b63 100644 --- a/src/satx.js +++ b/src/satx.js @@ -6,6 +6,18 @@ export default class SATX { return SAT.testCircleCircle(collider1, collider2) } + if (collider1 instanceof SAT.Circle && collider2 instanceof SAT.Polygon) { + return SAT.testCirclePolygon(collider1, collider2) + } + + if (collider1 instanceof SAT.Polygon && collider2 instanceof SAT.Circle) { + return SAT.testPolygonCircle(collider1, collider2) + } + + if (collider1 instanceof SAT.Polygon && collider2 instanceof SAT.Polygon) { + return SAT.testPolygonPolygon(collider1, collider2) + } + return false } } diff --git a/src/terrain.js b/src/terrain.js new file mode 100644 index 0000000..6978dd3 --- /dev/null +++ b/src/terrain.js @@ -0,0 +1,61 @@ +import SAT from 'sat' +import { Shape, ShapeUtils, Vector2 } from 'three' + +export default class Terrain { + id = crypto.randomUUID() + + position = new Vector2() + relativeVertices = [] + + #colliders = [] + #vertices = [] + + constructor(vertices) { + this.#vertices = vertices.map((v) => new Vector2(v.x, v.y)) + this.#calculateColliders() + this.#calculatePosition() + this.#calculateRelativeVertices() + } + + get colliders() { return this.#colliders } + get vertices() { return this.#vertices } + + state() { + return { + ...this, + position: { + x: this.x, + y: this.y, + }, + } + } + + #calculateColliders() { + const complexShape = new Shape() + + complexShape.moveTo(this.#vertices.at(0).x, this.#vertices.at(0).y) + this.#vertices.slice(1).forEach((v) => complexShape.lineTo(v.x, v.y)) + + const points = complexShape.extractPoints(16) + + const indicesToPolygon = (indices) => { + const satPoints = [ + new SAT.Vector(...points.shape[indices[0]].toArray()), + new SAT.Vector(...points.shape[indices[1]].toArray()), + new SAT.Vector(...points.shape[indices[2]].toArray()), + ] + + return new SAT.Polygon(satPoints[0], [new SAT.Vector(), satPoints[1], satPoints[2]]) + } + + this.#colliders = ShapeUtils.triangulateShape(points.shape, points.holes).map(indicesToPolygon) + } + + #calculatePosition() { + this.position = this.#vertices.reduce(((sum, v) => sum.add(v)), new Vector2()).divideScalar(this.#vertices.length) + } + + #calculateRelativeVertices() { + this.relativeVertices = this.#vertices.map((v) => v.clone().sub(this.position)) + } +}