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 /cfgPack/encryptcfg
.gemini/ .gemini/
.worktrees/ .worktrees/
TestFlowScopePackage/
GEMINI.md GEMINI.md
CLAUDE.md CLAUDE.md
AGENTS.md AGENTS.md

View File

@@ -1,8 +1,17 @@
# Changelog # 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 ## 0.2.0-preview.1
- Added the first Git-consumable FlowScope Game Core package layout. - Added the first Git-consumable FlowScope Game Core package layout.
- Mirrored Runtime, Addressables, and Editor sources from `Assets/FlowScope`. - Mirrored Runtime, Addressables, and Editor sources from `Assets/FlowScope`.
- Added package metadata and Git installation notes for consuming Unity projects. - Added package metadata and Git installation notes for consuming Unity projects.

View File

@@ -2,18 +2,28 @@
## Consumer Manifest ## 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 ```json
{ {
"scopedRegistries": [
{
"name": "Unity NuGet",
"url": "https://unitynuget-registry.openupm.com",
"scopes": [
"org.nuget"
]
}
],
"dependencies": { "dependencies": {
"com.cysharp.r3": "https://github.com/Cysharp/R3.git?path=src/R3.Unity/Assets/R3.Unity#1.3.0", "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 ## Validation
@@ -23,5 +33,6 @@ After Unity resolves the package:
2. Open `Tools/FlowScope/Diagnostics`. 2. Open `Tools/FlowScope/Diagnostics`.
3. Confirm Addressables and UGUI dependencies are present. 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`. 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. 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 ```json
{ {
"scopedRegistries": [
{
"name": "Unity NuGet",
"url": "https://unitynuget-registry.openupm.com",
"scopes": [
"org.nuget"
]
}
],
"dependencies": { "dependencies": {
"com.cysharp.r3": "https://github.com/Cysharp/R3.git?path=src/R3.Unity/Assets/R3.Unity#1.3.0", "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 ## Assembly Boundaries
- `FlowScope.Runtime` references `R3.Unity`. - `FlowScope.Runtime` references `R3.Unity`.
- `FlowScope.Addressables` references `FlowScope.Runtime`, `Unity.Addressables`, and `Unity.ResourceManager`. - `FlowScope.Addressables` references `FlowScope.Runtime`, `Unity.Addressables`, and `Unity.ResourceManager`.
- `FlowScope.Editor` is Editor-only and only provides read-only diagnostics. - `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. Runtime must not depend on Addressables, Editor, Samples, or Tests.

View File

@@ -1,5 +1,5 @@
fileFormatVersion: 2 fileFormatVersion: 2
guid: 20098efc5dc925a4194600eea4969d68 guid: 49d9c024747f4af0b560e21d931a7816
folderAsset: yes folderAsset: yes
DefaultImporter: DefaultImporter:
externalObjects: {} 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", "name": "com.flowscope.gamecore",
"displayName": "FlowScope Game Core", "displayName": "FlowScope Game Core",
"version": "0.2.0-preview.1", "version": "0.2.0-preview.2",
"unity": "2022.3", "unity": "2022.3",
"description": "A lightweight Unity game flow, resource, config, save, audio, and UI runtime foundation.", "description": "A lightweight Unity game flow, resource, config, save, audio, and UI runtime foundation.",
"keywords": [ "keywords": [
@@ -17,5 +17,12 @@
"dependencies": { "dependencies": {
"com.unity.addressables": "1.21.21", "com.unity.addressables": "1.21.21",
"com.unity.ugui": "1.0.0" "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 安装与最小接入 # 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 ### 本地 package path
当后续完成标准包目录后,可在使用方 Unity 项目的 `Packages/manifest.json` 中使用本地路径: 本机调试时可在消费方 Unity 项目的 `Packages/manifest.json` 中使用本地路径:
```json ```json
{ {
@@ -19,66 +48,59 @@
``` ```
适用场景: 适用场景:
- 同时开发 FlowScope 和消费方项目。
- 本机同时开发 FlowScope 与游戏项目 - 需要快速验证 Runtime、Addressables、Editor、Samples 或 Tests package 布局
- 需要在包仓库中调试 Runtime、Addressables、Editor 或 Samples - 不希望每次小改都重新打 Git tag
- 希望由 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避免使用浮动分支导致使用方项目在不同时间解析到不同代码。
### 项目内 Assets 开发模式 ### 项目内 Assets 开发模式
当前可用模式是将 FlowScope 放在 Unity 项目内 FlowScope 自身开发工程仍保留
```text
Assets/
FlowScope/
Runtime/
Addressables/
Samples/
Tests/
```
本仓库当前路径为:
```text ```text
My project/Assets/FlowScope My project/Assets/FlowScope
``` ```
适用场景 该路径继续承担
- P0/P1 Runtime 回归测试。
- 主项目内 Addressables 真实 fixture 验收。
- MainMenuP0 Assets 样例源。
- 迁移到 package `Samples~` / `Tests~` 前的稳定参照。
- P2 第一阶段继续验证 P0/P1 Runtime 行为 不要在消费方项目同时复制 `Assets/FlowScope` 并安装 Git package否则会混淆“项目内源码导入”和“package 消费”两条验证路径
- 并行开发 Addressables 验收、Editor 诊断与 package 文档。
- 在包迁移前保持 `MainMenuP0` 示例与测试路径稳定。
使用此模式时,不需要在 `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 示例 ## 最小 Bootstrap 示例
下面示例只使用当前代码中已经存在的 API 名称。示例使用 Addressables 作为资源后端,适合未来包接入;如果项目暂时没有 Addressables 资源,也可以参考 `MainMenuP0` 示例中的自定义 `IResourceBackend` 做本地验证。 下面示例只使用当前真实 API 名称。示例使用 Addressables 作为资源后端;如果项目暂时没有 Addressables 资源,也可以参考 `MainMenuP0` 中的 `SampleResourceBackend` 做本地验证。
```csharp ```csharp
using System.Collections.Generic;
using System; using System;
using System.Collections.Generic;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using FlowScope.Addressables; using FlowScope.Addressables;
@@ -213,67 +235,36 @@ public sealed class FlowScopeBootstrap : MonoBehaviour
_screenNavigator = screenNavigator; _screenNavigator = screenNavigator;
} }
public Task LoadAsync(FeatureContext context) public Task LoadAsync(FeatureContext context) => Task.CompletedTask;
{ public Task EnterAsync(FeatureContext context) => Task.CompletedTask;
return Task.CompletedTask; public Task ExitAsync(FeatureContext context) => Task.CompletedTask;
} public void Dispose() { }
public Task EnterAsync(FeatureContext context)
{
return Task.CompletedTask;
}
public Task ExitAsync(FeatureContext context)
{
return Task.CompletedTask;
}
public void Dispose()
{
}
} }
} }
``` ```
接入要点 ## 接入要点
- `GameFlow.StartupAsync<TInitialFeature>(Container root, CancellationToken cancellationToken)` 会先解析并加载 `IConfigProvider` - `GameFlow.StartupAsync<TInitialFeature>` 会先解析并加载 `IConfigProvider`
- 初始 Feature 必须实现 `IFeature`,并能从 Feature scope 中解析;示例使用 `_root.RegisterTransient(_ => new ExampleFeature(screenNavigator))` - 初始 Feature 必须实现 `IFeature`,并能从 root 或 Feature scope 中解析。
- 业务资源统一通过 `IResourceService``IResourceGroup` 加载,不直接在业务层调用 Addressables。 - 业务资源统一通过 `IResourceService``IResourceGroup` 加载,不直接调用 Addressables。
- UI 层使用 `UIManager.RegisterLayer(...)` 建立层级,再由 `UIScreenNavigator` 或业务 Feature 打开面板。 - UI 层使用 `UIManager.RegisterLayer(...)` 建立层级,再由 `UIScreenNavigator` 或业务 Feature 打开面板。
- Shutdown 时应调用 `GameFlow.ShutdownAsync(...)`,并释放 UI 预加载资源,例如 `_preloadService.ReleaseAll()` - 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` 访问资源 - 不要让业务 Feature 直接调用 `UnityEngine.AddressableAssets.Addressables`
- 不要把短生命周期对象注册 `RegisterInstance``RegisterSingletonFactory`;临时对象优先使用 `RegisterTransient`,需要随 Feature 生命周期释放的对象放在 Feature scope - 不要把短生命周期对象注册 `RegisterInstance``RegisterSingletonFactory`
- 不要跳过 `GameFlow.ShutdownAsync(...)`;它负责退出当前 Feature、释放 Feature scope并处置 root container - 不要跳过 `GameFlow.ShutdownAsync(...)`
- 不要把 `UIScreenNavigator`完整页面路由系统;它是当前 UI 导航辅助,不负责 URL、跨 Feature 页面恢复或全局路由 - 不要把 `UIScreenNavigator`完整页面路由系统。
- 不要让 `FlowScope.Runtime` 反向依赖 Addressables、Editor、Samples 或 Tests。 - 不要让 `FlowScope.Runtime` 反向依赖 Addressables、Editor、Samples 或 Tests。
- 不要在没有注册 UI layer 的情况下打开面板`UIManager.OpenAsync` 会要求面板声明的 layer 已经通过 `RegisterLayer` 注册 - 不要在没有注册 UI layer 的情况下打开面板。
- 不要在未初始化 `AudioService.Initialize(IResourceService, AudioServiceConfig)` 前调用播放接口。 - 不要在未初始化 `AudioService.Initialize(...)` 前调用播放接口。
- 不要假设 package path 与 Git URL 在 P2 第一阶段已经可安装;当前真实可用形态仍是项目内 `Assets/FlowScope` 开发模式。
## 当前限制 ## 当前限制
P2 第一阶段文档完成后FlowScope 仍需要后续 worker 或阶段完成以下验证后,才能把 package 安装方式视为可交付 P2 package/editor 已随 `v0.2.0-preview.2` 完成收口。后续仍需要单独规划
1. 建立真实 package 根目录与 `package.json` 1. package `Tests~` 全量策略和 PlayMode 测试迁移策略
2. 通过本地 package path 在干净 Unity 项目中验证安装 2. 是否将 package 作为源码主线
3. 验证 Git URL 安装与依赖解析。 3. 更完整的发布流水线和版本兼容策略。
4.`MainMenuP0``Assets/FlowScope/Samples` 整理为可导入 package sample。
5. 在干净 Unity 项目中复验 Addressables 测试资源、settings 与 PlayMode 用例;主项目内的真实 load/release 验收记录已完成。
6. 保持 generated csproj build 与 Unity Test Runner 记录为 0 error。

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 ```text
My project/ My project/Assets/FlowScope
Assets/ Packages/com.flowscope.gamecore
FlowScope/
Runtime/
Addressables/
Samples/
Tests/
``` ```
当前尚未建立标准 Unity Package 根目录,未发现包级 `package.json``Samples~``Tests~``Documentation~` 目录。`MainMenuP0` 示例仍位于 `Assets/FlowScope/Samples/MainMenuP0` `Assets/FlowScope` 仍是开发工程内的测试和样例源;`Packages/com.flowscope.gamecore` 是面向消费方的 Unity Package 雏形
## 目标包根目录 当前 package 目录:
目标包建议使用独立目录承载,例如:
```text ```text
Packages/ Packages/com.flowscope.gamecore/
com.flowscope.gamecore/ package.json
package.json Runtime/
Runtime/ Addressables/
Addressables/ Editor/
Editor/ Samples~/
Samples~/ Tests~/
Tests/ Documentation~/
Documentation~/ README.md
README.md CHANGELOG.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 版本依赖。 | | `package.json` | Unity Package Manager 元数据声明包名、显示名、版本、Unity 版本依赖和 sample。 |
| `Runtime/` | FlowScope 核心运行时代码,包括 Container、Feature、GameFlow、Resource、Config、Save、Audio、UI 等能力。 | | `Runtime/` | FlowScope 核心运行时代码,包括 Container、Feature、GameFlow、Resource、Config、Save、Audio、UI。 |
| `Addressables/` | Addressables 适配层,提供 `AddressablesResourceBackend``AddressablesResourceService`。 | | `Addressables/` | Addressables 适配层,提供 `AddressablesResourceBackend``AddressablesResourceService`。 |
| `Editor/` | 只放编辑器诊断、检查、菜单入口等 Editor-only 内容,不承载运行时逻辑。 | | `Editor/` | Editor-only 诊断入口,不承载运行时逻辑,不自动修改用户资源。 |
| `Samples~/` | Package Manager 可导入示例,例如 `MainMenuP0`示例可以依赖 Runtime 与所演示的适配包。 | | `Samples~/` | Package Manager 可导入示例。当前包含 `MainMenuP0`。 |
| `Tests/` | 包内测试,按 EditMode 与 PlayMode 分层保留测试 asmdef。 | | `Tests~/` | package 内测试候选。当前先放最小 EditMode smoke 测试,不替代开发工程内完整测试。 |
| `Documentation~/` | Package Manager 文档入口,包含安装接入、诊断与示例说明。 | | `Documentation~/` | Package Manager 文档入口,包含安装接入说明。 |
## package.json 建议形态 ## package.json 当前形态
目标包元数据建议保持最小可识别:
```json ```json
{ {
"name": "com.flowscope.gamecore", "name": "com.flowscope.gamecore",
"displayName": "FlowScope Game Core", "displayName": "FlowScope Game Core",
"version": "0.2.0", "version": "0.2.0-preview.2",
"unity": "2022.3", "unity": "2022.3",
"description": "A lightweight Unity game flow, resource, config, save, audio, and UI runtime foundation.",
"dependencies": { "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.addressables": "1.21.21",
"com.unity.ugui": "1.0.0" "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``org.nuget.r3` 当前由消费方项目显式声明,原因是 Unity Package Manager 不可靠支持 package 内 Git dependency 传递解析。
- `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 或嵌入式依赖策略。
## asmdef 依赖方向 ## asmdef 依赖方向
当前与目标结构都必须保持以下方向: 必须保持以下方向:
```text ```text
FlowScope.Runtime FlowScope.Runtime
@@ -93,45 +94,47 @@ FlowScope.Addressables
FlowScope.Editor FlowScope.Editor
-> FlowScope.Runtime -> FlowScope.Runtime
-> 需要诊断的编辑器程序集 -> 诊断所需的 Editor 程序集
FlowScope.Samples.MainMenuP0 FlowScope.Samples.MainMenuP0
-> FlowScope.Runtime -> FlowScope.Runtime
-> R3.Unity -> 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.Addressables`
- `FlowScope.Runtime` 不依赖 `FlowScope.Editor` - `FlowScope.Runtime` 不依赖 `FlowScope.Editor`
- `FlowScope.Runtime` 不依赖 `FlowScope.Samples.*` - `FlowScope.Runtime` 不依赖 `FlowScope.Samples.*`
- `FlowScope.Runtime` 不依赖测试程序集。 - `FlowScope.Runtime` 不依赖测试程序集。
- Samples 不作为 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 的稳定验证路径 1. `Assets/FlowScope` 承载完整 EditMode / PlayMode 回归测试
2. `MainMenuP0` 示例、PlayMode 测试与 Unity 生成的 csproj 仍依赖当前 Asset 路径 2. 主项目内 Addressables 真实 fixture 和 settings 是当前真实验收来源
3. 一次性移动 Runtime、Samples、Tests 会同时改变 asmdef 路径、资源路径、场景引用、prefab 引用与测试发现路径,容易把包结构问题和运行时回归混在一起 3. `Samples~/MainMenuP0` 是 package sample 候选,已在空白消费方手动导入验收
4. 第一阶段还有并行 worker 处理 Addressables 验收与 Editor 诊断Package 文档 worker 不应覆盖或移动他们的文件 4. 一次性把 Samples、Tests 和 Addressables fixture 全部迁入 package 会扩大验证面
5. 先用文档锁定目标结构与依赖方向,可以让第二阶段本地 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 测试资源: 真实 Addressables 测试资源:
- 路径:`Assets/FlowScope/Tests/PlayMode/Resources_moved/Fixtures/Addressables/FlowScopeAddressablesText.txt` - 路径:`Assets/FlowScope/Tests/PlayMode/Resources/Fixtures/Addressables/FlowScopeAddressablesText.txt`
- 内容:`FlowScope Addressables Acceptance` - 内容:`FlowScope Addressables Acceptance`
- GUID`7e04733ce9dab74459592d3bd99fcafd` - GUID`7e04733ce9dab74459592d3bd99fcafd`
- Addressables group`Default Local Group` - 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属于提交前清理候选。 - `Assets/FlowScope/Tests/PlayMode/Resources/Fixtures` 当前只剩空目录 meta属于提交前清理候选。
- 当前验收仍在主项目内完成P2 后续 package 阶段还需要在干净 Unity 项目中验证本地 package path 或 Git URL 安装。 - 当前验收仍在主项目内完成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属于提交前清理候选。 - `Assets/FlowScope/Tests/PlayMode/Resources/Fixtures` 当前只剩空目录 meta属于提交前清理候选。
### 4. Editor 最小诊断入口 ### 4. Editor 最小诊断入口
@@ -165,7 +165,7 @@ git diff --check
1. 尚未创建正式 package 根目录和 `package.json` 文件。 1. 尚未创建正式 package 根目录和 `package.json` 文件。
2. 尚未在干净 Unity 项目中验证本地 package path 或 Git URL 安装。 2. 尚未在干净 Unity 项目中验证本地 package path 或 Git URL 安装。
3. 尚未实际打开 `Tools/FlowScope/Diagnostics` 做人工 UI 检查。 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` 1. 创建 `Packages/com.flowscope.gamecore` 和最小 `package.json`
2. 先复制或镜像 Runtime 到 package 目录,不删除 `Assets/FlowScope` 2. 先复制或镜像 Runtime 到 package 目录,不删除 `Assets/FlowScope`
3. 在当前项目和一个干净 Unity 项目中验证本地 package path。 3. 在当前项目和一个干净 Unity 项目中验证本地 package path。
4. 整理 Addressables 测试资源目录,再迁移 package 内 Tests。 4. 基于已整理 Addressables fixture 路径,迁移或镜像 package 内 Tests。
5. 再考虑 `Samples~/MainMenuP0` 和 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 框架功能开发。