스탠다드 반에서 suika-game 만들기 프로젝트를 진행한다고 소식이 들려왔다.
이전 조에서 같은 팀이었던 팀원분이 고맙게도 정보를 나눠 주어서 관심을 가지게 되었다. 개발도 좋아하지만 게임이라면 더 좋아하는 나는 너무 하고 싶었었지만 현재 챌린저반을 참여하고 있는 상황...
내용은 똑같지만 괜찮은 유튜브 영상을 추천해주시곤 가셨던 팀원분께 감사를 전합니다..
곧 바로 클론 코딩에 돌입한다!
일단 내 계획은 이렇다.
1. 일단 메인컨텐츠인 게임을 만든다.
2. 백앤드로 서버를 만들어서 게임의 데이터를 저장.
3. 회원가입과 같은 기능을 추가한다.
4. 회원가입에는 sns로그인 방식 체택
4. 회원들끼리 랭킹 조회 시스템.
5. soket.io를 이용해서 실시간 채팅 구현
일단 이렇게 할 것이라고 생각하고 게임만들기에 돌입했다. 일단 부딪히고 보는 거니까 어려움이 생기면 그때그때 푸는걸로~
먼저 이 게임에 사용 될 라이브러리는 크게
2d기반 물리엔진 라이브러리를 지원하는matter.js
와 프론트 부분은 vite
라는 것을 사용할 것 이다.
npm create vite@latest
프로젝트 이름을 적고
사용 할 프레임워크를 선택하지만 나는 js
로 할 것이기 때문에 vanilla
로 선택!
cd suika-game
npm install
npm run dev
열린 주소로 들어가면 이렇게 바이트가 기다려준다.
그런다음 2d 물리엔진을 이용하기 위해서
npm install matter-js
기본적인 준비는 다 끝났고 main.js
에서 기본적인 게임 작업을 할 것이다.
// main.js
import { Engine, Render, Bodies } from "matter-js"
const engine = Engine.create();
// 게임 판 세팅
const render = Render.create({
engine,
element: document.body,
options: {
wireframes: false,
background: '#F7F4C8',
width: 620,
height: 850,
},
});
const world = engine.world;
// 왼쪽 벽 세팅
const leftWall = Bodies.rectangle(15, 395, 30, 790, {
isStatic: true,
render: { fillStyle: '#E7B143' },
});
// 오른쪽 벽 세팅
const leftWall = Bodies.rectangle(605, 395, 30, 790, {
isStatic: true,
render: { fillStyle: '#E7B143' },
});
// 바닥 세팅
const ground = Bodies.rectangle(310, 820, 620, 60, {
isStatic: true,
render: { fillStyle: '#E7B143' },
});
// 게임 오버 선 세팅
const topLine = Bodies.rectangle(310, 150, 620, 2, {
name: 'topLine',
isStatic: true,
isSensor: true,
render: { fillStyle: '#E6B143' },
});
World.add(world, [leftWall,leftWall,ground,topLine])
Render.run(render);
Runner.run(engine);
게임 판을 세팅해주었다.
그 다음 과일을 추가 해 줄것이다.
과일 이미지 파일을 넣어준 다음
// fruits.js
const FRUITS = [
{
name: '00_cherry',
radius: 33 / 2,
},
{
name: '01_strawberry',
radius: 48 / 2,
},
{
name: '02_grape',
radius: 61 / 2,
},
{
name: '03_gyool',
radius: 69 / 2,
},
{
name: '04_orange',
radius: 89 / 2,
},
{
name: '05_apple',
radius: 114 / 2,
},
{
name: '06_pear',
radius: 129 / 2,
},
{
name: '07_peach',
radius: 156 / 2,
},
{
name: '08_pineapple',
radius: 177 / 2,
},
{
name: '09_melon',
radius: 220 / 2,
},
{
name: '10_watermelon',
radius: 259 / 2,
},
]
export { FRUITS }
과일을 만들어준다.
// main.js
// 과일 추가 하는 함수
function addFruit() {
const index = Math.floor(Math.random() * 5);
const fruit = FRUITS[index];
const body = Bodies.circle(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);
}
과일을 움직이고, 떨어뜨리는 함수
window.onkeydown = (event) => {
if (disableAction) {
return;
}
switch (event.code) {
case 'ArrowLeft':
if (interval) return;
interval = setInterval(() => {
if (currentBody.position.x - currentFruit.radius > 30)
Body.setPosition(currentBody, {
x: currentBody.position.x - 2,
y: currentBody.position.y,
});
}, 5);
break;
case 'ArrowRight':
if (interval) return;
interval = setInterval(() => {
if (currentBody.position.x + currentFruit.radius < 590)
Body.setPosition(currentBody, {
x: currentBody.position.x + 2,
y: currentBody.position.y,
});
}, 5);
break;
case 'Space':
currentBody.isSleeping = false;
disableAction = true;
setTimeout(() => {
addFruit();
disableAction = false;
}, 500);
break;
}
};
과일의 이동을 부드럽게 만들어 주는 함수
// main.js
window.onkeyup = (event) => {
switch (event.code) {
case 'ArrowLeft':
clearInterval(interval);
interval = null;
case 'ArrowRight':
clearInterval(interval);
interval = null;
}
};
과일이 2개 부딪히면 합쳐지는 함수
Events.on(engine, 'collisionStart', (event) => {
event.pairs.forEach((collision) => {
if (collision.bodyA.index === collision.bodyB.index) {
const index = collision.bodyA.index;
if (index === FRUITS.length - 1) {
return;
}
World.remove(world, [collision.bodyA, collision.bodyB]);
const newFruit = FRUITS[index + 1];
main.js;
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,
}
);
World.add(world, newBody);
게임 오버/승리 조건
if (
!disableAction &&
(collision.bodyA.name === 'topLine' ||
collision.bodyB.name === 'topLine')
) {
alert('game over');
window.location.reload();
}
if (num_suika === 2) {
alert('VICTORY!');
window.location.reload();
}
});
});
window.addEventListener('keydown', (e) => console.log(e));
addFruit();
``
완성