Adds level prefabs, theme assets, audio, extensions, and deployment scripts for the Unity WebGL migration. Co-authored-by: Cursor <cursoragent@cursor.com>
357 lines
11 KiB
Python
357 lines
11 KiB
Python
#!/usr/bin/env python3
|
||
"""
|
||
从 Unity Assets/Texture 导入 PNG 到 Cocos assets/resources/textures/
|
||
|
||
用法:
|
||
python3 tools/import_unity_textures.py \\
|
||
--unity-root "/path/to/主站" \\
|
||
[--themes silu,snow,default]
|
||
"""
|
||
from __future__ import annotations
|
||
|
||
import argparse
|
||
import json
|
||
import shutil
|
||
import uuid
|
||
from pathlib import Path
|
||
|
||
# 与 GameController UIStyleNames 对应(Unity 目录名 → Cocos 子目录)
|
||
THEME_MAP = {
|
||
"default": ".", # Assets/Texture 根目录单层 png
|
||
"silu": "silu",
|
||
"chinese": "Chinese",
|
||
"redArmy": "redarmy",
|
||
"numMan": "numMan",
|
||
"snow": "snow",
|
||
"sanxing": "sanxing",
|
||
}
|
||
|
||
# 角色 + 砖块核心文件名(各主题有则拷贝,无则跳过)
|
||
CORE_NAMES = {
|
||
"Baseblock.png",
|
||
"JumpBlock.png",
|
||
"WallBlock.png",
|
||
"player_F.png",
|
||
"player_B.png",
|
||
"ship_F.png",
|
||
"ship_B.png",
|
||
"Ship_F.png",
|
||
"Prop.png",
|
||
"nProp.png",
|
||
"kuai11.png",
|
||
"素材切图-23.png",
|
||
"素材切图2-23.png",
|
||
"Prop_kuai1.png",
|
||
"Prop_kuai2.png",
|
||
"Prop_kuai.png",
|
||
"nProp_kuai1.png",
|
||
"nProp_kuai2.png",
|
||
"nProp_kuai.png",
|
||
"Prop_Dumpling.png",
|
||
"Prop_Mooncake.png",
|
||
"Prop_Ricedumpling.png",
|
||
"Prop_Zhongzi.png",
|
||
"Prop_star.png",
|
||
"nProp_star.png",
|
||
"nProp_Dumpling.png",
|
||
"nProp_Mooncake.png",
|
||
"nProp_Ricedumpling.png",
|
||
"nProp_Zhongzi.png",
|
||
}
|
||
|
||
|
||
# 丝路:对齐 sanxing/snow 命名(skin/待机正面、*Ship_F、Prop_kuai1)
|
||
SILU_CANONICAL_COPIES: list[tuple[str, str]] = [
|
||
("ship_F.png", "siluShip_F.png"),
|
||
("ship_B.png", "siluShip_B.png"),
|
||
("Prop.png", "Prop_kuai1.png"),
|
||
("nProp.png", "nProp_kuai1.png"),
|
||
]
|
||
|
||
|
||
def ensure_directory_meta(meta_path: Path) -> None:
|
||
if meta_path.is_file():
|
||
return
|
||
meta_path.write_text(
|
||
json.dumps(
|
||
{
|
||
"ver": "1.2.0",
|
||
"importer": "directory",
|
||
"imported": True,
|
||
"uuid": str(uuid.uuid4()),
|
||
"files": [],
|
||
"subMetas": {},
|
||
"userData": {},
|
||
},
|
||
indent=2,
|
||
),
|
||
encoding="utf-8",
|
||
)
|
||
|
||
|
||
def ensure_image_meta(meta_path: Path, display_name: str) -> None:
|
||
if meta_path.is_file():
|
||
return
|
||
base_uuid = str(uuid.uuid4())
|
||
meta_path.write_text(
|
||
json.dumps(
|
||
{
|
||
"ver": "1.0.27",
|
||
"importer": "image",
|
||
"imported": False,
|
||
"uuid": base_uuid,
|
||
"files": [".json", ".png"],
|
||
"subMetas": {
|
||
"6c48a": {
|
||
"importer": "texture",
|
||
"uuid": f"{base_uuid}@6c48a",
|
||
"displayName": display_name,
|
||
"id": "6c48a",
|
||
"name": "texture",
|
||
"userData": {
|
||
"imageUuidOrDatabaseUri": base_uuid,
|
||
"isUuid": True,
|
||
"visible": False,
|
||
},
|
||
"ver": "1.0.22",
|
||
"imported": False,
|
||
"files": [".json"],
|
||
"subMetas": {},
|
||
}
|
||
},
|
||
"userData": {"type": "texture", "redirect": f"{base_uuid}@6c48a"},
|
||
},
|
||
indent=2,
|
||
),
|
||
encoding="utf-8",
|
||
)
|
||
|
||
|
||
def ensure_png_meta(png_path: Path) -> None:
|
||
ensure_image_meta(png_path.with_suffix(".png.meta"), png_path.stem)
|
||
|
||
|
||
def first_existing_file(*candidates: Path) -> Path | None:
|
||
for path in candidates:
|
||
if path.is_file():
|
||
return path
|
||
return None
|
||
|
||
|
||
def copy_skin_tree(src_skin: Path, dst_skin: Path) -> None:
|
||
"""将 player/skin 整树复制到主题根 skin(对齐 sanxing 目录结构)。"""
|
||
if not src_skin.is_dir():
|
||
return
|
||
dst_skin.mkdir(parents=True, exist_ok=True)
|
||
ensure_directory_meta(dst_skin.parent / f"{dst_skin.name}.meta")
|
||
for src in sorted(src_skin.rglob("*")):
|
||
rel = src.relative_to(src_skin)
|
||
dst = dst_skin / rel
|
||
if src.is_dir():
|
||
dst.mkdir(parents=True, exist_ok=True)
|
||
ensure_directory_meta(dst.parent / f"{dst.name}.meta")
|
||
continue
|
||
if src.suffix.lower() != ".png":
|
||
continue
|
||
dst.parent.mkdir(parents=True, exist_ok=True)
|
||
shutil.copy2(src, dst)
|
||
ensure_png_meta(dst)
|
||
|
||
|
||
def normalize_silu_assets(theme_dir: Path) -> None:
|
||
"""拷贝丝路实体贴图为标准文件名(保留 Unity 原名,额外写规范别名)。"""
|
||
skin_root = theme_dir / "skin"
|
||
player_skin = theme_dir / "player" / "skin"
|
||
if player_skin.is_dir():
|
||
copy_skin_tree(player_skin, skin_root)
|
||
|
||
skin_front = skin_root / "待机正面"
|
||
skin_back = skin_root / "待机背面"
|
||
skin_front.mkdir(parents=True, exist_ok=True)
|
||
skin_back.mkdir(parents=True, exist_ok=True)
|
||
ensure_directory_meta(skin_root.parent / f"{skin_root.name}.meta")
|
||
ensure_directory_meta(skin_front.parent / f"{skin_front.name}.meta")
|
||
ensure_directory_meta(skin_back.parent / f"{skin_back.name}.meta")
|
||
|
||
front_src = first_existing_file(
|
||
skin_front / "1.png",
|
||
player_skin / "待机正面" / "1.png",
|
||
theme_dir / "player" / "idel-正" / "1.png",
|
||
theme_dir / "player_F.png",
|
||
)
|
||
if front_src and front_src != skin_front / "1.png":
|
||
shutil.copy2(front_src, skin_front / "1.png")
|
||
if (skin_front / "1.png").is_file():
|
||
ensure_png_meta(skin_front / "1.png")
|
||
|
||
back_src = first_existing_file(
|
||
skin_back / "1.png",
|
||
player_skin / "待机背面" / "1.png",
|
||
theme_dir / "player" / "idel-反" / "1.png",
|
||
theme_dir / "player_B.png",
|
||
)
|
||
if back_src and back_src != skin_back / "1.png":
|
||
shutil.copy2(back_src, skin_back / "1.png")
|
||
if (skin_back / "1.png").is_file():
|
||
ensure_png_meta(skin_back / "1.png")
|
||
|
||
for src_name, dst_name in SILU_CANONICAL_COPIES:
|
||
src = theme_dir / src_name
|
||
dst = theme_dir / dst_name
|
||
if src.is_file():
|
||
shutil.copy2(src, dst)
|
||
ensure_png_meta(dst)
|
||
|
||
|
||
def normalize_nprop_filenames(theme_dir: Path) -> None:
|
||
"""Unity numMan 等主题 nProp 文件名可能带尾空格,统一为无空格文件名。"""
|
||
for png in theme_dir.rglob("nProp*.png"):
|
||
stem = png.stem
|
||
if stem != stem.rstrip():
|
||
fixed = png.with_name(f"{stem.rstrip()}.png")
|
||
if fixed != png:
|
||
if fixed.is_file():
|
||
png.unlink()
|
||
else:
|
||
png.rename(fixed)
|
||
meta = png.with_suffix(".png.meta")
|
||
fixed_meta = fixed.with_suffix(".png.meta")
|
||
if meta.is_file() and not fixed_meta.is_file():
|
||
meta.rename(fixed_meta)
|
||
|
||
|
||
# HUD 按钮(UIMain 右侧,与 themes-database.json hud 字段对应)
|
||
HUD_GLOBS = (
|
||
"anniu_*.png",
|
||
"redarmy*.png",
|
||
"素材切图-*.png",
|
||
"素材切图2-*.png",
|
||
"1倍速.png",
|
||
"2倍速.png",
|
||
"4倍速.png",
|
||
"声音.png",
|
||
"声音关闭.png",
|
||
"导航图标.png",
|
||
"重置.png",
|
||
"放大图标.png",
|
||
"缩小图标.png",
|
||
)
|
||
|
||
|
||
def copy_hud_assets(unity_tex: Path, out_root: Path, style_key: str, rel: str) -> int:
|
||
"""拷贝 Unity UI 按钮贴图(原先 rglob 会跳过 anniu_/倍速/声音)"""
|
||
src = unity_tex if rel == "." else unity_tex / rel
|
||
if not src.is_dir():
|
||
return 0
|
||
dst = out_root / style_key
|
||
dst.mkdir(parents=True, exist_ok=True)
|
||
count = 0
|
||
seen: set[str] = set()
|
||
for pattern in HUD_GLOBS:
|
||
for png in src.glob(pattern):
|
||
if not png.is_file():
|
||
continue
|
||
key = str(png.resolve())
|
||
if key in seen:
|
||
continue
|
||
seen.add(key)
|
||
target = dst / png.name
|
||
shutil.copy2(png, target)
|
||
count += 1
|
||
return count
|
||
|
||
|
||
def copy_theme(unity_tex: Path, out_root: Path, style_key: str, rel: str) -> int:
|
||
src = unity_tex if rel == "." else unity_tex / rel
|
||
dst = out_root / style_key
|
||
dst.mkdir(parents=True, exist_ok=True)
|
||
count = 0
|
||
|
||
if rel == ".":
|
||
for png in src.glob("*.png"):
|
||
shutil.copy2(png, dst / png.name)
|
||
if png.name == "Ship_F.png":
|
||
shutil.copy2(png, dst / "ship_F.png")
|
||
count += 1
|
||
return count
|
||
|
||
if not src.is_dir():
|
||
print(f" skip missing theme dir: {src}")
|
||
return 0
|
||
|
||
for png in src.rglob("*.png"):
|
||
rel_path = png.relative_to(src)
|
||
# 跳过 HUD 专用碎图(由 copy_hud_assets 扁平拷贝到主题根目录)
|
||
if png.name.startswith("anniu_") or "倍速" in png.name or png.name.startswith("声音"):
|
||
continue
|
||
if png.name.startswith("素材切图") or png.name.startswith("redarmy"):
|
||
continue
|
||
target = dst / rel_path
|
||
target.parent.mkdir(parents=True, exist_ok=True)
|
||
shutil.copy2(png, target)
|
||
count += 1
|
||
|
||
# silu 装饰砖块别名(烘焙脚本用 Decor23)
|
||
decor = dst / "素材切图-23.png"
|
||
if decor.is_file():
|
||
shutil.copy2(decor, dst / "Decor23.png")
|
||
|
||
normalize_nprop_filenames(dst)
|
||
if style_key == "silu":
|
||
normalize_silu_assets(dst)
|
||
|
||
return count
|
||
|
||
|
||
def copy_prop(unity_tex: Path, out_root: Path) -> int:
|
||
src = unity_tex / "Prop"
|
||
if not src.is_dir():
|
||
return 0
|
||
dst = out_root / "prop"
|
||
dst.mkdir(parents=True, exist_ok=True)
|
||
n = 0
|
||
for png in src.glob("*.png"):
|
||
name = png.name.replace("载具 正面", "vehicle_F").replace("载具 背面", "vehicle_B")
|
||
shutil.copy2(png, dst / name)
|
||
n += 1
|
||
return n
|
||
|
||
|
||
def main():
|
||
ap = argparse.ArgumentParser()
|
||
ap.add_argument("--unity-root", required=True)
|
||
ap.add_argument("--out", default="assets/resources/textures")
|
||
ap.add_argument("--themes", default=",".join(THEME_MAP.keys()))
|
||
args = ap.parse_args()
|
||
|
||
project = Path(__file__).resolve().parent.parent
|
||
unity_tex = Path(args.unity_root) / "Assets" / "Texture"
|
||
out_root = project / args.out
|
||
|
||
if not unity_tex.is_dir():
|
||
raise SystemExit(f"Unity Texture 不存在: {unity_tex}")
|
||
|
||
themes = [t.strip() for t in args.themes.split(",") if t.strip()]
|
||
total = 0
|
||
for key in themes:
|
||
rel = THEME_MAP.get(key)
|
||
if rel is None:
|
||
print(f"unknown theme: {key}")
|
||
continue
|
||
n = copy_theme(unity_tex, out_root, key, rel)
|
||
hn = copy_hud_assets(unity_tex, out_root, key, rel)
|
||
print(f" {key}: {n} png (+ {hn} hud)")
|
||
total += n + hn
|
||
|
||
pn = copy_prop(unity_tex, out_root)
|
||
print(f" prop: {pn} png")
|
||
total += pn
|
||
|
||
print(f"Imported {total} files -> {out_root}")
|
||
print("请在 Cocos Creator 中刷新 assets/resources/textures,然后运行:")
|
||
print(" python3 tools/fix_tile_texture_metas.py")
|
||
|
||
|
||
if __name__ == "__main__":
|
||
main()
|