🤖

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

⚠️

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

이미지 로딩 중...

Flame 게임 물리 엔진 완벽 가이드 - 슬라이드 1/7
A

AI Generated

2025. 12. 30. · 0 Views

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

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


목차

  1. Forge2D 소개
  2. 물리 월드 생성
  3. Body와 Fixture
  4. 중력과 힘 적용
  5. 조인트와 제약
  6. 물리 충돌 처리

1. Forge2D 소개

김게임 씨는 Flutter로 첫 모바일 게임을 만들고 있습니다. 공이 화면 위에서 떨어지는 간단한 게임인데, 문이 없는 방처럼 공이 화면 밖으로 빠져나가 버립니다.

"물리 법칙이 전혀 적용되지 않네요." 선배 박물리 씨가 다가와 말합니다. "Forge2D를 사용해 보세요."

Forge2D는 Flame 게임 엔진에서 사용하는 2D 물리 시뮬레이션 라이브러리입니다. 마치 현실 세계의 중력, 마찰력, 충돌을 게임 세계에 그대로 옮겨놓은 것과 같습니다.

Box2D의 Dart 포팅 버전으로, 게임 오브젝트에 현실적인 물리 법칙을 쉽게 적용할 수 있습니다.

다음 코드를 살펴봅시다.

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

// Forge2D를 사용하는 게임 클래스
class MyPhysicsGame extends Forge2DGame {
  MyPhysicsGame() : super(gravity: Vector2(0, 10));

  @override
  Future<void> onLoad() async {
    await super.onLoad();
    // 물리 월드가 자동으로 생성됩니다
    // gravity: 중력 방향과 크기 (x: 0, y: 10)
  }
}

김게임 씨는 게임 개발 3개월 차 주니어 개발자입니다. Flutter로 멋진 UI는 만들 수 있지만, 게임은 완전히 다른 세계였습니다.

공을 화면에 그리고 아래로 움직이게 하는 건 쉬웠습니다. 하지만 바닥에 닿으면 튕겨야 하는데, 그냥 뚫고 지나가 버렸습니다.

"직접 충돌 계산을 하려니 너무 복잡해요." 김게임 씨가 한숨을 쉽니다. 공의 위치, 속도, 가속도를 매 프레임마다 계산하고, 바닥과의 충돌을 감지하고, 반발력을 계산하고...

생각만 해도 머리가 아픕니다. 선배 개발자 박물리 씨가 모니터를 들여다봅니다.

"게임 물리는 혼자 만들기엔 너무 복잡해요. 이미 검증된 물리 엔진을 사용하는 게 정답이죠." 그렇다면 Forge2D란 정확히 무엇일까요?

쉽게 비유하자면, Forge2D는 마치 게임 세계의 물리학 교수와 같습니다. 우리가 "이 공은 10의 중력을 받아요"라고 알려주기만 하면, 매 순간 공이 어디로 움직여야 하는지, 무엇과 부딪혔는지, 얼마나 튕겨야 하는지를 자동으로 계산해 줍니다.

복잡한 물리 공식을 외울 필요도, 미적분을 할 필요도 없습니다. Forge2D가 없던 시절에는 어땠을까요?

개발자들은 뉴턴의 운동 법칙을 직접 코드로 구현해야 했습니다. 위치 = 이전위치 + 속도 × 시간, 속도 = 이전속도 + 가속도 × 시간...

이런 식으로 매 프레임마다 계산했습니다. 코드가 길어지고, 실수하기도 쉬웠습니다.

더 큰 문제는 충돌 감지였습니다. 두 개의 원이 부딪혔는지 판단하는 건 그나마 쉽지만, 복잡한 도형끼리의 충돌은 악몽이었습니다.

프로젝트가 커질수록 이런 문제는 눈덩이처럼 불어났습니다. 오브젝트가 10개만 되어도 충돌 경우의 수가 45가지나 되니까요.

버그는 끊임없이 발생했고, 성능도 나빴습니다. 바로 이런 문제를 해결하기 위해 Box2D가 등장했고, Dart로 포팅된 것이 Forge2D입니다.

Forge2D를 사용하면 복잡한 물리 계산을 자동화할 수 있습니다. 또한 최적화된 충돌 감지 알고리즘도 얻을 수 있습니다.

무엇보다 검증된 라이브러리라는 큰 이점이 있습니다. 이미 수많은 게임에서 사용되어 안정성이 입증되었습니다.

위의 코드를 한 줄씩 살펴보겠습니다. 먼저 Forge2DGame을 상속받는 것이 핵심입니다.

일반 FlameGame 대신 Forge2DGame을 사용하면 물리 엔진이 자동으로 통합됩니다. 생성자에서 gravity: Vector2(0, 10)을 설정하면 아래 방향으로 중력이 작용합니다.

x는 0이고 y는 10이므로, 수평 방향은 힘이 없고 수직 아래로만 중력이 당깁니다. onLoad 메서드에서 물리 월드가 초기화됩니다.

별도로 월드를 생성할 필요 없이 Forge2DGame이 모든 걸 처리해 줍니다. 이제 이 게임에 추가되는 모든 물리 오브젝트는 자동으로 중력의 영향을 받습니다.

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

새를 날리면 포물선을 그리며 날아가고, 블록에 부딪히면 블록이 무너지고, 돼지가 깔려야 합니다. 이 모든 걸 직접 계산하려면 몇 달이 걸리겠지만, Forge2D를 활용하면 각 오브젝트에 질량과 마찰력만 설정해 주면 됩니다.

실제로 많은 모바일 게임이 Box2D 계열 엔진을 사용하고 있습니다. 하지만 주의할 점도 있습니다.

초보 개발자들이 흔히 하는 실수 중 하나는 중력 값을 너무 크게 설정하는 것입니다. 현실 세계의 중력은 9.8m/s²이지만, 게임에서는 화면 크기와 게임 플레이에 맞게 조정해야 합니다.

중력이 너무 크면 오브젝트가 순식간에 화면 밖으로 사라집니다. 따라서 적절한 값을 테스트하며 찾아야 합니다.

다시 김게임 씨의 이야기로 돌아가 봅시다. 박물리 씨의 설명을 들은 김게임 씨는 고개를 끄덕였습니다.

"아, 물리 엔진이 이런 거였군요!" Forge2D를 제대로 이해하면 게임에 현실감 넘치는 움직임을 쉽게 구현할 수 있습니다. 여러분도 오늘 배운 내용을 실제 프로젝트에 적용해 보세요.

실전 팁

💡 - 중력 값은 게임 플레이에 맞게 조정하세요 (보통 5~20 사이)

  • Forge2DGame은 자동으로 물리 시뮬레이션을 매 프레임마다 실행합니다
  • 디버그 모드에서는 debugMode를 켜서 물리 바디를 시각화할 수 있습니다

2. 물리 월드 생성

Forge2D를 추가한 김게임 씨는 이제 게임 오브젝트를 만들려고 합니다. 그런데 막상 코드를 작성하려니 막막합니다.

"물리 월드에 오브젝트를 어떻게 추가하죠?" 박물리 씨가 웃으며 답합니다. "BodyComponent를 만들어서 add하면 됩니다."

물리 월드는 모든 물리 오브젝트가 존재하는 가상의 공간입니다. 마치 연극 무대와 같아서, 배우(오브젝트)들이 이 무대 위에서 상호작용합니다.

Forge2D에서는 BodyComponent를 생성하여 이 월드에 추가하면 자동으로 물리 시뮬레이션이 적용됩니다.

다음 코드를 살펴봅시다.

import 'package:flame_forge2d/flame_forge2d.dart';

// 물리 오브젝트를 표현하는 컴포넌트
class Ball extends BodyComponent {
  final Vector2 position;

  Ball(this.position);

  @override
  Body createBody() {
    // BodyDef: 바디의 초기 설정
    final bodyDef = BodyDef(
      position: position,
      type: BodyType.dynamic, // 중력과 힘을 받는 동적 오브젝트
    );

    // 월드에 바디를 생성하여 반환
    return world.createBody(bodyDef);
  }
}

김게임 씨가 Forge2D를 게임에 추가하고 나서 가장 먼저 든 생각은 "이제 뭘 해야 하지?"였습니다. 물리 엔진은 준비되었는데, 정작 움직일 오브젝트가 하나도 없었습니다.

일반 Flame 컴포넌트처럼 SpriteComponent를 만들면 될까요? 아닙니다.

물리 엔진을 사용하려면 특별한 컴포넌트가 필요합니다. "일반 컴포넌트는 물리 법칙을 모릅니다." 박물리 씨가 설명합니다.

"물리 월드에 등록되어야 중력도 받고, 충돌도 감지되죠." 그렇다면 물리 월드란 정확히 무엇일까요? 쉽게 비유하자면, 물리 월드는 마치 거대한 체스판과 같습니다.

체스 말(오브젝트)들이 이 판 위에 놓여야 게임 규칙(물리 법칙)이 적용됩니다. 체스판 밖에 있는 말은 아무리 멋져도 게임에 참여할 수 없습니다.

마찬가지로 물리 월드에 등록되지 않은 오브젝트는 중력도 받지 않고, 다른 오브젝트와 충돌도 하지 않습니다. 물리 월드가 제대로 관리되지 않으면 어떤 문제가 생길까요?

과거에는 개발자가 직접 모든 오브젝트의 리스트를 관리했습니다. 새 오브젝트를 만들면 리스트에 추가하고, 삭제할 때는 리스트에서 제거하고, 매 프레임마다 모든 오브젝트를 순회하며 물리 계산을 했습니다.

오브젝트가 100개만 되어도 코드가 복잡해지고, 하나라도 빼먹으면 버그가 발생했습니다. 더 큰 문제는 메모리 관리였습니다.

삭제된 오브젝트를 리스트에서 제거하지 않으면 메모리 누수가 발생했습니다. 반대로 너무 일찍 제거하면 null 참조 에러가 났습니다.

게임이 복잡해질수록 이런 문제는 디버깅하기 어려워졌습니다. 바로 이런 문제를 해결하기 위해 물리 월드 개념이 등장했습니다.

물리 월드를 사용하면 오브젝트 생명주기를 자동 관리할 수 있습니다. 월드에 추가하면 시뮬레이션에 참여하고, 제거하면 자동으로 정리됩니다.

또한 공간 분할 최적화도 얻을 수 있습니다. 월드는 내부적으로 쿼드트리나 그리드를 사용해 가까운 오브젝트끼리만 충돌 검사를 합니다.

무엇보다 일관된 API라는 큰 이점이 있습니다. 어떤 게임을 만들든 동일한 방식으로 물리 오브젝트를 관리할 수 있습니다.

위의 코드를 한 줄씩 살펴보겠습니다. 먼저 BodyComponent를 상속받는 것이 핵심입니다.

이 클래스는 Flame의 일반 컴포넌트와 Forge2D의 물리 바디를 연결하는 다리 역할을 합니다. createBody 메서드를 반드시 오버라이드해야 하는데, 여기서 실제 물리 바디를 생성합니다.

BodyDef는 바디의 초기 설정을 담는 객체입니다. position은 월드 좌표계에서의 위치이고, type은 바디의 종류입니다.

BodyType.dynamic은 중력과 힘을 받는 동적 오브젝트를 의미합니다. 마지막으로 world.createBody(bodyDef)를 호출하면 월드에 바디가 등록되고, 이제부터 물리 시뮬레이션에 참여하게 됩니다.

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

공, 범퍼, 플리퍼, 벽... 수십 개의 오브젝트가 동시에 움직입니다.

각 오브젝트를 BodyComponent로 만들고 월드에 추가하기만 하면, Forge2D가 알아서 모든 충돌과 물리를 계산합니다. 개발자는 게임 로직에만 집중할 수 있습니다.

하지만 주의할 점도 있습니다. 초보 개발자들이 흔히 하는 실수 중 하나는 createBody를 여러 번 호출하는 것입니다.

이 메서드는 Flame이 자동으로 한 번만 호출합니다. 직접 호출하면 같은 오브젝트가 월드에 중복 등록되어 이상한 동작을 합니다.

따라서 초기화 로직만 작성하고, Flame에게 맡겨야 합니다. 다시 김게임 씨의 이야기로 돌아가 봅시다.

박물리 씨의 설명을 들은 김게임 씨는 이해했다는 표정을 지었습니다. "아, 그래서 BodyComponent를 쓰는 거군요!" 물리 월드의 개념을 제대로 이해하면 게임 오브젝트를 체계적으로 관리할 수 있습니다.

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

실전 팁

💡 - BodyComponent를 상속받으면 자동으로 물리 월드에 통합됩니다

  • createBody는 Flame이 자동 호출하므로 직접 호출하지 마세요
  • 게임에 오브젝트를 추가할 때는 gameRef.add(Ball(position))처럼 사용합니다

3. Body와 Fixture

김게임 씨가 드디어 공을 화면에 띄웠습니다. 하지만 공이 아래로 떨어지긴 하는데 바닥을 그냥 통과해 버립니다.

"충돌이 안 되네요?" 박물리 씨가 코드를 보더니 말합니다. "아, Fixture를 안 만들었네요.

Body만으로는 충돌이 안 됩니다."

Body는 물리 오브젝트의 위치와 움직임을 담당하고, Fixture는 모양과 물리적 속성을 담당합니다. 마치 사람의 뼈대(Body)와 살(Fixture)처럼 둘이 합쳐져야 완전한 물리 오브젝트가 됩니다.

Fixture가 없으면 충돌 감지가 불가능합니다.

다음 코드를 살펴봅시다.

import 'package:flame_forge2d/flame_forge2d.dart';

class Ball extends BodyComponent {
  final double radius;

  Ball({required Vector2 position, this.radius = 1.0}) : super(
    bodyDef: BodyDef(
      position: position,
      type: BodyType.dynamic,
    ),
  );

  @override
  Future<void> onLoad() async {
    await super.onLoad();

    // FixtureDef: 충돌 모양과 물리 속성 정의
    final fixtureDef = FixtureDef(
      CircleShape()..radius = radius, // 원형 모양
      density: 1.0,      // 밀도: 질량 계산에 사용
      friction: 0.5,     // 마찰력: 0(미끄러움)~1(거침)
      restitution: 0.8,  // 반발력: 0(안튕김)~1(완전탄성)
    );

    // Body에 Fixture 추가
    body.createFixture(fixtureDef);
  }
}

김게임 씨는 신이 났습니다. 드디어 공이 화면에 나타나고 중력을 받아 아래로 떨어졌습니다.

하지만 기쁨도 잠시, 공은 바닥을 무시하고 계속 아래로 사라졌습니다. 유령처럼 모든 걸 통과합니다.

"분명히 바닥도 만들었는데 왜 이러지?" 디버그 모드를 켜보니 더 이상했습니다. 공의 위치는 제대로 표시되는데, 충돌 영역이 보이지 않았습니다.

마치 투명인간처럼 존재하지만 만질 수 없는 상태였습니다. 선배 개발자 박물리 씨가 코드를 보고 바로 문제를 파악했습니다.

"Body만 만들고 Fixture를 안 만들었네요. Body는 위치만 있는 점이에요.

충돌하려면 크기와 모양이 필요하죠." 그렇다면 Body와 Fixture의 차이는 정확히 무엇일까요? 쉽게 비유하자면, Body는 게임 캐릭터의 좌표와 같습니다.

지도에서 "이 캐릭터는 x=100, y=200에 있어"라고 위치만 알려줍니다. 하지만 크기가 얼마나 큰지, 어떤 모양인지는 알 수 없습니다.

Fixture는 그 캐릭터의 실제 몸입니다. "이 캐릭터는 반지름 1미터의 원형이고, 무게는 80kg이야"라고 물리적 속성을 정의합니다.

이 둘을 분리한 이유가 있을까요? 초기 게임 엔진들은 위치와 모양을 하나로 합쳐서 관리했습니다.

하지만 문제가 생겼습니다. 예를 들어 로봇 캐릭터를 만든다면 머리, 몸통, 팔, 다리가 각각 다른 모양이지만, 전체는 하나의 Body로 움직여야 합니다.

하나의 Body에 여러 개의 Fixture를 붙일 수 있다면 훨씬 유연합니다. 또 다른 문제는 메모리 효율이었습니다.

같은 모양의 오브젝트 100개를 만들 때, Body는 100개 필요하지만 Fixture의 Shape는 하나만 만들어서 공유할 수 있습니다. 이렇게 하면 메모리를 크게 절약할 수 있습니다.

바로 이런 이유로 Body와 Fixture를 분리한 설계가 탄생했습니다. Body와 Fixture를 분리하면 복잡한 모양을 조합할 수 있습니다.

하나의 Body에 원, 사각형, 다각형을 여러 개 붙여서 자동차나 캐릭터를 만들 수 있습니다. 또한 물리 속성을 세밀하게 조정할 수 있습니다.

같은 Body라도 부위마다 다른 마찰력이나 반발력을 설정할 수 있습니다. 무엇보다 성능 최적화라는 큰 이점이 있습니다.

위의 코드를 한 줄씩 살펴보겠습니다. 먼저 BodyDef로 Body를 생성하는 건 이전과 동일합니다.

중요한 건 onLoad에서 Fixture를 추가하는 부분입니다. CircleShape()..radius = radius는 원형 모양을 정의합니다.

Dart의 캐스케이드 연산자 ..를 사용해 반지름을 설정합니다. FixtureDef는 Fixture의 물리 속성을 정의합니다.

density는 밀도로, 같은 크기라도 밀도가 높으면 무겁습니다. friction은 마찰력으로, 0에 가까우면 미끄럽고 1에 가까우면 거칩니다.

restitution은 반발력으로, 0이면 충돌 시 에너지를 잃고, 1이면 에너지 손실 없이 완전히 튕깁니다. 마지막으로 body.createFixture(fixtureDef)를 호출하면 Body에 Fixture가 붙습니다.

이제 이 Body는 충돌 감지가 가능한 완전한 물리 오브젝트가 됩니다. 실제 현업에서는 어떻게 활용할까요?

예를 들어 물리 퍼즐 게임을 개발한다고 가정해봅시다. 복잡한 모양의 블록을 만들어야 합니다.

하나의 Body에 여러 개의 Fixture를 붙이면 L자, T자, 십자가 모양 등을 자유롭게 만들 수 있습니다. 각 Fixture마다 다른 재질을 설정할 수도 있습니다.

나무 부분은 마찰력이 높고, 얼음 부분은 미끄럽게 설정하는 식입니다. 하지만 주의할 점도 있습니다.

초보 개발자들이 흔히 하는 실수 중 하나는 restitution을 1.0 이상으로 설정하는 것입니다. 1.0을 넘으면 충돌할 때마다 에너지가 증가해서 오브젝트가 무한히 튀어 오릅니다.

현실에서는 불가능한 일이죠. 따라서 0.0에서 1.0 사이의 값을 사용해야 합니다.

또 다른 실수는 Fixture를 너무 많이 붙이는 것입니다. 하나의 Body에 Fixture가 10개 이상이면 충돌 계산이 느려집니다.

가능하면 단순한 모양으로 근사하는 게 좋습니다. 다시 김게임 씨의 이야기로 돌아가 봅시다.

박물리 씨의 설명을 들은 김게임 씨는 즉시 코드를 수정했습니다. Fixture를 추가하자 공이 바닥에서 통통 튀기 시작했습니다.

"와, 진짜 공처럼 움직여요!" Body와 Fixture의 역할을 제대로 이해하면 원하는 대로 물리 오브젝트를 만들 수 있습니다. 여러분도 오늘 배운 내용을 실제 프로젝트에 적용해 보세요.

실전 팁

💡 - Fixture 없는 Body는 충돌이 불가능합니다

  • restitution은 0.0~1.0 사이 값을 사용하세요 (농구공 약 0.8, 나무 약 0.3)
  • 하나의 Body에 여러 Fixture를 붙여 복잡한 모양을 만들 수 있습니다

4. 중력과 힘 적용

공이 잘 튀기는 걸 본 김게임 씨는 이번엔 공을 손가락으로 튕기고 싶어졌습니다. 화면을 터치하면 공이 위로 날아가게 하고 싶은데, 어떻게 해야 할까요?

"Body에 힘을 가하면 됩니다." 박물리 씨가 말합니다. "applyLinearImpulse를 사용해 보세요."

은 오브젝트의 속도를 변화시키는 물리량입니다. Forge2D에서는 지속적인 힘을 가하는 applyForce와 순간적인 충격을 주는 applyLinearImpulse 두 가지 방법이 있습니다.

마치 자동차를 밀 때 계속 미는 것과 한 번 세게 치는 것의 차이와 같습니다.

다음 코드를 살펴봅시다.

import 'package:flame_forge2d/flame_forge2d.dart';

class Ball extends BodyComponent {
  Ball({required Vector2 position, double radius = 1.0}) : super(
    bodyDef: BodyDef(
      position: position,
      type: BodyType.dynamic,
    ),
  ) {
    final fixtureDef = FixtureDef(
      CircleShape()..radius = radius,
      density: 1.0,
      restitution: 0.8,
    );
    renderBody = false;
  }

  // 순간적인 충격을 가함 (터치, 충돌 등)
  void jump() {
    body.applyLinearImpulse(Vector2(0, -300));
    // 음수 y 방향 = 위쪽으로 힘 (중력 반대)
  }

  // 지속적인 힘을 가함 (로켓 분사, 바람 등)
  void applyWind(Vector2 windForce) {
    body.applyForce(windForce);
    // 매 프레임마다 호출하면 지속적으로 힘이 작용
  }
}

김게임 씨는 이제 공이 중력을 받아 떨어지고 바닥에서 튀는 것까지 구현했습니다. 하지만 게임다운 게임을 만들려면 플레이어가 공을 조작할 수 있어야 합니다.

"화면을 터치하면 공이 점프하게 하고 싶어요." 하지만 어떻게 해야 할지 막막합니다. 일반 Flame 게임이었다면 간단합니다.

공의 y 좌표를 직접 바꾸면 되니까요. 하지만 물리 엔진을 사용할 때는 다릅니다.

위치를 직접 바꾸면 물리 시뮬레이션과 충돌해서 이상한 동작을 합니다. 선배 개발자 박물리 씨가 설명합니다.

"물리 엔진을 사용할 땐 위치를 직접 바꾸지 말고 힘을 가해야 해요. 현실 세계에서도 물체를 움직이려면 힘을 가하잖아요?" 그렇다면 이란 정확히 무엇일까요?

쉽게 비유하자면, 힘은 마치 물체를 미는 손과 같습니다. 공을 손으로 치면 공이 날아갑니다.

약하게 치면 조금 움직이고, 세게 치면 멀리 날아갑니다. 게임에서도 마찬가지입니다.

Body에 힘을 가하면 가속도가 생기고, 속도가 변하고, 결국 위치가 바뀝니다. 이 모든 과정을 물리 엔진이 자동으로 계산합니다.

힘을 직접 계산하지 않으면 어떤 문제가 생길까요? 과거에는 개발자가 직접 속도를 변경했습니다.

점프 버튼을 누르면 velocity.y = -10 이런 식으로 속도를 바꿨습니다. 간단해 보이지만 문제가 많았습니다.

우선 질량을 고려하지 않습니다. 무거운 물체나 가벼운 물체나 같은 속도로 움직이는 건 비현실적입니다.

더 큰 문제는 물리 엔진과의 불일치였습니다. 속도를 직접 바꾸면 물리 엔진이 계산한 값이 무시됩니다.

예를 들어 공이 벽에 부딪혀 튕기는 중인데, 그 순간 속도를 직접 바꾸면 이상한 방향으로 날아갑니다. 버그가 발생하기 쉬운 구조였습니다.

바로 이런 문제를 해결하기 위해 힘 기반 제어가 등장했습니다. 힘을 사용하면 질량이 자동 반영됩니다.

같은 힘을 가해도 무거운 물체는 천천히, 가벼운 물체는 빠르게 움직입니다. 또한 물리 엔진과 조화롭게 동작합니다.

충돌, 중력, 마찰 등 모든 물리 효과가 자연스럽게 통합됩니다. 무엇보다 현실감이라는 큰 이점이 있습니다.

위의 코드를 한 줄씩 살펴보겠습니다. applyLinearImpulse는 순간적인 충격을 가합니다.

Vector2(0, -300)은 위쪽 방향으로 300의 힘을 줍니다. y축이 음수인 이유는 화면 좌표계에서 위쪽이 음수 방향이기 때문입니다.

이 메서드는 터치, 폭발, 충돌 같은 순간적인 이벤트에 적합합니다. applyForce는 지속적인 힘을 가합니다.

한 번 호출하면 그 프레임에만 힘이 작용합니다. 따라서 바람이나 로켓 분사처럼 계속 힘을 받으려면 매 프레임마다 호출해야 합니다.

update 메서드 안에서 호출하는 경우가 많습니다. 실제 현업에서는 어떻게 활용할까요?

예를 들어 플래피버드 같은 게임을 개발한다고 가정해봅시다. 화면을 터치하면 새가 위로 날아가야 합니다.

applyLinearImpulse(Vector2(0, -200))를 호출하면 새가 위로 튀어 오릅니다. 중력이 계속 작용하므로 포물선을 그리며 다시 떨어집니다.

터치할 때마다 충격을 주면 통통 튀는 듯한 움직임이 됩니다. 바람 효과를 주고 싶다면 update 메서드에서 applyForce(Vector2(10, 0))를 매 프레임 호출합니다.

그러면 오브젝트가 서서히 오른쪽으로 밀려납니다. 바람의 세기를 조절하려면 벡터의 크기를 바꾸면 됩니다.

하지만 주의할 점도 있습니다. 초보 개발자들이 흔히 하는 실수 중 하나는 힘의 크기를 너무 크게 설정하는 것입니다.

테스트해 보니 움직임이 약해서 값을 10배로 키우면, 오브젝트가 순식간에 화면 밖으로 날아갑니다. 적절한 값을 찾으려면 작은 값부터 시작해서 점진적으로 증가시켜야 합니다.

또 다른 실수는 applyForce와 applyLinearImpulse를 혼동하는 것입니다. applyForce는 매 프레임 호출해야 하고, applyLinearImpulse는 이벤트 발생 시 한 번만 호출합니다.

반대로 사용하면 원하는 효과를 얻을 수 없습니다. 다시 김게임 씨의 이야기로 돌아가 봅시다.

박물리 씨의 설명을 들은 김게임 씨는 터치 이벤트 핸들러에 ball.jump()를 추가했습니다. 화면을 터치하자 공이 통통 튀어 올랐습니다.

"우와, 진짜 게임 같아요!" 힘의 개념을 제대로 이해하면 자연스러운 움직임을 쉽게 구현할 수 있습니다. 여러분도 오늘 배운 내용을 실제 프로젝트에 적용해 보세요.

실전 팁

💡 - applyLinearImpulse는 터치나 충돌 같은 순간 이벤트에 사용하세요

  • applyForce는 바람이나 자석 같은 지속 효과에 사용하세요
  • 힘의 크기는 작은 값부터 시작해서 조정하는 게 안전합니다

5. 조인트와 제약

김게임 씨는 이제 로프로 연결된 두 개의 공을 만들고 싶어졌습니다. 그런데 막상 구현하려니 막막합니다.

"두 Body를 어떻게 연결하죠?" 박물리 씨가 웃으며 답합니다. "Joint를 사용하면 됩니다.

두 Body를 물리적으로 연결할 수 있어요."

조인트는 두 개 이상의 Body를 연결하여 특정한 방식으로 움직이도록 제약하는 기능입니다. 마치 인체의 관절처럼 뼈와 뼈를 연결하면서도 움직임을 허용합니다.

DistanceJoint, RevoluteJoint, PrismaticJoint 등 다양한 종류가 있어 로프, 힌지, 슬라이더 등을 구현할 수 있습니다.

다음 코드를 살펴봅시다.

import 'package:flame_forge2d/flame_forge2d.dart';

// 두 Body를 일정 거리로 연결 (로프, 체인 등)
void createRopeJoint(Body bodyA, Body bodyB, Forge2DGame game) {
  final jointDef = DistanceJointDef()
    ..initialize(
      bodyA,                    // 첫 번째 Body
      bodyB,                    // 두 번째 Body
      bodyA.worldCenter,        // bodyA의 연결점
      bodyB.worldCenter,        // bodyB의 연결점
    )
    ..length = 5.0              // 로프 길이
    ..frequencyHz = 2.0         // 진동 주파수 (탄성)
    ..dampingRatio = 0.5;       // 감쇠 비율 (진동 감소)

  // 월드에 조인트 생성
  game.world.createJoint(jointDef);
}

김게임 씨의 게임은 점점 발전했습니다. 공 하나가 떨어지고 튀는 건 이제 식은 죽 먹기입니다.

이번엔 좀 더 복잡한 걸 만들어 보고 싶었습니다. "두 개의 공을 로프로 연결해서 그네처럼 흔들리게 하면 재밌겠다!" 하지만 어떻게 구현해야 할지 감이 잡히지 않습니다.

처음엔 간단하게 생각했습니다. 매 프레임마다 두 공 사이의 거리를 계산해서, 너무 멀어지면 서로 당기면 되지 않을까?

하지만 코드를 작성해 보니 금방 복잡해졌습니다. 거리 계산, 방향 계산, 힘 적용...

그리고 진동이 멈추지 않고 계속 요동쳤습니다. 선배 개발자 박물리 씨가 모니터를 보고 피식 웃습니다.

"그걸 직접 구현하려고요? Joint 쓰면 한 줄이에요." 그렇다면 조인트란 정확히 무엇일까요?

쉽게 비유하자면, 조인트는 마치 사람의 관절과 같습니다. 팔꿈치 관절은 팔뚝과 전완을 연결하면서도, 한 방향으로만 구부러지도록 제약합니다.

어깨 관절은 360도 회전을 허용하지만, 뼈가 서로 떨어지지는 않습니다. 게임의 조인트도 마찬가지입니다.

두 Body를 연결하되, 특정한 방식으로만 움직이도록 제약합니다. 조인트가 없던 시절에는 어땠을까요?

개발자들은 직접 제약 조건을 코드로 구현해야 했습니다. 두 Body 사이의 거리를 매 프레임 체크하고, 너무 멀어지면 서로 당기는 힘을 계산하고, 너무 가까우면 밀어내는 힘을 계산했습니다.

코드가 복잡할 뿐만 아니라 안정성 문제가 심각했습니다. 특히 회전 제약은 악몽이었습니다.

문짝이 경첩으로 연결되어 한쪽 방향으로만 열리게 하려면, 각도를 계산하고, 각속도를 제한하고, 복원력을 적용해야 했습니다. 살짝만 잘못 계산해도 문짝이 경첩을 뚫고 날아가거나, 진동하며 폭발했습니다.

바로 이런 문제를 해결하기 위해 조인트 시스템이 등장했습니다. 조인트를 사용하면 복잡한 제약을 간단히 설정할 수 있습니다.

DistanceJoint로 로프를, RevoluteJoint로 경첩을, PrismaticJoint로 슬라이더를 몇 줄의 코드로 만듭니다. 또한 물리 엔진이 안정성을 보장합니다.

조인트는 내부적으로 반복 계산을 통해 정확하게 제약을 유지합니다. 무엇보다 조합의 다양성이라는 큰 이점이 있습니다.

위의 코드를 한 줄씩 살펴보겠습니다. 먼저 DistanceJointDef를 생성합니다.

이것은 두 Body를 일정한 거리로 유지하는 조인트 정의입니다. initialize 메서드로 연결할 두 Body와 각 Body의 연결점을 설정합니다.

worldCenter는 Body의 중심점을 의미합니다. length는 로프의 길이입니다.

두 Body는 이 거리를 유지하려고 합니다. frequencyHz는 진동 주파수로, 값이 클수록 단단한 로프가 됩니다.

0이면 완전히 고정되고, 값이 작으면 탄력적입니다. dampingRatio는 감쇠 비율로, 진동이 얼마나 빨리 사라지는지를 제어합니다.

마지막으로 world.createJoint(jointDef)를 호출하면 조인트가 생성됩니다. 이제 두 Body는 물리적으로 연결되어, 하나가 움직이면 다른 하나도 영향을 받습니다.

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

사탕이 로프에 매달려 있고, 플레이어가 로프를 잘라야 합니다. 천장과 사탕을 DistanceJoint로 연결하면 로프가 됩니다.

로프를 자를 때는 world.destroyJoint(joint)를 호출하면 됩니다. 사탕이 중력을 받아 떨어지는 건 자동입니다.

문을 만들고 싶다면 RevoluteJoint를 사용합니다. 벽과 문짝을 경첩으로 연결하고, lowerAngleupperAngle을 설정하면 문이 일정 각도 범위 내에서만 열립니다.

플레이어가 문을 밀면 자연스럽게 열리고, 놓으면 스프링처럼 닫히게 할 수도 있습니다. 하지만 주의할 점도 있습니다.

초보 개발자들이 흔히 하는 실수 중 하나는 너무 많은 조인트를 사용하는 것입니다. 조인트는 매 프레임마다 반복 계산을 하므로 성능에 영향을 줍니다.

100개 이상의 조인트가 있으면 게임이 느려질 수 있습니다. 따라서 꼭 필요한 경우에만 사용해야 합니다.

또 다른 실수는 조인트를 삭제하지 않는 것입니다. 조인트로 연결된 Body 중 하나를 삭제하면 조인트도 함께 삭제해야 합니다.

그렇지 않으면 null 참조 에러가 발생합니다. 다시 김게임 씨의 이야기로 돌아가 봅시다.

박물리 씨의 설명을 들은 김게임 씨는 몇 줄의 코드로 로프를 구현했습니다. 두 공이 로프로 연결되어 흔들리는 모습을 보고 감탄했습니다.

"와, 진짜 로프처럼 움직여요!" 조인트의 개념을 제대로 이해하면 복잡한 물리 관계를 쉽게 구현할 수 있습니다. 여러분도 오늘 배운 내용을 실제 프로젝트에 적용해 보세요.

실전 팁

💡 - DistanceJoint는 로프나 체인에, RevoluteJoint는 경첩이나 바퀴에 사용하세요

  • frequencyHz는 0이면 완전 고정, 1~5 정도면 적당한 탄성입니다
  • Body를 삭제할 때는 연결된 조인트도 함께 삭제해야 합니다

6. 물리 충돌 처리

김게임 씨의 게임이 거의 완성되어 갑니다. 공이 떨어지고, 튀고, 로프로 흔들리고...

하지만 한 가지 문제가 있습니다. 공이 특정 아이템에 닿았을 때 점수를 주고 싶은데, 충돌을 어떻게 감지하죠?

박물리 씨가 말합니다. "ContactCallbacks를 사용하세요."

충돌 콜백은 두 Fixture가 충돌했을 때 자동으로 호출되는 함수입니다. 마치 문에 센서를 달아놓으면 누가 지나갈 때 알림이 오는 것처럼, 충돌이 발생하면 게임 로직을 실행할 수 있습니다.

beginContact, endContact 등의 이벤트로 충돌 시작과 끝을 감지합니다.

다음 코드를 살펴봅시다.

import 'package:flame_forge2d/flame_forge2d.dart';

// 충돌 감지를 위한 컴포넌트
class Ball extends BodyComponent with ContactCallbacks {
  Ball({required Vector2 position, double radius = 1.0}) : super(
    bodyDef: BodyDef(position: position, type: BodyType.dynamic),
  );

  // 충돌이 시작될 때 호출됨
  @override
  void beginContact(Object other, Contact contact) {
    super.beginContact(other, contact);

    if (other is Coin) {
      // 동전과 충돌했을 때 처리
      print('코인 획득!');
      other.removeFromParent();  // 동전 제거
    } else if (other is Enemy) {
      // 적과 충돌했을 때 처리
      print('게임 오버!');
    }
  }

  // 충돌이 끝날 때 호출됨
  @override
  void endContact(Object other, Contact contact) {
    super.endContact(other, contact);
    print('충돌 종료: ${other.runtimeType}');
  }
}

김게임 씨는 드디어 게임의 핵심 기능을 거의 다 구현했습니다. 공이 물리 법칙을 따라 움직이고, 바닥에서 튕기고, 로프로 흔들립니다.

이제 게임다운 게임을 만들 시간입니다. "공이 동전에 닿으면 점수를 얻고, 적에게 닿으면 게임 오버가 되어야 해요." 처음엔 간단하게 생각했습니다.

매 프레임마다 공과 동전의 거리를 계산해서, 가까우면 충돌했다고 판단하면 되지 않을까? 하지만 오브젝트가 10개만 되어도 매 프레임마다 45번의 거리 계산을 해야 합니다.

성능이 걱정됩니다. 선배 개발자 박물리 씨가 고개를 저으며 말합니다.

"그럴 필요 없어요. Forge2D가 충돌을 자동으로 감지하고 알려줍니다." 그렇다면 충돌 콜백이란 정확히 무엇일까요?

쉽게 비유하자면, 충돌 콜백은 마치 현관문의 초인종과 같습니다. 누군가 문을 누르면 벨이 울려서 우리에게 알려줍니다.

우리가 매 순간 현관을 쳐다볼 필요가 없습니다. 게임도 마찬가지입니다.

충돌이 발생하면 Forge2D가 자동으로 beginContact 메서드를 호출해서 알려줍니다. 우리는 그때 필요한 로직을 실행하면 됩니다.

충돌을 직접 감지하던 시절에는 어떤 문제가 있었을까요? 개발자들은 매 프레임마다 모든 오브젝트 쌍을 검사했습니다.

이중 반복문을 돌며 거리를 계산하고, 충돌 여부를 판단했습니다. 오브젝트가 n개면 n×(n-1)/2번의 계산이 필요했습니다.

100개만 되어도 4950번입니다. 성능 문제가 심각했습니다.

더 큰 문제는 정확도였습니다. 빠르게 움직이는 오브젝트는 한 프레임에 상대방을 뚫고 지나갈 수 있습니다.

이전 프레임에선 충돌 전이었고, 다음 프레임에선 이미 지나간 뒤라서 충돌을 놓치는 겁니다. 총알이 적을 통과하는 버그가 자주 발생했습니다.

바로 이런 문제를 해결하기 위해 충돌 콜백 시스템이 등장했습니다. 충돌 콜백을 사용하면 물리 엔진이 정확히 계산합니다.

Forge2D는 연속 충돌 감지 알고리즘을 사용해서 빠른 오브젝트도 놓치지 않습니다. 또한 성능이 최적화됩니다.

공간 분할 자료구조를 사용해서 가까운 오브젝트끼리만 검사합니다. 무엇보다 사용하기 쉽다는 큰 이점이 있습니다.

위의 코드를 한 줄씩 살펴보겠습니다. 먼저 ContactCallbacks를 믹스인하는 것이 핵심입니다.

Dart의 믹스인 문법 with를 사용합니다. 이렇게 하면 이 컴포넌트가 충돌 이벤트를 받을 수 있습니다.

beginContact 메서드는 충돌이 시작될 때 자동으로 호출됩니다. other 매개변수는 충돌한 상대방 오브젝트입니다.

is 키워드로 타입을 확인해서 동전인지 적인지 판단합니다. 동전이면 점수를 주고 제거하고, 적이면 게임 오버 처리를 합니다.

endContact 메서드는 충돌이 끝날 때 호출됩니다. 예를 들어 공이 바닥에 닿았다가 다시 튀어 오를 때, 바닥과의 접촉이 끝나면서 호출됩니다.

디버깅이나 특수 효과에 유용합니다. 실제 현업에서는 어떻게 활용할까요?

예를 들어 슈팅 게임을 개발한다고 가정해봅시다. 총알이 적에게 맞으면 적의 체력을 깎고 총알을 제거해야 합니다.

총알 클래스에 ContactCallbacks를 믹스인하고, beginContact에서 상대방이 적인지 확인합니다. 적이면 enemy.takeDamage(damage)를 호출하고 removeFromParent()로 총알을 제거합니다.

플랫포머 게임에서는 캐릭터가 땅에 닿았는지 알아야 점프가 가능합니다. 캐릭터의 발 부분에 센서 Fixture를 추가하고, 땅과의 충돌을 감지합니다.

beginContact에서 isGrounded = true로 설정하고, endContact에서 isGrounded = false로 설정하면 됩니다. 하지만 주의할 점도 있습니다.

초보 개발자들이 흔히 하는 실수 중 하나는 beginContact에서 Body를 즉시 삭제하는 것입니다. 물리 엔진이 충돌 처리 중일 때 Body를 삭제하면 크래시가 발생할 수 있습니다.

따라서 플래그를 설정하고 다음 프레임에 삭제하거나, removeFromParent()처럼 안전한 메서드를 사용해야 합니다. 또 다른 실수는 센서를 활용하지 않는 것입니다.

FixtureDef에서 isSensor = true로 설정하면 충돌은 감지하지만 물리적으로 막지는 않습니다. 트리거 영역이나 아이템 습득에 유용합니다.

다시 김게임 씨의 이야기로 돌아가 봅시다. 박물리 씨의 설명을 들은 김게임 씨는 ContactCallbacks를 추가하고 충돌 로직을 구현했습니다.

공이 동전에 닿자 "코인 획득!" 메시지가 출력되고 동전이 사라졌습니다. "완벽해요!" 충돌 콜백의 개념을 제대로 이해하면 게임의 상호작용을 쉽게 구현할 수 있습니다.

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

실전 팁

💡 - ContactCallbacks 믹스인으로 충돌 이벤트를 받을 수 있습니다

  • beginContact에서 Body를 직접 삭제하지 말고 removeFromParent를 사용하세요
  • 센서(isSensor=true)를 활용하면 트리거 영역을 만들 수 있습니다

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

#Flutter#Flame#Forge2D#Physics#GameDev#Flutter,Flame,Game

댓글 (0)

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

함께 보면 좋은 카드 뉴스

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

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

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

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

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

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

타일맵 시스템 완벽 가이드

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

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

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