This guide explains how to implement and use grid-aware object rotation with GridPositioner2D, particularly for isometric games requiring 90-degree cardinal direction rotations.
Overview
The Grid Building plugin now includes comprehensive grid-aware rotation functionality through two main components:
- GBGridRotationUtils - Static utility class for grid-aligned rotation operations
- GridPositioner2D rotation integration - Keyboard input handling for rotating objects
Quick Start
1. Enable Rotation in Settings
# Configure your GridTargetingSettings
var targeting_settings := GridTargetingSettings.new()
targeting_settings.enable_keyboard_input = true
targeting_settings.enable_rotation_input = true # New setting for rotation
2. Configure Input Actions
Ensure your GBActions has rotation mappings defined:
var actions := GBActions.new()
# Existing movement actions
actions.positioner_up = "ui_up"
actions.positioner_down = "ui_down"
actions.positioner_left = "ui_left"
actions.positioner_right = "ui_right"
# New rotation actions
actions.rotate_left = "rotate_left" # Q key or custom action
actions.rotate_right = "rotate_right" # E key or custom action
3. Set Rotation Target
The positioner will automatically determine what to rotate:
# Rotate a specific object
targeting_state.target = my_game_object
# Or rotate the positioner itself (default fallback)
targeting_state.target = null # Will use positioner as rotation target
GBGridRotationUtils API Reference
Cardinal Direction Enum
enum CardinalDirection {
NORTH = 0, # 0 degrees / up
EAST = 1, # 90 degrees / right
SOUTH = 2, # 180 degrees / down
WEST = 3 # 270 degrees / left
}
Core Rotation Functions
Basic Direction Operations
# Convert between degrees and cardinal directions
var direction = GBGridRotationUtils.degrees_to_cardinal(90.0) # Returns EAST
var degrees = GBGridRotationUtils.cardinal_to_degrees(GBGridRotationUtils.CardinalDirection.SOUTH) # Returns 180.0
# Rotate directions
var next = GBGridRotationUtils.rotate_clockwise(GBGridRotationUtils.CardinalDirection.NORTH) # Returns EAST
var prev = GBGridRotationUtils.rotate_counter_clockwise(GBGridRotationUtils.CardinalDirection.NORTH) # Returns WEST
Node Rotation with Grid Alignment
# Rotate a Node2D while maintaining grid alignment
var new_direction = GBGridRotationUtils.rotate_node_clockwise(my_object, tile_map)
var new_direction = GBGridRotationUtils.rotate_node_counter_clockwise(my_object, tile_map)
# Set specific direction
GBGridRotationUtils.set_node_direction(my_object, GBGridRotationUtils.CardinalDirection.EAST, tile_map)
Movement and Utility Functions
# Get tile movement deltas for each direction
var north_delta = GBGridRotationUtils.get_direction_tile_delta(GBGridRotationUtils.CardinalDirection.NORTH) # Vector2i(0, -1)
var east_delta = GBGridRotationUtils.get_direction_tile_delta(GBGridRotationUtils.CardinalDirection.EAST) # Vector2i(1, 0)
# Direction utilities
var opposite = GBGridRotationUtils.get_opposite_direction(GBGridRotationUtils.CardinalDirection.NORTH) # SOUTH
var is_horizontal = GBGridRotationUtils.is_horizontal(GBGridRotationUtils.CardinalDirection.EAST) # true
var is_vertical = GBGridRotationUtils.is_vertical(GBGridRotationUtils.CardinalDirection.NORTH) # true
# Human-readable strings
var name = GBGridRotationUtils.direction_to_string(GBGridRotationUtils.CardinalDirection.EAST) # "East"
Advanced Operations
# Combined rotation and movement
var result = GBGridRotationUtils.rotate_and_move_node(
my_object, # Node to rotate/move
1, # 1 = clockwise, -1 = counter-clockwise, 0 = no rotation
GBGridRotationUtils.CardinalDirection.NORTH, # Direction to move (if move_after_rotation = true)
tile_map, # TileMapLayer for grid alignment
true # Whether to move after rotation
)
# Returns: { "rotation": CardinalDirection, "moved_to_tile": Vector2i }
Integration Examples
Example 1: Simple Isometric Building Rotation
extends Node2D
var building_object: Node2D
var tile_map: TileMapLayer
func _ready() -> void:
setup_building_with_rotation()
func setup_building_with_rotation() -> void:
# Create your building object
building_object = create_isometric_building()
add_child(building_object)
# Set initial position and rotation
building_object.global_position = Vector2(200, 200)
GBGridRotationUtils.set_node_direction(building_object, GBGridRotationUtils.CardinalDirection.NORTH, tile_map)
func _input(event: InputEvent) -> void:
if event is InputEventKey and event.pressed:
match event.keycode:
KEY_Q: # Rotate counter-clockwise
var new_direction = GBGridRotationUtils.rotate_node_counter_clockwise(building_object, tile_map)
print("Building rotated to: ", GBGridRotationUtils.direction_to_string(new_direction))
KEY_E: # Rotate clockwise
var new_direction = GBGridRotationUtils.rotate_node_clockwise(building_object, tile_map)
print("Building rotated to: ", GBGridRotationUtils.direction_to_string(new_direction))
Example 2: GridPositioner2D with Custom Rotation Target
extends Node2D
var positioner: GridPositioner2D
var preview_object: Node2D
func setup_custom_rotation_target() -> void:
# Create positioner with rotation enabled
positioner = GridPositioner2D.new()
add_child(positioner)
# Configure for rotation
var targeting_settings := GridTargetingSettings.new()
targeting_settings.enable_keyboard_input = true
targeting_settings.enable_rotation_input = true
# Create preview object that will rotate
preview_object = create_preview_object()
add_child(preview_object)
# Set preview as rotation target
var targeting_state := GridTargetingState.new(GBOwnerContext.new())
targeting_state.target = preview_object
# Inject dependencies
positioner._targeting_state = targeting_state
positioner._targeting_settings = targeting_settings
# ... other dependencies
# Override rotation target selection
func custom_get_rotation_target() -> Node2D:
if Input.is_action_pressed("select_building_mode"):
return preview_object # Rotate preview in building mode
else:
return positioner # Rotate positioner in other modes
Example 3: Programmatic Rotation with Animation
extends Node2D
func animate_rotation_to_direction(node: Node2D, target_direction: GBGridRotationUtils.CardinalDirection) -> void:
var current_rotation = rad_to_deg(node.rotation)
var target_rotation = GBGridRotationUtils.cardinal_to_degrees(target_direction)
# Ensure shortest rotation path
var rotation_diff = target_rotation - current_rotation
if rotation_diff > 180:
rotation_diff -= 360
elif rotation_diff < -180:
rotation_diff += 360
# Animate to target rotation
var tween = create_tween()
tween.tween_property(node, "rotation", deg_to_rad(current_rotation + rotation_diff), 0.3)
await tween.finished
# Snap to grid after animation
if tile_map:
GBGridRotationUtils._snap_node_to_grid(node, tile_map)
Isometric-Specific Features
The rotation system is designed with isometric games in mind:
- 90-degree increments - Perfect for isometric tile alignment
- Grid snapping - Maintains proper tile alignment after rotation
- Cardinal directions - North/East/South/West mapping works naturally with isometric layouts
- Direction deltas - Tile movement vectors that work with isometric coordinate systems
Isometric Direction Mapping
In isometric games, the cardinal directions typically map as follows:
- North: Up-left diagonal (towards top of diamond)
- East: Up-right diagonal (towards right point of diamond)
- South: Down-right diagonal (towards bottom of diamond)
- West: Down-left diagonal (towards left point of diamond)
The rotation utilities handle this naturally through the tile delta system and existing GBPositioning2DUtils integration.
Testing
The rotation system includes comprehensive unit tests:
# Run rotation utility tests
./run_tests_simple.sh godot/test/grid_placement_test/unit/gb_grid_rotation_utils_test.gd
Test coverage includes:
- Cardinal direction conversion (degrees ↔ enum)
- Clockwise/counter-clockwise rotation sequences
- Node rotation with grid alignment
- Direction utilities (opposite, horizontal/vertical classification)
- Tile delta calculations
Best Practices
- Always use grid alignment - Call rotation functions with a valid TileMapLayer for proper snapping
- Test rotation targets - Verify the target object exists before rotating
- Provide user feedback - Use direction_to_string() for user-friendly rotation state display
- Handle edge cases - Check for null objects and valid tile maps in your rotation logic
- Combine with existing systems - Leverage existing GBPositioning2DUtils for movement after rotation
Migration from Manual Rotation
If you have existing manual rotation code, you can easily migrate to the grid-aware system:
# Old manual rotation
object.rotation += PI/2 # 90 degrees
# New grid-aware rotation
var new_direction = GBGridRotationUtils.rotate_node_clockwise(object, tile_map)
The new system provides automatic grid alignment and consistent cardinal direction handling.
Troubleshooting
Common Issues
- Object not rotating: Check that
enable_rotation_inputis true in GridTargetingSettings - Rotation off-grid: Ensure you're passing a valid TileMapLayer to rotation functions
- Wrong rotation target: Verify targeting_state.target points to the desired object
- Input not detected: Confirm rotate_left and rotate_right actions are properly mapped
Debug Output
Enable debug logging to see rotation events:
# GridPositioner2D will log rotation events when trace logging is enabled
var debug_settings := GBDebugSettings.new()
debug_settings.enable_trace_logging = true
This will show rotation input detection and target selection in the console.
Source
docs/v5-0/guides/grid-aware-rotation-guide.md
Plugin docs root:gdscript/plugins/grid_placement_dev/docs