Architecture & Engineering

액티브 래그돌 시스템 아키텍처: Motor6D 분리, 비례-미분(PD) 균형 제어 및 피격 회복

By DopaBrain 절차적 애니메이션 및 물리 기술팀 • Published 2026-10-01

로블록스의 기본 캐릭터 애니메이션은 키네마틱 Motor6D 관절에 의존합니다. 이로 인해 50톤 트럭에 치이거나 계단에서 구르고 고속 포탄에 직격당해도 애니메이션이 부자연스럽게 그대로 재생됩니다. 반대로 전통적인 사망 래그돌은 Humanoid를 완전히 끄고 관절을 풀어버려 마치 축 늘어진 국수 가락처럼 무기력하게 무너질 뿐, 스스로 서 있거나 상호작용하지 못합니다.

갱 비스트(Gang Beasts), 휴먼 폴 플랫(Human: Fall Flat), 그리고 최신 물리 기반 대전 액션 게임 같은 묵직하고 생동감 넘치는 타격감을 구현하기 위해 개발자들은 '액티브 래그돌(Active Ragdoll)'을 도입합니다. 본 실전 가이드에서는 Luau 기반의 상용급 액티브 래그돌 프레임워크를 제작합니다. Motor6D를 BallSocketConstraint 물리 힌지로 분리하고, PD 토크 모터를 통해 목표 포즈를 유지하며, 역진자 모델로 중심을 잡고, 피격 시 자연스럽게 비틀거리다 일어나는 복원 파이프라인을 구축합니다.

1. 키네마틱의 한계: 왜 기본 관절은 물리적 몰입감을 깨뜨리는가

기본 로블록스 리그는 외부의 물리적 충격을 완전히 무시하는 절대 좌표 변환으로 구동됩니다:

2. 수학적 기반: PD 제어기와 역진자(Inverted Pendulum) 균형 모델

액티브 래그돌은 연속적인 피드백 제어 루프를 통해 자세와 균형을 실시간으로 유지합니다:

3. Luau 액티브 래그돌 엔진 구현체

RunService.Stepped에서 물리 스텝과 동기화되어 매끄러운 토크를 가하는 고성능 Luau 스크립트입니다:

ActiveRagdollController.luau (PD 토크 및 균형 코어)
--!strict
local RunService = game:GetService("RunService")
local Players = game:GetService("Players")

export type ActiveJoint = {
    Motor: Motor6D,
    Socket: BallSocketConstraint,
    AlignOrientation: AlignOrientation,
    Attachment0: Attachment,
    Attachment1: Attachment,
    StiffnessKp: number,
    DampingKd: number,
}

local ActiveRagdoll = {}
ActiveRagdoll.__index = ActiveRagdoll

function ActiveRagdoll.new(character: Model)
    local self = setmetatable({}, ActiveRagdoll)
    self.Character = character
    self.Humanoid = character:WaitForChild("Humanoid") :: Humanoid
    self.RootPart = character:WaitForChild("HumanoidRootPart") :: BasePart
    self.Joints = {} :: {ActiveJoint}
    self.IsActive = false
    self.BalanceRecoveryTime = 0

    self:SetupJoints()
    return self
end

function ActiveRagdoll:SetupJoints()
    for _, desc in ipairs(self.Character:GetDescendants()) do
        if desc:IsA("Motor6D") and desc.Name ~= "RootJoint" then
            local part0 = desc.Part0
            local part1 = desc.Part1
            if part0 and part1 then
                local att0 = Instance.new("Attachment")
                att0.CFrame = desc.C0
                att0.Parent = part0

                local att1 = Instance.new("Attachment")
                att1.CFrame = desc.C1
                att1.Parent = part1

                local socket = Instance.new("BallSocketConstraint")
                socket.Attachment0 = att0
                socket.Attachment1 = att1
                socket.LimitsEnabled = true
                socket.TwistLimitsEnabled = true
                socket.UpperAngle = 60
                socket.TwistLowerAngle = -45
                socket.TwistUpperAngle = 45
                socket.Enabled = false
                socket.Parent = part1

                local align = Instance.new("AlignOrientation")
                align.Mode = Enum.OrientationAlignmentMode.TwoAttachment
                align.Attachment0 = att1
                align.Attachment1 = att0
                align.MaxTorque = 8000
                align.Responsiveness = 40
                align.Enabled = false
                align.Parent = part1

                table.insert(self.Joints, {
                    Motor = desc,
                    Socket = socket,
                    AlignOrientation = align,
                    Attachment0 = att0,
                    Attachment1 = att1,
                    StiffnessKp = 8000,
                    DampingKd = 40,
                })
            end
        end
    end
end

function ActiveRagdoll:Enable()
    self.IsActive = true
    self.Humanoid.PlatformStand = true

    for _, joint in ipairs(self.Joints) do
        joint.Motor.Enabled = false
        joint.Socket.Enabled = true
        joint.AlignOrientation.Enabled = true
    end
end

function ActiveRagdoll:Disable()
    self.IsActive = false
    self.Humanoid.PlatformStand = false

    for _, joint in ipairs(self.Joints) do
        joint.AlignOrientation.Enabled = false
        joint.Socket.Enabled = false
        joint.Motor.Enabled = true
    end
end

function ActiveRagdoll:ApplyImpulse(hitPart: BasePart, impulse: Vector3)
    hitPart:ApplyImpulse(impulse)
    -- 피격 시 일시적으로 관절 힘을 빼서 리얼한 피격감 연출
    for _, joint in ipairs(self.Joints) do
        if joint.Motor.Part1 == hitPart then
            joint.AlignOrientation.MaxTorque = joint.StiffnessKp * 0.15
            task.delay(0.35, function()
                joint.AlignOrientation.MaxTorque = joint.StiffnessKp
            end)
        end
    end
end

return ActiveRagdoll

4. 질량 중심 제어와 상체 직립 안정화

진정한 액티브 래그돌은 정지된 포즈 유지를 넘어 중력과 외력에 맞서 두 발로 똑바로 서 있어야 합니다:

5. 네트워크 리플리케이션 및 멀티플레이어 성능 최적화

다수의 플레이어가 격투를 벌이는 환경에서 랙 없이 물리 래그돌을 구동하는 최적화 규칙입니다:

Frequently Asked Questions

액티브 래그돌을 켜면 관절이 심하게 떨리거나 폭발하듯 튕겨 나가는 이유는 무엇인가요?

AlignOrientation.Responsiveness 값이 너무 높거나, BallSocketConstraint의 각도 제한(Limit)과 애니메이션 목표 각도가 충돌할 때 발생합니다. Responsiveness를 25~50 사이로 조율하고 소켓의 각도 제한 범위를 넉넉하게 확보하십시오.

액티브 래그돌의 성능 비용은 일반 애니메이션에 비해 얼마나 높은가요?

물리 엔진 솔버가 사지 관절마다 실시간 토크를 계산하므로 연산량이 증가합니다. 하지만 네트워크 소유권을 각 클라이언트에 분산하고 원거리 캐릭터에 물리 LOD를 적용하면 모바일 기기에서도 16인 이상의 난투전을 60 FPS로 안정적으로 유지할 수 있습니다.

액티브 래그돌 상태에서도 무기 공격이나 펀치 모션을 휘두를 수 있나요?

가능합니다. 애니메이션의 CFrame 변환을 실시간으로 읽어와 AlignOrientation의 목표 방향을 갱신하면, 팔이 칼이나 주먹을 물리적으로 휘두르면서도 단단한 방패나 벽에 부딪히면 튕겨 나가는 극상의 타격감이 구현됩니다.

완전히 기절해 쓰러진 래그돌이 다시 일어나는 과정은 어떻게 매끄럽게 처리하나요?

먼저 상체와 척추 관절의 MaxTorque를 점진적으로 높여 상체를 일으켜 세웁니다. 질량 중심이 골반 위에 안착하면 기상 애니메이션을 가동하면서 Motor6D 변환을 서서히 블렌딩하고 물리 소켓을 비활성화합니다.

Explore More Interactive Tests & Guides

Discover personalized cognitive assessments, stress evaluations, and game psychology tools on DopaBrain.

Go to Portal Hub