React hook+Matter.js实现一个猫猫跳跃快跑游戏

1,181 阅读2分钟

一起用代码吸猫!本文正在参与【喵星人征文活动】

使用React和Matter.js做了一个简单的横版猫咪快跑的跳跃游戏,Neko Jump。

可以通过这个链接试玩: Neko Jump (game-neko-jump.vercel.app)

image.png

React是前端用于构建web页面的一个很常见的库,Matter.js是web端一个很强大的物理引擎,主要通过canvas来绘制对象。

游戏设计

猫咪(Neko)向前跑,跳跃躲避各种障碍物向前跑动,并尽可能跑的更远,拿到更高的分数,主要通过两个不同力度等级的按钮来跳跃,如果发现自己过于靠后,猫咪会往前追赶,如果遇到障碍没有跳过,落后了,就会输。

技术实现

猫咪使用一个长方形的刚体,覆盖一个贴图来实现。

实际上为了方便实现,场景并没有移动,主要是随机生成的障碍物设置了static=true,然后向后移动。

其中比较关键的就是在React中使用Matter.js这类并没有专门为React适配的库,需要使用useRef这个不常用的hook api来对对象进行托管。

其他就是通过Matter.js实现游戏的逻辑,技术上比如实现static物体的移动等,Matter.js的Example:matter-js/manipulation.js at master · liabru/matter-js (github.com)有很好的展示。

仓库地址:Kingfish404/game-neko-jump: A Game Power by react and matter.js (github.com)

核心代码:

function App() {
  const scene = useRef(null);
  const engine = useRef(Engine.create());
  const player = useRef(Body.create({}));
  const [isStart, setIsStart] = useState(false);
  const [isDead, setIsDead] = useState(false);
  const [score, setScore] = useState(0)

  useEffect(() => {
    // mount
    const cw = document.body.clientWidth;
    const ch = document.body.clientHeight / 2;

    const e = engine.current;

    let elem: HTMLElement = document.body;
    if (scene.current) {
      elem = scene.current;
    } else {
      return () => { };
    }
    const render = Render.create({
      element: elem,
      engine: e,
      options: {
        width: cw,
        height: ch,
        background: cloudImage,
        wireframes: false,
      }
    });

    // boundaries
    Composite.add(e.world, [
      // top
      Bodies.rectangle(cw / 2, 10, cw, 20, {
        isStatic: true,
        render: {
          fillStyle: 'black'
        }
      }),
      // left
      Bodies.rectangle(-50, ch / 2, 5, ch, {
        isStatic: true,
        render: {
          fillStyle: 'black'
        }
      }),
      // buttom
      Bodies.rectangle(cw / 2, ch - 10, cw * 2, 20, {
        isStatic: true,
        render: {
          fillStyle: 'black'
        }
      })
    ]);
    player.current = Bodies.rectangle(cw / 2,ch / 2,10,20,
       {
        render: {
          fillStyle: 'white',
          sprite: {
            texture: catImage,
            xScale: 0.05,
            yScale: 0.05,
          }
        },
        friction: 0,
        frictionStatic: 0,
        frictionAir: 0,
      }
    );

    Render.run(render);

    Composite.add(e.world, [player.current as Body]);

    const obstacles: Array<Body> = [];
    const map: Array<number> = [ch - 25, ch - 25, ch / 3, ch / 2, ch / 1.5];

    const gameloop = setInterval(() => {
      const index: number = Math.floor(Math.random() * map.length);
      const box = Bodies.rectangle(
        cw + 100,
        map[index]
        , 100, 20, {
        isStatic: true,
        friction: 0,
        frictionStatic: 0,
        frictionAir: 0,
      });
      obstacles.push(box);
      Body.setVelocity(box, { x: -100, y: 0 });
      Composite.add(e.world, [box])

      Body.setVelocity(player.current, { x: player.current.velocity.x / 10, y: player.current.velocity.y })
      if (player.current.position.x < 0) {
        setIsStart(false);
        setIsDead(true);
      }
      if (player.current.position.x < ch / 2) {
        Body.setVelocity(player.current, { x: 1, y: 0 });
      }
    }, 700);

    if (!isStart) {
      clearInterval(gameloop);
    }

    Events.on(e, 'beforeUpdate', () => {
      for (let i of obstacles) {
        if (i.position.x < -50) {
          Composite.remove(e.world, i);
        } else {
          Body.setVelocity(i, { x: -1, y: 0 })
          Body.setPosition(i, { x: i.position.x - 1, y: i.position.y });
        }
      }
      while (obstacles.length > 0 && obstacles[0].position.x < -50) {
        obstacles.splice(0, 1);
      }
      Body.setAngularVelocity(player.current, 0);
    });

    // unmount
    return () => {
      // destroy Matter
      Render.stop(render);
      Engine.clear(e);
      World.clear(e.world, false);
      Composite.clear(e.world, false);
      render.canvas.remove();
      render.textures = {};
      clearInterval(gameloop);
    }
  }, [isStart]);

  useEffect(() => {
    let id: any;
    if (isStart) {
      id = setInterval(() => {
        setScore(score => { return score + 1 });
      }, 1000);
    }

    return () => { clearInterval(id) }
  }, [isStart])

  const wait = 500;
  const handleTinyDown = _.throttle((e: { clientX: number; clientY: number; }) => {
    const p = player.current;
    Body.applyForce(p, { x: p.position.x, y: p.position.y }, { x: 0, y: -0.006 });
  }, wait)

  const handleStrongDown = _.throttle((e: { clientX: number; clientY: number; }) => {
    const p = player.current;
    Body.applyForce(p, { x: p.position.x, y: p.position.y }, { x: 0, y: -0.008 });
  }, wait)

  return (
    <div className="App">
      <div className="score">{score}</div>
      <div
        className="playground"
        ref={scene}
      />
      <div className="control">
        {isStart ?
          <>
            <div
              className="btn"
              onMouseDown={handleTinyDown}
            >Jump</div>
            <div
              className="btn"
              onMouseDown={handleStrongDown}
            >Jump Jump</div>
          </>
          :
          <>
            {isDead ?
              <div className="deadLogo">You Dead</div>
              :
              <div className="btn"
                onClick={() => {
                  // run the engine
                  Runner.run(engine.current);
                  setIsStart(true);
                }}
              >Start</div>}
          </>}
      </div>
    </div >
  );
}

export default App;

REF