Skip to content

Grid Placement v6.0

Custom Placement Rules

Write your own placement rules for game-specific validation logic.

Status
Current
Version
v6.0
Source updated
Active v6.0 guide line from Grid Placement repo
Generated on
2026-09-01

Use a custom rule for game policy that Grid Placement cannot know on its own: costs, progression, ownership, reserved zones, or other project-specific facts.

Do not use custom rules to replace core occupancy, GRID mount, SMOOTH footprint, or 3D slope/support validation.

Pick the base class

Base Use when
PlacementRule The rule checks game/session state and does not need per-cell 2D indicators.
TileCheckRule The rule needs 2D tile/indicator positions.

Start with PlacementRule unless you specifically need 2D indicator cells.

Minimal rule

class_name MyRule
extends PlacementRule

func validate_placement()-> RuleResult:
    if _should_block_placement():
        return RuleResult.build(self, ["Placement is blocked"])
    return RuleResult.build(self, [])

An empty issue list means the rule passed.

Side effects belong after success

Validation can run repeatedly while the preview moves. Keep it read-only.

Use the supported apply/post-success lifecycle for irreversible effects such as:

  • spending materials;
  • updating progression;
  • recording committed placement analytics;
  • applying game state that should exist only after commit.

Do not mutate those systems from validate_placement().

Optional lifecycle methods

Use additional rule lifecycle methods only when the rule needs them:

  • setup(...) — resolve required context before validation;
  • apply() — successful-placement side effects;
  • tear_down() — clear temporary rule state;
  • setup/runtime/editor issue methods — report configuration/diagnostic problems.

When extending base behavior, preserve the base method contract unless you intentionally replace it.

2D indicator-specific rules

TileCheckRule is useful when different covered 2D cells need different feedback.

Override get_failing_indicators(...) only when you need per-indicator results. If the whole placement has one answer, the default validation result is simpler and less error-prone.

When threaded 2D physics is enabled, prefer the plugin's cached indicator collision data instead of performing ad-hoc physics queries from rule code outside the physics tick.

Example: game-owned economy

class_name CreditPlacementRule
extends PlacementRule

@export var required_credits:= 100

func validate_placement()-> RuleResult:
    var economy:= _resolve_economy()
    if economy== null:
        return RuleResult.build(self, ["Economy service is unavailable"])
    if economy.credits< required_credits:
        return RuleResult.build(self, ["Not enough credits"])
    return RuleResult.build(self, [])

func apply()-> Array[String]:
    var economy:= _resolve_economy()
    if economy!= null:
        economy.credits-= required_credits
    return []

The important part is the lifecycle: check during validation, spend only after success.

For common material costs, prefer the built-in id-keyed cost rule instead of writing a custom economy rule from scratch.

Example: 2D cell restriction

A TileCheckRule can reject cells outside a game-defined 2D area or inspect TileMap cell data. Keep the rule focused on the project-specific restriction; bounds/collision behavior already provided by Grid Placement should stay in the core/built-in path.

For game-owned occupancy/reservation data, prefer Placement World Facts Provider over hard-coding a game registry directly into the addon.

Where to attach a rule

Location Best use
PlacementSettings.placement_rules Shared/default project rule.
PlacementProfile.placement_rules Category/profile behavior.
ScenePlacementEntry.placement_rules One placeable's unique requirement.

While developing a rule, attach it to one entry first. Promote it only after its intended scope is clear.

Failure messages

Return a short reason that helps the developer/player act:

Good: "Requires 10 wood", "Reserved construction zone", "Must be near water".

Avoid: "failed", raw internal object dumps, or messages that expose implementation details without a useful correction.

Test every rule

At minimum verify:

  • allowed case;
  • blocked case and issue text;
  • missing required context;
  • repeated preview validation causes no side effects;
  • successful commit applies side effects exactly once.

For TileCheckRule, also verify the expected per-cell/indicator behavior.