الفيزياء

Starter pack

تفتح الفيزياء عالماً جديداً من الإمكانيات لمشاريعك ثلاثية الأبعاد. يمكنك إنشاء عوالم واقعية، وتفاعلات مستخدم، وحتى ألعاب.

في هذا الدرس، سنكتشف المفاهيم الأساسية أثناء بناء لعبة بسيطة.

لا تقلق، لا يلزم وجود معرفة سابقة، سنبدأ من الصفر. (لكي أعلمك كنت طالباً سيئاً جداً في الفيزياء في المدرسة، لذا إذا كنت أستطيع فعلها، يمكنك فعلها أيضاً!)

محركات الفيزياء

لإضافة الفيزياء إلى مشاريعنا ثلاثية الأبعاد، سنستخدم محرك فيزياء. محرك الفيزياء هو مكتبة ستتعامل مع جميع الرياضيات المعقدة نيابة عنا، مثل الجاذبية، الاصطدامات، القوى، إلخ.

في نظام 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>
      {/* ... */}
    </>
  );
};

Cube on top of the ground

بالعودة إلى النقطة البداية، لدينا مكعب لا يتحرك فوق الأرض. لكن، وراء الكواليس، لدينا مكعب يتفاعل مع الجاذبية ويتوقف بفضل الاصطدام مع الأرض.

القوى

الآن بعد أن أصبح لدينا عالم فيزيائي وأجسام فيزيائية، يمكننا البدء في اللعب بالقوى.

سنجعل المكعب يتحرك باستخدام مفاتيح الأسهم في لوحة المفاتيح. للقيام بذلك، دعونا نستخدم 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 من إنشاء مصادم تلقائيًا.

Ball collider wrapping our cube

أنواع المصادمات المختلفة هي:

  • box: مصادم مكعب
  • ball: مصادم كرة
  • hull: فكر فيه كأنه غلاف حول الـmesh
  • trimesh: مصادم سيلف الـ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 باستخدام الأصول من مجموعة متنوعة من ألعاب كاي لوسبرغ الصغيرة.

Kay-kit mini game variety pack

إنها حزمة رائعة خالية من حقوق الملكية تحتوي على الكثير من الأصول، أوصي بها بشدة!

ساحة اللعب

لنستخدم مكون 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 يدوياً إلى الهياكل الشبكية.

Playground

لدينا ساحة اللعب، ولكن ليس لدينا أرضية بعد الآن. نحتاج إلى تغليف الهياكل الشبكية لساحة اللعب مع 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">

Trimesh collider

الآن كل الهياكل الشبكية مغطاة بشكل مثالي.

نظام تحكم الشخص الثالث

لجعل لعبتنا قابلة للعب، نحتاج إلى إضافة نظام تحكم الشخص الثالث لمكعبنا.

لنقم بجعل الكاميرا تتبع اللاعب الخاص بنا. بينما نحرك 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 الجميل يهدف إلى إخراجنا من ساحة اللعب!

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>
  );
}

Gate collider

يمكننا رؤية المنطقة التي سيتم فيها اكتشاف التصادم.

الآن في كود 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 للحصول على موقع البوابة ونقل اللاعب إليه.

نظام البوابة لدينا يعمل!

الظلال

ربما لاحظت أن الظلال لا تعمل بشكل صحيح. ملعبنا كبير جدًا بالنسبة لإعدادات الظل الافتراضية.

Shadows cut

الظلال مقطوعة.

لإصلاح ذلك، نحتاج إلى ضبط إعدادات 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.

للعثور عليها، تحتاج إلى أن تكون المشهد بالكامل بين المستويات القريبة والبعيدة مرسومة بواسطة المساعد.

راجع درس الظلال إذا كنت بحاجة إلى تجديد معلوماتك عن كيفية عمل الظلال.

Shadows helper

الآن تعمل ظلالنا بشكل صحيح...

... وانتهت لعبتنا! 🎉

الخاتمة

الآن لديك الأساسيات لـ إنشاء محاكاة الفيزياء والألعاب الخاصة بك باستخدام React Three Fiber!

لا تتوقف هنا، هناك العديد من الأشياء التي يمكنك القيام بها لتحسين هذه اللعبة وجعلها أكثر متعة:

  • إضافة مؤقت ونظام لأفضل النتائج
  • إنشاء مستويات أخرى باستخدام أصول الحزمة
  • إنشاء حركة رائعة عند الضغط على الزر النهائي من قبل اللاعب
  • إضافة عقبات أخرى
  • إضافة NPCs

استكشف محركات الفيزياء والمكتبات الأخرى للعثور على ما يناسب احتياجاتك بشكل أفضل.

لقد قمت بعمل دروس ألعاب على قناتي في YouTube باستخدام React Three Fiber. يمكنك مشاهدتها إذا كنت ترغب في معرفة المزيد عن الفيزياء و الألعاب و متعددة اللاعبين.

Three.js logoReact logo

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
Unlock the Full Course – Just $85

One-time payment. Lifetime updates included.