Initial Cocos Creator port of main-site Unity WebGL game.
Includes core gameplay, 600 exported levels, visual assets, web bridge, and bootstrap scene. Co-authored-by: Cursor <cursoragent@cursor.com>
24
.gitignore
vendored
Normal file
@@ -0,0 +1,24 @@
|
||||
|
||||
#///////////////////////////
|
||||
# Cocos Creator 3D Project
|
||||
#///////////////////////////
|
||||
library/
|
||||
temp/
|
||||
local/
|
||||
build/
|
||||
profiles/
|
||||
native
|
||||
#//////////////////////////
|
||||
# NPM
|
||||
#//////////////////////////
|
||||
node_modules/
|
||||
|
||||
#//////////////////////////
|
||||
# VSCode
|
||||
#//////////////////////////
|
||||
.vscode/
|
||||
|
||||
#//////////////////////////
|
||||
# WebStorm
|
||||
#//////////////////////////
|
||||
.idea/
|
||||
18
AGENTS.md
Normal file
@@ -0,0 +1,18 @@
|
||||
# Cocos 主站 tfrh001 — Agent 指南
|
||||
|
||||
- 引擎:**Cocos Creator 3.8.8**
|
||||
- 源 Unity 项目:`/Users/liuyufei/tfrh/主站文件/主站`
|
||||
- 设计对齐 Unity `Platformer.*` 命名与 JS API
|
||||
|
||||
## 开发约束
|
||||
|
||||
1. 关卡只改 `assets/scripts/level/LevelRegistry.ts` 或新增 JSON,勿硬编码在 GameManager。
|
||||
2. 保持 `SendMessage` 对象名:GameController、Player、Vehicle、UIMain。
|
||||
3. 移动规则只改 `assets/scripts/core/Define.ts`。
|
||||
4. 对外回调用 `JsBridge.call(name, jsonString)`。
|
||||
|
||||
## 关键文件
|
||||
|
||||
- `AppBootstrap.ts` — 场景自动搭建
|
||||
- `GameController.ts` — JS SendMessage 入口
|
||||
- `gameplay/Movement.ts` — 移动队列与规则
|
||||
75
README.md
Normal file
@@ -0,0 +1,75 @@
|
||||
# tfrh001 — 主站网格编程游戏(Cocos Creator 版)
|
||||
|
||||
Unity「主站」移植工程,**Cocos Creator 3.8.8** + **Web**,API 与 `unityInstance.SendMessage` 兼容。
|
||||
|
||||
## 当前进度
|
||||
|
||||
| 项 | 状态 |
|
||||
|----|------|
|
||||
| 核心玩法 / JS 桥 | ✅ |
|
||||
| Unity Levels600(600 关) | ✅ 已导出 |
|
||||
| 丝路主题 Sprite | ✅ player/ship/coin/tile |
|
||||
| 主站 Web 对接模板 | ✅ `web-template/` |
|
||||
|
||||
## 快速开始
|
||||
|
||||
1. Cocos Creator 3.8.8 打开本目录
|
||||
2. 新建场景 → 空节点挂 **AppBootstrap** → 存 `assets/scenes/main.scene` → 设为启动场景
|
||||
3. 播放预览
|
||||
|
||||
## 关卡
|
||||
|
||||
- **600 关**:`assets/scripts/level/levels-600.generated.ts`(由 `tools/export_unity_levels.py` 生成)
|
||||
- 注册入口:`assets/scripts/level/LevelRegistry.ts`
|
||||
- 重新导出:
|
||||
|
||||
```bash
|
||||
python3 tools/export_unity_levels.py \
|
||||
--input "/path/to/Unity/Assets/Scripts/Core/Levels600.cs" \
|
||||
--output assets/scripts/level/levels-600.generated.ts
|
||||
|
||||
# 或
|
||||
bash tools/export_all.sh "/path/to/Unity项目"
|
||||
```
|
||||
|
||||
## 贴图资源
|
||||
|
||||
已复制至 `assets/resources/textures/`(需在编辑器中刷新资源):
|
||||
|
||||
- `silu/player_F.png`, `player_B.png`, `ship_F.png`, `ship_B.png`, `Baseblock.png`
|
||||
- `ui/coin.png`, `ui/bg.png`
|
||||
|
||||
加载逻辑:`assets/scripts/visual/VisualAssets.ts`
|
||||
|
||||
## 主站 Web 联调
|
||||
|
||||
1. **构建发布** → Web Desktop → 得到 `build/web-desktop/`
|
||||
2. 阅读 [`web-template/主站对接说明.md`](web-template/主站对接说明.md)
|
||||
3. 使用 [`web-template/main-site.html`](web-template/main-site.html) + [`cocos-bridge.js`](web-template/cocos-bridge.js) 联调
|
||||
|
||||
```javascript
|
||||
// 与 Unity 完全相同
|
||||
unityInstance.SendMessage("GameController", "SwitchLevel", 21);
|
||||
unityInstance.SendMessage("Player", "CallMove", 2);
|
||||
unityInstance.SendMessage("Player", "CallPlayerInfo");
|
||||
```
|
||||
|
||||
## 目录
|
||||
|
||||
```
|
||||
assets/scripts/ 游戏逻辑
|
||||
assets/resources/ 贴图(Sprite)
|
||||
assets/scripts/level/levels-600.generated.ts
|
||||
web-template/ 主站对接
|
||||
tools/ 关卡导出脚本
|
||||
```
|
||||
|
||||
## 源项目
|
||||
|
||||
`/Users/liuyufei/tfrh/主站文件/主站`
|
||||
|
||||
## 后续
|
||||
|
||||
- 运行 `tools/export_all.sh` 导出 Levels1000、Levels10000 等
|
||||
- 将 Unity 动画改为 Cocos Animation / Spine
|
||||
- 用 Tiled 导出真实地块填充 `LevelConfig.ground`
|
||||
14
assets/resources.meta
Normal file
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"ver": "1.2.0",
|
||||
"importer": "directory",
|
||||
"imported": true,
|
||||
"uuid": "e4dc033d-0f14-4dfc-8475-f3f23d26d6dd",
|
||||
"files": [],
|
||||
"subMetas": {},
|
||||
"userData": {
|
||||
"isBundle": true,
|
||||
"bundleConfigID": "default",
|
||||
"bundleName": "resources",
|
||||
"priority": 8
|
||||
}
|
||||
}
|
||||
9
assets/resources/textures.meta
Normal file
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"ver": "1.2.0",
|
||||
"importer": "directory",
|
||||
"imported": true,
|
||||
"uuid": "d81d86b4-23de-4bc2-a3a5-82add6f6e534",
|
||||
"files": [],
|
||||
"subMetas": {},
|
||||
"userData": {}
|
||||
}
|
||||
9
assets/resources/textures/silu.meta
Normal file
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"ver": "1.2.0",
|
||||
"importer": "directory",
|
||||
"imported": true,
|
||||
"uuid": "cd9ff0f5-a276-4990-9d03-407d44cf21e9",
|
||||
"files": [],
|
||||
"subMetas": {},
|
||||
"userData": {}
|
||||
}
|
||||
BIN
assets/resources/textures/silu/Baseblock.png
Normal file
|
After Width: | Height: | Size: 7.7 KiB |
42
assets/resources/textures/silu/Baseblock.png.meta
Normal file
@@ -0,0 +1,42 @@
|
||||
{
|
||||
"ver": "1.0.27",
|
||||
"importer": "image",
|
||||
"imported": true,
|
||||
"uuid": "5625da25-9915-416f-be60-c6decb355672",
|
||||
"files": [
|
||||
".json",
|
||||
".png"
|
||||
],
|
||||
"subMetas": {
|
||||
"6c48a": {
|
||||
"importer": "texture",
|
||||
"uuid": "5625da25-9915-416f-be60-c6decb355672@6c48a",
|
||||
"displayName": "Baseblock",
|
||||
"id": "6c48a",
|
||||
"name": "texture",
|
||||
"userData": {
|
||||
"wrapModeS": "repeat",
|
||||
"wrapModeT": "repeat",
|
||||
"minfilter": "linear",
|
||||
"magfilter": "linear",
|
||||
"mipfilter": "none",
|
||||
"anisotropy": 0,
|
||||
"isUuid": true,
|
||||
"imageUuidOrDatabaseUri": "5625da25-9915-416f-be60-c6decb355672",
|
||||
"visible": false
|
||||
},
|
||||
"ver": "1.0.22",
|
||||
"imported": true,
|
||||
"files": [
|
||||
".json"
|
||||
],
|
||||
"subMetas": {}
|
||||
}
|
||||
},
|
||||
"userData": {
|
||||
"type": "texture",
|
||||
"fixAlphaTransparencyArtifacts": false,
|
||||
"hasAlpha": true,
|
||||
"redirect": "5625da25-9915-416f-be60-c6decb355672@6c48a"
|
||||
}
|
||||
}
|
||||
BIN
assets/resources/textures/silu/player_B.png
Normal file
|
After Width: | Height: | Size: 14 KiB |
42
assets/resources/textures/silu/player_B.png.meta
Normal file
@@ -0,0 +1,42 @@
|
||||
{
|
||||
"ver": "1.0.27",
|
||||
"importer": "image",
|
||||
"imported": true,
|
||||
"uuid": "fa305708-508c-4b5d-8ddc-f834daf89cfd",
|
||||
"files": [
|
||||
".json",
|
||||
".png"
|
||||
],
|
||||
"subMetas": {
|
||||
"6c48a": {
|
||||
"importer": "texture",
|
||||
"uuid": "fa305708-508c-4b5d-8ddc-f834daf89cfd@6c48a",
|
||||
"displayName": "player_B",
|
||||
"id": "6c48a",
|
||||
"name": "texture",
|
||||
"userData": {
|
||||
"wrapModeS": "repeat",
|
||||
"wrapModeT": "repeat",
|
||||
"minfilter": "linear",
|
||||
"magfilter": "linear",
|
||||
"mipfilter": "none",
|
||||
"anisotropy": 0,
|
||||
"isUuid": true,
|
||||
"imageUuidOrDatabaseUri": "fa305708-508c-4b5d-8ddc-f834daf89cfd",
|
||||
"visible": false
|
||||
},
|
||||
"ver": "1.0.22",
|
||||
"imported": true,
|
||||
"files": [
|
||||
".json"
|
||||
],
|
||||
"subMetas": {}
|
||||
}
|
||||
},
|
||||
"userData": {
|
||||
"type": "texture",
|
||||
"fixAlphaTransparencyArtifacts": false,
|
||||
"hasAlpha": true,
|
||||
"redirect": "fa305708-508c-4b5d-8ddc-f834daf89cfd@6c48a"
|
||||
}
|
||||
}
|
||||
BIN
assets/resources/textures/silu/player_F.png
Normal file
|
After Width: | Height: | Size: 14 KiB |
42
assets/resources/textures/silu/player_F.png.meta
Normal file
@@ -0,0 +1,42 @@
|
||||
{
|
||||
"ver": "1.0.27",
|
||||
"importer": "image",
|
||||
"imported": true,
|
||||
"uuid": "369d2601-41a6-41a4-8804-c59fb1ab1eef",
|
||||
"files": [
|
||||
".json",
|
||||
".png"
|
||||
],
|
||||
"subMetas": {
|
||||
"6c48a": {
|
||||
"importer": "texture",
|
||||
"uuid": "369d2601-41a6-41a4-8804-c59fb1ab1eef@6c48a",
|
||||
"displayName": "player_F",
|
||||
"id": "6c48a",
|
||||
"name": "texture",
|
||||
"userData": {
|
||||
"wrapModeS": "repeat",
|
||||
"wrapModeT": "repeat",
|
||||
"minfilter": "linear",
|
||||
"magfilter": "linear",
|
||||
"mipfilter": "none",
|
||||
"anisotropy": 0,
|
||||
"isUuid": true,
|
||||
"imageUuidOrDatabaseUri": "369d2601-41a6-41a4-8804-c59fb1ab1eef",
|
||||
"visible": false
|
||||
},
|
||||
"ver": "1.0.22",
|
||||
"imported": true,
|
||||
"files": [
|
||||
".json"
|
||||
],
|
||||
"subMetas": {}
|
||||
}
|
||||
},
|
||||
"userData": {
|
||||
"type": "texture",
|
||||
"fixAlphaTransparencyArtifacts": false,
|
||||
"hasAlpha": true,
|
||||
"redirect": "369d2601-41a6-41a4-8804-c59fb1ab1eef@6c48a"
|
||||
}
|
||||
}
|
||||
BIN
assets/resources/textures/silu/ship_B.png
Normal file
|
After Width: | Height: | Size: 16 KiB |
42
assets/resources/textures/silu/ship_B.png.meta
Normal file
@@ -0,0 +1,42 @@
|
||||
{
|
||||
"ver": "1.0.27",
|
||||
"importer": "image",
|
||||
"imported": true,
|
||||
"uuid": "6d61e1e0-435a-490e-af33-addbe85ee32d",
|
||||
"files": [
|
||||
".json",
|
||||
".png"
|
||||
],
|
||||
"subMetas": {
|
||||
"6c48a": {
|
||||
"importer": "texture",
|
||||
"uuid": "6d61e1e0-435a-490e-af33-addbe85ee32d@6c48a",
|
||||
"displayName": "ship_B",
|
||||
"id": "6c48a",
|
||||
"name": "texture",
|
||||
"userData": {
|
||||
"wrapModeS": "repeat",
|
||||
"wrapModeT": "repeat",
|
||||
"minfilter": "linear",
|
||||
"magfilter": "linear",
|
||||
"mipfilter": "none",
|
||||
"anisotropy": 0,
|
||||
"isUuid": true,
|
||||
"imageUuidOrDatabaseUri": "6d61e1e0-435a-490e-af33-addbe85ee32d",
|
||||
"visible": false
|
||||
},
|
||||
"ver": "1.0.22",
|
||||
"imported": true,
|
||||
"files": [
|
||||
".json"
|
||||
],
|
||||
"subMetas": {}
|
||||
}
|
||||
},
|
||||
"userData": {
|
||||
"type": "texture",
|
||||
"fixAlphaTransparencyArtifacts": false,
|
||||
"hasAlpha": true,
|
||||
"redirect": "6d61e1e0-435a-490e-af33-addbe85ee32d@6c48a"
|
||||
}
|
||||
}
|
||||
BIN
assets/resources/textures/silu/ship_F.png
Normal file
|
After Width: | Height: | Size: 15 KiB |
42
assets/resources/textures/silu/ship_F.png.meta
Normal file
@@ -0,0 +1,42 @@
|
||||
{
|
||||
"ver": "1.0.27",
|
||||
"importer": "image",
|
||||
"imported": true,
|
||||
"uuid": "6d5f9230-ee4e-46c2-aaef-12077b6936fc",
|
||||
"files": [
|
||||
".json",
|
||||
".png"
|
||||
],
|
||||
"subMetas": {
|
||||
"6c48a": {
|
||||
"importer": "texture",
|
||||
"uuid": "6d5f9230-ee4e-46c2-aaef-12077b6936fc@6c48a",
|
||||
"displayName": "ship_F",
|
||||
"id": "6c48a",
|
||||
"name": "texture",
|
||||
"userData": {
|
||||
"wrapModeS": "repeat",
|
||||
"wrapModeT": "repeat",
|
||||
"minfilter": "linear",
|
||||
"magfilter": "linear",
|
||||
"mipfilter": "none",
|
||||
"anisotropy": 0,
|
||||
"isUuid": true,
|
||||
"imageUuidOrDatabaseUri": "6d5f9230-ee4e-46c2-aaef-12077b6936fc",
|
||||
"visible": false
|
||||
},
|
||||
"ver": "1.0.22",
|
||||
"imported": true,
|
||||
"files": [
|
||||
".json"
|
||||
],
|
||||
"subMetas": {}
|
||||
}
|
||||
},
|
||||
"userData": {
|
||||
"type": "texture",
|
||||
"fixAlphaTransparencyArtifacts": false,
|
||||
"hasAlpha": true,
|
||||
"redirect": "6d5f9230-ee4e-46c2-aaef-12077b6936fc@6c48a"
|
||||
}
|
||||
}
|
||||
9
assets/resources/textures/ui.meta
Normal file
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"ver": "1.2.0",
|
||||
"importer": "directory",
|
||||
"imported": true,
|
||||
"uuid": "9310a763-9c47-4200-8140-97ff97d09e11",
|
||||
"files": [],
|
||||
"subMetas": {},
|
||||
"userData": {}
|
||||
}
|
||||
BIN
assets/resources/textures/ui/bg.png
Normal file
|
After Width: | Height: | Size: 146 KiB |
42
assets/resources/textures/ui/bg.png.meta
Normal file
@@ -0,0 +1,42 @@
|
||||
{
|
||||
"ver": "1.0.27",
|
||||
"importer": "image",
|
||||
"imported": true,
|
||||
"uuid": "2b1adaab-2489-4ff0-8125-665717c6275a",
|
||||
"files": [
|
||||
".json",
|
||||
".png"
|
||||
],
|
||||
"subMetas": {
|
||||
"6c48a": {
|
||||
"importer": "texture",
|
||||
"uuid": "2b1adaab-2489-4ff0-8125-665717c6275a@6c48a",
|
||||
"displayName": "bg",
|
||||
"id": "6c48a",
|
||||
"name": "texture",
|
||||
"userData": {
|
||||
"wrapModeS": "repeat",
|
||||
"wrapModeT": "repeat",
|
||||
"minfilter": "linear",
|
||||
"magfilter": "linear",
|
||||
"mipfilter": "none",
|
||||
"anisotropy": 0,
|
||||
"isUuid": true,
|
||||
"imageUuidOrDatabaseUri": "2b1adaab-2489-4ff0-8125-665717c6275a",
|
||||
"visible": false
|
||||
},
|
||||
"ver": "1.0.22",
|
||||
"imported": true,
|
||||
"files": [
|
||||
".json"
|
||||
],
|
||||
"subMetas": {}
|
||||
}
|
||||
},
|
||||
"userData": {
|
||||
"type": "texture",
|
||||
"fixAlphaTransparencyArtifacts": false,
|
||||
"hasAlpha": true,
|
||||
"redirect": "2b1adaab-2489-4ff0-8125-665717c6275a@6c48a"
|
||||
}
|
||||
}
|
||||
BIN
assets/resources/textures/ui/coin.png
Normal file
|
After Width: | Height: | Size: 20 KiB |
42
assets/resources/textures/ui/coin.png.meta
Normal file
@@ -0,0 +1,42 @@
|
||||
{
|
||||
"ver": "1.0.27",
|
||||
"importer": "image",
|
||||
"imported": true,
|
||||
"uuid": "f2192a39-42df-419b-9ef0-06b4a35fb223",
|
||||
"files": [
|
||||
".json",
|
||||
".png"
|
||||
],
|
||||
"subMetas": {
|
||||
"6c48a": {
|
||||
"importer": "texture",
|
||||
"uuid": "f2192a39-42df-419b-9ef0-06b4a35fb223@6c48a",
|
||||
"displayName": "coin",
|
||||
"id": "6c48a",
|
||||
"name": "texture",
|
||||
"userData": {
|
||||
"wrapModeS": "repeat",
|
||||
"wrapModeT": "repeat",
|
||||
"minfilter": "linear",
|
||||
"magfilter": "linear",
|
||||
"mipfilter": "none",
|
||||
"anisotropy": 0,
|
||||
"isUuid": true,
|
||||
"imageUuidOrDatabaseUri": "f2192a39-42df-419b-9ef0-06b4a35fb223",
|
||||
"visible": false
|
||||
},
|
||||
"ver": "1.0.22",
|
||||
"imported": true,
|
||||
"files": [
|
||||
".json"
|
||||
],
|
||||
"subMetas": {}
|
||||
}
|
||||
},
|
||||
"userData": {
|
||||
"type": "texture",
|
||||
"fixAlphaTransparencyArtifacts": false,
|
||||
"hasAlpha": true,
|
||||
"redirect": "f2192a39-42df-419b-9ef0-06b4a35fb223@6c48a"
|
||||
}
|
||||
}
|
||||
9
assets/scenes.meta
Normal file
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"ver": "1.2.0",
|
||||
"importer": "directory",
|
||||
"imported": true,
|
||||
"uuid": "e990322a-3266-4055-842e-e674f66f6fa3",
|
||||
"files": [],
|
||||
"subMetas": {},
|
||||
"userData": {}
|
||||
}
|
||||
494
assets/scenes/main.scene
Normal file
@@ -0,0 +1,494 @@
|
||||
[
|
||||
{
|
||||
"__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
|
||||
}
|
||||
],
|
||||
"_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
|
||||
},
|
||||
"autoReleaseAssets": false,
|
||||
"_globals": {
|
||||
"__id__": 9
|
||||
},
|
||||
"_id": "d071e7d3-2dc4-4815-8cb8-c258c4b7c515"
|
||||
},
|
||||
{
|
||||
"__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": "d381mi5H1Nyq4+wgo0zLCX"
|
||||
},
|
||||
{
|
||||
"__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": "b4ITpe9cBLQ7QkVb+TkXWc"
|
||||
},
|
||||
{
|
||||
"__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": "45wksGP0VK44RZQ6l+OECN"
|
||||
},
|
||||
{
|
||||
"__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": 1822425087,
|
||||
"_targetTexture": null,
|
||||
"_postProcess": null,
|
||||
"_usePostProcess": false,
|
||||
"_cameraType": -1,
|
||||
"_trackingType": 0,
|
||||
"_id": "80VFXpZJ9E8qFokxruq7p4"
|
||||
},
|
||||
{
|
||||
"__type__": "cc.Node",
|
||||
"_name": "AppRoot",
|
||||
"_objFlags": 0,
|
||||
"__editorExtras__": {},
|
||||
"_parent": {
|
||||
"__id__": 1
|
||||
},
|
||||
"_children": [],
|
||||
"_active": true,
|
||||
"_components": [
|
||||
{
|
||||
"__id__": 8
|
||||
}
|
||||
],
|
||||
"_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": "4fu9hupTlJ84UrhzTAt969"
|
||||
},
|
||||
{
|
||||
"__type__": "c0468XFPX1InLN14gtZ5PLf",
|
||||
"_name": "",
|
||||
"_objFlags": 0,
|
||||
"__editorExtras__": {},
|
||||
"node": {
|
||||
"__id__": 7
|
||||
},
|
||||
"_enabled": true,
|
||||
"__prefab": null,
|
||||
"_id": "eeKeOJWZBOzI20RcHfalSe"
|
||||
},
|
||||
{
|
||||
"__type__": "cc.SceneGlobals",
|
||||
"ambient": {
|
||||
"__id__": 10
|
||||
},
|
||||
"shadows": {
|
||||
"__id__": 11
|
||||
},
|
||||
"_skybox": {
|
||||
"__id__": 12
|
||||
},
|
||||
"fog": {
|
||||
"__id__": 13
|
||||
},
|
||||
"octree": {
|
||||
"__id__": 14
|
||||
},
|
||||
"skin": {
|
||||
"__id__": 15
|
||||
},
|
||||
"lightProbeInfo": {
|
||||
"__id__": 16
|
||||
},
|
||||
"postSettings": {
|
||||
"__id__": 17
|
||||
},
|
||||
"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
assets/scenes/main.scene.meta
Normal file
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"ver": "1.1.50",
|
||||
"importer": "scene",
|
||||
"imported": true,
|
||||
"uuid": "d071e7d3-2dc4-4815-8cb8-c258c4b7c515",
|
||||
"files": [
|
||||
".json"
|
||||
],
|
||||
"subMetas": {},
|
||||
"userData": {}
|
||||
}
|
||||
8
assets/scenes/场景搭建说明.txt
Normal file
@@ -0,0 +1,8 @@
|
||||
在 Cocos Creator 3.8.8 中:
|
||||
|
||||
1. 新建场景 → 保存为 main.scene(与本目录同级)
|
||||
2. 创建空节点 AppRoot,添加组件 AppBootstrap(脚本路径 assets/scripts/AppBootstrap.ts)
|
||||
3. 项目设置中将 main 设为启动场景
|
||||
4. 点击播放即可
|
||||
|
||||
无需手动创建 GameController / Player,AppBootstrap 会自动搭建。
|
||||
11
assets/scenes/场景搭建说明.txt.meta
Normal file
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"ver": "1.0.1",
|
||||
"importer": "text",
|
||||
"imported": true,
|
||||
"uuid": "2e31b18a-7901-449d-b1c1-d2a5c09dd773",
|
||||
"files": [
|
||||
".json"
|
||||
],
|
||||
"subMetas": {},
|
||||
"userData": {}
|
||||
}
|
||||
9
assets/scripts.meta
Normal file
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"ver": "1.2.0",
|
||||
"importer": "directory",
|
||||
"imported": true,
|
||||
"uuid": "8254b8f8-57eb-4bc8-ab6d-062a44e75e22",
|
||||
"files": [],
|
||||
"subMetas": {},
|
||||
"userData": {}
|
||||
}
|
||||
117
assets/scripts/AppBootstrap.ts
Normal file
@@ -0,0 +1,117 @@
|
||||
import {
|
||||
_decorator, Component, Node, Canvas, Camera, UITransform, view, Color,
|
||||
director, Label, find,
|
||||
} from 'cc';
|
||||
import { GameController } from './GameController';
|
||||
import { GameManager } from './manager/GameManager';
|
||||
import { VisualAssets } from './visual/VisualAssets';
|
||||
|
||||
const { ccclass, executionOrder } = _decorator;
|
||||
|
||||
/** 每格像素尺寸(UI 坐标) */
|
||||
export const CELL_PIXEL = 56;
|
||||
|
||||
@ccclass('AppBootstrap')
|
||||
@executionOrder(-100)
|
||||
export class AppBootstrap extends Component {
|
||||
async onLoad() {
|
||||
try {
|
||||
await this.bootstrap();
|
||||
} catch (e) {
|
||||
console.error('[AppBootstrap] 初始化失败', e);
|
||||
}
|
||||
}
|
||||
|
||||
private async bootstrap() {
|
||||
console.log('[AppBootstrap] 开始初始化…');
|
||||
await VisualAssets.preload();
|
||||
|
||||
const scene = director.getScene()!;
|
||||
let mainCam = find('Main Camera', scene)?.getComponent(Camera) ?? null;
|
||||
if (!mainCam) {
|
||||
const camNode = new Node('Main Camera');
|
||||
camNode.parent = scene;
|
||||
mainCam = camNode.addComponent(Camera);
|
||||
}
|
||||
this.setupCamera(mainCam);
|
||||
|
||||
const light = find('Main Light', scene);
|
||||
if (light) light.active = false;
|
||||
|
||||
let canvasNode = find('Canvas', scene);
|
||||
if (!canvasNode) {
|
||||
canvasNode = new Node('Canvas');
|
||||
canvasNode.parent = scene;
|
||||
canvasNode.addComponent(Canvas);
|
||||
}
|
||||
const canvas = canvasNode.getComponent(Canvas)!;
|
||||
canvas.cameraComponent = mainCam;
|
||||
let canvasUi = canvasNode.getComponent(UITransform);
|
||||
if (!canvasUi) canvasUi = canvasNode.addComponent(UITransform);
|
||||
const size = view.getVisibleSize();
|
||||
canvasUi.setContentSize(size.width, size.height);
|
||||
|
||||
let gameRoot = canvasNode.getChildByName('GameRoot');
|
||||
if (!gameRoot) {
|
||||
gameRoot = new Node('GameRoot');
|
||||
gameRoot.parent = canvasNode;
|
||||
const grUi = gameRoot.addComponent(UITransform);
|
||||
grUi.setContentSize(size.width, size.height);
|
||||
}
|
||||
|
||||
let gcNode = scene.getChildByName('GameController');
|
||||
if (!gcNode) {
|
||||
gcNode = new Node('GameController');
|
||||
gcNode.parent = scene;
|
||||
gcNode.addComponent(GameManager);
|
||||
gcNode.addComponent(GameController);
|
||||
}
|
||||
|
||||
const gctl = gcNode.getComponent(GameController);
|
||||
const gm = gcNode.getComponent(GameManager)!;
|
||||
if (gctl) {
|
||||
if (gctl.mainLevelEntrance) gm.mainLevelEntrance = gctl.mainLevelEntrance;
|
||||
gm.initialLevelID = gctl.initialLevelID;
|
||||
gm.playerSkin = gctl.playerSkin;
|
||||
}
|
||||
let entrance = gameRoot.getChildByName('MainLevelEntrance');
|
||||
if (!entrance) {
|
||||
entrance = new Node('MainLevelEntrance');
|
||||
entrance.parent = gameRoot;
|
||||
const eUi = entrance.addComponent(UITransform);
|
||||
eUi.setContentSize(size.width, size.height);
|
||||
}
|
||||
gm.mainLevelEntrance = entrance;
|
||||
gm.initialLevelID = 1;
|
||||
|
||||
this.ensureHint(canvasNode);
|
||||
|
||||
await gm.createNewLevel(gm.initialLevelID);
|
||||
console.log('[AppBootstrap] 关卡已加载');
|
||||
}
|
||||
|
||||
private setupCamera(cam: Camera) {
|
||||
const camNode = cam.node;
|
||||
camNode.setPosition(0, 0, 1000);
|
||||
camNode.setRotationFromEuler(0, 0, 0);
|
||||
cam.projection = Camera.ProjectionType.ORTHO;
|
||||
cam.orthoHeight = 360;
|
||||
cam.near = 1;
|
||||
cam.far = 2000;
|
||||
cam.clearFlags = Camera.ClearFlag.SOLID_COLOR;
|
||||
cam.clearColor = new Color(30, 40, 60, 255);
|
||||
}
|
||||
|
||||
private ensureHint(canvasNode: Node) {
|
||||
if (canvasNode.getChildByName('Hint')) return;
|
||||
const hint = new Node('Hint');
|
||||
hint.parent = canvasNode;
|
||||
const ui = hint.addComponent(UITransform);
|
||||
ui.setContentSize(400, 40);
|
||||
hint.setPosition(0, 280, 0);
|
||||
const label = hint.addComponent(Label);
|
||||
label.string = '主站 Cocos · 关卡运行中';
|
||||
label.fontSize = 22;
|
||||
label.color = new Color(200, 220, 255);
|
||||
}
|
||||
}
|
||||
9
assets/scripts/AppBootstrap.ts.meta
Normal file
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"ver": "4.0.24",
|
||||
"importer": "typescript",
|
||||
"imported": true,
|
||||
"uuid": "c04685c5-3d7d-489c-b375-e20b59e4f2df",
|
||||
"files": [],
|
||||
"subMetas": {},
|
||||
"userData": {}
|
||||
}
|
||||
275
assets/scripts/GameController.ts
Normal file
@@ -0,0 +1,275 @@
|
||||
import {
|
||||
_decorator, Component, Node, Enum, CCString, CCInteger, CCBoolean, game,
|
||||
} from 'cc';
|
||||
import { GameManager } from './manager/GameManager';
|
||||
import { Skin } from './core/Define';
|
||||
import { VisualAssets } from './visual/VisualAssets';
|
||||
import { getLevelCount } from './level/LevelRegistry';
|
||||
|
||||
const { ccclass, property, executionOrder } = _decorator;
|
||||
|
||||
/** UI 主题(与 Unity ChangeUIStyle 参数一致) */
|
||||
export enum UIStyleType {
|
||||
default = 0,
|
||||
chinese = 1,
|
||||
redArmy = 2,
|
||||
numMan = 3,
|
||||
snow = 4,
|
||||
sanxing = 5,
|
||||
}
|
||||
const UIStyleNames = ['default', 'chinese', 'redArmy', 'numMan', 'snow', 'sanxing'];
|
||||
|
||||
/** 多人角色 */
|
||||
export enum MultRole {
|
||||
PlayerA1 = 0,
|
||||
PlayerA2 = 1,
|
||||
PlayerA3 = 2,
|
||||
PlayerB1 = 3,
|
||||
PlayerB2 = 4,
|
||||
PlayerB3 = 5,
|
||||
}
|
||||
const MultRoleNames = ['PlayerA1', 'PlayerA2', 'PlayerA3', 'PlayerB1', 'PlayerB2', 'PlayerB3'];
|
||||
|
||||
/**
|
||||
* 对应 Unity 场景 GameController + Inspector 调试面板(TestGame2)
|
||||
* 挂在 GameController 节点,与 GameManager 同级或同节点
|
||||
*/
|
||||
@ccclass('GameController')
|
||||
@executionOrder(-50)
|
||||
export class GameController extends Component {
|
||||
@property({ group: { name: '场景', id: '1' }, type: Node, displayName: 'Main Level Entrance' })
|
||||
mainLevelEntrance: Node | null = null;
|
||||
|
||||
@property({ group: { name: '场景', id: '1' }, type: Node, displayName: 'Cur Level', readonly: true })
|
||||
curLevelNode: Node | null = null;
|
||||
|
||||
@property({ group: { name: '关卡', id: '2' }, type: CCInteger, displayName: 'Initial Level ID' })
|
||||
initialLevelID = 1;
|
||||
|
||||
@property({ group: { name: '关卡', id: '2' }, type: CCInteger, displayName: 'Cur Level ID', readonly: true })
|
||||
curLevelID = 1;
|
||||
|
||||
@property({ group: { name: '关卡', id: '2' }, displayName: '已注册关卡数', readonly: true })
|
||||
registeredLevelCount = 0;
|
||||
|
||||
@property({ group: { name: '角色', id: '3' }, type: Enum(Skin), displayName: 'Player Skin' })
|
||||
playerSkin: Skin = Skin.Silu;
|
||||
|
||||
@property({ group: { name: '多人', id: '4' }, displayName: 'Mult Mode' })
|
||||
multMode = false;
|
||||
|
||||
@property({ group: { name: '多人', id: '4' }, type: Enum(MultRole), displayName: 'Mult Player Role' })
|
||||
multPlayerRoleEnum: MultRole = MultRole.PlayerA1;
|
||||
|
||||
@property({ group: { name: '主题', id: '5' }, type: Enum(UIStyleType), displayName: 'UI Style' })
|
||||
uiStyleEnum: UIStyleType = UIStyleType.default;
|
||||
|
||||
// --- 与 Unity Inspector 输入框一致 ---
|
||||
@property({ group: { name: '调试输入', id: '6' }, displayName: 'inputLevel' })
|
||||
inputLevel = '1';
|
||||
|
||||
@property({ group: { name: '调试输入', id: '6' }, displayName: 'inputStyle' })
|
||||
inputStyle = 'default';
|
||||
|
||||
@property({ group: { name: '调试输入', id: '6' }, multiline: true, displayName: 'coinStr' })
|
||||
coinStr = '["-2,0","-2,2","-1,2"]';
|
||||
|
||||
// --- 播放时勾选即执行(等同 Unity 按钮) ---
|
||||
@property({ group: { name: '调试操作(播放时勾选)', id: '7' }, displayName: '▶ SwitchLevel' })
|
||||
execSwitchLevel = false;
|
||||
|
||||
@property({ group: { name: '调试操作(播放时勾选)', id: '7' }, displayName: '▶ EndInput' })
|
||||
execEndInput = false;
|
||||
|
||||
@property({ group: { name: '调试操作(播放时勾选)', id: '7' }, displayName: '▶ ChangeStyle' })
|
||||
execChangeStyle = false;
|
||||
|
||||
@property({ group: { name: '调试操作(播放时勾选)', id: '7' }, displayName: '▶ StartMultPlay' })
|
||||
execStartMultPlay = false;
|
||||
|
||||
@property({ group: { name: '调试操作(播放时勾选)', id: '7' }, displayName: '▶ Mute' })
|
||||
execMute = false;
|
||||
|
||||
@property({ group: { name: '调试操作(播放时勾选)', id: '7' }, displayName: '▶ Unmute' })
|
||||
execUnmute = false;
|
||||
|
||||
private gm!: GameManager;
|
||||
|
||||
onLoad() {
|
||||
this.gm = this.getComponent(GameManager) || this.node.getComponentInChildren(GameManager)!;
|
||||
if (!this.gm) {
|
||||
this.gm = this.node.addComponent(GameManager);
|
||||
}
|
||||
this.syncToManager();
|
||||
this.registerWebApi();
|
||||
this.registeredLevelCount = getLevelCount();
|
||||
}
|
||||
|
||||
start() {
|
||||
this.refreshReadonly();
|
||||
}
|
||||
|
||||
update() {
|
||||
if (!game.isRunning()) return;
|
||||
this.refreshReadonly();
|
||||
this.pollInspectorActions();
|
||||
}
|
||||
|
||||
private refreshReadonly() {
|
||||
this.curLevelID = this.gm?.curLevelID ?? this.curLevelID;
|
||||
if (this.gm?.mainLevelEntrance) {
|
||||
this.mainLevelEntrance = this.gm.mainLevelEntrance;
|
||||
const lv = this.gm.mainLevelEntrance.children.find((c) => c.name.startsWith('Level_'));
|
||||
this.curLevelNode = lv ?? null;
|
||||
}
|
||||
this.multMode = this.gm?.multMode ?? false;
|
||||
}
|
||||
|
||||
private syncToManager() {
|
||||
if (!this.gm) return;
|
||||
if (this.mainLevelEntrance) this.gm.mainLevelEntrance = this.mainLevelEntrance;
|
||||
this.gm.initialLevelID = this.initialLevelID;
|
||||
this.gm.playerSkin = this.playerSkin;
|
||||
this.gm.multMode = this.multMode;
|
||||
this.gm.multPlayerRole = MultRoleNames[this.multPlayerRoleEnum] ?? 'PlayerA1';
|
||||
this.gm.uiStyle = UIStyleNames[this.uiStyleEnum] ?? 'default';
|
||||
this.gm.curLevelID = this.initialLevelID;
|
||||
}
|
||||
|
||||
private pollInspectorActions() {
|
||||
if (this.execSwitchLevel) {
|
||||
this.execSwitchLevel = false;
|
||||
this.onInspectorSwitchLevel();
|
||||
}
|
||||
if (this.execEndInput) {
|
||||
this.execEndInput = false;
|
||||
this.callSetIsInputEnd(1);
|
||||
}
|
||||
if (this.execChangeStyle) {
|
||||
this.execChangeStyle = false;
|
||||
this.onInspectorChangeStyle();
|
||||
}
|
||||
if (this.execStartMultPlay) {
|
||||
this.execStartMultPlay = false;
|
||||
this.onInspectorStartMultPlay();
|
||||
}
|
||||
if (this.execMute) {
|
||||
this.execMute = false;
|
||||
this.callMute();
|
||||
}
|
||||
if (this.execUnmute) {
|
||||
this.execUnmute = false;
|
||||
this.callUnmute();
|
||||
}
|
||||
}
|
||||
|
||||
// --- Inspector / SendMessage 共用 ---
|
||||
onInspectorSwitchLevel() {
|
||||
const id = parseInt(this.inputLevel, 10);
|
||||
if (Number.isNaN(id)) {
|
||||
console.warn('[GameController] inputLevel 无效:', this.inputLevel);
|
||||
return;
|
||||
}
|
||||
this.syncToManager();
|
||||
this.gm.switchLevel(id);
|
||||
this.curLevelID = id;
|
||||
console.log('[GameController] SwitchLevel', id);
|
||||
}
|
||||
|
||||
onInspectorChangeStyle() {
|
||||
const style = this.inputStyle.trim() || UIStyleNames[this.uiStyleEnum];
|
||||
this.gm.changeUIStyle(style);
|
||||
this.uiStyleEnum = UIStyleNames.indexOf(style) as UIStyleType;
|
||||
if (this.uiStyleEnum < 0) this.uiStyleEnum = UIStyleType.default;
|
||||
console.log('[GameController] ChangeUIStyle', style);
|
||||
}
|
||||
|
||||
onInspectorStartMultPlay() {
|
||||
const role = MultRoleNames[this.multPlayerRoleEnum] ?? 'PlayerA1';
|
||||
this.gm.startMultPlay(role, this.coinStr);
|
||||
this.multMode = true;
|
||||
console.log('[GameController] StartMultPlay', role, this.coinStr);
|
||||
}
|
||||
|
||||
applyPlayerSkinFromInspector() {
|
||||
const player = this.gm.findNodeByName('Player')
|
||||
?? this.gm.findNodeByName(this.gm.multPlayerRole);
|
||||
const pc = player?.getComponent('PlayerController') as { callChangeSkin?: (n: number) => void };
|
||||
pc?.callChangeSkin?.(this.playerSkin);
|
||||
this.gm.playerSkin = this.playerSkin;
|
||||
}
|
||||
|
||||
// --- JS Bridge (SendMessage) ---
|
||||
private registerWebApi() {
|
||||
const api = {
|
||||
SendMessage: (objectName: string, methodName: string, param?: string | number) => {
|
||||
this.sendMessage(objectName, methodName, param);
|
||||
},
|
||||
};
|
||||
if (typeof window !== 'undefined') {
|
||||
(window as unknown as { cocosIns?: typeof api }).cocosIns = api;
|
||||
(window as unknown as { unityInstance?: typeof api }).unityInstance = api;
|
||||
}
|
||||
}
|
||||
|
||||
sendMessage(objectName: string, methodName: string, param?: string | number) {
|
||||
if (objectName === 'GameController') {
|
||||
this.invoke(this, methodName, param);
|
||||
return;
|
||||
}
|
||||
if (objectName === 'UIMain') {
|
||||
const n = this.gm.findNodeByName('UIMain');
|
||||
const c = n?.getComponent('UIMain');
|
||||
if (c) this.invoke(c, methodName, param);
|
||||
return;
|
||||
}
|
||||
const node = this.gm.findNodeByName(objectName);
|
||||
if (!node) {
|
||||
console.warn(`SendMessage: 未找到 ${objectName}`);
|
||||
return;
|
||||
}
|
||||
for (const comp of node.getComponents(Component)) {
|
||||
if (typeof (comp as Record<string, unknown>)[methodName] === 'function') {
|
||||
this.invoke(comp, methodName, param);
|
||||
return;
|
||||
}
|
||||
}
|
||||
console.warn(`SendMessage: ${objectName}.${methodName} 无此方法`);
|
||||
}
|
||||
|
||||
private invoke(target: object, methodName: string, param?: string | number) {
|
||||
const fn = (target as Record<string, unknown>)[methodName];
|
||||
if (typeof fn !== 'function') return;
|
||||
if (param === undefined) (fn as () => void).call(target);
|
||||
else (fn as (p: string | number) => void).call(target, param);
|
||||
}
|
||||
|
||||
switchLevel(levelID: number) {
|
||||
this.inputLevel = String(levelID);
|
||||
this.onInspectorSwitchLevel();
|
||||
}
|
||||
|
||||
callSetIsInputEnd(v: number) {
|
||||
this.gm.callSetIsInputEnd(v);
|
||||
}
|
||||
|
||||
changeUIStyle(style: string) {
|
||||
this.inputStyle = style;
|
||||
this.onInspectorChangeStyle();
|
||||
}
|
||||
|
||||
startMultPlay(role: string, coins: string) {
|
||||
this.coinStr = coins;
|
||||
const idx = MultRoleNames.indexOf(role);
|
||||
if (idx >= 0) this.multPlayerRoleEnum = idx as MultRole;
|
||||
this.onInspectorStartMultPlay();
|
||||
}
|
||||
|
||||
callMute() {
|
||||
this.gm.callMute();
|
||||
}
|
||||
|
||||
callUnmute() {
|
||||
this.gm.callUnmute();
|
||||
}
|
||||
}
|
||||
9
assets/scripts/GameController.ts.meta
Normal file
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"ver": "4.0.24",
|
||||
"importer": "typescript",
|
||||
"imported": true,
|
||||
"uuid": "8940a3d2-2898-4628-a201-13fa8a9c59bb",
|
||||
"files": [],
|
||||
"subMetas": {},
|
||||
"userData": {}
|
||||
}
|
||||
9
assets/scripts/bridge.meta
Normal file
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"ver": "1.2.0",
|
||||
"importer": "directory",
|
||||
"imported": true,
|
||||
"uuid": "ba5d5aa4-760d-4aae-aef0-83f80c1f2cba",
|
||||
"files": [],
|
||||
"subMetas": {},
|
||||
"userData": {}
|
||||
}
|
||||
28
assets/scripts/bridge/JsBridge.ts
Normal file
@@ -0,0 +1,28 @@
|
||||
/**
|
||||
* 对应 Unity Application.ExternalCall
|
||||
* Web 环境下调用 window 上的全局回调
|
||||
*/
|
||||
|
||||
declare global {
|
||||
interface Window {
|
||||
processData?: (json: string) => void;
|
||||
processVehicleData?: (json: string) => void;
|
||||
externalResult?: (json: string) => void;
|
||||
externalLevelInfo?: (json: string) => void;
|
||||
coinsData?: (json: string) => void;
|
||||
[key: string]: unknown;
|
||||
}
|
||||
}
|
||||
|
||||
export class JsBridge {
|
||||
static call(callbackName: string, jsonData: string) {
|
||||
if (typeof window !== 'undefined') {
|
||||
const fn = window[callbackName];
|
||||
if (typeof fn === 'function') {
|
||||
(fn as (json: string) => void)(jsonData);
|
||||
return;
|
||||
}
|
||||
}
|
||||
console.log(`[JsBridge] ${callbackName}`, jsonData);
|
||||
}
|
||||
}
|
||||
9
assets/scripts/bridge/JsBridge.ts.meta
Normal file
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"ver": "4.0.24",
|
||||
"importer": "typescript",
|
||||
"imported": true,
|
||||
"uuid": "d34fa121-5277-49b2-9938-ed0886a31fc4",
|
||||
"files": [],
|
||||
"subMetas": {},
|
||||
"userData": {}
|
||||
}
|
||||
9
assets/scripts/controller.meta
Normal file
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"ver": "1.2.0",
|
||||
"importer": "directory",
|
||||
"imported": true,
|
||||
"uuid": "6fcf8603-44fa-444f-900d-118529aca8dc",
|
||||
"files": [],
|
||||
"subMetas": {},
|
||||
"userData": {}
|
||||
}
|
||||
213
assets/scripts/controller/PlayerController.ts
Normal file
@@ -0,0 +1,213 @@
|
||||
import { _decorator, Vec3 } from 'cc';
|
||||
import { Direction, GameState, GridType, Skin } from '../core/Define';
|
||||
import { EventManager, EventType } from '../core/EventManager';
|
||||
import { JsBridge } from '../bridge/JsBridge';
|
||||
import { GameManager } from '../manager/GameManager';
|
||||
import { Movement } from '../gameplay/Movement';
|
||||
import { VehicleController } from './VehicleController';
|
||||
import { VisualAssets } from '../visual/VisualAssets';
|
||||
|
||||
const { ccclass } = _decorator;
|
||||
|
||||
export interface ExternalData {
|
||||
position: { x: number; y: number; z: number };
|
||||
gridType: number;
|
||||
direction: string;
|
||||
}
|
||||
|
||||
export interface ExternalDataList {
|
||||
direction: number;
|
||||
externalDatas: ExternalData[];
|
||||
}
|
||||
|
||||
export interface ExternalResult {
|
||||
isWin: boolean;
|
||||
stepNum: number;
|
||||
direction: number;
|
||||
isInputEnd: boolean;
|
||||
}
|
||||
|
||||
@ccclass('PlayerController')
|
||||
export class PlayerController extends Movement {
|
||||
coins = 0;
|
||||
private vehicle: VehicleController | null = null;
|
||||
private sendFinally = false;
|
||||
|
||||
onLoad() {
|
||||
this.moverRole = 'player';
|
||||
EventManager.register(EventType.LevelInit, this.onLevelInit);
|
||||
EventManager.register(EventType.InputEnd, this.onInputEnd);
|
||||
this.coins = 0;
|
||||
}
|
||||
|
||||
onDestroy() {
|
||||
EventManager.remove(EventType.LevelInit, this.onLevelInit);
|
||||
EventManager.remove(EventType.InputEnd, this.onInputEnd);
|
||||
}
|
||||
|
||||
start() {
|
||||
super.start();
|
||||
VisualAssets.applyPlayerSprite(this.node, this.direction);
|
||||
if (this.node.name === 'Player' && GameManager.instance) {
|
||||
this.callChangeSkin(GameManager.instance.playerSkin);
|
||||
}
|
||||
}
|
||||
|
||||
override setDirection(dir: Direction) {
|
||||
super.setDirection(dir);
|
||||
VisualAssets.applyPlayerSprite(this.node, dir);
|
||||
}
|
||||
|
||||
private onLevelInit = () => {
|
||||
this.checkRide();
|
||||
};
|
||||
|
||||
private onInputEnd = () => {
|
||||
this.externalCallResult(false);
|
||||
};
|
||||
|
||||
protected onMoveNextSet(isJump: boolean) {
|
||||
if (isJump && this.targetGridType === GridType.Jump) {
|
||||
const p = this.node.worldPosition.clone();
|
||||
p.y += 0.15;
|
||||
this.targetPosition.set(p);
|
||||
}
|
||||
}
|
||||
|
||||
onMoving() {
|
||||
if (this.targetGridType === GridType.None && this.vehicle && !Movement.callEach) {
|
||||
this.vehicle.setPosition(this.node.worldPosition);
|
||||
}
|
||||
}
|
||||
|
||||
protected onMoveFail(isJump: boolean) {
|
||||
this.externalCallResult(false);
|
||||
const gm = GameManager.instance!;
|
||||
if (gm.multMode) {
|
||||
const other = gm.findNodeByName(this.node.name === 'Player' ? 'Enemy' : 'Player');
|
||||
other?.getComponent(PlayerController)?.externalCallResult(true);
|
||||
}
|
||||
console.log(`${this.node.name} 无法移动`, isJump);
|
||||
}
|
||||
|
||||
protected onMoveToTarget() {
|
||||
if (this.curGrid === GridType.Ride) {
|
||||
const obj = GameManager.instance!.getGameObject(this.node.worldPosition);
|
||||
if (obj) {
|
||||
this.vehicle = obj.getComponent(VehicleController);
|
||||
this.vehicle?.setPlayer(this);
|
||||
if (this.vehicle) this.vehicle.setDirection(this.direction);
|
||||
}
|
||||
} else if (this.curGrid !== GridType.None && this.vehicle) {
|
||||
this.vehicle.setPlayer(null);
|
||||
this.vehicle = null;
|
||||
}
|
||||
if (this.vehicle) {
|
||||
this.vehicle.setPosition(this.node.worldPosition);
|
||||
GameManager.instance!.removeObj(this.lastPosition);
|
||||
GameManager.instance!.addObj(this.node.worldPosition, GridType.Ride, this.vehicle.node);
|
||||
}
|
||||
this.externalCall();
|
||||
}
|
||||
|
||||
callChangeSkin(n: number) {
|
||||
if (n < 0 || n > Skin.sanxing) return;
|
||||
if (GameManager.instance) GameManager.instance.playerSkin = n as Skin;
|
||||
}
|
||||
|
||||
callPlayerInfo() {
|
||||
this.externalCall();
|
||||
}
|
||||
|
||||
setName(name: string) {
|
||||
this.vehicle?.setName(name);
|
||||
}
|
||||
|
||||
getCoinsNum() {
|
||||
JsBridge.call('coinsData', JSON.stringify({ coinsNum: this.coins, gameObjectName: this.node.name }));
|
||||
}
|
||||
|
||||
addCoins() {
|
||||
this.coins++;
|
||||
}
|
||||
|
||||
setPosition(pos: Vec3) {
|
||||
this.node.setPosition(pos);
|
||||
}
|
||||
|
||||
/** 供 VehicleController 同步 */
|
||||
syncFromVehicle(targetGridType: GridType, isJump: boolean) {
|
||||
this.targetGridType = targetGridType;
|
||||
this.onMoveNextSet(isJump);
|
||||
}
|
||||
|
||||
syncMoveToTargetFromVehicle() {
|
||||
this.onMoveToTarget();
|
||||
}
|
||||
|
||||
externalCall() {
|
||||
const gm = GameManager.instance!;
|
||||
const list: ExternalDataList = { direction: this.direction, externalDatas: [] };
|
||||
const self = gm.worldToCell(this.node.worldPosition);
|
||||
list.externalDatas.push({
|
||||
position: { x: self.x, y: self.y, z: 0 },
|
||||
gridType: this.curGrid,
|
||||
direction: 'self',
|
||||
});
|
||||
for (let d = Direction.North; d <= Direction.West; d++) {
|
||||
const wp = gm.nextGridPosition(this.node.worldPosition, d);
|
||||
const cell = gm.worldToCell(wp);
|
||||
list.externalDatas.push({
|
||||
position: { x: cell.x, y: cell.y, z: 0 },
|
||||
gridType: gm.calculateNextGridType(this.node.worldPosition, d),
|
||||
direction: gm.getRelativePosition(this.direction, d),
|
||||
});
|
||||
}
|
||||
const json = JSON.stringify(list);
|
||||
if (gm.multMode) JsBridge.call(`process${this.node.name}`, json);
|
||||
else JsBridge.call('processData', json);
|
||||
}
|
||||
|
||||
externalCallResult(isWin: boolean) {
|
||||
const gm = GameManager.instance!;
|
||||
if (isWin && (this.node.name === 'Player' || this.node.name === gm.multPlayerRole)) {
|
||||
/* success audio */
|
||||
} else if (!isWin && (this.node.name === 'Player' || this.node.name === gm.multPlayerRole)) {
|
||||
/* fail audio */
|
||||
}
|
||||
let myStep = gm.stepNum;
|
||||
if (gm.multMode) {
|
||||
switch (gm.multPlayerRole) {
|
||||
case 'PlayerA1': myStep = gm.stepA1Num; break;
|
||||
case 'PlayerA2': myStep = gm.stepA2Num; break;
|
||||
case 'PlayerA3': myStep = gm.stepA3Num; break;
|
||||
case 'PlayerB1': myStep = gm.stepB1Num; break;
|
||||
case 'PlayerB2': myStep = gm.stepB2Num; break;
|
||||
case 'PlayerB3': myStep = gm.stepB3Num; break;
|
||||
}
|
||||
}
|
||||
if ((this.node.name === 'Player' || this.node.name === gm.multPlayerRole) && !this.sendFinally) {
|
||||
const payload: ExternalResult = {
|
||||
isWin,
|
||||
stepNum: myStep,
|
||||
direction: this.direction,
|
||||
isInputEnd: gm.isInputEnd,
|
||||
};
|
||||
JsBridge.call('externalResult', JSON.stringify(payload));
|
||||
this.sendFinally = true;
|
||||
}
|
||||
if (gm.gameState !== GameState.Run) return;
|
||||
gm.setGameState(isWin ? GameState.ResultWin : GameState.ResultFail);
|
||||
}
|
||||
|
||||
private checkRide() {
|
||||
if (this.curGrid === GridType.Ride) {
|
||||
const obj = GameManager.instance!.getGameObject(this.node.worldPosition);
|
||||
if (obj) {
|
||||
this.vehicle = obj.getComponent(VehicleController);
|
||||
this.vehicle?.setDirection(this.direction);
|
||||
this.vehicle?.setPlayer(this);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
9
assets/scripts/controller/PlayerController.ts.meta
Normal file
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"ver": "4.0.24",
|
||||
"importer": "typescript",
|
||||
"imported": true,
|
||||
"uuid": "93a110e7-a99e-4719-b6c2-d2c2d8469d7f",
|
||||
"files": [],
|
||||
"subMetas": {},
|
||||
"userData": {}
|
||||
}
|
||||
59
assets/scripts/controller/PropController.ts
Normal file
@@ -0,0 +1,59 @@
|
||||
import { _decorator, Component, Vec3 } from 'cc';
|
||||
import { GameManager } from '../manager/GameManager';
|
||||
import { PlayerController } from './PlayerController';
|
||||
|
||||
const { ccclass } = _decorator;
|
||||
|
||||
@ccclass('PropController')
|
||||
export class PropController extends Component {
|
||||
private collected = false;
|
||||
|
||||
update() {
|
||||
if (this.collected || !GameManager.instance) return;
|
||||
const gm = GameManager.instance;
|
||||
const players = gm.curLevel?.children.filter((c) => c.name.includes('Player')) ?? [];
|
||||
const propCell = gm.worldToCell(this.node.worldPosition);
|
||||
for (const p of players) {
|
||||
const pc = p.getComponent(PlayerController);
|
||||
if (!pc) continue;
|
||||
const pcCell = gm.worldToCell(p.worldPosition);
|
||||
if (pcCell.x !== propCell.x || pcCell.y !== propCell.y) continue;
|
||||
this.onCollected(pc);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private onCollected(player: PlayerController) {
|
||||
if (this.collected) return;
|
||||
this.collected = true;
|
||||
const gm = GameManager.instance!;
|
||||
player.addCoins();
|
||||
gm.removeProp(this.node.worldPosition);
|
||||
const remaining = (gm.curLevel?.children.filter((c) => c.name.includes('Prop') && c !== this.node).length ?? 0);
|
||||
this.node.destroy();
|
||||
|
||||
if (remaining === 0) {
|
||||
if (gm.multMode) this.resolveMultWin(gm);
|
||||
else player.externalCallResult(true);
|
||||
}
|
||||
}
|
||||
|
||||
private resolveMultWin(gm: GameManager) {
|
||||
const sum = (names: string[]) =>
|
||||
names.reduce((t, n) => t + (gm.findNodeByName(n)?.getComponent(PlayerController)?.coins ?? 0), 0);
|
||||
const totalA = sum(['PlayerA1', 'PlayerA2', 'PlayerA3']);
|
||||
const totalB = sum(['PlayerB1', 'PlayerB2', 'PlayerB3']);
|
||||
const winA = totalA > totalB || (totalA === totalB && gm.stepA1Num + gm.stepA2Num + gm.stepA3Num <
|
||||
gm.stepB1Num + gm.stepB2Num + gm.stepB3Num);
|
||||
const set = (names: string[], win: boolean) => {
|
||||
for (const n of names) gm.findNodeByName(n)?.getComponent(PlayerController)?.externalCallResult(win);
|
||||
};
|
||||
if (winA) {
|
||||
set(['PlayerA1', 'PlayerA2', 'PlayerA3'], true);
|
||||
set(['PlayerB1', 'PlayerB2', 'PlayerB3'], false);
|
||||
} else {
|
||||
set(['PlayerA1', 'PlayerA2', 'PlayerA3'], false);
|
||||
set(['PlayerB1', 'PlayerB2', 'PlayerB3'], true);
|
||||
}
|
||||
}
|
||||
}
|
||||
9
assets/scripts/controller/PropController.ts.meta
Normal file
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"ver": "4.0.24",
|
||||
"importer": "typescript",
|
||||
"imported": true,
|
||||
"uuid": "7b8d3aef-659e-486a-8e93-a08689bed871",
|
||||
"files": [],
|
||||
"subMetas": {},
|
||||
"userData": {}
|
||||
}
|
||||
114
assets/scripts/controller/VehicleController.ts
Normal file
@@ -0,0 +1,114 @@
|
||||
import { _decorator, Vec3 } from 'cc';
|
||||
import { Direction, GridType } from '../core/Define';
|
||||
import { EventManager, EventType } from '../core/EventManager';
|
||||
import { JsBridge } from '../bridge/JsBridge';
|
||||
import { GameManager } from '../manager/GameManager';
|
||||
import { Movement } from '../gameplay/Movement';
|
||||
import { PlayerController, ExternalDataList } from './PlayerController';
|
||||
import { VisualAssets } from '../visual/VisualAssets';
|
||||
|
||||
const { ccclass } = _decorator;
|
||||
|
||||
@ccclass('VehicleController')
|
||||
export class VehicleController extends Movement {
|
||||
private player: PlayerController | null = null;
|
||||
|
||||
onLoad() {
|
||||
this.moverRole = 'vehicle';
|
||||
this.moveState = 0;
|
||||
}
|
||||
|
||||
start() {
|
||||
super.start();
|
||||
this.setIcon();
|
||||
}
|
||||
|
||||
setPlayer(p: PlayerController | null) {
|
||||
this.player = p;
|
||||
}
|
||||
|
||||
setPosition(pos: Vec3) {
|
||||
this.node.setPosition(pos);
|
||||
}
|
||||
|
||||
setName(name: string) {
|
||||
/* 可挂 Label 显示名称 */
|
||||
}
|
||||
|
||||
override setDirection(dir: Direction) {
|
||||
super.setDirection(dir);
|
||||
this.setIcon();
|
||||
if (Movement.callEach) return;
|
||||
Movement.callEach = true;
|
||||
this.player?.setDirection(dir);
|
||||
Movement.callEach = false;
|
||||
}
|
||||
|
||||
setIcon() {
|
||||
const style = GameManager.instance?.uiStyle ?? 'default';
|
||||
VisualAssets.applyVehicleSprite(this.node, this.direction, style);
|
||||
}
|
||||
|
||||
protected onMoveNextSet(isJump: boolean) {
|
||||
if (Movement.callEach) return;
|
||||
Movement.callEach = true;
|
||||
this.player?.syncFromVehicle(this.targetGridType, isJump);
|
||||
Movement.callEach = false;
|
||||
}
|
||||
|
||||
protected onMoving() {
|
||||
if (Movement.callEach) return;
|
||||
Movement.callEach = true;
|
||||
if (this.player) {
|
||||
this.player.onMoving();
|
||||
this.player.setPosition(this.node.worldPosition);
|
||||
}
|
||||
Movement.callEach = false;
|
||||
}
|
||||
|
||||
protected onMoveToTarget() {
|
||||
GameManager.instance!.removeObj(this.lastPosition);
|
||||
GameManager.instance!.addObj(this.node.worldPosition, GridType.Ride, this.node);
|
||||
if (Movement.callEach) return;
|
||||
Movement.callEach = true;
|
||||
if (this.player) {
|
||||
this.player.setPosition(this.node.worldPosition);
|
||||
this.player.syncMoveToTargetFromVehicle();
|
||||
}
|
||||
Movement.callEach = false;
|
||||
this.externalCall();
|
||||
}
|
||||
|
||||
protected onMoveFail(isJump: boolean) {
|
||||
if (!GameManager.instance!.multMode) {
|
||||
EventManager.dispatch(EventType.InputEnd, GameManager.instance!.gameState);
|
||||
}
|
||||
}
|
||||
|
||||
callVehicleInfo() {
|
||||
this.externalCall();
|
||||
}
|
||||
|
||||
externalCall() {
|
||||
const gm = GameManager.instance!;
|
||||
const list: ExternalDataList = { direction: this.direction, externalDatas: [] };
|
||||
const self = gm.worldToCell(this.node.worldPosition);
|
||||
list.externalDatas.push({
|
||||
position: { x: self.x, y: self.y, z: 0 },
|
||||
gridType: this.curGrid,
|
||||
direction: 'self',
|
||||
});
|
||||
for (let d = Direction.North; d <= Direction.West; d++) {
|
||||
const wp = gm.nextGridPosition(this.node.worldPosition, d);
|
||||
const cell = gm.worldToCell(wp);
|
||||
list.externalDatas.push({
|
||||
position: { x: cell.x, y: cell.y, z: 0 },
|
||||
gridType: gm.calculateNextGridType(this.node.worldPosition, d),
|
||||
direction: gm.getRelativePosition(this.direction, d),
|
||||
});
|
||||
}
|
||||
const json = JSON.stringify(list);
|
||||
if (gm.multMode) JsBridge.call(`process${this.node.name}`, json);
|
||||
else JsBridge.call('processVehicleData', json);
|
||||
}
|
||||
}
|
||||
9
assets/scripts/controller/VehicleController.ts.meta
Normal file
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"ver": "4.0.24",
|
||||
"importer": "typescript",
|
||||
"imported": true,
|
||||
"uuid": "aa054a89-5920-470a-aac7-9af275a2d789",
|
||||
"files": [],
|
||||
"subMetas": {},
|
||||
"userData": {}
|
||||
}
|
||||
9
assets/scripts/core.meta
Normal file
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"ver": "1.2.0",
|
||||
"importer": "directory",
|
||||
"imported": true,
|
||||
"uuid": "f398f642-6b7c-47cf-9e92-19d6cf61c05c",
|
||||
"files": [],
|
||||
"subMetas": {},
|
||||
"userData": {}
|
||||
}
|
||||
152
assets/scripts/core/Define.ts
Normal file
@@ -0,0 +1,152 @@
|
||||
/** 与 Unity Platformer.Core.Define 对齐 */
|
||||
|
||||
export enum Direction {
|
||||
North = 0,
|
||||
East = 1,
|
||||
South = 2,
|
||||
West = 3,
|
||||
}
|
||||
|
||||
export enum MoveState {
|
||||
Idle = 0,
|
||||
Moving = 1,
|
||||
}
|
||||
|
||||
export enum GridType {
|
||||
Across = 0,
|
||||
Jump = 1,
|
||||
Block = 2,
|
||||
Ride = 3,
|
||||
None = 4,
|
||||
Boundary = 5,
|
||||
}
|
||||
|
||||
export enum Skin {
|
||||
Silu = 0,
|
||||
Panda = 1,
|
||||
RedArmy = 2,
|
||||
numMan = 3,
|
||||
snow = 4,
|
||||
sanxing = 5,
|
||||
}
|
||||
|
||||
export enum GameState {
|
||||
Run = 0,
|
||||
ResultWin = 1,
|
||||
ResultFail = 2,
|
||||
}
|
||||
|
||||
export type MoverRole = 'player' | 'vehicle';
|
||||
|
||||
export const CELL_SIZE = 1;
|
||||
|
||||
export const CommonDefine = {
|
||||
TilemapGround: 'Ground',
|
||||
TilemapBorder: 'Border',
|
||||
Prop: 'Prop',
|
||||
Vehicle: 'Vehicle',
|
||||
BlockBase: 'Baseblock',
|
||||
BlockJump: 'JumpBlock',
|
||||
};
|
||||
|
||||
export function addDirection(dir: Direction, delta: number): Direction {
|
||||
const n = 4;
|
||||
return (((dir + delta) % n) + n) % n as Direction;
|
||||
}
|
||||
|
||||
type MoveKey = string;
|
||||
|
||||
function mk(role: MoverRole, cur: GridType, next: GridType, jump: boolean): MoveKey {
|
||||
return `${role}|${cur}|${next}|${jump ? 1 : 0}`;
|
||||
}
|
||||
|
||||
function buildMoveTable(entries: [MoverRole, GridType, GridType, boolean, number][]): Map<MoveKey, number> {
|
||||
const m = new Map<MoveKey, number>();
|
||||
for (const [role, cur, next, jump, v] of entries) {
|
||||
m.set(mk(role, cur, next, jump), v);
|
||||
}
|
||||
return m;
|
||||
}
|
||||
|
||||
/** 单人 moveCondition */
|
||||
export const moveCondition = buildMoveTable([
|
||||
['player', GridType.Across, GridType.Across, false, 1],
|
||||
['player', GridType.Jump, GridType.Across, false, 1],
|
||||
['player', GridType.Jump, GridType.Across, true, 1],
|
||||
['player', GridType.Ride, GridType.Across, false, 1],
|
||||
['player', GridType.Ride, GridType.Across, true, 1],
|
||||
['player', GridType.Across, GridType.Jump, true, 1],
|
||||
['player', GridType.Across, GridType.Ride, false, 1],
|
||||
['player', GridType.Across, GridType.Ride, true, 1],
|
||||
['player', GridType.Jump, GridType.Ride, false, 1],
|
||||
['player', GridType.Jump, GridType.Ride, true, 1],
|
||||
['player', GridType.Jump, GridType.Jump, false, 1],
|
||||
['player', GridType.Jump, GridType.Jump, true, 1],
|
||||
['player', GridType.Ride, GridType.None, false, 1],
|
||||
['player', GridType.Ride, GridType.None, true, 1],
|
||||
['player', GridType.None, GridType.None, false, 1],
|
||||
['player', GridType.None, GridType.None, true, 1],
|
||||
['player', GridType.None, GridType.Across, false, 1],
|
||||
['player', GridType.None, GridType.Across, true, 1],
|
||||
['vehicle', GridType.None, GridType.None, false, 1],
|
||||
['vehicle', GridType.None, GridType.None, true, 1],
|
||||
['vehicle', GridType.Ride, GridType.None, false, 1],
|
||||
]);
|
||||
|
||||
/** 多人 moveConditionMult(与 Unity Define.moveConditionMult 一致) */
|
||||
const multEntries: [MoverRole, GridType, GridType, boolean, number][] = [
|
||||
['player', GridType.Across, GridType.Across, false, 1],
|
||||
['player', GridType.Across, GridType.Across, true, 1],
|
||||
['player', GridType.Jump, GridType.Across, false, 1],
|
||||
['player', GridType.Jump, GridType.Across, true, 1],
|
||||
['player', GridType.Ride, GridType.Across, false, 1],
|
||||
['player', GridType.Ride, GridType.Across, true, 1],
|
||||
['player', GridType.Across, GridType.Jump, true, 1],
|
||||
['player', GridType.Across, GridType.Jump, false, 1],
|
||||
['player', GridType.Across, GridType.Ride, false, 1],
|
||||
['player', GridType.Across, GridType.Ride, true, 1],
|
||||
['player', GridType.Jump, GridType.Ride, false, 1],
|
||||
['player', GridType.Jump, GridType.Ride, true, 1],
|
||||
['player', GridType.Jump, GridType.Jump, false, 1],
|
||||
['player', GridType.Jump, GridType.Jump, true, 1],
|
||||
['player', GridType.Ride, GridType.None, false, 1],
|
||||
['player', GridType.Ride, GridType.None, true, 1],
|
||||
['player', GridType.None, GridType.None, false, 1],
|
||||
['player', GridType.None, GridType.None, true, 1],
|
||||
['player', GridType.None, GridType.Across, false, 1],
|
||||
['player', GridType.None, GridType.Across, true, 1],
|
||||
['player', GridType.Across, GridType.Block, false, 0],
|
||||
['player', GridType.Across, GridType.Block, true, 0],
|
||||
['vehicle', GridType.Ride, GridType.Block, false, 0],
|
||||
['vehicle', GridType.Ride, GridType.Block, true, 0],
|
||||
['vehicle', GridType.Ride, GridType.Across, false, 0],
|
||||
['vehicle', GridType.Ride, GridType.Across, true, 0],
|
||||
['vehicle', GridType.None, GridType.None, false, 1],
|
||||
['vehicle', GridType.None, GridType.None, true, 1],
|
||||
['vehicle', GridType.Ride, GridType.None, false, 1],
|
||||
['vehicle', GridType.Ride, GridType.None, true, 1],
|
||||
];
|
||||
export const moveConditionMultMap = buildMoveTable(multEntries);
|
||||
|
||||
export const skinPath: Record<Skin, { unlockLevel: number }> = {
|
||||
[Skin.Silu]: { unlockLevel: 0 },
|
||||
[Skin.Panda]: { unlockLevel: 400 },
|
||||
[Skin.RedArmy]: { unlockLevel: 800 },
|
||||
[Skin.numMan]: { unlockLevel: 1200 },
|
||||
[Skin.snow]: { unlockLevel: 1600 },
|
||||
[Skin.sanxing]: { unlockLevel: 1600 },
|
||||
};
|
||||
|
||||
export function getMoveCondition(mult: boolean): Map<MoveKey, number> {
|
||||
return mult ? moveConditionMultMap : moveCondition;
|
||||
}
|
||||
|
||||
export function lookupMove(
|
||||
table: Map<MoveKey, number>,
|
||||
role: MoverRole,
|
||||
cur: GridType,
|
||||
next: GridType,
|
||||
jump: boolean,
|
||||
): number | undefined {
|
||||
return table.get(mk(role, cur, next, jump));
|
||||
}
|
||||
9
assets/scripts/core/Define.ts.meta
Normal file
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"ver": "4.0.24",
|
||||
"importer": "typescript",
|
||||
"imported": true,
|
||||
"uuid": "e5d9eb64-9856-42f8-b14c-ab18ad6ff50e",
|
||||
"files": [],
|
||||
"subMetas": {},
|
||||
"userData": {}
|
||||
}
|
||||
27
assets/scripts/core/EventManager.ts
Normal file
@@ -0,0 +1,27 @@
|
||||
export enum EventType {
|
||||
LevelInit = 'LevelInit',
|
||||
InputEnd = 'InputEnd',
|
||||
}
|
||||
|
||||
type Handler = (...args: unknown[]) => void;
|
||||
|
||||
const listeners = new Map<EventType, Handler[]>();
|
||||
|
||||
export const EventManager = {
|
||||
register(type: EventType, handler: Handler) {
|
||||
if (!listeners.has(type)) listeners.set(type, []);
|
||||
const list = listeners.get(type)!;
|
||||
if (!list.includes(handler)) list.push(handler);
|
||||
},
|
||||
remove(type: EventType, handler: Handler) {
|
||||
const list = listeners.get(type);
|
||||
if (!list) return;
|
||||
const i = list.indexOf(handler);
|
||||
if (i >= 0) list.splice(i, 1);
|
||||
},
|
||||
dispatch(type: EventType, ...args: unknown[]) {
|
||||
const list = listeners.get(type);
|
||||
if (!list) return;
|
||||
for (const h of [...list]) h(...args);
|
||||
},
|
||||
};
|
||||
9
assets/scripts/core/EventManager.ts.meta
Normal file
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"ver": "4.0.24",
|
||||
"importer": "typescript",
|
||||
"imported": true,
|
||||
"uuid": "d101e574-7e0a-4ccf-9ead-12d3872afe82",
|
||||
"files": [],
|
||||
"subMetas": {},
|
||||
"userData": {}
|
||||
}
|
||||
9
assets/scripts/gameplay.meta
Normal file
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"ver": "1.2.0",
|
||||
"importer": "directory",
|
||||
"imported": true,
|
||||
"uuid": "189e768a-9029-4554-9763-dd1099995a8d",
|
||||
"files": [],
|
||||
"subMetas": {},
|
||||
"userData": {}
|
||||
}
|
||||
178
assets/scripts/gameplay/Movement.ts
Normal file
@@ -0,0 +1,178 @@
|
||||
import { _decorator, Component, Vec3 } from 'cc';
|
||||
import {
|
||||
Direction, GameState, GridType, MoveState, MoverRole,
|
||||
addDirection, getMoveCondition, lookupMove,
|
||||
} from '../core/Define';
|
||||
import { GameManager } from '../manager/GameManager';
|
||||
|
||||
const { ccclass, property } = _decorator;
|
||||
|
||||
@ccclass('Movement')
|
||||
export class Movement extends Component {
|
||||
@property
|
||||
moveSpeed = 4;
|
||||
|
||||
direction: Direction = Direction.North;
|
||||
moveState: MoveState = MoveState.Idle;
|
||||
moverRole: MoverRole = 'player';
|
||||
|
||||
protected targetPosition = new Vec3();
|
||||
protected targetGridType: GridType = GridType.None;
|
||||
protected lastPosition = new Vec3();
|
||||
protected step = 0;
|
||||
|
||||
private moveWait = false;
|
||||
private queue: Promise<void> = Promise.resolve();
|
||||
static callEach = false;
|
||||
|
||||
get curGrid(): GridType {
|
||||
return GameManager.instance!.calculateGridType(this.node.position);
|
||||
}
|
||||
get nextGrid(): GridType {
|
||||
return GameManager.instance!.calculateNextGridType(this.node.position, this.direction);
|
||||
}
|
||||
get lastGrid(): GridType {
|
||||
return GameManager.instance!.calculateLastGridType(this.node.position, this.direction);
|
||||
}
|
||||
get isFront(): boolean {
|
||||
return this.direction === Direction.South || this.direction === Direction.East;
|
||||
}
|
||||
|
||||
start() {
|
||||
this.setDirection(this.direction);
|
||||
}
|
||||
|
||||
update(dt: number) {
|
||||
if (this.moveState !== MoveState.Moving) return;
|
||||
const pos = this.node.position;
|
||||
const next = new Vec3();
|
||||
Vec3.moveTowards(next, pos, this.targetPosition, this.moveSpeed * dt);
|
||||
this.node.setPosition(next);
|
||||
this.onMoving();
|
||||
if (Vec3.distance(next, this.targetPosition) < 0.01) {
|
||||
this.node.setPosition(this.targetPosition);
|
||||
this.moveState = MoveState.Idle;
|
||||
this.moveWait = false;
|
||||
this.onMoveToTarget();
|
||||
}
|
||||
}
|
||||
|
||||
setDirection(dir: Direction) {
|
||||
this.direction = dir;
|
||||
}
|
||||
|
||||
protected onMoving() {}
|
||||
protected onMoveToTarget() {}
|
||||
protected onMoveNextSet(_isJump: boolean) {}
|
||||
protected onMoveFail(_isJump: boolean) {}
|
||||
protected playMoveAnim() {}
|
||||
|
||||
private moveNextCheck(isJump: boolean, toFront: boolean): number {
|
||||
const gm = GameManager.instance!;
|
||||
const dir = toFront ? this.direction : addDirection(this.direction, 2);
|
||||
const targetTmp = gm.nextGridPosition(this.node.position, dir);
|
||||
const targetType = gm.calculateGridType(targetTmp);
|
||||
const table = getMoveCondition(gm.multMode);
|
||||
const nextG = toFront ? this.nextGrid : this.lastGrid;
|
||||
const v = lookupMove(table, this.moverRole, this.curGrid, nextG, isJump);
|
||||
if (v !== undefined) {
|
||||
if (v === 1) {
|
||||
if (gm.multMode) {
|
||||
const nextCell = gm.worldToCell(targetTmp);
|
||||
for (const n of ['PlayerA1', 'PlayerA2', 'PlayerA3', 'PlayerB1', 'PlayerB2', 'PlayerB3']) {
|
||||
const p = gm.findNodeByName(n);
|
||||
if (p) {
|
||||
const c = gm.worldToCell(p.worldPosition);
|
||||
if (c.x === nextCell.x && c.y === nextCell.y) return 0;
|
||||
}
|
||||
}
|
||||
if (nextG === GridType.Ride) {
|
||||
const ride = gm.getGameObject(targetTmp);
|
||||
const expect = this.node.name.replace('Player', 'Vehicle');
|
||||
if (ride && ride.name !== expect) return 0;
|
||||
}
|
||||
}
|
||||
this.targetPosition.set(targetTmp);
|
||||
this.targetGridType = targetType;
|
||||
}
|
||||
return v;
|
||||
}
|
||||
return gm.multMode ? 0 : -1;
|
||||
}
|
||||
|
||||
private enqueue(fn: () => Promise<void>) {
|
||||
this.queue = this.queue.then(fn).catch((e) => console.error(e));
|
||||
}
|
||||
|
||||
private waitUntil(cond: () => boolean): Promise<void> {
|
||||
return new Promise((resolve) => {
|
||||
const tick = () => {
|
||||
if (cond()) resolve();
|
||||
else requestAnimationFrame(tick);
|
||||
};
|
||||
tick();
|
||||
});
|
||||
}
|
||||
|
||||
private async moveCoroutine(n: number, isJump: boolean) {
|
||||
await this.waitUntil(() => !this.moveWait);
|
||||
const toFront = n > 0;
|
||||
this.step = Math.abs(n);
|
||||
const gm = GameManager.instance!;
|
||||
while (this.step > 0 && gm.gameState === GameState.Run) {
|
||||
this.step--;
|
||||
const r = this.moveNextCheck(isJump, toFront);
|
||||
if (r === -1) {
|
||||
this.onMoveFail(isJump);
|
||||
return;
|
||||
}
|
||||
if (r === 1) {
|
||||
this.moveWait = true;
|
||||
this.lastPosition.set(this.node.position);
|
||||
this.moveState = MoveState.Moving;
|
||||
this.onMoveNextSet(isJump);
|
||||
await this.waitUntil(() => !this.moveWait);
|
||||
} else {
|
||||
this.playMoveAnim();
|
||||
this.moveState = MoveState.Moving;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async rotateCoroutine(n: number) {
|
||||
await this.waitUntil(() => !this.moveWait);
|
||||
this.moveWait = true;
|
||||
this.setDirection(addDirection(this.direction, n));
|
||||
this.moveWait = false;
|
||||
this.onMoveToTarget();
|
||||
}
|
||||
|
||||
protected jsCallCheck(n: number): boolean {
|
||||
const gm = GameManager.instance!;
|
||||
const name = this.node.name;
|
||||
if (name === 'Player' || name === 'Vehicle') return gm.jsCallCheck(n);
|
||||
if (gm.multMode) return gm.jsCallCheckMultMode(n, name);
|
||||
return false;
|
||||
}
|
||||
|
||||
callMove(n: number) {
|
||||
if (!this.jsCallCheck(n)) return;
|
||||
this.enqueue(() => this.moveCoroutine(n, false));
|
||||
}
|
||||
|
||||
callRotateLeft(n: number) {
|
||||
if (!this.jsCallCheck(n)) return;
|
||||
this.enqueue(() => this.rotateCoroutine(-n));
|
||||
}
|
||||
|
||||
callRotateRight(n: number) {
|
||||
if (!this.jsCallCheck(n)) return;
|
||||
this.enqueue(() => this.rotateCoroutine(n));
|
||||
}
|
||||
|
||||
callJump() {
|
||||
if (this.moverRole !== 'player') return;
|
||||
if (!this.jsCallCheck(1)) return;
|
||||
this.enqueue(() => this.moveCoroutine(1, true));
|
||||
}
|
||||
}
|
||||
9
assets/scripts/gameplay/Movement.ts.meta
Normal file
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"ver": "4.0.24",
|
||||
"importer": "typescript",
|
||||
"imported": true,
|
||||
"uuid": "5fdb4109-5d6a-4002-90f9-a7568122ac93",
|
||||
"files": [],
|
||||
"subMetas": {},
|
||||
"userData": {}
|
||||
}
|
||||
9
assets/scripts/level.meta
Normal file
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"ver": "1.2.0",
|
||||
"importer": "directory",
|
||||
"imported": true,
|
||||
"uuid": "95df233f-f174-4e12-a4f4-cc94d163a21d",
|
||||
"files": [],
|
||||
"subMetas": {},
|
||||
"userData": {}
|
||||
}
|
||||
47
assets/scripts/level/LevelRegistry.ts
Normal file
@@ -0,0 +1,47 @@
|
||||
import { Direction } from '../core/Define';
|
||||
import { LevelConfig } from './LevelTypes';
|
||||
import { LEVELS_600 } from './levels-600.generated';
|
||||
|
||||
/** 额外关卡(多人等) */
|
||||
const EXTRA_LEVELS: Record<number, LevelConfig> = {
|
||||
601: {
|
||||
levelID: 601,
|
||||
boundary: { x: 20, y: 20 },
|
||||
spawns: [
|
||||
{ x: 0, y: 0, kind: 'player', playerDirection: Direction.North },
|
||||
{ x: 6, y: 6, kind: 'prop' },
|
||||
{ x: -1, y: 2, kind: 'prop' },
|
||||
],
|
||||
},
|
||||
999001: {
|
||||
levelID: 999001,
|
||||
boundary: { x: 999, y: 999 },
|
||||
spawns: [
|
||||
{ x: -9, y: -9, kind: 'player', playerDirection: Direction.South },
|
||||
{ x: 9, y: 9, kind: 'player', playerDirection: Direction.North },
|
||||
{ x: -9, y: -10, kind: 'vehicle', vehicleDirection: Direction.North },
|
||||
{ x: 9, y: 10, kind: 'vehicle', vehicleDirection: Direction.South },
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
const allLevels: Record<number, LevelConfig> = {
|
||||
...LEVELS_600,
|
||||
...EXTRA_LEVELS,
|
||||
};
|
||||
|
||||
export function getLevelConfig(levelID: number): LevelConfig | null {
|
||||
return allLevels[levelID] ?? null;
|
||||
}
|
||||
|
||||
export function hasLevel(levelID: number): boolean {
|
||||
return levelID in allLevels;
|
||||
}
|
||||
|
||||
export function registerLevel(config: LevelConfig) {
|
||||
allLevels[config.levelID] = config;
|
||||
}
|
||||
|
||||
export function getLevelCount(): number {
|
||||
return Object.keys(allLevels).length;
|
||||
}
|
||||
9
assets/scripts/level/LevelRegistry.ts.meta
Normal file
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"ver": "4.0.24",
|
||||
"importer": "typescript",
|
||||
"imported": true,
|
||||
"uuid": "05e4b37c-f71a-42c0-b8f6-88fda70b655d",
|
||||
"files": [],
|
||||
"subMetas": {},
|
||||
"userData": {}
|
||||
}
|
||||
22
assets/scripts/level/LevelTypes.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
import { Direction } from '../core/Define';
|
||||
|
||||
export type SpawnKind = 'player' | 'vehicle' | 'prop' | 'prop_decor' | 'enemy';
|
||||
|
||||
export interface SpawnConfig {
|
||||
x: number;
|
||||
y: number;
|
||||
kind: SpawnKind;
|
||||
playerDirection?: Direction;
|
||||
vehicleDirection?: Direction;
|
||||
}
|
||||
|
||||
/** 稀疏地块:key 为 "x,y" */
|
||||
export interface LevelConfig {
|
||||
levelID: number;
|
||||
boundary: { x: number; y: number };
|
||||
spawns: SpawnConfig[];
|
||||
/** Ground 层 tile 名 */
|
||||
ground?: Record<string, string>;
|
||||
/** Border 阻挡格 */
|
||||
border?: Record<string, boolean>;
|
||||
}
|
||||
9
assets/scripts/level/LevelTypes.ts.meta
Normal file
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"ver": "4.0.24",
|
||||
"importer": "typescript",
|
||||
"imported": true,
|
||||
"uuid": "9fd69354-fd48-4ecf-9bd3-2cb056258612",
|
||||
"files": [],
|
||||
"subMetas": {},
|
||||
"userData": {}
|
||||
}
|
||||
2022
assets/scripts/level/border-cache.json
Normal file
11
assets/scripts/level/border-cache.json.meta
Normal file
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"ver": "2.0.1",
|
||||
"importer": "json",
|
||||
"imported": true,
|
||||
"uuid": "c5bd9bd8-738e-4bbc-8f04-8b1c77279929",
|
||||
"files": [
|
||||
".json"
|
||||
],
|
||||
"subMetas": {},
|
||||
"userData": {}
|
||||
}
|
||||
7858
assets/scripts/level/levels-600.generated.ts
Normal file
9
assets/scripts/level/levels-600.generated.ts.meta
Normal file
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"ver": "4.0.24",
|
||||
"importer": "typescript",
|
||||
"imported": true,
|
||||
"uuid": "507007a3-8bd2-41d5-b962-44c38a653bbb",
|
||||
"files": [],
|
||||
"subMetas": {},
|
||||
"userData": {}
|
||||
}
|
||||
9
assets/scripts/manager.meta
Normal file
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"ver": "1.2.0",
|
||||
"importer": "directory",
|
||||
"imported": true,
|
||||
"uuid": "f289d7c6-e9b4-45e2-9068-532f08608094",
|
||||
"files": [],
|
||||
"subMetas": {},
|
||||
"userData": {}
|
||||
}
|
||||
388
assets/scripts/manager/GameManager.ts
Normal file
@@ -0,0 +1,388 @@
|
||||
import { _decorator, Component, Node, Vec3, Color, Graphics, UITransform, director } from 'cc';
|
||||
import { CELL_PIXEL } from '../AppBootstrap';
|
||||
import {
|
||||
CELL_SIZE, CommonDefine, Direction, GameState, GridType, Skin, addDirection,
|
||||
} from '../core/Define';
|
||||
import { EventManager, EventType } from '../core/EventManager';
|
||||
import { JsBridge } from '../bridge/JsBridge';
|
||||
import { getLevelConfig, hasLevel, registerLevel } from '../level/LevelRegistry';
|
||||
import { LevelConfig, SpawnConfig } from '../level/LevelTypes';
|
||||
import { PlayerController } from '../controller/PlayerController';
|
||||
import { VehicleController } from '../controller/VehicleController';
|
||||
import { PropController } from '../controller/PropController';
|
||||
import { VisualAssets } from '../visual/VisualAssets';
|
||||
import { SpawnKind } from '../level/LevelTypes';
|
||||
|
||||
const { ccclass, property } = _decorator;
|
||||
|
||||
interface GridEntry {
|
||||
type: GridType;
|
||||
node: Node;
|
||||
}
|
||||
|
||||
@ccclass('GameManager')
|
||||
export class GameManager extends Component {
|
||||
static instance: GameManager | null = null;
|
||||
|
||||
@property(Node)
|
||||
mainLevelEntrance: Node | null = null;
|
||||
|
||||
@property
|
||||
initialLevelID = 1;
|
||||
|
||||
playerSkin: Skin = Skin.Silu;
|
||||
multMode = false;
|
||||
multPlayerRole = '';
|
||||
gameState: GameState = GameState.Run;
|
||||
isInputEnd = false;
|
||||
uiStyle = 'default';
|
||||
curLevelID = 1;
|
||||
|
||||
stepNum = 0;
|
||||
stepA1Num = 0;
|
||||
stepA2Num = 0;
|
||||
stepA3Num = 0;
|
||||
stepB1Num = 0;
|
||||
stepB2Num = 0;
|
||||
stepB3Num = 0;
|
||||
|
||||
private creating = false;
|
||||
private curLevel: Node | null = null;
|
||||
private curConfig: LevelConfig | null = null;
|
||||
private gridTypes = new Map<string, GridEntry>();
|
||||
private gridTypesForProps = new Map<string, GridEntry>();
|
||||
private groundCells = new Map<string, string>();
|
||||
private borderCells = new Set<string>();
|
||||
|
||||
onLoad() {
|
||||
if (GameManager.instance && GameManager.instance !== this) {
|
||||
this.destroy();
|
||||
return;
|
||||
}
|
||||
GameManager.instance = this;
|
||||
}
|
||||
|
||||
start() {
|
||||
/* 关卡由 AppBootstrap 在就绪后加载 */
|
||||
}
|
||||
|
||||
onDestroy() {
|
||||
if (GameManager.instance === this) GameManager.instance = null;
|
||||
}
|
||||
|
||||
// --- 状态 ---
|
||||
setGameState(s: GameState) { this.gameState = s; }
|
||||
|
||||
initData() {
|
||||
this.setGameState(GameState.Run);
|
||||
this.stepNum = 0;
|
||||
this.stepA1Num = this.stepA2Num = this.stepA3Num = 0;
|
||||
this.stepB1Num = this.stepB2Num = this.stepB3Num = 0;
|
||||
this.callSetIsInputEnd(0);
|
||||
}
|
||||
|
||||
jsCallCheck(n: number): boolean {
|
||||
if (this.isInputEnd || this.gameState !== GameState.Run) return false;
|
||||
this.stepNum += Math.abs(n);
|
||||
return true;
|
||||
}
|
||||
|
||||
jsCallCheckMultMode(n: number, name: string): boolean {
|
||||
if (this.isInputEnd || this.gameState !== GameState.Run) return false;
|
||||
const a = Math.abs(n);
|
||||
switch (name) {
|
||||
case 'PlayerA1':
|
||||
case 'VehicleA1': this.stepA1Num += a; break;
|
||||
case 'PlayerA2':
|
||||
case 'VehicleA2': this.stepA2Num += a; break;
|
||||
case 'PlayerA3':
|
||||
case 'VehicleA3': this.stepA3Num += a; break;
|
||||
case 'PlayerB1':
|
||||
case 'VehicleB1': this.stepB1Num += a; break;
|
||||
case 'PlayerB2':
|
||||
case 'VehicleB2': this.stepB2Num += a; break;
|
||||
case 'PlayerB3':
|
||||
case 'VehicleB3': this.stepB3Num += a; break;
|
||||
default: break;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
callSetIsInputEnd(v: number) {
|
||||
this.isInputEnd = v !== 0;
|
||||
if (this.isInputEnd) EventManager.dispatch(EventType.InputEnd, this.gameState);
|
||||
}
|
||||
|
||||
getRelativePosition(given: Direction, input: Direction): string {
|
||||
const rel = (((input - given) % 4) + 4) % 4;
|
||||
return ['front', 'right', 'back', 'left'][rel];
|
||||
}
|
||||
|
||||
// --- 坐标 ---
|
||||
cellKey(x: number, y: number) { return `${x},${y}`; }
|
||||
|
||||
cellToWorld(cell: Vec3): Vec3 {
|
||||
return new Vec3(cell.x * CELL_PIXEL, cell.y * CELL_PIXEL, 0);
|
||||
}
|
||||
|
||||
worldToCell(world: Vec3): Vec3 {
|
||||
return new Vec3(Math.round(world.x / CELL_PIXEL), Math.round(world.y / CELL_PIXEL), 0);
|
||||
}
|
||||
|
||||
nextGridPosition(pos: Vec3, dir: Direction): Vec3 {
|
||||
const c = this.worldToCell(pos);
|
||||
switch (dir) {
|
||||
case Direction.North: c.x += 1; break;
|
||||
case Direction.South: c.x -= 1; break;
|
||||
case Direction.East: c.y -= 1; break;
|
||||
case Direction.West: c.y += 1; break;
|
||||
}
|
||||
return this.cellToWorld(c);
|
||||
}
|
||||
|
||||
calculateGridType(pos: Vec3): GridType {
|
||||
const cell = this.worldToCell(pos);
|
||||
const key = this.cellKey(cell.x, cell.y);
|
||||
const dyn = this.gridTypes.get(key);
|
||||
if (dyn) return dyn.type;
|
||||
if (this.borderCells.has(key)) return GridType.Block;
|
||||
const g = this.groundCells.get(key);
|
||||
if (g === CommonDefine.BlockBase) return GridType.Across;
|
||||
if (g === CommonDefine.BlockJump) return GridType.Jump;
|
||||
const b = this.curConfig?.boundary;
|
||||
if (b && (Math.abs(cell.x) >= b.x || Math.abs(cell.y) >= b.y)) return GridType.Boundary;
|
||||
return GridType.None;
|
||||
}
|
||||
|
||||
calculateNextGridType(pos: Vec3, dir: Direction): GridType {
|
||||
return this.calculateGridType(this.nextGridPosition(pos, dir));
|
||||
}
|
||||
|
||||
calculateLastGridType(pos: Vec3, dir: Direction): GridType {
|
||||
return this.calculateGridType(this.nextGridPosition(pos, addDirection(dir, 2)));
|
||||
}
|
||||
|
||||
getGameObject(pos: Vec3): Node | null {
|
||||
const key = this.cellKey(this.worldToCell(pos).x, this.worldToCell(pos).y);
|
||||
return this.gridTypes.get(key)?.node ?? null;
|
||||
}
|
||||
|
||||
addObj(pos: Vec3, type: GridType, node: Node) {
|
||||
const c = this.worldToCell(pos);
|
||||
this.gridTypes.set(this.cellKey(c.x, c.y), { type, node });
|
||||
}
|
||||
|
||||
removeObj(pos: Vec3) {
|
||||
const c = this.worldToCell(pos);
|
||||
this.gridTypes.delete(this.cellKey(c.x, c.y));
|
||||
}
|
||||
|
||||
removeProp(pos: Vec3) {
|
||||
const c = this.worldToCell(pos);
|
||||
this.gridTypesForProps.delete(this.cellKey(c.x, c.y));
|
||||
}
|
||||
|
||||
countProp(): number {
|
||||
if (!this.curLevel) return 0;
|
||||
return this.curLevel.children.filter((c) => c.isValid && c.getComponent(PropController)).length;
|
||||
}
|
||||
|
||||
getCurLevel() { return this.curConfig; }
|
||||
|
||||
findNodeByName(name: string): Node | null {
|
||||
const scene = director.getScene();
|
||||
if (!scene) return null;
|
||||
return this.findInTree(scene, name);
|
||||
}
|
||||
|
||||
private findInTree(root: Node, name: string): Node | null {
|
||||
if (root.name === name) return root;
|
||||
for (const ch of root.children) {
|
||||
const f = this.findInTree(ch, name);
|
||||
if (f) return f;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
// --- 关卡 ---
|
||||
destroyCurLevel() {
|
||||
if (this.curLevel?.isValid) this.curLevel.destroy();
|
||||
this.curLevel = null;
|
||||
this.gridTypes.clear();
|
||||
this.gridTypesForProps.clear();
|
||||
this.groundCells.clear();
|
||||
this.borderCells.clear();
|
||||
}
|
||||
|
||||
switchLevel(levelID: number) {
|
||||
if (!hasLevel(levelID) || this.creating) return;
|
||||
this.multMode = levelID >= 999000;
|
||||
this.destroyCurLevel();
|
||||
this.createNewLevel(levelID);
|
||||
}
|
||||
|
||||
async createNewLevel(levelID: number) {
|
||||
if (this.creating) return;
|
||||
this.creating = true;
|
||||
const config = getLevelConfig(levelID);
|
||||
if (!config || !this.mainLevelEntrance) {
|
||||
this.creating = false;
|
||||
return;
|
||||
}
|
||||
this.curLevelID = levelID;
|
||||
this.curConfig = config;
|
||||
|
||||
const levelRoot = new Node(`Level_${levelID}`);
|
||||
levelRoot.parent = this.mainLevelEntrance;
|
||||
this.curLevel = levelRoot;
|
||||
|
||||
if (config.ground) {
|
||||
for (const [k, v] of Object.entries(config.ground)) this.groundCells.set(k, v);
|
||||
}
|
||||
if (config.border) {
|
||||
for (const k of Object.keys(config.border)) this.borderCells.add(k);
|
||||
}
|
||||
|
||||
this.drawGridDebug(levelRoot, config);
|
||||
|
||||
const spawned: Node[] = [];
|
||||
for (const s of config.spawns) {
|
||||
const node = this.spawnEntity(levelRoot, s);
|
||||
if (node) spawned.push(node);
|
||||
}
|
||||
|
||||
this.initGridTypes();
|
||||
this.initData();
|
||||
EventManager.dispatch(EventType.LevelInit);
|
||||
this.externalCallLevelInfo(spawned);
|
||||
this.creating = false;
|
||||
}
|
||||
|
||||
resetLevel() {
|
||||
this.destroyCurLevel();
|
||||
this.createNewLevel(this.curLevelID);
|
||||
}
|
||||
|
||||
startMultPlay(role: string, coinsJson: string) {
|
||||
this.multMode = true;
|
||||
this.multPlayerRole = role;
|
||||
let coins: string[] = [];
|
||||
try {
|
||||
coins = JSON.parse(coinsJson) as string[];
|
||||
} catch (e) {
|
||||
console.error('StartMultPlay coins parse', e);
|
||||
}
|
||||
const base = getLevelConfig(999001);
|
||||
if (!base) return;
|
||||
const extra = coins.map((s) => {
|
||||
const [xs, ys] = s.split(',');
|
||||
return { x: parseInt(xs, 10), y: parseInt(ys, 10), kind: 'prop' as const };
|
||||
});
|
||||
registerLevel({ ...base, spawns: [...base.spawns, ...extra] });
|
||||
this.switchLevel(999001);
|
||||
}
|
||||
|
||||
changeUIStyle(style: string) {
|
||||
this.uiStyle = style;
|
||||
}
|
||||
|
||||
callMute() { /* 可由 UIMain 实现 */ }
|
||||
callUnmute() { /* 可由 UIMain 实现 */ }
|
||||
|
||||
private initGridTypes() {
|
||||
this.gridTypes.clear();
|
||||
this.gridTypesForProps.clear();
|
||||
if (!this.curLevel) return;
|
||||
const vehicles = this.curLevel.children.filter((c) => c.name.includes('Vehicle'));
|
||||
for (const v of vehicles) {
|
||||
const c = this.worldToCell(v.worldPosition);
|
||||
this.gridTypes.set(this.cellKey(c.x, c.y), { type: GridType.Ride, node: v });
|
||||
}
|
||||
const props = this.curLevel.children.filter((c) => c.getComponent(PropController));
|
||||
for (const p of props) {
|
||||
const c = this.worldToCell(p.worldPosition);
|
||||
this.gridTypesForProps.set(this.cellKey(c.x, c.y), {
|
||||
type: this.calculateGridType(p.worldPosition),
|
||||
node: p,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private spawnEntity(parent: Node, s: SpawnConfig): Node | null {
|
||||
const pos = this.cellToWorld(new Vec3(s.x, s.y, 0));
|
||||
let node: Node;
|
||||
if (s.kind === 'player') {
|
||||
node = new Node(s.x === -9 ? 'PlayerA1' : s.x === 9 ? 'PlayerB1' : 'Player');
|
||||
node.addComponent(PlayerController);
|
||||
const pc = node.getComponent(PlayerController)!;
|
||||
pc.direction = s.playerDirection ?? Direction.South;
|
||||
} else if (s.kind === 'vehicle') {
|
||||
node = new Node(s.x < 0 ? 'VehicleA1' : 'VehicleB1');
|
||||
node.addComponent(VehicleController);
|
||||
const vc = node.getComponent(VehicleController)!;
|
||||
vc.direction = s.vehicleDirection ?? Direction.North;
|
||||
} else if (s.kind === 'prop') {
|
||||
node = new Node('Prop');
|
||||
node.addComponent(PropController);
|
||||
} else if (s.kind === 'prop_decor') {
|
||||
node = new Node('PropDecor');
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
this.attachVisual(node, s.kind, s.playerDirection, s.vehicleDirection);
|
||||
node.setPosition(pos);
|
||||
const ui = node.getComponent(UITransform) || node.addComponent(UITransform);
|
||||
ui.setContentSize(CELL_PIXEL * 0.9, CELL_PIXEL * 0.9);
|
||||
node.parent = parent;
|
||||
return node;
|
||||
}
|
||||
|
||||
private attachVisual(
|
||||
node: Node,
|
||||
kind: SpawnKind,
|
||||
playerDir?: Direction,
|
||||
vehicleDir?: Direction,
|
||||
) {
|
||||
const dir = kind === 'player' ? playerDir : kind === 'vehicle' ? vehicleDir : undefined;
|
||||
VisualAssets.setupEntityVisual(node, kind, dir);
|
||||
}
|
||||
|
||||
private drawGridDebug(root: Node, config: LevelConfig) {
|
||||
const tiles = new Node('Ground');
|
||||
tiles.parent = root;
|
||||
const bx = config.boundary.x;
|
||||
const by = config.boundary.y;
|
||||
const half = CELL_PIXEL * 0.45;
|
||||
for (let x = -bx + 1; x < bx; x++) {
|
||||
for (let y = -by + 1; y < by; y++) {
|
||||
const key = this.cellKey(x, y);
|
||||
if (this.borderCells.has(key)) continue;
|
||||
const p = this.cellToWorld(new Vec3(x, y, 0));
|
||||
const cell = new Node(`cell_${x}_${y}`);
|
||||
cell.parent = tiles;
|
||||
cell.setPosition(p);
|
||||
const cui = cell.addComponent(UITransform);
|
||||
cui.setContentSize(CELL_PIXEL * 0.9, CELL_PIXEL * 0.9);
|
||||
VisualAssets.applySprite(cell, 'tile', false, 1, 50);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
externalCallLevelInfo(objects: Node[]) {
|
||||
const info: { LevelID: number; PlayerName: string; VehicleName: string } = {
|
||||
LevelID: this.curLevelID,
|
||||
PlayerName: '',
|
||||
VehicleName: '',
|
||||
};
|
||||
for (const obj of objects) {
|
||||
if (this.multMode) {
|
||||
if (obj.name === this.multPlayerRole) info.PlayerName = obj.name;
|
||||
else if (obj.name === this.multPlayerRole.replace('Player', 'Vehicle')) info.VehicleName = obj.name;
|
||||
} else {
|
||||
if (obj.name.includes('Player')) info.PlayerName = obj.name;
|
||||
if (obj.name.includes('Vehicle')) info.VehicleName = obj.name;
|
||||
}
|
||||
}
|
||||
JsBridge.call('externalLevelInfo', JSON.stringify(info));
|
||||
}
|
||||
}
|
||||
9
assets/scripts/manager/GameManager.ts.meta
Normal file
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"ver": "4.0.24",
|
||||
"importer": "typescript",
|
||||
"imported": true,
|
||||
"uuid": "bfc2e3c7-1217-4813-b019-2c0014cb1579",
|
||||
"files": [],
|
||||
"subMetas": {},
|
||||
"userData": {}
|
||||
}
|
||||
9
assets/scripts/ui.meta
Normal file
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"ver": "1.2.0",
|
||||
"importer": "directory",
|
||||
"imported": true,
|
||||
"uuid": "e5583f3f-b1d5-4acb-b0ff-33493e54c023",
|
||||
"files": [],
|
||||
"subMetas": {},
|
||||
"userData": {}
|
||||
}
|
||||
20
assets/scripts/ui/UIMain.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
import { _decorator, Component } from 'cc';
|
||||
import { GameManager } from '../manager/GameManager';
|
||||
|
||||
const { ccclass } = _decorator;
|
||||
|
||||
/** 对应 Unity UIMain,JS 可 SendMessage("UIMain", "SetText", ...) */
|
||||
@ccclass('UIMain')
|
||||
export class UIMain extends Component {
|
||||
private textVisible = false;
|
||||
private textContent = '';
|
||||
|
||||
setText(str: string) {
|
||||
this.textContent = str;
|
||||
console.log('[UIMain]', str);
|
||||
}
|
||||
|
||||
setTextActive(active: string) {
|
||||
this.textVisible = active === 'true';
|
||||
}
|
||||
}
|
||||
9
assets/scripts/ui/UIMain.ts.meta
Normal file
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"ver": "4.0.24",
|
||||
"importer": "typescript",
|
||||
"imported": true,
|
||||
"uuid": "0585296d-ea8c-427b-8cc0-8d89a56719bb",
|
||||
"files": [],
|
||||
"subMetas": {},
|
||||
"userData": {}
|
||||
}
|
||||
9
assets/scripts/visual.meta
Normal file
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"ver": "1.2.0",
|
||||
"importer": "directory",
|
||||
"imported": true,
|
||||
"uuid": "66712587-a8af-4a45-8c43-129ae3bc8ae7",
|
||||
"files": [],
|
||||
"subMetas": {},
|
||||
"userData": {}
|
||||
}
|
||||
147
assets/scripts/visual/VisualAssets.ts
Normal file
@@ -0,0 +1,147 @@
|
||||
import {
|
||||
Node, Sprite, SpriteFrame, UITransform, resources, Color, Graphics,
|
||||
ImageAsset, Texture2D,
|
||||
} from 'cc';
|
||||
import { Direction } from '../core/Define';
|
||||
import { SpawnKind } from '../level/LevelTypes';
|
||||
|
||||
type SpriteKey = 'player_F' | 'player_B' | 'ship_F' | 'ship_B' | 'coin' | 'tile';
|
||||
|
||||
const PATHS: Record<SpriteKey, string> = {
|
||||
player_F: 'textures/silu/player_F',
|
||||
player_B: 'textures/silu/player_B',
|
||||
ship_F: 'textures/silu/ship_F',
|
||||
ship_B: 'textures/silu/ship_B',
|
||||
coin: 'textures/ui/coin',
|
||||
tile: 'textures/silu/Baseblock',
|
||||
};
|
||||
|
||||
export class VisualAssets {
|
||||
private static frames = new Map<SpriteKey, SpriteFrame>();
|
||||
private static loading: Promise<void> | null = null;
|
||||
|
||||
static async preload(): Promise<void> {
|
||||
if (this.loading) return this.loading;
|
||||
this.loading = (async () => {
|
||||
const keys = Object.keys(PATHS) as SpriteKey[];
|
||||
const results = await Promise.all(keys.map((k) => this.loadOne(k)));
|
||||
const ok = results.filter(Boolean).length;
|
||||
console.log(`[VisualAssets] 贴图加载 ${ok}/${keys.length}`);
|
||||
if (ok === 0) {
|
||||
console.warn('[VisualAssets] 未加载到贴图,将使用色块。请确认 assets/resources/textures 已导入');
|
||||
}
|
||||
})().catch((e) => {
|
||||
console.error('[VisualAssets] preload failed', e);
|
||||
this.loading = null;
|
||||
});
|
||||
return this.loading;
|
||||
}
|
||||
|
||||
private static loadOne(key: SpriteKey): Promise<boolean> {
|
||||
if (this.frames.has(key)) return Promise.resolve(true);
|
||||
const base = PATHS[key];
|
||||
return new Promise((resolve) => {
|
||||
resources.load(`${base}/spriteFrame`, SpriteFrame, (err, sf) => {
|
||||
if (!err && sf) {
|
||||
this.frames.set(key, sf);
|
||||
resolve(true);
|
||||
return;
|
||||
}
|
||||
resources.load(base, SpriteFrame, (err2, sf2) => {
|
||||
if (!err2 && sf2) {
|
||||
this.frames.set(key, sf2);
|
||||
resolve(true);
|
||||
return;
|
||||
}
|
||||
resources.load(base, ImageAsset, (err3, img) => {
|
||||
if (!err3 && img) {
|
||||
const tex = new Texture2D();
|
||||
tex.image = img;
|
||||
const frame = new SpriteFrame();
|
||||
frame.texture = tex;
|
||||
this.frames.set(key, frame);
|
||||
resolve(true);
|
||||
} else {
|
||||
console.warn(`[VisualAssets] 加载失败: ${base}`, err3 || err2 || err);
|
||||
resolve(false);
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
static getFrame(key: SpriteKey): SpriteFrame | null {
|
||||
return this.frames.get(key) ?? null;
|
||||
}
|
||||
|
||||
static applyPlayerSprite(node: Node, direction: Direction) {
|
||||
const isFront = direction === Direction.South || direction === Direction.East;
|
||||
const flipX = direction === Direction.West || direction === Direction.East;
|
||||
const key: SpriteKey = isFront ? 'player_F' : 'player_B';
|
||||
this.applySprite(node, key, flipX);
|
||||
}
|
||||
|
||||
static applyVehicleSprite(node: Node, direction: Direction, uiStyle = 'default') {
|
||||
void uiStyle;
|
||||
const isFront = direction === Direction.South || direction === Direction.East;
|
||||
const flipX = direction === Direction.West || direction === Direction.East;
|
||||
const key: SpriteKey = isFront ? 'ship_F' : 'ship_B';
|
||||
this.applySprite(node, key, flipX);
|
||||
}
|
||||
|
||||
static setupEntityVisual(node: Node, kind: SpawnKind, direction?: Direction) {
|
||||
if (kind === 'player') {
|
||||
this.applyPlayerSprite(node, direction ?? Direction.South);
|
||||
return;
|
||||
}
|
||||
if (kind === 'vehicle') {
|
||||
this.applyVehicleSprite(node, direction ?? Direction.North);
|
||||
return;
|
||||
}
|
||||
if (kind === 'prop') {
|
||||
this.applySprite(node, 'coin', false, 0.85);
|
||||
return;
|
||||
}
|
||||
if (kind === 'prop_decor') {
|
||||
this.applySprite(node, 'coin', false, 0.6, 120);
|
||||
}
|
||||
}
|
||||
|
||||
/** Sprite 与 Graphics 不能共存,二选一 */
|
||||
static applySprite(node: Node, key: SpriteKey, flipX: boolean, scale = 1, alpha = 255) {
|
||||
let ui = node.getComponent(UITransform);
|
||||
if (!ui) {
|
||||
ui = node.addComponent(UITransform);
|
||||
ui.setContentSize(48, 48);
|
||||
}
|
||||
|
||||
const sf = this.getFrame(key);
|
||||
const spr = node.getComponent(Sprite);
|
||||
const g = node.getComponent(Graphics);
|
||||
|
||||
if (sf) {
|
||||
if (g) g.destroy();
|
||||
const sprite = spr || node.addComponent(Sprite);
|
||||
sprite.spriteFrame = sf;
|
||||
sprite.sizeMode = Sprite.SizeMode.CUSTOM;
|
||||
const w = ui.contentSize.width * scale;
|
||||
const h = ui.contentSize.height * scale;
|
||||
ui.setContentSize(w, h);
|
||||
sprite.color = new Color(255, 255, 255, alpha);
|
||||
} else {
|
||||
if (spr) spr.destroy();
|
||||
const graphics = g || node.addComponent(Graphics);
|
||||
graphics.fillColor = key === 'coin'
|
||||
? new Color(255, 220, 0, alpha)
|
||||
: new Color(80, 160, 255, alpha);
|
||||
const w = ui.contentSize.width * 0.45 * scale;
|
||||
graphics.clear();
|
||||
graphics.rect(-w, -w, w * 2, w * 2);
|
||||
graphics.fill();
|
||||
}
|
||||
|
||||
const sx = flipX ? -Math.abs(scale) : Math.abs(scale);
|
||||
node.setScale(sx, Math.abs(scale), 1);
|
||||
}
|
||||
}
|
||||
9
assets/scripts/visual/VisualAssets.ts.meta
Normal file
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"ver": "4.0.24",
|
||||
"importer": "typescript",
|
||||
"imported": true,
|
||||
"uuid": "c813fc15-e977-4bb5-9d75-abc460539690",
|
||||
"files": [],
|
||||
"subMetas": {},
|
||||
"userData": {}
|
||||
}
|
||||
16
extensions/game-controller-inspector/package.json
Normal file
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"package_version": 2,
|
||||
"name": "game-controller-inspector",
|
||||
"version": "1.0.0",
|
||||
"description": "GameController Inspector 调试按钮(对齐 Unity TestGame2)",
|
||||
"main": "./dist/main.js",
|
||||
"contributions": {
|
||||
"inspector": {
|
||||
"section": {
|
||||
"node": {
|
||||
"GameController": "./dist/inspector.js"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
83
extensions/game-controller-inspector/source/inspector.ts
Normal file
@@ -0,0 +1,83 @@
|
||||
/**
|
||||
* GameController 自定义 Inspector(需启用扩展 game-controller-inspector)
|
||||
* 构建: 在扩展目录执行 npm run build,或在 Creator 扩展管理器中启用
|
||||
*/
|
||||
|
||||
export const template = `
|
||||
<div class="game-controller-inspector">
|
||||
<ui-prop type="dump" prop="dump"></ui-prop>
|
||||
<ui-section header="关卡调试">
|
||||
<ui-input id="input-level" placeholder="inputLevel"></ui-input>
|
||||
<ui-button id="btn-switch">SwitchLevel</ui-button>
|
||||
<ui-button id="btn-end">EndInput</ui-button>
|
||||
</ui-section>
|
||||
<ui-section header="主题">
|
||||
<ui-input id="input-style" placeholder="inputStyle"></ui-input>
|
||||
<ui-button id="btn-style">ChangeStyle</ui-button>
|
||||
</ui-section>
|
||||
<ui-section header="多人">
|
||||
<ui-input id="input-coins" placeholder='coinStr JSON'></ui-input>
|
||||
<ui-button id="btn-mult">StartMultPlay</ui-button>
|
||||
</ui-section>
|
||||
<ui-section header="音频">
|
||||
<ui-button id="btn-mute">Mute</ui-button>
|
||||
<ui-button id="btn-unmute">Unmute</ui-button>
|
||||
</ui-section>
|
||||
</div>
|
||||
`;
|
||||
|
||||
export const $ = {
|
||||
inputLevel: '#input-level',
|
||||
inputStyle: '#input-style',
|
||||
inputCoins: '#input-coins',
|
||||
btnSwitch: '#btn-switch',
|
||||
btnEnd: '#btn-end',
|
||||
btnStyle: '#btn-style',
|
||||
btnMult: '#btn-mult',
|
||||
btnMute: '#btn-mute',
|
||||
btnUnmute: '#btn-unmute',
|
||||
} as Record<string, string>;
|
||||
|
||||
type Dump = { value: Record<string, { value: unknown }> };
|
||||
|
||||
export function update(this: { dump: Dump; $: Record<string, HTMLElement> }, dump: Dump) {
|
||||
this.dump = dump;
|
||||
const v = dump.value;
|
||||
if (this.$.inputLevel && v.inputLevel) {
|
||||
(this.$.inputLevel as unknown as { value: string }).value = String(v.inputLevel.value ?? '1');
|
||||
}
|
||||
if (this.$.inputStyle && v.inputStyle) {
|
||||
(this.$.inputStyle as unknown as { value: string }).value = String(v.inputStyle.value ?? 'default');
|
||||
}
|
||||
if (this.$.inputCoins && v.coinStr) {
|
||||
(this.$.inputCoins as unknown as { value: string }).value = String(v.coinStr.value ?? '[]');
|
||||
}
|
||||
}
|
||||
|
||||
function callMethod(uuid: string, name: string, args: unknown[] = []) {
|
||||
// @ts-expect-error Editor global
|
||||
if (typeof Editor !== 'undefined' && Editor.Message) {
|
||||
// @ts-expect-error Editor API
|
||||
Editor.Message.request('scene', 'execute-component-method', { uuid, name, args });
|
||||
}
|
||||
}
|
||||
|
||||
export function ready(this: { dump: Dump; $: Record<string, HTMLElement> }) {
|
||||
const uuid = () => this.dump.value.uuid?.value as string;
|
||||
|
||||
this.$.btnSwitch?.addEventListener('confirm', () => {
|
||||
const id = parseInt((this.$.inputLevel as unknown as { value: string }).value || '1', 10);
|
||||
callMethod(uuid(), 'switchLevel', [id]);
|
||||
});
|
||||
this.$.btnEnd?.addEventListener('confirm', () => callMethod(uuid(), 'callSetIsInputEnd', [1]));
|
||||
this.$.btnStyle?.addEventListener('confirm', () => {
|
||||
const s = (this.$.inputStyle as unknown as { value: string }).value || 'default';
|
||||
callMethod(uuid(), 'changeUIStyle', [s]);
|
||||
});
|
||||
this.$.btnMult?.addEventListener('confirm', () => {
|
||||
const coins = (this.$.inputCoins as unknown as { value: string }).value || '[]';
|
||||
callMethod(uuid(), 'startMultPlay', ['PlayerA1', coins]);
|
||||
});
|
||||
this.$.btnMute?.addEventListener('confirm', () => callMethod(uuid(), 'callMute', []));
|
||||
this.$.btnUnmute?.addEventListener('confirm', () => callMethod(uuid(), 'callUnmute', []));
|
||||
}
|
||||
7
package.json
Executable file
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"name": "tfrh001",
|
||||
"uuid": "4683ce98-5131-488a-9c82-647c32645c91",
|
||||
"creator": {
|
||||
"version": "3.8.8"
|
||||
}
|
||||
}
|
||||
3
settings/v2/packages/builder.json
Normal file
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"__version__": "1.3.9"
|
||||
}
|
||||
23
settings/v2/packages/cocos-service.json
Normal file
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"__version__": "3.0.9",
|
||||
"game": {
|
||||
"name": "未知游戏",
|
||||
"app_id": "UNKNOW",
|
||||
"c_id": "0"
|
||||
},
|
||||
"appConfigMaps": [
|
||||
{
|
||||
"app_id": "UNKNOW",
|
||||
"config_id": "38c154"
|
||||
}
|
||||
],
|
||||
"configs": [
|
||||
{
|
||||
"app_id": "UNKNOW",
|
||||
"config_id": "38c154",
|
||||
"config_name": "Default",
|
||||
"config_remarks": "",
|
||||
"services": []
|
||||
}
|
||||
]
|
||||
}
|
||||
3
settings/v2/packages/device.json
Normal file
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"__version__": "1.0.1"
|
||||
}
|
||||
3
settings/v2/packages/engine.json
Normal file
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"__version__": "1.0.12"
|
||||
}
|
||||
23
settings/v2/packages/information.json
Normal file
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"__version__": "1.0.1",
|
||||
"information": {
|
||||
"customSplash": {
|
||||
"id": "customSplash",
|
||||
"label": "customSplash",
|
||||
"enable": true,
|
||||
"customSplash": {
|
||||
"complete": false,
|
||||
"form": "https://creator-api.cocos.com/api/form/show?sid=84b648999874269884ca64920657fb28"
|
||||
}
|
||||
},
|
||||
"removeSplash": {
|
||||
"id": "removeSplash",
|
||||
"label": "removeSplash",
|
||||
"enable": true,
|
||||
"removeSplash": {
|
||||
"complete": false,
|
||||
"form": "https://creator-api.cocos.com/api/form/show?sid=84b648999874269884ca64920657fb28"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
3
settings/v2/packages/program.json
Normal file
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"__version__": "1.0.4"
|
||||
}
|
||||
3
settings/v2/packages/project.json
Normal file
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"__version__": "1.0.6"
|
||||
}
|
||||
4
settings/v2/packages/scene.json
Normal file
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"__version__": "1.0.3",
|
||||
"current-scene": "d071e7d3-2dc4-4815-8cb8-c258c4b7c515"
|
||||
}
|
||||
92
tools/deploy-to-main.sh
Normal file
@@ -0,0 +1,92 @@
|
||||
#!/bin/bash
|
||||
# 将 Cocos Web 构建产物 + cocos-bridge 合并到主站静态目录
|
||||
#
|
||||
# 用法:
|
||||
# ./tools/deploy-to-main.sh \
|
||||
# --build build/web-desktop \
|
||||
# --target "/path/to/主站静态目录/cocos"
|
||||
#
|
||||
# 示例(部署到 Unity 项目 Template 旁,便于本地对比):
|
||||
# ./tools/deploy-to-main.sh \
|
||||
# --build build/web-desktop \
|
||||
# --target "/Users/liuyufei/tfrh/主站文件/主站/Template/cocos"
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||
PROJECT_DIR="$(cd "$SCRIPT_DIR/.." && pwd)"
|
||||
BUILD_DIR=""
|
||||
TARGET_DIR=""
|
||||
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
--build) BUILD_DIR="$2"; shift 2 ;;
|
||||
--target) TARGET_DIR="$2"; shift 2 ;;
|
||||
-h|--help)
|
||||
echo "Usage: $0 --build <build/web-desktop> --target <主站/cocos目录>"
|
||||
exit 0
|
||||
;;
|
||||
*) echo "Unknown arg: $1"; exit 1 ;;
|
||||
esac
|
||||
done
|
||||
|
||||
if [[ -z "$BUILD_DIR" || -z "$TARGET_DIR" ]]; then
|
||||
echo "Error: 需要 --build 和 --target"
|
||||
echo "Example:"
|
||||
echo " $0 --build build/web-desktop --target /path/to/main-site/cocos"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
BUILD_DIR="$(cd "$BUILD_DIR" 2>/dev/null && pwd || true)"
|
||||
if [[ -z "$BUILD_DIR" || ! -f "$BUILD_DIR/index.html" ]]; then
|
||||
echo "Error: 构建目录无效或缺少 index.html: $BUILD_DIR"
|
||||
echo "请先在 Cocos Creator 中执行: 项目 → 构建发布 → Web Desktop"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
mkdir -p "$TARGET_DIR"
|
||||
echo ">>> 复制构建产物: $BUILD_DIR -> $TARGET_DIR"
|
||||
rsync -a --delete \
|
||||
--exclude '.DS_Store' \
|
||||
"$BUILD_DIR/" "$TARGET_DIR/"
|
||||
|
||||
echo ">>> 复制 cocos-bridge.js"
|
||||
cp "$PROJECT_DIR/web-template/cocos-bridge.js" "$TARGET_DIR/../cocos-bridge.js" 2>/dev/null || \
|
||||
cp "$PROJECT_DIR/web-template/cocos-bridge.js" "$TARGET_DIR/cocos-bridge.js"
|
||||
|
||||
# 若 target 是 .../cocos,bridge 放在上一级(与 index 同级)
|
||||
if [[ -f "$TARGET_DIR/cocos-bridge.js" ]]; then
|
||||
BRIDGE_PATH="$TARGET_DIR/cocos-bridge.js"
|
||||
else
|
||||
BRIDGE_PATH="$(dirname "$TARGET_DIR")/cocos-bridge.js"
|
||||
cp "$PROJECT_DIR/web-template/cocos-bridge.js" "$BRIDGE_PATH"
|
||||
fi
|
||||
|
||||
echo ">>> 生成联调页 cocos-demo.html"
|
||||
DEMO="$PROJECT_DIR/web-template/main-site.html"
|
||||
if [[ -f "$DEMO" ]]; then
|
||||
cp "$DEMO" "$(dirname "$TARGET_DIR")/cocos-demo.html" 2>/dev/null || \
|
||||
cp "$DEMO" "$TARGET_DIR/cocos-demo.html"
|
||||
fi
|
||||
|
||||
cat <<EOF
|
||||
|
||||
部署完成。
|
||||
|
||||
目录结构建议:
|
||||
$(dirname "$TARGET_DIR")/
|
||||
cocos-bridge.js ← JS 回调 + unityInstance 兼容
|
||||
cocos/ ← 构建产物(本脚本输出)
|
||||
index.html
|
||||
src/ ...
|
||||
cocos-demo.html ← 可选联调页
|
||||
|
||||
主站 index.html 需:
|
||||
1. 在 <head> 或游戏脚本前引入: <script src="cocos-bridge.js"></script>
|
||||
2. 用 cocos/index.html 中的 script 标签替换原 Unity Build/*.js 段
|
||||
3. 保留 processData / externalResult 等全局函数
|
||||
|
||||
本地预览(需先 cd 到含 cocos 的目录):
|
||||
npx --yes serve "$(dirname "$TARGET_DIR")" -p 8080
|
||||
|
||||
EOF
|
||||
24
tools/export_all.sh
Executable file
@@ -0,0 +1,24 @@
|
||||
#!/bin/bash
|
||||
# 批量导出 Unity Levels*.cs → Cocos generated ts
|
||||
set -e
|
||||
UNITY_ROOT="${1:-/Users/liuyufei/tfrh/主站文件/主站}"
|
||||
OUT_DIR="$(dirname "$0")/../assets/scripts/level"
|
||||
PY="$(dirname "$0")/export_unity_levels.py"
|
||||
|
||||
mkdir -p "$OUT_DIR"
|
||||
|
||||
export_one() {
|
||||
local file="$1"
|
||||
local name="$2"
|
||||
echo "Exporting $file -> levels-${name}.generated.ts"
|
||||
python3 "$PY" \
|
||||
--input "$UNITY_ROOT/Assets/Scripts/Core/${file}" \
|
||||
--output "$OUT_DIR/levels-${name}.generated.ts"
|
||||
}
|
||||
|
||||
export_one "Levels600.cs" "600"
|
||||
# 可按需取消注释:
|
||||
# export_one "Levels1000.cs" "1000"
|
||||
# export_one "Levels10000.cs" "10000"
|
||||
|
||||
echo "Done. Update LevelRegistry.ts to import new LEVELS_* maps."
|
||||
190
tools/export_unity_levels.py
Normal file
@@ -0,0 +1,190 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
从 Unity Levels*.cs 导出 Cocos LevelRegistry 数据。
|
||||
用法:
|
||||
python3 tools/export_unity_levels.py \
|
||||
--input "/path/to/Levels600.cs" \
|
||||
--output assets/scripts/level/levels-600.generated.ts \
|
||||
--border-cache assets/scripts/level/border-cache.json
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import re
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
DIR = {
|
||||
"Direction.North": "Direction.North",
|
||||
"Direction.East": "Direction.East",
|
||||
"Direction.South": "Direction.South",
|
||||
"Direction.West": "Direction.West",
|
||||
}
|
||||
|
||||
LEVEL_RE = re.compile(
|
||||
r"\{(\d+),new Level\(\)\{LevelID\s*=\s*\d+,spawns\s*=\s*new List<Spawn>\(\)\{",
|
||||
re.MULTILINE,
|
||||
)
|
||||
|
||||
SPAWN_RE = re.compile(
|
||||
r"new Spawn\(\)\{([^}]+(?:\{[^}]*\})?)\}",
|
||||
re.MULTILINE,
|
||||
)
|
||||
|
||||
POS_RE = re.compile(r"position\s*=\s*new Vector3Int\((-?\d+),(-?\d+),0\)")
|
||||
PATH_RE = re.compile(r'path\s*=\s*"?([^",\s]+)"?')
|
||||
PDIR_RE = re.compile(r"playerDirection\s*=\s*(Direction\.\w+)")
|
||||
VDIR_RE = re.compile(r"vehicleDirection\s*=\s*(Direction\.\w+)")
|
||||
BOUND_RE = re.compile(r"boundary\s*=\s*new Vector3Int\((\d+),(\d+),0\)")
|
||||
|
||||
|
||||
def kind_from_path(path: str) -> str:
|
||||
p = path.lower()
|
||||
if "player" in p and "multplay" not in p:
|
||||
return "player"
|
||||
if "vehicle" in p:
|
||||
return "vehicle"
|
||||
if "enemy" in p:
|
||||
return "enemy"
|
||||
if "nprop" in p:
|
||||
return "prop_decor"
|
||||
if "prop" in p:
|
||||
return "prop"
|
||||
return "prop_decor"
|
||||
|
||||
|
||||
def parse_spawns(block: str) -> list[dict]:
|
||||
spawns = []
|
||||
for m in SPAWN_RE.finditer(block):
|
||||
body = m.group(1)
|
||||
pm = POS_RE.search(body)
|
||||
if not pm:
|
||||
continue
|
||||
path_m = PATH_RE.search(body)
|
||||
path = path_m.group(1) if path_m else ""
|
||||
item: dict = {
|
||||
"x": int(pm.group(1)),
|
||||
"y": int(pm.group(2)),
|
||||
"kind": kind_from_path(path),
|
||||
}
|
||||
pdir = PDIR_RE.search(body)
|
||||
vdir = VDIR_RE.search(body)
|
||||
if pdir:
|
||||
item["playerDirection"] = pdir.group(1)
|
||||
if vdir:
|
||||
item["vehicleDirection"] = vdir.group(1)
|
||||
spawns.append(item)
|
||||
return spawns
|
||||
|
||||
|
||||
def ring_border_key(bx: int, by: int) -> str:
|
||||
return f"{bx},{by}"
|
||||
|
||||
|
||||
def parse_file(text: str) -> dict[int, dict]:
|
||||
levels: dict[int, dict] = {}
|
||||
matches = list(LEVEL_RE.finditer(text))
|
||||
for i, m in enumerate(matches):
|
||||
lid = int(m.group(1))
|
||||
start = m.end()
|
||||
end = matches[i + 1].start() if i + 1 < len(matches) else len(text)
|
||||
chunk = text[start:end]
|
||||
bound = BOUND_RE.search(chunk)
|
||||
bx, by = (10, 10)
|
||||
if bound:
|
||||
bx, by = int(bound.group(1)), int(bound.group(2))
|
||||
spawns = parse_spawns(chunk)
|
||||
levels[lid] = {
|
||||
"levelID": lid,
|
||||
"boundary": {"x": bx, "y": by},
|
||||
"borderKey": ring_border_key(bx, by),
|
||||
"spawns": spawns,
|
||||
}
|
||||
return levels
|
||||
|
||||
|
||||
def spawn_to_ts(s: dict, indent: str) -> str:
|
||||
parts = [
|
||||
f"x: {s['x']}",
|
||||
f"y: {s['y']}",
|
||||
f"kind: '{s['kind']}'",
|
||||
]
|
||||
if "playerDirection" in s:
|
||||
parts.append(f"playerDirection: {s['playerDirection']}")
|
||||
if "vehicleDirection" in s:
|
||||
parts.append(f"vehicleDirection: {s['vehicleDirection']}")
|
||||
return indent + "{ " + ", ".join(parts) + " }"
|
||||
|
||||
|
||||
def emit_ts(levels: dict[int, dict], border_cache: dict[str, dict]) -> str:
|
||||
lines = [
|
||||
"/* AUTO-GENERATED by tools/export_unity_levels.py — DO NOT EDIT */",
|
||||
"import { Direction } from '../core/Define';",
|
||||
"import { LevelConfig, SpawnConfig } from './LevelTypes';",
|
||||
"",
|
||||
"const BORDER_CACHE: Record<string, Record<string, boolean>> = " + json.dumps(border_cache, separators=(',', ':')) + ";",
|
||||
"",
|
||||
"function withBorder(key: string, cfg: Omit<LevelConfig, 'border'>): LevelConfig {",
|
||||
" return { ...cfg, border: BORDER_CACHE[key] };",
|
||||
"}",
|
||||
"",
|
||||
"export const LEVELS_600: Record<number, LevelConfig> = {",
|
||||
]
|
||||
for lid in sorted(levels.keys()):
|
||||
L = levels[lid]
|
||||
lines.append(f" {lid}: withBorder('{L['borderKey']}', {{")
|
||||
lines.append(f" levelID: {lid},")
|
||||
lines.append(f" boundary: {{ x: {L['boundary']['x']}, y: {L['boundary']['y']} }},")
|
||||
lines.append(" spawns: [")
|
||||
for s in L["spawns"]:
|
||||
lines.append(spawn_to_ts(s, " ") + ",")
|
||||
lines.append(" ],")
|
||||
lines.append(" }),")
|
||||
lines.append("};")
|
||||
lines.append("")
|
||||
return "\n".join(lines)
|
||||
|
||||
|
||||
def build_border_cache(levels: dict[int, dict]) -> dict[str, dict]:
|
||||
cache: dict[str, dict] = {}
|
||||
for L in levels.values():
|
||||
key = L["borderKey"]
|
||||
if key in cache:
|
||||
continue
|
||||
bx, by = L["boundary"]["x"], L["boundary"]["y"]
|
||||
border: dict[str, bool] = {}
|
||||
for x in range(-bx, bx + 1):
|
||||
for y in range(-by, by + 1):
|
||||
if abs(x) == bx or abs(y) == by:
|
||||
border[f"{x},{y}"] = True
|
||||
cache[key] = border
|
||||
return cache
|
||||
|
||||
|
||||
def main():
|
||||
ap = argparse.ArgumentParser()
|
||||
ap.add_argument("--input", required=True, help="Unity Levels*.cs path")
|
||||
ap.add_argument("--output", required=True, help="Output .ts path")
|
||||
ap.add_argument("--border-cache", default="", help="Optional border cache json")
|
||||
args = ap.parse_args()
|
||||
|
||||
text = Path(args.input).read_text(encoding="utf-8")
|
||||
levels = parse_file(text)
|
||||
if not levels:
|
||||
print("No levels parsed", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
border_cache = build_border_cache(levels)
|
||||
out = Path(args.output)
|
||||
out.parent.mkdir(parents=True, exist_ok=True)
|
||||
out.write_text(emit_ts(levels, border_cache), encoding="utf-8")
|
||||
|
||||
if args.border_cache:
|
||||
Path(args.border_cache).write_text(json.dumps(border_cache, indent=2), encoding="utf-8")
|
||||
|
||||
print(f"Exported {len(levels)} levels -> {out}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
55
tools/patch-main-index.js
Normal file
@@ -0,0 +1,55 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* 根据 Cocos 构建出的 index.html,生成可嵌入主站的片段说明
|
||||
*
|
||||
* node tools/patch-main-index.js build/web-desktop
|
||||
*/
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
const buildDir = process.argv[2];
|
||||
if (!buildDir) {
|
||||
console.error('Usage: node patch-main-index.js <build/web-desktop>');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const indexPath = path.join(buildDir, 'index.html');
|
||||
if (!fs.existsSync(indexPath)) {
|
||||
console.error('Not found:', indexPath);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const html = fs.readFileSync(indexPath, 'utf8');
|
||||
|
||||
// 提取所有 script 标签(Cocos 启动链)
|
||||
const scripts = [];
|
||||
const re = /<script[^>]*src=["']([^"']+)["'][^>]*><\/script>/gi;
|
||||
let m;
|
||||
while ((m = re.exec(html)) !== null) {
|
||||
scripts.push(m[1]);
|
||||
}
|
||||
|
||||
// 提取内联 script(System.import 等)
|
||||
const inline = html.match(/<script>([\s\S]*?)<\/script>/gi) || [];
|
||||
|
||||
console.log('=== 复制到主站 index.html(替换 Unity Build 段)===\n');
|
||||
console.log('<!-- ① 主站公共:放在 head 或 body 开头 -->');
|
||||
console.log('<script src="cocos-bridge.js"></script>\n');
|
||||
console.log('<!-- ② Cocos 启动脚本(路径相对主站页面,若游戏在 /cocos/ 子目录则加前缀 cocos/) -->');
|
||||
for (const src of scripts) {
|
||||
const rel = src.startsWith('http') ? src : `cocos/${src.replace(/^\.\//, '')}`;
|
||||
console.log(`<script src="${rel}" charset="utf-8"></script>`);
|
||||
}
|
||||
console.log('\n<!-- ③ 容器(若主站已有 #GameDiv 可保留原 id) -->');
|
||||
const canvasMatch = html.match(/<canvas[^>]*id=["']([^"']+)["'][^>]*>/i);
|
||||
const divMatch = html.match(/id=["'](GameDiv|Cocos3dGameContainer)["']/i);
|
||||
console.log('<!-- Cocos 构建页中的 canvas id:', canvasMatch ? canvasMatch[1] : 'GameCanvas', '-->');
|
||||
|
||||
if (inline.length) {
|
||||
console.log('\n<!-- ④ 内联启动(从 build/index.html 复制以下 block) -->');
|
||||
inline.forEach((block) => console.log(block));
|
||||
}
|
||||
|
||||
console.log('\n=== 删除原 Unity 段 ===');
|
||||
console.log('- Build/build.loader.js');
|
||||
console.log('- createUnityInstance(...)');
|
||||
30
tools/unity-level-export.md
Normal file
@@ -0,0 +1,30 @@
|
||||
# Unity 关卡迁移到 Cocos
|
||||
|
||||
Unity 原项目约 1.1 万个 `Level*.prefab`,Cocos 版使用 **JSON/TS 配置** 代替 Tilemap Prefab。
|
||||
|
||||
## 配置格式
|
||||
|
||||
```typescript
|
||||
registerLevel({
|
||||
levelID: 601,
|
||||
boundary: { x: 20, y: 20 },
|
||||
border: { "10,0": true, ... }, // 可选
|
||||
ground: { "0,0": "Baseblock", "1,0": "JumpBlock" }, // 可选
|
||||
spawns: [
|
||||
{ x: 0, y: 0, kind: 'player', playerDirection: 0 },
|
||||
{ x: 3, y: 0, kind: 'prop' },
|
||||
],
|
||||
});
|
||||
```
|
||||
|
||||
## 批量迁移步骤
|
||||
|
||||
1. 从 `Assets/Scripts/Core/Levels*.cs` 提取 `LevelID`、`spawns`、`boundary`
|
||||
2. 若有 Tiled 导出或 Tilemap 脚本,生成 `ground` / `border` 稀疏表
|
||||
3. 写入 `assets/scripts/level/LevelRegistry.ts` 或独立 `assets/resources/levels/{id}.json`
|
||||
4. 在 `LevelRegistry.ts` 的 `getLevelConfig` 中合并 JSON 加载
|
||||
|
||||
## 资源迁移
|
||||
|
||||
- 角色/载具贴图:放入 `assets/resources/textures/`,替换 `GameManager.attachVisual` 为 Sprite 渲染
|
||||
- 音频:放入 `assets/resources/audio/`
|
||||
9
tsconfig.json
Normal file
@@ -0,0 +1,9 @@
|
||||
{
|
||||
/* Base configuration. Do not edit this field. */
|
||||
"extends": "./temp/tsconfig.cocos.json",
|
||||
|
||||
/* Add your custom configuration here. */
|
||||
"compilerOptions": {
|
||||
"strict": false
|
||||
}
|
||||
}
|
||||
43
web-template/cocos-bridge.js
Normal file
@@ -0,0 +1,43 @@
|
||||
/**
|
||||
* 主站页面对接层:在 Cocos 启动后提供与 Unity WebGL 相同的 unityInstance.SendMessage API
|
||||
* 用法:构建产物 index.html 末尾引入本脚本,或在主站模板中引入
|
||||
*/
|
||||
(function (global) {
|
||||
function processData(json) { global.__tfrhOnProcessData?.(json); console.log('[processData]', JSON.parse(json)); }
|
||||
function processVehicleData(json) { global.__tfrhOnProcessVehicleData?.(json); console.log('[processVehicleData]', JSON.parse(json)); }
|
||||
function externalResult(json) { global.__tfrhOnExternalResult?.(json); console.log('[externalResult]', JSON.parse(json)); }
|
||||
function externalLevelInfo(json) { global.__tfrhOnExternalLevelInfo?.(json); console.log('[externalLevelInfo]', JSON.parse(json)); }
|
||||
function coinsData(json) { global.__tfrhOnCoinsData?.(json); console.log('[coinsData]', JSON.parse(json)); }
|
||||
|
||||
global.processData = processData;
|
||||
global.processVehicleData = processVehicleData;
|
||||
global.externalResult = externalResult;
|
||||
global.externalLevelInfo = externalLevelInfo;
|
||||
global.coinsData = coinsData;
|
||||
|
||||
// 动态 processPlayerA1 等
|
||||
['A1','A2','A3','B1','B2','B3'].forEach(function (id) {
|
||||
global['processPlayer' + id] = function (json) { console.log('[processPlayer' + id + ']', JSON.parse(json)); };
|
||||
global['processVehicle' + id] = function (json) { console.log('[processVehicle' + id + ']', JSON.parse(json)); };
|
||||
});
|
||||
|
||||
function waitInstance(maxMs) {
|
||||
maxMs = maxMs || 30000;
|
||||
return new Promise(function (resolve, reject) {
|
||||
var t0 = Date.now();
|
||||
(function tick() {
|
||||
if (global.cocosIns || global.unityInstance) return resolve(global.cocosIns || global.unityInstance);
|
||||
if (Date.now() - t0 > maxMs) return reject(new Error('Cocos instance timeout'));
|
||||
requestAnimationFrame(tick);
|
||||
})();
|
||||
});
|
||||
}
|
||||
|
||||
global.tfrhReady = waitInstance;
|
||||
|
||||
document.addEventListener('DOMContentLoaded', function () {
|
||||
waitInstance().then(function () {
|
||||
console.log('[tfrh] Cocos bridge ready, use unityInstance.SendMessage(...)');
|
||||
}).catch(function (e) { console.warn('[tfrh]', e.message); });
|
||||
});
|
||||
})(typeof window !== 'undefined' ? window : globalThis);
|
||||
24
web-template/index.html
Normal file
@@ -0,0 +1,24 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<title>主站 Cocos Web</title>
|
||||
<style>
|
||||
html, body { margin: 0; padding: 0; height: 100%; background: #1a1a2e; }
|
||||
#GameDiv { width: 100%; height: 100%; }
|
||||
#bar { color: #fff; font-family: sans-serif; padding: 8px; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div id="bar">加载中…</div>
|
||||
<div id="GameDiv"></div>
|
||||
<script>
|
||||
function processData(json) { console.log('processData', JSON.parse(json)); }
|
||||
function processVehicleData(json) { console.log('processVehicleData', JSON.parse(json)); }
|
||||
function externalResult(json) { console.log('externalResult', JSON.parse(json)); }
|
||||
function externalLevelInfo(json) { console.log('externalLevelInfo', JSON.parse(json)); }
|
||||
function coinsData(json) { console.log('coinsData', JSON.parse(json)); }
|
||||
</script>
|
||||
<!-- 构建后将 Cocos 生成的 index.js 引入此页 -->
|
||||
</body>
|
||||
</html>
|
||||
77
web-template/main-site.html
Normal file
@@ -0,0 +1,77 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no" />
|
||||
<title>主站 · Cocos 关卡引擎</title>
|
||||
<style>
|
||||
* { box-sizing: border-box; }
|
||||
html, body { margin: 0; height: 100%; background: #1a1a2e; font-family: system-ui, sans-serif; }
|
||||
#layout { display: flex; flex-direction: column; height: 100%; }
|
||||
#toolbar { padding: 8px 12px; background: #16213e; color: #eee; display: flex; gap: 8px; flex-wrap: wrap; align-items: center; }
|
||||
#toolbar button { padding: 6px 12px; cursor: pointer; border: none; border-radius: 4px; background: #0f3460; color: #fff; }
|
||||
#toolbar button:hover { background: #e94560; }
|
||||
#game-wrap { flex: 1; position: relative; min-height: 0; }
|
||||
#GameDiv, #Cocos3dGameContainer, #GameCanvas { width: 100% !important; height: 100% !important; }
|
||||
#log { height: 120px; overflow: auto; background: #0d1117; color: #8b949e; font: 12px/1.4 monospace; padding: 8px; }
|
||||
</style>
|
||||
<!-- ① 先挂回调(与 Unity Template/index.html 一致) -->
|
||||
<script src="cocos-bridge.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<div id="layout">
|
||||
<div id="toolbar">
|
||||
<strong>主站 Cocos 联调</strong>
|
||||
<button type="button" id="btn-l1">关卡 1</button>
|
||||
<button type="button" id="btn-l21">关卡 21(载具)</button>
|
||||
<button type="button" id="btn-move">前进 2 格</button>
|
||||
<button type="button" id="btn-info">获取坐标</button>
|
||||
<button type="button" id="btn-end">结束输入</button>
|
||||
</div>
|
||||
<div id="game-wrap">
|
||||
<!-- ② Cocos 构建后替换为 build 目录中的容器,常见为 #GameDiv -->
|
||||
<div id="GameDiv">
|
||||
<div id="Cocos3dGameContainer">
|
||||
<canvas id="GameCanvas" tabindex="0"></canvas>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<pre id="log"></pre>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
const logEl = document.getElementById('log');
|
||||
function log(msg) {
|
||||
logEl.textContent += msg + '\n';
|
||||
logEl.scrollTop = logEl.scrollHeight;
|
||||
}
|
||||
|
||||
window.__tfrhOnExternalLevelInfo = (j) => log('关卡就绪: ' + j);
|
||||
window.__tfrhOnProcessData = (j) => log('坐标: ' + j);
|
||||
window.__tfrhOnExternalResult = (j) => log('结果: ' + j);
|
||||
|
||||
function send(obj, method, arg) {
|
||||
const ins = window.unityInstance || window.cocosIns;
|
||||
if (!ins) { log('实例未就绪'); return; }
|
||||
if (arg === undefined) ins.SendMessage(obj, method);
|
||||
else ins.SendMessage(obj, method, arg);
|
||||
}
|
||||
|
||||
document.getElementById('btn-l1').onclick = () => send('GameController', 'SwitchLevel', 1);
|
||||
document.getElementById('btn-l21').onclick = () => send('GameController', 'SwitchLevel', 21);
|
||||
document.getElementById('btn-move').onclick = () => send('Player', 'CallMove', 2);
|
||||
document.getElementById('btn-info').onclick = () => send('Player', 'CallPlayerInfo');
|
||||
document.getElementById('btn-end').onclick = () => send('GameController', 'CallSetIsInputEnd', 1);
|
||||
</script>
|
||||
|
||||
<!-- ③ 在 Cocos 构建输出的 index.html 中,将 systemjs / index.js 等脚本复制到本页 GameDiv 之后 -->
|
||||
<!-- 示例(路径按实际构建目录修改):
|
||||
<script src="src/polyfills.bundle.js"></script>
|
||||
<script src="src/system.bundle.js"></script>
|
||||
<script src="src/import-map.json" type="systemjs-importmap"></script>
|
||||
<script>
|
||||
System.import('./index.js').catch(function(e){ console.error(e); });
|
||||
</script>
|
||||
-->
|
||||
</body>
|
||||
</html>
|
||||
204
web-template/主站对接说明.md
Normal file
@@ -0,0 +1,204 @@
|
||||
# 主站 Web 对接完整步骤(Cocos tfrh001)
|
||||
|
||||
## 一、在 Cocos Creator 中构建 Web Desktop
|
||||
|
||||
### 1. 打开工程
|
||||
|
||||
```
|
||||
/Users/liuyufei/tfrh/cocos/tfrh001
|
||||
```
|
||||
|
||||
确认已存在 `assets/scenes/main.scene`,且节点上挂了 **AppBootstrap**。
|
||||
|
||||
### 2. 设置启动场景
|
||||
|
||||
**项目 → 项目设置 → 项目数据 → 启动场景** → 选择 `main`。
|
||||
|
||||
### 3. 构建发布
|
||||
|
||||
1. 菜单 **项目 → 构建发布**(或顶部 **构建** 按钮)
|
||||
2. **发布平台**:选 **Web Desktop**(不要选 Web Mobile,除非只做手机)
|
||||
3. **构建路径**:默认 `build/web-desktop`(可保持默认)
|
||||
4. **初始场景**:`main`
|
||||
5. 建议勾选:
|
||||
- **内联所有 SpriteFrame**(减少请求数,可选)
|
||||
- **MD5 Cache**(按需)
|
||||
6. 点击 **构建**
|
||||
7. 构建成功后点击 **运行** 可先本地测一遍
|
||||
|
||||
### 4. 构建产物位置
|
||||
|
||||
```
|
||||
tfrh001/build/web-desktop/
|
||||
├── index.html ← 含 Cocos 启动 script 列表
|
||||
├── index.js
|
||||
├── src/
|
||||
│ ├── system.bundle.js
|
||||
│ ├── polyfills.bundle.js
|
||||
│ └── ...
|
||||
├── assets/ ← 资源
|
||||
└── ...
|
||||
```
|
||||
|
||||
> 若 `build/web-desktop` 不存在,说明尚未在编辑器里点过「构建」。
|
||||
|
||||
---
|
||||
|
||||
## 二、合并到主站静态目录
|
||||
|
||||
### 方式 A:自动脚本(推荐)
|
||||
|
||||
在终端执行(先完成上一节构建):
|
||||
|
||||
```bash
|
||||
cd /Users/liuyufei/tfrh/cocos/tfrh001
|
||||
|
||||
# 部署到主站 Template 下(与 Unity 模板并列,便于对比)
|
||||
bash tools/deploy-to-main.sh \
|
||||
--build build/web-desktop \
|
||||
--target "/Users/liuyufei/tfrh/主站文件/主站/Template/cocos"
|
||||
```
|
||||
|
||||
部署后目录:
|
||||
|
||||
```
|
||||
主站/Template/
|
||||
├── index.html ← 原 Unity 页(参考)
|
||||
├── cocos-bridge.js ← 新增:JS 桥
|
||||
├── cocos-demo.html ← 新增:联调页
|
||||
└── cocos/ ← Cocos 构建产物
|
||||
├── index.html
|
||||
└── src/ ...
|
||||
```
|
||||
|
||||
### 方式 B:手动复制
|
||||
|
||||
| 复制源 | 复制到 |
|
||||
|--------|--------|
|
||||
| `build/web-desktop/*` | 主站静态目录下的 `cocos/` |
|
||||
| `web-template/cocos-bridge.js` | 与主站 `index.html` **同级** |
|
||||
|
||||
---
|
||||
|
||||
## 三、修改主站 index.html
|
||||
|
||||
### 1. 引入 cocos-bridge(在 Unity 脚本之前)
|
||||
|
||||
```html
|
||||
<script src="cocos-bridge.js"></script>
|
||||
```
|
||||
|
||||
`cocos-bridge.js` 会注册:
|
||||
|
||||
- `processData` / `processVehicleData` / `externalResult` / `externalLevelInfo` / `coinsData`
|
||||
- 等待 `window.unityInstance` 就绪
|
||||
|
||||
### 2. 删除 Unity 加载段
|
||||
|
||||
删除类似内容:
|
||||
|
||||
```html
|
||||
<script src="Build/build.loader.js"></script>
|
||||
<script>
|
||||
createUnityInstance(canvas, config, ...).then((unityInstance) => {
|
||||
window.unityInstance = unityInstance;
|
||||
});
|
||||
</script>
|
||||
```
|
||||
|
||||
### 3. 粘贴 Cocos 启动脚本
|
||||
|
||||
构建完成后运行(自动生成粘贴清单):
|
||||
|
||||
```bash
|
||||
node tools/patch-main-index.js build/web-desktop
|
||||
```
|
||||
|
||||
将输出的 `<script src="cocos/...">` 按主站实际路径写入 `index.html`。
|
||||
|
||||
**路径规则**:
|
||||
|
||||
- 主站页面与 `cocos/` 同级 → `src="cocos/src/system.bundle.js"`
|
||||
- 游戏在根目录 → 不加 `cocos/` 前缀
|
||||
|
||||
### 4. 游戏容器
|
||||
|
||||
保留或添加 Cocos 构建页中的结构:
|
||||
|
||||
```html
|
||||
<div id="GameDiv">
|
||||
<div id="Cocos3dGameContainer">
|
||||
<canvas id="GameCanvas"></canvas>
|
||||
</div>
|
||||
</div>
|
||||
```
|
||||
|
||||
若主站原用 `#unity-canvas`,可只改 id 为 `GameCanvas`,或保留原 id 并在构建模板里改 canvas id(需改构建配置,一般直接用 Cocos 默认即可)。
|
||||
|
||||
### 5. unityInstance 无需手写
|
||||
|
||||
游戏启动后 `GameController` 会自动执行:
|
||||
|
||||
```javascript
|
||||
window.unityInstance = { SendMessage: ... };
|
||||
window.cocosIns = 同上;
|
||||
```
|
||||
|
||||
主站 Blockly 代码 **不用改**:
|
||||
|
||||
```javascript
|
||||
unityInstance.SendMessage("Player", "CallMove", n);
|
||||
unityInstance.SendMessage("GameController", "SwitchLevel", levelId);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 四、本地预览
|
||||
|
||||
### 仅预览 Cocos 构建(未合并主站)
|
||||
|
||||
用 Cocos 构建面板里的 **运行**,或:
|
||||
|
||||
```bash
|
||||
cd build/web-desktop
|
||||
npx --yes serve . -p 8080
|
||||
# 打开 http://localhost:8080
|
||||
```
|
||||
|
||||
### 预览合并后的主站目录
|
||||
|
||||
```bash
|
||||
cd "/Users/liuyufei/tfrh/主站文件/主站/Template"
|
||||
npx --yes serve . -p 8080
|
||||
```
|
||||
|
||||
- 联调页:`http://localhost:8080/cocos-demo.html`
|
||||
- 若已改主站 index:`http://localhost:8080/index.html`
|
||||
|
||||
---
|
||||
|
||||
## 五、联调自检清单
|
||||
|
||||
| 步骤 | 期望 |
|
||||
|------|------|
|
||||
| 打开页面 | 控制台无 404(检查 cocos/*.js 路径) |
|
||||
| 加载完成 | `[GameController] unityInstance.SendMessage 已就绪` |
|
||||
| | `[tfrh] Cocos bridge ready` |
|
||||
| `SwitchLevel(1)` | 日志 `externalLevelInfo` |
|
||||
| `CallMove(2)` | 角色移动 |
|
||||
| `CallPlayerInfo` | 日志 `processData` |
|
||||
|
||||
---
|
||||
|
||||
## 六、生产环境注意
|
||||
|
||||
1. **HTTPS**:与 Unity 相同,WebGL/Canvas 在混合内容下可能受限
|
||||
2. **缓存**:构建带 MD5 时,更新后清 CDN 缓存
|
||||
3. **跨域**:`cocos/` 与主站同域部署最省事
|
||||
4. **回退**:可保留 Unity `Build/` 目录,通过配置开关切换加载 Unity 或 Cocos
|
||||
|
||||
---
|
||||
|
||||
## 七、关卡
|
||||
|
||||
已导入 Levels600 共 **600** 关,`SwitchLevel(1..600)` 与 Unity 一致。
|
||||