로블록스의 전술 잠입, 서바이벌 호러, 비밀 침투 게임(Entry Point, The Rake 등)에서 사실적인 NPC 감각 지각은 몰입감을 결정짓는 핵심 요소입니다. 경비병이 콘크리트 벽을 뚫고 플레이어를 감지하는 전지전능한 버그가 발생하거나, 반대로 대낮의 밝은 마당을 전력 질주하는 침투자를 보고도 지나친다면 플레이어는 즉시 흥미를 잃게 됩니다.
완성도 높은 잠입 시스템을 구축하려면 시각 원뿔 계산, 물리적 그림자 차폐, 주변 조도 평가, 음향 파동 감쇠가 유기적으로 결합된 다중 모달 감각 파이프라인이 필수적입니다. 본 가이드에서는 Luau를 활용하여 내적 기반 시야각 콘, 태양광 차폐 레이캐스트, 재질별 발소리 전파, 무경계에서 교전 상태로 매끄럽게 전이되는 프로덕션급 스텔스 엔진을 단계별로 구현합니다.
1. 전지전능 감지의 한계: 단순 거리 판정이 실패하는 이유
유클리드 거리 판정(`(guard.Position - player.Position).Magnitude < range`)에만 의존하는 원시적인 감지 방식은 심각한 시스템적 결함을 초래합니다:
- 투시 감지(Wall-Hack): 두꺼운 콘크리트 벽, 닫힌 방화문, 층간 슬래브를 뚫고 플레이어를 감지하여 불합리한 경보를 울립니다.
- 후방 사각지대 부재: 경비병이 뒤돌아보지도 않고 등 뒤의 플레이어를 즉시 감지하여 은밀한 암살 및 테이크다운 플레이를 원천 차단합니다.
- 조도 및 그림자 무시: 칠흑 같은 어둠 속 환기구에 숨어 있는 플레이어와 대형 투광등 아래 서 있는 플레이어를 동일한 거리에서 감지합니다.
- 공간 차폐 해결책: 지각 시스템을 광학 시야 콘(내적 + 레이캐스트), 광원 노출도 샘플링, 음향 청취 반경으로 분리하여 모델링합니다.
2. 수학적 원리: 벡터 내적 시야각, 조도 역제곱 법칙 및 음향 전파 감쇠
사실적인 잠입 감각 시뮬레이션은 벡터 기하학, 광학 역제곱 감쇠 법칙 및 구조적 음향 흡수 공식을 기반으로 동작합니다:
- 시야각(FOV) 벡터 내적: 경비병의 시선 벡터 L과 대상 방향 단위 벡터 D의 내적 cos(theta) = dot(L, D) 계산. dot(L, D) >= cos(FOV / 2)이면 시야 호 내부에 위치합니다.
- 그림자 레이캐스트 차폐: 태양/달 방향 벡터를 역추적하여 플레이어 몸통으로 향하는 광선이 장애물에 가려지는지 검사하여 물리적 그림자 볼륨을 판정합니다.
- 포인트 라이트 역제곱 법칙: 조도 E = I / (d^2 + epsilon) 계산. 플레이어의 최종 시각 노출 계수 S는 기본 주변광에 감쇠된 인접 광원 조도의 합산으로 결정됩니다.
- 음향 재질별 감쇠: 발소리 강도 I_sound = 기준강도 * (이동속도 / 주행속도) * 재질계수 / (d^1.5)로 계산되며, 차단 벽면 통과 시 65% 감쇠됩니다.
3. 완전한 프로덕션 스텔스 감각 컨트롤러 Luau 구현
경비병의 시야선, 그림자 은폐율, 발소리 전파를 Heartbeat 주기에서 최적화된 RaycastParams로 판정하는 Luau 모듈입니다:
- 레이캐스트 시야각 콘: 경비병의 머리와 플레이어 뼈대 사이의 장애물 차폐 여부를 검증합니다.
- 그림자 및 조도 노출도 샘플링: 직사광선 차폐 여부에 따라 가시성 계수를 동적으로 조정합니다.
- 4단계 경계 상태 머신: 무경계(0%), 의심(25%), 경계(70%), 교전(100%) 상태를 유기적으로 전이합니다.
--!strict
-- StealthSensoryController.luau
-- 로블록스 스튜디오용 프로덕션 잠입 감각 제어 엔진
local Workspace = game:GetService("Workspace")
local Players = game:GetService("Players")
export type AlertState = "Unaware" | "Suspicious" | "Alerted" | "Combat"
export type GuardSensoryConfig = {
FieldOfViewDeg: number,
SightRangeMax: number,
HearingRangeMax: number,
ShadowDetectionFactor: number, -- 그림자 은폐 시 시각 감지 배율 (예: 0.35)
AlertBuildRate: number, -- 초당 경계치 상승량
AlertDecayRate: number, -- 초당 경계치 자연 감소량
}
local StealthController = {}
StealthController.__index = StealthController
function StealthController.new(guardModel: Model, config: GuardSensoryConfig)
local self = setmetatable({}, StealthController)
self.Guard = guardModel
self.Config = config
self.Head = guardModel:WaitForChild("Head") :: BasePart
self.Awareness = 0 -- 0 ~ 100 경계도 수치
self.State = "Unaware" :: AlertState
-- 충돌 필터링이 적용된 재사용 레이캐스트 파라미터
self.RayParams = RaycastParams.new()
self.RayParams.FilterType = RaycastFilterType.Exclude
self.RayParams.FilterDescendantsInstances = {guardModel}
self.RayParams.IgnoreWater = true
return self
end
function StealthController:IsTargetInSightCone(targetPos: Vector3): (boolean, number)
local eyePos = self.Head.Position
local lookDir = self.Head.CFrame.LookVector
local toTarget = targetPos - eyePos
local distance = toTarget.Magnitude
if distance > self.Config.SightRangeMax then
return false, 0
end
local dirNormalized = toTarget.Unit
local dotProduct = lookDir:Dot(dirNormalized)
local minDot = math.cos(math.rad(self.Config.FieldOfViewDeg * 0.5))
if dotProduct >= minDot then
return true, distance
end
return false, distance
end
function StealthController:IsTargetInShadow(targetPart: BasePart): boolean
-- 태양 방향 벡터 (게임 환경 설정에 맞게 동적 바인딩 가능)
local sunDir = Vector3.new(0.3, 0.9, 0.2).Unit
local rayOrigin = targetPart.Position + Vector3.new(0, 1, 0)
local rayDir = sunDir * 100
local result = Workspace:Raycast(rayOrigin, rayDir, self.RayParams)
-- 상공에 구조물이 있어 태양광이 차단되면 그림자 볼륨 내부에 위치함
return result ~= nil
end
function StealthController:EvaluateSensory(playerCharacter: Model, dt: number)
local targetTorso = playerCharacter:FindFirstChild("HumanoidRootPart") :: BasePart?
local targetHead = playerCharacter:FindFirstChild("Head") :: BasePart?
if not targetTorso or not targetHead then return end
local inCone, dist = self:IsTargetInSightCone(targetHead.Position)
local canSee = false
if inCone then
-- 시야 차폐 검증 레이캐스트
local eyePos = self.Head.Position
local toHead = targetHead.Position - eyePos
local hitResult = Workspace:Raycast(eyePos, toHead, self.RayParams)
if hitResult and hitResult.Instance:IsDescendantOf(playerCharacter) then
canSee = true
end
end
if canSee then
local inShadow = self:IsTargetInShadow(targetTorso)
local lightFactor = inShadow and self.Config.ShadowDetectionFactor or 1.0
local distanceFactor = 1.0 - math.clamp(dist / self.Config.SightRangeMax, 0, 1)
-- 근거리이거나 밝은 조명 아래 있을수록 경계 게이지가 빠르게 상승
local buildIncrement = self.Config.AlertBuildRate * distanceFactor * lightFactor * dt
self.Awareness = math.clamp(self.Awareness + buildIncrement, 0, 100)
else
-- 시야에서 사라졌을 때 경계치 자연 감소
self.Awareness = math.clamp(self.Awareness - (self.Config.AlertDecayRate * dt), 0, 100)
end
-- 상태 머신 전이 판정
if self.Awareness >= 100 then
self.State = "Combat"
elseif self.Awareness >= 70 then
self.State = "Alerted"
elseif self.Awareness >= 25 then
self.State = "Suspicious"
else
self.State = "Unaware"
end
end
return StealthController
4. 음향 전파 및 경계 단계별 행동 전이 상태 머신
시각뿐만 아니라 소리는 은신 잠입 플레이에서 긴장감 넘치는 두뇌 싸움을 유도하는 핵심 요소입니다:
- 지형 재질별 발소리 파동: 철망 위를 달릴 때는 반경 24스터드, 석조 바닥은 14스터드, 카펫 위에서 웅크려 이동할 때는 반경 3스터드로 소음이 제한됩니다.
- 구조물 음향 감쇠: 소리 파동이 콘크리트 벽을 통과할 때 도달 반경이 65% 감쇠되어 인접 방의 경비병이 부자연스럽게 감지하는 현상을 방지합니다.
- 의심 상태(Suspicious) 수색 행동: 경계치 25~69% 도달 시 정규 순찰을 멈추고 소리가 발생한 진원지로 고개를 돌리며 손전등을 비춥니다.
- 교전 상태(Combat) 분대 연계: 경계치 100% 도달 시 즉시 무전을 통해 반경 60스터드 내의 동료 NPC들에게 지원 신호를 발송하여 포위망을 형성합니다.
5. 모바일 최적화 및 레이캐스트 연산 부하 분산 전략
수십 명의 경비병 AI가 동작하는 대규모 잠입 맵에서 60 FPS를 유지하기 위한 최적화 기법입니다:
- 인터리빙 타임 슬라이싱: 모든 경비병을 매 프레임 연산하지 않고 4개 그룹으로 분할하여 4프레임마다 1회씩(15Hz) 교대로 지각 연산을 수행합니다.
- 공간 분할(Spatial Grid) 사전 필터링: 공간 해시 테이블을 활용하여 최대 감각 반경 바깥에 있는 플레이어-경비병 쌍은 내적 및 레이캐스트 연산에서 사전에 제외합니다.
- RaycastParams 인스턴스 재사용: 가비지 컬렉터 부하를 최소화하기 위해 감각 제어기 생성 시 단일 RaycastParams 객체를 할당하여 재사용합니다.
- 클라이언트 시각화 HUD 바인딩: 서버에서 동기화된 경계도 실수값을 클라이언트 GUI에 부드러운 호형 게이지 및 방향 지시 화살표로 렌더링합니다.
Frequently Asked Questions
철망 울타리나 반투명 유리창을 통해 플레이어가 감지되도록 하려면 어떻게 하나요?
RaycastParams의 필터 테이블을 조정하거나 투명도 속성을 검사합니다. 레이가 투명도 0.5 이상의 유리를 통과할 경우 광선을 완전히 차단하지 않고 감지 속도에 0.4배의 저하 계수를 적용하여 투과 시야를 구현할 수 있습니다.
레이캐스트를 쏘기 전에 왜 항상 벡터 내적을 먼저 계산해야 하나요?
레이캐스트는 3D 지형 물리 지오메트리를 탐색하는 무거운 연산입니다. 반면 벡터 내적은 3회의 곱셈으로 끝나는 초경량 연산이므로, 시야 밖 75%의 대상을 즉각 걸러내어 CPU 프레임타임을 비약적으로 절약할 수 있습니다.
순찰 중인 경비병의 손전등 불빛에 들어갔을 때의 판정은 어떻게 구현하나요?
손전등의 위치에서 플레이어 몸통으로 광선을 투사하여 스팟 조명의 투사 각도와 유효 거리 내에 있는지 검사합니다. 장애물에 가려지지 않았다면 즉시 조도 계수를 최댓값으로 설정하여 경계도를 급격히 상승시킵니다.
여러 명의 플레이어가 동시에 달릴 때 발소리 감지는 어떻게 처리하나요?
StealthController 내부에 음향 이벤트 큐를 유지하고 매 프레임 청취 반경 내에서 가장 데시벨이 높은 소리를 우선 처리합니다. 이를 통해 먼 곳의 발소리보다 가까운 곳의 총성이나 폭발 소리에 경비병이 먼저 반응하도록 제어합니다.
잠입 게임에서 가장 이상적인 경계도 자연 감소 속도는 얼마인가요?
초당 10~15포인트의 감소율을 설정할 때 플레이어가 엄폐물 뒤로 숨은 후 4~6초 내에 경비병의 의심을 풀 수 있어 가장 쾌적하고 긴장감 넘치는 잠입 루프를 제공합니다.