Complete Cocos Creator port with level bundles, themes, and tooling.
Adds level prefabs, theme assets, audio, extensions, and deployment scripts for the Unity WebGL migration. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
280
tools/fix_tile_texture_metas.py
Normal file
280
tools/fix_tile_texture_metas.py
Normal file
@@ -0,0 +1,280 @@
|
||||
#!/usr/bin/env python3
|
||||
"""将地块贴图 .meta 设为 sprite-frame,对齐 Unity pivot,按透明边裁剪以贴满格子。"""
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import re
|
||||
import struct
|
||||
import uuid
|
||||
from pathlib import Path
|
||||
|
||||
TILE_GLOBS = [
|
||||
"assets/resources/textures/*/Baseblock.png",
|
||||
"assets/resources/textures/*/JumpBlock.png",
|
||||
"assets/resources/textures/*/WallBlock.png",
|
||||
"assets/resources/textures/*/Decor23.png",
|
||||
"assets/resources/textures/*/kuai11.png",
|
||||
"assets/resources/textures/*/素材切图-23.png",
|
||||
"assets/resources/textures/*/素材切图2-23.png",
|
||||
]
|
||||
|
||||
COCOS_TO_UNITY = {
|
||||
"default": "Level",
|
||||
"silu": "silu",
|
||||
"snow": "snow",
|
||||
"sanxing": "sanxing",
|
||||
"chinese": "Chinese",
|
||||
"numMan": "numMan",
|
||||
"redarmy": "redArmy",
|
||||
"redArmy": "redArmy",
|
||||
}
|
||||
|
||||
|
||||
def png_size(path: Path) -> tuple[int, int]:
|
||||
with path.open("rb") as f:
|
||||
f.read(16)
|
||||
return struct.unpack(">II", f.read(8))
|
||||
|
||||
|
||||
def alpha_bbox(png_path: Path) -> tuple[int, int, int, int] | None:
|
||||
try:
|
||||
from PIL import Image
|
||||
except ImportError:
|
||||
return None
|
||||
try:
|
||||
img = Image.open(png_path).convert("RGBA")
|
||||
box = img.getbbox()
|
||||
if not box:
|
||||
return None
|
||||
return box
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
|
||||
def read_unity_pivot(unity_tex: Path, theme_folder: str, tile_name: str) -> tuple[float, float]:
|
||||
theme_dir = unity_tex if theme_folder == "Level" else unity_tex / theme_folder
|
||||
meta_path = theme_dir / f"{tile_name}.png.meta"
|
||||
if not meta_path.is_file() and tile_name == "Decor23":
|
||||
meta_path = theme_dir / "素材切图-23.png.meta"
|
||||
if not meta_path.is_file():
|
||||
return 0.5, 0.92
|
||||
text = meta_path.read_text(encoding="utf-8")
|
||||
m = re.search(r"spritePivot:\s*\{x:\s*([^,]+),\s*y:\s*([^}]+)\}", text)
|
||||
if not m:
|
||||
return 0.5, 0.92
|
||||
return float(m.group(1)), float(m.group(2))
|
||||
|
||||
|
||||
def resolve_trim(
|
||||
png_path: Path,
|
||||
full_w: int,
|
||||
full_h: int,
|
||||
pivot_x: float,
|
||||
pivot_y: float,
|
||||
) -> dict:
|
||||
"""按透明边裁剪,并换算 Unity pivot 到裁剪后 sprite。"""
|
||||
bbox = alpha_bbox(png_path)
|
||||
if not bbox:
|
||||
return {
|
||||
"trimType": "none",
|
||||
"trimX": 0,
|
||||
"trimY": 0,
|
||||
"width": full_w,
|
||||
"height": full_h,
|
||||
"rawWidth": full_w,
|
||||
"rawHeight": full_h,
|
||||
"offsetX": 0,
|
||||
"offsetY": 0,
|
||||
"pivotX": pivot_x,
|
||||
"pivotY": pivot_y,
|
||||
}
|
||||
|
||||
left, top, right, bottom = bbox
|
||||
trim_w = right - left
|
||||
trim_h = bottom - top
|
||||
if trim_w <= 0 or trim_h <= 0 or (trim_w >= full_w and trim_h >= full_h):
|
||||
return {
|
||||
"trimType": "none",
|
||||
"trimX": 0,
|
||||
"trimY": 0,
|
||||
"width": full_w,
|
||||
"height": full_h,
|
||||
"rawWidth": full_w,
|
||||
"rawHeight": full_h,
|
||||
"offsetX": 0,
|
||||
"offsetY": 0,
|
||||
"pivotX": pivot_x,
|
||||
"pivotY": pivot_y,
|
||||
}
|
||||
|
||||
pivot_bottom = pivot_y * full_h
|
||||
trim_bottom = full_h - bottom
|
||||
pivot_bottom_trim = pivot_bottom - trim_bottom
|
||||
new_pivot_y = max(0.0, min(1.0, pivot_bottom_trim / trim_h))
|
||||
new_pivot_x = max(0.0, min(1.0, (pivot_x * full_w - left) / trim_w))
|
||||
|
||||
return {
|
||||
"trimType": "custom",
|
||||
"trimX": left,
|
||||
"trimY": top,
|
||||
"width": trim_w,
|
||||
"height": trim_h,
|
||||
"rawWidth": full_w,
|
||||
"rawHeight": full_h,
|
||||
"offsetX": 0,
|
||||
"offsetY": 0,
|
||||
"pivotX": round(new_pivot_x, 4),
|
||||
"pivotY": round(new_pivot_y, 4),
|
||||
}
|
||||
|
||||
|
||||
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 patch_meta(meta_path: Path, png_path: Path, trim: dict) -> None:
|
||||
ensure_image_meta(meta_path, meta_path.stem.replace(".png", ""))
|
||||
meta = json.loads(meta_path.read_text(encoding="utf-8"))
|
||||
base_uuid = meta["uuid"]
|
||||
tex_uuid = f"{base_uuid}@6c48a"
|
||||
sf_uuid = f"{base_uuid}@f9941"
|
||||
|
||||
meta["userData"] = {
|
||||
"type": "sprite-frame",
|
||||
"redirect": tex_uuid,
|
||||
"hasAlpha": True,
|
||||
"fixAlphaTransparencyArtifacts": False,
|
||||
}
|
||||
meta["subMetas"]["f9941"] = {
|
||||
"ver": "1.0.12",
|
||||
"importer": "sprite-frame",
|
||||
"uuid": sf_uuid,
|
||||
"imported": True,
|
||||
"files": [".json"],
|
||||
"subMetas": {},
|
||||
"userData": {
|
||||
"wrapModeS": "clamp-to-edge",
|
||||
"wrapModeT": "clamp-to-edge",
|
||||
"minfilter": "linear",
|
||||
"magfilter": "linear",
|
||||
"premultiplyAlpha": False,
|
||||
"generateMipmap": False,
|
||||
"anisotropy": 1,
|
||||
"trimType": trim["trimType"],
|
||||
"trimThreshold": 1,
|
||||
"rotated": False,
|
||||
"offsetX": trim["offsetX"],
|
||||
"offsetY": trim["offsetY"],
|
||||
"trimX": trim["trimX"],
|
||||
"trimY": trim["trimY"],
|
||||
"width": trim["width"],
|
||||
"height": trim["height"],
|
||||
"rawWidth": trim["rawWidth"],
|
||||
"rawHeight": trim["rawHeight"],
|
||||
"borderTop": 0,
|
||||
"borderBottom": 0,
|
||||
"borderLeft": 0,
|
||||
"borderRight": 0,
|
||||
"isUuid": True,
|
||||
"imageUuidOrDatabaseUri": tex_uuid,
|
||||
"atlasUuid": "",
|
||||
"mipfilter": "none",
|
||||
"packable": True,
|
||||
"pixelsToUnit": 100,
|
||||
"pivotX": trim["pivotX"],
|
||||
"pivotY": trim["pivotY"],
|
||||
"meshType": 0,
|
||||
},
|
||||
"displayName": meta_path.stem,
|
||||
"id": "f9941",
|
||||
"name": "spriteFrame",
|
||||
}
|
||||
meta_path.write_text(json.dumps(meta, indent=2), encoding="utf-8")
|
||||
rel = meta_path.relative_to(meta_path.parents[4])
|
||||
print(
|
||||
f" patched {rel} -> draw {trim['width']}x{trim['height']} "
|
||||
f"(raw {trim['rawWidth']}x{trim['rawHeight']}) pivot=({trim['pivotX']},{trim['pivotY']})"
|
||||
)
|
||||
|
||||
|
||||
def collect_tile_pngs(root: Path, themes: set[str] | None) -> list[Path]:
|
||||
found: set[Path] = set()
|
||||
for pattern in TILE_GLOBS:
|
||||
for p in root.glob(pattern):
|
||||
if not p.is_file():
|
||||
continue
|
||||
if themes:
|
||||
theme = p.parent.name
|
||||
if theme not in themes:
|
||||
continue
|
||||
found.add(p)
|
||||
return sorted(found)
|
||||
|
||||
|
||||
def main():
|
||||
ap = argparse.ArgumentParser()
|
||||
ap.add_argument("--unity-root", help="Unity 项目根目录,用于读取 spritePivot")
|
||||
ap.add_argument("--themes", help="仅处理指定 Cocos 主题目录,逗号分隔,如 snow")
|
||||
args = ap.parse_args()
|
||||
|
||||
root = Path(__file__).resolve().parent.parent
|
||||
theme_filter = {t.strip() for t in args.themes.split(",") if t.strip()} if args.themes else None
|
||||
unity_tex = Path(args.unity_root) / "Assets" / "Texture" if args.unity_root else None
|
||||
|
||||
pngs = collect_tile_pngs(root, theme_filter)
|
||||
for png in pngs:
|
||||
meta = png.with_suffix(".png.meta")
|
||||
if not png.is_file():
|
||||
print(f" skip missing {png.relative_to(root)}")
|
||||
continue
|
||||
w, h = png_size(png)
|
||||
tile_name = png.stem
|
||||
cocos_theme = png.parent.name
|
||||
if unity_tex and unity_tex.is_dir():
|
||||
unity_folder = COCOS_TO_UNITY.get(cocos_theme, cocos_theme)
|
||||
pivot_x, pivot_y = read_unity_pivot(unity_tex, unity_folder, tile_name)
|
||||
else:
|
||||
pivot_x, pivot_y = 0.5, 0.92
|
||||
trim = resolve_trim(png, w, h, pivot_x, pivot_y)
|
||||
patch_meta(meta, png, trim)
|
||||
|
||||
print(f"Patched {len(pngs)} tile metas. 请在 Cocos Creator 中刷新 assets/resources/textures。")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user