Master Engineering & Neuroscience

SF 차량 동역학: 폐루프 PID 고도 제어, 4점 레이캐스트 서스펜션 및 지면 효과 공기역학

By DopaBrain 차량동역학 및 제어공학 연구팀 • 2026-10-01
2048 코치 PID 폐루프 제어 미적분, 미분 감쇠력 및 스러스터 유체 동역학 반응속도 테스트 뱅킹 선회 조향 반사, 장애물 회피 및 스러스터 반응 잠복기 뇌 유형 테스트 4점 레이캐스트 서스펜션 및 SF 비행 동역학 인지 아키타입 스트레스 지수 초고속 호버크래프트 드리프트 어지럼증, 절벽 낙하 패닉 및 수평 평정심

사이버펑크 레이싱, SF 우주선 탐사, 미래형 전투 시뮬레이터와 같은 로블록스 게임에서 기존 바퀴(Wheel) 기반 차량 물리는 심각한 이질감을 줍니다. 로블록스 기본 SpringConstraint 바퀴는 지형 턱에 걸려 전복되거나, 비현실적인 기계 마찰력을 발생시켜 공중에 떠다니는 반중력 기체의 유려함을 표현할 수 없습니다.

울퉁불퉁한 언덕, 모래 언덕, 수면 위를 덜덜 떨리는 떨림 없이 매끄럽게 활공하는 호버크래프트를 만들려면 제어공학의 폐루프(Closed-loop) PID 제어가 필수적입니다. 본 마스터 엔지니어링 가이드에서는 Luau로 프로덕션급 호버 서스펜션 엔진을 구축합니다. PID 고도 안정기를 수학적으로 유도하고, 4개 모서리 레이캐스트 서스펜션을 구현하며, 지면 효과(Ground Effect) 양력과 드리프트 뱅킹 선회를 완성합니다.

1. 진동의 함정: 단순 스프링 물리(Hooke's Law)가 부유 기체에서 실패하는 이유

단순 용수철 공식(F = -k * x)으로 호버크래프트를 제작하려 하면 심각한 물리적 불안정이 발생합니다:

2. 수학적 기초: PID 제어기 및 다점 레이캐스트 서스펜션 원리

호버크래프트 차체의 4개 모서리는 각각 독립적인 폐루프 고도 조절기로 작동합니다:

3. 완전한 호버크래프트 PID 스러스터 컨트롤러 Luau 구현체

RunService.Heartbeat에서 4점 레이캐스트를 투사하고 PID 수직 추력을 계산하는 프로덕션 Luau 모듈입니다:

HovercraftPIDEngine.luau (다점 서스펜션 및 제어 루프)
--!strict
local RunService = game:GetService("RunService")
local Workspace = game:GetService("Workspace")

export type PIDConfig = {
    Kp: number,
    Ki: number,
    Kd: number,
    MaxForce: number,
    MinForce: number,
}

export type ThrusterState = {
    Offset: Vector3,
    PrevError: number,
    Integral: number,
}

export type Hovercraft = {
    RootPart: BasePart,
    TargetHeight: number,
    Thrusters: { ThrusterState },
    PID: PIDConfig,
    LinearDrag: number,
    AngularDrag: number,
}

local HoverEngine = {}
HoverEngine.__index = HoverEngine

local GRAVITY = 196.2

function HoverEngine.new(root: BasePart, targetHeight: number): Hovercraft
    local halfX = root.Size.X * 0.45
    local halfZ = root.Size.Z * 0.45

    local thrusters: { ThrusterState } = {
        { Offset = Vector3.new(halfX, 0, halfZ), PrevError = 0, Integral = 0 },   -- Front Right
        { Offset = Vector3.new(-halfX, 0, halfZ), PrevError = 0, Integral = 0 },  -- Front Left
        { Offset = Vector3.new(halfX, 0, -halfZ), PrevError = 0, Integral = 0 },  -- Rear Right
        { Offset = Vector3.new(-halfX, 0, -halfZ), PrevError = 0, Integral = 0 }, -- Rear Left
    }

    local self: Hovercraft = setmetatable({
        RootPart = root,
        TargetHeight = targetHeight,
        Thrusters = thrusters,
        PID = {
            Kp = 850,
            Ki = 45,
            Kd = 120,
            MaxForce = root.AssemblyMass * GRAVITY * 2.5,
            MinForce = 0,
        },
        LinearDrag = 0.985,
        AngularDrag = 0.92,
    }, HoverEngine)

    return self
end

function HoverEngine:Update(dt: number)
    local root = self.RootPart
    local rootCF = root.CFrame
    local rootVel = root.AssemblyLinearVelocity
    local mass = root.AssemblyMass

    local rayParams = RaycastParams.new()
    rayParams.FilterType = RaycastFilterType.Exclude

    local totalThrustForce = Vector3.zero
    local totalThrustTorque = Vector3.zero

    for _, thruster in ipairs(self.Thrusters) do
        local worldPoint = rootCF:PointToWorldSpace(thruster.Offset)
        local rayDir = -rootCF.UpVector * (self.TargetHeight * 2.2)

        local result = Workspace:Raycast(worldPoint, rayDir, rayParams)
        local currentDist = result and (worldPoint - result.Position).Magnitude or (self.TargetHeight * 2.2)

        local errorVal = self.TargetHeight - currentDist

        -- P Term
        local pOut = self.PID.Kp * errorVal

        -- I Term with anti-windup clamping
        thruster.Integral = math.clamp(thruster.Integral + errorVal * dt, -50, 50)
        local iOut = self.PID.Ki * thruster.Integral

        -- D Term
        local dError = (errorVal - thruster.PrevError) / math.max(dt, 0.001)
        local dOut = self.PID.Kd * dError
        thruster.PrevError = errorVal

        -- Calculate net vertical thruster force
        local rawForce = pOut + iOut + dOut
        -- Counter gravity baseline
        local baseGravityShare = (mass * GRAVITY) / #self.Thrusters
        local finalThrust = math.clamp(rawForce + baseGravityShare, self.PID.MinForce, self.PID.MaxForce)

        if result then
            local thrustVector = rootCF.UpVector * finalThrust
            totalThrustForce += thrustVector

            local leverArm = worldPoint - root.AssemblyCenterOfMass
            totalThrustTorque += leverArm:Cross(thrustVector)
        end
    end

    -- Apply aerodynamic drag damping
    local horizontalVel = rootVel - (rootCF.UpVector * rootVel:Dot(rootCF.UpVector))
    local lateralDrift = horizontalVel:Dot(rootCF.RightVector)
    local lateralCounterForce = -rootCF.RightVector * (lateralDrift * mass * 4.0)

    root:ApplyAssemblyForce(totalThrustForce + lateralCounterForce)
    root:ApplyAssemblyTorque(totalThrustTorque)
end

return HoverEngine

4. 지면 효과 공기역학 및 뱅킹 선회 조작감

바퀴 달린 자동차와 완전히 구별되는 호버크래프트 특유의 조작감 설계:

5. 프로덕션 최적화 및 네트워크 물리 복제

50인 멀티플레이어 환경에서 지연 없는 고속 비행을 유지하기 위한 엔지니어링 규칙:

Frequently Asked Questions

일반 스프링 구속조건 대신 PID 제어기를 사용하는 이유는 무엇인가요?

스프링 구속조건은 정적인 기계 탄성만을 제공하므로, 언덕을 오를 때 지면에 부딪히거나 점프 착지 시 끝없이 튀어 오르는 문제가 발생합니다. PID 제어기는 오차의 변화율(미분항)을 실시간 감지하여 바운싱을 즉시 상쇄하고 항상 완벽한 목표 고도를 유지합니다.

경사면에서 호버크래프트가 미끄러져 떨어지는 현상을 어떻게 막나요?

측면 공기 스케그(Skeg) 감쇠력을 적용합니다. 차체의 로컬 우측 방향 속도 성분을 분리하여 이에 반대되는 횡방향 저항력을 적용하면, 미끄러짐 없는 정밀한 직진과 통제된 드리프트가 가능해집니다.

절벽에서 뛰어내릴 때 적분항(I)이 폭발하는 문제는 어떻게 해결하나요?

안티 와인드업(Anti-Windup) 클램핑을 적용합니다. 고도가 높아져 오차가 장시간 지속되더라도 적분 누적치가 안전 임계값(-50~50)을 넘지 못하게 잠가둠으로써, 착지 시 기체가 하늘로 솟구치는 버그를 차단합니다.

수면이나 움직이는 플랫폼 위에서도 호버링이 작동하나요?

네. Workspace:Raycast 충돌 마스크에 부유 가능한 지형(Terrain Water)과 오브젝트를 포함시키면, 지형 종류에 관계없이 균일한 PID 부유 반발력이 발생합니다.

Explore More Interactive Tests & Guides

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

Go to Portal Hub