Compare commits
10 Commits
b0938a9fb8
...
7301adbb43
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7301adbb43 | ||
|
|
bbed2c5ebb | ||
|
|
1d91daa726 | ||
|
|
d530b48e34 | ||
|
|
3612ee74ea | ||
|
|
aefe242b76 | ||
|
|
aaff0038a6 | ||
|
|
549dadfcb5 | ||
|
|
354c40bd9b | ||
|
|
b9573b3ad4 |
1926
client/.github/instructions/development-plan.md
vendored
1926
client/.github/instructions/development-plan.md
vendored
File diff suppressed because it is too large
Load Diff
9
client/assets/res/UI.meta
Normal file
9
client/assets/res/UI.meta
Normal file
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"ver": "1.2.0",
|
||||
"importer": "directory",
|
||||
"imported": true,
|
||||
"uuid": "3a436318-510c-475a-9cd0-0c43affaa595",
|
||||
"files": [],
|
||||
"subMetas": {},
|
||||
"userData": {}
|
||||
}
|
||||
9
client/assets/res/UI/Login.meta
Normal file
9
client/assets/res/UI/Login.meta
Normal file
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"ver": "1.2.0",
|
||||
"importer": "directory",
|
||||
"imported": true,
|
||||
"uuid": "a3652a6e-81bf-43c3-8ace-2d6736a7cafc",
|
||||
"files": [],
|
||||
"subMetas": {},
|
||||
"userData": {}
|
||||
}
|
||||
1234
client/assets/res/UI/Login/UILogin.prefab
Normal file
1234
client/assets/res/UI/Login/UILogin.prefab
Normal file
File diff suppressed because it is too large
Load Diff
13
client/assets/res/UI/Login/UILogin.prefab.meta
Normal file
13
client/assets/res/UI/Login/UILogin.prefab.meta
Normal 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"
|
||||
}
|
||||
}
|
||||
9
client/assets/res/UI/World.meta
Normal file
9
client/assets/res/UI/World.meta
Normal file
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"ver": "1.2.0",
|
||||
"importer": "directory",
|
||||
"imported": true,
|
||||
"uuid": "97a29502-5783-4ec5-8dac-e504158c6f2f",
|
||||
"files": [],
|
||||
"subMetas": {},
|
||||
"userData": {}
|
||||
}
|
||||
393
client/assets/res/UI/World/UIWorld.prefab
Normal file
393
client/assets/res/UI/World/UIWorld.prefab
Normal 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
|
||||
}
|
||||
]
|
||||
13
client/assets/res/UI/World/UIWorld.prefab.meta
Normal file
13
client/assets/res/UI/World/UIWorld.prefab.meta
Normal 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"
|
||||
}
|
||||
}
|
||||
9
client/assets/scenes.meta
Normal file
9
client/assets/scenes.meta
Normal file
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"ver": "1.2.0",
|
||||
"importer": "directory",
|
||||
"imported": true,
|
||||
"uuid": "0c114b5d-84d5-4ae1-a491-6bfba7e77ce5",
|
||||
"files": [],
|
||||
"subMetas": {},
|
||||
"userData": {}
|
||||
}
|
||||
765
client/assets/scenes/main.scene
Normal file
765
client/assets/scenes/main.scene
Normal 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
|
||||
}
|
||||
]
|
||||
11
client/assets/scenes/main.scene.meta
Normal file
11
client/assets/scenes/main.scene.meta
Normal file
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"ver": "1.1.50",
|
||||
"importer": "scene",
|
||||
"imported": true,
|
||||
"uuid": "bfc16657-6cbe-4167-b6ba-2766e0864cf2",
|
||||
"files": [
|
||||
".json"
|
||||
],
|
||||
"subMetas": {},
|
||||
"userData": {}
|
||||
}
|
||||
9
client/assets/scripts.meta
Normal file
9
client/assets/scripts.meta
Normal file
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"ver": "1.2.0",
|
||||
"importer": "directory",
|
||||
"imported": true,
|
||||
"uuid": "411a8b79-dfcf-4814-bee8-075f787510c8",
|
||||
"files": [],
|
||||
"subMetas": {},
|
||||
"userData": {}
|
||||
}
|
||||
9
client/assets/scripts/App.meta
Normal file
9
client/assets/scripts/App.meta
Normal file
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"ver": "1.2.0",
|
||||
"importer": "directory",
|
||||
"imported": true,
|
||||
"uuid": "4c66e5c4-282b-45bd-b8a0-ec4a9a08c7ec",
|
||||
"files": [],
|
||||
"subMetas": {},
|
||||
"userData": {}
|
||||
}
|
||||
9
client/assets/scripts/App/AppStatus.meta
Normal file
9
client/assets/scripts/App/AppStatus.meta
Normal file
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"ver": "1.2.0",
|
||||
"importer": "directory",
|
||||
"imported": true,
|
||||
"uuid": "e9decc58-bdc5-45ad-b085-aabd2012b936",
|
||||
"files": [],
|
||||
"subMetas": {},
|
||||
"userData": {}
|
||||
}
|
||||
67
client/assets/scripts/App/AppStatus/AppStatusBoot.ts
Normal file
67
client/assets/scripts/App/AppStatus/AppStatusBoot.ts
Normal 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] 离开启动状态");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"ver": "4.0.24",
|
||||
"importer": "typescript",
|
||||
"imported": true,
|
||||
"uuid": "6a737e69-0f9a-47d8-b836-f3d8f586fcaa",
|
||||
"files": [],
|
||||
"subMetas": {},
|
||||
"userData": {}
|
||||
}
|
||||
201
client/assets/scripts/App/AppStatus/AppStatusGame.ts
Normal file
201
client/assets/scripts/App/AppStatus/AppStatusGame.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"ver": "4.0.24",
|
||||
"importer": "typescript",
|
||||
"imported": true,
|
||||
"uuid": "fb4c25d0-b65c-4881-9997-5e3bd5d50325",
|
||||
"files": [],
|
||||
"subMetas": {},
|
||||
"userData": {}
|
||||
}
|
||||
121
client/assets/scripts/App/AppStatus/AppStatusLogin.ts
Normal file
121
client/assets/scripts/App/AppStatus/AppStatusLogin.ts
Normal 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);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"ver": "4.0.24",
|
||||
"importer": "typescript",
|
||||
"imported": true,
|
||||
"uuid": "f0f1f913-0bf0-440c-9b66-9712ed982479",
|
||||
"files": [],
|
||||
"subMetas": {},
|
||||
"userData": {}
|
||||
}
|
||||
107
client/assets/scripts/App/AppStatus/AppStatusManager.ts
Normal file
107
client/assets/scripts/App/AppStatus/AppStatusManager.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"ver": "4.0.24",
|
||||
"importer": "typescript",
|
||||
"imported": true,
|
||||
"uuid": "191d542e-df2a-43a0-998c-4daefe65c598",
|
||||
"files": [],
|
||||
"subMetas": {},
|
||||
"userData": {}
|
||||
}
|
||||
396
client/assets/scripts/App/AppStatus/README.md
Normal file
396
client/assets/scripts/App/AppStatus/README.md
Normal 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) - 登录模块
|
||||
11
client/assets/scripts/App/AppStatus/README.md.meta
Normal file
11
client/assets/scripts/App/AppStatus/README.md.meta
Normal file
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"ver": "1.0.1",
|
||||
"importer": "text",
|
||||
"imported": true,
|
||||
"uuid": "87d8c6a8-6b03-44b9-98a2-5eb7b8457271",
|
||||
"files": [
|
||||
".json"
|
||||
],
|
||||
"subMetas": {},
|
||||
"userData": {}
|
||||
}
|
||||
9
client/assets/scripts/App/Game.meta
Normal file
9
client/assets/scripts/App/Game.meta
Normal file
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"ver": "1.2.0",
|
||||
"importer": "directory",
|
||||
"imported": true,
|
||||
"uuid": "7884d98f-e4a9-4e92-aed3-214cddfcd2b4",
|
||||
"files": [],
|
||||
"subMetas": {},
|
||||
"userData": {}
|
||||
}
|
||||
207
client/assets/scripts/App/Game/PlayerController.ts
Normal file
207
client/assets/scripts/App/Game/PlayerController.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
9
client/assets/scripts/App/Game/PlayerController.ts.meta
Normal file
9
client/assets/scripts/App/Game/PlayerController.ts.meta
Normal file
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"ver": "4.0.24",
|
||||
"importer": "typescript",
|
||||
"imported": true,
|
||||
"uuid": "9e1f7c66-cb9e-4e5f-8642-605d3568c4ac",
|
||||
"files": [],
|
||||
"subMetas": {},
|
||||
"userData": {}
|
||||
}
|
||||
325
client/assets/scripts/App/Game/README.md
Normal file
325
client/assets/scripts/App/Game/README.md
Normal 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)
|
||||
11
client/assets/scripts/App/Game/README.md.meta
Normal file
11
client/assets/scripts/App/Game/README.md.meta
Normal file
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"ver": "1.0.1",
|
||||
"importer": "text",
|
||||
"imported": true,
|
||||
"uuid": "fe661d46-0792-4a28-9fcc-11ef37219910",
|
||||
"files": [
|
||||
".json"
|
||||
],
|
||||
"subMetas": {},
|
||||
"userData": {}
|
||||
}
|
||||
156
client/assets/scripts/App/Game/RemotePlayer.ts
Normal file
156
client/assets/scripts/App/Game/RemotePlayer.ts
Normal 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);
|
||||
}
|
||||
}
|
||||
9
client/assets/scripts/App/Game/RemotePlayer.ts.meta
Normal file
9
client/assets/scripts/App/Game/RemotePlayer.ts.meta
Normal file
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"ver": "4.0.24",
|
||||
"importer": "typescript",
|
||||
"imported": true,
|
||||
"uuid": "48c1adc3-a2a6-4f7f-97b2-4df92ee822ee",
|
||||
"files": [],
|
||||
"subMetas": {},
|
||||
"userData": {}
|
||||
}
|
||||
45
client/assets/scripts/App/Game/UIGame.ts
Normal file
45
client/assets/scripts/App/Game/UIGame.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
9
client/assets/scripts/App/Game/UIGame.ts.meta
Normal file
9
client/assets/scripts/App/Game/UIGame.ts.meta
Normal file
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"ver": "4.0.24",
|
||||
"importer": "typescript",
|
||||
"imported": true,
|
||||
"uuid": "1fa0f67a-24a5-4acc-a866-d5c288f16fc7",
|
||||
"files": [],
|
||||
"subMetas": {},
|
||||
"userData": {}
|
||||
}
|
||||
228
client/assets/scripts/App/Game/World.ts
Normal file
228
client/assets/scripts/App/Game/World.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
9
client/assets/scripts/App/Game/World.ts.meta
Normal file
9
client/assets/scripts/App/Game/World.ts.meta
Normal file
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"ver": "4.0.24",
|
||||
"importer": "typescript",
|
||||
"imported": true,
|
||||
"uuid": "80116901-37bd-4c32-85b3-da5aefa12b10",
|
||||
"files": [],
|
||||
"subMetas": {},
|
||||
"userData": {}
|
||||
}
|
||||
9
client/assets/scripts/App/Login.meta
Normal file
9
client/assets/scripts/App/Login.meta
Normal file
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"ver": "1.2.0",
|
||||
"importer": "directory",
|
||||
"imported": true,
|
||||
"uuid": "6cbaee3d-3077-4a01-96f8-bf0ffd8225b0",
|
||||
"files": [],
|
||||
"subMetas": {},
|
||||
"userData": {}
|
||||
}
|
||||
390
client/assets/scripts/App/Login/README.md
Normal file
390
client/assets/scripts/App/Login/README.md
Normal 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) - 应用状态机
|
||||
11
client/assets/scripts/App/Login/README.md.meta
Normal file
11
client/assets/scripts/App/Login/README.md.meta
Normal file
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"ver": "1.0.1",
|
||||
"importer": "text",
|
||||
"imported": true,
|
||||
"uuid": "4319cc73-c0a0-4a35-bafc-60b997c63e49",
|
||||
"files": [
|
||||
".json"
|
||||
],
|
||||
"subMetas": {},
|
||||
"userData": {}
|
||||
}
|
||||
119
client/assets/scripts/App/Login/UILogin.ts
Normal file
119
client/assets/scripts/App/Login/UILogin.ts
Normal 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] 登录界面清理');
|
||||
}
|
||||
}
|
||||
9
client/assets/scripts/App/Login/UILogin.ts.meta
Normal file
9
client/assets/scripts/App/Login/UILogin.ts.meta
Normal file
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"ver": "4.0.24",
|
||||
"importer": "typescript",
|
||||
"imported": true,
|
||||
"uuid": "e0b2fb88-6d47-45ff-97cf-f6f4ad861e75",
|
||||
"files": [],
|
||||
"subMetas": {},
|
||||
"userData": {}
|
||||
}
|
||||
9
client/assets/scripts/Boot.meta
Normal file
9
client/assets/scripts/Boot.meta
Normal file
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"ver": "1.2.0",
|
||||
"importer": "directory",
|
||||
"imported": true,
|
||||
"uuid": "be7fbdee-0511-4b30-bcde-9b890c6121ea",
|
||||
"files": [],
|
||||
"subMetas": {},
|
||||
"userData": {}
|
||||
}
|
||||
81
client/assets/scripts/Boot/Boot.ts
Normal file
81
client/assets/scripts/Boot/Boot.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
9
client/assets/scripts/Boot/Boot.ts.meta
Normal file
9
client/assets/scripts/Boot/Boot.ts.meta
Normal file
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"ver": "4.0.24",
|
||||
"importer": "typescript",
|
||||
"imported": true,
|
||||
"uuid": "324af443-86c7-4983-b726-c1ca5c603593",
|
||||
"files": [],
|
||||
"subMetas": {},
|
||||
"userData": {}
|
||||
}
|
||||
9
client/assets/scripts/CC.meta
Normal file
9
client/assets/scripts/CC.meta
Normal file
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"ver": "1.2.0",
|
||||
"importer": "directory",
|
||||
"imported": true,
|
||||
"uuid": "4602b30b-05bf-465d-8537-b3afbbc83fb0",
|
||||
"files": [],
|
||||
"subMetas": {},
|
||||
"userData": {}
|
||||
}
|
||||
11
client/assets/scripts/Framework/FSM/README.md.meta
Normal file
11
client/assets/scripts/Framework/FSM/README.md.meta
Normal file
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"ver": "1.0.1",
|
||||
"importer": "text",
|
||||
"imported": true,
|
||||
"uuid": "0f4b90cd-984e-421f-ad07-97ba164f3b8d",
|
||||
"files": [
|
||||
".json"
|
||||
],
|
||||
"subMetas": {},
|
||||
"userData": {}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"ver": "4.0.24",
|
||||
"importer": "typescript",
|
||||
"imported": true,
|
||||
"uuid": "b5331a2b-20fa-4653-8013-deb54bad8d2e",
|
||||
"files": [],
|
||||
"subMetas": {},
|
||||
"userData": {}
|
||||
}
|
||||
11
client/assets/scripts/Framework/Net/README.md.meta
Normal file
11
client/assets/scripts/Framework/Net/README.md.meta
Normal file
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"ver": "1.0.1",
|
||||
"importer": "text",
|
||||
"imported": true,
|
||||
"uuid": "6eb67b95-26c3-410a-a9ee-441d4fa23371",
|
||||
"files": [
|
||||
".json"
|
||||
],
|
||||
"subMetas": {},
|
||||
"userData": {}
|
||||
}
|
||||
281
client/assets/scripts/Framework/ResMgr/BundleProxy.ts
Normal file
281
client/assets/scripts/Framework/ResMgr/BundleProxy.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"ver": "4.0.24",
|
||||
"importer": "typescript",
|
||||
"imported": true,
|
||||
"uuid": "728bdb32-e73a-4fae-b942-b66412e95c6a",
|
||||
"files": [],
|
||||
"subMetas": {},
|
||||
"userData": {}
|
||||
}
|
||||
11
client/assets/scripts/Framework/ResMgr/README.md.meta
Normal file
11
client/assets/scripts/Framework/ResMgr/README.md.meta
Normal file
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"ver": "1.0.1",
|
||||
"importer": "text",
|
||||
"imported": true,
|
||||
"uuid": "3ab80465-bd0c-4023-b253-b019b2c686df",
|
||||
"files": [
|
||||
".json"
|
||||
],
|
||||
"subMetas": {},
|
||||
"userData": {}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
const bundleProxy = this.getBundleProxy(bundleName);
|
||||
return bundleProxy.loadDir(dir, type, onProgress);
|
||||
}
|
||||
|
||||
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[]);
|
||||
});
|
||||
});
|
||||
/**
|
||||
* 预加载目录
|
||||
* @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}`);
|
||||
}
|
||||
}
|
||||
|
||||
171
client/assets/scripts/Framework/ResMgr/ResProxy.ts
Normal file
171
client/assets/scripts/Framework/ResMgr/ResProxy.ts
Normal 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}`;
|
||||
}
|
||||
}
|
||||
9
client/assets/scripts/Framework/ResMgr/ResProxy.ts.meta
Normal file
9
client/assets/scripts/Framework/ResMgr/ResProxy.ts.meta
Normal file
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"ver": "4.0.24",
|
||||
"importer": "typescript",
|
||||
"imported": true,
|
||||
"uuid": "e33b5325-cbc7-4082-9e05-093bb8478e2d",
|
||||
"files": [],
|
||||
"subMetas": {},
|
||||
"userData": {}
|
||||
}
|
||||
@@ -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,利用缓存避免重复加载
|
||||
|
||||
11
client/assets/scripts/Framework/UI/README.md.meta
Normal file
11
client/assets/scripts/Framework/UI/README.md.meta
Normal file
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"ver": "1.0.1",
|
||||
"importer": "text",
|
||||
"imported": true,
|
||||
"uuid": "fbfe85a1-0fe1-4ac7-b004-36c370c6a4eb",
|
||||
"files": [
|
||||
".json"
|
||||
],
|
||||
"subMetas": {},
|
||||
"userData": {}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
);
|
||||
|
||||
|
||||
111
client/preview-template/dist/assets/index.02b86726.js
vendored
Normal file
111
client/preview-template/dist/assets/index.02b86726.js
vendored
Normal file
File diff suppressed because one or more lines are too long
1
client/preview-template/dist/assets/index.1d01bced.css
vendored
Normal file
1
client/preview-template/dist/assets/index.1d01bced.css
vendored
Normal file
File diff suppressed because one or more lines are too long
3
client/preview-template/dist/index.html
vendored
Normal file
3
client/preview-template/dist/index.html
vendored
Normal 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>
|
||||
47
client/preview-template/index.ejs
Normal file
47
client/preview-template/index.ejs
Normal 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>
|
||||
|
||||
1
client/preview-template/version.json
Normal file
1
client/preview-template/version.json
Normal file
@@ -0,0 +1 @@
|
||||
{"name":"ccc-devtools","version":"2022/7/17","author":"Next","repo":"https://github.com/potato47/ccc-devtools.git"}
|
||||
Reference in New Issue
Block a user