Socket.io, React Three Fiber Kezdő Példa
February 5, 2024
February 5, 2024
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.
A glb fájl feltöltése és elérése, hogy kövesse a kattintás helyét
POWERSHELLmkdir client
POWERSHELLcd client
POWERSHELLnpm create vite@latest
Válaszd a react + typescript + SWC
POWERSHELLnpm install three @types/three @react-three/fiber @react-three/drei
A későbbiekben a szerver hozzáféréshez
POWERSHELLnpm install socket.io-client
Létrehozzuk a kamerát, a vásznat, a világítást és egy síkot.
TYPESCRIPT//client/src/App.tsximport { Canvas } from "@react-three/fiber";import { Scene } from "./Scene";function App() {return (<><Canvascamera={{fov: 60,near: 0.1,far: 300,position: [0, 7, 8],><Scene /></Canvas></>);}export default App;
TYPESCRIPT//client/src/Scene.tsximport { Environment, OrbitControls } from "@react-three/drei";import * as THREE from "three"export const Scene = () => {return (<><Environment preset="sunset" /><ambientLight intensity={0.2} /><directionalLightposition={[5, 5, 5]}/><OrbitControls makeDefault /><meshrotation-x={-Math.PI / 2}><planeGeometry args={[20, 10]} /><meshStandardMaterial color="#70543E" /></mesh></>);};
Itt a végeredményben már látunk egy síkot
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:
POWERSHELLnpx 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.tsximport { Dog } from "./components/Dog";...<meshrotation-x={-Math.PI / 2}><planeGeometry args={[20, 10]} /><meshStandardMaterial color="#70543E" /></mesh><Dogposition={new THREE.Vector3(0,0,0)}dogColor={character.dogColor}/></>);};
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<Canvascamera={{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} /><directionalLightposition={[5, 5, 5]}castShadowshadow-camera-left={-10}shadow-camera-right={10}shadow-camera-top={10}shadow-camera-bottom={-10}/><OrbitControls makeDefault /><meshrotation-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...<skinnedMeshname="Dog"geometry={dogGeom.geometry}material={materials.AtlasMaterial}skeleton={dogGeom.skeleton}rotation={[-Math.PI / 2, 0, 0]}scale={100}castShadowreceiveShadow>...
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.tsximport { 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ó kattintottsetDogPosition({ x: e.point.x, y: e.point.y, z: e.point.z });};useCursor(onFloor);return (...<meshrotation-x={-Math.PI / 2}onClick={handlePlaneClick}onPointerEnter={() => setOnFloor(true)}onPointerLeave={() => setOnFloor(false)}receiveShadow><planeGeometry args={[20, 10]} /><meshStandardMaterial color="#70543E" /></mesh><Dogposition={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.tsximport * 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-depsconst 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 definedif (!group.current || !props.position) {return;}// Calculate the distance to the target positionconst distance = group.current.position.distanceTo(props.position);// Check if the character needs to moveif (distance > 0.1) {// Calculate the direction vector towards the target positionconst direction = props.position.clone().sub(group.current.position).normalize();// Move the character towards the target positionconst moveStep = direction.multiplyScalar(SPEED);group.current.position.add(moveStep);// Make the character face the target positiongroup.current.lookAt(props.position);// Switch to the "run" animation if not already runningif (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 positionif (animation !== "AnimalArmature|AnimalArmature|AnimalArmature|Idle") {setAnimation("AnimalArmature|AnimalArmature|AnimalArmature|Idle");}}});...
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.
POWERSHELLcd ..
POWERSHELLmkdir server
POWERSHELLcd server
POWERSHELLnpm i socket.io nodemon
package.json-t módosítjuk a szerver indításhoz
JSON//server/package.json"scripts": {"dev": "nodemon index.js"},
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.jsimport { 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 colorconst hexColor = `#${red.toString(16)}${green.toString(16)}${blue.toString(16)}`;return hexColor;};io.on("connection", (socket) => {console.log("user connected");// Add character to Mapcharacters.set(socket.id, {id: socket.id,position: randomPosition(),dogColor: randomBrownHexColor()});// Emit all characters to all clientsio.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 charactersio.emit("characters", Array.from(characters.values()));}});socket.on("disconnect", () => {console.log("user disconnected");// Remove character from Mapcharacters.delete(socket.id);// Emit updated charactersio.emit("characters", Array.from(characters.values()));});});
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.tsximport 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 valuereturn () => {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.tsimport { 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.tsimport { 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.tsximport { Canvas } from "@react-three/fiber";import { Scene } from "./Scene";import { SocketProvider } from "./components/SocketProvider";function App() {return (<><SocketProvider><Canvascamera={{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.tsximport { 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 serversocket.emit("move", [newPosition.x, newPosition.y, newPosition.z]);};...{characters.map((character) => (<Dogkey={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.
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
A teljes kód megtalálható:
https://github.com/balazsfaragodev/socketio-react-three-fiber-tutorial
Oszd meg ezt a cikket
Indítsd a napod a legújabb technológiai áttörésekkel. Csatlakozz most, és merülj el az innovációban!