| name | game-godot-genre-simulation |
| version | 1.1.1 |
| description | Implements Godot 4.2+ management-sim and tycoon loops: tick-decoupled clocks, multi-resource economies, workers and facilities, demand, unlocks, and AStarGrid2D logistics. Trigger when wiring a tick manager, production chain, or logistics grid for a Godot tycoon. Not a shooter, platformer, or visual-novel genre chair. Never price primary currency in floating point or bind simulation ticks to rendered FPS. |
| risk | safe |
| source | open-router-deepsearch |
| date_added | 2026-06-16T00:00:00.000Z |
Genre: Simulation / Tycoon
Optimization, systems mastery, and satisfying feedback loops define management games. This skill provides expert patterns, anti-patterns, and production-grade Godot implementations for simulation and tycoon genres (SimCity, RollerCoaster Tycoon, Factorio, Two Point Hospital).
When to Use
Use this skill when building management simulations, tycoon games, city builders, or resource optimization games in Godot 4.2+. Trigger keywords: tycoon, economy system, resource management, time scale, feedback loop, progression unlock, simulation tick, city builder, production chain, logistics grid.
Prerequisites
- Godot 4.2 or above (stable). Never use deprecated APIs from earlier versions.
- Windows host is primary (PowerShell). All path examples use Windows conventions (
res://, ~).
- Familiarity with Godot
Resource, RefCounted, Node, Signal, and @export systems.
- Scripts referenced below live in
scripts/ relative to this skill folder.
Procedure
1. Load Expert Components
Before writing any simulation code, load the reference scripts from scripts/:
| Script | When to Load |
|---|
scripts/sim_tick_manager.gd | When you need a variable-speed tick system that decouples simulation from rendering. Load first—it is the backbone of every other system. |
scripts/tycoon_economy.gd | When you need a multi-resource economic engine with integer-precision currency. Load after the tick manager so economy updates are driven by ticks. |
scripts/simulation_patterns.gd | When you need reusable patterns: AStarGrid2D logistics, low-processor modes, and batched entity processing. Load when implementing pathfinding or optimizing stationary management screens. |
2. Design the Economy (Multi-Resource System)
The heart of any tycoon game is its economy. Key principle: multiple interconnected resources that force trade-offs.
class_name TycoonEconomy
extends Node
signal resource_changed(resource_type: String, amount: float)
signal went_bankrupt
var resources: Dictionary = {
"money": 10000.0,
"reputation": 50.0,
"workers": 0,
"materials": 100.0,
"energy": 100.0
}
var resource_caps: Dictionary = {
"reputation": 100.0,
"workers": 50,
"energy": 1000.0
}
func modify_resource(type: String, amount: float) -> bool:
if amount < 0 and resources[type] + amount < 0:
if type == "money":
went_bankrupt.emit()
return false
resources[type] = clamp(
resources[type] + amount,
0,
resource_caps.get(type, INF)
)
resource_changed.emit(type, resources[type])
return true
Income/Expense Tracking — always provide detailed breakdowns so players can make optimization decisions:
class_name FinancialTracker
extends Node
var income_sources: Dictionary = {}
var expense_sources: Dictionary = {}
signal financial_update(profit: float, income: float, expenses: float)
func calculate_tick() -> float:
var total_income := 0.0
var total_expenses := 0.0
for source in income_sources.values():
total_income += source
for source in expense_sources.values():
total_expenses += source
var profit := total_income - total_expenses
financial_update.emit(profit, total_income, total_expenses)
return profit
3. Implement the Time System
Simulation games need controllable time with variable speed:
class_name SimulationTime
extends Node
signal time_tick(delta_game_hours: float)
signal day_changed(day: int)
signal speed_changed(new_speed: int)
enum Speed { PAUSED, NORMAL, FAST, ULTRA }
@export var seconds_per_game_hour := 30.0
var current_speed := Speed.NORMAL
var speed_multipliers := {
Speed.PAUSED: 0.0,
Speed.NORMAL: 1.0,
Speed.FAST: 3.0,
Speed.ULTRA: 10.0
}
var current_hour := 8.0
var current_day := 1
func _process(delta: float) -> void:
if current_speed == Speed.PAUSED:
return
var game_delta := (delta / seconds_per_game_hour) * speed_multipliers[current_speed]
current_hour += game_delta
if current_hour >= 24.0:
current_hour -= 24.0
current_day += 1
day_changed.emit(current_day)
time_tick.emit(game_delta)
func set_speed(speed: Speed) -> void:
current_speed = speed
speed_changed.emit(speed)
4. Manage Entities (Workers, NPCs, Facilities)
Workers/NPCs:
# workstation.gd
class_name Workstation extends Node
func work(amount: float) -> void:
print("Work done: ", amount)
# worker.gd
class_name Worker
extends Node
enum State { IDLE, WORKING, RESTING, COMMUTING }
@export var wage_per_hour: float = 10.0
@export var skill_level: float = 1.0
@export var morale: float = 80.0
var current_state := State.IDLE
var assigned_workstation: Workstation
func update(game_hours: float) -> void:
match current_state:
State.WORKING:
if assigned_workstation:
var productivity := skill_level * (morale / 100.0)
assigned_workstation.work(game_hours * productivity)
morale -= game_hours * 0.5
State.RESTING:
morale = min(100.0, morale + game_hours * 2.0)
func calculate_hourly_cost() -> float:
return wage_per_hour
Buildings/Facilities:
class_name Facility
extends Node3D
@export var build_cost: Dictionary
@export var operating_cost_per_hour: float = 5.0
@export var capacity: int = 5
@export var output_per_hour: Dictionary
var assigned_workers: Array[Worker] = []
var is_operational := true
var efficiency := 1.0
func calculate_output(game_hours: float) -> Dictionary:
if not is_operational or assigned_workers.is_empty():
return {}
var worker_efficiency := 0.0
for worker in assigned_workers:
worker_efficiency += worker.skill_level * (worker.morale / 100.0)
worker_efficiency /= capacity
var result := {}
for resource in output_per_hour:
result[resource] = output_per_hour[resource] * game_hours * worker_efficiency * efficiency
return result
5. Implement Customer/Demand System
class_name Customer extends RefCounted:
var patience: float = 60.0
var spending_budget: float = 50.0
class_name CustomerSimulation
extends Node
@export var base_customers_per_hour := 10.0
@export var demand_curve: Curve
var customer_queue: Array[Customer] = []
func generate_customers(game_hour: float, delta_hours: float) -> void:
var demand_mult = 1.0
if demand_curve:
demand_mult = demand_curve.sample(game_hour / 24.0)
var reputation_mult = 1.0
var economy_node = get_node_or_null("/root/Economy")
if economy_node and "resources" in economy_node:
reputation_mult = economy_node.resources["reputation"] / 50.0
var customers_to_spawn := base_customers_per_hour * delta_hours * demand_mult * reputation_mult
for i in int(customers_to_spawn):
spawn_customer()
func spawn_customer() -> void:
var customer := Customer.new()
customer.patience = randf_range(30.0, 120.0)
customer.spending_budget = randf_range(10.0, 100.0)
customer_queue.append(customer)
6. Add Feedback Systems
Visual Feedback (floating income/expense indicators):
class_name ResourceFlowVisualizer
extends Node
@export var coin_scene: PackedScene
func show_income(amount: float, from: Vector2, to: Vector2) -> void:
if not coin_scene:
return
var coin = coin_scene.instantiate() as Control
coin.position = from
add_child(coin)
var tween := create_tween()
tween.tween_property(coin, "position", to, 0.5)
tween.tween_callback(coin.queue_free)
var label := Label.new()
label.text = "+$" + str(int(amount))
label.position = from
add_child(label)
var label_tween := create_tween()
label_tween.tween_property(label, "position:y", label.position.y - 30, 0.5)
label_tween.parallel().tween_property(label, "modulate:a", 0.0, 0.5)
label_tween.tween_callback(label.queue_free)
Statistics Dashboard (rolling income/expense graph):
class_name StatsDashboard
extends Control
@export var graph_history_hours := 24
var income_history: Array[float] = []
var expense_history: Array[float] = []
func record_financial_tick(income: float, expenses: float) -> void:
income_history.append(income)
expense_history.append(expenses)
while income_history.size() > graph_history_hours:
income_history.pop_front()
expense_history.pop_front()
queue_redraw()
func _draw() -> void:
draw_line_graph(income_history, Color.GREEN)
draw_line_graph(expense_history, Color.RED)
func draw_line_graph(history: Array[float], color: Color) -> void:
if history.size() < 2:
return
for i in range(history.size() - 1):
var p1 = Vector2(i * 10.0, history[i])
var p2 = Vector2((i + 1) * 10.0, history[i + 1])
draw_line(p1, p2, color, 2.0)
7. Implement Progression & Unlocks
class_name UnlockSystem
extends Node
var unlocks: Dictionary = {
"basic_facility": true,
"advanced_facility": false,
"marketing": false,
"automation": false
}
var unlock_conditions: Dictionary = {
"advanced_facility": {"money_earned": 50000},
"marketing": {"reputation": 70},
"automation": {"workers_hired": 20}
}
var progress: Dictionary = {
"money_earned": 0.0,
"workers_hired": 0
}
func check_unlocks() -> Array[String]:
var newly_unlocked: Array[String] = []
for unlock in unlock_conditions:
if unlocks[unlock]:
continue
var conditions := unlock_conditions[unlock]
var all_met := true
for condition in conditions:
if progress.get(condition, 0) < conditions[condition]:
all_met = false
break
if all_met:
unlocks[unlock] = true
newly_unlocked.append(unlock)
return newly_unlocked
8. Advanced Patterns
Dependency-Graph Pattern (Production Chains)
Represent complex production chains (e.g., Raw Materials → Intermediate → Finished Goods) using nested Resource structures. This allows deep, recursive data definitions fully editable in the Inspector.
# item_resource.gd
class_name ItemResource extends Resource:
@export var item_name: String = ""
@export var base_value: int = 0
# production_recipe.gd
class_name ProductionRecipe extends Resource
@export var required_inputs: Dictionary[ItemResource, int] = {}
@export var output_item: ItemResource
@export var output_yield: int = 1
@export var production_time: float = 5.0
func can_produce(available_inventory: Dictionary[ItemResource, int]) -> bool:
for input_item in required_inputs:
var required_amount: int = required_inputs[input_item]
var available_amount: int = available_inventory.get(input_item, 0)
if available_amount < required_amount:
return false
return true
AStarGrid2D for Logistics & NPC Jobs
AStarGrid2D is specialized for 2D grids, eliminating the need to manually connect points. Ideal for factory floors, warehouse logistics, and NPC pathfinding in management sims. See scripts/simulation_patterns.gd for the full implementation.
class_name LogisticsGrid extends Node
var _astar_grid: AStarGrid2D
func _ready() -> void:
_astar_grid = AStarGrid2D.new()
_astar_grid.region = Rect2i(0, 0, 100, 100)
_astar_grid.cell_size = Vector2(32, 32)
_astar_grid.diagonal_mode = AStarGrid2D.DIAGONAL_MODE_ONLY_IF_NO_OBSTACLES
_astar_grid.update()
func place_obstacle(cell_coords: Vector2i) -> void:
if _astar_grid.is_in_bounds(cell_coords.x, cell_coords.y):
_astar_grid.set_point_solid(cell_coords, true)
func get_npc_path(start_cell: Vector2i, target_cell: Vector2i) -> Array[Vector2i]:
return _astar_grid.get_id_path(start_cell, target_cell)
CSV-to-Resource Workflow (Rapid Balancing)
Automate conversion of spreadsheet data (CSV) into native .tres files. Designers can balance thousands of entities in Excel/Google Sheets and "bake" them into performant Godot resources.
@tool
class_name CSVResourceBaker extends EditorScript
func _run() -> void:
var csv_path := "res://data/balancing_sheet.csv"
var output_dir := "res://data/generated_items/"
var file := FileAccess.open(csv_path, FileAccess.READ)
if not file: return
var rows := file.get_as_text().split("\n", false)
for i in range(1, rows.size()):
var columns := rows[i].split(",", false)
if columns.size() < 2: continue
var item_name: String = columns[0].strip_edges()
var base_value: int = columns[1].to_int()
var new_item := ItemResource.new()
new_item.item_name = item_name
new_item.base_value = base_value
ResourceSaver.save(new_item, output_dir + item_name.to_lower() + ".tres")
9. Godot-Specific Tips
- UI: Use
Control nodes extensively, Tree for lists, GraphEdit for connections.
- Performance: Process entities in batches, not every frame. Use a Tick Manager.
- Save/Load: Convert all game state to
Dictionary for JSON serialization. For large saves, use threaded serialization or optimized binary .res formats—never parse large JSON on the main thread.
- Isometric view: Use
Camera2D with orthographic projection.
- Low-power mode: Enable
OS.low_processor_usage_mode for stationary management screens to save CPU/battery.
Pitfalls
HARD RULES (Never Violate)
- NEVER use floating-point for primary currency; strictly use Integer Cents (or fixed-point math) to prevent accumulated precision errors in financial models.
- NEVER process 1000+ entities individually in
_process(); strictly use a Tick Manager to batch updates or process entities in rotating pools.
- NEVER rely on linear cost scaling; strictly use Exponential Growth (
Base * pow(1.15, Level)) to maintain challenge and strategic tension.
- NEVER hide critical metrics from the player; strictly provide Detailed Breakdowns (Income vs. Expense) so players can make optimization-based decisions.
- NEVER allow infinite resource stacking; strictly enforce Logistical Caps (warehouses/silos) to create meaningful space-management gameplay loops.
- NEVER let the early game become a "Waiting Simulator"; strictly Front-Load Decisions and quick early wins to build player momentum.
- NEVER modify a shared
Resource directly; strictly use duplicate() to avoid unintentionally updating every building of that type.
- NEVER tie simulation logic to the visual framerate; strictly use
_physics_process() or delta accumulators for deterministic simulation results.
- NEVER update UI labels every frame; strictly use Event-Driven Signals to refresh UI ONLY when the underlying data changes.
- NEVER run heavy economic loops synchronously; strictly use
WorkerThreadPool to offload complex calculations and prevent UI stutters.
- NEVER store massive resource data as
Nodes; strictly use RefCounted or Data Resources to avoid the memory/CPU overhead of the SceneTree.
- NEVER ignore
OS.low_processor_usage_mode; strictly enable it for stationary management screens to save massive CPU/Battery life.
- NEVER manipulate the SceneTree from background threads; strictly use
call_deferred() for thread-safe UI updates.
- NEVER parse large JSON save files on the main thread; strictly use or optimized binary formats.
Common Design Pitfalls
| Pitfall | Solution |
|---|
| Economy too easy to break | Extensive balancing, soft caps, diminishing returns |
| Boring early game | Front-load interesting decisions, quick early progression |
| Information overload | Progressive disclosure, collapsible UI panels |
| No clear goals | Milestones, achievements, scenarios |
| Tedious micromanagement | Automation unlocks, batch operations |
Verification
Run through this checklist to verify your implementation:
Related Skills