Overview
The Grid Building plugin provides foundational building mechanics - placement, validation, collision detection, and visual feedback. This guide explains how to extend the plugin with game-specific features while maintaining clear boundaries between core plugin functionality and custom game logic.
Plugin Scope vs Game-Specific Features
✅ What the Plugin Provides (Core Features)
Core Building Mechanics:
- Grid-based placement system
- Collision detection and validation
- Visual indicators (valid/invalid placement)
- Rule-based placement validation
- Mouse and keyboard input handling
- Scene tree management for building objects
Architecture Components:
BuildingSystem- Core placement logicGridPositioner2D- Input handling and positioningPlacementValidator- Rule validation systemCollisionMapper- Collision detectionIndicatorManager- Visual feedbackManipulationSystem- Object selection and manipulation
❌ What the Plugin Does NOT Provide (Game-Specific)
Game Design Features:
- Building variants and customization systems
- Quantity limits and resource management
- Upgrade and progression systems
- Unlockable content and tech trees
- Economy and resource costs
- Combat stats and game balance
- Save/load for game progress
- Custom UI layouts and themes
Why These Are Excluded:
- Scope Management: Keeps plugin focused and maintainable
- Game Flexibility: Each game needs different implementations
- Performance: Avoids unnecessary complexity for all users
- Opinionless Design: Plugin doesn't impose specific game design patterns
Implementation Guidelines
🎯 Core Principle: Composition Over Extension
✅ Recommended Approach:
# Build game-specific systems AROUND the plugin
class_name GameBuildingManager
extends Node
var grid_building_system: BuildingSystem
var quantity_limits: Dictionary = {}
var unlocked_buildings: Array[String] = []
func can_place_building(placeable: Placeable) -> bool:
# Check plugin validation first
if not grid_building_system.can_place(placeable):
return false
# Add game-specific checks
if not is_building_unlocked(placeable.building_type):
return false
if exceeds_quantity_limit(placeable.building_type):
return false
return true
❌ Avoid Direct Plugin Modification:
# Don't modify plugin classes directly
class_name CustomBuildingSystem extends BuildingSystem:
# This creates maintenance burden and update conflicts
Game-Specific Feature Implementation
1. Building Variants (Issue #18)
Problem: Single building type with multiple visual/functional variants
Solution: Custom variant selection system using existing plugin foundation
# Custom variant manager
class_name BuildingVariantManager
extends Node
var current_variants: Dictionary = {}
var variant_definitions: Dictionary = {}
func setup_variant_definitions():
variant_definitions["cannon_tower"] = [
{"name": "Basic", "scene": "res://buildings/cannon_basic.tscn", "stats": {"damage": 10}},
{"name": "Heavy", "scene": "res://buildings/cannon_heavy.tscn", "stats": {"damage": 25}},
{"name": "Rapid", "scene": "res://buildings/cannon_rapid.tscn", "stats": {"damage": 8, "fire_rate": 2.0}}
]
func get_current_variant(building_type: String) -> Dictionary:
return current_variants.get(building_type, variant_definitions[building_type][0])
func cycle_variant(building_type: String, direction: int = 1):
var variants = variant_definitions[building_type]
var current_index = variants.find(get_current_variant(building_type))
var new_index = (current_index + direction) % variants.size()
current_variants[building_type] = variants[new_index]
# Update the placeable resource with new variant
update_placeable_for_variant(building_type, variants[new_index])
# UI Integration Example
func setup_variant_ui():
var left_arrow = $VariantUI/LeftArrow
var right_arrow = $VariantUI/RightArrow
var variant_label = $VariantUI/VariantLabel
left_arrow.pressed.connect(func(): cycle_variant(selected_building_type, -1))
right_arrow.pressed.connect(func(): cycle_variant(selected_building_type, 1))
Integration Points:
- Modify
Placeableresources dynamically based on selected variant - Use plugin's existing placement system with variant-specific scenes
- Handle variant persistence in save/load system
2. Building Quantity Limits (Issue #19)
Problem: Restrict maximum number of specific building types
Solution: Custom validation rule + tracking system
# Custom placement rule for quantity limits
class_name QuantityLimitRule
extends PlacementRule
@export var max_quantity: int = 1
@export var building_type: String = ""
var placed_count: int = 0
func get_name() -> String:
return "QuantityLimit_%s" % building_type
func validate_condition(params: RuleValidationParameters) -> RuleResult:
var result = RuleResult.new()
result.is_valid = placed_count < max_quantity
if not result.is_valid:
result.failure_reason = "Maximum %d %s buildings allowed" % [max_quantity, building_type]
return result
# Building quantity tracker
class_name BuildingQuantityTracker
extends Node
var building_counts: Dictionary = {}
var quantity_limits: Dictionary = {}
func setup_limits():
quantity_limits["command_center"] = 1
quantity_limits["tesla_tower"] = 3
quantity_limits["super_weapon"] = 1
func on_building_placed(building_type: String):
building_counts[building_type] = building_counts.get(building_type, 0) + 1
update_ui_availability()
func on_building_removed(building_type: String):
building_counts[building_type] = max(0, building_counts.get(building_type, 0) - 1)
update_ui_availability()
func can_place_more(building_type: String) -> bool:
var current = building_counts.get(building_type, 0)
var limit = quantity_limits.get(building_type, 999)
return current < limit
func update_ui_availability():
# Disable UI buttons for buildings at limit
for building_type in quantity_limits:
var button = building_ui.get_button(building_type)
button.disabled = not can_place_more(building_type)
Integration Points:
- Add custom placement rules to existing validation system
- Connect to plugin's building placement/removal signals
- Update UI state based on current counts
3. Building Upgrades (Issue #20)
Problem: Upgrade existing buildings to more powerful variants
Solution: Scene replacement system using plugin's manipulation tools
# Building upgrade system
class_name BuildingUpgradeManager
extends Node
var upgrade_paths: Dictionary = {}
var upgrade_costs: Dictionary = {}
func setup_upgrade_definitions():
upgrade_paths["basic_tower"] = "advanced_tower"
upgrade_paths["advanced_tower"] = "elite_tower"
upgrade_costs["basic_tower"] = {"gold": 100, "tech_points": 1}
upgrade_costs["advanced_tower"] = {"gold": 250, "tech_points": 3}
func can_upgrade_building(building_node: Node2D) -> bool:
var building_type = get_building_type(building_node)
# Check if upgrade path exists
if not upgrade_paths.has(building_type):
return false
# Check resources
var cost = upgrade_costs.get(building_type, {})
return player_resources.can_afford(cost)
func upgrade_building(building_node: Node2D):
var building_type = get_building_type(building_node)
var upgraded_type = upgrade_paths[building_type]
# Get current position and state
var position = building_node.global_position
var tile_position = grid_system.world_to_tile(position)
# Remove old building (using plugin's manipulation system)
manipulation_system.remove_building(building_node)
# Create upgraded building
var upgraded_placeable = load("res://buildings/%s.tres" % upgraded_type)
var placement_result = building_system.try_place_at_tile(upgraded_placeable, tile_position)
if placement_result.is_successful():
# Deduct resources
player_resources.spend(upgrade_costs[building_type])
# Transfer any persistent data (health, experience, etc.)
transfer_building_data(building_node, placement_result.placed_building)
# UI Integration for upgrade prompts
func show_upgrade_ui(building_node: Node2D):
var upgrade_panel = $UpgradePanel
var building_type = get_building_type(building_node)
if can_upgrade_building(building_node):
upgrade_panel.setup_upgrade_option(building_type, upgrade_paths[building_type])
upgrade_panel.show_at_position(building_node.global_position)
Integration Points:
- Use plugin's
ManipulationSystemfor building selection - Leverage
BuildingSystemfor placement validation - Integrate with existing placement rules and collision detection
4. Unlockable Recipes (Issue #21)
Problem: Dynamic building availability based on game progression
Solution: Dynamic resource management + UI filtering
# Building unlock system
class_name BuildingUnlockManager
extends Node
var unlocked_buildings: Array[String] = ["basic_tower", "wall"]
var unlock_conditions: Dictionary = {}
func setup_unlock_conditions():
unlock_conditions["advanced_tower"] = {"wave": 5, "research": "advanced_weapons"}
unlock_conditions["missile_silo"] = {"wave": 10, "buildings_placed": 20}
unlock_conditions["shield_generator"] = {"research": "energy_shields", "gold_spent": 1000}
func check_unlock_conditions():
for building_type in unlock_conditions:
if building_type in unlocked_buildings:
continue
var conditions = unlock_conditions[building_type]
if meets_all_conditions(conditions):
unlock_building(building_type)
func unlock_building(building_type: String):
if building_type not in unlocked_buildings:
unlocked_buildings.append(building_type)
# Load the placeable resource dynamically
var placeable_path = "res://buildings/%s.tres" % building_type
var placeable = load(placeable_path)
# Add to available buildings in UI
building_ui.add_building_option(placeable)
# Show unlock notification
show_unlock_notification(building_type)
func filter_available_buildings(all_buildings: Array[Placeable]) -> Array[Placeable]:
var available = []
for placeable in all_buildings:
if placeable.building_type in unlocked_buildings:
available.append(placeable)
return available
# Dynamic UI updates
class_name DynamicBuildingUI
extends Control
var building_buttons: Dictionary = {}
func setup_initial_buildings():
var available = unlock_manager.filter_available_buildings(all_building_types)
for placeable in available:
add_building_button(placeable)
func add_building_option(placeable: Placeable):
var button = create_building_button(placeable)
building_container.add_child(button)
building_buttons[placeable.building_type] = button
# Connect to existing building system
button.pressed.connect(func(): building_system.select_building_type(placeable))
Integration Points:
- Filter available buildings before passing to plugin systems
- Use existing
BuildingSystemplacement logic - Connect unlock events to game progression systems
Custom Integration Patterns
Resource Management Integration
# Wrap plugin calls with resource checks
class_name ResourceAwareBuildingSystem
extends Node
func attempt_building_placement(placeable: Placeable, position: Vector2i) -> bool:
# Check resources first
var cost = get_building_cost(placeable)
if not player_resources.can_afford(cost):
show_insufficient_resources_message()
return false
# Use plugin for actual placement
var result = building_system.try_place_at_tile(placeable, position)
if result.is_successful():
player_resources.spend(cost)
return true
return false
Save/Load Integration
# Save/load game state including plugin data
class_name GameSaveManager
extends Node
func save_game() -> Dictionary:
var save_data = {}
# Save plugin state
save_data["buildings"] = building_system.get_placed_buildings_data()
save_data["grid_state"] = grid_system.get_state_data()
# Save game-specific state
save_data["unlocked_buildings"] = unlock_manager.unlocked_buildings
save_data["building_counts"] = quantity_tracker.building_counts
save_data["player_resources"] = player_resources.to_dict()
return save_data
func load_game(save_data: Dictionary):
# Restore plugin state
building_system.restore_buildings_from_data(save_data["buildings"])
grid_system.restore_state_from_data(save_data["grid_state"])
# Restore game-specific state
unlock_manager.unlocked_buildings = save_data["unlocked_buildings"]
quantity_tracker.building_counts = save_data["building_counts"]
player_resources.from_dict(save_data["player_resources"])
# Update UI to reflect loaded state
building_ui.refresh_available_buildings()
Best Practices
1. Maintain Plugin Boundaries
- Never modify plugin source files directly
- Use composition and delegation patterns
- Extend functionality through events and signals
2. Use Plugin Extension Points
# Plugin provides signals for extension
building_system.building_placed.connect(_on_building_placed)
building_system.building_removed.connect(_on_building_removed)
manipulation_system.building_selected.connect(_on_building_selected)
3. Custom Validation Rules
# Create game-specific placement rules
class_name CustomResourceRule extends PlacementRule:
func validate_condition(params: RuleValidationParameters) -> RuleResult:
# Add to existing validation pipeline
pass
4. UI Integration Patterns
# Wrap plugin UI components with game-specific logic
class_name GameBuildingUI extends Control:
var core_building_ui: BuildingUI # Plugin component
func _ready():
# Enhance plugin UI with game features
add_variant_selection()
add_resource_display()
add_upgrade_buttons()
Conclusion
The Grid Building plugin provides a solid foundation for grid-based building mechanics. By following these patterns, you can implement sophisticated game-specific features while maintaining the plugin's simplicity and your game's flexibility.
Key Takeaways:
- Compose, don't modify: Build around the plugin, not into it
- Use provided extension points: Signals, custom rules, and UI enhancement
- Separate concerns: Keep game logic separate from core building mechanics
- Maintain upgrade path: Avoid changes that prevent plugin updates
This approach ensures your game can leverage the plugin's robust foundation while implementing unique gameplay features that match your vision.
Source
docs/v5-0/guides/extending-grid-building-plugin.md
Plugin docs root:gdscript/plugins/grid_placement_dev/docs