In der modernen Webentwicklung verschwimmen die Grenzen zwischen klassischen und Webanwendungen täglich. Heute können wir nicht nur interaktive Websites, sondern auch vollwertige Spiele direkt im Browser erstellen. Eines der Tools, das dies ermöglicht, ist die Bibliothek – ein leistungsstarkes Tool zum Erstellen von 3D-Grafiken auf Basis von mithilfe der React- Technologie.
React Three Fiber ist ein Wrapper für Three.js , der die Struktur und Prinzipien von React nutzt, um 3D-Grafiken im Web zu erstellen. Mit diesem Stack können Entwickler die Leistungsfähigkeit von Three.js mit dem Komfort und der Flexibilität von React kombinieren und so den Prozess der Anwendungserstellung intuitiver und organisierter gestalten.
Das Herzstück von React Three Fiber ist die Idee, dass alles, was Sie in einer Szene erstellen, eine React- Komponente ist. Dadurch können Entwickler bekannte Muster und Methoden anwenden.
Einer der Hauptvorteile von React Three Fiber ist die einfache Integration in das React- Ökosystem. Alle anderen React- Tools können bei Verwendung dieser Bibliothek weiterhin problemlos integriert werden.
Web-GameDev hat in den letzten Jahren große Veränderungen erfahren und sich von einfachen 2D- Spielen zu komplexen 3D-Projekten entwickelt, die mit Desktop-Anwendungen vergleichbar sind. Diese wachsende Beliebtheit und Leistungsfähigkeit machen Web-GameDev zu einem Bereich, der nicht ignoriert werden darf.
Moderne Browser haben einen langen Weg zurückgelegt und sich von relativ einfachen Webbrowser-Tools zu leistungsstarken Plattformen für die Ausführung komplexer Anwendungen und Spiele entwickelt. Große Browser wie Chrome , Firefox , Edge und andere werden ständig optimiert und weiterentwickelt, um eine hohe Leistung zu gewährleisten, was sie zu einer idealen Plattform für die Entwicklung komplexer Anwendungen macht.
Eines der wichtigsten Tools, das die Entwicklung browserbasierter Spiele vorangetrieben hat, ist . Dieser Standard ermöglichte Entwicklern die Nutzung der Hardware-Grafikbeschleunigung, was die Leistung von 3D-Spielen deutlich verbesserte. Zusammen mit anderen webAPIs eröffnet WebGL neue Möglichkeiten, beeindruckende Webanwendungen direkt im Browser zu erstellen.
Zunächst benötigen wir eine React- Projektvorlage. Beginnen wir also mit der Installation.
npm create vite@latest
npm install three @react-three/fiber @react-three/drei @react three/rapier zustand @tweenjs/tween.js
Fügen Sie in der Datei main.jsx ein div-Element hinzu, das auf der Seite als Bereich angezeigt wird. Fügen Sie eine Canvas- Komponente ein und legen Sie das Sichtfeld der Kamera fest. Platzieren Sie innerhalb der Canvas- Komponente die App- Komponente.
Fügen wir index.css Stile hinzu, um die UI-Elemente auf die volle Höhe des Bildschirms auszudehnen und den Bereich als Kreis in der Mitte des Bildschirms anzuzeigen.
In der App- Komponente fügen wir eine Sky- Komponente hinzu, die als Hintergrund in unserer Spielszene in Form eines Himmels angezeigt wird.
Erstellen wir eine Ground- Komponente und platzieren sie in der App- Komponente.
Erstellen Sie in Ground ein flaches Oberflächenelement. Bewegen Sie es auf der Y-Achse nach unten, sodass diese Ebene im Sichtfeld der Kamera liegt. Und drehen Sie die Ebene auch um die X-Achse, um sie horizontal zu machen.
Standardmäßig gibt es in der Szene keine Beleuchtung, also fügen wir eine Lichtquelle ambientLight hinzu, die das Objekt von allen Seiten beleuchtet und keinen gerichteten Strahl hat. Stellen Sie als Parameter die Intensität des Glühens ein.
Fügen Sie im Assets- Ordner ein PNG-Bild mit einer Textur hinzu.
Um eine Textur in die Szene zu laden, verwenden wir den useTexture- Hook aus dem @react-drei/drei- Paket. Und als Parameter für den Hook übergeben wir das in die Datei importierte Texturbild. Stellen Sie die Wiederholung des Bildes in den horizontalen Achsen ein.
Fixieren Sie mithilfe der PointerLockControls- Komponente aus dem @react-drei/drei- Paket den Cursor auf dem Bildschirm, sodass er sich nicht bewegt, wenn Sie die Maus bewegen, sondern die Position der Kamera in der Szene ändert.
Nehmen wir eine kleine Änderung für die Ground- Komponente vor.
<mesh position={[0, 3, -5]}> <boxGeometry /> </mesh>
Verwenden Sie die Physics- Komponente aus dem Paket @react- three/rapier, um der Szene „Physik“ hinzuzufügen. Als Parameter konfigurieren wir das Schwerkraftfeld, wobei wir die Gravitationskräfte entlang der Achsen einstellen.
<Physics gravity={[0, -20, 0]}> <Ground /> <mesh position={[0, 3, -5]}> <boxGeometry /> </mesh> </Physics>
Allerdings befindet sich unser Würfel innerhalb der Physikkomponente, es passiert ihm aber nichts. Damit sich der Würfel wie ein echtes physisches Objekt verhält, müssen wir ihn in die RigidBody- Komponente aus dem @react-drei/rapier- Paket einbinden.
Kehren wir zur Ground- Komponente zurück und fügen eine RigidBody- Komponente als Wrapper über der Bodenoberfläche hinzu.
Erstellen wir eine Player- Komponente, die den Charakter in der Szene steuert.
Der Charakter ist das gleiche physische Objekt wie der hinzugefügte Würfel, daher muss er sowohl mit der Bodenoberfläche als auch mit dem Würfel in der Szene interagieren. Deshalb fügen wir die RigidBody- Komponente hinzu. Und lassen Sie uns den Charakter in Form einer Kapsel erstellen.
Platzieren Sie die Player- Komponente innerhalb der Physics-Komponente.
Der Charakter wird mit den WASD- Tasten gesteuert und mit der Leertaste springt er.
Mit unserem eigenen React-Hook implementieren wir die Logik zum Bewegen des Charakters.
Erstellen wir eine Datei „hooks.js“ und fügen dort eine neue Funktion „usePersonControls“ hinzu.
Nach der Implementierung des usePersonControls- Hooks sollte dieser zur Steuerung des Charakters verwendet werden. In der Player- Komponente fügen wir die Verfolgung des Bewegungszustands hinzu und aktualisieren den Vektor der Bewegungsrichtung des Charakters.
Um die Position des Charakters zu aktualisieren, verwenden wirFrame, der vom @react-two/fiber- Paket bereitgestellt wird. Dieser Hook funktioniert ähnlich wie requestAnimationFrame und führt den Hauptteil der Funktion etwa 60 Mal pro Sekunde aus.
Code-Erklärung:
1. const playerRef = useRef(); Erstellen Sie einen Link für das Player-Objekt. Dieser Link ermöglicht eine direkte Interaktion mit dem Spielerobjekt in der Szene.
2. const { vorwärts, rückwärts, links, rechts, springen } = usePersonControls(); Wenn ein Hook verwendet wird, wird ein Objekt mit booleschen Werten zurückgegeben, das angibt, welche Steuertasten gerade vom Spieler gedrückt werden.
3. useFrame((state) => { ... }); Der Hook wird in jedem Frame der Animation aufgerufen. Innerhalb dieses Hakens werden die Position und die lineare Geschwindigkeit des Spielers aktualisiert.
4. if (!playerRef.current) return; Prüft, ob ein Spielerobjekt vorhanden ist. Wenn kein Spielerobjekt vorhanden ist, stoppt die Funktion die Ausführung, um Fehler zu vermeiden.
5. const Velocity = playerRef.current.linvel(); Ermitteln Sie die aktuelle lineare Geschwindigkeit des Spielers.
6. frontVector.set(0, 0, rückwärts – vorwärts); Stellen Sie den Vorwärts-/Rückwärtsbewegungsvektor basierend auf den gedrückten Tasten ein.
7. sideVector.set(links - rechts, 0, 0); Legen Sie den Links-/Rechts-Bewegungsvektor fest.
8. Direction.subVectors(frontVector, sideVector).normalize().multiplyScalar(MOVE_SPEED); Berechnen Sie den endgültigen Vektor der Spielerbewegung, indem Sie die Bewegungsvektoren subtrahieren, das Ergebnis normalisieren (so dass die Vektorlänge 1 ist) und mit der Bewegungsgeschwindigkeitskonstante multiplizieren.
9. playerRef.current.wakeUp(); „Weckt“ das Spielerobjekt auf, um sicherzustellen, dass es auf Änderungen reagiert. Wenn Sie diese Methode nicht verwenden, wird das Objekt nach einiger Zeit „schlafen“ und nicht auf Positionsänderungen reagieren.
10. playerRef.current.setLinvel({ x: Direction.x, y: Velocity.y, z: Direction.z }); Stellen Sie die neue lineare Geschwindigkeit des Spielers basierend auf der berechneten Bewegungsrichtung ein und behalten Sie die aktuelle vertikale Geschwindigkeit bei (um Sprünge oder Stürze nicht zu beeinträchtigen).
Als Ergebnis begann sich die Figur beim Drücken der WASD- Tasten in der Szene zu bewegen. Er kann auch mit dem Würfel interagieren, da es sich bei beiden um physische Objekte handelt.
Um den Sprung zu implementieren, verwenden wir die Funktionalität der Pakete @dimforge/rapier3d-compat und @react-drei/rapier . In diesem Beispiel überprüfen wir, ob sich die Figur auf dem Boden befindet und die Sprungtaste gedrückt wurde. In diesem Fall legen wir die Richtung und Beschleunigungskraft des Charakters auf der Y-Achse fest.
Für den Spieler werden wir Masse hinzufügen und die Rotation auf allen Achsen blockieren, damit er nicht in verschiedene Richtungen umfällt, wenn er mit anderen Objekten in der Szene kollidiert.
Code-Erklärung:
- const world = rapier.world; Erhalten Sie Zugang zur Rapier- Physik-Engine-Szene. Es enthält alle physischen Objekte und verwaltet deren Interaktion.
- const ray = world.castRay(new RAPIER.Ray(playerRef.current.translation(), { x: 0, y: -1, z: 0 })); Hier findet „Raycasting“ (Raycasting) statt. Es wird ein Strahl erstellt, der an der aktuellen Position des Spielers beginnt und entlang der y-Achse nach unten zeigt. Dieser Strahl wird in die Szene „geworfen“, um festzustellen, ob er ein Objekt in der Szene schneidet.
- constgrounded = ray && ray.collider && Math.abs(ray.toi) <= 1.5; Die Bedingung wird überprüft, wenn der Spieler am Boden liegt:
- Strahl – ob der Strahl erstellt wurde;
- ray.collider – ob der Strahl mit einem beliebigen Objekt in der Szene kollidiert ist;
- Math.abs(ray.toi) – die „Belichtungszeit“ des Strahls. Wenn dieser Wert kleiner oder gleich dem angegebenen Wert ist, kann dies darauf hindeuten, dass sich der Spieler nahe genug an der Oberfläche befindet, um als „am Boden“ zu gelten.
Sie müssen auch die Bodenkomponente ändern, damit der Raytrace-Algorithmus zur Bestimmung des „Lande“-Status korrekt funktioniert, indem Sie ein physisches Objekt hinzufügen, das mit anderen Objekten in der Szene interagiert.
Um die Kamera zu bewegen, ermitteln wir die aktuelle Position des Players und ändern die Position der Kamera jedes Mal, wenn das Bild aktualisiert wird. Und damit sich der Charakter genau entlang der Flugbahn bewegt, auf die die Kamera gerichtet ist, müssen wir applyEuler hinzufügen.
Code-Erklärung:
Die Methode applyEuler wendet eine Drehung auf einen Vektor basierend auf angegebenen Euler-Winkeln an. In diesem Fall wird die Kameradrehung auf den Richtungsvektor angewendet. Dies wird verwendet, um die Bewegung relativ zur Kameraausrichtung anzupassen, sodass sich der Spieler in die Richtung bewegt, in die die Kamera gedreht wird.
Passen wir die Größe von Player leicht an und machen ihn im Verhältnis zum Würfel höher, wodurch wir CapsuleCollider vergrößern und die „Sprung“-Logik korrigieren.
Damit sich die Szene nicht völlig leer anfühlt, fügen wir die Würfelgenerierung hinzu. Listen Sie in der JSON-Datei die Koordinaten der einzelnen Würfel auf und zeigen Sie sie dann in der Szene an. Erstellen Sie dazu eine Datei „cubes.json“ , in der wir ein Array von Koordinaten auflisten.
[ [0, 0, -7], [2, 0, -7], [4, 0, -7], [6, 0, -7], [8, 0, -7], [10, 0, -7] ]
Erstellen Sie in der Datei Cube.jsx eine Cubes- Komponente, die Cubes in einer Schleife generiert. Und die Cube- Komponente wird direkt zum generierten Objekt.
import {RigidBody} from "@react-three/rapier"; import cubes from "./cubes.json"; export const Cubes = () => { return cubes.map((coords, index) => <Cube key={index} position={coords} />); } const Cube = (props) => { return ( <RigidBody {...props}> <mesh castShadow receiveShadow> <meshStandardMaterial color="white" /> <boxGeometry /> </mesh> </RigidBody> ); }
Fügen wir die erstellte Cubes- Komponente zur App- Komponente hinzu, indem wir den vorherigen einzelnen Cube löschen.
Um das Format zu erhalten, das wir zum Importieren des Modells in die Szene benötigen, müssen wir das Zusatzpaket gltf-pipeline installieren.
npm i -D gltf-pipeline
Konvertieren Sie das Modell mithilfe des gltf-pipeline- Pakets erneut vom GLTF-Format in das GLB-Format , da in diesem Format alle Modelldaten in einer Datei abgelegt werden. Als Ausgabeverzeichnis für die generierte Datei geben wir den öffentlichen Ordner an.
gltf-pipeline -i weapon/scene.gltf -o public/weapon.glb
Dann müssen wir eine Reaktionskomponente generieren, die das Markup dieses Modells enthält, um es der Szene hinzuzufügen. Nutzen wir die der @react-two/fiber- Entwickler.
Wenn Sie zum Konverter gehen, müssen Sie die konvertierte Datei Weapon.glb laden.
Im Konverter sehen wir die generierte Reaktionskomponente, deren Code wir in einer neuen Datei WeaponModel.jsx in unser Projekt übertragen und dabei den Namen der Komponente in denselben Namen wie die Datei ändern.
Jetzt importieren wir das erstellte Modell in die Szene. Fügen Sie in der App.jsx- Datei die WeaponModel- Komponente hinzu.
Um Schatten in der Szene zu aktivieren, müssen Sie der Canvas- Komponente das Schattenattribut hinzufügen.
Als nächstes müssen wir eine neue Lichtquelle hinzufügen. Trotz der Tatsache, dass ambientLight bereits vor Ort ist, kann es keine Schatten für Objekte erzeugen, da es keinen gerichteten Lichtstrahl hat. Fügen wir also eine neue Lichtquelle namens DirectionalLight hinzu und konfigurieren sie. Das Attribut zum Aktivieren des „ cast “-Schattenmodus ist castShadow . Das Hinzufügen dieses Parameters gibt an, dass dieses Objekt einen Schatten auf andere Objekte werfen kann.
Danach fügen wir der Bodenkomponente ein weiteres Attribut „receiveShadow“ hinzu, was bedeutet, dass die Komponente in der Szene Schatten auf sich selbst empfangen und anzeigen kann.
Ähnliche Attribute sollten anderen Objekten in der Szene hinzugefügt werden: Würfeln und Spieler. Für die Würfel werden wir „castShadow “ und „receiveShadow“ hinzufügen, da sie sowohl Schatten werfen als auch empfangen können, und für den Spieler werden wir nur „castShadow“ hinzufügen.
Fügen wir castShadow für Player hinzu.
Fügen Sie „castShadow“ und „receiveShadow“ für Cube hinzu.
Der Grund dafür ist, dass die Kamera standardmäßig nur einen kleinen Bereich der angezeigten Schatten von DirectionalLight erfasst. Wir können für die DirectionalLight- Komponente durch Hinzufügen zusätzlicher Attribute „shadow-camera-“ (oben, unten, links, rechts) diesen Sichtbarkeitsbereich erweitern. Nach dem Hinzufügen dieser Attribute wird der Schatten leicht unscharf. Um die Qualität zu verbessern, werden wir das Attribut „shadow-mapSize“ hinzufügen.
Fügen wir nun die Ego-Waffenanzeige hinzu. Erstellen Sie eine neue Waffenkomponente , die die Verhaltenslogik der Waffe und das 3D-Modell selbst enthält.
import {WeaponModel} from "./WeaponModel.jsx"; export const Weapon = (props) => { return ( <group {...props}> <WeaponModel /> </group> ); }
Platzieren wir diese Komponente auf derselben Ebene wie den RigidBody des Charakters und legen wir im useFrame- Hook die Position und den Drehwinkel basierend auf der Position der Werte von der Kamera fest.
Um den Gang des Charakters natürlicher zu gestalten, fügen wir ein leichtes Wackeln der Waffe während der Bewegung hinzu. Zum Erstellen der Animation verwenden wir die installierte Bibliothek tween.js .
Die Weapon- Komponente wird in ein Gruppen-Tag eingeschlossen, sodass Sie über den useRef- Hook einen Verweis darauf hinzufügen können.
Fügen wir einen useState hinzu, um die Animation zu speichern.
Code-Erklärung:
- const twSwayingAnimation = new TWEEN.Tween(currentPosition) ... Erstellen einer Animation eines Objekts, das von seiner aktuellen Position zu einer neuen Position „schwingt“.
- const twSwayingBackAnimation = new TWEEN.Tween(currentPosition) ... Erstellen einer Animation des Objekts, das nach Abschluss der ersten Animation zu seiner Startposition zurückkehrt.
- twSwayingAnimation.chain(twSwayingBackAnimation); Verbinden Sie zwei Animationen, sodass die zweite Animation automatisch startet, wenn die erste Animation abgeschlossen ist.
In useEffect rufen wir die Animationsinitialisierungsfunktion auf.
Code-Erklärung:
- const isMoving = Direction.length() > 0; Hier wird der Bewegungszustand des Objekts überprüft. Wenn der Richtungsvektor eine Länge größer als 0 hat, bedeutet dies, dass das Objekt eine Bewegungsrichtung hat.
- if (isMoving && isSwayingAnimationFinished) { ... } Dieser Zustand wird ausgeführt, wenn sich das Objekt bewegt und die „schwingende“ Animation beendet ist.
Fügen wir in der App- Komponente einen useFrame hinzu, in dem wir die Tween-Animation aktualisieren.
TWEEN.update() aktualisiert alle aktiven Animationen in der TWEEN.js- Bibliothek. Diese Methode wird bei jedem Animationsframe aufgerufen, um sicherzustellen, dass alle Animationen reibungslos ablaufen.
Wir müssen den Moment definieren, in dem ein Schuss abgefeuert wird, also wenn die Maustaste gedrückt wird. Fügen wir useState hinzu, um diesen Zustand zu speichern, useRef , um einen Verweis auf das Waffenobjekt zu speichern, und zwei Ereignishandler zum Drücken und Loslassen der Maustaste.
Lassen Sie uns eine Rückstoßanimation beim Klicken mit der Maustaste implementieren. Zu diesem Zweck verwenden wir die Bibliothek tween.js .
Lassen Sie uns Funktionen erstellen, um einen zufälligen Vektor der Rückstoßanimation zu erhalten – „generateRecoilOffset “ und „generateNewPositionOfRecoil “.
Erstellen Sie eine Funktion, um die Rückstoßanimation zu initialisieren. Wir werden auch useEffect hinzufügen, in dem wir den „Shot“-Status als Abhängigkeit angeben, sodass bei jedem Shot die Animation erneut initialisiert und neue Endkoordinaten generiert werden.
Und in useFrame fügen wir eine Prüfung zum „Gedrückthalten“ der Maustaste zum Abfeuern hinzu, damit die Abfeueranimation nicht stoppt, bis die Taste losgelassen wird.
Dazu fügen wir über useState einige neue Zustände hinzu.