Socket.io, React Three Fiber Kezdő Példa

February 5, 2024

Socket.io egyszerű implementálása React Three Fiber-rel

Egy egyszerű, de hatékony megoldás a React Three Fiber és a Socket.io kombinálására a multiplayer játékfejlesztés területén. A projekt célja, hogy bemutassa, hogyan lehet egy .glb formátumú 3D modellt integrálni egy React alkalmazásba TypeScript használatával, majd ezt a modellt valós időben szinkronizálni több felhasználó között a Socket.io segítségével.

A projekt két fő lépésből áll majd. Először a kliens oldlaon létrehozzuk az egér kattintásra animációval reagáló glb fájlt. Második lépésben létrehozzuk a szerver oldalt, ami össze lesz kötve az előzőleg létrehozott kliens oldallal.

I. Alap kliens oldal működése:

A glb fájl feltöltése és elérése, hogy kövesse a kattintás helyét

1. Szükséges csomagok instalálása

POWERSHELL
mkdir client
POWERSHELL
cd client
POWERSHELL
npm create vite@latest

Válaszd a react + typescript + SWC

POWERSHELL
npm install three @types/three @react-three/fiber @react-three/drei

A későbbiekben a szerver hozzáféréshez

POWERSHELL
npm install socket.io-client

2. A kamera és az alap tér létrehozása és beállítása

Létrehozzuk a kamerát, a vásznat, a világítást és egy síkot.

TYPESCRIPT
//client/src/App.tsx
import { Canvas } from "@react-three/fiber";
import { Scene } from "./Scene";
function App() {
return (
<>
<Canvas
camera={{
fov: 60,
near: 0.1,
far: 300,
position: [0, 7, 8],
>
<Scene />
</Canvas>
</>
);
}
export default App;
TYPESCRIPT
//client/src/Scene.tsx
import { Environment, OrbitControls } from "@react-three/drei";
import * as THREE from "three"
export const Scene = () => {
return (
<>
<Environment preset="sunset" />
<ambientLight intensity={0.2} />
<directionalLight
position={[5, 5, 5]}
/>
<OrbitControls makeDefault />
<mesh
rotation-x={-Math.PI / 2}
>
<planeGeometry args={[20, 10]} />
<meshStandardMaterial color="#70543E" />
</mesh>
</>
);
};

Itt a végeredményben már látunk egy síkot

3. A glb fájl kezelő létrehozása

Az előzőleg letöltött Dog.glb fájlt a public könyvtárba másoljuk.

A glb fájl kezelőt a gltfjsx-el hozzuk létre. Tehát futtatjuk a következő kódot:

POWERSHELL
npx gltfjsx public/Dog.glb -o src/components/Dog.tsx --transformlb --transform --types

A létrehozott fájl az src/components-be megjelenik. Ezt utána hozzá adjuk az előbb létrehozott Scene.tsx fájlhoz

POWERSHELL
//client/src/Scene.tsx
import { Dog } from "./components/Dog";
...
<mesh
rotation-x={-Math.PI / 2}
>
<planeGeometry args={[20, 10]} />
<meshStandardMaterial color="#70543E" />
</mesh>
<Dog
position={
new THREE.Vector3(0,0,0)
}
dogColor={character.dogColor}
/>
</>
);
};

4. Hozzáadjuk az árnyékokat az elemekhez

Az App.tsx, Scene.tsx és a Dog.tsx fájlt kiegészítjük a szükséges árnyékok hozzáadásával.

A shadows attribűtum engedélyezi, hogy az árnyékok használatát a Canvas elemen belül

TYPESCRIPT
//client/src/App.tsx
<Canvas
camera={{
fov: 60,
near: 0.1,
far: 300,
position: [0, 7, 8],
}}
shadows
>

A castShadow attribútum engedélyezi az árnyékok vetítését a fényforrás által. Ha ez nincs bekapcsolva, a fényforrás nem hoz létre árnyékot az objektumokon, ami csökkentheti a vizuális realizmust és a térbeli érzetet a 3D jelenetben. Ez az attribútum alapértelmezetten false, tehát explicit módon be kell állítani true értékre az árnyékok engedélyezéséhez.

shadow-camera-left, shadow-camera-right, shadow-camera-top, és shadow-camera-bottom attribútumokkal állíthatjuk be az árnyékvetítő kamera láthatósági téglalapjának (frustum) határait. Ezek a beállítások határozzák meg, hogy milyen széles és magas területen számítódik ki és jelenik meg az árnyék. Ha ezeket a beállításokat nem optimalizáljuk a jelenetünknek megfelelően, előfordulhat, hogy az árnyékok nem jelennek meg helyesen, vagy teljesen hiányoznak bizonyos területekről.

TYPESCRIPT
//client/src/Scene.tsx
...
<Environment preset="sunset" />
<ambientLight intensity={0.2} />
<directionalLight
position={[5, 5, 5]}
castShadow
shadow-camera-left={-10}
shadow-camera-right={10}
shadow-camera-top={10}
shadow-camera-bottom={-10}
/>
<OrbitControls makeDefault />
<mesh
rotation-x={-Math.PI / 2}
receiveShadow
>
<planeGeometry args={[20, 10]} />
<meshStandardMaterial color="#70543E" />
...

És adjuk a castShadow-t és reciveShadow-t a kutyához is

TYPESCRIPT
//client/src/components/Dog.tsx
...
<skinnedMesh
name="Dog"
geometry={dogGeom.geometry}
material={materials.AtlasMaterial}
skeleton={dogGeom.skeleton}
rotation={[-Math.PI / 2, 0, 0]}
scale={100}
castShadow
receiveShadow
>
...

5. A kurzor követést biztosítjuk a kutya számára

A síklapon kattintás után a kutya a jelenlegi helyéről a kattintás helyére fog menni.

TYPESCRIPT
//client/src/Scene.tsx
import { Environment, OrbitControls, useCursor } from "@react-three/drei";
import { useState } from "react";
import { Dog } from "./components/Dog";
import * as THREE from "three";
export const Scene = () => {
const [onFloor, setOnFloor] = useState(false);
const [dogPosition, setDogPosition] = useState({ x: 0, y: 0, z: 0 });
const handlePlaneClick = (e: { point: { x: number; z: number; }; }) => {
// Frissítsük a kutya pozícióját az új értékekre, ahol a felhasználó kattintott
setDogPosition({ x: e.point.x, y: e.point.y, z: e.point.z });
};
useCursor(onFloor);
return (
...
<mesh
rotation-x={-Math.PI / 2}
onClick={handlePlaneClick}
onPointerEnter={() => setOnFloor(true)}
onPointerLeave={() => setOnFloor(false)}
receiveShadow
>
<planeGeometry args={[20, 10]} />
<meshStandardMaterial color="#70543E" />
</mesh>
<Dog
position={
new THREE.Vector3(dogPosition.x, dogPosition.y, dogPosition.z)
}
dogColor={"#222222"}
/>
...

A kutya távolságát nézzük a jelenlegi és a jövőbeli helyének a figyelembe vételével, ha a kettő közötti távolság nagyobb, mint 0.1 akkor run animation-re vált a rendszer.

TYPESCRIPT
//client/src/components/Dog.tsx
import * as THREE from "three";
import { useEffect, useRef, useMemo, useState } from "react";
import { useGLTF, useAnimations } from "@react-three/drei";
import { useFrame, useGraph } from "@react-three/fiber";
import { SkeletonUtils } from "three-stdlib";
import { DogProps } from "../lib/types";
const SPEED = 0.1;
export function Dog({ dogColor = "green", ...props }: DogProps) {
// eslint-disable-next-line react-hooks/exhaustive-deps
const position = useMemo(() => props.position, []);
const group = useRef<THREE.Group>(null);
const { scene, materials, animations } = useGLTF("/Dog-transformed.glb");
const clone = useMemo(() => SkeletonUtils.clone(scene), [scene]);
const { nodes } = useGraph(clone);
const { actions } = useAnimations(animations, group);
const [animation, setAnimation] = useState(
"AnimalArmature|AnimalArmature|AnimalArmature|Idle"
);
const dogGeom = nodes.Dog as THREE.SkinnedMesh;
useEffect(() => {
const currentAction = actions[animation];
if (currentAction) {
currentAction.reset().fadeIn(0.32).play();
return () => {
currentAction.fadeOut(0.32);
};
}
}, [actions, animation]);
useFrame(() => {
// Ensure the group and its target position are defined
if (!group.current || !props.position) {
return;
}
// Calculate the distance to the target position
const distance = group.current.position.distanceTo(props.position);
// Check if the character needs to move
if (distance > 0.1) {
// Calculate the direction vector towards the target position
const direction = props.position
.clone()
.sub(group.current.position)
.normalize();
// Move the character towards the target position
const moveStep = direction.multiplyScalar(SPEED);
group.current.position.add(moveStep);
// Make the character face the target position
group.current.lookAt(props.position);
// Switch to the "run" animation if not already running
if (animation !== "AnimalArmature|AnimalArmature|AnimalArmature|Run") {
setAnimation("AnimalArmature|AnimalArmature|AnimalArmature|Run");
}
} else {
// Switch to the "idle" animation if not already idle and the character is close enough to the target position
if (animation !== "AnimalArmature|AnimalArmature|AnimalArmature|Idle") {
setAnimation("AnimalArmature|AnimalArmature|AnimalArmature|Idle");
}
}
});
...

II. Szerver létrehozása és hozzá kapcsolása a kliens oldalhoz

A multiplayer játékfejlesztés magában foglalja a kliens és szerver közötti kommunikáció létrehozását és kezelését, ami lehetővé teszi a játékosok számára, hogy valós időben lássák és interakcióba lépjenek egymással egy megosztott virtuális térben. A következőkben bemutatjuk, hogyan hozható létre egy egyszerű multiplayer infrastruktúra egy szerver és kliens oldali alkalmazás segítségével, amely a Socket.io-t használja a kommunikáció megvalósítására.

1. Szerver instalálás

POWERSHELL
cd ..
POWERSHELL
mkdir server
POWERSHELL
cd server
POWERSHELL
npm i socket.io nodemon

package.json-t módosítjuk a szerver indításhoz

JSON
//server/package.json
"scripts": {
"dev": "nodemon index.js"
},

2. Socket.io szerver oldali rendszer létrehozása

A socket.io-n keresztül tudjuk, hogy a karakterünk pozícióját, színét és amennyiben az egyik karakter mozog akkor a pozíció változást kell majd kezelnünk. A jelenlegi példában egy egyszerű Map rendszert hozunk létre majd, ami biztosítja, hogy minden kulcs egyedi legyen, ami megkönnyíti az entitások kezelését, különösen, ha dinamikusan hozzáadsz vagy távolítasz el elemeket. Ez különösen hasznos multiplayer játékokban, ahol minden játékos vagy karakter egyedi azonosítóval rendelkezik.

Az újonnan keletkező karaktereknek random pozíciót és random barna színt állítunk be

JAVASCRIPT
//server/index.js
import { Server } from "socket.io";
const io = new Server({
cors: {
origin: "http://localhost:5173",
},
});
io.listen(3001);
const characters = new Map();
const randomPosition = () => {
return [Math.random() * 10, 0, Math.random() * 3];
};
const randomBrownHexColor = () => {
const red = Math.floor(Math.random() * 50) + 100;
const green = Math.floor(Math.random() * 30) + 70;
const blue = Math.floor(Math.random() * 20) + 30;
// Convert the components to hexadecimal and format the color
const hexColor = `#${red.toString(16)}${green.toString(16)}${blue.toString(16)}`;
return hexColor;
};
io.on("connection", (socket) => {
console.log("user connected");
// Add character to Map
characters.set(socket.id, {
id: socket.id,
position: randomPosition(),
dogColor: randomBrownHexColor()
});
// Emit all characters to all clients
io.emit("characters", Array.from(characters.values()));
socket.on("move", (position) => {
if (characters.has(socket.id)) {
const character = characters.get(socket.id);
character.position = position;
// Emit updated characters
io.emit("characters", Array.from(characters.values()));
}
});
socket.on("disconnect", () => {
console.log("user disconnected");
// Remove character from Map
characters.delete(socket.id);
// Emit updated characters
io.emit("characters", Array.from(characters.values()));
});
});

3. Klienshez hozzáadjuk a socket.io szervert

Létrehozunk egy plusz fájlt, amiben a socket.io-t kezeljük. Frissítjük a karakterek és a connection adatokat a szerver oldallal.

TYPESCRIPT
//client/src/components/SocketProvider.tsx
import React, { useEffect, useState } from "react";
import { Character, SocketProviderProps } from "../lib/types";
import { SocketContext, socket } from "../lib/constants";
export const SocketProvider: React.FC<SocketProviderProps> = ({ children }) => {
const [characters, setCharacters] = useState<Character[]>([]);
useEffect(() => {
const handleCharactersUpdate = (
newCharacters: React.SetStateAction<Character[]>
) => {
setCharacters(newCharacters);
};
socket.on("connect", () => console.log("Connected"));
socket.on("characters", handleCharactersUpdate);
// Return a cleanup function that correctly performs cleanup without returning a value
return () => {
socket.off("connect");
socket.off("characters", handleCharactersUpdate);
};
}, []);
return (
<SocketContext.Provider value={{ characters }}>
{children}
</SocketContext.Provider>
);
};

Az exportálandó konstansoknak létrehozunk egy külön fájlt, ahonnan könnyen elérhető lesz.

TYPESCRIPT
//client/src/lib/constants.ts
import { io } from "socket.io-client";
import { SocketContextType } from "./types";
import { createContext, useContext } from "react";
export const socket = io("http://localhost:3001");
export const SocketContext = createContext<SocketContextType>({
characters: [],
});
export const useSocket = () => useContext(SocketContext);
TYPESCRIPT
//client/src/lib/types.ts
import { ReactNode } from "react";
export interface Character {
dogColor: string;
id: string;
position: [number, number, number];
}
export type DogProps = {
dogColor?: string;
position: THREE.Vector3;
};
export interface SocketContextType {
characters: Character[];
}
export interface SocketProviderProps {
children: ReactNode;
}

Az App.tsx kiegészítése

TYPESCRIPT
//client/src/App.tsx
import { Canvas } from "@react-three/fiber";
import { Scene } from "./Scene";
import { SocketProvider } from "./components/SocketProvider";
function App() {
return (
<>
<SocketProvider>
<Canvas
camera={{
fov: 60,
near: 0.1,
far: 300,
position: [0, 7, 8],
}}
shadows
>
<Scene />
</Canvas>
</SocketProvider>
</>
);
}
export default App;

Scene.tsx frissítése, hogy ne csak egy karaktert hozzon létre, hanem az összeset. A pozíciókat így a move változásával már a szerverre küldjük és továbbítjuk az összes többi kliens oldalra.

TYPESCRIPT
//client/src/Scene.tsx
import { Environment, OrbitControls, useCursor } from "@react-three/drei";
import { useState } from "react";
import { Dog } from "./components/Dog";
import { socket, useSocket } from "./lib/constants";
import * as THREE from "three";
export const Scene = () => {
const { characters } = useSocket();
const [onFloor, setOnFloor] = useState(false);
const [, setPosition] = useState({ x: 0, y: 0, z: 0 });
const handlePlaneClick = (e: { point: { x: number; z: number } }) => {
const newPosition = { x: e.point.x, y: 0, z: e.point.z };
setPosition(newPosition);
// The new updated code send to server
socket.emit("move", [newPosition.x, newPosition.y, newPosition.z]);
};
...
{characters.map((character) => (
<Dog
key={character.id}
position={
new THREE.Vector3(
character.position[0],
character.position[1],
character.position[2]
)
}
dogColor={character.dogColor}
/>
))}
</>
);
};

Több ablak megnyitásával tesztelhető, hogy a weboldalon több kutya jelenik meg.

Hova haladhatunk tovább

Ez egy jó kezdő pont react three fiber-hez és a 3D környezet megértéséhez node.js-ben. Bele tekintettünk a Socket.io használatába és egy egyszerű multiplayer megoldásba a three.js példa során

Code

A teljes kód megtalálható:

https://github.com/balazsfaragodev/socketio-react-three-fiber-tutorial

A visual depiction of what is being written about

Oszd meg ezt a cikket

Merülj el az izgalmas tudásban, amíg a buszra vársz!

Indítsd a napod a legújabb technológiai áttörésekkel. Csatlakozz most, és merülj el az innovációban!

Kapcsolódó Cikkek