ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Phaser Compact Texture Atlas(PCT)格式规范详解:从行格式到加载器实现

Phaser Compact Texture Atlas(PCT)格式规范详解:从行格式到加载器实现 Phaser Compact Texture AtlasPCT格式规范详解从行格式到加载器实现【免费下载链接】phaserPhaser is a fun, free and fast 2D game framework for making HTML5 games for desktop and mobile web browsers, supporting Canvas and WebGL rendering.项目地址: https://gitcode.com/gh_mirrors/ph/phaser导读Phaser Compact Texture Atlas简称 PCT是 Phaser 4 引入的一种紧凑型纹理图集描述格式。它以纯文本、逐行记录的方式描述纹理图集相比传统的 JSON 图集描述文件体积通常缩小 90%95%同时保持运行时可极低成本解析。本文以仓库中的官方规范文档Phaser Compact Texture Atlas Format Specification为骨架结合源码实现与测试用例完整讲解 PCT 的 8 种记录类型、名称编码规则、解码算法与设计取舍。读完本文你将具备独立实现 PCT 解析器/加载器的能力并理解 Phaser 内部如何用atlasPCT加载器把.pct文件与图集图片组装成可用的多源纹理。一、PCT 格式总览一个.pct文件是普通的 UTF-8 纯文本文件每一行是一条记录record解析器自上而下单趟扫描。文件中共有 8 种记录类型通过前缀字符或模式区分前缀记录类型用途PCT:版本头文件标识与格式版本声明必须是第一行P:页头声明一张图集图片及其尺寸F:文件夹条目在文件夹字典中声明一个文件夹名#页选择器将后续帧切换到不同的图集页B:块头声明一个同尺寸精灵组成的网格块A:别名将重复精灵映射到已存在的帧name\|单帧一个带显式位置的精灵names,...块名称行前一块的名称列表逗号分隔记录必须按如下顺序出现第 1 行是PCT:版本头之后是全部P:页头再之后是全部F:文件夹条目然后是帧数据可以穿插#、B:、块名称行与单帧行最后是A:别名记录。别名放在最后是因为它们按名称引用帧加载器通过复制已存在的条目来解析别名。实现事实仓库中的解码器 PCTDecode.js 严格实现了上述顺序约定且对未知前缀行采取静默跳过策略见 PCTDecode.js 中对不足 3 个|分段的行直接continue的处理。二、八种记录类型详解2.1 版本头PCT:每个 PCT 文件的第一行必须是版本头。它同时充当文件标识magic string与格式版本声明PCT:1.0版本号采用major.minor的 semver 风格编号组件含义Major破坏性变更。v2 文件无法被 v1 解析器加载。Minor增量特性。v1.2 文件可以在 v1.0 解析器中加载——未知特性会被安全忽略。加载器校验逻辑const firstLine lines[0]; if (!firstLine.startsWith(PCT:)) { throw new Error(Not a PCT file); } const [major, minor] firstLine.slice(4).split(.).map(Number); if (major 1) { throw new Error(Unsupported PCT version major); } // Minor features can be checked as needed: // const hasRotation minor 1;加载器应当拒绝无法识别的主版本号但接受无法识别的次版本号。未来次版本中引入的未知行前缀应被静默跳过——前缀驱动的解析器天然支持这一点。源码实现中PCTDecode.js 对版本校验做了额外的健壮性处理先剥离首行末尾可能的\r兼容 CRLF 换行再校验PCT:前缀并解析主版本号当主版本缺失或大于 1 时通过console.warn输出警告并返回null。这些边界行为都有对应的测试覆盖见 PCTDecode.test.js非字符串输入、空字符串、缺失PCT:头、PCT:2.0主版本超限都应返回null而PCT:1.5这类次版本增量必须能正常加载CRLF 换行也能被正确处理。版本历史版本变更1.0初始发布。支持块、文件夹、范围、扩展名、别名、多页。2.2 页头P:声明一张图集页。多图集文件有多个P:行单图集文件有一个。P:atlas_0.png,RGBA8888,2048,512,2字段逗号分隔位置字段类型描述0filenamestring纹理图片文件名1formatstring像素格式当前始终为RGBA88882widthint图集宽度像素3heightint图集高度像素4paddingint打包时使用的形状填充shape padding值padding 值仅作信息用途——它告诉加载器精灵之间预留的间隙。帧坐标已经包含了 padding因此加载器不需要再应用它。所有P:行出现在文件顶部、任何其他记录类型之前。页按出现顺序从 0 开始编号。在解码器中每个P:行被解析为pages数组中的一个对象filename / format / width / height / padding见 PCTDecode.js。测试 PCTDecode.test.js 验证了单页与多页两种情况下页数组的解析结果。2.3 文件夹条目F:在文件夹字典中声明一个文件夹名。文件夹条目出现在所有P:页头之后、任何帧数据之前F:warrior F:knight/idle F:effects每个F:行向文件夹字典添加一个条目按出现顺序从 0 开始编号。帧名称通过该数字索引引用文件夹而不是重复完整的文件夹字符串。如果没有文件夹所有精灵都在根层级则没有F:行。源码中folders数组按声明顺序压入见 PCTDecode.js测试验证了无文件夹时返回空数组、文件夹按顺序解析、以及文件夹名可以包含斜杠如knight/idle三种情况PCTDecode.test.js。2.4 页选择器#将后续帧数据切换到另一张图集页。仅在存在多个页时出现#0 #1#后的数字是零基页索引对应P:页头的顺序。跟在#N行之后的所有帧、块和单帧都属于页 N直到下一个#行或文件结束。如果只有一个页则没有#行所有帧数据隐式属于页 0。解码器中currentPage变量记录当前页#行直接更新该变量PCTDecode.js。测试验证了块帧和单帧都能被正确路由到目标页且缺省时默认页为 0PCTDecode.test.js。2.5 块头B:声明一个网格块——由同尺寸精灵按网格排列形成的矩形区域。B:头的下一行总是该块的名称行。未裁剪untrimmed块B:2,2,8,64,64已裁剪trimmed块B:2,2,6,120,108|134,120,4,6|之前的字段始终存在位置字段类型描述0xint块在图集中的原点 X第一个单元格左上角1yint块在图集中的原点 Y2colsint网格列数3frameWint每个帧的宽度裁剪/打包后的尺寸4frameHint每个帧的高度|之后的字段仅裁剪时存在位置字段类型描述0sourceWint裁剪前的原始源图像宽度1sourceHint裁剪前的原始源图像高度2trimXint裁剪区域在源图像中的 X 偏移3trimYint裁剪区域在源图像中的 Y 偏移对于裁剪块spriteSourceSize.w和spriteSourceSize.h分别等于frameW和frameH——它们不会被单独存储。行数通过推导得出rows ceil(spriteCount / cols)。2.6 块名称行紧跟在B:头之后包含块内所有精灵的名称可以使用范围压缩、文件夹索引和扩展名索引0/idle#01-24该行被解析为有序精灵名称列表。每个名称映射到一个网格单元索引为i的精灵占据列i % cols、行floor(i / cols)的单元。每个精灵的位置按如下推导cellW frameW padding * 2 cellH frameH padding * 2 sprite[i].x blockX (i % cols) * cellW padding sprite[i].y blockY floor(i / cols) * cellH padding其中padding是页头中的形状填充值。实现细节源码中有一个非常关键的约定——待处理的块pendingBlock总是把下一个非空行当作名称行消费无论该行以什么前缀字符开头PCTDecode.js。这意味着即使名称行恰好以#开头也不会被误判为页选择器。测试 PCTDecode.test.js 专门构造了B:0,0,3,10,10后跟#1-3的边界用例验证展开出的帧名为1、2、3而不是被当作页选择器。2.7 单帧一个带显式位置和尺寸的独立精灵用于不属于任何块组的精灵。未裁剪sword|0|726,2,86,42已裁剪shield|2|726,48,72,68|80,80,4,6字段以|分隔分段字段描述0name精灵名称可含文件夹索引与扩展名索引1flags位标志bit 0 旋转bit 1 已裁剪2x,y,w,h图集中的帧矩形3sw,sh,sx,sy源尺寸与源内裁剪偏移仅裁剪时存在分段 3 存在时把 4 个裁剪值打包进一个逗号分隔的字段与B:块头中|分隔的裁剪字段布局保持一致。因此已裁剪的单帧恰好有 4 个|分段未裁剪的单帧有 3 个。对于未裁剪帧flags bit 1 0sourceSize{ w: w, h: h }spriteSourceSize{ x: 0, y: 0, w: w, h: h }对于已裁剪帧flags bit 1 1sourceSize{ w: sw, h: sh }spriteSourceSize{ x: sx, y: sy, w: w, h: h }旋转标志bit 0保留给未来使用。目前所有帧都按无旋转方式打包。源码中 flags 的位运算解析见 PCTDecode.jsisTrimmed (flags 2) ! 0、rotated (flags 1) ! 0。测试覆盖了 flags0未裁剪、flags1仅旋转、flags2仅裁剪、flags3裁剪旋转四种组合见 PCTDecode.test.js。值得注意的是虽然规范说明旋转目前保留未用但解析器已经实现了旋转位——当src.rotated为真时PCT.js 会把帧标记为rotated并调用updateUVsInverted()更新 UV 坐标说明实现层面已经为旋转帧预留了完整能力。2.8 别名A:将重复精灵打包时检测到的像素完全相同的帧映射到图集中已有的原始帧。别名行出现在所有帧数据之后A:0/idle_010/idle_12,0/idle_18格式A:originalNameduplicateName1,duplicateName2,...重复名称列表可以使用范围压缩。加载器应为每个重复名称创建帧条目复制原帧的所有属性位置、尺寸、裁剪数据。重复名称不出现在纹理图片中——它们共享原帧的图集区域。源码中别名的处理逻辑见 PCTDecode.js先解析两侧的名称两侧都支持文件夹/扩展名索引若原帧存在则对每个重复名执行属性复制并覆盖key字段若原帧不存在或缺失则静默跳过。测试 PCTDecode.test.js 验证了别名帧共享原帧位置、副本 key 为自己的名称、深拷贝修改副本不影响原帧、缺失原帧时静默跳过、缺失时静默跳过、以及两侧都解析文件夹/扩展名索引等行为。三、名称编码规则PCT 格式中的精灵名称包含三层编码文件夹索引、扩展名索引和顺序范围压缩。解码时按以下顺序应用。3.1 文件夹索引名称可以以数字文件夹索引加/作为前缀0/idle_01 → folder[0] / idle_01 2/spark_05 → folder[2] / spark_05 sword → no folder (root level)如果名称以数字加/开头斜杠前的数字是文件夹字典索引。从F:条目中查出文件夹名并加/分隔符拼成完整 key。没有/前缀的名称是根级精灵。源码中对数字的判定使用了isAllDigits辅助函数PCTDecode.js逐字符检查charCodeAt是否落在4857即 ASCII09区间。测试专门验证了abc/def这种非数字前缀不会被误判为文件夹索引PCTDecode.test.js。3.2 扩展名索引名称可以以~N结尾其中 N 是硬编码的扩展名字典索引索引扩展名1.png2.webp3.jpg4.jpeg5.gifsword~1 → sword.png frame1~3 → frame1.jpg 0/idle_01~2 → folder[0] /idle_01.webp heightmap.tga → heightmap.tga (unknown ext stored raw)如果名称以~加单个数字 1-5 结尾去掉后缀并追加对应扩展名。如果名称包含.但没有~说明它带的是字典之外的原始扩展名——原样使用。如果既没有~也没有.说明名称没有扩展名打包时被剥离。源码中的扩展名字典定义在 PCTDecode.jsEXT { 1: .png, 2: .webp, 3: .jpg, 4: .jpeg, 5: .gif }。测试验证了 5 个扩展名索引都能正确映射PCTDecode.test.js以及heightmap.tga这种原始扩展名原样保留PCTDecode.test.js。3.3 范围压缩在块名称行中连续名称可以压缩为范围记号frame#1-23 → frame1, frame2, frame3, ..., frame23 walk_#01-08 → walk_01, walk_02, ..., walk_08 idle#000-059 → idle000, idle001, ..., idle059格式prefix#start-end解码规则在逗号处拆分名称行得到分段检查每个分段是否包含#若包含在#处拆分为prefix和range在-处拆分range得到startStr和endStr将两者解析为整数start和end如果startStr有前导零长度 1 且首字符为0生成的所有数字按startStr.length补零生成名称对start到end含的每个i输出prefix pad(i)如果没有#该分段是字面名称范围、字面名称和文件夹/扩展名索引可以组合0/frame#1-23~1展开为文件夹0、帧frame1.png到frame23.png。组合形式的解码顺序在,处拆分得到分段展开任何#范围为单个名称字符串对每个名称解析文件夹索引去掉N/前缀对每个名称解析扩展名索引去掉~N后缀补零逻辑在源码中体现为zeroPad辅助函数PCTDecode.js以及padLen的判断startStr.length 1 startStr.charAt(0) 0。测试验证了idle_#001-012只生成补零的idle_001idle_012而不会生成idle_12PCTDecode.test.js以及frame#1-5不补零PCTDecode.test.js。同一行混用字面名称与范围如intro,frame#1-3,outro也有测试覆盖PCTDecode.test.js。3.4 带扩展名后缀的范围压缩当块名称行带扩展名后缀时它只出现在整行末尾一次在所有名称和范围之后0/frame#1-23~1~1应用于行内所有名称。解码器应当检查行是否以~NN 为 1-5结尾若是剥离并记录该扩展名解析剩余字符串中的范围和名称将扩展名应用到每个生成的名称注意优先级源码实现中逐名称的~N会覆盖行级后缀。resolveFullName先检查名称自身的~N匹配PCTDecode.js命中则直接替换扩展名否则才使用行级extSuffix。测试a,b,c~1验证了行级后缀应用到所有名称PCTDecode.test.js。四、完整解码算法以下伪代码完整呈现 PCT 文件的解码流程对应仓库实现 PCTDecode.jsfunction decodePCT(text): lines text.split(\n) pages [] folders [] currentPage 0 frames {} // key → frame data // Extension dictionary EXT { 1:.png, 2:.webp, 3:.jpg, 4:.jpeg, 5:.gif } pendingBlock null for line in lines: if line is empty: continue // A pending block ALWAYS consumes the next non-empty line as its names // line, regardless of what prefix character that line happens to start // with. This must be checked before any prefix-based dispatch below. if pendingBlock is not null: // This line is the names for the pending block block pendingBlock pendingBlock null padding pages[block.page].padding cellW block.frameW padding * 2 cellH block.frameH padding * 2 names expandNames(line, folders, EXT) for i, name in enumerate(names): col i % block.cols row floor(i / block.cols) frame { key: name, page: block.page, x: block.x col * cellW padding, y: block.y row * cellH padding, w: block.frameW, h: block.frameH, trimmed: block.trimmed, rotated: false } if block.trimmed: frame.sourceW block.sourceW frame.sourceH block.sourceH frame.trimX block.trimX frame.trimY block.trimY else: frame.sourceW block.frameW frame.sourceH block.frameH frame.trimX 0 frame.trimY 0 frames[name] frame continue if line starts with P:: parts line[2:].split(,) pages.push({ filename: parts[0], format: parts[1], width: int(parts[2]), height: int(parts[3]), padding: int(parts[4]) }) else if line starts with F:: folders.push(line[2:]) else if line starts with #: currentPage int(line[1:]) else if line starts with B:: // Parse block header trimParts line[2:].split(|) main trimParts[0].split(,) block { page: currentPage, x: int(main[0]), y: int(main[1]), cols: int(main[2]), frameW: int(main[3]), frameH: int(main[4]), trimmed: trimParts.length 1 } if block.trimmed: trim trimParts[1].split(,) block.sourceW int(trim[0]) block.sourceH int(trim[1]) block.trimX int(trim[2]) block.trimY int(trim[3]) pendingBlock block else if line starts with A:: // Parse alias eqIdx line.indexOf(, 2) originalName resolveFullName(line[2:eqIdx], folders, EXT, ) dupNames expandNames(line[eqIdx1:], folders, EXT) for name in dupNames: frames[name] copy(frames[originalName]) frames[name].key name else: // Individual frame line parts line.split(|) name resolveFullName(parts[0], folders, EXT, ) flags int(parts[1]) trimmed (flags 2) ! 0 fv parts[2].split(,) frame { key: name, page: currentPage, x: int(fv[0]), y: int(fv[1]), w: int(fv[2]), h: int(fv[3]), trimmed: trimmed, rotated: (flags 1) ! 0 } if trimmed: tv parts[3].split(,) frame.sourceW int(tv[0]) frame.sourceH int(tv[1]) frame.trimX int(tv[2]) frame.trimY int(tv[3]) else: frame.sourceW frame.w frame.sourceH frame.h frame.trimX 0 frame.trimY 0 frames[name] frame return { pages, folders, frames }辅助函数expandNames(line, folders, EXT)function expandNames(line, folders, EXT): // Check for trailing extension suffix: ~N at very end extSuffix match line.match(/~([1-5])$/) if match: extSuffix EXT[int(match[1])] line line[0:-2] // strip ~N results [] segments line.split(,) for segment in segments: if # in segment: hashIdx segment.indexOf(#) prefix segment[0:hashIdx] range segment[hashIdx1:] dashIdx range.indexOf(-) startStr range[0:dashIdx] endStr range[dashIdx1:] start int(startStr) end int(endStr) padLen startStr.length if (startStr.length 1 and startStr[0] 0) else 0 for i from start to end inclusive: numStr padLen 0 ? zeroPad(i, padLen) : str(i) rawName prefix numStr results.push(resolveFullName(rawName, folders, EXT, extSuffix)) else: results.push(resolveFullName(segment, folders, EXT, extSuffix)) return results辅助函数resolveFullName(raw, folders, EXT, extSuffix)function resolveFullName(raw, folders, EXT, extSuffix): name raw folder // Folder index: N/rest slashIdx name.indexOf(/) if slashIdx 0 and name[0:slashIdx] is all digits: folderIdx int(name[0:slashIdx]) folder folders[folderIdx] name name[slashIdx1:] // Extension index: name~N (per-name, overrides line-level) match name.match(/~([1-5])$/) if match: ext EXT[int(match[1])] name name[0:-2] ext else if extSuffix: name name extSuffix // else: name has no extension or contains a raw one (e.g. .tga) if folder: return folder / name return name与规范的差异说明仓库实现与上述伪代码在健壮性上略有增强——解码前会先校验输入是否为非空字符串并剥离每行末尾的\r以兼容 CRLF 换行文件PCTDecode.js校验失败时通过console.warn输出警告而非抛异常并返回null交由上层处理。测试 PCTDecode.test.js 覆盖了 CRLF 与空行跳过两种边界情况。五、完整示例5.1 示例 1简单块8 个 64×64 未裁剪精灵打包进单行。PCT:1.0 P:atlas_0.png,RGBA8888,1024,256,2 B:2,2,8,64,64 frame#1-8产生 8 个名为frame1到frame8的帧每个 64×64 像素位置由块网格推导单元间 2px padding。解码后的帧位置名称XYframe144frame2724frame31404frame42084frame52764frame63444frame74124frame84804单元宽度 64 2×2 68。位置 blockX col × 68 2。这个示例与源码测试 PCTDecode.test.js 中的 spec Example 1 用例完全一致测试断言了这 8 个帧的精确坐标4、72、140、208、276、344、412、480可直接对照验证。5.2 示例 2多页 全部特性两张图集页、三个文件夹、裁剪块、单帧、扩展名索引、范围压缩和别名。PCT:1.0 P:atlas_0.png,RGBA8888,2048,512,2 P:atlas_1.png,RGBA8888,2048,256,2 F:warrior F:knight F:effects #0 B:2,2,6,120,108|134,120,4,6 0/idle_#01-24~1 B:2,222,6,120,108|134,120,4,6 1/idle_#01-18~1 sword~1|0|726,2,86,42 shield~1|2|726,48,72,68|80,80,4,6 #1 B:2,2,10,48,48 2/spark_#01-30~1 A:0/idle_01~10/idle_12,0/idle_18~1 A:1/idle_01~11/idle_09~1解码结果页头页 0atlas_0.png2048×512padding 2页 1atlas_1.png2048×256padding 2文件夹0warrior1knight2effects页 0 帧(2,2) 处的块6 列 120×108 帧从 134×120 源裁剪偏移 (4,6)。名称warrior/idle_01.pngwarrior/idle_24.png24 帧4 行 × 6 列(2,222) 处的块尺寸相同名称knight/idle_01.pngknight/idle_18.png18 帧3 行 × 6 列sword.png未裁剪单帧位于 (726,2)尺寸 86×42页 0shield.png裁剪单帧位于 (726,48)帧 72×68源 80×80裁剪偏移 (4,6)页 1 帧(2,2) 处的块10 列 48×48 未裁剪帧。名称effects/spark_01.pngeffects/spark_30.png别名warrior/idle_12.png和warrior/idle_18.png与warrior/idle_01.png像素相同——共享其图集位置knight/idle_09.png与knight/idle_01.png像素相同这个完整示例被端到端测试原样采用PCTDecode.test.js测试断言了2 页、3 文件夹、74 个帧2418302、warrior/idle_01.png位于文档所述 (4,4)、warrior/idle_24.png位于 4 行块的最后一个单元 (624,340)对应 col5、row3、cellW124、cellH112、effects/spark_01.png路由到页 1、别名帧共享原帧坐标等细节。这些断言既是测试也是规范可执行化的最佳佐证。5.3 示例 3最小化单个未裁剪精灵无文件夹、无块。PCT:1.0 P:atlas_0.png,RGBA8888,256,256,1 logo|0|1,1,200,180一个名为logo的帧位于 (1,1)尺寸 200×180未裁剪256×256 图集1px padding。六、帧数据结构解码完成后每个帧应为渲染引擎提供以下字段字段类型描述keystring唯一帧名称含文件夹路径与扩展名若存在pageint该帧所在的图集页pages 数组索引xint帧在图集中的 X 位置yint帧在图集中的 Y 位置wint帧在图集中的宽度hint帧在图集中的高度sourceWint原始源图像宽度sourceHint原始源图像高度trimXint帧在原始源中的 X 偏移trimYint帧在原始源中的 Y 偏移trimmedbool该帧是否从源中裁剪过rotatedbool该帧是否在图集中顺时针旋转 90°保留当前恒为 false未裁剪帧sourceW w、sourceH h、trimX 0、trimY 0。按原始源尺寸渲染时在(sourceW, sourceH)的画布中以偏移(trimX, trimY)绘制图集区域(x, y, w, h)。在 Phaser 中这个帧结构通过 PCT.js 解析器落到实际的Frame对象上texture.add(src.key || key, sourceIndex, src.x, src.y, src.w, src.h)创建帧setTrim(sourceW, sourceH, trimX, trimY, w, h)设置裁剪信息PCT.js。该解析器还额外做了一件事为页 0 源添加__BASE帧与 JSONArray/JSONHash 解析器行为一致并把pages与folders元数据挂到纹理的customData[pct]上方便运行时检视。七、在 Phaser 中加载 PCT 图集7.1 加载器用法PCT 图集通过LoaderPlugin#atlasPCT方法加载定义在 PCTAtlasFile.jsfunction preload () { this.load.atlasPCT(level1, images/Level1.pct); }也支持配置对象形式this.load.atlasPCT({ key: level1, atlasURL: images/Level1.pct });加载完成后即可按 key 使用帧this.add.image(x, y, level1, background);解码后的 PCT 数据含页、文件夹与帧元数据还可以从 Atlas 缓存中取回var data this.cache.atlas.get(level1);如果未指定 URL加载器会取 key 并生成key.pct作为文件名例如 key 为alien时 URL 为alien.pct扩展名固定为.pct。7.2 内部工作流程从 PCTAtlasFile.js 的实现可以看到完整的内部链路加载数据文件内部类PCTDataFile以responseType: text请求.pct文件onProcess阶段调用PCTDecode(this.xhrLoader.responseText)把文本解码为结构化对象含pages、folders、frames解码失败则进入onProcessErrorPCTAtlasFile.js。动态排队图片onFileComplete中检查解码出的pages数组为每一页动态创建并排队一个ImageFile图片 key 形如PCT{multiKeyIndex}_{filename}期间会临时覆盖加载器的 baseURL / path / prefix 以支持页图片位于不同路径的场景处理完再恢复PCTAtlasFile.js。组装纹理addToCache阶段按文件名把各页图片按页顺序组装为images数组将解码数据写入 Atlas 缓存并调用textureManager.addAtlasPCT(key, images, decoded)生成单一的多源纹理PCTAtlasFile.js。TextureManager.addAtlasPCT是最终入口TextureManager.js它接收按页顺序排列的图片源与解码数据交给 PCT.js 解析器逐帧创建Frame。加载器测试 PCTAtlasFile.test.js 验证了这些行为构造器只创建一个pct类型的子文件并指向 atlas 缓存、无 URL 时默认扩展名为.pct、onFileComplete为每页创建ImageFile、处理后恢复加载器原有 baseURL / path / prefix 等。八、设计说明8.1 为什么用文本而不是二进制在典型图集规模50-500 帧下文本 PCT 文件只有 200-2000 字节。HTTP 标准使用的 Gzip 压缩会让文本与二进制格式的体积差距缩小到几个百分点以内。而文本格式带来的是人类可读、版本控制中易于 diff、用split()即可平凡解析、没有字节序endianness问题。8.2 块分组标准当 4 个或更多精灵共享相同的签名相同的frameW、frameH若裁剪则还需相同的sourceW、sourceH、trimX、trimY时它们被分组成块。这个阈值在数据节省与打包效率之间取得平衡——更小的分组会过度约束 MaxRects 打包器却无法获得足够的数据体积收益。8.3 组感知裁剪当多个精灵共享相同的源尺寸时裁剪计算的是所有帧的并集包围盒而不是各自独立裁剪。这确保了动画序列中的所有帧具有一致的裁剪边界防止播放期间出现视觉抖动同时使块分组成为可能。8.4 列策略块列数不固定为最大宽度。打包器测试三种策略近方形、半宽、全宽选择总图集最紧凑的方案。全宽条带在最后一行不完整时会浪费空间更方正的块则为其他精灵在旁留出空间。8.5 行拆分如果块最后一行未填满它会被拆分成一个满行的块和一个较小的余数块。这可以防止浪费的单元格占用本可由其他精灵填充的图集空间。九、PCT 在 Phaser 源码中的完整脉络如果你希望深入研读 PCT 的实现仓库中的关键文件如下规范文档Phaser Compact Texture Atlas Format Specification.md本文依据的原始规范核心解码器PCTDecode.jsPCTDecode(text)返回{ pages, folders, frames }解码失败返回null纹理解析器PCT.js把解码结构转换为 Texture 上的 Frame解析器注册表parsers/index.jsPCT与PCTDecode均挂在Phaser.Textures.Parsers命名空间下从代码注释看两者自 Phaser 4.0.0 起提供加载器PCTAtlasFile.jsatlasPCT加载器负责数据文件 多页图片的组合加载纹理管理器入口TextureManager.jsaddAtlasPCT(key, source, data, dataSource)自 4.0.0 起提供单元测试PCTDecode.test.js解码器全量行为验证含规范三个示例的端到端断言、PCTAtlasFile.test.js加载器组装行为验证结语PCT 是 Phaser 对纹理图集描述格式的一次务实优化在保持人类可读、极易解析的前提下通过页头 文件夹字典 网格块 范围压缩 扩展名索引 别名的组合把数据体积压缩 90%95%。规范文档为每种记录类型、每种编码组合都给出了精确定义而仓库中的 PCTDecode.js、PCT.js 与两套测试文件则让这份规范有了可执行、可验证的实现参照。无论是想为 PCT 编写第三方打包工具还是要在自己的引擎中实现兼容解析器本文所整理的内容都足以作为完整的实现依据。【免费下载链接】phaserPhaser is a fun, free and fast 2D game framework for making HTML5 games for desktop and mobile web browsers, supporting Canvas and WebGL rendering.项目地址: https://gitcode.com/gh_mirrors/ph/phaser创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表