Chris' Tutorials
Docs/Grid Placement

Grid Placement v5.0.8

Grid-Aware Rotation

Implement grid-aware object rotation for isometric and cardinal-direction games.

StatusCurrent
Versionv5.0.8
UpdatedActive v5.0.8 guide line from Grid Placement repo
Source note:This page rendered plugin-owned docs or generated metadata inside unified Astro docs shell.

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:

  1. GBGridRotationUtils - Static utility class for grid-aligned rotation operations
  2. 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:

  1. 90-degree increments - Perfect for isometric tile alignment
  2. Grid snapping - Maintains proper tile alignment after rotation
  3. Cardinal directions - North/East/South/West mapping works naturally with isometric layouts
  4. 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

  1. Always use grid alignment - Call rotation functions with a valid TileMapLayer for proper snapping
  2. Test rotation targets - Verify the target object exists before rotating
  3. Provide user feedback - Use direction_to_string() for user-friendly rotation state display
  4. Handle edge cases - Check for null objects and valid tile maps in your rotation logic
  5. 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

  1. Object not rotating: Check that enable_rotation_input is true in GridTargetingSettings
  2. Rotation off-grid: Ensure you're passing a valid TileMapLayer to rotation functions
  3. Wrong rotation target: Verify targeting_state.target points to the desired object
  4. 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