🤖

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

⚠️

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

이미지 로딩 중...

Flutter Flame 게임 애니메이션 완벽 가이드 - 슬라이드 1/7
A

AI Generated

2025. 12. 30. · 0 Views

Flutter Flame 게임 애니메이션 완벽 가이드

Flutter의 Flame 엔진으로 스프라이트 애니메이션을 구현하는 방법을 배웁니다. 기초부터 캐릭터 걷기 애니메이션까지 단계별로 살펴봅니다. 게임 개발 입문자를 위한 실전 가이드입니다.


목차

  1. 스프라이트_시트란
  2. SpriteAnimation_생성
  3. SpriteAnimationComponent_사용
  4. 애니메이션_속도_조절
  5. 여러_애니메이션_전환
  6. 캐릭터_걷기_애니메이션_구현

1. 스프라이트 시트란

게임 개발을 시작한 김개발 씨가 첫 번째 캐릭터를 만들었습니다. 하지만 캐릭터가 화면에 서 있기만 할 뿐 움직이지 않았습니다.

"캐릭터가 걷는 것처럼 보이게 하려면 어떻게 해야 할까요?"

스프라이트 시트는 여러 장의 이미지를 하나의 파일로 모아둔 것입니다. 마치 만화책의 한 페이지에 여러 컷이 그려진 것처럼, 게임 캐릭터의 여러 동작 프레임을 한 이미지에 담습니다.

이렇게 하면 파일 관리가 쉽고 로딩 속도도 빨라집니다. Flame 엔진은 이 스프라이트 시트를 자동으로 잘라서 애니메이션으로 만들어줍니다.

다음 코드를 살펴봅시다.

// assets 폴더에 스프라이트 시트 준비
// character_walk.png: 8프레임이 가로로 배열된 이미지
// pubspec.yaml에 등록
flutter:
  assets:
    - assets/images/character_walk.png

// 스프라이트 시트 정보 정의
final spriteSheet = await images.load('character_walk.png');
// 8개 프레임, 각 프레임 크기 64x64 픽셀
final spriteSize = Vector2(64, 64);
final frameCount = 8;

김개발 씨는 게임 개발 스터디에 참여한 지 한 달이 되었습니다. 드디어 자신만의 2D 게임을 만들기로 결심했습니다.

캐릭터 이미지를 준비하고, 화면에 띄우는 것까지는 성공했습니다. 하지만 캐릭터가 움직이지 않았습니다.

그저 한 장의 그림처럼 서 있을 뿐이었습니다. 스터디 리더인 박시니어 씨가 김개발 씨의 코드를 살펴봤습니다.

"아, 아직 애니메이션을 추가하지 않았군요. 먼저 스프라이트 시트가 필요합니다." 그렇다면 스프라이트 시트란 정확히 무엇일까요?

쉽게 비유하자면, 스프라이트 시트는 만화책의 한 페이지와 같습니다. 만화책 한 페이지에는 여러 컷이 순서대로 그려져 있습니다.

왼쪽에서 오른쪽으로, 위에서 아래로 컷을 따라가다 보면 이야기가 자연스럽게 흐릅니다. 스프라이트 시트도 마찬가지입니다.

캐릭터가 걷는 모습을 여러 장면으로 나눠서 한 이미지에 배치합니다. 이 장면들을 빠르게 전환하면 캐릭터가 실제로 걷는 것처럼 보입니다.

스프라이트 시트가 없던 시절에는 어땠을까요? 개발자들은 각 프레임을 별도의 이미지 파일로 관리해야 했습니다.

걷기 애니메이션에 8프레임이 필요하다면 8개의 파일을 만들어야 했습니다. 달리기, 점프, 공격 등 다양한 동작을 추가하면 파일이 수십 개로 늘어났습니다.

파일 이름을 잘못 지으면 순서가 꼬이기도 했습니다. 더 큰 문제는 로딩 속도였습니다.

파일이 많을수록 로딩 시간이 길어졌고, 메모리 관리도 복잡해졌습니다. 바로 이런 문제를 해결하기 위해 스프라이트 시트가 등장했습니다.

스프라이트 시트를 사용하면 여러 프레임을 하나의 파일로 관리할 수 있습니다. 파일 하나만 로딩하면 모든 프레임을 사용할 수 있습니다.

또한 파일 개수가 줄어들어 프로젝트 관리가 훨씬 쉬워집니다. 무엇보다 로딩 시간이 대폭 단축된다는 큰 이점이 있습니다.

모바일 게임에서는 특히 중요한 부분입니다. 스프라이트 시트를 준비하는 방법을 알아보겠습니다.

먼저 그래픽 도구나 온라인 서비스를 사용해서 스프라이트 시트를 만듭니다. 무료 도구로는 Piskel, Aseprite 같은 프로그램이 유명합니다.

혹은 이미 만들어진 스프라이트 시트를 다운로드할 수도 있습니다. OpenGameArt.orgitch.io 같은 사이트에서 무료 리소스를 찾을 수 있습니다.

스프라이트 시트는 보통 프레임들이 가로로 일렬로 배치됩니다. 예를 들어 걷기 애니메이션이 8프레임이라면, 8개의 그림이 왼쪽부터 오른쪽으로 나열됩니다.

각 프레임의 크기는 동일해야 합니다. 64x64 픽셀, 128x128 픽셀 등 정사각형으로 만드는 것이 일반적입니다.

위의 코드를 살펴보겠습니다. 먼저 pubspec.yaml 파일에 이미지를 등록합니다.

Flutter 프로젝트에서 이미지를 사용하려면 반드시 이 단계가 필요합니다. assets 폴더 안에 images 폴더를 만들고, 그 안에 스프라이트 시트를 넣습니다.

다음으로 Flame의 images.load 메서드로 이미지를 로딩합니다. 마지막으로 프레임 크기와 개수를 정의합니다.

이 정보는 나중에 애니메이션을 만들 때 사용됩니다. 실제 현업에서는 어떻게 활용할까요?

대부분의 2D 게임은 스프라이트 시트를 기본으로 사용합니다. 유명한 모바일 게임인 쿠키런이나 브롤스타즈도 모두 스프라이트 시트 방식입니다.

캐릭터뿐만 아니라 아이템, 이펙트, UI 요소까지도 스프라이트 시트로 관리합니다. 한 게임에 수백 개의 스프라이트 시트가 사용되기도 합니다.

하지만 주의할 점도 있습니다. 초보 개발자들이 흔히 하는 실수 중 하나는 프레임 크기를 맞추지 않는 것입니다.

스프라이트 시트의 각 프레임은 정확히 같은 크기여야 합니다. 크기가 다르면 Flame이 프레임을 제대로 잘라내지 못합니다.

따라서 그래픽 작업 단계에서부터 프레임 크기를 정확하게 맞춰야 합니다. 다시 김개발 씨의 이야기로 돌아가 봅시다.

박시니어 씨의 설명을 들은 김개발 씨는 고개를 끄덕였습니다. "아, 그래서 게임마다 assets 폴더에 큰 이미지 파일들이 있었군요!" 스프라이트 시트를 제대로 이해하면 게임 리소스를 효율적으로 관리할 수 있습니다.

여러분도 오늘 배운 내용을 실제 게임 프로젝트에 적용해 보세요.

실전 팁

💡 - 프레임 크기는 2의 제곱수로 만들면 성능이 좋습니다 (64, 128, 256 등)

  • 하나의 스프라이트 시트에는 같은 종류의 애니메이션만 모으세요 (걷기는 걷기끼리)

2. SpriteAnimation 생성

스프라이트 시트를 준비한 김개발 씨는 다음 단계로 넘어갔습니다. "이제 이 이미지로 실제 애니메이션을 만들어야 하는데, 어떻게 하는 거지?" 박시니어 씨가 웃으며 말했습니다.

"Flame의 SpriteAnimation을 사용하면 됩니다."

SpriteAnimation은 스프라이트 시트를 실제 애니메이션으로 변환하는 클래스입니다. 마치 플립북을 빠르게 넘기는 것처럼 프레임을 순차적으로 전환합니다.

각 프레임이 화면에 표시되는 시간을 설정할 수 있어서 애니메이션 속도를 조절할 수 있습니다. Flame이 제공하는 편리한 메서드로 간단하게 만들 수 있습니다.

다음 코드를 살펴봅시다.

import 'package:flame/game.dart';
import 'package:flame/components.dart';

// SpriteAnimation 생성 함수
Future<SpriteAnimation> createWalkAnimation() async {
  final spriteSheet = await Flame.images.load('character_walk.png');

  // fromFrameData: 프레임 정보로 애니메이션 생성
  return SpriteAnimation.fromFrameData(
    spriteSheet,
    SpriteAnimationData.sequenced(
      amount: 8,           // 총 8개 프레임
      stepTime: 0.1,       // 각 프레임 0.1초 표시
      textureSize: Vector2(64, 64),  // 프레임 크기
    ),
  );
}

김개발 씨는 스프라이트 시트를 assets 폴더에 잘 배치했습니다. 하지만 이것만으로는 아무 일도 일어나지 않았습니다.

이미지는 그저 파일일 뿐, 화면에서 움직이지 않았습니다. "이제 어떻게 해야 하나요?" 김개발 씨가 물었습니다.

박시니어 씨가 화면을 가리키며 설명했습니다. "지금까지는 재료만 준비한 거예요.

이제 요리를 해야죠. Flame의 SpriteAnimation을 사용하면 됩니다." 그렇다면 SpriteAnimation이란 정확히 무엇일까요?

쉽게 비유하자면, SpriteAnimation은 플립북과 같습니다. 어렸을 때 공책 모서리에 조금씩 다른 그림을 그려본 적 있나요?

공책을 빠르게 넘기면 그림이 움직이는 것처럼 보입니다. SpriteAnimation도 똑같은 원리입니다.

스프라이트 시트의 프레임들을 빠르게 전환하면서 애니메이션 효과를 만들어냅니다. 초당 몇 프레임을 보여줄지 우리가 직접 정할 수 있습니다.

SpriteAnimation이 없던 시절에는 어땠을까요? 개발자들은 타이머를 직접 만들어야 했습니다.

일정 시간마다 다음 프레임으로 전환하는 로직을 손수 작성했습니다. 현재 프레임 인덱스를 추적하고, 마지막 프레임에 도달하면 처음으로 돌아가는 코드를 만들었습니다.

코드가 길어지고 복잡해졌으며, 버그가 생기기 쉬웠습니다. 더 큰 문제는 성능 최적화였습니다.

여러 캐릭터가 동시에 애니메이션하면 프레임이 뚝뚝 끊기기도 했습니다. 바로 이런 문제를 해결하기 위해 SpriteAnimation이 등장했습니다.

SpriteAnimation을 사용하면 단 몇 줄의 코드로 애니메이션을 만들 수 있습니다. 프레임 전환 로직은 Flame이 알아서 처리합니다.

또한 성능 최적화도 자동으로 이루어집니다. 무엇보다 코드가 간결하고 읽기 쉽다는 큰 이점이 있습니다.

유지보수할 때도 훨씬 편리합니다. 위의 코드를 한 줄씩 살펴보겠습니다.

먼저 Flame.images.load로 스프라이트 시트를 로딩합니다. 이 부분은 비동기 작업이므로 await을 사용합니다.

다음으로 SpriteAnimation.fromFrameData 메서드를 호출합니다. 이 메서드가 핵심입니다.

SpriteAnimationData.sequenced를 사용하면 프레임이 순차적으로 배열된 스프라이트 시트를 쉽게 처리할 수 있습니다. amount 매개변수는 프레임 개수를 지정합니다.

우리 예제에서는 8개입니다. stepTime은 각 프레임이 화면에 표시되는 시간입니다.

0.1초로 설정하면 1초에 10프레임이 재생됩니다. textureSize는 각 프레임의 크기를 Vector2로 지정합니다.

64x64 픽셀이라면 Vector2(64, 64)로 씁니다. 실제 현업에서는 어떻게 활용할까요?

대부분의 게임 프로젝트에서는 애니메이션 관리 클래스를 따로 만듭니다. 캐릭터마다 걷기, 달리기, 점프, 공격 등 여러 애니메이션이 필요하기 때문입니다.

예를 들어 PlayerAnimations 클래스를 만들고, 그 안에 각 동작별 SpriteAnimation을 멤버 변수로 저장합니다. 필요할 때마다 꺼내서 사용하는 방식입니다.

이렇게 하면 코드가 체계적으로 정리됩니다. 하지만 주의할 점도 있습니다.

초보 개발자들이 흔히 하는 실수 중 하나는 stepTime을 너무 작게 설정하는 것입니다. 예를 들어 0.01초로 하면 애니메이션이 너무 빨라서 제대로 보이지 않습니다.

또한 프레임이 빠르게 전환되면 불필요한 연산이 늘어나 성능이 떨어집니다. 따라서 적절한 속도를 찾는 것이 중요합니다.

보통 0.05~0.15초 사이가 적당합니다. 또 다른 실수는 amount를 잘못 세는 것입니다.

스프라이트 시트에 프레임이 8개인데 amount를 7로 설정하면 마지막 프레임이 재생되지 않습니다. 반대로 9로 설정하면 빈 공간을 읽어서 오류가 발생합니다.

스프라이트 시트를 만들 때 프레임 개수를 정확히 세어두는 습관을 들이세요. 다시 김개발 씨의 이야기로 돌아가 봅시다.

박시니어 씨의 코드를 따라 작성한 김개발 씨는 신기한 표정을 지었습니다. "와, 이렇게 간단하게 애니메이션을 만들 수 있다니!" SpriteAnimation을 제대로 이해하면 게임에 생동감을 불어넣을 수 있습니다.

여러분도 오늘 배운 내용을 실제 프로젝트에 적용해 보세요.

실전 팁

💡 - stepTime은 0.1초 전후가 가장 자연스럽습니다

  • 복잡한 스프라이트 시트는 SpriteAnimationData.variable로 프레임별 시간을 다르게 설정할 수 있습니다

3. SpriteAnimationComponent 사용

김개발 씨는 SpriteAnimation 객체를 성공적으로 만들었습니다. 하지만 화면에는 여전히 아무것도 나타나지 않았습니다.

"애니메이션을 만들었는데 왜 안 보이는 거죠?" 박시니어 씨가 설명했습니다. "화면에 표시하려면 Component가 필요합니다."

SpriteAnimationComponent는 SpriteAnimation을 화면에 렌더링하는 컴포넌트입니다. Flame 게임 엔진의 기본 단위인 Component를 상속받아 만들어졌습니다.

위치, 크기, 각도 등을 설정할 수 있으며 게임 월드에 추가하면 자동으로 애니메이션이 재생됩니다. 게임의 캐릭터나 오브젝트를 만들 때 가장 많이 사용하는 컴포넌트입니다.

다음 코드를 살펴봅시다.

import 'package:flame/game.dart';
import 'package:flame/components.dart';

class WalkingCharacter extends SpriteAnimationComponent {
  WalkingCharacter() : super(
    // size를 설정하면 화면에서 차지할 크기가 결정됨
    size: Vector2(128, 128),
    // anchor를 center로 하면 위치가 중심 기준이 됨
    anchor: Anchor.center,
  );

  @override
  Future<void> onLoad() async {
    // 애니메이션 로딩 및 설정
    animation = await createWalkAnimation();
    position = Vector2(200, 300);  // 화면 위치 지정
  }
}

// 게임에 추가하기
final character = WalkingCharacter();
await game.add(character);  // 자동으로 애니메이션 재생됨

김개발 씨는 고민에 빠졌습니다. SpriteAnimation을 만드는 데는 성공했지만, 화면에는 아무것도 나타나지 않았습니다.

"분명히 코드는 맞게 작성한 것 같은데..." 콘솔 창에는 오류도 없었습니다. 뭔가 빠뜨린 것 같았습니다.

박시니어 씨가 다가와 화면을 살펴봤습니다. "아, SpriteAnimation만 만들고 화면에 추가하지 않았네요.

SpriteAnimationComponent를 사용해야 합니다." 그렇다면 SpriteAnimationComponent란 정확히 무엇일까요? 쉽게 비유하자면, SpriteAnimationComponent는 액자와 같습니다.

아무리 멋진 그림을 그렸어도 액자에 넣어 벽에 걸어야 다른 사람들이 볼 수 있습니다. SpriteAnimation은 그림이고, SpriteAnimationComponent는 액자입니다.

이 액자를 게임 월드라는 벽에 거는 것이 add 메서드입니다. 액자에는 그림의 위치, 크기, 기울기 등을 조절하는 기능도 있습니다.

SpriteAnimationComponent가 없던 시절에는 어땠을까요? 개발자들은 렌더링 로직을 직접 작성해야 했습니다.

게임 루프의 update 메서드에서 시간을 계산하고, render 메서드에서 canvas에 직접 그려야 했습니다. 위치를 옮기거나 회전시키려면 행렬 변환 코드를 손수 만들었습니다.

캐릭터가 10개만 되어도 코드가 수백 줄로 늘어났습니다. 더 큰 문제는 충돌 감지계층 구조 같은 기능을 추가하기가 매우 어려웠다는 점입니다.

바로 이런 문제를 해결하기 위해 SpriteAnimationComponent가 등장했습니다. SpriteAnimationComponent를 사용하면 복잡한 렌더링 로직을 신경 쓸 필요가 없습니다.

위치, 크기, 각도만 설정하면 Flame이 알아서 화면에 그려줍니다. 또한 onLoad, update, onRemove 같은 라이프사이클 메서드를 제공해서 컴포넌트를 쉽게 관리할 수 있습니다.

무엇보다 재사용이 쉽다는 큰 이점이 있습니다. 한번 만들어두면 여러 곳에서 사용할 수 있습니다.

위의 코드를 한 줄씩 살펴보겠습니다. 먼저 SpriteAnimationComponent를 상속받는 클래스를 만듭니다.

WalkingCharacter라는 이름으로 우리만의 캐릭터 컴포넌트를 정의합니다. 생성자에서 size를 설정하면 화면에서 캐릭터가 차지할 크기가 결정됩니다.

스프라이트 시트의 원본 크기가 64x64라도, size를 128x128로 설정하면 2배로 확대됩니다. anchor는 컴포넌트의 기준점입니다.

Anchor.center로 설정하면 position이 중심점이 됩니다. 예를 들어 position을 (200, 300)으로 하면 캐릭터의 중심이 그 좌표에 위치합니다.

Anchor.topLeft로 하면 왼쪽 위 모서리가 기준점이 됩니다. onLoad 메서드는 컴포넌트가 게임에 추가될 때 자동으로 호출됩니다.

이곳에서 애니메이션을 로딩하고 초기 설정을 합니다. animation 변수에 SpriteAnimation을 할당하면 자동으로 재생이 시작됩니다.

position으로 화면상의 위치를 지정합니다. 마지막으로 game.add로 게임 월드에 추가하면 화면에 나타납니다.

실제 현업에서는 어떻게 활용할까요? 대부분의 게임에서는 캐릭터 클래스를 SpriteAnimationComponent로 만듭니다.

Player, Enemy, NPC 같은 클래스들이 모두 SpriteAnimationComponent를 상속받습니다. 그리고 각 클래스마다 고유한 로직을 추가합니다.

예를 들어 Player 클래스에는 키보드 입력 처리를, Enemy 클래스에는 AI 로직을 넣습니다. 이렇게 하면 코드가 객체 지향적으로 깔끔하게 정리됩니다.

하지만 주의할 점도 있습니다. 초보 개발자들이 흔히 하는 실수 중 하나는 onLoad를 async로 만들지 않는 것입니다.

애니메이션 로딩은 비동기 작업이므로 onLoad 앞에 반드시 async를 붙여야 합니다. 그렇지 않으면 애니메이션이 로딩되기 전에 렌더링이 시작되어 오류가 발생합니다.

따라서 Future<void> onLoad() async 형태로 작성해야 합니다. 또 다른 실수는 size를 너무 크게 설정하는 것입니다.

원본 이미지가 64x64인데 size를 1024x1024로 하면 이미지가 뭉개져서 보기 흉해집니다. 적절한 배율을 유지하는 것이 중요합니다.

보통 원본의 1~3배 정도가 적당합니다. 다시 김개발 씨의 이야기로 돌아가 봅시다.

박시니어 씨의 설명대로 코드를 수정한 김개발 씨는 탄성을 질렀습니다. "와!

드디어 캐릭터가 화면에서 걷고 있어요!" SpriteAnimationComponent를 제대로 이해하면 게임 오브젝트를 쉽게 만들고 관리할 수 있습니다. 여러분도 오늘 배운 내용을 실제 프로젝트에 적용해 보세요.

실전 팁

💡 - anchor는 Anchor.center를 가장 많이 사용합니다 (위치 계산이 직관적)

  • onLoad에서 await을 빼먹으면 애니메이션이 제대로 로딩되지 않으니 주의하세요

4. 애니메이션 속도 조절

김개발 씨의 캐릭터가 화면에서 걷기 시작했지만, 뭔가 어색했습니다. 너무 빠르게 움직여서 마치 달리는 것처럼 보였습니다.

"애니메이션 속도를 조절할 수 있을까요?" 박시니어 씨가 미소 지으며 대답했습니다. "물론이죠.

여러 가지 방법이 있습니다."

애니메이션 속도 조절은 stepTime을 변경하거나 timeScale을 사용해서 할 수 있습니다. stepTime은 각 프레임이 표시되는 시간을 직접 지정하는 방식이고, timeScale은 재생 속도 배율을 조절하는 방식입니다.

실시간으로 속도를 바꿀 수도 있어서 캐릭터가 달릴 때는 빠르게, 걸을 때는 느리게 만들 수 있습니다. 게임의 느낌을 결정하는 중요한 요소입니다.

다음 코드를 살펴봅시다.

import 'package:flame/game.dart';
import 'package:flame/components.dart';

class AdjustableCharacter extends SpriteAnimationComponent {
  @override
  Future<void> onLoad() async {
    // stepTime을 0.1초로 설정 (기본 속도)
    animation = await createAnimationWithSpeed(0.1);
    size = Vector2(128, 128);
    position = Vector2(200, 300);
  }

  // 걷기 → 달리기로 전환
  void startRunning() {
    // timeScale을 2.0으로 하면 2배 빠르게 재생
    animation?.timeScale = 2.0;
  }

  // 달리기 → 걷기로 전환
  void startWalking() {
    animation?.timeScale = 1.0;  // 원래 속도로 복귀
  }
}

김개발 씨의 캐릭터가 드디어 화면에서 걷기 시작했습니다. 하지만 뭔가 이상했습니다.

걷는 것이 아니라 마치 경주를 하는 것처럼 빠르게 움직였습니다. 테스트해보니 너무 어색해서 게임 느낌이 살지 않았습니다.

"이 속도를 조절할 수 있을까요?" 박시니어 씨가 노트북을 가리키며 설명했습니다. "Flame은 애니메이션 속도를 조절하는 두 가지 방법을 제공합니다.

stepTimetimeScale이에요." 그렇다면 애니메이션 속도 조절은 정확히 어떻게 하는 걸까요? 쉽게 비유하자면, stepTime은 메트로놈의 속도를 설정하는 것과 같습니다.

피아노를 연습할 때 메트로놈을 느리게 틀면 천천히 연주하게 됩니다. 빠르게 틀면 빠르게 연주하게 됩니다.

stepTime도 마찬가지입니다. 0.1초로 설정하면 각 프레임이 0.1초씩 표시됩니다.

0.05초로 줄이면 2배 빠르게 재생됩니다. 반면 timeScale은 음악 재생 속도 조절과 같습니다.

유튜브에서 영상 재생 속도를 1.5배로 올리거나 0.5배로 줄이는 기능을 써본 적 있나요? timeScale도 똑같은 원리입니다.

1.0이 기본 속도이고, 2.0으로 하면 2배 빠르게, 0.5로 하면 절반 속도로 재생됩니다. 애니메이션 속도 조절이 없던 시절에는 어땠을까요?

개발자들은 각 상황마다 별도의 애니메이션을 만들어야 했습니다. 걷기용 애니메이션 하나, 달리기용 애니메이션 하나.

같은 스프라이트 시트를 사용하지만 stepTime만 다르게 설정한 SpriteAnimation 객체를 여러 개 만들었습니다. 메모리도 낭비되고, 코드도 중복되었습니다.

더 큰 문제는 실시간 속도 변경이 어려웠다는 점입니다. 캐릭터가 걷다가 달리기로 전환할 때 애니메이션 객체 자체를 바꿔야 했습니다.

바로 이런 문제를 해결하기 위해 timeScale이 등장했습니다. timeScale을 사용하면 하나의 애니메이션으로 여러 속도를 표현할 수 있습니다.

걷기와 달리기를 같은 애니메이션으로 처리하되, timeScale만 조절하면 됩니다. 또한 실시간으로 부드럽게 전환할 수 있습니다.

무엇보다 메모리를 절약할 수 있다는 큰 이점이 있습니다. 한 개의 애니메이션 객체만 유지하면 되니까요.

위의 코드를 한 줄씩 살펴보겠습니다. 먼저 onLoad에서 기본 속도의 애니메이션을 생성합니다.

stepTime을 0.1초로 설정해서 적당한 걷기 속도를 만듭니다. 다음으로 startRunning 메서드를 보면 animation?.timeScale = 2.0으로 설정합니다.

이렇게 하면 애니메이션이 2배 빠르게 재생됩니다. 같은 걷기 애니메이션이지만 빠르게 재생되면 달리는 것처럼 보입니다.

startWalking 메서드에서는 timeScale을 1.0으로 돌려놓습니다. 원래 속도로 복귀하는 것입니다.

이렇게 timeScale을 조절하면 게임 도중에도 자유롭게 속도를 바꿀 수 있습니다. 물음표 연산자를 사용한 이유는 animation이 null일 수도 있기 때문입니다.

안전한 코딩 습관입니다. 실제 현업에서는 어떻게 활용할까요?

많은 게임에서 상태에 따른 속도 조절을 사용합니다. 예를 들어 캐릭터가 독에 중독되면 애니메이션을 느리게 재생해서 둔해 보이게 만듭니다.

반대로 강화 아이템을 먹으면 빠르게 재생해서 민첩해 보이게 합니다. 슬로우 모션 효과도 timeScale로 구현합니다.

결정타를 날릴 때 0.3배 속도로 줄이면 드라마틱한 연출이 됩니다. 하지만 주의할 점도 있습니다.

초보 개발자들이 흔히 하는 실수 중 하나는 stepTime을 너무 작게 설정하는 것입니다. 0.01초 같은 극단적인 값은 눈으로 구분하기도 어렵고 성능에도 부담을 줍니다.

보통 0.05~0.2초 범위가 적절합니다. 또한 timeScale을 음수로 설정하면 오류가 발생할 수 있으니 주의하세요.

역재생을 하려면 다른 방법을 사용해야 합니다. 또 다른 주의점은 timeScale과 게임 속도를 혼동하는 것입니다.

timeScale은 애니메이션 재생 속도만 바꿉니다. 캐릭터의 실제 이동 속도는 별도로 조절해야 합니다.

애니메이션은 빠른데 이동이 느리면 발이 헛돌아 보입니다. 두 속도를 동기화하는 것이 중요합니다.

다시 김개발 씨의 이야기로 돌아가 봅시다. timeScale을 조절한 김개발 씨는 만족스러운 표정을 지었습니다.

"이제 걷는 게 자연스러워 보여요!" 애니메이션 속도 조절을 제대로 이해하면 게임에 생동감과 다양성을 더할 수 있습니다. 여러분도 오늘 배운 내용을 실제 프로젝트에 적용해 보세요.

실전 팁

💡 - 걷기는 stepTime 0.1초, 달리기는 timeScale 2.0이 자연스럽습니다

  • 슬로우 모션은 0.3~0.5 정도가 가장 드라마틱합니다

5. 여러 애니메이션 전환

김개발 씨의 캐릭터가 이제 자연스럽게 걷기 시작했습니다. 하지만 한 가지 문제가 있었습니다.

캐릭터가 멈춰도 계속 걷는 애니메이션이 재생되었습니다. "서 있을 때는 idle 애니메이션을 보여주고 싶은데요." 박시니어 씨가 고개를 끄덕였습니다.

"애니메이션 전환을 배울 시간이군요."

여러 애니메이션 전환은 캐릭터의 상태에 따라 다른 애니메이션을 재생하는 기법입니다. idle, walk, run, jump, attack 등 각 동작마다 별도의 애니메이션을 준비하고 상황에 맞게 전환합니다.

SpriteAnimationGroupComponent를 사용하면 여러 애니메이션을 쉽게 관리하고 전환할 수 있습니다. 게임 캐릭터를 사실적으로 만드는 핵심 기술입니다.

다음 코드를 살펴봅시다.

import 'package:flame/game.dart';
import 'package:flame/components.dart';

// 캐릭터 상태 정의
enum CharacterState { idle, walk, run }

class MultiAnimationCharacter extends SpriteAnimationGroupComponent<CharacterState> {
  @override
  Future<void> onLoad() async {
    // 각 상태별 애니메이션 생성
    final idleAnimation = await createIdleAnimation();
    final walkAnimation = await createWalkAnimation();
    final runAnimation = await createRunAnimation();

    // animations 맵에 등록
    animations = {
      CharacterState.idle: idleAnimation,
      CharacterState.walk: walkAnimation,
      CharacterState.run: runAnimation,
    };

    current = CharacterState.idle;  // 초기 상태는 idle
    size = Vector2(128, 128);
  }

  // 상태 변경 메서드
  void changeState(CharacterState newState) {
    current = newState;  // 자동으로 애니메이션 전환됨
  }
}

김개발 씨는 캐릭터가 걷는 모습을 보며 뿌듯해했습니다. 하지만 곧 이상한 점을 발견했습니다.

캐릭터를 멈추게 했는데도 발은 계속 움직이고 있었습니다. 마치 제자리 걷기를 하는 것처럼 보였습니다.

"이건 좀 어색한데요?" 박시니어 씨가 웃으며 설명했습니다. "지금은 걷기 애니메이션만 있잖아요.

서 있을 때는 idle 애니메이션이 필요합니다. 그리고 상황에 따라 애니메이션을 전환할 수 있어야 해요." 그렇다면 여러 애니메이션 전환은 정확히 어떻게 하는 걸까요?

쉽게 비유하자면, 애니메이션 전환은 옷 갈아입기와 같습니다. 사람은 상황에 따라 다른 옷을 입습니다.

집에서는 편한 옷, 회사에서는 정장, 운동할 때는 운동복을 입습니다. 캐릭터도 마찬가지입니다.

서 있을 때는 idle 애니메이션, 걸을 때는 walk 애니메이션, 달릴 때는 run 애니메이션을 입습니다. 상황이 바뀌면 옷을 갈아입듯이 애니메이션을 바꿉니다.

여러 애니메이션 전환이 없던 시절에는 어땠을까요? 개발자들은 상태 관리 로직을 직접 작성해야 했습니다.

현재 상태를 변수로 추적하고, update 메서드에서 상태를 확인해서 애니메이션을 수동으로 교체했습니다. if-else 문이 길게 늘어섰습니다.

상태가 10개만 되어도 코드가 복잡해졌습니다. 더 큰 문제는 전환 타이밍을 맞추기 어려웠다는 점입니다.

애니메이션이 중간에 끊겨 보이거나 부자연스럽게 튀었습니다. 바로 이런 문제를 해결하기 위해 SpriteAnimationGroupComponent가 등장했습니다.

SpriteAnimationGroupComponent를 사용하면 여러 애니메이션을 깔끔하게 관리할 수 있습니다. 각 상태마다 애니메이션을 맵에 등록하고, current만 바꾸면 자동으로 전환됩니다.

또한 타입 안전성도 보장됩니다. enum을 사용하면 오타로 인한 버그를 방지할 수 있습니다.

무엇보다 코드가 깔끔하고 읽기 쉽다는 큰 이점이 있습니다. 위의 코드를 한 줄씩 살펴보겠습니다.

먼저 CharacterState라는 enum을 정의합니다. idle, walk, run 세 가지 상태를 만듭니다.

이렇게 enum을 사용하면 문자열 오타를 방지할 수 있습니다. 다음으로 **SpriteAnimationGroupComponent<CharacterState>**를 상속받습니다.

제네릭 타입으로 CharacterState를 지정합니다. onLoad에서 각 상태별 애니메이션을 생성합니다.

각각 다른 스프라이트 시트나 다른 프레임 범위를 사용할 수 있습니다. 그리고 animations 맵에 등록합니다.

맵의 키는 CharacterState이고 값은 SpriteAnimation입니다. current에 초기 상태를 설정하면 그 애니메이션부터 재생됩니다.

changeState 메서드는 상태를 전환하는 역할을 합니다. current만 바꿔주면 Flame이 알아서 애니메이션을 전환합니다.

별도의 복잡한 로직이 필요 없습니다. 매우 간단하고 직관적입니다.

실제 현업에서는 어떻게 활용할까요? 대부분의 게임에서는 상태 머신 패턴을 사용합니다.

캐릭터의 상태를 체계적으로 관리하기 위해서입니다. 예를 들어 idle 상태에서는 walk나 jump로만 전환 가능하고, jump 상태에서는 다시 idle로만 돌아올 수 있게 제한합니다.

이렇게 하면 이상한 상태 조합을 방지할 수 있습니다. 점프 중에 걷기 애니메이션이 재생되는 버그 같은 것 말이죠.

또한 블렌딩 효과를 추가하기도 합니다. 애니메이션 전환이 부드럽게 이어지도록 중간 프레임을 보간하는 기법입니다.

고급 게임에서는 필수적인 기능입니다. Flame에서는 아직 기본 지원하지 않지만, 커스텀 로직으로 구현할 수 있습니다.

하지만 주의할 점도 있습니다. 초보 개발자들이 흔히 하는 실수 중 하나는 너무 자주 상태를 바꾸는 것입니다.

매 프레임마다 current를 설정하면 애니메이션이 계속 처음으로 돌아가서 제대로 재생되지 않습니다. 상태가 실제로 바뀔 때만 current를 설정해야 합니다.

예를 들어 이전 상태와 다를 때만 업데이트하는 식입니다. 또 다른 실수는 애니메이션을 등록하지 않는 것입니다.

animations 맵에 없는 상태로 current를 설정하면 오류가 발생합니다. 모든 상태에 대해 애니메이션을 반드시 준비해야 합니다.

없으면 기본 애니메이션이라도 만들어두는 것이 좋습니다. 다시 김개발 씨의 이야기로 돌아가 봅시다.

박시니어 씨의 코드를 따라 작성한 김개발 씨는 감탄했습니다. "와!

캐릭터가 멈추면 idle 애니메이션이 자동으로 나오네요!" 여러 애니메이션 전환을 제대로 이해하면 캐릭터를 훨씬 생동감 있게 만들 수 있습니다. 여러분도 오늘 배운 내용을 실제 프로젝트에 적용해 보세요.

실전 팁

💡 - 상태가 실제로 바뀔 때만 current를 업데이트하세요 (매 프레임 X)

  • enum을 사용하면 오타를 방지하고 자동완성도 됩니다

6. 캐릭터 걷기 애니메이션 구현

김개발 씨는 이제 애니메이션의 기초를 모두 배웠습니다. "지금까지 배운 걸 종합해서 실제로 움직이는 캐릭터를 만들어볼까요?" 박시니어 씨가 제안했습니다.

"좋은 생각이에요. 키보드로 조작할 수 있는 캐릭터를 만들어봅시다."

캐릭터 걷기 애니메이션 구현은 지금까지 배운 모든 개념을 종합하는 실전 예제입니다. 키보드 입력을 받아 캐릭터를 이동시키고, 움직일 때는 walk 애니메이션을, 멈추면 idle 애니메이션을 재생합니다.

또한 좌우 이동 시 캐릭터를 뒤집어서 방향감을 표현합니다. 실제 게임에서 사용할 수 있는 완성도 높은 캐릭터 컨트롤러입니다.

다음 코드를 살펴봅시다.

import 'package:flame/game.dart';
import 'package:flame/components.dart';
import 'package:flame/input.dart';
import 'package:flutter/services.dart';

enum PlayerState { idle, walk }

class Player extends SpriteAnimationGroupComponent<PlayerState>
    with KeyboardHandler {
  final double moveSpeed = 200.0;  // 초당 200픽셀 이동
  Vector2 velocity = Vector2.zero();

  @override
  Future<void> onLoad() async {
    animations = {
      PlayerState.idle: await createIdleAnimation(),
      PlayerState.walk: await createWalkAnimation(),
    };
    current = PlayerState.idle;
    size = Vector2(128, 128);
    anchor = Anchor.center;
  }

  @override
  bool onKeyEvent(KeyEvent event, Set<LogicalKeyboardKey> keysPressed) {
    // 좌우 이동 처리
    velocity.x = 0;
    if (keysPressed.contains(LogicalKeyboardKey.arrowLeft)) {
      velocity.x = -moveSpeed;
      flipHorizontallyAroundCenter();  // 왼쪽 보기
    } else if (keysPressed.contains(LogicalKeyboardKey.arrowRight)) {
      velocity.x = moveSpeed;
      flipHorizontallyAroundCenter();  // 오른쪽 보기
    }

    // 애니메이션 전환
    current = velocity.x != 0 ? PlayerState.walk : PlayerState.idle;
    return true;
  }

  @override
  void update(double dt) {
    position += velocity * dt;  // 위치 업데이트
    super.update(dt);
  }
}

김개발 씨는 지금까지 배운 내용을 복습했습니다. 스프라이트 시트를 만들고, SpriteAnimation을 생성하고, SpriteAnimationComponent로 화면에 표시하고, 여러 애니메이션을 전환하는 방법까지 모두 익혔습니다.

이제 이것들을 하나로 합쳐서 진짜 게임 캐릭터를 만들 차례였습니다. 박시니어 씨가 미소 지으며 말했습니다.

"이제 마지막 단계입니다. 키보드로 조작할 수 있는 캐릭터를 만들어봅시다.

화살표 키를 누르면 캐릭터가 움직이고, 떼면 멈추는 거죠." 그렇다면 캐릭터 걷기 애니메이션 구현은 정확히 어떻게 하는 걸까요? 쉽게 비유하자면, 이것은 인형극을 하는 것과 같습니다.

인형극에서는 인형사가 줄을 당겨 인형을 움직입니다. 인형이 걸으면 걷는 동작을 보여주고, 멈추면 서 있는 모습을 보여줍니다.

우리가 만드는 캐릭터도 마찬가지입니다. 플레이어가 키보드라는 줄을 당기면 캐릭터가 반응합니다.

왼쪽 화살표를 누르면 왼쪽으로 걸어가고, 키를 떼면 멈춰 섭니다. 캐릭터 조작이 없던 게임은 상상할 수 없습니다.

게임의 본질은 상호작용입니다. 플레이어의 입력을 받아 캐릭터가 반응하고, 그 결과가 화면에 나타나는 것이 게임의 기본 루프입니다.

이 부분이 없으면 게임이 아니라 그냥 애니메이션 영상일 뿐입니다. 바로 이런 상호작용을 구현하기 위해 KeyboardHandler가 등장했습니다.

KeyboardHandler는 Flame이 제공하는 믹스인입니다. 이것을 컴포넌트에 추가하면 키보드 이벤트를 받을 수 있습니다.

onKeyEvent 메서드를 오버라이드해서 키 입력을 처리합니다. 또한 keysPressed 세트로 현재 눌린 키들을 확인할 수 있습니다.

무엇보다 Flame 엔진과 완벽하게 통합되어 있다는 큰 이점이 있습니다. 별도의 설정 없이 바로 사용할 수 있습니다.

위의 코드를 한 줄씩 살펴보겠습니다. 먼저 KeyboardHandler를 믹스인으로 추가합니다.

with 키워드를 사용합니다. 다음으로 moveSpeed를 정의합니다.

이것은 캐릭터가 초당 몇 픽셀 이동할지 결정합니다. 200.0으로 설정하면 1초에 200픽셀 움직입니다.

velocity는 현재 속도 벡터입니다. x축 방향으로만 움직인다면 velocity.x만 사용합니다.

onKeyEvent 메서드는 키 입력이 있을 때마다 호출됩니다. keysPressed.contains로 특정 키가 눌렸는지 확인합니다.

왼쪽 화살표가 눌렸다면 velocity.x를 음수로 설정합니다. 오른쪽 화살표라면 양수로 설정합니다.

flipHorizontallyAroundCenter는 스프라이트를 좌우 반전시키는 메서드입니다. 이것으로 캐릭터의 방향을 바꿀 수 있습니다.

애니메이션 전환은 삼항 연산자로 간단하게 처리합니다. velocity.x가 0이 아니면 걷는 중이므로 walk 애니메이션을, 0이면 멈춘 상태이므로 idle 애니메이션을 재생합니다.

한 줄로 깔끔하게 처리됩니다. update 메서드는 매 프레임마다 호출됩니다.

여기서 position을 업데이트합니다. velocity * dt는 현재 프레임 시간 동안 이동할 거리를 계산합니다.

dt는 delta time으로, 이전 프레임과의 시간 차이입니다. 이렇게 하면 프레임레이트와 무관하게 일정한 속도로 움직입니다.

실제 현업에서는 어떻게 활용할까요? 대부분의 2D 게임은 이런 패턴을 기본으로 사용합니다.

여기에 점프, 공격, 대시 같은 추가 동작을 넣습니다. 각 동작마다 애니메이션과 상태를 추가하면 됩니다.

또한 충돌 감지를 추가해서 벽을 통과하지 못하게 만들기도 합니다. 카메라 추적을 넣으면 캐릭터를 따라 화면이 스크롤됩니다.

이런 기능들을 하나씩 추가하면 완성도 높은 게임이 됩니다. 하지만 주의할 점도 있습니다.

초보 개발자들이 흔히 하는 실수 중 하나는 dt를 곱하지 않는 것입니다. position += velocity로만 쓰면 프레임레이트에 따라 속도가 달라집니다.

60fps에서는 빠르고 30fps에서는 느립니다. 반드시 dt를 곱해야 일정한 속도가 유지됩니다.

이것은 게임 개발의 기본 원칙입니다. 또 다른 실수는 flipHorizontally를 매 프레임 호출하는 것입니다.

이 메서드는 현재 상태를 뒤집는 토글 방식입니다. 한번 호출하면 뒤집히고, 또 호출하면 원래대로 돌아옵니다.

따라서 방향이 바뀔 때만 호출해야 합니다. 매 프레임 호출하면 캐릭터가 계속 좌우로 깜빡입니다.

좀 더 나은 방법은 이전 방향을 기억하는 것입니다. lastDirection 변수를 만들고, 현재 방향과 비교해서 다를 때만 flip을 호출합니다.

이렇게 하면 불필요한 연산을 줄일 수 있습니다. 다시 김개발 씨의 이야기로 돌아가 봅시다.

코드를 완성한 김개발 씨는 화살표 키를 눌러봤습니다. 캐릭터가 좌우로 자연스럽게 걸어갔습니다.

키를 떼자 멈춰 섰습니다. "와!

진짜 게임 캐릭터 같아요!" 박시니어 씨가 어깨를 두드려주며 말했습니다. "축하합니다.

이제 여러분만의 게임을 만들 준비가 되었어요. 여기에 점프, 공격, 아이템 같은 기능을 추가하면 완전한 게임이 됩니다." 캐릭터 걷기 애니메이션 구현을 제대로 이해하면 모든 종류의 2D 게임을 만들 수 있습니다.

여러분도 오늘 배운 내용을 실제 프로젝트에 적용해 보세요. 처음에는 어렵게 느껴질 수 있지만, 한 번 만들고 나면 계속 재사용할 수 있습니다.

행운을 빕니다!

실전 팁

💡 - position을 업데이트할 때는 반드시 dt를 곱하세요 (프레임레이트 독립성)

  • 방향 전환 시 flipHorizontally는 한 번만 호출하세요 (이전 방향과 비교)
  • moveSpeed는 200~300 정도가 2D 게임에서 자연스럽습니다

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

#Flutter#Flame#SpriteAnimation#GameDevelopment#Animation#Flutter,Flame,Game

댓글 (0)

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

함께 보면 좋은 카드 뉴스

네트워크 동기화 고급 완벽 가이드

멀티플레이어 게임 개발에서 가장 까다로운 네트워크 동기화의 핵심 기법들을 다룹니다. 클라이언트 예측부터 P2P 아키텍처까지, 실무에서 바로 적용할 수 있는 고급 기술을 술술 읽히는 스토리로 풀어냅니다.

Flutter Flame 파티클 시스템 완벽 가이드

게임에서 폭발, 연기, 불꽃 등 화려한 특수 효과를 만드는 파티클 시스템을 단계별로 배워봅니다. Flame 엔진의 ParticleSystemComponent를 활용하여 실무에서 바로 적용 가능한 예제를 다룹니다.

프로시저럴 생성으로 무한 게임 세계 만들기

게임 개발에서 매번 손으로 맵을 그리는 대신, 알고리즘으로 자동 생성하는 프로시저럴 생성 기법을 배웁니다. 펄린 노이즈부터 던전 생성, 무한 맵 시스템까지 실전 예제로 익혀봅시다.

Flame 게임 물리 엔진 완벽 가이드

Flutter 게임 엔진 Flame에서 Forge2D를 활용한 물리 시뮬레이션을 초급자도 이해할 수 있도록 실무 스토리로 풀어낸 완벽 가이드입니다. 중력, 충돌, 조인트 등 게임 물리의 핵심을 배워보세요.

Flame 게임 엔진 스프라이트와 이미지 완벽 가이드

Flutter 게임 엔진 Flame에서 이미지와 스프라이트를 다루는 방법을 실무 중심으로 배웁니다. 이미지 로딩부터 스프라이트 컴포넌트 생성, 크기 조정까지 게임 개발의 기초를 탄탄하게 익힙니다.