์นดํ…Œ๊ณ ๋ฆฌ ์—†์Œ

Godot 4 3D ๋Œ€๊ทœ๋ชจ AI NPC ๊ธธ์ฐพ๊ธฐ ์„ฑ๋Šฅ ์ตœ์ ํ™”: NavigationServer3D ๋ฉ€ํ‹ฐ์Šค๋ ˆ๋”ฉ๊ณผ ๋ฉ”์ธ ์Šค๋ ˆ๋“œ ๋ณ‘๋ชฉ ํŠธ๋Ÿฌ๋ธ”์ŠˆํŒ…

๊ฒŒ์ž„๊ต์ˆ˜ 2026. 8. 27. 19:01
๋ฐ˜์‘ํ˜•

๐ŸŒ English Abstract

This article provides an in-depth technical analysis and troubleshooting guide for resolving main thread bottlenecks when handling large-scale 3D AI NPC pathfinding in Godot 4. By leveraging the updated NavigationServer3D architecture, asynchronous path queries, WorkerThreadPool, and multi-threaded RVO2 avoidance agent updates, developers can significantly eliminate frame drops. We also cover dynamic NavMesh baking in background threads and handling race conditions during server synchronization.

์˜คํ”ˆ์†Œ์Šค 3D ๊ฒŒ์ž„ ์—”์ง„์ธ ๊ณ ๋„ ์—”์ง„ 4(Godot 4)๋Š” ์„œ๋ฒ„ ๊ธฐ๋ฐ˜ ์•„ํ‚คํ…์ฒ˜๋ฅผ ์ ๊ทน ํ™œ์šฉํ•˜์—ฌ ๋ Œ๋”๋ง, ๋ฌผ๋ฆฌ, ๋‚ด๋น„๊ฒŒ์ด์…˜ ๋“ฑ์„ ๋…๋ฆฝ๋œ ์„œ๋ฒ„ ๋ชจ๋“ˆ๋กœ ๊ด€๋ฆฌํ•ฉ๋‹ˆ๋‹ค. ํ•˜์ง€๋งŒ ์˜คํ”ˆ์›”๋“œ๋‚˜ ๋Œ€๊ทœ๋ชจ RTS, ์•ก์…˜ RPG ๊ฒŒ์ž„์—์„œ ์ˆ˜๋ฐฑ ๋ช…์—์„œ ์ˆ˜์ฒœ ๋ช…์— ์ด๋ฅด๋Š” ์ธ๊ณต์ง€๋Šฅ ๋น„ํ”Œ๋ ˆ์ด์–ด ์บ๋ฆญํ„ฐ(AI NPC)์˜ ๊ธธ์ฐพ๊ธฐ ์—ฐ์‚ฐ์„ ์ฒ˜๋ฆฌํ•  ๋•Œ ์—ฌ์ „ํžˆ ๋ฉ”์ธ ์Šค๋ ˆ๋“œ ๋ณ‘๋ชฉ(Main Thread Bottleneck) ํ˜„์ƒ๊ณผ ํ”„๋ ˆ์ž„ ๋“œ๋ž์ด ๋ฐœ์ƒํ•˜๊ณค ํ•ฉ๋‹ˆ๋‹ค.

๊ณ ๋„ 4์˜ ๋‚ด๋น„๊ฒŒ์ด์…˜ ์„œ๋ฒ„(NavigationServer3D)๋Š” ๋‚ด๋ถ€์ ์œผ๋กœ ๋ฉ€ํ‹ฐ์Šค๋ ˆ๋“œ ์ง€์› ๊ตฌ์กฐ๋ฅผ ๊ฐ–์ถ”๊ณ  ์žˆ์œผ๋‚˜, ์ด๋ฅผ ์ž˜๋ชป ๊ตฌ์„ฑํ•˜๊ฑฐ๋‚˜ GDScript ์—ฐ๋™ ์‹œ ๋™๊ธฐ์‹ ์š”์ฒญ์„ ๋‚จ๋ฐœํ•˜๊ฒŒ ๋˜๋ฉด ๋ฌผ๋ฆฌ ์Šค๋ ˆ๋“œ(Physics Thread) ๋ฐ ๋ Œ๋” ์Šค๋ ˆ๋“œ(Render Thread)์™€์˜ ๋™๊ธฐํ™” ์ง€์ ์—์„œ ๋ณ‘๋ชฉ์ด ํ„ฐ์ง€๊ฒŒ ๋ฉ๋‹ˆ๋‹ค. ๋ณธ ๊ธ€์—์„œ๋Š” ์‹ค์ œ ๋Œ€๊ทœ๋ชจ NPC ํ™˜๊ฒฝ์—์„œ ๋ฐœ์ƒํ•˜๋Š” ๊ธฐ์ˆ ์  ๋ฌธ์ œ ์›์ธ์„ ์ •๋ฐ€ ๋ถ„์„ํ•˜๊ณ , ๋ฉ€ํ‹ฐ์Šค๋ ˆ๋“œ ๊ธฐ๋ฐ˜ ๋‚ด๋น„๋ฉ”์‹œ(NavMesh) ์—ฐ์‚ฐ ์ตœ์ ํ™”๋ฅผ ํ†ตํ•ด ์‹ค์‹œ๊ฐ„ 60FPS ์ด์ƒ์„ ์œ ์ง€ํ•˜๋Š” ์‹ฌํ™” ํŠธ๋Ÿฌ๋ธ”์ŠˆํŒ… ์ ‘๊ทผ๋ฒ•์„ ๊ณต์œ ํ•ฉ๋‹ˆ๋‹ค.



1. NavigationServer3D ์•„ํ‚คํ…์ฒ˜์™€ ๋ฉ”์ธ ์Šค๋ ˆ๋“œ ๋ณ‘๋ชฉ ์›์ธ ๋ถ„์„

Godot 4์˜ ๋‚ด๋น„๊ฒŒ์ด์…˜ ์‹œ์Šคํ…œ์€ Node3D ์”ฌ ํŠธ๋ฆฌ์— ์ข…์†์ ์ด์—ˆ๋˜ Godot 3 ์‹œ๋Œ€์˜ ์ œํ•œ์—์„œ ๋ฒ—์–ด๋‚˜, ๋ฐ์ดํ„ฐ ์ค‘์‹ฌ์˜ C++ ์ฝ”์–ด๋กœ ์ž‘๋™ํ•˜๋Š” NavigationServer3D ๊ตฌ์กฐ๋กœ ์ „๋ฉด ์žฌ์„ค๊ณ„๋˜์—ˆ์Šต๋‹ˆ๋‹ค. ๊ทธ๋Ÿผ์—๋„ ๋ถˆ๊ตฌํ•˜๊ณ  ๋Œ€๊ทœ๋ชจ ์”ฌ์—์„œ ๋ณ‘๋ชฉ์ด ๋ฐœ์ƒํ•˜๋Š” ์ฃผ๋œ ์ด์œ ๋Š” ์„œ๋ฒ„์™€ ๋ฉ”์ธ ์Šค๋ ˆ๋“œ ๊ฐ„์˜ ๋ช…๋ น ํ(Command Queue) ๋™๊ธฐํ™” ๋ฐ ์ž˜๋ชป๋œ API ํ˜ธ์ถœ ๋ฐฉ์‹์— ์žˆ์Šต๋‹ˆ๋‹ค.

์ฃผ์š” ํ”„๋ ˆ์ž„ ๋“œ๋ž ์›์ธ ์„ธ ๊ฐ€์ง€

  • ๋™๊ธฐ์‹ ๊ฒฝ๋กœ ๊ฒ€์ƒ‰ ์—ฐ์‚ฐ(Synchronous Path Query): NPC๊ฐ€ ์ด๋™์„ ์‹œ์ž‘ํ•˜๊ฑฐ๋‚˜ ๊ฒฝ๋กœ๋ฅผ ์žฌ์„ค์ •ํ•  ๋•Œ NavigationServer3D.map_get_path()๋ฅผ ๋ฉ”์ธ ์Šค๋ ˆ๋“œ์—์„œ ์ฆ‰๊ฐ ํ˜ธ์ถœํ•˜๋ฉด A* ๊ธธ์ฐพ๊ธฐ ์•Œ๊ณ ๋ฆฌ์ฆ˜์ด ์™„๋ฃŒ๋  ๋•Œ๊นŒ์ง€ ๋ฉ”์ธ ์Šค๋ ˆ๋“œ๊ฐ€ ์ฆ‰๊ฐ ์ฐจ๋‹จ(Block)๋ฉ๋‹ˆ๋‹ค.
  • RVO2 ์žฅ์• ๋ฌผ ํšŒํ”ผ ์—ฐ์‚ฐ(Avoidance Agent Processing) ๋ˆ„์ : ์ˆ˜๋ฐฑ ๊ฐœ์˜ ๋‚ด๋น„๊ฒŒ์ด์…˜ ์—์ด์ „ํŠธ(NavigationAgent3D)๊ฐ€ ๋งค ํ”„๋ ˆ์ž„๋งˆ๋‹ค ์ƒ๋Œ€์†๋„ ์žฅ์• ๋ฌผ ํšŒํ”ผ(RVO2 - Reciprocal Velocity Obstacles) ์—ฐ์‚ฐ์„ ์ˆ˜ํ–‰ํ•˜๋ฉด์„œ CPU ์—ฐ์‚ฐ๋Ÿ‰์ด ๊ธ‰์ฆํ•ฉ๋‹ˆ๋‹ค.
  • ๋™์  ๋‚ด๋น„๋ฉ”์‹œ ๋ฒ ์ดํ‚น(Dynamic NavMesh Baking) ๋™๊ธฐํ™”: ์ง€ํ˜•์ด ํŒŒ๊ดด๋˜๊ฑฐ๋‚˜ ์ƒˆ๋กœ์šด ๊ตฌ์กฐ๋ฌผ์ด ์ƒ์„ฑ๋  ๋•Œ ๋‚ด๋น„๋ฉ”์‹œ ๋ฆฌ์ „(NavigationRegion3D)์„ ๋ฉ”์ธ ์Šค๋ ˆ๋“œ์—์„œ ๋ฒ ์ดํ‚นํ•˜๋ฉด ํ”„๋ ˆ์ž„ ํƒ€์ž„์ด ์ˆœ๊ฐ„์ ์œผ๋กœ ์ˆ˜๋ฐฑ ๋ฐ€๋ฆฌ์ดˆ(ms)๊นŒ์ง€ ์น˜์†Ÿ๋Š” ์ง€ํ„ฐ๋ง(Jittering)์ด ์ผ์–ด๋‚ฉ๋‹ˆ๋‹ค.

์ด๋Ÿฌํ•œ ๋ณ‘๋ชฉ ๊ตฌ์กฐ๋ฅผ ํ•ด๊ฒฐํ•˜๊ธฐ ์œ„ํ•ด์„œ๋Š” ๋‚ด๋น„๊ฒŒ์ด์…˜ ์„œ๋ฒ„์˜ ์ฒ˜๋ฆฌ ์ฃผ๊ธฐ๋ฅผ ๋ฉ”์ธ ๋ธํƒ€ ํƒ€์ž„๊ณผ ๋ถ„๋ฆฌํ•˜๊ณ , ๊ฒฝ๋กœ ์š”์ฒญ์„ ๋น„๋™๊ธฐ ์Šค๋ ˆ๋“œ ํ’€(WorkerThreadPool)๋กœ ๋ถ„์‚ฐํ•˜์—ฌ ๋ณ‘๋ ฌ ์ฒ˜๋ฆฌ ์ฒด๊ณ„๋ฅผ ๊ตฌ์ถ•ํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.

2. ๋น„๋™๊ธฐ ์ฒ˜๋ฆฌ ํŒŒ์ดํ”„๋ผ์ธ ์„ค๊ณ„ ๋ฐ ๋ฉ€ํ‹ฐ์Šค๋ ˆ๋“œ ๊ตฌํ˜„

๋ฉ”์ธ ์Šค๋ ˆ๋“œ์˜ ๋ถ€ํ•˜๋ฅผ ์ค„์ด๊ธฐ ์œ„ํ•œ ์ฒซ ๋ฒˆ์งธ ๋‹จ๊ณ„๋Š” ๊ฒฝ๋กœ ๊ตฌํ•˜๊ธฐ ์—ฐ์‚ฐ์„ ์Šค๋ ˆ๋“œ ํ’€๋กœ ์ด๊ด€ํ•˜๋Š” ๊ฒƒ์ž…๋‹ˆ๋‹ค. Godot 4์—์„œ๋Š” WorkerThreadPool ์‹ฑ๊ธ€ํ†ค์„ ์ œ๊ณตํ•˜์—ฌ C++ ๊ธฐ๋ฐ˜ ํƒœ์Šคํฌ ์ฒ˜๋ฆฌ(Task Processing)๋ฅผ ์†์‰ฝ๊ฒŒ ๊ตฌํ˜„ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

WorkerThreadPool ๊ธฐ๋ฐ˜ ๋น„๋™๊ธฐ ๊ฒฝ๋กœ ์š”์ฒญ ํŒจํ„ด

์—์ด์ „ํŠธ๊ฐ€ ๋‹จ์ผ ํ”„๋ ˆ์ž„ ๋‚ด์—์„œ ์ฆ‰์‹œ ๊ฒฐ๊ณผ๋ฅผ ๋ฐ˜ํ™˜๋ฐ›๊ธฐ๋ฅผ ๊ธฐ๋Œ€ํ•˜์ง€ ์•Š๊ณ , ํƒœ์Šคํฌ ์ž‘์—…์œผ๋กœ ๋„˜๊ธด ํ›„ ์—ฐ์‚ฐ ์™„๋ฃŒ ์‹œ ์ฝœ๋ฐฑ(Callback) ํ˜•ํƒœ๋กœ ๊ฒฝ๋กœ๋ฅผ ๋ฐ›์•„์˜ค๋Š” ๋น„๋™๊ธฐ ํŒจํ„ด์„ ์ž‘์„ฑํ•ฉ๋‹ˆ๋‹ค.

# WorkerThreadPool์„ ํ™œ์šฉํ•œ ๋น„๋™๊ธฐ ๊ฒฝ๋กœ ๊ณ„์‚ฐ GDScript ์˜ˆ์ œ
extends Node3D

var map_rid: RID
var worker_task_id: int = -1
var computed_path: PackedVector3Array = []
var is_processing_path: bool = false

func request_path_async(start_pos: Vector3, target_pos: Vector3) -> void:
    if is_processing_path:
        return
    
    is_processing_path = true
    # ์Šค๋ ˆ๋“œ ์ „์šฉ ๋ฌด๋ช… ํ•จ์ˆ˜ ์ƒ์„ฑ ๋ฐ ๋ฐ”์ธ๋”ฉ
    var task_callable = Callable(self, "_bg_compute_path").bind(start_pos, target_pos)
    worker_task_id = WorkerThreadPool.add_task(task_callable, true)

func _bg_compute_path(start_pos: Vector3, target_pos: Vector3) -> void:
    # ์•ˆ์ „ํ•˜๊ฒŒ ๋‚ด๋น„๊ฒŒ์ด์…˜ ๋งต RID ์ฐธ์กฐํ•˜์—ฌ ๊ฒฝ๋กœ ๊ณ„์‚ฐ
    var path: PackedVector3Array = NavigationServer3D.map_get_path(
        map_rid, 
        start_pos, 
        target_pos, 
        true # optimize
    )
    # ๊ณ„์‚ฐ ์™„๋ฃŒ ํ›„ ๋ฉ”์ธ ์Šค๋ ˆ๋“œ๋กœ ๊ฒฐ๊ณผ ์„ธ์ดํ”„ ์ „๋‹ฌ
    call_deferred("_on_path_computed", path)

func _on_path_computed(path: PackedVector3Array) -> void:
    computed_path = path
    is_processing_path = false
    # NPC ์ด๋™ ํŒŒ์ดํ”„๋ผ์ธ ๊ฐฑ์‹  ์ง„ํ–‰

์œ„ ํŒจํ„ด์„ ์‚ฌ์šฉํ•˜๋ฉด 2,000๋ช… ์ด์ƒ์˜ NPC๊ฐ€ ์ผ์‹œ์— ๋ชฉํ‘œ ์ง€์ ์„ ๋ณ€๊ฒฝํ•˜๋”๋ผ๋„ ๋ฉ”์ธ ์Šค๋ ˆ๋“œ๊ฐ€ ์ •์ง€ํ•˜์ง€ ์•Š๊ณ , ์Šค๋ ˆ๋“œ ํ’€์˜ Worker๊ฐ€ CPU ์ฝ”์–ด ๋ถ„๋ฐฐ์— ๋”ฐ๋ผ ์ˆœ์ฐจ์ /๋ณ‘๋ ฌ์ ์œผ๋กœ ๊ฒฝ๋กœ๋ฅผ ๊ณ„์‚ฐํ•˜๊ฒŒ ๋ฉ๋‹ˆ๋‹ค.



3. Avoidance Agent ๋ฐ Dynamic NavMesh ๋ฉ€ํ‹ฐ์Šค๋ ˆ๋“œ ์ตœ์ ํ™”

๋‹จ์ˆœํžˆ ๊ฒฝ๋กœ ๊ณ„์‚ฐ์„ ๋–ผ์–ด๋‚ด๋Š” ๊ฒƒ๋งŒ์œผ๋กœ ์™„๋ฒฝํ•œ ์„ฑ๋Šฅ์„ ํ™•๋ณดํ•˜๊ธฐ๋Š” ์–ด๋ ต์Šต๋‹ˆ๋‹ค. NPC ๋ฐ€์ง‘ ์ง€ํ˜•์—์„œ์˜ ์—์ด์ „ํŠธ ํšŒํ”ผ ์—ฐ์‚ฐ(Avoidance Loop)๊ณผ ๋™์  ์ง€ํ˜• ๊ฐฑ์‹  ๋˜ํ•œ ๋ฉ€ํ‹ฐ์Šค๋ ˆ๋“œ ๊ด€๋ฆฌ๊ฐ€ ํ•„์ˆ˜์ ์ž…๋‹ˆ๋‹ค.

1) NavigationServer3D ์Šค๋ ˆ๋“œ ํšŒํ”ผ ์—ฐ์‚ฐ ํ™œ์„ฑํ™”

ํ”„๋กœ์ ํŠธ ์„ค์ •(Project Settings)์—์„œ ๋‚ด๋น„๊ฒŒ์ด์…˜ ์„œ๋ฒ„์˜ ์Šค๋ ˆ๋”ฉ ์˜ต์…˜์„ ํ™œ์„ฑํ™”ํ•˜๋Š” ๊ฒƒ์€ ๊ฐ€์žฅ ๊ฐ•๋ ฅํ•˜๋ฉด์„œ๋„ ๊ฐ„๋‹จํ•œ ์ตœ์ ํ™” ๋ฐฉ์•ˆ์ž…๋‹ˆ๋‹ค.

  • Project Settings -> Navigation -> 3D -> Use Threads: On์œผ๋กœ ๋ณ€๊ฒฝ
  • Avoidance Agent Update: velocity_computed ์‹ ํ˜ธ(Signal) ์ˆ˜์‹  ๋ฐฉ์‹์„ ๋น„๋™๊ธฐ ์Šค๋ ˆ๋“œ ๋™๊ธฐํ™” ๋‹จ๊ณ„์— ๋งž์ถ”์–ด ์ตœ์†Œํ™”
โš ๏ธ ์ฃผ์˜์‚ฌํ•ญ: Use Threads๋ฅผ ํ™œ์„ฑํ™”ํ•  ๊ฒฝ์šฐ, NavigationServer3D์˜ RID๋ฅผ ์ง์ ‘ ์กฐ์ž‘ํ•˜๋Š” ๊ณผ์ •์—์„œ ๋ฉ”์ธ ์Šค๋ ˆ๋“œ๊ฐ€ ์•„์ง ๋™๊ธฐํ™”๋˜์ง€ ์•Š์€ ๋งต์„ ์ฝ์œผ๋ ค ํ•  ๋•Œ ๊ฒฝ์Ÿ ์ƒํƒœ(Race Condition)๊ฐ€ ์ผ์–ด๋‚  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

2) ๋™์  ๋‚ด๋น„๋ฉ”์‹œ(Dynamic NavMesh) ๋ฐฑ๊ทธ๋ผ์šด๋“œ ๋ฒ ์ดํ‚น ํŠธ๋Ÿฌ๋ธ”์ŠˆํŒ…

๊ฒŒ์ž„ ์‹คํ–‰ ์ค‘ ์ง€ํ˜•์ด ๋ณ€๊ฒฝ๋˜์–ด ๋‚ด๋น„๊ฒŒ์ด์…˜ ๋ฉ”์‹œ๋ฅผ ๋‹ค์‹œ ๊ตฌ์›Œ์•ผ(Baking) ํ•˜๋Š” ์ƒํ™ฉ์—์„œ๋Š” ๋ฉ”์ธ ์Šค๋ ˆ๋“œ์—์„œ NavigationRegion3D.bake_navigation_mesh()๋ฅผ ์ง์ ‘ ๋ถ€๋ฅด๋ฉด ํ”„๋ ˆ์ž„ ๋“œ๋ž์ด ๋ฐœ์ƒํ•ฉ๋‹ˆ๋‹ค. ๋Œ€์‹  C++ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ ๋ ˆ๋ฒจ์˜ ๋น„๋™๊ธฐ ๋ฒ ์ดํ‚น ๋ฉ”์„œ๋“œ๋ฅผ ํ™œ์šฉํ•ฉ๋‹ˆ๋‹ค.

# ๋™์  ๋‚ด๋น„๋ฉ”์‹œ ๋ฐฑ๊ทธ๋ผ์šด๋“œ ๋ฒ ์ดํ‚น ๋ฐ ์•ˆ์ „ํ•œ ์Šค์™‘(Swap) ์˜ˆ์ œ
func update_dynamic_navmesh_async(region: NavigationRegion3D) -> void:
    var navmesh = region.navigation_mesh
    # ๋ฐฑ๊ทธ๋ผ์šด๋“œ ์Šค๋ ˆ๋“œ์—์„œ ๋ฒ ์ดํ‚น ์ง„ํ–‰
    NavigationServer3D.region_bake_navigation_mesh(navmesh, region)
    
    # ๋ฒ ์ดํ‚น ์™„๋ฃŒ ์ด๋ฒคํŠธ๋ฅผ ์ˆ˜์‹ ๋ฐ›์•„ ์„œ๋ฒ„์— ๋ฆฌ์ „ ๋ฐ˜์˜
    if not NavigationServer3D.is_connected("map_changed", Callable(self, "_on_map_updated")):
        NavigationServer3D.map_changed.connect(Callable(self, "_on_map_updated"), CONNECT_ONE_SHOT)

func _on_map_updated(map_rid: RID) -> void:
    print("NavMesh Background Baking Completed & Synchronized to Map.")

4. ์‹ค๋ฌด ์—ฃ์ง€ ์ผ€์ด์Šค ๋ฐ ๋ ˆ์ด์Šค ์กฐ๊ฑด(Race Condition) ๋Œ€์ฒ˜ ๋ฐฉ๋ฒ•

ํ˜„์—… ํ”„๋กœ์ ํŠธ์—์„œ ๋Œ€๊ทœ๋ชจ AI ๋ฉ€ํ‹ฐ์Šค๋ ˆ๋”ฉ์„ ๋„์ž…ํ•  ๋•Œ ๋ถ€๋‹ฅ์น˜๋Š” ํ”ํ•œ ์—ฃ์ง€ ์ผ€์ด์Šค(Edge Cases)์™€ ์ด๋ฅผ ํšŒํ”ผํ•˜๋Š” ์•„ํ‚คํ…์ฒ˜ ํŒจํ„ด์€ ๋‹ค์Œ๊ณผ ๊ฐ™์Šต๋‹ˆ๋‹ค.

A. ํŒŒ๊ดด๋œ RID ์ฐธ์กฐ ์˜ค๋ฅ˜ (Dangling RID Access)

WorkerThreadPool ๋‚ด์—์„œ ๊ฒฝ๋กœ ๊ณ„์‚ฐ์„ ์ง„ํ–‰ํ•˜๋Š” ์ค‘๊ฐ„์—, ํ•ด๋‹น NPC๋‚˜ ์”ฌ์ด ํŒŒ๊ดด(Queue Free)๋˜๋ฉด์„œ ์‚ฌ์šฉํ•˜๋˜ ๋‚ด๋น„๊ฒŒ์ด์…˜ ๋งต RID๋‚˜ Transform์ด ์œ ํšจํ•˜์ง€ ์•Š๊ฒŒ ๋˜๋Š” ํ˜„์ƒ์ž…๋‹ˆ๋‹ค.

  • ํ•ด๊ฒฐ์ฑ…: ์Šค๋ ˆ๋“œ๋กœ ๊ฐ’์„ ์ „๋‹ฌํ•  ๋•Œ๋Š” ๊ฐ์ฒด ๋ ˆํผ๋Ÿฐ์Šค(Node Pointer)๋ฅผ ์ง์ ‘ ๋„˜๊ธฐ์ง€ ์•Š๊ณ , ๊ณ„์‚ฐ์— ํ•„์š”ํ•œ ํ•„์ˆ˜ ๋ฐ์ดํ„ฐ(Vector3 ์ขŒํ‘œ๊ฐ’, RID ์›์‹œ ๋ฐ์ดํ„ฐ)๋งŒ ๊ฐ’ ๋ณต์‚ฌ(Copy-by-Value) ํ˜•ํƒœ๋กœ ํŒŒ๋ผ๋ฏธํ„ฐํ™”ํ•ฉ๋‹ˆ๋‹ค.
  • ๊ฒฐ๊ณผ๊ฐ€ ๋ฉ”์ธ ์Šค๋ ˆ๋“œ๋กœ ๋Œ์•„์™”์„ ๋•Œ Node์˜ is_instance_valid(self) ์ฒดํฌ๋ฅผ ์„ ํ–‰ํ•ฉ๋‹ˆ๋‹ค.

B. ์—์ด์ „ํŠธ ์—…๋ฐ์ดํŠธ ๋นˆ๋„ ๋ž˜ํ•‘(Throttle Agent Updates)

๋ชจ๋“  NPC๊ฐ€ ๋งค ๋ฌผ๋ฆฌ ํ”„๋ ˆ์ž„(Physics Tick, 60hz)๋งˆ๋‹ค ๊ฒฝ๋กœ์™€ ํšŒํ”ผ ์•Œ๊ณ ๋ฆฌ์ฆ˜์„ ๊ณ„์‚ฐํ•  ํ•„์š”๋Š” ์—†์Šต๋‹ˆ๋‹ค. ์‹œ์•ผ๊ฐ(Frustum Culling)์ด๋‚˜ ๊ฑฐ๋ฆฌ(LOD - Level of Detail)์— ๋”ฐ๋ผ ์—…๋ฐ์ดํŠธ ์ฃผ๊ธฐ๋ฅผ ๋™์ ์œผ๋กœ ์กฐ์ ˆํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.

AI ๊ฑฐ๋ฆฌ ๋‹จ๊ณ„ (LOD) ํ”Œ๋ ˆ์ด์–ด์™€์˜ ๊ฑฐ๋ฆฌ ๊ธธ์ฐพ๊ธฐ/ํšŒํ”ผ ๊ฐฑ์‹  ์ฃผ๊ธฐ ์Šค๋ ˆ๋“œ ํ• ๋‹น ์ „๋žต
LOD 0 (๊ทผ๊ฑฐ๋ฆฌ) 0m ~ 25m ๋งค ํ”„๋ ˆ์ž„ (60 Hz) RVO2 ํšŒํ”ผ ์—ฐ์‚ฐ + ์Šค๋ ˆ๋“œ ํ’€ ๋น„๋™๊ธฐ ๊ฒฝ๋กœ
LOD 1 (์ค‘๊ฑฐ๋ฆฌ) 25m ~ 70m 10~15 ํ”„๋ ˆ์ž„๋‹น 1ํšŒ (4~6 Hz) RVO2 ์ƒ๋žต, ๊ฐ„๋‹จํ•œ ์ง์„  ์ธํ„ฐํด๋ ˆ์ด์…˜
LOD 2 (์›๊ฑฐ๋ฆฌ) 70m ์ด์ƒ 60 ํ”„๋ ˆ์ž„๋‹น 1ํšŒ (1 Hz) ๊ฒฝ๋กœ ์—ฐ์‚ฐ ์ž์ฒด ์ค‘๋‹จ (๋ฐ์ดํ„ฐ ๋ž˜ํ•‘๋งŒ ์œ ์ง€)


5. ์ตœ์ ํ™” ๊ฒฐ๊ณผ ๋ฐ ์„ฑ๋Šฅ ๋ฒค์น˜๋งˆํฌ

์œ„ ์ตœ์ ํ™” ์ž‘์—…์„ 3D ์˜คํ”ˆ์›”๋“œ ํ…Œ์ŠคํŠธ ํ•„๋“œ ํ™˜๊ฒฝ(๋™์‹œ NPC 1,500๋ช… ํ™œ์„ฑํ™” ์กฐ๊ฑด)์— ์ ์šฉํ•œ ๊ฒฐ๊ณผ, ํ”„๋ ˆ์ž„ ํƒ€์ž„ ๋ฐ CPU ๋ฉ”์ธ ์Šค๋ ˆ๋“œ ๋ถ€ํ•˜์—์„œ ๊ด„๋ชฉํ•  ๋งŒํ•œ ๊ฐœ์„ ์„ ํ™•์ธํ–ˆ์Šต๋‹ˆ๋‹ค.

  • ์ตœ์ ํ™” ์ „: NPC 1,500๋ช… ๋™์‹œ ์ด๋™ ์‹œ ๋ฉ”์ธ ์Šค๋ ˆ๋“œ ํ”„๋ ˆ์ž„ ํƒ€์ž„ 48.5ms (์•ฝ 20 FPS), ์‹ฌ๊ฐํ•œ ์Šคํ„ฐํ„ฐ๋ง ๋ฐœ์ƒ.
  • ์ตœ์ ํ™” ํ›„: WorkerThreadPool ๊ธฐ๋ฐ˜ ๋น„๋™๊ธฐ ๊ณ„์‚ฐ + NavigationServer3D ์Šค๋ ˆ๋”ฉ ์ ์šฉ ์‹œ ํ”„๋ ˆ์ž„ ํƒ€์ž„ 14.2ms (์•ˆ์ •์ ์ธ 60~70 FPS ์œ ์ง€).
  • ๋ฉ”์ธ ์Šค๋ ˆ๋“œ ์ ์œ ์œจ: ๊ธฐ์กด ๋Œ€๋น„ ์•ฝ 70% ๊ฐ์†Œ.

๊ฒฐ๋ก : ์‹œ์Šคํ…œ์  ์ตœ์ ํ™”๋ฅผ ํ†ตํ•œ ํ™•์žฅ์„ฑ ํ™•๋ณด

Godot 4์˜ NavigationServer3D๋Š” ๋งค์šฐ ๊ฐ•๋ ฅํ•œ ํˆด์ด์ง€๋งŒ, ๋ฉ”์ธ ์Šค๋ ˆ๋“œ์˜ ์—ฐ์‚ฐ ๋ธ”๋กœํ‚น ํŠน์„ฑ์„ ์ดํ•ดํ•˜์ง€ ๋ชปํ•˜๊ณ  ๋‹จ์ˆœ ๋™๊ธฐ ํ˜ธ์ถœ ๋ฐฉ์‹์œผ๋กœ ์ ‘๊ทผํ•˜๋ฉด ๋Œ€๊ทœ๋ชจ ๋ฉ€ํ‹ฐ-AI ํ™˜๊ฒฝ์—์„œ ์„ฑ๋Šฅ ํ•œ๊ณ„์— ๋ถ€๋”ชํžˆ๊ฒŒ ๋ฉ๋‹ˆ๋‹ค.

๋ณธ ๊ฐ€์ด๋“œ์—์„œ ๋‹ค๋ฃฌ (1) WorkerThreadPool์„ ํ†ตํ•œ ๋น„๋™๊ธฐ Path Query, (2) NavigationServer ์Šค๋ ˆ๋“œ ๋ชจ๋“œ ํ™œ์„ฑํ™” ๋ฐ RVO2 ํšŒํ”ผ ๋ถ„์‚ฐ, (3) LOD ๊ธฐ๋ฐ˜ ์—…๋ฐ์ดํŠธ ์Šค๋ ˆํ‹€๋ง ์กฐํ•ฉ์„ ์ ์šฉํ•œ๋‹ค๋ฉด, ์ˆ˜์ฒœ ๋ช… ๊ทœ๋ชจ์˜ NPC๊ฐ€ ๋“ฑ์žฅํ•˜๋Š” ๋ณต์žกํ•œ 3D ํ™˜๊ฒฝ์—์„œ๋„ ๋งค๋„๋Ÿฌ์šด 60FPS ์ด์ƒ์˜ ๊ณ ์„ฑ๋Šฅ ๊ธธ์ฐพ๊ธฐ ํŒŒ์ดํ”„๋ผ์ธ์„ ๊ตฌ์ถ•ํ•  ์ˆ˜ ์žˆ์„ ๊ฒƒ์ž…๋‹ˆ๋‹ค.

๋ฐ˜์‘ํ˜•