
🕹️ Flappy-Boo : 플래피 부
유령 캐릭터 "Boo"가 장애물을 피하며 날아가는 단순하면서도 중독성 있는 게임이다.
space키를 조작하여 유령을 점프시키며 장애물에 부딪히지 않고 최대한 오래 비행하는 것이 목표이다.
- 이 게임은
Kaplay라이브러리를 활용하여 구현하였다.Kaplay는 복잡한 게임 로직을 간결하게 구현할 수 있도록 도와주는 라이브러리이다.객체 관리와 충돌 감지, 애니메이션 처리등 플래피 부 게임을 개발하는 데 있어 필수적인 요소들을 간단한 코드로 구현할 수 있게 도와주었다.- 이를 통해
반복적인 코드를 줄이고 직관적인 메서드를 사용하여 빠르게 게임을 개발할 수 있었다.
k = kaplay({
width: 1300,
height: 750,
letterbox: true,
global: true,
canvas: canvas,
});
kaplay() : 게임 엔진을 초기화하고 캔버스를 설정letterbox : 화면 비율 유지를 위해 레터박스를 추가global : 전역으로 접근할 수 있도록 설정canvas : 사용할 HTML 캔버스를 지정
- 게임이 로딩되는
게임 시작 화면과실제 게임이 진행되는 게임 화면을 구분하여 구현하였다.- 게임 시작 화면에서
play버튼을 누르거나space를 누르면 게임 화면으로 넘어가도록 하였다.
scene 기능을 사용하여 Start,Game 씬을 나누고 go()로 전환할 수 있게 구현하였다.k.scene("이름", 콜백함수) / k.go("이름")// 🏁 Start 씬: 시작 화면
k.scene("start", async () => {
makeBackground(k);
const playBtn = k.add([
k.sprite("playBtn"),
k.scale(0.35),
k.area(),
k.anchor("center"),
k.pos(k.center().x + 20, k.center().y + 40),
]);
playBtn.onClick(goToGame); // 버튼 클릭 시 main 씬으로 이동
});
// 🎮 Game 씬: 실제 게임 플레이 화면
k.scene("main", async () => {
let score = 0;
makeBackground(k);
// 플레이어 생성 및 컨트롤 설정
const player = makePlayer(k);
player.setControls();
// 충돌 처리
player.onCollide("obstacle", async () => {
if (player.isDead) return;
player.isDead = true;
isGameStarted.value = false;
emit("open-game-over", score, currentTime.value);
reset();
});
k.onKeyPress("space", () => {
if (!isGameStarted.value) {
startGame();
} else if (!player.isDead) {
player.jump(400);
}
});
});
k.loadSprite("boo", "/assets/images/game/flappy/Boo.png");
k.loadSprite("불러올 이름", "파일 경로")k.sprite("불러올 이름") 으로 나중에 사용하도록 준비k.loadSound("jump", "/assets/images/game/flappy/jump.wav");
k.play("jump", { volume: 0.02 });
k.loadSound("불러올 이름", "파일 경로") : 사운드를 로드하는 메서드로 게임의 사운드를 k.play("불러올 이름" {볼륨}) : 사운드를 재생const player = k.add([k.sprite("boo"), k.pos(100, 100), k.area()]);
k.add() : 게임 내 새로운 오브젝트(플레이어, 장애물, 배경 등)를 추가할 때 사용k.pos() : 위치 지정k.area() : 충돌 감지를 위한 hitbox 충돌 영역 지정k.sprite() : 오브젝트에 사용할 이미지 지정k.body() : 물리 속성k.setGravity(2500);
k.onUpdate(() => {
if (isGameStarted.value) {
clouds.forEach((cloud) => {
cloud.move(cloud.speed, 0);
if (cloud.pos.x > canvas.width) {
cloud.pos.x = -cloud.width;}})
}});
k.onUpdate(callback) : 매 프레임마다 callback 실행k.onKeyPress("space", () => {
if (!isGameStarted.value) {
startGame();
} else if (!player.isDead) {
player.jump(400);
if (audioEnabled.value) k.play("jump", { volume: 0.02 });
}
});
k.onkeyPress(key, callback) : 특정 키가 눌렸을 경우, 1번만 callback 실행k.loop(1, () => {
if (isGameStarted.value && !player.isDead) {
score += 50;
scoreLabel.updateScore(score);}
});
k.loop(interval, callback)player.onCollide("obstacle", async () => {
if (player.isDead) return;
if (audioEnabled.value) k.play("hurt");
player.isDead = true;
player.disableControls();
isGameStarted.value = false;
obstaclesLayer.speed = 0;
map.speed = 0;
stop();
emit("open-game-over", score, currentTime.value);
reset();
});
player.onCollide(tag, callback) : 특정 태그가 부딪혔을 때, 1번만 callback 실행k.setCamScale(k.vec2(1.2));
player.onUpdate(() => {
if (isGameStarted.value && !player.isDead) {
k.setCamPos(player.pos.x + 100, 400);}
});
🚨 문제점
모니터의 해상도가 다를 때, 게임 캔버스 크기가 고정되어 있으면 너무 작거나 크게 보이는 문제가 발생
✅ 해결
window.addEventListener("resize", setCanvasSize)를 사용하여 화면 크기가 변경될 때마다 캔버스 크기를 조절하도록 구현
// 캔버스 크기 조정 함수
function setCanvasSize() {
const width = window.innerWidth;
const height = window.innerHeight;
game.setSize(width, height);
}
// 초기 캔버스 설정 및 리사이즈 이벤트 연결
setCanvasSize();
window.addEventListener("resize", setCanvasSize);
🚨 문제점
장애물이 1번이 아닌 플레이어가 게임 오버 될 때까지 무한히 생성되어야 하며, 장애물을 전부 그릴 수 없기 때문에 1개의 장애물 파일로 계속 재활용해야 하는 상황
✅ 해결
onUpdate메서드를 사용하여 장애물을 주기적으로 생성하고 화면 밖으로 나간 장애물을 다시 오른쪽 끝으로 재배치하는 로직을 구현
// 장애물 배열 및 속도 설정
const obstaclesLayer = {
speed: -100,
parts: [
k.add([
k.sprite("obstacles"),
k.pos(0, 0),
k.area(),
k.scale(SCALE_FACTOR),
]),
k.add([
k.sprite("obstacles"),
k.pos(IMAGE_WIDTH, 0),
k.area(),
k.scale(SCALE_FACTOR),
]),
],
};
// 장애물 무한 스크롤 처리
k.onUpdate(() => {
const currentTime = performance.now();
const deltaTime = (currentTime - lastUpdateTime) / 1000;
lastUpdateTime = currentTime;
if (isGameStarted.value) {
for (let i = 0; i < obstaclesLayer.parts.length; i++) {
const currentPart = obstaclesLayer.parts[i];
const nextPart =
obstaclesLayer.parts[(i + 1) % obstaclesLayer.parts.length];
if (currentPart.pos.x < -IMAGE_WIDTH) {
currentPart.pos.x = nextPart.pos.x + IMAGE_WIDTH;
}
currentPart.move(obstaclesLayer.speed * deltaTime * 60, 0);
}
}
if (isGameStarted.value) {
obstaclesLayer.speed -= 5 * deltaTime;
}
});
obstaclesLayer 로 2개의 장애물 파트를 배열로 관리하고 각각의 장애물이 화면 밖으로 나가면 다른 장애물 뒤로 재배치k.onUpdate 를 사용하여 모든 장애물을 매 프레임마다 왼쪽으로 이동시키고 화면 밖으로 나가면 다시 오른쪽 끝으로 보내는 방식으로 무한 스크롤을 구현📎 참고
🚨 문제점
똑같은 장애물이 반복되기 때문에 사용자들이 재미를 느끼려면 점차 난이도가 상승되는 포인트가 필요
✅ 해결
게임이 진행됨에 따라 장애물의 속도를 점차 증가시켜 난이도를 증가시키는 로직 추가
if (isGameStarted.value) {
obstaclesLayer.speed -= 5 * deltaTime;
}
-100 으로 설정 ( 왼쪽 방향으로 이동 )5*deltaTime 만큼 감소🚨 문제점
플레이어와 장애물이 부딪혔을 시, 게임 오버 처리를 위해 충돌 감지 로직이 필요
✅ 해결
onCollide를 사용하여 플레이어와 장애물 충돌을 감지하고 충돌 시 게임 오버 화면으로 전환하도록 구현
// 플레이어 충돌 감지 및 게임 오버 처리
player.onCollide("obstacle", async () => {
if (player.isDead) return;
if (audioEnabled.value) k.play("hurt");
player.isDead = true;
player.disableControls();
isGameStarted.value = false;
obstaclesLayer.speed = 0;
map.speed = 0;
stop();
// 게임 오버 화면 호출
emit("open-game-over", score, currentTime.value);
reset();
});
player.isDead=true 로 설정하고 장애물과의 상호작용을 비활성화