pmndrs / ecctrl

🕹️ A floating rigibody character controller
MIT License
564 stars 64 forks source link

feature request: flying and climbing states #111

Open hichemfantar opened 1 month ago

hichemfantar commented 1 month ago

these 2 states are critical for many use cases

relevant resources: https://youtu.be/jxecc2IGlWA?si=PP8NB8Nu1-I1WJpJ https://www.youtube.com/watch?v=cIZaqeJd28Y

related to #109

elisherer commented 1 month ago

Related to #29 too

elisherer commented 1 month ago

This is a flying POC I created. A drop-in replacement for <Ecctrl /> (replace with <EcctrlWithFlight />)

Press "F" to toggle flying mode. Press "Space" to go up, Press "Shift" to go down. Arrows move on the x/z axis

import { forwardRef, KeyboardEvent, useEffect, useImperativeHandle, useRef, useState } from "react";
import { useKeyboardControls } from "@react-three/drei";
import Ecctrl, { EcctrlProps } from "ecctrl";
import { useFrame } from "@react-three/fiber";
import { RapierRigidBody } from "@react-three/rapier";

const Flying: EcctrlProps = {
  jumpVel: 2,
  jumpForceToGroundMult: 0,
  fallingMaxVel: 0,
  fallingGravityScale: 0
};

function useIsInsideKeyboardControls() {
  try {
    return !!useKeyboardControls();
  } catch (e) {
    return false;
  }
}

export type EcctrlWithFlightProps = EcctrlProps & {
  forceFlying?: boolean;
  domElement?: HTMLElement;
};

export default forwardRef(function EcctrlWithFlight(
  { forceFlying, domElement, ...props }: EcctrlWithFlightProps,
  fref,
) {
  const cref = useRef<RapierRigidBody | undefined>();
  useImperativeHandle(fref, () => cref.current);

  const lastImpulseTime = useRef<number | null>(null);
  const isInsideKeyboardControls = useIsInsideKeyboardControls();
  const [, getKeys] = isInsideKeyboardControls ? useKeyboardControls() : [null];
  const [flying, setFlying] = useState(false);

  useEffect(() => {
    function toggleFlight(e: KeyboardEvent) {
      if (e.code === "KeyF") {
        setFlying(f => !f);
      }
    }
    const source = domElement || window;
    source.addEventListener("keyup", toggleFlight as any);
    return () => source.removeEventListener("keyup", toggleFlight as any);
  }, [domElement]);

  useFrame(state => {
    if (!getKeys || !(forceFlying ?? flying) || !cref.current || !isInsideKeyboardControls) return;
    if (lastImpulseTime.current !== null && state.clock.getElapsedTime() - lastImpulseTime.current < 0.1) return;
    const { jump, run } = getKeys();
    if (lastImpulseTime.current === null && (jump || run)) {
      // just started ascending or descending
      lastImpulseTime.current = state.clock.getElapsedTime();
    }
    if (jump) {
      cref.current.applyImpulse({ x: 0, y: 0.02, z: 0 }, true);
    } else if (run) {
      cref.current.applyImpulse({ x: 0, y: -0.02, z: 0 }, true);
    } else {
      lastImpulseTime.current = null;
    }
  });
  return <Ecctrl ref={cref as any} {...props} {...((forceFlying ?? flying) ? Flying : null)} />;
});

Sandbox (convert from above TS to JS)