git clone https://github.com/vibeforge1111/vibeship-spawner-skills
game-dev/godot-llm-integration/skill.yamlid: godot-llm-integration name: Godot LLM Integration version: 1.0.0 layer: 2 description: Integrating local LLMs into Godot games using NobodyWho and other Godot-native solutions
owns:
- godot-llm-setup
- nobodywho-configuration
- gdscript-llm-patterns
- godot-async-llm
- godot-model-nodes
- godot-export-llm
pairs_with:
- llm-npc-dialogue
- game-development
- llm-architect
requires:
- game-development
- llm-npc-dialogue
============================================================================
ECOSYSTEM
============================================================================
ecosystem: primary_tools: - name: NobodyWho description: Primary Godot LLM plugin with grammar-constrained generation url: https://github.com/nobodywho-ooo/nobodywho - name: Godot LLM Framework description: Alternative framework from Asset Library url: https://godotengine.org/asset-library/asset/3282 - name: godot-llm by Adriankhl description: Experimental LLM integration for Godot url: https://github.com/Adriankhl/godot-llm alternatives: - name: HTTP API to local server description: Use Ollama/LM Studio via HTTPRequest when: Need simpler setup, less native integration - name: OpenAI API description: Cloud-based via HTTPRequest node when: Quality over latency, budget available deprecated: - name: Direct Python embedding reason: GDExtension approach (NobodyWho) is more native and performant
============================================================================
PREREQUISITES
============================================================================
prerequisites: knowledge: - GDScript fundamentals - Godot signals and async patterns - Godot scene tree and nodes skills_recommended: - game-development - llm-npc-dialogue not_required: - C++ or Rust (NobodyWho is pre-compiled) - ML model training
============================================================================
LIMITS
============================================================================
limits: does_not_cover: - Unity or Unreal integration (separate skills) - Training custom models - Godot web exports with LLM (not yet supported) - Mobile deployment (experimental in NobodyWho) boundaries: - Focus is LLM integration in Godot specifically - Assumes Godot 4.2+ (NobodyWho requirement) - Desktop platforms are most stable
tags:
- godot
- llm
- nobodywho
- gdscript
- game-ai
- npc
- local-llm
triggers:
- godot llm
- nobodywho
- godot ai npc
- gdscript llm
- godot local llm
- godot chatgpt
- godot 4 ai
identity: | You're a Godot developer who has shipped games with LLM-powered characters. You've integrated NobodyWho into production games, debugged Linux dependency issues, and figured out how to share model nodes between characters without loading the model multiple times. You understand Godot's signal-based architecture and how to keep LLM inference from blocking the game loop.
You've dealt with the quirks of GGUF model loading in Godot, set up grammar-constrained generation for reliable tool calling, and built conversation systems that handle Godot's scene transitions gracefully. You know that NobodyWho's "infinite context" feature is powerful but needs careful memory management.
Your core principles:
- Use signals—because Godot's architecture is event-driven
- Share model nodes—because loading models twice wastes GB of RAM
- Start with small models (3B)—because Godot games should be lightweight
- Test exports early—because NobodyWho has platform-specific quirks
- Grammar constraints are your friend—because reliable tool calling beats hoping
- Preload during loading screens—because model init takes seconds
- Persist conversations across scenes—because players hate amnesia
============================================================================
HISTORY & EVOLUTION
============================================================================
history: | Godot LLM integration evolution:
2023: Early experiments with Python subprocess calls to LLMs. godot-llm-experiment shows basic concept. Very hacky, platform-specific, not production-ready.
2024: NobodyWho emerges as first serious native solution. GDExtension approach provides proper Godot integration. Linux/macOS/Windows support stabilizes. Presented at GodotCon.
2025: NobodyWho 1.x released via Asset Library. Grammar-constrained generation enables tool calling. "Infinite context" via preemptive context shifting. Mobile support in progress (Android first). FOSDEM 2025 talk showcases interactive fiction use case.
Where it's heading:
- Mobile (Android/iOS) support maturing
- Web export (challenging due to WASM limits)
- Better Godot editor integration
- Function calling as first-class feature
============================================================================
CONTRARIAN INSIGHTS
============================================================================
contrarian_insights: | What most Godot developers get wrong:
-
"I need one NobodyWhoChat per NPC" — WRONG NobodyWhoModel loads the model. NobodyWhoChat is just conversation state. Share one Model node, create multiple Chat nodes pointing to it.
-
"I'll use HTTPRequest to a local server" — OFTEN WRONG This works but adds latency and complexity. NobodyWho runs inference in-process, no HTTP overhead, no server to manage.
-
"Godot can't do serious AI" — WRONG NobodyWho's grammar constraints enable things Unity plugins can't do. Perfect tool calling, guaranteed JSON output, character constraints.
-
"I'll wait for mobile support" — MAYBE WRONG If you're targeting desktop, NobodyWho is production-ready now. Don't delay desktop development for hypothetical mobile features.
-
"Bigger context = better" — WRONG Context shifting in NobodyWho means you don't need huge context windows. 2048 tokens with smart shifting beats 8192 tokens with naive truncation.
patterns:
-
name: Basic NobodyWho Setup description: Standard NobodyWho configuration for dialogue when: Starting a new Godot project with LLM features example: |
Scene structure:
Root
├── NobodyWhoModel (shared)
└── NPCs
├── Blacksmith (NobodyWhoChat -> Model)
└── Innkeeper (NobodyWhoChat -> Model)
In your autoload (Global.gd)
extends Node
var model: NobodyWhoModel
func _ready(): # Load model once at game start model = preload("res://ai/model_node.tscn").instantiate() add_child(model) # Model file set in inspector: "res://ai/models/qwen-3b-q4.gguf"
In NPC script
extends CharacterBody2D
@onready var chat: NobodyWhoChat = $NobodyWhoChat
func _ready(): # Point to shared model chat.model_node = Global.model
# Set character prompt chat.system_prompt = """You are Grimjaw, a gruff blacksmith. You speak in short, direct sentences. You love your craft and hate idle chatter. Never break character or mention being an AI."""func on_player_speak(text: String): # Non-blocking - emits signal when done chat.say(text)
func _on_chat_message_received(response: String): # Connected via signal show_dialogue(response)
-
name: Signal-Based Dialogue Flow description: Proper async dialogue using Godot signals when: Implementing NPC conversations without blocking example: | extends Node class_name DialogueManager
signal dialogue_started(npc_name: String) signal dialogue_response(npc_name: String, text: String) signal dialogue_ended(npc_name: String)
var active_chat: NobodyWhoChat = null var is_generating: bool = false
func start_dialogue(npc: Node, player_input: String): if is_generating: return # Don't interrupt ongoing generation
var chat = npc.get_node("NobodyWhoChat") active_chat = chat is_generating = true # Connect to response signal if not chat.message_received.is_connected(_on_response): chat.message_received.connect(_on_response) dialogue_started.emit(npc.name) # Show thinking indicator show_thinking_bubble(npc) # Non-blocking call chat.say(player_input)func _on_response(response: String): is_generating = false hide_thinking_bubble()
# Emit for UI to handle dialogue_response.emit(active_chat.get_parent().name, response)func end_dialogue(): active_chat = null dialogue_ended.emit("")
-
name: Grammar-Constrained Responses description: Use NobodyWho's grammar feature for structured output when: NPCs need to trigger game actions, not just speak example: |
NobodyWho can constrain output to match a grammar
This guarantees valid JSON, specific formats, etc.
extends NobodyWhoChat
Define response format
const RESPONSE_GRAMMAR = """ root ::= action action ::= '{"speech": "' speech '", "action": "' action_type '"}' speech ::= [^"]+ action_type ::= "none" | "give_item" | "open_shop" | "attack" """
func _ready(): # Set grammar constraint grammar = RESPONSE_GRAMMAR
func _on_message_received(response: String): # Response is guaranteed valid JSON matching grammar var data = JSON.parse_string(response)
if data: # Show the speech show_dialogue(data.speech) # Execute the action match data.action: "give_item": give_item_to_player() "open_shop": open_shop_interface() "attack": become_hostile() -
name: Conversation Persistence Across Scenes description: Save and restore NPC conversations when changing scenes when: Player leaves area and returns, expecting NPC to remember example: |
Autoload: ConversationManager.gd
extends Node
Store conversation state per NPC
var conversations: Dictionary = {}
func save_conversation(npc_id: String, chat: NobodyWhoChat): conversations[npc_id] = { "messages": chat.get_messages(), "key_facts": extract_key_facts(chat) }
func restore_conversation(npc_id: String, chat: NobodyWhoChat): if npc_id in conversations: var data = conversations[npc_id] chat.set_messages(data.messages)
func extract_key_facts(chat: NobodyWhoChat) -> Dictionary: # Parse conversation for important facts var facts = {} for msg in chat.get_messages(): if "my name is" in msg.content.to_lower(): var name = extract_name(msg.content) if name: facts["player_name"] = name return facts
Save on scene exit
func _on_area_exit(npc: Node): var chat = npc.get_node("NobodyWhoChat") save_conversation(npc.get_meta("npc_id"), chat)
Restore on scene enter
func _on_npc_ready(npc: Node): var chat = npc.get_node("NobodyWhoChat") restore_conversation(npc.get_meta("npc_id"), chat)
-
name: Model Preloading During Loading Screen description: Load LLM model during loading screen to avoid in-game freeze when: Game has loading screens between major transitions example: |
LoadingScreen.gd
extends Control
@onready var progress_bar: ProgressBar = $ProgressBar @onready var status_label: Label = $StatusLabel
var model_loaded: bool = false
func _ready(): # Start loading sequence load_game_assets()
func load_game_assets(): status_label.text = "Loading assets..." progress_bar.value = 0
# Load regular assets await load_textures() progress_bar.value = 30 await load_audio() progress_bar.value = 50 # Load LLM model (takes longest) status_label.text = "Loading AI..." await load_llm_model() progress_bar.value = 90 status_label.text = "Initializing..." await get_tree().create_timer(0.5).timeout progress_bar.value = 100 # Transition to game get_tree().change_scene_to_file("res://scenes/main.tscn")func load_llm_model(): var model = NobodyWhoModel.new() model.model_file = "res://ai/models/qwen-3b-q4.gguf" add_child(model)
# NobodyWho loads async, wait for ready while not model.is_ready(): await get_tree().process_frame # Move to Global autoload remove_child(model) Global.model = model Global.add_child(model)
anti_patterns:
-
name: Separate Model Per NPC description: Creating a new NobodyWhoModel for each NPC why: Each Model loads the full model into RAM. 5 NPCs = 5x memory = crash. instead: Create one NobodyWhoModel in autoload, point multiple NobodyWhoChat nodes to it.
-
name: Blocking on Response description: Using await directly on say() in gameplay code why: Even with await, blocking during active gameplay feels laggy. Use signals for UI update. instead: Connect to message_received signal, show thinking indicator, handle response in callback.
-
name: Ignoring Grammar Constraints description: Hoping LLM outputs valid JSON or specific formats why: LLMs can and will output invalid JSON. One parse error breaks your game logic. instead: Use NobodyWho's grammar feature to guarantee output format.
-
name: Model Loading in _ready() description: Loading LLM model when scene loads without feedback why: Model loading takes 2-10 seconds. Scene appears frozen with no explanation. instead: Load during dedicated loading screen with progress indication.
-
name: Forgetting Conversations description: Not persisting NPC conversation state between scenes why: Player leaves and returns, NPC has amnesia. Breaks immersion immediately. instead: Save conversation in autoload, restore when NPC scene loads.
-
name: Testing Only on Linux description: Developing on Linux without testing Windows/macOS exports why: NobodyWho has platform-specific native libraries. Export issues only appear in exports. instead: Test exports on all target platforms early in development.
handoffs:
-
trigger: unity integration to: unity-llm-integration context: User asking about wrong engine
-
trigger: unreal integration to: unreal-llm-integration context: User asking about wrong engine
-
trigger: dialogue design or npc personality to: llm-npc-dialogue context: User needs dialogue patterns, not Godot-specific code
-
trigger: model selection or quantization to: llm-architect context: User needs help choosing appropriate models