[ COMMA 아카이브 ] #3. 나의 Dev-Log : 플래피 부 게임 구현

BOOa·2025년 3월 26일

Archive

목록 보기
11/17

🕹️ Flappy-Boo : 플래피 부

유령 캐릭터 "Boo"가 장애물을 피하며 날아가는 단순하면서도 중독성 있는 게임이다. space 키를 조작하여 유령을 점프시키며 장애물에 부딪히지 않고 최대한 오래 비행하는 것이 목표이다.

📍 Kaplay 라이브러리 사용

  • 이 게임은 Kaplay 라이브러리를 활용하여 구현하였다. Kaplay 는 복잡한 게임 로직을 간결하게 구현할 수 있도록 도와주는 라이브러리이다.
  • 객체 관리와 충돌 감지, 애니메이션 처리 등 플래피 부 게임을 개발하는 데 있어 필수적인 요소들을 간단한 코드로 구현할 수 있게 도와주었다.
  • 이를 통해 반복적인 코드를 줄이고 직관적인 메서드를 사용하여 빠르게 게임을 개발할 수 있었다.

🎬 Kaplay 초기화 설정

k = kaplay({
  width: 1300,
  height: 750,
  letterbox: true,
  global: true,
  canvas: canvas,
});
  • kaplay() : 게임 엔진을 초기화하고 캔버스를 설정
  • letterbox : 화면 비율 유지를 위해 레터박스를 추가
  • global : 전역으로 접근할 수 있도록 설정
  • canvas : 사용할 HTML 캔버스를 지정

🎬 Scene 분리 — Start/Game

  • 게임이 로딩되는 게임 시작 화면 과 실제 게임이 진행되는 게임 화면 을 구분하여 구현하였다.
  • 게임 시작 화면에서 play 버튼을 누르거나 space 를 누르면 게임 화면으로 넘어가도록 하였다.

✔ㅤ구현 방식

  • kaplay의 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);
    }
  });
});

⚙️ 게임 요소 관리

1. 게임 요소 로드

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("불러올 이름" {볼륨}) : 사운드를 재생

2. 객체 추가

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);
  • 게임의 중력 값 설정

3. 업데이트 처리

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 실행
  • 무한 스크롤 구현에 활용 : 화면에 벗어나면 오른쪽으로 다시 재설정

4. 특정 키 처리

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 실행
  • 게임의 시작과 플레이어의 점프 이벤트 구현에 활용

5. 점수 관리

k.loop(1, () => {
  if (isGameStarted.value && !player.isDead) {
    score += 50;
    scoreLabel.updateScore(score);}
});
  • 주어진 시간 간격마다 콜백함수를 실행
  • k.loop(interval, callback)
  • 게임 내 점수를 주기적으로 증가 시키는 로직에 활용

6. 충돌 감지

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 실행
  • 플레이어가 장애물에 부딪혔을 경우 충돌 감지에 활용

7. 카메라 세팅

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 로 설정하고 장애물과의 상호작용을 비활성화

0개의 댓글