Skip to main content
Summary

손맛은 정보 전달에서 시작한다

게임의 손맛은 화려한 이펙트만 뜻하지 않는다. 플레이어가 무엇을 했고, 그 결과가 어떻게 되었는지 즉시 느끼게 만드는 피드백이다. 코인을 먹었을 때 짧은 소리가 나고, 점수 UI가 살짝 커졌다가 돌아오며, 코인 위치에서 작은 빛 입자가 터지면 플레이어는 보상을 더 분명하게 느낀다.

이번 장에서는 세 가지 도구를 다룬다.

  • 사운드: 배경음악과 효과음으로 사건을 들려준다.

  • Tween: 오브젝트의 위치, 크기, 투명도 등을 부드럽게 바꾼다.

  • ParticleEmitter: 작은 입자를 흩뿌려 충돌, 폭발, 획득 효과를 만든다.

여기에 카메라 흔들림과 플래시를 더하면 피격, 클리어, 강한 충돌 같은 순간을 화면 전체로 전달할 수 있다. 핵심은 많이 넣는 것이 아니라, 필요한 순간에 정확히 넣는 것이다.

사운드, 트윈, 파티클로 손맛 더하기AI 생성 이미지

사운드 불러오기와 재생하기

사운드도 이미지와 마찬가지로 preload()에서 먼저 불러온다. 효과음은 짧고 자주 재생된다. 배경음악은 길고 반복 재생되는 경우가 많다.

preload() {
  this.load.audio('bgm-stage1', [
    'assets/audio/stage1.ogg',
    'assets/audio/stage1.mp3'
  ]);

  this.load.audio('sfx-coin', [
    'assets/audio/coin.ogg',
    'assets/audio/coin.mp3'
  ]);

  this.load.audio('sfx-hit', 'assets/audio/hit.wav');
  this.load.audio('sfx-jump', 'assets/audio/jump.wav');
  this.load.audio('sfx-clear', 'assets/audio/clear.wav');
}

하나의 사운드에 여러 파일 형식을 넘기면 브라우저가 지원하는 형식을 골라 사용할 수 있다. 짧은 효과음은 용량이 크지 않다면 WAV도 괜찮지만, 배경음악은 OGG와 MP3를 함께 준비하는 편이 안전하다. 모바일 브라우저에서는 사용자의 터치나 클릭 없이 소리가 자동 재생되지 않는 경우가 많으므로, 시작 버튼을 누른 뒤 음악을 재생하는 구조가 좋다1.

효과음은 보통 this.sound.play()로 바로 재생한다.

jump() {
  this.player.setVelocityY(-420);
  this.sound.play('sfx-jump', { volume: 0.4 });
}

배경음악은 인스턴스로 만들어 두면 반복, 정지, 볼륨 조절을 관리하기 쉽다.

create() {
  this.bgm = this.sound.add('bgm-stage1', {
    loop: true,
    volume: 0.25
  });

  this.input.once('pointerdown', () => {
    if (!this.bgm.isPlaying) {
      this.bgm.play();
    }
  });

  this.events.once(Phaser.Scenes.Events.SHUTDOWN, () => {
    this.bgm.stop();
  });
}

실제 게임에서는 메뉴 화면의 시작 버튼이 첫 입력이 되는 경우가 많다. 그때 오디오가 잠금 해제되고, 이후 Play Scene에서 자연스럽게 배경음악을 시작할 수 있다. 여러 Scene에서 같은 배경음악을 이어서 쓰고 싶다면, 음악을 담당하는 별도 Scene이나 전역 매니저를 두는 방식도 가능하다. 여기서는 한 Scene 안에서 시작하고 종료할 때 정리하는 가장 단순한 형태로 이해하면 된다.

볼륨 설정은 나중에 옵션 화면과 연결할 수 있도록 Registry에 넣어 두면 좋다.

playSfx(key, baseVolume = 1) {
  const sfxVolume = this.registry.get('sfxVolume') ?? 0.8;
  this.sound.play(key, {
    volume: baseVolume * sfxVolume
  });
}

이제 효과음을 재생할 때마다 같은 계산을 반복하지 않아도 된다.

this.playSfx('sfx-coin', 0.5);
this.playSfx('sfx-hit', 0.7);

짧은 효과음은 많이 겹치면 소음처럼 들린다. 코인 20개를 한꺼번에 먹는 상황에서 같은 소리가 20번 동시에 나면 보상이 아니라 피로가 된다. 이런 경우에는 소리를 아주 짧게 제한하거나, 여러 번 중 일부만 재생하는 규칙을 둔다.

Tween으로 움직임에 부드러움 주기

Tween은 값을 시간에 따라 자동으로 바꾸는 기능이다. UI 버튼이 등장할 때 커지며 나타나거나, 점수 텍스트가 튀었다가 돌아오는 동작을 직접 매 프레임 계산하지 않아도 된다.

가장 간단한 버튼 등장 효과를 보자.

const startButton = this.add.text(400, 300, 'START', {
  fontSize: '36px',
  color: '#ffffff'
}).setOrigin(0.5).setInteractive();

this.tweens.add({
  targets: startButton,
  alpha: { from: 0, to: 1 },
  scale: { from: 0.8, to: 1 },
  duration: 220,
  ease: 'Back.Out'
});

targets는 움직일 대상이다. alpha는 투명도, scale은 크기다. duration은 밀리초 단위 시간이고, ease는 변화 속도의 느낌이다. Back.Out은 목표보다 살짝 넘어갔다가 돌아오는 느낌이라 버튼이나 팝업에 잘 맞는다.

마우스를 올렸을 때 버튼을 살짝 키우려면 다음처럼 쓸 수 있다.

startButton.on('pointerover', () => {
  this.tweens.killTweensOf(startButton);
  this.tweens.add({
    targets: startButton,
    scale: 1.08,
    duration: 80,
    ease: 'Sine.Out'
  });
});

startButton.on('pointerout', () => {
  this.tweens.killTweensOf(startButton);
  this.tweens.add({
    targets: startButton,
    scale: 1,
    duration: 80,
    ease: 'Sine.Out'
  });
});

killTweensOf()를 먼저 호출한 이유는 같은 버튼에 Tween이 계속 쌓이는 것을 막기 위해서다. 입력 이벤트는 짧은 시간에 여러 번 발생할 수 있다. 기존 Tween을 정리하지 않으면 버튼 크기가 의도와 다르게 튈 수 있다.

점수 UI에는 짧은 강조 효과가 잘 어울린다.

pulseScoreText() {
  this.tweens.killTweensOf(this.scoreText);
  this.scoreText.setScale(1);

  this.tweens.add({
    targets: this.scoreText,
    scale: 1.25,
    duration: 90,
    yoyo: true,
    ease: 'Quad.Out'
  });
}

yoyo: true는 목표값까지 갔다가 다시 원래 값으로 돌아오게 한다. 점수가 올랐다는 사실을 짧고 명확하게 보여 줄 때 유용하다.

주의할 점도 있다. 물리 충돌을 사용하는 플레이어의 x, y를 Tween으로 계속 움직이면 Arcade Physics의 속도와 충돌 계산이 어긋날 수 있다. 플레이어 이동은 이전 장처럼 setVelocityX(), setVelocityY()로 처리하고, Tween은 크기, 투명도, UI, 보상 연출처럼 물리 판정과 직접 충돌하지 않는 부분에 주로 사용하자. 꼭 플레이어 위치를 Tween해야 한다면, 스테이지 클리어처럼 조작과 충돌을 잠시 멈춘 연출 구간에서만 사용하는 편이 안전하다.

파티클로 획득과 충돌 표현하기

파티클은 작은 이미지 조각을 여러 개 흩뿌려 효과를 만드는 기능이다. 코인을 먹을 때 반짝임이 터지거나, 플레이어가 착지할 때 먼지가 일어나는 장면에 적합하다.

먼저 작은 입자 이미지를 불러온다.

preload() {
  this.load.image('particle-spark', 'assets/images/particle-spark.png');
  this.load.image('particle-dust', 'assets/images/particle-dust.png');
}

코인 획득용 파티클을 만들어 보자.

create() {
  this.coinEmitter = this.add.particles(0, 0, 'particle-spark', {
    speed: { min: 60, max: 180 },
    angle: { min: 0, max: 360 },
    scale: { start: 0.7, end: 0 },
    alpha: { start: 1, end: 0 },
    lifespan: 450,
    blendMode: 'ADD',
    emitting: false
  });
}

emitting: false로 두면 계속 뿜지 않고, 필요할 때만 터뜨릴 수 있다. 위 코드는 Phaser 3.60 이후의 파티클 작성 방식이다. 이 버전부터 this.add.particles(x, y, texture, config)가 바로 ParticleEmitter를 돌려주므로, 곧바로 explode()를 호출할 수 있다.

collectCoin(player, coin) {
  const x = coin.x;
  const y = coin.y;

  coin.disableBody(true, true);
  this.playSfx('sfx-coin', 0.45);
  this.coinEmitter.explode(12, x, y);

  this.score += 10;
  this.registry.set('score', this.score);
}

여기서 중요한 점은 효과가 사건이 일어난 위치에서 나온다는 것이다. 코인을 먹었는데 화면 중앙에서 반짝이면 정보가 흐려진다. 플레이어는 코인이 있던 자리에서 효과가 터질 때 보상을 즉시 이해한다.

착지 먼지도 같은 방식으로 만들 수 있다.

create() {
  this.dustEmitter = this.add.particles(0, 0, 'particle-dust', {
    speedX: { min: -80, max: 80 },
    speedY: { min: -40, max: -10 },
    gravityY: 160,
    scale: { start: 0.5, end: 0 },
    alpha: { start: 0.6, end: 0 },
    lifespan: 300,
    emitting: false
  });

  this.wasOnFloor = false;
}

update() {
  const onFloor = this.player.body.blocked.down;

  if (onFloor && !this.wasOnFloor) {
    this.dustEmitter.explode(
      8,
      this.player.x,
      this.player.y + this.player.height * 0.4
    );
  }

  this.wasOnFloor = onFloor;
}

이 코드는 플레이어가 바닥에 닿는 순간에만 먼지를 만든다. 바닥에 서 있는 동안 매 프레임 먼지가 나오지 않기 때문에 효과가 과하지 않다. 파티클과 Tween, 카메라 효과를 함께 쓰면 간단한 에셋만으로도 충분히 생생한 장면을 만들 수 있다2.

카메라 흔들림과 플래시 연결하기

카메라 효과는 화면 전체에 영향을 준다. 그래서 강력하지만 조심해서 써야 한다. 약한 코인 획득마다 화면을 흔들면 플레이어가 피곤해진다. 대신 피격, 큰 폭발, 스테이지 클리어처럼 중요한 순간에 사용하자.

피격 처리에 흔들림을 붙여 보자.

takeDamage(amount) {
  if (this.isInvincible) return;

  this.health -= amount;
  this.registry.set('health', this.health);

  this.playSfx('sfx-hit', 0.7);
  this.cameras.main.shake(140, 0.006);

  this.tweens.add({
    targets: this.player,
    alpha: 0.35,
    duration: 70,
    yoyo: true,
    repeat: 3
  });

  this.isInvincible = true;
  this.time.delayedCall(800, () => {
    this.isInvincible = false;
    this.player.setAlpha(1);
  });
}

shake(시간, 강도)에서 강도는 작게 시작하는 편이 좋다. 0.003에서 0.01 사이만 되어도 충분히 느껴진다. 플레이어가 계속 조작해야 하는 게임이라면 흔들림 시간을 짧게 유지하자.

클리어 순간에는 플래시가 잘 어울린다.

clearStage() {
  this.physics.pause();
  this.player.setVelocity(0, 0);

  this.playSfx('sfx-clear', 0.8);
  this.cameras.main.flash(220, 255, 255, 255);

  this.tweens.add({
    targets: this.player,
    y: this.player.y - 40,
    duration: 220,
    yoyo: true,
    ease: 'Quad.Out',
    onComplete: () => {
      this.scene.start('NextStageScene');
    }
  });
}

카메라 플래시는 새 이미지를 준비하지 않아도 화면 전체에 강한 변화를 줄 수 있다. 다만 너무 자주 쓰면 중요한 순간과 평범한 순간의 차이가 사라진다. 위 예제처럼 클리어 연출에서 플레이어의 위치를 살짝 움직이고 싶다면, 먼저 물리 처리를 멈춰 충돌 계산과 Tween이 서로 다른 방향으로 오브젝트를 움직이지 않게 해 두자.

코인 획득 효과를 하나로 묶기

이제 사운드, 파티클, Tween, 상태 변경을 한 흐름으로 묶어 보자. 이전 장에서 점수와 UI를 다뤘다면, 여기서는 같은 사건에 피드백을 붙이는 방식이다.

collectCoin(player, coin) {
  const x = coin.x;
  const y = coin.y;

  coin.disableBody(true, true);

  this.score += 10;
  this.registry.set('score', this.score);

  this.playSfx('sfx-coin', 0.45);
  this.coinEmitter.explode(12, x, y);

  const floatingText = this.add.text(x, y - 18, '+10', {
    fontSize: '18px',
    color: '#ffe066'
  }).setOrigin(0.5);

  this.tweens.add({
    targets: floatingText,
    y: y - 42,
    alpha: 0,
    duration: 500,
    ease: 'Quad.Out',
    onComplete: () => floatingText.destroy()
  });
}

이 코드는 단순하지만 효과의 역할이 분명하다.

  • 효과음은 획득이 성공했음을 알려 준다.

  • 파티클은 코인이 사라진 위치를 강조한다.

  • +10 텍스트는 보상의 내용을 보여 준다.

  • Tween은 텍스트가 자연스럽게 사라지게 한다.

이런 식으로 사건 하나에 필요한 피드백을 작게 묶으면 게임이 훨씬 완성도 있게 느껴진다.

효과를 배치하는 기준

효과는 많을수록 좋은 것이 아니다. 플레이 정보 전달을 방해하면 오히려 게임을 흐린다. 다음 기준으로 판단하자.

첫째, 원인이 분명해야 한다. 소리나 흔들림이 났다면 플레이어가 왜 그런지 알 수 있어야 한다.

둘째, 위치가 맞아야 한다. 획득 효과는 아이템 위치에서, 피격 효과는 플레이어 위치나 적의 공격 위치에서 나와야 한다.

셋째, 중요도에 따라 강도를 나눠야 한다. 코인은 작은 소리와 짧은 파티클이면 충분하다. 피격은 소리, 깜빡임, 약한 카메라 흔들림을 함께 줄 수 있다. 보스 처치나 스테이지 클리어는 더 큰 플래시와 긴 연출을 써도 된다.

넷째, 반복되는 효과는 피로하지 않아야 한다. 점프 소리, 총알 발사음, 코인 소리처럼 자주 나오는 효과는 볼륨을 낮추고 길이를 짧게 만든다.

다섯째, 조작을 방해하지 않아야 한다. Tween이나 카메라 흔들림 때문에 플레이어가 플랫폼 위치를 놓치면 안 된다. 손맛은 조작을 돕는 방향이어야 한다.

사운드, Tween, 파티클, 카메라는 게임에 생기를 넣는 도구다. 하지만 이 도구들의 목적은 장식이 아니라 피드백이다. 플레이어의 행동에 즉시 반응하고, 중요한 정보를 더 잘 보이게 만들 때 게임의 손맛은 자연스럽게 살아난다.

참고

1Phaser Audio Guide: Sound Effects, Music, Volume & Spatial Audio

2Phaser Animation Tutorial: Sprite Sheets, Tweens & Visual Effects

책 목록

Phaser로 시작하는 웹 2D 게임 개발

전체 목차 보기

이 노트는 보도·비평·교육·연구 목적으로 작성되었습니다. 저작권 문의가 있으시면 에서 알려주세요.

틸노트는 사용자가 AI를 활용해 노트를 쉽게 작성하거나 편집할 수 있는 서비스입니다.

AI 스퀘어 단톡방에 함께해요

AI 활용과 틸노트 사용 팁을 나누는 오픈채팅방입니다. 입장 비밀번호는 tilnote입니다.

참여하기