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

Godot 4 2D ํƒ‘๋‹ค์šด ๊ฒŒ์ž„์˜ NavigationServer2D ๋Œ€๊ทœ๋ชจ ๊ธธ์ฐพ๊ธฐ ๋ณ‘๋ชฉ ํ•ด๊ฒฐ ๋ฐ ๋น„๋™๊ธฐ ๋ฉ€ํ‹ฐ์Šค๋ ˆ๋“œ ์ตœ์ ํ™” ๊ฐ€์ด๋“œ

๊ฒŒ์ž„๊ต์ˆ˜ 2026. 10. 12. 04:02
๋ฐ˜์‘ํ˜•

๐ŸŒ English Abstract

This article explores methods to resolve severe frame drops caused by pathfinding bottlenecks in Godot 4 2D top-down games featuring large numbers of AI agents. It provides an in-depth analysis of the NavigationServer2D architecture, RVO2 avoidance overhead, and asynchronous multi-threaded path request strategies using WorkerThreadPool. Furthermore, practical troubleshooting tips and code implementations are detailed to handle edge cases like dynamic map updates and thread-safety issues during high-density unit simulation.

Godot 4 ์—”์ง„์€ ์„œ๋ฒ„ ๊ธฐ๋ฐ˜ ์•„ํ‚คํ…์ฒ˜(Server-based Architecture)๋ฅผ ์ „๋ฉด์— ๋„์ž…ํ•˜๋ฉด์„œ Rendering, Physics์™€ ํ•จ๊ป˜ Navigation ๋ถ„์•ผ์—์„œ๋„ ๋น„์•ฝ์ ์ธ ์„ฑ๋Šฅ ํ–ฅ์ƒ์„ ์ด๋ฃจ์–ด๋ƒˆ์Šต๋‹ˆ๋‹ค. ํ•˜์ง€๋งŒ ๋ฑ€ํŒŒ์ด์–ด ์„œ๋ฐ”์ด๋ฒ„์ฆˆ(Vampire Survivors) ์Šคํƒ€์ผ์˜ ์Šค์›œ(Swarm)๋ฅ˜ ๊ฒŒ์ž„์ด๋‚˜ RTS, 2D ํƒ‘๋‹ค์šด ์•ก์…˜ RPG์ฒ˜๋Ÿผ ์ˆ˜๋ฐฑ ๊ฐœ์—์„œ ์ˆ˜์ฒœ ๊ฐœ์˜ ์—์ด์ „ํŠธ(Agent)๊ฐ€ ๋™์‹œ์— ๊ธธ์ฐพ๊ธฐ(Pathfinding) ์—ฐ์‚ฐ์„ ์ˆ˜ํ–‰ํ•˜๋Š” ํ™˜๊ฒฝ์—์„œ๋Š” ์—ฌ์ „ํžˆ ์‹ฌ๊ฐํ•œ ๋ฉ”์ธ ์Šค๋ ˆ๋“œ ๋ณ‘๋ชฉ ํ˜„์ƒ(Main Thread Bottleneck)๊ณผ ํ”„๋ ˆ์ž„ ๋“œ๋กญ(Frame Drop)์ด ๋ฐœ์ƒํ•ฉ๋‹ˆ๋‹ค.

๋ณธ ํฌ์ŠคํŒ…์—์„œ๋Š” Godot 4์˜ NavigationServer2D ์ž‘๋™ ์›๋ฆฌ๋ฅผ ๊นŠ์ด ์žˆ๊ฒŒ ๋ถ„์„ํ•˜๊ณ , ๋™๊ธฐ์‹(Synchronous) ๋‚ด๋น„๊ฒŒ์ด์…˜ ์—ฐ์‚ฐ์ด ๋ฐœ์ƒํ•˜๋Š” ๊ตฌ์กฐ์  ์›์ธ์„ ํŒŒ์•…ํ•ฉ๋‹ˆ๋‹ค. ์•„์šธ๋Ÿฌ ์ด๋ฅผ ๋น„๋™๊ธฐ ๋ฉ€ํ‹ฐ์Šค๋ ˆ๋“œ(Asynchronous Multi-threading) ํŒŒ์ดํ”„๋ผ์ธ์œผ๋กœ ์ „ํ™˜ํ•˜์—ฌ 60 FPS ์ด์ƒ์˜ ์•ˆ์ •์ ์ธ ํ”„๋ ˆ์ž„์„ ์œ ์ง€ํ•˜๋Š” ์‹ค์ œ ํŠธ๋Ÿฌ๋ธ”์ŠˆํŒ… ๊ณผ์ •๊ณผ ์ตœ์ ํ™” ๊ธฐ๋ฒ•์„ ๊ณต์œ ํ•ฉ๋‹ˆ๋‹ค.



1. NavigationServer2D์˜ ์•„ํ‚คํ…์ฒ˜ ๋ฐ ๋ณ‘๋ชฉ ์›์ธ ๋ถ„์„

Godot 4์˜ ๋‚ด๋น„๊ฒŒ์ด์…˜ ์‹œ์Šคํ…œ์€ ๋…ธ๋“œ ํŠธ๋ฆฌ(Node Tree)์™€ ๋ถ„๋ฆฌ๋œ ์ „์šฉ ์„œ๋ฒ„(Server) ๊ตฌ์กฐ๋กœ ๋™์ž‘ํ•ฉ๋‹ˆ๋‹ค. NavigationRegion2D๋Š” ๋‚ด๋น„๊ฒŒ์ด์…˜ ๋ฉ”์‹œ(Navigation Mesh)๋ฅผ ๊ด€๋ฆฌํ•˜๊ณ , NavigationAgent2D๋Š” ๊ฐœ๋ณ„ ๊ฐ์ฒด์˜ ์ด๋™ ์š”์ฒญ ๋ฐ ํšŒํ”ผ ์—ฐ์‚ฐ์„ ๋‹ด๋‹นํ•ฉ๋‹ˆ๋‹ค.

์ˆ˜๋ฐฑ ๊ฐœ ์ด์ƒ์˜ ์  AI๊ฐ€ ํ•œ ํ™”๋ฉด์— ๋“ฑ์žฅํ•  ๋•Œ ํ”„๋ ˆ์ž„ ๋“œ๋กญ์ด ๋ฐœ์ƒํ•˜๋Š” ์ฃผ์š” ์›์ธ์€ ๋‹ค์Œ๊ณผ ๊ฐ™์Šต๋‹ˆ๋‹ค.

  • ๋™๊ธฐ์‹ ๊ฒฝ๋กœ ๊ณ„์‚ฐ ์ŠคํŒŒ์ดํฌ(Synchronous Pathfinding Spike): ์ˆ˜๋ฐฑ ๊ฐœ์˜ ์—์ด์ „ํŠธ๊ฐ€ ๋™์ผํ•œ ํ”„๋ ˆ์ž„์— NavigationServer2D.map_get_path()๋ฅผ ํ˜ธ์ถœํ•˜๋ฉด, CPU์˜ ๋ฉ”์ธ ์Šค๋ ˆ๋“œ๊ฐ€ A* ์•Œ๊ณ ๋ฆฌ์ฆ˜ ์—ฐ์‚ฐ์„ ํ•œ๊บผ๋ฒˆ์— ์ฒ˜๋ฆฌํ•˜๋А๋ผ ๋ Œ๋”๋ง ๋ฐ ๋ฌผ๋ฆฌ ํ”„๋ ˆ์ž„ ํƒ€์ž„(Frame Time)์ด ์ˆœ๊ฐ„์ ์œผ๋กœ ํญ์ฆํ•ฉ๋‹ˆ๋‹ค.
  • RVO2 ํšŒํ”ผ ์—ฐ์‚ฐ(Reciprocal Velocity Obstacles Overhead): ์—์ด์ „ํŠธ ๊ฐ„ ์ถฉ๋Œ์„ ๋ฐฉ์ง€ํ•˜๋Š” ํšŒํ”ผ ์•Œ๊ณ ๋ฆฌ์ฆ˜์€ $O(N^2)$์— ๊ฐ€๊นŒ์šด ๊ณ„์‚ฐ ๋ณต์žก๋„๋ฅผ ๊ฐ€์ง‘๋‹ˆ๋‹ค. ๊ฐ ์—์ด์ „ํŠธ์˜ ์ถฉ๋Œ ๋ฐ˜๊ฒฝ๊ณผ ์†๋„ ๋ฒกํ„ฐ๋ฅผ ๋งค ํ”„๋ ˆ์ž„ ๋น„๊ตํ•˜๋Š” ์—ฐ์‚ฐ์€ ๋ฉ”์ธ ์Šค๋ ˆ๋“œ์— ์—„์ฒญ๋‚œ ๋ถ€ํ•˜๋ฅผ ์ค๋‹ˆ๋‹ค.
  • ๋ถˆํ•„์š”ํ•˜๊ฒŒ ๋†’์€ ์žฌ๊ณ„์‚ฐ ์ฃผ๊ธฐ(High Path Recalculation Frequency): ํƒ€๊ฒŸ(ํ”Œ๋ ˆ์ด์–ด)์ด ์กฐ๊ธˆ๋งŒ ์›€์ง์—ฌ๋„ ๋ชจ๋“  ์—์ด์ „ํŠธ๊ฐ€ ๋งค ํ”„๋ ˆ์ž„(๋˜๋Š” Physics Frame๋งˆ๋‹ค) ๊ฒฝ๋กœ๋ฅผ ์žฌ๊ณ„์‚ฐ(Path Refresh)ํ•˜๋„๋ก ์„ค์ •๋˜์–ด ์žˆ๋Š” ๊ฒฝ์šฐ์ž…๋‹ˆ๋‹ค.

Godot Profiler๋ฅผ ํ†ตํ•œ ํ”„๋ ˆ์ž„ ํƒ€์ž„ ๋ถ„์„

์—”์ง„ ๋‚ด๋ถ€ ํ”„๋กœํŒŒ์ผ๋Ÿฌ(Profiler)๋ฅผ ํ†ตํ•ด ํ™•์ธํ•ด ๋ณด๋ฉด, Physics Frame Time ํ•ญ๋ชฉ์—์„œ navigation_process ์‹œ๊ฐ„์ด 16.6ms(60FPS ๊ธฐ์ค€)๋ฅผ ํ›จ์”ฌ ์ดˆ๊ณผํ•˜์—ฌ 30~50ms๊นŒ์ง€ ์น˜์†Ÿ๋Š” ํ˜„์ƒ์„ ๊ด€์ฐฐํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ์ด๋Š” ๋ฉ”์ธ ์Šค๋ ˆ๋“œ๊ฐ€ ๋‚ด๋น„๊ฒŒ์ด์…˜ ๋™๊ธฐํ™” ์ฒ˜๋ฆฌ๊ฐ€ ์™„๋ฃŒ๋  ๋•Œ๊นŒ์ง€ ๋Œ€๊ธฐ(Wait State) ์ƒํƒœ์— ๋น ์ง€๊ธฐ ๋•Œ๋ฌธ์ž…๋‹ˆ๋‹ค.

2. ๋น„๋™๊ธฐ ๋ฉ€ํ‹ฐ์Šค๋ ˆ๋“œ ๊ธธ์ฐพ๊ธฐ ํŒŒ์ดํ”„๋ผ์ธ ๊ตฌ์ถ•

๋ฉ”์ธ ์Šค๋ ˆ๋“œ์˜ ๋ถ€๋‹ด์„ ๋œ์–ด์ฃผ๊ธฐ ์œ„ํ•ด ๊ฒฝ๋กœ ์—ฐ์‚ฐ ์ž‘์—…์„ ๋ฐฑ๊ทธ๋ผ์šด๋“œ ์Šค๋ ˆ๋“œ(Background Thread)๋กœ ์ด์ „ํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค. Godot 4์—์„œ๋Š” WorkerThreadPool์„ ํ™œ์šฉํ•˜์—ฌ ์ด ์ž‘์—…์„ ํšจ์œจ์ ์œผ๋กœ ์ฒ˜๋ฆฌํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

๊ฒฝ๋กœ ๊ณ„์‚ฐ์˜ ๋ถ„์‚ฐ ์ฒ˜๋ฆฌ (Time-Slicing & Throttling)

๋ชจ๋“  ์—์ด์ „ํŠธ๊ฐ€ ๋™์‹œ์— ๊ฒฝ๋กœ๋ฅผ ๊ตฌํ•˜์ง€ ์•Š๋„๋ก ํƒ€์ž„ ์Šฌ๋ผ์ด์‹ฑ(Time-Slicing) ๊ธฐ๋ฒ•์„ ์ ์šฉํ•ฉ๋‹ˆ๋‹ค. ์—์ด์ „ํŠธ ๊ทธ๋ฃน์„ ๋‚˜๋ˆˆ ๋’ค, ๊ฐ ๊ทธ๋ฃน์ด ์„œ๋กœ ๋‹ค๋ฅธ ํ”„๋ ˆ์ž„์— ๋น„๋™๊ธฐ ๊ฒฝ๋กœ ์š”์ฒญ์„ ๋ณด๋‚ผ ์ˆ˜ ์žˆ๋„๋ก ๋งค๋‹ˆ์ € ํด๋ž˜์Šค(NavigationManager)๋ฅผ ์„ค๊ณ„ํ•ฉ๋‹ˆ๋‹ค.

์•„๋ž˜๋Š” WorkerThreadPool์„ ์ด์šฉํ•˜์—ฌ ๋น„๋™๊ธฐ์ ์œผ๋กœ ๊ฒฝ๋กœ๋ฅผ ์ƒ์„ฑํ•˜๊ณ  ์ฝœ๋ฐฑ์„ ํ†ตํ•ด ์ „๋‹ฌ๋ฐ›๋Š” ํŒจํ„ด์˜ GDScript ๊ตฌํ˜„ ์˜ˆ์‹œ์ž…๋‹ˆ๋‹ค.

# AsyncNavigationManager.gd
extends Node

# ๋งค ํ”„๋ ˆ์ž„ ์ฒ˜๋ฆฌํ•  ์ตœ๋Œ€ ๋น„๋™๊ธฐ ๊ธธ์ฐพ๊ธฐ ์ž‘์—… ์ˆ˜
const MAX_ASYNC_TASKS_PER_FRAME = 20
var task_queue: Array = []

func request_path_async(map_rid: RID, start_pos: Vector2, end_pos: Vector2, callback: Callable) -> void:
    task_queue.append({
        "map": map_rid,
        "start": start_pos,
        "end": end_pos,
        "callback": callback
    })

func _process(_delta: float) -> void:
    if task_queue.is_empty():
        return
        
    var count = min(task_queue.size(), MAX_ASYNC_TASKS_PER_FRAME)
    for i in range(count):
        var task_data = task_queue.pop_front()
        WorkerThreadPool.add_task(_calculate_path_worker.bind(task_data), true)

func _calculate_path_worker(task_data: Dictionary) -> void:
    # NavigationServer2D ์Šค๋ ˆ๋“œ ์•ˆ์ „ ํ•จ์ˆ˜ ํ˜ธ์ถœ
    var path = NavigationServer2D.map_get_path(
        task_data["map"], 
        task_data["start"], 
        task_data["end"], 
        true
    )
    # ๊ฒฐ๊ณผ๋ฅผ ๋ฉ”์ธ ์Šค๋ ˆ๋“œ๋กœ ์•ˆ์ „ํ•˜๊ฒŒ ์ „๋‹ฌ
    task_data["callback"].call_deferred(path)


3. ์‹ค๋ฌด ํŠธ๋Ÿฌ๋ธ”์ŠˆํŒ…: ๋ฉ€ํ‹ฐ์Šค๋ ˆ๋”ฉ ๋„์ž… ์‹œ ๋ฐœ์ƒ ๋ฌธ์ œ ๋ฐ ์—ฃ์ง€ ์ผ€์ด์Šค

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

1) ๋™์  ํƒ€์ผ๋งต ํŒŒ๊ดด/์ƒ์„ฑ ์‹œ ๋‚ด๋น„๊ฒŒ์ด์…˜ ๋งต ๊ฐฑ์‹  ๋™๊ธฐํ™” ๋ฌธ์ œ

2D ํƒ‘๋‹ค์šด ๊ฒŒ์ž„์—์„œ ์ง€ํ˜•์ด ํŒŒ๊ดด๋˜๊ฑฐ๋‚˜ ๊ฑด๋ฌผ์ด ๊ฑด์„ค๋˜์–ด TileMap์˜ ๋‚ด๋น„๊ฒŒ์ด์…˜ ์˜์—ญ์ด ์žฌ๋ฒ ์ดํ‚น(Re-baking)๋  ๋•Œ, ๋ฐฑ๊ทธ๋ผ์šด๋“œ ์Šค๋ ˆ๋“œ๊ฐ€ ๊ตฌ๋ฒ„์ „ ๋‚ด๋น„๊ฒŒ์ด์…˜ RID ๋งต ๋ฐ์ดํ„ฐ์— ์ ‘๊ทผํ•˜๋ฉด ์˜ฌ๋ฐ”๋ฅด์ง€ ์•Š์€ ๊ฒฝ๋กœ๊ฐ€ ๋ฐ˜ํ™˜๋˜๊ฑฐ๋‚˜ ์—”์ง„ ๋‚ด๋ถ€ ์„ธ๊ทธ๋จผํŠธ ์—๋Ÿฌ(Segmentation Fault)๊ฐ€ ๋ฐœ์ƒํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

ํ•ด๊ฒฐ์ฑ…: ์ง€ํ˜• ๋ฐ์ดํ„ฐ๊ฐ€ ๋ณ€๊ฒฝ๋˜๋Š” ์‹œ์ ์—๋Š” NavigationServer2D.map_changed ์‹ ํ˜ธ๋ฅผ ์ˆ˜์‹ ํ•˜๊ณ , ํ˜„์žฌ ๋Œ€๊ธฐ ์ค‘์ธ ๋ชจ๋“  ๋น„๋™๊ธฐ ์ž‘์—… ํ(Task Queue)๋ฅผ ์ฆ‰์‹œ ๋น„์šด(Flush) ํ›„ ๋‚ด๋น„๊ฒŒ์ด์…˜ ๋งต ๋™๊ธฐํ™”๊ฐ€ ๋๋‚œ ํ”„๋ ˆ์ž„ ๋‹ค์Œ๋ถ€ํ„ฐ ๋‹ค์‹œ ๊ฒฝ๋กœ ์š”์ฒญ์„ ๋ฐ›๋„๋ก ์ œ์–ด ๋กœ์ง์„ ์ž‘์„ฑํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.

2) RVO2 Avoidance ์—ฐ์‚ฐ ์„ธํŒ… ์ตœ์ ํ™”

NavigationAgent2D์˜ enable_avoidance = true ์„ค์ •์€ ๋งค์šฐ ์œ ์šฉํ•˜์ง€๋งŒ, 500๊ฐœ ์ด์ƒ์˜ ๊ฐœ์ฒด์—์„œ๋Š” ํšŒํ”ผ ์—ฐ์‚ฐ ์ž์ฑ„๊ฐ€ ๋ณ‘๋ชฉ์ด ๋ฉ๋‹ˆ๋‹ค. ํšŒํ”ผ ์—ฐ์‚ฐ์€ ์™„๋ฒฝํ•œ ์Šค๋ ˆ๋“œ ๋ถ„๋ฆฌ๊ฐ€ ์–ด๋ ต๊ธฐ ๋•Œ๋ฌธ์— ํŒŒ๋ผ๋ฏธํ„ฐ ํŠœ๋‹์ด ํ•„์ˆ˜์ ์ž…๋‹ˆ๋‹ค.

  • Avoidance Layers ๊ตฌ๋ถ„: ์ฃผ์š” ์—˜๋ฆฌํŠธ ๋ชฌ์Šคํ„ฐ๋‚˜ ํ”Œ๋ ˆ์ด์–ด์—๊ฒŒ๋งŒ ๋†’์€ ์šฐ์„ ์ˆœ์œ„ ํšŒํ”ผ ๋ ˆ์ด์–ด๋ฅผ ๋ถ€์—ฌํ•˜๊ณ , ์žก๋ชน(Minions) ๊ทธ๋ฃน์€ ์„œ๋กœ ๊ฐ„์˜ Avoidance๋ฅผ ๋น„ํ™œ์„ฑํ™”(avoidance_enabled = false)ํ•˜๊ฑฐ๋‚˜ ๋‹จ์ˆœ ๊ฑฐ๋ฆฌ ๊ธฐ๋ฐ˜ ๋ฐ€์ณ๋‚ด๊ธฐ(Soft Collision / Separation Force) 2D ๋ฌผ๋ฆฌ ์—ฐ์‚ฐ์œผ๋กœ ๋Œ€์ฒดํ•ฉ๋‹ˆ๋‹ค.
  • Max Neighbors ์ œํ•œ: neighbor_distance๋ฅผ ์ค„์ด๊ณ  max_neighbors ์ˆ˜์น˜๋ฅผ ๊ธฐ๋ณธ๊ฐ’(10 ์ด์ƒ)์—์„œ 3~4 ์ •๋„๋กœ ๊ณผ๊ฐํ•˜๊ฒŒ ํ•˜ํ–ฅ ์กฐ์ •ํ•ฉ๋‹ˆ๋‹ค.

3) ๊ฑฐ๋ฆฌ์— ๋”ฐ๋ฅธ LOD(Level of Detail) ๊ธธ์ฐพ๊ธฐ ๊ตฌํ˜„

ํ™”๋ฉด(Viewport) ๋ฐ–์— ์žˆ๊ฑฐ๋‚˜ ํ”Œ๋ ˆ์ด์–ด์™€ ๋ฉ€๋ฆฌ ๋–จ์–ด์ง„ AI ์—์ด์ „ํŠธ๋Š” ๊ณ ์ •๋ฐ€ A* ์—ฐ์‚ฐ์„ ๋งค๋ฒˆ ์ˆ˜ํ–‰ํ•  ํ•„์š”๊ฐ€ ์—†์Šต๋‹ˆ๋‹ค.

  1. Near Zone (ํ”Œ๋ ˆ์ด์–ด ๊ทผ์ฒ˜): 0.2~0.5์ดˆ ์ฃผ๊ธฐ๋กœ ๋น„๋™๊ธฐ ๊ฒฝ๋กœ ์—…๋ฐ์ดํŠธ. RVO2 ํšŒํ”ผ ์—ฐ์‚ฐ ํ™œ์„ฑํ™”.
  2. Far Zone (ํ™”๋ฉด ๋ฐ–): 2.0~3.0์ดˆ ์ฃผ๊ธฐ๋กœ ์—…๋ฐ์ดํŠธ ์ฃผ๊ธฐ๋ฅผ ๋Œ€ํญ ๋‚ฎ์ถค. ํšŒํ”ผ ์—ฐ์‚ฐ ๋น„ํ™œ์„ฑํ™” ๋ฐ ์ง์„  ๊ฑฐ๋ฆฌ ๋ฒกํ„ฐ ๋‹จ์ˆœ ์ด๋™(Direct Vector Movement) ํ˜ผ์šฉ.

4. ์„ฑ๋Šฅ ์ตœ์ ํ™” ์ „ํ›„ ๋ฒค์น˜๋งˆํฌ ๋น„๊ต

์•„๋ž˜ ํ‘œ๋Š” ๋™์ผํ•œ PC ํ™˜๊ฒฝ(Intel i7-12700K, RTX 3070)์—์„œ 800๊ฐœ์˜ 2D ์—์ด์ „ํŠธ๊ฐ€ ๋™์‹œ์— ํ”Œ๋ ˆ์ด์–ด๋ฅผ ์ถ”์ ํ•˜๋„๋ก ํ…Œ์ŠคํŠธํ•œ ๊ฒฐ๊ณผ์ž…๋‹ˆ๋‹ค.

๊ตฌ๋ถ„ ๊ธฐ์กด (๋™๊ธฐ์‹ + ๊ธฐ๋ณธ ์„ธํŒ…) ์ตœ์ ํ™” ํ›„ (๋น„๋™๊ธฐ ์Šค๋ ˆ๋“œ + LOD)
ํ‰๊ท  ํ”„๋ ˆ์ž„ (FPS) 18 ~ 24 FPS 58 ~ 60 FPS
Physics Frame Time 42.5 ms 6.2 ms
CPU ๋ฉ”์ธ ์Šค๋ ˆ๋“œ ์ ์œ ์œจ 98% (๋‹จ์ผ ์ฝ”์–ด ๊ณผ๋ถ€ํ•˜) 35% (๋ฉ€ํ‹ฐ ์ฝ”์–ด ๋ถ„์‚ฐ)
๋งˆ์ดํฌ๋กœ ์Šคํ„ฐํ„ฐ๋ง ๋งค์šฐ ์‹ฌํ•จ (๊ฒฝ๋กœ ์žฌ๊ณ„์‚ฐ ์‹œ) ๊ด€์ฐฐ๋˜์ง€ ์•Š์Œ


5. ๊ฒฐ๋ก  ๋ฐ ํ•ต์‹ฌ ์š”์•ฝ

Godot 4์—์„œ 2D ๋Œ€๊ทœ๋ชจ ๊ธธ์ฐพ๊ธฐ ๋ณ‘๋ชฉ ํ˜„์ƒ์„ ํ•ด๊ฒฐํ•˜๋Š” ํ•ต์‹ฌ ์š”์†Œ๋Š” "์—ฐ์‚ฐ์˜ ๋น„๋™๊ธฐํ™”"์™€ "๋ถˆํ•„์š”ํ•œ ๊ณ„์‚ฐ์˜ ์ตœ์†Œํ™”"์ž…๋‹ˆ๋‹ค.

  • NavigationServer2D.map_get_path() ์ง์ ‘ ํ˜ธ์ถœ์„ ๋ฉ”์ธ ์Šค๋ ˆ๋“œ์—์„œ ๋ฐฐ์ œํ•˜๊ณ  WorkerThreadPool ๊ธฐ๋ฐ˜์œผ๋กœ ๋น„๋™๊ธฐ ์ฒ˜๋ฆฌํ•˜์„ธ์š”.
  • ๋ชจ๋“  ์—์ด์ „ํŠธ์˜ ๊ฒฝ๋กœ๋ฅผ ๊ฐ™์€ ํ”„๋ ˆ์ž„์— ๊ตฌํ•˜์ง€ ์•Š๋„๋ก ํƒ€์ž„ ์Šฌ๋ผ์ด์‹ฑ(Time-Slicing)์„ ์ ์šฉํ•˜์„ธ์š”.
  • RVO2 ํšŒํ”ผ ์—ฐ์‚ฐ์€ ์‹œ๊ฐ์ ์œผ๋กœ ์ค‘์š”ํ•œ ๊ฐœ์ฒด์—๋งŒ ์ œํ•œ์ ์œผ๋กœ ์ ์šฉํ•˜๊ณ , ์ˆ˜๋งŽ์€ ์žก๋ชน์€ Distance LOD ๊ธฐ๋ฒ•๊ณผ ๋‹จ์ˆœ ๋ฌผ๋ฆฌ ํŽ„์Šค(Impulse) ๋ถ„๋ฆฌ๋กœ ๋Œ€์ฒดํ•˜์„ธ์š”.
  • ๋™์  ๋งต ๋ณ€๊ฒฝ ์‹œ ๋ฐœ์ƒํ•  ์ˆ˜ ์žˆ๋Š” ์Šค๋ ˆ๋“œ ์•ˆ์ „์„ฑ(Thread-Safety) ์ด์Šˆ๋ฅผ ๋Œ€๋น„ํ•ด Task Queue Flush ๋งค์ปค๋‹ˆ์ฆ˜์„ ๋ฐ˜๋“œ์‹œ ๊ฐ–์ถ”์–ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.

์ด๋Ÿฌํ•œ ์ตœ์ ํ™” ๊ธฐ๋ฒ• ๊ธฐํ‹€์„ ์„ธ์›Œ๋‘”๋‹ค๋ฉด, ์ˆ˜์ฒœ ๊ฐœ์˜ ๊ตฐ์ง‘ AI๊ฐ€ ๋“ฑ์žฅํ•˜๋Š” 2D ํƒ‘๋‹ค์šด ๊ฒŒ์ž„์—์„œ๋„ ํ”„๋ ˆ์ž„ ์ €ํ•˜ ์—†์ด ๋งค๋„๋Ÿฝ๊ณ  ๋ชฐ์ž…๊ฐ ๋†’์€ ํ”Œ๋ ˆ์ด ๊ฒฝํ—˜์„ ์ œ๊ณตํ•  ์ˆ˜ ์žˆ์„ ๊ฒƒ์ž…๋‹ˆ๋‹ค.

๋ฐ˜์‘ํ˜•