🤖

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

⚠️

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

이미지 로딩 중...

Flame 게임 입력 처리 완벽 가이드 - 슬라이드 1/7
A

AI Generated

2025. 12. 30. · 0 Views

Flame 게임 입력 처리 완벽 가이드

Flutter의 Flame 엔진에서 터치, 드래그, 키보드 등 다양한 입력을 처리하는 방법을 배웁니다. 게임 개발에 필수적인 제스처 인식과 조이스틱 구현까지 실전 예제로 마스터하세요.


목차

  1. 터치 입력 감지
  2. 탭 이벤트 처리
  3. 드래그 처리
  4. 키보드 입력 받기
  5. 제스처 인식
  6. 조이스틱 구현

1. 터치 입력 감지

어느 날 김개발 씨가 첫 모바일 게임 프로젝트를 시작했습니다. 화면을 터치하면 캐릭터가 움직이는 간단한 게임을 만들려고 했는데, 막상 Flame 엔진 문서를 보니 TapDetector, Draggable, HasTappables 같은 낯선 용어들이 가득했습니다.

"도대체 어떻게 시작해야 하지?"

터치 입력 감지는 사용자가 화면을 터치했을 때 그 위치와 동작을 감지하는 기본 메커니즘입니다. Flame에서는 TapDetector 믹스인을 사용하여 게임 컴포넌트에 터치 기능을 추가할 수 있습니다.

마치 버튼에 클릭 이벤트를 연결하듯이, 게임 객체에 터치 반응성을 부여하는 것입니다.

다음 코드를 살펴봅시다.

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

class MyGame extends FlameGame with TapDetector {
  @override
  void onTapDown(TapDownInfo info) {
    // 터치된 위치를 가져옵니다
    final touchPosition = info.eventPosition.game;
    print('터치 위치: ${touchPosition.x}, ${touchPosition.y}');

    // 터치 위치에 효과 생성
    add(EffectComponent(position: touchPosition));
  }

  @override
  void onTapUp(TapUpInfo info) {
    print('터치 해제됨');
  }
}

김개발 씨는 Flame 게임 개발 강의를 듣기 시작한 지 일주일 된 주니어 개발자입니다. Flutter로 앱은 만들어봤지만, 게임 엔진은 처음이었습니다.

오늘은 가장 기본이 되는 터치 입력을 배우는 날입니다. 강사 박시니어 씨가 화면을 가리키며 설명합니다.

"게임에서 입력 처리는 마치 대화와 같아요. 사용자가 화면을 터치하면, 게임이 그 신호를 받아서 반응하는 거죠." 그렇다면 터치 입력 감지란 정확히 무엇일까요?

쉽게 비유하자면, 터치 입력 감지는 마치 현관문의 초인종과 같습니다. 누군가 초인종을 누르면 집 안에서 소리가 울리듯이, 사용자가 화면을 터치하면 게임이 그 신호를 받습니다.

이때 어느 위치를 눌렀는지, 얼마나 세게 눌렀는지 같은 정보도 함께 전달됩니다. 게임은 이 정보를 바탕으로 캐릭터를 움직이거나 아이템을 생성하는 등의 반응을 보입니다.

터치 감지가 없던 시절에는 어땠을까요? 초창기 모바일 게임들은 버튼 기반 UI를 사용했습니다.

개발자들은 화면 곳곳에 보이지 않는 버튼 영역을 만들고, 각 버튼마다 별도의 이벤트 핸들러를 작성해야 했습니다. 코드가 복잡해지고, 자유로운 터치 인터랙션을 구현하기 어려웠습니다.

더 큰 문제는 성능이었습니다. 수많은 버튼 위젯을 관리하다 보면 게임이 버벅거리기 시작했습니다.

바로 이런 문제를 해결하기 위해 Flame의 TapDetector가 등장했습니다. TapDetector를 사용하면 게임 전체 화면에서 터치를 감지할 수 있습니다.

별도의 UI 위젯 없이도 순수하게 좌표 기반으로 입력을 처리할 수 있습니다. 또한 터치 시작, 터치 해제, 터치 취소 등 다양한 상태를 구분할 수 있어서 섬세한 제어가 가능합니다.

무엇보다 게임 루프와 통합되어 있어서 성능 오버헤드가 거의 없습니다. 위의 코드를 한 줄씩 살펴보겠습니다.

먼저 3번째 줄을 보면 FlameGame에 with TapDetector를 믹스인하고 있습니다. 이것이 핵심입니다.

이 한 줄로 게임 전체가 터치 가능한 캔버스가 됩니다. 다음으로 6번째 줄의 onTapDown 메서드에서는 터치가 시작될 때의 동작을 정의합니다.

info.eventPosition.game을 통해 게임 좌표계에서의 정확한 터치 위치를 얻을 수 있습니다. 마지막으로 15번째 줄의 onTapUp에서는 손가락을 뗐을 때의 처리를 담당합니다.

실제 현업에서는 어떻게 활용할까요? 예를 들어 캐주얼 슈팅 게임을 개발한다고 가정해봅시다.

플레이어가 화면을 터치한 위치로 총알이 발사되어야 합니다. onTapDown에서 터치 위치를 감지하고, 그 방향으로 Bullet 컴포넌트를 생성하여 add()로 게임에 추가하면 됩니다.

Supercell의 Brawl Stars 같은 게임도 이런 방식으로 조작을 구현했습니다. 하지만 주의할 점도 있습니다.

초보 개발자들이 흔히 하는 실수 중 하나는 모든 터치 이벤트에서 복잡한 로직을 실행하는 것입니다. 터치 이벤트는 초당 수십 번 발생할 수 있기 때문에, 무거운 계산을 넣으면 게임이 느려집니다.

따라서 터치 이벤트에서는 좌표 저장이나 플래그 설정 같은 가벼운 작업만 하고, 실제 게임 로직은 update() 메서드에서 처리해야 합니다. 다시 김개발 씨의 이야기로 돌아가 봅시다.

박시니어 씨의 설명을 들은 김개발 씨는 고개를 끄덕였습니다. "아, 그래서 믹스인을 사용하는 거였군요!" 터치 입력 감지를 제대로 이해하면 더 직관적이고 반응성 좋은 게임을 만들 수 있습니다.

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

실전 팁

💡 - eventPosition.game을 사용하여 게임 좌표계 기준으로 위치를 받으세요 (화면 좌표가 아닌)

  • 터치 이벤트는 자주 발생하므로 무거운 로직은 update()에서 처리하세요
  • 디버깅 시 print로 터치 좌표를 출력하면 문제를 쉽게 찾을 수 있습니다

2. 탭 이벤트 처리

김개발 씨가 첫 번째 미니게임을 만들었습니다. 화면 어디든 터치하면 별이 생기는 간단한 게임이었죠.

그런데 QA 팀에서 피드백이 왔습니다. "버튼을 눌렀는데 배경도 같이 반응해요.

특정 객체만 터치 가능하게 할 수는 없나요?"

탭 이벤트 처리는 개별 게임 컴포넌트에 터치 기능을 부여하는 방식입니다. Tappable 믹스인을 컴포넌트에 추가하면 해당 객체만 터치에 반응하게 됩니다.

마치 웹페이지의 특정 버튼만 클릭 가능한 것처럼, 게임 내 특정 스프라이트나 영역만 터치 가능하게 만드는 것입니다.

다음 코드를 살펴봅시다.

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

class TappableButton extends SpriteComponent with Tappable {
  TappableButton() : super(size: Vector2(100, 50));

  @override
  bool onTapDown(TapDownInfo info) {
    // 이 컴포넌트가 터치되었을 때만 실행됩니다
    print('버튼이 눌렸습니다!');
    scale = Vector2.all(0.9); // 눌림 효과
    return true; // 이벤트 전파 중단
  }

  @override
  bool onTapUp(TapUpInfo info) {
    scale = Vector2.all(1.0); // 원래 크기로
    return true;
  }
}

김개발 씨는 문제를 해결하기 위해 선배에게 물어봤습니다. 박시니어 씨가 화면을 보더니 말합니다.

"아, 지금은 게임 전체에 TapDetector를 썼네요. 개별 컴포넌트에 반응이 필요하면 Tappable을 써야 해요." 그렇다면 탭 이벤트 처리와 앞서 배운 터치 입력 감지는 어떻게 다를까요?

쉽게 비유하자면, TapDetector는 방 전체에 설치된 모션 센서 같은 것입니다. 어디서든 움직임이 감지되면 반응합니다.

반면 Tappable은 특정 사물에만 부착된 버튼입니다. 그 사물을 만졌을 때만 반응하죠.

게임 화면에 버튼이 10개 있다면, 각 버튼마다 Tappable을 붙여서 독립적으로 동작하게 만듭니다. 전역 터치만 사용하면 어떤 문제가 생길까요?

게임 화면에 시작 버튼, 설정 버튼, 종료 버튼이 있다고 해봅시다. TapDetector만 사용하면 터치 좌표를 받아서 직접 "이 좌표가 버튼 영역 안에 있나?"를 계산해야 합니다.

버튼이 움직이거나 회전하면 계산이 복잡해집니다. 또한 여러 객체가 겹쳐있을 때 어느 것을 먼저 처리할지도 직접 관리해야 합니다.

코드가 점점 스파게티처럼 엉켜갑니다. 바로 이런 문제를 해결하기 위해 Tappable 믹스인이 등장했습니다.

Tappable을 사용하면 각 컴포넌트가 자신의 영역을 자동으로 판단합니다. 회전이나 크기 변경이 있어도 Flame이 알아서 계산해줍니다.

또한 z-index 순서대로 터치 우선순위가 결정되어 겹침 문제도 자동으로 해결됩니다. 무엇보다 객체지향적으로 각 버튼이 자신의 동작을 캡슐화할 수 있습니다.

위의 코드를 한 줄씩 살펴보겠습니다. 먼저 5번째 줄을 보면 SpriteComponent에 with Tappable을 믹스인하고 있습니다.

이제 이 컴포넌트는 독립적인 터치 영역이 됩니다. 9번째 줄의 onTapDown은 이 컴포넌트의 영역 내에서 터치가 발생했을 때만 호출됩니다.

12번째 줄의 scale 조작으로 시각적 피드백을 줍니다. 마지막으로 13번째 줄의 return true가 중요합니다.

true를 반환하면 이벤트가 하위 레이어로 전파되지 않습니다. 실제 현업에서는 어떻게 활용할까요?

예를 들어 타워 디펜스 게임을 개발한다고 가정해봅시다. 화면에 타워들이 여러 개 배치되어 있고, 각 타워를 터치하면 업그레이드 메뉴가 나와야 합니다.

각 Tower 컴포넌트에 Tappable을 믹스인하고, onTapDown에서 자신의 업그레이드 UI를 표시하면 됩니다. Supercell의 Clash of Clans 같은 게임이 이런 패턴을 사용합니다.

하지만 주의할 점도 있습니다. 초보 개발자들이 흔히 하는 실수는 Tappable을 추가했는데도 반응이 없는 경우입니다.

이유는 게임 클래스에 HasTappables 믹스인을 추가하지 않았기 때문입니다. FlameGame with HasTappables로 선언해야 Tappable 컴포넌트들이 작동합니다.

또 다른 실수는 컴포넌트의 크기를 설정하지 않는 것입니다. size가 0이면 터치 영역도 0이 되어 아무리 눌러도 반응하지 않습니다.

다시 김개발 씨의 이야기로 돌아가 봅시다. 박시니어 씨의 조언대로 코드를 수정한 김개발 씨는 신기한 듯 화면을 터치해봤습니다.

"와, 이제 버튼만 정확하게 반응하네요!" 탭 이벤트 처리를 제대로 이해하면 더 정교하고 직관적인 UI를 가진 게임을 만들 수 있습니다. 여러분도 오늘 배운 내용을 실제 프로젝트에 적용해 보세요.

실전 팁

💡 - 게임 클래스에 HasTappables 믹스인을 반드시 추가하세요

  • 컴포넌트의 size를 명시적으로 설정해야 터치 영역이 생깁니다
  • return true로 이벤트 전파를 제어하여 하위 레이어 터치를 막을 수 있습니다

3. 드래그 처리

김개발 씨가 퍼즐 게임을 만들고 있었습니다. 블록을 드래그해서 이동시키는 기능이 필요했는데, 터치 시작과 끝만 감지하는 Tappable로는 부족했습니다.

"드래그하는 동안 계속 위치를 추적하려면 어떻게 해야 하지?"

드래그 처리는 사용자가 화면을 터치한 채로 움직일 때 연속적으로 위치를 추적하는 기능입니다. Draggable 믹스인을 사용하면 드래그 시작, 진행, 종료 이벤트를 받을 수 있습니다.

마치 마우스를 클릭한 채로 움직이는 것처럼, 손가락의 이동 경로를 실시간으로 추적할 수 있습니다.

다음 코드를 살펴봅시다.

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

class DraggableBox extends PositionComponent with Draggable {
  DraggableBox() : super(size: Vector2.all(80));

  @override
  bool onDragStart(DragStartInfo info) {
    // 드래그 시작 시 투명도 변경
    opacity = 0.7;
    return true;
  }

  @override
  bool onDragUpdate(DragUpdateInfo info) {
    // 드래그 중 위치 업데이트 (delta는 이동 거리)
    position += info.delta.game;
    return true;
  }

  @override
  bool onDragEnd(DragEndInfo info) {
    opacity = 1.0; // 원래 투명도로
    return true;
  }
}

김개발 씨는 고민에 빠졌습니다. onTapDown과 onTapUp 사이의 움직임을 어떻게 감지할까요?

혼자 머리를 싸매고 있는데 박시니어 씨가 지나가다 물었습니다. "드래그 기능 필요해요?

그럼 Draggable 써보세요." 그렇다면 드래그 처리란 정확히 무엇일까요? 쉽게 비유하자면, 드래그는 마치 종이 위에 연필로 선을 그리는 것과 같습니다.

연필을 종이에 대는 순간이 드래그 시작이고, 연필을 움직이는 동안 계속 위치가 기록되며, 연필을 떼는 순간이 드래그 종료입니다. 게임에서도 마찬가지로 손가락이 화면에 닿는 순간부터 떼는 순간까지 모든 이동 경로를 추적할 수 있습니다.

단순 터치만으로는 어떤 제약이 있을까요? Tappable은 눌렀다/뗐다만 알 수 있습니다.

예를 들어 체스 게임에서 말을 다른 칸으로 옮기고 싶다면, 어느 칸에서 어느 칸으로 이동했는지 알아야 합니다. 시작 위치와 끝 위치만으로는 중간 과정을 알 수 없어서, 사용자가 어디로 드래그하는 중인지 시각적 피드백을 줄 수 없습니다.

또한 스와이프 속도나 방향 같은 제스처 정보도 얻을 수 없습니다. 바로 이런 문제를 해결하기 위해 Draggable 믹스인이 등장했습니다.

Draggable을 사용하면 드래그의 전체 생명주기를 관리할 수 있습니다. 시작할 때 객체를 하이라이트하고, 진행 중에는 손가락을 따라 움직이며, 끝날 때 최종 위치를 확정할 수 있습니다.

또한 delta 값을 통해 프레임마다 얼마나 움직였는지 알 수 있어서 부드러운 애니메이션이 가능합니다. 무엇보다 멀티터치 환경에서도 각 손가락의 드래그를 독립적으로 처리할 수 있습니다.

위의 코드를 한 줄씩 살펴보겠습니다. 먼저 4번째 줄을 보면 PositionComponent에 with Draggable을 믹스인하고 있습니다.

이제 이 컴포넌트는 드래그 가능한 객체가 됩니다. 8번째 줄의 onDragStart는 드래그가 시작될 때 한 번 호출됩니다.

여기서 시각적 피드백을 줍니다. 15번째 줄의 onDragUpdate가 핵심입니다.

드래그하는 동안 계속 호출되며, info.delta.game은 이전 프레임 대비 이동량을 나타냅니다. 이것을 position에 더하면 손가락을 따라 움직입니다.

22번째 줄의 onDragEnd는 드래그가 끝날 때 정리 작업을 합니다. 실제 현업에서는 어떻게 활용할까요?

예를 들어 카드 배틀 게임을 개발한다고 가정해봅시다. 플레이어가 손패의 카드를 드래그해서 필드에 내려놓으면 소환됩니다.

onDragStart에서 카드를 확대하고, onDragUpdate에서 카드가 손가락을 따라다니게 하며, onDragEnd에서 드롭 위치가 유효한지 검사하여 소환하거나 원래 자리로 되돌립니다. Hearthstone 같은 게임이 이런 방식을 사용합니다.

하지만 주의할 점도 있습니다. 초보 개발자들이 흔히 하는 실수는 onDragUpdate에서 position을 직접 대입하는 것입니다.

position = info.eventPosition.game처럼 하면 손가락 위치에 객체가 순간이동합니다. 올바른 방법은 position += info.delta.game으로 이동량만큼 더하는 것입니다.

또 다른 실수는 게임 클래스에 HasDraggables 믹스인을 추가하지 않는 것입니다. FlameGame with HasDraggables로 선언해야 작동합니다.

다시 김개발 씨의 이야기로 돌아가 봅시다. 박시니어 씨의 조언대로 Draggable을 적용한 김개발 씨는 감탄했습니다.

"와, 블록이 손가락을 부드럽게 따라다니네요!" 드래그 처리를 제대로 이해하면 더 직관적이고 인터랙티브한 게임을 만들 수 있습니다. 여러분도 오늘 배운 내용을 실제 프로젝트에 적용해 보세요.

실전 팁

💡 - 게임 클래스에 HasDraggables 믹스인을 반드시 추가하세요

  • delta 값을 사용하여 점진적으로 위치를 변경하세요 (절대 위치 대입 금지)
  • 드래그 끝에서 유효성 검사를 하고, 실패 시 원래 위치로 되돌리는 로직을 추가하세요

4. 키보드 입력 받기

김개발 씨가 PC용 게임을 개발하게 되었습니다. 모바일만 생각했던 터라 키보드 입력은 생각지도 못했죠.

"WASD로 캐릭터를 움직이게 하려면 어떻게 해야 하지?" 막막한 순간, 박시니어 씨가 문서 링크를 보내줬습니다.

키보드 입력 받기는 PC나 웹 플랫폼에서 키보드의 키 눌림과 해제를 감지하는 기능입니다. KeyboardEvents 믹스인을 사용하면 특정 키의 상태를 추적하고, 조합키도 처리할 수 있습니다.

마치 이벤트 리스너를 등록하듯이, 원하는 키에 대한 동작을 정의할 수 있습니다.

다음 코드를 살펴봅시다.

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

class MyGame extends FlameGame with KeyboardEvents {
  final Set<LogicalKeyboardKey> pressedKeys = {};

  @override
  KeyEventResult onKeyEvent(
    RawKeyEvent event,
    Set<LogicalKeyboardKey> keysPressed,
  ) {
    // 눌린 키 저장
    pressedKeys.addAll(keysPressed);

    if (event is RawKeyDownEvent) {
      if (event.logicalKey == LogicalKeyboardKey.space) {
        handleJump(); // 스페이스바로 점프
      }
    }
    return KeyEventResult.handled;
  }

  @override
  void update(double dt) {
    // WASD 이동 처리
    if (pressedKeys.contains(LogicalKeyboardKey.keyW)) {
      player.moveUp(dt);
    }
    if (pressedKeys.contains(LogicalKeyboardKey.keyA)) {
      player.moveLeft(dt);
    }
    super.update(dt);
  }
}

김개발 씨는 문서를 읽으며 고개를 갸우뚱했습니다. "onKeyEvent라는 메서드가 있네?

그런데 keysPressed는 또 뭐지?" 옆에서 지켜보던 박시니어 씨가 설명을 시작했습니다. "터치와 달리 키보드는 여러 키를 동시에 누를 수 있어요.

그래서 조금 다르게 처리해야 합니다." 그렇다면 키보드 입력 처리는 터치 입력과 어떻게 다를까요? 쉽게 비유하자면, 터치는 화면의 한 점을 가리키는 것과 같습니다.

한 번에 하나의 좌표만 중요하죠. 반면 키보드는 피아노와 같습니다.

여러 건반을 동시에 누를 수 있고, 각 건반마다 다른 소리가 납니다. 게임에서도 W(앞으로)와 A(왼쪽)를 동시에 눌러서 대각선으로 움직이는 것처럼, 여러 키의 조합이 중요합니다.

키보드 입력 없이 PC 게임을 만들면 어떻게 될까요? 화면에 방향키 버튼을 만들어서 마우스로 클릭하게 할 수도 있습니다.

하지만 PC 게이머들은 키보드 조작에 익숙합니다. 마우스로 버튼을 클릭하는 것은 느리고 불편합니다.

또한 동시 입력이 어렵습니다. 점프하면서 앞으로 달리는 동작을 마우스로는 구현하기 힘듭니다.

게임의 속도감과 몰입감이 크게 떨어지게 됩니다. 바로 이런 문제를 해결하기 위해 Flame의 KeyboardEvents가 등장했습니다.

KeyboardEvents를 사용하면 모든 키 입력을 실시간으로 감지할 수 있습니다. keysPressed 세트를 통해 현재 눌려있는 모든 키를 확인할 수 있어서, WASD 동시 입력 같은 복잡한 조작도 쉽게 처리됩니다.

또한 키가 눌린 순간(KeyDown)과 떼어진 순간(KeyUp)을 구분하여, 한 번만 실행되어야 하는 동작(총 쏘기)과 계속 실행되어야 하는 동작(이동)을 다르게 처리할 수 있습니다. 위의 코드를 한 줄씩 살펴보겠습니다.

먼저 5번째 줄을 보면 FlameGame에 with KeyboardEvents를 믹스인하고 있습니다. 이제 키보드 이벤트를 받을 준비가 되었습니다.

6번째 줄의 pressedKeys 세트는 현재 눌려있는 키들을 저장합니다. 9번째 줄의 onKeyEvent 메서드가 핵심입니다.

키 이벤트가 발생할 때마다 호출되며, keysPressed 매개변수로 현재 눌려있는 모든 키를 받습니다. 16번째 줄처럼 RawKeyDownEvent를 체크하면 키가 처음 눌린 순간만 감지할 수 있습니다.

27번째 줄의 update() 메서드에서는 pressedKeys를 확인하여 계속 눌려있는 동안 반복 실행되는 동작을 처리합니다. 실제 현업에서는 어떻게 활용할까요?

예를 들어 2D 플랫포머 게임을 개발한다고 가정해봅시다. 플레이어는 화살표 키나 WASD로 움직이고, 스페이스바로 점프하며, Shift를 누르면 달리기 속도가 빨라집니다.

onKeyEvent에서 Space를 감지하여 점프 동작을 한 번 실행하고, update()에서는 W/A/S/D와 Shift의 조합을 확인하여 매 프레임마다 이동 속도를 계산합니다. Celeste 같은 인디 게임이 이런 방식으로 정교한 조작감을 구현했습니다.

하지만 주의할 점도 있습니다. 초보 개발자들이 흔히 하는 실수는 onKeyEvent에서 모든 이동 로직을 처리하는 것입니다.

이벤트는 불규칙하게 발생하므로 움직임이 끊겨 보입니다. 올바른 방법은 onKeyEvent에서는 상태만 변경하고, update()에서 dt를 곱해서 부드러운 이동을 구현하는 것입니다.

또 다른 실수는 플랫폼 차이를 고려하지 않는 것입니다. 웹에서는 일부 키 조합이 브라우저 단축키와 충돌할 수 있으므로, KeyEventResult.handled를 반환하여 이벤트 전파를 막아야 합니다.

다시 김개발 씨의 이야기로 돌아가 봅시다. 박시니어 씨의 설명을 들은 김개발 씨는 코드를 작성하기 시작했습니다.

"아, keysPressed로 동시 입력을 확인하는 거였군요!" 키보드 입력 처리를 제대로 이해하면 PC 플랫폼에 최적화된 게임을 만들 수 있습니다. 여러분도 오늘 배운 내용을 실제 프로젝트에 적용해 보세요.

실전 팁

💡 - 한 번만 실행할 동작은 onKeyEvent에서, 계속 실행할 동작은 **update()**에서 처리하세요

  • keysPressed 세트로 현재 눌린 모든 키를 확인하여 조합키를 쉽게 처리할 수 있습니다
  • 웹 플랫폼에서는 KeyEventResult.handled를 반환하여 브라우저 기본 동작을 막으세요

5. 제스처 인식

김개발 씨가 리듬 게임을 개발하고 있었습니다. 화면을 빠르게 스와이프하면 콤보가 연결되는 기능을 넣고 싶었는데, 단순 드래그로는 속도와 방향을 정확히 알 수 없었습니다.

"스와이프 제스처를 어떻게 인식하지?"

제스처 인식은 터치 입력의 패턴을 분석하여 스와이프, 핀치, 회전 같은 복잡한 동작을 감지하는 기능입니다. Flutter의 GestureDetector를 Flame과 통합하여 사용하면 방향, 속도, 거리 등의 정보를 얻을 수 있습니다.

마치 손짓 언어를 이해하듯이, 사용자의 의도를 파악할 수 있습니다.

다음 코드를 살펴봅시다.

import 'package:flame/game.dart';
import 'package:flutter/gestures.dart';

class MyGame extends FlameGame {
  @override
  void onLoad() {
    // PanGestureRecognizer로 스와이프 감지
    final panRecognizer = PanGestureRecognizer()
      ..onUpdate = (details) {
        // 스와이프 속도와 방향
        final velocity = details.delta;
        if (velocity.dx.abs() > velocity.dy.abs()) {
          // 좌우 스와이프
          handleHorizontalSwipe(velocity.dx);
        } else {
          // 상하 스와이프
          handleVerticalSwipe(velocity.dy);
        }
      };

    // 제스처 인식기 등록
    addGestureRecognizer(panRecognizer);
  }

  void handleHorizontalSwipe(double velocity) {
    if (velocity > 50) {
      print('오른쪽으로 빠른 스와이프!');
    } else if (velocity < -50) {
      print('왼쪽으로 빠른 스와이프!');
    }
  }
}

김개발 씨는 검색을 시작했습니다. "Flame gesture recognition"이라고 검색하니 여러 자료가 나왔습니다.

그런데 대부분 Flutter의 GestureDetector 얘기였습니다. "게임 엔진인데 Flutter 위젯을 써도 되나?" 궁금해하던 차에 박시니어 씨가 말했습니다.

"Flame은 Flutter 위에서 돌아가니까, Flutter의 제스처 시스템을 그대로 쓸 수 있어요." 그렇다면 제스처 인식이란 정확히 무엇일까요? 쉽게 비유하자면, 제스처 인식은 마치 수화 통역사와 같습니다.

단순히 손의 위치만 보는 게 아니라, 움직임의 패턴을 분석하여 의미를 파악합니다. 빠르게 왼쪽으로 휘두르면 "이전", 천천히 위로 밀면 "스크롤 업" 같은 식이죠.

게임에서도 같은 터치 입력이라도 속도, 방향, 경로에 따라 다른 동작으로 해석할 수 있습니다. 단순 드래그만으로는 어떤 한계가 있을까요?

Draggable은 위치 변화만 알려줍니다. 사용자가 얼마나 빠르게 움직였는지, 어떤 패턴으로 움직였는지는 직접 계산해야 합니다.

예를 들어 Fruit Ninja처럼 과일을 베는 게임을 만들 때, 단순히 터치만 감지하면 안 됩니다. 빠른 스와이프인지 느린 드래그인지, 직선인지 곡선인지를 구분해야 제대로 된 베기 판정을 할 수 있습니다.

이런 계산을 직접 하면 코드가 복잡해집니다. 바로 이런 문제를 해결하기 위해 Flutter의 GestureRecognizer를 활용합니다.

GestureRecognizer는 터치 이벤트를 분석하여 고수준의 제스처로 변환해줍니다. PanGestureRecognizer는 드래그와 스와이프를 감지하고, ScaleGestureRecognizer는 핀치 줌을 인식합니다.

velocity 정보를 통해 속도를 알 수 있고, distance를 통해 이동 거리를 파악할 수 있습니다. 무엇보다 여러 제스처를 동시에 인식하고 우선순위를 관리하는 복잡한 로직이 이미 구현되어 있습니다.

위의 코드를 한 줄씩 살펴보겠습니다. 먼저 8번째 줄을 보면 PanGestureRecognizer를 생성하고 있습니다.

Pan은 드래그와 스와이프를 통칭하는 제스처입니다. 9번째 줄의 onUpdate 콜백은 제스처가 진행되는 동안 계속 호출됩니다.

11번째 줄의 details.delta는 이전 프레임 대비 이동량입니다. 12번째 줄에서 dx와 dy의 절댓값을 비교하여 수평/수직 스와이프를 구분합니다.

22번째 줄의 addGestureRecognizer로 제스처 인식기를 게임에 등록합니다. 26번째 줄에서는 속도(velocity)가 임계값(50)을 넘는지 확인하여 빠른 스와이프만 인식합니다.

실제 현업에서는 어떻게 활용할까요? 예를 들어 카드 게임을 개발한다고 가정해봅시다.

손패의 카드를 위로 빠르게 스와이프하면 버리고, 좌우로 스와이프하면 순서를 바꿉니다. PanGestureRecognizer의 onEnd 콜백에서 최종 velocity를 확인하여 스와이프 방향과 속도를 판정합니다.

velocity.dy < -500이면 위로 빠르게 스와이프한 것으로 판단하여 카드를 버립니다. Hearthstone의 모바일 버전이 이런 방식을 사용합니다.

하지만 주의할 점도 있습니다. 초보 개발자들이 흔히 하는 실수는 GestureDetector 위젯을 게임 위에 덮어씌우는 것입니다.

이렇게 하면 작동은 하지만 성능이 떨어집니다. 올바른 방법은 addGestureRecognizer로 인식기만 등록하는 것입니다.

또 다른 실수는 너무 낮은 임계값을 설정하는 것입니다. 사용자가 의도하지 않은 작은 움직임까지 제스처로 인식되면 오작동이 발생합니다.

실제 디바이스에서 테스트하며 적절한 값을 찾아야 합니다. 다시 김개발 씨의 이야기로 돌아가 봅시다.

박시니어 씨의 조언대로 GestureRecognizer를 적용한 김개발 씨는 신기해했습니다. "와, 속도와 방향을 자동으로 계산해주네요!" 제스처 인식을 제대로 이해하면 더 풍부하고 직관적인 입력 시스템을 가진 게임을 만들 수 있습니다.

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

실전 팁

💡 - PanGestureRecognizer로 스와이프, ScaleGestureRecognizer로 핀치 줌을 인식하세요

  • velocity 임계값을 설정하여 빠른 제스처만 인식하도록 필터링하세요
  • 실제 디바이스에서 테스트하여 적절한 감도를 찾으세요 (에뮬레이터와 다름)

6. 조이스틱 구현

김개발 씨가 액션 게임을 개발하고 있었습니다. 모바일에서 WASD 같은 키보드가 없으니 캐릭터를 자유롭게 움직이기 어려웠습니다.

"화면에 가상 조이스틱을 만들려면 어떻게 해야 하지?" 다시 박시니어 씨를 찾아갔습니다.

조이스틱 구현은 화면에 가상의 조이스틱 UI를 만들어 360도 방향 입력을 받는 기능입니다. JoystickComponent를 사용하면 스틱의 위치와 방향을 추적하여 캐릭터를 정밀하게 조작할 수 있습니다.

마치 게임 패드의 아날로그 스틱처럼, 미세한 조작도 가능합니다.

다음 코드를 살펴봅시다.

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

class MyGame extends FlameGame with HasDraggables {
  late JoystickComponent joystick;

  @override
  Future<void> onLoad() async {
    // 조이스틱 생성
    joystick = JoystickComponent(
      knob: CircleComponent(radius: 30, paint: Paint()..color = Color(0xFF0000FF)),
      background: CircleComponent(radius: 80, paint: Paint()..color = Color(0x88888888)),
      margin: const EdgeInsets.only(left: 40, bottom: 40),
    );
    add(joystick);
  }

  @override
  void update(double dt) {
    // 조이스틱 방향과 강도 읽기
    if (!joystick.delta.isZero()) {
      final direction = joystick.relativeDelta; // -1.0 ~ 1.0
      player.position += direction * player.speed * dt;
      player.angle = direction.angleToSigned(Vector2(1, 0));
    }
    super.update(dt);
  }
}

김개발 씨는 화면 구석에 동그란 조이스틱을 그려야겠다고 생각했습니다. 하지만 드래그 감지, 각도 계산, 거리 제한 같은 것들을 직접 구현하려니 막막했습니다.

박시니어 씨가 화면을 보더니 말했습니다. "Flame에 JoystickComponent라는 게 있어요.

이미 다 만들어져 있답니다." 그렇다면 조이스틱 구현이란 정확히 무엇일까요? 쉽게 비유하자면, 조이스틱은 마치 나침반과 같습니다.

중심에서 어느 방향으로 얼마나 기울어졌는지를 알려줍니다. 위쪽으로 많이 기울이면 북쪽으로 빠르게, 살짝만 기울이면 천천히 움직이는 식이죠.

게임에서도 스틱을 얼마나 밀었는지에 따라 캐릭터의 이동 속도와 방향이 결정됩니다. 조이스틱 없이 모바일 게임을 만들면 어떻게 될까요?

화면 왼쪽을 터치하면 왼쪽으로, 오른쪽을 터치하면 오른쪽으로 움직이는 버튼 방식을 쓸 수 있습니다. 하지만 대각선 이동이 어렵고, 속도 조절이 불가능합니다.

또는 터치한 방향으로 캐릭터가 움직이게 할 수도 있지만, 이러면 화면 전체가 입력 영역이 되어 UI 배치가 어려워집니다. 무엇보다 게이머들이 익숙한 조이스틱 조작감을 제공할 수 없습니다.

바로 이런 문제를 해결하기 위해 Flame의 JoystickComponent가 등장했습니다. JoystickComponent는 완전한 가상 조이스틱 기능을 제공합니다.

knob(손잡이)과 background(배경) 스타일을 커스터마이즈할 수 있고, 화면의 원하는 위치에 배치할 수 있습니다. delta 속성으로 중심으로부터 얼마나 떨어졌는지 픽셀 단위로 알 수 있고, relativeDelta는 이를 -1.0~1.0 범위로 정규화하여 제공합니다.

무엇보다 자동으로 스틱이 배경 영역을 벗어나지 않도록 제한하고, 손을 떼면 중심으로 돌아오는 동작까지 구현되어 있습니다. 위의 코드를 한 줄씩 살펴보겠습니다.

먼저 11번째 줄을 보면 JoystickComponent를 생성하고 있습니다. knob은 사용자가 드래그하는 작은 원이고, background는 조이스틱의 이동 범위를 나타내는 큰 원입니다.

14번째 줄의 margin으로 화면 왼쪽 아래에 배치합니다. 22번째 줄에서 **joystick.delta.isZero()**로 조이스틱이 움직였는지 확인합니다.

0이면 중심에 있다는 뜻입니다. 23번째 줄의 relativeDelta가 핵심입니다.

이것은 Vector2 타입으로, x와 y가 각각 -1.0~1.0 범위입니다. 24번째 줄에서 이것을 이동 속도에 곱하면 조이스틱을 기울인 만큼 빠르게 움직입니다.

25번째 줄은 캐릭터가 이동 방향을 바라보도록 회전시킵니다. 실제 현업에서는 어떻게 활용할까요?

예를 들어 탑다운 슈팅 게임을 개발한다고 가정해봅시다. 화면 왼쪽에는 이동용 조이스틱, 오른쪽에는 조준용 조이스틱을 배치합니다.

왼쪽 조이스틱의 relativeDelta로 캐릭터를 움직이고, 오른쪽 조이스틱의 방향으로 총알을 발사합니다. Brawl Stars 같은 게임이 이런 듀얼 조이스틱 방식을 사용합니다.

각 조이스틱을 독립적으로 add()하면 자동으로 멀티터치가 지원됩니다. 하지만 주의할 점도 있습니다.

초보 개발자들이 흔히 하는 실수는 deltarelativeDelta를 혼동하는 것입니다. delta는 픽셀 단위의 절대값이라서 조이스틱 크기에 따라 달라집니다.

이동 계산에는 항상 relativeDelta를 사용해야 일관된 속도를 얻을 수 있습니다. 또 다른 실수는 update()에서 dt를 곱하지 않는 것입니다.

dt를 곱해야 프레임레이트와 무관하게 일정한 속도로 움직입니다. 다시 김개발 씨의 이야기로 돌아가 봅시다.

박시니어 씨의 조언대로 JoystickComponent를 추가한 김개발 씨는 감탄했습니다. "와, 이렇게 간단하게 조이스틱이 만들어지다니!" 조이스틱 구현을 제대로 이해하면 모바일 게임에 콘솔 게임 수준의 정교한 조작감을 부여할 수 있습니다.

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

실전 팁

💡 - 이동 계산에는 delta가 아닌 relativeDelta를 사용하세요 (정규화된 값)

  • 반드시 dt를 곱해서 프레임레이트 독립적인 움직임을 만드세요
  • 듀얼 조이스틱이 필요하면 JoystickComponent를 두 개 생성하여 각각 add()하세요

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

#Flutter#Flame#GameInput#TouchEvent#GestureDetection#Flutter,Flame,Game

댓글 (0)

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

함께 보면 좋은 카드 뉴스

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

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

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

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

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

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

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

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

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

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