initial commit
This commit is contained in:
commit
b2a0e586b2
4
.gitignore
vendored
Normal file
4
.gitignore
vendored
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
/dist
|
||||||
|
/node_modules
|
||||||
|
/.idea
|
||||||
|
package-lock.json
|
11
index.html
Normal file
11
index.html
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<title>Troonia</title>
|
||||||
|
<style>html, body {margin: 0; padding: 0; width: 100%; height: 100%;}</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<script src="bundle.js" type="module"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
37
package.json
Normal file
37
package.json
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
{
|
||||||
|
"name": "troonia",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"author": "vance",
|
||||||
|
"type": "module",
|
||||||
|
"scripts": {
|
||||||
|
"build": "concurrently \"tsc -p ./src/server\" \"webpack --config ./webpack.prod.cjs\"",
|
||||||
|
"dev": "concurrently -k \"tsc -p ./src/server -w\" \"nodemon ./dist/server/server/main.js\" \"webpack serve --config ./webpack.dev.cjs\"",
|
||||||
|
"test": "echo \"Error: no test specified\" && exit 1",
|
||||||
|
"start": "node ./dist/server/server/main.js",
|
||||||
|
"format": "find ./src -type f -exec clang-format -i -style=Google {} \\;"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/dat.gui": "^0.7.7",
|
||||||
|
"@types/express": "^4.17.14",
|
||||||
|
"@types/node": "^17.0.45",
|
||||||
|
"@types/three": "^0.144.0",
|
||||||
|
"concurrently": "^7.5.0",
|
||||||
|
"copy-webpack-plugin": "^11.0.0",
|
||||||
|
"nodemon": "^2.0.20",
|
||||||
|
"ts-loader": "^9.4.1",
|
||||||
|
"typescript": "^4.8.4",
|
||||||
|
"webpack": "^5.74.0",
|
||||||
|
"webpack-cli": "^4.10.0",
|
||||||
|
"webpack-dev-server": "^4.11.1",
|
||||||
|
"webpack-merge": "^5.8.0"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"cannon-es": "^0.20.0",
|
||||||
|
"dat.gui": "^0.7.9",
|
||||||
|
"express": "^4.18.2",
|
||||||
|
"socket.io": "^4.5.3",
|
||||||
|
"socket.io-client": "^4.5.3",
|
||||||
|
"three": "^0.145.0",
|
||||||
|
"@tweenjs/tween.js": "^18.6.4"
|
||||||
|
}
|
||||||
|
}
|
20
src/client/main.ts
Normal file
20
src/client/main.ts
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
import {Troonia} from './troonia';
|
||||||
|
import {get_viewport_size} from './util';
|
||||||
|
|
||||||
|
// noinspection SpellCheckingInspection
|
||||||
|
const troonia = new Troonia();
|
||||||
|
|
||||||
|
window.addEventListener('resize', resize, false);
|
||||||
|
|
||||||
|
function animate() {
|
||||||
|
requestAnimationFrame(animate);
|
||||||
|
troonia.update();
|
||||||
|
}
|
||||||
|
animate();
|
||||||
|
|
||||||
|
function resize() {
|
||||||
|
const viewport = get_viewport_size();
|
||||||
|
troonia.camera.aspect = viewport.width / viewport.height;
|
||||||
|
troonia.camera.updateProjectionMatrix();
|
||||||
|
troonia.renderer.setSize(viewport.width, viewport.height);
|
||||||
|
}
|
23
src/client/physics.ts
Normal file
23
src/client/physics.ts
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
import {BasePhysics, IPhysicsObject} from '../common/physics';
|
||||||
|
import type {World} from './world';
|
||||||
|
import * as TWEEN from '@tweenjs/tween.js';
|
||||||
|
import type {Quaternion} from 'three';
|
||||||
|
|
||||||
|
export class Physics extends BasePhysics {
|
||||||
|
constructor(baseWorld: World) {
|
||||||
|
super(baseWorld);
|
||||||
|
}
|
||||||
|
|
||||||
|
override setPos(object: IPhysicsObject, body: IPhysicsObject) {
|
||||||
|
new TWEEN.Tween(object.position).to(body.position, 50).start();
|
||||||
|
new TWEEN.Tween(object.quaternion)
|
||||||
|
.to(body.quaternion, 50)
|
||||||
|
.onUpdate(() => (<Quaternion>object.quaternion).normalize())
|
||||||
|
.start();
|
||||||
|
}
|
||||||
|
|
||||||
|
// @ts-ignore
|
||||||
|
update(delta: number) {
|
||||||
|
TWEEN.update();
|
||||||
|
}
|
||||||
|
}
|
127
src/client/troonia.ts
Normal file
127
src/client/troonia.ts
Normal file
@ -0,0 +1,127 @@
|
|||||||
|
import type {IBaseTroonia, TrooniaState} from '../common/troonia';
|
||||||
|
import {io, Socket} from 'socket.io-client';
|
||||||
|
import {World} from './world';
|
||||||
|
import {get_viewport_size} from './util';
|
||||||
|
import {PerspectiveCamera, WebGLRenderer} from 'three';
|
||||||
|
import {GUI} from 'dat.gui';
|
||||||
|
import {OrbitControls} from 'three/examples/jsm/controls/OrbitControls';
|
||||||
|
import {BaseTroonia} from '../common/troonia';
|
||||||
|
import {TestScene} from '../common/scenes';
|
||||||
|
import type {IPlayerData} from '../common/player';
|
||||||
|
|
||||||
|
|
||||||
|
export class Troonia extends BaseTroonia implements IBaseTroonia {
|
||||||
|
socket: Socket;
|
||||||
|
renderer: WebGLRenderer;
|
||||||
|
camera: PerspectiveCamera;
|
||||||
|
gui: GUI = new GUI();
|
||||||
|
world: World;
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
super();
|
||||||
|
this.socket = io();
|
||||||
|
|
||||||
|
const viewport = get_viewport_size();
|
||||||
|
|
||||||
|
this.renderer = new WebGLRenderer({antialias: true});
|
||||||
|
this.renderer.setSize(viewport.width, viewport.height);
|
||||||
|
document.body.appendChild(this.renderer.domElement);
|
||||||
|
|
||||||
|
this.camera =
|
||||||
|
new PerspectiveCamera(75, viewport.width / viewport.height, 0.1, 2000);
|
||||||
|
this.camera.position.set(0, 50, 0);
|
||||||
|
this.camera.lookAt(0, 0, 0);
|
||||||
|
|
||||||
|
const controls = new OrbitControls(this.camera, this.renderer.domElement);
|
||||||
|
controls.target.set(0, 1.25, 0);
|
||||||
|
controls.update();
|
||||||
|
|
||||||
|
this.socket.on('connect', () => {
|
||||||
|
this.onConnect();
|
||||||
|
});
|
||||||
|
this.socket.on('disconnect', (message: any) => {
|
||||||
|
this.onDisconnect(message);
|
||||||
|
});
|
||||||
|
|
||||||
|
this.world = new World(this);
|
||||||
|
this.world.load(TestScene);
|
||||||
|
|
||||||
|
this.socket.on('syncState', (state: TrooniaState) => {
|
||||||
|
this.world.physics.bodies = state.objects;
|
||||||
|
this.world.physics.syncState();
|
||||||
|
});
|
||||||
|
|
||||||
|
this.socket.on('syncPlayers', (players: {[id: string]: IPlayerData}) => {
|
||||||
|
for (const player in players) {
|
||||||
|
if (player !== this.socket.id) {
|
||||||
|
this.world.addPlayer(player);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
this.socket.on('addPlayer', (id: string) => {
|
||||||
|
if (id !== this.socket.id) {
|
||||||
|
this.world.addPlayer(id);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
this.socket.on('removePlayer', (id: string) => {
|
||||||
|
if (id !== this.socket.id) {
|
||||||
|
this.world.removePlayer(id);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
document.addEventListener('keydown', (event: KeyboardEvent) => {
|
||||||
|
switch (event.code) {
|
||||||
|
case 'KeyW':
|
||||||
|
this.world.player.vec3.z = 1;
|
||||||
|
break;
|
||||||
|
case 'KeyA':
|
||||||
|
this.world.player.vec3.x = 1;
|
||||||
|
break;
|
||||||
|
case 'Space':
|
||||||
|
this.world.player.vec3.y = 1;
|
||||||
|
break;
|
||||||
|
case 'KeyS':
|
||||||
|
this.world.player.vec3.z = -1;
|
||||||
|
break;
|
||||||
|
case 'KeyD':
|
||||||
|
this.world.player.vec3.x = -1;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
this.socket.emit('updatePlayer', this.world.player);
|
||||||
|
}, false);
|
||||||
|
document.addEventListener('keyup', (event: KeyboardEvent) => {
|
||||||
|
switch (event.code) {
|
||||||
|
case 'KeyW':
|
||||||
|
case 'KeyS':
|
||||||
|
this.world.player.vec3.z = 0;
|
||||||
|
break;
|
||||||
|
case 'KeyA':
|
||||||
|
case 'KeyD':
|
||||||
|
this.world.player.vec3.x = 0;
|
||||||
|
break;
|
||||||
|
case 'Space':
|
||||||
|
this.world.player.vec3.y = 0;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
this.socket.emit('updatePlayer', this.world.player);
|
||||||
|
}, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
onConnect() {
|
||||||
|
console.log('Connected to Troonia.');
|
||||||
|
this.world.addPlayer();
|
||||||
|
}
|
||||||
|
|
||||||
|
onDisconnect(message: any) {
|
||||||
|
this.world.removePlayer();
|
||||||
|
console.log(`Disconnected from Troonia: ${message}.`);
|
||||||
|
}
|
||||||
|
|
||||||
|
update() {
|
||||||
|
const delta = this.clock.getDelta();
|
||||||
|
this.world.update(delta);
|
||||||
|
this.renderer.render(this.world.scene, this.camera);
|
||||||
|
}
|
||||||
|
}
|
33
src/client/tsconfig.json
Normal file
33
src/client/tsconfig.json
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
{
|
||||||
|
"$schema": "https://json.schemastore.org/tsconfig",
|
||||||
|
"display": "Node 16 + ESM + Strictest",
|
||||||
|
"compilerOptions": {
|
||||||
|
"lib": [
|
||||||
|
"ES2021",
|
||||||
|
"dom"
|
||||||
|
],
|
||||||
|
"module": "ES2022",
|
||||||
|
"moduleResolution": "Node",
|
||||||
|
"target": "ES2021",
|
||||||
|
"strict": true,
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"forceConsistentCasingInFileNames": true,
|
||||||
|
"allowUnusedLabels": false,
|
||||||
|
"allowUnreachableCode": false,
|
||||||
|
"exactOptionalPropertyTypes": true,
|
||||||
|
"noFallthroughCasesInSwitch": true,
|
||||||
|
"noImplicitOverride": true,
|
||||||
|
"noImplicitReturns": true,
|
||||||
|
"noPropertyAccessFromIndexSignature": true,
|
||||||
|
"noUncheckedIndexedAccess": true,
|
||||||
|
"noUnusedLocals": true,
|
||||||
|
"noUnusedParameters": true,
|
||||||
|
"importsNotUsedAsValues": "error",
|
||||||
|
"outDir": "../../dist/client/"
|
||||||
|
},
|
||||||
|
"include": [
|
||||||
|
"../client/*.ts",
|
||||||
|
"../common/*.ts"
|
||||||
|
]
|
||||||
|
}
|
7
src/client/util.ts
Normal file
7
src/client/util.ts
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
export function get_viewport_size(): {width: number, height: number} {
|
||||||
|
const vw = Math.max(
|
||||||
|
document.documentElement.clientWidth || 0, window.innerWidth || 0);
|
||||||
|
const vh = Math.max(
|
||||||
|
document.documentElement.clientHeight || 0, window.innerHeight || 0);
|
||||||
|
return {width: vw, height: vh};
|
||||||
|
}
|
51
src/client/world.ts
Normal file
51
src/client/world.ts
Normal file
@ -0,0 +1,51 @@
|
|||||||
|
import type {IBaseWorld} from '../common/world';
|
||||||
|
import type {Troonia} from './troonia';
|
||||||
|
import {SphereGeometry, Mesh, MeshPhongMaterial, Object3D, Scene} from 'three';
|
||||||
|
import {BaseWorld} from '../common/world';
|
||||||
|
import {Physics} from './physics';
|
||||||
|
import type {TSceneObject} from '../common/scenes';
|
||||||
|
import type {Socket} from 'socket.io-client';
|
||||||
|
|
||||||
|
export class World extends BaseWorld implements IBaseWorld {
|
||||||
|
scene: Scene = new Scene();
|
||||||
|
physics: Physics;
|
||||||
|
player = {vec3: {x: 0, y: 0, z: 0}};
|
||||||
|
|
||||||
|
constructor(game: Troonia) {
|
||||||
|
super(game);
|
||||||
|
this.physics = new Physics(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
addPlayer(id?: string) {
|
||||||
|
id = id ? id : (<Socket>this.game.socket).id;
|
||||||
|
const player = new Object3D();
|
||||||
|
player.name = id;
|
||||||
|
const mesh = new Mesh(
|
||||||
|
new SphereGeometry(.5), new MeshPhongMaterial({color: 0xFF00FF}));
|
||||||
|
player.add(mesh);
|
||||||
|
this.objects[id] = player;
|
||||||
|
this.scene.add(player);
|
||||||
|
}
|
||||||
|
|
||||||
|
removePlayer(id?: string) {
|
||||||
|
id = id ? id : (<Socket>this.game.socket).id;
|
||||||
|
this.scene.remove(<Object3D>this.scene.getObjectByName(id));
|
||||||
|
delete this.objects[id];
|
||||||
|
}
|
||||||
|
|
||||||
|
update(delta: number) {
|
||||||
|
this.physics.update(delta);
|
||||||
|
|
||||||
|
const player = <Object3D>this.objects[(<Socket>this.game.socket).id];
|
||||||
|
(<Troonia>this.game).camera.lookAt(player.position);
|
||||||
|
(<Troonia>this.game)
|
||||||
|
.camera.position.set(player.position.x / 2, 50 - Math.abs(player.position.y), player.position.z / 2)
|
||||||
|
}
|
||||||
|
|
||||||
|
load(scene: TSceneObject[]) {
|
||||||
|
for (const sceneObject of scene) {
|
||||||
|
this.objects[sceneObject.id] = sceneObject.object;
|
||||||
|
this.scene.add(sceneObject.object);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
38
src/common/physics.ts
Normal file
38
src/common/physics.ts
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
import type {IBaseWorld} from './world.js';
|
||||||
|
import type {ICoord, IQuaternion, IVec3} from './threed.js';
|
||||||
|
|
||||||
|
export interface IPhysicsObject {
|
||||||
|
position: ICoord;
|
||||||
|
quaternion: IQuaternion;
|
||||||
|
angularVelocity?: IVec3;
|
||||||
|
velocity?: IVec3;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class BasePhysics {
|
||||||
|
baseWorld: IBaseWorld;
|
||||||
|
bodies: {[id: string]: IPhysicsObject} = {};
|
||||||
|
constructor(baseWorld: IBaseWorld) {
|
||||||
|
this.baseWorld = baseWorld;
|
||||||
|
}
|
||||||
|
|
||||||
|
setPos(object: IPhysicsObject, body: IPhysicsObject) {
|
||||||
|
object.position.x = body.position.x;
|
||||||
|
object.position.y = body.position.y;
|
||||||
|
object.position.z = body.position.z;
|
||||||
|
object.quaternion.x = body.quaternion.x;
|
||||||
|
object.quaternion.y = body.quaternion.y;
|
||||||
|
object.quaternion.z = body.quaternion.z;
|
||||||
|
object.quaternion.w = body.quaternion.w;
|
||||||
|
}
|
||||||
|
|
||||||
|
syncState() {
|
||||||
|
for (const id in this.bodies) {
|
||||||
|
const object = this.baseWorld.objects[id];
|
||||||
|
const body = this.bodies[id];
|
||||||
|
|
||||||
|
if (object && body) {
|
||||||
|
this.setPos(object, body);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
5
src/common/player.ts
Normal file
5
src/common/player.ts
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
import type {IVec3} from './threed.js';
|
||||||
|
|
||||||
|
export interface IPlayerData {
|
||||||
|
vec3?: IVec3;
|
||||||
|
}
|
159
src/common/scenes.ts
Normal file
159
src/common/scenes.ts
Normal file
@ -0,0 +1,159 @@
|
|||||||
|
import * as CANNON from 'cannon-es';
|
||||||
|
import * as THREE from 'three';
|
||||||
|
|
||||||
|
export type TSceneObject = {
|
||||||
|
id: string; object: THREE.Object3D;
|
||||||
|
shape?: CANNON.Shape;
|
||||||
|
size?: {x: number, y: number, z: number};
|
||||||
|
mass?: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
const light: TSceneObject = {
|
||||||
|
id: 'light',
|
||||||
|
object: new THREE.AmbientLight(0xFFFFFF, 0.9)
|
||||||
|
};
|
||||||
|
|
||||||
|
const floor: TSceneObject = {
|
||||||
|
id: 'floor',
|
||||||
|
object: function() {
|
||||||
|
const floor = new THREE.Object3D();
|
||||||
|
const mesh = new THREE.Mesh(
|
||||||
|
new THREE.BoxGeometry(100, 10, 100, 1),
|
||||||
|
new THREE.MeshPhongMaterial({color: 0x002288}));
|
||||||
|
mesh.receiveShadow = true;
|
||||||
|
floor.add(mesh);
|
||||||
|
const grid = new THREE.GridHelper(100, 20, 0xFF00FF, 0x880088);
|
||||||
|
grid.position.y = 5;
|
||||||
|
floor.add(grid);
|
||||||
|
return floor;
|
||||||
|
}(),
|
||||||
|
// shape: new CANNON.Box(new CANNON.Vec3(50, 0.05, 50)),
|
||||||
|
size: {x: 100, y: 10, z: 100}
|
||||||
|
// shape: new CANNON.Plane()
|
||||||
|
};
|
||||||
|
|
||||||
|
const floor2: TSceneObject = {
|
||||||
|
id: 'floor2',
|
||||||
|
object: function() {
|
||||||
|
const floor2 = new THREE.Object3D();
|
||||||
|
const mesh = new THREE.Mesh(
|
||||||
|
new THREE.BoxGeometry(150, 10, 150, 1),
|
||||||
|
new THREE.MeshPhongMaterial({color: 0x7500FF}));
|
||||||
|
mesh.receiveShadow = true;
|
||||||
|
floor2.add(mesh);
|
||||||
|
const grid = new THREE.GridHelper(150, 20, 0xFF00FF, 0x880088);
|
||||||
|
grid.position.y = 5;
|
||||||
|
floor2.add(grid);
|
||||||
|
floor2.position.set(-100, -20, 0);
|
||||||
|
return floor2;
|
||||||
|
}(),
|
||||||
|
// shape: new CANNON.Box(new CANNON.Vec3(50, 0.05, 50)),
|
||||||
|
size: {x: 150, y: 10, z: 150}
|
||||||
|
// shape: new CANNON.Plane()
|
||||||
|
};
|
||||||
|
|
||||||
|
const planet: TSceneObject = {
|
||||||
|
id: 'planet',
|
||||||
|
object: function() {
|
||||||
|
const planet = new THREE.Object3D();
|
||||||
|
const mesh = new THREE.Mesh(
|
||||||
|
new THREE.SphereGeometry(75),
|
||||||
|
new THREE.MeshPhongMaterial({color: 0x38761D}));
|
||||||
|
mesh.receiveShadow = true;
|
||||||
|
planet.add(mesh);
|
||||||
|
planet.position.set(200, -20, 0);
|
||||||
|
return planet;
|
||||||
|
}(),
|
||||||
|
shape: new CANNON.Sphere(75),
|
||||||
|
};
|
||||||
|
|
||||||
|
const donut: TSceneObject = {
|
||||||
|
id: 'donut',
|
||||||
|
object: function() {
|
||||||
|
const donut = new THREE.Object3D();
|
||||||
|
const mesh = new THREE.Mesh(
|
||||||
|
new THREE.RingGeometry(12.5, 75),
|
||||||
|
new THREE.MeshPhongMaterial({color: 0x877652}));
|
||||||
|
mesh.receiveShadow = true;
|
||||||
|
mesh.rotation.x = -Math.PI / 2;
|
||||||
|
donut.add(mesh);
|
||||||
|
donut.position.set(0, -40, 200);
|
||||||
|
return donut;
|
||||||
|
}(),
|
||||||
|
size: {x: 150, y: 10, z: 150}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const TestScene: TSceneObject[] = [
|
||||||
|
light,
|
||||||
|
floor,
|
||||||
|
floor2,
|
||||||
|
planet,
|
||||||
|
donut,
|
||||||
|
...function() {
|
||||||
|
let i: number = 1;
|
||||||
|
const boxes: TSceneObject[] = [];
|
||||||
|
do {
|
||||||
|
const box = new THREE.Object3D();
|
||||||
|
const mesh = new THREE.Mesh(
|
||||||
|
new THREE.BoxGeometry(1, 1, 1),
|
||||||
|
new THREE.MeshPhongMaterial({color: Math.sin(i) * 0xFFFFFF}));
|
||||||
|
mesh.receiveShadow = true;
|
||||||
|
box.add(mesh);
|
||||||
|
box.position.x = Math.sin(i) - 0.5;
|
||||||
|
box.position.y = i * 10
|
||||||
|
|
||||||
|
boxes.push({id: 'cube_' + i, object: box, mass: Math.random()});
|
||||||
|
|
||||||
|
i++;
|
||||||
|
} while (i <= 20)
|
||||||
|
return boxes;
|
||||||
|
}(),
|
||||||
|
...function() {
|
||||||
|
let i: number = 1;
|
||||||
|
const spheres: TSceneObject[] = [];
|
||||||
|
do {
|
||||||
|
const sphere = new THREE.Object3D();
|
||||||
|
const mesh = new THREE.Mesh(
|
||||||
|
new THREE.SphereGeometry(.5),
|
||||||
|
new THREE.MeshPhongMaterial({color: Math.cos(i) * 0xFFFFFF}));
|
||||||
|
mesh.receiveShadow = true;
|
||||||
|
sphere.add(mesh);
|
||||||
|
sphere.position.x = 5 + Math.sin(i) - 0.5;
|
||||||
|
sphere.position.y = i * 10;
|
||||||
|
|
||||||
|
spheres.push({
|
||||||
|
id: 'sphere_' + i,
|
||||||
|
object: sphere,
|
||||||
|
shape: new CANNON.Sphere(.5),
|
||||||
|
mass: Math.random()
|
||||||
|
});
|
||||||
|
|
||||||
|
i++;
|
||||||
|
} while (i <= 20)
|
||||||
|
return spheres;
|
||||||
|
}(),
|
||||||
|
...function() {
|
||||||
|
let i: number = 1;
|
||||||
|
const objs: TSceneObject[] = [];
|
||||||
|
do {
|
||||||
|
const box = new THREE.Object3D();
|
||||||
|
const mesh = new THREE.Mesh(
|
||||||
|
new THREE.ConeGeometry(.5),
|
||||||
|
new THREE.MeshPhongMaterial({color: Math.tan(i) * 0xFFFFFF}));
|
||||||
|
mesh.receiveShadow = true;
|
||||||
|
box.add(mesh);
|
||||||
|
box.position.x = -5 + Math.sin(i) - 0.5;
|
||||||
|
box.position.y = i * 10;
|
||||||
|
|
||||||
|
objs.push({
|
||||||
|
id: 'cone_' + i,
|
||||||
|
object: box,
|
||||||
|
shape: new CANNON.Cylinder(.01, .5, 1),
|
||||||
|
mass: Math.random()
|
||||||
|
});
|
||||||
|
|
||||||
|
i++;
|
||||||
|
} while (i <= 20)
|
||||||
|
return objs;
|
||||||
|
}(),
|
||||||
|
];
|
11
src/common/threed.ts
Normal file
11
src/common/threed.ts
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
export interface ICoord {
|
||||||
|
x: number;
|
||||||
|
y: number;
|
||||||
|
z: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type IVec3 = ICoord;
|
||||||
|
|
||||||
|
export interface IQuaternion extends ICoord {
|
||||||
|
w: number;
|
||||||
|
}
|
21
src/common/troonia.ts
Normal file
21
src/common/troonia.ts
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
import type {Server} from 'socket.io';
|
||||||
|
import type {Socket} from 'socket.io-client';
|
||||||
|
import type {IBaseWorld} from './world.js';
|
||||||
|
import {Clock} from 'three';
|
||||||
|
import type {IPhysicsObject} from './physics';
|
||||||
|
|
||||||
|
export type TrooniaState = {
|
||||||
|
objects: {[id: string]: IPhysicsObject};
|
||||||
|
}
|
||||||
|
|
||||||
|
export abstract class BaseTroonia {
|
||||||
|
clock: Clock = new Clock();
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IBaseTroonia extends BaseTroonia {
|
||||||
|
socket: Socket|Server;
|
||||||
|
world: IBaseWorld;
|
||||||
|
update(): void;
|
||||||
|
onConnect(socket?: any): void;
|
||||||
|
onDisconnect(message: any, socket?: any): void;
|
||||||
|
}
|
23
src/common/world.ts
Normal file
23
src/common/world.ts
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
import type {IPhysicsObject} from './physics.js';
|
||||||
|
import type {IBaseTroonia} from './troonia.js';
|
||||||
|
import type {BasePhysics} from './physics.js';
|
||||||
|
import type {TSceneObject} from './scenes.js'
|
||||||
|
import type {IPlayerData} from './player.js';
|
||||||
|
|
||||||
|
export abstract class BaseWorld {
|
||||||
|
game: IBaseTroonia;
|
||||||
|
objects: {[id: string]: IPhysicsObject} = {};
|
||||||
|
players: {[id: string]: IPlayerData} = {};
|
||||||
|
|
||||||
|
constructor(game: IBaseTroonia) {
|
||||||
|
this.game = game;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IBaseWorld extends BaseWorld {
|
||||||
|
physics: BasePhysics;
|
||||||
|
addPlayer(id?: string): void;
|
||||||
|
removePlayer(id?: string): void;
|
||||||
|
update(delta: number): void;
|
||||||
|
load(scene: TSceneObject[]): void;
|
||||||
|
}
|
39
src/server/main.ts
Normal file
39
src/server/main.ts
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
import express from 'express';
|
||||||
|
import http from 'http';
|
||||||
|
import path from 'path';
|
||||||
|
import {Server} from 'socket.io';
|
||||||
|
import {fileURLToPath} from 'url';
|
||||||
|
|
||||||
|
import {Troonia} from './troonia.js';
|
||||||
|
|
||||||
|
const port: number = 3001;
|
||||||
|
|
||||||
|
class App {
|
||||||
|
private readonly server: http.Server;
|
||||||
|
private readonly port: number;
|
||||||
|
|
||||||
|
private readonly io: Server;
|
||||||
|
|
||||||
|
constructor(port: number) {
|
||||||
|
this.port = port;
|
||||||
|
const app = express();
|
||||||
|
|
||||||
|
const __filename = fileURLToPath(import.meta.url);
|
||||||
|
const __dirname = path.dirname(__filename);
|
||||||
|
|
||||||
|
app.use(express.static(path.join(__dirname, '../../client')));
|
||||||
|
|
||||||
|
this.server = new http.Server(app);
|
||||||
|
|
||||||
|
this.io = new Server(this.server);
|
||||||
|
new Troonia(this.io);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Start() {
|
||||||
|
this.server.listen(this.port, () => {
|
||||||
|
console.log(`Server listening on port ${this.port}.`);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
new App(port).Start();
|
71
src/server/physics.ts
Normal file
71
src/server/physics.ts
Normal file
@ -0,0 +1,71 @@
|
|||||||
|
import {Body, Box, Shape, Vec3, World as PhysicsWorld} from 'cannon-es';
|
||||||
|
import type {World as GameWorld} from './world.js';
|
||||||
|
import type {IPhysicsObject} from '../common/physics.js';
|
||||||
|
import {BasePhysics} from '../common/physics.js';
|
||||||
|
import type {IVec3} from '../common/threed';
|
||||||
|
|
||||||
|
export class Physics extends BasePhysics {
|
||||||
|
world: PhysicsWorld = new PhysicsWorld();
|
||||||
|
|
||||||
|
constructor(baseWorld: GameWorld) {
|
||||||
|
super(baseWorld);
|
||||||
|
this.world.gravity.set(0, -9.82, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
addObject(
|
||||||
|
id: string, object: IPhysicsObject, mass: number, size: IVec3,
|
||||||
|
shape?: Shape) {
|
||||||
|
const body = new Body({mass: mass});
|
||||||
|
|
||||||
|
if (!shape) {
|
||||||
|
body.addShape(new Box(new Vec3(size.x / 2, size.y / 2, size.z / 2)));
|
||||||
|
} else {
|
||||||
|
body.addShape(shape);
|
||||||
|
}
|
||||||
|
|
||||||
|
body.position.set(object.position.x, object.position.y, object.position.z);
|
||||||
|
body.quaternion.set(
|
||||||
|
object.quaternion.x, object.quaternion.y, object.quaternion.z,
|
||||||
|
object.quaternion.w);
|
||||||
|
|
||||||
|
this.bodies[id] = body;
|
||||||
|
this.world.addBody(body);
|
||||||
|
}
|
||||||
|
|
||||||
|
removeObject(id: string) {
|
||||||
|
this.world.removeBody(<Body>this.bodies[id]);
|
||||||
|
delete this.bodies[id];
|
||||||
|
}
|
||||||
|
|
||||||
|
update(delta: number) {
|
||||||
|
for (const player in (<GameWorld>this.baseWorld).players) {
|
||||||
|
// (this.bodies[player] as Body)
|
||||||
|
// .angularVelocity.copy(
|
||||||
|
// (this.baseWorld as GameWorld).players[player]?.vec3 as Vec3);
|
||||||
|
|
||||||
|
let px = this.baseWorld?.players[player]?.vec3?.x;
|
||||||
|
let py = this.baseWorld?.players[player]?.vec3?.y;
|
||||||
|
let pz = this.baseWorld?.players[player]?.vec3?.z;
|
||||||
|
px = px ? px : 0;
|
||||||
|
py = py ? py : 0;
|
||||||
|
pz = pz ? pz : 0;
|
||||||
|
if (px > 0) {
|
||||||
|
(<Body>this.bodies[player]).angularVelocity.x += 1;
|
||||||
|
}
|
||||||
|
if (py > 0) {
|
||||||
|
(<Body>this.bodies[player]).velocity.y += 0.5;
|
||||||
|
}
|
||||||
|
if (pz > 0) {
|
||||||
|
(<Body>this.bodies[player]).angularVelocity.z += 1;
|
||||||
|
}
|
||||||
|
if (px < 0) {
|
||||||
|
(<Body>this.bodies[player]).angularVelocity.x -= 1;
|
||||||
|
}
|
||||||
|
if (pz < 0) {
|
||||||
|
(<Body>this.bodies[player]).angularVelocity.z -= 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.world.fixedStep(delta);
|
||||||
|
this.syncState();
|
||||||
|
}
|
||||||
|
}
|
70
src/server/troonia.ts
Normal file
70
src/server/troonia.ts
Normal file
@ -0,0 +1,70 @@
|
|||||||
|
import type {IBaseTroonia} from '../common/troonia.js';
|
||||||
|
import type {Server} from 'socket.io';
|
||||||
|
import {World} from './world.js';
|
||||||
|
import {BaseTroonia} from '../common/troonia.js';
|
||||||
|
import type {Socket} from 'socket.io';
|
||||||
|
import {TestScene} from '../common/scenes.js';
|
||||||
|
// import type {Body} from 'cannon-es';
|
||||||
|
import type {IPlayerData} from '../common/player.js';
|
||||||
|
|
||||||
|
export class Troonia extends BaseTroonia implements IBaseTroonia {
|
||||||
|
socket: Server;
|
||||||
|
world: World;
|
||||||
|
|
||||||
|
constructor(socket: Server) {
|
||||||
|
super();
|
||||||
|
this.socket = socket;
|
||||||
|
|
||||||
|
this.socket.on('connection', (socket: any) => {
|
||||||
|
this.onConnect(socket);
|
||||||
|
});
|
||||||
|
|
||||||
|
this.world = new World(this);
|
||||||
|
this.world.load(TestScene);
|
||||||
|
|
||||||
|
setInterval(() => {
|
||||||
|
this.socket.emit('syncState', {objects: this.world.objects});
|
||||||
|
}, 1000 / 20);
|
||||||
|
|
||||||
|
setInterval(() => {
|
||||||
|
this.update();
|
||||||
|
}, 1000 / 50);
|
||||||
|
|
||||||
|
// setInterval(() => {
|
||||||
|
// this.world.objects = {};
|
||||||
|
// for (const body of Object.values(this.world.physics.bodies)) {
|
||||||
|
// this.world.physics.world.removeBody(body as Body);
|
||||||
|
// }
|
||||||
|
// this.world.physics.bodies = {};
|
||||||
|
// this.world.load(TestScene);
|
||||||
|
// }, 1000 * 10);
|
||||||
|
}
|
||||||
|
|
||||||
|
onConnect(socket: Socket) {
|
||||||
|
console.log(`User ${socket.id} connected.`);
|
||||||
|
|
||||||
|
socket.emit('syncPlayers', this.world.players);
|
||||||
|
|
||||||
|
this.socket.emit('addPlayer', socket.id);
|
||||||
|
this.world.addPlayer(socket.id);
|
||||||
|
|
||||||
|
socket.on('disconnect', (message: any) => {
|
||||||
|
this.onDisconnect(message, socket);
|
||||||
|
});
|
||||||
|
|
||||||
|
socket.on('updatePlayer', (data: IPlayerData) => {
|
||||||
|
this.world.players[socket.id] = data;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
onDisconnect(message: any, socket: any) {
|
||||||
|
this.world.removePlayer(socket.id);
|
||||||
|
this.socket.emit('removePlayer', socket.id);
|
||||||
|
console.log(`User ${socket.id} disconnected: ${message}.`);
|
||||||
|
}
|
||||||
|
|
||||||
|
update() {
|
||||||
|
const delta = this.clock.getDelta();
|
||||||
|
this.world.update(delta);
|
||||||
|
}
|
||||||
|
}
|
32
src/server/tsconfig.json
Normal file
32
src/server/tsconfig.json
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
{
|
||||||
|
"$schema": "https://json.schemastore.org/tsconfig",
|
||||||
|
"display": "Node 16 + ESM + Strictest",
|
||||||
|
"compilerOptions": {
|
||||||
|
"lib": [
|
||||||
|
"ES2021"
|
||||||
|
],
|
||||||
|
"module": "ES2022",
|
||||||
|
"moduleResolution": "Node",
|
||||||
|
"target": "ES2021",
|
||||||
|
"strict": true,
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"forceConsistentCasingInFileNames": true,
|
||||||
|
"allowUnusedLabels": false,
|
||||||
|
"allowUnreachableCode": false,
|
||||||
|
"exactOptionalPropertyTypes": true,
|
||||||
|
"noFallthroughCasesInSwitch": true,
|
||||||
|
"noImplicitOverride": true,
|
||||||
|
"noImplicitReturns": true,
|
||||||
|
"noPropertyAccessFromIndexSignature": true,
|
||||||
|
"noUncheckedIndexedAccess": true,
|
||||||
|
"noUnusedLocals": true,
|
||||||
|
"noUnusedParameters": true,
|
||||||
|
"importsNotUsedAsValues": "error",
|
||||||
|
"outDir": "../../dist/server"
|
||||||
|
},
|
||||||
|
"include": [
|
||||||
|
"../server/*.ts",
|
||||||
|
"../common/*.ts"
|
||||||
|
]
|
||||||
|
}
|
81
src/server/world.ts
Normal file
81
src/server/world.ts
Normal file
@ -0,0 +1,81 @@
|
|||||||
|
import type {IBaseWorld} from '../common/world.js';
|
||||||
|
import type {IPhysicsObject} from '../common/physics.js'
|
||||||
|
import {Physics} from './physics.js';
|
||||||
|
import {Box3} from 'three';
|
||||||
|
import {BaseWorld} from '../common/world.js';
|
||||||
|
import type {Troonia} from './troonia.js';
|
||||||
|
import type {TSceneObject} from '../common/scenes.js';
|
||||||
|
import {Sphere} from 'cannon-es';
|
||||||
|
import type {IVec3} from '../common/threed';
|
||||||
|
|
||||||
|
export class World extends BaseWorld implements IBaseWorld {
|
||||||
|
physics: Physics;
|
||||||
|
|
||||||
|
constructor(game: Troonia) {
|
||||||
|
super(game);
|
||||||
|
this.physics = new Physics(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
addPlayer(id: string) {
|
||||||
|
const playerObject: IPhysicsObject = {
|
||||||
|
position: {x: Math.random() * 90 - 45, y: 10, z: Math.random() * 90 - 45},
|
||||||
|
quaternion: {x: 0, y: 0, z: 0, w: 1}
|
||||||
|
};
|
||||||
|
this.objects[id] = playerObject;
|
||||||
|
this.physics.addObject(
|
||||||
|
id, playerObject, 10, {x: 1, y: 1, z: 1}, new Sphere(.5));
|
||||||
|
this.players[id] = {vec3: {x: 0, y: 0, z: 0}};
|
||||||
|
}
|
||||||
|
|
||||||
|
removePlayer(id: string) {
|
||||||
|
delete this.players[id];
|
||||||
|
delete this.objects[id];
|
||||||
|
this.physics.removeObject(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
update(delta: number) {
|
||||||
|
this.physics.update(delta);
|
||||||
|
}
|
||||||
|
|
||||||
|
load(scene: TSceneObject[]) {
|
||||||
|
for (const sceneObject of scene) {
|
||||||
|
const physicsObject: IPhysicsObject = {
|
||||||
|
position: {x: 0, y: 0, z: 0},
|
||||||
|
quaternion: {x: 0, y: 0, z: 0, w: 0}
|
||||||
|
};
|
||||||
|
|
||||||
|
physicsObject.position = {
|
||||||
|
x: sceneObject.object.position.x,
|
||||||
|
y: sceneObject.object.position.y,
|
||||||
|
z: sceneObject.object.position.z
|
||||||
|
};
|
||||||
|
|
||||||
|
physicsObject.quaternion = {
|
||||||
|
x: sceneObject.object.quaternion.x,
|
||||||
|
y: sceneObject.object.quaternion.y,
|
||||||
|
z: sceneObject.object.quaternion.z,
|
||||||
|
w: sceneObject.object.quaternion.w
|
||||||
|
};
|
||||||
|
|
||||||
|
let size: IVec3;
|
||||||
|
if (!sceneObject.size) {
|
||||||
|
size = function() {
|
||||||
|
const bounds = new Box3().setFromObject(sceneObject.object);
|
||||||
|
return {
|
||||||
|
x: Math.abs(bounds.max.x - bounds.min.x),
|
||||||
|
y: Math.abs(bounds.max.y - bounds.min.y),
|
||||||
|
z: Math.abs(bounds.max.y - bounds.min.y)
|
||||||
|
};
|
||||||
|
}();
|
||||||
|
} else {
|
||||||
|
size = sceneObject.size;
|
||||||
|
}
|
||||||
|
|
||||||
|
let mass = ((sceneObject.mass) ? sceneObject.mass : 0);
|
||||||
|
|
||||||
|
this.objects[sceneObject.id] = physicsObject;
|
||||||
|
this.physics.addObject(
|
||||||
|
sceneObject.id, physicsObject, mass, size, sceneObject.shape);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
31
webpack.common.cjs
Normal file
31
webpack.common.cjs
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
const path = require("path");
|
||||||
|
const CopyPlugin = require("copy-webpack-plugin");
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
entry: './src/client/main.ts',
|
||||||
|
plugins: [
|
||||||
|
new CopyPlugin({
|
||||||
|
patterns: [{
|
||||||
|
from: "index.html",
|
||||||
|
to: "."
|
||||||
|
}]
|
||||||
|
})
|
||||||
|
],
|
||||||
|
module: {
|
||||||
|
rules: [
|
||||||
|
{
|
||||||
|
test: /\.tsx?$/,
|
||||||
|
use: 'ts-loader',
|
||||||
|
exclude: [/node_modules/, /server/],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
resolve: {
|
||||||
|
alias: {three: path.resolve('./node_modules/three/')},
|
||||||
|
extensions: ['.tsx', '.ts', '.js'],
|
||||||
|
},
|
||||||
|
output: {
|
||||||
|
filename: 'bundle.js',
|
||||||
|
path: path.resolve(__dirname, './dist/client/'),
|
||||||
|
}
|
||||||
|
};
|
16
webpack.dev.cjs
Normal file
16
webpack.dev.cjs
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
const { merge } = require("webpack-merge");
|
||||||
|
const common = require("./webpack.common.cjs");
|
||||||
|
const path = require("path");
|
||||||
|
|
||||||
|
// noinspection JSCheckFunctionSignatures
|
||||||
|
module.exports = merge(common, {
|
||||||
|
mode: 'development',
|
||||||
|
devtool: 'eval-source-map',
|
||||||
|
devServer: {
|
||||||
|
static: {
|
||||||
|
directory: path.join(__dirname, 'dist/client'),
|
||||||
|
},
|
||||||
|
hot: true,
|
||||||
|
proxy: {'/socket.io': {target: 'http://127.0.0.1:3001', ws: true}}
|
||||||
|
}
|
||||||
|
});
|
6
webpack.prod.cjs
Normal file
6
webpack.prod.cjs
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
const { merge } = require("webpack-merge");
|
||||||
|
const common = require("./webpack.common.cjs");
|
||||||
|
|
||||||
|
// noinspection JSCheckFunctionSignatures
|
||||||
|
module.exports =
|
||||||
|
merge(common, {mode: 'production', performance: {hints: false}});
|
Loading…
Reference in New Issue
Block a user