Dev_logcomma

Kaplay로 Flappy 게임 구현하기 – COMMA #2

2025-03-26
ProjectKaplayLib

📍 Kaplay 라이브러리 사용

Kaplay는 객체 관리, 충돌 감지, 애니메이션 처리 등을 간단한 코드로 구현할 수 있는 게임 엔진으로, 반복적인 코드를 줄이고 빠르게 게임 로직을 구성할 수 있었다.


🎬 Kaplay 초기화 설정

js
1
k = kaplay({
  width: 1300,
  height: 750,
  letterbox: true,
  global: true,
  canvas: canvas,
});
  • kaplay() : 게임 엔진 초기화
  • letterbox : 비율 유지
  • global : 전역 접근 허용
  • canvas : HTML 캔버스 지정

🎬 Scene 분리 — Start / Game

시작 화면과 실제 게임 화면을 Kaplay의 scene 기능으로 분리

✔ 구현 방식

k.scene("name", callback) / k.go("name") 로 전환 가능

js
1
// 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 씬
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);
  });
});

⚙️ 게임 요소 관리

1. 스프라이트/사운드 로드

js
1
k.loadSprite("boo", "/assets/images/game/flappy/Boo.png");
k.loadSound("jump", "/assets/images/game/flappy/jump.wav");
k.play("jump", { volume: 0.02 });

2. 객체 추가

js
1
const player = k.add([
  k.sprite("boo"),
  k.pos(100, 100),
  k.area(),
]);
k.setGravity(2500);

3. 프레임 업데이트

js
1
k.onUpdate(() => {
  if (isGameStarted.value) {
    clouds.forEach((cloud) => {
      cloud.move(cloud.speed, 0);
      if (cloud.pos.x > canvas.width) {
        cloud.pos.x = -cloud.width;
      }
    });
  }
});

4. 입력 처리

js
1
k.onKeyPress("space", () => {
  if (!isGameStarted.value) startGame();
  else if (!player.isDead) {
    player.jump(400);
    if (audioEnabled.value) k.play("jump", { volume: 0.02 });
  }
});

5. 점수 증가

js
1
k.loop(1, () => {
  if (isGameStarted.value && !player.isDead) {
    score += 50;
    scoreLabel.updateScore(score);
  }
});

6. 충돌 감지

js
1
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();
});

7. 카메라 설정

js
1
k.setCamScale(k.vec2(1.2));
player.onUpdate(() => {
  if (isGameStarted.value && !player.isDead) {
    k.setCamPos(player.pos.x + 100, 400);
  }
});

💡 화면 크기 대응

해상도가 다른 환경에서 캔버스 스케일 문제 발생

✔ 해결 방식

js
1
function setCanvasSize() {
  const width = window.innerWidth;
  const height = window.innerHeight;
  game.setSize(width, height);
}
 
setCanvasSize();
window.addEventListener("resize", setCanvasSize);

💡 장애물 무한 스크롤 구현

반복 생성 대신 2개의 장애물 스프라이트를 순환 사용

js
1
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;
});

💡 난이도 조절

장애물 속도를 프레임마다 증가시키는 방식으로 난이도 상승 구현

js
1
if (isGameStarted.value) {
  obstaclesLayer.speed -= 5 * deltaTime;
}

💡 충돌 처리 및 게임 종료

플레이어가 장애물에 부딪힐 때 onCollide로 게임 오버 처리

js
1
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();
});

🛠️ 개선 아이디어

  • 드래그 앤 드랍 업로드
  • Remove/Insert 시 애니메이션 추가
  • 용량 및 파일 확장자 유효성 검사
  • Multi 업로드 + Progress 표시
  • 서버 업로드 큐 관리