الفيزياء
تفتح الفيزياء عالماً جديداً من الإمكانيات لمشاريعك ثلاثية الأبعاد. يمكنك إنشاء عوالم واقعية، وتفاعلات مستخدم، وحتى ألعاب.
في هذا الدرس، سنكتشف المفاهيم الأساسية أثناء بناء لعبة بسيطة.
لا تقلق، لا يلزم وجود معرفة سابقة، سنبدأ من الصفر. (لكي أعلمك كنت طالباً سيئاً جداً في الفيزياء في المدرسة، لذا إذا كنت أستطيع فعلها، يمكنك فعلها أيضاً!)
محركات الفيزياء
لإضافة الفيزياء إلى مشاريعنا ثلاثية الأبعاد، سنستخدم محرك فيزياء. محرك الفيزياء هو مكتبة ستتعامل مع جميع الرياضيات المعقدة نيابة عنا، مثل الجاذبية، الاصطدامات، القوى، إلخ.
في نظام JavaScript البيئي، هناك العديد من محركات الفيزياء المتوفرة.
اثنان من الأكثر شهرة هما Cannon.js و Rapier.js.
قام Poimandres (مرة أخرى) بإنشاء مكتبتين رائعين لاستخدام هذه المحركات مع React Three Fiber: react-three-rapier و use-cannon.
في هذا الدرس، سنستخدم react-three-rapier ولكنها متشابهة جداً والمفاهيم التي سنتعلمها هنا يمكن تطبيقها على كليهما.
لتثبيته، قم بتشغيل:
yarn add @react-three/rapier
نحن الآن جاهزون للبدء!
عالم الفيزياء
قبل إنشاء اللعبة، دعونا نتناول المفاهيم الأساسية.
أولاً، نحتاج إلى إنشاء عالم فيزياء. هذا العالم سيحتوي على جميع الكائنات الفيزيائية في مشهدنا. باستخدام react-three-rapier، نحتاج ببساطة إلى تغليف جميع كائناتنا بمكون <Physics>
:
// ... import { Physics } from "@react-three/rapier"; function App() { return ( <> <Canvas camera={{ position: [0, 6, 6], fov: 60 }} shadows> <color attach="background" args={["#171720"]} /> <Physics> <Experience /> </Physics> </Canvas> </> ); } export default App;
عالمنا الآن جاهز ولكن لا يحدث شيء! ذلك لأن ليس لدينا كائنات فيزيائية حتى الآن.
Rigidbody
لإضافة فيزياء إلى كائن، نحتاج إلى إضافة rigidbody. الـrigidbody هو مكون سيجعل كائننا يتحرك في العالم الفيزيائي.
ما الذي يمكن أن يؤدي إلى تحريك الكائن؟ القوى، مثل الجاذبية، التصادمات، أو تفاعلات المستخدم.
لنخبر عالم الفيزياء لدينا أن المكعب لدينا الموجود في Player.jsx
هو كائن فيزيائي بإضافة rigidbody له:
import { RigidBody } from "@react-three/rapier"; export const Player = () => { return <RigidBody>{/* ... */}</RigidBody>; };
الآن، المكعب لدينا يستجيب للجاذبية ويسقط لأسفل. ولكن للأسف، يستمر في السقوط إلى الأبد!
نحتاج إلى جعل الأرض كائن فيزيائي أيضًا حتى يمكن للمكعب أن يصطدم بها ويتوقف عن السقوط.
لنضف rigidbody إلى الأرض في Experience.jsx
، ولكن لأننا لا نريدها أن تتحرك وتسقط مثل المكعب، سنضيف الخاصية type="fixed"
:
// ... import { RigidBody } from "@react-three/rapier"; export const Experience = () => { return ( <> {/* ... */} <RigidBody type="fixed"> <mesh position-y={-0.251} receiveShadow> <boxGeometry args={[20, 0.5, 20]} /> <meshStandardMaterial color="mediumpurple" /> </mesh> </RigidBody> {/* ... */} </> ); };
بالعودة إلى النقطة البداية، لدينا مكعب لا يتحرك فوق الأرض. لكن، وراء الكواليس، لدينا مكعب يتفاعل مع الجاذبية ويتوقف بفضل الاصطدام مع الأرض.
القوى
الآن بعد أن أصبح لدينا عالم فيزيائي وأجسام فيزيائية، يمكننا البدء في اللعب بالقوى.
سنجعل المكعب يتحرك باستخدام مفاتيح الأسهم في لوحة المفاتيح. للقيام بذلك، دعونا نستخدم KeyboardControls التي اكتشفناها في درس الأحداث:
// ... import { KeyboardControls } from "@react-three/drei"; import { useMemo } from "react"; export const Controls = { forward: "forward", back: "back", left: "left", right: "right", jump: "jump", }; function App() { const map = useMemo( () => [ { name: Controls.forward, keys: ["ArrowUp", "KeyW"] }, { name: Controls.back, keys: ["ArrowDown", "KeyS"] }, { name: Controls.left, keys: ["ArrowLeft", "KeyA"] }, { name: Controls.right, keys: ["ArrowRight", "KeyD"] }, { name: Controls.jump, keys: ["Space"] }, ], [] ); return <KeyboardControls map={map}>{/* ... */}</KeyboardControls>; } export default App;
يمكننا الآن الحصول على المفاتيح المضغوطة في مكون Player.jsx
الخاص بنا:
// ... import { Controls } from "../App"; import { useKeyboardControls } from "@react-three/drei"; import { useFrame } from "@react-three/fiber"; export const Player = () => { const [, get] = useKeyboardControls(); useFrame(() => { if (get()[Controls.forward]) { } if (get()[Controls.back]) { } if (get()[Controls.left]) { } if (get()[Controls.right]) { } if (get()[Controls.jump]) { } }); // ... };
get() هو طريقة بديلة للحصول على المفاتيح المضغوطة باستخدام KeyboardControls.
الآن بعد أن حصلنا على المفاتيح المضغوطة، يمكننا تطبيق القوى على المكعب. يمكننا القيام بذلك باستخدام طريقتين:
applyImpulse
: تطبيق قوة فورية على الكائنsetLinVel
: ضبط السرعة الخطية للكائن
دعونا نكتشف كلا الطريقتين.
لنقم بإضافة useRef
إلى RigidBody، واستخدامها لتطبيق دفعة لتحريك المكعب في الاتجاه الصحيح:
import { useRef } from "react"; import { Vector3 } from "three"; const MOVEMENT_SPEED = 0.5; export const Player = () => { const rb = useRef(); const [, get] = useKeyboardControls(); const impulse = new Vector3(); useFrame(() => { impulse.x = 0; impulse.y = 0; impulse.z = 0; if (get()[Controls.forward]) { impulse.z -= MOVEMENT_SPEED; } if (get()[Controls.back]) { impulse.z += MOVEMENT_SPEED; } if (get()[Controls.left]) { impulse.x -= MOVEMENT_SPEED; } if (get()[Controls.right]) { impulse.x += MOVEMENT_SPEED; } if (get()[Controls.jump]) { } rb.current.applyImpulse(impulse, true); }); return <RigidBody ref={rb}>{/* ... */}</RigidBody>; };
احرص على تعيين المرجع إلى RigidBody وليس إلى mesh.
يعمل ذلك، ولكن السرعة تزداد بسرعة كبيرة وينزلق على الأرض. يمكننا إضافة المزيد من الاحتكاك إلى الأرض لإصلاح ذلك:
// ... export const Experience = () => { // ... return ( <> {/* ... */} <RigidBody type="fixed" friction={5}> {/* ... */} </RigidBody> {/* ... */} </> ); };
يؤدي الاحتكاك إلى دوران المكعب لأنه يتشبث بالأرض. يمكننا إصلاح ذلك عن طريق قفل دوران المكعب:
// ... export const Player = () => { // ... return ( <RigidBody ref={rb} lockRotations> {/* ... */} </RigidBody> ); };
الأمر أصبح أفضل بكثير الآن، لكن المكعب لا يزال ينزلق بشكل بسيط. يمكننا إصلاح ذلك عن طريق تعديل linear damping للمكعب، لكن لن نتابع هذا المسار في هذا الدرس.
لأنه سنحتاج أيضًا إلى ضبط السرعة القصوى للمكعب لمنعه من زيادة السرعة باستمرار. سنواجه مشكلات عندما نستخدم مفاتيح اليسار واليمين لدوران المكعب بدلاً من تحريكه.
فلنغير نظامنا لاستخدام setLinVel
بدلاً من applyImpulse
:
// ... import { useRef } from "react"; import { Vector3 } from "three"; const MOVEMENT_SPEED = 5; export const Player = () => { // ... const rb = useRef(); const vel = new Vector3(); useFrame(() => { vel.x = 0; vel.y = 0; vel.z = 0; if (get()[Controls.forward]) { vel.z -= MOVEMENT_SPEED; } if (get()[Controls.back]) { vel.z += MOVEMENT_SPEED; } if (get()[Controls.left]) { vel.x -= MOVEMENT_SPEED; } if (get()[Controls.right]) { vel.x += MOVEMENT_SPEED; } if (get()[Controls.jump]) { } rb.current.setLinvel(vel, true); }); return <RigidBody ref={rb}>{/* ... */}</RigidBody>; };
يمكنك أيضًا إزالة الاحتكاك من الأرض لأنه لم يعد ضروريًا.
F2
هو صديقك لتسمية المتغيرات بسرعة.
رائع! لدينا الآن مكعب يمكن تحريكه باستخدام مفاتيح الأسهم في لوحة المفاتيح.
لنقم بإضافة قوة القفز عندما يضغط المستخدم على مفتاح المسافة:
// ... const JUMP_FORCE = 8; export const Player = () => { // ... useFrame(() => { // ... if (get()[Controls.jump]) { vel.y += JUMP_FORCE; } rb.current.setLinvel(vel, true); }); return ( <RigidBody ref={rb} lockRotations> {/* ... */} </RigidBody> ); };
لدينا مشكلتان:
- المكعب لا يتفاعل بشكل صحيح مع الجاذبية (قارن مع المكعب الساقط في بداية الدرس)
- يمكن للمكعب القفز عندما يكون بالفعل في الهواء
مشكلة الجاذبية هي لأننا قمنا بتعيين السرعة يدويًا للمكعب على المحور y. نحتاج إلى تغييرها فقط عندما نقفل، وندع محرك الفيزياء يتولى الجاذبية في بقية الوقت:
// ... export const Player = () => { // ... useFrame(() => { const curVel = rb.current.linvel(); if (get()[Controls.jump]) { vel.y += JUMP_FORCE; } else { vel.y = curVel.y; } rb.current.setLinvel(vel, true); }); // ... };
نحن نحصل على السرعة الحالية للمكعب باستخدام rb.current.linvel()
وإذا لم نكن نقفز، نقوم بتعيين سرعة y
إلى السرعة الحالية.
لمنع المكعب من القفز عندما يكون بالفعل في الهواء، يمكننا التحقق مما إذا كان المكعب قد لمس الأرض قبل أن يتمكن من القفز مرة أخرى:
// ... export const Player = () => { // ... const inTheAir = useRef(false); useFrame(() => { // ... if (get()[Controls.jump] && !inTheAir.current) { vel.y += JUMP_FORCE; inTheAir.current = true; } else { vel.y = curVel.y; } rb.current.setLinvel(vel, true); }); // ... };
نحن الآن يمكننا القفز مرة واحدة فقط، والجاذبية تعمل ولكن بشكل بطئ قليلاً.
قبل أن نقوم بإصلاحها، دعونا نرى كيف يعمل نظام التصادم الخاص بنا.
المصادمات
المصادمات مسؤولة عن اكتشاف التصادمات بين الأجسام. يتم إرفاقها بمكون RigidBody.
يقوم Rapier تلقائيًا بإضافة مصادم لمكون RigidBody بناءً على هندسة الـmesh لكن يمكننا أيضًا إضافتها يدويًا.
لتصور المصادمات، يمكننا استخدام خاصية debug
على مكون Physics:
// ... function App() { // ... return ( <KeyboardControls map={map}> <Canvas camera={{ position: [0, 6, 6], fov: 60 }} shadows> <color attach="background" args={["#171720"]} /> <Physics debug> <Experience /> </Physics> </Canvas> </KeyboardControls> ); } export default App;
بما أننا نستخدم هندسة المكعب للأرضية، فإن المصادمات الحالية تغلفها بشكل مثالي ويمكن أن نراها بالكاد بسبب أوضاع التصحيح.
لنقم بتغيير مصادم المكعب إلى مصادم كرة:
import { vec3 } from "@react-three/rapier"; // ... export const Player = () => { // ... return ( <RigidBody ref={rb} lockRotations colliders={"ball"}> {/* ... */} </RigidBody> ); };
هذه الطريقة شبه أوتوماتيكية، نحن نخبر Rapier بنوع المصادم الذي نريده وسينشئه تلقائيًا بناءً على حجم الـmesh.
يمكننا أيضًا إضافة المصادم يدويًا وتعديله:
import { BallCollider } from "@react-three/rapier"; // ... export const Player = () => { // ... return ( <RigidBody ref={rb} lockRotations colliders={false}> {/* ... */} <BallCollider args={[1.5]} /> </RigidBody> ); };
قمنا بضبط colliders
لتكون false
لمنع Rapier من إنشاء مصادم تلقائيًا.
أنواع المصادمات المختلفة هي:
box
: مصادم مكعبball
: مصادم كرةhull
: فكر فيه كأنه غلاف حول الـmeshtrimesh
: مصادم سيلف الـmesh بشكل مثالي
استخدم دائمًا المصادم الأبسط لتحسين الأداء.
الآن بعد أن نعرف مصادمات المكعب والأرضية، دعنا نكتشف التصادم بينهما لمعرفة متى يكون المكعب على الأرض.
لنقم بإزالة ball collider من المكعب وإضافة خاصية onCollisionEnter
على RigidBody:
// ... export const Player = () => { // ... return ( <RigidBody {/* ... */} onCollisionEnter={({ other }) => { if (other.rigidBodyObject.name === "ground") { inTheAir.current = false; } }} > {/* ... */} </RigidBody> ); };
يمكننا الوصول إلى المصادم الآخر مع other.rigidBodyObject
والتحقق من اسمه لمعرفة ما إذا كان الأرض.
نحتاج إلى إضافة اسم لمصادم الأرضية:
// ... export const Experience = () => { return ( <> {/* ... */} <RigidBody type="fixed" name="ground"> {/* ... */} </RigidBody> {/* ... */} </> ); };
يمكننا الآن القفز مجددًا عند لمس الأرض.
الجاذبية
تم اكتشاف الجاذبية بواسطة إسحاق نيوتن في عام 1687... 🍎
حسنًا، أنا أمزح، نحن نعلم ما هي الجاذبية.
لدينا خياران لتغيير الجاذبية، يمكننا تغييرها بشكل عالمي باستخدام الخاصية gravity
في مكون Physics:
<Physics debug gravity={[0, -50, 0]}>
تأخذ مصفوفة من ثلاثة أرقام، واحدة لكل محور. على سبيل المثال يمكنك إنشاء تأثير رياح يؤثر فقط على الكائنات ذات الكتلة المنخفضة. يمكنك أيضًا إنشاء تأثير جاذبية القمر بضبط محور y لقيمة أقل.
لكن الجاذبية الافتراضية واقعية وتعمل بشكل جيد للعبتنا، لذا سنحتفظ بها ونؤثر على جاذبية المكعب الخاص بنا باستخدام الخاصية gravityScale
في RigidBody:
// ... export const Player = () => { // ... return ( <RigidBody // ... gravityScale={2.5} > {/* ... */} </RigidBody> ); };
الآن تبدو حركتنا في القفز مقنعة تمامًا!
لنضف كرة لنرى كيف يتفاعلون مع بعضهم البعض:
// ... export const Experience = () => { return ( <> {/* ... */} <RigidBody colliders={false} position-x={3} position-y={3} gravityScale={0.2} restitution={1.2} mass={1} > <Gltf src="/models/ball.glb" castShadow /> <BallCollider args={[1]} /> </RigidBody> {/* ... */} </> ); };
نحن بحاجة إلى إنشاء Collider الكرة يدويًا لأن حتى لو كانت تبدو ككرة، النموذج أكثر تعقيدًا وRapier لا ينشئ تلقائيًا Collider الصحيح لها.
يمكنك تعديل مستوى restitution
لجعل الكرة ترتد أكثر أو أقل، وgravityScale
لتجعلها تسقط بسرعة أكبر أو أقل.
يبدو جيدًا!
حان وقت إنشاء لعبتنا!
لعبة
لإنشاء هذه اللعبة، قمت بإعداد خريطة في Blender باستخدام الأصول من مجموعة متنوعة من ألعاب كاي لوسبرغ الصغيرة.
إنها حزمة رائعة خالية من حقوق الملكية تحتوي على الكثير من الأصول، أوصي بها بشدة!
ساحة اللعب
لنستخدم مكون Playground
في Experience
الخاص بنا، ونزيل الأرضية:
import { Playground } from "./Playground"; export const Experience = () => { return ( <> {/* ... */} {/* <RigidBody type="fixed" name="ground"> <mesh position-y={-0.251} receiveShadow> <boxGeometry args={[20, 0.5, 20]} /> <meshStandardMaterial color="mediumpurple" /> </mesh> </RigidBody> */} <Playground /> </> ); };
لا يوجد شيء مميز هنا، فهو الكود الناتج من
gltfjsx
والنموذج ثلاثي الأبعاد، لقد قمت فقط بإضافة خاصيتيreceiveShadow
وcastShadow
يدوياً إلى الهياكل الشبكية.
لدينا ساحة اللعب، ولكن ليس لدينا أرضية بعد الآن. نحتاج إلى تغليف الهياكل الشبكية لساحة اللعب مع RigidBody:
// ... import { RigidBody } from "@react-three/rapier"; export function Playground(props) { // ... return ( <group {...props} dispose={null}> <RigidBody type="fixed" name="ground"> {/* ... */} </RigidBody> </group> ); } // ...
لقد تم تغليف كل الهياكل الشبكية مع RigidBody وتم إضافة box collider لكل منها. ولكن لأن لدينا أشكال أكثر تعقيداً، سنستخدم trimesh collider بدلاً من ذلك:
<RigidBody type="fixed" name="ground" colliders="trimesh">
الآن كل الهياكل الشبكية مغطاة بشكل مثالي.
نظام تحكم الشخص الثالث
لجعل لعبتنا قابلة للعب، نحتاج إلى إضافة نظام تحكم الشخص الثالث لمكعبنا.
لنقم بجعل الكاميرا تتبع اللاعب الخاص بنا. بينما نحرك RigidBody الخاص به، يمكننا إدخال الكاميرا بداخله:
// ... import { PerspectiveCamera } from "@react-three/drei"; export const Player = () => { // ... return ( <RigidBody // ... > <PerspectiveCamera makeDefault position={[0, 5, 8]} /> {/* ... */} </RigidBody> ); };
موضعها يعمل بشكل مثالي، لكن بشكل افتراضي، الكاميرا تنظر إلى الأصل [0, 0, 0]
ونريدها أن تنظر إلى المكعب.
سنحتاج إلى تحديثها في كل frame. للقيام بذلك، يمكننا إنشاء ref على الكاميرا واستخدام useFrame
hook:
// ... export const Player = () => { const camera = useRef(); const cameraTarget = useRef(new Vector3(0, 0, 0)); // ... useFrame(() => { cameraTarget.current.lerp(vec3(rb.current.translation()), 0.5); camera.current.lookAt(cameraTarget.current); // ... }); return ( <RigidBody // ... > <PerspectiveCamera makeDefault position={[0, 5, 8]} ref={camera} /> {/* ... */} </RigidBody> ); };
لاسترجاع موضع المكعب، بما أنه جسم Rapier، نحتاج لاستخدام rb.current.translation()
. نستخدم طريقة vec3
لتحويل المتجه الخاص بـ Rapier إلى متجه three.js.
نستخدم lerp
و cameraTarget
لتحريك الكاميرا بسلاسة إلى موضع المكعب. نستخدم lookAt
لجعل الكاميرا تنظر إلى المكعب.
الآن الكاميرا تتبع المكعب بشكل صحيح، ولكن نظام الحركة الخاص بنا ليس الأنسب لهذا النوع من الألعاب.
بدلاً من استخدام الأسهم الأعلى/الأسفل للتحرك على المحور z
، والأسهم اليسار/اليمين للتحرك على المحور x
، سنستخدم الأسهم اليسار/اليمين لتدوير اللاعب الخاص بنا والأسهم الأعلى/الأسفل للتحرك للأمام/للخلف:
// ... import { euler, quat } from "@react-three/rapier"; // ... const ROTATION_SPEED = 5; export const Player = () => { // ... useFrame(() => { cameraTarget.current.lerp(vec3(rb.current.translation()), 0.5); camera.current.lookAt(cameraTarget.current); const rotVel = { x: 0, y: 0, z: 0, }; const curVel = rb.current.linvel(); vel.x = 0; vel.y = 0; vel.z = 0; if (get()[Controls.forward]) { vel.z -= MOVEMENT_SPEED; } if (get()[Controls.back]) { vel.z += MOVEMENT_SPEED; } if (get()[Controls.left]) { rotVel.y += ROTATION_SPEED; } if (get()[Controls.right]) { rotVel.y -= ROTATION_SPEED; } rb.current.setAngvel(rotVel, true); // apply rotation to x and z to go in the right direction const eulerRot = euler().setFromQuaternion(quat(rb.current.rotation())); vel.applyEuler(eulerRot); if (get()[Controls.jump] && !inTheAir.current) { vel.y += JUMP_FORCE; inTheAir.current = true; } else { vel.y = curVel.y; } rb.current.setLinvel(vel, true); }); // ... };
لنقم بتفصيلها:
- نقوم بإنشاء متغير
rotVel
لتخزين سرعة الدوران - نقوم بتغيير سرعة الدوران على المحور
y
عندما يضغط المستخدم على السهم الأيسر أو الأيمن - نطبق سرعة الدوران على المكعب باستخدام
rb.current.setAngvel(rotVel, true)
- نسترجع الدوران الحالي للمكعب باستخدام
rb.current.rotation()
- نحوله إلى زوايا euler باستخدام
euler().setFromQuaternion
- نطبق الدوران على السرعة باستخدام
applyEuler
لتحويلها إلى الاتجاه الصحيح
إعادة الظهور
حاليًا، إذا سقطنا من الملعب، فإننا نسقط إلى الأبد. نحتاج إلى إضافة نظام إعادة الظهور.
ما سنفعله هو إضافة RigidBodies كبيرة جدًا تحت الملعب. عندما يتصل اللاعب بها، سنقوم بنقله إلى نقطة الظهور:
// ... import { CuboidCollider } from "@react-three/rapier"; export const Experience = () => { return ( <> {/* ... */} <RigidBody type="fixed" colliders={false} sensor name="space" position-y={-5} > <CuboidCollider args={[50, 0.5, 50]} /> </RigidBody> {/* ... */} </> ); };
قمنا بتسمية RigidBody الخاص بنا بـ space
وقمنا بتعيينه كـ sensor. لا يؤثر المستشعر على عالم الفيزياء، بل يُستخدم فقط لاكتشاف الاصطدامات.
الآن في مكون Player
الخاص بنا، يمكننا استخدام الخاصية onIntersectionEnter
على RigidBody واستدعاء دالة respawn
:
// ... export const Player = () => { // ... const respawn = () => { rb.current.setTranslation({ x: 0, y: 5, z: 0, }); }; return ( <RigidBody // ... onIntersectionEnter={({ other }) => { if (other.rigidBodyObject.name === "space") { respawn(); } }} > {/* ... */} </RigidBody> ); };
نستخدم أسلوب setTranslation
لنقل المكعب إلى نقطة الظهور.
onIntersectionEnter
هو المكافئ للاصطدام لـonCollisionEnter
للمستشعرات.
جاهز للقفز في الفراغ؟
نظام إعادة الظهور لدينا يعمل! بدأت تبدو وكأنها لعبة.
Swiper
هذا swiper الجميل يهدف إلى إخراجنا من ساحة اللعب!
لأنه نحتاج إلى تطبيق قوة لتحريكه، سنقوم بنقله إلى RigidBody منفصل:
import React, { useRef } from "react"; export function Playground(props) { // ... const swiper = useRef(); return ( <group {...props} dispose={null}> <RigidBody type="kinematicVelocity" colliders={"trimesh"} ref={swiper} restitution={3} name="swiper" > <group name="swiperDouble_teamRed" rotation-y={Math.PI / 4} position={[0.002, -0.106, -21.65]} > {/* ... */} </group> </RigidBody> {/* ... */} </group> ); }
النوع kinematicVelocity
هو نوع خاص من RigidBody يمكن تحريكه باستخدام الدالتين setLinvel
و setAngvel
ولكنه لن يتأثر بالقوى الخارجية. (هذا يمنع لاعبنا من تحريك الـ swiper)
لنحدد السرعة الزاوية للـ swiper في مكون Experience
الخاص بنا:
import React, { useEffect, useRef } from "react"; export function Playground(props) { // ... useEffect(() => { swiper.current.setAngvel({ x: 0, y: 3, z: 0 }, true); }); // ... }
إذا كنت تتساءل لماذا نستخدم
useEffect
بدلاً منuseFrame
، فهذا لأن السرعة ثابتة، طالما أننا لا نغيرها، فستستمر بالدوران.
إنه يدور! لا تتردد في ضبط قيمة y
لتغيير سرعة الدوران.
التأثير عند طردنا من ساحة اللعب ليس طبيعيًا. هذا هو الجانب السلبي لاستخدام setLinvel
على اللاعب الخاص بنا بدلاً من applyImpulse
ولكنه أيضًا بسط العديد من الأمور.
ما نراه هو: يتم ركل المكعب وإسقاطه، ويتوقف فورًا لأن setLinvel
توقفه.
حل سريع لهذه المشكلة هو تعطيل setLinvel
عندما نتعرض للركل لفترة قصيرة من الوقت:
// ... export const Player = () => { // ... const punched = useRef(false); useFrame(() => { // ... if (!punched.current) { rb.current.setLinvel(vel, true); } }); // ... return ( <RigidBody // ... onCollisionEnter={({ other }) => { if (other.rigidBodyObject.name === "ground") { inTheAir.current = false; } if (other.rigidBodyObject.name === "swiper") { punched.current = true; setTimeout(() => { punched.current = false; }, 200); } }} // ... > {/* ... */} </RigidBody> ); };
التأثير الآن أفضل بكثير!
البوابات
لننهِ لعبتنا عن طريق نقل اللاعب إلى خط النهاية عندما يدخل البوابات.
نبدأ بفصل البوابات عن جسم الصلب الرئيسي للملعب، ونقوم بإنشاء جسم صلب جديد sensor مع collider مخصص:
// ... import { CuboidCollider } from "@react-three/rapier"; export function Playground(props) { // ... return ( <group {...props} dispose={null}> {/* ... */} <RigidBody type="fixed" name="gateIn" sensor colliders={false} position={[-20.325, -0.249, -28.42]} > <mesh receiveShadow castShadow name="gateLargeWide_teamBlue" geometry={nodes.gateLargeWide_teamBlue.geometry} material={materials["Blue.020"]} rotation={[0, 1.571, 0]} /> <CuboidCollider position={[-1, 0, 0]} args={[0.5, 2, 1.5]} /> </RigidBody> {/* ... */} </group> ); }
يمكننا رؤية المنطقة التي سيتم فيها اكتشاف التصادم.
الآن في كود Player
يمكننا معالجة هذا السيناريو:
// ... import { useThree } from "@react-three/fiber"; export const Player = () => { // ... const respawn = () => { rb.current.setTranslation({ x: 0, y: 5, z: 0, }); }; const scene = useThree((state) => state.scene); const teleport = () => { const gateOut = scene.getObjectByName("gateLargeWide_teamYellow"); rb.current.setTranslation(gateOut.position); }; return ( <RigidBody // ... onIntersectionEnter={({ other }) => { if (other.rigidBodyObject.name === "space") { respawn(); } if (other.rigidBodyObject.name === "gateIn") { teleport(); } }} > {/* ... */} </RigidBody> ); };
نستخدم getObjectByName
للحصول على موقع البوابة ونقل اللاعب إليه.
نظام البوابة لدينا يعمل!
الظلال
ربما لاحظت أن الظلال لا تعمل بشكل صحيح. ملعبنا كبير جدًا بالنسبة لإعدادات الظل الافتراضية.
الظلال مقطوعة.
لإصلاح ذلك، نحتاج إلى ضبط إعدادات shadow camera:
// ... import { useHelper } from "@react-three/drei"; import { useRef } from "react"; import * as THREE from "three"; export const Experience = () => { const shadowCameraRef = useRef(); useHelper(shadowCameraRef, THREE.CameraHelper); return ( <> <directionalLight position={[-50, 50, 25]} intensity={0.4} castShadow shadow-mapSize-width={1024} shadow-mapSize-height={1024} > <PerspectiveCamera ref={shadowCameraRef} attach={"shadow-camera"} near={55} far={86} fov={80} /> </directionalLight> <directionalLight position={[10, 10, 5]} intensity={0.2} /> {/* ... */} </> ); };
سيكون من المستحيل العثور على القيم الصحيحة بدون CameraHelper
.
للعثور عليها، تحتاج إلى أن تكون المشهد بالكامل بين المستويات القريبة والبعيدة مرسومة بواسطة المساعد.
راجع درس الظلال إذا كنت بحاجة إلى تجديد معلوماتك عن كيفية عمل الظلال.
الآن تعمل ظلالنا بشكل صحيح...
... وانتهت لعبتنا! 🎉
الخاتمة
الآن لديك الأساسيات لـ إنشاء محاكاة الفيزياء والألعاب الخاصة بك باستخدام React Three Fiber!
لا تتوقف هنا، هناك العديد من الأشياء التي يمكنك القيام بها لتحسين هذه اللعبة وجعلها أكثر متعة:
- إضافة مؤقت ونظام لأفضل النتائج
- إنشاء مستويات أخرى باستخدام أصول الحزمة
- إنشاء حركة رائعة عند الضغط على الزر النهائي من قبل اللاعب
- إضافة عقبات أخرى
- إضافة NPCs
استكشف محركات الفيزياء والمكتبات الأخرى للعثور على ما يناسب احتياجاتك بشكل أفضل.
لقد قمت بعمل دروس ألعاب على قناتي في YouTube باستخدام React Three Fiber. يمكنك مشاهدتها إذا كنت ترغب في معرفة المزيد عن الفيزياء و الألعاب و متعددة اللاعبين.
React Three Fiber: The Ultimate Guide to 3D Web Development
✨ You have reached the end of the preview ✨
Go to the next level with Three.js and React Three Fiber!
Get full access to this lesson and the complete course when you enroll:
- 🔓 Full lesson videos with no limits
- 💻 Access to the final source code
- 🎓 Course progress tracking & completion
- 💬 Invite to our private Discord community
One-time payment. Lifetime updates included.