사이버펑크 레이싱, SF 우주선 탐사, 미래형 전투 시뮬레이터와 같은 로블록스 게임에서 기존 바퀴(Wheel) 기반 차량 물리는 심각한 이질감을 줍니다. 로블록스 기본 SpringConstraint 바퀴는 지형 턱에 걸려 전복되거나, 비현실적인 기계 마찰력을 발생시켜 공중에 떠다니는 반중력 기체의 유려함을 표현할 수 없습니다.
울퉁불퉁한 언덕, 모래 언덕, 수면 위를 덜덜 떨리는 떨림 없이 매끄럽게 활공하는 호버크래프트를 만들려면 제어공학의 폐루프(Closed-loop) PID 제어가 필수적입니다. 본 마스터 엔지니어링 가이드에서는 Luau로 프로덕션급 호버 서스펜션 엔진을 구축합니다. PID 고도 안정기를 수학적으로 유도하고, 4개 모서리 레이캐스트 서스펜션을 구현하며, 지면 효과(Ground Effect) 양력과 드리프트 뱅킹 선회를 완성합니다.
1. 진동의 함정: 단순 스프링 물리(Hooke's Law)가 부유 기체에서 실패하는 이유
단순 용수철 공식(F = -k * x)으로 호버크래프트를 제작하려 하면 심각한 물리적 불안정이 발생합니다:
- 공진 및 무한 통통 튐: 감쇠력(Damping)이 지형 변화에 능동적으로 반응하지 못하면, 범프를 넘을 때마다 차량이 공진을 일으켜 점점 더 높이 튕겨 오릅니다.
- 지형 관통(바닥 부딪힘): 단순 스프링은 급경사 하강 시 가속도를 예측하지 못해 바닥 지오메트리를 뚫고 충돌하는 현상이 일어납니다.
- 비대칭 전복: 경사면에 한쪽 모서리가 올라탈 때 비대칭 반발력으로 인해 기체가 뒤집히는 전복 사고가 빈번합니다.
- PID 제어기의 승리: 비례(P), 적분(I), 미분(D) 3개 항을 조합한 폐루프 제어기는 오버슈트와 진동을 즉시 소쇄하여 완벽한 수평 부유를 유지합니다.
2. 수학적 기초: PID 제어기 및 다점 레이캐스트 서스펜션 원리
호버크래프트 차체의 4개 모서리는 각각 독립적인 폐루프 고도 조절기로 작동합니다:
- 고도 오차 정의: 목표 부유 높이 H_target과 레이캐스트 실측 높이 H_current에 대해 오차 e(t) = H_target - H_current.
- 비례항(P): F_p = K_p * e(t). 서스펜션 압축 깊이에 비례하는 즉각적인 복원 추력 발생.
- 미분항(D): F_d = K_d * (de/dt) = -K_d * v_vertical. 수직 속도를 억제하여 튀어 오르는 진동(Bouncing)을 원천 차단.
- 적분항(I): F_i = K_i * sum(e * dt). 화물 탑재나 경사면에서 발생하는 장기적인 정상상태 처짐(Steady-state error)을 보정.
- 지면 효과(Ground Effect) 쿠션: 지표면 근접 시(H < 0.3 * 폭) 하부 공기 압축으로 양력이 증폭되는 수식을 적용하여 바닥 충돌 방지.
3. 완전한 호버크래프트 PID 스러스터 컨트롤러 Luau 구현체
RunService.Heartbeat에서 4점 레이캐스트를 투사하고 PID 수직 추력을 계산하는 프로덕션 Luau 모듈입니다:
- 독립 4모서리 PID 솔버: 차체 4곳에서 지면 거리를 측정하여 개별적인 맞춤 추력 부여.
- 안티 와인드업(Anti-Windup) 보호: 절벽에서 떨어질 때 적분값이 무한 누적되어 착지 시 로켓처럼 솟구치는 버그 방지.
- 뱅킹 롤-요 선회 결합: 조향 각속도에 비례하여 차체를 회전 안쪽으로 자연스럽게 기울이는 뱅킹 물리 적용.
--!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. 지면 효과 공기역학 및 뱅킹 선회 조작감
바퀴 달린 자동차와 완전히 구별되는 호버크래프트 특유의 조작감 설계:
- 공기역학적 지면 쿠션: 차체 폭의 절반 이하 고도에서는 지면 효과로 인해 양력 계수가 상승하여 바닥에 내리꽂히는 현상 방지.
- 좌우 선회 시 뱅킹 롤: 방향키 입력 시 차체를 회전 안쪽으로 10~15도 기울여 원심력을 상쇄하고 레이싱 스포츠카 같은 코너링 선사.
- 횡방향 스케그 드리프트: 얼음판 위를 미끄러지는 듯한 무마찰 표류를 막기 위해 측면 이동 속도에 비례한 공기 저항 감쇠력 적용.
- 지형 법선 자동 정렬: 4개의 독립 스러스터가 지면 높이를 각각 맞추므로 가파른 언덕이나 둔덕에서도 기체가 지형 경사면에 자연스럽게 정렬.
5. 프로덕션 최적화 및 네트워크 물리 복제
50인 멀티플레이어 환경에서 지연 없는 고속 비행을 유지하기 위한 엔지니어링 규칙:
- 탑승자 클라이언트 네트워크 소유권: 운전 중인 플레이어에게 SetNetworkOwner를 부여하여 로컬 기기에서 지연 시간 0으로 PID 루프 계산.
- 무인 기체 슬립 모드: 탑승자가 내리면 스러스터 PID 루프를 정지시키고 최소 유지 모드로 전환하여 불필요한 레이캐스트 차단.
- 레이캐스트 필터링: 기체 모델 및 탑승자 캐릭터 파츠를 RaycastParams에서 엄격히 제외하여 자체 충돌로 인한 튕김 버그 방지.
- 스러스터 제트 파티클: 계산된 PID 추력 퍼센트에 비례하여 열기 왜곡 및 분사 연기 방출량을 실시간 조절.
Frequently Asked Questions
일반 스프링 구속조건 대신 PID 제어기를 사용하는 이유는 무엇인가요?
스프링 구속조건은 정적인 기계 탄성만을 제공하므로, 언덕을 오를 때 지면에 부딪히거나 점프 착지 시 끝없이 튀어 오르는 문제가 발생합니다. PID 제어기는 오차의 변화율(미분항)을 실시간 감지하여 바운싱을 즉시 상쇄하고 항상 완벽한 목표 고도를 유지합니다.
경사면에서 호버크래프트가 미끄러져 떨어지는 현상을 어떻게 막나요?
측면 공기 스케그(Skeg) 감쇠력을 적용합니다. 차체의 로컬 우측 방향 속도 성분을 분리하여 이에 반대되는 횡방향 저항력을 적용하면, 미끄러짐 없는 정밀한 직진과 통제된 드리프트가 가능해집니다.
절벽에서 뛰어내릴 때 적분항(I)이 폭발하는 문제는 어떻게 해결하나요?
안티 와인드업(Anti-Windup) 클램핑을 적용합니다. 고도가 높아져 오차가 장시간 지속되더라도 적분 누적치가 안전 임계값(-50~50)을 넘지 못하게 잠가둠으로써, 착지 시 기체가 하늘로 솟구치는 버그를 차단합니다.
수면이나 움직이는 플랫폼 위에서도 호버링이 작동하나요?
네. Workspace:Raycast 충돌 마스크에 부유 가능한 지형(Terrain Water)과 오브젝트를 포함시키면, 지형 종류에 관계없이 균일한 PID 부유 반발력이 발생합니다.