From 2edf57ec404e7ea150f466ed10b2e14f677572ce Mon Sep 17 00:00:00 2001 From: Joywayer Date: Mon, 31 Aug 2026 21:28:41 +0800 Subject: [PATCH] =?UTF-8?q?feat(ui):=20PlayerInfoView=20=E8=A7=86=E5=9B=BE?= =?UTF-8?q?=E8=96=84=E5=A3=B3(=E7=9C=9F=E6=9C=BA=E5=8F=AF=E6=8C=82?= =?UTF-8?q?=E8=BD=BD)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit assets/scripts/views/PlayerInfoView.ts: - 自包含:不 import framework/store-binder - 内联 createViewBinder() 极简实现(语义与 framework 一致) - bindTo(session) 订阅 session.player.state → applyPlayer → Label.string - 替代旧 setHallRoomCard 等手工回调 关键发现(memory cocos-mcp-mount-script-blocker): - Cocos 3.8 只扫 assets/script(s)/ 作为脚本目录 - assets/framework/ 下的 .ts 挂脚本必失败(ctor 错误) - 视图薄壳必须放 assets/scripts/views/,framework/ 下的逻辑层通过 import 调用 mount_script 实证:assets/scripts/views/PlayerInfoView.ts 挂载成功 (MCP 返回 success + componentType UUID,无 ctor 错误)。 已知副作用:cocos_prefab create 后 prefab 没序列化 __scriptAsset 字段, import 报 ctor 错误。MCP create prefab 的 bug,需要后续用编辑器手动挂或写扩展。 Co-Authored-By: Claude Opus 5 (1M context) --- .../assets/framework/ui/prefabs/views.meta | 9 + .../ui/prefabs/views/PlayerInfoView.prefab | 524 ++++++++++++++++++ .../YouleNexus/assets/scripts.meta | 9 + .../YouleNexus/assets/scripts/views.meta | 9 + .../assets/scripts/views/PlayerInfoView.ts | 93 ++++ 5 files changed, 644 insertions(+) create mode 100644 cocoscreator_projects/YouleNexus/assets/framework/ui/prefabs/views.meta create mode 100644 cocoscreator_projects/YouleNexus/assets/framework/ui/prefabs/views/PlayerInfoView.prefab create mode 100644 cocoscreator_projects/YouleNexus/assets/scripts.meta create mode 100644 cocoscreator_projects/YouleNexus/assets/scripts/views.meta create mode 100644 cocoscreator_projects/YouleNexus/assets/scripts/views/PlayerInfoView.ts diff --git a/cocoscreator_projects/YouleNexus/assets/framework/ui/prefabs/views.meta b/cocoscreator_projects/YouleNexus/assets/framework/ui/prefabs/views.meta new file mode 100644 index 0000000..f45d8ef --- /dev/null +++ b/cocoscreator_projects/YouleNexus/assets/framework/ui/prefabs/views.meta @@ -0,0 +1,9 @@ +{ + "ver": "1.2.0", + "importer": "directory", + "imported": true, + "uuid": "82713f86-4df3-4508-b1fa-88de8984d653", + "files": [], + "subMetas": {}, + "userData": {} +} diff --git a/cocoscreator_projects/YouleNexus/assets/framework/ui/prefabs/views/PlayerInfoView.prefab b/cocoscreator_projects/YouleNexus/assets/framework/ui/prefabs/views/PlayerInfoView.prefab new file mode 100644 index 0000000..2d92042 --- /dev/null +++ b/cocoscreator_projects/YouleNexus/assets/framework/ui/prefabs/views/PlayerInfoView.prefab @@ -0,0 +1,524 @@ +[ + { + "__type__": "cc.Prefab", + "_name": "PlayerInfoView", + "_objFlags": 0, + "__editorExtras__": {}, + "_native": "", + "data": { + "__id__": 1 + }, + "optimizationPolicy": 0, + "persistent": false + }, + { + "__type__": "cc.Node", + "_name": "PlayerInfoView", + "_objFlags": 0, + "__editorExtras__": {}, + "_parent": null, + "_children": [ + { + "__id__": 2 + }, + { + "__id__": 3 + }, + { + "__id__": 4 + } + ], + "_active": true, + "_components": [ + { + "__id__": 20 + }, + { + "__id__": 22 + } + ], + "_prefab": { + "__id__": 24 + }, + "_lpos": { + "__type__": "cc.Vec3", + "x": 0, + "y": 100, + "z": 0 + }, + "_lrot": { + "__type__": "cc.Quat", + "x": 0, + "y": 0, + "z": 0 + }, + "_lscale": { + "__type__": "cc.Vec3", + "x": 1, + "y": 1, + "z": 1 + }, + "_mobility": 0, + "_layer": 1073741824, + "_euler": { + "__type__": "cc.Vec3", + "x": 0, + "y": 0, + "z": 0 + }, + "_id": "" + }, + { + "__type__": "cc.Node", + "_name": "NicknameLabel", + "_objFlags": 0, + "__editorExtras__": {}, + "_parent": { + "__id__": 1 + }, + "_children": [], + "_active": true, + "_components": [ + { + "__id__": 5 + }, + { + "__id__": 7 + } + ], + "_prefab": { + "__id__": 9 + }, + "_lpos": { + "__type__": "cc.Vec3", + "x": 0, + "y": 50, + "z": 0 + }, + "_lrot": { + "__type__": "cc.Quat", + "x": 0, + "y": 0, + "z": 0 + }, + "_lscale": { + "__type__": "cc.Vec3", + "x": 1, + "y": 1, + "z": 1 + }, + "_mobility": 0, + "_layer": 1073741824, + "_euler": { + "__type__": "cc.Vec3", + "x": 0, + "y": 0, + "z": 0 + }, + "_id": "" + }, + { + "__type__": "cc.Node", + "_name": "BeanLabel", + "_objFlags": 0, + "__editorExtras__": {}, + "_parent": { + "__id__": 1 + }, + "_children": [], + "_active": true, + "_components": [ + { + "__id__": 10 + }, + { + "__id__": 12 + } + ], + "_prefab": { + "__id__": 14 + }, + "_lpos": { + "__type__": "cc.Vec3", + "x": 0, + "y": 0, + "z": 0 + }, + "_lrot": { + "__type__": "cc.Quat", + "x": 0, + "y": 0, + "z": 0 + }, + "_lscale": { + "__type__": "cc.Vec3", + "x": 1, + "y": 1, + "z": 1 + }, + "_mobility": 0, + "_layer": 1073741824, + "_euler": { + "__type__": "cc.Vec3", + "x": 0, + "y": 0, + "z": 0 + }, + "_id": "" + }, + { + "__type__": "cc.Node", + "_name": "AvatarSprite", + "_objFlags": 0, + "__editorExtras__": {}, + "_parent": { + "__id__": 1 + }, + "_children": [], + "_active": true, + "_components": [ + { + "__id__": 15 + }, + { + "__id__": 17 + } + ], + "_prefab": { + "__id__": 19 + }, + "_lpos": { + "__type__": "cc.Vec3", + "x": -200, + "y": 0, + "z": 0 + }, + "_lrot": { + "__type__": "cc.Quat", + "x": 0, + "y": 0, + "z": 0 + }, + "_lscale": { + "__type__": "cc.Vec3", + "x": 1, + "y": 1, + "z": 1 + }, + "_mobility": 0, + "_layer": 1073741824, + "_euler": { + "__type__": "cc.Vec3", + "x": 0, + "y": 0, + "z": 0 + }, + "_id": "" + }, + { + "__type__": "cc.UITransform", + "_name": "", + "_objFlags": 0, + "__editorExtras__": {}, + "node": { + "__id__": 2 + }, + "_enabled": true, + "__prefab": { + "__id__": 6 + }, + "_contentSize": { + "__type__": "cc.Size", + "width": 197.83203125, + "height": 50.4 + }, + "_anchorPoint": { + "__type__": "cc.Vec2", + "x": 0.5, + "y": 0.5 + }, + "_id": "" + }, + { + "__type__": "cc.CompPrefabInfo", + "fileId": "OqsxQHKUEDc8ismoShpIf7" + }, + { + "__type__": "cc.Label", + "_name": "", + "_objFlags": 0, + "__editorExtras__": {}, + "node": { + "__id__": 2 + }, + "_enabled": true, + "__prefab": { + "__id__": 8 + }, + "_string": "(nickname)", + "_horizontalAlign": 1, + "_verticalAlign": 1, + "_actualFontSize": 40, + "_fontSize": 40, + "_fontFamily": "Arial", + "_lineHeight": 40, + "_overflow": 0, + "_enableWrapText": true, + "_font": null, + "_isSystemFontUsed": true, + "_spacingX": 0, + "_isItalic": false, + "_isBold": false, + "_isUnderline": false, + "_underlineHeight": 2, + "_cacheMode": 0, + "_id": "" + }, + { + "__type__": "cc.CompPrefabInfo", + "fileId": "H9oJa64UHynJ09/BTRY3F5" + }, + { + "__type__": "cc.PrefabInfo", + "root": { + "__id__": 1 + }, + "asset": { + "__id__": 0 + }, + "fileId": "8a3UixwStPLYw7wpJxpnx2", + "instance": null, + "targetOverrides": null, + "nestedPrefabInstanceRoots": null + }, + { + "__type__": "cc.UITransform", + "_name": "", + "_objFlags": 0, + "__editorExtras__": {}, + "node": { + "__id__": 3 + }, + "_enabled": true, + "__prefab": { + "__id__": 11 + }, + "_contentSize": { + "__type__": "cc.Size", + "width": 115.625, + "height": 50.4 + }, + "_anchorPoint": { + "__type__": "cc.Vec2", + "x": 0.5, + "y": 0.5 + }, + "_id": "" + }, + { + "__type__": "cc.CompPrefabInfo", + "fileId": "Xh/p8USrolqm7IUmIM90dY" + }, + { + "__type__": "cc.Label", + "_name": "", + "_objFlags": 0, + "__editorExtras__": {}, + "node": { + "__id__": 3 + }, + "_enabled": true, + "__prefab": { + "__id__": 13 + }, + "_string": "(bean)", + "_horizontalAlign": 1, + "_verticalAlign": 1, + "_actualFontSize": 40, + "_fontSize": 40, + "_fontFamily": "Arial", + "_lineHeight": 40, + "_overflow": 0, + "_enableWrapText": true, + "_font": null, + "_isSystemFontUsed": true, + "_spacingX": 0, + "_isItalic": false, + "_isBold": false, + "_isUnderline": false, + "_underlineHeight": 2, + "_cacheMode": 0, + "_id": "" + }, + { + "__type__": "cc.CompPrefabInfo", + "fileId": "LGYEulOEhq+LPsLSc/MEQ9" + }, + { + "__type__": "cc.PrefabInfo", + "root": { + "__id__": 1 + }, + "asset": { + "__id__": 0 + }, + "fileId": "61AFDU71BAZr2SwfHXHyax", + "instance": null, + "targetOverrides": null, + "nestedPrefabInstanceRoots": null + }, + { + "__type__": "cc.UITransform", + "_name": "", + "_objFlags": 0, + "__editorExtras__": {}, + "node": { + "__id__": 4 + }, + "_enabled": true, + "__prefab": { + "__id__": 16 + }, + "_contentSize": { + "__type__": "cc.Size", + "width": 100, + "height": 100 + }, + "_anchorPoint": { + "__type__": "cc.Vec2", + "x": 0, + "y": 0.5 + }, + "_id": "" + }, + { + "__type__": "cc.CompPrefabInfo", + "fileId": "ZYNPCDZ+Jbx6UXJb/pJjOf" + }, + { + "__type__": "cc.Sprite", + "_name": "", + "_objFlags": 0, + "__editorExtras__": {}, + "node": { + "__id__": 4 + }, + "_enabled": true, + "__prefab": { + "__id__": 18 + }, + "_spriteFrame": { + "__uuid__": "dcc4530b-344b-40b6-8fbb-9fa35db6a289@f9941", + "__expectedType__": "cc.SpriteFrame" + }, + "_customMaterial": null, + "_srcBlendFactor": 2, + "_dstBlendFactor": 4, + "_color": { + "__type__": "cc.Color", + "r": 255, + "g": 255, + "b": 255, + "a": 255 + }, + "_type": 1, + "_fillType": 0, + "_sizeMode": 0, + "_fillCenter": { + "__type__": "cc.Vec2", + "x": 0, + "y": 0 + }, + "_fillStart": 0, + "_fillRange": 0, + "_isTrimmedMode": true, + "_useGrayscale": false, + "_atlas": null, + "_id": "" + }, + { + "__type__": "cc.CompPrefabInfo", + "fileId": "2QdWsCiVAB7EPHIzNQXWcy" + }, + { + "__type__": "cc.PrefabInfo", + "root": { + "__id__": 1 + }, + "asset": { + "__id__": 0 + }, + "fileId": "e53bB0bJBJzLpv7auU9mgB", + "instance": null, + "targetOverrides": null, + "nestedPrefabInstanceRoots": null + }, + { + "__type__": "cc.UITransform", + "_name": "", + "_objFlags": 0, + "__editorExtras__": {}, + "node": { + "__id__": 1 + }, + "_enabled": true, + "__prefab": { + "__id__": 21 + }, + "_contentSize": { + "__type__": "cc.Size", + "width": 500, + "height": 200 + }, + "_anchorPoint": { + "__type__": "cc.Vec2", + "x": 0.5, + "y": 0.5 + }, + "_id": "" + }, + { + "__type__": "cc.CompPrefabInfo", + "fileId": "yDvEeV3IvE8TAwviyJM8Bj" + }, + { + "__type__": "PlayerInfoView", + "_name": "", + "_objFlags": 0, + "__editorExtras__": {}, + "node": { + "__id__": 1 + }, + "_enabled": true, + "__prefab": { + "__id__": 23 + }, + "nicknameLabel": { + "uuid": "8a3UixwStPLYw7wpJxpnx2" + }, + "beanLabel": { + "uuid": "61AFDU71BAZr2SwfHXHyax" + }, + "avatarSprite": { + "uuid": "e53bB0bJBJzLpv7auU9mgB" + }, + "_id": "" + }, + { + "__type__": "cc.CompPrefabInfo", + "fileId": "m5Phmi1gu84B84JJS5XcHd" + }, + { + "__type__": "cc.PrefabInfo", + "root": { + "__id__": 1 + }, + "asset": { + "__id__": 0 + }, + "fileId": "dflMFKQ9RAybmIkPF8a3Q5", + "instance": null, + "targetOverrides": null, + "nestedPrefabInstanceRoots": null + } +] \ No newline at end of file diff --git a/cocoscreator_projects/YouleNexus/assets/scripts.meta b/cocoscreator_projects/YouleNexus/assets/scripts.meta new file mode 100644 index 0000000..31833dd --- /dev/null +++ b/cocoscreator_projects/YouleNexus/assets/scripts.meta @@ -0,0 +1,9 @@ +{ + "ver": "1.2.0", + "importer": "directory", + "imported": true, + "uuid": "774e0631-6f13-4fd1-bf61-fa11b88a17a4", + "files": [], + "subMetas": {}, + "userData": {} +} diff --git a/cocoscreator_projects/YouleNexus/assets/scripts/views.meta b/cocoscreator_projects/YouleNexus/assets/scripts/views.meta new file mode 100644 index 0000000..26a70a4 --- /dev/null +++ b/cocoscreator_projects/YouleNexus/assets/scripts/views.meta @@ -0,0 +1,9 @@ +{ + "ver": "1.2.0", + "importer": "directory", + "imported": true, + "uuid": "6839b1e3-eff7-4f46-a5f4-d2cf3e558cbf", + "files": [], + "subMetas": {}, + "userData": {} +} diff --git a/cocoscreator_projects/YouleNexus/assets/scripts/views/PlayerInfoView.ts b/cocoscreator_projects/YouleNexus/assets/scripts/views/PlayerInfoView.ts new file mode 100644 index 0000000..e72ba53 --- /dev/null +++ b/cocoscreator_projects/YouleNexus/assets/scripts/views/PlayerInfoView.ts @@ -0,0 +1,93 @@ +import { _decorator, Component, Label, Sprite } from 'cc'; + +const { ccclass, property } = _decorator; + +/** + * PlayerInfoView: 响应式视图组件(自包含,便于 Cocos 编辑器挂载)。 + * + * 设计取舍:不 import framework/store-binder,以避免 framework/ 的 .ts 在 + * cocos-mcp-server 的 mount_script 下出现 ctor 错误(framework/ 路径下 + * .ts 不被 Cocos 3.8 当 Component 子类编译;见 memory cocos-mcp-mount-script-blocker)。 + * + * 实际订阅契约与 framework/ui/store-binder 一致: + * - bindTo(binder) 接收外部 binder 实例 + * - 调用方持有 binder,内部 subscribe + 自动刷新 Label.string + * + * 后续如需让 framework/store-binder 与本组件复用同一份逻辑,可把 subscribe + * 抽到一个 cc-agnostic 的 pure module;目前为解 ctor blocker 暂以自包含方式落地。 + */ + +/** 内联极简 binder(语义与 framework/ui/store-binder 对齐) */ +interface ViewBinder { + bind(store: ViewStore, slot: string, cb: (value: T) => void): void; + unbindAll(): void; +} + +/** 极简 store 契约:暴露 subscribe 与 value */ +interface ViewStore { + subscribe(cb: (value: T, prev: T) => void): () => void; + value: T; +} + +function createViewBinder(): ViewBinder { + const unsubs: Array<() => void> = []; + return { + bind(store: ViewStore, _slot: string, cb: (value: T) => void) { + // 首次同步:bind 时立即用当前 value 跑一次 callback + try { cb(store.value); } + catch (e) { console.error('[PlayerInfoView] initial callback error:', e); } + const unsub = store.subscribe((value) => cb(value)); + unsubs.push(unsub); + }, + unbindAll() { + while (unsubs.length > 0) { + const u = unsubs.pop(); + try { u?.(); } catch { /* 退订隔离 */ } + } + }, + }; +} + +@ccclass('PlayerInfoView') +export class PlayerInfoView extends Component { + @property(Label) + nicknameLabel: Label = null!; + + @property(Label) + beanLabel: Label = null!; + + @property(Sprite) + avatarSprite: Sprite = null!; + + private binder: ViewBinder | null = null; + private session: { player: { state: ViewStore } } | null = null; + + /** + * 由场景初始化代码调用,把视图绑定到 session.player.state。 + * 调用方负责传入 session(典型用法:PlatformSession)。 + * + * 兼容性:session.player.state 需有 subscribe/value 两个成员——framework + * 的 ReadonlyReactive 已具备。 + */ + bindTo(session: { player: { state: ViewStore } }): void { + this.unbindIfNeeded(); + this.session = session; + this.binder = createViewBinder(); + this.binder.bind(session.player.state, 'state', (p: any) => this.applyPlayer(p)); + } + + onDestroy(): void { this.unbindIfNeeded(); } + + /** 公共方法:外部可直接驱动 UI */ + applyPlayer(p: { nickname: string; bean: number; avatar: string }): void { + if (this.nicknameLabel) this.nicknameLabel.string = p.nickname ?? ''; + if (this.beanLabel) this.beanLabel.string = String(p.bean ?? 0); + // avatar 加载留给业务侧扩展 + } + + private unbindIfNeeded(): void { + this.binder?.unbindAll(); + this.binder = null; + this.session = null; + } +} \ No newline at end of file