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.