From 054d22d01a1cddd56b01d781d2e592448cc30b27 Mon Sep 17 00:00:00 2001 From: Thayol Date: Sun, 22 Dec 2024 23:52:56 +0900 Subject: [PATCH] replace most systems with THREE --- package-lock.json | 36 ------------------------- package.json | 2 -- public/index.html | 1 + public/main.js | 56 ++++++++++++++++++++++++++++++--------- src/entity.js | 67 +++++++++++++++++++++++++++++++---------------- src/game.js | 20 +++++++++----- src/index.js | 12 ++++----- 7 files changed, 107 insertions(+), 87 deletions(-) diff --git a/package-lock.json b/package-lock.json index 50daf47..df9ae8f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,9 +9,7 @@ "version": "1.0.0", "license": "UNLICENSED", "dependencies": { - "detect-collisions": "^9.24.0", "three": "^0.171.0", - "victor": "^1.1.0", "websocket-express": "^3.1.2" } }, @@ -100,12 +98,6 @@ "license": "MIT", "peer": true }, - "node_modules/@types/sat": { - "version": "0.0.35", - "resolved": "https://registry.npmjs.org/@types/sat/-/sat-0.0.35.tgz", - "integrity": "sha512-e6hhgprjvfncsniUtblt2Q8yV+cMf/Vqxl4CrnhkwPML4Lb7F+1fMNZs+UJ4L6yp2j/vDH8oHpNaubM0rgHXTQ==", - "license": "MIT" - }, "node_modules/@types/send": { "version": "0.17.4", "resolved": "https://registry.npmjs.org/@types/send/-/send-0.17.4.tgz", @@ -297,17 +289,6 @@ "npm": "1.2.8000 || >= 1.4.16" } }, - "node_modules/detect-collisions": { - "version": "9.24.0", - "resolved": "https://registry.npmjs.org/detect-collisions/-/detect-collisions-9.24.0.tgz", - "integrity": "sha512-99PXmFG7KAu8qYtnEWPH7saC0LTkINMbjB2pERpDcngNMtxPyZV61Q9/yYsdQCa65PM72CHfhDPQVXvftiScLw==", - "license": "MIT", - "dependencies": { - "@types/sat": "^0.0.35", - "poly-decomp-es": "^0.4.2", - "sat": "^0.9.0" - } - }, "node_modules/dunder-proto": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", @@ -733,12 +714,6 @@ "license": "MIT", "peer": true }, - "node_modules/poly-decomp-es": { - "version": "0.4.2", - "resolved": "https://registry.npmjs.org/poly-decomp-es/-/poly-decomp-es-0.4.2.tgz", - "integrity": "sha512-akjZkaTeyfECMNkMqv0g9gFiU2HjpZ5ubJA/aczw1ILdgWQbv1OPwCi05/9JbY2GnGpjm9k85Jdbd/j/p/lg/g==", - "license": "MIT" - }, "node_modules/proxy-addr": { "version": "2.0.7", "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", @@ -823,12 +798,6 @@ "license": "MIT", "peer": true }, - "node_modules/sat": { - "version": "0.9.0", - "resolved": "https://registry.npmjs.org/sat/-/sat-0.9.0.tgz", - "integrity": "sha512-mxdv5RZJO4tdMnUURGU3gAMcnDUEwcNJwE+lPO0/V+rBeDvFLH3wEZEOR0fH7cTN0zQaNxBEbHnyQL9DzupwQQ==", - "license": "MIT" - }, "node_modules/send": { "version": "0.19.0", "resolved": "https://registry.npmjs.org/send/-/send-0.19.0.tgz", @@ -1047,11 +1016,6 @@ "node": ">= 0.8" } }, - "node_modules/victor": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/victor/-/victor-1.1.0.tgz", - "integrity": "sha512-s+EQHqIs5TNgLa3HHPr8oCpWFulQZzDX/PLaskVDeWXqEW12l54H5XD+cZ3gXrgvdOscqEcdAMXgEUZlJckQ4w==" - }, "node_modules/websocket-express": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/websocket-express/-/websocket-express-3.1.2.tgz", diff --git a/package.json b/package.json index a91b29b..463a8c3 100644 --- a/package.json +++ b/package.json @@ -10,9 +10,7 @@ "license": "UNLICENSED", "description": "", "dependencies": { - "detect-collisions": "^9.24.0", "three": "^0.171.0", - "victor": "^1.1.0", "websocket-express": "^3.1.2" } } diff --git a/public/index.html b/public/index.html index 192bf27..aa0c7cd 100644 --- a/public/index.html +++ b/public/index.html @@ -34,6 +34,7 @@ border: 5px solid gray; border-top: none; border-right: none; + width: 400px; } diff --git a/public/main.js b/public/main.js index 524ba2c..041fe35 100644 --- a/public/main.js +++ b/public/main.js @@ -3,25 +3,35 @@ import { GLTFLoader } from 'three/addons/loaders/GLTFLoader.js' const global = (0,eval)("this") const scene = new THREE.Scene() -const camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000) +const camera = new THREE.PerspectiveCamera(30, window.innerWidth / window.innerHeight, 0.1, 1000) const raycaster = new THREE.Raycaster() const renderer = new THREE.WebGLRenderer() const entities = {} renderer.setSize(window.innerWidth, window.innerHeight) renderer.setAnimationLoop(animate) -const geometry = new THREE.PlaneGeometry(1000, 1000) -const material = new THREE.MeshBasicMaterial({ color: 0x115011 }) +const geometry = new THREE.PlaneGeometry(0, 0) +const material = new THREE.MeshToonMaterial({ color: 0x115011 }) const ground = new THREE.Mesh(geometry, material) scene.add(ground) +const ambientLight = new THREE.AmbientLight(0x404040) +scene.add(ambientLight) + +const directionalLight = new THREE.DirectionalLight(0xffffff, 2.5) +directionalLight.position.set(-0.3, 0.1, 1) +directionalLight.power = 3000 +scene.add(directionalLight) + // const loader = new GLTFLoader() // loader.load('player.gltf', (gltf) => scene.add(gltf.scene), undefined, (err) => console.log(err)) +global.THREE = THREE +global.renderer = renderer global.camera = camera global.scene = scene -camera.position.set(0, -250, 500) -camera.rotation.set((30 / 180) * Math.PI, 0, 0) +camera.position.set(3, -12, 10) +camera.rotation.set((60 / 180) * Math.PI, 0, 0) // camera.lookAt(0, 0, 0) function animate() { @@ -52,20 +62,26 @@ function connectWebSocket() { websocket.onmessage = (event) => { let state = JSON.parse(event.data) + + if (state.width != null && state.height != null && (ground.geometry.attributes.width != state.width || ground.geometry.attributes.height != state.height)) { + ground.geometry = new THREE.PlaneGeometry(state.width / 100, state.height / 100) + ground.position.set(state.width / 200, state.height / 200, 0) + } + for (const e of state.entities) { let entity if (e.id in entities) { entity = entities[e.id] } else { - entity = new THREE.Mesh(new THREE.SphereGeometry(e.radius), new THREE.MeshBasicMaterial({ color: 0xffffff, wireframe: true })) + entity = new THREE.Mesh(new THREE.SphereGeometry(e.radius / 100), new THREE.MeshToonMaterial({ color: 0xffffff })) entity.userData.type = 'entity' entity.userData.id = e.id scene.add(entity) entities[e.id] = entity } - entity.position.set(e.pos.x, e.pos.y, e.radius) + entity.position.set(e.pos.x / 100, e.pos.y / 100, e.radius / 100) } document.getElementById('state').innerHTML = JSON.stringify(state, null, 2) @@ -78,11 +94,19 @@ window.addEventListener('load', () => { const canvas = renderer.domElement canvas.addEventListener('mousedown', (event) => { raycaster.setFromCamera(new THREE.Vector2((event.clientX / canvas.clientWidth) * 2 - 1, (event.clientY / canvas.clientHeight) * -2 + 1), camera) - const intersect = raycaster.intersectObject(ground).at(0) - if (event.button == 0) { - const x = Math.round(intersect.point.x) - const y = Math.round(intersect.point.y) - websocket.send(JSON.stringify({ action: 'move', id: '1', x, y })) + const intersect = raycaster.intersectObject(ground).at(0)?.point + if (intersect != null) { + if (event.button == 0) { + const x = Math.round(intersect.x * 100) + const y = Math.round(intersect.y * 100) + websocket.send(JSON.stringify({ action: 'move', id: '2', x, y })) + } + + if (event.button == 2) { + const x = Math.round(intersect.x * 100) + const y = Math.round(intersect.y * 100) + websocket.send(JSON.stringify({ action: 'move', id: '1', x, y })) + } } }) @@ -103,5 +127,13 @@ window.addEventListener('load', () => { } }) + window.addEventListener("resize", (event) => { + camera.aspect = window.innerWidth / window.innerHeight + camera.updateProjectionMatrix() + renderer.setSize(window.innerWidth, window.innerHeight) + }) + + document.addEventListener("contextmenu", (event) => event.preventDefault()) + document.body.appendChild(canvas) }) diff --git a/src/entity.js b/src/entity.js index 92d4af0..94d89a5 100644 --- a/src/entity.js +++ b/src/entity.js @@ -1,51 +1,72 @@ -import { Circle } from 'detect-collisions' -import Victor from 'victor' +import * as THREE from 'three' export default class Entity { id = crypto.randomUUID() - pos = new Victor(0, 0) - speed = 280 - radius = 0 - #collider = null + speed = 400 + #dest = null #game = null + #mesh = null constructor(...options) { Object.entries(options).forEach((value, key) => this[key] = value) - this.#collider = new Circle(this.pos, this.radius) + const geometry = new THREE.CircleGeometry(options.radius ?? 0) + this.#mesh = new THREE.Mesh(geometry) } - get collider() { return this.#collider } get game() { return this.#game } - get system() { return this.game?.system } + get mesh() { return this.#mesh } + get pos() { return this.#mesh.position } + get radius() { return this.#mesh.userData.radius } + get x() { return this.#mesh.position.x } + get y() { return this.#mesh.position.y } set game(value) { this.#game = value } + set x(value) { this.#mesh.position.x = value } + set y(value) { this.#mesh.position.y = value } + + set radius(value) { + this.#mesh.geometry = new THREE.CircleGeometry(value) + this.#mesh.userData.radius = value + } moveAction(x, y) { - this.#dest = new Victor(x, y) + this.#dest = new THREE.Vector3(x, y, 0) } - async takeStep() { + state() { + return { + ...this, + pos: { + x: this.x, + y: this.y, + }, + radius: this.radius, + } + } + + teleport(x, y) { + this.#mesh.position.set(x, y, 0) + } + + takeStep() { const speed = this.speed / (this.game?.tickBudget ?? 1000) if (this.#dest != null) { - this.pos.add(this.#dest.clone().subtract(this.pos).normalize().multiplyScalar(speed)) - if (this.pos.clone().subtract(this.#dest).length() <= speed) { - this.pos = this.#dest + const fixedDest = new THREE.Vector3( + Math.min(Math.max(this.radius, this.#dest.x), this.game?.height ?? Infinity), + Math.min(Math.max(this.radius, this.#dest.y), this.game?.height ?? Infinity), + 0, + ) + + this.pos.add(fixedDest.clone().sub(this.pos).normalize().multiplyScalar(speed)) + if (this.pos.clone().sub(fixedDest).length() <= speed) { + this.pos.copy(fixedDest) this.#dest = null } } } - async updateCollider() { - if (this.pos.x != this.collider.pos.x || this.pos.y != this.collider.pos.y || this.radius != this.collider.unscaledRadius) { - this.system?.remove(this.#collider) - this.#collider = new Circle(this.pos, this.radius) - this.system?.insert(this.#collider) - } - } - async update() { await Promise.allSettled([ - this.updateCollider(), this.takeStep(), ]) } diff --git a/src/game.js b/src/game.js index 4f0effb..9584cd5 100644 --- a/src/game.js +++ b/src/game.js @@ -1,25 +1,31 @@ -import { System } from 'detect-collisions' import { EventEmitter } from 'node:events' export default class Game { - entities = [] tickRate = 30 currentTick = 0 + width = 15000 + height = 15000 + #entities = [] #eventEmitter = new EventEmitter() - #system = new System() #tickBudget = Math.floor(1000 / this.tickRate) + get entities() { return this.#entities } get eventEmitter() { return this.#eventEmitter } - get system() { return this.#system } get tickBudget() { return this.#tickBudget } spawn_entity(entity) { - this.entities.push(entity) - this.#system.insert(entity.collider) + this.#entities.push(entity) entity.game = this } + state() { + return { + ...this, + entities: this.#entities.map((e) => e.state()), + } + } + async start() { const start = performance.now() await this.update() @@ -27,7 +33,7 @@ export default class Game { } async update() { - Promise.allSettled(this.entities.map((e) => e.update())) + Promise.allSettled(this.#entities.map((e) => e.update())) this.currentTick++ this.eventEmitter.emit('tick') } diff --git a/src/index.js b/src/index.js index 116fb61..aa795ed 100644 --- a/src/index.js +++ b/src/index.js @@ -2,7 +2,6 @@ import express from 'express' import { WebSocketExpress } from 'websocket-express' import Game from './game.js' import Entity from './entity.js' -import Victor from 'victor' const app = new WebSocketExpress() const port = 1280 @@ -15,7 +14,7 @@ app.use(express.urlencoded({ extended: true })) app.ws('/ws', async (req, res) => { const websocket = await res.accept() - const subscription = () => websocket.send(JSON.stringify(game)) + const subscription = () => websocket.send(JSON.stringify(game.state())) game.eventEmitter.on('tick', subscription) websocket.on('close', () => { @@ -24,15 +23,14 @@ 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 console.log(message) if (message.action == 'teleport') { - const entity = game.entities.find((e) => e.id == message.id) - entity.pos = new Victor(message.x, message.y) + entity.teleport(message.x, message.y) } if (message.action == 'move') { - const entity = game.entities.find((e) => e.id == message.id) entity.moveAction(message.x, message.y) } }) @@ -43,13 +41,13 @@ app.listen(port, () => { const entity = new Entity() entity.id = '1' - entity.pos = new Victor(0, 0) + entity.teleport(0, 0) entity.radius = 35 game.spawn_entity(entity) const entity2 = new Entity() entity2.id = '2' - entity2.pos = new Victor(200, 100) + entity.teleport(200, 100) entity2.radius = 35 game.spawn_entity(entity2)