MiniGame 만들기 - matter.js 물리엔진 처음 사용해본 리뷰

Project Diary/React + Firebase (Snack ShoppingMall)

인기 모바일 게임인 수박 게임을 모티브로 하여 제작하였습니다

 

프로젝트 개요

  • 목표 : 간단한 미니게임을 만들어보는 것
  • 사용 기술 : React, Matter.js.

 

주요 기능

  1. 물리 엔진 설정 : Matter.js를 사용해 물리 엔진을 설정하고, 렌더러를 통해 캔버스에 그립니다.
  2. 키보드 입력 처리 : 키보드 입력을 받아 스낵을 좌우로 움직이거나 아래로 떨어뜨립니다.
  3. 충돌 감지 및 스낵 합성 : 동일한 종류의 스낵이 충돌하면 더 큰 스낵으로 합성됩니다.
  4. 게임 오버 및 승리 조건 : 게임 오버와 승리 조건을 설정하여 게임을 관리합니다.

 

<코드 요약>

import React, { useEffect, useRef } from "react";
import Matter from "matter-js";
import { Snack } from "@/components/game/Snack";
import styled from "styled-components";

// 스타일 컴포넌트 정의
const MiniGameBlock = styled.div`
  display: flex;
  justify-content: center;
`;

const MiniGame = () => {
  const canvasRef = useRef(null);

  useEffect(() => {
    const canvas = canvasRef.current;

    // Matter.js 엔진 및 렌더러 생성
    const engine = Matter.Engine.create();
    const render = Matter.Render.create({
      engine,
      canvas,
      options: {
        wireframes: false,
        background: "#fcecdc",
        width: 500,
        height: 680,
      },
    });
    const world = engine.world;

    // 벽 및 바닥 생성
    const leftWall = Matter.Bodies.rectangle(10, 340, 30, 680, { isStatic: true });
    const rightWall = Matter.Bodies.rectangle(490, 340, 30, 680, { isStatic: true });
    const ground = Matter.Bodies.rectangle(250, 670, 500, 30, { isStatic: true });
    const topLine = Matter.Bodies.rectangle(250, 145, 500, 1, { isStatic: true, isSensor: true });

    Matter.World.add(world, [leftWall, rightWall, ground, topLine]);

    Matter.Render.run(render);
    Matter.Runner.run(engine);

    let currentBody = null;
    let currentSnack = null;

    // 새로운 스낵 추가 함수
    const addSnack = () => {
      const index = Math.floor(Math.random() * 6);
      const snack = Snack[index];

      const body = Matter.Bodies.circle(250, 68, snack.radius, {
        render: { sprite: { texture: `/assets/image/game/${snack.name}.png` } },
      });

      currentBody = body;
      currentSnack = snack;
      Matter.World.add(world, body);
    };

    // 키보드 이벤트 핸들러
    window.onkeydown = (event) => {
      switch (event.code) {
        case "KeyA":
          if (currentBody.position.x - currentSnack.radius > 30)
            Matter.Body.setPosition(currentBody, { x: currentBody.position.x - 10 });
          break;
        case "KeyD":
          if (currentBody.position.x + currentSnack.radius < 470)
            Matter.Body.setPosition(currentBody, { x: currentBody.position.x + 10 });
          break;
        case "KeyS":
          currentBody.isSleeping = false;
          setTimeout(() => addSnack(), 1000);
          break;
      }
    };

    // 충돌 이벤트 핸들러
    Matter.Events.on(engine, "collisionStart", (event) => {
      event.pairs.forEach((collision) => {
        const { bodyA, bodyB } = collision;
        if (bodyA.index === bodyB.index) {
          const index = collision.bodyA.index;

          Matter.Composite.remove(world, [bodyA, bodyB]);

          const newSnack = Snack[index + 1];
          const newBody = Matter.Bodies.circle(collision.collision.supports[0].x, collision.collision.supports[0].y, newSnack.radius, {
            render: { sprite: { texture: `/assets/image/game/${newSnack.name}.png` } },
            index: index + 1,
          });

          Matter.World.add(world, newBody);
        }

        if (collision.bodyA.name === "topLine" || collision.bodyB.name === "topLine") {
          alert("Game over");
          Matter.Composite.allBodies(world).forEach((body) => {
            if (body.label === "Circle Body") Matter.World.remove(world, body);
          });
          addSnack();
        }
      });
    });

    addSnack();

    return () => {
      Matter.Render.stop(render);
      Matter.Runner.stop(engine);
      Matter.Engine.clear(engine);
    };
  }, []);

  return (
    <MiniGameBlock>
      <canvas ref={canvasRef} width={500} height={680} style={{ background: "#fcecdc" }}></canvas>
    </MiniGameBlock>
  );
};

export default MiniGame;

 

 

후기

  • Matter.js의 첫인상
    처음 사용해보는 라이브러리라서 설정과 사용법을 익히는 데 시간이 걸렸지만, 문서가 잘 되어 있어 많은 도움이 되었습니다.
  • 어려웠던 점
    충돌 처리 및 이벤트 핸들링 로직을 작성하는 데 어려움이 있었습니다.
    특히, 충돌 후 스낵을 합성하는 로직을 구현하는 부분이 복잡했습니다.
  • 좋았던 점
    Matter.js의 물리 엔진 덕분에 게임이 매우 자연스럽고 부드럽게 동작했습니다.
    간단한 설정만으로도 복잡한 물리 효과를 쉽게 구현할 수 있었습니다.
  • 개선하고 싶은 점
    게임 오버 및 승리 조건을 좀 더 다양하게 설정하여 게임의 난이도와 재미를 추가할 수 있을 것 같습니다.

결론

이번 프로젝트는 단순한 미니게임이지만, 사용자가 사이트에 더 오래 머물게 하고, 게임을 통해 즐거운 경험을 제공할 수 있는 좋은 시도였습니다. 앞으로도 다양한 방법으로 사용자 참여를 유도하는 콘텐츠를 개발해 나가고 싶습니다.