🤖

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

⚠️

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

이미지 로딩 중...

Flame 게임 엔진 스프라이트와 이미지 완벽 가이드 - 슬라이드 1/7
A

AI Generated

2025. 12. 30. · 0 Views

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

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


목차

  1. 이미지 에셋 추가하기
  2. Sprite 클래스 사용법
  3. SpriteComponent 생성
  4. 이미지 로딩 및 캐싱
  5. 스프라이트 크기와 위치 조정
  6. 배경 이미지 설정

1. 이미지 에셋 추가하기

게임 개발 입문 1주차, 김게임 씨는 첫 Flutter 게임 프로젝트를 시작했습니다. 캐릭터 이미지를 화면에 띄우려고 하는데, 이미지 파일을 프로젝트 어디에 넣어야 할지 막막했습니다.

선배 박플레임 씨가 웃으며 말했습니다. "게임 개발의 첫걸음은 바로 에셋 관리부터예요."

게임 개발에서 이미지 에셋 추가는 모든 비주얼 요소의 출발점입니다. Flame 엔진은 Flutter의 에셋 시스템을 그대로 활용하기 때문에, pubspec.yaml 파일에서 이미지 경로를 등록해야 합니다.

이 과정을 제대로 이해하면 수백 개의 게임 리소스도 체계적으로 관리할 수 있습니다.

다음 코드를 살펴봅시다.

# pubspec.yaml 파일 설정
flutter:
  assets:
    # 이미지 파일들을 assets/images 폴더에 배치
    - assets/images/
    - assets/images/characters/
    - assets/images/backgrounds/

# 프로젝트 구조
# project_root/
#   assets/
#     images/
#       player.png
#       enemy.png
#       background.png

김게임 씨는 Flutter로 간단한 슈팅 게임을 만들어보기로 했습니다. 디자이너로부터 받은 우주선 이미지 파일이 있는데, 이것을 어떻게 프로젝트에 넣어야 할까요?

박플레임 씨가 커피를 한 모금 마시고 설명을 시작했습니다. "게임 개발에서 가장 먼저 해야 할 일은 리소스를 정리하는 거예요.

코드보다 에셋 관리가 먼저입니다." 에셋 관리는 마치 도서관에서 책을 분류하는 것과 같습니다. 아무렇게나 쌓아두면 나중에 필요한 이미지를 찾기가 너무 어려워집니다.

특히 게임 프로젝트는 캐릭터, 배경, UI, 이펙트 등 수백 개의 이미지를 다루기 때문에 처음부터 체계적으로 관리해야 합니다. Flame 엔진은 Flutter의 에셋 시스템을 그대로 사용합니다.

따라서 Flutter 개발 경험이 있다면 익숙한 방식일 겁니다. 먼저 프로젝트 루트에 assets 폴더를 만듭니다.

그 안에 images 폴더를 만들고, 용도별로 하위 폴더를 추가합니다. characters 폴더에는 캐릭터 이미지, backgrounds 폴더에는 배경 이미지를 넣는 식입니다.

폴더 구조를 만들었다면 다음 단계는 Flutter에게 "이 폴더에 있는 파일들을 사용할 거야"라고 알려주는 것입니다. 이 작업은 pubspec.yaml 파일에서 진행됩니다.

pubspec.yaml 파일을 열어보면 flutter 섹션이 있습니다. 그 아래에 assets 항목을 추가하고, 이미지가 있는 경로를 적어줍니다.

중요한 점은 경로 끝에 슬래시를 붙이면 해당 폴더의 모든 파일이 포함된다는 것입니다. 예를 들어 "assets/images/"라고 적으면 images 폴더 안의 모든 이미지 파일을 사용할 수 있습니다.

하위 폴더까지 포함하려면 각 폴더를 따로 명시해야 합니다. 김게임 씨가 의아한 표정을 지었습니다.

"그냥 프로젝트에 파일을 넣으면 자동으로 인식되지 않나요?" 박플레임 씨가 고개를 저었습니다. "Flutter는 앱 크기를 최소화하기 위해 명시적으로 선언된 에셋만 패키징합니다.

사용하지 않는 이미지까지 앱에 포함되면 용량이 불필요하게 커지니까요." 설정을 변경한 후에는 반드시 터미널에서 "flutter pub get" 명령을 실행해야 합니다. 이 명령은 pubspec.yaml의 변경사항을 프로젝트에 반영합니다.

실무에서는 이미지 파일 이름도 중요합니다. "img1.png", "img2.png" 같은 이름 대신 "player_idle.png", "enemy_explosion.png"처럼 용도를 명확히 알 수 있는 이름을 사용하세요.

또한 게임 캐릭터는 여러 상태를 가집니다. 걷는 모습, 점프하는 모습, 공격하는 모습 등.

이런 이미지들을 "player_walk_01.png", "player_walk_02.png"처럼 일련번호를 붙여 관리하면 나중에 애니메이션 만들기가 훨씬 쉬워집니다. 김게임 씨는 선배의 조언대로 assets/images/characters 폴더를 만들고, 우주선 이미지를 player.png라는 이름으로 저장했습니다.

pubspec.yaml도 수정하고 flutter pub get을 실행했습니다. "이제 준비가 끝났네요!" 김게임 씨가 뿌듯해했습니다.

하지만 이것은 시작일 뿐입니다. 이미지를 등록했으니 이제 코드에서 불러와야 합니다.

실전 팁

💡 - 이미지 파일은 용도별로 폴더를 나눠 관리하세요 (characters, backgrounds, ui, effects 등)

  • 파일명은 snake_case를 사용하고 용도를 명확히 표현하세요 (player_idle.png, enemy_death_01.png)
  • pubspec.yaml 수정 후 반드시 flutter pub get 실행을 잊지 마세요

2. Sprite 클래스 사용법

에셋 등록을 마친 김게임 씨는 이제 이미지를 화면에 그려보고 싶었습니다. "그런데 이미지를 어떻게 불러오죠?" 박플레임 씨가 화이트보드를 가리키며 말했습니다.

"Flame의 핵심 클래스 중 하나가 바로 Sprite입니다."

Sprite는 게임에서 사용할 2D 이미지를 나타내는 클래스입니다. 단순히 이미지 파일을 불러오는 것이 아니라, 게임 엔진이 효율적으로 렌더링할 수 있도록 최적화된 형태로 관리합니다.

Sprite 객체는 이미지 데이터와 렌더링 정보를 함께 담고 있어, 화면에 그리기 위한 모든 준비가 완료된 상태입니다.

다음 코드를 살펴봅시다.

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

class MyGame extends FlameGame {
  late Sprite playerSprite;

  @override
  Future<void> onLoad() async {
    // Sprite 객체 생성 - 이미지 로딩
    playerSprite = await loadSprite('player.png');

    // Sprite를 화면에 직접 그릴 수도 있음
    // render 메서드에서 사용
  }

  @override
  void render(Canvas canvas) {
    super.render(canvas);
    // Sprite를 특정 위치에 그리기
    playerSprite.render(canvas, position: Vector2(100, 100), size: Vector2(64, 64));
  }
}

김게임 씨는 이미지를 등록했으니 이제 코드 몇 줄이면 화면에 나타날 거라고 생각했습니다. 하지만 막상 코드를 작성하려니 어디서부터 시작해야 할지 막막했습니다.

박플레임 씨가 설명을 시작했습니다. "게임 엔진에서 이미지를 다루는 방식은 일반 앱과 조금 다릅니다.

성능이 매우 중요하거든요." Sprite라는 단어는 게임 개발에서 자주 듣게 될 용어입니다. 원래 스프라이트는 '요정', '정령'이라는 뜻인데, 컴퓨터 그래픽스에서는 2D 이미지 객체를 의미하게 되었습니다.

Flame에서 Sprite는 단순히 이미지 파일을 의미하는 것이 아닙니다. 이미지를 게임 엔진이 효율적으로 처리할 수 있도록 감싼 래퍼 클래스입니다.

비유하자면, Sprite는 마치 액자에 넣은 사진과 같습니다. 사진만 있으면 어디에 걸어야 할지, 어느 방향으로 걸어야 할지 애매합니다.

하지만 액자에 넣으면 걸 위치, 크기, 방향이 명확해집니다. Sprite도 이미지에 렌더링 정보를 더한 것입니다.

Sprite 객체를 생성하는 방법은 간단합니다. FlameGame 클래스는 loadSprite 메서드를 제공합니다.

이 메서드에 이미지 파일 이름을 전달하면 Sprite 객체가 반환됩니다. 주의할 점은 loadSprite가 비동기 함수라는 것입니다.

이미지 파일을 디스크에서 읽어와 메모리에 로딩하는 작업은 시간이 걸립니다. 따라서 async/await를 사용해야 합니다.

일반적으로 게임의 onLoad 메서드에서 Sprite를 로딩합니다. onLoad는 게임이 시작될 때 한 번 호출되는 메서드로, 모든 리소스를 준비하기에 적합한 시점입니다.

김게임 씨가 질문했습니다. "Sprite 객체를 만들었으면 어떻게 화면에 그리나요?" 박플레임 씨가 코드를 가리켰습니다.

"Sprite 객체는 render 메서드를 가지고 있어요. 이 메서드에 Canvas와 위치, 크기를 전달하면 화면에 그려집니다." Canvas는 Flutter의 그리기 도구입니다.

마치 화가의 캔버스처럼, 여기에 도형이나 이미지를 그릴 수 있습니다. Sprite의 render 메서드는 이 Canvas에 이미지를 그려줍니다.

position 파라미터는 Vector2 타입으로, 화면에서의 x, y 좌표를 나타냅니다. Vector2(100, 100)은 화면의 왼쪽 위에서 오른쪽으로 100픽셀, 아래로 100픽셀 떨어진 위치입니다.

size 파라미터도 Vector2 타입으로, 이미지의 가로와 세로 크기를 픽셀 단위로 지정합니다. 원본 이미지가 512x512여도 Vector2(64, 64)로 지정하면 작게 그려집니다.

하지만 실무에서는 Sprite를 직접 render 메서드에서 그리는 경우는 드뭅니다. 대신 SpriteComponent라는 더 편리한 클래스를 사용합니다.

김게임 씨는 코드를 따라 작성해보았습니다. 게임을 실행하자 화면에 우주선 이미지가 나타났습니다.

"와, 진짜 나타났어요!" 박플레임 씨가 미소 지었습니다. "Sprite의 기본 개념을 이해했으니, 이제 더 실용적인 SpriteComponent를 배워봅시다."

실전 팁

💡 - Sprite 로딩은 항상 비동기이므로 onLoad에서 await로 처리하세요

  • Vector2는 Flame에서 위치와 크기를 나타내는 기본 타입입니다
  • 실무에서는 Sprite보다 SpriteComponent를 더 많이 사용합니다

3. SpriteComponent 생성

Sprite로 이미지를 그릴 수 있게 된 김게임 씨는 한 가지 불편함을 느꼈습니다. 캐릭터를 움직이려면 위치를 계속 업데이트해야 하는데, render 메서드에서 직접 그리는 방식은 코드가 복잡해질 것 같았습니다.

박플레임 씨가 웃으며 말했습니다. "바로 그 문제를 해결하기 위해 Component 시스템이 있습니다."

SpriteComponent는 Sprite를 게임 오브젝트로 만들어주는 컴포넌트입니다. 위치, 크기, 회전, 앵커 등의 속성을 가지며, 자동으로 화면에 렌더링됩니다.

Component 시스템 덕분에 각 게임 오브젝트를 독립적으로 관리할 수 있어, 수십 개의 캐릭터와 아이템을 쉽게 다룰 수 있습니다.

다음 코드를 살펴봅시다.

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

class Player extends SpriteComponent {
  @override
  Future<void> onLoad() async {
    // Sprite 로딩
    sprite = await Sprite.load('player.png');

    // 컴포넌트 속성 설정
    size = Vector2(64, 64); // 크기
    position = Vector2(100, 100); // 위치
    anchor = Anchor.center; // 앵커 포인트 (중심점)
  }
}

class MyGame extends FlameGame {
  @override
  Future<void> onLoad() async {
    // Player 컴포넌트를 게임에 추가
    final player = Player();
    add(player);
  }
}

김게임 씨는 우주선 하나는 그릴 수 있게 되었지만, 여러 개의 적 우주선을 관리하려니 막막했습니다. render 메서드에서 하나하나 그려야 한다면 코드가 엄청나게 길어질 것 같았습니다.

박플레임 씨가 화이트보드에 그림을 그렸습니다. "게임 개발의 핵심 패턴 중 하나가 바로 컴포넌트 시스템입니다." 컴포넌트 시스템은 마치 레고 블록과 같습니다.

각 블록은 독립적으로 존재하지만, 여러 블록을 조합하면 복잡한 구조물을 만들 수 있습니다. 게임도 마찬가지입니다.

플레이어, 적, 총알, 아이템 등을 각각 컴포넌트로 만들면 관리가 훨씬 쉬워집니다. SpriteComponent는 Flame에서 가장 많이 사용하는 컴포넌트입니다.

이름에서 알 수 있듯이 Sprite를 담는 컴포넌트입니다. SpriteComponent를 사용하려면 상속받은 클래스를 만듭니다.

예제의 Player 클래스처럼 SpriteComponent를 extends하면 됩니다. 컴포넌트에도 onLoad 메서드가 있습니다.

여기서 sprite 속성에 Sprite 객체를 할당합니다. Sprite.load는 FlameGame의 loadSprite를 더 간결하게 쓸 수 있는 정적 메서드입니다.

SpriteComponent의 장점은 다양한 속성을 제공한다는 것입니다. size로 크기를 지정하고, position으로 위치를 정하고, anchor로 중심점을 설정할 수 있습니다.

앵커는 조금 생소한 개념일 수 있습니다. 앵커는 컴포넌트의 기준점입니다.

예를 들어 position이 (100, 100)일 때, 앵커가 topLeft면 왼쪽 위 모서리가 (100, 100)에 위치합니다. 하지만 앵커가 center면 중심이 (100, 100)에 위치합니다.

김게임 씨가 고개를 갸우뚱했습니다. "왜 앵커가 필요한가요?

그냥 왼쪽 위 기준으로 통일하면 안 되나요?" 박플레임 씨가 설명했습니다. "캐릭터를 회전시킬 때를 생각해보세요.

왼쪽 위 모서리를 기준으로 회전하면 이상하게 보입니다. 하지만 중심을 기준으로 회전하면 자연스럽습니다." 실제로 게임에서 캐릭터는 대부분 center 앵커를 사용합니다.

UI 요소들은 topLeft나 bottomRight 등을 사용하기도 합니다. 컴포넌트를 만들었다면 게임에 추가해야 합니다.

FlameGame의 add 메서드를 사용하면 됩니다. add로 추가된 컴포넌트는 자동으로 업데이트되고 렌더링됩니다.

여기서 핵심은 "자동으로"입니다. 우리가 render 메서드를 오버라이드하지 않아도, Flame 엔진이 알아서 모든 컴포넌트를 화면에 그려줍니다.

김게임 씨가 눈을 반짝였습니다. "그럼 적 우주선 100개를 만들려면?" 박플레임 씨가 웃었습니다.

"반복문으로 Enemy 컴포넌트를 100개 만들어서 add하면 됩니다. 각각 독립적으로 움직이고 렌더링됩니다." 이것이 바로 컴포넌트 시스템의 강력함입니다.

코드 몇 줄로 수백 개의 오브젝트를 관리할 수 있습니다. 김게임 씨는 Player 클래스를 작성하고 게임에 추가해보았습니다.

이전과 똑같이 우주선이 화면에 나타났지만, 이번에는 코드가 훨씬 깔끔했습니다. "Component 시스템을 제대로 활용하면 게임 개발이 정말 쉬워집니다." 박플레임 씨의 말에 김게임 씨는 고개를 끄덕였습니다.

실전 팁

💡 - SpriteComponent를 상속받은 클래스로 게임 오브젝트를 만드세요

  • 앵커는 대부분 Anchor.center를 사용하지만, 상황에 따라 적절히 변경하세요
  • add 메서드로 추가된 컴포넌트는 자동으로 업데이트되고 렌더링됩니다

4. 이미지 로딩 및 캐싱

게임이 복잡해지면서 김게임 씨는 성능 문제를 겪기 시작했습니다. 적 우주선이 생성될 때마다 이미지를 새로 로딩하니 게임이 버벅거렸습니다.

박플레임 씨가 모니터를 보며 고개를 끄덕였습니다. "이미지 캐싱을 안 하셨군요.

게임 개발에서 리소스 관리는 정말 중요합니다."

Flame 엔진은 Images 클래스를 통해 이미지 캐싱 시스템을 제공합니다. 한 번 로딩한 이미지는 메모리에 보관되었다가 재사용됩니다.

같은 이미지를 여러 번 로딩하지 않아도 되므로 성능이 크게 향상됩니다. FlameGame.images를 통해 접근할 수 있으며, 게임 시작 시 필요한 모든 이미지를 미리 로딩하는 것이 모범 사례입니다.

다음 코드를 살펴봅시다.

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

class MyGame extends FlameGame {
  @override
  Future<void> onLoad() async {
    // 게임 시작 시 모든 이미지를 미리 로딩 (프리로딩)
    await images.loadAll([
      'player.png',
      'enemy.png',
      'bullet.png',
      'background.png',
    ]);

    // 이미 로딩된 이미지로 Sprite 생성 (캐시에서 가져옴)
    final playerSprite = Sprite(images.fromCache('player.png'));

    // 또는 SpriteComponent에서 사용
    add(Player());
  }
}

class Player extends SpriteComponent with HasGameRef<MyGame> {
  @override
  Future<void> onLoad() async {
    // 캐시된 이미지 사용 - 즉시 로딩됨
    sprite = Sprite(game.images.fromCache('player.png'));
    size = Vector2(64, 64);
  }
}

김게임 씨의 게임은 이제 꽤 그럴듯해졌습니다. 플레이어 우주선, 적 우주선, 총알이 화면을 오갑니다.

하지만 적이 많아지면 게임이 끊기는 현상이 발생했습니다. 박플레임 씨가 코드를 살펴보더니 문제를 찾았습니다.

"적이 생성될 때마다 Sprite.load를 호출하고 있네요. 이미지를 매번 새로 로딩하니까 느린 겁니다." 이미지 로딩은 생각보다 무거운 작업입니다.

파일을 디스크에서 읽어야 하고, 압축을 해제하고, 메모리에 올려야 합니다. 밀리초 단위로 보면 적지 않은 시간이 걸립니다.

만약 적 우주선 100개가 같은 이미지를 사용한다면, 같은 파일을 100번 로딩하는 것은 엄청난 낭비입니다. 한 번만 로딩해서 재사용하면 훨씬 효율적입니다.

이것이 바로 캐싱의 개념입니다. 캐싱은 마치 책상 위에 자주 쓰는 물건을 올려두는 것과 같습니다.

서랍에서 꺼내는 것보다 책상에서 집는 것이 훨씬 빠릅니다. Flame 엔진은 Images라는 캐싱 시스템을 제공합니다.

FlameGame 클래스는 images라는 속성을 가지고 있으며, 이것이 이미지 캐시 매니저입니다. Images 클래스의 loadAll 메서드를 사용하면 여러 이미지를 한 번에 로딩할 수 있습니다.

파일 이름 리스트를 전달하면 모든 이미지를 메모리에 올려둡니다. 이렇게 미리 로딩하는 것을 프리로딩이라고 합니다.

게임이 시작될 때 로딩 화면을 보여주면서 모든 리소스를 미리 준비해두는 것입니다. 프리로딩이 끝나면 fromCache 메서드로 이미지를 가져올 수 있습니다.

fromCache는 즉시 반환됩니다. 이미 메모리에 있는 이미지를 참조하기만 하면 되기 때문입니다.

김게임 씨가 질문했습니다. "그럼 Sprite.load는 언제 쓰나요?" 박플레임 씨가 답했습니다.

"Sprite.load는 내부적으로 이미지를 캐시에 저장합니다. 하지만 비동기이기 때문에 게임플레이 중에 호출하면 끊김이 발생할 수 있어요.

그래서 onLoad에서 미리 로딩하는 게 좋습니다." 실무에서는 보통 게임의 onLoad에서 loadAll로 모든 이미지를 프리로딩합니다. 그리고 각 컴포넌트에서는 fromCache로 캐시된 이미지를 사용합니다.

HasGameRef 믹스인을 사용하면 컴포넌트에서 game 속성으로 FlameGame 인스턴스에 접근할 수 있습니다. 따라서 game.images.fromCache()로 이미지를 가져올 수 있습니다.

주의할 점은 fromCache를 호출하기 전에 반드시 해당 이미지가 로딩되어 있어야 한다는 것입니다. 로딩되지 않은 이미지를 fromCache로 가져오려고 하면 에러가 발생합니다.

김게임 씨는 코드를 수정했습니다. 게임 시작 시 모든 이미지를 loadAll로 프리로딩하고, 적이 생성될 때는 fromCache를 사용했습니다.

게임을 실행하자 끊김 현상이 사라졌습니다. 적이 아무리 많아져도 부드럽게 작동했습니다.

"리소스 관리는 게임 개발의 기본입니다. 특히 모바일 게임에서는 메모리와 성능을 항상 고려해야 해요." 박플레임 씨의 조언에 김게임 씨는 고개를 끄덕였습니다.

실전 팁

💡 - 게임 시작 시 loadAll로 모든 이미지를 미리 로딩하세요

  • fromCache는 이미 로딩된 이미지만 사용할 수 있으므로 순서에 주의하세요
  • HasGameRef 믹스인으로 컴포넌트에서 game 인스턴스에 접근할 수 있습니다

5. 스프라이트 크기와 위치 조정

이미지를 화면에 띄우는 데 성공한 김게임 씨는 새로운 문제에 부딪혔습니다. 디자이너가 준 이미지가 너무 커서 화면을 가득 채웠습니다.

"크기를 조절하려면 어떻게 해야 하죠?" 박플레임 씨가 웃으며 답했습니다. "SpriteComponent의 다양한 속성들을 활용하면 됩니다."

SpriteComponent는 size, position, scale, angle 등 다양한 속성으로 스프라이트의 외형을 제어할 수 있습니다. size는 절대 크기를, scale은 비율을 조정하며, angle로 회전도 가능합니다.

또한 anchor 속성으로 기준점을 설정하여 정확한 배치가 가능합니다. 이런 속성들은 게임 루프 중에도 변경할 수 있어 동적인 연출이 가능합니다.

다음 코드를 살펴봅시다.

import 'package:flame/components.dart';
import 'dart:math' as math;

class Player extends SpriteComponent with HasGameRef {
  @override
  Future<void> onLoad() async {
    sprite = Sprite(game.images.fromCache('player.png'));

    // 절대 크기 지정 (픽셀 단위)
    size = Vector2(64, 64);

    // 화면 중앙에 배치
    position = game.size / 2;

    // 중심점을 기준으로 배치 및 회전
    anchor = Anchor.center;

    // 45도 회전 (라디안 단위)
    angle = math.pi / 4;

    // 크기 비율 조정 (원본의 1.5배)
    scale = Vector2.all(1.5);
  }

  @override
  void update(double dt) {
    // 매 프레임마다 회전 (초당 1바퀴)
    angle += math.pi * 2 * dt;
  }
}

김게임 씨가 받은 우주선 이미지는 고해상도였습니다. 1024x1024 픽셀이나 되어서 화면 전체를 차지했습니다.

작은 모바일 화면에서는 너무 컸습니다. 박플레임 씨가 화이트보드에 벡터를 그렸습니다.

"게임 개발에서는 위치, 크기, 속도 등을 Vector2로 표현합니다." Vector2는 2D 벡터를 나타내는 클래스입니다. x와 y 두 개의 값을 가지고 있습니다.

수학 시간에 배운 그 벡터가 맞습니다. 게임 개발에서는 벡터를 정말 많이 사용합니다.

SpriteComponent의 size 속성은 Vector2 타입입니다. Vector2(64, 64)로 설정하면 원본 이미지 크기와 상관없이 64x64 픽셀로 그려집니다.

position도 Vector2입니다. 화면에서의 x, y 좌표를 나타냅니다.

예를 들어 position = Vector2(100, 200)이면 화면의 왼쪽 위에서 오른쪽으로 100픽셀, 아래로 200픽셀 떨어진 곳에 위치합니다. 화면 중앙에 배치하고 싶다면 어떻게 할까요?

game.size는 게임 화면의 크기를 나타내는 Vector2입니다. 이것을 2로 나누면 중앙 좌표가 됩니다.

하지만 더 간단한 방법이 있습니다. game.size / 2로 중앙 위치를 계산할 수 있습니다.

Vector2는 연산자 오버로딩을 지원하여 벡터끼리 더하고 빼고 곱하고 나눌 수 있습니다. 김게임 씨가 궁금해했습니다.

"그런데 중앙에 배치했는데 왼쪽 위로 치우쳐 보여요." 박플레임 씨가 웃었습니다. "앵커를 center로 설정하지 않아서 그래요." 기본적으로 position은 컴포넌트의 왼쪽 위 모서리 위치입니다.

앵커가 topLeft이기 때문입니다. 앵커를 center로 바꾸면 position이 컴포넌트의 중심 위치가 됩니다.

angle 속성으로 회전도 가능합니다. 하지만 주의할 점은 각도 단위가 도(degree)가 아니라 **라디안(radian)**이라는 것입니다.

수학에서 각도를 표현하는 또 다른 방법입니다. 90도는 π/2 라디안, 180도는 π 라디안, 360도는 2π 라디안입니다.

Dart에서는 math.pi로 π 값을 사용할 수 있습니다. scale 속성은 크기 비율을 조정합니다.

Vector2(1.5, 1.5)로 설정하면 원본 크기의 1.5배가 됩니다. Vector2.all(1.5)는 x와 y 모두 1.5로 설정하는 편리한 생성자입니다.

scale과 size의 차이는 무엇일까요? size는 절대 크기이고, scale은 상대 비율입니다.

둘을 함께 사용하면 size를 먼저 적용하고 scale을 곱합니다. 이런 속성들은 onLoad에서만 설정하는 것이 아닙니다.

update 메서드에서 매 프레임마다 변경할 수도 있습니다. update는 게임 루프에서 매 프레임마다 호출되는 메서드입니다.

dt 파라미터는 delta time으로, 이전 프레임과의 시간 차이를 초 단위로 나타냅니다. 예를 들어 angle += math.pi * 2 * dt는 "1초에 360도(2π 라디안) 회전"을 의미합니다.

dt가 1/60이면 1프레임에 6도씩 회전합니다. 김게임 씨는 코드를 수정했습니다.

우주선이 화면 중앙에 적절한 크기로 나타났고, 천천히 회전하기 시작했습니다. "이제 게임 오브젝트를 자유자재로 다룰 수 있게 되었네요." 박플레임 씨의 말에 김게임 씨는 뿌듯해했습니다.

실무에서는 적절한 크기와 위치 설정이 게임의 느낌을 크게 좌우합니다. 너무 크거나 작으면 이상해 보이고, 배치가 어색하면 게임이 불편합니다.

실전 팁

💡 - Vector2는 게임 개발의 기본 타입이므로 익숙해지세요

  • 각도는 라디안 단위이며, math.pi를 활용하세요
  • update 메서드에서 속성을 변경하면 동적인 움직임을 만들 수 있습니다

6. 배경 이미지 설정

게임이 점점 완성되어 가는데 김게임 씨는 한 가지 아쉬운 점이 있었습니다. 검은 배경이 너무 단조로웠습니다.

"우주 배경 이미지를 넣고 싶은데, 화면 전체에 어떻게 깔죠?" 박플레임 씨가 모니터를 가리키며 설명했습니다. "배경 이미지는 특별한 방식으로 처리해야 합니다."

게임 배경은 화면 전체를 채우는 큰 이미지입니다. SpriteComponent를 사용하되, size를 게임 화면 크기로 설정하고, 우선순위를 조정하여 다른 오브젝트보다 뒤에 그려지도록 해야 합니다.

priority 속성으로 렌더링 순서를 제어할 수 있으며, 배경은 음수 값을 주어 가장 먼저 그려지게 합니다.

다음 코드를 살펴봅시다.

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

class Background extends SpriteComponent with HasGameRef {
  @override
  Future<void> onLoad() async {
    sprite = Sprite(game.images.fromCache('background.png'));

    // 화면 전체 크기로 설정
    size = game.size;

    // 위치는 왼쪽 위 (0, 0)
    position = Vector2.zero();

    // 렌더링 우선순위 - 음수일수록 먼저 그려짐
    priority = -1;
  }
}

class MyGame extends FlameGame {
  @override
  Future<void> onLoad() async {
    await images.loadAll(['background.png', 'player.png']);

    // 배경을 가장 먼저 추가
    add(Background());

    // 그 다음 플레이어 추가
    add(Player());
  }
}

김게임 씨의 게임은 이제 플레이어 우주선, 적, 총알이 모두 작동했습니다. 하지만 검은 배경이 너무 심심했습니다.

디자이너가 멋진 우주 배경 이미지를 줬는데, 어떻게 화면에 깔아야 할까요? 박플레임 씨가 설명을 시작했습니다.

"배경 이미지는 일반 스프라이트와 조금 다르게 처리해야 합니다." 배경은 화면 전체를 덮어야 합니다. 일부만 보이면 어색합니다.

따라서 size를 game.size로 설정해야 합니다. game.size는 게임 화면의 가로와 세로 크기를 나타내는 Vector2입니다.

position은 Vector2.zero()로 설정합니다. Vector2.zero()는 Vector2(0, 0)과 같습니다.

즉, 화면의 왼쪽 위 구석부터 시작하는 것입니다. 여기서 중요한 것이 렌더링 순서입니다.

Flame은 컴포넌트를 추가된 순서대로 그립니다. 나중에 추가된 것이 위에 그려집니다.

마치 종이에 그림을 그리는 것과 같습니다. 먼저 그린 것 위에 나중에 그린 것이 덮입니다.

배경을 마지막에 추가하면 모든 게임 오브젝트를 덮어버립니다. 따라서 배경은 가장 먼저 추가해야 합니다.

onLoad에서 add(Background())를 제일 처음 호출하면 됩니다. 하지만 더 확실한 방법이 있습니다.

바로 priority 속성입니다. priority는 렌더링 우선순위를 나타내는 정수입니다.

숫자가 낮을수록 먼저 그려집니다. 기본값은 0입니다.

배경의 priority를 -1로 설정하면 다른 모든 컴포넌트보다 먼저 그려집니다. 플레이어나 적의 priority는 0이므로 배경 위에 그려집니다.

김게임 씨가 질문했습니다. "UI 요소는 맨 위에 그려져야 하잖아요.

그럼 priority를 양수로 주면 되나요?" 박플레임 씨가 고개를 끄덕였습니다. "정확합니다.

점수 표시나 버튼 같은 UI는 priority를 10이나 100처럼 큰 값으로 주면 항상 맨 위에 그려집니다." 실무에서는 배경, 게임 오브젝트, UI를 priority로 명확히 구분합니다. 배경은 -10, 일반 오브젝트는 0, UI는 100처럼 여유있게 간격을 둡니다.

배경 이미지가 화면 비율과 다를 수도 있습니다. 이미지는 16:9인데 화면은 4:3일 수도 있습니다.

이럴 때는 srcSizesrcPosition으로 이미지의 일부만 잘라서 사용할 수도 있습니다. 하지만 대부분의 경우 디자이너가 화면 비율에 맞는 이미지를 제공합니다.

그냥 size를 game.size로 설정하면 자동으로 늘어나거나 줄어들어 화면을 채웁니다. 김게임 씨는 Background 컴포넌트를 만들고 게임에 추가했습니다.

멋진 우주 배경이 화면을 가득 채웠고, 우주선들이 그 위에서 날아다녔습니다. "이제 진짜 게임 같아요!" 김게임 씨가 감탄했습니다.

박플레임 씨가 미소 지었습니다. "스프라이트와 이미지의 기본을 마스터했네요.

이제 애니메이션과 스프라이트 시트를 배워봅시다." 배경 이미지는 게임의 분위기를 결정하는 중요한 요소입니다. 적절한 배경은 플레이어를 게임 세계에 몰입하게 만듭니다.

실전 팁

💡 - 배경은 priority를 음수로 설정하여 다른 오브젝트보다 먼저 그려지게 하세요

  • size를 game.size로 설정하면 화면 전체를 채울 수 있습니다
  • UI 요소는 priority를 큰 양수로 주어 항상 맨 위에 표시되게 하세요

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

#Flutter#Flame#Sprite#SpriteComponent#GameDevelopment#Flutter,Flame,Game

댓글 (0)

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

함께 보면 좋은 카드 뉴스

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

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

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

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

Flame 고급 물리 시뮬레이션 완벽 가이드

Flutter Flame 엔진의 고급 물리 시뮬레이션 기법을 다룹니다. 유체, 소프트 바디, 래그돌부터 파괴 효과와 물리 최적화까지, 실전에서 바로 사용할 수 있는 고급 물리 엔진 활용법을 초급자도 이해할 수 있도록 쉽게 풀어냈습니다.

타일맵 시스템 완벽 가이드

Flame 게임 엔진에서 타일맵을 활용하여 효율적으로 게임 월드를 구성하는 방법을 배웁니다. Tiled 에디터부터 충돌 처리, 동적 타일 변경까지 실무에서 바로 활용할 수 있는 내용을 다룹니다.

게임 루프와 컴포넌트 완벽 가이드

Flame 게임 엔진의 핵심인 게임 루프와 컴포넌트 시스템을 이해하고, 실전에서 활용하는 방법을 배웁니다. 초급 개발자도 쉽게 따라할 수 있도록 실무 예제와 함께 설명합니다.