로블록스의 기본 지형(Terrain) 수면은 아바타의 기초적인 수영과 플로팅을 지원하지만, 사실적인 해양 시뮬레이션, 고속 모터보트, 범선, 해상 전투 게임을 구현하기에는 물리적 한계가 큽니다. 기본 부력은 선박의 제어 불가능한 요동, 전복 버그, 움직이는 파도와의 비동기 문제를 야기합니다.
선체 각 모서리에 다지점 아르키메데스 부력 시스템을 구축하면 선체의 각 부분에 가해지는 양력을 독립적으로 계산할 수 있습니다. Luau 기반의 해석적 게르스트너(Gerstner) 파도 공식, 2차 유체 항력 텐서, AngularVelocity 감쇠 제어를 결합하면 파도의 고저차에 맞춰 자연스럽게 피치(Pitch)와 롤(Roll)을 형성하며 흔들리지 않는 조작감을 제공합니다.
1. 기본 지형 수면의 한계와 커스텀 선박 물리 엔진의 필요성
로블록스 기본 수면이 복잡한 해양 이동체에서 실패하는 구조적 이유:
- 균일한 바운딩 박스 부력: 내장 지형 수면은 선체 전체에 단일한 상향 가속도를 부여하므로, 선수(Bow) 들림, 선회 시 측면 뱅킹, 파도 타기 연산이 불가능합니다.
- 평면 수면 가정: 기본 지형 수면은 3D로 너울지는 파도로 변형되지 않으므로, 수학적 파도 변위 알고리즘 없이는 폭풍우 치는 바다를 연출할 수 없습니다.
- 고속 주행 충돌 이탈(Physics Launch): 고속 보트가 수면에 진입할 때 이산 충돌(Discrete collision) 침투 스파이크로 인해 배가 공중으로 솟구치는 현상이 발생합니다.
- 선회 롤 제어 불가: 선회 시 발생하는 원심력과 수면 저항에 따른 복원력 토크가 없어 보트가 뒤집히거나 팽이처럼 회전합니다.
2. 다지점 아르키메데스 부력 & 게르스트너 파도 샘플링
선체 분할 샘플링 포인트를 활용한 정밀 양력 연산:
- 4지점 선체 레이캐스팅: 선체의 4개 모서리(선수 좌/우, 선미 좌/우)에 Attachment를 배치하여 수면 깊이를 독립적으로 샘플링합니다.
- 해석적 게르스트너 파도 공식: 파도의 파장, 진폭, 이동 속도, 첨두도(Steepness) 벡터를 결합하여 임의의 좌표 `(X, Z)`에서 파도 높이 `Y`를 수학적으로 계산합니다.
- 침수 체적 기반 아르키메데스 양력: 수면 아래로 잠긴 깊이에 비례하여 상향 부력을 계산합니다: `F = (잠긴깊이 * 체적 * 중력 * 밀도) - (수직속도 * 댐핑)`.
- 자연스러운 복원력(Righting Torque): 파도나 선회로 인해 한쪽이 기울어지면 더 깊이 잠긴 쪽의 부력이 자동으로 커져 배가 원래 자세로 복원됩니다.
--!strict
local RunService = game:GetService("RunService")
local Workspace = game:GetService("Workspace")
local BoatPhysics = {}
-- 게르스트너 파도 구성 요소: {방향벡터, 첨두도, 파장, 전파속도}
local WAVES = {
{Dir = Vector2.new(1, 0).Unit, Steepness = 0.4, Length = 60, Speed = 12},
{Dir = Vector2.new(0.7, 0.7).Unit, Steepness = 0.25, Length = 35, Speed = 9},
{Dir = Vector2.new(-0.3, 0.9).Unit, Steepness = 0.15, Length = 18, Speed = 7}
}
local GRAVITY = Workspace.Gravity
local WATER_DENSITY = 1.025
local POINT_SUBMERGED_VOLUME = 850 -- 각 포인트별 가상 배수 체적
local DAMPING = 280
-- 월드 X, Z 좌표에서 현재 시간 기준 수면 높이 Y 계산
function BoatPhysics.GetWaveHeight(worldX: number, worldZ: number, timeVal: number): number
local elevation = 0
for _, wave in ipairs(WAVES) do
local k = (2 * math.pi) / wave.Length
local c = wave.Speed
local dot = wave.Dir.X * worldX + wave.Dir.Y * worldZ
local phase = k * (dot - c * timeVal)
local amplitude = wave.Steepness / k
elevation += math.sin(phase) * amplitude
end
return elevation
end
-- 선체 Attachment들의 부력 및 유체 저항 실시간 업데이트
function BoatPhysics.UpdateBuoyancy(hullRoot: BasePart, sampleAttachments: {Attachment}, vectorForces: {VectorForce})
local t = Workspace:GetServerTimeNow()
for i, attach in ipairs(sampleAttachments) do
local worldPos = attach.WorldPosition
local waterY = BoatPhysics.GetWaveHeight(worldPos.X, worldPos.Z, t)
local depth = waterY - worldPos.Y
local vf = vectorForces[i]
if depth > 0 then
-- 수면 아래 침수: 부력 및 수직 감쇠력 적용
local pointVelocity = hullRoot:GetVelocityAtPosition(worldPos)
local upwardForce = (depth * POINT_SUBMERGED_VOLUME * GRAVITY * WATER_DENSITY) - (pointVelocity.Y * DAMPING)
upwardForce = math.clamp(upwardForce, 0, 80000)
-- 수평 방향 물 저항(횡미끄러짐 억제)
local horizontalDrag = -Vector3.new(pointVelocity.X, 0, pointVelocity.Z) * 45
vf.Force = Vector3.new(horizontalDrag.X, upwardForce, horizontalDrag.Z)
else
-- 수면 위: 부력 제로
vf.Force = Vector3.zero
end
end
end
return BoatPhysics
3. 비등방성 유체 항력 텐서와 선체 유체역학
선체 형태에 따른 물의 저항력을 수학적으로 모델링하는 기법:
- 비등방성 2차 항력(Anisotropic Quadratic Drag): 선체는 전진 방향(용골 방향) 저항 계수가 낮고(~0.05), 측면 횡미끄러짐 저항 계수가 매우 높아(~1.2) 미끄러지지 않고 레일을 타듯 직진합니다.
- 활주(Planing) 모드 전환: 저속에서는 물을 밀어내는 배수(Displacement) 모드로 작동하다가, 가속 시 동적 양력이 선체를 들어 올려 저항이 급감하는 활주 모드로 전환됩니다.
- 각속도 댐핑(Angular Velocity Damping): 피치, 롤, 요 축의 회전 진동을 감쇠하기 위해 AngularVelocity 제약 조건이나 반대 방향 토크를 가해 흔들림을 안정화합니다.
- 선미 추진기 및 방향타 벡터: 선미(Stern) 프로펠러 위치에서 추진력과 방향타 회전 각도를 인가하여 실제 보트처럼 선미가 회전하며 안쪽으로 뱅킹(Heel)을 유도합니다.
4. 네트워크 소유권 및 래그 없는 멀티플레이어 동기화
운전자와 관전자 모두에게 완벽한 물리 동기화를 보장하는 네트워크 설계:
- 조종자 네트워크 소유권(Network Ownership): 플레이어가 조타석에 탑승하는 순간 `hullRoot:SetNetworkOwner(player)`를 호출하여 로컬에서 무지연 물리 시뮬레이션을 수행합니다.
- 동기화된 결정론적 파도 연산: 게르스트너 수식은 서버와 모든 클라이언트가 `Workspace:GetServerTimeNow()`를 기준으로 계산하므로, 별도의 네트워크 패킷 전송 없이 완벽히 일치하는 수면을 공유합니다.
- 승객 CFrame 앵커링: 격렬한 파도 주행 시 승객 아바타가 선체 바깥으로 튕겨 나가지 않도록 Seat Attachment나 물리 Weld로 선체에 구속합니다.
- 빈 선박 저주파 절전 모드: 운전자가 없는 정박 선박은 부력 연산 빈도를 낮추거나 앵커링 상태로 전환하여 서버 CPU 부하를 극소화합니다.
5. 상용 프로덕션 최적화 체크리스트
로블록스 스튜디오에서 버그 없는 선박 시스템을 출시하기 위한 핵심 원칙:
- 선체 무게중심(Center of Mass) 하향 배치: 무게중심(`AssemblyCenterOfMass`)을 흘수선(Waterline)보다 낮게 설정하여 전복 위험을 원천 차단합니다.
- 충돌 지오메트리 간소화: 선체 충돌 모델을 `PreciseConvexDecomposition` 또는 마찰력 0인 보이지 않는 웨지 블록으로 단순화하여 지형 끼임 현상을 방지합니다.
- 항적(Wake) 파티클 및 사운드 연동: 선체 속도와 파도 충격 강도에 맞춰 물보라 파티클과 바다 사운드그룹 음량을 실시간 조절합니다.
- 클라이언트 전용 수면 메시 디포메이션: `EditableMesh`나 Bone 리깅을 활용하여 Luau 게르스트너 파도 높이와 시각적 수면 렌더링을 100% 일치시킵니다.
Frequently Asked Questions
지형 수면 레이캐스트 대신 수학적 게르스트너 파도를 사용하는 이유는 무엇인가요?
로블록스 기본 지형 수면은 정적이며 3D 파도로 변형되지 않습니다. 해석적 게르스트너 파도를 사용하면 지오메트리 쿼리 없이 서버와 클라이언트 모두에서 무한한 대양 파도를 가볍고 정확하게 동기화할 수 있습니다.
보트 하나에 몇 개의 부력 샘플링 포인트가 가장 적절한가요?
모터보트나 일반 크기 선박은 선수 좌/우, 선미 좌/우 4개 포인트로도 충분히 안정적인 피치 및 롤 복원력을 냅니다. 거대 군함이나 화물선은 6~8개 포인트를 사용하여 여러 파도 봉우리에 걸치는 물리 현상을 구현합니다.
파도에 고속으로 부딪힐 때 배가 하늘로 튀어 오르는 버그는 어떻게 막나요?
프레임당 인가되는 최대 상향 부력을 클램핑(Clamp)하고, 선체가 수면을 완전히 벗어났을 때는 즉시 부력을 0으로 설정하며 공기 저항을 인가해야 합니다.