简体   繁体   中英

How to detect a sequence of button press with gamepad api

I am trying to use the Gamepad API in react to detect a sequence of button or key presses such as the konami code. I found a blog post that shows how to detect when a gamepad is connected and I can do that as well as handle a state change however I'm stuck when it comes to detecting a sequence. The reason is that there is no 'onKeyUp' type of event and if a button is held it will emit an event every 1/60th of a second (because I am using requestAnimationFrame). I made a useGamepads hook following the blog and I have a Controller component where I am trying to detect the sequence.

export default function useGamepads(callback) {
  const gamepads = useRef({});
  const requestRef = useRef();

  var haveEvents = "ongamepadconnected" in window;

  const addGamepad = (gamepad) => {

    // console.log('gamepad: ', gamepad);

    gamepads.current = {
      ...gamepads.current,
      [gamepad.index]: {
        id: gamepad.id,
        axes: gamepad.axes,
        buttons: gamepad.buttons,
        connected: gamepad.connected,
        mapping: gamepad.mapping,
        index: gamepad.index,
        vibrationActuator: gamepad.vibrationActuator,
      }
    };

    callback(gamepads.current);
  };

  const connectGamepadHandler = (e) => {
    addGamepad(e.gamepad);
  };

  const scanGamepads = () => {
    // Grab gamepads from browser API
    const detectedGamepads = navigator.getGamepads
      ? navigator.getGamepads()
      : navigator.webkitGetGamepads
      ? navigator.webkitGetGamepads()
      : [];

    // Loop through all detected controllers and add if not already in state
    for (let i = 0; i < detectedGamepads.length; i++) {
      if (detectedGamepads[i]) {
        addGamepad(detectedGamepads[i]);
      }
    }
  };

  // Add event listener for gamepad connecting
  useEffect(() => {
    window.addEventListener("gamepadconnected", connectGamepadHandler);
    return window.removeEventListener("gamepadconnected", connectGamepadHandler);
  }, []);

  // Update each gamepad's status on each "tick"
  const animate = (time) => {
    if (!haveEvents) scanGamepads();
    requestRef.current = requestAnimationFrame(animate);
  };

  useEffect(() => {
    requestRef.current = requestAnimationFrame(animate);
    return () => cancelAnimationFrame(requestRef.current);
  }, []);

  return gamepads.current;
}
const Controller = ({
  activeColor = "#2F80ED",
  inactiveColor = "#E0E0E0",
  showController = true,
  showControllerName = true,
  showLastControllerUpdate = true,
  onKonamiUnlocked = () => {},
}) => {
  
  const [gamepad, setGamepad] = useState({});
  const [gamepads, setGamepads] = useState(null);

  const [lastControllerUpdate, setLastControllerUpdate] = useState({});
  const [controllerName, setControllerName] = useState('');

  // konami
  const konamiCodeSequence = ['directionUp', 'directionUp', 'directionDown', 'directionDown', 'directionLeft', 'directionRight', 'directionLeft', 'directionRight', 'buttonDown', 'buttonRight'];
  const [sequence, setSequence] = useState([]);
  const [konamiUnlocked, setKonamiUnlocked] = useState(false);

    
  useGamepads((gp) => setGamepads(gp));

  const debouncedSetSequence = debounce(setSequence, 750);
  const throttledSetSequence = throttle(setSequence, 750);

  useEffect(() => {
    if (!lastControllerUpdate) return;
    throttledSetSequence(prev => {
      return [...prev, lastControllerUpdate];
    });
  }, [lastControllerUpdate]); // gamepad

  

  const calcDirectionVertical = (axe) => {
    // Up
    if (axe < -0.2) {
      return "up";
    }
    // Down
    if (axe > 0.2) {
      return "down";
    }
  };

  const calcDirectionHorizontal = (axe) => {
    // Left
    if (axe < -0.2) {
      return "left";
    }
    // Right
    if (axe > 0.2) {
      return "right";
    }
  };

  const createTransform = (direction) => {
    switch (direction) {
      case "up":
        return "translateY(-10px)";
      case "down":
        return "translateY(10px)";
      case "left":
        return "translateX(-10px)";
      case "right":
        return "translateX(10px)";

      default:
        return "";
    }
  };

  const onGamepadUpdate = (newGamePadState) => {
    for (const [key, value] of Object.entries(newGamePadState)) {
      if (typeof value === "boolean" && value === true) {
        const newVal = { 
          id: Math.random().toString(36).substr(2, 4),
          val: key.toString(),
        };
        // console.log('newVal: ', newVal);
        setLastControllerUpdate(newVal); // key.toString());
      }
    }
  };

  const throttledGamepadUpdate = throttle(onGamepadUpdate, 1000);
  const debouncedGamepadUpdate = debounce(onGamepadUpdate, 1000);

  useEffect(() => {

    if (gamepads && gamepads.length !== 0) {

      if (controllerName === '') {
        setControllerName(gamepads[0].id);
      }

      const newGamePadState = {
        directionUp: gamepads[0].buttons[12].pressed,
        directionDown: gamepads[0].buttons[13].pressed,
        directionLeft: gamepads[0].buttons[14].pressed,
        directionRight: gamepads[0].buttons[15].pressed,
        buttonDown: gamepads[0].buttons[0].pressed,
        buttonRight: gamepads[0].buttons[1].pressed,
        buttonLeft: gamepads[0].buttons[2].pressed,
        buttonUp: gamepads[0].buttons[3].pressed,
        buttonX: gamepads[0].buttons[16].pressed,
        // top of controller
        buttonLT: gamepads[0].buttons[6].pressed,
        buttonLB: gamepads[0].buttons[4].pressed,
        buttonRT: gamepads[0].buttons[7].pressed,
        buttonRB: gamepads[0].buttons[5].pressed,

        select: gamepads[0].buttons[8].pressed,
        start: gamepads[0].buttons[9].pressed,
        analogLeft: gamepads[0].axes[0] > 0.3 || gamepads[0].axes[0] < -0.3 || gamepads[0].axes[1] > 0.3 || gamepads[0].axes[1] < -0.3,
        analogRight: gamepads[0].axes[2] > 0.3 || gamepads[0].axes[2] < -0.3 || gamepads[0].axes[3] > 0.3 || gamepads[0].axes[3] < -0.3,
        analogLeftDirection: [
          calcDirectionHorizontal(gamepads[0].axes[0]),
          calcDirectionVertical(gamepads[0].axes[1])
        ],
        analogRightDirection: [
          calcDirectionHorizontal(gamepads[0].axes[2]),
          calcDirectionVertical(gamepads[0].axes[3])
        ],
        
      };

      // throttle and debounce do not seem to work...
      throttledGamepadUpdate(newGamePadState);
      // debouncedGamepadUpdate(newGamePadState);
      // onGamepadUpdate(newGamePadState);
      

      setGamepad({ ...newGamePadState });
    }
  }, [gamepads]);

  const {
    directionUp,
    directionRight,
    directionDown,
    directionLeft,
    select,
    start,
    buttonUp,
    buttonRight,
    buttonDown,
    buttonLeft,
    analogLeft,
    analogLeftDirection,
    analogRight,
    analogRightDirection,
  } = gamepad;

  return (
    <div>
      { showController && 
        (
          <svg width={288} height={144} viewBox="0 0 1280 819" fill="none" >
            <path
              className="background"
              d="M209.5 7.246c11.7-2.7 26.5-5.2 38.5-6.6 12.5-1.4 38.5-.4 49 1.8 19.7 4.3 31.2 10.6 43.7 24.1 7.8 8.4 21.9 28.7 25.2 36.4 4.4 10.1 12.6 47.8 12.6 58.3v3.1h522v-3.1c0-5.2 4.8-32.2 7.6-43 3.5-13.1 6-18.6 13.5-29.9 12-17.9 23.6-30.5 33.3-36.2 6.4-3.7 19-8.1 29.2-10.1 11-2.2 40.4-2.5 54.4-.5 26.1 3.6 47.3 9.1 61 15.8 21 10.2 31.8 27.5 41.4 66 1.9 7.6 4 16.3 4.6 19.4l1.1 5.5 11.2 8c29 20.4 53.9 42.9 63.3 57.1 11.4 17.1 20.1 37.4 28.8 67.5 7.1 24.6 7.5 27.6 17.5 138.3 9.3 101.8 11.5 142.5 11.6 213 0 54.6-1.2 87.9-4 110.6-3.5 27.8-13.4 49.3-31.2 68-23.4 24.5-47.6 38.4-78.6 45.1-14.5 3.1-41.5 3.1-53 0-16.6-4.5-33.9-14.7-51.7-30.5-24.5-21.7-42.3-49.1-72.6-111.7-18.2-37.4-19.9-40.6-26.2-47.5-3.1-3.3-8-9.3-10.9-13.2l-5.4-7.3-10.2 8.3c-23.1 18.7-34.4 24.2-60.9 29.8-12.4 2.6-36.9 3.1-48.8 1-27.3-4.8-51.2-13.8-71-26.9-17.2-11.4-27.6-24.6-41.3-52.4l-7.2-14.6H573l-7.2 14.6c-13.7 27.8-24.1 41-41.3 52.4-20.1 13.2-43.7 22.1-71 26.9-11.9 2.1-36.4 1.6-48.8-1-26.5-5.6-37.8-11.1-60.9-29.8l-10.2-8.3-5.4 7.3c-3 3.9-8 10.1-11.3 13.7-4 4.4-7.6 9.9-11.1 17-2.8 5.8-10.8 22-17.6 36-28.5 58.3-47.1 86.1-71.4 107.1-17.8 15.4-33.8 24.7-50.1 29.1-11.4 3.1-38.5 3.1-52.9 0-31-6.7-55.2-20.6-78.6-45.1-17.8-18.7-27.7-40.2-31.2-68-2.8-22.7-4-56-4-110.6.1-70.4 2.3-111.1 11.6-213 10.2-112.6 10-111.3 15.9-132.9 8-29.2 17-51.6 27.4-68.6 10-16.2 33.5-38 65.4-60.8 6.4-4.5 11.7-8.4 11.8-8.5.2-.1 1.7-6.8 3.4-14.7 6.1-27.9 16.2-53.4 24.5-62.2 11.4-12 24.5-18.4 49.5-24.2z"
              fill="#C4C4C4"
            />
            <path
              className="direction_up"
              d="M269 165h-77v56c9.333 11.333 30 34 38 34s29.333-22.667 39-34v-56z"
              fill={directionUp ? activeColor : inactiveColor}
            />
            <path
              className="direction_right"
              d="M341 240v77h-56c-11.333-9.333-34-30-34-38s22.667-29.333 34-39h56z"
              fill={directionRight ? activeColor : inactiveColor}
            />
            <path
              className="direction_down"
              d="M269 392h-77v-56c9.333-11.333 30-34 38-34s29.333 22.667 39 34v56z"
              fill={directionDown ? activeColor : inactiveColor}
            />
            <path
              className="direction_left"
              d="M119 240v77h56c11.333-9.333 34-30 34-38s-22.667-29.333-34-39h-56z"
              fill={directionLeft ? activeColor : inactiveColor}
            />
            <path
              className="select"
              fill={select ? activeColor : inactiveColor}
              d="M471 262h75v47h-75z"
            />
            <path
              className="start"
              d="M728 309v-49l72 23-72 26z"
              fill={start ? activeColor : inactiveColor}
            />
            <circle
              className="button_up"
              cx={1050.5}
              cy={183.5}
              r={47.5}
              fill={buttonUp ? activeColor : inactiveColor}
            />
            <circle
              className="button_right"
              cx={1162.5}
              cy={283.5}
              r={47.5}
              fill={buttonRight ? activeColor : inactiveColor}
            />
            <circle
              className="button_down"
              cx={1050.5}
              cy={383.5}
              r={47.5}
              fill={buttonDown ? activeColor : inactiveColor}
            />
            <circle
              className="button_left"
              cx={935.5}
              cy={283.5}
              r={47.5}
              fill={buttonLeft ? activeColor : inactiveColor}
            />
            <circle
              className="analog_left"
              cx={429}
              cy={511}
              r={93}
              fill={analogLeft ? activeColor : inactiveColor}
              style={{
                position: "relative",
                transition: "transform 200ms ease-out",
                transform: analogLeftDirection && analogLeftDirection.length > 0 ? `${createTransform(analogLeftDirection[0])} ${createTransform(analogLeftDirection[1])}` : "",
              }}
            />
            <circle
              className="analog_right"
              cx={843}
              cy={511}
              r={93}
              fill={analogRight ? activeColor : inactiveColor}
              style={{
                position: "relative",
                transition: "transform 200ms ease-out",
                transform: analogRightDirection && analogRightDirection.length > 0 ? `${createTransform(analogRightDirection[0])} ${createTransform(analogRightDirection[1])}` : "",
              }}
            />
          </svg>
        )
      }
    </div>
  );
};

export default Controller;

This isn't supported by the Gamepad API (yet!) but I've drafted a proposal to add events that would make this a lot easier. Currently the only way to do it is how you've done it, poll frequently and compare against the previous poll to detect button presses. You can lose button presses if you poll too slowly.

You may be interested in the gamepad-plus library which adds gamepadbuttondown , gamepadbuttonup , and gamepadaxismove events.

I solved this by implementing my own checks for a button down / up event. I tried to use CustomEvents but it did not work as expected so I ended up allowing the user of the hook specify callbacks but I think events would have maybe been a bit cleaner. Anyways, if someone is interested in the code I can post the full solution.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM