MiniGame 만들기 - matter.js 물리엔진 처음 사용해본 리뷰
ㆍProject Diary/React + Firebase (Snack ShoppingMall)
인기 모바일 게임인 수박 게임을 모티브로 하여 제작하였습니다
프로젝트 개요
- 목표 : 간단한 미니게임을 만들어보는 것
- 사용 기술 : React, Matter.js.
주요 기능
- 물리 엔진 설정 : Matter.js를 사용해 물리 엔진을 설정하고, 렌더러를 통해 캔버스에 그립니다.
- 키보드 입력 처리 : 키보드 입력을 받아 스낵을 좌우로 움직이거나 아래로 떨어뜨립니다.
- 충돌 감지 및 스낵 합성 : 동일한 종류의 스낵이 충돌하면 더 큰 스낵으로 합성됩니다.
- 게임 오버 및 승리 조건 : 게임 오버와 승리 조건을 설정하여 게임을 관리합니다.
<코드 요약>
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의 물리 엔진 덕분에 게임이 매우 자연스럽고 부드럽게 동작했습니다.
간단한 설정만으로도 복잡한 물리 효과를 쉽게 구현할 수 있었습니다. - 개선하고 싶은 점
게임 오버 및 승리 조건을 좀 더 다양하게 설정하여 게임의 난이도와 재미를 추가할 수 있을 것 같습니다.
결론
이번 프로젝트는 단순한 미니게임이지만, 사용자가 사이트에 더 오래 머물게 하고, 게임을 통해 즐거운 경험을 제공할 수 있는 좋은 시도였습니다. 앞으로도 다양한 방법으로 사용자 참여를 유도하는 콘텐츠를 개발해 나가고 싶습니다.