13 Commits

Author SHA1 Message Date
JSD\13999
b17b6237f3 关闭 P2 package editor 收尾 2026-06-08 17:05:05 +08:00
JSD\13999
185d4830d4 记录 preview2 消费方刷新确认 2026-06-08 16:48:02 +08:00
JSD\13999
b91fc5484a 发布 package preview2 版本收口 2026-06-08 16:38:02 +08:00
JSD\13999
d42c8f9d1d 记录 sample 导入脚本修正 2026-06-08 10:45:55 +08:00
JSD\13999
e92fed2d4f 记录当前提交导入验证状态 2026-06-06 18:58:47 +08:00
JSD\13999
e6d1f00e23 记录空白项目复验通过 2026-06-06 18:17:34 +08:00
JSD\13999
55fda1e0cd 记录空白项目复验阻塞 2026-06-06 17:52:01 +08:00
JSD\13999
8f3982c165 补记 P2 EditMode 验证通过 2026-06-06 17:30:48 +08:00
JSD\13999
dfa25aaf15 记录 P2 收尾验证状态 2026-06-06 16:58:51 +08:00
JSD\13999
7b2243a40b 补齐 package Tests 第一刀 2026-06-06 16:14:17 +08:00
JSD\13999
0930e7c173 整理 Addressables 测试资源路径 2026-06-05 11:06:20 +08:00
JSD\13999
ac8fcc3c24 镜像 MainMenuP0 包样例 2026-06-04 20:16:28 +08:00
JSD\13999
3b3957fb74 记录 P2 第二阶段包预览验证 2026-06-04 17:26:59 +08:00
48 changed files with 2122 additions and 182 deletions

1
.gitignore vendored
View File

@@ -6,6 +6,7 @@ Client/ServerData
/cfgPack/encryptcfg
.gemini/
.worktrees/
TestFlowScopePackage/
GEMINI.md
CLAUDE.md
AGENTS.md

View File

@@ -1,8 +1,17 @@
# Changelog
## Unreleased
## 0.2.0-preview.2
- Added `Samples~/MainMenuP0` package sample metadata and content.
- Added the first `Tests~/EditMode` package smoke test slice.
- Updated package installation and layout documentation to reflect the current P2 package state.
- Fixed the package `Documentation~` content so consuming projects no longer see an empty documentation folder warning.
- Validated Git package installation, package smoke references, and manual `MainMenuP0` sample import in a clean consumer project.
## 0.2.0-preview.1
- Added the first Git-consumable FlowScope Game Core package layout.
- Mirrored Runtime, Addressables, and Editor sources from `Assets/FlowScope`.
- Added package metadata and Git installation notes for consuming Unity projects.

View File

@@ -2,18 +2,28 @@
## Consumer Manifest
Add FlowScope and its Git-hosted R3 dependency to the consuming project's `Packages/manifest.json`:
Add FlowScope, R3.Unity, and the NuGet R3 core dependency to the consuming project's `Packages/manifest.json`:
```json
{
"scopedRegistries": [
{
"name": "Unity NuGet",
"url": "https://unitynuget-registry.openupm.com",
"scopes": [
"org.nuget"
]
}
],
"dependencies": {
"com.cysharp.r3": "https://github.com/Cysharp/R3.git?path=src/R3.Unity/Assets/R3.Unity#1.3.0",
"com.flowscope.gamecore": "ssh://git@192.168.9.108:2222/Developer/BombGameFramework.git?path=/Packages/com.flowscope.gamecore#codex/pre-p2-architecture-fixes"
"com.flowscope.gamecore": "ssh://git@git.zz.com:2222/Developer/FlowScope.git?path=/Packages/com.flowscope.gamecore#v0.2.0-preview.2",
"org.nuget.r3": "1.3.0"
}
}
```
Keep the R3 dependency in the project manifest. Unity Package Manager can install a Git package from the project manifest, but it does not support Git dependencies declared inside another package's `package.json`.
Keep both R3 entries in the project manifest. Unity Package Manager can install a Git package from the project manifest, but Git dependencies declared inside another package's `package.json` are not resolved reliably. `org.nuget.r3` supplies the core R3 and BCL DLLs required by `R3.Unity`.
## Validation
@@ -23,5 +33,6 @@ After Unity resolves the package:
2. Open `Tools/FlowScope/Diagnostics`.
3. Confirm Addressables and UGUI dependencies are present.
4. Add a small probe script in the consuming project that references `FlowScope.Container.Container` and `FlowScope.Flow.GameFlow`.
5. Confirm the consuming project has zero compile errors and zero Console errors after package resolution.
Do not copy `Assets/FlowScope` into the consuming project when testing the Git package path. That would validate project-internal source import rather than package consumption.

View File

@@ -10,20 +10,31 @@ Add the package to the consuming project's `Packages/manifest.json`:
```json
{
"scopedRegistries": [
{
"name": "Unity NuGet",
"url": "https://unitynuget-registry.openupm.com",
"scopes": [
"org.nuget"
]
}
],
"dependencies": {
"com.cysharp.r3": "https://github.com/Cysharp/R3.git?path=src/R3.Unity/Assets/R3.Unity#1.3.0",
"com.flowscope.gamecore": "ssh://git@192.168.9.108:2222/Developer/BombGameFramework.git?path=/Packages/com.flowscope.gamecore#codex/pre-p2-architecture-fixes"
"com.flowscope.gamecore": "ssh://git@git.zz.com:2222/Developer/FlowScope.git?path=/Packages/com.flowscope.gamecore#v0.2.0-preview.2",
"org.nuget.r3": "1.3.0"
}
}
```
`com.cysharp.r3` must be declared by the consuming project because Unity Package Manager does not support Git dependencies inside a package's `package.json`.
`com.cysharp.r3` and `org.nuget.r3` must be declared by the consuming project. Unity Package Manager can install Git packages from the project manifest, but Git dependencies declared inside another package's `package.json` are not resolved reliably. `org.nuget.r3` supplies the core R3 and BCL DLLs required by `R3.Unity`.
## Assembly Boundaries
- `FlowScope.Runtime` references `R3.Unity`.
- `FlowScope.Addressables` references `FlowScope.Runtime`, `Unity.Addressables`, and `Unity.ResourceManager`.
- `FlowScope.Editor` is Editor-only and only provides read-only diagnostics.
- `Samples~/MainMenuP0` is a package sample candidate mirrored from the project-internal MainMenuP0 sample.
- `Tests~/EditMode` contains the first package-level smoke tests.
Runtime must not depend on Addressables, Editor, Samples, or Tests.

View File

@@ -1,5 +1,5 @@
fileFormatVersion: 2
guid: 20098efc5dc925a4194600eea4969d68
guid: 49d9c024747f4af0b560e21d931a7816
folderAsset: yes
DefaultImporter:
externalObjects: {}

View File

@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: dffe7921ed374f83b8077b49644bc744
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 76085fdd1da44ab08b69395b17064b59
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,3 @@
{
"Gold": 0
}

View File

@@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: bd391f0c499b42a3a264d0a9de2483f8
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,17 @@
{
"name": "FlowScope.Samples.MainMenuP0",
"rootNamespace": "FlowScope.Samples.MainMenuP0",
"references": [
"FlowScope.Runtime",
"R3.Unity"
],
"includePlatforms": [],
"excludePlatforms": [],
"allowUnsafeCode": false,
"overrideReferences": false,
"precompiledReferences": [],
"autoReferenced": true,
"defineConstraints": [],
"versionDefines": [],
"noEngineReferences": false
}

View File

@@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: eb3b4bf3dfdc46e4a9352fc2203bbfd4
AssemblyDefinitionImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: c88fe653c15d473cac0029539a2ff355
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,453 @@
%YAML 1.1
%TAG !u! tag:unity3d.com,2011:
--- !u!1 &4224295462618148274
GameObject:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
serializedVersion: 6
m_Component:
- component: {fileID: 2785319262204863001}
- component: {fileID: 6821847843857072034}
- component: {fileID: 8750438775342100074}
- component: {fileID: 8038936202738359261}
m_Layer: 0
m_Name: MainMenuPanel
m_TagString: Untagged
m_Icon: {fileID: 0}
m_NavMeshLayer: 0
m_StaticEditorFlags: 0
m_IsActive: 1
--- !u!224 &2785319262204863001
RectTransform:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 4224295462618148274}
m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
m_LocalPosition: {x: 0, y: 0, z: 0}
m_LocalScale: {x: 1, y: 1, z: 1}
m_ConstrainProportionsScale: 0
m_Children:
- {fileID: 6248245286658728259}
- {fileID: 6082938367091974448}
- {fileID: 8836034375789789371}
m_Father: {fileID: 0}
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
m_AnchorMin: {x: 0.5, y: 0.5}
m_AnchorMax: {x: 0.5, y: 0.5}
m_AnchoredPosition: {x: 0, y: 0}
m_SizeDelta: {x: 420, y: 240}
m_Pivot: {x: 0.5, y: 0.5}
--- !u!114 &6821847843857072034
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 4224295462618148274}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: 3a641bd6447847f2af2cd5af40305a5a, type: 3}
m_Name:
m_EditorClassIdentifier:
goldText: {fileID: 1736696561195866944}
incrementButton: {fileID: 4338913550008398238}
--- !u!222 &8750438775342100074
CanvasRenderer:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 4224295462618148274}
m_CullTransparentMesh: 1
--- !u!114 &8038936202738359261
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 4224295462618148274}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: fe87c0e1cc204ed48ad3b37840f39efc, type: 3}
m_Name:
m_EditorClassIdentifier:
m_Material: {fileID: 0}
m_Color: {r: 0.08, g: 0.1, b: 0.12, a: 0.92}
m_RaycastTarget: 1
m_RaycastPadding: {x: 0, y: 0, z: 0, w: 0}
m_Maskable: 1
m_OnCullStateChanged:
m_PersistentCalls:
m_Calls: []
m_Sprite: {fileID: 0}
m_Type: 0
m_PreserveAspect: 0
m_FillCenter: 1
m_FillMethod: 4
m_FillAmount: 1
m_FillClockwise: 1
m_FillOrigin: 0
m_UseSpriteMesh: 0
m_PixelsPerUnitMultiplier: 1
--- !u!1 &4373334964405757163
GameObject:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
serializedVersion: 6
m_Component:
- component: {fileID: 8836034375789789371}
- component: {fileID: 2680961909818563798}
- component: {fileID: 1639113149993436237}
- component: {fileID: 4338913550008398238}
m_Layer: 0
m_Name: IncrementGoldButton
m_TagString: Untagged
m_Icon: {fileID: 0}
m_NavMeshLayer: 0
m_StaticEditorFlags: 0
m_IsActive: 1
--- !u!224 &8836034375789789371
RectTransform:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 4373334964405757163}
m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
m_LocalPosition: {x: 0, y: 0, z: 0}
m_LocalScale: {x: 1, y: 1, z: 1}
m_ConstrainProportionsScale: 0
m_Children:
- {fileID: 9086714161511357950}
m_Father: {fileID: 2785319262204863001}
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
m_AnchorMin: {x: 0.5, y: 0}
m_AnchorMax: {x: 0.5, y: 0}
m_AnchoredPosition: {x: 0, y: 48}
m_SizeDelta: {x: 220, y: 48}
m_Pivot: {x: 0.5, y: 0.5}
--- !u!222 &2680961909818563798
CanvasRenderer:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 4373334964405757163}
m_CullTransparentMesh: 1
--- !u!114 &1639113149993436237
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 4373334964405757163}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: fe87c0e1cc204ed48ad3b37840f39efc, type: 3}
m_Name:
m_EditorClassIdentifier:
m_Material: {fileID: 0}
m_Color: {r: 0.2, g: 0.54, b: 0.8, a: 1}
m_RaycastTarget: 1
m_RaycastPadding: {x: 0, y: 0, z: 0, w: 0}
m_Maskable: 1
m_OnCullStateChanged:
m_PersistentCalls:
m_Calls: []
m_Sprite: {fileID: 0}
m_Type: 0
m_PreserveAspect: 0
m_FillCenter: 1
m_FillMethod: 4
m_FillAmount: 1
m_FillClockwise: 1
m_FillOrigin: 0
m_UseSpriteMesh: 0
m_PixelsPerUnitMultiplier: 1
--- !u!114 &4338913550008398238
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 4373334964405757163}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: 4e29b1a8efbd4b44bb3f3716e73f07ff, type: 3}
m_Name:
m_EditorClassIdentifier:
m_Navigation:
m_Mode: 3
m_WrapAround: 0
m_SelectOnUp: {fileID: 0}
m_SelectOnDown: {fileID: 0}
m_SelectOnLeft: {fileID: 0}
m_SelectOnRight: {fileID: 0}
m_Transition: 1
m_Colors:
m_NormalColor: {r: 1, g: 1, b: 1, a: 1}
m_HighlightedColor: {r: 0.9607843, g: 0.9607843, b: 0.9607843, a: 1}
m_PressedColor: {r: 0.78431374, g: 0.78431374, b: 0.78431374, a: 1}
m_SelectedColor: {r: 0.9607843, g: 0.9607843, b: 0.9607843, a: 1}
m_DisabledColor: {r: 0.78431374, g: 0.78431374, b: 0.78431374, a: 0.5019608}
m_ColorMultiplier: 1
m_FadeDuration: 0.1
m_SpriteState:
m_HighlightedSprite: {fileID: 0}
m_PressedSprite: {fileID: 0}
m_SelectedSprite: {fileID: 0}
m_DisabledSprite: {fileID: 0}
m_AnimationTriggers:
m_NormalTrigger: Normal
m_HighlightedTrigger: Highlighted
m_PressedTrigger: Pressed
m_SelectedTrigger: Selected
m_DisabledTrigger: Disabled
m_Interactable: 1
m_TargetGraphic: {fileID: 1639113149993436237}
m_OnClick:
m_PersistentCalls:
m_Calls: []
--- !u!1 &5175570728316865318
GameObject:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
serializedVersion: 6
m_Component:
- component: {fileID: 6082938367091974448}
- component: {fileID: 901708792770677843}
- component: {fileID: 1736696561195866944}
m_Layer: 0
m_Name: GoldText
m_TagString: Untagged
m_Icon: {fileID: 0}
m_NavMeshLayer: 0
m_StaticEditorFlags: 0
m_IsActive: 1
--- !u!224 &6082938367091974448
RectTransform:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 5175570728316865318}
m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
m_LocalPosition: {x: 0, y: 0, z: 0}
m_LocalScale: {x: 1, y: 1, z: 1}
m_ConstrainProportionsScale: 0
m_Children: []
m_Father: {fileID: 2785319262204863001}
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
m_AnchorMin: {x: 0, y: 0.5}
m_AnchorMax: {x: 1, y: 0.5}
m_AnchoredPosition: {x: 0, y: 18}
m_SizeDelta: {x: -48, y: 60}
m_Pivot: {x: 0.5, y: 0.5}
--- !u!222 &901708792770677843
CanvasRenderer:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 5175570728316865318}
m_CullTransparentMesh: 1
--- !u!114 &1736696561195866944
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 5175570728316865318}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: 5f7201a12d95ffc409449d95f23cf332, type: 3}
m_Name:
m_EditorClassIdentifier:
m_Material: {fileID: 0}
m_Color: {r: 1, g: 1, b: 1, a: 1}
m_RaycastTarget: 1
m_RaycastPadding: {x: 0, y: 0, z: 0, w: 0}
m_Maskable: 1
m_OnCullStateChanged:
m_PersistentCalls:
m_Calls: []
m_FontData:
m_Font: {fileID: 10102, guid: 0000000000000000e000000000000000, type: 0}
m_FontSize: 40
m_FontStyle: 0
m_BestFit: 0
m_MinSize: 10
m_MaxSize: 40
m_Alignment: 4
m_AlignByGeometry: 0
m_RichText: 1
m_HorizontalOverflow: 0
m_VerticalOverflow: 0
m_LineSpacing: 1
m_Text: 0
--- !u!1 &7474348118067574816
GameObject:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
serializedVersion: 6
m_Component:
- component: {fileID: 6248245286658728259}
- component: {fileID: 8827375201563443825}
- component: {fileID: 1429476040966027426}
m_Layer: 0
m_Name: Title
m_TagString: Untagged
m_Icon: {fileID: 0}
m_NavMeshLayer: 0
m_StaticEditorFlags: 0
m_IsActive: 1
--- !u!224 &6248245286658728259
RectTransform:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 7474348118067574816}
m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
m_LocalPosition: {x: 0, y: 0, z: 0}
m_LocalScale: {x: 1, y: 1, z: 1}
m_ConstrainProportionsScale: 0
m_Children: []
m_Father: {fileID: 2785319262204863001}
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
m_AnchorMin: {x: 0, y: 1}
m_AnchorMax: {x: 1, y: 1}
m_AnchoredPosition: {x: 0, y: -24}
m_SizeDelta: {x: -48, y: 46}
m_Pivot: {x: 0.5, y: 1}
--- !u!222 &8827375201563443825
CanvasRenderer:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 7474348118067574816}
m_CullTransparentMesh: 1
--- !u!114 &1429476040966027426
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 7474348118067574816}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: 5f7201a12d95ffc409449d95f23cf332, type: 3}
m_Name:
m_EditorClassIdentifier:
m_Material: {fileID: 0}
m_Color: {r: 1, g: 1, b: 1, a: 1}
m_RaycastTarget: 1
m_RaycastPadding: {x: 0, y: 0, z: 0, w: 0}
m_Maskable: 1
m_OnCullStateChanged:
m_PersistentCalls:
m_Calls: []
m_FontData:
m_Font: {fileID: 10102, guid: 0000000000000000e000000000000000, type: 0}
m_FontSize: 28
m_FontStyle: 0
m_BestFit: 0
m_MinSize: 10
m_MaxSize: 40
m_Alignment: 4
m_AlignByGeometry: 0
m_RichText: 1
m_HorizontalOverflow: 0
m_VerticalOverflow: 0
m_LineSpacing: 1
m_Text: FlowScope Main Menu
--- !u!1 &8599479246113536964
GameObject:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
serializedVersion: 6
m_Component:
- component: {fileID: 9086714161511357950}
- component: {fileID: 2312868584952693925}
- component: {fileID: 9151466043238822226}
m_Layer: 0
m_Name: Label
m_TagString: Untagged
m_Icon: {fileID: 0}
m_NavMeshLayer: 0
m_StaticEditorFlags: 0
m_IsActive: 1
--- !u!224 &9086714161511357950
RectTransform:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 8599479246113536964}
m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
m_LocalPosition: {x: 0, y: 0, z: 0}
m_LocalScale: {x: 1, y: 1, z: 1}
m_ConstrainProportionsScale: 0
m_Children: []
m_Father: {fileID: 8836034375789789371}
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
m_AnchorMin: {x: 0, y: 0}
m_AnchorMax: {x: 1, y: 1}
m_AnchoredPosition: {x: 0, y: 0}
m_SizeDelta: {x: 0, y: 0}
m_Pivot: {x: 0.5, y: 0.5}
--- !u!222 &2312868584952693925
CanvasRenderer:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 8599479246113536964}
m_CullTransparentMesh: 1
--- !u!114 &9151466043238822226
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 8599479246113536964}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: 5f7201a12d95ffc409449d95f23cf332, type: 3}
m_Name:
m_EditorClassIdentifier:
m_Material: {fileID: 0}
m_Color: {r: 1, g: 1, b: 1, a: 1}
m_RaycastTarget: 1
m_RaycastPadding: {x: 0, y: 0, z: 0, w: 0}
m_Maskable: 1
m_OnCullStateChanged:
m_PersistentCalls:
m_Calls: []
m_FontData:
m_Font: {fileID: 10102, guid: 0000000000000000e000000000000000, type: 0}
m_FontSize: 20
m_FontStyle: 0
m_BestFit: 0
m_MinSize: 10
m_MaxSize: 40
m_Alignment: 4
m_AlignByGeometry: 0
m_RichText: 1
m_HorizontalOverflow: 0
m_VerticalOverflow: 0
m_LineSpacing: 1
m_Text: Add Gold

View File

@@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: 05cda67df190bb746b57ab6f944469c5
PrefabImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,44 @@
# MainMenuP0 Package Sample
`MainMenuP0` is the minimal FlowScope package sample. It demonstrates the recommended bootstrap path for a small Unity project without introducing a full game template.
## What It Covers
- Creating a root `Container`.
- Registering config, resource, save, audio, UI, preload, and screen navigation services.
- Registering `MainMenuFeature` with `RegisterTransient`.
- Preloading `MainMenuPanel` through `UIPreloadService`.
- Opening the main menu through `UIScreenNavigator`.
- Updating `PlayerData.Gold` through R3-backed view data.
- Saving data and releasing preloaded resources during `ShutdownAsync`.
## How To Import
In a consuming Unity project, open Package Manager, select `FlowScope Game Core`, and import the `MainMenuP0` sample.
Do not import this package sample into the FlowScope development project while `Assets/FlowScope/Samples/MainMenuP0` is still present. The development project keeps the Assets-based sample as the source of truth for tests during P2 migration.
## How To Run
After importing the sample, open:
```text
Assets/Samples/FlowScope Game Core/<version>/MainMenuP0/Scenes/MainMenuP0.unity
```
Enter Play Mode. The scene should show the main menu panel, a Gold text, and an increment button.
## Validation Points
- The first startup shows default Gold.
- Clicking the button increments Gold immediately.
- `ShutdownAsync` writes the `main-menu-player` save.
- Restarting restores the last saved Gold.
- Shutdown releases preloaded UI resources.
- Console has no unexpected Error or Exception.
## Current Limits
- The sample uses `SampleResourceBackend` instead of real Addressables assets.
- Runtime fallback UI exists so the sample can still run if scene or prefab references are missing.
- P2 package tests are not migrated yet; current automated tests still live under the FlowScope development project's `Assets/FlowScope/Tests`.

View File

@@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: 9df9d94a34fa4ec2b16df2c8cf2a651b
TextScriptImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 946526d21b06440688088de21f383782
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,440 @@
%YAML 1.1
%TAG !u! tag:unity3d.com,2011:
--- !u!29 &1
OcclusionCullingSettings:
m_ObjectHideFlags: 0
serializedVersion: 2
m_OcclusionBakeSettings:
smallestOccluder: 5
smallestHole: 0.25
backfaceThreshold: 100
m_SceneGUID: 00000000000000000000000000000000
m_OcclusionCullingData: {fileID: 0}
--- !u!104 &2
RenderSettings:
m_ObjectHideFlags: 0
serializedVersion: 9
m_Fog: 0
m_FogColor: {r: 0.5, g: 0.5, b: 0.5, a: 1}
m_FogMode: 3
m_FogDensity: 0.01
m_LinearFogStart: 0
m_LinearFogEnd: 300
m_AmbientSkyColor: {r: 0.212, g: 0.227, b: 0.259, a: 1}
m_AmbientEquatorColor: {r: 0.114, g: 0.125, b: 0.133, a: 1}
m_AmbientGroundColor: {r: 0.047, g: 0.043, b: 0.035, a: 1}
m_AmbientIntensity: 1
m_AmbientMode: 0
m_SubtractiveShadowColor: {r: 0.42, g: 0.478, b: 0.627, a: 1}
m_SkyboxMaterial: {fileID: 10304, guid: 0000000000000000f000000000000000, type: 0}
m_HaloStrength: 0.5
m_FlareStrength: 1
m_FlareFadeSpeed: 3
m_HaloTexture: {fileID: 0}
m_SpotCookie: {fileID: 10001, guid: 0000000000000000e000000000000000, type: 0}
m_DefaultReflectionMode: 0
m_DefaultReflectionResolution: 128
m_ReflectionBounces: 1
m_ReflectionIntensity: 1
m_CustomReflection: {fileID: 0}
m_Sun: {fileID: 0}
m_UseRadianceAmbientProbe: 0
--- !u!157 &3
LightmapSettings:
m_ObjectHideFlags: 0
serializedVersion: 12
m_GIWorkflowMode: 1
m_GISettings:
serializedVersion: 2
m_BounceScale: 1
m_IndirectOutputScale: 1
m_AlbedoBoost: 1
m_EnvironmentLightingMode: 0
m_EnableBakedLightmaps: 1
m_EnableRealtimeLightmaps: 0
m_LightmapEditorSettings:
serializedVersion: 12
m_Resolution: 2
m_BakeResolution: 40
m_AtlasSize: 1024
m_AO: 0
m_AOMaxDistance: 1
m_CompAOExponent: 1
m_CompAOExponentDirect: 0
m_ExtractAmbientOcclusion: 0
m_Padding: 2
m_LightmapParameters: {fileID: 0}
m_LightmapsBakeMode: 1
m_TextureCompression: 1
m_FinalGather: 0
m_FinalGatherFiltering: 1
m_FinalGatherRayCount: 256
m_ReflectionCompression: 2
m_MixedBakeMode: 2
m_BakeBackend: 1
m_PVRSampling: 1
m_PVRDirectSampleCount: 32
m_PVRSampleCount: 512
m_PVRBounces: 2
m_PVREnvironmentSampleCount: 256
m_PVREnvironmentReferencePointCount: 2048
m_PVRFilteringMode: 1
m_PVRDenoiserTypeDirect: 1
m_PVRDenoiserTypeIndirect: 1
m_PVRDenoiserTypeAO: 1
m_PVRFilterTypeDirect: 0
m_PVRFilterTypeIndirect: 0
m_PVRFilterTypeAO: 0
m_PVREnvironmentMIS: 1
m_PVRCulling: 1
m_PVRFilteringGaussRadiusDirect: 1
m_PVRFilteringGaussRadiusIndirect: 5
m_PVRFilteringGaussRadiusAO: 2
m_PVRFilteringAtrousPositionSigmaDirect: 0.5
m_PVRFilteringAtrousPositionSigmaIndirect: 2
m_PVRFilteringAtrousPositionSigmaAO: 1
m_ExportTrainingData: 0
m_TrainingDataDestination: TrainingData
m_LightProbeSampleCountMultiplier: 4
m_LightingDataAsset: {fileID: 0}
m_LightingSettings: {fileID: 0}
--- !u!196 &4
NavMeshSettings:
serializedVersion: 2
m_ObjectHideFlags: 0
m_BuildSettings:
serializedVersion: 3
agentTypeID: 0
agentRadius: 0.5
agentHeight: 2
agentSlope: 45
agentClimb: 0.4
ledgeDropHeight: 0
maxJumpAcrossDistance: 0
minRegionArea: 2
manualCellSize: 0
cellSize: 0.16666667
manualTileSize: 0
tileSize: 256
buildHeightMesh: 0
maxJobWorkers: 0
preserveTilesOutsideBounds: 0
debug:
m_Flags: 0
m_NavMeshData: {fileID: 0}
--- !u!1 &204902849
GameObject:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
serializedVersion: 6
m_Component:
- component: {fileID: 204902852}
- component: {fileID: 204902851}
- component: {fileID: 204902850}
m_Layer: 0
m_Name: Main Camera
m_TagString: Untagged
m_Icon: {fileID: 0}
m_NavMeshLayer: 0
m_StaticEditorFlags: 0
m_IsActive: 1
--- !u!81 &204902850
AudioListener:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 204902849}
m_Enabled: 1
--- !u!20 &204902851
Camera:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 204902849}
m_Enabled: 1
serializedVersion: 2
m_ClearFlags: 2
m_BackGroundColor: {r: 0.05, g: 0.06, b: 0.07, a: 1}
m_projectionMatrixMode: 1
m_GateFitMode: 2
m_FOVAxisMode: 0
m_Iso: 200
m_ShutterSpeed: 0.005
m_Aperture: 16
m_FocusDistance: 10
m_FocalLength: 50
m_BladeCount: 5
m_Curvature: {x: 2, y: 11}
m_BarrelClipping: 0.25
m_Anamorphism: 0
m_SensorSize: {x: 36, y: 24}
m_LensShift: {x: 0, y: 0}
m_NormalizedViewPortRect:
serializedVersion: 2
x: 0
y: 0
width: 1
height: 1
near clip plane: 0.3
far clip plane: 1000
field of view: 60
orthographic: 0
orthographic size: 5
m_Depth: 0
m_CullingMask:
serializedVersion: 2
m_Bits: 4294967295
m_RenderingPath: -1
m_TargetTexture: {fileID: 0}
m_TargetDisplay: 0
m_TargetEye: 3
m_HDR: 1
m_AllowMSAA: 1
m_AllowDynamicResolution: 0
m_ForceIntoRT: 0
m_OcclusionCulling: 1
m_StereoConvergence: 10
m_StereoSeparation: 0.022
--- !u!4 &204902852
Transform:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 204902849}
serializedVersion: 2
m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
m_LocalPosition: {x: 0, y: 0, z: 0}
m_LocalScale: {x: 1, y: 1, z: 1}
m_ConstrainProportionsScale: 0
m_Children: []
m_Father: {fileID: 0}
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
--- !u!1 &1061918572
GameObject:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
serializedVersion: 6
m_Component:
- component: {fileID: 1061918574}
- component: {fileID: 1061918573}
m_Layer: 0
m_Name: GameBootstrap
m_TagString: Untagged
m_Icon: {fileID: 0}
m_NavMeshLayer: 0
m_StaticEditorFlags: 0
m_IsActive: 1
--- !u!114 &1061918573
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 1061918572}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: cd97432f2838454cbbf2d19dbe14d7f3, type: 3}
m_Name:
m_EditorClassIdentifier:
hudCanvas: {fileID: 1965407523}
mainMenuPanelPrefab: {fileID: 6821847843857072034, guid: 05cda67df190bb746b57ab6f944469c5, type: 3}
defaultGold: 0
--- !u!4 &1061918574
Transform:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 1061918572}
serializedVersion: 2
m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
m_LocalPosition: {x: 0, y: 0, z: 0}
m_LocalScale: {x: 1, y: 1, z: 1}
m_ConstrainProportionsScale: 0
m_Children: []
m_Father: {fileID: 0}
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
--- !u!1 &1965407522
GameObject:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
serializedVersion: 6
m_Component:
- component: {fileID: 1965407526}
- component: {fileID: 1965407523}
- component: {fileID: 1965407525}
- component: {fileID: 1965407524}
m_Layer: 0
m_Name: HUD Canvas
m_TagString: Untagged
m_Icon: {fileID: 0}
m_NavMeshLayer: 0
m_StaticEditorFlags: 0
m_IsActive: 1
--- !u!223 &1965407523
Canvas:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 1965407522}
m_Enabled: 1
serializedVersion: 3
m_RenderMode: 0
m_Camera: {fileID: 0}
m_PlaneDistance: 100
m_PixelPerfect: 0
m_ReceivesEvents: 1
m_OverrideSorting: 0
m_OverridePixelPerfect: 0
m_SortingBucketNormalizedSize: 0
m_VertexColorAlwaysGammaSpace: 0
m_AdditionalShaderChannelsFlag: 0
m_UpdateRectTransformForStandalone: 0
m_SortingLayerID: 0
m_SortingOrder: 0
m_TargetDisplay: 0
--- !u!114 &1965407524
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 1965407522}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: dc42784cf147c0c48a680349fa168899, type: 3}
m_Name:
m_EditorClassIdentifier:
m_IgnoreReversedGraphics: 1
m_BlockingObjects: 0
m_BlockingMask:
serializedVersion: 2
m_Bits: 4294967295
--- !u!114 &1965407525
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 1965407522}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: 0cd44c1031e13a943bb63640046fad76, type: 3}
m_Name:
m_EditorClassIdentifier:
m_UiScaleMode: 0
m_ReferencePixelsPerUnit: 100
m_ScaleFactor: 1
m_ReferenceResolution: {x: 800, y: 600}
m_ScreenMatchMode: 0
m_MatchWidthOrHeight: 0
m_PhysicalUnit: 3
m_FallbackScreenDPI: 96
m_DefaultSpriteDPI: 96
m_DynamicPixelsPerUnit: 1
m_PresetInfoIsWorld: 0
--- !u!224 &1965407526
RectTransform:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 1965407522}
m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
m_LocalPosition: {x: 0, y: 0, z: 0}
m_LocalScale: {x: 0, y: 0, z: 0}
m_ConstrainProportionsScale: 0
m_Children: []
m_Father: {fileID: 0}
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
m_AnchorMin: {x: 0, y: 0}
m_AnchorMax: {x: 0, y: 0}
m_AnchoredPosition: {x: 0, y: 0}
m_SizeDelta: {x: 0, y: 0}
m_Pivot: {x: 0, y: 0}
--- !u!1 &2023591418
GameObject:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
serializedVersion: 6
m_Component:
- component: {fileID: 2023591421}
- component: {fileID: 2023591420}
- component: {fileID: 2023591419}
m_Layer: 0
m_Name: EventSystem
m_TagString: Untagged
m_Icon: {fileID: 0}
m_NavMeshLayer: 0
m_StaticEditorFlags: 0
m_IsActive: 1
--- !u!114 &2023591419
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 2023591418}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: 4f231c4fb786f3946a6b90b886c48677, type: 3}
m_Name:
m_EditorClassIdentifier:
m_SendPointerHoverToParent: 1
m_HorizontalAxis: Horizontal
m_VerticalAxis: Vertical
m_SubmitButton: Submit
m_CancelButton: Cancel
m_InputActionsPerSecond: 10
m_RepeatDelay: 0.5
m_ForceModuleActive: 0
--- !u!114 &2023591420
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 2023591418}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: 76c392e42b5098c458856cdf6ecaaaa1, type: 3}
m_Name:
m_EditorClassIdentifier:
m_FirstSelected: {fileID: 0}
m_sendNavigationEvents: 1
m_DragThreshold: 10
--- !u!4 &2023591421
Transform:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 2023591418}
serializedVersion: 2
m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
m_LocalPosition: {x: 0, y: 0, z: 0}
m_LocalScale: {x: 1, y: 1, z: 1}
m_ConstrainProportionsScale: 0
m_Children: []
m_Father: {fileID: 0}
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
--- !u!1660057539 &9223372036854775807
SceneRoots:
m_ObjectHideFlags: 0
m_Roots:
- {fileID: 204902852}
- {fileID: 2023591421}
- {fileID: 1965407526}
- {fileID: 1061918574}

View File

@@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: 03630d34cf304668afe00ab221f3557e
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 93879cc02e2644039382a2006c5434b1
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,303 @@
using System;
using System.Collections.Generic;
using System.Runtime.ExceptionServices;
using System.Threading;
using System.Threading.Tasks;
using FlowScope.Audio;
using FlowScope.Config;
using FlowScope.Flow;
using FlowScope.Resources;
using FlowScope.Save;
using FlowScope.UI;
using UnityEngine;
using UnityEngine.UI;
using ContainerType = FlowScope.Container.Container;
namespace FlowScope.Samples.MainMenuP0
{
public sealed class GameBootstrap : MonoBehaviour
{
private const string SaveKey = "main-menu-player";
[SerializeField] private Canvas hudCanvas;
[SerializeField] private MainMenuPanel mainMenuPanelPrefab;
[SerializeField] private int defaultGold;
private ContainerType _root;
private GameFlow _gameFlow;
private ISaveService _saveService;
private UIPreloadService _preloadService;
private SampleResourceBackend _sampleBackend;
private PlayerData _playerData;
private CancellationTokenSource _lifetime;
private bool _started;
private bool _shutdownInProgress;
private bool _shutdown;
public int ReleasedResourceCount => _sampleBackend?.ReleaseCount ?? 0;
/// <summary>
/// True after runtime cleanup has completed; shutdown sub-step failures are still reported by ShutdownAsync.
/// </summary>
public bool IsShutdown => _shutdown;
public Task StartupTask { get; private set; }
private void Awake()
{
_lifetime = new CancellationTokenSource();
StartupTask = RunStartupWithLoggingAsync(_lifetime.Token);
}
public async Task StartupAsync(CancellationToken cancellationToken)
{
if (_started)
{
return;
}
_started = true;
_shutdown = false;
_root = new ContainerType();
_gameFlow = new GameFlow();
_sampleBackend = new SampleResourceBackend();
_sampleBackend.Register(
"FlowScope/Samples/MainMenuP0/MainMenuPanel",
ResolveMainMenuPanelPrefab().gameObject);
var resources = new ResourceService(_sampleBackend);
_preloadService = new UIPreloadService(resources);
var uiManager = new UIManager(resources, _preloadService);
uiManager.RegisterLayer("hud", ResolveHudCanvas(), 0, PanelStrategy.Destroy);
var screenNavigator = new UIScreenNavigator(uiManager);
var migrations = new SaveMigrationRegistry();
migrations.Register(new PlayerDataV1ToV2Migration());
_saveService = new SaveService(
new FileSaveStorage(),
new JsonSaveSerializer(2, migrations));
_playerData = await _saveService.LoadAsync(
SaveKey,
new PlayerData { Gold = new R3.ReactiveProperty<int>(defaultGold) },
cancellationToken);
await _preloadService.PreloadAsync<MainMenuPanel>(cancellationToken);
var audioService = gameObject.AddComponent<AudioService>().Initialize(resources);
_root.RegisterInstance(_root);
_root.RegisterInstance<IConfigProvider>(
new JsonConfigProvider(new[]
{
JsonConfigProvider.Mapping<SampleMenuConfig>(
"main_menu.json",
new InMemoryConfigSource(
new Dictionary<string, string>
{
{ "main_menu.json", "[{\"Id\":1,\"Title\":\"Main Menu P1\"}]" }
},
"MainMenuP0"),
new JsonConfigParser())
}));
_root.RegisterInstance<IResourceService>(resources);
_root.RegisterInstance<ISaveService>(_saveService);
_root.RegisterInstance<IAudioService>(audioService);
_root.RegisterInstance(uiManager);
_root.RegisterInstance<IUIPreloadService>(_preloadService);
_root.RegisterInstance<IUIScreenNavigator>(screenNavigator);
_root.RegisterInstance(_playerData);
_root.RegisterTransient(_ => new MainMenuFeature(screenNavigator, _playerData));
await _gameFlow.StartupAsync<MainMenuFeature>(_root, cancellationToken);
}
public async Task ShutdownAsync(CancellationToken cancellationToken)
{
if (_shutdown || _shutdownInProgress)
{
return;
}
_shutdownInProgress = true;
var exceptions = new List<Exception>();
try
{
if (_saveService != null && _playerData != null)
{
try
{
await _saveService.SaveAsync(SaveKey, _playerData, cancellationToken);
}
catch (Exception exception)
{
exceptions.Add(exception);
}
}
if (_gameFlow != null && _gameFlow.State != GameFlowState.Disposed)
{
try
{
await _gameFlow.ShutdownAsync(cancellationToken);
}
catch (Exception exception)
{
exceptions.Add(exception);
}
}
try
{
_preloadService?.ReleaseAll();
}
catch (Exception exception)
{
exceptions.Add(exception);
}
_shutdown = true;
}
finally
{
_shutdownInProgress = false;
}
if (exceptions.Count == 1)
{
ExceptionDispatchInfo.Capture(exceptions[0]).Throw();
}
if (exceptions.Count > 1)
{
throw new AggregateException("MainMenuP0 shutdown completed with failures.", exceptions);
}
}
private void OnApplicationQuit()
{
_lifetime?.Cancel();
_ = RunShutdownWithLoggingAsync(CancellationToken.None);
}
private void OnDestroy()
{
_lifetime?.Cancel();
_lifetime?.Dispose();
}
private async Task RunStartupWithLoggingAsync(CancellationToken cancellationToken)
{
try
{
await StartupAsync(cancellationToken);
}
catch (Exception exception)
{
Debug.LogException(exception);
throw;
}
}
private async Task RunShutdownWithLoggingAsync(CancellationToken cancellationToken)
{
try
{
await ShutdownAsync(cancellationToken);
}
catch (Exception exception)
{
Debug.LogException(exception);
}
}
private Canvas ResolveHudCanvas()
{
if (hudCanvas != null)
{
return hudCanvas;
}
var canvasObject = new GameObject("FlowScope HUD Canvas");
canvasObject.transform.SetParent(transform, false);
hudCanvas = canvasObject.AddComponent<Canvas>();
hudCanvas.renderMode = RenderMode.ScreenSpaceOverlay;
return hudCanvas;
}
private MainMenuPanel ResolveMainMenuPanelPrefab()
{
if (mainMenuPanelPrefab != null)
{
return mainMenuPanelPrefab;
}
var panelObject = new GameObject("MainMenuPanel Template");
panelObject.transform.SetParent(transform, false);
panelObject.SetActive(false);
var panel = panelObject.AddComponent<MainMenuPanel>();
var goldObject = new GameObject("GoldText");
goldObject.transform.SetParent(panelObject.transform, false);
var goldText = goldObject.AddComponent<Text>();
goldText.text = defaultGold.ToString();
goldText.alignment = TextAnchor.MiddleCenter;
var buttonObject = new GameObject("IncrementGoldButton");
buttonObject.transform.SetParent(panelObject.transform, false);
buttonObject.AddComponent<Image>();
var button = buttonObject.AddComponent<Button>();
panel.Configure(goldText, button);
mainMenuPanelPrefab = panel;
return mainMenuPanelPrefab;
}
private sealed class SampleResourceBackend : IResourceBackend
{
private readonly Dictionary<string, GameObject> _prefabs = new();
public string Name => "MainMenuP0Sample";
public int ReleaseCount { get; private set; }
public void Register(string key, GameObject prefab)
{
_prefabs[key] = prefab;
}
public Task<T> LoadAsync<T>(
string key,
CancellationToken cancellationToken)
where T : class
{
cancellationToken.ThrowIfCancellationRequested();
if (typeof(T) == typeof(GameObject) &&
_prefabs.TryGetValue(key, out var prefab))
{
return Task.FromResult(prefab as T);
}
throw new InvalidOperationException($"Sample resource is not registered: {key}");
}
public void Release<T>(string key, T asset)
where T : class
{
ReleaseCount++;
}
}
private sealed class SampleMenuConfig : IConfigRow
{
public int Id { get; set; }
public string Title { get; set; }
}
private sealed class PlayerDataV1ToV2Migration : ISaveMigration
{
public string Key => typeof(PlayerData).FullName;
public int FromVersion => 1;
public int ToVersion => 2;
public string Migrate(string json) => json;
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: cd97432f2838454cbbf2d19dbe14d7f3
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,45 @@
using System.Threading.Tasks;
using FlowScope.Flow;
using FlowScope.UI;
namespace FlowScope.Samples.MainMenuP0
{
public sealed class MainMenuFeature : FeatureBase
{
private readonly IUIScreenNavigator _screenNavigator;
private readonly PlayerData _playerData;
private MainMenuViewModel _viewModel;
public MainMenuFeature(IUIScreenNavigator screenNavigator, PlayerData playerData)
{
_screenNavigator = screenNavigator;
_playerData = playerData;
}
public override Task LoadAsync(FeatureContext context)
{
base.LoadAsync(context);
_viewModel = new MainMenuViewModel(_playerData);
return Task.CompletedTask;
}
public override async Task EnterAsync(FeatureContext context)
{
await _screenNavigator.PushAsync<MainMenuPanel, MainMenuViewModel>(
_viewModel,
context.CancellationToken);
}
public override Task ExitAsync(FeatureContext context)
{
_screenNavigator.Clear();
return base.ExitAsync(context);
}
public override void Dispose()
{
_viewModel = null;
base.Dispose();
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: b015663928874766b56036f39f0aca6d
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,62 @@
using FlowScope.UI;
using UnityEngine;
using UnityEngine.UI;
namespace FlowScope.Samples.MainMenuP0
{
[UIPanel("hud", "FlowScope/Samples/MainMenuP0/MainMenuPanel")]
public sealed class MainMenuPanel : UIPanelBase<MainMenuViewModel>
{
[SerializeField] private Text goldText;
[SerializeField] private Button incrementButton;
public void Configure(Text goldLabel, Button incrementGoldButton)
{
goldText = goldLabel;
incrementButton = incrementGoldButton;
}
protected override void OnBind(MainMenuViewModel viewModel)
{
if (goldText == null)
{
goldText = GetComponentInChildren<Text>(true);
}
if (incrementButton == null)
{
incrementButton = GetComponentInChildren<Button>(true);
}
UpdateGold(viewModel.Gold.Value);
viewModel.GoldChanged += UpdateGold;
if (incrementButton != null)
{
incrementButton.onClick.AddListener(viewModel.IncrementGold);
}
}
public override void Unbind()
{
if (ViewModel != null)
{
ViewModel.GoldChanged -= UpdateGold;
}
if (incrementButton != null)
{
incrementButton.onClick.RemoveAllListeners();
}
base.Unbind();
}
private void UpdateGold(int value)
{
if (goldText != null)
{
goldText.text = value.ToString();
}
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 3a641bd6447847f2af2cd5af40305a5a
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,24 @@
using System;
using R3;
namespace FlowScope.Samples.MainMenuP0
{
public sealed class MainMenuViewModel
{
private readonly PlayerData _data;
public MainMenuViewModel(PlayerData data)
{
_data = data ?? throw new ArgumentNullException(nameof(data));
}
public ReactiveProperty<int> Gold => _data.Gold;
public event Action<int> GoldChanged;
public void IncrementGold()
{
Gold.Value++;
GoldChanged?.Invoke(Gold.Value);
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 68e2343c920540a280ac2718ceae12ed
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,9 @@
using R3;
namespace FlowScope.Samples.MainMenuP0
{
public sealed class PlayerData
{
public ReactiveProperty<int> Gold { get; set; } = new(0);
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 8d62814450dc46fba84df1fba2a372f4
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 9593988203244d3796c30233d04321de
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: f7e85b5827334420a6d81b9f37a5c8d1
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,26 @@
{
"name": "FlowScope.PackageTests.EditMode",
"rootNamespace": "FlowScope.PackageTests.EditMode",
"references": [
"FlowScope.Runtime",
"FlowScope.Addressables",
"R3.Unity"
],
"includePlatforms": [
"Editor"
],
"excludePlatforms": [],
"allowUnsafeCode": false,
"overrideReferences": true,
"precompiledReferences": [
"R3.dll",
"nunit.framework.dll"
],
"autoReferenced": false,
"defineConstraints": [],
"versionDefines": [],
"optionalUnityReferences": [
"TestAssemblies"
],
"noEngineReferences": false
}

View File

@@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: e213b3e4bd60472087919ca2efba94bc
AssemblyDefinitionImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,70 @@
using System.Threading;
using System.Threading.Tasks;
using FlowScope.Addressables;
using FlowScope.Container;
using FlowScope.Resources;
using NUnit.Framework;
namespace FlowScope.PackageTests.EditMode
{
public sealed class PackageSmokeTests
{
[Test]
public void PackageAssemblies_CanBeReferencedFromPackageTests()
{
var container = new Container();
var backend = new AddressablesResourceBackend();
IResourceService resources = new ResourceService(new ImmediateBackend());
container.RegisterInstance(resources);
Assert.AreEqual("Addressables", backend.Name);
Assert.AreSame(resources, container.Resolve<IResourceService>());
container.Dispose();
}
[Test]
public void ResourceService_LoadAsync_CanRunInsidePackageTestAssembly()
{
var backend = new ImmediateBackend();
var service = new ResourceService(backend);
using var handle = service
.LoadAsync<TestAsset>("package-smoke", CancellationToken.None)
.GetAwaiter()
.GetResult();
Assert.That(handle.Asset, Is.Not.Null);
Assert.AreEqual(1, backend.LoadCalls);
handle.Dispose();
Assert.AreEqual(1, backend.ReleaseCalls);
}
private sealed class ImmediateBackend : IResourceBackend
{
public string Name => "Immediate";
public int LoadCalls { get; private set; }
public int ReleaseCalls { get; private set; }
public Task<T> LoadAsync<T>(string key, CancellationToken cancellationToken)
where T : class
{
LoadCalls++;
return Task.FromResult(new TestAsset() as T);
}
public void Release<T>(string key, T asset)
where T : class
{
ReleaseCalls++;
}
}
private sealed class TestAsset
{
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: adbf3a7109f64fea8a6c75febc85d0f7
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,22 @@
# FlowScope Package Tests
This directory contains the first package-level test slice for FlowScope Game Core.
The current tests are intentionally small. They prove that a package test assembly can reference:
- `FlowScope.Runtime`
- `FlowScope.Addressables`
- Unity Test Framework / NUnit
The full development test suite still lives under `My project/Assets/FlowScope/Tests` during P2 migration. Real Addressables fixture validation also remains in the development project because Addressables settings are project-level assets.
## Current Scope
- `EditMode/PackageSmokeTests.cs`
## Not Migrated Yet
- Full Runtime EditMode tests.
- Full PlayMode tests.
- MainMenuP0 sample tests.
- Real Addressables fixture tests.

View File

@@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: 0b532bf970be439aab8ef4bc3eb5cf2c
TextScriptImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -1,7 +1,7 @@
{
"name": "com.flowscope.gamecore",
"displayName": "FlowScope Game Core",
"version": "0.2.0-preview.1",
"version": "0.2.0-preview.2",
"unity": "2022.3",
"description": "A lightweight Unity game flow, resource, config, save, audio, and UI runtime foundation.",
"keywords": [
@@ -17,5 +17,12 @@
"dependencies": {
"com.unity.addressables": "1.21.21",
"com.unity.ugui": "1.0.0"
},
"samples": [
{
"displayName": "MainMenuP0",
"description": "Minimal FlowScope bootstrap, feature, UI, save, and preload sample.",
"path": "Samples~/MainMenuP0"
}
]
}

View File

@@ -1,14 +1,43 @@
# FlowScope 安装与最小接入
日期2026-05-26
日期2026-06-06
本文说明 FlowScope 在 P2 第一阶段面向使用者的安装路径、项目内开发模式,以及基于当前真实 API 的最小 Bootstrap 写法。当前仓库尚未完成标准 Unity Package 物理搬迁package path 与 Git URL 方式是目标接入形态;项目内 `Assets/FlowScope` 开发模式是当前可用形态
本文说明 FlowScope 在 P2 package/editor 阶段的当前安装方式、最小接入写法和已知限制。当前仓库已经存在真实 Unity Package 目录 `Packages/com.flowscope.gamecore`,同时仍保留 `My project/Assets/FlowScope` 作为开发工程内的测试与样例源
## 安装方式
## 当前可用安装方式
### Git URL
消费方项目推荐通过 SSH Git URL 安装,并固定到 preview tag 或明确 commit
```json
{
"scopedRegistries": [
{
"name": "Unity NuGet",
"url": "https://unitynuget-registry.openupm.com",
"scopes": [
"org.nuget"
]
}
],
"dependencies": {
"com.cysharp.r3": "https://github.com/Cysharp/R3.git?path=src/R3.Unity/Assets/R3.Unity#1.3.0",
"com.flowscope.gamecore": "ssh://git@git.zz.com:2222/Developer/FlowScope.git?path=/Packages/com.flowscope.gamecore#v0.2.0-preview.2",
"org.nuget.r3": "1.3.0"
}
}
```
注意:
- `com.cysharp.r3` 提供 Unity 适配层。
- `org.nuget.r3` 提供 `R3.dll` 和相关 BCL DLL。
- Unity Package Manager 不可靠支持在 package 的 `package.json` 中声明 Git dependency因此 R3 必须由消费方 manifest 显式声明。
- 当前 `v0.2.0-preview.2` 覆盖空白项目 Git URL 安装、最小 smoke probe 编译、`Documentation~` 导入内容,以及 `Samples~/MainMenuP0` 手动导入验收。
### 本地 package path
当后续完成标准包目录后,可在使用方 Unity 项目的 `Packages/manifest.json` 中使用本地路径:
本机调试时可在消费方 Unity 项目的 `Packages/manifest.json` 中使用本地路径:
```json
{
@@ -19,66 +48,59 @@
```
适用场景:
- 本机同时开发 FlowScope 与游戏项目
- 需要在包仓库中调试 Runtime、Addressables、Editor 或 Samples
- 希望由 Unity Package Manager 识别包结构与 `Samples~`
注意P2 第一阶段当前仓库还没有 `Packages/com.flowscope.gamecore` 物理目录,因此此方式需要等第二阶段本地 package 试迁移完成后再用于实装验证。
### Git URL
当包目录与发布分支稳定后,可在 `Packages/manifest.json` 中使用 Git URL
```json
{
"dependencies": {
"com.flowscope.gamecore": "<最终发布仓库 Git URL>?path=/Packages/com.flowscope.gamecore#v0.2.0"
}
}
```
适用场景:
- 使用方只消费稳定版本,不直接修改 FlowScope 源码。
- 团队希望通过 tag 或固定 commit 控制接入版本。
当前仓库 `origin` 仍带有旧项目名,不应直接写入对外安装文档。发布前需要由团队确认最终 FlowScope 包仓库地址;使用 Git URL 时应固定 tag 或 commit避免使用浮动分支导致使用方项目在不同时间解析到不同代码。
- 同时开发 FlowScope 和消费方项目。
- 需要快速验证 Runtime、Addressables、Editor、Samples 或 Tests package 布局
- 不希望每次小改都重新打 Git tag
### 项目内 Assets 开发模式
当前可用模式是将 FlowScope 放在 Unity 项目内
```text
Assets/
FlowScope/
Runtime/
Addressables/
Samples/
Tests/
```
本仓库当前路径为:
FlowScope 自身开发工程仍保留
```text
My project/Assets/FlowScope
```
适用场景
该路径继续承担
- P0/P1 Runtime 回归测试。
- 主项目内 Addressables 真实 fixture 验收。
- MainMenuP0 Assets 样例源。
- 迁移到 package `Samples~` / `Tests~` 前的稳定参照。
- P2 第一阶段继续验证 P0/P1 Runtime 行为
- 并行开发 Addressables 验收、Editor 诊断与 package 文档。
- 在包迁移前保持 `MainMenuP0` 示例与测试路径稳定。
不要在消费方项目同时复制 `Assets/FlowScope` 并安装 Git package否则会混淆“项目内源码导入”和“package 消费”两条验证路径
使用此模式时,不需要在 `manifest.json` 中声明 `com.flowscope.gamecore`。仍需要确保项目已有当前代码依赖的包,例如 `com.cysharp.r3``com.unity.addressables``com.unity.ugui`
## 当前 package 内容
```text
Packages/com.flowscope.gamecore/
package.json
Runtime/
Addressables/
Editor/
Samples~/
Tests~/
Documentation~/
README.md
CHANGELOG.md
```
已完成:
- Runtime / Addressables / Editor 镜像到 package。
- `Samples~/MainMenuP0` 作为 package sample 候选,并已在空白消费方手动导入验收。
- `Tests~/EditMode` 第一刀最小 smoke 测试。
- Git package 在 FishingFrenzy 中完成最小消费方验证。
- `v0.2.0-preview.2` 已在 `TestFlowScopePackage` 完成空白消费方刷新确认。
不作为 P2 package/editor 收尾阻塞:
- package `Tests~` 全量策略与 PlayMode 测试迁移。
- 是否将 package 作为源码主线。
## 最小 Bootstrap 示例
下面示例只使用当前代码中已经存在的 API 名称。示例使用 Addressables 作为资源后端,适合未来包接入;如果项目暂时没有 Addressables 资源,也可以参考 `MainMenuP0` 示例中的自定义 `IResourceBackend` 做本地验证。
下面示例只使用当前真实 API 名称。示例使用 Addressables 作为资源后端;如果项目暂时没有 Addressables 资源,也可以参考 `MainMenuP0` 中的 `SampleResourceBackend` 做本地验证。
```csharp
using System.Collections.Generic;
using System;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using FlowScope.Addressables;
@@ -213,67 +235,36 @@ public sealed class FlowScopeBootstrap : MonoBehaviour
_screenNavigator = screenNavigator;
}
public Task LoadAsync(FeatureContext context)
{
return Task.CompletedTask;
}
public Task EnterAsync(FeatureContext context)
{
return Task.CompletedTask;
}
public Task ExitAsync(FeatureContext context)
{
return Task.CompletedTask;
}
public void Dispose()
{
}
public Task LoadAsync(FeatureContext context) => Task.CompletedTask;
public Task EnterAsync(FeatureContext context) => Task.CompletedTask;
public Task ExitAsync(FeatureContext context) => Task.CompletedTask;
public void Dispose() { }
}
}
```
接入要点
## 接入要点
- `GameFlow.StartupAsync<TInitialFeature>(Container root, CancellationToken cancellationToken)` 会先解析并加载 `IConfigProvider`
- 初始 Feature 必须实现 `IFeature`,并能从 Feature scope 中解析;示例使用 `_root.RegisterTransient(_ => new ExampleFeature(screenNavigator))`
- 业务资源统一通过 `IResourceService``IResourceGroup` 加载,不直接在业务层调用 Addressables。
- `GameFlow.StartupAsync<TInitialFeature>` 会先解析并加载 `IConfigProvider`
- 初始 Feature 必须实现 `IFeature`,并能从 root 或 Feature scope 中解析。
- 业务资源统一通过 `IResourceService``IResourceGroup` 加载,不直接调用 Addressables。
- UI 层使用 `UIManager.RegisterLayer(...)` 建立层级,再由 `UIScreenNavigator` 或业务 Feature 打开面板。
- Shutdown 时应调用 `GameFlow.ShutdownAsync(...)`,并释放 UI 预加载资源,例如 `_preloadService.ReleaseAll()`
## Addressables 接入说明
使用 Addressables 时,推荐注册:
```csharp
IResourceService resources = new AddressablesResourceService(
new AddressablesResourceBackend());
```
这会把业务层依赖收敛到 `IResourceService``AddressablesResourceBackend` 只位于 Addressables 适配程序集Runtime 不需要知道 Addressables 的存在。
如果某个系统需要独立资源生命周期,应通过 `resources.CreateGroup()` 创建 `IResourceGroup`,让 Feature 的资源释放跟随 `FeatureContext.Resources.Dispose()`
## 常见误用
- 不要让业务 Feature 直接调用 `UnityEngine.AddressableAssets.Addressables`;应通过 `IResourceService``IResourceGroup` 访问资源
- 不要把短生命周期对象注册 `RegisterInstance``RegisterSingletonFactory`;临时对象优先使用 `RegisterTransient`,需要随 Feature 生命周期释放的对象放在 Feature scope
- 不要跳过 `GameFlow.ShutdownAsync(...)`;它负责退出当前 Feature、释放 Feature scope并处置 root container
- 不要把 `UIScreenNavigator`完整页面路由系统;它是当前 UI 导航辅助,不负责 URL、跨 Feature 页面恢复或全局路由
- 不要让业务 Feature 直接调用 `UnityEngine.AddressableAssets.Addressables`
- 不要把短生命周期对象注册 `RegisterInstance``RegisterSingletonFactory`
- 不要跳过 `GameFlow.ShutdownAsync(...)`
- 不要把 `UIScreenNavigator`完整页面路由系统。
- 不要让 `FlowScope.Runtime` 反向依赖 Addressables、Editor、Samples 或 Tests。
- 不要在没有注册 UI layer 的情况下打开面板`UIManager.OpenAsync` 会要求面板声明的 layer 已经通过 `RegisterLayer` 注册
- 不要在未初始化 `AudioService.Initialize(IResourceService, AudioServiceConfig)` 前调用播放接口。
- 不要假设 package path 与 Git URL 在 P2 第一阶段已经可安装;当前真实可用形态仍是项目内 `Assets/FlowScope` 开发模式。
- 不要在没有注册 UI layer 的情况下打开面板。
- 不要在未初始化 `AudioService.Initialize(...)` 前调用播放接口。
## 当前限制
P2 第一阶段文档完成后FlowScope 仍需要后续 worker 或阶段完成以下验证后,才能把 package 安装方式视为可交付
P2 package/editor 已随 `v0.2.0-preview.2` 完成收口。后续仍需要单独规划
1. 建立真实 package 根目录与 `package.json`
2. 通过本地 package path 在干净 Unity 项目中验证安装
3. 验证 Git URL 安装与依赖解析。
4.`MainMenuP0``Assets/FlowScope/Samples` 整理为可导入 package sample。
5. 在干净 Unity 项目中复验 Addressables 测试资源、settings 与 PlayMode 用例;主项目内的真实 load/release 验收记录已完成。
6. 保持 generated csproj build 与 Unity Test Runner 记录为 0 error。
1. package `Tests~` 全量策略和 PlayMode 测试迁移策略
2. 是否将 package 作为源码主线
3. 更完整的发布流水线和版本兼容策略。

View File

@@ -1,86 +1,87 @@
# FlowScope Package 目标结构
# FlowScope Package 结构与迁移边界
日期2026-05-26
日期2026-06-06
本文说明 FlowScope P2 阶段的目标 Unity Package 目录形态,以及第一阶段暂不搬迁 `Assets/FlowScope` 的边界。本文只描述目标结构与迁移约束,不代表当前仓库已经完成物理搬迁
本文说明 FlowScope P2 阶段当前 package 结构、仍保留的 Assets 开发模式,以及后续迁移边界
## 当前状态
当前 FlowScope 仍处于项目内开发模式
FlowScope 当前采用双轨结构
```text
My project/
Assets/
FlowScope/
Runtime/
Addressables/
Samples/
Tests/
My project/Assets/FlowScope
Packages/com.flowscope.gamecore
```
当前尚未建立标准 Unity Package 根目录,未发现包级 `package.json``Samples~``Tests~``Documentation~` 目录。`MainMenuP0` 示例仍位于 `Assets/FlowScope/Samples/MainMenuP0`
`Assets/FlowScope` 仍是开发工程内的测试和样例源;`Packages/com.flowscope.gamecore` 是面向消费方的 Unity Package 雏形
## 目标包根目录
目标包建议使用独立目录承载,例如:
当前 package 目录:
```text
Packages/
com.flowscope.gamecore/
Packages/com.flowscope.gamecore/
package.json
Runtime/
Addressables/
Editor/
Samples~/
Tests/
Tests~/
Documentation~/
README.md
CHANGELOG.md
LICENSE.md
```
目录职责如下
已完成
- Runtime 镜像到 `Runtime/`
- Addressables adapter 镜像到 `Addressables/`
- Editor 诊断窗口镜像到 `Editor/`
- MainMenuP0 镜像到 `Samples~/MainMenuP0`
- `Tests~/EditMode` 已建立最小 smoke 测试。
- package 顶层 `.meta` 已补齐,避免 immutable package 导入错误。
- `v0.2.0-preview.2` 已完成空白消费方刷新确认。
不作为 P2 package/editor 收尾阻塞:
- package `Tests~` 全量迁移策略。
- 是否将 package 作为源码主线。
## 目录职责
| 目录 | 职责 |
| --- | --- |
| `package.json` | Unity Package Manager 元数据声明包名、显示名、版本、Unity 版本依赖。 |
| `Runtime/` | FlowScope 核心运行时代码,包括 Container、Feature、GameFlow、Resource、Config、Save、Audio、UI 等能力。 |
| `package.json` | Unity Package Manager 元数据声明包名、显示名、版本、Unity 版本依赖和 sample。 |
| `Runtime/` | FlowScope 核心运行时代码,包括 Container、Feature、GameFlow、Resource、Config、Save、Audio、UI。 |
| `Addressables/` | Addressables 适配层,提供 `AddressablesResourceBackend``AddressablesResourceService`。 |
| `Editor/` | 只放编辑器诊断、检查、菜单入口等 Editor-only 内容,不承载运行时逻辑。 |
| `Samples~/` | Package Manager 可导入示例,例如 `MainMenuP0`示例可以依赖 Runtime 与所演示的适配包。 |
| `Tests/` | 包内测试,按 EditMode 与 PlayMode 分层保留测试 asmdef。 |
| `Documentation~/` | Package Manager 文档入口,包含安装接入、诊断与示例说明。 |
| `Editor/` | Editor-only 诊断入口,不承载运行时逻辑,不自动修改用户资源。 |
| `Samples~/` | Package Manager 可导入示例。当前包含 `MainMenuP0`。 |
| `Tests~/` | package 内测试候选。当前先放最小 EditMode smoke 测试,不替代开发工程内完整测试。 |
| `Documentation~/` | Package Manager 文档入口,包含安装接入说明。 |
## package.json 建议形态
目标包元数据建议保持最小可识别:
## package.json 当前形态
```json
{
"name": "com.flowscope.gamecore",
"displayName": "FlowScope Game Core",
"version": "0.2.0",
"version": "0.2.0-preview.2",
"unity": "2022.3",
"description": "A lightweight Unity game flow, resource, config, save, audio, and UI runtime foundation.",
"dependencies": {
"com.cysharp.r3": "https://github.com/Cysharp/R3.git?path=src/R3.Unity/Assets/R3.Unity#1.3.0",
"com.unity.addressables": "1.21.21",
"com.unity.ugui": "1.0.0"
},
"samples": [
{
"displayName": "MainMenuP0",
"description": "Minimal FlowScope bootstrap, feature, UI, save, and preload sample.",
"path": "Samples~/MainMenuP0"
}
]
}
```
依赖说明:
- `com.cysharp.r3` 是当前 `FlowScope.Runtime.asmdef` 直接引用 `R3.Unity` 的来源。
- `com.unity.addressables` 只服务 `FlowScope.Addressables`Runtime 不直接依赖 Addressables。
- `com.unity.ugui` 服务当前 UI 与示例中的 `Canvas``Button``Text` 等 UGUI 类型。
如果后续发布渠道不接受 Git URL 形式依赖,应在发布前改为团队确认过的 OpenUPM、内网 registry 或嵌入式依赖策略。
注意:`com.cysharp.r3``org.nuget.r3` 当前由消费方项目显式声明,原因是 Unity Package Manager 不可靠支持 package 内 Git dependency 传递解析。
## asmdef 依赖方向
当前与目标结构都必须保持以下方向:
必须保持以下方向:
```text
FlowScope.Runtime
@@ -93,45 +94,47 @@ FlowScope.Addressables
FlowScope.Editor
-> FlowScope.Runtime
-> 需要诊断的编辑器程序集
-> 诊断所需的 Editor 程序集
FlowScope.Samples.MainMenuP0
-> FlowScope.Runtime
-> R3.Unity
FlowScope.Tests.EditMode / FlowScope.Tests.PlayMode
-> 被测程序集
FlowScope.PackageTests.EditMode
-> FlowScope.Runtime
-> FlowScope.Addressables
-> NUnit / Unity Test Framework
```
禁止反向依赖:
- `FlowScope.Runtime` 不依赖 `FlowScope.Addressables`
- `FlowScope.Runtime` 不依赖 `FlowScope.Editor`
- `FlowScope.Runtime` 不依赖 `FlowScope.Samples.*`
- `FlowScope.Runtime` 不依赖测试程序集。
- Samples 不作为 Runtime 的依赖来源。
`AddressablesResourceService` 当前命名空间位于 `FlowScope.Resources`,但文件`Assets/FlowScope/Addressables` 并编译进 `FlowScope.Addressables` 程序集;迁移时应保留程序集边界,不应因为命名空间相同而把 Addressables 适配代码并回 Runtime。
`AddressablesResourceService` 当前命名空间 `FlowScope.Resources`,但文件Addressables 适配程序集;迁移和镜像时必须保留程序集边界,不应因为命名空间相同而把 Addressables 适配代码并回 Runtime。
## 第一阶段暂不搬迁 Assets/FlowScope 的原因
## 双轨边界
P2 第一阶段只建立分发边界、安装接入文档、Addressables 验收与最小 Editor 诊断入口,不做物理搬迁,原因如下
当前不删除 `Assets/FlowScope`,原因:
1. 当前 `Assets/FlowScope` 承载 P0/P1 Runtime、Addressables、Samples、Tests 的稳定验证路径
2. `MainMenuP0` 示例、PlayMode 测试与 Unity 生成的 csproj 仍依赖当前 Asset 路径
3. 一次性移动 Runtime、Samples、Tests 会同时改变 asmdef 路径、资源路径、场景引用、prefab 引用与测试发现路径,容易把包结构问题和运行时回归混在一起
4. 第一阶段还有并行 worker 处理 Addressables 验收与 Editor 诊断Package 文档 worker 不应覆盖或移动他们的文件
5. 先用文档锁定目标结构与依赖方向,可以让第二阶段本地 package 试迁移按批次验证,而不是一次性大搬迁。
1. `Assets/FlowScope` 承载完整 EditMode / PlayMode 回归测试
2. 主项目内 Addressables 真实 fixture 和 settings 是当前真实验收来源
3. `Samples~/MainMenuP0` 是 package sample 候选,已在空白消费方手动导入验收
4. 一次性把 Samples、Tests 和 Addressables fixture 全部迁入 package 会扩大验证面
## 建议迁移顺序
当前迁移策略是“镜像 + 逐步验收”:
后续进入 P2 第二阶段时,建议按以下顺序迁移,每一步都保留独立验证记录:
1. package 内保留 Runtime / Addressables / Editor 镜像。
2. package sample 先镜像 MainMenuP0不删除 Assets 样例。
3. package tests 先建立最小 smoke 测试,不删除 Assets 测试。
4. package preview gate 已由 `v0.2.0-preview.2` 收口;是否把 package 作为源码主线留到后续阶段决策。
1. 创建 package 根目录与 `package.json`,先不移动 Runtime。
2.`Runtime/` 移入包目录并验证 `FlowScope.Runtime` 编译边界。
3.`Addressables/` 移入包目录并验证 `FlowScope.Addressables` 仍只依赖 Runtime 与 Unity Addressables。
4. 将文档迁入 `Documentation~/` 或同步保留到仓库 `docs/`
5.`MainMenuP0` 作为包示例迁入 `Samples~/MainMenuP0`验证场景、prefab、配置与测试路径。
6. 将 EditMode / PlayMode 测试整理进包内 `Tests/`,验证 Unity Test Runner 发现与执行。
## 后续迁移顺序
每一步完成前都应验证 generated csproj build、Unity Test Runner 和关键示例路径;如果某一步失败,应先回到该步骤修复,不继续叠加后续搬迁。
P2 package/editor 已收口,后续迁移建议按以下顺序单独规划:
1. 决定哪些测试可迁入 package哪些仍留在开发工程。
2. 规划 package PlayMode 测试和 Addressables fixture 镜像策略。
3. 决定是否把 package 作为源码主线。

View File

@@ -20,7 +20,7 @@
真实 Addressables 测试资源:
- 路径:`Assets/FlowScope/Tests/PlayMode/Resources_moved/Fixtures/Addressables/FlowScopeAddressablesText.txt`
- 路径:`Assets/FlowScope/Tests/PlayMode/Resources/Fixtures/Addressables/FlowScopeAddressablesText.txt`
- 内容:`FlowScope Addressables Acceptance`
- GUID`7e04733ce9dab74459592d3bd99fcafd`
- Addressables group`Default Local Group`
@@ -68,7 +68,7 @@ m_Address: FlowScope.Tests.Addressables.Text
## 剩余注意事项
- 当前 Addressables 测试资源位于 `Resources_moved` 路径下;这是本轮真实验收的有效路径,但后续 package 迁移时应统一测试资源目录命名。
- 当前 Addressables 测试资源已整理到 `Resources/Fixtures/Addressables` 路径下,为后续 package `Tests~` 迁移去掉临时目录命名。
- `Assets/FlowScope/Tests/PlayMode/Resources/Fixtures` 当前只剩空目录 meta属于提交前清理候选。
- 当前验收仍在主项目内完成P2 后续 package 阶段还需要在干净 Unity 项目中验证本地 package path 或 Git URL 安装。

View File

@@ -62,7 +62,7 @@
剩余缺口:
- 当前 Addressables 测试资源路径为 `Assets/FlowScope/Tests/PlayMode/Resources_moved/Fixtures/Addressables/FlowScopeAddressablesText.txt`后续 package 迁移时应统一测试资源目录命名
- 当前 Addressables 测试资源路径为 `Assets/FlowScope/Tests/PlayMode/Resources/Fixtures/Addressables/FlowScopeAddressablesText.txt`早期临时目录命名已在 P2 第二阶段整理
- `Assets/FlowScope/Tests/PlayMode/Resources/Fixtures` 当前只剩空目录 meta属于提交前清理候选。
### 4. Editor 最小诊断入口
@@ -165,7 +165,7 @@ git diff --check
1. 尚未创建正式 package 根目录和 `package.json` 文件。
2. 尚未在干净 Unity 项目中验证本地 package path 或 Git URL 安装。
3. 尚未实际打开 `Tools/FlowScope/Diagnostics` 做人工 UI 检查。
4. Addressables 测试资源目录命名仍需在 package 迁移前统一
4. Addressables 测试资源目录命名已统一到 `Resources/Fixtures/Addressables`,后续仍需决定是否随 Tests 迁移到 package `Tests~`
## 下一阶段建议
@@ -176,5 +176,5 @@ git diff --check
1. 创建 `Packages/com.flowscope.gamecore` 和最小 `package.json`
2. 先复制或镜像 Runtime 到 package 目录,不删除 `Assets/FlowScope`
3. 在当前项目和一个干净 Unity 项目中验证本地 package path。
4. 整理 Addressables 测试资源目录,再迁移 package 内 Tests。
4. 基于已整理 Addressables fixture 路径,迁移或镜像 package 内 Tests。
5. 再考虑 `Samples~/MainMenuP0` 和 Tests 的分批迁移。

View File

@@ -0,0 +1,218 @@
# FlowScope P2 第二阶段 Package Preview 记录
日期2026-06-04
## 本轮范围
本轮承接 P2 第一阶段后的 package 试迁移工作,重点不是改 Runtime API而是把 FlowScope Git package 从“仓库内结构存在”推进到“真实消费方可用”的 preview 状态。
## 已完成事项
1. FlowScope package preview tag
- tag`v0.2.0-preview.1`
- 提交:`31641b2c35d1d6c4f056772f144562007f0d0782`
- 远端:`ssh://git@git.zz.com:2222/Developer/FlowScope.git`
2. 消费方 Git package 接入
- 消费项目:`C:\Users\13999\Documents\FishingFrenzy\FishingFrenzy`
- package URL`ssh://git@git.zz.com:2222/Developer/FlowScope.git?path=/Packages/com.flowscope.gamecore#v0.2.0-preview.1`
- R3 依赖:同时保留 `com.cysharp.r3``org.nuget.r3`
- Unity NuGet registry`https://unitynuget-registry.openupm.com`scope 为 `org.nuget`
3. 最小消费方验证脚本
- 文件:`FishingFrenzy/Assets/Scripts/FlowScopePackageSmokeProbe.cs`
- 覆盖:编译期引用 `FlowScope.Container.Container``FlowScope.Flow.GameFlow``FlowScope.Addressables.AddressablesResourceBackend``FlowScope.Resources.IResourceService`
- 运行方式:挂到临时 GameObject 后,通过组件右键菜单 `FlowScope/Validate Package References` 手动执行。
- 设计边界:不自动挂场景、不自动启动、不加载真实业务资源,只证明消费方工程能引用 package API。
4. 消费方验证结果
- UnitySkills `debug_check_compilation``isCompiling = false``isUpdating = false`
- UnitySkills `debug_get_errors``count = 0`
- UnitySkills `console_get_stats``errors = 0`
- UnitySkills `package_list`:可见 `com.flowscope.gamecore`,版本为 `0.2.0-preview.1`
5. 安装文档同步
- `Packages/com.flowscope.gamecore/README.md`
- `Packages/com.flowscope.gamecore/Documentation~/installation.md`
- 已改为正式 SSH URL、preview tag、Unity NuGet registry 和 `org.nuget.r3` 说明。
## 本轮踩坑记录
1. FlowScope remote 曾残留旧仓库地址
- 本地 `origin` 一度指向旧的 `BombGameFramework` 地址。
- 已改为 `ssh://git@git.zz.com:2222/Developer/FlowScope.git` 后再推送 tag。
2. R3 Git package 不等于完整 R3 依赖
- `com.cysharp.r3` 提供 Unity 适配层。
- `R3.dll` 与 BCL DLL 需要由 `org.nuget.r3` 及其 NuGet 传递依赖提供。
3. Git package 必须保留 Unity `.meta`
- preview tag 基于已补齐 package 顶层 `.meta` 的提交。
- 缺少 `.meta` 时,消费方会出现 immutable package 相关导入错误。
## 剩余 P2 第二阶段事项
1. 当前 preview tag 已可被消费方解析,但 tag 内 package 文档仍是打 tag 当时的快照。后续如果要让 package 内文档也随 tag 发布,应在文档修正后创建下一个 preview tag。
2. 已在后续切片镜像 `Samples~/MainMenuP0`,但尚未在消费方导入 package sample 做人工验收。
3. 已在后续切片建立 package `Tests~/EditMode` 第一刀 smoke 测试,但尚未迁移完整 Runtime/EditMode/PlayMode 测试。
4. Addressables 测试资源已统一到主项目测试目录的 `Resources/Fixtures/Addressables`,后续迁移 Tests 前需要决定是否镜像到 package `Tests~`
5. 仍需在一个真正空白 Unity 项目中重复 Git package 接入FishingFrenzy 是真实消费方,但不是空白项目。
## 2026-06-06 验证补充
本轮按 P2 收尾顺序补做以下检查:
1. Addressables 测试资源 entry
- `Default Local Group.asset` 曾出现本地未提交改动,内容为清空 `FlowScope.Tests.Addressables.Text` 对应 entry。
- 已确认该改动不是预期变更,并恢复到干净工作树;当前 `git status` 无该文件 diff。
2. 静态与生成 csproj 编译验证
- `Packages/com.flowscope.gamecore/package.json` 可被 `ConvertFrom-Json` 解析。
- `Packages/com.flowscope.gamecore/Tests~/EditMode/FlowScope.PackageTests.EditMode.asmdef` 可被 `ConvertFrom-Json` 解析。
- `FlowScope.Runtime.csproj``FlowScope.Addressables.csproj``FlowScope.Tests.EditMode.csproj``FlowScope.Tests.PlayMode.csproj` 均可 `dotnet build --no-restore` 成功。
- 编译仍有 Unity 生成 csproj 常见的 `MSB3277` 引用版本冲突 warning但无错误。
3. Unity Test Runner
- UnitySkills `http://127.0.0.1:8090/health` 当前不可连接。
- 去掉 `-quit` 后重跑 Unity batchmode EditMode Test Runner已生成 `C:\tmp\flowscope-p2-editmode-retry3-results.xml`
- 结果:`testcasecount=56``result=Passed``passed=56``failed=0``skipped=0`
- 注意Unity 在保存结果后未自动退出,已手动停止残留 batchmode 进程;这不影响本次 XML 结果判定。
4. 空白 Unity 项目 Git URL 复验
- 使用 `C:\tmp\flowscope-upm-probe-p2` 手工创建最小 probe 并写入 Git package manifest 后Unity batchmode 能启动,但 UPM 在更新 manifest 时失败:`The "path" argument must be of type string. Received undefined`,未生成 `packages-lock.json`
- 该 probe 不是由 Unity Hub/template 正式创建的完整项目,因此本轮结果不能判定为 FlowScope package 安装失败,只能记录为“手工最小 probe 不足以完成空白项目 gate”。
- 后续仍需使用一个真正由 Unity 创建的空白项目复验 Git URL 安装。
## 2026-06-06 空白项目复验补充
用户已在仓库内创建完整 Unity 空白项目 `TestFlowScopePackage`,本轮将该目录加入根 `.gitignore`,避免误提交验证工程。
验证步骤与结果:
1.`TestFlowScopePackage/Packages/manifest.json` 写入 FlowScope Git package、R3 Git package、Unity NuGet scoped registry 和 `org.nuget.r3`
2. Unity batchmode 打开项目时 UPM 失败:`The "path" argument must be of type string. Received undefined`,未生成可用的 FlowScope package lock。
3. 为定位原因,分别移除 FlowScope、R3 Git package、`org.nuget.r3`、scoped registry 做隔离验证。
4. 清理 `Library``Temp``Packages/packages-lock.json` 后,使用不含 FlowScope/R3/NuGet 的原始 manifest 重跑,仍出现同一个 UPM resolve 错误。
当前判断:该错误不是 FlowScope Git package 本身触发;`TestFlowScopePackage` 在原始 manifest 下也无法完成 UPM resolve。空白项目 gate 需要先在 Unity GUI 中确认该项目自身 Package Manager 可正常打开且不报错,再继续验证 FlowScope Git package。
## 2026-06-06 空白项目复验通过
用户使用 Unity GUI 打开并关闭 `TestFlowScopePackage` 后,项目自身 UPM 状态恢复正常;随后继续复验 FlowScope Git package。
验证结果:
1. `Packages/packages-lock.json` 已解析出:
- `com.flowscope.gamecore`
- source`git`
- version`ssh://git@git.zz.com:2222/Developer/FlowScope.git?path=/Packages/com.flowscope.gamecore#v0.2.0-preview.1`
- hash`31641b2c35d1d6c4f056772f144562007f0d0782`
- `com.cysharp.r3`
- source`git`
- hash`329ca7915713417f2e0837b0e0a80b4da074db4a`
- `org.nuget.r3`
- source`registry`
- version`1.3.0`
2. Unity batchmode 导入验证通过:
- log`C:\tmp\flowscope-testpackage-import-aftergui.log`
- exit code`0`
- 日志可见 `com.flowscope.gamecore@...#v0.2.0-preview.1` 被注册到 `Library/PackageCache/com.flowscope.gamecore@31641b2c35`
3. 空白消费方 smoke probe 编译验证通过:
- 文件:`TestFlowScopePackage/Assets/FlowScopePackageSmokeProbe.cs`
- log`C:\tmp\flowscope-testpackage-smoke-compile2.log`
- exit code`0`
- probe 覆盖 `FlowScope.Container.Container``FlowScope.Resources.ResourceService``FlowScope.Resources.IResourceService``FlowScope.Addressables.AddressablesResourceBackend`
非阻塞注意事项:
- batchmode 日志提示 `Packages/com.flowscope.gamecore/Documentation~` 对应 `.meta` 存在但目录缺失Unity 自动创建了该空目录。下一个 preview tag 前建议补齐 `Documentation~` 实际内容或移除空目录 `.meta`,避免消费方导入时出现该提示。
## 2026-06-06 当前提交导入与 sample 验收尝试
为验证下一个 preview tag 是否能消除 `Documentation~` 空目录提示,临时将忽略工程 `TestFlowScopePackage` 的 manifest 指向当前提交:
- `ssh://git@git.zz.com:2222/Developer/FlowScope.git?path=/Packages/com.flowscope.gamecore#e6d1f00e235a6d7cd81d84cbe9a1f8be2d27b89e`
验证观察:
1. Unity 默认 `Editor.log` 可见 `Packages/com.flowscope.gamecore/Documentation~` 被正常导入,未再出现 `Documentation~.meta` 存在但目录缺失的提示。
2. Unity 默认 `Editor.log` 可见 `Packages/com.flowscope.gamecore/Runtime``Addressables``Editor``README.md``CHANGELOG.md``package.json` 等内容被导入。
3. 未发现 `error CS``Compilation failed``Scripts have compiler errors` 等编译错误日志。
未完成项:
- 尝试通过 `FlowScopeSampleImportVerifier.ImportMainMenuP0` 自动导入 `Samples~/MainMenuP0`,但 `TestFlowScopePackage` 的 batchmode 没有稳定生成验证脚本对应的 Editor 程序集,也没有执行到 sample import 日志。
- 因此本轮只能确认“当前提交已补齐 `Documentation~` 导入内容”,不能宣称 `Samples~/MainMenuP0` 消费方导入验收通过。
后续处理:
1. 下一个 preview tag 可以覆盖 `Documentation~` 空目录 warning。
2. `Samples~/MainMenuP0` 仍需要在 Unity GUI 的 Package Manager 中人工导入,或在验证工程稳定生成 Editor 程序集后再跑自动导入脚本。
## 2026-06-08 MainMenuP0 手动导入与验证脚本修正
用户已在 `TestFlowScopePackage` 通过 Unity GUI 手动导入 `Samples~/MainMenuP0`,本地可见导入目录:
- `TestFlowScopePackage/Assets/Samples/FlowScope Game Core/0.2.0-preview.1/MainMenuP0`
导入后曾出现两个编译错误,来源均为忽略工程中的临时验证脚本 `Assets/Editor/FlowScopeSampleImportVerifier.cs`,不是 FlowScope package 本体:
1. `CS0104: 'PackageInfo' is an ambiguous reference between 'UnityEditor.PackageManager.PackageInfo' and 'UnityEditor.PackageInfo'`
- 修正:使用完整类型名 `UnityEditor.PackageManager.PackageInfo.FindForAssetPath(...)`
2. `CS0019: Operator '==' cannot be applied to operands of type 'Sample' and '<null>'`
- 修正:`UnityEditor.PackageManager.UI.Sample` 不能按 `null` 判断;改用 `string.IsNullOrEmpty(sample.displayName)` 判断是否找到 sample。
当前状态:
- `MainMenuP0` sample 已完成手动导入。
- 临时验证脚本已在 `TestFlowScopePackage` 内修正。
- Unity batchmode 在当前机器状态下仍未稳定生成指定 log 并退出,因此还不能把自动 batchmode sample 验收记为通过。
## 2026-06-08 MainMenuP0 手动导入验收通过
用户重新打开 `TestFlowScopePackage` 后确认:
- Unity Console 已无 `FlowScopeSampleImportVerifier.cs``CS0104` / `CS0019` 编译错误。
- `Samples~/MainMenuP0` 已通过 Unity GUI 手动导入。
本轮判定:
- `Samples~/MainMenuP0` 消费方手动导入验收通过。
- 自动 batchmode sample import 仍不作为通过项记录,因为当前机器状态下 batchmode 未稳定生成指定 log 并退出。
- P2 package preview 可以进入 `0.2.0-preview.2` 版本收口;该版本覆盖空白项目 Git URL 安装、smoke probe 编译、`Documentation~` 内容补齐、`Tests~/EditMode` 第一刀和 `MainMenuP0` 手动导入验收。
## 2026-06-08 preview.2 消费方刷新确认
用户在 `TestFlowScopePackage` 中将 FlowScope package 刷新到 `v0.2.0-preview.2` 后确认:
- Package Manager 显示版本已从 `0.2.0-preview.1` 更新到 `0.2.0-preview.2`
- 本轮刷新过程中未报告新的 Unity Console 编译错误。
问题原因记录:
- 之前未更新到 preview.2 的原因是 `TestFlowScopePackage/Packages/manifest.json` 仍指向临时验证提交 `e6d1f00e235a6d7cd81d84cbe9a1f8be2d27b89e`,该提交内 package 版本仍为 `0.2.0-preview.1`
- 已将该忽略工程的 manifest 改为正式 tag`ssh://git@git.zz.com:2222/Developer/FlowScope.git?path=/Packages/com.flowscope.gamecore#v0.2.0-preview.2`,并清理旧 `packages-lock.json` 与旧 `Library/PackageCache` 后完成刷新。
本轮判定:
- `v0.2.0-preview.2` 已完成空白消费方刷新确认。
- P2 package/editor 的 package preview 收口 gate 已满足;后续可以转回框架功能开发。
## 2026-06-08 P2 package/editor 正式收尾
外部审查结论P2 package/editor 可以正式收尾。
收尾依据:
- 当前分支工作树干净package 版本为 `0.2.0-preview.2`
- `v0.2.0-preview.2` tag 已发布,指向 `b91fc54`
- `TestFlowScopePackage` 已刷新到 `v0.2.0-preview.2`,且未报告新的 Unity Console 编译错误。
- `Samples~/MainMenuP0` 已完成空白消费方手动导入验收。
- 空白项目 Git URL 安装、最小 smoke probe 编译、`Documentation~` 内容补齐、`Tests~/EditMode` 第一刀均已有记录。
不作为当前收尾阻塞:
- package `Tests~` 全量迁移策略和 PlayMode 测试迁移策略。
- 是否将 package 作为源码主线。
下一步:关闭 P2 package/editor 工作面,转回 FlowScope 框架功能开发。