Nextjs로 수박게임(suika game) 만들기 1일차

  1. 배경
  2. 라이브러리
  3. 코드설명
  4. 최종코드
  5. 참고한 곳

 

1. 배경

- 해커톤 대회를 나가기 위해 쉬우면서 중독성 있는 겜을 만들고 거기에 우리가 여러가지 기능을 넣어 보상해주는 시스템을 만들기 위해 수박게임을 어레인지하여 만들게 되었습니다.

 

2. 라이브러리

- zustand :  블록체인 지갑로그인의 정보를 관리하기 위해 사용하였습니다.

- matter-js : 수박게임을 만들기 위해 게임엔진 라이브러리인 matter-js를 사용하였습니다.

- mongodb : 게임 등수, 아이템.. 등을 Nextjs 서버단에서 관리하기 위하여 사용하였습니다. 

 

3. 코드설명

    주의 : 코드설명에 앞서 문서가 정리는 잘 되어있지만.. 제 주관적인 생각이 많이 들어간 글이라고 생각합니다.
 

3-1. 게임 엔진 생성

- engine : 게임 엔진을 생성

 

- render : 어디에 보여줄지 지정해주는 함수이다. 여러가지 옵션이 있지만 몇가지만 설명하겠습니다.  canvas  useRef를 사용하여 canvas태그에 ref를 사용하여 위치를 알려주었고 option에는 width, height을 지정해 주고 background(배경색)을 지정해준다. width, height을 지정하면 그 태그의 크기가 바뀌어도 게임 화면만 커질뿐 기본width,height을 기준으로 게임이 진행된다.

 

- world : 물리엔진의 공간을 의미한다.

 const engine = Engine.create();
    const render = Render.create({
      engine,
      canvas: CRef.current,
      options: {
        wireframes: false,
        background: "#F7F4C8",
        width: 620,
        height: 850,
      },
    });
    const world = engine.world;

 

 

 

3-2. 게임 벽 만들기

- leftwall, rightwall.. 등 벽을 생성해야 물체가 밖으로 빠져나가지 않게 할 수 있습니다. 그러기 위해서 matter-js의 Bodies를 사용하여 물체를 생성합니다. 하지만 물체가 생성되어도 고정이 안되기에 isStatic을 true로 하여 고정시켜 줍니다. render 안에 {fillstyle}은 그 물체 색을 정해줄수 있습니다.

 

- topLine은 다 똑같지만 isSensor와 name이 추가된것을 볼 수 있습니다. isSensor는 이 수박게임의 Object가 선에 닿았을 때 감지하기 위해 true로 둔 것이고 name은 이 선에 닿았을때 이 name으로 감지하기 위해서 지정하였습니다.

 

- World.add(first, second) : first에는 집어넣을 공간 즉 우리는 world라는 공간을 생성하여 첫 번째 인자는 world를 주면 됩니다. 두번째 인자는 집어넣을 Object를 넣으면 됩니다.

 

- Render.run(render), Runner.run(engine) : 렌더링 후 엔진을 실행시키는 코드

    const leftWall = Bodies.rectangle(15, 395, 30, 790, {
      isStatic: true,
      render: { fillStyle: "#E6B143" },
    });

    const rightWall = Bodies.rectangle(605, 395, 30, 790, {
      isStatic: true,
      render: { fillStyle: "#E6B143" },
    });

    const ground = Bodies.rectangle(310, 820, 620, 60, {
      isStatic: true,
      render: { fillStyle: "#E6B143" },
    });

    const topLine = Bodies.rectangle(310, 150, 620, 2, {
      name: "topLine",
      isStatic: true,
      isSensor: true,
      render: { fillStyle: "#E6B143" },
    });

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

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

 

 

 

3-3. 게임 벽 만들기

- window.onkeydown : 버튼이 눌렸을때 event를 사용해 Object를 움직이는 이벤트

- window.onkeyup : 앞에 onkeydown에서 setIneterval를 clear해줬습니다.

- Events(first, second, thrid) : 세개의 각각의 인자가 있다.
             1. first는 사용한 엔진

 

             2. second는 이벤트라고 생각하면된다. 예를들어 mouseup, mousedown 이벤트를 줘서 마우스로 Object이동 및 수박 떨어트리는 이벤트를 만들수 있다. 실제로 "mouseup"이라고 하면 그 이벤트를 사용할 수 있다.

 

             3. third는 앞의 이벤트가 벌어졌을 때 사용할 함수를 넣어주면된다. 여기서는 collisionStart라는 이벤트가 발생하면 그 발생한 기점으로 Object와 Object가 부딪혔을 때 같으면 합쳐지고 아까 topline에 name을 지정했는데 거기에 닿으면 알람창으로 game over가 되게 해놨습니다. 이처럼 이벤트가 벌여졌을때 사용할 함수를 넣어주면 됩니다.

    let currentBody = null;
    let currentFruit = null;
    let interval = null;
    let disableAction = false;
    
    window.onkeydown = (event) => {
      if (disableAction) {
        return;
      }

      switch (event.code) {
        case "KeyA":
          if (interval) return;

          interval = setInterval(() => {
            if (currentBody.position.x - currentFruit.radius > 30)
              Body.setPosition(currentBody, {
                x: currentBody.position.x - 1,
                y: currentBody.position.y,
              });
          }, 5);
          break;

        case "KeyD":
          if (interval) return;

          interval = setInterval(() => {
            if (currentBody.position.x + currentFruit.radius < 590)
              Body.setPosition(currentBody, {
                x: currentBody.position.x + 1,
                y: currentBody.position.y,
              });
          }, 5);
          break;

        case "KeyS":
          addBallOnClick();
          break;
      }
    };

    window.onkeyup = (event) => {
      console.log(event);
      switch (event.code) {
        case "KeyA":
        case "KeyD":
          clearInterval(interval);
          interval = null;
      }
    };

    Events.on(engine, "collisionStart", (event) => {
      event.pairs.forEach((collision) => {
        if (collision.bodyA.index === collision.bodyB.index) {
          const index = collision.bodyA.index;

          if (index === FRUITS_BASE.length - 1) {
            return;
          }
          World.remove(world, [collision.bodyA, collision.bodyB]);
          const newFruit = FRUITS_BASE[index + 1];

          const newBody = Bodies.circle(
            collision.collision.supports[0].x,
            collision.collision.supports[0].y,
            newFruit.radius,
            {
              render: {
                sprite: { texture: `${newFruit.name}.png` },
              },
              index: index + 1,
            }
          );

          const AddFruit = World.add(world, newBody);
          if (AddFruit.id !== 0) {
            PointUpdate(index);
          }
        } else if (
          !disableAction &&
          collision.bodyA.index !== collision.bodyB.index
        ) {
          console.log("CD");
        }

        if (
          !disableAction &&
          (collision.bodyA.name === "topLine" ||
            collision.bodyB.name === "topLine")
        ) {
          alert("Game over your score : " + point);
        }
      });
    });

 

 

오늘은 Nextjs에서 matter-js를 사용하여 게임부분을 완성시켰습니다. 다음에는 마우스 이벤트를 사용해 현재의 키보드로만 게임이 아닌 마우스로 편하게 게임 플레이를 할 수 있게 코드를 추가하겠습니다. + 브금(bgm) 넣기

 

4. 최종코드

 수박게임 코드

    const engine = Engine.create();
    const render = Render.create({
      engine,
      canvas: CRef.current,
      options: {
        wireframes: false,
        background: "#F7F4C8",
        width: 620,
        height: 850,
      },
    });
    const world = engine.world;
    const mouse = Mouse.create(render.canvas);

    const mouseConstraint = MouseConstraint.create(engine, {
      mouse: mouse,
      constraint: {
        stiffness: 0.2,
        render: {
          visible: false,
        },
      },
    });
    render.mouse = mouse;

    const leftWall = Bodies.rectangle(15, 395, 30, 790, {
      isStatic: true,
      render: { fillStyle: "#E6B143" },
    });

    const rightWall = Bodies.rectangle(605, 395, 30, 790, {
      isStatic: true,
      render: { fillStyle: "#E6B143" },
    });

    const ground = Bodies.rectangle(310, 820, 620, 60, {
      isStatic: true,
      render: { fillStyle: "#E6B143" },
    });

    const topLine = Bodies.rectangle(310, 150, 620, 2, {
      name: "topLine",
      isStatic: true,
      isSensor: true,
      render: { fillStyle: "#E6B143" },
    });

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

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

    let currentBody = null;
    let currentFruit = null;
    let interval = null;
    let disableAction = false;
    
    const addFruit = (positionX) => {
      const index = Math.floor(Math.random() * 5);
      const fruit = FRUITS_BASE[index];

      const body = Bodies.circle(
        positionX ? positionX : 300,
        50,
        fruit.radius,
        {
          index: index,
          isSleeping: true,
          render: {
            sprite: { texture: `${fruit.name}.png` },
          },
          restitution: 0.4,
        }
      );

      currentBody = body;
      currentFruit = fruit;

      World.add(world, body);
      disableAction = false;
    };
    
    const PointUpdate = (index) => {
      const Addpoint = 2 ** index;
      setGameInfo((prev) => ({
        ...prev,
        point: prev.point + Addpoint,
      }));
      sound.sum.play();
    };
    
    const addBallOnClick = (e) => {
      currentBody.isSleeping = false;
      disableAction = true;

      setTimeout(() => {
        addFruit();
        disableAction = false;
      }, 1000);
    };
    
    window.onkeydown = (event) => {
      console.log(currentBody.position.x);
      if (disableAction) {
        return;
      }

      switch (event.code) {
        case "KeyA":
          if (interval) return;

          interval = setInterval(() => {
            if (currentBody.position.x - currentFruit.radius > 30)
              Body.setPosition(currentBody, {
                x: currentBody.position.x - 1,
                y: currentBody.position.y,
              });
          }, 5);
          break;

        case "KeyD":
          if (interval) return;

          interval = setInterval(() => {
            if (currentBody.position.x + currentFruit.radius < 590)
              Body.setPosition(currentBody, {
                x: currentBody.position.x + 1,
                y: currentBody.position.y,
              });
          }, 5);
          break;

        case "KeyS":
          addBallOnClick();
          break;
      }
    };

    window.onkeyup = (event) => {
      switch (event.code) {
        case "KeyA":
        case "KeyD":
          clearInterval(interval);
          interval = null;
      }
    };

    Events.on(engine, "collisionStart", async (event) => {
      event.pairs.forEach((collision) => {
        if (collision.bodyA.index === collision.bodyB.index) {
          const index = collision.bodyA.index;

          if (index === FRUITS_BASE.length - 1) {
            return;
          }
          World.remove(world, [collision.bodyA, collision.bodyB]);
          const newFruit = FRUITS_BASE[index + 1];

          const newBody = Bodies.circle(
            collision.collision.supports[0].x,
            collision.collision.supports[0].y,
            newFruit.radius,
            {
              render: {
                sprite: { texture: `${newFruit.name}.png` },
              },
              index: index + 1,
            }
          );

          const AddFruit = World.add(world, newBody);
          if (AddFruit.id !== 0) {
            PointUpdate(index);
          }
        } 

        if (
          !disableAction &&
          (collision.bodyA.name === "topLine" ||
            collision.bodyB.name === "topLine")
        ) {
          alert("Game over your score : " + point);
        }
      });
    });
    CRef.current.addEventListener("click", () => {
      if (!disableAction) {
        addBallOnClick();
      }
    });
    Events.on(mouseConstraint, "mousemove", function (e) {
      if (
        e.mouse.position.x > 50 &&
        e.mouse.position.x < 580 &&
        currentBody.isSleeping
      ) {
        Body.setPosition(currentBody, {
          x: e.mouse.position.x,
          y: currentBody.position.y,
        });
      }
    });

    addFruit();

 

5. 참고한 곳

https://www.youtube.com/watch?v=LZvEDigv0Ww&t=1737s

https://brm.io/matter-js/docs/classes/World.html

 

Matter.js Physics Engine API Docs - matter-js 0.19.0

 

brm.io

 

+ Recent posts