Compare commits

..

10 Commits

Author SHA1 Message Date
janing
7301adbb43 更新主场景、更新md文档 2025-12-14 23:36:39 +08:00
janing
bbed2c5ebb 优化Framework.ResMgr抽象代理类使逻辑更整洁 2025-12-14 23:36:20 +08:00
janing
1d91daa726 优化Framework.UI新增便捷接口 2025-12-14 23:35:54 +08:00
janing
d530b48e34 接入登录、游戏逻辑 2025-12-14 23:35:24 +08:00
janing
3612ee74ea 游戏逻辑模块 2025-12-14 23:35:08 +08:00
janing
aefe242b76 登录模块 2025-12-14 23:34:50 +08:00
janing
aaff0038a6 UILogin\UIWorld 2025-12-14 23:34:24 +08:00
janing
549dadfcb5 ccc-devtools调试工具 2025-12-14 23:34:02 +08:00
janing
354c40bd9b 启动场景 2025-12-14 22:41:50 +08:00
janing
b9573b3ad4 简单应用状态机 2025-12-14 22:41:10 +08:00
62 changed files with 6258 additions and 1903 deletions

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,9 @@
{
"ver": "1.2.0",
"importer": "directory",
"imported": true,
"uuid": "3a436318-510c-475a-9cd0-0c43affaa595",
"files": [],
"subMetas": {},
"userData": {}
}

View File

@@ -0,0 +1,9 @@
{
"ver": "1.2.0",
"importer": "directory",
"imported": true,
"uuid": "a3652a6e-81bf-43c3-8ace-2d6736a7cafc",
"files": [],
"subMetas": {},
"userData": {}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,13 @@
{
"ver": "1.1.50",
"importer": "prefab",
"imported": true,
"uuid": "27b83266-f9c4-4a10-bfe1-0a277fd05c7e",
"files": [
".json"
],
"subMetas": {},
"userData": {
"syncNodeName": "UILogin"
}
}

View File

@@ -0,0 +1,9 @@
{
"ver": "1.2.0",
"importer": "directory",
"imported": true,
"uuid": "97a29502-5783-4ec5-8dac-e504158c6f2f",
"files": [],
"subMetas": {},
"userData": {}
}

View File

@@ -0,0 +1,393 @@
[
{
"__type__": "cc.Prefab",
"_name": "UIWorld",
"_objFlags": 0,
"__editorExtras__": {},
"_native": "",
"data": {
"__id__": 1
},
"optimizationPolicy": 0,
"persistent": false
},
{
"__type__": "cc.Node",
"_name": "UIWorld",
"_objFlags": 0,
"__editorExtras__": {},
"_parent": null,
"_children": [
{
"__id__": 2
}
],
"_active": true,
"_components": [
{
"__id__": 14
}
],
"_prefab": {
"__id__": 16
},
"_lpos": {
"__type__": "cc.Vec3",
"x": 0,
"y": 0,
"z": 0
},
"_lrot": {
"__type__": "cc.Quat",
"x": 0,
"y": 0,
"z": 0,
"w": 1
},
"_lscale": {
"__type__": "cc.Vec3",
"x": 1,
"y": 1,
"z": 1
},
"_mobility": 0,
"_layer": 1073741824,
"_euler": {
"__type__": "cc.Vec3",
"x": 0,
"y": 0,
"z": 0
},
"_id": ""
},
{
"__type__": "cc.Node",
"_name": "Widget",
"_objFlags": 0,
"__editorExtras__": {},
"_parent": {
"__id__": 1
},
"_children": [
{
"__id__": 3
}
],
"_active": true,
"_components": [
{
"__id__": 9
},
{
"__id__": 11
}
],
"_prefab": {
"__id__": 13
},
"_lpos": {
"__type__": "cc.Vec3",
"x": 0,
"y": 0,
"z": 0
},
"_lrot": {
"__type__": "cc.Quat",
"x": 0,
"y": 0,
"z": 0,
"w": 1
},
"_lscale": {
"__type__": "cc.Vec3",
"x": 1,
"y": 1,
"z": 1
},
"_mobility": 0,
"_layer": 1073741824,
"_euler": {
"__type__": "cc.Vec3",
"x": 0,
"y": 0,
"z": 0
},
"_id": ""
},
{
"__type__": "cc.Node",
"_name": "text_id",
"_objFlags": 0,
"__editorExtras__": {},
"_parent": {
"__id__": 2
},
"_children": [],
"_active": true,
"_components": [
{
"__id__": 4
},
{
"__id__": 6
}
],
"_prefab": {
"__id__": 8
},
"_lpos": {
"__type__": "cc.Vec3",
"x": 31.078,
"y": -21.978,
"z": 0
},
"_lrot": {
"__type__": "cc.Quat",
"x": 0,
"y": 0,
"z": 0,
"w": 1
},
"_lscale": {
"__type__": "cc.Vec3",
"x": 1,
"y": 1,
"z": 1
},
"_mobility": 0,
"_layer": 1073741824,
"_euler": {
"__type__": "cc.Vec3",
"x": 0,
"y": 0,
"z": 0
},
"_id": ""
},
{
"__type__": "cc.UITransform",
"_name": "",
"_objFlags": 0,
"__editorExtras__": {},
"node": {
"__id__": 3
},
"_enabled": true,
"__prefab": {
"__id__": 5
},
"_contentSize": {
"__type__": "cc.Size",
"width": 42.255859375,
"height": 50.4
},
"_anchorPoint": {
"__type__": "cc.Vec2",
"x": 0.5,
"y": 0.5
},
"_id": ""
},
{
"__type__": "cc.CompPrefabInfo",
"fileId": "33BScnN89CzKM4A5Mz24+P"
},
{
"__type__": "cc.Label",
"_name": "",
"_objFlags": 0,
"__editorExtras__": {},
"node": {
"__id__": 3
},
"_enabled": true,
"__prefab": {
"__id__": 7
},
"_customMaterial": null,
"_srcBlendFactor": 2,
"_dstBlendFactor": 4,
"_color": {
"__type__": "cc.Color",
"r": 255,
"g": 255,
"b": 255,
"a": 255
},
"_string": "label",
"_horizontalAlign": 1,
"_verticalAlign": 1,
"_actualFontSize": 20,
"_fontSize": 20,
"_fontFamily": "Arial",
"_lineHeight": 40,
"_overflow": 0,
"_enableWrapText": true,
"_font": null,
"_isSystemFontUsed": true,
"_spacingX": 0,
"_isItalic": false,
"_isBold": false,
"_isUnderline": false,
"_underlineHeight": 2,
"_cacheMode": 0,
"_enableOutline": false,
"_outlineColor": {
"__type__": "cc.Color",
"r": 0,
"g": 0,
"b": 0,
"a": 255
},
"_outlineWidth": 2,
"_enableShadow": false,
"_shadowColor": {
"__type__": "cc.Color",
"r": 0,
"g": 0,
"b": 0,
"a": 255
},
"_shadowOffset": {
"__type__": "cc.Vec2",
"x": 2,
"y": 2
},
"_shadowBlur": 2,
"_id": ""
},
{
"__type__": "cc.CompPrefabInfo",
"fileId": "1704UNQUlLGa0RTU9eoRe/"
},
{
"__type__": "cc.PrefabInfo",
"root": {
"__id__": 1
},
"asset": {
"__id__": 0
},
"fileId": "b2RXSisfFEE5ZAdz9aLDTX",
"instance": null,
"targetOverrides": null,
"nestedPrefabInstanceRoots": null
},
{
"__type__": "cc.UITransform",
"_name": "",
"_objFlags": 0,
"__editorExtras__": {},
"node": {
"__id__": 2
},
"_enabled": true,
"__prefab": {
"__id__": 10
},
"_contentSize": {
"__type__": "cc.Size",
"width": 100,
"height": 100
},
"_anchorPoint": {
"__type__": "cc.Vec2",
"x": 0.5,
"y": 0.5
},
"_id": ""
},
{
"__type__": "cc.CompPrefabInfo",
"fileId": "56HTPxFM1C9pvbEq3hBsJK"
},
{
"__type__": "cc.Widget",
"_name": "",
"_objFlags": 0,
"__editorExtras__": {},
"node": {
"__id__": 2
},
"_enabled": true,
"__prefab": {
"__id__": 12
},
"_alignFlags": 9,
"_target": null,
"_left": 0,
"_right": 0,
"_top": 0,
"_bottom": 0,
"_horizontalCenter": 0,
"_verticalCenter": 0,
"_isAbsLeft": true,
"_isAbsRight": true,
"_isAbsTop": true,
"_isAbsBottom": true,
"_isAbsHorizontalCenter": true,
"_isAbsVerticalCenter": true,
"_originalWidth": 0,
"_originalHeight": 0,
"_alignMode": 2,
"_lockFlags": 0,
"_id": ""
},
{
"__type__": "cc.CompPrefabInfo",
"fileId": "81a3Xf3plAX5HUnp1kHuSa"
},
{
"__type__": "cc.PrefabInfo",
"root": {
"__id__": 1
},
"asset": {
"__id__": 0
},
"fileId": "8crMTt2WJJYLgZNqdID4aV",
"instance": null,
"targetOverrides": null,
"nestedPrefabInstanceRoots": null
},
{
"__type__": "cc.UITransform",
"_name": "",
"_objFlags": 0,
"__editorExtras__": {},
"node": {
"__id__": 1
},
"_enabled": true,
"__prefab": {
"__id__": 15
},
"_contentSize": {
"__type__": "cc.Size",
"width": 100,
"height": 100
},
"_anchorPoint": {
"__type__": "cc.Vec2",
"x": 0.5,
"y": 0.5
},
"_id": ""
},
{
"__type__": "cc.CompPrefabInfo",
"fileId": "88XN6P/1lK0b1S3neIoDFp"
},
{
"__type__": "cc.PrefabInfo",
"root": {
"__id__": 1
},
"asset": {
"__id__": 0
},
"fileId": "c46/YsCPVOJYA4mWEpNYRx",
"instance": null,
"targetOverrides": null
}
]

View File

@@ -0,0 +1,13 @@
{
"ver": "1.1.50",
"importer": "prefab",
"imported": true,
"uuid": "17ca623e-4b2f-41e7-b334-c9e0948b3df6",
"files": [
".json"
],
"subMetas": {},
"userData": {
"syncNodeName": "UIWorld"
}
}

View File

@@ -0,0 +1,9 @@
{
"ver": "1.2.0",
"importer": "directory",
"imported": true,
"uuid": "0c114b5d-84d5-4ae1-a491-6bfba7e77ce5",
"files": [],
"subMetas": {},
"userData": {}
}

View File

@@ -0,0 +1,765 @@
[
{
"__type__": "cc.SceneAsset",
"_name": "main",
"_objFlags": 0,
"__editorExtras__": {},
"_native": "",
"scene": {
"__id__": 1
}
},
{
"__type__": "cc.Scene",
"_name": "main",
"_objFlags": 0,
"__editorExtras__": {},
"_parent": null,
"_children": [
{
"__id__": 2
},
{
"__id__": 5
},
{
"__id__": 7
},
{
"__id__": 8
},
{
"__id__": 14
}
],
"_active": true,
"_components": [],
"_prefab": {
"__id__": 16
},
"_lpos": {
"__type__": "cc.Vec3",
"x": 0,
"y": 0,
"z": 0
},
"_lrot": {
"__type__": "cc.Quat",
"x": 0,
"y": 0,
"z": 0,
"w": 1
},
"_lscale": {
"__type__": "cc.Vec3",
"x": 1,
"y": 1,
"z": 1
},
"_mobility": 0,
"_layer": 1073741824,
"_euler": {
"__type__": "cc.Vec3",
"x": 0,
"y": 0,
"z": 0
},
"autoReleaseAssets": false,
"_globals": {
"__id__": 17
},
"_id": "bfc16657-6cbe-4167-b6ba-2766e0864cf2"
},
{
"__type__": "cc.Node",
"_name": "Main Light",
"_objFlags": 0,
"__editorExtras__": {},
"_parent": {
"__id__": 1
},
"_children": [],
"_active": true,
"_components": [
{
"__id__": 3
}
],
"_prefab": null,
"_lpos": {
"__type__": "cc.Vec3",
"x": 0,
"y": 0,
"z": 0
},
"_lrot": {
"__type__": "cc.Quat",
"x": -0.06397656665577071,
"y": -0.44608233363525845,
"z": -0.8239028751062036,
"w": -0.3436591377065261
},
"_lscale": {
"__type__": "cc.Vec3",
"x": 1,
"y": 1,
"z": 1
},
"_mobility": 0,
"_layer": 1073741824,
"_euler": {
"__type__": "cc.Vec3",
"x": -117.894,
"y": -194.909,
"z": 38.562
},
"_id": "c0y6F5f+pAvI805TdmxIjx"
},
{
"__type__": "cc.DirectionalLight",
"_name": "",
"_objFlags": 0,
"__editorExtras__": {},
"node": {
"__id__": 2
},
"_enabled": true,
"__prefab": null,
"_color": {
"__type__": "cc.Color",
"r": 255,
"g": 250,
"b": 240,
"a": 255
},
"_useColorTemperature": false,
"_colorTemperature": 6550,
"_staticSettings": {
"__id__": 4
},
"_visibility": -325058561,
"_illuminanceHDR": 65000,
"_illuminance": 65000,
"_illuminanceLDR": 1.6927083333333335,
"_shadowEnabled": false,
"_shadowPcf": 0,
"_shadowBias": 0.00001,
"_shadowNormalBias": 0,
"_shadowSaturation": 1,
"_shadowDistance": 50,
"_shadowInvisibleOcclusionRange": 200,
"_csmLevel": 4,
"_csmLayerLambda": 0.75,
"_csmOptimizationMode": 2,
"_csmAdvancedOptions": false,
"_csmLayersTransition": false,
"_csmTransitionRange": 0.05,
"_shadowFixedArea": false,
"_shadowNear": 0.1,
"_shadowFar": 10,
"_shadowOrthoSize": 5,
"_id": "597uMYCbhEtJQc0ffJlcgA"
},
{
"__type__": "cc.StaticLightSettings",
"_baked": false,
"_editorOnly": false,
"_castShadow": false
},
{
"__type__": "cc.Node",
"_name": "Main Camera",
"_objFlags": 0,
"__editorExtras__": {},
"_parent": {
"__id__": 1
},
"_children": [],
"_active": true,
"_components": [
{
"__id__": 6
}
],
"_prefab": null,
"_lpos": {
"__type__": "cc.Vec3",
"x": -10,
"y": 10,
"z": 10
},
"_lrot": {
"__type__": "cc.Quat",
"x": -0.27781593346944056,
"y": -0.36497167621709875,
"z": -0.11507512748638377,
"w": 0.8811195706053617
},
"_lscale": {
"__type__": "cc.Vec3",
"x": 1,
"y": 1,
"z": 1
},
"_mobility": 0,
"_layer": 1073741824,
"_euler": {
"__type__": "cc.Vec3",
"x": -35,
"y": -45,
"z": 0
},
"_id": "c9DMICJLFO5IeO07EPon7U"
},
{
"__type__": "cc.Camera",
"_name": "",
"_objFlags": 0,
"__editorExtras__": {},
"node": {
"__id__": 5
},
"_enabled": true,
"__prefab": null,
"_projection": 1,
"_priority": 0,
"_fov": 45,
"_fovAxis": 0,
"_orthoHeight": 10,
"_near": 1,
"_far": 1000,
"_color": {
"__type__": "cc.Color",
"r": 51,
"g": 51,
"b": 51,
"a": 255
},
"_depth": 1,
"_stencil": 0,
"_clearFlags": 14,
"_rect": {
"__type__": "cc.Rect",
"x": 0,
"y": 0,
"width": 1,
"height": 1
},
"_aperture": 19,
"_shutter": 7,
"_iso": 0,
"_screenScale": 1,
"_visibility": 1074790400,
"_targetTexture": null,
"_postProcess": null,
"_usePostProcess": false,
"_cameraType": -1,
"_trackingType": 0,
"_id": "7dWQTpwS5LrIHnc1zAPUtf"
},
{
"__type__": "cc.Node",
"_name": "Plane",
"_objFlags": 0,
"__editorExtras__": {},
"_parent": {
"__id__": 1
},
"_children": [],
"_active": true,
"_components": [],
"_prefab": null,
"_lpos": {
"__type__": "cc.Vec3",
"x": 0,
"y": 0,
"z": 0
},
"_lrot": {
"__type__": "cc.Quat",
"x": 0,
"y": 0,
"z": 0,
"w": 1
},
"_lscale": {
"__type__": "cc.Vec3",
"x": 1,
"y": 1,
"z": 1
},
"_mobility": 0,
"_layer": 1073741824,
"_euler": {
"__type__": "cc.Vec3",
"x": 0,
"y": 0,
"z": 0
},
"_id": "9emVSyd4FGs44YVqfPmIY0"
},
{
"__type__": "cc.Node",
"_name": "Canvas",
"_objFlags": 0,
"__editorExtras__": {},
"_parent": {
"__id__": 1
},
"_children": [
{
"__id__": 9
}
],
"_active": true,
"_components": [
{
"__id__": 11
},
{
"__id__": 12
},
{
"__id__": 13
}
],
"_prefab": null,
"_lpos": {
"__type__": "cc.Vec3",
"x": 640,
"y": 360,
"z": 0
},
"_lrot": {
"__type__": "cc.Quat",
"x": 0,
"y": 0,
"z": 0,
"w": 1
},
"_lscale": {
"__type__": "cc.Vec3",
"x": 1,
"y": 1,
"z": 1
},
"_mobility": 0,
"_layer": 33554432,
"_euler": {
"__type__": "cc.Vec3",
"x": 0,
"y": 0,
"z": 0
},
"_id": "917ZNBP8lFl6QtL6HdUGVv"
},
{
"__type__": "cc.Node",
"_name": "Camera",
"_objFlags": 0,
"__editorExtras__": {},
"_parent": {
"__id__": 8
},
"_children": [],
"_active": true,
"_components": [
{
"__id__": 10
}
],
"_prefab": null,
"_lpos": {
"__type__": "cc.Vec3",
"x": 0,
"y": 0,
"z": 1000
},
"_lrot": {
"__type__": "cc.Quat",
"x": 0,
"y": 0,
"z": 0,
"w": 1
},
"_lscale": {
"__type__": "cc.Vec3",
"x": 1,
"y": 1,
"z": 1
},
"_mobility": 0,
"_layer": 1073741824,
"_euler": {
"__type__": "cc.Vec3",
"x": 0,
"y": 0,
"z": 0
},
"_id": "b0QvhuM8VJLJqxUQvrm8nO"
},
{
"__type__": "cc.Camera",
"_name": "",
"_objFlags": 0,
"__editorExtras__": {},
"node": {
"__id__": 9
},
"_enabled": true,
"__prefab": null,
"_projection": 0,
"_priority": 1073741824,
"_fov": 45,
"_fovAxis": 0,
"_orthoHeight": 360,
"_near": 1,
"_far": 2000,
"_color": {
"__type__": "cc.Color",
"r": 0,
"g": 0,
"b": 0,
"a": 255
},
"_depth": 1,
"_stencil": 0,
"_clearFlags": 6,
"_rect": {
"__type__": "cc.Rect",
"x": 0,
"y": 0,
"width": 1,
"height": 1
},
"_aperture": 19,
"_shutter": 7,
"_iso": 0,
"_screenScale": 1,
"_visibility": 41943040,
"_targetTexture": null,
"_postProcess": null,
"_usePostProcess": false,
"_cameraType": -1,
"_trackingType": 0,
"_id": "718x5Z5JlHJKQTU3SIPvlZ"
},
{
"__type__": "cc.UITransform",
"_name": "",
"_objFlags": 0,
"__editorExtras__": {},
"node": {
"__id__": 8
},
"_enabled": true,
"__prefab": null,
"_contentSize": {
"__type__": "cc.Size",
"width": 1280,
"height": 720
},
"_anchorPoint": {
"__type__": "cc.Vec2",
"x": 0.5,
"y": 0.5
},
"_id": "f3E6tFsFtIOJjS3BX0pOPZ"
},
{
"__type__": "cc.Canvas",
"_name": "",
"_objFlags": 0,
"__editorExtras__": {},
"node": {
"__id__": 8
},
"_enabled": true,
"__prefab": null,
"_cameraComponent": {
"__id__": 10
},
"_alignCanvasWithScreen": true,
"_id": "f88L9qlk9IaaDk9C04yl0H"
},
{
"__type__": "cc.Widget",
"_name": "",
"_objFlags": 0,
"__editorExtras__": {},
"node": {
"__id__": 8
},
"_enabled": true,
"__prefab": null,
"_alignFlags": 45,
"_target": null,
"_left": 0,
"_right": 0,
"_top": 0,
"_bottom": 0,
"_horizontalCenter": 0,
"_verticalCenter": 0,
"_isAbsLeft": true,
"_isAbsRight": true,
"_isAbsTop": true,
"_isAbsBottom": true,
"_isAbsHorizontalCenter": true,
"_isAbsVerticalCenter": true,
"_originalWidth": 0,
"_originalHeight": 0,
"_alignMode": 2,
"_lockFlags": 0,
"_id": "845MM0J/FB74UYhIeYO2+T"
},
{
"__type__": "cc.Node",
"_name": "Boot",
"_objFlags": 0,
"__editorExtras__": {},
"_parent": {
"__id__": 1
},
"_children": [],
"_active": true,
"_components": [
{
"__id__": 15
}
],
"_prefab": null,
"_lpos": {
"__type__": "cc.Vec3",
"x": 0,
"y": 0,
"z": 0
},
"_lrot": {
"__type__": "cc.Quat",
"x": 0,
"y": 0,
"z": 0,
"w": 1
},
"_lscale": {
"__type__": "cc.Vec3",
"x": 1,
"y": 1,
"z": 1
},
"_mobility": 0,
"_layer": 1073741824,
"_euler": {
"__type__": "cc.Vec3",
"x": 0,
"y": 0,
"z": 0
},
"_id": "2eOr9/vnZCDaQvPz75/lrU"
},
{
"__type__": "324afRDhsdJg7cmwcpcYDWT",
"_name": "",
"_objFlags": 0,
"__editorExtras__": {},
"node": {
"__id__": 14
},
"_enabled": true,
"__prefab": null,
"_id": "edfnyk1dhK8ZRzXmAqhDwq"
},
{
"__type__": "cc.PrefabInfo",
"root": null,
"asset": null,
"fileId": "bfc16657-6cbe-4167-b6ba-2766e0864cf2",
"instance": null,
"targetOverrides": []
},
{
"__type__": "cc.SceneGlobals",
"ambient": {
"__id__": 18
},
"shadows": {
"__id__": 19
},
"_skybox": {
"__id__": 20
},
"fog": {
"__id__": 21
},
"octree": {
"__id__": 22
},
"skin": {
"__id__": 23
},
"lightProbeInfo": {
"__id__": 24
},
"postSettings": {
"__id__": 25
},
"bakedWithStationaryMainLight": false,
"bakedWithHighpLightmap": false
},
{
"__type__": "cc.AmbientInfo",
"_skyColorHDR": {
"__type__": "cc.Vec4",
"x": 0.2,
"y": 0.5,
"z": 0.8,
"w": 0.520833125
},
"_skyColor": {
"__type__": "cc.Vec4",
"x": 0.2,
"y": 0.5,
"z": 0.8,
"w": 0.520833125
},
"_skyIllumHDR": 20000,
"_skyIllum": 20000,
"_groundAlbedoHDR": {
"__type__": "cc.Vec4",
"x": 0.2,
"y": 0.2,
"z": 0.2,
"w": 1
},
"_groundAlbedo": {
"__type__": "cc.Vec4",
"x": 0.2,
"y": 0.2,
"z": 0.2,
"w": 1
},
"_skyColorLDR": {
"__type__": "cc.Vec4",
"x": 0.452588,
"y": 0.607642,
"z": 0.755699,
"w": 0
},
"_skyIllumLDR": 0.8,
"_groundAlbedoLDR": {
"__type__": "cc.Vec4",
"x": 0.618555,
"y": 0.577848,
"z": 0.544564,
"w": 0
}
},
{
"__type__": "cc.ShadowsInfo",
"_enabled": false,
"_type": 0,
"_normal": {
"__type__": "cc.Vec3",
"x": 0,
"y": 1,
"z": 0
},
"_distance": 0,
"_planeBias": 1,
"_shadowColor": {
"__type__": "cc.Color",
"r": 76,
"g": 76,
"b": 76,
"a": 255
},
"_maxReceived": 4,
"_size": {
"__type__": "cc.Vec2",
"x": 1024,
"y": 1024
}
},
{
"__type__": "cc.SkyboxInfo",
"_envLightingType": 0,
"_envmapHDR": {
"__uuid__": "d032ac98-05e1-4090-88bb-eb640dcb5fc1@b47c0",
"__expectedType__": "cc.TextureCube"
},
"_envmap": {
"__uuid__": "d032ac98-05e1-4090-88bb-eb640dcb5fc1@b47c0",
"__expectedType__": "cc.TextureCube"
},
"_envmapLDR": {
"__uuid__": "6f01cf7f-81bf-4a7e-bd5d-0afc19696480@b47c0",
"__expectedType__": "cc.TextureCube"
},
"_diffuseMapHDR": null,
"_diffuseMapLDR": null,
"_enabled": true,
"_useHDR": true,
"_editableMaterial": null,
"_reflectionHDR": null,
"_reflectionLDR": null,
"_rotationAngle": 0
},
{
"__type__": "cc.FogInfo",
"_type": 0,
"_fogColor": {
"__type__": "cc.Color",
"r": 200,
"g": 200,
"b": 200,
"a": 255
},
"_enabled": false,
"_fogDensity": 0.3,
"_fogStart": 0.5,
"_fogEnd": 300,
"_fogAtten": 5,
"_fogTop": 1.5,
"_fogRange": 1.2,
"_accurate": false
},
{
"__type__": "cc.OctreeInfo",
"_enabled": false,
"_minPos": {
"__type__": "cc.Vec3",
"x": -1024,
"y": -1024,
"z": -1024
},
"_maxPos": {
"__type__": "cc.Vec3",
"x": 1024,
"y": 1024,
"z": 1024
},
"_depth": 8
},
{
"__type__": "cc.SkinInfo",
"_enabled": true,
"_blurRadius": 0.01,
"_sssIntensity": 3
},
{
"__type__": "cc.LightProbeInfo",
"_giScale": 1,
"_giSamples": 1024,
"_bounces": 2,
"_reduceRinging": 0,
"_showProbe": true,
"_showWireframe": true,
"_showConvex": false,
"_data": null,
"_lightProbeSphereVolume": 1
},
{
"__type__": "cc.PostSettingsInfo",
"_toneMappingType": 0
}
]

View File

@@ -0,0 +1,11 @@
{
"ver": "1.1.50",
"importer": "scene",
"imported": true,
"uuid": "bfc16657-6cbe-4167-b6ba-2766e0864cf2",
"files": [
".json"
],
"subMetas": {},
"userData": {}
}

View File

@@ -0,0 +1,9 @@
{
"ver": "1.2.0",
"importer": "directory",
"imported": true,
"uuid": "411a8b79-dfcf-4814-bee8-075f787510c8",
"files": [],
"subMetas": {},
"userData": {}
}

View File

@@ -0,0 +1,9 @@
{
"ver": "1.2.0",
"importer": "directory",
"imported": true,
"uuid": "4c66e5c4-282b-45bd-b8a0-ec4a9a08c7ec",
"files": [],
"subMetas": {},
"userData": {}
}

View File

@@ -0,0 +1,9 @@
{
"ver": "1.2.0",
"importer": "directory",
"imported": true,
"uuid": "e9decc58-bdc5-45ad-b085-aabd2012b936",
"files": [],
"subMetas": {},
"userData": {}
}

View File

@@ -0,0 +1,67 @@
import { find } from "cc";
import { BaseState } from "../../Framework/FSM/BaseState";
import { NetManager } from "../../Framework/Net/NetManager";
import { UIMgr } from "../../Framework/UI/UIMgr";
/**
* 应用启动状态
* 职责:
* - 初始化游戏引擎
* - 加载基础配置
* - 初始化网络管理器
* - 准备第一个UI界面
*/
export class AppStatusBoot extends BaseState {
constructor(fsm: any) {
super(fsm, "Boot");
}
/**
* 进入启动状态
*/
async onEnter(params?: any): Promise<void> {
super.onEnter(params);
console.log("[AppStatusBoot] 开始初始化应用...");
try {
// 初始化UI
console.log("[AppStatusBoot] 初始化UI管理器...");
UIMgr.getInstance().setUIRoot(find("Canvas")!);
// 1. 初始化并连接网络
await this.initAndConnectNet();
// 2. 初始化完成,切换到登录状态
console.log("[AppStatusBoot] 启动完成,切换到登录状态");
this._fsm.changeState("Login");
} catch (error) {
console.error("[AppStatusBoot] 初始化失败:", error);
}
}
/**
* 初始化并连接网络
*/
private async initAndConnectNet(): Promise<void> {
console.log("[AppStatusBoot] 初始化网络管理器...");
// TODO: 从配置文件读取服务器地址
// import { serviceProto } from '../../Shared/protocols/serviceProto';
// const netManager = NetManager.getInstance();
// netManager.setServiceProto(serviceProto);
// netManager.init({ serverUrl: 'http://localhost:3000' });
// await netManager.connect();
console.log("[AppStatusBoot] 网络连接完成(待配置)");
}
/**
* 退出启动状态
*/
onExit(): void {
super.onExit();
console.log("[AppStatusBoot] 离开启动状态");
}
}

View File

@@ -0,0 +1,9 @@
{
"ver": "4.0.24",
"importer": "typescript",
"imported": true,
"uuid": "6a737e69-0f9a-47d8-b836-f3d8f586fcaa",
"files": [],
"subMetas": {},
"userData": {}
}

View File

@@ -0,0 +1,201 @@
import { BaseState } from "../../Framework/FSM/BaseState";
import { UIMgr } from "../../Framework/UI/UIMgr";
import { UIGame } from "../Game/UIGame";
import { World } from "../Game/World";
import { PlayerInfo } from "../../Shared/protocols/PtlLogin";
/**
* 应用游戏状态
* 职责:
* - 加载游戏场景
* - 初始化玩家角色
* - 监听服务器广播(其他玩家加入、移动等)
* - 游戏主循环
*/
export class AppStatusGame extends BaseState {
private _player: PlayerInfo = null;
private _isNewPlayer: boolean = false;
private _uiGame: UIGame = null;
constructor(fsm: any) {
super(fsm, "Game");
}
/**
* 进入游戏状态
*/
async onEnter(params?: any): Promise<void> {
super.onEnter(params);
console.log("[AppStatusGame] 进入游戏世界");
// 保存玩家信息
if (params) {
this._player = params.player || null;
this._isNewPlayer = params.isNewPlayer || false;
console.log(`[AppStatusGame] 玩家信息:`, this._player);
console.log(`[AppStatusGame] 是否新玩家: ${this._isNewPlayer}`);
}
try {
// 1. 加载游戏场景
await this.loadGameScene();
// 2. 初始化游戏
await this.initGame();
// 3. 开始监听服务器广播
this.listenServerMessages();
// 4. 开始游戏
this.startGame();
} catch (error) {
console.error("[AppStatusGame] 进入游戏失败:", error);
// 返回登录
this._fsm.changeState("Login");
}
}
/**
* 加载游戏场景
*/
private async loadGameScene(): Promise<void> {
console.log("[AppStatusGame] 加载游戏场景...");
// 加载游戏UI
this._uiGame = await UIMgr.getInstance().load(UIGame);
console.log("[AppStatusGame] 游戏场景加载完成");
}
/**
* 初始化游戏
*/
private async initGame(): Promise<void> {
console.log("[AppStatusGame] 初始化游戏...");
if (!this._uiGame) {
throw new Error("UIGame 未加载");
}
// 获取世界根节点
const worldRoot = this._uiGame.getWorldRoot();
if (!worldRoot) {
throw new Error("世界根节点未找到");
}
// 初始化世界,传入本地玩家信息
await World.getInstance().init(worldRoot, this._player);
console.log("[AppStatusGame] 游戏初始化完成");
}
/**
* 监听服务器广播消息
*/
private listenServerMessages(): void {
console.log("[AppStatusGame] 开始监听服务器广播...");
// 网络消息监听已在 World 中注册
// World 会自动处理 MsgPlayerJoin 和 MsgPlayerMove
console.log("[AppStatusGame] 服务器广播监听已设置");
}
/**
* 开始游戏
*/
private startGame(): void {
console.log("[AppStatusGame] 游戏开始!");
// 游戏已启动,玩家可以通过 WASD 控制角色移动
}
/**
* 更新游戏状态(每帧调用)
*/
onUpdate(dt: number): void {
// TODO: 游戏主循环逻辑
// - 更新角色位置
// - 检测碰撞
// - 更新敌人AI
// - 同步网络状态
}
/**
* 暂停游戏
*/
pauseGame(): void {
console.log("[AppStatusGame] 游戏暂停");
// TODO: 暂停游戏逻辑
// - 停止游戏更新
// - 显示暂停菜单
}
/**
* 恢复游戏
*/
resumeGame(): void {
console.log("[AppStatusGame] 游戏恢复");
// TODO: 恢复游戏逻辑
// - 继续游戏更新
// - 隐藏暂停菜单
}
/**
* 玩家死亡
*/
onPlayerDeath(): void {
console.log("[AppStatusGame] 玩家死亡");
// TODO: 处理玩家死亡
// - 显示死亡界面
// - 显示复活选项或返回登录
// 延迟后返回登录
setTimeout(() => {
this._fsm.changeState("Login");
}, 3000);
}
/**
* 退出游戏(返回登录)
*/
quitGame(): void {
console.log("[AppStatusGame] 退出游戏");
// TODO: 断开连接或通知服务器
// const netManager = NetManager.getInstance();
// await netManager.disconnect();
// 返回登录
this._fsm.changeState("Login");
}
/**
* 延迟辅助函数
*/
private delay(ms: number): Promise<void> {
return new Promise(resolve => setTimeout(resolve, ms));
}
/**
* 退出游戏状态
*/
onExit(): void {
super.onExit();
console.log("[AppStatusGame] 离开游戏状态");
// 清理世界
World.clear();
// 卸载游戏UI
if (this._uiGame) {
UIMgr.getInstance().unload(UIGame);
this._uiGame = null;
}
this._player = null;
}
}

View File

@@ -0,0 +1,9 @@
{
"ver": "4.0.24",
"importer": "typescript",
"imported": true,
"uuid": "fb4c25d0-b65c-4881-9997-5e3bd5d50325",
"files": [],
"subMetas": {},
"userData": {}
}

View File

@@ -0,0 +1,121 @@
import { BaseState } from "../../Framework/FSM/BaseState";
import { NetManager } from "../../Framework/Net/NetManager";
import { UIMgr } from "../../Framework/UI/UIMgr";
import { UILogin } from "../Login/UILogin";
/**
* 应用登录状态
* 职责:
* - 显示登录界面
* - 处理玩家ID输入
* - 发送登录请求(Login API)
* - 登录成功后直接进入游戏世界
*/
export class AppStatusLogin extends BaseState {
constructor(fsm: any) {
super(fsm, "Login");
}
/**
* 进入登录状态
*/
async onEnter(params?: any): Promise<void> {
super.onEnter(params);
console.log("[AppStatusLogin] 显示登录界面");
// 1. 显示登录界面
await this.showLoginUI();
// 2. 等待用户输入玩家ID并点击登录
// 在 UI 中处理输入,调用 this.login(playerId, playerName)
}
/**
* 显示登录界面
*/
private async showLoginUI(): Promise<void> {
console.log("[AppStatusLogin] 加载登录UI...");
try {
const uiMgr = UIMgr.getInstance();
await uiMgr.load(UILogin);
console.log("[AppStatusLogin] 登录UI加载成功");
} catch (error) {
console.error("[AppStatusLogin] 登录UI加载失败:", error);
}
}
/**
* 执行登录
* @param playerId 玩家ID(用于识别玩家)
* @param playerName 玩家昵称(可选,新玩家时使用)
*/
async login(playerId: string, playerName?: string): Promise<void> {
console.log(`[AppStatusLogin] 尝试登录, 玩家ID: ${playerId}`);
if (!playerId || playerId.trim().length === 0) {
console.error("[AppStatusLogin] 玩家ID不能为空");
// TODO: 显示错误提示
return;
}
try {
// 1. 发送登录请求到服务器
// const netManager = NetManager.getInstance();
// const result = await netManager.callApi("Login", {
// playerId,
// playerName
// });
// 模拟登录请求
await this.delay(500);
const mockResult = {
success: true,
message: "登录成功",
player: {
id: playerId,
name: playerName || `Player_${playerId}`,
position: { x: 0, y: 0, z: 0 },
spawnPoint: { x: 0, y: 0, z: 0 },
hp: 100,
maxHp: 100,
isAlive: true,
createdAt: Date.now(),
lastLoginAt: Date.now()
},
isNewPlayer: false
};
console.log("[AppStatusLogin] 登录成功,玩家信息:", mockResult.player);
// 2. 登录成功后直接进入游戏状态
this._fsm.changeState("Game", {
player: mockResult.player,
isNewPlayer: mockResult.isNewPlayer
});
} catch (error) {
console.error("[AppStatusLogin] 登录失败:", error);
// TODO: 显示错误提示
}
}
/**
* 延迟辅助函数
*/
private delay(ms: number): Promise<void> {
return new Promise(resolve => setTimeout(resolve, ms));
}
/**
* 退出登录状态
*/
onExit(): void {
super.onExit();
console.log("[AppStatusLogin] 离开登录状态");
// 隐藏登录界面
const uiMgr = UIMgr.getInstance();
uiMgr.hide(UILogin);
}
}

View File

@@ -0,0 +1,9 @@
{
"ver": "4.0.24",
"importer": "typescript",
"imported": true,
"uuid": "f0f1f913-0bf0-440c-9b66-9712ed982479",
"files": [],
"subMetas": {},
"userData": {}
}

View File

@@ -0,0 +1,107 @@
import { FSM } from "../../Framework/FSM/FSM";
import { AppStatusBoot } from "./AppStatusBoot";
import { AppStatusLogin } from "./AppStatusLogin";
import { AppStatusGame } from "./AppStatusGame";
/**
* 应用状态管理器
* 职责:
* - 管理应用的整体状态流转
* - 提供单例访问
* - 初始化所有应用状态
*
* 状态流程: Boot -> Login -> Game
*/
export class AppStatusManager {
private static _instance: AppStatusManager | null = null;
private _fsm: FSM;
/**
* 构造函数(私有)
*/
private constructor() {
this._fsm = new FSM();
this.initStates();
}
/**
* 获取单例
*/
static getInstance(): AppStatusManager {
if (!this._instance) {
this._instance = new AppStatusManager();
}
return this._instance;
}
/**
* 初始化所有状态
*/
private initStates(): void {
console.log("[AppStatusManager] 初始化应用状态...");
// 添加应用状态: Boot -> Login -> Game
this._fsm.addState(new AppStatusBoot(this._fsm));
this._fsm.addState(new AppStatusLogin(this._fsm));
this._fsm.addState(new AppStatusGame(this._fsm));
console.log("[AppStatusManager] 应用状态初始化完成");
}
/**
* 启动应用
* 从Boot状态开始
*/
start(): void {
console.log("[AppStatusManager] 启动应用...");
this._fsm.changeState("Boot");
}
/**
* 切换状态
* @param stateName 状态名称
* @param params 可选参数
*/
changeState(stateName: string, params?: any): void {
this._fsm.changeState(stateName, params);
}
/**
* 获取当前状态名称
*/
getCurrentStateName(): string | null {
return this._fsm.getCurrentStateName();
}
/**
* 获取当前状态
*/
getCurrentState(): any {
return this._fsm.getCurrentState();
}
/**
* 更新状态机(在主循环中调用)
* @param dt 距离上一帧的时间增量(秒)
*/
update(dt: number): void {
this._fsm.update(dt);
}
/**
* 获取状态机实例
* 用于高级操作
*/
getFSM(): FSM {
return this._fsm;
}
/**
* 销毁管理器
*/
destroy(): void {
console.log("[AppStatusManager] 销毁应用状态管理器");
this._fsm.clear();
AppStatusManager._instance = null;
}
}

View File

@@ -0,0 +1,9 @@
{
"ver": "4.0.24",
"importer": "typescript",
"imported": true,
"uuid": "191d542e-df2a-43a0-998c-4daefe65c598",
"files": [],
"subMetas": {},
"userData": {}
}

View File

@@ -0,0 +1,396 @@
# 应用状态机 (App/AppStatus)
## 📋 模块概述
管理应用的整体状态流转,包括启动、登录、游戏等状态,基于 Framework/FSM 实现。
## 🎯 核心特性
- ✅ 应用状态流转管理
- ✅ 网络初始化
- ✅ 登录流程
- ✅ 游戏状态管理
- ✅ 状态生命周期
- ✅ 服务器消息监听
## 🗂️ 文件结构
```
App/AppStatus/
├── AppStatusManager.ts # 应用状态管理器
├── AppStatusBoot.ts # 启动状态
├── AppStatusLogin.ts # 登录状态
└── AppStatusGame.ts # 游戏状态
```
## 🔄 状态流转图
```
[启动 Boot] → [登录 Login] → [游戏 Game]
↑ ↓ ↓
└────────────┴──────────────┘
(退出/死亡返回登录)
```
## 📘 核心类详解
### AppStatusManager - 应用状态管理器
**职责**: 管理应用的整体状态流转
```typescript
class AppStatusManager {
private _fsm: FSM; // 底层状态机
// 获取单例
static getInstance(): AppStatusManager;
// 启动应用(从 Boot 状态开始)
start(): void;
// 切换状态
changeState(stateName: string, params?: any): void;
// 获取当前状态名称
getCurrentStateName(): string | null;
// 获取当前状态实例
getCurrentState(): any;
// 更新状态机(在主循环中调用)
update(dt: number): void;
// 获取底层 FSM 实例
getFSM(): FSM;
// 销毁管理器
destroy(): void;
}
```
### AppStatusBoot - 启动状态
**职责**: 初始化网络管理器并连接服务器
```typescript
class AppStatusBoot extends BaseState {
constructor(fsm: FSM);
// 进入启动状态
async onEnter(params?: any): Promise<void>;
// 初始化并连接网络
private async initAndConnectNet(): Promise<void>;
// 退出启动状态
onExit(): void;
}
```
**执行流程**:
1. 设置服务协议
2. 初始化网络配置
3. 监听网络事件
4. 连接服务器
5. 连接成功后切换到登录状态
### AppStatusLogin - 登录状态
**职责**: 显示登录界面,处理登录逻辑
```typescript
class AppStatusLogin extends BaseState {
constructor(fsm: FSM);
// 进入登录状态
async onEnter(params?: any): Promise<void>;
// 显示登录 UI
private async showLoginUI(): Promise<void>;
// 退出登录状态
onExit(): void;
}
```
**执行流程**:
1. 通过 UIMgr 加载并显示登录 UI
2. 等待用户输入账号并点击登录
3. UILogin 调用 NetManager 发送登录请求
4. 登录成功后切换到游戏状态
### AppStatusGame - 游戏状态
**职责**: 游戏主循环,处理游戏逻辑和服务器消息
```typescript
class AppStatusGame extends BaseState {
private _player: any; // 玩家信息
private _isNewPlayer: boolean; // 是否新玩家
private _isPaused: boolean; // 是否暂停
constructor(fsm: FSM);
// 进入游戏状态
async onEnter(params?: any): Promise<void>;
// 加载游戏场景
private async loadGameScene(): Promise<void>;
// 初始化游戏
private async initGame(): Promise<void>;
// 监听服务器广播消息
private listenServerMessages(): void;
// 开始游戏
private startGame(): void;
// 游戏主循环
onUpdate(dt: number): void;
// 暂停游戏
pauseGame(): void;
// 恢复游戏
resumeGame(): void;
// 玩家死亡
private onPlayerDeath(): void;
// 退出游戏(返回登录)
quitGame(): void;
// 退出游戏状态
onExit(): void;
}
```
**执行流程**:
1. 接收玩家信息
2. 加载游戏场景
3. 初始化玩家角色
4. 监听服务器广播消息
5. 开始游戏主循环
6. 处理游戏逻辑和网络消息
## 📝 使用指南
### 1. 启动应用
```typescript
import { AppStatusManager } from './App/AppStatus/AppStatusManager';
// 在 Boot 组件中启动
@ccclass('Boot')
export class Boot extends Component {
start() {
// 获取管理器实例
const appManager = AppStatusManager.getInstance();
// 启动应用
appManager.start();
}
update(deltaTime: number) {
// 在主循环中更新状态机
AppStatusManager.getInstance().update(deltaTime);
}
}
```
### 2. 切换状态
```typescript
import { AppStatusManager } from './App/AppStatus/AppStatusManager';
// 从登录状态切换到游戏状态
AppStatusManager.getInstance().changeState('Game', {
player: playerInfo,
isNewPlayer: false
});
// 从游戏状态返回登录状态
AppStatusManager.getInstance().changeState('Login');
```
### 3. 获取当前状态
```typescript
// 获取当前状态名称
const stateName = AppStatusManager.getInstance().getCurrentStateName();
console.log(`当前状态: ${stateName}`);
// 获取当前状态实例
const currentState = AppStatusManager.getInstance().getCurrentState();
```
## 🔄 完整流程示例
### 启动到游戏的完整流程
```
1. Boot 组件启动
2. AppStatusManager.start()
3. 切换到 Boot 状态
4. 初始化网络并连接服务器
5. 连接成功,切换到 Login 状态
6. 加载并显示登录 UI
7. 用户输入账号并点击登录
8. 调用 Login API
9. 登录成功,切换到 Game 状态
10. 加载游戏场景
11. 初始化玩家角色
12. 监听服务器广播消息
13. 开始游戏主循环
```
## 📡 网络协议使用
### Boot 状态
```typescript
// 配置网络
const config: NetConfig = {
serverUrl: 'http://localhost:3000',
timeout: 30000,
autoReconnect: true,
reconnectInterval: 3000,
maxReconnectTimes: 5
};
```
### Login 状态
```typescript
// 由 UILogin 调用登录 API
const result = await netManager.callApi<ReqLogin, ResLogin>('Login', {
account: account
});
// 登录成功后切换状态
if (result && result.success) {
AppStatusManager.getInstance().changeState('Game', {
player: result.player,
isNewPlayer: result.isNewPlayer
});
}
```
### Game 状态
```typescript
// 监听服务器广播
netManager.listenMsg<MsgPlayerJoin>('PlayerJoin', (msg) => {
console.log(`玩家 ${msg.playerName} 加入游戏`);
// 在场景中创建其他玩家
});
netManager.listenMsg<MsgPlayerMove>('PlayerMove', (msg) => {
console.log(`玩家 ${msg.playerName} 移动`);
// 更新其他玩家位置
});
// 发送 API
const result = await netManager.callApi<ReqMove, ResMove>('Move', {
x: targetX,
y: targetY
});
```
## ⚠️ 注意事项
1. **状态切换参数**: 切换到 Game 状态时必须传递 player 参数
2. **网络初始化**: Boot 状态必须在网络连接成功后才能切换状态
3. **资源清理**: 切换状态时注意清理前一个状态的资源
4. **消息监听**: Game 状态退出时要取消所有服务器消息监听
5. **异步处理**: 状态的 onEnter 方法是异步的,注意使用 await
## 🔍 调试技巧
### 状态切换日志
```typescript
// BaseState 会自动输出状态切换日志
// [FSM] Enter state: Boot
// [FSM] Exit state: Boot
// [FSM] Enter state: Login
```
### 网络状态日志
```typescript
// NetManager 会输出网络状态日志
// [NetManager] 网络已连接
// [NetManager] 网络已断开
// [NetManager] 正在重连...
```
### 游戏状态信息
```typescript
// 在 Game 状态中查看玩家信息
const gameState = AppStatusManager.getInstance()
.getCurrentState() as AppStatusGame;
console.log('玩家信息:', gameState._player);
console.log('是否新玩家:', gameState._isNewPlayer);
```
## 💡 最佳实践
1. **单一入口**: 通过 AppStatusManager 统一管理所有状态切换
2. **参数传递**: 使用 params 在状态间传递必要的数据
3. **资源管理**: 每个状态负责自己的资源加载和释放
4. **错误处理**: 网络错误时提供友好的提示并返回合适的状态
5. **状态独立**: 每个状态保持独立,避免直接依赖其他状态
## 🎯 扩展状态
### 添加新状态
```typescript
// 1. 创建新状态类
class AppStatusLobby extends BaseState {
constructor(fsm: FSM) {
super(fsm, "Lobby");
}
onEnter(params?: any): void {
super.onEnter(params);
console.log("[AppStatusLobby] 进入大厅");
// 显示大厅 UI
}
onExit(): void {
super.onExit();
// 清理大厅资源
}
}
// 2. 在 AppStatusManager 中注册
private initStates(): void {
this._fsm.addState(new AppStatusBoot(this._fsm));
this._fsm.addState(new AppStatusLogin(this._fsm));
this._fsm.addState(new AppStatusLobby(this._fsm)); // 新增
this._fsm.addState(new AppStatusGame(this._fsm));
}
// 3. 修改状态流转
// Login -> Lobby -> Game
```
## 📚 相关文档
- [Framework/FSM README](../../Framework/FSM/README.md) - 状态机框架
- [Framework/Net README](../../Framework/Net/README.md) - 网络通信
- [App/Login README](../Login/README.md) - 登录模块

View File

@@ -0,0 +1,11 @@
{
"ver": "1.0.1",
"importer": "text",
"imported": true,
"uuid": "87d8c6a8-6b03-44b9-98a2-5eb7b8457271",
"files": [
".json"
],
"subMetas": {},
"userData": {}
}

View File

@@ -0,0 +1,9 @@
{
"ver": "1.2.0",
"importer": "directory",
"imported": true,
"uuid": "7884d98f-e4a9-4e92-aed3-214cddfcd2b4",
"files": [],
"subMetas": {},
"userData": {}
}

View File

@@ -0,0 +1,207 @@
import { _decorator, Component, Node, EventKeyboard, KeyCode, Input, input, Vec3 } from 'cc';
import { NetManager } from '../../Framework/Net/NetManager';
import { ReqMove, ResMove } from '../../Shared/protocols/PtlMove';
import { PlayerInfo } from '../../Shared/protocols/PtlLogin';
import { RoleController } from '../../CC/RoleController';
const { ccclass } = _decorator;
/**
* PlayerController 本地玩家控制器
* 负责处理本地玩家的输入、移动和动画
*/
@ccclass('PlayerController')
export class PlayerController extends Component {
/** 玩家信息 */
private playerInfo: PlayerInfo = null;
/** 角色控制器(控制动画) */
private roleController: RoleController = null;
/** 移动速度 */
private moveSpeed: number = 5;
/** 当前移动方向 */
private moveDirection: Vec3 = new Vec3(0, 0, 0);
/** 按键状态 */
private keyStates: Map<KeyCode, boolean> = new Map();
/** 是否正在移动 */
private isMoving: boolean = false;
/** 上次发送移动请求的位置 */
private lastSentPosition: Vec3 = new Vec3();
/** 移动阈值(超过这个距离才发送移动请求) */
private moveSendThreshold: number = 0.5;
/**
* 初始化玩家控制器
*/
public init(playerInfo: PlayerInfo): void {
this.playerInfo = playerInfo;
this.lastSentPosition.set(playerInfo.position.x, 0, playerInfo.position.y);
// 获取 RoleController 组件
this.roleController = this.node.getComponentInChildren(RoleController);
if (!this.roleController) {
console.warn('[PlayerController] 未找到 RoleController 组件');
} else {
// 初始播放待机动画
this.roleController.PlayAnimation('idle', true);
}
console.log('[PlayerController] 初始化完成:', playerInfo.name);
}
protected onEnable(): void {
// 注册键盘事件
input.on(Input.EventType.KEY_DOWN, this.onKeyDown, this);
input.on(Input.EventType.KEY_UP, this.onKeyUp, this);
}
protected onDisable(): void {
// 取消注册键盘事件
input.off(Input.EventType.KEY_DOWN, this.onKeyDown, this);
input.off(Input.EventType.KEY_UP, this.onKeyUp, this);
}
/**
* 键盘按下事件
*/
private onKeyDown(event: EventKeyboard): void {
this.keyStates.set(event.keyCode, true);
}
/**
* 键盘抬起事件
*/
private onKeyUp(event: EventKeyboard): void {
this.keyStates.set(event.keyCode, false);
}
protected update(dt: number): void {
// 计算移动方向
this.updateMoveDirection();
// 如果有移动方向,移动角色
if (this.moveDirection.lengthSqr() > 0) {
this.move(dt);
} else {
// 没有移动,播放待机动画
if (this.isMoving) {
this.isMoving = false;
if (this.roleController) {
this.roleController.PlayAnimation('idle', true);
}
}
}
}
/**
* 更新移动方向
*/
private updateMoveDirection(): void {
this.moveDirection.set(0, 0, 0);
// W - 向前
if (this.keyStates.get(KeyCode.KEY_W)) {
this.moveDirection.z -= 1;
}
// S - 向后
if (this.keyStates.get(KeyCode.KEY_S)) {
this.moveDirection.z += 1;
}
// A - 向左
if (this.keyStates.get(KeyCode.KEY_A)) {
this.moveDirection.x -= 1;
}
// D - 向右
if (this.keyStates.get(KeyCode.KEY_D)) {
this.moveDirection.x += 1;
}
// 归一化移动方向
if (this.moveDirection.lengthSqr() > 0) {
this.moveDirection.normalize();
}
}
/**
* 移动角色
*/
private move(dt: number): void {
// 计算移动增量
const moveOffset = this.moveDirection.clone().multiplyScalar(this.moveSpeed * dt);
// 更新节点位置
const currentPos = this.node.position.clone();
currentPos.add(moveOffset);
this.node.setPosition(currentPos);
// 更新朝向(让角色面向移动方向)
if (this.moveDirection.lengthSqr() > 0) {
const targetAngle = Math.atan2(this.moveDirection.x, -this.moveDirection.z) * (180 / Math.PI);
this.node.setRotationFromEuler(0, targetAngle, 0);
}
// 播放移动动画
if (!this.isMoving) {
this.isMoving = true;
if (this.roleController) {
this.roleController.PlayAnimation('move', true);
}
}
// 检查是否需要发送移动请求
this.checkSendMoveRequest(currentPos);
}
/**
* 检查是否需要发送移动请求
*/
private checkSendMoveRequest(currentPos: Vec3): void {
const distance = Vec3.distance(currentPos, this.lastSentPosition);
// 如果移动距离超过阈值,发送移动请求
if (distance >= this.moveSendThreshold) {
this.sendMoveRequest(currentPos.x, currentPos.z);
this.lastSentPosition.set(currentPos);
}
}
/**
* 发送移动请求到服务器
*/
private async sendMoveRequest(x: number, z: number): Promise<void> {
try {
const netManager = NetManager.getInstance();
const result = await netManager.callApi<ReqMove, ResMove>('Move', {
x: x,
y: z
});
if (!result) {
console.error('[PlayerController] 移动请求失败');
return;
}
// 服务器可能会修正位置,使用服务器返回的位置
if (result.position) {
const serverPos = result.position;
this.node.setPosition(serverPos.x, 0, serverPos.y);
this.lastSentPosition.set(serverPos.x, 0, serverPos.y);
}
} catch (error) {
console.error('[PlayerController] 发送移动请求异常:', error);
}
}
/**
* 获取玩家信息
*/
public getPlayerInfo(): PlayerInfo {
return this.playerInfo;
}
}

View File

@@ -0,0 +1,9 @@
{
"ver": "4.0.24",
"importer": "typescript",
"imported": true,
"uuid": "9e1f7c66-cb9e-4e5f-8642-605d3568c4ac",
"files": [],
"subMetas": {},
"userData": {}
}

View File

@@ -0,0 +1,325 @@
# Game 模块
## 概述
Game 模块是游戏的核心业务模块,负责管理游戏世界、玩家控制、网络同步等功能。该模块基于玩家输入发送给服务器,服务器广播给所有玩家的在线游戏开发思路,确保帧间同步。
## 目录结构
```
App/Game/
├── World.ts # 世界管理器,管理所有玩家
├── PlayerController.ts # 本地玩家控制器,处理输入和移动
├── RemotePlayer.ts # 远程玩家类,处理其他玩家的显示和同步
├── UIGame.ts # 游戏主界面UI
└── README.md # 本文档
```
## 核心类说明
### 1. World (世界管理器)
**职责:**
- 管理游戏世界中的所有玩家(本地玩家 + 远程玩家)
- 加载玩家模型预制体
- 监听网络消息(MsgPlayerJoin、MsgPlayerMove)
- 创建和销毁玩家对象
**主要方法:**
- `init(worldRoot, localPlayer)`: 初始化世界,传入世界根节点和本地玩家信息
- `destroy()`: 销毁世界,清理所有资源
- `getLocalPlayerController()`: 获取本地玩家控制器
**使用示例:**
```typescript
import { World } from './App/Game/World';
import { PlayerInfo } from './Shared/protocols/PtlLogin';
const worldRoot = this.node; // 世界根节点
const playerInfo: PlayerInfo = { ... }; // 登录返回的玩家信息
await World.getInstance().init(worldRoot, playerInfo);
```
### 2. PlayerController (本地玩家控制器)
**职责:**
- 监听键盘输入(WASD)
- 控制本地玩家移动
- 控制角色动画(待机、移动)
- 向服务器发送移动请求(PtlMove)
**主要特性:**
- **WASD 移动控制**: W(前)、S(后)、A(左)、D(右)
- **自动朝向**: 角色会自动面向移动方向
- **动画切换**: 移动时播放 move 动画,静止时播放 idle 动画
- **阈值同步**: 移动距离超过 0.5 单位时才向服务器发送移动请求,减少网络负担
**主要方法:**
- `init(playerInfo)`: 初始化玩家控制器
- `getPlayerInfo()`: 获取玩家信息
**内部实现:**
```typescript
// 键盘输入 → 计算移动方向 → 更新本地位置 → 检查阈值 → 发送网络请求
```
### 3. RemotePlayer (远程玩家)
**职责:**
- 显示其他玩家的角色模型
- 接收服务器广播的移动消息
- 平滑移动到目标位置(使用 Tween 补间)
- 控制远程玩家的动画
**主要特性:**
- **平滑移动**: 使用 Tween 让远程玩家平滑移动到目标位置
- **自动朝向**: 根据移动方向自动旋转角色
- **动画同步**: 移动时播放 move 动画,到达目标后播放 idle 动画
**主要方法:**
- `init(playerNode, playerId, playerName, position)`: 初始化远程玩家
- `updatePosition(position)`: 更新远程玩家位置(收到 MsgPlayerMove 时调用)
- `destroy()`: 销毁远程玩家
### 4. UIGame (游戏主界面)
**职责:**
- 游戏主界面 UI 组件
- 提供世界根节点(用于创建玩家)
- 继承自 Framework/UI/UIBase
**主要方法:**
- `onGetUrl()`: 返回 UI 预制体路径 `res://UI/UIGame`
- `getWorldRoot()`: 获取世界根节点
## 协议使用
### 1. 登录返回数据 (ResLogin)
登录成功后,服务器返回玩家信息,包含:
- `player`: 玩家详细信息(id、name、position 等)
- `isNewPlayer`: 是否新玩家
该数据在进入 Game 状态时传递给 World 和 PlayerController。
### 2. 移动请求 (PtlMove)
**请求 (ReqMove):**
```typescript
{
x: number, // 目标 X 坐标
y: number // 目标 Y 坐标(对应 3D 中的 Z 轴)
}
```
**响应 (ResMove):**
```typescript
{
success: boolean,
message?: string,
position?: Position // 服务器可能修正的位置
}
```
### 3. 玩家加入广播 (MsgPlayerJoin)
当有新玩家登录或加入游戏时,服务器广播给所有在线玩家:
```typescript
{
playerId: string,
playerName: string,
position: Position,
isNewPlayer: boolean,
timestamp: number
}
```
### 4. 玩家移动广播 (MsgPlayerMove)
当有玩家移动时,服务器广播给所有在线玩家:
```typescript
{
playerId: string,
playerName: string,
position: Position,
timestamp: number
}
```
## 工作流程
### 初始化流程
```
登录成功 (ResLogin)
进入 AppStatusGame 状态
加载 UIGame 界面
初始化 World
加载玩家模型预制体 (res://Actor/M1/M1)
注册网络监听 (MsgPlayerJoin, MsgPlayerMove)
创建本地玩家 (PlayerController)
游戏开始
```
### 本地玩家移动流程
```
按下 WASD 键
PlayerController 计算移动方向
更新本地节点位置
播放 move 动画
检查移动距离是否超过阈值 (0.5)
发送 PtlMove 请求到服务器
服务器返回确认
(可选)使用服务器修正的位置
```
### 远程玩家同步流程
```
服务器广播 MsgPlayerMove
World 接收消息
查找对应的 RemotePlayer
RemotePlayer.updatePosition()
计算移动方向和距离
启动 Tween 补间动画
播放 move 动画
平滑移动到目标位置
到达后播放 idle 动画
```
## 角色模型和动画
### 模型路径
- 预制体路径: `res://Actor/M1/M1`
- 使用 ResMgr 加载
### RoleController 脚本
模型下挂载了 `RoleController` 脚本,可以控制角色动画:
- `PlayAnimation(action, loop)`: 播放指定动画
- `action`: "idle"(待机) | "move"(移动) | "attack"(攻击) | "death"(死亡)
- `loop`: 是否循环播放
**使用示例:**
```typescript
const roleController = playerNode.getComponentInChildren(RoleController);
roleController.PlayAnimation('move', true); // 播放移动动画并循环
roleController.PlayAnimation('idle', true); // 播放待机动画并循环
```
## 输入控制
### 键盘映射
- **W**: 向前移动(世界坐标 -Z 方向)
- **S**: 向后移动(世界坐标 +Z 方向)
- **A**: 向左移动(世界坐标 -X 方向)
- **D**: 向右移动(世界坐标 +X 方向)
### 移动特性
- **多方向合成**: 可以同时按多个方向键,移动方向会自动合成并归一化
- **移动速度**: 默认 5 单位/秒
- **自动朝向**: 角色会自动旋转面向移动方向
## 网络同步策略
### 本地玩家
- **客户端预测**: 立即更新本地位置,无需等待服务器确认
- **阈值同步**: 移动距离超过 0.5 单位时才发送网络请求,减少网络流量
- **服务器修正**: 如果服务器返回修正后的位置,使用服务器位置
### 远程玩家
- **延迟补偿**: 使用 Tween 补间动画平滑移动到目标位置
- **基于速度的时间**: 根据距离和移动速度计算补间时间,确保移动看起来自然
## 常见问题
### 1. 如何修改移动速度?
`PlayerController.ts` 中修改:
```typescript
private moveSpeed: number = 5; // 修改为你想要的速度
```
### 2. 如何修改网络同步阈值?
`PlayerController.ts` 中修改:
```typescript
private moveSendThreshold: number = 0.5; // 修改为你想要的阈值
```
### 3. 如何添加更多动画?
在 RoleController 中添加对应的动画剪辑,然后在 PlayerController 或 RemotePlayer 中调用:
```typescript
this.roleController.PlayAnimation('attack', false); // 播放攻击动画,不循环
```
### 4. 如何处理玩家离线?
目前未实现玩家离线处理,可以添加 `MsgPlayerLeave` 消息监听:
```typescript
netManager.listenMsg('PlayerLeave', (msg) => {
const remotePlayer = this.remotePlayers.get(msg.playerId);
if (remotePlayer) {
remotePlayer.destroy();
this.remotePlayers.delete(msg.playerId);
}
});
```
## 待实现功能
- [ ] 玩家名称显示
- [ ] 玩家血条显示
- [ ] 玩家离线处理
- [ ] 战斗系统
- [ ] 技能系统
- [ ] 物品拾取
- [ ] 聊天系统 UI
- [ ] 小地图
## 注意事项
1. **坐标系转换**: Cocos 3D 使用 (x, y, z),服务器协议使用 (x, y),需要注意:
- 服务器的 y 对应 Cocos 的 z
- Cocos 的 y 是高度,固定为 0
2. **网络消息监听**: 所有网络消息监听在 World 中注册,退出时需要取消监听
3. **资源释放**: 退出游戏时需要调用 `World.clear()` 清理所有资源
4. **单例模式**: World 使用单例模式,确保全局只有一个实例
5. **UI 预制体**: 需要在 Cocos Creator 中创建 `res://UI/UIGame` 预制体,并添加 `worldRoot` 节点
## 相关文档
- [Framework/FSM 模块文档](../../Framework/FSM/README.md)
- [Framework/Net 模块文档](../../Framework/Net/README.md)
- [Framework/ResMgr 模块文档](../../Framework/ResMgr/README.md)
- [Framework/UI 模块文档](../../Framework/UI/README.md)
- [App/AppStatus 模块文档](../AppStatus/README.md)

View File

@@ -0,0 +1,11 @@
{
"ver": "1.0.1",
"importer": "text",
"imported": true,
"uuid": "fe661d46-0792-4a28-9fcc-11ef37219910",
"files": [
".json"
],
"subMetas": {},
"userData": {}
}

View File

@@ -0,0 +1,156 @@
import { Node, Vec3, Tween, tween } from 'cc';
import { Position } from '../../Shared/protocols/base';
import { RoleController } from '../../CC/RoleController';
/**
* RemotePlayer 远程玩家
* 负责管理远程玩家的显示和位置同步
*/
export class RemotePlayer {
/** 玩家节点 */
private playerNode: Node = null;
/** 玩家ID */
private playerId: string = '';
/** 玩家名称 */
private playerName: string = '';
/** 当前位置 */
private currentPosition: Vec3 = new Vec3();
/** 目标位置 */
private targetPosition: Vec3 = new Vec3();
/** 角色控制器(控制动画) */
private roleController: RoleController = null;
/** 移动补间 */
private moveTween: Tween<Node> = null;
/** 是否正在移动 */
private isMoving: boolean = false;
/**
* 初始化远程玩家
*/
public init(playerNode: Node, playerId: string, playerName: string, position: Position): void {
this.playerNode = playerNode;
this.playerId = playerId;
this.playerName = playerName;
this.currentPosition.set(position.x, 0, position.y);
this.targetPosition.set(position.x, 0, position.y);
// 获取 RoleController 组件
this.roleController = this.playerNode.getComponentInChildren(RoleController);
if (!this.roleController) {
console.warn('[RemotePlayer] 未找到 RoleController 组件');
} else {
// 初始播放待机动画
this.roleController.PlayAnimation('idle', true);
}
console.log('[RemotePlayer] 初始化完成:', playerName);
}
/**
* 更新位置
* 收到服务器广播的移动消息时调用
*/
public updatePosition(position: Position): void {
this.targetPosition.set(position.x, 0, position.y);
// 计算移动方向和距离
const direction = this.targetPosition.clone().subtract(this.currentPosition);
const distance = direction.length();
// 如果距离太小,不做处理
if (distance < 0.1) {
return;
}
// 停止之前的移动补间
if (this.moveTween) {
this.moveTween.stop();
this.moveTween = null;
}
// 计算朝向
if (distance > 0.01) {
const targetAngle = Math.atan2(direction.x, -direction.z) * (180 / Math.PI);
this.playerNode.setRotationFromEuler(0, targetAngle, 0);
}
// 播放移动动画
if (!this.isMoving) {
this.isMoving = true;
if (this.roleController) {
this.roleController.PlayAnimation('move', true);
}
}
// 计算移动时间(根据距离,假设速度为5单位/秒)
const moveSpeed = 5;
const moveDuration = distance / moveSpeed;
// 创建移动补间
this.moveTween = tween(this.playerNode)
.to(moveDuration, { position: this.targetPosition }, {
onUpdate: (target, ratio) => {
// 更新当前位置
this.currentPosition.set(this.playerNode.position);
},
onComplete: () => {
// 移动完成,播放待机动画
this.isMoving = false;
if (this.roleController) {
this.roleController.PlayAnimation('idle', true);
}
this.moveTween = null;
}
})
.start();
}
/**
* 获取玩家ID
*/
public getPlayerId(): string {
return this.playerId;
}
/**
* 获取玩家名称
*/
public getPlayerName(): string {
return this.playerName;
}
/**
* 获取玩家节点
*/
public getPlayerNode(): Node {
return this.playerNode;
}
/**
* 销毁远程玩家
*/
public destroy(): void {
// 停止移动补间
if (this.moveTween) {
this.moveTween.stop();
this.moveTween = null;
}
// 销毁节点
if (this.playerNode) {
this.playerNode.destroy();
this.playerNode = null;
}
this.roleController = null;
console.log('[RemotePlayer] 已销毁:', this.playerName);
}
}

View File

@@ -0,0 +1,9 @@
{
"ver": "4.0.24",
"importer": "typescript",
"imported": true,
"uuid": "48c1adc3-a2a6-4f7f-97b2-4df92ee822ee",
"files": [],
"subMetas": {},
"userData": {}
}

View File

@@ -0,0 +1,45 @@
import { _decorator, Component, Node } from 'cc';
import { UIBase } from '../../Framework/UI/UIBase';
import { World } from './World';
const { ccclass, property } = _decorator;
/**
* UIGame 游戏主界面
* 显示游戏UI和世界场景
*/
@ccclass('UIGame')
export class UIGame extends UIBase {
@property(Node)
worldRoot: Node = null;
protected onLoad(): void {
console.log('[UIGame] onLoad');
}
protected onEnable(): void {
console.log('[UIGame] onEnable');
}
protected onDisable(): void {
console.log('[UIGame] onDisable');
}
protected onDestroy(): void {
console.log('[UIGame] onDestroy');
}
/**
* 获取 UI 预制体路径
*/
public onGetUrl(): string {
return 'res://UI/UIGame';
}
/**
* 获取世界根节点
*/
public getWorldRoot(): Node {
return this.worldRoot;
}
}

View File

@@ -0,0 +1,9 @@
{
"ver": "4.0.24",
"importer": "typescript",
"imported": true,
"uuid": "1fa0f67a-24a5-4acc-a866-d5c288f16fc7",
"files": [],
"subMetas": {},
"userData": {}
}

View File

@@ -0,0 +1,228 @@
import { Node, Vec3, instantiate, Prefab } from 'cc';
import { NetManager } from '../../Framework/Net/NetManager';
import { ResMgr } from '../../Framework/ResMgr/ResMgr';
import { MsgPlayerJoin } from '../../Shared/protocols/MsgPlayerJoin';
import { MsgPlayerMove } from '../../Shared/protocols/MsgPlayerMove';
import { PlayerInfo } from '../../Shared/protocols/PtlLogin';
import { PlayerController } from './PlayerController';
import { RemotePlayer } from './RemotePlayer';
/**
* World 世界管理器
* 负责管理游戏世界中的所有玩家
* 包括本地玩家和远程玩家的创建、更新和销毁
*/
export class World {
private static instance: World = null;
/** 世界根节点 */
private worldRoot: Node = null;
/** 本地玩家信息 */
private localPlayer: PlayerInfo = null;
/** 本地玩家控制器 */
private localPlayerController: PlayerController = null;
/** 本地玩家节点 */
private localPlayerNode: Node = null;
/** 远程玩家列表 Map<playerId, RemotePlayer> */
private remotePlayers: Map<string, RemotePlayer> = new Map();
/** 玩家模型预制体 */
private playerPrefab: Prefab = null;
private constructor() {}
public static getInstance(): World {
if (!World.instance) {
World.instance = new World();
}
return World.instance;
}
/**
* 初始化世界
* @param worldRoot 世界根节点
* @param localPlayer 本地玩家信息
*/
public async init(worldRoot: Node, localPlayer: PlayerInfo): Promise<void> {
this.worldRoot = worldRoot;
this.localPlayer = localPlayer;
// 加载玩家模型预制体
await this.loadPlayerPrefab();
// 注册网络消息监听
this.registerNetworkListeners();
// 创建本地玩家
await this.createLocalPlayer();
console.log('[World] 世界初始化完成');
}
/**
* 加载玩家模型预制体
*/
private async loadPlayerPrefab(): Promise<void> {
try {
this.playerPrefab = await ResMgr.getInstance().load('resources', 'res://Actor/M1/M1', Prefab);
console.log('[World] 玩家模型预制体加载成功');
} catch (error) {
console.error('[World] 加载玩家模型预制体失败:', error);
}
}
/**
* 注册网络消息监听
*/
private registerNetworkListeners(): void {
const netManager = NetManager.getInstance();
// 监听玩家加入消息
netManager.listenMsg('PlayerJoin', (msg: MsgPlayerJoin) => {
this.onPlayerJoin(msg);
});
// 监听玩家移动消息
netManager.listenMsg('PlayerMove', (msg: MsgPlayerMove) => {
this.onPlayerMove(msg);
});
console.log('[World] 网络消息监听注册完成');
}
/**
* 创建本地玩家
*/
private async createLocalPlayer(): Promise<void> {
if (!this.playerPrefab) {
console.error('[World] 玩家模型预制体未加载');
return;
}
// 实例化玩家节点
this.localPlayerNode = instantiate(this.playerPrefab);
this.localPlayerNode.name = `Player_${this.localPlayer.id}_Local`;
this.localPlayerNode.setPosition(this.localPlayer.position.x, 0, this.localPlayer.position.y);
this.worldRoot.addChild(this.localPlayerNode);
// 创建本地玩家控制器
this.localPlayerController = this.localPlayerNode.addComponent(PlayerController);
this.localPlayerController.init(this.localPlayer);
console.log('[World] 本地玩家创建完成:', this.localPlayer.name);
}
/**
* 处理玩家加入消息
*/
private onPlayerJoin(msg: MsgPlayerJoin): void {
console.log('[World] 玩家加入:', msg.playerName);
// 如果是本地玩家,不处理
if (msg.playerId === this.localPlayer.id) {
return;
}
// 创建远程玩家
this.createRemotePlayer(msg);
}
/**
* 创建远程玩家
*/
private async createRemotePlayer(msg: MsgPlayerJoin): Promise<void> {
if (!this.playerPrefab) {
console.error('[World] 玩家模型预制体未加载');
return;
}
// 检查是否已存在
if (this.remotePlayers.has(msg.playerId)) {
console.warn('[World] 远程玩家已存在:', msg.playerId);
return;
}
// 实例化玩家节点
const playerNode = instantiate(this.playerPrefab);
playerNode.name = `Player_${msg.playerId}_Remote`;
playerNode.setPosition(msg.position.x, 0, msg.position.y);
this.worldRoot.addChild(playerNode);
// 创建远程玩家控制器
const remotePlayer = new RemotePlayer();
remotePlayer.init(playerNode, msg.playerId, msg.playerName, msg.position);
this.remotePlayers.set(msg.playerId, remotePlayer);
console.log('[World] 远程玩家创建完成:', msg.playerName);
}
/**
* 处理玩家移动消息
*/
private onPlayerMove(msg: MsgPlayerMove): void {
// 如果是本地玩家,不处理
if (msg.playerId === this.localPlayer.id) {
return;
}
// 更新远程玩家位置
const remotePlayer = this.remotePlayers.get(msg.playerId);
if (remotePlayer) {
remotePlayer.updatePosition(msg.position);
}
}
/**
* 获取本地玩家控制器
*/
public getLocalPlayerController(): PlayerController {
return this.localPlayerController;
}
/**
* 销毁世界
*/
public destroy(): void {
// 注意: TSRPC 的 listenMsg 不提供取消监听的方法
// 在实际使用中,监听会在连接断开时自动清除
// 销毁本地玩家
if (this.localPlayerNode) {
this.localPlayerNode.destroy();
this.localPlayerNode = null;
}
this.localPlayerController = null;
// 销毁所有远程玩家
this.remotePlayers.forEach((remotePlayer) => {
remotePlayer.destroy();
});
this.remotePlayers.clear();
// 释放资源
if (this.playerPrefab) {
ResMgr.getInstance().release('resources', 'res://Actor/M1/M1');
this.playerPrefab = null;
}
this.worldRoot = null;
this.localPlayer = null;
console.log('[World] 世界已销毁');
}
/**
* 清理单例
*/
public static clear(): void {
if (World.instance) {
World.instance.destroy();
World.instance = null;
}
}
}

View File

@@ -0,0 +1,9 @@
{
"ver": "4.0.24",
"importer": "typescript",
"imported": true,
"uuid": "80116901-37bd-4c32-85b3-da5aefa12b10",
"files": [],
"subMetas": {},
"userData": {}
}

View File

@@ -0,0 +1,9 @@
{
"ver": "1.2.0",
"importer": "directory",
"imported": true,
"uuid": "6cbaee3d-3077-4a01-96f8-bf0ffd8225b0",
"files": [],
"subMetas": {},
"userData": {}
}

View File

@@ -0,0 +1,390 @@
# 登录模块 (App/Login)
## 📋 模块概述
用户登录功能模块,包括登录界面 UI 和登录业务逻辑,登录成功后切换到游戏状态。
## 🎯 核心特性
- ✅ 登录界面 UI
- ✅ 账号输入处理
- ✅ 登录按钮交互
- ✅ 网络登录请求
- ✅ 状态切换
- ✅ 错误处理
## 🗂️ 文件结构
```
App/Login/
└── UILogin.ts # 登录界面组件
```
## 📘 核心类详解
### UILogin - 登录界面
**职责**: 显示登录 UI,处理用户输入,发送登录请求
```typescript
@ccclass('UILogin')
class UILogin extends UIBase {
private _inputAccount: EditBox | null; // 账号输入框
private _btnLogin: Node | null; // 登录按钮
private _isLogging: boolean; // 是否正在登录中
// 获取 UI 资源路径
onGetUrl(): string {
return 'res/UI/Login/UILogin';
}
// UI 初始化
async onStart(params?: any): Promise<void>;
// 查找 UI 节点
private findNodes(): void;
// 绑定事件
private bindEvents(): void;
// 解绑事件
private unbindEvents(): void;
// 登录按钮点击
private async onLoginClick(): Promise<void>;
// 执行登录
private async login(account: string): Promise<void>;
// UI 清理
onEnd(): void;
}
```
## 🎨 UI 结构要求
### 资源路径
`assets/res/UI/Login/UILogin.prefab`
### 节点结构
```
UILogin (根节点)
├── mid/
│ └── input_account (EditBox) # 账号输入框
└── btn_login # 登录按钮
```
### 组件要求
| 节点路径 | 必需组件 | 说明 |
|---------|---------|------|
| `mid/input_account` | EditBox | 账号输入框 |
| `btn_login` | - | 登录按钮(监听点击事件) |
## 📝 使用指南
### 1. 创建 UI 预制体
1. 在 Cocos Creator 中创建 UI 预制体
2. 路径: `assets/res/UI/Login/UILogin.prefab`
3. 添加必需节点和组件
4. 保存预制体
### 2. 在登录状态中使用
```typescript
import { UIMgr } from '../../Framework/UI/UIMgr';
import { UILogin } from '../Login/UILogin';
// 在 AppStatusLogin 中加载登录 UI
export class AppStatusLogin extends BaseState {
async onEnter(params?: any): Promise<void> {
// 加载并显示登录界面
await UIMgr.getInstance().load(UILogin);
}
onExit(): void {
// 隐藏登录界面
UIMgr.getInstance().hide(UILogin);
}
}
```
### 3. 登录流程
```
用户打开应用
AppStatusLogin.onEnter()
加载并显示 UILogin
用户输入账号
点击登录按钮
onLoginClick()
login(account)
调用 NetManager.callApi('Login', ...)
收到服务器响应
登录成功?
├─ 是: 切换到 Game 状态
└─ 否: 显示错误提示
```
## 📡 网络协议
### 登录请求 (ReqLogin)
```typescript
interface ReqLogin {
account: string; // 账号
password?: string; // 密码(可选)
}
```
### 登录响应 (ResLogin)
```typescript
interface ResLogin {
success: boolean; // 是否成功
message?: string; // 消息
player?: { // 玩家信息
id: string;
name: string;
position: { x: number; y: number; z: number };
spawnPoint: { x: number; y: number; z: number };
hp: number;
maxHp: number;
isAlive: boolean;
createdAt: number;
lastLoginAt: number;
};
isNewPlayer?: boolean; // 是否新玩家
}
```
### 使用示例
```typescript
import { NetManager } from '../../Framework/Net/NetManager';
import { ReqLogin, ResLogin } from '../../Framework/Net/LoginProtocol';
// 发送登录请求
const result = await NetManager.getInstance().callApi<ReqLogin, ResLogin>('Login', {
account: 'player123'
});
if (result && result.success) {
console.log('登录成功:', result.player);
// 切换到游戏状态
AppStatusManager.getInstance().changeState('Game', {
player: result.player,
isNewPlayer: result.isNewPlayer
});
}
```
## 🔄 完整登录流程
```typescript
// UILogin.ts 中的登录流程
// 1. 用户点击登录按钮
private async onLoginClick(): Promise<void> {
// 防止重复点击
if (this._isLogging) {
return;
}
// 获取输入的账号
const account = this._inputAccount?.string?.trim();
if (!account) {
console.warn('[UILogin] 请输入账号');
return;
}
// 标记登录中
this._isLogging = true;
try {
// 执行登录
await this.login(account);
} catch (error) {
console.error('[UILogin] 登录失败:', error);
} finally {
this._isLogging = false;
}
}
// 2. 执行登录逻辑
private async login(account: string): Promise<void> {
// 调用登录 API
const result = await NetManager.getInstance()
.callApi<ReqLogin, ResLogin>('Login', { account });
if (result && result.success) {
// 登录成功,切换状态
AppStatusManager.getInstance().changeState('Game', {
player: result.player,
isNewPlayer: result.isNewPlayer
});
// 隐藏登录界面
this.hide();
} else {
// 登录失败,显示错误
console.error('[UILogin] 登录失败:', result?.message);
}
}
```
## ⚠️ 注意事项
1. **UI 资源准备**: 必须在 `assets/res/UI/Login/` 创建 UILogin 预制体
2. **节点结构**: 预制体必须包含 `mid/input_account``btn_login` 节点
3. **EditBox 组件**: `input_account` 节点必须挂载 EditBox 组件
4. **协议同步**: 运行 `npm run sync-shared` 同步服务端协议后,替换临时协议定义
5. **模块归属**: 业务 UI 必须放在 `App/` 对应的业务模块下
6. **防重复点击**: 登录过程中禁用按钮,防止重复请求
## 🔍 调试技巧
### 日志输出
```typescript
// UILogin 包含详细日志
// [UILogin] 登录界面初始化
// [UILogin] 已绑定登录按钮事件
// [UILogin] 开始登录,账号: xxx
// [UILogin] 登录成功
// [UILogin] 登录界面清理
```
### 检查节点
```typescript
// 在 onStart 中检查节点是否找到
private findNodes(): void {
const inputNode = this._node.getChildByPath('mid/input_account');
console.log('input_account 节点:', inputNode ? '找到' : '未找到');
const btnNode = this._node.getChildByPath('btn_login');
console.log('btn_login 节点:', btnNode ? '找到' : '未找到');
}
```
### 常见问题
**问题1**: 节点找不到
```typescript
// 检查预制体中的节点路径是否正确
// 确保节点名称完全匹配(区分大小写)
```
**问题2**: EditBox 组件为 null
```typescript
// 确保 input_account 节点挂载了 EditBox 组件
const editBox = inputNode.getComponent(EditBox);
if (!editBox) {
console.error('未挂载 EditBox 组件');
}
```
**问题3**: 登录请求无响应
```typescript
// 检查网络是否已连接
// 检查服务器地址是否正确
// 检查协议是否已同步
```
## 💡 最佳实践
1. **输入验证**: 登录前验证账号格式
2. **加载状态**: 显示加载动画或禁用按钮
3. **错误提示**: 友好的错误提示 UI
4. **记住账号**: 可选的记住账号功能
5. **自动登录**: 可选的自动登录功能
6. **超时处理**: 设置合理的请求超时时间
## 🎯 扩展功能
### 添加密码输入
```typescript
// 1. 在 UI 中添加密码输入框
private _inputPassword: EditBox | null = null;
// 2. 在 findNodes 中查找
this._inputPassword = this._node.getChildByPath('mid/input_password')
?.getComponent(EditBox) || null;
// 3. 登录时传递密码
const result = await NetManager.getInstance()
.callApi<ReqLogin, ResLogin>('Login', {
account: account,
password: password
});
```
### 添加记住账号功能
```typescript
import { sys } from 'cc';
// 保存账号
private saveAccount(account: string): void {
sys.localStorage.setItem('last_account', account);
}
// 读取账号
private loadAccount(): string {
return sys.localStorage.getItem('last_account') || '';
}
// 在 onStart 中自动填充
async onStart(): Promise<void> {
// ... 其他初始化
// 自动填充上次的账号
const lastAccount = this.loadAccount();
if (this._inputAccount && lastAccount) {
this._inputAccount.string = lastAccount;
}
}
```
### 添加加载动画
```typescript
private _loadingNode: Node | null = null;
private showLoading(): void {
if (this._loadingNode) {
this._loadingNode.active = true;
}
// 禁用登录按钮
if (this._btnLogin) {
this._btnLogin.getComponent(Button)!.interactable = false;
}
}
private hideLoading(): void {
if (this._loadingNode) {
this._loadingNode.active = false;
}
// 启用登录按钮
if (this._btnLogin) {
this._btnLogin.getComponent(Button)!.interactable = true;
}
}
```
## 📚 相关文档
- [Framework/UI README](../../Framework/UI/README.md) - UI 系统
- [Framework/Net README](../../Framework/Net/README.md) - 网络通信
- [App/AppStatus README](../AppStatus/README.md) - 应用状态机

View File

@@ -0,0 +1,11 @@
{
"ver": "1.0.1",
"importer": "text",
"imported": true,
"uuid": "4319cc73-c0a0-4a35-bafc-60b997c63e49",
"files": [
".json"
],
"subMetas": {},
"userData": {}
}

View File

@@ -0,0 +1,119 @@
import { _decorator, EditBox, Button } from 'cc';
import { UIBase } from '../../Framework/UI/UIBase';
import { NetManager } from '../../Framework/Net/NetManager';
import { ReqLogin, ResLogin } from '../../Framework/Net/LoginProtocol';
import { AppStatusManager } from '../AppStatus/AppStatusManager';
const { ccclass } = _decorator;
/**
* 登录界面
* 职责:
* - 显示登录UI
* - 处理账号输入
* - 处理登录按钮点击
* - 发送登录请求
* - 登录成功后切换到游戏状态
*/
@ccclass('UILogin')
export class UILogin extends UIBase {
/** 账号输入框 */
private _inputAccount: EditBox | null = null;
/** 登录按钮 */
private _btnLogin: Button | null = null;
/** 是否正在登录中 */
private _isLogging: boolean = false;
/**
* 获取UI资源路径
*/
onGetUrl(): string {
return 'res://UI/Login/UILogin';
}
/**
* UI初始化
*/
async onStart(params?: any): Promise<void> {
console.log('[UILogin] 登录界面初始化', params);
// 使用GetChild查找子节点
this._inputAccount = this.GetChild('mid/input_account', EditBox);
this._btnLogin = this.GetChild('mid/btn_login', Button);
// 使用SetClick绑定点击事件
this.SetClick(this._btnLogin, this.onLoginClick, this);
}
/**
* 登录按钮点击
*/
private async onLoginClick(): Promise<void> {
if (this._isLogging) {
console.log('[UILogin] 正在登录中,请稍候...');
return;
}
// 获取账号
const account = this._inputAccount?.string?.trim() || '';
if (!account) {
console.warn('[UILogin] 请输入账号');
// TODO: 显示提示UI
return;
}
console.log(`[UILogin] 开始登录,账号: ${account}`);
// 开始登录
this._isLogging = true;
try {
await this.login(account);
} catch (error) {
console.error('[UILogin] 登录失败:', error);
// TODO: 显示错误提示UI
} finally {
this._isLogging = false;
}
}
/**
* 执行登录
* @param account 账号
*/
private async login(account: string): Promise<void> {
const netManager = NetManager.getInstance();
// 使用类型化的登录协议
const result = await netManager.callApi<ReqLogin, ResLogin>('Login', {
account: account
});
if (result && result.success) {
console.log('[UILogin] 登录成功:', result);
// 登录成功,切换到游戏状态
const appStatusManager = AppStatusManager.getInstance();
appStatusManager.changeState('Game', {
player: result.player,
isNewPlayer: result.isNewPlayer || false
});
// 隐藏登录界面
this.hide();
} else {
console.error('[UILogin] 登录失败:', result?.message || '返回结果为空');
// TODO: 显示错误提示UI
}
}
/**
* UI清理
*/
onEnd(): void {
console.log('[UILogin] 登录界面清理');
}
}

View File

@@ -0,0 +1,9 @@
{
"ver": "4.0.24",
"importer": "typescript",
"imported": true,
"uuid": "e0b2fb88-6d47-45ff-97cf-f6f4ad861e75",
"files": [],
"subMetas": {},
"userData": {}
}

View File

@@ -0,0 +1,9 @@
{
"ver": "1.2.0",
"importer": "directory",
"imported": true,
"uuid": "be7fbdee-0511-4b30-bcde-9b890c6121ea",
"files": [],
"subMetas": {},
"userData": {}
}

View File

@@ -0,0 +1,81 @@
import { _decorator, Component } from 'cc';
import { AppStatusManager } from '../App/AppStatus/AppStatusManager';
const { ccclass, property } = _decorator;
/**
* Boot启动组件
*
* 使用方法:
* 1. 在Cocos Creator中打开主场景(main.scene)
* 2. 创建一个空节点,命名为 "Boot"
* 3. 将此脚本挂载到Boot节点上
* 4. 运行游戏,应用将自动启动
*
* 职责:
* - 作为整个应用的入口点
* - 初始化AppStatusManager
* - 启动应用状态流转
* - 在每帧更新状态机
*/
@ccclass('Boot')
export class Boot extends Component {
private _appStatusManager: AppStatusManager | null = null;
/**
* 组件首次激活时调用
*/
start() {
console.log("=================================");
console.log(" Cocos3.x Roguelike Game");
console.log(" Boot Component Started");
console.log("=================================");
// 初始化并启动应用
this.initApp();
}
/**
* 初始化应用
*/
private initApp(): void {
console.log("[Boot] 初始化应用...");
try {
// 1. 获取AppStatusManager单例
this._appStatusManager = AppStatusManager.getInstance();
// 2. 启动应用(从Boot状态开始)
this._appStatusManager.start();
console.log("[Boot] 应用启动成功");
} catch (error) {
console.error("[Boot] 应用启动失败:", error);
}
}
/**
* 每帧更新
* @param deltaTime 距离上一帧的时间增量(秒)
*/
update(deltaTime: number) {
// 更新应用状态机
if (this._appStatusManager) {
this._appStatusManager.update(deltaTime);
}
}
/**
* 组件销毁时调用
*/
onDestroy() {
console.log("[Boot] 组件销毁");
// 销毁应用状态管理器
if (this._appStatusManager) {
this._appStatusManager.destroy();
this._appStatusManager = null;
}
}
}

View File

@@ -0,0 +1,9 @@
{
"ver": "4.0.24",
"importer": "typescript",
"imported": true,
"uuid": "324af443-86c7-4983-b726-c1ca5c603593",
"files": [],
"subMetas": {},
"userData": {}
}

View File

@@ -0,0 +1,9 @@
{
"ver": "1.2.0",
"importer": "directory",
"imported": true,
"uuid": "4602b30b-05bf-465d-8537-b3afbbc83fb0",
"files": [],
"subMetas": {},
"userData": {}
}

View File

@@ -0,0 +1,11 @@
{
"ver": "1.0.1",
"importer": "text",
"imported": true,
"uuid": "0f4b90cd-984e-421f-ad07-97ba164f3b8d",
"files": [
".json"
],
"subMetas": {},
"userData": {}
}

View File

@@ -0,0 +1,9 @@
{
"ver": "4.0.24",
"importer": "typescript",
"imported": true,
"uuid": "b5331a2b-20fa-4653-8013-deb54bad8d2e",
"files": [],
"subMetas": {},
"userData": {}
}

View File

@@ -0,0 +1,11 @@
{
"ver": "1.0.1",
"importer": "text",
"imported": true,
"uuid": "6eb67b95-26c3-410a-a9ee-441d4fa23371",
"files": [
".json"
],
"subMetas": {},
"userData": {}
}

View File

@@ -0,0 +1,281 @@
import { Asset, AssetManager, assetManager } from 'cc';
import { ResProxy } from './ResProxy';
/**
* Bundle代理类
* 职责:
* - 异步加载Bundle
* - 管理Bundle的生命周期
* - 提供从Bundle加载资源的统一接口
*/
export class BundleProxy {
private _bundleName: string;
private _bundle: AssetManager.Bundle | null = null;
private _loading: Promise<AssetManager.Bundle> | null = null;
constructor(bundleName: string, bundle?: AssetManager.Bundle) {
this._bundleName = bundleName;
this._bundle = bundle || null;
}
/**
* 获取Bundle名称
*/
get bundleName(): string {
return this._bundleName;
}
/**
* 获取Bundle实例(同步)
*/
get bundle(): AssetManager.Bundle | null {
return this._bundle;
}
/**
* 异步加载Bundle
* @returns Promise<AssetManager.Bundle>
*/
async loadBundle(): Promise<AssetManager.Bundle> {
// 如果已经加载完成,直接返回
if (this._bundle) {
return this._bundle;
}
// 如果正在加载中,返回加载Promise
if (this._loading) {
return this._loading;
}
// 开始加载
this._loading = new Promise<AssetManager.Bundle>((resolve, reject) => {
console.log(`[BundleProxy] 开始加载Bundle: ${this._bundleName}`);
assetManager.loadBundle(this._bundleName, (err, bundle) => {
if (err) {
console.error(`[BundleProxy] 加载Bundle失败: ${this._bundleName}`, err);
this._loading = null;
reject(err);
return;
}
this._bundle = bundle;
this._loading = null;
console.log(`[BundleProxy] Bundle加载成功: ${this._bundleName}`);
resolve(bundle);
});
});
return this._loading;
}
/**
* 创建资源代理
* @param path 资源路径
* @param type 资源类型
* @returns ResProxy
*/
createResProxy<T extends Asset>(
path: string,
type: new (...args: any[]) => T
): ResProxy<T> {
return new ResProxy<T>(this, path, type);
}
/**
* 加载资源
* @param path 资源路径
* @param type 资源类型
* @returns Promise<资源>
*/
async loadRes<T extends Asset>(
path: string,
type: new (...args: any[]) => T
): Promise<T> {
const bundle = await this.loadBundle();
return new Promise((resolve, reject) => {
console.log(`[BundleProxy] 开始加载资源: ${this._bundleName}/${path}`);
bundle.load(path, type, (err, asset) => {
if (err) {
console.error(`[BundleProxy] 加载资源失败: ${this._bundleName}/${path}`, err);
reject(err);
return;
}
console.log(`[BundleProxy] 资源加载成功: ${this._bundleName}/${path}`);
resolve(asset as T);
});
});
}
/**
* 预加载资源
* @param path 资源路径
* @param type 资源类型
* @param onProgress 进度回调
* @returns Promise<void>
*/
async preload<T extends Asset>(
path: string,
type: new (...args: any[]) => T,
onProgress?: (finished: number, total: number) => void
): Promise<void> {
const bundle = await this.loadBundle();
return new Promise((resolve, reject) => {
console.log(`[BundleProxy] 开始预加载资源: ${this._bundleName}/${path}`);
bundle.preload(
path,
type,
(finished, total) => {
if (onProgress) {
onProgress(finished, total);
}
},
(err) => {
if (err) {
console.error(`[BundleProxy] 预加载资源失败: ${this._bundleName}/${path}`, err);
reject(err);
return;
}
console.log(`[BundleProxy] 资源预加载成功: ${this._bundleName}/${path}`);
resolve();
}
);
});
}
/**
* 加载目录
* @param dir 目录路径
* @param type 资源类型
* @param onProgress 进度回调
* @returns Promise<资源数组>
*/
async loadDir<T extends Asset>(
dir: string,
type: new (...args: any[]) => T,
onProgress?: (finished: number, total: number) => void
): Promise<T[]> {
const bundle = await this.loadBundle();
return new Promise((resolve, reject) => {
console.log(`[BundleProxy] 开始加载目录: ${this._bundleName}/${dir}`);
bundle.loadDir(
dir,
type,
(finished, total) => {
if (onProgress) {
onProgress(finished, total);
}
},
(err, assets) => {
if (err) {
console.error(`[BundleProxy] 加载目录失败: ${this._bundleName}/${dir}`, err);
reject(err);
return;
}
console.log(`[BundleProxy] 目录加载成功: ${this._bundleName}/${dir}, 共 ${assets.length} 个资源`);
resolve(assets as T[]);
}
);
});
}
/**
* 预加载目录
* @param dir 目录路径
* @param type 资源类型
* @param onProgress 进度回调
* @returns Promise<void>
*/
async preloadDir<T extends Asset>(
dir: string,
type: new (...args: any[]) => T,
onProgress?: (finished: number, total: number) => void
): Promise<void> {
const bundle = await this.loadBundle();
return new Promise((resolve, reject) => {
console.log(`[BundleProxy] 开始预加载目录: ${this._bundleName}/${dir}`);
bundle.preloadDir(
dir,
type,
(finished, total) => {
if (onProgress) {
onProgress(finished, total);
}
},
(err) => {
if (err) {
console.error(`[BundleProxy] 预加载目录失败: ${this._bundleName}/${dir}`, err);
reject(err);
return;
}
console.log(`[BundleProxy] 目录预加载成功: ${this._bundleName}/${dir}`);
resolve();
}
);
});
}
/**
* 释放资源
* @param path 资源路径
*/
release(path: string): void {
if (!this._bundle) {
console.warn(`[BundleProxy] Bundle未加载,无法释放资源: ${this._bundleName}/${path}`);
return;
}
this._bundle.release(path);
console.log(`[BundleProxy] 释放资源: ${this._bundleName}/${path}`);
}
/**
* 释放目录资源
* @param dir 目录路径
*/
releaseDir(dir: string): void {
if (!this._bundle) {
console.warn(`[BundleProxy] Bundle未加载,无法释放目录: ${this._bundleName}/${dir}`);
return;
}
// Cocos没有直接的releaseDir方法,需要手动释放目录下的资源
// 这里简化处理,实际使用时可能需要追踪加载的资源
console.log(`[BundleProxy] 释放目录资源: ${this._bundleName}/${dir}`);
}
/**
* 释放所有资源
*/
releaseAll(): void {
if (!this._bundle) {
console.warn(`[BundleProxy] Bundle未加载,无法释放所有资源: ${this._bundleName}`);
return;
}
this._bundle.releaseAll();
console.log(`[BundleProxy] 释放所有资源: ${this._bundleName}`);
}
/**
* 销毁Bundle
*/
destroy(): void {
if (this._bundle) {
this.releaseAll();
this._bundle = null;
}
this._loading = null;
}
}

View File

@@ -0,0 +1,9 @@
{
"ver": "4.0.24",
"importer": "typescript",
"imported": true,
"uuid": "728bdb32-e73a-4fae-b942-b66412e95c6a",
"files": [],
"subMetas": {},
"userData": {}
}

View File

@@ -0,0 +1,11 @@
{
"ver": "1.0.1",
"importer": "text",
"imported": true,
"uuid": "3ab80465-bd0c-4023-b253-b019b2c686df",
"files": [
".json"
],
"subMetas": {},
"userData": {}
}

View File

@@ -1,39 +1,41 @@
import { Asset, AssetManager, assetManager, resources } from 'cc';
import { BundleProxy } from './BundleProxy';
import { ResProxy } from './ResProxy';
/**
* 资源管理器
* 职责:
* - 统一管理资源加载
* - 提供从bundle中按路径加载资源的接口
* - 支持资源加载
* - 统一管理Bundle和资源
* - 提供BundleProxy和ResProxy的创建和管理
* - 提供便捷的资源加载接口
* - 管理资源缓存和释放
*/
export class ResMgr {
private static _instance: ResMgr | null = null;
/**
* 资源缓存
* key: bundleName:path
* value: Asset
* BundleProxy缓存
* key: bundleName
* value: BundleProxy
*/
private _cache: Map<string, Asset>;
private _bundleProxies: Map<string, BundleProxy>;
/**
* Bundle缓存
* key: bundleName
* value: AssetManager.Bundle
* ResProxy缓存
* key: bundleName:path
* value: ResProxy
*/
private _bundles: Map<string, AssetManager.Bundle>;
private _resProxies: Map<string, ResProxy<any>>;
/**
* 构造函数(私有)
*/
private constructor() {
this._cache = new Map<string, Asset>();
this._bundles = new Map<string, AssetManager.Bundle>();
this._bundleProxies = new Map<string, BundleProxy>();
this._resProxies = new Map<string, ResProxy<any>>();
// 默认添加resources bundle
this._bundles.set('resources', resources);
this._bundleProxies.set('resources', new BundleProxy('resources', resources));
}
/**
@@ -46,27 +48,6 @@ export class ResMgr {
return this._instance;
}
/**
* 获取Bundle
* @param bundleName bundle名称
*/
private getBundle(bundleName: string): AssetManager.Bundle | null {
// 从缓存中获取
if (this._bundles.has(bundleName)) {
return this._bundles.get(bundleName)!;
}
// 使用 assetManager.getBundle() 方法获取已加载的 Bundle
const bundle = assetManager.getBundle(bundleName);
if (bundle) {
this._bundles.set(bundleName, bundle);
return bundle;
}
console.error(`[ResMgr] Bundle "${bundleName}" 不存在`);
return null;
}
/**
* 生成缓存key
*/
@@ -74,6 +55,55 @@ export class ResMgr {
return `${bundleName}:${path}`;
}
/**
* 获取或创建BundleProxy
* @param bundleName bundle名称
* @returns BundleProxy
*/
getBundleProxy(bundleName: string): BundleProxy {
if (!this._bundleProxies.has(bundleName)) {
// 尝试从assetManager获取已加载的bundle
const bundle = assetManager.getBundle(bundleName);
const bundleProxy = new BundleProxy(bundleName, bundle || undefined);
this._bundleProxies.set(bundleName, bundleProxy);
}
return this._bundleProxies.get(bundleName)!;
}
/**
* 加载Bundle
* @param bundleName bundle名称
* @returns Promise<BundleProxy>
*/
async loadBundle(bundleName: string): Promise<BundleProxy> {
const bundleProxy = this.getBundleProxy(bundleName);
await bundleProxy.loadBundle();
return bundleProxy;
}
/**
* 获取或创建ResProxy
* @param bundleName bundle名称
* @param path 资源路径
* @param type 资源类型
* @returns ResProxy
*/
getResProxy<T extends Asset>(
bundleName: string,
path: string,
type: new (...args: any[]) => T
): ResProxy<T> {
const cacheKey = this.getCacheKey(bundleName, path);
if (!this._resProxies.has(cacheKey)) {
const bundleProxy = this.getBundleProxy(bundleName);
const resProxy = bundleProxy.createResProxy(path, type);
this._resProxies.set(cacheKey, resProxy);
}
return this._resProxies.get(cacheKey)! as ResProxy<T>;
}
/**
* 加载单个资源
* @param bundleName bundle名称
@@ -81,44 +111,13 @@ export class ResMgr {
* @param type 资源类型
* @returns Promise<资源>
*/
load<T extends Asset>(
async load<T extends Asset>(
bundleName: string,
path: string,
type: new (...args: any[]) => T
): Promise<T> {
return new Promise((resolve, reject) => {
const cacheKey = this.getCacheKey(bundleName, path);
// 检查缓存
if (this._cache.has(cacheKey)) {
const cached = this._cache.get(cacheKey) as T;
console.log(`[ResMgr] 从缓存加载资源: ${path}`);
resolve(cached);
return;
}
// 获取bundle
const bundle = this.getBundle(bundleName);
if (!bundle) {
reject(new Error(`Bundle "${bundleName}" 不存在`));
return;
}
// 加载资源
console.log(`[ResMgr] 开始加载资源: ${bundleName}/${path}`);
bundle.load(path, type, (err, asset) => {
if (err) {
console.error(`[ResMgr] 加载资源失败: ${bundleName}/${path}`, err);
reject(err);
return;
}
// 缓存资源
this._cache.set(cacheKey, asset as Asset);
console.log(`[ResMgr] 资源加载成功: ${bundleName}/${path}`);
resolve(asset as T);
});
});
const resProxy = this.getResProxy(bundleName, path, type);
return resProxy.load();
}
/**
@@ -129,43 +128,14 @@ export class ResMgr {
* @param onProgress 进度回调
* @returns Promise<void>
*/
preload<T extends Asset>(
async preload<T extends Asset>(
bundleName: string,
path: string,
type: new (...args: any[]) => T,
onProgress?: (finished: number, total: number) => void
): Promise<void> {
return new Promise((resolve, reject) => {
// 获取bundle
const bundle = this.getBundle(bundleName);
if (!bundle) {
reject(new Error(`Bundle "${bundleName}" 不存在`));
return;
}
console.log(`[ResMgr] 开始预加载资源: ${bundleName}/${path}`);
// 预加载资源
bundle.preload(
path,
type,
(finished, total) => {
if (onProgress) {
onProgress(finished, total);
}
},
(err) => {
if (err) {
console.error(`[ResMgr] 预加载资源失败: ${bundleName}/${path}`, err);
reject(err);
return;
}
console.log(`[ResMgr] 资源预加载成功: ${bundleName}/${path}`);
resolve();
}
);
});
const resProxy = this.getResProxy(bundleName, path, type);
return resProxy.preload(onProgress);
}
/**
@@ -173,42 +143,35 @@ export class ResMgr {
* @param bundleName bundle名称
* @param dir 目录路径
* @param type 资源类型
* @param onProgress 进度回调
* @returns Promise<资源数组>
*/
loadDir<T extends Asset>(
async loadDir<T extends Asset>(
bundleName: string,
dir: string,
type: new (...args: any[]) => T
type: new (...args: any[]) => T,
onProgress?: (finished: number, total: number) => void
): Promise<T[]> {
return new Promise((resolve, reject) => {
// 获取bundle
const bundle = this.getBundle(bundleName);
if (!bundle) {
reject(new Error(`Bundle "${bundleName}" 不存在`));
return;
}
console.log(`[ResMgr] 开始加载目录: ${bundleName}/${dir}`);
// 加载目录
bundle.loadDir(dir, type, (err, assets) => {
if (err) {
console.error(`[ResMgr] 加载目录失败: ${bundleName}/${dir}`, err);
reject(err);
return;
}
// 缓存所有资源
assets.forEach((asset) => {
const path = `${dir}/${asset.name}`;
const cacheKey = this.getCacheKey(bundleName, path);
this._cache.set(cacheKey, asset);
});
console.log(`[ResMgr] 目录加载成功: ${bundleName}/${dir}, 共 ${assets.length} 个资源`);
resolve(assets as T[]);
});
});
const bundleProxy = this.getBundleProxy(bundleName);
return bundleProxy.loadDir(dir, type, onProgress);
}
/**
* 预加载目录
* @param bundleName bundle名称
* @param dir 目录路径
* @param type 资源类型
* @param onProgress 进度回调
* @returns Promise<void>
*/
async preloadDir<T extends Asset>(
bundleName: string,
dir: string,
type: new (...args: any[]) => T,
onProgress?: (finished: number, total: number) => void
): Promise<void> {
const bundleProxy = this.getBundleProxy(bundleName);
return bundleProxy.preloadDir(dir, type, onProgress);
}
/**
@@ -219,17 +182,16 @@ export class ResMgr {
release(bundleName: string, path: string): void {
const cacheKey = this.getCacheKey(bundleName, path);
// 从缓存中移除
if (this._cache.has(cacheKey)) {
const asset = this._cache.get(cacheKey)!;
this._cache.delete(cacheKey);
// 释放资源
const bundle = this.getBundle(bundleName);
if (bundle) {
bundle.release(path);
console.log(`[ResMgr] 释放资源: ${bundleName}/${path}`);
}
// 从ResProxy缓存中获取并释放
if (this._resProxies.has(cacheKey)) {
const resProxy = this._resProxies.get(cacheKey)!;
resProxy.release();
// 可选:从缓存中移除ResProxy
// this._resProxies.delete(cacheKey);
} else {
// 如果没有ResProxy,直接从BundleProxy释放
const bundleProxy = this.getBundleProxy(bundleName);
bundleProxy.release(path);
}
}
@@ -239,55 +201,136 @@ export class ResMgr {
* @param dir 目录路径
*/
releaseDir(bundleName: string, dir: string): void {
// 释放所有以dir开头的缓存资源
// 释放所有以dir开头的ResProxy
const prefix = `${bundleName}:${dir}`;
const keysToDelete: string[] = [];
this._cache.forEach((_, key) => {
this._resProxies.forEach((resProxy, key) => {
if (key.startsWith(prefix)) {
resProxy.release();
keysToDelete.push(key);
}
});
// 可选:从缓存中移除ResProxy
keysToDelete.forEach((key) => {
this._cache.delete(key);
// this._resProxies.delete(key);
});
// 释放目录
const bundle = this.getBundle(bundleName);
if (bundle) {
bundle.releaseAll();
console.log(`[ResMgr] 释放目录资源: ${bundleName}/${dir}, 共 ${keysToDelete.length} 个资源`);
}
// 从BundleProxy释放目录
const bundleProxy = this.getBundleProxy(bundleName);
bundleProxy.releaseDir(dir);
console.log(`[ResMgr] 释放目录资源: ${bundleName}/${dir}, 共 ${keysToDelete.length} 个资源`);
}
/**
* 释放Bundle的所有资源
* @param bundleName bundle名称
*/
releaseBundle(bundleName: string): void {
// 释放该Bundle下所有的ResProxy
const prefix = `${bundleName}:`;
const keysToDelete: string[] = [];
this._resProxies.forEach((resProxy, key) => {
if (key.startsWith(prefix)) {
resProxy.release();
keysToDelete.push(key);
}
});
// 从缓存中移除ResProxy
keysToDelete.forEach((key) => {
this._resProxies.delete(key);
});
// 释放BundleProxy的所有资源
const bundleProxy = this.getBundleProxy(bundleName);
bundleProxy.releaseAll();
console.log(`[ResMgr] 释放Bundle所有资源: ${bundleName}, 共 ${keysToDelete.length}`);
}
/**
* 释放所有资源
*/
releaseAll(): void {
console.log(`[ResMgr] 释放所有资源, 共 ${this._cache.size}`);
this._cache.clear();
console.log(`[ResMgr] 释放所有资源, 共 ${this._resProxies.size}ResProxy`);
// 释放所有bundle的资源
this._bundles.forEach((bundle, name) => {
// 释放所有ResProxy
this._resProxies.forEach((resProxy) => {
resProxy.release();
});
this._resProxies.clear();
// 释放所有BundleProxy的资源(除了resources)
this._bundleProxies.forEach((bundleProxy, name) => {
if (name !== 'resources') {
bundle.releaseAll();
bundleProxy.releaseAll();
}
});
}
/**
* 获取缓存大小
* 获取缓存的ResProxy数量
*/
getCacheSize(): number {
return this._cache.size;
return this._resProxies.size;
}
/**
* 获取缓存的BundleProxy数量
*/
getBundleCount(): number {
return this._bundleProxies.size;
}
/**
* 清空缓存(不释放资源)
*/
clearCache(): void {
console.log(`[ResMgr] 清空缓存, 共 ${this._cache.size}`);
this._cache.clear();
console.log(`[ResMgr] 清空缓存, 共 ${this._resProxies.size}ResProxy`);
// 清空所有ResProxy的缓存
this._resProxies.forEach((resProxy) => {
resProxy.clearCache();
});
this._resProxies.clear();
}
/**
* 销毁Bundle
* @param bundleName bundle名称
*/
destroyBundle(bundleName: string): void {
if (bundleName === 'resources') {
console.warn('[ResMgr] 不能销毁resources bundle');
return;
}
// 释放并移除该Bundle的所有ResProxy
const prefix = `${bundleName}:`;
const keysToDelete: string[] = [];
this._resProxies.forEach((resProxy, key) => {
if (key.startsWith(prefix)) {
resProxy.release();
keysToDelete.push(key);
}
});
keysToDelete.forEach((key) => {
this._resProxies.delete(key);
});
// 销毁BundleProxy
if (this._bundleProxies.has(bundleName)) {
const bundleProxy = this._bundleProxies.get(bundleName)!;
bundleProxy.destroy();
this._bundleProxies.delete(bundleName);
}
console.log(`[ResMgr] 销毁Bundle: ${bundleName}`);
}
}

View File

@@ -0,0 +1,171 @@
import { Asset } from 'cc';
import { BundleProxy } from './BundleProxy';
/**
* 资源代理类
* 职责:
* - 封装单个资源的异步加载逻辑
* - 管理资源的缓存状态
* - 提供资源的加载、预加载、释放接口
*/
export class ResProxy<T extends Asset> {
private _bundleProxy: BundleProxy;
private _path: string;
private _type: new (...args: any[]) => T;
private _asset: T | null = null;
private _loading: Promise<T> | null = null;
private _preloading: Promise<void> | null = null;
constructor(
bundleProxy: BundleProxy,
path: string,
type: new (...args: any[]) => T
) {
this._bundleProxy = bundleProxy;
this._path = path;
this._type = type;
}
/**
* 获取资源路径
*/
get path(): string {
return this._path;
}
/**
* 获取Bundle名称
*/
get bundleName(): string {
return this._bundleProxy.bundleName;
}
/**
* 获取资源实例(同步)
*/
get asset(): T | null {
return this._asset;
}
/**
* 是否已加载
*/
get isLoaded(): boolean {
return this._asset !== null;
}
/**
* 是否正在加载中
*/
get isLoading(): boolean {
return this._loading !== null;
}
/**
* 是否正在预加载中
*/
get isPreloading(): boolean {
return this._preloading !== null;
}
/**
* 加载资源
* @returns Promise<资源>
*/
async load(): Promise<T> {
// 如果已经加载完成,直接返回
if (this._asset) {
console.log(`[ResProxy] 从缓存获取资源: ${this.bundleName}/${this._path}`);
return this._asset;
}
// 如果正在加载中,返回加载Promise
if (this._loading) {
return this._loading;
}
// 开始加载
this._loading = this._bundleProxy.loadRes(this._path, this._type);
try {
this._asset = await this._loading;
return this._asset;
} finally {
this._loading = null;
}
}
/**
* 预加载资源
* @param onProgress 进度回调
* @returns Promise<void>
*/
async preload(onProgress?: (finished: number, total: number) => void): Promise<void> {
// 如果已经加载完成,直接返回
if (this._asset) {
console.log(`[ResProxy] 资源已加载,无需预加载: ${this.bundleName}/${this._path}`);
return;
}
// 如果正在预加载中,返回预加载Promise
if (this._preloading) {
return this._preloading;
}
// 如果正在加载中,等待加载完成
if (this._loading) {
await this._loading;
return;
}
// 开始预加载
this._preloading = this._bundleProxy.preload(this._path, this._type, onProgress);
try {
await this._preloading;
} finally {
this._preloading = null;
}
}
/**
* 释放资源
*/
release(): void {
if (!this._asset) {
console.warn(`[ResProxy] 资源未加载,无法释放: ${this.bundleName}/${this._path}`);
return;
}
this._bundleProxy.release(this._path);
this._asset = null;
console.log(`[ResProxy] 释放资源: ${this.bundleName}/${this._path}`);
}
/**
* 清除缓存(不释放资源)
*/
clearCache(): void {
this._asset = null;
this._loading = null;
this._preloading = null;
}
/**
* 重新加载资源
* @returns Promise<资源>
*/
async reload(): Promise<T> {
if (this._asset) {
this.release();
}
return this.load();
}
/**
* 获取完整路径
*/
getFullPath(): string {
return `${this.bundleName}/${this._path}`;
}
}

View File

@@ -0,0 +1,9 @@
{
"ver": "4.0.24",
"importer": "typescript",
"imported": true,
"uuid": "e33b5325-cbc7-4082-9e05-093bb8478e2d",
"files": [],
"subMetas": {},
"userData": {}
}

View File

@@ -123,9 +123,16 @@ export class UILogin extends UIBase {
/**
* 必须重载: 返回 UI 资源路径
*
* 编写规则:
* 1. 普通资源路径: 'prefabs/ui/UILogin'
* 2. Bundle资源路径: '[bundle名]://资源路径'
* 示例: 'ui-bundle://prefabs/UILogin'
*/
onGetUrl(): string {
return 'prefabs/ui/UILogin';
// 或使用 bundle 方式:
// return 'ui-bundle://prefabs/UILogin';
}
/**
@@ -323,6 +330,8 @@ onEnd() - 清理资源(可选)
## ⚠️ 注意事项
1. **必须重载 onGetUrl()**: 每个 UI 必须明确定义资源路径
- 普通资源路径格式: `'prefabs/ui/UILogin'`
- Bundle资源路径格式: `'[bundle名]://资源路径'` (例如: `'ui-bundle://prefabs/UILogin'`)
2. **UI 根节点设置**: 在加载任何 UI 之前,必须先设置 UI 根节点
3. **缓存机制**: 已加载的 UI 会被缓存,再次加载时直接使用缓存
4. **资源自动管理**: UIMgr 会自动管理资源加载和释放
@@ -367,7 +376,11 @@ if (canvas) {
```typescript
// 检查: 资源路径是否正确
onGetUrl(): string {
return 'prefabs/ui/UILogin'; // 确保路径正确
// 普通资源路径
return 'prefabs/ui/UILogin';
// 或使用 Bundle 资源路径
// return 'ui-bundle://prefabs/UILogin';
}
```
@@ -383,6 +396,8 @@ if (!UIMgr.getInstance().has(UILogin)) {
1. **单一职责**: 每个 UI 类只负责一个界面
2. **资源路径统一**: 建议在配置文件中统一管理 UI 资源路径
- 对于需要分包加载的 UI,使用 Bundle 路径格式: `'[bundle名]://资源路径'`
- 对于通用 UI,使用普通路径格式: `'资源路径'`
3. **事件解绑**: 在 onEnd() 中解绑所有事件,避免内存泄漏
4. **参数传递**: 使用 params 参数在加载时传递初始数据
5. **缓存利用**: 对频繁切换的 UI,利用缓存避免重复加载

View File

@@ -0,0 +1,11 @@
{
"ver": "1.0.1",
"importer": "text",
"imported": true,
"uuid": "fbfe85a1-0fe1-4ac7-b004-36c370c6a4eb",
"files": [
".json"
],
"subMetas": {},
"userData": {}
}

View File

@@ -1,4 +1,4 @@
import { Node } from 'cc';
import { Node, Component, Button } from 'cc';
/**
* UI基类
@@ -121,4 +121,67 @@ export abstract class UIBase {
isLoaded(): boolean {
return this._isLoaded;
}
/**
* 查找子节点并获取组件
* @param path 节点路径,支持多层级查找(如: 'mid/input_account')
* @param componentType 组件类型
* @returns 组件实例,未找到则返回null
* @example
* const editBox = this.GetChild('mid/input_account', EditBox);
* const button = this.GetChild('btn_login', Button);
*/
protected GetChild<T extends Component>(path: string, componentType: { new(): T }): T | null {
if (!this._node) {
console.error('[UIBase] GetChild失败: UI根节点不存在');
return null;
}
const childNode = this._node.getChildByPath(path);
if (!childNode) {
console.error(`[UIBase] GetChild失败: 未找到节点 ${path}`);
return null;
}
const component = childNode.getComponent(componentType);
if (!component) {
console.error(`[UIBase] GetChild失败: 节点 ${path} 未挂载 ${componentType.name} 组件`);
return null;
}
return component;
}
/**
* 设置按钮点击事件
* @param button Button组件或包含Button组件的Node
* @param callback 点击回调函数
* @param target 回调函数的this指向
* @example
* const btn = this.GetChild('btn_login', Button);
* this.SetClick(btn, this.onLoginClick, this);
*/
protected SetClick(button: Button | Node | null, callback: () => void, target?: any): void {
if (!button) {
console.error('[UIBase] SetClick失败: button为null');
return;
}
let targetNode: Node | null = null;
// 判断是Button组件还是Node
if (button instanceof Button) {
targetNode = button.node;
} else if (button instanceof Node) {
targetNode = button;
}
if (!targetNode) {
console.error('[UIBase] SetClick失败: 无法获取目标节点');
return;
}
// 绑定点击事件
targetNode.on(Node.EventType.TOUCH_END, callback, target);
}
}

View File

@@ -82,12 +82,14 @@ export class UIMgr {
}
console.log(`[UIMgr] 开始加载UI: ${className} (${url})`);
const args = url.split('://')
const bundleName = args[0];
const resourcePath = args[1];
// 通过ResMgr加载预制体
try {
const prefab = await ResMgr.getInstance().load<Prefab>(
'resources',
url,
bundleName,
resourcePath,
Prefab
);

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,3 @@
<script type="module" crossorigin src="/dist/assets/index.02b86726.js"></script>
<link rel="stylesheet" href="/dist/assets/index.1d01bced.css">
<div id="app"></div>

View File

@@ -0,0 +1,47 @@
<html class="dark">
<head>
<link rel="icon" href="./favicon.ico" />
<meta charset="utf-8" />
<title><%=title%></title>
<meta
name="viewport"
content="width=device-width,user-scalable=no,initial-scale=1,minimum-scale=1,maximum-scale=1,minimal-ui=true"
/>
<meta name="apple-mobile-web-app-capable" content="yes" />
<meta name="full-screen" content="yes" />
<meta name="screen-orientation" content="portrait" />
<meta name="x5-fullscreen" content="true" />
<meta name="360-fullscreen" content="true" />
<meta name="renderer" content="webkit" />
<meta name="force-rendering" content="webkit" />
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1" />
<link rel="stylesheet" type="text/css" href="./index.css" />
</head>
<body>
<%- include(cocosToolBar, {config: config}) %>
<div id="content" class="content">
<div class="contentWrap">
<div id="GameDiv" class="wrapper">
<div id="Cocos3dGameContainer">
<canvas id="GameCanvas"></canvas>
</div>
<div id="splash">
<div class="progress-bar stripes"><span></span></div>
</div>
<div id="bulletin">
<div id="sceneIsEmpty" class="inner"><%=tip_sceneIsEmpty%></div>
</div>
<div class="error" id="error">
<div class="title">Error <i>(Please open the console to see detailed errors)</i></div>
<div class="error-main"></div>
</div>
</div>
</div>
<p class="footer">
<% include ./dist/index.html %>
</p>
</div>
<%- include(cocosTemplate, {}) %>
</body>
</html>

View File

@@ -0,0 +1 @@
{"name":"ccc-devtools","version":"2022/7/17","author":"Next","repo":"https://github.com/potato47/ccc-devtools.git"}