mcpField
G
godot-optimization

godot-optimization

github.com/zate開発20719 回インストール51 週間前に更新
リポジトリ

godot-optimization について

godot-optimizationは、Godot開発者向けに2D・3Dゲームの性能分析と改善を支援するスキルです。ボトルネックの特定に加え、スクリプト、描画、メモリ、プラットフォーム別の調整方法を扱います。

主な機能

  1. 01フレーム・物理・描画時間の分析
  2. 02スクリプトや検索処理のボトルネック特定
  3. 03描画呼び出しと衝突判定の削減
  4. 04プーリング、LOD、空間分割の適用
  5. 05メモリとプラットフォーム性能の調整

活用シーン

  1. 01フレーム落ちやカクつきの診断
  2. 02Godotゲームのメモリ使用量の最適化
  3. 03モバイルやWeb向けのゲーム調整

インストール

コマンドをコピーしてターミナルで実行します。

$npx skills add https://github.com/zate/cc-godot --skill godot-optimization

インストール後、Claude Code で/godot-optimizationを入力して読み込まれたか確認します。

詳細

プロファイリング、ボトルネックの特定、2D ゲームと 3D ゲームの最適化手法に関する深い知識を持つ、Godot のパフォーマンス最適化エキスパートです。

パフォーマンスプロファイリング

Godot の組み込みプロファイラー

プロファイラーへのアクセス:

  • デバッグ → プロファイラー(ゲームの実行中)
  • タブ:フレーム、モニター、ネットワーク、ビジュアル

確認すべき主な指標:

  • FPS(1 秒あたりのフレーム数):スムーズなゲームプレイには 60(モバイルでは 30)が目安です
  • フレーム時間:60 FPS では 16.67ms 未満が目安です
  • 物理フレーム時間:物理処理にかかる時間です
  • アイドル時間:物理以外の処理にかかる時間です

パフォーマンスモニター

# Enable performance monitoring in code
func _ready():
    # Available monitors
    Performance.get_monitor(Performance.TIME_FPS)
    Performance.get_monitor(Performance.TIME_PROCESS)
    Performance.get_monitor(Performance.TIME_PHYSICS_PROCESS)
    Performance.get_monitor(Performance.MEMORY_STATIC)
    Performance.get_monitor(Performance.MEMORY_DYNAMIC)
    Performance.get_monitor(Performance.OBJECT_COUNT)
    Performance.get_monitor(Performance.OBJECT_NODE_COUNT)
    Performance.get_monitor(Performance.RENDER_OBJECTS_IN_FRAME)
    Performance.get_monitor(Performance.RENDER_VERTICES_IN_FRAME)

# Display FPS counter
func _process(_delta):
    var fps = Performance.get_monitor(Performance.TIME_FPS)
    $FPSLabel.text = "FPS: %d" % fps

よくあるパフォーマンスのボトルネック

1. _process() の呼び出しが多すぎる

問題:

# BAD: Running every frame when not needed
func _process(delta):
    check_for_enemies()  # Expensive operation
    update_ui()
    scan_environment()

解決策:

# GOOD: Use timers or reduce frequency
var check_timer: float = 0.0
const CHECK_INTERVAL: float = 0.5  # Check twice per second

func _process(delta):
    check_timer += delta
    if check_timer >= CHECK_INTERVAL:
        check_timer = 0.0
        check_for_enemies()

# Or disable processing when not needed
func _ready():
    set_process(false)  # Enable only when active

2. ノード検索が非効率

問題:

# BAD: Getting nodes every frame
func _process(delta):
    var player = get_node("/root/Main/Player")  # Slow lookup every frame
    look_at(player.global_position)

解決策:

# GOOD: Cache node references
@onready var player: Node2D = get_node("/root/Main/Player")

func _process(delta):
    if player:
        look_at(player.global_position)

3. get_tree() の呼び出しが過剰

問題:

# BAD: Repeated tree searches
func update():
    for enemy in get_tree().get_nodes_in_group("enemies"):
        # Process enemy

func check():
    for item in get_tree().get_nodes_in_group("items"):
        # Process item

解決策:

# GOOD: Cache groups or use signals
var enemies: Array = []

func _ready():
    enemies = get_tree().get_nodes_in_group("enemies")
    # Update when enemies added/removed via signals

4. 衝突判定が非効率

問題:

# BAD: Checking all objects every frame
func _physics_process(delta):
    for object in all_objects:
        if global_position.distance_to(object.global_position) < 100:
            # Do something

解決策:

# GOOD: Use Area2D/Area3D for automatic detection
@onready var detection_area = $DetectionArea

func _ready():
    detection_area.body_entered.connect(_on_body_detected)

func _on_body_detected(body):
    # Only called when something enters range
    pass

5. ドローコールが多すぎる

問題:

  • 個別のスプライトが多すぎる
  • テクスチャアトラスを使用していない
  • パーティクルが過剰
  • ライトが多すぎる

解決策:

# Use TileMap instead of individual Sprite2D nodes
# Use MultiMeshInstance for repeated objects
# Use texture atlases to batch sprites
# Limit number of lights and particles

# Example: MultiMesh for coins
@onready var multimesh_instance = $MultiMeshInstance2D

func _ready():
    var multimesh = MultiMesh.new()
    multimesh.mesh = preload("res://meshes/coin.tres")
    multimesh.instance_count = 100

    for i in range(100):
        var transform = Transform2D()
        transform.origin = Vector2(i * 50, 0)
        multimesh.set_instance_transform_2d(i, transform)

    multimesh_instance.multimesh = multimesh

6. スクリプトが最適化されていない

問題:

# BAD: Creating new objects every frame
func _process(delta):
    var direction = Vector2.ZERO  # New object every frame
    direction = (target.position - position).normalized()

解決策:

# GOOD: Reuse objects
var direction: Vector2 = Vector2.ZERO  # Reused

func _process(delta):
    direction = (target.position - position).normalized()

最適化手法

1. オブジェクトプーリング

# Instead of creating/destroying objects frequently
class_name ObjectPool

var pool: Array = []
var prefab: PackedScene
var pool_size: int = 20

func _init(scene: PackedScene, size: int):
    prefab = scene
    pool_size = size
    _fill_pool()

func _fill_pool():
    for i in range(pool_size):
        var obj = prefab.instantiate()
        obj.set_process(false)
        obj.visible = false
        pool.append(obj)

func get_object():
    if pool.is_empty():
        return prefab.instantiate()
    var obj = pool.pop_back()
    obj.set_process(true)
    obj.visible = true
    return obj

func return_object(obj):
    obj.set_process(false)
    obj.visible = false
    pool.append(obj)

2. 詳細度(LOD)

# Switch to simpler models/sprites when far away
@export var lod_distances: Array[float] = [50.0, 100.0, 200.0]
@onready var camera = get_viewport().get_camera_3d()

func _process(_delta):
    var distance = global_position.distance_to(camera.global_position)

    if distance < lod_distances[0]:
        _set_lod(0)  # High detail
    elif distance < lod_distances[1]:
        _set_lod(1)  # Medium detail
    elif distance < lod_distances[2]:
        _set_lod(2)  # Low detail
    else:
        _set_lod(3)  # Minimal/hidden

func _set_lod(level: int):
    match level:
        0:
            $HighDetailMesh.visible = true
            $MedDetailMesh.visible = false
            set_physics_process(true)
        1:
            $HighDetailMesh.visible = false
            $MedDetailMesh.visible = true
            set_physics_process(true)
        2:
            $MedDetailMesh.visible = true
            set_physics_process(false)
        3:
            visible = false
            set_process(false)

3. 空間分割

# Only process objects in active area
class_name ChunkManager

var active_chunks: Dictionary = {}
var chunk_size: float = 100.0

func get_chunk_key(pos: Vector2) -> Vector2i:
    return Vector2i(
        int(pos.x / chunk_size),
        int(pos.y / chunk_size)
    )

func update_active_chunks(player_position: Vector2):
    var player_chunk = get_chunk_key(player_position)

    # Activate nearby chunks
    for x in range(-1, 2):
        for y in range(-1, 2):
            var chunk_key = player_chunk + Vector2i(x, y)
            if chunk_key not in active_chunks:
                _load_chunk(chunk_key)

    # Deactivate far chunks
    for chunk_key in active_chunks.keys():
        if chunk_key.distance_to(player_chunk) > 2:
            _unload_chunk(chunk_key)

func _load_chunk(key: Vector2i):
    # Load and activate objects in this chunk
    active_chunks[key] = true

func _unload_chunk(key: Vector2i):
    # Deactivate or remove objects in this chunk
    active_chunks.erase(key)

4. 効率的な衝突レイヤー

# Set up collision layers properly
# Project Settings → Layer Names → 2D Physics

# Layer 1: Players
# Layer 2: Enemies
# Layer 3: Environment
# Layer 4: Projectiles

# Player only collides with enemies and environment
func _ready():
    collision_layer = 1  # Player is on layer 1
    collision_mask = 6   # Collides with layers 2 (enemies) and 3 (environment)
    # Binary: 110 = 6 (layers 2 and 3)

5. 物理処理での遅延呼び出し

# Don't modify physics objects during physics callback
func _on_body_entered(body):
    # BAD
    # body.queue_free()
    # $CollisionShape2D.disabled = true

    # GOOD
    body.call_deferred("queue_free")
    $CollisionShape2D.call_deferred("set_disabled", true)

メモリの最適化

1. テクスチャ圧縮

プロジェクト設定:

  • インポートタブ:テクスチャを圧縮する
  • デスクトップでは VRAM 圧縮を使用する
  • モバイルでは ETC2/ASTC を使用する
  • 可能な場合はテクスチャサイズを小さくする

2. オーディオの最適化

# Use streaming for long audio (music, voice)
# Use samples for short audio (SFX)

# In import settings:
# - Loop Mode: Disabled for SFX, Forward for music
# - Compress Mode: RAM for SFX, Streaming for music

3. シーンのインスタンス化

# Use instancing instead of duplicating
const ENEMY_SCENE = preload("res://enemies/enemy.tscn")

func spawn_enemy():
    var enemy = ENEMY_SCENE.instantiate()  # Shares resources
    add_child(enemy)

# Avoid:
# var enemy = $EnemyTemplate.duplicate()  # Duplicates everything

4. リソース管理

# Free resources when done
func remove_level():
    for child in get_children():
        child.queue_free()  # Properly free memory

    # Clear cached resources if needed
    ResourceLoader.clear_cache()

レンダリングの最適化

2D の最適化

# 1. Use CanvasLayer for UI (prevents redraw of game world)
# 2. Limit particle count
# 3. Use Light2D sparingly
# 4. Batch sprites with same texture

# Efficient particle system
@onready var particles = $GPUParticles2D

func _ready():
    particles.amount = 50  # Not 500
    particles.lifetime = 1.0  # Short lifetime
    particles.one_shot = true  # Don't loop unnecessarily

3D の最適化

# 1. Use occlusion culling
# 2. Bake lighting where possible
# 3. Use LOD for distant objects
# 4. Limit shadow-casting lights

# Efficient 3D setup
func _ready():
    # Bake lighting
    $WorldEnvironment.environment.background_mode = Environment.BG_SKY

    # Limit view distance
    var camera = $Camera3D
    camera.far = 500.0  # Don't render beyond this

    # Use SDFGI for global illumination (Godot 4)
    $WorldEnvironment.environment.sdfgi_enabled = true

プロファイリングのワークフロー

1. ボトルネックを特定する

  1. プロファイラーを開いた状態でゲームを実行する
  2. 最も遅い領域を特定する:
    • プロセス
    • 物理
    • レンダリング
    • スクリプト

2. 問題箇所を特定する

# Add timing to suspect code
var start_time = Time.get_ticks_usec()

# Suspect code here
_expensive_function()

var end_time = Time.get_ticks_usec()
print("Function took: ", (end_time - start_time) / 1000.0, " ms")

3. 最適化を適用する

  • 検索結果をキャッシュする
  • 実行頻度を下げる
  • より効率的なアルゴリズムを使用する
  • 不要な処理を取り除く

4. 結果を測定する

  • プロファイラーを再実行する
  • 改善を確認する
  • デグレードがないことを確認する

プラットフォーム別の最適化

モバイルの最適化

# Detect mobile platform
func _ready():
    if OS.get_name() in ["Android", "iOS"]:
        _apply_mobile_optimizations()

func _apply_mobile_optimizations():
    # Reduce particle count
    $Particles.amount = $Particles.amount / 2

    # Simplify shaders
    # Lower resolution
    get_viewport().size = get_viewport().size * 0.75

    # Disable expensive effects
    $WorldEnvironment.environment.ssao_enabled = false
    $WorldEnvironment.environment.glow_enabled = false

Web(HTML5)の最適化

# Reduce initial load
# Use streaming for assets
# Limit memory usage
# Avoid heavy physics calculations

パフォーマンステストのチェックリスト

  • フレームレートが目標値(60 FPS または 30 FPS)を維持している
  • 負荷の高いシーンでもフレーム落ちがない
  • メモリ使用量が安定している(メモリリークがない)
  • ロード時間が許容範囲内(3 秒未満)
  • 物理挙動が安定している(揺れやすり抜けがない)
  • モバイル:バッテリー消費が妥当
  • Web:初回ロードが速く、フリーズがない

このスキルを有効にするタイミング

ユーザーが次のような場合に有効にします:

  • ラグ、カクつき、パフォーマンスの低下に言及した場合
  • 最適化手法について質問した場合
  • パフォーマンス分析を依頼した場合
  • FPS の低下やフレームレートの問題に言及した場合
  • プロファイリングやベンチマークについて質問した場合
  • モバイル/Web の最適化で支援が必要な場合
  • メモリの問題やクラッシュに言及した場合
  • 「ゲームが遅いのはなぜ?」と質問した場合

最適化のワークフロー

  1. プロファイリング - Godot のプロファイラーを使ってボトルネックを特定する
  2. 特定 - 問題の原因となっているコードを見つける
  3. 最適化 - 適切な最適化手法を適用する
  4. テスト - 機能を損なわずに改善されたことを確認する
  5. 記録 - 変更内容と理由を記録する

必ず次の内容を説明してください:

  • なぜ遅いのか
  • どの最適化手法を使うべきか
  • どのように実装するか
  • どのような改善が期待できるか

コメント