Skip to content

Grid Placement v6.3.0

Custom Placement Rules

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

Status
Current
Version
v6.3.0
Source updated
2026-10-29
Generated on
2026-10-29

6.3. These pages document the 6.3 release, where placeable is now entry (what changed) and PlacementOwner is deprecated (migration). Still on 6.2? Your version's guides ship inside the plugin itself, under addons/grid_placement/guides/; the 6.2 URLs on this site now redirect to this series.

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.
PlacementCellPolicy3D A 3D rule needs the mount cell, the footprint cells or the preview yaw.

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

PlacementCellPolicy3D is the only base that receives 3D placement geometry, so it is the right tool for "one row of depth", "one facing" or "not in this reserved band". A PlacementRule cannot answer those: it never sees the mount cell, the footprint columns or the preview yaw.

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.

Example: 3D cell policy (one row, one facing)

PlacementCellPolicy3D receives what only a 3D attempt knows:

  • p_cell — the mount cell; its y is the storey;
  • p_cells — the footprint's columns, flattened to y = 0;
  • p_context — committed mounts, the excluded node of a move, and preview_yaw_steps.

Return an empty string to let the plugin decide, or one sentence to refuse. That sentence is what the player reads in the preview, on commit, and on the counterfactual a move asks.

The demo project carries a working one: demos/3d/rooms/row_band_policy.gd — a row band plus one fixed facing. Copy the file into your project, change the numbers, attach it to ScenePlacementEntry.cell_policy_3d. It is inert — no class_name, no scene reference — so nothing in the addon depends on it.

extends PlacementCellPolicy3D

@export var first_row:int = 0
@export var last_row:int = 0
@export var facing_yaw_steps:int = 0

func issue_for_cells(
        _p_cell:Vector3i,
        p_cells:Array[Vector3i],
        _p_entry:ScenePlacementEntry,
        p_context:PlacementValidation3D.PlacementValidationContext3D)-> String:
    if p_context.preview_yaw_steps% 4 != facing_yaw_steps% 4:
        return "This piece has one facing; rotate the preview back."
    for cell:Vector3i in p_cells:
        if cell.z< first_rowor cell.z> last_row:
            return "Only rows%d to%d can hold this piece." % [first_row, last_row]
    return ""

Keep it read-only. The policy may consult the registry through p_context.mounts, but the service stays the only writer, so nothing it does can desync the world.

A game-authored rule answers questions like "this piece may only stand in these rows, facing this way". The plugin ships no such rule and attaches none to its own demos: the demo project keeps one next to its rooms as a starting point, and no entry uses it.

Note what is not on that list any more: "one room per storey". The plugin owns that itself — 3D occupancy is booked per storey cell, so a second piece on an occupied row is refused while the same column one row up stays free. A policy that re-implements it would only duplicate the plugin's own refusal. Use the policy seam for rules the plugin cannot own: where a piece may stand, which facing it may take, which band is reserved.

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 entry'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.

Using this in a game?

Grid Placement v6.3.0 — full source, playable demos, commercial license

The guide above is the same setup the shipped demos use. Try the build in your browser before you buy it.