플로팅 스코어 기능 추가 - 슈팅 게임의 기본 UX

Ethan·2025년 10월 20일

타겟을 맞추면 우측 상단의 점수에 방금의 사격 점수가 더해진다. 하지만 게임 중 우측 상단을 확인하기란 속도가 빨라지는 중후반부에는 어렵다. 조금 더 생동감 있고 즉각적인 피드백을 주고 싶어서 '타겟을 맞춘 지점에서 점수가 떠오르며 사라지는' 플로팅 스코어 기능을 추가했다. 슈팅게임엔 어떤 방식으로든 늘 있는 UI중 하나다.

1. 기능 추가 목적 - 타격감은 슈팅 게임의 생명

기존의 게임에선 타겟을 명중시켰을 경우, 삑 하는 효과음만이 발생하고 타겟이 사라진다. 우측 상단 게임 상태에 점수가 더해지긴 하지만, 이걸 확인하는 건 게임 중반만 넘어가도 어려운 일이다.

정리한 이유는 다음과 같다.

1) 타겟 히트와 점수 획득의 연관성 부족

타겟을 맞췄을 때 점수가 상단에만 표시되다 보니, 실제로 몇 점을 얻었는지 즉시 파악하기 어려웠다. 특히 빠른 속도로 여러 타겟을 연속으로 맞출 때는 어떤 타겟에서 몇 점을 얻었는지 구분하기 힘들었다.

게임의 핵심인 '정확도'가 점수에 직결되는 구조인데, 정작 몇 점을 얻었는지 바로 알기 어렵다는 게 아쉬운 점이었다.

2) 시각적 피드백의 부족

타겟을 맞추는 순간의 쾌감이나 만족감을 시각적으로 더 강화할 수 있는 요소가 부족했다. 단순히 타겟이 사라지고 점수만 상단에 올라가는 것보다는, 타겟이 맞춰진 지점에서 점수가 떠오르는 애니메이션이 있다면 훨씬 더 몰입감 있는 게임이 될 것 같았다.


2. 구현 아이디어

타겟 히트 지점에서 점수가 위로 떠오르며 페이드아웃되는 애니메이션

구현하고자 하는 내용은 이러했다.

사격 정확도에 따라 다른 시각적 피드백을 제공하기 위해,
1점과 2점은 그냥 흰색 글씨, 단 3점-정중앙 적중 시 노란색에다 조금 큰 글씨로 점수가 표시되도록 했다.

정리하자면 다음과 같다.

기존 점수 표시

  • 상단 게임 상태 바에만 점수 누적 표시
  • 타겟 히트 시 시각적 피드백 부족

변경된 점수 표시

  • 타겟 히트 지점에서 점수가 위로 떠오르는 애니메이션
  • 점수에 따른 색상 구분:
    • 일반 히트 (1-2점): 흰색
    • 크리티컬 히트 (3점): 노란색
    • 미스 (0점): 없음
  • 정중앙 사격 시 폰트 크기 20% 증가 및 지속시간 연장
  • 0.8초(1-2점) / 1초(3점) 동안 페이드아웃되며 사라짐

파일 구성

지난 렌더 루프 통합 리팩토링을 통해 타겟을 그리는 부분을 별개의 렌더 루프가 아니라 렌더 함수 모듈로 분리했었다.
플로팅 스코어도 타겟과 같이 렌더링 되는 요소이므로,
floatingScoreRenderer.ts라는 파일명으로 /renderers 폴더에 생성했다.


3. 적용 방법

조금 전 얘기했듯 플로팅 스코어는 독립적인 렌더러로 분리하여 구현했다. 기존의 타겟 렌더링과 맵 렌더링 사이에 위치시켜 적절한 레이어 순서를 유지했다.

구현 방법에 대해 고민한 부분은 두 가지다.

1) 애니메이션 이징 함수 선택

가장 먼저 고민한 부분은 어떤 이징 함수를 사용할지였다.

// floatingScoreRenderer.ts

const t = Math.min(1, it.life / it.ttl);
const eased = 1 - Math.pow(1 - t, 3); // cubic ease-out
const alpha = Math.max(0, Math.min(1, 1 - eased));
const scale = 1 + 0.3 * (1 - eased);

cubic ease-out 함수를 선택한 이유는 점수가 자연스럽게 위로 떠오르면서 점점 느려지는 효과를 주기 위함이었다. 단순한 선형 보간보다는 더 부드럽고 자연스러운 움직임을 원했다.

2) 타겟 크기에 비례한 스케일링

두 번째로 고민한 부분은 타겟 크기에 비례한 폰트 크기와 이동 거리를 설정하는 것이었다.

// 타겟 사이즈(타겟 컨테이너 기준)를 기준으로한 offset
const FONT_PER_SIZE = 0.42;
const RISE_PER_SIZE = 0.54;
const LW_PER_SIZE = 0.09;
const BLUR_PER_SIZE = 0.18;

// 타겟 사이즈 비율로 계산
const fontPx = targetSize * FONT_PER_SIZE * (it.crit ? CRIT_FONT_SCALE : 1);
const rise = targetSize * RISE_PER_SIZE;
const lw = Math.max(1, targetSize * LW_PER_SIZE);
const blur = targetSize * BLUR_PER_SIZE;

처음엔 반응형 단위(vh, rem등)을 사용하고자 했는데, 점수 크기가 타겟과 일정한 비율로 유지되지 않았다. 돌이켜보니, 타겟은 맵이 아니라 타겟 컨테이너의 너비를 기준으로 크기를 계산하도록 구혔했던 것을 확인했다.

반드시 타겟과 비율을 맞춰야 하기 때문에,
동일한 기준을 사용하기 위해 TargetManageruseTargetManager를 수정해 타겟 컨테이너 너비의 게터를 만들어 floatingScoreRenderer로 전달했다.

메인 게임 루프에서 플로팅 스코어를 추가하는 부분은 타겟 히트 체크 로직에 통합했다. 미스일 때는 별도 ui를 추가하지 않았기 때문이다.

// GameWorld.tsx

const isHited = targetManagerActions.checkHit(
  screenX,
  screenY,
  (target) => {
    gameActions.handleHit();
    gameActions.addScore(target.score || 0);

    addFloatingScore(
      target.x,
      target.y,
      target.score || 0,
      target.score == 3  // 크리티컬 히트 체크
    );
  }
);

타겟이 히트된 순간 해당 위치에 플로팅 스코어를 추가하고, 렌더링 루프에서 지속적으로 업데이트하여 애니메이션을 처리한다.

4. 결과

(금방 사라져서 캡쳐가 힘들었다..)

플로팅 스코어가 추가된 후 타겟을 맞추는 순간의 만족감이 확실히 높아졌다! 사실 그동안은 이 정도면 충분하다 생각했던 프로젝트인데, 이렇게 사소한 기능 하나씩 더해지고 플레이해보면 아직 높일 수 있는 완성도가 한참 남았다는 생각이 든다.

카메라 전환이나 타겟 처리도 전부 굳이 따지면 정적인 구현들이었는데, 이번에 동적인 Canvas 애니메이션을 직접 구현해보면서 이징 함수나 렌더링 최적화 같은 부분들에 대해 더 깊이 생각해볼 기회가 된 것도 좋았다.

5. 새롭게 발생한 고려사항

이렇게 구현한 플로팅 스코어 기능이 순조롭게 작동함에 만족함과 동시에, 어김없이 염려되는 사항도 새롭게 생겼다.

성능 최적화

게임 중후반부에 빠른 속도로 여러 타겟을 연속으로 맞추게 되면, 동시에 여러 개의 플로팅 스코어가 화면에 표시될 수 있다.

물론 개수가 어느정도 제한 되어 있다지만, 타겟처럼 단순히 그리고 지우기만 하는 것이 아니라 Canvas 렌더링에서 매 프레임마다 모든 플로팅 스코어의 애니메이션을 그리는 작업이 부담이 될 수 있어서 적절한 최적화가 필요했다. 페이드아웃 애니메이션이 더해지면 렌더링 작업에 분명 부담을 줄 것이다.

추가로 재현은 되지 않았지만, 실제로 빌드 직후 테스트할 때 과거 state로 카메라 전환을 구현했을 때 발생했던 카메라 점프 현상이 발생했었다. 이후 다시 빌드해봐도 재현은 잘 되지 않아 우선은 손을 뗐지만, 걱정은 된다.


6. 정리

추가한 기능은 게임의 핵심 로직과는 직접적인 관련이 없는 디자인적 수정이라고 볼 수 있다. 하지만 이런 세심한 디테일들이 모여서 전체적인 게임 경험의 질을 높인다고 생각한다.

간단해 보이지만 실제로는 성능, 애니메이션 이징 등 여러 고려사항이 있는 기능이었다. 타겟 수가 제한되어 메모리까진 크게 신경 쓰지 않아도 되서 다행이다. 다행인가? 경험은 해봐야 하는데.

아무튼 이런 작은 기능 하나하나를 신경 쓰면서 완성도를 높여가는 것이 재미있어서 지난 번에 이어 역시 뿌듯하다. 테스트해보면서 정말 사소해보이는 개선 하나가 사용자 경험을 좌지주지할 수도 있다는 걸 다시금 깨닫게 되었다.


🧩 다음 목표

기술적으로 고칠 만한 건 거의 한 것 같다. 새 프로젝트도 해야 하니, aim test의 마무리는 스타일링이다. 최근에 깨달은 건데, 지금껏 타겟과 게임 로직에만 신경쓰느라 애니메이션에는 거의 신경 쓰지 못하고 있었다. 블러나 호버링정도 적용했을 뿐..

그래서 다음은 메뉴와 각 창들의 슬라이딩 애니메이션을 추가할 생각이다.
이게 다 끝나고 나면 다시 당분간은 aimtest는 놀려야겠지.

profile
"Actions speak louder than words"

0개의 댓글