Blog으로 돌아가기
2026.08.10.11 min read

총 스킬이 앞으로 안 나가요, 제자리 판정 상자와 한 장짜리 스프라이트

팀원이 브라우저 콘솔에서 스킬을 하나씩 켜보며 테스트하다가 이상한 걸 발견했습니다. 관통탄을 쏘면 이펙트가 번쩍하고 데미지도 들어가는데, 정작 아무것도 앞으로 날아가지 않는다는 겁니다. 확인해 보니 관통탄만이 아니었습니다. 총검돌격도, 확산탄도, 심지어 대시 스킬인 질주의 잔영도 몸이 제자리였습니다. 겉보기에는 발사체가 안 나가는 버그 하나지만, 파보니 판정 구조의 문제와 에셋 파이프라인의 문제가 겹쳐 있었습니다. 이 글은 그 두 겹을 벗겨낸 기록입니다.

Phaser스프라이트게임개발

데미지는 들어가는데 나가는 게 없다

관통탄 구현을 열어 보니 이유가 바로 보였습니다. 플레이어 앞 90픽셀 지점에 150x24짜리 보이지 않는 판정 상자를 만들고, 120밀리초 뒤에 지웁니다. 그게 전부였습니다. 상자에 속도가 없으니 아무것도 날아가지 않습니다. 그런데 바로 앞의 적은 맞으니까 데미지 숫자는 뜹니다. 그래서 증상이 더 헷갈립니다. 스킬 이름은 꿰뚫는 한 발인데 실제로는 앞에 잠깐 생겼다 사라지는 장판이었던 거죠.

재미있는 건 같은 게임의 검기 스킬은 제대로 날아가고 있었다는 점입니다. 검기는 판정 상자에 setVelocityX로 속도를 주고 이펙트 스프라이트가 매 프레임 상자 위치를 따라가는 구조였습니다. 올바른 패턴이 같은 파일 안에 이미 있었던 겁니다. 총 스킬 세 개만 나중에 급하게 추가되면서 제자리 상자 방식으로 구현됐고, 이펙트가 화려해서 아무도 판정이 제자리라는 걸 눈치채지 못했습니다.

typescript수정. 판정 상자에 속도를 싣고 이펙트가 따라가게 했다.
// before: 제자리 상자 120ms. 사실상 장판이었다.
const box = this.scene.physics.add.image(sprite.x + dir * 90, sprite.y, TEXTURE.playerAttack);
this.scene.time.delayedCall(120, () => box.destroy());

// after: 날아가는 관통탄. 경로의 적을 전부 맞힌다.
const body = box.body as Phaser.Physics.Arcade.Body;
body.setAllowGravity(false);
body.setVelocityX(dir * upgrade.pierceSpeed);           // 1500px/s
this.scene.time.delayedCall(upgrade.pierceLifeMs, () => box.destroy());
this.followSkillVfx("skillPierceBurst", box, dir, scale); // 이펙트 동행

스킬마다 성격에 맞게 수치를 달리 줬습니다. 관통탄은 빠르고 길게 1500px/s로 420밀리초, 총검돌격은 짧고 묵직하게 900px/s로 220밀리초, 확산탄은 세 갈래가 부챗살로 벌어지도록 위아래 갈래에 y속도를 얹었습니다. 질주의 잔영은 발사체가 아니라 몸이 나가야 하는 스킬이라 접근을 달리했습니다. 기존 대시의 상태 기계인 isDashing을 빌려서 실제로 150픽셀을 짓쳐들게 했습니다. 대시 경로를 재사용하니 지상에서 수평 유지하는 처리, 잔상 이펙트, 대시 중 무적까지 전부 공짜로 따라왔습니다. 특히 잔상은 스킬 이름이 질주의 잔영이라 오히려 더 어울렸고요.

두 번째 겹, 스프라이트가 한 장뿐이었다

판정을 고치고 나니 팀원이 두 번째 제보를 했습니다. "분명 모든 스킬 프레임을 5장 이상 줬는데 왜 그림이 한 장만 나와?" 시트 등록을 확인해 보니 정말로 총 스킬 세 개만 frames: 1로 잡혀 있었고, PNG 파일 자체도 한 프레임 크기였습니다. 며칠 전 여러 이펙트가 뒤섞인 큰 콜라주 이미지에서 스킬별 그림을 잘라 넣는 작업을 했는데, 그때 총 스킬들은 여러 프레임 중 한 장씩만 잘려 들어갔습니다.

그럼 원본 콜라주에서 나머지 프레임을 다시 뽑으면 되겠네, 하고 가볍게 시작했는데 이게 만만치 않았습니다. 프레임들이 균일한 격자에 있지 않고 제각각의 간격으로 흩어져 있었거든요. 알파 채널을 세로로 투영해서 빈 열을 찾아 자르는 고전적인 방법을 먼저 시도했습니다. 이펙트의 잔광이 이웃 프레임과 겹쳐 있어서 여러 프레임이 한 덩어리로 붙어 나왔습니다. 밝은 코어를 기준으로 나누는 방법, 투영 그래프의 골짜기를 찾아 자르는 방법까지 갔습니다.

검기 이펙트 스트립에 골짜기 분할로 찾은 프레임 경계선이 그려진 모습
골짜기 분할로 검기 스트립을 자른 결과. 여기까지는 그럭저럭 갔지만, 권총 이펙트에서는 총과 화염이 반씩 잘렸다.

골짜기 분할로 검기 같은 스트립은 그럭저럭 잘렸지만, 권총 계열에서 한계가 왔습니다. 권총 본체와 총구 화염이 딱 붙어 있어서 어디를 잘라도 반쪽짜리 프레임이 나왔습니다. 알고리즘을 더 정교하게 만들 수도 있었겠지만, 여기서 잠깐 멈추고 생각했습니다. 내가 지금 뭘 하고 있는 거지?

자동 추출을 포기하고 규격을 협상했다

추출 코드를 고도화하는 대신 에셋을 만드는 팀원에게 규격을 제안했습니다. 프레임당 정사각형 셀, 가로 한 줄, 잔광 포함해서 셀 침범 금지, 배경 투명. 이렇게만 맞춰 주면 코드는 등분해서 자르기만 하면 된다고요.

팀원이 156픽셀 그리드로 전 스킬 스트립을 다시 정렬해 줬고, 그 뒤로 추출은 산수가 됐습니다. 이미지 너비를 셀 수로 나눠 등분하고 각 셀 안에서 알파 픽셀의 타이트한 경계만 다시 재면 끝입니다.

팀원이 156픽셀 그리드로 재정렬한 권총 스킬 스트립. 셀 경계선이 프레임 사이를 깔끔하게 지나간다
팀원이 그리드로 재정렬해 준 관통탄 스트립. 경계선이 그림을 건드리지 않고 지나간다. 이러면 코드는 등분만 하면 된다.

덕분에 검기 9프레임, 검극 8, 검무 5, 질주 7, 도약 6프레임이 그대로 살아났습니다. 관통탄은 한 발 더 나가서 총구 화염과 비행 탄환을 아예 분리했습니다. 발사 지점에는 화염 4프레임이 제자리에서 터지고, 탄환 3프레임은 루프로 돌면서 날아갑니다. 발사감이 확실히 달라졌습니다.

재추출한 총 스킬 시트 세 장. 관통탄 탄환, 총검돌격 궤적, 확산탄 부채꼴이 프레임별로 정리되어 있다
재추출을 마친 총 스킬 시트들. 위에서부터 관통탄 비행 탄환, 총검돌격 3단 궤적, 확산탄 부채꼴 2프레임.

돌아보면 이게 이번 작업에서 제일 중요한 결정이었습니다. 불규칙한 데이터를 코드로 감당하는 것과 데이터를 규칙적으로 만들어 코드를 단순하게 유지하는 것 사이에서 후자를 고른 거니까요. 추출 스크립트는 30줄이 됐고 앞으로 에셋이 추가돼도 같은 규격이면 코드는 그대로입니다. 협업에서 파이프라인 규격을 정하는 대화 10분이 세그멘테이션 알고리즘 몇 시간보다 값쌌습니다.

루프 애니메이션이 남긴 함정

비행 탄환을 루프 애니메이션으로 바꾸면서 함정이 하나 드러났습니다. 이펙트를 발사체에 따라붙이는 함수가 애니메이션이 끝나면 이펙트를 지운다는 정리 방식을 쓰고 있었는데, 루프 애니메이션은 영원히 끝나지 않습니다. 발사체가 수명을 다해 사라져도 이펙트만 마지막 위치에 남아 계속 재생되는 유령이 생기는 거죠.

typescript따라다니는 틱에서 대상이 사라지면 이펙트도 함께 지운다.
const tick = this.scene.time.addEvent({
  delay: 16, loop: true,
  callback: () => {
    if (!fx.active) { tick.remove(false); return; }
    // 루프 애니메이션은 ANIMATION_COMPLETE가 영영 안 온다.
    // 발사체가 사라지는 순간 여기서 함께 지워야 한다.
    if (!target.active) { tick.remove(false); fx.destroy(); return; }
    fx.setPosition(target.x, target.y);
  },
});

완료 시 정리라는 방식은 유한한 애니메이션에서만 성립하는 가정입니다. 루프를 도입하는 순간 정리 조건을 대상의 수명으로 옮겨야 했고, 위치를 따라가던 틱이 마침 매 프레임 대상을 보고 있었으니 그 자리에서 지우게 했습니다.

덤, 이펙트가 몸 뒤에서 튀어나오던 이유

마무리 즈음에 "확산탄이 내 뒤에서 생성된다"는 제보가 하나 더 왔습니다. 판정 상자는 분명 앞에 만드는데도요. 원인은 이펙트 그림의 원점이었습니다. 부채꼴 이펙트의 셀 폭이 207픽셀인데 원점이 가운데라 발사 지점에 걸면 그림의 뒤쪽 절반 100픽셀이 플레이어 뒤로 삐져나왔습니다. 원점을 그림 앞머리 쪽 0.08로 옮기니 부채꼴이 진행 방향으로만 펼쳐졌습니다. 좌우 반전도 원점 기준으로 뒤집히니까 왼쪽을 볼 때도 똑같이 앞으로만 나갑니다.

결국 이 글의 문제들은 하나로 수렴합니다. 판정에는 속도가, 시트에는 프레임 수가, 이펙트에는 원점이 있었고, 각각 보이지 않는 속성 하나씩이 잘못되어 있었습니다. 그리고 화면에 그럴듯한 그림이 나오는 동안은 아무도 눈치채지 못했습니다. 게임의 버그는 그림 뒤에 숨습니다. 다음부터 스킬을 테스트할 때는 이펙트를 끄고 판정 박스부터 그려보려고 합니다. 그게 가장 정직한 화면이니까요.

고친 스킬들은 아래에서 직접 써볼 수 있습니다. 시작하고 무기를 얻으면 스킬 슬롯이 열립니다.

게임 플레이하기GitHub 저장소