본 콘텐츠의 이미지 및 내용은 AI로 생성되었습니다.
본 콘텐츠의 이미지 및 내용을 무단으로 복제, 배포, 수정하여 사용할 경우 저작권법에 의해 법적 제재를 받을 수 있습니다.
이미지 로딩 중...
AI Generated
2026. 2. 2. · 3 Views
Flutter Flame 게임 UI와 오버레이 완벽 가이드
Flame 게임 엔진에서 오버레이 시스템을 활용해 메뉴, 점수판, 일시정지 화면, 게임 오버 화면 등 게임 UI를 구현하는 방법을 배웁니다. Flutter 위젯과 Flame 게임을 자연스럽게 통합하는 실무 테크닉을 익힐 수 있습니다.
목차
1. Overlay 개념
김개발 씨는 첫 번째 Flame 게임을 완성했습니다. 캐릭터도 움직이고, 적도 나타나고, 점수도 올라갑니다.
그런데 한 가지 고민이 생겼습니다. "게임 시작 버튼은 어디에 넣지?
일시정지 메뉴는 어떻게 만들지?" 게임 로직은 잘 돌아가는데, 정작 사용자가 조작할 UI가 없었던 것입니다.
Overlay는 Flame 게임 화면 위에 Flutter 위젯을 얹어주는 시스템입니다. 마치 투명한 유리판 위에 스티커를 붙이는 것처럼, 게임 캔버스 위에 버튼, 텍스트, 메뉴 같은 UI 요소를 자유롭게 배치할 수 있습니다.
이를 통해 Flame의 강력한 게임 렌더링과 Flutter의 풍부한 위젯 생태계를 동시에 활용할 수 있습니다.
다음 코드를 살펴봅시다.
// Flame 게임 클래스에서 오버레이 정의
class MyGame extends FlameGame {
@override
Future<void> onLoad() async {
// 게임 로드 완료 후 메인 메뉴 오버레이 표시
overlays.add('MainMenu');
}
void startGame() {
// 메뉴를 숨기고 게임 시작
overlays.remove('MainMenu');
resumeEngine();
}
void showPauseMenu() {
// 게임 일시정지 후 메뉴 표시
pauseEngine();
overlays.add('PauseMenu');
}
}
김개발 씨는 입사 3개월 차 주니어 개발자입니다. 회사에서 간단한 모바일 게임 프로젝트를 맡게 되었고, Flame 엔진을 선택해 열심히 게임 로직을 구현했습니다.
캐릭터가 점프하고, 코인을 먹으면 점수가 올라가고, 장애물에 부딪히면 게임이 끝나는 기본 구조는 완성되었습니다. 그런데 막상 테스트를 해보니 뭔가 이상했습니다.
앱을 실행하자마자 게임이 바로 시작되어 버렸습니다. 시작 버튼도 없고, 점수가 화면 어디에 표시되는지도 알 수 없었습니다.
게임이 끝나도 다시 시작할 방법이 없었습니다. 선배 개발자 박시니어 씨가 다가와 화면을 살펴봅니다.
"아, 오버레이를 안 썼구나. Flame에서 UI를 만들려면 오버레이 시스템을 알아야 해요." 그렇다면 오버레이란 정확히 무엇일까요?
쉽게 비유하자면, 오버레이는 마치 TV 화면 위에 붙이는 투명 필름과 같습니다. TV에서는 영화가 재생되고 있고, 투명 필름 위에는 "현재 시각"이나 "채널 번호" 같은 정보가 적혀 있습니다.
영화 내용과 정보 표시가 서로 독립적으로 존재하면서도, 시청자에게는 하나의 화면으로 보이는 것입니다. Flame의 오버레이도 마찬가지입니다.
게임 캔버스에서는 캐릭터와 배경이 렌더링되고, 그 위의 오버레이 레이어에서는 Flutter 위젯으로 만든 UI가 표시됩니다. 두 레이어가 겹쳐져 하나의 완성된 게임 화면을 만들어냅니다.
오버레이가 없던 시절에는 어땠을까요? 개발자들은 게임 캔버스 안에서 직접 UI를 그려야 했습니다.
버튼 하나를 만들려면 사각형을 그리고, 텍스트를 배치하고, 터치 영역을 계산해서 클릭 이벤트를 처리해야 했습니다. 코드가 복잡해지고, 실수하기도 쉬웠습니다.
더 큰 문제는 Flutter의 풍부한 위젯들을 전혀 활용할 수 없다는 점이었습니다. Material 디자인 버튼, 애니메이션, 폼 입력 같은 것들을 처음부터 다시 만들어야 했습니다.
바로 이런 문제를 해결하기 위해 Flame의 오버레이 시스템이 등장했습니다. 오버레이를 사용하면 Flutter의 모든 위젯을 게임 UI로 활용할 수 있습니다.
ElevatedButton, TextField, Dialog 같은 익숙한 위젯들을 그대로 쓸 수 있습니다. 또한 게임 로직과 UI 로직이 깔끔하게 분리됩니다.
무엇보다 핫 리로드가 가능해서 UI를 빠르게 수정하고 테스트할 수 있다는 큰 이점이 있습니다. 위의 코드를 한 줄씩 살펴보겠습니다.
먼저 overlays.add('MainMenu') 부분을 보면 'MainMenu'라는 이름의 오버레이를 화면에 추가합니다. 이 이름은 나중에 GameWidget에서 실제 위젯과 연결됩니다.
overlays.remove('MainMenu') 에서는 표시된 오버레이를 화면에서 제거합니다. **pauseEngine()**과 **resumeEngine()**은 게임 루프를 멈추거나 재개하는 메서드로, 오버레이와 함께 사용하면 일시정지 기능을 구현할 수 있습니다.
실제 현업에서는 어떻게 활용할까요? 예를 들어 캐주얼 게임을 개발한다고 가정해봅시다.
게임 시작 시 메인 메뉴 오버레이를 표시하고, 플레이 버튼을 누르면 메뉴를 숨기고 게임을 시작합니다. 게임 중에는 점수 오버레이만 표시하고, 일시정지 버튼을 누르면 일시정지 메뉴 오버레이를 추가합니다.
게임 오버 시에는 결과 화면 오버레이를 보여줍니다. 이렇게 상황에 따라 오버레이를 켜고 끄는 방식으로 게임 흐름을 자연스럽게 관리할 수 있습니다.
하지만 주의할 점도 있습니다. 초보 개발자들이 흔히 하는 실수 중 하나는 오버레이 이름을 오타 내는 것입니다.
'MainMenu'와 'mainMenu'는 다른 오버레이로 인식됩니다. 또한 이미 표시된 오버레이를 다시 add하면 중복으로 추가될 수 있으니, overlays.isActive('MainMenu') 로 먼저 확인하는 습관을 들이는 것이 좋습니다.
다시 김개발 씨의 이야기로 돌아가 봅시다. 박시니어 씨의 설명을 들은 김개발 씨는 고개를 끄덕였습니다.
"아, 그래서 Flutter 위젯으로 UI를 만들 수 있었군요!" 오버레이 개념을 제대로 이해하면 게임과 UI를 효과적으로 분리하면서도 자연스럽게 통합할 수 있습니다. 다음 장에서는 이 오버레이를 활용해 실제 게임 메뉴를 만들어보겠습니다.
실전 팁
💡 - 오버레이 이름은 상수로 관리하면 오타 실수를 방지할 수 있습니다
- pauseEngine()과 함께 사용하면 메뉴가 열린 동안 게임이 멈추는 자연스러운 동작을 구현할 수 있습니다
2. 게임 메뉴 만들기
오버레이 개념을 이해한 김개발 씨가 본격적으로 메인 메뉴를 만들기 시작했습니다. 하지만 막상 코드를 작성하려니 어디서부터 시작해야 할지 막막했습니다.
"오버레이를 add하는 건 알겠는데, 실제 메뉴 화면은 어떻게 연결하지?" 게임 클래스와 Flutter 위젯을 이어주는 다리가 필요했습니다.
게임 메뉴는 GameWidget의 overlayBuilderMap을 통해 오버레이 이름과 실제 위젯을 연결하여 만듭니다. 마치 전화번호부에서 이름으로 번호를 찾는 것처럼, 오버레이 이름을 키로 사용해 해당하는 위젯을 불러오는 구조입니다.
이 방식으로 메인 메뉴, 설정 화면, 튜토리얼 등 다양한 게임 메뉴를 구현할 수 있습니다.
다음 코드를 살펴봅시다.
// main.dart에서 GameWidget 설정
GameWidget<MyGame>(
game: MyGame(),
overlayBuilderMap: {
// 오버레이 이름과 위젯 빌더 연결
'MainMenu': (context, game) => MainMenuOverlay(game: game),
'PauseMenu': (context, game) => PauseMenuOverlay(game: game),
},
initialActiveOverlays: const ['MainMenu'], // 시작 시 표시할 오버레이
)
// 메인 메뉴 위젯
class MainMenuOverlay extends StatelessWidget {
final MyGame game;
const MainMenuOverlay({required this.game});
@override
Widget build(BuildContext context) {
return Center(
child: ElevatedButton(
onPressed: () => game.startGame(),
child: Text('게임 시작'),
),
);
}
}
김개발 씨는 오버레이의 개념을 이해하고 나서 바로 실습에 들어갔습니다. 게임 클래스에서 overlays.add('MainMenu') 를 호출하면 메뉴가 나타날 것이라 기대했지만, 화면에는 아무것도 표시되지 않았습니다.
에러도 없고, 그냥 게임 화면만 덩그러니 보였습니다. "이상하네, 분명히 add 했는데..." 김개발 씨가 고개를 갸웃거렸습니다.
박시니어 씨가 코드를 보더니 웃으며 말했습니다. "오버레이를 추가한 건 맞는데, 그 오버레이가 뭔지를 정의 안 했잖아.
GameWidget에서 연결해줘야 해요." overlayBuilderMap은 오버레이 시스템의 핵심입니다. 이것을 전화번호부에 비유해보겠습니다.
누군가에게 전화를 걸려면 이름만으로는 안 됩니다. 전화번호부에서 그 이름에 해당하는 번호를 찾아야 합니다.
overlayBuilderMap이 바로 이 전화번호부 역할을 합니다. 'MainMenu'라는 이름을 입력하면, 그에 해당하는 위젯을 반환해주는 것입니다.
overlayBuilderMap이 없다면 어떻게 될까요? 게임 클래스에서 아무리 overlays.add('MainMenu') 를 호출해도, 시스템은 'MainMenu'가 어떤 위젯인지 알 수 없습니다.
마치 전화번호부에 등록되지 않은 이름으로 전화를 걸려는 것과 같습니다. 당연히 아무 일도 일어나지 않습니다.
위의 코드 구조를 자세히 살펴보겠습니다. GameWidget은 Flame 게임을 Flutter 앱에 임베드하는 위젯입니다.
여기에 game 인스턴스를 전달하고, overlayBuilderMap으로 오버레이 정의를 추가합니다. 맵의 키는 오버레이 이름 문자열이고, 값은 BuildContext와 game 인스턴스를 받아 Widget을 반환하는 함수입니다.
initialActiveOverlays는 게임 시작 시 자동으로 표시할 오버레이 목록입니다. 여기에 'MainMenu'를 넣어두면 앱 실행과 동시에 메인 메뉴가 나타납니다.
MainMenuOverlay 위젯을 보면, 생성자에서 game 인스턴스를 받고 있습니다. 이것이 중요합니다.
이 game 인스턴스를 통해 메뉴 위젯에서 게임을 제어할 수 있습니다. 버튼을 누르면 game.startGame() 을 호출해서 게임을 시작하는 식입니다.
실무에서 메인 메뉴는 보통 여러 버튼을 포함합니다. 게임 시작 버튼, 설정 버튼, 랭킹 버튼, 크레딧 버튼 등이 있을 수 있습니다.
각 버튼은 해당하는 오버레이를 열거나, 화면을 전환하거나, 외부 링크를 여는 등의 동작을 수행합니다. Flutter의 모든 위젯을 사용할 수 있으므로, 애니메이션 효과가 들어간 화려한 메뉴도 어렵지 않게 만들 수 있습니다.
주의할 점이 있습니다. 메뉴 위젯에서 game 인스턴스의 메서드를 호출할 때, 해당 메서드가 게임 클래스에 정의되어 있어야 합니다.
없는 메서드를 호출하면 당연히 에러가 발생합니다. 또한 오버레이 위젯은 게임 화면 전체를 덮으므로, 배경을 투명하게 하려면 Colors.transparent 나 반투명 색상을 사용해야 합니다.
김개발 씨는 overlayBuilderMap을 추가하고 나서 다시 앱을 실행했습니다. 드디어 화면 중앙에 "게임 시작" 버튼이 나타났습니다.
버튼을 누르니 메뉴가 사라지고 게임이 시작되었습니다. "오, 이제야 게임 같아 보이네요!" 게임 메뉴 구조를 이해하면 다양한 화면을 자유롭게 추가할 수 있습니다.
다음 장에서는 게임 중에 항상 표시되는 점수 UI를 만들어보겠습니다.
실전 팁
💡 - overlayBuilderMap의 키 이름은 상수로 관리하면 오타를 방지할 수 있습니다
- initialActiveOverlays를 활용하면 게임 시작 시 자동으로 메뉴를 표시할 수 있습니다
- 메뉴 배경에 GestureDetector를 추가하면 바깥 영역 터치로 메뉴를 닫는 UX를 구현할 수 있습니다
3. 점수 표시 UI
게임 시작 버튼까지 만든 김개발 씨는 다음 과제에 착수했습니다. 바로 화면 상단에 현재 점수를 표시하는 것입니다.
그런데 한 가지 고민이 생겼습니다. "점수는 게임 중에 계속 바뀌는데, 오버레이 위젯이 이걸 어떻게 알지?" 게임 내부의 데이터와 UI를 실시간으로 동기화하는 방법이 필요했습니다.
점수 표시 UI는 게임 상태가 변할 때마다 위젯을 다시 빌드해서 업데이트합니다. Flame은 ValueNotifier나 ChangeNotifier와 자연스럽게 연동되므로, 점수가 바뀔 때마다 UI가 자동으로 갱신되도록 구현할 수 있습니다.
마치 주식 앱에서 시세가 실시간으로 업데이트되는 것처럼, 게임 점수도 즉각 반영됩니다.
다음 코드를 살펴봅시다.
// 게임 클래스에 점수 관리 추가
class MyGame extends FlameGame {
final ValueNotifier<int> score = ValueNotifier<int>(0);
void addScore(int points) {
score.value += points;
}
}
// 점수 표시 오버레이 위젯
class ScoreOverlay extends StatelessWidget {
final MyGame game;
const ScoreOverlay({required this.game});
@override
Widget build(BuildContext context) {
return Positioned(
top: 20,
left: 20,
child: ValueListenableBuilder<int>(
valueListenable: game.score,
builder: (context, score, child) {
return Text('점수: $score', style: TextStyle(fontSize: 24));
},
),
);
}
}
김개발 씨는 점수 표시 오버레이를 만들기 시작했습니다. 처음에는 단순하게 생각했습니다.
"그냥 Text 위젯에 점수를 넣으면 되겠지?" 그래서 이렇게 코드를 작성했습니다. Text('점수: ${game.score}').
하지만 문제가 있었습니다. 게임에서 코인을 먹어도 화면의 점수는 그대로 0이었습니다.
위젯이 처음 빌드될 때의 점수 값만 표시되고, 이후 변화를 감지하지 못했던 것입니다. 박시니어 씨가 설명했습니다.
"Flutter 위젯은 그냥 변수를 보여주는 게 아니야. 상태가 바뀌면 다시 빌드해야 해.
그래서 ValueNotifier를 써야 해요." ValueNotifier는 값의 변화를 감지하고 알려주는 도구입니다. 마치 온도계에 연결된 알람 시스템과 같습니다.
온도가 바뀌면 알람이 울리고, 그 알람을 듣는 장치들이 반응합니다. ValueNotifier가 온도계이고, ValueListenableBuilder가 알람을 듣는 장치입니다.
점수가 바뀌면 ValueNotifier가 "점수 바뀌었어!"라고 알리고, ValueListenableBuilder가 이를 듣고 UI를 새로 그립니다. 왜 이런 구조가 필요할까요?
게임에서 점수는 매우 빈번하게 바뀝니다. 코인을 먹을 때, 적을 처치할 때, 시간 보너스를 받을 때 등등.
이때마다 전체 화면을 다시 그리면 성능에 문제가 생깁니다. ValueListenableBuilder를 사용하면 점수 텍스트 부분만 선택적으로 다시 빌드됩니다.
효율적이고 부드러운 업데이트가 가능한 것입니다. 코드를 자세히 살펴보겠습니다.
게임 클래스에서 ValueNotifier<int> 타입의 score 변수를 선언합니다. 초기값은 0입니다.
addScore 메서드에서는 score.value 에 점수를 더합니다. .value를 통해 값을 변경하면 자동으로 리스너들에게 알림이 갑니다.
오버레이 위젯에서는 ValueListenableBuilder를 사용합니다. valueListenable에 game.score를 연결하면, 점수가 바뀔 때마다 builder 함수가 다시 호출됩니다.
builder 함수의 두 번째 파라미터 score가 현재 점수 값입니다. Positioned 위젯으로 점수 텍스트의 위치를 지정했습니다.
top: 20, left: 20으로 화면 왼쪽 상단에 표시됩니다. 이 Positioned가 제대로 동작하려면 부모에 Stack이 있어야 하는데, GameWidget 자체가 Stack처럼 동작하므로 오버레이에서 Positioned를 바로 사용할 수 있습니다.
실무에서는 점수 외에도 다양한 정보를 표시합니다. 남은 생명, 현재 스테이지, 획득한 아이템, 경과 시간 등을 함께 보여주는 경우가 많습니다.
각각을 별도의 ValueNotifier로 관리하거나, 게임 상태 전체를 담는 클래스를 만들어 ChangeNotifier로 관리할 수도 있습니다. 주의할 점이 있습니다.
ValueNotifier는 가벼운 상태 관리에 적합합니다. 하지만 게임 상태가 복잡해지면 Provider나 Riverpod 같은 상태 관리 라이브러리를 도입하는 것도 고려해볼 만합니다.
또한 오버레이 위젯이 게임 입력을 가리지 않도록 IgnorePointer로 감싸는 것이 좋습니다. 그래야 점수판 영역을 터치해도 게임 조작이 정상적으로 동작합니다.
김개발 씨는 ValueNotifier를 적용하고 다시 테스트했습니다. 코인을 먹을 때마다 화면의 점수가 실시간으로 올라갔습니다.
"오, 진짜 게임 같다!" 게임의 완성도가 한층 높아진 느낌이었습니다. 점수 UI를 이해하면 다양한 실시간 정보 표시에 응용할 수 있습니다.
다음 장에서는 게임을 잠시 멈추는 일시정지 화면을 만들어보겠습니다.
실전 팁
💡 - IgnorePointer로 점수 UI를 감싸면 터치가 게임으로 전달되어 조작에 방해가 되지 않습니다
- 여러 값을 표시해야 할 때는 ChangeNotifier 클래스로 묶어서 관리하면 깔끔합니다
- 점수에 애니메이션을 추가하면 (숫자가 올라가는 효과 등) 더 생동감 있는 UI가 됩니다
4. 일시정지 화면
게임을 테스트하던 김개발 씨에게 갑자기 전화가 왔습니다. 전화를 받으려는데 게임을 멈출 수가 없었습니다.
캐릭터는 계속 달리고, 장애물은 계속 다가오고, 결국 게임 오버가 되어버렸습니다. "아, 일시정지 기능이 없으니까 이런 일이..." 모든 게임에 필수인 일시정지 기능을 구현할 때가 온 것입니다.
일시정지 화면은 게임 루프를 멈추고 메뉴를 표시하는 기능입니다. Flame에서는 pauseEngine() 으로 게임을 정지하고 오버레이를 추가하며, resumeEngine() 으로 게임을 재개하고 오버레이를 제거합니다.
마치 리모컨의 일시정지 버튼처럼, 게임의 시간을 멈추고 다시 재생할 수 있게 해줍니다.
다음 코드를 살펴봅시다.
// 게임 클래스의 일시정지 로직
class MyGame extends FlameGame {
bool isPaused = false;
void togglePause() {
if (isPaused) {
resumeEngine();
overlays.remove('PauseMenu');
} else {
pauseEngine();
overlays.add('PauseMenu');
}
isPaused = !isPaused;
}
}
// 일시정지 메뉴 오버레이
class PauseMenuOverlay extends StatelessWidget {
final MyGame game;
const PauseMenuOverlay({required this.game});
@override
Widget build(BuildContext context) {
return Container(
color: Colors.black54, // 반투명 배경
child: Center(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Text('일시정지', style: TextStyle(color: Colors.white)),
ElevatedButton(onPressed: game.togglePause, child: Text('계속하기')),
ElevatedButton(onPressed: game.returnToMenu, child: Text('메뉴로')),
],
),
),
);
}
}
일시정지 기능이 없는 게임은 플레이어에게 상당히 불친절합니다. 전화가 올 수도 있고, 갑자기 화장실에 가야 할 수도 있고, 잠깐 다른 앱을 확인해야 할 수도 있습니다.
이럴 때 게임을 멈출 수 없다면 플레이어는 진행 상황을 잃게 됩니다. 좋은 게임이라면 언제든 멈췄다가 다시 시작할 수 있어야 합니다.
Flame에서 게임을 멈추는 것은 생각보다 간단합니다. pauseEngine() 메서드 하나로 게임 루프 전체가 멈춥니다.
캐릭터의 움직임, 적의 AI, 물리 연산, 애니메이션 등 모든 것이 정지합니다. 마치 영화를 일시정지한 것처럼 화면이 그 상태 그대로 멈춰 있습니다.
resumeEngine() 을 호출하면 멈췄던 지점부터 게임이 다시 진행됩니다. 코드를 살펴보면, isPaused 변수로 현재 상태를 추적합니다.
togglePause 메서드는 현재 상태에 따라 게임을 멈추거나 재개합니다. 일시정지할 때는 pauseEngine()을 호출하고 'PauseMenu' 오버레이를 추가합니다.
재개할 때는 resumeEngine()을 호출하고 오버레이를 제거합니다. 일시정지 메뉴 위젯을 보면, Colors.black54 배경을 사용했습니다.
이것은 54% 불투명도의 검은색으로, 뒤의 게임 화면이 어렴풋이 보이면서도 메뉴가 잘 보이는 효과를 줍니다. 플레이어는 "아, 게임이 멈춘 상태구나"를 직관적으로 인식할 수 있습니다.
메뉴에는 두 가지 버튼이 있습니다. "계속하기" 버튼은 togglePause를 호출해서 게임을 재개합니다.
"메뉴로" 버튼은 returnToMenu를 호출해서 메인 메뉴로 돌아갑니다. 이 외에도 "설정", "게임 저장", "처음부터" 같은 버튼을 추가할 수 있습니다.
실무에서 일시정지 기능을 구현할 때 고려할 점이 있습니다. 앱이 백그라운드로 내려갈 때 자동으로 일시정지되어야 합니다.
Flutter의 WidgetsBindingObserver를 사용하면 앱 라이프사이클을 감지할 수 있습니다. 또한 일시정지 중에도 배경 음악은 계속 재생할지, 함께 멈출지 결정해야 합니다.
보통은 음악도 함께 멈추거나 볼륨을 낮추는 것이 자연스럽습니다. 주의할 점도 있습니다.
pauseEngine()은 게임 루프만 멈추지, Flutter 위젯의 애니메이션은 멈추지 않습니다. 만약 게임 안에서 Flutter AnimationController를 사용하고 있다면 별도로 멈춰줘야 합니다.
또한 네트워크 게임이라면 일시정지 동안 서버와의 연결 상태를 어떻게 처리할지도 고려해야 합니다. 김개발 씨는 일시정지 기능을 추가하고 다시 게임을 테스트했습니다.
화면 한쪽에 작은 일시정지 버튼을 배치하고, 누르면 게임이 멈추고 메뉴가 나타났습니다. "이제 전화 와도 괜찮겠네요!" 일시정지 화면을 구현했으니, 다음은 게임이 끝났을 때 보여줄 게임 오버 화면을 만들어보겠습니다.
실전 팁
💡 - WidgetsBindingObserver를 활용하면 앱이 백그라운드로 갈 때 자동 일시정지를 구현할 수 있습니다
- 일시정지 메뉴 바깥 영역을 터치해도 게임이 재개되지 않도록 Container로 전체 화면을 덮는 것이 좋습니다
- 일시정지 상태에서도 특정 애니메이션(메뉴 버튼 반짝임 등)은 유지하면 UI가 더 생동감 있어 보입니다
5. 게임 오버 화면
김개발 씨의 게임에서 캐릭터가 장애물에 부딪혔습니다. 게임 오버입니다.
그런데 화면에는 아무런 변화가 없었습니다. 게임이 그냥 멈춰버린 것처럼 보였습니다.
"게임이 끝났는지 안 끝났는지도 모르겠네..." 플레이어에게 결과를 보여주고, 다시 도전할 기회를 주는 게임 오버 화면이 필요했습니다.
게임 오버 화면은 플레이어에게 게임 종료를 알리고 최종 점수를 보여주며 재도전 옵션을 제공하는 중요한 UI입니다. 마치 경기가 끝난 후 전광판에 점수가 표시되는 것처럼, 플레이어의 성과를 정리해서 보여주고 다음 행동을 선택하게 합니다.
좋은 게임 오버 화면은 플레이어가 다시 도전하고 싶게 만듭니다.
다음 코드를 살펴봅시다.
// 게임 클래스의 게임 오버 로직
class MyGame extends FlameGame {
final ValueNotifier<int> score = ValueNotifier<int>(0);
int highScore = 0;
void gameOver() {
pauseEngine();
// 최고 점수 갱신 확인
if (score.value > highScore) {
highScore = score.value;
}
overlays.add('GameOver');
}
void restartGame() {
score.value = 0;
overlays.remove('GameOver');
// 게임 상태 초기화 로직
resetGameState();
resumeEngine();
}
}
// 게임 오버 오버레이
class GameOverOverlay extends StatelessWidget {
final MyGame game;
const GameOverOverlay({required this.game});
@override
Widget build(BuildContext context) {
return Container(
color: Colors.black87,
child: Center(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Text('게임 오버', style: TextStyle(color: Colors.red)),
Text('점수: ${game.score.value}', style: TextStyle(color: Colors.white)),
Text('최고 점수: ${game.highScore}', style: TextStyle(color: Colors.yellow)),
ElevatedButton(onPressed: game.restartGame, child: Text('다시 하기')),
ElevatedButton(onPressed: game.returnToMenu, child: Text('메뉴로')),
],
),
),
);
}
}
게임 오버 화면은 단순히 "끝났습니다"를 알리는 것이 아닙니다. 좋은 게임 오버 화면은 플레이어의 성취감을 높이고, 다시 도전하고 싶은 욕구를 자극합니다.
이번 점수가 얼마인지, 최고 기록을 갱신했는지, 다음 목표는 무엇인지를 보여줍니다. 그래서 "아쉽다, 한 번 더 해볼까?"라는 생각이 들게 만듭니다.
김개발 씨는 게임 오버 로직을 구현하기 시작했습니다. 캐릭터가 장애물에 부딪히면 gameOver() 메서드가 호출됩니다.
이 메서드에서는 먼저 pauseEngine() 으로 게임을 멈춥니다. 그 다음 현재 점수가 최고 점수보다 높은지 확인합니다.
높다면 최고 점수를 갱신합니다. 마지막으로 'GameOver' 오버레이를 표시합니다.
게임 오버 화면에는 어떤 정보를 보여줘야 할까요? 필수적인 것은 현재 점수입니다.
플레이어가 이번 판에서 얼마나 잘했는지 알 수 있어야 합니다. 다음으로 최고 점수를 보여주면 좋습니다.
최고 기록을 갱신했다면 축하 메시지나 특별한 효과를 추가하면 더 좋습니다. "신기록!"이라는 문구 하나가 플레이어에게 큰 성취감을 줍니다.
버튼도 중요합니다. "다시 하기" 버튼은 필수입니다.
대부분의 플레이어는 한 판 더 하고 싶어합니다. 이 버튼을 눈에 잘 띄게, 손이 닿기 쉬운 위치에 배치해야 합니다.
"메뉴로" 버튼도 있어야 합니다. 그만 하고 싶거나 다른 모드를 선택하고 싶은 플레이어를 위해서입니다.
restartGame() 메서드를 보겠습니다. 점수를 0으로 초기화합니다.
오버레이를 제거합니다. resetGameState() 로 게임의 모든 상태를 초기화합니다.
캐릭터 위치, 적의 상태, 아이템 등을 처음 상태로 되돌립니다. 그 다음 resumeEngine() 으로 게임을 재개합니다.
이렇게 하면 처음부터 새 게임이 시작됩니다. 실무에서 게임 오버 화면을 더 풍성하게 만드는 방법이 있습니다.
통계를 보여주면 좋습니다. 플레이 시간, 처치한 적 수, 획득한 코인 수 등을 표시하면 플레이어가 자신의 플레이를 되돌아볼 수 있습니다.
소셜 공유 기능을 추가해서 점수를 친구들과 공유하게 할 수도 있습니다. 광고를 시청하면 이어하기를 할 수 있게 하는 것도 모바일 게임에서 흔히 쓰는 방법입니다.
주의할 점이 있습니다. 게임 오버 화면이 너무 빨리 나타나면 플레이어가 당황합니다.
충돌 후 잠깐의 딜레이나 연출을 넣으면 더 자연스럽습니다. 또한 resetGameState() 에서 모든 것을 제대로 초기화하지 않으면 버그가 생깁니다.
이전 판의 적이 그대로 남아있거나, 점수가 이상하게 계산되는 문제가 발생할 수 있습니다. 김개발 씨는 게임 오버 화면을 완성하고 테스트했습니다.
장애물에 부딪히자 화면이 어두워지면서 점수가 표시되었습니다. "다시 하기"를 누르니 바로 새 게임이 시작되었습니다.
최고 점수를 갱신했을 때는 괜히 뿌듯했습니다. 게임의 기본적인 UI 흐름이 완성되었습니다.
다음 장에서는 이 모든 것을 하나로 통합하는 방법을 알아보겠습니다.
실전 팁
💡 - 게임 오버 시 잠깐의 딜레이(0.5~1초)를 두면 더 자연스러운 연출이 됩니다
- 최고 점수 갱신 시 특별한 효과나 메시지를 추가하면 성취감이 높아집니다
- SharedPreferences를 활용해 최고 점수를 저장하면 앱을 껐다 켜도 기록이 유지됩니다
6. Flutter 위젯과 통합
김개발 씨는 드디어 게임의 모든 UI를 완성했습니다. 메인 메뉴, 점수판, 일시정지, 게임 오버 화면까지.
그런데 이것들을 어떻게 한데 모아서 동작하게 할지 막막했습니다. "각각은 만들었는데, 전체가 유기적으로 돌아가려면 어떻게 해야 하지?" 모든 조각을 맞춰 완성된 퍼즐을 만들 시간입니다.
Flutter 위젯과의 통합은 GameWidget을 중심으로 모든 오버레이를 연결하고, 게임 상태에 따라 적절한 UI가 표시되도록 구성하는 것입니다. 마치 오케스트라의 지휘자가 각 악기를 조화롭게 이끄는 것처럼, GameWidget이 게임과 여러 오버레이의 등장과 퇴장을 지휘합니다.
이 통합이 제대로 되어야 완성된 게임이 됩니다.
다음 코드를 살펴봅시다.
// main.dart - 전체 통합
void main() => runApp(const MyApp());
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
home: GameScreen(),
);
}
}
class GameScreen extends StatefulWidget {
@override
State<GameScreen> createState() => _GameScreenState();
}
class _GameScreenState extends State<GameScreen> {
late final MyGame game;
@override
void initState() {
super.initState();
game = MyGame();
}
@override
Widget build(BuildContext context) {
return Scaffold(
body: GameWidget<MyGame>(
game: game,
overlayBuilderMap: {
'MainMenu': (_, game) => MainMenuOverlay(game: game),
'Score': (_, game) => ScoreOverlay(game: game),
'PauseMenu': (_, game) => PauseMenuOverlay(game: game),
'GameOver': (_, game) => GameOverOverlay(game: game),
},
initialActiveOverlays: const ['MainMenu'],
),
);
}
}
지금까지 만든 조각들을 하나로 조립할 시간입니다. 김개발 씨는 main.dart 파일을 열고 전체 구조를 정리하기 시작했습니다.
먼저 큰 그림을 그려봅니다. 앱이 시작되면 MaterialApp이 실행되고, 그 안에 GameScreen이 표시됩니다.
GameScreen 안에 GameWidget이 있고, 이 GameWidget이 게임과 모든 오버레이를 관리합니다. 왜 StatefulWidget을 사용할까요?
게임 인스턴스는 앱이 실행되는 동안 계속 유지되어야 합니다. 화면이 다시 빌드되더라도 게임 상태가 사라지면 안 됩니다.
initState에서 게임 인스턴스를 생성하고, 이를 위젯의 생명주기 동안 유지합니다. 만약 StatelessWidget을 사용하면 빌드할 때마다 새 게임이 생성되어 상태가 초기화되어 버립니다.
overlayBuilderMap의 구조를 보겠습니다. 맵의 키는 오버레이 이름이고, 값은 빌더 함수입니다.
빌더 함수는 BuildContext와 game을 받아서 Widget을 반환합니다. 여기서 _로 context를 무시한 것은 사용하지 않기 때문입니다.
game 파라미터를 각 오버레이 위젯에 전달해서, 오버레이에서 게임을 제어할 수 있게 합니다. initialActiveOverlays는 앱 시작 시 바로 표시할 오버레이입니다.
'MainMenu'를 지정했으므로 앱이 실행되면 바로 메인 메뉴가 나타납니다. 플레이어가 "게임 시작"을 누르면 메뉴가 사라지고 'Score' 오버레이가 추가되며 게임이 시작됩니다.
게임 흐름을 정리해보면 이렇습니다. 앱 시작시 MainMenu가 표시됩니다.
게임 시작 버튼을 누르면 MainMenu가 사라지고 Score가 표시되며 게임이 시작됩니다. 일시정지 버튼을 누르면 게임이 멈추고 PauseMenu가 추가됩니다.
계속하기를 누르면 PauseMenu가 사라지고 게임이 재개됩니다. 게임 오버가 되면 게임이 멈추고 GameOver가 표시됩니다.
다시 하기를 누르면 GameOver가 사라지고 게임이 재시작됩니다. 여러 오버레이가 동시에 표시될 수도 있습니다.
예를 들어 게임 중에는 Score 오버레이가 계속 표시됩니다. 일시정지하면 Score와 PauseMenu가 함께 보입니다.
이렇게 오버레이를 레이어처럼 쌓아서 복잡한 UI 상태를 표현할 수 있습니다. 실무에서 더 확장하는 방법도 있습니다.
설정 화면 오버레이를 추가해서 볼륨 조절, 그래픽 설정 등을 제공할 수 있습니다. 튜토리얼 오버레이로 처음 플레이하는 유저에게 조작법을 알려줄 수 있습니다.
상점 오버레이로 인앱 구매 기능을 넣을 수도 있습니다. 기본 구조만 잘 잡아두면 확장은 어렵지 않습니다.
주의할 점이 있습니다. 오버레이가 많아지면 관리가 복잡해집니다.
오버레이 이름을 상수로 관리하고, 게임 상태 전이 로직을 명확하게 정리해두는 것이 좋습니다. 또한 메모리 누수를 방지하기 위해, 사용하지 않는 오버레이는 확실하게 제거해야 합니다.
김개발 씨는 모든 코드를 정리하고 마지막 테스트를 진행했습니다. 앱을 실행하니 메인 메뉴가 나타났습니다.
게임 시작을 누르니 점수가 표시되며 게임이 시작되었습니다. 일시정지도 잘 동작하고, 게임 오버 후 다시 하기도 문제없었습니다.
박시니어 씨가 완성된 게임을 보며 말했습니다. "오, 이제 진짜 게임 같네요.
잘했어요!" 오버레이 시스템을 마스터하면 어떤 게임 UI든 구현할 수 있습니다. Flame의 강력한 렌더링과 Flutter의 풍부한 위젯 생태계를 모두 활용해서, 여러분만의 멋진 게임을 만들어보세요.
실전 팁
💡 - 오버레이 이름은 별도 클래스나 enum으로 관리하면 오타와 중복을 방지할 수 있습니다
- 복잡한 게임은 상태 머신(State Machine) 패턴을 도입해 게임 흐름을 관리하면 좋습니다
- 오버레이 전환 시 페이드 인/아웃 애니메이션을 추가하면 더 부드러운 UX를 제공할 수 있습니다
이상으로 학습을 마칩니다. 위 내용을 직접 코드로 작성해보면서 익혀보세요!
댓글 (0)
함께 보면 좋은 카드 뉴스
AAA급 게임 프로젝트 완벽 가이드
Flutter와 Flame 엔진을 활용하여 AAA급 퀄리티의 모바일 게임을 개발하는 전체 과정을 다룹니다. 기획부터 앱 스토어 출시까지, 실무에서 필요한 모든 단계를 이북처럼 술술 읽히는 스타일로 설명합니다.
빌드와 배포 자동화 완벽 가이드
Flutter 앱 개발에서 GitHub Actions를 활용한 CI/CD 파이프라인 구축부터 앱 스토어 자동 배포까지, 초급 개발자도 쉽게 따라할 수 있는 빌드 자동화의 모든 것을 다룹니다.
게임 분석과 메트릭스 완벽 가이드
Flutter와 Flame으로 개발한 게임의 성공을 측정하고 개선하는 방법을 배웁니다. Firebase Analytics 연동부터 A/B 테스팅, 리텐션 분석까지 데이터 기반 게임 운영의 모든 것을 다룹니다.
게임 보안과 치팅 방지 완벽 가이드
Flutter와 Flame 게임 엔진에서 클라이언트 보안부터 서버 검증까지, 치터들로부터 게임을 보호하는 핵심 기법을 다룹니다. 초급 개발자도 쉽게 따라할 수 있는 실전 보안 코드와 함께 설명합니다.
애니메이션 시스템 커스터마이징 완벽 가이드
Flutter와 Flame 게임 엔진에서 고급 애니메이션 시스템을 구현하는 방법을 다룹니다. 스켈레탈 애니메이션부터 절차적 애니메이션까지, 게임 개발에 필요한 핵심 애니메이션 기법을 실무 예제와 함께 배워봅니다.