Cocos dynamic ui panel
Skill ChrisLamDev/cocos-creator-debug-skills/skills/cocos-dynamic-ui-panel
17 executable AI agent skills for Cocos Creator 3.x debugging
npx -y skills add ChrisLamDev/cocos-creator-debug-skills --skill cocos-dynamic-ui-panelAssembled from the repository path, not quoted from the project. Check it against their README if it does not work.
One thing to look at
- 0 stars0 stars. Stars are a popularity signal and not a quality one, but at this level it is likely that nobody has read this closely except its author, and you would be relying on your own review.
What its author says it does
Copied from the file, not written here
Use when building full runtime UI panels in Cocos Creator 3.x. triggers: - ui panel - dynamic ui - cocos ui - cocos panel
SKILL.md
8.9 KB, ~2.2k tokens by cl100k_base, as published. Nobody here has run it
Cocos Creator 3.x Dynamic UI Panel Pattern
Use when: you need a complex UI panel (buttons, labels, timers, status displays, panel transitions) but want to build it entirely in code without .scene editing or dragging Prefabs. Useful for rapid prototyping and test rigs.
Core Pattern
- Create all UI nodes in
start()using helper methods - Store references to dynamic controls (Labels, UITransforms)
- Use
update(dt)with a timer accumulator instead ofschedule() - Show/hide panels by toggling
node.active - Always set
node.layer = UI_LAYERon new nodes
Boilerplate
import { _decorator, Component, Node, Label, Button, UITransform, Color } from 'cc';
const { ccclass } = _decorator;
const UI_LAYER = 33554432; // 1<<25 — UI_2D
@ccclass('MyPanel')
export class MyPanel extends Component {
// === Label references for dynamic updates ===
private _statusLabel: Label = null!;
private _timerLabel: Label = null!;
// === Panel references for switching ===
private _mainPanel: Node = null!;
private _sessionPanel: Node = null!;
// === Update timer ===
private _updateTimer: number = 0;
private readonly _UPDATE_INTERVAL = 1.0; // seconds
start() {
this._buildUI();
}
update(dt: number) {
this._updateTimer += dt;
if (this._updateTimer >= this._UPDATE_INTERVAL) {
this._updateTimer = 0;
this._refreshStatus();
this._refreshTimer();
}
}
// ===== UI Build Helpers =====
private _buildUI(): void {
const root = this.node;
this._statusLabel = this._label(root, 'Status text', 0, 200, 20);
this._timerLabel = this._label(root, '0:00', 0, 100, 36);
this._btn(root, 'Action', 0, -50, () => this._onAction());
// Hidden session panel
this._sessionPanel = new Node('SessionPanel');
this._sessionPanel.parent = root;
this._sessionPanel.layer = UI_LAYER;
this._sessionPanel.active = false;
this._label(this._sessionPanel, 'In Session...', 0, 0, 24);
}
// ===== Helper Methods =====
private _label(p: Node, text: string, x: number, y: number, size: number): Label {
const n = new Node();
n.parent = p;
n.layer = UI_LAYER;
n.setPosition(x, y, 0);
n.addComponent(UITransform).setContentSize(400, 36);
const l = n.addComponent(Label);
l.string = text;
l.fontSize = size;
l.lineHeight = size + 8;
l.color = new Color(255, 255, 255, 255);
return l;
}
private _btn(p: Node, text: string, x: number, y: number, cb: () => void): Node {
const n = new Node();
n.parent = p;
n.layer = UI_LAYER;
n.setPosition(x, y, 0);
n.addComponent(UITransform).setContentSize(200, 44);
n.addComponent(Button);
const l = n.addComponent(Label);
l.string = text;
l.fontSize = 20;
l.lineHeight = 28;
l.color = new Color(255, 255, 255, 255);
n.on(Node.EventType.TOUCH_END, cb, this);
return n;
}
// ===== Panel Switching =====
private switchToSessionPanel(): void {
this._mainPanel.active = false;
this._sessionPanel.active = true;
}
private switchToMainPanel(): void {
this._sessionPanel.active = false;
this._mainPanel.active = true;
}
}
Why update(dt) Instead of schedule()
In Creator 3.8 Preview runtime, this.schedule(callback, interval) may not fire reliably when called from:
- Components dynamically added via
addComponent()at runtime - Non-root components in complex hierarchies
Safer alternative: Manual timer in update(dt):
private _uiTimer: number = 0;
update(dt: number): void {
this._uiTimer += dt;
if (this._uiTimer >= 1.0) {
this._uiTimer -= 1.0; // subtract, don't reset (avoids drift)
this._refreshUI();
}
}
Code-Driven ScrollView (Cocos 3.x)
Building a ScrollView entirely in code requires precise node hierarchy. Missing any node or component causes scroll to fail silently.
Required Hierarchy
ScrollView Node (UITransform + ScrollView)
└── View Node (UITransform + Mask.Type.RECT)
└── Content Node (UITransform + Layout.Type.VERTICAL)
Boilerplate
import { ScrollView, Mask, Layout, UITransform, v3 } from 'cc';
private _buildScrollView(parent: Node, width: number, height: number, padding = 30, spacing = 20): ScrollView {
const svNode = new Node('scrollView');
svNode.setPosition(v3(0, 0, 0));
parent.addChild(svNode);
const svTrans = svNode.addComponent(UITransform);
svTrans.width = width;
svTrans.height = height;
const scrollView = svNode.addComponent(ScrollView);
scrollView.horizontal = false;
scrollView.vertical = true;
scrollView.inertia = true;
scrollView.brake = 0.85;
scrollView.elastic = true;
scrollView.verticalScrollBar = null as any; // hide scrollbar
// View node — required child with Mask for clipping
const viewNode = new Node('view');
viewNode.setPosition(v3(0, 0, 0));
svNode.addChild(viewNode);
const viewTrans = viewNode.addComponent(UITransform);
viewTrans.width = width;
viewTrans.height = height;
viewNode.addComponent(Mask).type = Mask.Type.RECT;
// Content node — must have anchorY=1 for correct scroll start
const contentNode = new Node('content');
contentNode.setPosition(v3(0, height / 2, 0));
viewNode.addChild(contentNode);
const contentTrans = contentNode.addComponent(UITransform);
contentTrans.width = width;
contentTrans.anchorX = 0.5;
contentTrans.anchorY = 1; // ⚠️ CRITICAL
// Layout — auto-expands content height as children are added
const layout = contentNode.addComponent(Layout);
layout.type = Layout.Type.VERTICAL;
layout.resizeMode = Layout.ResizeMode.CONTAINER;
layout.horizontalDirection = Layout.HorizontalDirection.LEFT_TO_RIGHT;
layout.verticalDirection = Layout.VerticalDirection.TOP_TO_BOTTOM;
layout.paddingTop = padding;
layout.paddingLeft = padding;
layout.paddingRight = padding;
layout.spacingY = spacing;
scrollView.content = contentNode;
return scrollView;
}
⚠️ Critical Gotchas
- Content
anchorY = 1is mandatory — without this, scroll starts from the wrong position. - Content needs
LayoutwithResizeMode.CONTAINER— children auto-expand the content height. - View needs
Mask.Type.RECT— without it, content bleeds outside the visible area. verticalScrollBar = null as any— hide scrollbar explicitly (Creator 3.x defaults show one).scrollView.content = contentNodemust be set — otherwise nothing scrolls.- Use
new Node()notinstantiate— for content children in dynamic UIs. - Children added to content need
UITransformwith width — otherwise Layout can't arrange them.
Touch Detection on ScrollView Children (Name-Based Dispatching)
Use Node name-based detection to avoid Button-component interference with scroll gestures:
this.node.on(Node.EventType.TOUCH_END, (event: EventTouch) => {
const target = event.target;
if (!target) return;
const name = target.name;
// Tab/button using name prefix tagging
if (name.startsWith('tab_')) { this._onTabSelect(name.substring(4)); }
if (name.startsWith('card_')) { this._onCardClick(name.substring(5)); }
if (name === 'btn_back') { this._goBack(); }
if (name === 'btn_close') { this.node.destroy(); }
}, this);
Key Takeaways (BuddhaHeart Project)
- Always import classes used in
getComponent().getComponent(PracticeMoteCanvas)withoutimport { PracticeMoteCanvas }throwsReferenceErrorin Creator 3.8 ES Module runtime. schedule()is unreliable for runtime-added components. Use manualupdate(dt)timer accumulators in 3.8.8.- Panel switching via
activeflag works well. Pre-create both panels instart(), toggle visibility. Avoid creating/destroying nodes during gameplay. - Button Node hierarchy matters.
Buttoncomponent must be on the same node asUITransform. Label should also be on the same node (or a child) — but same node works fine. Node.EventType.TOUCH_ENDis the click event in Creator 3.x. NotCLICK(which requires a Collider/EventTrigger). UseTOUCH_ENDfor button interactions.- Dynamic Labels with changing content — just assign
label.string = newValuedirectly. No need to recreate nodes. - Layout responsibility is yours. Unlike scene-editor UIs with Widget/Layout components, dynamic UIs must position each node manually via
setPosition(x, y, 0).