Grid Placement can refund some or all of an object's or terrain entry's build cost when it is demolished. Your game owns the inventory; the plugin sends it the item id and amount to add.
For objects, set the cost through SpendMaterialsRuleById. For terrain, set
TilePlacementEntry.build_cost. Terrain demolition shares the refunder pipeline;
custom refunders receive a null object target for terrain.
Data flow
SpendMaterialsRuleById defines cost
→ BuildCost resolves that cost for the placed object
→ demolish reaches the pre-demolish/refund path
→ RefundCalculator applies refund_ratio
→ RefundService sends id + amount to your inventory bridgeUse one cost source for both spending and refunds so those values cannot drift.
For terrain, TilePlacementEntry.build_cost supplies the cost instead. A brush
demolish multiplies that cost by the number of erased cells.
Main types
| Type | Purpose |
|---|---|
SpendMaterialsRuleById |
Defines id-keyed object build costs. |
TilePlacementEntry |
Defines an optional terrain demolish cost. |
BuildCost |
Stores the item ids and quantities to refund. |
RefundCalculator |
Calculates refunded id→amount values. |
RefundService |
Applies refunds through the configured inventory bridge. |
Refunder |
Optional pre-demolish hook that can inspect/veto demolition. |
The current public service name is RefundService; old RefundSystem wording is legacy.
Inventory bridge
For refunds, the inventory bridge must implement try_add_by_id. When the
same inventory also handles placement costs, it must also implement
get_count_by_id and try_remove_by_id:
func get_count_by_id(id:StringName)-> int:
return GameInventory.get_item_count(id)
func try_remove_by_id(id:StringName, amount:int)-> int:
return GameInventory.remove_items(id, amount)
func try_add_by_id(id:StringName, amount:int)-> int:
return GameInventory.add_items(id, amount)Return the amount actually added or removed. RefundService.refund() returns
the amount the inventory accepted, so a capacity limit is visible to callers.
Configure refunds
In the standard host setup, GridPlacementHost creates and registers one
RefundService when manipulation is enabled. After the host configures its
session, set the service's ratio and a locator that finds the inventory under
PlacementOwner.owner_root:
func configure_refunds(host:GridPlacementHost)-> void:
var refund_service:RefundService = host.get_refund_service()
if refund_service== null:
push_error("Refunds require a configured manipulation service")
return
refund_service.refund_ratio= 0.5
refund_service.locator= NodeLocator.new(
NodeLocator.SEARCH_METHOD.NODE_NAME,
"Inventory",
)Call this after the host has configured its session. The example returns half
the cost; the default ratio is already 0.5.
Use a NodeLocator for split-screen: the host rebinds the service to each
session, while a direct container_override is cleared when the session changes.
Example policy:
build cost: 100 wood
refund_ratio: 0.5
refund: 50 woodDo not attach an independent refund service to every building; multiple handlers can refund the same demolition more than once.
Use RefundCalculator directly only when your game intentionally owns a custom demolish pipeline. Use a custom Refunder when demolition itself needs a veto/inspection step.
Common mistakes
- Spend and refund ids differ (
&"Wood"vs&"wood"). - Several handlers refund the same object.
- The inventory bridge cannot be resolved from the active placement owner/configuration.
- A custom runtime discount changes the paid cost but the placed object's resolved
BuildCostis not updated. - A custom refunder assumes the demolished target is non-null; terrain demolition passes
null.
Related guides
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.