공중전 시뮬레이터, 해전 함대 게임, SF 메카 슈팅 장르의 로블록스 게임—예를 들어 Aero Fighters, Warship Tycoon 등—에서 유도 미사일의 궤적 품질은 게임의 긴장감을 결정합니다. 단순 CFrame.lookAt(target)이나 선형 보간(Lerp)에 의존하는 초보적인 유도 로직은 이동하는 목표를 맞추지 못하고 뒤꽁무니만 쫓다가 빗나가며, 회전각이 튀어 매우 인공적인 느낌을 줍니다.
실제 공대공 미사일(AIM-9 사이드와인더, 암람)의 위협적인 포물선 요격 궤적을 구현하려면 비례 항법(Proportional Navigation, PN)과 이산 PID 컨트롤러를 결합해야 합니다. 본 가이드에서는 Luau를 활용해 프로덕션 등급의 유도 미사일 엔진을 구축합니다. 시선(Line of Sight) 회전율 도출, PID 자세 제어 토크, 추진 및 공기 항력 탄도학, 최근접점(CPA) 기반 근접 신관을 체계적으로 다룹니다.
1. 순수 추적(Pure Pursuit)의 결함: 왜 비례 항법인가?
단순히 표적의 현재 위치를 바라보며 쫓아가는 순수 추적(Pure Pursuit) 방식은 다음과 같은 치명적 물리 결함을 낳습니다:
- 꼬리잡기 지연(Tail-Chase Lag): 표적이 직교 방향으로 회전하면 미사일은 표적의 과거 위치를 향해 궤적을 굽히면서 막대한 운동 에너지를 공중에 낭비합니다.
- 종말 G-포스 발산: 요격 직전 표적과의 거리가 좁혀지면 미사일이 요구받는 선회 각속도가 무한대로 치솟아 목표물을 지나치고 허공으로 튕겨 나갑니다.
- 비례 항법(Proportional Navigation)의 해결책: 시선 벡터(Line-of-Sight)의 회전 속도에 비례하는 횡방향 가속도(a_cmd = N * V_c * omega_los)를 인가하여 미래의 충돌 지점을 향해 직선으로 날아갑니다.
- 운동 에너지 보존: 비례 항법은 종말 선회각을 최소화하여 미사일의 속도를 끝까지 유지하므로 표적의 회피 기동을 극도로 어렵게 만듭니다.
2. 수학적 기초: 비례 항법 벡터 및 이산 PID 제어
요격 기하학은 미사일과 표적 간의 상대 운동학 벡터 방정식으로 계산됩니다:
- 상대 거리 벡터: R = TargetPosition - MissilePosition (거리 D = R.Magnitude).
- 접근 속도(Closing Velocity): V_closing = -(R:Dot(TargetVelocity - MissileVelocity)) / D.
- 시선(LOS) 각속도 벡터: omega_los = (R:Cross(TargetVelocity - MissileVelocity)) / (D * D).
- 지령 횡방향 가속도: a_cmd = N * V_closing * (omega_los:Cross(MissileVelocity.Unit)) (항법 상수 N은 통상 3.0 ~ 5.0).
- 이산 PID 조향 루프: 현재 진행 방향과 목표 지령 가속도 벡터 사이의 각도 오차를 비례(P), 적분(I), 미분(D) 항으로 피드백하여 미사일 핀을 부드럽게 제어합니다.
3. 프로덕션 등급 비례 항법 유도 미사일 Luau 구현
다음은 RunService.Heartbeat에서 정확한 물리 단위로 구동되는 모듈형 유도탄 컨트롤러 전체 코드입니다:
- 로켓 모터 연소 프로파일: 발사 후 초기 3.5초 동안 강력한 축방향 추진력을 가하고, 연료 소진 후에는 관성 활강 상태로 전환합니다.
- 최대 G-포스 클램핑: 미사일의 최대 횡선회 가속도를 25G(약 800 studs/s^2)로 제한하여 기동성이 뛰어난 전투기가 고선회 회피를 시도할 수 있도록 설계합니다.
- 예측형 근접 신관(Proximity Fuze): 표적과의 거리가 10 stud 이내로 좁혀지거나, 최근접점을 지나 거리가 다시 멀어지는 첫 번째 프레임에 파편 폭발을 격발합니다.
--!strict
local RunService = game:GetService("RunService")
local Workspace = game:GetService("Workspace")
local MissileEngine = {}
MissileEngine.__index = MissileEngine
export type MissileConfig = {
RootPart: BasePart,
TargetPart: BasePart,
ThrustForce: number,
MotorBurnDuration: number,
DragCoefficient: number,
NavConstant: number,
MaxTurnG: number,
ProximityRadius: number,
}
export type MissileState = {
Config: MissileConfig,
ElapsedTime: number,
PreviousDistance: number,
IsDetonated: boolean,
RayParams: RaycastParams,
}
local GRAVITY = Vector3.new(0, -Workspace.Gravity, 0)
function MissileEngine.New(missilePart: BasePart, target: BasePart): MissileState
local rayParams = RaycastParams.new()
rayParams.FilterType = RaycastFilterType.Exclude
rayParams.FilterDescendantsInstances = { missilePart.Parent or missilePart }
local state: MissileState = {
Config = {
RootPart = missilePart,
TargetPart = target,
ThrustForce = 350.0,
MotorBurnDuration = 3.5,
DragCoefficient = 0.002,
NavConstant = 4.0,
MaxTurnG = 25.0 * 32.2,
ProximityRadius = 10.0,
},
ElapsedTime = 0,
PreviousDistance = math.huge,
IsDetonated = false,
RayParams = rayParams,
}
return state
end
function MissileEngine.Step(state: MissileState, dt: number)
if state.IsDetonated then return end
local root = state.Config.RootPart
local target = state.Config.TargetPart
local cfg = state.Config
state.ElapsedTime += dt
local missilePos = root.Position
local targetPos = target.Position
local rangeVec = targetPos - missilePos
local distance = rangeVec.Magnitude
-- 근접 신관 감지: 최근접점(CPA) 통과 확인
if distance < cfg.ProximityRadius or (state.PreviousDistance < cfg.ProximityRadius and distance > state.PreviousDistance) then
state.IsDetonated = true
root:Destroy()
return
end
state.PreviousDistance = distance
local missileVel = root.AssemblyLinearVelocity
local targetVel = target.AssemblyLinearVelocity
local relVel = targetVel - missileVel
-- 시선(LOS) 회전 각속도 계산
local losRate = rangeVec:Cross(relVel) / (distance * distance)
local closingSpeed = -rangeVec:Dot(relVel) / distance
-- 비례 항법(PN) 지령 횡가속도 계산
local navAccel = Vector3.zero
if closingSpeed > 0 and missileVel.Magnitude > 10 then
local missileHeading = missileVel.Unit
local rawAccel = losRate:Cross(missileHeading) * (cfg.NavConstant * closingSpeed)
local clampedMagnitude = math.min(rawAccel.Magnitude, cfg.MaxTurnG)
if rawAccel.Magnitude > 0.001 then
navAccel = rawAccel.Unit * clampedMagnitude
end
end
-- 로켓 추진력 및 공기역학 항력 연산
local isBurning = state.ElapsedTime < cfg.MotorBurnDuration
local forwardThrust = isBurning and (root.CFrame.LookVector * cfg.ThrustForce) or Vector3.zero
local speed = missileVel.Magnitude
local dragForce = -missileVel * (speed * cfg.DragCoefficient)
local totalAccel = forwardThrust + navAccel + dragForce + GRAVITY
root.AssemblyLinearVelocity = missileVel + (totalAccel * dt)
-- 미사일 헤딩 노즈를 속도 벡터 방향으로 정렬
if speed > 5 then
root.CFrame = CFrame.lookAt(missilePos, missilePos + root.AssemblyLinearVelocity)
end
end
return MissileEngine
4. 공기역학 항력, 연료 소진 및 회피 기동 메커니즘
현실적인 미사일 시스템은 강력한 요격력과 정교한 유저 회피 플레이가 균형을 이루어야 합니다:
- 2단계 추진 동역학: 초기 3.5초 연소 단계에서 마하 2 수준까지 가속된 후, 연료 소진 시 공기 항력과 중력에 의해 포물선으로 떨어지는 활강 탄도를 형성합니다.
- 플레어(Flare) 열원 교란: 열원 기만체(Flare) 사출 시 미사일의 추적 타겟을 순간적으로 분리하여 미사일이 허공으로 급선회하도록 유도합니다.
- S자 급선회 유도 에너지 고갈: 미사일 진행 축에 수직으로 연속 급선회를 가하면 미사일이 최대 G-포스로 선회하며 유도 항력으로 속도가 급격히 감속됩니다.
- 배럴 롤 기동 요격 회피: 미사일 돌입 방향을 향해 배럴 롤을 수행하면 시선 각속도(LOS Rate)가 한계치를 초과하여 미사일 조향 솔버의 추적 성능을 무력화할 수 있습니다.
5. 멀티플레이어 네트워크 동기화 및 근접 폭발 연출
고속 비행 발사체를 멀티플레이어 환경에서 대역폭 낭비 없이 매끄럽게 동기화하는 구조:
- 서버 권한 물리 시뮬레이션: 미사일 위치와 궤적 계산을 서버 RunService.Heartbeat에서 전담하여 순간이동 핵 및 비인가 궤적 조작을 원천 차단합니다.
- 클라이언트 로컬 파티클 연출: 연막 트레일과 화염 파티클(Beam, ParticleEmitter)을 클라이언트 로컬에서 렌더링하여 네트워크 대역폭 패킷 전송을 0바이트로 절감합니다.
- 도플러 효과 음향 모듈레이션: 카메라와의 상대 접근 속도를 실시간 계산하여 제트 엔진 굉음의 Pitch를 동적으로 높여 음속 돌파의 공포감을 전달합니다.
- 최종 변위 Spherecast 충돌 보장: 폭발 직전 프레임 이동 벡터를 따라 전방 Spherecast를 발사하여 얇은 기체 표면을 뚫고 지나가는 물리 누락을 방지합니다.
Frequently Asked Questions
왜 단순 예측 사격 대신 비례 항법(Proportional Navigation)을 사용하나요?
단순 예측 사격은 표적이 등속 직선 운동을 한다고 가정하므로 표적이 방향을 꺾으면 빗나갑니다. 비례 항법은 표적의 시선 각속도 변화를 실시간으로 추적하여 선회 기동 중인 비행체도 정확히 요격하기 때문입니다.
항법 상수 N(Navigation Constant)의 크기는 미사일에 어떤 영향을 주나요?
N이 3일 때는 완만하고 에너지 효율적인 요격 궤적을 그리며, N이 5일 때는 발사 직후 표적 미래 위치로 급격히 머리를 꺾어 빠르게 요격하지만 공기 저항으로 속도가 더 빨리 줄어듭니다.
미사일이 너무 빨라 표적을 스쳐 지나가는 버그는 어떻게 막나요?
표적과의 거리가 좁혀지다가 다시 멀어지기 시작하는 순간(최근접점 CPA)을 감지하는 근접 신관(Proximity Fuze) 로직을 적용하여 직접 물리 충돌하지 않아도 치명적 반경 내에서 즉시 폭발하도록 처리합니다.