Use this guide when Grid Placement works, but your game needs extra placement truth from the world: occupied cells, reserved build zones, terrain restrictions, custom collision data, or facts from another system.
Version note: This guide is for Grid Placement 6.0.0 on the 2D
TileMapLayerpath. The provider described here is intentionally source-agnostic. It can be fed by Godot Physics, a project-owned occupancy grid, Avian 2D integration code, hand-authored data, or a test stub.
Beginner Mental Model
Grid Placement should answer:
What is the player trying to place?
Where is the target cell?
Which placement rules should run?
Should the preview/commit pass or fail?Your game should answer:
What does my world know about this cell?
Is this cell blocked by occupancy, terrain, reserved zones, or game rules?
What short reason should be shown when it is blocked?The bridge between those two questions is PlacementWorldFactsProvider2D.
Think of it as a small adapter object:
your game world / project data
→ PlacementWorldFactsProvider2D
→ ProviderCellBlockRule2D
→ Grid Placement preview and validationThis keeps Grid Placement reusable. The plugin does not need to know whether your collision truth comes from Godot Physics, Avian 2D, a project-owned occupancy grid, a save file, or a dictionary in a test.
When You Need This
You probably need a world facts provider when:
- Existing collision rules are not enough for your game.
- Your real occupancy data lives outside Godot collision objects.
- Another system in your project decides whether a cell is blocked.
- You want the same truth source for preview, validation, and placement reports.
- You are integrating a game-specific data source without making the plugin depend on that source.
You probably do not need this for your first object placement. Start with Getting Started, make a simple object place successfully, then add a provider only when you have game-specific world facts to expose.
The Two Pieces
| Piece | Lives in | Job |
|---|---|---|
PlacementWorldFactsProvider2D |
Your project or plugin setup | Reports whether a cell is blocked and why. |
ProviderCellBlockRule2D |
Placement rules pipeline | Reads the provider and turns blocked cells into validation issues. |
PlacementWorldFactsProvider2D is permissive by default. If you do nothing, it blocks nothing. That keeps existing projects working.
Minimal Provider Example
Create a script in your game, for example res://placement/my_world_facts_provider.gd:
class_name MyWorldFactsProvider
extends PlacementWorldFactsProvider2D
var blocked_cells:Dictionary = {}
func block_cell(cell:Vector2i, reason:String)-> void:
blocked_cells[cell]= reason
func is_cell_blocked(cell:Vector2i)-> bool:
return blocked_cells.has(cell)
func get_block_reason(cell:Vector2i)-> String:
return String(blocked_cells.get(cell,""))This example uses a dictionary so the idea is easy to see. A real game might query a physics server, an occupancy grid, a building registry, or a simulation bridge instead.
Connecting the Provider to a Session
The provider is stored on GridTargetingState:
var provider:= MyWorldFactsProvider.new()
provider.block_cell(Vector2i(10,4),"occupied by town hall")
session.get_states().targeting.world_facts_provider_2d= providerIn an editor-composed scene, the exact place you assign this depends on how your scene is wired. The important part is that the active PlacementSession uses the GridTargetingState that has your provider assigned.
Beginner check:
print(session.get_states().targeting.world_facts_provider_2d)If this prints <null>, the rule has no provider to ask and will not block anything.
Adding the Rule
Add ProviderCellBlockRule2D to the rule list that should use these facts.
Common places:
| Location | Use when |
|---|---|
PlacementSettings.placement_rules |
Every placement should respect the provider. |
PlacementProfile.placement_rules |
Only a category, such as buildings, should use it. |
ScenePlacementEntry.placement_rules |
Only one specific placeable should use it. |
For beginners, start on one ScenePlacementEntry. After you confirm it works, move it to a profile or global settings if it should apply broadly.
What the Player Sees
When the provider blocks a cell, ProviderCellBlockRule2D returns an issue like:
Placement blocked at cell (10, 4): occupied by town hallYour UI should display placement issues from the placement report/action data. Do not duplicate the same provider check in UI code. The rule result is the source of truth for the preview and the final commit.
Example: Querying a Game Occupancy Registry
This pattern is useful when your game already tracks placed buildings by cell:
class_name BuildingRegistryFactsProvider
extends PlacementWorldFactsProvider2D
var building_registry:Node
func is_cell_blocked(cell:Vector2i)-> bool:
if building_registry== null:
return false
return building_registry.has_building_at(cell)
func get_block_reason(cell:Vector2i)-> String:
if is_cell_blocked(cell):
return "occupied by another building"
return ""This avoids making Grid Placement depend on your BuildingRegistry class. The dependency stays in your game-owned adapter.
Example: Reading Cell Facts from Another System
If another part of your project owns collision truth, keep that source hidden behind the provider:
class_name ProjectWorldFactsProvider
extends PlacementWorldFactsProvider2D
var world_state:Node
func is_cell_blocked(cell:Vector2i)-> bool:
if world_state== null:
return false
if world_state.has_method("is_cell_blocked"):
return bool(world_state.call("is_cell_blocked", cell.x, cell.y))
return false
func get_block_reason(cell:Vector2i)-> String:
if is_cell_blocked(cell):
return "blocked by world state"
return ""The important design rule is: put source-specific calls in your provider, not inside Grid Placement rules or UI scripts.
Possible sources include external physics engines, server-authoritative worlds, deterministic simulations, ECS storage, or test stubs. Use whichever your project already owns.
Beginner Checklist
- First placement works without a custom provider.
- Your provider returns
truefromis_cell_blocked(cell)for a known blocked cell. - Your provider returns a short, readable
get_block_reason(cell)string. - The active session's
targeting.world_facts_provider_2dpoints to your provider. -
ProviderCellBlockRule2Dis attached to the relevant settings/profile/entry. - Your UI shows
PlacementReportissues instead of repeating provider logic. - You test both a blocked cell and an allowed cell.
Common Mistakes
| Symptom | Likely cause |
|---|---|
| Provider rule never blocks anything | world_facts_provider_2d is not assigned on the active session. |
| Preview is red but commit succeeds | UI or preview uses custom logic instead of the real rule path. |
| Commit fails but message is vague | get_block_reason() returns an empty or unclear string. |
| Only some objects use the provider | Rule is attached to one entry/profile, not global settings. |
| Game-specific classes leak into plugin scripts | Backend calls were put in plugin code instead of the provider. |
Related Guides
- Getting Started
- Placement Workflow
- Placement Rules
- Custom Placement Rules
- Targeting Flow
- Troubleshooting