Nextjs로 수박게임(suika game) 만들기 1일차
- 배경
- 라이브러리
- 코드설명
- 최종코드
- 참고한 곳
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