Modern web geliştirmede klasik ve web uygulamaları arasındaki sınırlar her geçen gün bulanıklaşıyor. Bugün yalnızca etkileşimli web siteleri değil, aynı zamanda doğrudan tarayıcıda tam teşekküllü oyunlar da oluşturabiliyoruz. Bunu mümkün kılan araçlardan biri, React teknolojisini kullanarak tabanlı 3D grafikler oluşturmaya yönelik güçlü bir araç olan kitaplığıdır.
React Three Fiber, web üzerinde 3D grafikler oluşturmak için React'in yapısını ve ilkelerini kullanan, Three.js üzerinde bir sarmalayıcıdır. Bu yığın, geliştiricilerin Three.js'nin gücünü React'ın rahatlığı ve esnekliğiyle birleştirmesine olanak tanıyarak uygulama oluşturma sürecini daha sezgisel ve organize hale getirir.
React Three Fiber'in temelinde, bir sahnede yarattığınız her şeyin bir React bileşeni olduğu fikri yatmaktadır. Bu, geliştiricilerin tanıdık kalıpları ve metodolojileri uygulamalarına olanak tanır.
React Three Fiber'in temel avantajlarından biri React ekosistemiyle entegrasyon kolaylığıdır. Bu kütüphaneyi kullanırken diğer React araçları hala kolayca entegre edilebilir.
Web-GameDev son yıllarda basit 2D oyunlarından masaüstü uygulamalarıyla karşılaştırılabilecek karmaşık 3D projelere doğru evrimleşerek büyük değişiklikler geçirdi. Popülerlik ve yeteneklerdeki bu artış, Web-GameDev'i göz ardı edilemeyecek bir alan haline getiriyor.
Modern tarayıcılar, oldukça basit web tarama araçlarından, karmaşık uygulamaları ve oyunları çalıştırmak için güçlü platformlara dönüşerek uzun bir yol kat etti. Chrome , Firefox , Edge ve diğerleri gibi önemli tarayıcılar, yüksek performans sağlamak için sürekli olarak optimize ediliyor ve geliştiriliyor; bu da onları karmaşık uygulamalar geliştirmek için ideal bir platform haline getiriyor.
Tarayıcı tabanlı oyunların gelişimini hızlandıran temel araçlardan biri . Bu standart, geliştiricilerin, 3D oyunların performansını önemli ölçüde artıran donanım grafik hızlandırmasını kullanmasına olanak tanıdı. Diğer webAPI'lerle birlikte WebGL , doğrudan tarayıcıda etkileyici web uygulamaları oluşturmaya yönelik yeni olanaklar sunar.
Öncelikle bir React proje şablonuna ihtiyacımız olacak. Öyleyse onu yükleyerek başlayalım.
npm create vite@latest
npm install three @react-three/fiber @react-three/drei @react three/rapier zustand @tweenjs/tween.js
Main.jsx dosyasına sayfada kapsam olarak görüntülenecek bir div öğesi ekleyin. Bir Canvas bileşeni ekleyin ve kameranın görüş alanını ayarlayın. Canvas bileşeninin içine Uygulama bileşenini yerleştirin.
Kullanıcı arayüzü öğelerini ekranın tam yüksekliğine kadar genişletmek ve kapsamı ekranın ortasında bir daire olarak görüntülemek için index.css'ye stiller ekleyelim.
Uygulama bileşenine, oyun sahnemizde arka plan olarak gökyüzü şeklinde görüntülenecek bir Gökyüzü bileşeni ekliyoruz.
Bir Ground bileşeni oluşturalım ve onu App bileşenine yerleştirelim.
Ground'da düz bir yüzey elemanı oluşturun. Y ekseninde, bu düzlem kameranın görüş alanı içinde olacak şekilde onu aşağı doğru hareket ettirin. Ayrıca düzlemi yatay hale getirmek için X ekseninde çevirin.
Varsayılan olarak sahnede aydınlatma yoktur, bu nedenle nesneyi her taraftan aydınlatan ve yönlendirilmiş bir ışına sahip olmayan ambientLight ışık kaynağını ekleyelim. Bir parametre olarak ışığın yoğunluğunu ayarlayın.
Varlıklar klasörüne dokulu bir PNG görüntüsü ekleyin.
Sahneye bir doku yüklemek için @react- three/drei paketindeki useTexture kancasını kullanalım. Ve kancanın parametresi olarak dosyaya aktarılan doku görüntüsünü aktaracağız. Görüntünün yatay eksenlerdeki tekrarını ayarlayın.
@react-third/drei paketindeki PointerLockControls bileşenini kullanarak, fareyi hareket ettirdiğinizde hareket etmeyecek, ancak kameranın sahnedeki konumunu değiştirecek şekilde imleci ekrana sabitleyin.
Ground bileşeni için küçük bir düzenleme yapalım.
<mesh position={[0, 3, -5]}> <boxGeometry /> </mesh>
Sahneye "fizik" eklemek için @react-third/rapier paketindeki Fizik bileşenini kullanın. Parametre olarak, yerçekimi kuvvetlerini eksenler boyunca ayarladığımız yerçekimi alanını yapılandırın.
<Physics gravity={[0, -20, 0]}> <Ground /> <mesh position={[0, 3, -5]}> <boxGeometry /> </mesh> </Physics>
Ancak küpümüz fizik bileşeninin içinde ama ona hiçbir şey olmuyor. Küpün gerçek bir fiziksel nesne gibi davranmasını sağlamak için onu @react- three/rapier paketindeki RigidBody bileşenine sarmamız gerekiyor.
Ground bileşenine geri dönelim ve zemin yüzeyinin üzerine sarmalayıcı olarak bir RigidBody bileşeni ekleyelim.
Sahnedeki karakteri kontrol edecek Player bileşeni oluşturalım.
Karakter, eklenen küple aynı fiziksel nesnedir, bu nedenle sahnedeki küpün yanı sıra zemin yüzeyiyle de etkileşime girmelidir. Bu yüzden RigidBody bileşenini ekliyoruz. Ve karakteri kapsül şeklinde yapalım.
Player bileşenini Fizik bileşeninin içine yerleştirin.
Karakter WASD tuşları kullanılarak kontrol edilecek ve Ara Çubuğu kullanılarak atlanacaktır.
Kendi reaksiyon kancamızla karakteri hareket ettirme mantığını uyguluyoruz.
Bir hooks.js dosyası oluşturalım ve buraya yeni bir usePersonControls fonksiyonu ekleyelim.
usePersonControls kancasını uyguladıktan sonra, karakteri kontrol ederken kullanılmalıdır. Oyuncu bileşeninde hareket durumu izlemeyi ekleyeceğiz ve karakterin hareket yönünün vektörünü güncelleyeceğiz.
Karakterin konumunu güncellemek için @react- three/fiber paketi tarafından sağlanan Frame'i kullanalım. Bu kanca, requestAnimationFrame'e benzer şekilde çalışır ve işlevin gövdesini saniyede yaklaşık 60 kez çalıştırır.
Kod Açıklaması:
1. const playerRef = useRef(); Oynatıcı nesnesi için bir bağlantı oluşturun. Bu bağlantı, sahnedeki oynatıcı nesnesi ile doğrudan etkileşime izin verecektir.
2. const { ileri, geri, sol, sağ, atlama } = usePersonControls(); Bir kanca kullanıldığında, oynatıcının o anda hangi kontrol düğmelerine basıldığını gösteren boole değerlerine sahip bir nesne döndürülür.
3. useFrame((durum) => { ... }); Kanca, animasyonun her karesinde çağrılır. Bu kancanın içinde oyuncunun konumu ve doğrusal hızı güncellenir.
4. if (!playerRef.current) geri dönerse; Bir oynatıcı nesnesinin varlığını kontrol eder. Oynatıcı nesnesi yoksa işlev, hataları önlemek için yürütmeyi durduracaktır.
5. sabit hız = playerRef.current.linvel(); Oyuncunun mevcut doğrusal hızını alın.
6. frontVector.set(0, 0, geri - ileri); Basılan düğmelere göre ileri/geri hareket vektörünü ayarlayın.
7. sideVector.set(sol - sağ, 0, 0); Sol/sağ hareket vektörünü ayarlayın.
8. Direction.subVectors(frontVector, sideVector).normalize().multiplyScalar(MOVE_SPEED); Oyuncu hareketinin son vektörünü, hareket vektörlerini çıkararak, sonucu normalleştirerek (vektör uzunluğu 1 olacak şekilde) ve hareket hızı sabitiyle çarparak hesaplayın.
9. playerRef.current.wakeUp(); Değişikliklere tepki verdiğinden emin olmak için oynatıcı nesnesini "uyandırır". Bu yöntemi kullanmazsanız, bir süre sonra nesne "uykuya" girecek ve konum değişikliklerine tepki vermeyecektir.
10. playerRef.current.setLinvel({ x: yön.x, y: hız.y, z: yön.z }); Hesaplanan hareket yönüne göre oyuncunun yeni doğrusal hızını ayarlayın ve mevcut dikey hızı koruyun (atlamaları veya düşmeleri etkilemeyecek şekilde).
Sonuç olarak WASD tuşlarına basıldığında karakter sahnede hareket etmeye başladı. Ayrıca küple de etkileşime girebilir çünkü her ikisi de fiziksel nesnelerdir.
Atlamayı uygulamak için @dimforge/rapier3d-compat ve @react-third/rapier paketlerindeki işlevselliği kullanalım. Bu örnekte karakterin yerde olduğunu ve atlama tuşuna basıldığını kontrol edelim. Bu durumda karakterin yönünü ve ivme kuvvetini Y eksenine ayarlıyoruz.
Oyuncu için kütle ekleyeceğiz ve tüm eksenlerde dönüşü engelleyeceğiz, böylece sahnedeki diğer nesnelerle çarpıştığında farklı yönlere düşmeyecek.
Kod Açıklaması:
- const dünyası = rapier.world; Rapier fizik motoru sahnesine erişim elde ediliyor. Tüm fiziksel nesneleri içerir ve etkileşimlerini yönetir.
- const ray = world.castRay(new RAPIER.Ray(playerRef.current.translation(), { x: 0, y: -1, z: 0 })); Burası "raycasting"in (raycasting) gerçekleştiği yerdir. Oyuncunun mevcut konumundan başlayan ve y ekseninin aşağısını gösteren bir ışın oluşturulur. Bu ışın, sahnedeki herhangi bir nesneyle kesişip kesişmediğini belirlemek için sahneye "dökülür".
- const grounded = ray && ray.collider && Math.abs(ray.toi) <= 1.5; Oyuncunun yerde olup olmadığı kontrol edilir:
- ışın - ışının yaratılıp yaratılmadığı;
- ray.collider - ışının olay yerindeki herhangi bir nesneyle çarpışıp çarpışmadığı;
- Math.abs(ray.toi) - ışının "maruz kalma süresi". Bu değerin verilen değerden küçük veya ona eşit olması, oyuncunun "yerde" sayılacak kadar yüzeye yakın olduğunu gösterebilir.
Ayrıca, sahnedeki diğer nesnelerle etkileşime girecek fiziksel bir nesne ekleyerek "iniş" durumunu belirlemek için kullanılan ışın izlemeli algoritmanın doğru çalışması için Zemin bileşenini değiştirmeniz gerekir.
Kamerayı hareket ettirmek için oyuncunun mevcut konumunu alacağız ve çerçeve her yenilendiğinde kameranın konumunu değiştireceğiz. Ve karakterin kameranın yönlendirildiği yörünge boyunca tam olarak hareket etmesi için applicationEuler'ı eklememiz gerekiyor.
Kod Açıklaması:
ApplyEuler yöntemi, belirtilen Euler açılarına dayalı olarak bir vektöre rotasyon uygular. Bu durumda kamera döndürme yön vektörüne uygulanır. Bu, oyuncunun kameranın döndürüldüğü yönde hareket etmesi için hareketi kamera yönüne göre eşleştirmek için kullanılır.
Player'ın boyutunu biraz ayarlayalım ve küpe göre daha uzun hale getirelim, CapsuleCollider'ın boyutunu artıralım ve "atlama" mantığını düzeltelim.
Sahnenin tamamen boş hissettirmemesi için küp oluşturmayı ekleyelim. Json dosyasında küplerin her birinin koordinatlarını listeleyin ve ardından bunları sahnede görüntüleyin. Bunu yapmak için, içinde bir koordinat dizisini listeleyeceğimiz cubes.json dosyasını oluşturun.
[ [0, 0, -7], [2, 0, -7], [4, 0, -7], [6, 0, -7], [8, 0, -7], [10, 0, -7] ]
Cube.jsx dosyasında, döngü halinde küpler oluşturacak bir Küpler bileşeni oluşturun. Ve Cube bileşeni doğrudan oluşturulan nesne olacaktır.
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> ); }
Oluşturduğumuz Küpler bileşenini önceki tek küpü silerek Uygulama bileşenine ekleyelim.
Modeli sahneye aktarmak için ihtiyacımız olan formatı elde etmek için gltf-pipeline eklenti paketini kurmamız gerekecek.
npm i -D gltf-pipeline
gltf-pipeline paketini kullanarak modeli GLTF formatından GLB formatına yeniden dönüştürün, çünkü bu formatta tüm model verileri tek bir dosyaya yerleştirilir. Oluşturulan dosyanın çıktı dizini olarak ortak klasörü belirtiyoruz.
gltf-pipeline -i weapon/scene.gltf -o public/weapon.glb
Daha sonra sahneye eklemek için bu modelin işaretlemesini içerecek bir reaksiyon bileşeni oluşturmamız gerekiyor. @react- three/fiber geliştiricilerinin kullanalım.
Dönüştürücüye gitmek, dönüştürülen silah.glb dosyasını yüklemenizi gerektirecektir.
Dönüştürücüde, kodunu projemize yeni bir WeaponModel.jsx dosyasına aktaracağımız, bileşenin adını dosyayla aynı adla değiştireceğimiz, oluşturulan reaksiyon bileşenini göreceğiz.
Şimdi oluşturulan modeli sahneye aktaralım. App.jsx dosyasına WeaponModel bileşenini ekleyin.
Sahnede gölgeleri etkinleştirmek için Canvas bileşenine gölgeler niteliğini eklemeniz gerekir.
Daha sonra yeni bir ışık kaynağı eklememiz gerekiyor. Sahnede zaten ambientLight olmasına rağmen, yönlü bir ışık huzmesine sahip olmadığından nesneler için gölge oluşturamaz. O halde directionLight adında yeni bir ışık kaynağı ekleyelim ve onu yapılandıralım. " Yayınlama " gölge modunu etkinleştirme özelliği castShadow'dur . Bu nesnenin diğer nesnelere gölge düşürebileceğini belirten bu parametrenin eklenmesidir.
Daha sonra Ground bileşenine bir başka özellik olan takeShadow'u ekleyelim, bu da sahnedeki bileşenin gölgeleri kendi üzerinde alıp görüntüleyebilmesi anlamına gelir.
Sahnedeki diğer nesnelere de benzer özellikler eklenmelidir: küpler ve oynatıcı. Küpler için castShadow ve getShadow'u ekleyeceğiz, çünkü hem gölge oluşturabilir hem de gölge alabilirler ve oyuncu için yalnızca castShadow'u ekleyeceğiz.
Player için castShadow'u ekleyelim.
Cube için castShadow'u ve getShadow'u ekleyin.
Bunun nedeni, varsayılan olarak kameranın, DirectionLight'tan görüntülenen gölgelerin yalnızca küçük bir alanını yakalamasıdır. Bu görünürlük alanını genişletmek için ilave gölge-kamera-(üst, alt, sol, sağ) niteliklerini ekleyerek directionLight bileşenini kullanabiliriz. Bu nitelikleri ekledikten sonra gölge biraz bulanıklaşacaktır. Kaliteyi artırmak için shadow-mapSize özelliğini ekleyeceğiz.
Şimdi birinci şahıs silah gösterimini ekleyelim. Silah davranışı mantığını ve 3D modelin kendisini içerecek yeni bir Silah bileşeni oluşturun.
import {WeaponModel} from "./WeaponModel.jsx"; export const Weapon = (props) => { return ( <group {...props}> <WeaponModel /> </group> ); }
Bu bileşeni karakterin RigidBody'si ile aynı seviyeye yerleştirelim ve useFrame kancasında kameradan gelen değerlerin konumuna göre konumu ve dönüş açısını ayarlayacağız.
Karakterin yürüyüşünü daha doğal hale getirmek için hareket ederken silaha hafif bir hareket ekleyeceğiz. Animasyonu oluşturmak için kurulu tween.js kütüphanesini kullanacağız.
Silah bileşeni, useRef kancası aracılığıyla ona bir referans ekleyebilmeniz için bir grup etiketine sarılacaktır.
Animasyonu kaydetmek için biraz useState ekleyelim.
Kod Açıklaması:
- const twSwayingAnimation = new TWEEN.Tween(currentPosition) ... Geçerli konumundan yeni bir konuma "sallanan" bir nesnenin animasyonunu oluşturma.
- const twSwayingBackAnimation = new TWEEN.Tween(currentPosition) ... İlk animasyon tamamlandıktan sonra nesnenin başlangıç konumuna geri dönmesini içeren bir animasyon oluşturma.
- twSwayingAnimation.chain(twSwayingBackAnimation); İki animasyonu birbirine bağlayarak ilk animasyon tamamlandığında ikinci animasyonun otomatik olarak başlamasını sağlar.
UseEffect'te animasyon başlatma fonksiyonunu çağırıyoruz.
Kod Açıklaması:
- const isMoving = yön.uzunluk() > 0; Burada nesnenin hareket durumu kontrol edilir. Yön vektörünün uzunluğu 0'dan büyükse bu, nesnenin bir hareket yönüne sahip olduğu anlamına gelir.
- if (isMoving && isSwayingAnimationFinished) { ... } Bu durum, nesne hareket ediyorsa ve "sallanma" animasyonu bitmişse yürütülür.
App bileşeninde ara animasyonunu güncelleyeceğimiz bir useFrame ekleyelim.
TWEEN.update(), TWEEN.js kitaplığındaki tüm etkin animasyonları günceller. Bu yöntem, tüm animasyonların sorunsuz çalışmasını sağlamak için her animasyon karesinde çağrılır.
Atışın yapıldığı anı, yani fare tuşuna basıldığı anı tanımlamamız gerekiyor. Bu durumu depolamak için useState'i , silah nesnesine bir referansı depolamak için useRef'i ve fare düğmesine basıp bırakmak için iki olay işleyicisini ekleyelim.
Fare tuşuna basıldığında geri tepme animasyonu uygulayalım. Bu amaçla tween.js kütüphanesini kullanacağız.
Rastgele bir geri tepme animasyonu vektörü elde etmek için işlevler oluşturalım - createdRecoilOffset ve createdNewPositionOfRecoil .
Geri tepme animasyonunu başlatmak için bir işlev oluşturun. Ayrıca, her çekimde animasyonun yeniden başlatılması ve yeni bitiş koordinatlarının oluşturulması için "çekim" durumunu bağımlılık olarak belirleyeceğimiz useEffect'i de ekleyeceğiz.
Ve useFrame'e , ateş etmek için fare tuşunu "basılı tutmak" için bir kontrol ekleyelim, böylece ateşleme animasyonu, tuş bırakılana kadar durmaz.
Bunu yapmak için useState aracılığıyla bazı yeni durumlar ekleyelim.