🤖

본 콘텐츠의 이미지 및 내용은 AI로 생성되었습니다.

⚠️

본 콘텐츠의 이미지 및 내용을 무단으로 복제, 배포, 수정하여 사용할 경우 저작권법에 의해 법적 제재를 받을 수 있습니다.

이미지 로딩 중...

스프라이트 시트와 애니메이션 완벽 가이드 - 슬라이드 1/7
A

AI Generated

2025. 12. 3. · 17 Views

스프라이트 시트와 애니메이션 완벽 가이드

게임 개발에서 캐릭터와 오브젝트에 생명을 불어넣는 스프라이트 시트와 애니메이션의 모든 것을 다룹니다. Phaser 3를 기반으로 실무에서 바로 활용할 수 있는 기법을 배워봅니다.


목차

  1. 스프라이트_시트란
  2. atlas_vs_spritesheet
  3. 프레임_정의하기
  4. 애니메이션_생성
  5. 애니메이션_재생과_제어
  6. 애니메이션_이벤트_처리

1. 스프라이트 시트란

김개발 씨는 처음으로 2D 게임을 만들어보기로 했습니다. 캐릭터가 걷고, 뛰고, 점프하는 모습을 구현하고 싶었습니다.

그런데 캐릭터의 각 동작마다 별도의 이미지 파일을 불러오니 게임이 버벅거리기 시작했습니다.

스프라이트 시트는 여러 개의 이미지를 하나의 큰 이미지 파일에 타일처럼 배치한 것입니다. 마치 만화책의 한 페이지에 여러 컷이 들어있는 것과 같습니다.

이렇게 하면 이미지 로딩 횟수를 줄여 성능을 크게 향상시킬 수 있습니다.

다음 코드를 살펴봅시다.

// preload 함수에서 스프라이트 시트 로드
function preload() {
    // 스프라이트 시트 이미지와 프레임 크기 지정
    this.load.spritesheet('player', 'assets/player.png', {
        frameWidth: 32,   // 각 프레임의 가로 크기
        frameHeight: 48,  // 각 프레임의 세로 크기
        startFrame: 0,    // 시작 프레임 번호
        endFrame: 11      // 끝 프레임 번호
    });
}

// create 함수에서 스프라이트 생성
function create() {
    // 화면 중앙에 플레이어 스프라이트 배치
    this.player = this.add.sprite(400, 300, 'player');
}

김개발 씨는 입사 후 첫 번째 게임 프로젝트에 투입되었습니다. 간단한 플랫포머 게임이었는데, 캐릭터가 걷는 애니메이션을 구현해야 했습니다.

처음에는 각 동작마다 별도의 이미지 파일을 만들어 로드했습니다. walk1.png, walk2.png, walk3.png, walk4.png...

파일이 점점 늘어났습니다. 게임을 실행하니 캐릭터가 움직일 때마다 화면이 끊기는 느낌이 들었습니다.

무엇이 문제였을까요? 선배 개발자 박시니어 씨가 다가와 화면을 보더니 고개를 저었습니다.

"스프라이트 시트를 써야죠. 이렇게 파일을 따로따로 로드하면 성능이 나빠질 수밖에 없어요." 스프라이트 시트란 무엇일까요?

쉽게 비유하자면, 마치 영화 필름과 같습니다. 영화 필름에는 연속된 장면들이 일정한 간격으로 나열되어 있습니다.

이 필름을 빠르게 돌리면 정지된 이미지들이 움직이는 것처럼 보이죠. 스프라이트 시트도 마찬가지입니다.

하나의 큰 이미지 파일 안에 캐릭터의 여러 동작을 격자 형태로 배치합니다. 예를 들어 가로 4칸, 세로 3칸으로 구성하면 총 12개의 프레임을 하나의 파일에 담을 수 있습니다.

게임 엔진은 이 이미지에서 필요한 부분만 잘라내어 화면에 표시합니다. 왜 이렇게 해야 할까요?

웹 브라우저나 게임 엔진에서 이미지를 로드할 때마다 일정한 오버헤드가 발생합니다. 파일을 열고, 메모리에 올리고, 텍스처로 변환하는 과정이 필요하기 때문입니다.

파일이 100개라면 이 과정을 100번 반복해야 합니다. 반면 스프라이트 시트를 사용하면 한 번만 로드하면 됩니다.

GPU에서도 하나의 텍스처로 처리하기 때문에 렌더링 효율이 크게 향상됩니다. 이것을 텍스처 아틀라스 또는 배칭이라고 부르기도 합니다.

위의 코드를 살펴보겠습니다. load.spritesheet 메서드는 이미지 파일과 함께 각 프레임의 크기를 지정받습니다.

frameWidth와 frameHeight는 스프라이트 시트를 자르는 격자의 크기입니다. 32x48 픽셀 크기로 설정하면, 엔진이 자동으로 이미지를 해당 크기로 분할합니다.

startFrame과 endFrame은 사용할 프레임의 범위를 지정합니다. 스프라이트 시트에 더 많은 프레임이 있더라도 필요한 부분만 사용할 수 있습니다.

프레임 번호는 왼쪽 위부터 0번으로 시작하여 오른쪽으로, 그 다음 줄로 순차적으로 매겨집니다. 실제 게임 개발에서는 대부분의 그래픽 에셋을 스프라이트 시트로 관리합니다.

캐릭터뿐만 아니라 아이템, 이펙트, UI 요소까지 모두 스프라이트 시트로 만들어 최적화합니다. TexturePacker나 ShoeBox 같은 도구를 사용하면 여러 이미지를 자동으로 스프라이트 시트로 합쳐줍니다.

김개발 씨는 박시니어 씨의 조언대로 모든 이미지를 스프라이트 시트로 합쳤습니다. 게임을 다시 실행하니 끊김 없이 부드럽게 동작했습니다.

"이렇게 간단한 방법으로 이렇게 큰 차이가 나다니!" 김개발 씨는 감탄했습니다.

실전 팁

💡 - 프레임 크기는 2의 거듭제곱(32, 64, 128 등)으로 설정하면 GPU 성능이 좋아집니다

  • 스프라이트 시트 이미지 전체 크기도 2048x2048 이하로 유지하는 것이 좋습니다

2. atlas vs spritesheet

스프라이트 시트의 개념을 익힌 김개발 씨에게 새로운 고민이 생겼습니다. 캐릭터의 프레임 크기가 동작마다 다르다는 것이었습니다.

걷기 동작은 32x48이지만, 공격 동작은 64x48로 더 넓었습니다. 모든 프레임을 같은 크기로 맞추려니 메모리가 낭비되는 것 같았습니다.

spritesheet은 모든 프레임이 동일한 크기를 가져야 하지만, atlas는 각 프레임이 서로 다른 크기를 가질 수 있습니다. atlas는 JSON 파일로 각 프레임의 위치와 크기를 별도로 정의합니다.

프레임 크기가 다양하거나 빈 공간을 최소화하고 싶을 때 atlas가 더 효율적입니다.

다음 코드를 살펴봅시다.

// Atlas 로드 - 이미지와 JSON 파일을 함께 지정
function preload() {
    this.load.atlas(
        'characters',           // 키 이름
        'assets/characters.png', // 이미지 파일
        'assets/characters.json' // 프레임 정보 JSON
    );
}

// Atlas에서 특정 프레임 사용
function create() {
    // JSON에 정의된 프레임 이름으로 접근
    this.player = this.add.sprite(400, 300, 'characters', 'player_idle_01');
    this.enemy = this.add.sprite(500, 300, 'characters', 'goblin_walk_01');
}

김개발 씨는 스프라이트 시트를 잘 활용하고 있었습니다. 그런데 디자이너에게서 새로운 에셋을 받았을 때 문제가 생겼습니다.

캐릭터의 대기 동작은 작은데, 점프 동작은 팔다리가 쭉 펴져서 훨씬 컸습니다. 모든 프레임을 가장 큰 동작에 맞추니 스프라이트 시트에 빈 공간이 너무 많아졌습니다.

"이건 메모리 낭비 아닌가요?" 김개발 씨가 물었습니다. 박시니어 씨가 대답했습니다.

"그럴 때는 atlas를 사용해야 해요. spritesheet과 atlas는 비슷해 보이지만 중요한 차이가 있거든요." spritesheet은 격자 기반입니다.

마치 바둑판처럼 일정한 크기로 이미지를 분할합니다. 모든 칸의 크기가 같기 때문에 설정이 간단합니다.

하지만 프레임 크기가 다르면 가장 큰 것에 맞춰야 해서 공간이 낭비될 수 있습니다. 반면 atlas는 퍼즐 조각처럼 각 프레임을 빈틈없이 배치할 수 있습니다.

각 프레임의 위치와 크기 정보를 담은 JSON 파일이 함께 제공됩니다. 이 JSON 파일 덕분에 엔진은 정확히 어디서 어떤 크기로 이미지를 잘라낼지 알 수 있습니다.

JSON 파일의 구조는 어떻게 생겼을까요? 대표적인 형식으로 JSON HashJSON Array가 있습니다.

TexturePacker 같은 도구로 생성하면 프레임 이름, x와 y 좌표, 너비와 높이, 원본 크기 등의 정보가 자동으로 기록됩니다. 위의 코드에서 load.atlas 메서드는 세 개의 인자를 받습니다.

첫 번째는 나중에 참조할 키 이름, 두 번째는 이미지 파일 경로, 세 번째는 JSON 파일 경로입니다. 스프라이트 시트와 달리 프레임 크기를 직접 지정하지 않아도 됩니다.

JSON에 모든 정보가 들어있기 때문입니다. 스프라이트를 생성할 때도 차이가 있습니다.

spritesheet은 프레임 번호(0, 1, 2...)로 접근하지만, atlas는 프레임 이름('player_idle_01' 등)으로 접근합니다. 이름으로 접근하면 코드 가독성이 좋아지고, 프레임 순서가 바뀌어도 문제가 없습니다.

그렇다면 언제 무엇을 써야 할까요? 프레임 크기가 모두 같고 간단한 애니메이션이라면 spritesheet이 설정도 쉽고 충분합니다.

하지만 여러 캐릭터, 다양한 크기의 동작, 복잡한 에셋을 다룬다면 atlas가 더 효율적입니다. 실무에서는 대부분 atlas를 사용합니다.

TexturePacker 같은 도구가 이미지들을 최적으로 배치해주고 JSON 파일도 자동 생성해주기 때문입니다. 빈 공간을 최소화하고, 필요하면 이미지를 회전시켜 배치하기도 합니다.

김개발 씨는 TexturePacker로 에셋을 다시 정리했습니다. 같은 이미지들인데 atlas로 만드니 파일 크기가 30퍼센트나 줄어들었습니다.

"도구 하나 제대로 쓰는 게 이렇게 중요하군요."

실전 팁

💡 - 간단한 프로젝트는 spritesheet, 복잡한 프로젝트는 atlas를 선택하세요

  • TexturePacker, ShoeBox 등의 도구를 활용하면 atlas 제작이 훨씬 쉬워집니다

3. 프레임 정의하기

김개발 씨는 atlas를 사용해 에셋을 효율적으로 관리하게 되었습니다. 그런데 애니메이션을 만들려고 보니, 수십 개의 프레임 중에서 어떤 것들이 걷기 동작이고 어떤 것들이 공격 동작인지 코드에서 구분해야 했습니다.

프레임을 체계적으로 정의하는 방법이 필요했습니다.

Phaser에서 프레임 정의는 애니메이션에 사용할 프레임들을 지정하는 것입니다. spritesheet은 숫자 범위로, atlas는 프레임 이름 배열로 정의합니다.

generateFrameNumbersgenerateFrameNames 메서드를 사용하면 규칙적인 프레임들을 쉽게 생성할 수 있습니다.

다음 코드를 살펴봅시다.

function create() {
    // spritesheet용: 숫자 범위로 프레임 생성
    const walkFrames = this.anims.generateFrameNumbers('player', {
        start: 0,    // 시작 프레임 번호
        end: 7,      // 끝 프레임 번호
        first: 0     // 첫 번째로 표시할 프레임
    });

    // atlas용: 이름 패턴으로 프레임 생성
    const attackFrames = this.anims.generateFrameNames('characters', {
        prefix: 'player_attack_',  // 프레임 이름 접두사
        start: 1,                   // 시작 번호
        end: 6,                     // 끝 번호
        zeroPad: 2,                 // 숫자 자릿수 (01, 02...)
        suffix: ''                  // 접미사 (필요시)
    });
}

김개발 씨가 만드는 게임의 주인공은 여러 동작을 할 수 있었습니다. 걷기, 뛰기, 점프, 공격, 피격, 사망까지 총 6가지 동작이 있었고, 각 동작마다 여러 프레임이 필요했습니다.

이 모든 프레임을 어떻게 관리해야 할까요? 박시니어 씨가 설명을 시작했습니다.

"프레임을 정의한다는 건, 애니메이션에 어떤 프레임들이 포함되는지 엔진에게 알려주는 거예요. 마치 영화 편집자가 어떤 장면을 어떤 순서로 이어붙일지 정하는 것과 같죠." spritesheet을 사용할 때는 generateFrameNumbers 메서드를 씁니다.

이 메서드는 숫자 범위를 받아서 프레임 배열을 생성합니다. start가 0이고 end가 7이면 0, 1, 2, 3, 4, 5, 6, 7번 프레임을 순서대로 담은 배열이 만들어집니다.

first 옵션은 애니메이션이 시작될 때 가장 먼저 보여줄 프레임입니다. 보통은 start와 같지만, 특별한 경우 다르게 설정할 수도 있습니다.

예를 들어 대기 애니메이션의 중간 프레임부터 시작하고 싶을 때 유용합니다. atlas를 사용할 때는 generateFrameNames 메서드를 씁니다.

이 메서드는 이름 패턴을 기반으로 프레임을 찾습니다. 대부분의 에셋 도구는 'player_attack_01', 'player_attack_02' 같은 규칙적인 이름을 붙여줍니다.

이 규칙을 활용하는 것입니다. prefix는 프레임 이름의 앞부분입니다.

'player_attack_'처럼 공통된 접두사를 지정합니다. start와 end는 번호의 범위입니다.

zeroPad는 숫자의 자릿수를 맞춰줍니다. zeroPad가 2이면 1은 '01'로, 10은 '10'으로 변환됩니다.

결과적으로 'player_attack_01', 'player_attack_02', ... 'player_attack_06'까지 6개의 프레임 이름이 자동으로 생성됩니다.

이렇게 하면 수십 개의 프레임 이름을 일일이 적지 않아도 됩니다. 만약 프레임 순서가 규칙적이지 않다면 어떻게 할까요?

그럴 때는 직접 배열을 만들 수 있습니다. frames 옵션에 원하는 프레임 번호나 이름을 직접 나열하면 됩니다.

예를 들어 특정 프레임을 건너뛰거나, 역순으로 재생하고 싶을 때 사용합니다. 실무에서는 에셋 파일의 이름 규칙을 미리 정해두는 것이 중요합니다.

디자이너와 개발자가 같은 규칙을 따르면 generateFrameNames로 쉽게 프레임을 생성할 수 있습니다. '캐릭터명_동작명_번호' 형식이 일반적입니다.

김개발 씨는 디자이너에게 이름 규칙을 공유했습니다. 그러자 새 에셋을 받을 때마다 프레임 정의가 훨씬 수월해졌습니다.

"규칙 하나 정해두니까 일이 반으로 줄었네요."

실전 팁

💡 - 에셋 이름 규칙을 프로젝트 초기에 정하고 문서화하세요

  • zeroPad 값은 최대 프레임 수에 맞춰 설정하세요 (100개 이상이면 3 이상)

4. 애니메이션 생성

프레임을 정의하는 방법을 배운 김개발 씨는 이제 본격적으로 애니메이션을 만들 차례였습니다. 정의한 프레임들을 어떤 속도로 재생할지, 반복은 어떻게 할지 설정해야 했습니다.

마치 영화 필름의 재생 속도를 조절하는 것처럼요.

애니메이션 생성은 정의된 프레임들에 재생 속도, 반복 횟수 등의 속성을 부여하는 과정입니다. this.anims.create 메서드로 애니메이션을 등록하면, 이후 어떤 스프라이트에서든 해당 애니메이션을 재생할 수 있습니다.

한 번 생성한 애니메이션은 전역적으로 재사용됩니다.

다음 코드를 살펴봅시다.

function create() {
    // 걷기 애니메이션 생성
    this.anims.create({
        key: 'walk',              // 애니메이션 고유 이름
        frames: this.anims.generateFrameNumbers('player', { start: 0, end: 7 }),
        frameRate: 10,            // 초당 프레임 수
        repeat: -1                // -1은 무한 반복
    });

    // 공격 애니메이션 생성
    this.anims.create({
        key: 'attack',
        frames: this.anims.generateFrameNames('characters', {
            prefix: 'player_attack_', start: 1, end: 6, zeroPad: 2
        }),
        frameRate: 15,
        repeat: 0,                // 0은 한 번만 재생
        yoyo: false               // true면 역재생 후 종료
    });
}

김개발 씨는 프레임들을 정의해 두었습니다. 하지만 프레임만으로는 애니메이션이 되지 않습니다.

마치 사진 여러 장을 가지고 있다고 해서 자동으로 영상이 되지 않는 것과 같습니다. 이 사진들을 어떤 속도로 넘길지, 끝나면 어떻게 할지 정해야 합니다.

박시니어 씨가 화이트보드에 그림을 그리며 설명했습니다. "애니메이션 생성은 레시피를 만드는 것과 비슷해요.

재료가 프레임이라면, 레시피는 그 프레임들을 어떻게 조합할지 정의하는 거죠." this.anims.create 메서드가 그 레시피를 만드는 역할을 합니다. 가장 중요한 것은 key입니다.

이것은 애니메이션의 이름표와 같습니다. 나중에 애니메이션을 재생할 때 이 key로 호출합니다.

따라서 직관적이고 일관된 이름을 붙이는 것이 좋습니다. frames에는 앞서 배운 generateFrameNumbers나 generateFrameNames의 결과를 넣습니다.

또는 직접 프레임 배열을 만들어 넣을 수도 있습니다. 프레임 순서대로 애니메이션이 재생됩니다.

frameRate는 초당 몇 개의 프레임을 보여줄지 정합니다. 10이면 1초에 10개의 프레임, 즉 0.1초마다 프레임이 바뀝니다.

이 값이 클수록 애니메이션이 빨라지고, 작을수록 느려집니다. 일반적인 게임 애니메이션은 8에서 15 사이를 많이 사용합니다.

repeat는 애니메이션 반복 횟수입니다. 0이면 한 번만 재생하고 멈춥니다.

공격이나 점프처럼 한 번 실행되는 동작에 적합합니다. -1이면 무한히 반복됩니다.

걷기나 대기 동작처럼 계속 반복되어야 하는 애니메이션에 사용합니다. yoyo 옵션도 있습니다.

true로 설정하면 애니메이션이 끝까지 재생된 후 역순으로 처음까지 돌아갑니다. 숨쉬기 애니메이션처럼 부드럽게 왕복하는 동작에 유용합니다.

애니메이션을 생성하면 Phaser의 전역 애니메이션 매니저에 등록됩니다. 이것은 매우 중요한 특징입니다.

한 번 만들어 놓으면 어떤 스프라이트에서든 같은 애니메이션을 재생할 수 있습니다. 적 10마리가 같은 걷기 애니메이션을 공유할 수 있다는 뜻입니다.

주의할 점이 있습니다. 같은 key로 애니메이션을 두 번 생성하면 오류가 발생합니다.

따라서 애니메이션 생성 코드는 보통 게임의 create 함수에서 한 번만 실행되도록 합니다. 또는 이미 존재하는지 확인하는 방어 코드를 넣기도 합니다.

김개발 씨는 캐릭터의 모든 동작에 대해 애니메이션을 생성했습니다. walk, run, jump, attack, hurt, die 총 6개의 애니메이션이 준비되었습니다.

"이제 진짜 움직이게 할 수 있겠어요!"

실전 팁

💡 - 애니메이션 key는 '캐릭터_동작' 형식으로 일관성 있게 지으세요 (예: player_walk, enemy_attack)

  • frameRate는 실제로 테스트하며 자연스러운 값을 찾으세요. 너무 빠르거나 느리면 어색합니다

5. 애니메이션 재생과 제어

애니메이션을 모두 생성한 김개발 씨는 드디어 캐릭터를 움직이게 만들 차례였습니다. 플레이어가 키보드를 누르면 걷기 애니메이션이 재생되고, 키를 떼면 대기 애니메이션으로 돌아가야 했습니다.

단순히 재생만 하는 것이 아니라, 상황에 맞게 제어하는 방법이 필요했습니다.

play 메서드로 애니메이션을 시작하고, stop으로 멈추며, pauseresume으로 일시정지와 재개를 합니다. play의 두 번째 인자로 true를 전달하면 현재 재생 중인 애니메이션이 있어도 강제로 재시작하지 않습니다.

이를 활용해 자연스러운 애니메이션 전환을 구현할 수 있습니다.

다음 코드를 살펴봅시다.

function update() {
    const cursors = this.input.keyboard.createCursorKeys();

    if (cursors.left.isDown || cursors.right.isDown) {
        // 걷기 애니메이션 재생 (이미 재생 중이면 무시)
        this.player.play('walk', true);

        // 방향에 따라 스프라이트 뒤집기
        this.player.flipX = cursors.left.isDown;
    } else if (cursors.space.isDown) {
        // 공격 애니메이션 (강제로 처음부터 재생)
        this.player.play('attack', false);
    } else {
        // 아무 키도 안 눌렸으면 대기 애니메이션
        this.player.play('idle', true);
    }
}

김개발 씨가 만든 애니메이션들은 준비는 되었지만 아직 화면에서 움직이지 않았습니다. 마치 녹화된 영상이 재생 버튼을 기다리는 것과 같았습니다.

이제 그 재생 버튼을 눌러야 할 때입니다. 가장 기본적인 메서드는 play입니다.

스프라이트 객체에서 호출하며, 인자로 애니메이션 key를 전달합니다. this.player.play('walk')라고 하면 player 스프라이트에서 'walk' 애니메이션이 재생됩니다.

그런데 여기서 중요한 문제가 있습니다. update 함수는 매 프레임 호출됩니다.

초당 60번 정도 실행된다는 뜻입니다. 만약 매번 play를 호출하면 어떻게 될까요?

애니메이션이 계속 처음부터 다시 시작되어 첫 프레임만 반복될 것입니다. 이 문제를 해결하기 위해 play의 두 번째 인자가 있습니다.

true를 전달하면 "이미 이 애니메이션이 재생 중이면 무시해"라는 의미입니다. 이것을 ignoreIfPlaying 옵션이라고 합니다.

이 옵션 덕분에 매 프레임 play를 호출해도 애니메이션이 끊기지 않습니다. 반대로 공격처럼 매번 처음부터 재생해야 하는 동작도 있습니다.

연속으로 공격 키를 누르면 공격 동작이 처음부터 다시 시작되어야 자연스럽습니다. 이럴 때는 두 번째 인자를 false로 주거나 생략합니다.

flipX 속성은 스프라이트를 좌우로 뒤집습니다. 캐릭터가 왼쪽으로 걸을 때 별도의 왼쪽 방향 애니메이션을 만들 필요 없이, 오른쪽 애니메이션을 뒤집어 사용할 수 있습니다.

true면 뒤집히고, false면 원래 방향입니다. stop 메서드는 애니메이션을 완전히 멈춥니다.

멈추면 현재 프레임에서 정지합니다. pauseresume은 일시정지와 재개입니다.

게임이 일시정지될 때 애니메이션도 함께 멈추고 싶다면 pause를 사용합니다. 애니메이션 전환을 자연스럽게 하려면 상태 관리가 중요합니다.

현재 어떤 상태인지 변수로 관리하고, 상태가 바뀔 때만 애니메이션을 전환하는 방식이 일반적입니다. 이렇게 하면 복잡한 캐릭터 동작도 체계적으로 관리할 수 있습니다.

한 가지 팁이 있습니다. 점프 중에 걷기 키를 누르면 어떻게 될까요?

우선순위를 정해야 합니다. 보통 점프나 피격 같은 동작이 걷기보다 우선순위가 높습니다.

이런 우선순위 로직을 잘 설계해야 버그 없는 캐릭터 컨트롤러를 만들 수 있습니다. 김개발 씨는 키보드 입력에 따라 애니메이션이 전환되도록 코드를 작성했습니다.

화살표 키를 누르니 캐릭터가 걷기 시작했고, 스페이스바를 누르니 공격 동작이 나왔습니다. "드디어 살아 움직이는 것 같아요!"

실전 팁

💡 - 반복되는 동작(걷기, 대기)에는 play의 두 번째 인자를 true로 설정하세요

  • 상태 머신 패턴을 사용하면 복잡한 애니메이션 전환을 깔끔하게 관리할 수 있습니다

6. 애니메이션 이벤트 처리

김개발 씨의 캐릭터는 이제 자연스럽게 움직였습니다. 그런데 새로운 요구사항이 생겼습니다.

공격 애니메이션의 특정 프레임에서 데미지 판정을 해야 했습니다. 또한 애니메이션이 끝나면 다른 동작으로 자연스럽게 전환되어야 했습니다.

애니메이션의 특정 시점을 감지하는 방법이 필요했습니다.

Phaser의 애니메이션 이벤트를 활용하면 애니메이션 재생 중 특정 시점에 코드를 실행할 수 있습니다. animationcomplete 이벤트는 애니메이션 종료 시점을, animationupdate는 프레임이 바뀔 때마다 발생합니다.

이를 통해 공격 판정, 효과음 재생, 상태 전환 등을 정확한 타이밍에 처리할 수 있습니다.

다음 코드를 살펴봅시다.

function create() {
    // 애니메이션 완료 이벤트 리스너
    this.player.on('animationcomplete', (anim, frame) => {
        if (anim.key === 'attack') {
            // 공격 애니메이션이 끝나면 대기 상태로 전환
            this.player.play('idle');
        }
    });

    // 특정 프레임 도달 이벤트
    this.player.on('animationupdate', (anim, frame) => {
        // 공격 애니메이션의 3번째 프레임에서 데미지 판정
        if (anim.key === 'attack' && frame.index === 3) {
            this.checkDamage();
        }
    });
}

김개발 씨의 게임에서 공격 시스템을 구현해야 했습니다. 문제는 타이밍이었습니다.

공격 버튼을 누르자마자 데미지가 들어가면 어색합니다. 캐릭터가 칼을 휘두르는 순간, 정확히 그 프레임에서 데미지 판정이 이루어져야 자연스럽습니다.

박시니어 씨가 힌트를 주었습니다. "애니메이션 이벤트를 사용하면 돼요.

특정 프레임에 도달했을 때, 또는 애니메이션이 끝났을 때 원하는 코드를 실행할 수 있어요." 가장 많이 사용하는 이벤트는 animationcomplete입니다. 이름 그대로 애니메이션 재생이 완료되면 발생합니다.

콜백 함수의 첫 번째 인자로 어떤 애니메이션이 끝났는지 정보가 전달됩니다. anim.key를 확인하면 'attack', 'jump' 등 애니메이션을 구분할 수 있습니다.

공격 애니메이션이 끝나면 대기 상태로 돌아가야 합니다. animationcomplete 이벤트에서 anim.key가 'attack'인지 확인하고, 맞다면 idle 애니메이션을 재생합니다.

이렇게 하면 공격 동작이 끝나면 자연스럽게 대기 상태로 전환됩니다. animationupdate 이벤트는 프레임이 바뀔 때마다 발생합니다.

두 번째 인자 frame에서 현재 프레임 정보를 얻을 수 있습니다. frame.index가 몇 번째 프레임인지 알려줍니다.

이를 활용해 특정 프레임에서 원하는 동작을 실행합니다. 공격 애니메이션이 총 6프레임이라고 가정해봅시다.

1, 2번 프레임은 칼을 들어올리는 준비 동작이고, 3번 프레임에서 칼이 적에게 닿습니다. 4, 5, 6번 프레임은 칼을 거두는 동작입니다.

데미지 판정은 3번 프레임에서만 이루어져야 합니다. frame.index가 3일 때 checkDamage 함수를 호출하면 됩니다.

이 함수에서 적과의 충돌을 검사하고, 충돌했다면 데미지를 적용합니다. 이렇게 하면 칼이 휘둘러지는 정확한 순간에만 판정이 이루어집니다.

효과음 재생에도 이벤트가 유용합니다. 걷기 애니메이션에서 발이 땅에 닿는 프레임에 발소리를 재생하면 훨씬 자연스럽습니다.

animationupdate에서 해당 프레임을 감지하고 사운드를 재생하면 됩니다. 한 가지 주의할 점이 있습니다.

이벤트 리스너는 한 번 등록하면 계속 유지됩니다. 씬이 바뀌거나 스프라이트가 제거될 때 리스너도 함께 정리해야 메모리 누수를 방지할 수 있습니다.

off 메서드로 리스너를 제거하거나, once 메서드로 한 번만 실행되는 리스너를 등록할 수 있습니다. 김개발 씨는 이벤트 시스템을 활용해 공격 판정을 구현했습니다.

칼이 휘둘러지는 순간 적에게 데미지가 들어가고, 동작이 끝나면 대기 상태로 돌아갔습니다. "이제 진짜 액션 게임 같아요!"

실전 팁

💡 - 이벤트 리스너는 씬 종료 시 반드시 정리하세요 (this.player.off 사용)

  • 복잡한 타이밍 로직은 별도의 함수로 분리하면 관리하기 쉽습니다

이상으로 학습을 마칩니다. 위 내용을 직접 코드로 작성해보면서 익혀보세요!

#Phaser#SpriteSheet#Animation#GameDev#FrameAnimation#Game,JavaScript,Phaser

댓글 (0)

댓글을 작성하려면 로그인이 필요합니다.