| name | godot-genre-idle-clicker |
| description | Expert blueprint for idle/clicker games including big number handling (mantissa + exponent system), exponential growth curves (cost_growth_factor 1.15x), generator systems (auto-producers), offline progress calculation, prestige systems (reset for permanent multipliers), number formatting (K/M/B suffixes, scientific notation). Use for incremental games, idle games, or cookie clicker derivatives. Trigger keywords: idle_game, big_number, exponential_growth, generator_system, offline_progress, prestige_system, number_formatting. |
Genre: Idle / Clicker
Expert blueprint for idle/clicker games with exponential progression and prestige mechanics.
NEVER Do (Expert Anti-Patterns)
Economics & Math
- NEVER use standard floats for currency; strictly implement a BigNumber (Mantissa/Exponent) system (e.g.,
1.5e300) to prevent INF crashes at 1e308.
- NEVER use
Timer nodes for revenue generation; strictly use a manual accumulator in _process(delta) to prevent drift during frame fluctuations.
- NEVER hardcode generator costs or growth; strictly use an exponential formula:
Cost = BasePrice * pow(GrowthFactor, OwnedCount) (industry standard 1.15x).
- NEVER evaluate exact float equality (
==); strictly use is_equal_approx() or >= to prevent "stuck" progress due to precision loss.
- NEVER parse scientific notation strings with
to_int(); strictly use to_float() or a dedicated BigNumber parser.
Performance & Optimization
- NEVER update all UI labels every frame; strictly use Signals to update labels ONLY when values change, or throttle updates to 10 FPS.
- NEVER ignore Low Processor Usage Mode for mobile; strictly enable
OS.low_processor_usage_mode = true to preserve battery life.
- NEVER instantiate/delete hundreds of text nodes per second; strictly use Object Pooling or
MultiMeshInstance for click-feedback.
- NEVER update massive logs by modifying the
text property; strictly use append_text() to prevent main thread blocking.
Player Experience & Persistence
- NEVER ignore Offline Progress; strictly calculate
seconds_offline * total_revenue using system UNIX timestamps (Time.get_unix_time_from_system()).
- NEVER make the "Prestige" reset feel like a loss; strictly provide a global multiplier that makes the next run significantly faster (2-5x).
- NEVER calculate offline time using
Time.get_ticks_msec(); strictly use Persistent UNIX timestamps as ticks reset on app restart.
- NEVER use Node hierarchies for raw data; strictly use
RefCounted or Resource objects for lightweight, serializable logic.
🛠 Expert Components (scripts/)
Original Expert Patterns
Modular Components
Core Loop
- Click: Player performs manual action to gain currency.
- Buy: Player purchases "generators" (auto-clickers).
- Wait: Game plays itself, numbers go up.
- Upgrade: Player buys multipliers to increase efficiency.
- Prestige: Player resets progress for a permanent global multiplier.
Skill Chain
| Phase | Skills | Purpose |
|---|
| 1. Math | godot-gdscript-mastery | Handling numbers larger than 64-bit float |
| 2. UI | godot-ui-containers, labels | Displaying "1.5e12" or "1.5T" cleanly |
| 3. Data | godot-save-load-systems | Saving progress, offline time calculation |
| 4. Logic | signals | Decoupling UI from the economic simulation |
| 5. Meta | json-serialization | Balancing hundreds of upgrades via data |
Architecture Overview
1. Big Number System
Standard float goes to INF around 1.8e308. Idle games often go beyond.
You need a custom BigNumber class (Mantissa + Exponent).
# big_number.gd
class_name BigNumber
var mantissa: float = 0.0 # 1.0 to 10.0
var exponent: int = 0 # Power of 10
func _init(m: float, e: int) -> void:
mantissa = m
exponent = e
normalize()
func normalize() -> void:
if mantissa >= 10.0:
mantissa /= 10.0
exponent += 1
elif mantissa < 1.0 and mantissa != 0.0:
mantissa *= 10.0
exponent -= 1
2. Generator System
The core entities that produce currency.
# generator.gd
class_name Generator extends Resource
@export var id: String
@export var base_cost: BigNumber
@export var base_revenue: BigNumber
@export var cost_growth_factor: float = 1.15
var count: int = 0
func get_cost() -> BigNumber:
# Cost = Base * (Growth ^ Count)
return base_cost.multiply(pow(cost_growth_factor, count))
3. Simulation Manager (Offline Progress)
Calculating gains while the game was closed.
# game_manager.gd
func _ready() -> void:
var last_save_time = save_data.timestamp
var current_time = Time.get_unix_time_from_system()
var seconds_offline = current_time - last_save_time
if seconds_offline > 60:
var revenue = calculate_revenue_per_second().multiply(seconds_offline)
add_currency(revenue)
show_welcome_back_popup(revenue)
Key Mechanics Implementation
Prestige System (Reset)
Resetting generators but keeping prestige_currency.
func prestige() -> void:
if current_money.less_than(prestige_threshold):
return
# Formula: Cube root of money / 1 million
# (Just an example, depends on balance)
var gained_keys = calculate_prestige_gain()
save_data.prestige_currency += gained_keys
save_data.global_multiplier = 1.0 + (save_data.prestige_currency * 0.10)
# Reset
save_data.money = BigNumber.new(0, 0)
save_data.generators = ResetGenerators()
save_game()
reload_scene()
Formatting Numbers
Displaying 1234567 as 1.23M.
static func format(bn: BigNumber) -> String:
if bn.exponent < 3:
return str(int(bn.mantissa * pow(10, bn.exponent)))
var suffixes = ["", "K", "M", "B", "T", "Qa", "Qi"]
var suffix_idx = bn.exponent / 3
if suffix_idx < suffixes.size():
return "%.2f%s" % [bn.mantissa * pow(10, bn.exponent % 3), suffixes[suffix_idx]]
else:
return "%.2fe%d" % [bn.mantissa, bn.exponent]
Godot-Specific Tips
- Timers: Do NOT use
Timer nodes for revenue generation (drifting). Use _process(delta) and accumulate time.
- GridContainer: Perfect for the "Generators" list.
- Resources: Use
.tres files to define every generator (Farm, Mine, Factory) so you can tweak balance without touching code.
Common Pitfalls
- Floating Point Errors: Using standard
float for money. Fix: Use BigNumber implementation immediately.
- Boring Prestige: Resetting feels like a punishment. Fix: Ensure the post-prestige run is significantly faster (2x-5x speed).
- UI Lag: Updating 50 text labels every frame. Fix: Only update labels when values actually change (Signal-based), or throttling updates to 10fps.
🚀 Elite Technical Implementations (Batch 09)
1. BigReal-Math-Structure (Handling > 1e308)
Idle games often exceed the limits of 64-bit floats (~1.8e308). Use a custom RefCounted class to store numbers in scientific notation (mantissa + exponent), allowing for virtually infinite growth.
class_name BigReal extends RefCounted
@export var mantissa: float = 0.0
@export var exponent: int = 0
func _init(m: float = 0.0, e: int = 0) -> void:
mantissa = m
exponent = e
_normalize()
func _normalize() -> void:
if mantissa == 0.0:
exponent = 0
return
while abs(mantissa) >= 10.0:
mantissa /= 10.0
exponent += 1
while abs(mantissa) < 1.0 and mantissa != 0.0:
mantissa *= 10.0
exponent -= 1
func multiply(other: BigReal) -> BigReal:
return BigReal.new(mantissa * other.mantissa, exponent + other.exponent)
2. Multi-Offline-Progression Pattern
Calculate retroactively what the player earned while the game was closed using Time.get_unix_time_from_system(). Save the timestamp to user:// and compare it upon relaunch.
class_name OfflineProgressionManager extends Node
signal offline_earnings_calculated(seconds_offline: float)
const SAVE_PATH: String = "user://offline_save.json"
func _ready() -> void:
_process_offline_time()
func _process_offline_time() -> void:
var current_time = Time.get_unix_time_from_system()
var last_time = load_timestamp() # From FileAccess
var delta = current_time - last_time
if delta > 60.0:
offline_earnings_calculated.emit(delta)
func save_timestamp() -> void:
var file = FileAccess.open(SAVE_PATH, FileAccess.WRITE)
file.store_string(JSON.stringify({"last_time": Time.get_unix_time_from_system()}))
3. Particle-Batch-Juice (Manual Emission)
Spawning new nodes for click-juice is expensive. Use a single GPUParticles2D and call emit_particle() manually on every click to batch spawn particles directly at the mouse coordinates.
class_name ClickJuiceManager extends Node2D
@export var click_particles: GPUParticles2D
func _input(event: InputEvent) -> void:
if event.is_action_pressed(&"click"):
var click_pos = get_global_mouse_position()
_burst_particles(click_pos)
func _burst_particles(pos: Vector2) -> void:
for i in range(15):
var xform = Transform2D(0.0, pos)
var velocity = Vector2(randf_range(-200.0, 200.0), randf_range(-200.0, 200.0))
# Direct GPU emission bypasses SceneTree overhead
click_particles.emit_particle(xform, velocity, Color.WHITE, Color.WHITE, 0)