godot-genre-stealth
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseGenre: Stealth
游戏类型:潜行
Player choice, systemic AI, and clear communication define stealth games.
玩家选择、系统性AI以及清晰的信息传递是潜行游戏的核心要素。
Available Scripts
可用脚本
stealth_ai_controller.gd
stealth_ai_controller.gd
Expert AI controller with graduated detection, sound response, and alert state management.
具备渐进式检测、声音响应和警戒状态管理功能的专业AI控制器。
Core Loop
核心循环
Observe → Plan → Execute → Adapt → Complete观察 → 规划 → 执行 → 适应 → 完成NEVER Do in Stealth Games
潜行游戏中的绝对禁忌
- NEVER use instant binary detection — Gradual 0-100% detection with visual feedback (filling meter). Binary "seen/not seen" removes player agency and feels unfair.
- NEVER make guards see through walls — Raycast-based vision with collision masks. must check geometry. Wallhacks destroy stealth integrity.
has_line_of_sight() - NEVER use simple distance checks for sound — Sound propagates along paths, NOT straight-line distance. Through-wall hearing breaks immersion. -NEVER make combat as viable as stealth — If guns are easier than sneaking, players ignore stealth. Combat should be risky (outnumbered, limited ammo, loud alerts).
NavigationServer3D - NEVER hide detection reasons from player — Show WHY detected (light level high, made noise, in vision cone). "Gotcha" deaths frustrate, don't teach.
- NEVER use single sample point for player visibility — Sample multiple body parts (head, torso, feet). Hiding behind low cover should hide torso but expose head.
- NEVER forget peripheral vision — Humans have ~180° peripheral (less effective) + 60° focused vision. Single cone = unrealistic. Use composite shapes (Splinter Cell method).
- 绝对不要使用即时二元检测 —— 应采用带视觉反馈(填充进度条)的0-100%渐进式检测。二元化的"看见/没看见"会剥夺玩家的主动权,且让玩家感觉不公。
- 绝对不要让守卫穿墙视物 —— 采用基于射线检测(Raycast)的视野系统,并配合碰撞掩码。必须检查几何遮挡。穿墙透视会彻底破坏潜行游戏的完整性。
has_line_of_sight() - 绝对不要使用简单的距离检测来判断声音 —— 声音应沿路径传播,而非直线距离。穿墙听音会破坏沉浸感。
NavigationServer3D - 绝对不要让战斗和潜行同样可行 —— 如果枪战比潜行更容易,玩家就会忽略潜行玩法。战斗应具备高风险(敌人数量占优、弹药有限、触发大范围警报)。
- 绝对不要向玩家隐藏被检测的原因 —— 要明确显示被检测的原因(光照等级过高、发出噪音、进入视野锥)。"突然死亡"的设计只会让玩家受挫,无法起到教学作用。
- 绝对不要仅用单个采样点判断玩家可见性 —— 应对玩家身体的多个部位(头部、躯干、脚部)进行采样。躲在低矮掩体后应能遮挡躯干,但暴露头部。
- 绝对不要忘记周边视野 —— 人类的周边视野约为180°(辨识度较低)+ 60°的聚焦视野。单一视野锥不符合现实。应采用复合形状(《细胞分裂》的实现方式)。
Design Principles
设计原则
From industry experts (Splinter Cell, Dishonored, Hitman developers):
- Player Choice: Multiple valid approaches to every scenario
- Systemic Design: Rules-based AI that players can learn and exploit
- Clear Communication: Player always understands game state and threats
- Fair Detection: No "gotcha" moments - threats visible before dangerous
来自行业专家(《细胞分裂》《耻辱》《杀手》开发者)的经验:
- 玩家选择权:每个场景都有多种可行的解决方式
- 系统性设计:基于规则的AI,让玩家可以学习并利用其规律
- 清晰的信息传递:玩家始终了解游戏状态和威胁
- 公平的检测机制:没有"突然死亡"的陷阱——危险出现前玩家就能察觉威胁
AI Detection System
AI检测系统
Vision Cone Implementation
视野锥实现
Based on Splinter Cell Blacklist GDC talk - realistic vision uses composite shapes:
gdscript
class_name EnemyVision
extends Node3D
@export var forward_vision_range := 20.0 # Main vision cone
@export var peripheral_range := 10.0 # Side vision
@export var forward_fov := 60.0 # Degrees
@export var peripheral_fov := 120.0 # Degrees
@export var detection_speed := 1.0 # How fast detection builds
var detection_level := 0.0 # 0-100
var target: Node3D = null
func _physics_process(delta: float) -> void:
var player := get_player_if_visible()
if player:
# Detection rate varies by:
# - Distance (closer = faster)
# - Lighting on player
# - Player movement (moving = more visible)
# - In peripheral vs direct vision
var rate := calculate_detection_rate(player)
detection_level = min(100, detection_level + rate * delta)
else:
detection_level = max(0, detection_level - detection_speed * 0.5 * delta)
func get_player_if_visible() -> Player:
var player := get_tree().get_first_node_in_group("player")
if not player:
return null
var to_player := player.global_position - global_position
var distance := to_player.length()
var angle := rad_to_deg(global_basis.z.angle_to(-to_player.normalized()))
# Check forward cone
if angle < forward_fov / 2.0 and distance < forward_vision_range:
if has_line_of_sight(player):
return player
# Check peripheral (less effective)
elif angle < peripheral_fov / 2.0 and distance < peripheral_range:
if has_line_of_sight(player):
return player
return null
func calculate_detection_rate(player: Player) -> float:
var distance := global_position.distance_to(player.global_position)
var distance_factor := 1.0 - (distance / forward_vision_range)
var light_factor := player.get_light_level() # 0.0 = dark, 1.0 = lit
var movement_factor := 1.0 if player.velocity.length() > 0.5 else 0.3
return detection_speed * distance_factor * light_factor * movement_factor * 50.0基于《细胞分裂:黑名单》GDC演讲内容——真实的视野系统采用复合形状:
gdscript
class_name EnemyVision
extends Node3D
@export var forward_vision_range := 20.0 # Main vision cone
@export var peripheral_range := 10.0 # Side vision
@export var forward_fov := 60.0 # Degrees
@export var peripheral_fov := 120.0 # Degrees
@export var detection_speed := 1.0 # How fast detection builds
var detection_level := 0.0 # 0-100
var target: Node3D = null
func _physics_process(delta: float) -> void:
var player := get_player_if_visible()
if player:
# Detection rate varies by:
# - Distance (closer = faster)
# - Lighting on player
# - Player movement (moving = more visible)
# - In peripheral vs direct vision
var rate := calculate_detection_rate(player)
detection_level = min(100, detection_level + rate * delta)
else:
detection_level = max(0, detection_level - detection_speed * 0.5 * delta)
func get_player_if_visible() -> Player:
var player := get_tree().get_first_node_in_group("player")
if not player:
return null
var to_player := player.global_position - global_position
var distance := to_player.length()
var angle := rad_to_deg(global_basis.z.angle_to(-to_player.normalized()))
# Check forward cone
if angle < forward_fov / 2.0 and distance < forward_vision_range:
if has_line_of_sight(player):
return player
# Check peripheral (less effective)
elif angle < peripheral_fov / 2.0 and distance < peripheral_range:
if has_line_of_sight(player):
return player
return null
func calculate_detection_rate(player: Player) -> float:
var distance := global_position.distance_to(player.global_position)
var distance_factor := 1.0 - (distance / forward_vision_range)
var light_factor := player.get_light_level() # 0.0 = dark, 1.0 = lit
var movement_factor := 1.0 if player.velocity.length() > 0.5 else 0.3
return detection_speed * distance_factor * light_factor * movement_factor * 50.0Sound Detection System
声音检测系统
Based on Thief/Hitman implementation - sounds propagate along navigation paths:
gdscript
class_name SoundPropagation
extends Node基于《神偷》/《杀手》的实现方式——声音沿导航路径传播:
gdscript
class_name SoundPropagation
extends NodeSound travels through connected navigation points, not through walls
Sound travels through connected navigation points, not through walls
func propagate_sound(origin: Vector3, loudness: float, sound_type: String) -> void:
for enemy in get_tree().get_nodes_in_group("enemies"):
var path := NavigationServer3D.map_get_path(
get_world_3d().navigation_map,
origin,
enemy.global_position,
true
)
if path.is_empty():
continue # No path = sound blocked
var path_distance := calculate_path_length(path)
var heard_loudness := loudness - (path_distance * 0.5) # Falloff
if heard_loudness > enemy.hearing_threshold:
enemy.hear_sound(origin, sound_type, heard_loudness)func calculate_path_length(path: PackedVector3Array) -> float:
var length := 0.0
for i in range(1, path.size()):
length += path[i].distance_to(path[i - 1])
return length
undefinedfunc propagate_sound(origin: Vector3, loudness: float, sound_type: String) -> void:
for enemy in get_tree().get_nodes_in_group("enemies"):
var path := NavigationServer3D.map_get_path(
get_world_3d().navigation_map,
origin,
enemy.global_position,
true
)
if path.is_empty():
continue # No path = sound blocked
var path_distance := calculate_path_length(path)
var heard_loudness := loudness - (path_distance * 0.5) # Falloff
if heard_loudness > enemy.hearing_threshold:
enemy.hear_sound(origin, sound_type, heard_loudness)func calculate_path_length(path: PackedVector3Array) -> float:
var length := 0.0
for i in range(1, path.size()):
length += path[i].distance_to(path[i - 1])
return length
undefinedPlayer Light Level
玩家光照等级
gdscript
class_name LightDetector
extends Node3D
@export var sample_points: Array[Marker3D] # Multiple points on player body
func get_light_level() -> float:
var total := 0.0
var space := get_world_3d().direct_space_state
for point in sample_points:
for light in get_tree().get_nodes_in_group("lights"):
var dir := light.global_position - point.global_position
var query := PhysicsRayQueryParameters3D.create(
point.global_position,
light.global_position
)
var result := space.intersect_ray(query)
if result.is_empty(): # Not blocked
total += light.light_energy / dir.length_squared()
return clamp(total / sample_points.size(), 0.0, 1.0)gdscript
class_name LightDetector
extends Node3D
@export var sample_points: Array[Marker3D] # Multiple points on player body
func get_light_level() -> float:
var total := 0.0
var space := get_world_3d().direct_space_state
for point in sample_points:
for light in get_tree().get_nodes_in_group("lights"):
var dir := light.global_position - point.global_position
var query := PhysicsRayQueryParameters3D.create(
point.global_position,
light.global_position
)
var result := space.intersect_ray(query)
if result.is_empty(): # Not blocked
total += light.light_energy / dir.length_squared()
return clamp(total / sample_points.size(), 0.0, 1.0)AI Alert States
AI警戒状态
Three-phase system (industry standard):
gdscript
enum AlertState { IDLE, SUSPICIOUS, ALERTED, COMBAT }
class_name EnemyAI
extends CharacterBody3D
var alert_state := AlertState.IDLE
var suspicion_point: Vector3
var search_timer := 0.0
signal alert_state_changed(new_state: AlertState)
func transition_to(new_state: AlertState) -> void:
alert_state = new_state
alert_state_changed.emit(new_state)
match new_state:
AlertState.SUSPICIOUS:
play_animation("suspicious")
speak_dialogue("what_was_that")
AlertState.ALERTED:
speak_dialogue("who_goes_there")
# Other guards in range hear and become suspicious
alert_nearby_guards()
AlertState.COMBAT:
speak_dialogue("intruder")
trigger_alarm()行业标准的三阶段系统:
gdscript
enum AlertState { IDLE, SUSPICIOUS, ALERTED, COMBAT }
class_name EnemyAI
extends CharacterBody3D
var alert_state := AlertState.IDLE
var suspicion_point: Vector3
var search_timer := 0.0
signal alert_state_changed(new_state: AlertState)
func transition_to(new_state: AlertState) -> void:
alert_state = new_state
alert_state_changed.emit(new_state)
match new_state:
AlertState.SUSPICIOUS:
play_animation("suspicious")
speak_dialogue("what_was_that")
AlertState.ALERTED:
speak_dialogue("who_goes_there")
# Other guards in range hear and become suspicious
alert_nearby_guards()
AlertState.COMBAT:
speak_dialogue("intruder")
trigger_alarm()Visual Feedback (Critical!)
视觉反馈(至关重要!)
gdscript
class_name AlertIndicator
extends Node3D
@export var idle_icon: Texture2D
@export var suspicious_icon: Texture2D # "?"
@export var alerted_icon: Texture2D # "!"
@export var detection_meter: ProgressBar # Shows filling detection
func update_indicator(state: AlertState, detection: float) -> void:
detection_meter.value = detection
match state:
AlertState.IDLE:
icon.texture = idle_icon
detection_meter.visible = false
AlertState.SUSPICIOUS:
icon.texture = suspicious_icon
detection_meter.visible = true
AlertState.ALERTED:
icon.texture = alerted_icon
detection_meter.visible = falsegdscript
class_name AlertIndicator
extends Node3D
@export var idle_icon: Texture2D
@export var suspicious_icon: Texture2D # "?"
@export var alerted_icon: Texture2D # "!"
@export var detection_meter: ProgressBar # Shows filling detection
func update_indicator(state: AlertState, detection: float) -> void:
detection_meter.value = detection
match state:
AlertState.IDLE:
icon.texture = idle_icon
detection_meter.visible = false
AlertState.SUSPICIOUS:
icon.texture = suspicious_icon
detection_meter.visible = true
AlertState.ALERTED:
icon.texture = alerted_icon
detection_meter.visible = falsePlayer Abilities
玩家能力
Five categories of stealth tools (per Mark Brown's analysis):
根据Mark Brown的分析,潜行工具分为五大类:
1. Movement Alteration
1. 移动方式改变
gdscript
undefinedgdscript
undefinedCrouch, crawl, run (noisy vs quiet)
Crouch, crawl, run (noisy vs quiet)
func calculate_noise_level() -> float:
if is_crouching:
return 0.2
elif is_running:
return 1.0
else:
return 0.5
undefinedfunc calculate_noise_level() -> float:
if is_crouching:
return 0.2
elif is_running:
return 1.0
else:
return 0.5
undefined2. Information Gathering
2. 信息收集
gdscript
undefinedgdscript
undefinedPeek, scout, mark enemies
Peek, scout, mark enemies
func activate_detective_vision() -> void:
for enemy in get_tree().get_nodes_in_group("enemies"):
enemy.show_outline()
enemy.show_vision_cone()
undefinedfunc activate_detective_vision() -> void:
for enemy in get_tree().get_nodes_in_group("enemies"):
enemy.show_outline()
enemy.show_vision_cone()
undefined3. AI Manipulation
3. AI操控
gdscript
undefinedgdscript
undefinedThrow distractions
Throw distractions
func throw_distraction(target_position: Vector3) -> void:
var rock := distraction_scene.instantiate()
rock.global_position = target_position
add_child(rock)
SoundPropagation.propagate_sound(target_position, 30.0, "impact")
undefinedfunc throw_distraction(target_position: Vector3) -> void:
var rock := distraction_scene.instantiate()
rock.global_position = target_position
add_child(rock)
SoundPropagation.propagate_sound(target_position, 30.0, "impact")
undefined4. Space Control
4. 空间控制
gdscript
undefinedgdscript
undefinedShoot out lights, create hiding spots
Shoot out lights, create hiding spots
func shoot_light(light: Light3D) -> void:
light.visible = false
# Update light level for area
undefinedfunc shoot_light(light: Light3D) -> void:
light.visible = false
# Update light level for area
undefined5. Enemy Elimination
5. 敌人清除
gdscript
func perform_takedown(enemy: EnemyAI, lethal: bool) -> void:
if enemy.alert_state == AlertState.COMBAT:
return # Can't stealth kill alert enemy
if lethal:
enemy.die()
else:
enemy.knockout()
# Body becomes interactable
spawn_body(enemy)gdscript
func perform_takedown(enemy: EnemyAI, lethal: bool) -> void:
if enemy.alert_state == AlertState.COMBAT:
return # Can't stealth kill alert enemy
if lethal:
enemy.die()
else:
enemy.knockout()
# Body becomes interactable
spawn_body(enemy)Level Design
关卡设计
Outpost Design (Open Areas)
前哨站设计(开放区域)
[Safe perimeter for observation]
|
[Sparse guards at edges - isolatable]
|
[Dense center with objective]
|
[Multiple entry points/routes] [用于观察的安全外围区域]
|
[边缘区域守卫稀疏 - 可单独处理]
|
[核心区域守卫密集,包含目标点]
|
[多个入口与路线选择]Limited Encounter Design (Corridors)
受限场景设计(走廊)
- Enemies visible 8+ meters before engagement
- Multiple paths through
- Cover objects and hiding spots
- Emergency escape routes
- 敌人在交战前8米以上即可被玩家看见
- 提供多条通行路径
- 设置掩体和隐藏点
- 规划紧急逃生路线
UI Communication
UI信息传递
Based on Thief's "light gem" innovation:
gdscript
class_name StealthHUD
extends Control
@onready var visibility_meter: TextureProgressBar
@onready var sound_meter: TextureProgressBar
@onready var minimap: Control
func _process(_delta: float) -> void:
visibility_meter.value = player.get_light_level() * 100
sound_meter.value = player.current_noise_level * 100基于《神偷》"光宝石"的创新设计:
gdscript
class_name StealthHUD
extends Control
@onready var visibility_meter: TextureProgressBar
@onready var sound_meter: TextureProgressBar
@onready var minimap: Control
func _process(_delta: float) -> void:
visibility_meter.value = player.get_light_level() * 100
sound_meter.value = player.current_noise_level * 100Common Pitfalls
常见陷阱
| Pitfall | Solution |
|---|---|
| Instant detection | Use gradual detection with clear feedback |
| Guards see through walls | Raycast-based vision with proper collision |
| Unfair patrol patterns | Make patterns learnable, with tells |
| Two games (stealth + combat) | Either commit to stealth or make combat risky |
| Unclear detection | Always show WHY player was detected |
| 陷阱 | 解决方案 |
|---|---|
| 即时检测 | 使用带清晰反馈的渐进式检测 |
| 守卫穿墙视物 | 采用基于射线检测的视野系统,并正确设置碰撞 |
| 不合理的巡逻路线 | 让巡逻路线可被玩家学习,并设置明显的提示 |
| 两种玩法并行(潜行+战斗) | 要么专注于潜行,要么让战斗具备高风险 |
| 检测原因不明确 | 始终向玩家展示被检测的原因 |
Godot-Specific Tips
Godot专属技巧
- Raycasts for vision: Use with collision masks
PhysicsRayQueryParameters3D - NavigationAgent3D: For patrol routes and pathfinding
- Area3D: For sound propagation zones and trigger areas
- AnimationTree: Blend between alert state animations
- 视野射线检测:使用并配合碰撞掩码
PhysicsRayQueryParameters3D - NavigationAgent3D:用于实现巡逻路线和寻路
- Area3D:用于声音传播区域和触发区域
- AnimationTree:实现警戒状态之间的动画过渡
Reference
参考资料
- Master Skill: godot-master
- 大师技能:godot-master