Skip to content

Commit

Permalink
Merge pull request #1 from codernineteen/v0.1.1
Browse files Browse the repository at this point in the history
V0.1.1
  • Loading branch information
codernineteen authored Feb 28, 2023
2 parents f1316e0 + 53273ee commit 34b3431
Show file tree
Hide file tree
Showing 13 changed files with 140 additions and 75 deletions.
1 change: 1 addition & 0 deletions client-vanilla/.gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -22,3 +22,4 @@ dist-ssr
*.njsproj
*.sln
*.sw?
.vercel
55 changes: 34 additions & 21 deletions client-vanilla/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion client-vanilla/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,8 @@
"vite": "^4.0.0"
},
"dependencies": {
"@geckos.io/client": "^2.3.0",
"cannon-es": "^0.20.0",
"node": "^19.4.0",
"socket.io-client": "^4.5.4",
"three": "^0.148.0"
}
Expand Down
101 changes: 71 additions & 30 deletions client-vanilla/src/App.ts
Original file line number Diff line number Diff line change
@@ -1,67 +1,98 @@
//runtime
//client modules
import * as THREE from "three";
import Canvas from "./classes/scene/Canvas";
import Character from "./classes/character/Character";
import GLTFModels from "./classes/models/GLTFModels";
import MouseRaycaster from "./classes/events/MouseRaycaster";
import { CreateARoom } from "./modules/createRoom";
import { Clock } from "three";
import { io, Socket } from "socket.io-client";
import {
ServerToClientEvents,
ClientToServerEvents,
} from "./types/socket-client-types";
import NetworkPlayerController from "./classes/character/controller/NetworkPlayerController";
import UserInterface from "./classes/ui/UserInterface";

export type SocketType = Socket<ServerToClientEvents, ClientToServerEvents>;
import { CreateARoom } from "./modules/createRoom";
import { Clock } from "three";
//udp server modules
import { geckos } from "@geckos.io/client";
import type { ClientChannel } from "@geckos.io/client";
import { KeyInput } from "./classes/character/inputs/PlayerInput";

//types for udp channel data
export type ChatDataType = { message: string; id: string };
export type UserDataType = {
userId: string;
pos: [x: number, y: number, z: number];
quat: [x: number, y: number, z: number, w: number];
state: string | undefined;
input: KeyInput;
};

export default class VirtualClassroom {
private canvas: Canvas;
private gltfInstance: GLTFModels;
private controlledPlayer: Character | null;
private players: { [id: string]: Character };
private mouseRaycaster: MouseRaycaster;
private clock: THREE.Clock;
private socket: SocketType;
//private socket: SocketType;
private channel: ClientChannel;
private ui: UserInterface;

constructor() {
this.socket = io("http://localhost:3333", { transports: ["websocket"] });
//production server url : https://virtual-classroom-backend.onrender.com/
//dev server (socket) : localhost:3333
//dev server (gecko) : 127.0.0.1:5555;
//gecko server should use 127.0.0.1 for local environment instead of localhost
//Connect app to udp server
this.channel = geckos({ port: 5555, url: "http://127.0.0.1" });
this.canvas = new Canvas();
this.clock = new Clock();
this.gltfInstance = new GLTFModels();
this.controlledPlayer = null;
this.players = {};
this.mouseRaycaster = new MouseRaycaster(this.canvas);
this.ui = new UserInterface(this.socket);

this.ui = new UserInterface(this.channel);
new MouseRaycaster(this.canvas); // create member later if it needed
//Create level
const Room = CreateARoom();
this.canvas.scene.add(Room);

this.socket.on("connect", () => {});
// window.addEventListener("beforeunload", () => {
// // Send an HTTP request to your server to notify it that the tab has been closed
// });

//channel connect
let channelId;
this.channel.onConnect((error) => {
//if there is any connection error, stop application
if (error) {
console.log("UDP channel connection error: " + error.message);
return;
}

window.addEventListener("beforeunload", () => {
channelId = this.channel.id;
var xhr = new XMLHttpRequest();
xhr.open("POST", "http://127.0.0.1:5555/leave");
xhr.setRequestHeader("Content-Type", "application/json");
xhr.send(JSON.stringify({ key: channelId }));
});
});

//initialize a player which in controlled by current client
this.socket.on("Initialize", (data) => {
const { userId, pos, quat } = data;
const newPlayer = new Character(this.socket, userId, false);
this.channel.on("initialize", (data) => {
const { userId, pos, quat } = data as UserDataType;
const newPlayer = new Character(this.channel, userId, false);
newPlayer.LoadFromGLTFModels(this.gltfInstance);
newPlayer.Mesh.position.set(...pos);
newPlayer.Mesh.quaternion.set(...quat);
this.canvas.scene.add(newPlayer.Mesh);
this.canvas.scene.add(Room); // create level after player initialized
this.controlledPlayer = newPlayer;
//User input on/off for chat focus
this.ui.chatBox.OnFocusInHandler(this.controlledPlayer);
this.ui.chatBox.OnFocusOutHandler(this.controlledPlayer);
});

//listening on movement user's input and send it to socket server
this.socket.on("TransformUpdate", (data) => {
const { userId, pos, quat, state, input } = data;
this.channel.on("transform update", (data) => {
const { userId, pos, quat, state, input } = data as UserDataType;

if (!(userId in this.players)) {
const remotePlayer = new Character(this.socket, userId, true, input);
const remotePlayer = new Character(this.channel, userId, true, input);
remotePlayer.LoadFromGLTFModels(this.gltfInstance);
remotePlayer.Mesh.position.set(...pos);
remotePlayer.Mesh.quaternion.set(...quat);
Expand All @@ -82,13 +113,17 @@ export default class VirtualClassroom {
}
});

this.socket.on("CleanUpMesh", (userId: string) => {
this.canvas.scene.remove(this.players[userId].Mesh);
delete this.players[userId];
//listen chat message from server
this.channel.on("chat message", (data: Object) => {
const chatData = data as ChatDataType;
this.ui.chatBox.CreateMessageList(chatData.message, chatData.id);
});

this.socket.on("ResponseMessage", (message, id) => {
this.ui.chatBox.CreateMessageList(message, id);
//Cleanup mesh when a user logout from application
this.channel.on("cleanup mesh", (userId) => {
this.canvas.scene.remove(this.players[userId as string].Mesh);
this.ui.chatBox.CreateLeaveMessage(userId as string);
delete this.players[userId as string];
});
}

Expand All @@ -98,6 +133,7 @@ export default class VirtualClassroom {
const EventTick = () => {
requestAnimationFrame(EventTick);

//calculate delta time
const elapsedTime = this.clock.getElapsedTime();
const deltaTime = elapsedTime - previousTime;
previousTime = elapsedTime;
Expand All @@ -110,13 +146,18 @@ export default class VirtualClassroom {
}
}

//composer render(render pass handle this)
this.canvas.composer.render(deltaTime);
//This is the most important tip.
//When we use composer, we only need to use render method of composer because we already passed scene and camera component to render pass
//If we render twice(composer and renderer), we can't see the post processing effects because renderer override render pass's rendering
//this.canvas.renderer.render(this.canvas.scene, this.canvas.camera);
//this.canvas.renderer.render(this.canvas.scene, this.canvas.camera); <-- Don't use this duplicate render with composer
};

EventTick();
}

get Channel() {
return this.channel;
}
}
8 changes: 4 additions & 4 deletions client-vanilla/src/classes/character/Character.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@ import { GLTFLoader } from "three/examples/jsm/loaders/GLTFLoader";
import GLTFModels from "../models/GLTFModels";
import LocalPlayerController from "./controller/LocalPlayerController";
import NetworkPlayerController from "./controller/NetworkPlayerController";
import { SocketType } from "../../App";
import { KeyInput } from "./inputs/PlayerInput";
import type { ClientChannel } from "@geckos.io/client";

interface AnimationObject {
[animName: string]: {
Expand All @@ -21,7 +21,7 @@ export default class Character {
private controller: LocalPlayerController | NetworkPlayerController;

constructor(
public socket: SocketType,
public channel: ClientChannel,
public userId: string,
public isRemote: boolean,
public input?: KeyInput
Expand All @@ -33,11 +33,11 @@ export default class Character {
this.controller = isRemote
? new NetworkPlayerController(
this,
this.socket,
this.channel,
this.userId,
this.input as KeyInput
)
: new LocalPlayerController(this, this.socket, this.userId); // give 'this' context to state machine
: new LocalPlayerController(this, this.channel, this.userId); // give 'this' context to state machine
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { Vector3, Quaternion } from "three";
import PlayerInput from "../inputs/PlayerInput";
import PlayerStateMachine from "../animation/PlayerStateMachine";
import Character from "../Character";
import { SocketType } from "../../../App";
import type { ClientChannel } from "@geckos.io/client";

export default class LocalPlayerController {
public input: PlayerInput;
Expand All @@ -15,10 +15,10 @@ export default class LocalPlayerController {

constructor(
public parent: Character,
public socket: SocketType,
public channel: ClientChannel,
public userId: string
) {
this.input = new PlayerInput(this.socket, this.parent, this.userId);
this.input = new PlayerInput(this.channel, this.parent, this.userId);
this.stateMachine = new PlayerStateMachine(this.parent, this.input);
this.decceleration = new Vector3(-0.0005, -0.0001, -5.0);
this.acceleration = new Vector3(1, 0.25, 50.0);
Expand Down Expand Up @@ -120,7 +120,7 @@ export default class LocalPlayerController {
if (this.isPosUpdated || this.isRotUpdated) {
console.log("send request");
//if transform changed
this.socket.emit("TransformUpdate", {
this.channel.emit("transform update", {
userId: this.userId,
pos: [prevPosition.x, prevPosition.y, prevPosition.z],
quat: [
Expand Down
Original file line number Diff line number Diff line change
@@ -1,16 +1,15 @@
import NetworkPlayerStateMachine from "../animation/NetworkPlayerStateMachine";
import Character from "../Character";
import { SocketType } from "../../../App";
import { KeyInput } from "../inputs/PlayerInput";
import type { ClientChannel } from "@geckos.io/client";

//remove input function, update by socket on event
export default class NetworkPlayerController {
//public stateMachine: PlayerStateMachine;
public stateMachine: NetworkPlayerStateMachine;

constructor(
public parent: Character,
public socket: SocketType,
public channel: ClientChannel,
public userId: string,
public input: KeyInput
) {
Expand Down
7 changes: 4 additions & 3 deletions client-vanilla/src/classes/character/inputs/PlayerInput.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { SocketType } from "../../../App";
import Character from "../Character";
import type { ClientChannel } from "@geckos.io/client";

export interface KeyInput {
Forward: boolean;
Expand All @@ -16,7 +16,7 @@ export default class PlayerInput {
public keyupHandler: (evt: KeyboardEvent) => void;

constructor(
public socket: SocketType,
public channel: ClientChannel,
public player: Character,
public userId: string
) {
Expand Down Expand Up @@ -107,10 +107,11 @@ export default class PlayerInput {
document.addEventListener("keydown", this.keydownHandler);
}

//change current state to 'idle' state when keyup
StopTransformUpdate() {
const playerPos = this.player.Mesh.position;
const playerQuat = this.player.Mesh.quaternion;
this.socket.emit("TransformUpdate", {
this.channel.emit("transform update", {
userId: this.userId,
pos: [playerPos.x, playerPos.y, playerPos.z],
quat: [playerQuat.x, playerQuat.y, playerQuat.z, playerQuat.w],
Expand Down
Loading

0 comments on commit 34b3431

Please sign in to comment.