- name
- f8-features-ui-workflow
- description
- Use when creating, modifying, opening, closing, animating, binding, loading, debugging, or validating F8Framework UI views and items, including UIManager layers, BaseView/BaseItem templates, ComponentBind generation, canvas setup, Notify/Dialog behavior, and UI resource cleanup.
# F8 UI Feature Workflow
## Mission and operating rules
Use this skill as the runtime source of truth for the F8Framework UI feature flow. Make the workflow executable by another agent: identify the correct source files, preserve generator-owned code, implement the smallest change, and verify the complete open/close path.
When receiving a UI task, follow this order:
1. Classify the request as runtime behavior, editor authoring, generated code, or a combination.
2. Read the relevant source files listed below before editing. Treat current source as authoritative over README examples.
3. Check the bootstrap gate and the asset key before diagnosing a UI symptom.
4. Decide the layer and lifecycle (`BaseView` or `BaseItem`) before writing code.
5. Keep generated regions intact and regenerate them from the editor when the prefab hierarchy changes.
6. Validate both the first activation and a cached re-open; test destruction separately when `isDestroy` is involved.
7. Report the UIID, asset key, layer, lifecycle hooks, binding mode, cleanup behavior, tests run, and remaining risks.
Do not silently invent a new UI manager, layer, prefab-loading path, or binding convention. If the requested behavior conflicts with the implementation, show the conflict and route the change to the owning source file.
## Scope, paths, and routing
Prefer `Assets/F8Framework`. If the project is package-based, apply the same relative paths below under `Packages/F8Framework`.
| Area | Source of truth |
| --- | --- |
| UI manager and public API | `Assets/F8Framework/Runtime/UI/UIManager.cs` |
| View lifecycle and tracked resources | `Assets/F8Framework/Runtime/UI/Base/BaseView.cs` |
| Item lifecycle and tracked resources | `Assets/F8Framework/Runtime/UI/Base/BaseItem.cs` |
| Async handle | `Assets/F8Framework/Runtime/UI/Base/UILoader.cs` |
| Callback/state records | `Assets/F8Framework/Runtime/UI/ViewParams.cs` and `DelegateComponent.cs` |
| Layer behavior | `Assets/F8Framework/Runtime/UI/Layer/` |
| Binding generator | `Assets/F8Framework/Runtime/ComponentBind/ComponentBind.cs` |
| Binding name map | `Assets/F8Framework/Runtime/ComponentBind/DefaultCodeBindNameTypeConfig.cs` |
| Binding inspector | `Assets/F8Framework/Editor/ComponentBind/ComponentBindEditor.cs` |
| Script templates and creation menu | `Assets/F8Framework/Editor/UI/BaseViewTemplate.cs.txt`, `BaseItemTemplate.cs.txt`, `CreateUIScript.cs` |
| UI editor utilities | `Assets/F8Framework/Editor/UI/` |
| Usage examples | `Assets/F8Framework/Tests/UI/README.md`, `README_EN.md`, and `DemoUIManager.cs` |
Route related work as follows:
- Bootstrap or module-order questions: `f8-foundation-bootstrap-workflow`.
- Prefab/resource keys, Resources/AssetBundle mode, or unloading details: `f8-features-assetmanager-workflow`.
- Open/close tweens and sequences: `f8-features-tween-workflow`.
- Event ownership and global messages: `f8-features-event-workflow`.
- Component naming or Inspector generation: `f8-editor-componentbind-workflow`.
- Template creation, atlas/slicing, image tools, and UI editor menus: `f8-editor-ui-workflow`.
- Localization components or the Chinese-text collector: `f8-features-localization-workflow`.
## Bootstrap gate
Do not debug `FF8.UI` before confirming that F8 has been initialized. The minimum valid order is:
```csharp
IEnumerator Start()
{
ModuleCenter.Initialize(this); // required first
FF8.Asset = ModuleCenter.CreateModule<AssetManager>();
// The standard launcher loads AssetBundleManifest here when AB mode is used.
FF8.Tween = ModuleCenter.CreateModule<Tween>(); // required before UI tween code
FF8.UI = ModuleCenter.CreateModule<UIManager>();
FF8.UI.Initialize(configs); // creates the six UI layers
}
```
The project launcher creates the other dependency modules and awaits `AssetBundleManager.Instance.LoadAssetBundleManifest()` after `AssetManager` and before later modules. Preserve that order; use the bootstrap skill for the complete launcher sequence. `MessageManager`, `TimerManager`, `InputManager`, and `Tween` must exist before code that uses their APIs.
`ModuleCenter.CreateModule<UIManager>()` invokes `UIManager.OnInit`. If no `EventSystem.current` exists, `OnInit` creates an `EventSystem` and the input module selected by the Unity compile symbols. Still verify that the scene has the expected input setup.
`UIManager.Initialize(...)` builds `LayerGame`, `LayerUI`, `LayerPopUp`, `LayerDialog`, `LayerNotify`, and `LayerGuide`. Call it once after creating a manager and before any `Open` call. If the manager is terminated and recreated, initialize the new instance again; do not repeatedly initialize the same live manager just to change one config, because initialization creates new layer objects.
Press F8 before relying on AssetBundle indexes or generated config data. In development, follow the AssetManager skill when `IsEditorMode` is required.
## Configuration and layer selection
`UIConfig` contains only a `LayerType` and an AssetManager `AssetName`. The key can be an enum or an `int`; enum overloads are converted to `int` internally.
```csharp
public enum UIID
{
Main = 1,
Notify = 2,
}
private readonly Dictionary<UIID, UIConfig> configs = new Dictionary<UIID, UIConfig>
{
{ UIID.Main, new UIConfig(LayerType.UI, "Main") },
{ UIID.Notify, new UIConfig(LayerType.Notify, "Notify") },
};
```
Use the exact asset key recognized by AssetManager. Do not add a `.prefab` suffix or a new path convention unless that key is how the project indexes the asset.
| Layer | Default sort order | Select it for | Important behavior |
| --- | ---: | --- | --- |
| `Game` | 100 | Game-world or game HUD bucket | Standard `LayerUI` behavior. |
| `UI` | 200 | Normal screens, menus, HUD panels | One active/loading view per prefab path. |
| `PopUp` | 300 | A separate standard popup bucket | No special queue logic in the current implementation. |
| `Dialog` | 400 | Modal dialogs that must serialize | One queue per `uiId`; only the oldest is shown. |
| `Notify` | 500 | Toasts/notifications that may coexist | Multiple instances are allowed; close by `Guid` for one instance. |
| `Guide` | 600 | Tutorial/guide bucket | Standard `LayerUI` behavior. |
For normal layers, opening an already active or loading prefab is a duplicate: synchronous `Open` usually returns `string.Empty`, while asynchronous `OpenAsync` returns the existing loader. `isDestroy = false` hides and caches the instance; `isDestroy = true` destroys it and releases the prefab load. A cached instance is not active, so `Has` only reports currently active views.
Normal-layer uniqueness is keyed by `UIConfig.AssetName` (the prefab path), not only by `uiId`. Do not register the same prefab under multiple normal-layer IDs unless sharing one instance is intentional. A direct `Close` while a view is still pending its first async load is ignored by the current layer implementation; `Clear(...)` is the API that cancels pending loads. A view in `Closing` state is also treated as a duplicate, so wait for the close tween/removal to finish before reopening the same prefab.
`Dialog` requests with the same `uiId` are queued. The oldest request is displayed; after it closes, the next request is shown after one frame. Do not implement a second queue in the view script. For a queued asynchronous request, wait for its own `UILoader` and then verify that its view is actually active.
For a queued synchronous `Dialog` request, the returned guid identifies the currently shown or cached view rather than a newly instantiated queued object. Use the callbacks or an async loader when the caller must track an individual queued request.
`Notify` can show the same prefab more than once. Each active instance has a unique `Guid`; call `FF8.UI.Close(uiId, isDestroy, guid)` or `BaseView.Close(...)` when closing one notification. Omitting the guid closes instances for that prefab according to the layer implementation.
## End-to-end workflow for a new view
Execute these steps in order and stop to fix the first failed gate:
1. Prepare a prefab under a loadable location (`AssetBundles` for the normal production path or `Resources` for prototyping). Confirm its AssetManager key and press F8 when indexes are stale.
2. Select the project folder in Unity and use `Assets/(F8UI界面管理功能)/(BaseView.cs)` to create a view script, or `(BaseItem.cs)` for a reusable list/card item. Attach the generated script to the prefab root (not to a nested visual child).
3. Keep the generated class name, namespace, and file name compilable. A `BaseView` root represents one UIManager-managed prefab; a `BaseItem` root represents a reusable child item.
4. Add a stable `UIID` and a matching `UIConfig`. Choose the layer from the table above; keep the asset key identical to the prefab key used by AssetManager.
5. Build the hierarchy and name bindable children using the ComponentBind contract below. Put a `BaseItem` subtree behind its own binding boundary.
6. On the root component, click the convention or all-components binding button. The Inspector labels are `按照约定名称UI组件绑定(需要点击两次)` and `搜索全部UI组件绑定(需要点击两次)`; click again if Unity has not refreshed the generated script.
7. Wait for compilation and inspect the generated fields, listener override, and `SetComponents` assignments. If a field is null, fix the hierarchy/name and regenerate; do not hand-patch the generated block.
8. Initialize UI once, then configure Canvas and CanvasScaler if the defaults are not suitable. Configure them before opening views.
9. Use `Open` for an immediate synchronous path or `OpenAsync` when loading may take time. Pass view data through `object[]` and use callbacks only for external orchestration.
10. Implement lifecycle logic in the appropriate hook. Test first activation, close, cached reopen, and destroy/recreate before calling the feature complete.
For a change to an existing view, first identify whether the change belongs in the prefab, the hand-written region of the view script, the template, or `ComponentBind.cs`. Regenerate after hierarchy changes and keep the generated region reproducible.
## AI code-generation protocol
Use this protocol whenever an AI agent is asked to create or extend a UI script. Separate authored behavior from Unity-generated bindings:
| Responsibility | Owner | Required action |
| --- | --- | --- |
| Script skeleton and lifecycle hooks | Unity template + AI | Create from the BaseView/BaseItem template when possible; then fill only the hand-written hooks. |
| Prefab hierarchy and component placement | Unity Editor/user | Create or inspect the prefab and attach the root script. Do not fabricate hierarchy facts from a screenshot-free prompt. |
| Serialized component fields, paths, arrays, and control listeners | `ComponentBind` | Name the objects, click the binding button, and let the generator write the delimited region. |
| UI behavior and data flow | AI | Implement `OnAdded`, `Refresh`, `ButtonClick`, `ValueChange`, tween hooks, callbacks, and cleanup outside the generated region. |
| Compile and prefab-reference verification | Unity Editor | Refresh/compile, inspect the generated fields, and run the first-open/reopen test. |
Follow these AI steps:
1. Inspect the target prefab, existing script, UIID/config, asset key, and selected layer. If the prefab hierarchy is unavailable, state that binding cannot be verified instead of guessing field names.
2. Choose `BaseView` for a UIManager-managed panel or `BaseItem` for a reusable child. Use the exact template menu/path or mirror the current template hook surface when editor automation is unavailable.
3. Preserve the marker pair in the new script. Use `// Auto-generated component bindings below` for new code; accept `// 自动获取组件(自动生成,不能删除)` in existing code. Never place hand-written logic between the markers.
4. Build a binding plan from actual child names and components. Record expected fields, including TMP legacy/type choices and array indices, before asking Unity to bind.
5. Invoke the Inspector binding action (`Bind` or `BindAllUIComponents`) and wait for Unity refresh/compile. Click twice when the Inspector label requires it. Read the generated fields and listener override back from the script; do not infer them from the plan.
6. Generate behavior against the fields that actually exist. Prefer generated `ButtonClick`/`ValueChange` callbacks; make `OnAdded` and `Refresh` idempotent for cached reuse; reset visual state in `OnViewTweenInit` when close tweens are used.
7. Keep all custom state, API calls, event ownership, and teardown outside the generated region. If a generator limitation is found, fix the template/config/generator source and regenerate rather than patching one view's generated code.
8. Validate the script in Unity: compile, open synchronously or asynchronously, verify `GetByGuid`/`Has`, exercise close/reopen/destroy, and inspect the Console. If Unity is unavailable, report editor validation as pending.
When handing off an AI-generated UI script, report: script path/class/base type, UIID and asset key, layer, prefab binding mode, actual generated field names, lifecycle hooks implemented, close/cache policy, explicit cleanup points, and editor/runtime checks performed.
Do not hand-write `[SerializeField]` references that `ComponentBind` can generate, do not invent `transform.Find` paths, and do not edit a generated listener or `SetComponents` method to make a single prefab compile. The generated output must be reproducible from the prefab hierarchy.
## ComponentBind contract
### Modes and hierarchy boundaries
- `Bind()` is convention mode. It scans descendants, but if a descendant has `BaseItem` (or a subclass), it does not traverse that item's children. Bind the item with its own script.
- `BindAllUIComponents()` scans configured component types on every scanned child, excluding `GameObject` and `Transform` from all-components mode.
- The component must be attached to the same GameObject as the name match; the generator does not search up or down for the component.
- Both modes generate serialized fields, reference assignments in `SetComponents()`, and listener code for supported interactive controls.
### Naming rules
The current map is `DefaultCodeBindNameTypeConfig.BindNameTypeDict`. In convention mode, split the GameObject name on `_`; each segment is compared with mapping keys using case-sensitive `Contains`. For example, `Title_Text`, `Confirm_btn`, and `Btn_Button_Image` are meaningful conventions only when the corresponding components are on that same object.
The generated field is the normalized child name plus `_` plus the matched mapping key. Normalization keeps ASCII letters, digits, underscores, and Chinese characters; it removes spaces, brackets, parentheses, and other punctuation; a leading digit receives `_`. Collisions receive `_2`, `_3`, and so on. Renaming a child changes the field name and requires regeneration plus compile.
In all-components mode, the suffix is derived from the component type name (for example, `TMP_Text` is normalized to `TMPText`) rather than from the GameObject's alias. In convention mode, the suffix is the matched map key.
The underscore in an alias is significant: the generator splits names on `_` before matching. Therefore a natural name such as `Search_TMP_InputField` becomes the segments `TMP` and `InputField` and does not match the configured `TMP_InputField` key. For a TMP input in convention mode, use one unsplit key such as `Search_InputField (TMP)` (with the component on that object), add a project alias without `_`, or use all-components mode. Apply the same check to any custom alias containing `_`.
Common current keys include:
| Component family | Keys/aliases |
| --- | --- |
| Object and transform | `GameObject`, `go`, `Transform`, `tf`, `RectTransform` |
| Basic UI | `Button`, `btn`, `Image`, `img`, `RawImage`, `rimg`, `Text`, `Text (Legacy)`, `txt` |
| Input/value controls | `InputField`, `InputField (Legacy)`, `Slider`, `Toggle`, `ToggleGroup`, `Scrollbar`, `ScrollRect` |
| TextMeshPro | `Text (TMP)`, `tmp`, `TextMeshProUGUI` |
| TMP controls | `Dropdown`, `TMP_Dropdown`, `InputField (TMP)`, `TMP_InputField` |
| Layout and rendering | `VerticalLayoutGroup`, `HorizontalLayoutGroup`, `GridLayoutGroup`, `Canvas`, `CanvasGroup`, `Animator`, `Animation`, `SpriteRenderer`, `Mask`, `RectMask2D` |
The map distinguishes legacy and TMP controls by the exact key. In particular, `Dropdown`/`TMP_Dropdown` bind `TMPro.TMP_Dropdown`, while `Dropdown (Legacy)` binds `UnityEngine.UI.Dropdown`; `InputField`/`InputField (Legacy)` are legacy and `InputField (TMP)`/`TMP_InputField` are TMP.
Add project-specific aliases to `DefaultCodeBindNameTypeConfig.cs`; do not duplicate the map inside a view script. Keep aliases unambiguous because matching is substring-based and case-sensitive.
### Arrays and generated listeners
Use a trailing numeric index such as `Item_Image[0]`, `Item_Image[1]`. The generator groups the indexed paths and allocates an array of `maxIndex + 1`; use contiguous indices to avoid null slots. The current generator also emits its normal per-child field before the array aggregation, so inspect the generated block instead of assuming that only one array field exists. Regenerate after adding, removing, or reordering indexed children and inspect the resulting field names.
The generator creates listener fields and an `OnAddUIComponentListener()` override for `Button`, `Slider`, `Scrollbar`, legacy/TMP `Dropdown`, `Toggle`, legacy/TMP `InputField`, and `ScrollRect`. Buttons call `ButtonClick(UIBehaviour)`; value controls call `ValueChange<T>(UIBehaviour, T)`. Do not add a second override with the same name. Put custom behavior in `ButtonClick`/`ValueChange` or outside the generated region.
Prefer the generated callback contract for a TMP input instead of adding another `onValueChanged` listener:
```csharp
protected override void ValueChange<T>(UIBehaviour ui, T value)
{
if (ui is TMPro.TMP_InputField && value is string text)
{
// React to the generated listener once; do not subscribe again in OnAdded.
}
}
```
### Generated markers and compatibility
The canonical delimiter in new templates is exactly:
```csharp
// Auto-generated component bindings below
// ... generated fields, listeners, and SetComponents ...
// Auto-generated component bindings below
```
The generator also accepts the legacy delimiter exactly as written below:
```csharp
// 自动获取组件(自动生成,不能删除)
// ... old generated region ...
// 自动获取组件(自动生成,不能删除)
```
`ComponentBind.cs` matches either pair and writes the English marker back when regenerating. The two identical marker comments in the templates are intentional start/end delimiters; do not remove one, translate it, or “deduplicate” it. If neither pair exists, generation logs that the insertion marker cannot be found.
Treat everything between the markers as generator-owned, including `[SerializeField]` fields, the generated listener override, and the editor-only `SetComponents()` method. Keep hand-written fields and overrides outside the delimiters. After a prefab rename or hierarchy change, regenerate instead of editing serialized references by hand.
## BaseView lifecycle and template rules
Use `BaseView` for a UIManager-opened view. The effective order is:
| Phase | Hook/order | Frequency and responsibility |
| --- | --- | --- |
| Unity activation | `Awake` -> `OnAwake` -> generated `OnAddUIComponentListener` | Once per instantiated object. Generated fields must already be serialized. |
| First Unity activation | `Start` -> `OnStart` (Unity may defer this until the first frame) | Once per instance, not once per open; it is not a prerequisite for the first `OnAdded`. |
| Every open | assign `Args`, `UIid`, `Guid` -> `OnAdded` -> `OnViewTweenInit` -> `OnPlayViewTween` | Runs on every open, including cached re-open. Read `Args` in `OnAdded`. |
| Open completion | `OnViewOpen` | Default `OnPlayViewTween` calls it immediately; a real open sequence calls it on sequence completion. |
| Every close | `OnPlayViewCloseTween` | A close sequence delays removal; no sequence continues immediately. |
| Before removal | internal tracked event/timer cleanup -> `OnBeforeRemove`; then external `UICallbacks.OnBeforeRemove` | Runs on every close. Unsubscribe direct global/input/UnityEvent listeners here. |
| After removal | external `UICallbacks.OnRemoved` -> `OnRemoved` -> asset tracker release | Runs after close; `isDestroy` controls full unload/destruction. |
`OnAdded` is the place to refresh from arguments, but do not accumulate global subscriptions there without first removing them. Cached objects run `OnAdded` again while `OnStart` does not run again.
`LayerUI.CreateNode` calls `BaseView.Added` immediately after instantiation, while Unity can defer `Start`; therefore the first `OnAdded`/tween hooks may run before `OnStart`. Put required reference/setup initialization in `Awake` or `OnAdded`, not only in `OnStart`.
Override the protected template hooks; do not add derived `Awake`/`Start` methods that hide BaseView's private Unity message methods. Use `OnAwake` and `OnStart` instead.
`UICallbacks.OnAdded` is invoked after `BaseView.Added` starts the view; it does not mean that an open tween has finished. Use `OnViewOpen` for visual-open completion. During removal, external `UICallbacks.OnRemoved` runs before `BaseView.OnRemoved`.
Treat `Args` as nullable. `ShowNotify`/`ShowNotifyAsync` wrap the content in a one-element array (`Args[0]`), while an ordinary `Open` call with no payload leaves `Args` null.
View on GitHub