Dynamic Ragdoll 물리 연동과 크리티컬 피격 연출 구현

Dynamic Ragdoll 물리 연동과 크리티컬 피격 연출 구현

애니메이션 포즈를 물리 관절의 목표값으로 사용해 Dynamic Ragdoll을 안정적으로 구동하고 크리티컬 피격 시 힘과 회전 반응을 설계하는 방법을 정리합니다.

Dynamic Ragdoll이 필요한 이유

일반적인 래그돌은 캐릭터가 쓰러지는 순간 애니메이션 제어를 끄고 물리 시뮬레이션에 맡긴다. 구현은 단순하지만 공격을 맞은 뒤에도 이동하거나 공격 동작을 이어 가야 하는 게임에서는 몸이 쉽게 무너지고 조작감도 끊긴다.

Dynamic Ragdoll은 애니메이션이 만든 기준 포즈를 물리 관절의 목표로 삼는다. 각 뼈는 강체로 충돌과 관성을 유지하면서도 관절 모터가 목표 회전으로 되돌리려 한다. 따라서 발이 지형에 걸리거나 큰 충격을 받을 때도 자연스러운 반응을 만들 수 있다.

애니메이션 목표 포즈와 물리 강체가 함께 동작하는 Dynamic Ragdoll 캐릭터

구성 요소와 데이터 흐름

필요한 구성은 크게 세 가지다.

  • 애니메이터가 계산한 로컬 뼈 회전
  • 각 뼈에 대응하는 강체와 관절
  • 애니메이션 회전을 관절 목표 회전으로 바꾸는 추종 제어기

Animator가 갱신한 포즈를 읽고 물리 스텝에서 관절 목표를 갱신하는 순서를 분리해야 한다. 렌더링 프레임마다 들어오는 애니메이션 값을 마지막으로 저장해 두고 FixedUpdate 또는 물리 틱에서 사용하면 프레임률 변동에 덜 민감하다.

flowchart LR
    A[애니메이션 포즈 계산] --> B[뼈별 목표 로컬 회전 저장]
    B --> C[물리 틱]
    C --> D[관절 목표 회전과 강성 갱신]
    D --> E[강체 충돌 및 적분]
    E --> F[최종 물리 포즈 렌더링]

애니메이션용 스켈레톤과 물리용 스켈레톤을 분리하면 안정적이다. 애니메이션 스켈레톤은 목표 포즈를 제공하고 물리 스켈레톤은 강체와 관절만 담당한다. 렌더러는 물리 스켈레톤을 따르게 한다. 하나의 본 계층을 애니메이터와 물리가 동시에 직접 수정하면 갱신 순서에 따라 떨림이 생기기 쉽다.

관절 목표 회전 만들기

관절이 기대하는 회전 좌표계와 본의 로컬 회전 좌표계는 일치하지 않는 경우가 많다. 특히 Unity의 ConfigurableJoint.targetRotation은 관절 축과 연결 강체의 기준 회전을 고려해야 한다. 초기 상대 회전을 저장한 뒤 목표 로컬 회전과 조합하는 방식이 안전하다.

아래 예시는 개념을 드러내기 위한 Unity C# 코드다. 실제 프로젝트에서는 관절 축과 보조 축을 반영한 변환 함수를 별도로 검증해야 한다.

using UnityEngine;

public sealed class RagdollJointDriver : MonoBehaviour
{
    [SerializeField] private ConfigurableJoint joint;
    [SerializeField] private Transform animationBone;
    [SerializeField] private float spring = 1200f;
    [SerializeField] private float damper = 90f;

    private Quaternion restLocalRotation;

    private void Awake()
    {
        restLocalRotation = animationBone.localRotation;
    }

    private void FixedUpdate()
    {
        Quaternion animationDelta = animationBone.localRotation *
                                   Quaternion.Inverse(restLocalRotation);

        joint.targetRotation = Quaternion.Inverse(animationDelta);

        JointDrive drive = joint.slerpDrive;
        drive.positionSpring = spring;
        drive.positionDamper = damper;
        drive.maximumForce = Mathf.Infinity;
        joint.slerpDrive = drive;
    }
}

이 코드를 그대로 모든 리그에 적용하면 축 방향 때문에 반대로 회전하거나 축이 틀어질 수 있다. 캐릭터를 T 포즈에 둔 상태에서 팔꿈치, 무릎처럼 축 제한이 뚜렷한 관절부터 디버그하고 관절의 로컬 축과 연결 강체의 초기 회전을 기준으로 변환식을 맞추는 것이 좋다.

언리얼 엔진에서는 Physics Asset의 Constraint와 Physical Animation Component가 같은 역할을 제공한다. 물리 본에 프로파일을 적용하고 Set All Bodies Below Simulate Physics 또는 물리 블렌드 값을 이용해 적용 범위를 제어할 수 있다. C++에서 힘을 줄 때는 물리 본 이름과 충돌 지점을 함께 전달해 AddImpulse 또는 AddForce를 호출한다.

강성, 감쇠, 질량의 균형

관절 모터는 목표 각도와 현재 각도 사이의 오차를 줄이는 방향으로 토크를 낸다. 단순화하면 회전 제어 토크는 다음처럼 생각할 수 있다.

τ=kp(θtargetθ)kdω\tau = k_p(\theta_{target}-\theta)-k_d\omega

여기서 k_p는 목표 자세로 끌어당기는 강성이고 k_d는 흔들림을 줄이는 감쇠다. 강성만 높이면 자세를 잘 유지하는 대신 충격 후 진동하거나 관절이 불안정해질 수 있다. 감쇠를 함께 올리되 캐릭터가 지나치게 딱딱해지지 않는 범위를 찾는다.

튜닝은 한 관절씩 하기보다 부위별 프로파일로 시작하는 편이 효율적이다.

  • 골반과 척추는 큰 힘을 견디도록 높은 강성과 충분한 감쇠를 준다.
  • 팔과 다리는 척추보다 낮은 강성을 사용해 피격 반응을 보인다.
  • 손목, 발목, 목은 과도한 각도 제한과 낮은 최대 힘을 조합해 폭주를 막는다.
  • 질량은 골반과 몸통이 가장 무겁고 팔다리 끝으로 갈수록 가벼워지게 배분한다.

몸통은 자세를 유지하고 팔다리는 충격에 반응하도록 부위별 물리 프로파일을 나눈 예시

크리티컬 피격을 힘으로 설계하기

크리티컬 피격은 단순히 일반 피격의 힘을 크게 하는 방식보다 충격 위치, 방향, 자세 제어 약화를 함께 다루는 편이 설득력 있다. 먼저 공격 방향과 피격 지점을 계산한다.

Vector3 hitDirection = (victimCenter - attackerPosition).normalized;
Vector3 impulse = hitDirection * criticalImpulse;

hitRigidbody.AddForceAtPosition(
    impulse,
    hitPoint,
    ForceMode.Impulse
);

AddForceAtPosition처럼 지점에 충격량을 적용하면 선형 이동뿐 아니라 회전도 생긴다. 복부 중앙보다 어깨나 옆구리에 가해진 충격이 더 크게 비틀리는 이유다. 다만 네트워크 게임에서는 hitPoint, hitDirection, 공격 시점과 난수 시드를 서버 또는 권한 주체가 결정해야 클라이언트마다 다른 자세가 되는 문제를 줄일 수 있다.

크리티컬 상태에서는 짧은 시간 동안 해당 부위의 관절 드라이브를 약화한다. 예를 들어 몸통은 70%, 맞은 팔은 25%로 낮추고 0.15초 후 복구를 시작한다. 이때 즉시 원래 강성으로 되돌리면 반동이 튈 수 있으므로 보간을 사용한다.

public IEnumerator ApplyCriticalReaction(float duration)
{
    SetDriveMultiplier(spineMultiplier: 0.7f, armMultiplier: 0.25f);
    yield return new WaitForSeconds(duration);

    const float recoverTime = 0.2f;
    for (float t = 0f; t < recoverTime; t += Time.fixedDeltaTime)
    {
        float blend = Mathf.SmoothStep(0f, 1f, t / recoverTime);
        SetDriveMultiplier(
            spineMultiplier: Mathf.Lerp(0.7f, 1f, blend),
            armMultiplier: Mathf.Lerp(0.25f, 1f, blend)
        );
        yield return new WaitForFixedUpdate();
    }

    SetDriveMultiplier(1f, 1f);
}

연출을 더하고 싶다면 카메라 흔들림, 짧은 히트 스톱, 피격 방향 반대쪽으로의 상체 기울기 애니메이션을 같은 이벤트에서 제어한다. 이들은 물리 충격을 대체하는 요소가 아니라 플레이어가 충격을 읽기 쉽게 만드는 보조 수단이다. 히트 스톱 중 물리 시간을 완전히 멈출지 애니메이션만 멈출지도 게임의 조작 설계에 맞춰 결정해야 한다.

애니메이션 포즈를 유지한 채 흉부 피격 방향으로 상체가 휘는 Dynamic Ragdoll 치명타 반응

흔한 문제와 해결 순서

캐릭터가 계속 떨린다

가장 먼저 애니메이터와 물리가 같은 본을 동시에 쓰는지 확인한다. 구조가 분리되어 있다면 강성을 낮추고 감쇠를 높여 본다. 물리 고정 시간 간격이 너무 크거나 강체 질량 비율이 지나치게 큰 경우도 흔들림의 원인이다.

피격 후 땅속으로 파고든다

충돌체가 메시와 맞지 않거나 빠른 충격으로 터널링이 발생한 경우가 많다. 중요 부위에는 단순한 캡슐 또는 박스 충돌체를 배치하고 필요한 강체에 연속 충돌 감지를 적용한다. 힘을 무한히 키우기보다 충격량 상한을 두는 편이 안전하다.

다시 일어설 때 자세가 튄다

래그돌의 현재 물리 포즈와 기상 애니메이션의 첫 프레임이 다르면 큰 위치·회전 차이가 발생한다. 물리 포즈에서 엎드림과 뒤집힘을 판정한 뒤 대응하는 기상 애니메이션을 선택하고 루트 위치를 바닥에 맞춘 다음 짧게 블렌드한다. 완전 물리 상태에서 Dynamic Ragdoll 상태로 복귀할 때도 관절 강성을 단계적으로 올려야 한다.

구현 체크리스트

  • 애니메이션 포즈와 물리 포즈의 책임을 분리했는가
  • 관절 축과 목표 회전 변환을 본별로 검증했는가
  • 몸통과 팔다리에 서로 다른 강성·감쇠 프로파일을 사용했는가
  • 크리티컬 충격량에 상한값과 부위별 보정값이 있는가
  • 힘을 가한 뒤 관절 드라이브를 일시적으로 약화하고 부드럽게 복구하는가
  • 기상 동작의 시작 포즈가 현재 물리 포즈와 자연스럽게 이어지는가

Dynamic Ragdoll의 품질은 힘의 크기보다 자세를 유지하려는 제어와 충격에 양보하는 제어를 얼마나 자연스럽게 전환하느냐에 달려 있다. 기본 자세 추종을 먼저 안정화한 뒤 피격 부위, 충격 위치, 관절 약화 시간을 조절하면 재사용하기 좋은 피격 시스템으로 확장할 수 있다.

#Unity#Unreal Engine#C++#Physics#Ragdoll

계속 읽어보기

이런 글은 어떠세요?

< Back to Logs