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>
This commit is contained in:
92
tools/deploy-to-main.sh
Normal file
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
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
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
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
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/`
|
||||
Reference in New Issue
Block a user