Skip to article frontmatterSkip to article content

Movements

In the context of developing this game, we have developed scripts allowing the player to move in ways other than movement in the real world.

These movements are:

These components are available for download here: link.

To use movements, you need to include the components within the scene.

export default function App() {
  const xrOrigin: any = useRef(null);
  const [scene, setScene] = useState < string > "danubebleu";

  return (
    <div className="canvas-container">
      <button onClick={() => store.enterVR()}>Enter VR</button>
      <Canvas style={{ background: "skyblue" }}>
        <XR store={store}>
          <XROrigin ref={xrOrigin} />

          <MovePlayer xrOrigin={xrOrigin} />
          <FlyPlayer xrOrigin={xrOrigin} />
          <RotatePlayer />
          <JumpPlayer xrOrigin={xrOrigin} />
        </XR>
      </Canvas>
    </div>
  );
}

MovePlayer

This component allows the player to move in a way similar to walking.

FlyPlayer

This component allows horizontal movement of the player (similar to flying).

It takes the following options:

optiondescriptiondefault value
alwaysallows enabling or disabling always active modetrue
joystickallows selecting the button managing movement. true = right joystick, false = A and B buttonstrue

RotatePlayer

This component allows the player to rotate on themselves.

To reduce simulator sickness, two modes are available:

It takes the following option:

optiondescriptiondefault value
discreteselects rotation mode: discrete / continuousfalse (continuous)

JumpPlayer

This component allows the player to jump.

Warning, it is controlled by the A key which can conflict with flying. You must therefore manage both together.

optiondescriptiondefault value
enableallows enabling or disabling jumpingtrue
alwaysFly (unused)allows indicating if flight mode is always activatedfalse

Things to Know

Accessing Reference Space

To access the reference space, you need to use the WebGL rendering component.

const { gl } = useThree() as { gl: WebGLRenderer & { xr: any } };

const referenceSpace = gl.xr.getReferenceSpace();

Accessing Controller Buttons

To get the values of player controller buttons, you need to get the controller then its “gamepad” where inputs are exposed:

const controller = useXRInputSourceState("controller", "left"); //get the controller

const thumbstickState = controller.gamepad?.["xr-standard-thumbstick"]; //Access joystick state on this controller
const buttonA = controller.gamepad?.["a-button"]?.button; //access A button

Buttons are booleans set to true if pressed and false otherwise.

Accessing Headset Orientation

To access headset orientation, you will need the webGL renderer and its xr property.

const frame = gl.xr.getFrame();
if (!frame) return;

const viewerPose = frame.getViewerPose(referenceSpace);
if (!viewerPose) return;

const headQuaternion = new Quaternion(
  viewerPose.transform.orientation.x,
  viewerPose.transform.orientation.y,
  viewerPose.transform.orientation.z,
  viewerPose.transform.orientation.w
);

The quaternion contains the headset orientation. You can then apply it to correctly orient the player’s movement.

In React Three Fiber, this way to get the frame const frame = gl.xr.getFrame(); works only in useFrame() hook. There is an other way, maybe more conventionnal in R3F to get it : through useFrame parameters.

useFrame((_, __, frame) => {

  //some code

  const viewerPose = frame.getViewerPose(referenceSpace);
  if (!viewerPose) return;

  const headQuaternion = new Quaternion(
    viewerPose.transform.orientation.x,
    viewerPose.transform.orientation.y,
    viewerPose.transform.orientation.z,
    viewerPose.transform.orientation.w
  );

  //some code

}

How to Make the Player Move

There are two ways to make the player move. First, using the XRReferenceSpace: we make the space move around the player who always stays at the origin. Second, a specific way of React Three Fiber: the XROrigin which represents the virtual position of the player.

One issue that appears is that moving only the XROrigin makes the player’s real and virtual positions unsynchronized, which creates problems when we want to access the positions of certain elements (like hand position) which are in the “real world / XRReferenceSpace” coordinate space. You might say “then let’s use only the XRReferenceSpace then”, but no: when using it we have a one-frame delay between the movements of the player and the movements of the controllers.

We must investigate to find the better way to make a movement

How to Move with XROrigin

Before, make sure you have access to an XROrigin reference.

useFrame(() => {
  if (xrOrigin?.current == null || controller == null) {
    return;
  }

  // Retrieve controller thumbstick
  const thumbstickState = controller.gamepad?.["xr-standard-thumbstick"];
  if (thumbstickState == null) {
    return;
  }

  const referenceSpace = gl.xr.getReferenceSpace();
  if (!referenceSpace) {
    return;
  }

  const moveX: number = thumbstickState.xAxis ?? 0;
  const moveZ: number = thumbstickState.yAxis ?? 0;

  const speed: number = 0.03; // meter or unit per frame

  /* --- Claim headset orientation --- */
  //
  /* ----------------------------------------- */

  // Apply orientation
  const forward = new Vector3();
  forward.set(0, 0, -1).applyQuaternion(headQuaternion);
  forward.y = 0;
  forward.normalize();

  const right = new Vector3();
  right.set(1, 0, 0).applyQuaternion(headQuaternion);
  right.y = 0;
  right.normalize();

  const moveDirection = new Vector3();
  moveDirection.addScaledVector(forward, -moveZ);
  moveDirection.addScaledVector(right, moveX);

  // Update position of xrOrigin
  xrOrigin.current.position.x += moveDirection.x * speed;
  xrOrigin.current.position.z += moveDirection.z * speed;
});

How to Move with XRReferenceSpace

To move this XRReferenceSpace, we need to apply a translation to it. The base code of movement is the same as with xrOrigin (get joystick value and head orientation and then apply translation). Here is how to apply translation:

// Base code of movement

const offsetTransform = new XRRigidTransform(
  {
    x: -moveDirection.x * speed,
    y: 0,
    z: -moveDirection.z * speed,
  },
  { x: 0, y: 0, z: 0, w: 1 } // Optional (a quaternion for reference space rotation, here it's a null rotation just to show)
);

try {
  const newReferenceSpace =
    referenceSpace.getOffsetReferenceSpace(offsetTransform);
  this.context.renderer.xr.setReferenceSpace(newReferenceSpace);
} catch (e) {
  console.error("Error during referenceSpace modification", e);
}