Display action feedback
When to use
Display action feedback when a preview or committed result should highlight affected or invalid cells. Visual feedback is presentation state: it does not mutate the board or advance its revision.
Before you start
Choose the logical request in Choose a built-in action. Connect a GridBoardView to a ready renderer with a compatible intent renderer.
Procedure
Publish preview feedback
Preview the action, then give the same action and result to the view's GridVisualState.
Excerpt — publish a preview result. Tested using public APIs.
GridActionResult preview = GridActionRunner.Preview(state, action);
boardView.VisualState.SetActionPreview(action, preview);
Apply and replace the channel
Apply only after a successful preview. SetActionResult replaces the action feedback channel with
the committed result.
Excerpt — apply and publish the result. Tested using public APIs.
GridActionResult applied = GridActionRunner.Apply(state, action);
boardView.VisualState.SetActionResult(action, applied);
Choose the feedback route
| Feedback | Use |
|---|---|
| Generic affected or invalid cells | SetActionPreview or SetActionResult |
| Placement footprint | SetPlacementPreview |
| Inventory move, rotate, sort, transfer, or merge | Inventory Visualization bridge |
| Project-specific semantics | IGridActionVisualIntentContributor |
A custom contributor that returns no valid intents falls back to generic action feedback. Inventory quantity changes also use generic action feedback because Inventory has no quantity-specific intent ID.
GridVisualIntentIds defines the Stable built-in intent and channel IDs:
| Purpose | Intent ID | Channel ID |
|---|---|---|
| Hover | grid.hover |
grid.visual.channel.hover |
| Selection | grid.selection |
grid.visual.channel.selection |
| Valid placement | grid.placement.valid |
grid.visual.channel.placement |
| Invalid placement | grid.placement.invalid |
grid.visual.channel.placement |
| Affected action cells | grid.action.affected |
grid.visual.channel.action-affected |
| Invalid action cells | grid.action.invalid |
grid.visual.channel.action-invalid |
| Project overlay | grid.overlay |
Project-owned channel |
GridInventoryVisualIntentIds defines these Preview intent IDs:
| Operation | Accepted intent | Rejected intent |
|---|---|---|
| Move | grid.inventory.move |
grid.inventory.move.invalid |
| Transfer | grid.inventory.transfer |
grid.inventory.transfer.invalid |
| Merge | grid.inventory.merge |
grid.inventory.merge.invalid |
| Rotate | grid.inventory.rotate |
grid.inventory.rotate.invalid |
| Sort | grid.inventory.sort |
grid.inventory.sort.invalid |
The current Inventory bridge emits grid.inventory.move, grid.inventory.rotate, and
grid.inventory.sort channels for same-board actions.
Cross-board operations use one channel per participant:
- transfers use
grid.inventory.transfer.sourceandgrid.inventory.transfer.destination; - merges use
grid.inventory.merge.sourceandgrid.inventory.merge.destination.
These channel strings are current Preview-tier behavior rather than Stable built-in constants.
Clear temporary feedback
Excerpt — clear the action-feedback channels. Tested using public APIs.
boardView ??= GetComponent<GridBoardView>();
if (boardView == null)
return;
boardView.VisualState.ClearIntentChannel(
GridVisualIntentIds.ActionAffectedChannel);
boardView.VisualState.ClearIntentChannel(
GridVisualIntentIds.ActionInvalidChannel);
Complete tested example
using System;
using LostLily.GridToolkit.Actions;
using LostLily.GridToolkit.Authoring;
using LostLily.GridToolkit.Board;
using LostLily.GridToolkit.Topology.Square;
using LostLily.GridToolkit.Visualization;
using UnityEngine;
namespace GridToolkit.Tutorials
{
[RequireComponent(typeof(GridBoardAuthoring), typeof(GridBoardView))]
public sealed class ActionFeedbackExample : MonoBehaviour
{
[SerializeField] private GridBoardAuthoring boardAuthoring;
[SerializeField] private GridBoardView boardView;
public (
bool PreviewSucceeded,
bool ApplySucceeded,
long RevisionBeforePreview,
long RevisionAfterPreview,
long RevisionAfterApply,
int BatchesAfterPreview,
int CommittedBatchCount,
bool CellExists,
bool FeedbackVisible) LastReport { get; private set; }
private void Start()
{
LastReport = Run();
}
public (
bool PreviewSucceeded,
bool ApplySucceeded,
long RevisionBeforePreview,
long RevisionAfterPreview,
long RevisionAfterApply,
int BatchesAfterPreview,
int CommittedBatchCount,
bool CellExists,
bool FeedbackVisible) Run()
{
return Run(new SquareGridCoordinate(8, 0));
}
public (
bool PreviewSucceeded,
bool ApplySucceeded,
long RevisionBeforePreview,
long RevisionAfterPreview,
long RevisionAfterApply,
int BatchesAfterPreview,
int CommittedBatchCount,
bool CellExists,
bool FeedbackVisible) Run(SquareGridCoordinate coordinate)
{
boardAuthoring ??= GetComponent<GridBoardAuthoring>();
boardView ??= GetComponent<GridBoardView>();
if (boardAuthoring == null || boardAuthoring.State == null
|| boardView == null)
{
Debug.LogError(
"Action feedback needs a ready GridBoardAuthoring and GridBoardView.",
this);
return default;
}
GridBoardState state = boardAuthoring.State;
IGridAction action = GridActions.AddCell(coordinate);
long revisionBeforePreview = state.Revision;
int batchCount = 0;
void CountBatch(GridBoardEventBatch _) => batchCount++;
state.EventsCommitted += CountBatch;
try
{
GridActionResult preview = GridActionRunner.Preview(state, action);
boardView.VisualState.SetActionPreview(action, preview);
long revisionAfterPreview = state.Revision;
int batchesAfterPreview = batchCount;
if (!preview.Success)
{
Debug.LogError(preview.FormatDiagnostics(), this);
LastReport = (
false,
false,
revisionBeforePreview,
revisionAfterPreview,
state.Revision,
batchesAfterPreview,
batchCount,
state.ContainsCell(coordinate),
HasFeedback());
return LastReport;
}
GridActionResult applied = GridActionRunner.Apply(state, action);
boardView.VisualState.SetActionResult(action, applied);
if (!applied.Success)
Debug.LogError(applied.FormatDiagnostics(), this);
LastReport = (
true,
applied.Success,
revisionBeforePreview,
revisionAfterPreview,
state.Revision,
batchesAfterPreview,
batchCount,
state.ContainsCell(coordinate),
HasFeedback());
Debug.Log(
$"Action feedback: revision {revisionBeforePreview} -> "
+ $"{LastReport.RevisionAfterApply}, batches "
+ $"{LastReport.CommittedBatchCount}, cell {coordinate} exists: "
+ $"{LastReport.CellExists}.",
this);
return LastReport;
}
finally
{
state.EventsCommitted -= CountBatch;
}
}
public void ClearFeedback()
{
boardView ??= GetComponent<GridBoardView>();
if (boardView == null)
return;
boardView.VisualState.ClearIntentChannel(
GridVisualIntentIds.ActionAffectedChannel);
boardView.VisualState.ClearIntentChannel(
GridVisualIntentIds.ActionInvalidChannel);
}
private bool HasFeedback()
{
return boardView.VisualState.TryGetIntentChannel(
GridVisualIntentIds.ActionAffectedChannel,
out _)
|| boardView.VisualState.TryGetIntentChannel(
GridVisualIntentIds.ActionInvalidChannel,
out _);
}
}
}
Result
Preview feedback appears without a commit. Apply publishes the committed result, and the renderer keeps each feedback node under a stable key that includes its owning channel. Replacing or clearing that channel updates or removes only its own retained nodes.
Troubleshooting
| Symptom | Check |
|---|---|
| The action succeeds but nothing appears | Assign an intent renderer whose required capabilities match the active backend. |
| Old feedback remains visible | Clear the exact owner channel or the relevant transient visual category. |
| Custom feedback disappears | Return at least one valid intent and keep its channel ID stable. |