🤖

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

⚠️

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

이미지 로딩 중...

애니메이션 시스템 커스터마이징 완벽 가이드 - 슬라이드 1/7
A

AI Generated

2026. 2. 3. · 4 Views

애니메이션 시스템 커스터마이징 완벽 가이드

Flutter와 Flame 게임 엔진에서 고급 애니메이션 시스템을 구현하는 방법을 다룹니다. 스켈레탈 애니메이션부터 절차적 애니메이션까지, 게임 개발에 필요한 핵심 애니메이션 기법을 실무 예제와 함께 배워봅니다.


목차

  1. 스켈레탈_애니메이션
  2. Spine_Rive_통합
  3. IK_역운동학_시스템
  4. 블렌딩과_전환
  5. 애니메이션_레이어
  6. 절차적_애니메이션

1. 스켈레탈 애니메이션

김개발 씨는 첫 번째 모바일 게임 프로젝트에 투입되었습니다. 캐릭터가 걷고, 뛰고, 점프하는 애니메이션을 구현해야 하는데, 단순히 이미지를 교체하는 방식으로는 자연스러운 움직임을 표현할 수 없었습니다.

선배 개발자가 다가와 말했습니다. "스켈레탈 애니메이션을 써야 해요."

스켈레탈 애니메이션은 캐릭터 내부에 뼈대를 심고, 그 뼈대를 움직여 자연스러운 동작을 만드는 기법입니다. 마치 인형극에서 줄로 마리오네트를 조종하는 것과 같습니다.

뼈대 하나를 움직이면 연결된 다른 뼈대들도 자연스럽게 따라 움직이기 때문에, 적은 리소스로 다양한 동작을 표현할 수 있습니다.

다음 코드를 살펴봅시다.

// 스켈레탈 애니메이션을 위한 기본 뼈대 구조
class Bone {
  String name;
  Vector2 position;
  double rotation;
  Bone? parent;
  List<Bone> children = [];

  // 부모 뼈대를 기준으로 월드 좌표 계산
  Vector2 get worldPosition {
    if (parent == null) return position;
    return parent!.worldPosition + position.rotated(parent!.rotation);
  }

  // 뼈대 회전 시 자식 뼈대도 함께 회전
  void rotate(double angle) {
    rotation += angle;
    for (final child in children) {
      child.rotate(angle * 0.5); // 자식은 절반만 회전
    }
  }
}

김개발 씨는 입사 첫 달, 2D 플랫포머 게임 프로젝트에 배정되었습니다. 기획서를 받아든 그는 고민에 빠졌습니다.

주인공 캐릭터가 걷기, 달리기, 점프, 공격 등 수십 가지 동작을 해야 하는데, 각 동작마다 수십 장의 이미지를 일일이 그려야 한다니. 이건 디자이너 팀이 몇 달을 매달려도 끝나지 않을 작업이었습니다.

선배 개발자 박시니어 씨가 김개발 씨의 화면을 슬쩍 들여다봤습니다. "스프라이트 시트 방식으로 하려고요?

그것도 방법이긴 한데, 요즘은 스켈레탈 애니메이션을 많이 써요." 그렇다면 스켈레탈 애니메이션이란 정확히 무엇일까요? 쉽게 비유하자면, 스켈레탈 애니메이션은 마치 우리 몸의 골격과 같습니다.

사람이 팔을 들어올리면, 어깨 관절이 움직이고, 그에 따라 팔꿈치, 손목, 손가락까지 자연스럽게 따라 움직입니다. 뼈대 하나하나가 독립적으로 존재하지만, 서로 연결되어 있기 때문에 유기적인 움직임이 가능한 것입니다.

스켈레탈 애니메이션이 없던 시절에는 어땠을까요? 개발자들은 프레임 바이 프레임 방식을 사용했습니다.

캐릭터가 한 발 내딛는 동작 하나를 위해 10장이 넘는 이미지를 준비해야 했습니다. 더 큰 문제는 수정이 필요할 때였습니다.

걷는 속도를 조금 바꾸고 싶으면 모든 이미지를 다시 그려야 했습니다. 프로젝트가 커질수록 이런 비효율은 눈덩이처럼 불어났습니다.

바로 이런 문제를 해결하기 위해 스켈레탈 애니메이션이 등장했습니다. 스켈레탈 애니메이션을 사용하면 하나의 캐릭터 이미지에 뼈대만 심으면 됩니다.

뼈대의 위치와 회전 값만 조절하면 걷기, 뛰기, 점프가 모두 가능해집니다. 무엇보다 런타임에서 애니메이션을 동적으로 수정할 수 있다는 큰 이점이 있습니다.

위의 코드를 한 줄씩 살펴보겠습니다. 먼저 Bone 클래스를 보면, 각 뼈대가 이름, 위치, 회전 값을 가지고 있음을 알 수 있습니다.

핵심은 parentchildren 속성입니다. 이 부모-자식 관계가 바로 뼈대들을 연결하는 끈입니다.

worldPosition 게터에서는 부모 뼈대의 좌표와 회전을 고려해서 실제 화면상의 위치를 계산합니다. 실제 현업에서는 어떻게 활용할까요?

예를 들어 RPG 게임을 개발한다고 가정해봅시다. 전사, 마법사, 궁수 캐릭터가 있다면, 기본 뼈대 구조는 동일하게 가져가고 외형만 다르게 입히면 됩니다.

새로운 무기를 추가할 때도 손 뼈대에 무기 스프라이트만 붙이면 되니까 확장이 매우 쉬워집니다. 하지만 주의할 점도 있습니다.

초보 개발자들이 흔히 하는 실수 중 하나는 뼈대 계층 구조를 너무 깊게 만드는 것입니다. 부모-자식-손자-증손자로 이어지는 깊은 계층은 계산량을 급격히 늘립니다.

따라서 꼭 필요한 관절만 뼈대로 만들고, 나머지는 스프라이트로 처리하는 것이 좋습니다. 다시 김개발 씨의 이야기로 돌아가 봅시다.

박시니어 씨의 설명을 들은 김개발 씨는 고개를 끄덕였습니다. "아, 그래서 캐릭터 하나에 뼈대만 잘 설정하면 되는 거군요!" 스켈레탈 애니메이션을 제대로 이해하면 적은 리소스로 풍부한 애니메이션을 구현할 수 있습니다.

여러분도 다음 게임 프로젝트에서 이 기법을 활용해 보세요.

실전 팁

💡 - 뼈대 계층은 3~4단계 이내로 유지하면 성능과 유지보수 모두 좋습니다

  • 루트 뼈대는 캐릭터의 중심(보통 골반)에 두는 것이 자연스럽습니다
  • 뼈대 이름은 leftArm, rightLeg처럼 명확하게 지어야 나중에 코드에서 찾기 쉽습니다

2. Spine Rive 통합

김개발 씨가 스켈레탈 애니메이션의 개념을 이해하자, 박시니어 씨가 새로운 질문을 던졌습니다. "그런데 뼈대를 일일이 코드로 만들 건 아니죠?

Spine이나 Rive 써본 적 있어요?" 김개발 씨는 고개를 저었습니다. 처음 듣는 이름이었습니다.

SpineRive는 전문 애니메이션 제작 도구입니다. 디자이너가 GUI 환경에서 뼈대를 배치하고 애니메이션을 만들면, 개발자는 그 결과물을 게임에 바로 불러와 사용할 수 있습니다.

마치 포토샵으로 만든 이미지를 프로그램에서 불러오는 것처럼, 애니메이션 에셋을 손쉽게 통합할 수 있습니다.

다음 코드를 살펴봅시다.

// Rive 애니메이션을 Flame에 통합하는 예제
class RiveCharacter extends PositionComponent {
  late RiveAnimation riveAnimation;
  late StateMachineController controller;

  @override
  Future<void> onLoad() async {
    // Rive 파일 로드
    final riveFile = await RiveFile.asset('assets/character.riv');
    final artboard = riveFile.mainArtboard;

    // 상태 머신 컨트롤러 연결
    controller = StateMachineController.fromArtboard(artboard, 'CharacterState')!;
    artboard.addController(controller);

    // 입력에 따라 상태 전환
    walkInput = controller.findInput<bool>('isWalking') as SMIBool;
    jumpInput = controller.findInput<bool>('isJumping') as SMIBool;
  }

  void startWalking() => walkInput.value = true;
  void stopWalking() => walkInput.value = false;
}

김개발 씨는 스켈레탈 애니메이션의 원리를 이해했지만, 막상 구현하려니 막막했습니다. 뼈대 좌표를 일일이 계산하고, 각 프레임마다 회전 값을 지정하고, 이를 부드럽게 보간하는 작업은 생각만 해도 머리가 아팠습니다.

박시니어 씨가 웃으며 말했습니다. "걱정 마세요.

그런 건 도구가 다 해줍니다. 우리 회사에서는 Rive를 쓰고 있어요.

예전에는 Spine도 많이 썼고요." 그렇다면 SpineRive는 어떤 도구일까요? 쉽게 비유하자면, 이 도구들은 마치 파워포인트와 같습니다.

파워포인트에서 도형을 배치하고 애니메이션 효과를 주듯이, Spine과 Rive에서는 캐릭터에 뼈대를 배치하고 움직임을 설정합니다. 개발자는 복잡한 수학 계산 없이, 도구가 만들어낸 결과물을 그대로 사용하면 됩니다.

두 도구의 차이점은 무엇일까요? Spine은 게임 업계에서 오랫동안 사랑받아온 베테랑입니다.

특히 2D 게임에서 압도적인 점유율을 자랑합니다. 반면 Rive는 비교적 최근에 등장했지만, 벡터 그래픽 기반이라 확대해도 깨지지 않고, 상태 머신 기능이 강력하다는 장점이 있습니다.

Flutter 생태계에서는 Rive가 더 좋은 통합 지원을 제공합니다. 위의 코드를 살펴보겠습니다.

먼저 RiveFile.asset으로 에셋 폴더에서 .riv 파일을 불러옵니다. 이 파일 안에는 디자이너가 만들어둔 모든 뼈대 정보와 애니메이션이 담겨 있습니다.

다음으로 StateMachineController를 연결하는데, 이것이 바로 Rive의 핵심 기능입니다. 상태 머신 덕분에 "걷는 중", "점프 중" 같은 상태를 정의하고, 조건에 따라 자동으로 전환되도록 설정할 수 있습니다.

실제 워크플로우는 어떻게 진행될까요? 디자이너가 Rive Editor에서 캐릭터를 그리고 뼈대를 배치합니다.

걷기, 뛰기, 점프 애니메이션을 만들고 상태 머신으로 연결합니다. 완성된 .riv 파일을 개발자에게 전달하면, 개발자는 위 코드처럼 파일을 로드하고 입력에 따라 상태만 바꿔주면 됩니다.

협업이 매우 깔끔해집니다. 주의해야 할 점이 있습니다.

Spine과 Rive 모두 라이선스 정책이 있습니다. Spine은 유료 라이선스를 구매해야 상용 게임에 사용할 수 있고, Rive는 무료 플랜이 있지만 기능 제한이 있습니다.

프로젝트 시작 전에 라이선스를 꼭 확인하세요. 김개발 씨는 Rive Editor를 설치하고 튜토리얼을 따라해봤습니다.

생각보다 직관적인 인터페이스에 감탄이 나왔습니다. "이걸 진작 알았으면 좋았을 텐데요!" Spine과 Rive를 활용하면 디자이너와 개발자의 협업이 훨씬 수월해집니다.

여러분의 프로젝트에 맞는 도구를 선택해서 활용해 보세요.

실전 팁

💡 - Flutter 프로젝트라면 Rive가 공식 패키지를 제공하므로 통합이 더 쉽습니다

  • 상태 머신을 잘 설계하면 코드에서 조건문 없이도 애니메이션 전환이 가능합니다
  • 에셋 파일 크기를 줄이려면 불필요한 뼈대와 애니메이션을 정리하세요

3. IK 역운동학 시스템

어느 날 김개발 씨는 플레이어가 마우스로 클릭한 위치를 캐릭터가 손으로 가리키는 기능을 구현해야 했습니다. 어깨, 팔꿈치, 손목 뼈대의 회전 값을 일일이 계산하려니 정신이 아득해졌습니다.

박시니어 씨가 웃으며 한마디 했습니다. "IK를 쓰면 돼요."

**IK(Inverse Kinematics, 역운동학)**는 끝점의 위치만 지정하면 중간 관절들의 회전을 자동으로 계산해주는 시스템입니다. 마치 팔을 뻗어 물건을 잡을 때, 우리가 어깨와 팔꿈치 각도를 의식하지 않아도 자연스럽게 움직이는 것과 같습니다.

목표 지점만 알려주면 나머지는 알고리즘이 해결합니다.

다음 코드를 살펴봅시다.

// Two-bone IK 알고리즘 구현
class TwoBoneIK {
  late Bone shoulder;  // 어깨
  late Bone elbow;     // 팔꿈치
  late Bone hand;      // 손

  void solve(Vector2 targetPosition) {
    final upperLength = elbow.position.length;  // 상완 길이
    final lowerLength = hand.position.length;   // 하완 길이

    // 어깨에서 타겟까지의 거리
    final toTarget = targetPosition - shoulder.worldPosition;
    final targetDist = toTarget.length.clamp(0.1, upperLength + lowerLength - 0.01);

    // 코사인 법칙으로 팔꿈치 각도 계산
    final cosElbow = (upperLength * upperLength + lowerLength * lowerLength
                      - targetDist * targetDist) / (2 * upperLength * lowerLength);
    final elbowAngle = acos(cosElbow.clamp(-1.0, 1.0));

    // 어깨 각도 계산
    final cosShoul = (targetDist * targetDist + upperLength * upperLength
                      - lowerLength * lowerLength) / (2 * targetDist * upperLength);
    shoulder.rotation = toTarget.angleToSigned(Vector2(1, 0)) - acos(cosShoul.clamp(-1.0, 1.0));
    elbow.rotation = pi - elbowAngle;
  }
}

김개발 씨는 새로운 미션을 받았습니다. 캐릭터가 총을 들고 조준하는 게임인데, 마우스 커서 위치에 따라 캐릭터의 팔이 그 방향을 가리켜야 합니다.

처음에는 간단해 보였습니다. "어깨 뼈대를 마우스 방향으로 회전시키면 되겠지?" 그런데 막상 해보니 문제가 생겼습니다.

어깨만 회전하면 팔꿈치가 이상한 방향으로 꺾였습니다. 팔꿈치도 따로 계산해야 했습니다.

마우스 위치가 멀면 팔을 쭉 펴야 하고, 가까우면 팔꿈치를 구부려야 했습니다. 삼각함수가 머릿속에서 뒤엉켰습니다.

바로 이런 상황을 위해 IK가 존재합니다. 쉽게 비유하자면, IK는 마치 내비게이션과 같습니다.

우리가 "강남역"이라는 목적지만 입력하면, 내비게이션이 어떤 길로 가야 하는지 알아서 계산해줍니다. IK도 마찬가지입니다.

"손이 여기 있어야 해"라고 끝점만 지정하면, 어깨와 팔꿈치의 회전을 알고리즘이 자동으로 계산합니다. **FK(Forward Kinematics, 순운동학)**와는 무엇이 다를까요?

FK는 부모에서 자식 방향으로 계산합니다. 어깨를 30도 돌리고, 팔꿈치를 45도 구부리면, 손이 어디에 있는지 계산하는 방식입니다.

반면 IK는 그 반대입니다. 손이 어디에 있어야 하는지 정하면, 어깨와 팔꿈치가 몇 도여야 하는지 역으로 계산합니다.

위의 코드에서 사용된 알고리즘을 살펴보겠습니다. Two-bone IK는 두 개의 뼈대로 이루어진 체인을 푸는 알고리즘입니다.

팔(어깨-팔꿈치-손)이나 다리(골반-무릎-발)에 주로 사용됩니다. 핵심은 코사인 법칙입니다.

삼각형의 세 변의 길이를 알면 각 꼭지점의 각도를 구할 수 있다는 중학교 수학을 활용합니다. 상완 길이, 하완 길이, 타겟까지의 거리로 삼각형을 만들고, 각 관절의 각도를 계산합니다.

실제 게임에서는 어떻게 활용될까요? 슈팅 게임에서 캐릭터가 조준하는 방향을 마우스로 지정할 때 IK가 사용됩니다.

등반 게임에서 캐릭터가 벽에 손을 짚을 때도 IK가 필요합니다. 캐릭터가 물건을 집어드는 동작, 문고리를 돌리는 동작 등 "특정 위치에 손이나 발이 닿아야 하는" 모든 상황에서 IK가 빛을 발합니다.

주의할 점도 있습니다. IK는 해가 여러 개일 수 있습니다.

같은 목표 위치라도 팔꿈치가 위로 꺾이느냐 아래로 꺾이느냐에 따라 결과가 달라집니다. 또한 타겟이 팔 길이보다 멀면 닿을 수 없는 위치가 됩니다.

이런 엣지 케이스를 처리하는 것이 중요합니다. 김개발 씨는 IK를 적용한 후 마우스를 움직여봤습니다.

캐릭터의 팔이 마우스를 따라 자연스럽게 움직였습니다. "와, 이게 바로 그 느낌이에요!" IK를 이해하면 캐릭터와 환경의 상호작용을 자연스럽게 구현할 수 있습니다.

여러분도 다음 프로젝트에서 활용해 보세요.

실전 팁

💡 - 팔꿈치나 무릎이 반대로 꺾이지 않도록 pole vector(방향 힌트)를 사용하세요

  • 타겟이 너무 멀면 clamp하여 최대 팔 길이 내로 제한하세요
  • 복잡한 체인은 FABRIK 알고리즘을 검토해 보세요

4. 블렌딩과 전환

김개발 씨의 게임에서 캐릭터가 걷다가 갑자기 뛰는 애니메이션으로 바뀌는 순간, 뚝뚝 끊기는 느낌이 들었습니다. 마치 로봇이 동작을 전환하는 것 같았습니다.

박시니어 씨가 말했습니다. "애니메이션 블렌딩을 안 했네요.

전환 구간에서 두 애니메이션을 섞어줘야 자연스러워요."

애니메이션 블렌딩은 두 개 이상의 애니메이션을 섞어 부드러운 전환을 만드는 기법입니다. 마치 DJ가 두 곡을 믹싱할 때 페이드아웃과 페이드인을 사용하는 것처럼, 이전 애니메이션이 서서히 사라지면서 새 애니메이션이 점점 강해지는 방식으로 자연스러운 전환을 만들어냅니다.

다음 코드를 살펴봅시다.

// 애니메이션 블렌딩 시스템
class AnimationBlender {
  Animation? currentAnim;
  Animation? nextAnim;
  double blendFactor = 0.0;
  double blendDuration = 0.3;  // 0.3초 동안 블렌딩

  void transitionTo(Animation newAnim) {
    if (currentAnim == null) {
      currentAnim = newAnim;
      return;
    }
    nextAnim = newAnim;
    blendFactor = 0.0;
  }

  Map<String, double> getBlendedPose(double dt) {
    if (nextAnim != null) {
      blendFactor += dt / blendDuration;
      if (blendFactor >= 1.0) {
        currentAnim = nextAnim;
        nextAnim = null;
        blendFactor = 0.0;
      }
    }

    final currentPose = currentAnim!.getCurrentPose();
    if (nextAnim == null) return currentPose;

    final nextPose = nextAnim!.getCurrentPose();
    // 선형 보간으로 두 포즈 혼합
    return _lerpPose(currentPose, nextPose, blendFactor);
  }
}

김개발 씨는 플레이어 피드백을 읽다가 마음이 무거워졌습니다. "캐릭터 움직임이 로봇 같아요", "애니메이션 전환이 너무 갑작스러워요".

분명 걷기 애니메이션도 만들었고, 뛰기 애니메이션도 만들었는데, 왜 부자연스럽다는 걸까요? 박시니어 씨가 화면을 보더니 바로 원인을 짚었습니다.

"프레임 0에서 갑자기 다른 애니메이션 프레임 0으로 점프하고 있네요. 중간에 블렌딩 구간이 없어서 그래요." 애니메이션 블렌딩이란 무엇일까요?

쉽게 비유하자면, 슬라이드 쇼의 페이드 전환과 같습니다. 한 이미지가 서서히 사라지면서 다음 이미지가 천천히 나타나면 전환이 부드럽게 느껴집니다.

그냥 딱 바뀌어버리면 눈이 피로하고 어색합니다. 애니메이션도 마찬가지입니다.

걷기 동작에서 뛰기 동작으로 바뀔 때, 두 동작을 잠시 동안 섞어주면 훨씬 자연스럽습니다. 블렌딩의 핵심은 **보간(Interpolation)**입니다.

현재 애니메이션에서 각 뼈대의 위치와 회전 값이 있고, 다음 애니메이션에서도 각 뼈대의 값이 있습니다. 블렌딩은 이 두 값 사이를 일정 비율로 섞는 것입니다.

0%면 현재 애니메이션만, 100%면 다음 애니메이션만, 50%면 딱 중간 값이 됩니다. 이 비율을 시간에 따라 0에서 100으로 올리면 부드러운 전환이 완성됩니다.

위의 코드를 살펴보겠습니다. blendFactor가 핵심입니다.

새 애니메이션으로 전환을 시작하면 blendFactor는 0에서 시작합니다. 매 프레임마다 blendDuration에 맞춰 조금씩 증가합니다.

_lerpPose 함수에서 두 포즈를 blendFactor 비율로 섞습니다. blendFactor가 1에 도달하면 전환이 완료되고, 다음 애니메이션이 현재 애니메이션이 됩니다.

블렌딩 시간은 얼마가 적당할까요? 일반적으로 0.1초에서 0.3초 사이가 많이 사용됩니다.

너무 짧으면 여전히 끊기는 느낌이 들고, 너무 길면 동작이 늘어지는 느낌이 듭니다. 빠른 액션 게임은 0.1초 정도로 짧게, 느긋한 시뮬레이션 게임은 0.3초 정도로 여유롭게 설정하는 것이 좋습니다.

고급 기법으로 동기화 블렌딩도 있습니다. 걷기에서 뛰기로 바뀔 때, 단순히 블렌딩만 하면 발 위치가 어긋날 수 있습니다.

이를 해결하려면 두 애니메이션의 발 위치가 비슷한 시점에서 블렌딩을 시작하는 것이 좋습니다. 왼발이 앞에 나와 있는 프레임에서 다음 애니메이션도 왼발이 앞에 나와 있는 프레임으로 전환하는 식입니다.

김개발 씨는 0.2초 블렌딩을 적용했습니다. 캐릭터가 걷다가 뛸 때 훨씬 자연스러워졌습니다.

"이제야 좀 사람 같네요!" 애니메이션 블렌딩을 적용하면 게임의 전체적인 퀄리티가 올라갑니다. 작은 디테일이지만 플레이어 경험에 큰 차이를 만드는 기법입니다.

실전 팁

💡 - 비슷한 포즈 사이의 전환은 블렌딩 시간을 짧게, 다른 포즈면 길게 설정하세요

  • 점프나 피격 같은 즉각적인 반응은 블렌딩 없이 즉시 전환해야 할 수도 있습니다
  • 이징(Easing) 함수를 적용하면 더 자연스러운 가속/감속 효과를 줄 수 있습니다

5. 애니메이션 레이어

게임 개발이 한창 진행 중일 때, 기획자가 새로운 요청을 들고 왔습니다. "캐릭터가 걸으면서도 손을 흔들 수 있게 해주세요." 김개발 씨는 당황했습니다.

걷기 애니메이션과 손 흔들기 애니메이션을 따로 만들었는데, 이걸 어떻게 동시에 재생하지?

애니메이션 레이어는 신체 부위별로 다른 애니메이션을 동시에 적용하는 시스템입니다. 마치 포토샵에서 여러 레이어를 겹쳐 하나의 이미지를 만드는 것처럼, 하체에는 걷기 애니메이션을, 상체에는 손 흔들기 애니메이션을 적용하여 복합적인 동작을 표현할 수 있습니다.

다음 코드를 살펴봅시다.

// 애니메이션 레이어 시스템
class AnimationLayer {
  String name;
  Animation? animation;
  Set<String> affectedBones;  // 이 레이어가 영향주는 뼈대 목록
  double weight;  // 레이어 가중치 (0.0 ~ 1.0)

  AnimationLayer(this.name, this.affectedBones, {this.weight = 1.0});
}

class LayeredAnimationController {
  final List<AnimationLayer> layers = [];

  void addLayer(AnimationLayer layer) => layers.add(layer);

  Map<String, BonePose> computeFinalPose() {
    final Map<String, BonePose> finalPose = {};

    for (final layer in layers) {
      if (layer.animation == null) continue;
      final layerPose = layer.animation!.getCurrentPose();

      for (final boneName in layer.affectedBones) {
        if (layerPose.containsKey(boneName)) {
          final existing = finalPose[boneName];
          final newPose = layerPose[boneName]!;
          // 가중치 적용하여 레이어 합성
          finalPose[boneName] = existing == null
              ? newPose
              : BonePose.lerp(existing, newPose, layer.weight);
        }
      }
    }
    return finalPose;
  }
}

김개발 씨는 기획서를 다시 읽어봤습니다. 캐릭터가 할 수 있는 동작 조합이 엄청났습니다.

서 있으면서 대화, 걸으면서 대화, 뛰면서 총 쏘기, 앉아서 물건 줍기... 이 모든 조합을 개별 애니메이션으로 만들면 수백 개가 필요했습니다.

박시니어 씨가 힌트를 줬습니다. "레이어로 분리해서 생각해봐요.

하체 동작과 상체 동작을 따로 만들면 조합할 수 있잖아요." 애니메이션 레이어는 어떤 개념일까요? 영화 촬영에서 블루스크린 합성을 생각하면 이해하기 쉽습니다.

배경과 배우를 따로 촬영한 후 합성하면 어떤 배경에서든 배우가 연기하는 장면을 만들 수 있습니다. 애니메이션 레이어도 마찬가지입니다.

하체 애니메이션과 상체 애니메이션을 따로 만들어두고 합성하면 다양한 조합을 만들어낼 수 있습니다. 레이어를 어떻게 나눌까요?

일반적으로 베이스 레이어오버라이드 레이어로 구분합니다. 베이스 레이어는 전신 애니메이션입니다.

서기, 걷기, 뛰기, 앉기처럼 전체 몸의 기본 자세를 담당합니다. 오버라이드 레이어는 특정 부위만 덮어씁니다.

상체만 영향을 주는 총 쏘기, 팔만 영향을 주는 손 흔들기 같은 것들입니다. 위의 코드 구조를 살펴보겠습니다.

AnimationLayer 클래스에서 affectedBones가 핵심입니다. 이 집합에 포함된 뼈대만 해당 레이어의 영향을 받습니다.

상체 레이어라면 spine, chest, shoulder, arm, hand 같은 뼈대만 포함됩니다. LayeredAnimationController는 모든 레이어를 순회하면서 각 뼈대의 최종 포즈를 계산합니다.

같은 뼈대에 여러 레이어가 영향을 주면 가중치에 따라 블렌딩됩니다. 실무에서 자주 사용되는 레이어 구성이 있습니다.

첫 번째는 기본 레이어입니다. 서기, 걷기, 뛰기 같은 이동 애니메이션이 여기 들어갑니다.

두 번째는 상체 액션 레이어입니다. 무기 휘두르기, 아이템 사용 같은 상체 동작입니다.

세 번째는 표정 레이어입니다. 얼굴 뼈대만 따로 분리해서 기쁨, 슬픔, 분노 등을 표현합니다.

네 번째는 애디티브 레이어입니다. 숨쉬기, 흔들림 같은 미세한 움직임을 더합니다.

주의할 점이 있습니다. 레이어가 너무 많아지면 관리가 어려워지고 성능에도 영향을 줍니다.

보통 3~4개 레이어면 충분합니다. 또한 레이어 간에 뼈대가 겹칠 때 어떤 레이어가 우선권을 가질지 잘 설계해야 합니다.

김개발 씨는 하체 이동 레이어와 상체 액션 레이어로 분리했습니다. 걷기 6개, 상체 동작 10개로 60가지 조합이 가능해졌습니다.

"이렇게 효율적일 줄이야!" 애니메이션 레이어를 활용하면 적은 에셋으로 다양한 동작 조합을 만들 수 있습니다. 여러분의 게임에서도 꼭 활용해 보세요.

실전 팁

💡 - 척추(Spine) 뼈대를 기준으로 상체/하체를 나누면 자연스러운 분리가 됩니다

  • 애디티브 레이어는 가중치를 낮게(0.1~0.3) 설정해야 티가 나지 않습니다
  • 레이어 우선순위는 배열 순서로 관리하는 것이 직관적입니다

6. 절차적 애니메이션

마감이 다가오던 어느 날, 기획자가 또 새로운 요청을 했습니다. "캐릭터가 경사면을 걸을 때 발이 지면에 맞게 기울어졌으면 좋겠어요." 김개발 씨는 속으로 한숨을 쉬었습니다.

모든 각도의 경사면에 대해 애니메이션을 만들 수는 없으니까요. 박시니어 씨가 말했습니다.

"절차적 애니메이션으로 해결해야 해요."

**절차적 애니메이션(Procedural Animation)**은 미리 만들어둔 애니메이션 대신 코드로 실시간 동작을 생성하는 기법입니다. 마치 재즈 연주자가 악보 없이 즉흥 연주를 하는 것처럼, 알고리즘이 상황에 맞는 움직임을 그때그때 만들어냅니다.

환경과의 상호작용이나 물리 기반 움직임에 특히 유용합니다.

다음 코드를 살펴봅시다.

// 절차적 발 배치 시스템
class ProceduralFootPlacement {
  late Bone leftFoot;
  late Bone rightFoot;
  double groundCheckDistance = 50.0;

  void update(Terrain terrain) {
    // 왼발 지면 적응
    final leftGroundHit = terrain.raycast(
      leftFoot.worldPosition + Vector2(0, -10),
      Vector2(0, 1),
      groundCheckDistance,
    );
    if (leftGroundHit != null) {
      // 발 높이 조절
      leftFoot.position.y += leftGroundHit.distance - groundCheckDistance * 0.5;
      // 지면 법선에 맞춰 발 회전
      leftFoot.rotation = leftGroundHit.normal.angleToSigned(Vector2(0, -1));
    }

    // 오른발도 동일하게 처리
    final rightGroundHit = terrain.raycast(
      rightFoot.worldPosition + Vector2(0, -10),
      Vector2(0, 1),
      groundCheckDistance,
    );
    if (rightGroundHit != null) {
      rightFoot.position.y += rightGroundHit.distance - groundCheckDistance * 0.5;
      rightFoot.rotation = rightGroundHit.normal.angleToSigned(Vector2(0, -1));
    }
  }
}

김개발 씨는 게임을 테스트하다가 이상한 장면을 목격했습니다. 캐릭터가 비탈길을 걸어올라가는데, 발이 땅을 뚫고 들어가거나 공중에 떠 있었습니다.

평평한 바닥에서 만든 걷기 애니메이션이라 경사면에서는 어색할 수밖에 없었습니다. "경사면용 애니메이션을 따로 만들까요?" 김개발 씨가 물었습니다.

박시니어 씨가 고개를 저었습니다. "10도 경사, 20도 경사, 30도 경사...

다 만들 건가요? 이건 절차적으로 처리해야 해요." 절차적 애니메이션이란 무엇일까요?

쉽게 비유하자면, 미리 짜여진 안무와 즉흥 댄스의 차이와 같습니다. 일반 애니메이션은 안무처럼 모든 동작이 미리 정해져 있습니다.

반면 절차적 애니메이션은 음악에 맞춰 그 순간 가장 적절한 동작을 만들어내는 즉흥 댄스입니다. 코드가 실시간으로 상황을 분석하고 그에 맞는 움직임을 생성합니다.

어떤 상황에서 절차적 애니메이션이 필요할까요? 가장 대표적인 것이 지형 적응입니다.

계단을 오르내릴 때 발이 정확히 계단에 닿게 하거나, 울퉁불퉁한 바닥에서 발이 자연스럽게 기울어지게 합니다. 물리 기반 반응도 있습니다.

캐릭터가 밀렸을 때 비틀거리거나, 무거운 물건을 들 때 몸이 기울어지는 것을 표현합니다. 시선 처리도 절차적으로 할 수 있습니다.

캐릭터가 플레이어나 중요한 오브젝트를 자동으로 바라보게 합니다. 위의 코드는 발 배치(Foot Placement) 시스템입니다.

Raycast가 핵심 기술입니다. 발 위치에서 아래로 광선을 쏴서 지면과의 충돌점을 찾습니다.

충돌점의 높이에 맞춰 발 뼈대의 y좌표를 조절합니다. 충돌면의 법선(normal) 벡터에 맞춰 발을 회전시키면 경사면에서도 발바닥이 지면에 딱 맞습니다.

절차적 애니메이션과 기존 애니메이션을 함께 쓸 수 있습니다. 보통은 기존 애니메이션을 기반으로 하고, 절차적 애니메이션으로 보정하는 방식을 사용합니다.

걷기 애니메이션을 재생하되, 발이 지면에 닿는 순간에만 절차적으로 높이와 회전을 조절하는 식입니다. 이렇게 하면 기본적인 움직임의 자연스러움은 유지하면서 환경 적응력을 높일 수 있습니다.

주의해야 할 점이 있습니다. 절차적 애니메이션은 계산량이 많습니다.

매 프레임마다 레이캐스트를 하고, 수학 계산을 수행해야 합니다. 캐릭터가 많아지면 성능에 영향을 줄 수 있으므로, LOD(Level of Detail)를 적용하여 멀리 있는 캐릭터는 간단한 처리만 하는 것이 좋습니다.

김개발 씨는 발 배치 시스템을 적용했습니다. 캐릭터가 경사면을 걸을 때 발이 지면에 딱 맞았습니다.

계단을 오를 때도 자연스러웠습니다. "이제 진짜 게임 같아요!" 절차적 애니메이션을 활용하면 미리 만들어둔 에셋만으로는 불가능한 자연스러운 상호작용을 구현할 수 있습니다.

여러분의 게임에도 이 기법을 도입해 보세요.

실전 팁

💡 - 절차적 보정은 기존 애니메이션 위에 적용하는 것이 가장 자연스럽습니다

  • 레이캐스트는 매 프레임이 아닌 일정 간격으로만 수행해도 충분합니다
  • 결과값에 스무딩을 적용하면 떨림 현상을 줄일 수 있습니다

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

#Flutter#Flame#Animation#Spine#Rive#IK#ProceduralAnimation#Flutter,Flame,Game

댓글 (0)

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

함께 보면 좋은 카드 뉴스