본 콘텐츠의 이미지 및 내용은 AI로 생성되었습니다.
본 콘텐츠의 이미지 및 내용을 무단으로 복제, 배포, 수정하여 사용할 경우 저작권법에 의해 법적 제재를 받을 수 있습니다.
이미지 로딩 중...
AI Generated
2026. 2. 2. · 3 Views
Flame 게임 플랫폼별 최적화 완벽 가이드
Flutter Flame 게임 엔진으로 개발한 게임을 iOS, Android, 웹 등 다양한 플랫폼에서 최적의 성능으로 실행하기 위한 최적화 기법을 다룹니다. 플랫폼 감지부터 Metal, Vulkan 활용, 해상도 스케일링까지 실무에서 바로 적용할 수 있는 내용을 담았습니다.
목차
1. 플랫폼 감지 및 분기
김개발 씨는 첫 번째 모바일 게임을 완성하고 기쁜 마음으로 테스트를 시작했습니다. 그런데 이상한 일이 벌어졌습니다.
안드로이드에서는 부드럽게 돌아가던 게임이 iOS에서는 뚝뚝 끊기고, 웹에서는 아예 다른 문제가 발생한 것입니다. "같은 코드인데 왜 이렇게 다르게 동작하는 걸까요?"
플랫폼 감지란 현재 앱이 실행되고 있는 운영체제나 환경을 파악하는 것입니다. 마치 여행자가 현지 언어를 확인하고 그에 맞게 대화하는 것처럼, 게임도 플랫폼에 따라 다른 전략을 취해야 합니다.
이를 통해 각 플랫폼의 특성에 맞는 최적화된 코드를 실행할 수 있습니다.
다음 코드를 살펴봅시다.
import 'dart:io' show Platform;
import 'package:flutter/foundation.dart' show kIsWeb;
// 플랫폼별 설정을 관리하는 클래스
class PlatformConfig {
// 현재 플랫폼 타입을 반환합니다
static PlatformType get current {
if (kIsWeb) return PlatformType.web;
if (Platform.isIOS) return PlatformType.ios;
if (Platform.isAndroid) return PlatformType.android;
if (Platform.isWindows) return PlatformType.windows;
if (Platform.isMacOS) return PlatformType.macos;
return PlatformType.unknown;
}
// 플랫폼별 최대 FPS 설정
static int get targetFps {
switch (current) {
case PlatformType.ios: return 120; // ProMotion 지원
case PlatformType.android: return 60;
case PlatformType.web: return 60;
default: return 60;
}
}
}
enum PlatformType { ios, android, web, windows, macos, unknown }
김개발 씨는 입사 6개월 차 게임 개발자입니다. 회사에서 처음으로 크로스 플랫폼 게임 프로젝트를 맡게 되었습니다.
Flutter와 Flame 엔진을 사용하면 한 번의 코드로 여러 플랫폼에 배포할 수 있다고 들었기에, 자신만만하게 개발을 시작했습니다. 그러나 현실은 녹록지 않았습니다.
개발팀 리더인 박시니어 씨가 다가와 말했습니다. "김개발 씨, 크로스 플랫폼이라고 해서 모든 게 자동으로 최적화되는 건 아니에요.
각 플랫폼의 특성을 이해하고 그에 맞게 대응해야 합니다." 그렇다면 플랫폼 감지란 정확히 무엇일까요? 쉽게 비유하자면, 플랫폼 감지는 마치 만능 리모컨과 같습니다.
하나의 리모컨으로 TV, 에어컨, 셋톱박스를 모두 조작하지만, 내부적으로는 어떤 기기에 신호를 보내는지 먼저 파악합니다. 게임도 마찬가지입니다.
실행 환경을 먼저 파악해야 그에 맞는 명령을 내릴 수 있습니다. 플랫폼 감지가 없던 시절에는 어땠을까요?
개발자들은 플랫폼별로 완전히 다른 프로젝트를 만들어야 했습니다. iOS용 Swift 프로젝트, Android용 Kotlin 프로젝트, 웹용 JavaScript 프로젝트를 각각 관리했습니다.
버그 하나를 수정하려면 세 곳을 모두 고쳐야 했고, 기능 하나를 추가하려면 세 배의 시간이 들었습니다. 바로 이런 문제를 해결하기 위해 크로스 플랫폼 프레임워크가 등장했고, 그 핵심에 플랫폼 감지 기능이 있습니다.
Flutter에서는 두 가지 방법으로 플랫폼을 감지합니다. 먼저 kIsWeb 상수는 웹 환경인지 아닌지를 알려줍니다.
이 값은 컴파일 타임에 결정되므로 매우 효율적입니다. 다음으로 Platform 클래스는 네이티브 환경에서 구체적인 운영체제를 알려줍니다.
위의 코드를 살펴보겠습니다. PlatformConfig 클래스의 current getter는 현재 플랫폼을 판별합니다.
중요한 점은 kIsWeb을 가장 먼저 확인한다는 것입니다. 웹 환경에서는 dart:io 패키지를 사용할 수 없기 때문에, Platform 클래스에 접근하기 전에 웹인지 먼저 확인해야 합니다.
실제 현업에서는 이 패턴을 어떻게 활용할까요? 예를 들어 레이싱 게임을 개발한다고 가정해봅시다.
iOS에서는 ProMotion 디스플레이를 지원하는 기기가 있어 120fps까지 가능합니다. 반면 일반 Android 기기는 60fps가 표준입니다.
플랫폼을 감지하여 targetFps를 다르게 설정하면, 각 기기에서 최적의 경험을 제공할 수 있습니다. 주의할 점도 있습니다.
플랫폼 감지 로직을 게임 루프 안에서 매 프레임마다 호출하면 성능 저하가 발생합니다. 게임 초기화 시점에 한 번만 감지하고 그 결과를 캐싱해두는 것이 좋습니다.
다시 김개발 씨의 이야기로 돌아가 봅시다. 박시니어 씨의 조언을 듣고 PlatformConfig 클래스를 만든 김개발 씨는 이제 플랫폼별로 다른 설정을 적용할 수 있게 되었습니다.
"이제 각 플랫폼의 특성에 맞게 최적화할 준비가 된 거군요!"
실전 팁
💡 - kIsWeb 체크를 항상 Platform 클래스 접근보다 먼저 수행하세요
- 플랫폼 감지 결과는 게임 시작 시 캐싱하여 재사용하세요
- 플랫폼별 설정값은 별도의 설정 클래스로 관리하면 유지보수가 쉬워집니다
2. iOS Metal 최적화
김개발 씨가 만든 게임이 iOS에서 유독 느리게 동작한다는 피드백이 들어왔습니다. 테스터들은 "아이폰이 이렇게 느릴 리가 없는데..."라며 의아해했습니다.
박시니어 씨가 프로파일링 도구를 열어보더니 말했습니다. "GPU 활용률이 너무 낮네요.
Metal을 제대로 활용하고 있지 않은 것 같아요."
Metal은 Apple이 개발한 저수준 그래픽 API입니다. OpenGL보다 훨씬 효율적으로 GPU를 활용할 수 있도록 설계되었습니다.
마치 고속도로의 하이패스처럼, Metal은 CPU와 GPU 사이의 병목을 줄여 그래픽 명령을 빠르게 전달합니다. Flame 게임에서 Metal을 제대로 활용하면 iOS 기기의 잠재력을 최대한 끌어낼 수 있습니다.
다음 코드를 살펴봅시다.
import 'package:flame/game.dart';
import 'package:flutter/services.dart';
class iOSOptimizedGame extends FlameGame {
@override
Future<void> onLoad() async {
await super.onLoad();
// iOS Metal 최적화를 위한 설정
if (PlatformConfig.current == PlatformType.ios) {
await _configureMetalOptimizations();
}
}
Future<void> _configureMetalOptimizations() async {
// 화면 주사율 최대화 (ProMotion 지원)
await SystemChrome.setPreferredOrientations([
DeviceOrientation.landscapeLeft,
DeviceOrientation.landscapeRight,
]);
// 이미지 캐싱 전략 - Metal 텍스처 최적화
images.prefix = 'assets/images/ios/';
// 배치 렌더링을 위한 스프라이트 아틀라스 사용
await images.loadAll([
'spritesheet_2x.png', // iOS Retina용 2배 해상도
'effects_atlas.png',
]);
}
}
김개발 씨는 프로파일러 화면을 보며 고개를 갸우뚱했습니다. GPU 사용률이 고작 30% 남짓인데, 프레임은 뚝뚝 끊기고 있었습니다.
박시니어 씨가 설명을 이어갔습니다. "iOS는 Metal이라는 자체 그래픽 API를 사용해요.
이걸 제대로 활용하지 않으면 아이폰의 강력한 GPU가 놀게 됩니다." 그렇다면 Metal이란 정확히 무엇일까요? 쉽게 비유하자면, Metal은 마치 전용 고속도로와 같습니다.
일반 도로인 OpenGL을 이용하면 여러 차량이 뒤섞여 정체가 발생합니다. 하지만 Metal이라는 전용 고속도로를 이용하면, CPU가 GPU에게 보내는 그래픽 명령이 막힘없이 빠르게 전달됩니다.
Metal 이전에는 어땠을까요? iOS 개발자들은 OpenGL ES를 사용했습니다.
OpenGL은 범용적이지만, 그만큼 Apple 하드웨어에 최적화되지 않았습니다. 매 프레임마다 CPU가 GPU에게 명령을 전달할 때 상당한 오버헤드가 발생했습니다.
특히 수백 개의 스프라이트를 그리는 게임에서는 이 병목 현상이 심각했습니다. Apple은 2014년 Metal을 발표하며 이 문제를 해결했습니다.
Metal은 Apple 하드웨어에 맞춤 설계되어, 불필요한 추상화 계층을 제거했습니다. 결과적으로 드로우 콜 오버헤드가 10배 이상 감소했습니다.
Flutter와 Flame은 내부적으로 Metal을 활용하지만, 개발자가 몇 가지 최적화를 적용해야 최대 성능을 끌어낼 수 있습니다. 위의 코드를 살펴보겠습니다.
onLoad 메서드에서 플랫폼이 iOS인 경우에만 Metal 최적화 설정을 적용합니다. 가장 중요한 것은 스프라이트 아틀라스의 활용입니다.
여러 개의 작은 이미지 파일 대신, 하나의 큰 아틀라스 이미지를 사용하면 텍스처 교체 횟수가 줄어듭니다. iOS Retina 디스플레이를 위해 2배 해상도 에셋을 준비하는 것도 중요합니다.
1배 해상도 이미지를 확대하면 흐릿해 보이고, 4배 해상도는 메모리를 낭비합니다. iOS에서는 2배 해상도가 최적의 균형점입니다.
실제 현업에서는 어떻게 활용할까요? 예를 들어 탄막 슈팅 게임을 개발한다고 가정해봅시다.
화면에 수백 개의 총알 스프라이트가 동시에 나타납니다. 각 총알을 개별 이미지로 로드하면 Metal이 매번 텍스처를 교체해야 합니다.
하지만 모든 총알 이미지를 하나의 스프라이트 아틀라스에 담으면, 단 한 번의 텍스처 바인딩으로 모든 총알을 그릴 수 있습니다. 주의할 점도 있습니다.
스프라이트 아틀라스의 크기가 너무 커지면 오히려 메모리 문제가 발생합니다. iOS 기기별 최대 텍스처 크기를 확인하고, 필요하다면 여러 개의 아틀라스로 분리해야 합니다.
iPhone 최신 모델은 16384x16384까지 지원하지만, 구형 모델은 4096x4096으로 제한됩니다. 다시 김개발 씨의 이야기로 돌아가 봅시다.
스프라이트 아틀라스를 적용하고 iOS 전용 에셋을 준비한 결과, 게임의 프레임레이트가 눈에 띄게 향상되었습니다. "GPU 사용률이 80%까지 올라갔어요!
이제야 아이폰다운 성능이 나오네요."
실전 팁
💡 - 스프라이트 아틀라스를 활용하여 드로우 콜을 최소화하세요
- iOS Retina용 2배 해상도 에셋을 별도로 준비하세요
- Instruments의 Metal System Trace로 GPU 병목을 분석하세요
3. Android Vulkan 활용
iOS 최적화를 마친 김개발 씨는 이번에는 Android 테스트에 나섰습니다. 그런데 최신 플래그십 폰에서는 매끄럽게 돌아가던 게임이, 중저가 폰에서는 버벅거렸습니다.
"안드로이드는 기기가 너무 다양해서 어디에 맞춰야 할지 모르겠어요." 박시니어 씨가 웃으며 말했습니다. "Vulkan을 알면 해결책이 보일 거예요."
Vulkan은 Khronos Group이 개발한 차세대 그래픽 API입니다. OpenGL ES의 후속작으로, Metal과 마찬가지로 저수준 제어를 제공합니다.
마치 자동차의 수동 변속기처럼, 더 많은 제어권을 개발자에게 주어 섬세한 최적화가 가능합니다. Android 7.0 이상에서 지원되며, 특히 고사양 게임에서 큰 성능 향상을 기대할 수 있습니다.
다음 코드를 살펴봅시다.
class AndroidOptimizedGame extends FlameGame {
late final bool _supportsVulkan;
late final int _gpuTier;
@override
Future<void> onLoad() async {
await super.onLoad();
if (PlatformConfig.current == PlatformType.android) {
await _detectGpuCapabilities();
await _configureAndroidOptimizations();
}
}
Future<void> _detectGpuCapabilities() async {
// Android API 레벨 확인 (Vulkan은 API 24+)
final deviceInfo = await DeviceInfoPlugin().androidInfo;
_supportsVulkan = deviceInfo.version.sdkInt >= 24;
// GPU 티어 분류 (메모리 기반 간이 판별)
final totalMemory = deviceInfo.systemFeatures.length;
_gpuTier = totalMemory > 100 ? 3 : (totalMemory > 50 ? 2 : 1);
}
Future<void> _configureAndroidOptimizations() async {
// GPU 티어별 품질 설정
final quality = _gpuTier >= 2 ? 'high' : 'low';
images.prefix = 'assets/images/android_$quality/';
// Vulkan 지원 시 고급 이펙트 활성화
if (_supportsVulkan && _gpuTier >= 2) {
enableAdvancedParticles = true;
enablePostProcessing = true;
}
}
bool enableAdvancedParticles = false;
bool enablePostProcessing = false;
}
김개발 씨는 Android 기기 테스트 결과표를 보며 한숨을 쉬었습니다. 최신 Galaxy S 시리즈에서는 60fps가 나오는데, 몇 년 된 중저가폰에서는 20fps도 간신히 나왔습니다.
"iOS는 기기 종류가 적어서 대응하기 쉬웠는데, Android는 수천 가지 기기가 있잖아요..." 박시니어 씨가 다가와 조언했습니다. "Android의 다양성은 약점이 아니라 특성이에요.
Vulkan과 동적 품질 조절을 활용하면 모든 기기에서 쾌적한 경험을 제공할 수 있습니다." 그렇다면 Vulkan이란 정확히 무엇일까요? 쉽게 비유하자면, Vulkan은 마치 맞춤 정장과 같습니다.
기성복인 OpenGL ES는 다양한 체형에 대충 맞지만, 맞춤 정장인 Vulkan은 해당 기기의 GPU에 딱 맞게 조정됩니다. 물론 맞춤 정장을 만들려면 더 많은 노력이 필요하지만, 결과물은 훨씬 뛰어납니다.
OpenGL ES 시절에는 어떤 문제가 있었을까요? OpenGL ES는 1990년대에 설계된 OpenGL을 기반으로 합니다.
당시에는 단일 코어 CPU가 일반적이었습니다. 하지만 현대의 스마트폰은 8개 이상의 CPU 코어를 가지고 있습니다.
OpenGL ES는 단일 스레드에서만 GPU 명령을 전달할 수 있어, 멀티코어의 장점을 전혀 활용하지 못했습니다. Vulkan은 이 문제를 해결합니다.
멀티스레드 렌더링을 지원하여 여러 CPU 코어가 동시에 GPU에 명령을 전달할 수 있습니다. 또한 명시적 메모리 관리로 메모리 할당과 해제를 개발자가 직접 제어할 수 있어, 불필요한 오버헤드를 제거할 수 있습니다.
위의 코드를 살펴보겠습니다. 핵심은 GPU 티어 분류입니다.
모든 Android 기기에 동일한 그래픽 설정을 적용하면, 저사양 기기에서는 버벅거리고 고사양 기기에서는 잠재력을 낭비하게 됩니다. 기기의 성능을 감지하여 3단계로 분류하고, 각 티어에 맞는 에셋과 이펙트를 적용합니다.
device_info_plus 패키지를 사용하면 Android SDK 버전을 확인할 수 있습니다. SDK 24 이상이면 Vulkan을 지원합니다.
하지만 Vulkan 지원 여부만으로는 충분하지 않습니다. GPU 성능도 함께 고려해야 합니다.
실제 현업에서는 어떻게 활용할까요? 예를 들어 오픈월드 RPG 게임을 개발한다고 가정해봅시다.
고사양 기기에서는 먼 거리까지 풍경을 렌더링하고, 실시간 그림자와 반사 효과를 적용합니다. 중간 사양에서는 그림자 품질을 낮추고 반사 효과를 끕니다.
저사양에서는 시야 거리를 줄이고 단순한 쉐이더를 사용합니다. 같은 게임이지만, 각 기기에서 최선의 경험을 제공하는 것입니다.
주의할 점도 있습니다. GPU 티어 판별 로직이 너무 단순하면 오분류가 발생합니다.
실제 상용 게임에서는 벤치마크 씬을 짧게 실행하여 실측 성능을 기반으로 티어를 결정하기도 합니다. 또한 사용자가 직접 그래픽 옵션을 조절할 수 있는 설정 화면을 제공하는 것도 좋은 방법입니다.
다시 김개발 씨의 이야기로 돌아가 봅시다. GPU 티어별 동적 품질 조절을 적용한 결과, 저사양 기기에서도 안정적인 30fps를 유지하면서 고사양 기기에서는 화려한 이펙트를 즐길 수 있게 되었습니다.
"이제 어떤 기기든 자신 있게 지원할 수 있겠어요!"
실전 팁
💡 - device_info_plus 패키지로 Android SDK 버전과 기기 정보를 확인하세요
- GPU 티어를 3단계로 분류하여 동적으로 품질을 조절하세요
- 사용자가 직접 그래픽 옵션을 조절할 수 있는 설정 화면을 제공하세요
4. 웹 브라우저 최적화
모바일 최적화를 마친 김개발 씨에게 새로운 미션이 떨어졌습니다. "이 게임, 웹에서도 돌아가게 해줄 수 있어요?" 김개발 씨는 자신만만하게 flutter build web 명령을 실행했습니다.
빌드는 성공했지만, 브라우저에서 실행하니 로딩이 끝없이 계속되었습니다. "웹은 또 다른 세계군요..."
웹 최적화는 브라우저라는 제한된 환경에서 게임을 원활하게 실행하기 위한 기술입니다. 네이티브 앱과 달리 웹은 JavaScript 엔진 위에서 돌아가고, 에셋을 네트워크로 다운로드해야 합니다.
마치 해외여행 시 현지 통화와 언어에 맞춰야 하듯, 웹 환경에 맞는 별도의 최적화 전략이 필요합니다.
다음 코드를 살펴봅시다.
class WebOptimizedGame extends FlameGame {
@override
Future<void> onLoad() async {
await super.onLoad();
if (PlatformConfig.current == PlatformType.web) {
await _configureWebOptimizations();
}
}
Future<void> _configureWebOptimizations() async {
// WebGL 렌더러 설정 확인
final isWebGL2Supported = _checkWebGL2Support();
// 웹용 경량화된 에셋 사용
images.prefix = 'assets/images/web/';
// 에셋 프리로딩 (네트워크 지연 최소화)
await _preloadCriticalAssets();
// 브라우저 가시성 API 연동
_setupVisibilityListener();
}
bool _checkWebGL2Support() {
// 실제로는 js interop으로 WebGL2 지원 확인
return true; // 간략화된 예시
}
Future<void> _preloadCriticalAssets() async {
// 게임 시작에 필수적인 에셋만 먼저 로드
await images.loadAll([
'ui_essential.png',
'player_sprite.png',
]);
}
void _setupVisibilityListener() {
// 탭이 비활성화되면 게임 일시정지
// 브라우저 리소스 절약
}
}
김개발 씨는 브라우저 개발자 도구의 Network 탭을 열어보고 경악했습니다. 게임 에셋이 무려 50MB나 되었고, 이걸 다운로드하는 데만 한참이 걸렸습니다.
박시니어 씨가 화면을 보며 말했습니다. "웹은 네이티브와 완전히 다른 환경이에요.
모든 것을 네트워크로 전송해야 하니까요." 그렇다면 웹 최적화란 정확히 무엇일까요? 쉽게 비유하자면, 웹 게임 배포는 마치 해외 이사와 같습니다.
국내 이사는 짐을 트럭에 싣고 바로 옮기면 됩니다. 하지만 해외 이사는 컨테이너에 담아 배로 운송해야 합니다.
짐을 최대한 줄이고, 도착하면 바로 사용할 필수품은 따로 챙겨야 합니다. 웹 게임도 마찬가지입니다.
네이티브 앱과 웹의 차이점은 무엇일까요? 네이티브 앱은 설치 시 모든 에셋이 기기에 저장됩니다.
게임을 실행하면 로컬 스토리지에서 즉시 에셋을 불러옵니다. 하지만 웹 게임은 실행할 때마다 서버에서 에셋을 다운로드합니다.
네트워크 속도에 따라 로딩 시간이 크게 달라지고, 다운로드 중에는 게임을 시작할 수 없습니다. 이 문제를 해결하기 위해 여러 전략이 필요합니다.
첫째, 에셋 경량화입니다. 웹용 에셋은 네이티브용보다 작아야 합니다.
PNG 대신 WebP 포맷을 사용하면 화질 손실 없이 30~50% 용량을 줄일 수 있습니다. 음악 파일도 WAV 대신 OGG나 MP3를 사용합니다.
둘째, 점진적 로딩입니다. 모든 에셋을 한 번에 로드하는 대신, 게임 시작에 필수적인 에셋만 먼저 로드합니다.
나머지는 게임을 플레이하는 동안 백그라운드에서 로드합니다. 플레이어는 빠르게 게임을 시작할 수 있고, 추가 에셋은 필요할 때 사용 가능해집니다.
셋째, 브라우저 가시성 API 활용입니다. 사용자가 다른 탭으로 이동하면 게임 탭은 보이지 않습니다.
이때도 게임이 계속 돌아가면 CPU와 메모리를 낭비하게 됩니다. 가시성 API를 사용하여 탭이 비활성화되면 게임을 일시정지하고, 다시 활성화되면 재개합니다.
위의 코드를 살펴보겠습니다. _preloadCriticalAssets 메서드는 게임 시작에 꼭 필요한 UI와 플레이어 스프라이트만 먼저 로드합니다.
배경이나 적 캐릭터는 나중에 로드해도 됩니다. _setupVisibilityListener는 브라우저의 Page Visibility API와 연동하여 탭 전환을 감지합니다.
실제 현업에서는 어떻게 활용할까요? 예를 들어 웹 퍼즐 게임을 개발한다고 가정해봅시다.
전체 100개 레벨의 에셋을 모두 로드하면 수백 MB가 됩니다. 대신 첫 10개 레벨의 에셋만 먼저 로드하고, 플레이어가 진행하는 동안 다음 레벨들을 백그라운드에서 로드합니다.
플레이어는 대기 시간 없이 게임을 즐길 수 있습니다. 주의할 점도 있습니다.
브라우저마다 WebGL 지원 수준이 다릅니다. Safari는 WebGL2 지원이 늦었고, 일부 모바일 브라우저는 WebGL 자체를 지원하지 않습니다.
게임 시작 전에 WebGL 지원 여부를 확인하고, 지원하지 않으면 안내 메시지를 표시해야 합니다. 다시 김개발 씨의 이야기로 돌아가 봅시다.
에셋을 WebP로 변환하고 점진적 로딩을 적용한 결과, 초기 로딩 시간이 10초에서 2초로 단축되었습니다. "이제 웹에서도 쾌적하게 플레이할 수 있겠네요!"
실전 팁
💡 - 웹용 에셋은 WebP 포맷으로 변환하여 용량을 줄이세요
- 점진적 로딩으로 필수 에셋만 먼저 로드하고 나머지는 백그라운드에서 로드하세요
- Page Visibility API로 비활성 탭에서는 게임을 일시정지하세요
5. 디바이스별 설정
김개발 씨의 게임이 드디어 출시되었습니다. 그런데 리뷰에서 예상치 못한 불만이 터져 나왔습니다.
"태블릿에서 UI가 너무 작아요", "폴더블폰에서 화면이 잘려요", "구형폰에서 발열이 심해요". 박시니어 씨가 리뷰를 함께 보며 말했습니다.
"플랫폼만 구분해서는 부족해요. 디바이스별 특성도 고려해야 합니다."
디바이스별 설정은 화면 크기, 메모리 용량, 배터리 상태 등 개별 기기의 특성에 맞춰 게임을 조정하는 것입니다. 같은 iOS라도 iPhone SE와 iPad Pro는 완전히 다른 경험을 제공해야 합니다.
마치 같은 요리라도 어린이용과 성인용 분량이 다르듯, 게임도 디바이스에 맞게 조절되어야 합니다.
다음 코드를 살펴봅시다.
import 'package:flame/game.dart';
class DeviceAwareGame extends FlameGame {
late final DeviceProfile _deviceProfile;
@override
Future<void> onLoad() async {
await super.onLoad();
_deviceProfile = await _analyzeDevice();
_applyDeviceSettings();
}
Future<DeviceProfile> _analyzeDevice() async {
final screenSize = size;
final aspectRatio = screenSize.x / screenSize.y;
final isTablet = screenSize.x > 1024 || screenSize.y > 1024;
final isFoldable = aspectRatio > 2.0 || aspectRatio < 0.5;
return DeviceProfile(
screenWidth: screenSize.x,
screenHeight: screenSize.y,
isTablet: isTablet,
isFoldable: isFoldable,
uiScale: _calculateUIScale(screenSize),
);
}
double _calculateUIScale(Vector2 screenSize) {
// 기준 해상도 대비 UI 스케일 계산
const baseWidth = 1080.0;
final scale = screenSize.x / baseWidth;
return scale.clamp(0.8, 1.5); // 최소 0.8배, 최대 1.5배
}
void _applyDeviceSettings() {
// 태블릿이면 더 많은 콘텐츠 표시
if (_deviceProfile.isTablet) {
visibleEnemyCount = 20;
renderDistance = 2000;
} else {
visibleEnemyCount = 10;
renderDistance = 1000;
}
}
int visibleEnemyCount = 10;
double renderDistance = 1000;
}
class DeviceProfile {
final double screenWidth;
final double screenHeight;
final bool isTablet;
final bool isFoldable;
final double uiScale;
DeviceProfile({
required this.screenWidth,
required this.screenHeight,
required this.isTablet,
required this.isFoldable,
required this.uiScale,
});
}
김개발 씨는 사용자 리뷰를 하나씩 읽으며 머리가 복잡해졌습니다. 같은 Android라도 6인치 폰과 12인치 태블릿은 화면 크기가 두 배 이상 차이났습니다.
같은 화면 크기라도 폴더블폰은 접었을 때와 펼쳤을 때가 달랐습니다. "도대체 몇 가지 경우의 수를 고려해야 하는 거죠?" 박시니어 씨가 화이트보드에 그림을 그리며 설명했습니다.
"플랫폼은 큰 틀이고, 디바이스는 세부 조정이에요. 디바이스 프로필을 만들어서 각 기기의 특성을 파악하면 됩니다." 그렇다면 디바이스별 설정이란 정확히 무엇일까요?
쉽게 비유하자면, 디바이스별 설정은 마치 안경 맞춤과 같습니다. 같은 시력 저하라도 사람마다 얼굴 크기, 코 높이, 귀 위치가 다릅니다.
기성품 안경은 어색하지만, 맞춤 안경은 얼굴에 딱 맞습니다. 게임도 마찬가지입니다.
기기마다 다른 맞춤 설정이 필요합니다. 디바이스별 설정이 없으면 어떤 문제가 발생할까요?
첫째, UI 크기 문제입니다. 폰에서 적당한 크기의 버튼이 태블릿에서는 너무 작아집니다.
손가락으로 정확히 누르기 어려워져 사용성이 떨어집니다. 둘째, 성능 불균형입니다.
저사양 기기에 맞추면 고사양 기기에서는 화면이 허전합니다. 고사양 기기에 맞추면 저사양 기기에서는 버벅거립니다.
셋째, 폴더블 대응 실패입니다. 폴더블폰은 접으면 일반 폰, 펼치면 작은 태블릿이 됩니다.
화면 크기가 실시간으로 변하는데, 게임이 이에 대응하지 못하면 UI가 깨지거나 잘립니다. 위의 코드를 살펴보겠습니다.
DeviceProfile 클래스는 기기의 특성을 담는 그릇입니다. _analyzeDevice 메서드에서 화면 크기를 분석하여 태블릿 여부와 폴더블 여부를 판별합니다.
가로나 세로 중 하나라도 1024 픽셀을 넘으면 태블릿으로 분류합니다. 화면비가 2:1을 넘거나 1:2보다 작으면 폴더블로 분류합니다.
_calculateUIScale 메서드는 기준 해상도인 1080 픽셀 대비 현재 화면 너비의 비율을 계산합니다. 이 비율을 0.8~1.5 사이로 제한하여 UI가 너무 작거나 크지 않도록 합니다.
실제 현업에서는 어떻게 활용할까요? 예를 들어 전략 게임을 개발한다고 가정해봅시다.
태블릿에서는 넓은 화면을 활용하여 더 많은 유닛과 넓은 지도를 표시합니다. 폰에서는 화면이 작으므로 핵심 유닛만 표시하고 지도 줌을 조절합니다.
폴더블폰에서는 화면이 접히면 자동으로 레이아웃을 재배치합니다. 주의할 점도 있습니다.
디바이스 프로필 분석은 게임 초기화 시점에 한 번만 수행해야 합니다. 매 프레임마다 분석하면 성능이 저하됩니다.
단, 폴더블폰의 화면 크기 변경은 실시간으로 감지해야 합니다. Flutter의 WidgetsBindingObserver를 활용하면 화면 크기 변경 이벤트를 받을 수 있습니다.
다시 김개발 씨의 이야기로 돌아가 봅시다. 디바이스 프로필을 도입하고 UI 스케일링을 적용한 결과, 태블릿 사용자들의 불만이 사라졌습니다.
폴더블폰에서도 접고 펼칠 때 자연스럽게 화면이 재배치되었습니다. "이제야 모든 기기에서 제대로 된 경험을 제공할 수 있게 되었어요!"
실전 팁
💡 - DeviceProfile 클래스로 기기 특성을 한 곳에서 관리하세요
- UI 스케일은 0.8~1.5 범위로 제한하여 극단적인 크기 변화를 방지하세요
- 폴더블폰은 화면 크기 변경 이벤트를 실시간으로 감지하세요
6. 해상도 스케일링
마지막 최적화 작업이 남았습니다. 김개발 씨는 게임의 픽셀 아트가 어떤 기기에서는 선명하고 어떤 기기에서는 흐릿하게 보이는 문제를 발견했습니다.
"분명히 같은 이미지인데 왜 다르게 보이는 거죠?" 박시니어 씨가 설명했습니다. "해상도 스케일링을 제대로 이해해야 해요.
이게 게임 그래픽의 마지막 퍼즐 조각입니다."
해상도 스케일링은 다양한 화면 해상도와 픽셀 밀도에서 일관된 그래픽을 제공하는 기술입니다. 요즘 스마트폰은 FHD부터 QHD+까지 다양한 해상도를 가집니다.
마치 같은 그림을 작은 액자와 큰 액자에 넣을 때 비율을 맞춰야 하듯, 게임 그래픽도 화면에 맞게 조절되어야 합니다.
다음 코드를 살펴봅시다.
import 'package:flame/game.dart';
import 'package:flame/camera.dart';
class ScaledGame extends FlameGame {
// 게임 내부 논리 해상도 (고정)
static const double gameWidth = 1920;
static const double gameHeight = 1080;
@override
Future<void> onLoad() async {
await super.onLoad();
_setupCamera();
}
void _setupCamera() {
// 고정 해상도 뷰포트 설정
camera.viewport = FixedResolutionViewport(
resolution: Vector2(gameWidth, gameHeight),
);
// 레터박스 또는 필러박스 적용
// 화면비가 다르면 검은 여백 표시
}
@override
void onGameResize(Vector2 size) {
super.onGameResize(size);
// 화면 크기 변경 시 스케일 재계산
final scaleX = size.x / gameWidth;
final scaleY = size.y / gameHeight;
final scale = scaleX < scaleY ? scaleX : scaleY;
// 픽셀 아트용 정수 스케일링
if (isPixelArt) {
final intScale = scale.floor();
camera.viewfinder.zoom = intScale > 0 ? intScale.toDouble() : 1;
} else {
camera.viewfinder.zoom = scale;
}
}
bool isPixelArt = false;
}
김개발 씨는 테스트용 기기 두 대를 나란히 놓고 비교했습니다. 왼쪽 폰은 1080p, 오른쪽 폰은 1440p 해상도였습니다.
같은 캐릭터 스프라이트인데, 1440p 폰에서는 왠지 흐릿하게 보였습니다. "해상도가 높으면 더 선명해야 하는 거 아닌가요?" 박시니어 씨가 확대경으로 화면을 가리키며 설명했습니다.
"이게 바로 스케일링 아티팩트예요. 비정수 배율로 확대하면 픽셀이 뭉개지거든요." 그렇다면 해상도 스케일링이란 정확히 무엇일까요?
쉽게 비유하자면, 해상도 스케일링은 마치 복사기의 확대 축소와 같습니다. 원본 문서를 100%로 복사하면 깔끔합니다.
150%로 확대해도 비교적 깔끔합니다. 하지만 137%처럼 어정쩡한 비율로 확대하면 글자가 뭉개지고 흐릿해집니다.
게임 그래픽도 마찬가지입니다. 이 문제는 특히 픽셀 아트 게임에서 심각합니다.
픽셀 아트는 하나하나의 픽셀이 의도된 위치에 있어야 합니다. 1.7배처럼 비정수 배율로 확대하면, 일부 픽셀은 2픽셀로 늘어나고 일부는 1픽셀로 유지되어 울퉁불퉁한 외곽선이 생깁니다.
이 문제를 해결하는 방법은 두 가지입니다. 첫째, 논리 해상도 고정입니다.
게임 내부에서는 항상 1920x1080 같은 고정 해상도를 사용합니다. 실제 화면이 2560x1440이든 1280x720이든, 게임 로직은 같은 좌표계에서 동작합니다.
화면에 표시할 때만 적절한 비율로 스케일링합니다. 둘째, 정수 배율 스케일링입니다.
픽셀 아트 게임에서는 1배, 2배, 3배처럼 정수 배율로만 확대합니다. 1.5배가 필요한 상황이면 1배로 축소하거나 2배로 확대합니다.
남는 공간은 검은 여백(레터박스)으로 채웁니다. 위의 코드를 살펴보겠습니다.
FixedResolutionViewport는 Flame에서 제공하는 뷰포트로, 논리 해상도를 고정합니다. 화면 비율이 16:9가 아니어도 게임 내용은 항상 1920x1080 기준으로 렌더링됩니다.
onGameResize 메서드에서는 화면 크기가 변경될 때마다 적절한 스케일을 계산합니다. scaleX와 scaleY 중 작은 값을 선택하여 화면 밖으로 내용이 잘리지 않도록 합니다.
픽셀 아트 게임이면 floor로 내림하여 정수 배율을 적용합니다. 실제 현업에서는 어떻게 활용할까요?
예를 들어 레트로 스타일 플랫포머 게임을 개발한다고 가정해봅시다. 원본 해상도는 320x180의 저해상도 픽셀 아트입니다.
1080p 화면에서는 6배로 확대하여 1920x1080으로 표시합니다. 1440p 화면에서는 8배로 확대하고, 남는 세로 공간은 검은색 레터박스로 채웁니다.
모든 화면에서 픽셀이 선명하게 보입니다. 주의할 점도 있습니다.
레터박스가 너무 두꺼우면 플레이어가 불만을 가질 수 있습니다. 이때는 레터박스 영역에 장식적인 테두리나 게임 정보를 표시하는 방법도 있습니다.
또한 UI 요소는 게임 월드와 별도로 스케일링하여 항상 화면 가장자리에 배치되도록 하는 것이 좋습니다. 다시 김개발 씨의 이야기로 돌아가 봅시다.
논리 해상도를 고정하고 정수 배율 스케일링을 적용한 결과, 모든 기기에서 픽셀 아트가 선명하게 보이게 되었습니다. "드디어 제가 의도한 대로 그래픽이 표시되네요!" 박시니어 씨가 김개발 씨의 어깨를 두드리며 말했습니다.
"플랫폼 감지부터 해상도 스케일링까지, 이제 진정한 크로스 플랫폼 게임 개발자가 되었네요. 어떤 기기에서든 최고의 경험을 제공할 수 있게 되었어요."
실전 팁
💡 - 논리 해상도를 고정하고 FixedResolutionViewport를 사용하세요
- 픽셀 아트 게임은 반드시 정수 배율로 스케일링하세요
- 레터박스 영역에 장식 요소를 추가하면 더 세련된 느낌을 줄 수 있습니다
이상으로 학습을 마칩니다. 위 내용을 직접 코드로 작성해보면서 익혀보세요!
댓글 (0)
함께 보면 좋은 카드 뉴스
AAA급 게임 프로젝트 완벽 가이드
Flutter와 Flame 엔진을 활용하여 AAA급 퀄리티의 모바일 게임을 개발하는 전체 과정을 다룹니다. 기획부터 앱 스토어 출시까지, 실무에서 필요한 모든 단계를 이북처럼 술술 읽히는 스타일로 설명합니다.
빌드와 배포 자동화 완벽 가이드
Flutter 앱 개발에서 GitHub Actions를 활용한 CI/CD 파이프라인 구축부터 앱 스토어 자동 배포까지, 초급 개발자도 쉽게 따라할 수 있는 빌드 자동화의 모든 것을 다룹니다.
게임 분석과 메트릭스 완벽 가이드
Flutter와 Flame으로 개발한 게임의 성공을 측정하고 개선하는 방법을 배웁니다. Firebase Analytics 연동부터 A/B 테스팅, 리텐션 분석까지 데이터 기반 게임 운영의 모든 것을 다룹니다.
게임 보안과 치팅 방지 완벽 가이드
Flutter와 Flame 게임 엔진에서 클라이언트 보안부터 서버 검증까지, 치터들로부터 게임을 보호하는 핵심 기법을 다룹니다. 초급 개발자도 쉽게 따라할 수 있는 실전 보안 코드와 함께 설명합니다.
애니메이션 시스템 커스터마이징 완벽 가이드
Flutter와 Flame 게임 엔진에서 고급 애니메이션 시스템을 구현하는 방법을 다룹니다. 스켈레탈 애니메이션부터 절차적 애니메이션까지, 게임 개발에 필요한 핵심 애니메이션 기법을 실무 예제와 함께 배워봅니다.