<?xml version="1.0" encoding="utf-8"?><feed xmlns="http://www.w3.org/2005/Atom" ><generator uri="https://jekyllrb.com/" version="3.10.0">Jekyll</generator><link href="https://me.bitgong.cn/feed.xml" rel="self" type="application/atom+xml" /><link href="https://me.bitgong.cn/" rel="alternate" type="text/html" /><updated>2026-08-19T03:55:03+00:00</updated><id>https://me.bitgong.cn/feed.xml</id><title type="html">gongxh</title><subtitle>拥有10+年游戏开发经验的技术分享平台，专注于Cocos Creator游戏开发。 深度分享开发工具使用、踩坑经验总结、技术原理解析， 致力于帮助游戏开发者提升技能，携手共同成长。</subtitle><author><name>gongxh</name></author><entry><title type="html">ECS架构在游戏开发中的应用</title><link href="https://me.bitgong.cn/posts/ECS%E6%9E%B6%E6%9E%84%E5%9C%A8%E6%B8%B8%E6%88%8F%E5%BC%80%E5%8F%91%E4%B8%AD%E7%9A%84%E5%BA%94%E7%94%A8/" rel="alternate" type="text/html" title="ECS架构在游戏开发中的应用" /><published>2025-12-23T10:20:09+00:00</published><updated>2025-12-23T10:20:09+00:00</updated><id>https://me.bitgong.cn/posts/ECS%E6%9E%B6%E6%9E%84%E5%9C%A8%E6%B8%B8%E6%88%8F%E5%BC%80%E5%8F%91%E4%B8%AD%E7%9A%84%E5%BA%94%E7%94%A8</id><content type="html" xml:base="https://me.bitgong.cn/posts/ECS%E6%9E%B6%E6%9E%84%E5%9C%A8%E6%B8%B8%E6%88%8F%E5%BC%80%E5%8F%91%E4%B8%AD%E7%9A%84%E5%BA%94%E7%94%A8/"><![CDATA[<h1 id="我对ecs架构在游戏开发中的理解与应用">我对ECS架构在游戏开发中的理解与应用</h1>

<p>实体组件系统（Entity-Component-System，简称ECS）是我在游戏开发领域中非常推崇的一种架构模式。它通过将数据与行为彻底分离，旨在解决传统面向对象设计中我曾遇到的继承复杂性、耦合度过高以及扩展性不足等问题。在这篇文章中，我将分享我对ECS架构优劣的看法、它适用的游戏项目类型，以及结合我的项目经验和<code class="language-plaintext highlighter-rouge">kunpo-ecs</code>框架，深入探讨ECS架构在游戏开发中的具体应用。</p>

<h2 id="1-ecs架构的优劣">1. ECS架构的优劣</h2>

<h3 id="优点">优点</h3>

<ul>
  <li>
    <p><strong>数据与逻辑的彻底分离（解耦）</strong>: 在我的理解中，ECS最核心的优势就是将数据（组件）和逻辑（系统）彻底分开。实体本身只是一个ID，它没有任何数据或行为，只是组件的容器。这种纯粹的解耦让我的代码变得异常清晰，每个组件都只关注数据，每个系统都只关注处理特定类型组件的逻辑。</p>
  </li>
  <li>
    <p><strong>强大的扩展性</strong>: 由于实体是通过组合不同的组件来获得行为的，我可以在不修改现有代码的情况下，通过添加或移除组件来轻松改变实体的功能。这种高度模块化的设计让我能够非常灵活地扩展游戏功能，比如给敌人添加一个新的行为，只需要给它添加对应的组件和处理该组件的系统即可，无需修改复杂的继承关系。</p>
  </li>
  <li>
    <p><strong>高可维护性</strong>: 因为逻辑与数据分离，并且每个系统都只关注拥有特定组件组合的实体，我的代码结构变得非常清晰。每个系统职责单一，这不仅方便我个人理解和修改代码，也极大地提升了团队协作的效率和项目的长期可维护性。</p>
  </li>
  <li>
    <p><strong>高复用性</strong>: 我发现ECS架构的组件和系统具有极高的复用潜力。由于组件是纯粹的数据结构，而系统是针对特定数据组合的纯逻辑处理，这意味着在我的下一个项目中，大部分通用的组件（如位置、生命值、速度等）和系统（如移动系统、渲染系统、碰撞检测系统等）都可以被直接拿来使用，大大减少了重复开发的工作量，提升了开发效率。</p>
  </li>
  <li>
    <p><strong>数据驱动</strong>: 游戏中的各种行为和状态主要通过系统对数据（组件）的操作来体现，而非通过对象方法调用。这种数据驱动的模式让游戏配置化内容提供了天然的支持。</p>
  </li>
</ul>

<h3 id="缺点">缺点</h3>

<ul>
  <li>
    <p><strong>初期学习曲线和适应成本</strong>: 对于我这样习惯了传统面向对象设计的开发者而言，ECS的思维模式确实需要一些时间来适应。如何合理地拆分组件、设计系统、以及有效地管理实体生命周期，这些都需要通过实践来积累经验。</p>
  </li>
  <li>
    <p><strong>初期开发复杂性</strong>: 在项目初期，ECS架构的基础设施搭建和完善可能需要我投入更多的时间。组件和系统的设计需要我进行前瞻性思考，以避免后期频繁重构。</p>
  </li>
  <li>
    <p><strong>过度设计风险</strong>: 我也清楚，对于一些小型、逻辑相对简单的游戏，如果盲目引入ECS架构，可能会导致不必要的复杂性，反而增加了我的开发成本和维护难度。</p>
  </li>
</ul>

<h2 id="2-ecs架构适用的游戏项目类型">2. ECS架构适用的游戏项目类型</h2>

<p>根据我的经验，ECS架构在以下类型的游戏项目中能发挥巨大优势：</p>

<ul>
  <li>
    <p><strong>需要管理大量动态实体的游戏</strong>: 例如策略游戏（RTS）、模拟经营类游戏、MMORPG等，这些游戏往往需要同时管理成千上万个实体，并且这些实体可能拥有复杂的行为和交互。ECS的解耦和模块化特性让我能够更有效地组织和管理这些海量的游戏对象。</p>
  </li>
  <li>
    <p><strong>物理模拟和复杂AI</strong>: 涉及到大量碰撞检测、路径寻路、行为树等复杂计算的游戏，ECS架构清晰的职责划分让我能够更好地设计和优化这些复杂的系统。</p>
  </li>
  <li>
    <p><strong>数据驱动和高度可配置的游戏</strong>: 如果游戏内容需要高度可配置、易于迭代和修改，ECS的数据驱动特性将极大地帮助我。我可以让游戏策划通过简单的配置数据来调整游戏行为，而无需频繁修改代码。</p>
  </li>
</ul>

<h2 id="3-ecs架构不适用的游戏项目类型">3. ECS架构不适用的游戏项目类型</h2>

<p>尽管我认为ECS优势显著，但它并非我的万金油：</p>

<ul>
  <li>
    <p><strong>实体数量少、逻辑简单的小游戏</strong>: 对于这类游戏，我可能会选择更传统、更快速高效的面向对象或脚本式开发，引入ECS反而会增加不必要的开销。</p>
  </li>
  <li>
    <p><strong>强调美术表现和动画、逻辑相对固定的游戏</strong>: 例如一些剧情驱动的单机游戏，其核心在于高质量的视听体验和预设的剧情流程。这类游戏对逻辑的动态扩展性要求不高，ECS带来的额外设计成本可能不划算。</p>
  </li>
  <li>
    <p><strong>开发周期短，资源有限的项目</strong>: 在这种情况下，我可能会优先选择自己最熟悉、上手最快的架构，以确保项目能够按时交付。学习和实践ECS可能需要额外的投入。</p>
  </li>
</ul>

<h2 id="4-为什么我热衷于使用ecs架构来设计游戏">4. 为什么我热衷于使用ECS架构来设计游戏</h2>

<p>我个人非常热衷于在游戏开发中采用ECS架构，这在我的项目实践中，特别是像《宫爆老奶奶》《比特小队》这样的游戏中得到了充分验证。</p>

<p>《宫爆老奶奶》是一款Roguelike割草游戏，它的核心特点是：</p>

<ul>
  <li>
    <p><strong>大量敌人同屏</strong>: 玩家需要面对源源不断的敌人，数量可达数百甚至上千。</p>
  </li>
  <li>
    <p><strong>丰富的技能和道具</strong>: 玩家可以通过多种技能和道具组合，产生各种华丽的攻击效果。</p>
  </li>
  <li>
    <p><strong>复杂的碰撞和伤害计算</strong>: 技能与敌人之间、敌人与敌人之间都需要进行实时的碰撞检测和伤害计算。</p>
  </li>
</ul>

<p>在传统面向对象架构下，管理如此庞大的对象数量和复杂的交互逻辑，我很容易遭遇维护困难，例如对象间复杂的引用关系、继承链的膨胀以及难以预测的副作用。</p>

<p>而ECS架构完美契合了《宫爆老奶奶》的需求，让我在开发过程中受益匪浅：</p>

<ul>
  <li>
    <p><strong>清晰的职责分离，轻松管理海量实体</strong>: 每个敌人、每个子弹、每个掉落物对我来说都是一个实体。我将它们的位置、生命值、攻击力等纯数据存储在组件中，并通过系统集中处理它们的移动、攻击、销毁等逻辑。这种职责分离让我在面对大量同屏单位时，依然能够保持代码的清晰和逻辑的独立性，从而轻松地管理复杂的游戏状态。</p>
  </li>
  <li>
    <p><strong>灵活的技能和道具系统，强大的扩展性</strong>: 技能和道具对我来说可以被设计为独立的组件。当实体拥有某个技能组件时，相应的系统就会处理其技能逻辑。这种设计让技能的组合和扩展变得异常容易，我可以在不修改核心逻辑的情况下，快速迭代和增加新的技能，且不会导致复杂的继承链。</p>
  </li>
  <li>
    <p><strong>高内聚低耦合的系统设计</strong>: 移动系统只处理带有位置和速度组件的实体；碰撞系统只关注带有碰撞体组件的实体。这种职责单一的设计，让每个系统的实现都非常简洁明了，易于我调试和优化，极大地降低了模块间的耦合。</p>
  </li>
</ul>

<h2 id="5-ecs架构在游戏中的应用">5. ECS架构在游戏中的应用</h2>

<p><code class="language-plaintext highlighter-rouge">kunpo-ecs</code>是一个我用 TypeScript 实现的实体组件系统 (ECS) 框架，它是我在游戏开发中实践ECS架构的一个具体案例。它很好地展示了我是如何将ECS架构的核心思想应用到实际项目中的。</p>

<h3 id="我对kunpo-ecs核心概念的理解与实现">我对<code class="language-plaintext highlighter-rouge">kunpo-ecs</code>核心概念的理解与实现</h3>

<ul>
  <li>
    <p><strong>实体 (Entity)</strong>: 在 <code class="language-plaintext highlighter-rouge">kunpo-ecs</code> 中，实体对我来说就是游戏对象的唯一标识符，本质上是一个纯数字 ID。它们本身不包含任何数据或逻辑，只作为组件的容器，这符合我追求的“纯粹”实体概念。</p>
  </li>
  <li>
    <p><strong>组件 (Component)</strong>: 我强调组件是纯数据结构。在 <code class="language-plaintext highlighter-rouge">kunpo-ecs</code> 中，我使用 <code class="language-plaintext highlighter-rouge">@ecsclass</code> 和 <code class="language-plaintext highlighter-rouge">@ecsprop</code> 装饰器来定义组件及其属性。例如，一个 <code class="language-plaintext highlighter-rouge">PositionComponent</code> 可能包含 <code class="language-plaintext highlighter-rouge">x</code> 和 <code class="language-plaintext highlighter-rouge">y</code> 坐标，一个 <code class="language-plaintext highlighter-rouge">HealthComponent</code> 包含 <code class="language-plaintext highlighter-rouge">currentHealth</code> 和 <code class="language-plaintext highlighter-rouge">maxHealth</code>。我要求组件实现 <code class="language-plaintext highlighter-rouge">reset</code> 方法，以便在对象池回收时重置数据，这有助于提高内存效率并减少GC，尤其是在TypeScript环境中。</p>
  </li>
  <li>
    <p><strong>系统 (System)</strong>: 系统是我放置游戏逻辑的地方。在 <code class="language-plaintext highlighter-rouge">kunpo-ecs</code> 中，我使用 <code class="language-plaintext highlighter-rouge">@ecsystem</code> 装饰器定义系统，并通过 <code class="language-plaintext highlighter-rouge">matcher</code> 配置查询规则。例如，一个 <code class="language-plaintext highlighter-rouge">MovementSystem</code> 可能匹配拥有 <code class="language-plaintext highlighter-rouge">PositionComponent</code> 和 <code class="language-plaintext highlighter-rouge">VelocityComponent</code> 的实体，并在其 <code class="language-plaintext highlighter-rouge">update</code> 方法中更新实体的位置。<code class="language-plaintext highlighter-rouge">kunpo-ecs</code> 的查询系统非常灵活，支持 <code class="language-plaintext highlighter-rouge">allOf</code> (必须包含所有组件), <code class="language-plaintext highlighter-rouge">anyOf</code> (包含任意一个组件), <code class="language-plaintext highlighter-rouge">excludeOf</code> (排除特定组件) 或 <code class="language-plaintext highlighter-rouge">optionalOf</code> (可选包含组件) 等多种匹配模式，这让我能精确地定义系统作用的范围。</p>
  </li>
  <li>
    <p><strong>世界 (World)</strong>: <code class="language-plaintext highlighter-rouge">World</code> 是管理所有实体、组件和系统的容器。我需要创建 <code class="language-plaintext highlighter-rouge">World</code> 实例，注册系统和系统组，并调用 <code class="language-plaintext highlighter-rouge">world.initialize()</code> 初始化。在游戏循环中，我通过调用 <code class="language-plaintext highlighter-rouge">world.update(dt)</code> 来驱动所有系统的逻辑，让它们协同工作。</p>
  </li>
</ul>

<h3 id="kunpo-ecs的特性及其我的应用"><code class="language-plaintext highlighter-rouge">kunpo-ecs</code>的特性及其我的应用</h3>

<ol>
  <li>
    <p><strong>数据编辑器</strong>: <code class="language-plaintext highlighter-rouge">kunpo-ecs</code> 提供了一个基于 Cocos Creator 3.8.6 开发的数据编辑器。这对我来说非常重要，因为它使得游戏策划和开发者可以直观地配置 ECS 数据，例如定义不同类型的实体、为实体预设组件及其初始值，从而实现真正的数据驱动开发，极大地降低了代码修改的频率，提升了迭代效率。</p>
  </li>
  <li>
    <p><strong>内存高效</strong>: 尽管TypeScript环境对缓存命中率的直接提升有限，但<code class="language-plaintext highlighter-rouge">kunpo-ecs</code>通过优化的数据结构和算法，如稀疏集合 + 密集数组的数据布局方式，以及对象池的使用，显著减少了内存开销和垃圾回收（GC）压力。这对我处理大量实体时非常有帮助。</p>
  </li>
  <li>
    <p><strong>简洁API与完整类型支持</strong>: TypeScript 的强类型特性为 <code class="language-plaintext highlighter-rouge">kunpo-ecs</code> 提供了全面的类型安全，这帮助我减少了运行时错误。简洁直观的API设计也降低了我的学习成本，使得我可以快速上手并投入开发。</p>
  </li>
  <li>
    <p><strong>灵活的查询系统</strong>: 强大的实体查询功能是ECS的核心。<code class="language-plaintext highlighter-rouge">kunpo-ecs</code> 的查询器在条件相同时会复用，并且提供了 <code class="language-plaintext highlighter-rouge">iterate</code> 和 <code class="language-plaintext highlighter-rouge">iterate1/2/3/4</code> 等迭代器，实现零GC或少量GC的组件遍历，这对于我构建性能敏感的游戏逻辑至关重要。</p>
  </li>
  <li>
    <p><strong>命令缓冲区</strong>: <code class="language-plaintext highlighter-rouge">kunpo-ecs</code> 采用了命令缓冲区模式来管理实体和组件的变更。这意味着像删除实体、添加/删除组件等操作不会立即执行，而是延迟到下一帧的 <code class="language-plaintext highlighter-rouge">update</code> 之前处理。这种设计避免了在系统迭代过程中修改数据结构可能导致的错误，保证了数据的一致性和系统的稳定性，这在多系统交互时尤其重要。</p>
  </li>
  <li>
    <p><strong>掩码高效匹配</strong>: 我利用掩码机制实现高效的组件匹配，这是ECS框架中常见的优化手段，可以极大地提升系统筛选实体的速度，即使在JavaScript/TypeScript环境中，这也是一个有效的优化。</p>
  </li>
</ol>

<h2 id="6-总结">6. 总结</h2>

<p>在我看来，ECS架构作为一种强大的游戏开发模式，通过其数据与行为分离的核心思想，为我带来了极高的模块化、强大的扩展性、优异的可维护性和显著的高复用性。我发现它特别适用于那些需要处理大量实体、复杂逻辑和高度可扩展性的游戏项目。虽然其学习曲线和初期投入可能相对较高，但一旦我掌握了它，它能极大地提升我的游戏开发效率和质量。</p>

<p>通过我开发的<code class="language-plaintext highlighter-rouge">kunpo-ecs</code>这样的开源项目，我希望能够更深入地理解ECS架构在实际游戏开发中的落地方式，从组件的定义、系统的编写、到世界的管理和内存优化，都能找到清晰的实践路径。展望未来，随着游戏复杂度的不断提升，我相信ECS架构无疑将在游戏开发领域扮演越来越重要的角色，它将是我未来项目中的首选架构之一。</p>

<p><code class="language-plaintext highlighter-rouge">kunpo-ecs</code>github:<code class="language-plaintext highlighter-rouge">https://github.com/gongxh0901/kunpo-ecs</code></p>]]></content><author><name>gongxh</name></author><category term="博客文章" /><category term="ECS" /><category term="游戏架构" /><category term="Cocos Creator" /><category term="kunpo-ecs" /><category term="实体组件系统" /><summary type="html"><![CDATA[深入探讨 ECS（实体组件系统）架构在游戏开发中的应用。从架构优劣分析到适用场景，结合《宫爆老奶奶》《比特小队》项目实践和 kunpo-ecs 开源框架，分享 ECS 架构如何解决传统面向对象设计的继承复杂性、提升游戏开发的扩展性和可维护性。]]></summary></entry><entry><title type="html">自动化打包工具架构设计</title><link href="https://me.bitgong.cn/posts/%E8%87%AA%E5%8A%A8%E5%8C%96%E6%89%93%E5%8C%85%E5%B7%A5%E5%85%B7%E6%9E%B6%E6%9E%84%E8%AE%BE%E8%AE%A1/" rel="alternate" type="text/html" title="自动化打包工具架构设计" /><published>2025-11-27T02:00:00+00:00</published><updated>2025-11-27T02:00:00+00:00</updated><id>https://me.bitgong.cn/posts/%E8%87%AA%E5%8A%A8%E5%8C%96%E6%89%93%E5%8C%85%E5%B7%A5%E5%85%B7%E6%9E%B6%E6%9E%84%E8%AE%BE%E8%AE%A1</id><content type="html" xml:base="https://me.bitgong.cn/posts/%E8%87%AA%E5%8A%A8%E5%8C%96%E6%89%93%E5%8C%85%E5%B7%A5%E5%85%B7%E6%9E%B6%E6%9E%84%E8%AE%BE%E8%AE%A1/"><![CDATA[<h2 id="前言">前言</h2>

<p>说实话，写这个工具纯粹是被逼的。</p>

<p>发版那天晚上，对着10+个渠道的打包列表，我一遍遍地重复着：打开 Creator → 选平台 → 构建 → 等 → 开 Android Studio → 编译 → 签名 → 上传 → 发通知…</p>

<p>到第三个渠道我就开始怀疑人生了，到第七个渠道我开始思考为什么要做游戏开发。</p>

<p>那天晚上我就下定决心：<strong>这种重复劳动必须自动化</strong>。</p>

<p>一条命令，所有渠道自动打包、自动上传、自动通知。我该干嘛干嘛去，等着收结束消息就行。</p>

<h2 id="一先聊聊我想解决什么问题">一、先聊聊我想解决什么问题</h2>

<p>在动手之前，我先梳理了一下痛点：</p>

<ol>
  <li>
    <p><strong>渠道太多</strong>：Android 官方包、TapTap、抖音APP、微信小游戏、抖音小游戏、支付宝小游戏、华为快游戏、OPPO快游戏、小米快游戏、鸿蒙…</p>
  </li>
  <li>
    <p><strong>流程重复</strong>：每个渠道的打包流程大同小异，但手动操作每次都要从头来</p>
  </li>
  <li>
    <p><strong>容易出错</strong>：手动切换配置、手动签名，稍不注意就搞错</p>
  </li>
  <li>
    <p><strong>耗时太长</strong>：一个渠道打完要好几分钟，10个渠道就是半天起步</p>
  </li>
</ol>

<p>所以我的目标很明确：<strong>一条命令搞定所有事情</strong>。</p>

<h2 id="二核心设计思路pipeline-模式">二、核心设计思路：Pipeline 模式</h2>

<h3 id="21-为什么选-pipeline">2.1 为什么选 Pipeline？</h3>

<p>最开始我也想过用一个大函数把所有逻辑写进去，但很快就发现不行。</p>

<p>不同平台的打包流程差异很大：</p>

<ul>
  <li>Android：Creator 构建 → 修改配置 → Gradle 编译 → 签名 → 上传</li>
  <li>微信小游戏：Creator 构建 → 上传远程资源 → 调用微信CI上传</li>
  <li>鸿蒙：Creator 构建 → 修改配置 → hvigorw 编译 → 签名 → 上传</li>
</ul>

<p>如果用一个大函数，代码里全是 <code class="language-plaintext highlighter-rouge">if (platform === 'android') {...}</code>，维护起来简直是噩梦。</p>

<p>于是我想到了 <strong>Pipeline 模式</strong>：把构建流程拆解为独立的步骤（Step），通过 Pipeline 串联执行。</p>

<h3 id="22-pipeline-长什么样">2.2 Pipeline 长什么样？</h3>

<p>说白了就是把打包流程拆成一个个小步骤，然后按顺序执行：</p>

<div class="language-javascript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// Android 打包流程</span>
<span class="kd">class</span> <span class="nx">BuilderAndroid</span> <span class="p">{</span>
    <span class="nx">buildPipeline</span><span class="p">()</span> <span class="p">{</span>
        <span class="k">return</span> <span class="p">[</span>
            <span class="k">new</span> <span class="nx">ModifyVersionJsonStep</span><span class="p">(),</span>      <span class="c1">// 修改版本号</span>
            <span class="k">new</span> <span class="nx">BuildCreatorStep</span><span class="p">(),</span>           <span class="c1">// Creator 构建</span>
            <span class="k">new</span> <span class="nx">ModifyMainJsStep</span><span class="p">(),</span>           <span class="c1">// 修改 main.js（热更新用）</span>
            <span class="k">new</span> <span class="nx">GenerateManifestStep</span><span class="p">(),</span>       <span class="c1">// 生成 manifest</span>
            <span class="k">new</span> <span class="nx">ModifyNativeConfigAndroid</span><span class="p">(),</span>  <span class="c1">// 修改 Android 原生配置</span>
            <span class="k">new</span> <span class="nx">CompileAndroid</span><span class="p">(),</span>             <span class="c1">// Gradle 编译</span>
            <span class="k">new</span> <span class="nx">CopyArtifactStep</span><span class="p">(</span><span class="dl">'</span><span class="s1">apk</span><span class="dl">'</span><span class="p">),</span>      <span class="c1">// 拷贝 APK</span>
            <span class="k">new</span> <span class="nx">SignApkStep</span><span class="p">(),</span>                <span class="c1">// 签名</span>
            <span class="k">new</span> <span class="nx">UploadArtifactStep</span><span class="p">()</span>          <span class="c1">// 上传 CDN</span>
        <span class="p">];</span>
    <span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>

<div class="language-javascript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// 微信小游戏打包流程</span>
<span class="kd">class</span> <span class="nx">BuilderWechat</span> <span class="p">{</span>
    <span class="nx">buildPipeline</span><span class="p">()</span> <span class="p">{</span>
        <span class="k">return</span> <span class="p">[</span>
            <span class="k">new</span> <span class="nx">ModifyVersionJsonStep</span><span class="p">(),</span>   <span class="c1">// 修改版本号</span>
            <span class="k">new</span> <span class="nx">BuildCreatorStep</span><span class="p">(),</span>        <span class="c1">// Creator 构建</span>
            <span class="k">new</span> <span class="nx">UploadRemoteResStep</span><span class="p">(),</span>     <span class="c1">// 上传远程资源到 CDN</span>
            <span class="k">new</span> <span class="nx">CompileWechat</span><span class="p">()</span>            <span class="c1">// 调用微信CI上传到后台</span>
        <span class="p">];</span>
    <span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>

<p>看到没？两个平台共用了 <code class="language-plaintext highlighter-rouge">ModifyVersionJsonStep</code> 和 <code class="language-plaintext highlighter-rouge">BuildCreatorStep</code>，但后面的步骤完全不同。</p>

<p>这种设计的好处就是：</p>
<ul>
  <li><strong>步骤复用</strong>：相同的步骤在不同 Builder 中复用</li>
  <li><strong>易于扩展</strong>：新增渠道 = 组装新 Pipeline，不用改已有代码</li>
  <li><strong>流程清晰</strong>：看 Pipeline 就知道整个构建流程</li>
</ul>

<h2 id="三核心组件设计">三、核心组件设计</h2>

<h3 id="31-context数据容器">3.1 Context：数据容器</h3>

<p>这是我从 Linus 那句话学来的：<strong>数据结构比代码重要</strong>。</p>

<p>Context 就是一个数据容器，所有步骤共享同一个 Context，通过它传递数据：</p>

<div class="language-javascript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">class</span> <span class="nx">Context</span> <span class="p">{</span>
    <span class="err">#</span><span class="nx">data</span> <span class="o">=</span> <span class="k">new</span> <span class="nb">Map</span><span class="p">();</span>  <span class="c1">// 用 Map 存储，性能更好</span>
    
    <span class="kd">constructor</span><span class="p">(</span><span class="nx">params</span><span class="p">)</span> <span class="p">{</span>
        <span class="c1">// 存储构建参数</span>
        <span class="k">this</span><span class="p">.</span><span class="kd">set</span><span class="p">(</span><span class="dl">'</span><span class="s1">channel</span><span class="dl">'</span><span class="p">,</span> <span class="nx">params</span><span class="p">.</span><span class="nx">channel</span><span class="p">);</span>     <span class="c1">// 渠道</span>
        <span class="k">this</span><span class="p">.</span><span class="kd">set</span><span class="p">(</span><span class="dl">'</span><span class="s1">platform</span><span class="dl">'</span><span class="p">,</span> <span class="nx">params</span><span class="p">.</span><span class="nx">platform</span><span class="p">);</span>   <span class="c1">// 平台</span>
        <span class="k">this</span><span class="p">.</span><span class="kd">set</span><span class="p">(</span><span class="dl">'</span><span class="s1">version</span><span class="dl">'</span><span class="p">,</span> <span class="nx">params</span><span class="p">.</span><span class="nx">version</span><span class="p">);</span>     <span class="c1">// 版本号</span>
        <span class="k">this</span><span class="p">.</span><span class="kd">set</span><span class="p">(</span><span class="dl">'</span><span class="s1">mode</span><span class="dl">'</span><span class="p">,</span> <span class="nx">params</span><span class="p">.</span><span class="nx">mode</span><span class="p">);</span>           <span class="c1">// debug/release</span>
        <span class="k">this</span><span class="p">.</span><span class="kd">set</span><span class="p">(</span><span class="dl">'</span><span class="s1">build</span><span class="dl">'</span><span class="p">,</span> <span class="nx">params</span><span class="p">.</span><span class="nx">build</span><span class="p">);</span>         <span class="c1">// 构建号</span>
        <span class="c1">// ...</span>
    <span class="p">}</span>
    
    <span class="c1">// 基础操作</span>
    <span class="kd">get</span><span class="p">(</span><span class="nx">key</span><span class="p">)</span> <span class="p">{</span> <span class="k">return</span> <span class="k">this</span><span class="p">.</span><span class="err">#</span><span class="nx">data</span><span class="p">.</span><span class="kd">get</span><span class="p">(</span><span class="nx">key</span><span class="p">);</span> <span class="p">}</span>
    <span class="kd">set</span><span class="p">(</span><span class="nx">key</span><span class="p">,</span> <span class="nx">value</span><span class="p">)</span> <span class="p">{</span> <span class="k">this</span><span class="p">.</span><span class="err">#</span><span class="nx">data</span><span class="p">.</span><span class="kd">set</span><span class="p">(</span><span class="nx">key</span><span class="p">,</span> <span class="nx">value</span><span class="p">);</span> <span class="p">}</span>
    
    <span class="c1">// 步骤结果管理</span>
    <span class="nx">setStepResult</span><span class="p">(</span><span class="nx">stepName</span><span class="p">,</span> <span class="nx">result</span><span class="p">)</span> <span class="p">{</span> <span class="p">...</span> <span class="p">}</span>
    <span class="nx">getStepResult</span><span class="p">(</span><span class="nx">stepName</span><span class="p">)</span> <span class="p">{</span> <span class="p">...</span> <span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>

<p>为啥要这么设计？</p>

<ol>
  <li><strong>单一数据源</strong>：所有步骤共享同一个 Context，避免参数传递</li>
  <li><strong>解耦</strong>：Step 之间不直接依赖，只依赖 Context 中的数据</li>
  <li><strong>可追溯</strong>：Context 记录了整个构建过程的所有信息</li>
</ol>

<p>举个例子，<code class="language-plaintext highlighter-rouge">CompileAndroid</code> 步骤编译完 APK 后，把产物路径写入 Context：</p>

<div class="language-javascript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nx">context</span><span class="p">.</span><span class="kd">set</span><span class="p">(</span><span class="dl">'</span><span class="s1">OUTPUT_NAME</span><span class="dl">'</span><span class="p">,</span> <span class="dl">'</span><span class="s1">app-release.apk</span><span class="dl">'</span><span class="p">);</span>
</code></pre></div></div>

<p>后面的 <code class="language-plaintext highlighter-rouge">SignApkStep</code> 直接从 Context 读取：</p>

<div class="language-javascript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">const</span> <span class="nx">apkPath</span> <span class="o">=</span> <span class="nx">context</span><span class="p">.</span><span class="kd">get</span><span class="p">(</span><span class="dl">'</span><span class="s1">OUTPUT_NAME</span><span class="dl">'</span><span class="p">);</span>
</code></pre></div></div>

<p>Step 之间完全解耦，互不依赖。</p>

<h3 id="32-step步骤基类">3.2 Step：步骤基类</h3>

<p>每个 Step 只做一件事，保持简单。我用了模板方法模式：</p>

<div class="language-javascript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">class</span> <span class="nx">Step</span> <span class="p">{</span>
    <span class="c1">// 步骤名称（子类必须实现）</span>
    <span class="kd">get</span> <span class="nx">name</span><span class="p">()</span> <span class="p">{</span>
        <span class="k">throw</span> <span class="k">new</span> <span class="nb">Error</span><span class="p">(</span><span class="dl">'</span><span class="s1">子类必须实现 name</span><span class="dl">'</span><span class="p">);</span>
    <span class="p">}</span>
    
    <span class="c1">// 判断是否可跳过（子类可选实现）</span>
    <span class="nx">canSkip</span><span class="p">(</span><span class="nx">context</span><span class="p">)</span> <span class="p">{</span>
        <span class="k">return</span> <span class="nx">context</span><span class="p">.</span><span class="nx">isStepExecuted</span><span class="p">(</span><span class="k">this</span><span class="p">.</span><span class="nx">name</span><span class="p">);</span>  <span class="c1">// 默认：已执行过则跳过</span>
    <span class="p">}</span>
    
    <span class="c1">// 执行步骤（模板方法，统一处理日志和错误）</span>
    <span class="k">async</span> <span class="nx">execute</span><span class="p">(</span><span class="nx">context</span><span class="p">)</span> <span class="p">{</span>
        <span class="k">if</span> <span class="p">(</span><span class="k">this</span><span class="p">.</span><span class="nx">canSkip</span><span class="p">(</span><span class="nx">context</span><span class="p">))</span> <span class="p">{</span>
            <span class="nx">Logger</span><span class="p">.</span><span class="nx">log</span><span class="p">(</span><span class="s2">`⊳ 跳过步骤: </span><span class="p">${</span><span class="k">this</span><span class="p">.</span><span class="nx">name</span><span class="p">}</span><span class="s2">`</span><span class="p">);</span>
            <span class="k">return</span> <span class="nx">context</span><span class="p">.</span><span class="nx">getStepResult</span><span class="p">(</span><span class="k">this</span><span class="p">.</span><span class="nx">name</span><span class="p">);</span>
        <span class="p">}</span>
        
        <span class="k">try</span> <span class="p">{</span>
            <span class="nx">Logger</span><span class="p">.</span><span class="nx">log</span><span class="p">(</span><span class="s2">`▶ 开始执行: </span><span class="p">${</span><span class="k">this</span><span class="p">.</span><span class="nx">name</span><span class="p">}</span><span class="s2">`</span><span class="p">);</span>
            <span class="kd">const</span> <span class="nx">startTime</span> <span class="o">=</span> <span class="nb">Date</span><span class="p">.</span><span class="nx">now</span><span class="p">();</span>
            
            <span class="kd">const</span> <span class="nx">result</span> <span class="o">=</span> <span class="k">await</span> <span class="k">this</span><span class="p">.</span><span class="nx">run</span><span class="p">(</span><span class="nx">context</span><span class="p">);</span>  <span class="c1">// 调用子类实现</span>
            
            <span class="kd">const</span> <span class="nx">duration</span> <span class="o">=</span> <span class="p">((</span><span class="nb">Date</span><span class="p">.</span><span class="nx">now</span><span class="p">()</span> <span class="o">-</span> <span class="nx">startTime</span><span class="p">)</span> <span class="o">/</span> <span class="mi">1000</span><span class="p">).</span><span class="nx">toFixed</span><span class="p">(</span><span class="mi">2</span><span class="p">);</span>
            <span class="nx">Logger</span><span class="p">.</span><span class="nx">success</span><span class="p">(</span><span class="s2">`✓ 完成: </span><span class="p">${</span><span class="k">this</span><span class="p">.</span><span class="nx">name</span><span class="p">}</span><span class="s2"> (耗时: </span><span class="p">${</span><span class="nx">duration</span><span class="p">}</span><span class="s2">s)`</span><span class="p">);</span>
            
            <span class="nx">context</span><span class="p">.</span><span class="nx">setStepResult</span><span class="p">(</span><span class="k">this</span><span class="p">.</span><span class="nx">name</span><span class="p">,</span> <span class="nx">result</span><span class="p">);</span>
            <span class="nx">context</span><span class="p">.</span><span class="nx">markStepExecuted</span><span class="p">(</span><span class="k">this</span><span class="p">.</span><span class="nx">name</span><span class="p">);</span>
            <span class="k">return</span> <span class="nx">result</span><span class="p">;</span>
        <span class="p">}</span> <span class="k">catch</span> <span class="p">(</span><span class="nx">error</span><span class="p">)</span> <span class="p">{</span>
            <span class="nx">Logger</span><span class="p">.</span><span class="nx">error</span><span class="p">(</span><span class="s2">`✗ 失败: </span><span class="p">${</span><span class="k">this</span><span class="p">.</span><span class="nx">name</span><span class="p">}</span><span class="s2">`</span><span class="p">);</span>
            <span class="k">throw</span> <span class="nx">error</span><span class="p">;</span>
        <span class="p">}</span>
    <span class="p">}</span>
    
    <span class="c1">// 实际执行逻辑（子类必须实现）</span>
    <span class="k">async</span> <span class="nx">run</span><span class="p">(</span><span class="nx">context</span><span class="p">)</span> <span class="p">{</span>
        <span class="k">throw</span> <span class="k">new</span> <span class="nb">Error</span><span class="p">(</span><span class="dl">'</span><span class="s1">子类必须实现 run()</span><span class="dl">'</span><span class="p">);</span>
    <span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>

<p>基类帮你处理了日志、计时、错误处理、结果缓存，子类只需要专注于业务逻辑。</p>

<h3 id="33-pipeline执行器">3.3 Pipeline：执行器</h3>

<p>Pipeline 的实现超级简单，就是按顺序执行 Step 数组：</p>

<div class="language-javascript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">class</span> <span class="nx">Pipeline</span> <span class="p">{</span>
    <span class="err">#</span><span class="nx">steps</span> <span class="o">=</span> <span class="p">[];</span>
    
    <span class="kd">constructor</span><span class="p">(</span><span class="nx">steps</span> <span class="o">=</span> <span class="p">[])</span> <span class="p">{</span>
        <span class="k">this</span><span class="p">.</span><span class="err">#</span><span class="nx">steps</span> <span class="o">=</span> <span class="nx">steps</span><span class="p">;</span>
    <span class="p">}</span>
    
    <span class="k">async</span> <span class="nx">execute</span><span class="p">(</span><span class="nx">context</span><span class="p">)</span> <span class="p">{</span>
        <span class="nx">Logger</span><span class="p">.</span><span class="nx">blue</span><span class="p">(</span><span class="dl">'</span><span class="s1">==================== Pipeline 开始执行 ====================</span><span class="dl">'</span><span class="p">);</span>
        <span class="nx">Logger</span><span class="p">.</span><span class="nx">log</span><span class="p">(</span><span class="s2">`总步骤数: </span><span class="p">${</span><span class="k">this</span><span class="p">.</span><span class="err">#</span><span class="nx">steps</span><span class="p">.</span><span class="nx">length</span><span class="p">}</span><span class="s2">`</span><span class="p">);</span>
        
        <span class="kd">const</span> <span class="nx">startTime</span> <span class="o">=</span> <span class="nb">Date</span><span class="p">.</span><span class="nx">now</span><span class="p">();</span>
        
        <span class="k">for</span> <span class="p">(</span><span class="kd">let</span> <span class="nx">i</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span> <span class="nx">i</span> <span class="o">&lt;</span> <span class="k">this</span><span class="p">.</span><span class="err">#</span><span class="nx">steps</span><span class="p">.</span><span class="nx">length</span><span class="p">;</span> <span class="nx">i</span><span class="o">++</span><span class="p">)</span> <span class="p">{</span>
            <span class="kd">const</span> <span class="nx">step</span> <span class="o">=</span> <span class="k">this</span><span class="p">.</span><span class="err">#</span><span class="nx">steps</span><span class="p">[</span><span class="nx">i</span><span class="p">];</span>
            <span class="nx">Logger</span><span class="p">.</span><span class="nx">log</span><span class="p">(</span><span class="s2">`\n[</span><span class="p">${</span><span class="nx">i</span> <span class="o">+</span> <span class="mi">1</span><span class="p">}</span><span class="s2">/</span><span class="p">${</span><span class="k">this</span><span class="p">.</span><span class="err">#</span><span class="nx">steps</span><span class="p">.</span><span class="nx">length</span><span class="p">}</span><span class="s2">] </span><span class="p">${</span><span class="nx">step</span><span class="p">.</span><span class="nx">name</span><span class="p">}</span><span class="s2">`</span><span class="p">);</span>
            <span class="k">await</span> <span class="nx">step</span><span class="p">.</span><span class="nx">execute</span><span class="p">(</span><span class="nx">context</span><span class="p">);</span>  <span class="c1">// 每个步骤共享同一个 context</span>
        <span class="p">}</span>
        
        <span class="kd">const</span> <span class="nx">totalDuration</span> <span class="o">=</span> <span class="p">((</span><span class="nb">Date</span><span class="p">.</span><span class="nx">now</span><span class="p">()</span> <span class="o">-</span> <span class="nx">startTime</span><span class="p">)</span> <span class="o">/</span> <span class="mi">1000</span><span class="p">).</span><span class="nx">toFixed</span><span class="p">(</span><span class="mi">2</span><span class="p">);</span>
        <span class="nx">Logger</span><span class="p">.</span><span class="nx">success</span><span class="p">(</span><span class="s2">`✓ 总耗时: </span><span class="p">${</span><span class="nx">totalDuration</span><span class="p">}</span><span class="s2">s`</span><span class="p">);</span>
    <span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>

<h3 id="34-builderfactory工厂类">3.4 BuilderFactory：工厂类</h3>

<p>这里我用了数据驱动的方式，消除了一堆 if/else：</p>

<div class="language-javascript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">class</span> <span class="nx">BuilderFactory</span> <span class="p">{</span>
    <span class="c1">// 平台 → Builder 映射表</span>
    <span class="kd">static</span> <span class="err">#</span><span class="nx">builderMap</span> <span class="o">=</span> <span class="p">{</span>
        <span class="dl">'</span><span class="s1">android</span><span class="dl">'</span><span class="p">:</span> <span class="nx">BuilderAndroid</span><span class="p">,</span>
        <span class="dl">'</span><span class="s1">ios</span><span class="dl">'</span><span class="p">:</span> <span class="nx">BuilderIOS</span><span class="p">,</span>
        <span class="dl">'</span><span class="s1">harmonyos-next</span><span class="dl">'</span><span class="p">:</span> <span class="nx">BuilderHarmony</span><span class="p">,</span>
        <span class="dl">'</span><span class="s1">wechatgame</span><span class="dl">'</span><span class="p">:</span> <span class="nx">BuilderWechat</span><span class="p">,</span>
        <span class="dl">'</span><span class="s1">alipay-mini-game</span><span class="dl">'</span><span class="p">:</span> <span class="nx">BuilderAlipay</span><span class="p">,</span>
        <span class="dl">'</span><span class="s1">bytedance-mini-game</span><span class="dl">'</span><span class="p">:</span> <span class="nx">BuilderBytedance</span><span class="p">,</span>
        <span class="dl">'</span><span class="s1">huawei-quick-game</span><span class="dl">'</span><span class="p">:</span> <span class="nx">BuilderHuaweiQuick</span><span class="p">,</span>
        <span class="dl">'</span><span class="s1">oppo-mini-game</span><span class="dl">'</span><span class="p">:</span> <span class="nx">BuilderOppoQuick</span><span class="p">,</span>
        <span class="dl">'</span><span class="s1">xiaomi-quick-game</span><span class="dl">'</span><span class="p">:</span> <span class="nx">BuilderXiaomiQuick</span><span class="p">,</span>
        <span class="dl">'</span><span class="s1">vivo-mini-game</span><span class="dl">'</span><span class="p">:</span> <span class="nx">BuilderVivoQuick</span>
    <span class="p">};</span>
    
    <span class="kd">static</span> <span class="nx">createByPlatform</span><span class="p">(</span><span class="nx">platform</span><span class="p">,</span> <span class="nx">params</span><span class="p">)</span> <span class="p">{</span>
        <span class="kd">const</span> <span class="nx">BuilderClass</span> <span class="o">=</span> <span class="k">this</span><span class="p">.</span><span class="err">#</span><span class="nx">builderMap</span><span class="p">[</span><span class="nx">platform</span><span class="p">];</span>
        <span class="k">if</span> <span class="p">(</span><span class="o">!</span><span class="nx">BuilderClass</span><span class="p">)</span> <span class="p">{</span>
            <span class="k">throw</span> <span class="k">new</span> <span class="nb">Error</span><span class="p">(</span><span class="s2">`不支持的平台: </span><span class="p">${</span><span class="nx">platform</span><span class="p">}</span><span class="s2">`</span><span class="p">);</span>
        <span class="p">}</span>
        <span class="k">return</span> <span class="k">new</span> <span class="nx">BuilderClass</span><span class="p">(</span><span class="nx">params</span><span class="p">);</span>
    <span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>

<p>新增平台只需要往 Map 里加一行，不改逻辑。</p>

<h2 id="四关键优化buildgroup-共享机制">四、关键优化：buildGroup 共享机制</h2>

<p>这个优化真的让我省了不少时间。</p>

<h3 id="41-问题背景">4.1 问题背景</h3>

<p>Android 平台有多个渠道：官方包、TapTap、抖音APP…</p>

<p>它们都要先执行 Creator 构建，而 Creator 构建是最耗时的（5分钟起步）。</p>

<p>如果打3个 Android 渠道，就要执行3次 Creator 构建，15分钟就没了。</p>

<p>但实际上，<strong>同平台的渠道可以共享 Creator 构建产物</strong>！</p>

<h3 id="42-解决方案">4.2 解决方案</h3>

<p>在配置文件里，给同平台的渠道设置相同的 <code class="language-plaintext highlighter-rouge">group</code>：</p>

<div class="language-json highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">[</span><span class="w">
    </span><span class="p">{</span><span class="w"> </span><span class="nl">"channel"</span><span class="p">:</span><span class="w"> </span><span class="s2">"official"</span><span class="p">,</span><span class="w"> </span><span class="nl">"platform"</span><span class="p">:</span><span class="w"> </span><span class="s2">"android"</span><span class="p">,</span><span class="w"> </span><span class="nl">"group"</span><span class="p">:</span><span class="w"> </span><span class="s2">"android"</span><span class="w"> </span><span class="p">},</span><span class="w">
    </span><span class="p">{</span><span class="w"> </span><span class="nl">"channel"</span><span class="p">:</span><span class="w"> </span><span class="s2">"taptap"</span><span class="p">,</span><span class="w"> </span><span class="nl">"platform"</span><span class="p">:</span><span class="w"> </span><span class="s2">"android"</span><span class="p">,</span><span class="w"> </span><span class="nl">"group"</span><span class="p">:</span><span class="w"> </span><span class="s2">"android"</span><span class="w"> </span><span class="p">},</span><span class="w">
    </span><span class="p">{</span><span class="w"> </span><span class="nl">"channel"</span><span class="p">:</span><span class="w"> </span><span class="s2">"douyin"</span><span class="p">,</span><span class="w"> </span><span class="nl">"platform"</span><span class="p">:</span><span class="w"> </span><span class="s2">"android"</span><span class="p">,</span><span class="w"> </span><span class="nl">"group"</span><span class="p">:</span><span class="w"> </span><span class="s2">"android"</span><span class="w"> </span><span class="p">}</span><span class="w">
</span><span class="p">]</span><span class="w">
</span></code></pre></div></div>

<p>然后在 <code class="language-plaintext highlighter-rouge">BuildCreatorStep</code> 里实现跳过逻辑：</p>

<div class="language-javascript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">class</span> <span class="nx">BuildCreatorStep</span> <span class="kd">extends</span> <span class="nx">Step</span> <span class="p">{</span>
    <span class="nx">canSkip</span><span class="p">(</span><span class="nx">context</span><span class="p">)</span> <span class="p">{</span>
        <span class="kd">const</span> <span class="nx">channel</span> <span class="o">=</span> <span class="nx">context</span><span class="p">.</span><span class="kd">get</span><span class="p">(</span><span class="dl">'</span><span class="s1">channel</span><span class="dl">'</span><span class="p">);</span>
        <span class="kd">const</span> <span class="nx">buildGroup</span> <span class="o">=</span> <span class="nx">DataHelper</span><span class="p">.</span><span class="nx">channels</span><span class="p">.</span><span class="nx">getBuildGroup</span><span class="p">(</span><span class="nx">channel</span><span class="p">);</span>
        
        <span class="c1">// 检查该 buildGroup 是否已经构建过</span>
        <span class="k">if</span> <span class="p">(</span><span class="nx">buildGroup</span> <span class="o">&amp;&amp;</span> <span class="nx">BuildCache</span><span class="p">.</span><span class="nx">isGroupBuilt</span><span class="p">(</span><span class="nx">buildGroup</span><span class="p">))</span> <span class="p">{</span>
            <span class="nx">Logger</span><span class="p">.</span><span class="nx">log</span><span class="p">(</span><span class="s2">`跳过 Creator 构建：buildGroup [</span><span class="p">${</span><span class="nx">buildGroup</span><span class="p">}</span><span class="s2">] 已构建（共享产物）`</span><span class="p">);</span>
            <span class="k">return</span> <span class="kc">true</span><span class="p">;</span>
        <span class="p">}</span>
        <span class="k">return</span> <span class="kc">false</span><span class="p">;</span>
    <span class="p">}</span>
    
    <span class="k">async</span> <span class="nx">run</span><span class="p">(</span><span class="nx">context</span><span class="p">)</span> <span class="p">{</span>
        <span class="c1">// 执行 Creator 构建</span>
        <span class="k">await</span> <span class="nx">runCreatorBuild</span><span class="p">(...);</span>
        
        <span class="c1">// 标记 buildGroup 已构建</span>
        <span class="kd">const</span> <span class="nx">buildGroup</span> <span class="o">=</span> <span class="nx">DataHelper</span><span class="p">.</span><span class="nx">channels</span><span class="p">.</span><span class="nx">getBuildGroup</span><span class="p">(</span><span class="nx">channel</span><span class="p">);</span>
        <span class="k">if</span> <span class="p">(</span><span class="nx">buildGroup</span><span class="p">)</span> <span class="p">{</span>
            <span class="nx">BuildCache</span><span class="p">.</span><span class="nx">markGroupBuilt</span><span class="p">(</span><span class="nx">buildGroup</span><span class="p">,</span> <span class="nx">channel</span><span class="p">);</span>
        <span class="p">}</span>
    <span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>

<p>效果：</p>
<ul>
  <li>打包 <code class="language-plaintext highlighter-rouge">official</code> 渠道：执行 Creator 构建，标记 <code class="language-plaintext highlighter-rouge">android</code> 组已构建</li>
  <li>打包 <code class="language-plaintext highlighter-rouge">taptap</code> 渠道：检测到 <code class="language-plaintext highlighter-rouge">android</code> 组已构建，<strong>跳过 Creator 构建</strong></li>
  <li>打包 <code class="language-plaintext highlighter-rouge">douyin</code> 渠道：检测到 <code class="language-plaintext highlighter-rouge">android</code> 组已构建，<strong>跳过 Creator 构建</strong></li>
</ul>

<p>3个渠道只需要1次 Creator 构建，从15分钟优化到5分钟！</p>

<h3 id="43-buildcache-实现">4.3 BuildCache 实现</h3>

<div class="language-javascript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">class</span> <span class="nx">BuildCache</span> <span class="p">{</span>
    <span class="kd">static</span> <span class="err">#</span><span class="nx">builtGroups</span> <span class="o">=</span> <span class="k">new</span> <span class="nb">Map</span><span class="p">();</span>  <span class="c1">// 已构建的 buildGroup</span>
    
    <span class="kd">static</span> <span class="nx">isGroupBuilt</span><span class="p">(</span><span class="nx">buildGroup</span><span class="p">)</span> <span class="p">{</span>
        <span class="k">return</span> <span class="k">this</span><span class="p">.</span><span class="err">#</span><span class="nx">builtGroups</span><span class="p">.</span><span class="nx">has</span><span class="p">(</span><span class="nx">buildGroup</span><span class="p">);</span>
    <span class="p">}</span>
    
    <span class="kd">static</span> <span class="nx">markGroupBuilt</span><span class="p">(</span><span class="nx">buildGroup</span><span class="p">,</span> <span class="nx">channel</span><span class="p">)</span> <span class="p">{</span>
        <span class="k">if</span> <span class="p">(</span><span class="o">!</span><span class="k">this</span><span class="p">.</span><span class="err">#</span><span class="nx">builtGroups</span><span class="p">.</span><span class="nx">has</span><span class="p">(</span><span class="nx">buildGroup</span><span class="p">))</span> <span class="p">{</span>
            <span class="k">this</span><span class="p">.</span><span class="err">#</span><span class="nx">builtGroups</span><span class="p">.</span><span class="kd">set</span><span class="p">(</span><span class="nx">buildGroup</span><span class="p">,</span> <span class="p">{</span>
                <span class="na">builtBy</span><span class="p">:</span> <span class="nx">channel</span><span class="p">,</span>   <span class="c1">// 首次构建的渠道</span>
                <span class="na">sharedBy</span><span class="p">:</span> <span class="p">[]</span>        <span class="c1">// 共享的渠道列表</span>
            <span class="p">});</span>
        <span class="p">}</span> <span class="k">else</span> <span class="p">{</span>
            <span class="kd">const</span> <span class="nx">info</span> <span class="o">=</span> <span class="k">this</span><span class="p">.</span><span class="err">#</span><span class="nx">builtGroups</span><span class="p">.</span><span class="kd">get</span><span class="p">(</span><span class="nx">buildGroup</span><span class="p">);</span>
            <span class="nx">info</span><span class="p">.</span><span class="nx">sharedBy</span><span class="p">.</span><span class="nx">push</span><span class="p">(</span><span class="nx">channel</span><span class="p">);</span>
        <span class="p">}</span>
    <span class="p">}</span>
    
    <span class="kd">static</span> <span class="nx">clear</span><span class="p">()</span> <span class="p">{</span>
        <span class="k">this</span><span class="p">.</span><span class="err">#</span><span class="nx">builtGroups</span><span class="p">.</span><span class="nx">clear</span><span class="p">();</span>  <span class="c1">// 每次批量构建开始时清空</span>
    <span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>

<p>批量打包结束后，还会打印共享汇总：</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>==================== Creator 构建共享 ====================
[android]
  构建者: official
  共享者: taptap, douyin (节省 2 次 Creator 构建)
</code></pre></div></div>

<h2 id="五命令行入口设计">五、命令行入口设计</h2>

<p>我设计了两种使用方式：</p>

<h3 id="51-交互式打包">5.1 交互式打包</h3>

<p>运行 <code class="language-plaintext highlighter-rouge">npm run build</code>，会弹出交互式界面：</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>? 请选择渠道类型: <span class="o">(</span>Use arrow keys<span class="o">)</span>
  ━━━━━━━━ 渠道分组 ━━━━━━━━
  🌍 全部渠道 <span class="o">(</span>all<span class="o">)</span>
  🎮 小游戏 <span class="o">(</span>minigame<span class="o">)</span>
  ⚡ 快游戏 <span class="o">(</span>quickgame<span class="o">)</span>
  📱 原生平台 <span class="o">(</span>native<span class="o">)</span>
  ━━━━━━━━ 单个渠道 ━━━━━━━━
❯ 官方android <span class="o">(</span>official<span class="o">)</span>
  TapTap <span class="o">(</span>taptap<span class="o">)</span>
  微信小游戏 <span class="o">(</span>wechatgame<span class="o">)</span>
  ...
</code></pre></div></div>

<p>按上下键切换选中渠道，回车确认，继续输入版本号、构建号、选择模式等。</p>

<p>这种方式适合开发时用，直观方便。</p>

<h3 id="52-命令式打包cicd">5.2 命令式打包（CI/CD）</h3>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>npm run build:jenkins <span class="nt">--</span> <span class="se">\</span>
  <span class="nt">-p</span> official,taptap <span class="se">\</span>
  <span class="nt">-v</span> 1.0.0 <span class="se">\</span>
  <span class="nt">-b</span> 100 <span class="se">\</span>
  <span class="nt">-d</span> <span class="nb">false</span> <span class="se">\</span>
  <span class="nt">-r</span> 15 <span class="se">\</span>
  <span class="nt">-m</span> <span class="s2">"修复bug"</span> <span class="se">\</span>
  <span class="nt">-n</span> <span class="nb">true</span>
</code></pre></div></div>

<p>参数都通过命令行传入，适合 Jenkins 等 CI 系统调用。</p>

<h3 id="53-渠道分组">5.3 渠道分组</h3>

<p>为了方便批量打包，我还设计了渠道分组：</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nt">-p</span> all          <span class="c"># 所有渠道</span>
<span class="nt">-p</span> minigame     <span class="c"># 小游戏渠道（微信、抖音、支付宝）</span>
<span class="nt">-p</span> quickgame    <span class="c"># 快游戏（华为、oppo、vivo、小米）</span>
<span class="nt">-p</span> native       <span class="c"># 原生平台（Android、iOS、鸿蒙）</span>
<span class="nt">-p</span> official,taptap  <span class="c"># 指定多个渠道</span>
</code></pre></div></div>

<p>实现也很简单：</p>

<div class="language-javascript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">class</span> <span class="nx">ChannelParser</span> <span class="p">{</span>
    <span class="kd">static</span> <span class="nx">parseChannels</span><span class="p">(</span><span class="nx">channelInput</span><span class="p">)</span> <span class="p">{</span>
        <span class="kd">const</span> <span class="nx">inputs</span> <span class="o">=</span> <span class="nx">channelInput</span><span class="p">.</span><span class="nx">split</span><span class="p">(</span><span class="sr">/</span><span class="se">[</span><span class="sr">,</span><span class="se">\s]</span><span class="sr">+/</span><span class="p">);</span>
        <span class="kd">const</span> <span class="nx">channelSet</span> <span class="o">=</span> <span class="k">new</span> <span class="nb">Set</span><span class="p">();</span>
        
        <span class="k">for</span> <span class="p">(</span><span class="kd">const</span> <span class="nx">input</span> <span class="k">of</span> <span class="nx">inputs</span><span class="p">)</span> <span class="p">{</span>
            <span class="c1">// getChannelsByGroupKey 会根据关键字返回对应的渠道列表</span>
            <span class="kd">const</span> <span class="nx">channels</span> <span class="o">=</span> <span class="nx">DataHelper</span><span class="p">.</span><span class="nx">channels</span><span class="p">.</span><span class="nx">getChannelsByGroupKey</span><span class="p">(</span><span class="nx">input</span><span class="p">);</span>
            <span class="nx">channels</span><span class="p">.</span><span class="nx">forEach</span><span class="p">(</span><span class="nx">ch</span> <span class="o">=&gt;</span> <span class="nx">channelSet</span><span class="p">.</span><span class="nx">add</span><span class="p">(</span><span class="nx">ch</span><span class="p">));</span>
        <span class="p">}</span>
        
        <span class="k">return</span> <span class="nb">Array</span><span class="p">.</span><span class="k">from</span><span class="p">(</span><span class="nx">channelSet</span><span class="p">);</span>  <span class="c1">// 去重后返回</span>
    <span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>

<h2 id="六通知系统">六、通知系统</h2>

<p>打包完成后，自动发送飞书通知。</p>

<h3 id="61-消息收集">6.1 消息收集</h3>

<p>每个渠道打包完成（成功或失败），都会把结果收集到 <code class="language-plaintext highlighter-rouge">MessageHelper</code>：</p>

<div class="language-javascript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// BuilderBase.start()</span>
<span class="k">async</span> <span class="nx">start</span><span class="p">()</span> <span class="p">{</span>
    <span class="k">try</span> <span class="p">{</span>
        <span class="k">await</span> <span class="nx">pipeline</span><span class="p">.</span><span class="nx">execute</span><span class="p">(</span><span class="k">this</span><span class="p">.</span><span class="nx">context</span><span class="p">);</span>
        
        <span class="c1">// 成功，收集消息</span>
        <span class="kd">const</span> <span class="nx">message</span> <span class="o">=</span> <span class="k">this</span><span class="p">.</span><span class="nx">context</span><span class="p">.</span><span class="nx">getBuildMessage</span><span class="p">();</span>
        <span class="nx">message</span><span class="p">.</span><span class="nx">succeed</span> <span class="o">=</span> <span class="kc">true</span><span class="p">;</span>
        <span class="nx">MessageHelper</span><span class="p">.</span><span class="nx">addMessage</span><span class="p">(</span><span class="nx">message</span><span class="p">);</span>
    <span class="p">}</span> <span class="k">catch</span> <span class="p">(</span><span class="nx">error</span><span class="p">)</span> <span class="p">{</span>
        <span class="c1">// 失败，也收集消息</span>
        <span class="kd">const</span> <span class="nx">message</span> <span class="o">=</span> <span class="k">this</span><span class="p">.</span><span class="nx">context</span><span class="p">.</span><span class="nx">getBuildMessage</span><span class="p">();</span>
        <span class="nx">message</span><span class="p">.</span><span class="nx">succeed</span> <span class="o">=</span> <span class="kc">false</span><span class="p">;</span>
        <span class="nx">message</span><span class="p">.</span><span class="nx">message</span> <span class="o">=</span> <span class="nx">error</span><span class="p">.</span><span class="nx">message</span><span class="p">;</span>
        <span class="nx">MessageHelper</span><span class="p">.</span><span class="nx">addMessage</span><span class="p">(</span><span class="nx">message</span><span class="p">);</span>
    <span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>

<h3 id="62-批量发送">6.2 批量发送</h3>

<p>批量打包时，所有渠道都打完后，统一发送一条飞书卡片消息：</p>

<div class="language-javascript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// build-core.js</span>
<span class="k">async</span> <span class="kd">function</span> <span class="nx">startBatchBuild</span><span class="p">(</span><span class="nx">params</span><span class="p">)</span> <span class="p">{</span>
    <span class="c1">// 初始化</span>
    <span class="nx">MessageHelper</span><span class="p">.</span><span class="nx">initBaseInfo</span><span class="p">(</span><span class="dl">'</span><span class="s1">build</span><span class="dl">'</span><span class="p">,</span> <span class="nx">version</span><span class="p">,</span> <span class="nx">mode</span><span class="p">,</span> <span class="dl">''</span><span class="p">,</span> <span class="nx">buildCode</span><span class="p">);</span>
    
    <span class="c1">// 逐个打包</span>
    <span class="k">for</span> <span class="p">(</span><span class="kd">const</span> <span class="nx">channel</span> <span class="k">of</span> <span class="nx">channels</span><span class="p">)</span> <span class="p">{</span>
        <span class="k">try</span> <span class="p">{</span>
            <span class="k">await</span> <span class="nx">startBuild</span><span class="p">({</span> <span class="p">...</span><span class="nx">params</span><span class="p">,</span> <span class="nx">channel</span> <span class="p">},</span> <span class="kc">true</span><span class="p">);</span>  <span class="c1">// true = 跳过单独通知</span>
        <span class="p">}</span> <span class="k">catch</span> <span class="p">(</span><span class="nx">error</span><span class="p">)</span> <span class="p">{</span>
            <span class="c1">// 继续打包下一个</span>
        <span class="p">}</span>
    <span class="p">}</span>
    
    <span class="c1">// 统一发送通知</span>
    <span class="k">await</span> <span class="nx">MessageHelper</span><span class="p">.</span><span class="nx">send</span><span class="p">();</span>
<span class="p">}</span>
</code></pre></div></div>

<p>这样打包10个渠道，只会收到1条汇总消息，不会被通知轰炸。</p>

<h3 id="63-飞书卡片">6.3 飞书卡片</h3>

<p>针对不同平台，飞书卡片的展示也不一样：</p>

<ul>
  <li><strong>Android/鸿蒙</strong>：显示下载按钮 + CDN 链接</li>
  <li><strong>微信/抖音小游戏</strong>：显示二维码图片</li>
  <li><strong>打包失败</strong>：显示错误信息</li>
</ul>

<p>代码里用不同的方法处理：</p>

<div class="language-javascript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="err">#</span><span class="nx">parseBuildMessageElement</span><span class="p">(</span><span class="nx">messageType</span><span class="p">,</span> <span class="nx">info</span><span class="p">)</span> <span class="p">{</span>
    <span class="k">if</span> <span class="p">(</span><span class="o">!</span><span class="nx">info</span><span class="p">.</span><span class="nx">succeed</span><span class="p">)</span> <span class="p">{</span>
        <span class="k">return</span> <span class="k">this</span><span class="p">.</span><span class="err">#</span><span class="nx">buildFailMessage</span><span class="p">(</span><span class="nx">info</span><span class="p">);</span>
    <span class="p">}</span>
    <span class="k">if</span> <span class="p">(</span><span class="nx">DataHelper</span><span class="p">.</span><span class="nx">platforms</span><span class="p">.</span><span class="nx">isQRCode</span><span class="p">(</span><span class="nx">info</span><span class="p">.</span><span class="nx">platform</span><span class="p">))</span> <span class="p">{</span>
        <span class="k">return</span> <span class="k">this</span><span class="p">.</span><span class="err">#</span><span class="nx">buildMessageQrcode</span><span class="p">(</span><span class="nx">info</span><span class="p">);</span>
    <span class="p">}</span>
    <span class="k">if</span> <span class="p">(</span><span class="nx">info</span><span class="p">.</span><span class="nx">platform</span> <span class="o">===</span> <span class="dl">"</span><span class="s2">harmonyos-next</span><span class="dl">"</span><span class="p">)</span> <span class="p">{</span>
        <span class="k">return</span> <span class="k">this</span><span class="p">.</span><span class="err">#</span><span class="nx">buildMessageHarmony</span><span class="p">(</span><span class="nx">info</span><span class="p">);</span>
    <span class="p">}</span>
    <span class="k">return</span> <span class="k">this</span><span class="p">.</span><span class="err">#</span><span class="nx">buildMessageDefault</span><span class="p">(</span><span class="nx">info</span><span class="p">);</span>
<span class="p">}</span>
</code></pre></div></div>

<h2 id="七热更新支持">七、热更新支持</h2>

<p>除了完整打包，我还支持了热更新流程。</p>

<p>热更新的 Pipeline 更简单：</p>

<div class="language-javascript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">class</span> <span class="nx">BuilderHotUpdate</span> <span class="kd">extends</span> <span class="nx">BuilderBase</span> <span class="p">{</span>
    <span class="nx">buildPipeline</span><span class="p">()</span> <span class="p">{</span>
        <span class="k">return</span> <span class="p">[</span>
            <span class="k">new</span> <span class="nx">ModifyVersionJsonStep</span><span class="p">(),</span>    <span class="c1">// 修改 version.json</span>
            <span class="k">new</span> <span class="nx">BuildCreatorStep</span><span class="p">(),</span>         <span class="c1">// 构建 Creator 项目</span>
            <span class="k">new</span> <span class="nx">ModifyMainJsStep</span><span class="p">(),</span>         <span class="c1">// 修改 main.js</span>
            <span class="k">new</span> <span class="nx">GenerateManifestStep</span><span class="p">(),</span>     <span class="c1">// 生成热更新 manifest</span>
            <span class="k">new</span> <span class="nx">UploadHotUpdateResStep</span><span class="p">()</span>    <span class="c1">// 上传热更新资源到 CDN</span>
        <span class="p">];</span>
    <span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>

<p>复用了 Creator 构建相关的 Step，只是后面的步骤不一样。</p>

<h2 id="八配置文件设计">八、配置文件设计</h2>

<p>所有配置都放在 <code class="language-plaintext highlighter-rouge">config/</code> 目录下，模版在 <code class="language-plaintext highlighter-rouge">template/</code> 目录：</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>config/
├── base.json           # 基础配置（Creator 路径、项目路径）
├── channels.json       # 渠道配置（渠道列表、分组）
├── platforms.json      # 平台配置（构建路径、打包配置文件）
├── certificates.json   # 证书配置（Android/鸿蒙签名证书）
├── oss.json           # OSS 配置（上传地址）
├── hotupdate.json     # 热更新配置
└── notification.json  # 通知配置（飞书 webhook）
</code></pre></div></div>

<p>配置和代码分离，不同项目只需要改配置文件，不用改代码。</p>

<h2 id="九目录结构">九、目录结构</h2>

<p>最后放一下项目的目录结构：</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>src/
├── core/                   # 核心框架
│   ├── Context.js         # 数据容器
│   ├── Step.js            # 步骤基类
│   ├── Pipeline.js        # 执行器
│   └── BuildCache.js      # 构建缓存（buildGroup 共享）

├── builders/               # 构建器
│   ├── BuilderBase.js     # 构建器基类
│   ├── BuilderFactory.js  # 工厂类
│   ├── BuilderAndroid.js  # Android
│   ├── BuilderWechat.js   # 微信小游戏
│   ├── BuilderHarmony.js  # 鸿蒙
│   └── ...

├── steps/                  # 步骤
│   ├── creator/           # Creator 相关
│   │   ├── BuildCreatorStep.js
│   │   ├── ModifyVersionJsonStep.js
│   │   ├── ModifyMainJsStep.js
│   │   └── GenerateManifestStep.js
│   ├── compile/           # 编译相关
│   │   ├── CompileAndroid.js
│   │   ├── CompileWechat.js
│   │   └── ...
│   ├── sign/              # 签名相关
│   └── upload/            # 上传相关

├── config/                 # 配置加载器
├── notification/           # 通知系统
├── oss/                    # OSS 上传
├── utils/                  # 工具类
└── bin/                    # 入口脚本
    ├── build.js           # 交互式打包
    ├── build-jenkins.js   # CI/CD 打包
    ├── hotupdate.js       # 交互式热更新
    └── hotupdate-jenkins.js  # CI/CD 热更新
</code></pre></div></div>

<h2 id="十总结">十、总结</h2>

<p>说实话，这个工具我写了大概一周，后面又断断续续优化了几天。</p>

<p>核心就这几个东西：</p>

<ol>
  <li><strong>Pipeline 模式</strong>：把流程拆成步骤，组装执行</li>
  <li><strong>Context 数据容器</strong>：步骤间解耦，通过数据通信</li>
  <li><strong>buildGroup 共享</strong>：同平台渠道共享构建产物，省时间</li>
  <li><strong>工厂模式</strong>：数据驱动，消除 if/else</li>
</ol>

<p>其实架构不复杂，关键是想清楚要解决什么问题。</p>

<p>之后每次发版，我就运行一条命令，然后去喝杯咖啡。等收到飞书通知，所有渠道都打完了。</p>

<p>后来我把它集成到Jenkins后，每次发版，策划直接在网页上点击几下就可以完成打包了。</p>

<p>任务顺利交出去了，反正我再也不用发版当天加班到凌晨了。</p>

<hr />

<p><strong>如果你也在做游戏开发，也被多渠道打包折磨过，希望这篇文章能给你一些启发。</strong></p>

<h2 id="十一扩展阅读">十一、扩展阅读</h2>

<p>当然也有Store版本，不想自己动手的可以购买使用：</p>
<ul>
  <li><a href="https://www.bilibili.com/video/BV1d9C4BVEMo/">B站视频教程</a></li>
  <li><a href="https://store.cocos.com/app/detail/8368">Cocos Store 下载</a></li>
</ul>]]></content><author><name>gongxh</name></author><category term="博客文章" /><category term="cocos-creator" /><category term="自动化打包" /><category term="工程化" /><category term="Pipeline" /><category term="CI/CD" /><summary type="html"><![CDATA[分享一个 Cocos Creator 多渠道自动化打包工具的设计思路，采用 Pipeline 模式实现灵活的构建流程，支持 Android、iOS、鸿蒙、微信小游戏等10+平台一键打包。]]></summary></entry><entry><title type="html">热更新最佳实践【CocosCreator】</title><link href="https://me.bitgong.cn/posts/%E7%83%AD%E6%9B%B4%E6%96%B0%E6%9C%80%E4%BD%B3%E5%AE%9E%E8%B7%B5-CocosCreator/" rel="alternate" type="text/html" title="热更新最佳实践【CocosCreator】" /><published>2025-11-18T11:35:00+00:00</published><updated>2025-11-18T11:35:00+00:00</updated><id>https://me.bitgong.cn/posts/%E7%83%AD%E6%9B%B4%E6%96%B0%E6%9C%80%E4%BD%B3%E5%AE%9E%E8%B7%B5%E3%80%90CocosCreator%E3%80%91</id><content type="html" xml:base="https://me.bitgong.cn/posts/%E7%83%AD%E6%9B%B4%E6%96%B0%E6%9C%80%E4%BD%B3%E5%AE%9E%E8%B7%B5-CocosCreator/"><![CDATA[<h2 id="前言">前言</h2>

<p>热更新的本质很简单，说白了就是把游戏资源放到远程服务器，然后客户端在需要时下载远程资源，使用远程资源替换本地文件。</p>

<h2 id="一creator官方提供的热更新方案">一、Creator官方提供的热更新方案</h2>
<p>Creator官方提供了热更新方案，详细信息请参考文档<a href="https://docs.cocos.com/creator/3.8/manual/zh/advanced-topics/hot-update.html">热更新</a>。
这里简单介绍一下:</p>

<ul>
  <li><strong>project.manifest</strong>文件
用来比对本地和远程的资源差异，确定哪些文件需要下载。
    <ul>
      <li>本地和远程各有一份 <strong>project.manifest</strong> 文件，<strong>project.manifest</strong> 文件中包含了所有资源的简略信息，比如文件名、大小、MD5等。</li>
      <li>通过比对本地和远程的<strong>project.manifest</strong> 中文件的md5，来确定哪些文件需要下载。</li>
    </ul>
  </li>
  <li><strong>version.manifest</strong>文件
减少流量消耗，减少计算量
    <ul>
      <li>由于<strong>project.manifest</strong>可能比较大，所以官方还提供了一份<strong>version.manifest</strong> 文件，有用的信息其实只有<strong>资源版本号</strong></li>
      <li>通过比对本地<strong>project.manifest</strong>和远程<strong>version.manifest</strong> 中的<strong>资源版本号</strong>，来确定是否需要下载<strong>project.manifest</strong>以及是否需要做文件比对</li>
    </ul>
  </li>
  <li>生成热更新资源流程
    <ol>
      <li>构建项目（构建选项不要选中 MD5 Cache 选项）</li>
      <li>使用构建后的资源生成 <strong>project.manifest</strong> 和 <strong>version.manifest</strong> 文件</li>
      <li>上传资源到CDN</li>
    </ol>
  </li>
  <li>客户端更新的流程 <strong>(每次都会下载version.manifest)</strong>
    <ol>
      <li>初始化AssetsManager，加载本地<strong>project.manifest</strong>文件</li>
      <li>下载远程的<strong>version.manifest</strong>文件，比对资源版本号，不需要更新跳过，需要更新继续下一步</li>
      <li>下载远程的<strong>project.manifest</strong>文件，比对文件MD5</li>
      <li>下载差异文件到本地，设置文件搜索路径</li>
      <li>重启游戏</li>
    </ol>
  </li>
</ul>

<h2 id="二踩坑经历">二、踩坑经历</h2>

<h3 id="21-远程资源存储路径问题">2.1 远程资源存储路径问题</h3>
<p>很多年前第一次做热更新功能时，我们把资源按照游戏版本号放到CDN上
最初的方案是这样的</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>cdn.example.com/game/1.0.0/  # 游戏版本 1.0.0
  ├── project.manifest
  ├── version.manifest
  └── remote-assets/
      ├── assets/
      ├── src/
      └── jsb-adapter/
</code></pre></div></div>
<p>每次发布热更新,直接在 <code class="language-plaintext highlighter-rouge">1.0.0/</code> 目录下覆盖资源再覆盖 manifest 文件和 version.manifest 文件。看起来没问题,但实际上有个时间窗口问题:</p>
<ol>
  <li>玩家 A 在 10:00 开始检测更新， 发现远程资源版本号是<code class="language-plaintext highlighter-rouge">2</code>，需要更新 100 个文件</li>
  <li>玩家 A 开始下载,已经下载了 50 个文件</li>
  <li>这时候 10:10 我们发了资源版本，把CDN上的资源全部覆盖了</li>
  <li>玩家 A 继续下载剩下的 50 个文件,但这些文件已经是版本 3 的了</li>
  <li>最终玩家 A 本地的资源是版本 2 和版本 3 混合的,文件完整性被破坏，启动游戏时直接崩了</li>
</ol>

<h3 id="22-本地资源污染的问题">2.2 本地资源污染的问题</h3>
<p>从CDN下载的远程资源会保存到本地，开始就直接在可写目录下新建了一个<code class="language-plaintext highlighter-rouge">hotupdate</code>文件夹，并没有其他区分机制，导致不同包版本的热更新资源全部放到同一个目录下，跨包版本更新时旧文件没清理干净，运行的是四不像版本，各种诡异 bug。</p>

<p>后来才找到靠谱的方案。这篇文章说说我是怎么踩坑、怎么填坑的。</p>

<h2 id="三解决方案">三、解决方案</h2>

<p>先明确两个概念:</p>

<ul>
  <li><strong>游戏版本号</strong>:指的是通过应用商店发布的包版本,比如 1.0.0、1.1.0、2.0.0</li>
  <li><strong>资源版本号</strong>:指的是同一个游戏版本下的热更的资源版本,这里我们用数字表示,比如 1、2、3、4…，发布包中的资源版本号用0表示初始资源版本</li>
</ul>

<p>举个例子:游戏版本 1.0.0 可能有多个资源版本(1、2、3…),每次热更新都会递增资源版本号。</p>

<h3 id="31-远程资源存储路径问题的解决方案">3.1 远程资源存储路径问题的解决方案</h3>
<blockquote>
  <p>按照下面这样分布热更新资源，还有另外一个好处
可以提前准备热更新资源，并上传到CDN，但是不上传<strong>version.manifest</strong>文件
等时间到发布的时间节点，再替换掉远程的<strong>version.manifest</strong>文件，指向最新版本的资源</p>
</blockquote>

<p>上文中<code class="language-plaintext highlighter-rouge">2.1</code>中提到的问题，是因为我们直接操作了已经发布了的热更新资源，我们不能保证瞬间上传完成，所以就需要已经发布的资源保证不再修改。
所以我们改成了下面这样的CDN文件布局:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>cdn.example.com/game/
  └── 1.0.0/  # 游戏版本 1.0.0
      ├── version.manifest  # 固定地址,内容指向最新的资源版本
      ├── 1/  # 资源版本 1
      │   ├── project.manifest
      │   └── remote-assets/
      ├── 2/  # 资源版本 2
      │   ├── project.manifest
      │   └── remote-assets/
      └── 3/  # 资源版本 3
          ├── project.manifest
          └── remote-assets/
</code></pre></div></div>

<p>每次发布热更新,直接在<code class="language-plaintext highlighter-rouge">1.0.0/</code>目录下根据资源版本号新建一个目录，然后上传 <strong>project.manifest</strong> 和 <strong>远程资源</strong> 文件。
由于每次都会重新下载<strong>version.manifest</strong>文件，所以我们把变更后的新地址保存到<strong>version.manifest</strong>中</p>
<div class="language-json highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">{</span><span class="w">
    </span><span class="nl">"packageUrl"</span><span class="p">:</span><span class="s2">"https://cdn.example.com/game/1.0.0/3"</span><span class="p">,</span><span class="w">  </span><span class="err">//</span><span class="w"> </span><span class="err">指向新的资源地址</span><span class="w">
    </span><span class="nl">"remoteManifestUrl"</span><span class="p">:</span><span class="s2">"https://cdn.example.com/game/1.0.0/3/project.manifest"</span><span class="p">,</span><span class="w"> </span><span class="err">//</span><span class="w"> </span><span class="err">指向新的</span><span class="w"> </span><span class="err">project.manifest</span><span class="w"> </span><span class="err">地址</span><span class="w">
    </span><span class="nl">"remoteVersionUrl"</span><span class="p">:</span><span class="s2">"https://cdn.example.com/game/1.0.0/version.manifest"</span><span class="p">,</span><span class="w">    </span><span class="err">//</span><span class="w"> </span><span class="err">这个地址保持不变</span><span class="w">
    </span><span class="nl">"version"</span><span class="p">:</span><span class="s2">"3"</span><span class="w">  </span><span class="err">//</span><span class="w"> </span><span class="err">新的资源版本号</span><span class="w">
</span><span class="p">}</span><span class="w">
</span></code></pre></div></div>
<p>当所有文件上传完成之后，最后替换远程的<code class="language-plaintext highlighter-rouge">1.0.0/version.manifest</code> 文件
这样就保证了已经发布的资源不会被修改，也不会被覆盖。</p>

<h4 id="311-引入了新的问题">3.1.1 引入了新的问题</h4>
<p>玩家热更新时，下载远程的<strong>version.manifest</strong>文件和<strong>project.manifest</strong>文件使用的都是本地的<strong>project.manifest</strong>文件中的地址。
这些地址是不会变化的，指向的远程资源还是老的文件，当然远程<strong>version.manifest</strong>文件中的地址已经发生变化了。
但是<strong>version.manifest</strong>目前有用的信息只用到了<code class="language-plaintext highlighter-rouge">version</code>字段，也就是资源版本号。</p>

<h4 id="312-解决问题动态变更资源的指向">3.1.2 解决问题（动态变更资源的指向）</h4>

<p>通过查看官方AssetsManager的源码，发现AssetsManager中其实是提供了动态修改远程地址的问题
下载远程<strong>version.manifest</strong>文件后，把其中的地址相关的信息，覆盖到本地的<strong>project.manifest</strong>文件中</p>

<div class="language-typescript highlighter-rouge"><div class="highlight"><pre class="highlight"><code>    <span class="c1">// 参数1: 本地project.manifest文件路径，可以传空字符串，或者加载后的资源Asset的nativeUrl属性</span>
    <span class="c1">// 参数2: 本地存储目录</span>
    <span class="c1">// 参数3: 版本号比对函数，这里我用的是数字，比较大小就行了</span>
    <span class="k">this</span><span class="p">.</span><span class="nx">_am</span> <span class="o">=</span> <span class="k">new</span> <span class="nx">native</span><span class="p">.</span><span class="nx">AssetsManager</span><span class="p">(</span><span class="dl">""</span><span class="p">,</span> <span class="nx">writablePath</span><span class="p">,</span> <span class="p">(</span><span class="nx">v1</span><span class="p">,</span> <span class="nx">v2</span><span class="p">)</span> <span class="o">=&gt;</span> <span class="p">{</span>
        <span class="k">return</span> <span class="nx">v1</span> <span class="o">&gt;</span> <span class="nx">v2</span><span class="p">;</span>
    <span class="p">});</span>
    <span class="c1">// 之后下载远程的version.manifest文件，替换本地project.manifest文件中的地址后，再重新加载LocalManifest</span>
    <span class="k">this</span><span class="p">.</span><span class="nx">_am</span><span class="p">.</span><span class="nx">getLocalManifest</span><span class="p">().</span><span class="nx">parseJSONString</span><span class="p">(</span><span class="nx">JSON</span><span class="p">.</span><span class="nx">stringify</span><span class="p">(</span><span class="nx">content</span><span class="p">),</span> <span class="nx">manifestRoot</span><span class="p">);</span>
</code></pre></div></div>

<h3 id="22-本地资源污染解决">2.2 本地资源污染解决</h3>
<p>这个就很简单了，可以根据<strong>游戏版本号</strong>组织本地资源的缓存目录，或者跨游戏版本后，清理之前存的本地资源。</p>
<div class="language-typescript highlighter-rouge"><div class="highlight"><pre class="highlight"><code>    <span class="c1">// 之前的资源存储路径</span>
    <span class="kd">let</span> <span class="nx">storagePath</span> <span class="o">=</span> <span class="s2">`</span><span class="p">${</span><span class="nx">jsb</span><span class="p">.</span><span class="nx">fileUtils</span><span class="p">.</span><span class="nx">getWritablePath</span><span class="p">()}</span><span class="s2">hotupdate/`</span><span class="p">;</span>

    <span class="c1">// 新的资源存储路径</span>
    <span class="kd">let</span> <span class="nx">gameVersion</span> <span class="o">=</span> <span class="dl">'</span><span class="s1">1.0.0</span><span class="dl">'</span><span class="p">;</span> <span class="c1">// 游戏版本号</span>
    <span class="kd">let</span> <span class="nx">storagePath</span> <span class="o">=</span> <span class="s2">`</span><span class="p">${</span><span class="nx">jsb</span><span class="p">.</span><span class="nx">fileUtils</span><span class="p">.</span><span class="nx">getWritablePath</span><span class="p">()}</span><span class="s2">hotupdate/</span><span class="p">${</span><span class="nx">gameVerison</span><span class="p">}</span><span class="s2">/`</span><span class="p">;</span>
</code></pre></div></div>

<h3 id="33-完整流程">3.3 完整流程：</h3>

<h4 id="331-打包流程">3.3.1 打包流程：</h4>
<ol>
  <li>构建Creator项目</li>
  <li>修改构建后的<code class="language-plaintext highlighter-rouge">main.js</code>文件，添加热更代码</li>
  <li>生成<code class="language-plaintext highlighter-rouge">project.manifest</code>文件</li>
  <li>原生平台打包(Android、iOS、鸿蒙)</li>
</ol>

<h4 id="332-热更新资源上传">3.3.2 热更新资源上传：</h4>
<ol>
  <li>构建Creator项目</li>
  <li>生成<code class="language-plaintext highlighter-rouge">project.manifest</code> 和 <code class="language-plaintext highlighter-rouge">version.manifest</code> 文件</li>
  <li>先按资源版本号上传资源文件到CDN</li>
  <li>再上传<code class="language-plaintext highlighter-rouge">project.manifest</code> 文件到CDN</li>
  <li>最后上传<code class="language-plaintext highlighter-rouge">version.manifest</code> 文件到CDN</li>
</ol>

<h4 id="333-游戏热更新流程">3.3.3 游戏热更新流程：</h4>
<ol>
  <li>启动游戏</li>
  <li>创建AssetsManager实例</li>
  <li>下载远程<code class="language-plaintext highlighter-rouge">version.manifest</code>文件，比对资源版本号，不需要更新跳过，需要更新继续下一步</li>
  <li>读取本地<code class="language-plaintext highlighter-rouge">project.manifest</code>文件，使用<code class="language-plaintext highlighter-rouge">3</code>下载的<code class="language-plaintext highlighter-rouge">version.manifest</code>文件中的地址，替换本地<code class="language-plaintext highlighter-rouge">project.manifest</code>文件中的地址</li>
  <li>重新加载<code class="language-plaintext highlighter-rouge">LocalManifest</code></li>
  <li>调用<code class="language-plaintext highlighter-rouge">AssetsManager</code>的<code class="language-plaintext highlighter-rouge">update</code>方法，开始更新</li>
  <li>更新完成，设置搜索路径，重启游戏</li>
</ol>

<h2 id="四总结">四、总结</h2>
<blockquote>
  <p>跨游戏版本更新，只需要更新对应版本文件夹下的<strong>version.manifest</strong>文件，指向最新版本的资源即可。</p>
  <ul>
    <li>不要修改已发布热更新资源</li>
    <li>动态修改manifest是关键</li>
    <li>热更的资源按游戏版本隔离</li>
    <li>使用脚本自动化构建、热更流程，防止人为操作失误</li>
  </ul>
</blockquote>

<p>这篇文章分享的方案不一定是最优的,但起码是我在实际项目中验证过的,基本能覆盖常见场景。如果你也在做热更新,希望能帮你少踩点坑。</p>

<p>有问题随时交流,大家一起进步。</p>

<h2 id="五扩展阅读">五、扩展阅读</h2>
<ul>
  <li><a href="https://docs.cocos.com/creator/3.8/manual/zh/advanced-topics/hot-update.html">Cocos Creator 热更新教程</a> - 官方基础教程</li>
  <li><a href="https://docs.cocos.com/creator/3.8/api/zh/class/AssetManager">AssetsManager 使用指南</a> - 热更新管理器 API</li>
  <li><a href="https://github.com/gongxh0901/kunpolibrary/tree/main/src/hotupdate">热更新的完整实现</a> - 开源项目</li>
</ul>]]></content><author><name>gongxh</name></author><category term="博客文章" /><category term="cocos-creator" /><category term="热更新" /><category term="原生平台" /><category term="问题排查" /><summary type="html"><![CDATA[CocosCreator热更新的最佳实践]]></summary></entry><entry><title type="html">资源加载【CocosCreator】</title><link href="https://me.bitgong.cn/posts/%E8%B5%84%E6%BA%90%E5%8A%A0%E8%BD%BD-CocosCreator/" rel="alternate" type="text/html" title="资源加载【CocosCreator】" /><published>2025-10-24T11:26:00+00:00</published><updated>2025-10-24T11:26:00+00:00</updated><id>https://me.bitgong.cn/posts/%E8%B5%84%E6%BA%90%E5%8A%A0%E8%BD%BD%E3%80%90CocosCreator%E3%80%91</id><content type="html" xml:base="https://me.bitgong.cn/posts/%E8%B5%84%E6%BA%90%E5%8A%A0%E8%BD%BD-CocosCreator/"><![CDATA[<h1 id="cocoscreator资源加载">CocosCreator资源加载</h1>

<h2 id="先吐槽一下">先吐槽一下</h2>

<p>用 Cocos Creator 开发也有段时间了，说实话资源加载这块一直让我挺头疼的。</p>

<p>每次写加载代码都是这样，或者先加载bundle再这样：</p>

<div class="language-typescript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nx">resources</span><span class="p">.</span><span class="nx">load</span><span class="p">(</span><span class="dl">"</span><span class="s2">prefab/player</span><span class="dl">"</span><span class="p">,</span> <span class="nx">Prefab</span><span class="p">,</span> <span class="p">(</span><span class="nx">err</span><span class="p">,</span> <span class="nx">prefab</span><span class="p">)</span> <span class="o">=&gt;</span> <span class="p">{</span>
    <span class="k">if</span> <span class="p">(</span><span class="nx">err</span><span class="p">)</span> <span class="p">{</span>
        <span class="nx">console</span><span class="p">.</span><span class="nx">error</span><span class="p">(</span><span class="nx">err</span><span class="p">);</span>
        <span class="k">return</span><span class="p">;</span>
    <span class="p">}</span>
    <span class="c1">// 终于可以用了</span>
<span class="p">});</span>
</code></pre></div></div>

<p>加载个资源要写这么一坨，关键是项目里到处都是这种代码。</p>

<p>更麻烦的是批量加载。比如进入战斗场景，我要加载：</p>
<ul>
  <li>config 文件夹下的配置</li>
  <li>icon 文件夹下的图标</li>
  <li>prefab 文件夹下的预制体</li>
  <li>还有其他 bundle 里的资源</li>
</ul>

<p>这时候就得自己写循环、计算进度、处理异常…每个项目都要封装一遍，烦死了。</p>

<p>还有资源释放的问题。退出战斗场景时，怎么把刚才加载的资源都释放掉？一个个记录路径？太蠢了。引用计数？自己管理容易出错。</p>

<p>反正就是觉得官方的 API 不太够用，每次都要自己折腾。</p>

<h2 id="干脆自己封装一个">干脆自己封装一个</h2>

<p>既然每个项目都要写，不如封装成库，以后直接用。于是就有了 bit-assets。</p>

<p>核心思路很简单：</p>
<ol>
  <li>用配置的方式批量加载资源</li>
  <li>自动管理资源池和引用计数</li>
  <li>支持按批次释放资源</li>
</ol>

<h3 id="批量加载资源">批量加载资源</h3>
<div class="language-typescript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">import</span> <span class="p">{</span> <span class="nx">AssetLoader</span><span class="p">,</span> <span class="nx">IAssetConfig</span> <span class="p">}</span> <span class="k">from</span> <span class="dl">"</span><span class="s2">kunpocc-assets</span><span class="dl">"</span><span class="p">;</span>

<span class="c1">// 把要加载的资源配置好</span>
<span class="kd">let</span> <span class="nx">configs</span><span class="p">:</span> <span class="nx">IAssetConfig</span><span class="p">[]</span> <span class="o">=</span> <span class="p">[</span>
    <span class="p">{</span> <span class="na">path</span><span class="p">:</span> <span class="dl">"</span><span class="s2">config</span><span class="dl">"</span> <span class="p">},</span>
    <span class="p">{</span> <span class="na">path</span><span class="p">:</span> <span class="dl">"</span><span class="s2">icon</span><span class="dl">"</span><span class="p">,</span> <span class="na">type</span><span class="p">:</span> <span class="nx">SpriteFrame</span> <span class="p">},</span>
    <span class="p">{</span> <span class="na">path</span><span class="p">:</span> <span class="dl">"</span><span class="s2">prefab</span><span class="dl">"</span><span class="p">,</span> <span class="na">type</span><span class="p">:</span> <span class="nx">Prefab</span> <span class="p">},</span>
    <span class="p">{</span> <span class="na">path</span><span class="p">:</span> <span class="dl">"</span><span class="s2">texture/avatar</span><span class="dl">"</span><span class="p">,</span> <span class="na">type</span><span class="p">:</span> <span class="nx">SpriteFrame</span><span class="p">,</span> <span class="na">isFile</span><span class="p">:</span> <span class="kc">true</span> <span class="p">},</span>  <span class="c1">// 单个文件</span>
    <span class="p">{</span> <span class="na">path</span><span class="p">:</span> <span class="dl">"</span><span class="s2">pet</span><span class="dl">"</span><span class="p">,</span> <span class="na">type</span><span class="p">:</span> <span class="nx">SpriteFrame</span><span class="p">,</span> <span class="na">bundle</span><span class="p">:</span> <span class="dl">"</span><span class="s2">battle</span><span class="dl">"</span> <span class="p">},</span>     <span class="c1">// 其他 bundle</span>
<span class="p">];</span>

<span class="c1">// 创建加载器</span>
<span class="kd">let</span> <span class="nx">loader</span> <span class="o">=</span> <span class="k">new</span> <span class="nx">AssetLoader</span><span class="p">(</span><span class="dl">"</span><span class="s2">battle</span><span class="dl">"</span><span class="p">);</span>  <span class="c1">// 给个名字，后面释放用得上</span>
<span class="nx">loader</span><span class="p">.</span><span class="nx">parallel</span> <span class="o">=</span> <span class="mi">10</span><span class="p">;</span>  <span class="c1">// 并行的任务数量</span>
<span class="nx">loader</span><span class="p">.</span><span class="nx">retry</span> <span class="o">=</span> <span class="mi">3</span><span class="p">;</span>      <span class="c1">// 失败了重试 3 次</span>

<span class="nx">loader</span><span class="p">.</span><span class="nx">setCallbacks</span><span class="p">({</span>
    <span class="na">complete</span><span class="p">:</span> <span class="p">()</span> <span class="o">=&gt;</span> <span class="p">{</span>
        <span class="nx">console</span><span class="p">.</span><span class="nx">log</span><span class="p">(</span><span class="dl">"</span><span class="s2">加载完了</span><span class="dl">"</span><span class="p">);</span>
    <span class="p">},</span>
    <span class="na">fail</span><span class="p">:</span> <span class="p">(</span><span class="nx">code</span><span class="p">,</span> <span class="nx">msg</span><span class="p">)</span> <span class="o">=&gt;</span> <span class="p">{</span>
        <span class="nx">console</span><span class="p">.</span><span class="nx">error</span><span class="p">(</span><span class="dl">"</span><span class="s2">加载失败:</span><span class="dl">"</span><span class="p">,</span> <span class="nx">msg</span><span class="p">);</span>
    <span class="p">},</span>
    <span class="na">progress</span><span class="p">:</span> <span class="p">(</span><span class="nx">percent</span><span class="p">)</span> <span class="o">=&gt;</span> <span class="p">{</span>
        <span class="nx">console</span><span class="p">.</span><span class="nx">log</span><span class="p">(</span><span class="dl">"</span><span class="s2">进度:</span><span class="dl">"</span><span class="p">,</span> <span class="nb">Math</span><span class="p">.</span><span class="nx">floor</span><span class="p">(</span><span class="nx">percent</span> <span class="o">*</span> <span class="mi">100</span><span class="p">)</span> <span class="o">+</span> <span class="dl">"</span><span class="s2">%</span><span class="dl">"</span><span class="p">);</span>
    <span class="p">}</span>
<span class="p">});</span>

<span class="nx">loader</span><span class="p">.</span><span class="nx">start</span><span class="p">(</span><span class="nx">configs</span><span class="p">);</span>
</code></pre></div></div>

<h3 id="使用已加载的资源">使用已加载的资源</h3>

<div class="language-typescript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">import</span> <span class="p">{</span> <span class="nx">AssetPool</span> <span class="p">}</span> <span class="k">from</span> <span class="dl">"</span><span class="s2">kunpocc-assets</span><span class="dl">"</span><span class="p">;</span>

<span class="kd">let</span> <span class="nx">prefab</span> <span class="o">=</span> <span class="nx">AssetPool</span><span class="p">.</span><span class="kd">get</span><span class="o">&lt;</span><span class="nx">Prefab</span><span class="o">&gt;</span><span class="p">(</span><span class="dl">"</span><span class="s2">prefab/player</span><span class="dl">"</span><span class="p">);</span>
<span class="kd">let</span> <span class="nx">icon</span> <span class="o">=</span> <span class="nx">AssetPool</span><span class="p">.</span><span class="kd">get</span><span class="o">&lt;</span><span class="nx">SpriteFrame</span><span class="o">&gt;</span><span class="p">(</span><span class="dl">"</span><span class="s2">icon/gold</span><span class="dl">"</span><span class="p">,</span> <span class="dl">"</span><span class="s2">bundleName</span><span class="dl">"</span><span class="p">);</span>

<span class="c1">// 也可以用 UUID 获取</span>
<span class="kd">let</span> <span class="nx">asset</span> <span class="o">=</span> <span class="nx">AssetPool</span><span class="p">.</span><span class="nx">getByUUID</span><span class="o">&lt;</span><span class="nx">SpriteFrame</span><span class="o">&gt;</span><span class="p">(</span><span class="dl">"</span><span class="s2">xxxxx-xxxx-xxxx</span><span class="dl">"</span><span class="p">);</span>
</code></pre></div></div>

<h3 id="释放资源">释放资源</h3>

<div class="language-typescript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">import</span> <span class="p">{</span> <span class="nx">AssetPool</span> <span class="p">}</span> <span class="k">from</span> <span class="dl">"</span><span class="s2">kunpocc-assets</span><span class="dl">"</span><span class="p">;</span>

<span class="c1">// 释放单个资源</span>
<span class="nx">AssetPool</span><span class="p">.</span><span class="nx">releasePath</span><span class="p">(</span><span class="dl">"</span><span class="s2">prefab/player</span><span class="dl">"</span><span class="p">);</span>

<span class="c1">// 释放整个文件夹下的 SpriteFrame 类型资源</span>
<span class="nx">AssetPool</span><span class="p">.</span><span class="nx">releaseDir</span><span class="p">(</span><span class="dl">"</span><span class="s2">icon</span><span class="dl">"</span><span class="p">,</span> <span class="dl">"</span><span class="s2">resources</span><span class="dl">"</span><span class="p">,</span> <span class="nx">SpriteFrame</span><span class="p">);</span>

<span class="c1">// 重点：按批次释放</span>
<span class="nx">AssetPool</span><span class="p">.</span><span class="nx">releaseBatchAssets</span><span class="p">(</span><span class="dl">"</span><span class="s2">battle</span><span class="dl">"</span><span class="p">);</span>  <span class="c1">// 把刚才加载的战斗资源全释放掉</span>

<span class="c1">// 释放所有资源</span>
<span class="nx">AssetPool</span><span class="p">.</span><span class="nx">releaseAll</span><span class="p">();</span>
</code></pre></div></div>

<p>这个批次释放真的很好用。比如我做了个战斗场景和商城场景：</p>

<div class="language-typescript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// 进战斗</span>
<span class="kd">let</span> <span class="nx">battleLoader</span> <span class="o">=</span> <span class="k">new</span> <span class="nx">AssetLoader</span><span class="p">(</span><span class="dl">"</span><span class="s2">battle</span><span class="dl">"</span><span class="p">);</span>
<span class="nx">battleLoader</span><span class="p">.</span><span class="nx">start</span><span class="p">(</span><span class="nx">battleConfigs</span><span class="p">);</span>

<span class="c1">// 进商城</span>
<span class="kd">let</span> <span class="nx">shopLoader</span> <span class="o">=</span> <span class="k">new</span> <span class="nx">AssetLoader</span><span class="p">(</span><span class="dl">"</span><span class="s2">shop</span><span class="dl">"</span><span class="p">);</span>
<span class="nx">shopLoader</span><span class="p">.</span><span class="nx">start</span><span class="p">(</span><span class="nx">shopConfigs</span><span class="p">);</span>

<span class="c1">// 退出战斗，一行代码释放所有战斗资源</span>
<span class="nx">AssetPool</span><span class="p">.</span><span class="nx">releaseBatchAssets</span><span class="p">(</span><span class="dl">"</span><span class="s2">battle</span><span class="dl">"</span><span class="p">);</span>

<span class="c1">// 退出商城，一行代码释放所有商城资源</span>
<span class="nx">AssetPool</span><span class="p">.</span><span class="nx">releaseBatchAssets</span><span class="p">(</span><span class="dl">"</span><span class="s2">shop</span><span class="dl">"</span><span class="p">);</span>
</code></pre></div></div>

<p>场景切换的时候再也不用担心资源释放的问题了。</p>

<h3 id="其他功能">其他功能</h3>

<p>用Creator加载的资源也可以加入到管理中，最后统一释放</p>

<div class="language-typescript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nx">resources</span><span class="p">.</span><span class="nx">load</span><span class="p">(</span><span class="dl">"</span><span class="s2">prefab/player</span><span class="dl">"</span><span class="p">,</span> <span class="nx">Prefab</span><span class="p">,</span> <span class="p">(</span><span class="nx">err</span><span class="p">,</span> <span class="nx">prefab</span><span class="p">)</span> <span class="o">=&gt;</span> <span class="p">{</span>
    <span class="k">if</span> <span class="p">(</span><span class="nx">err</span><span class="p">)</span> <span class="p">{</span>
        <span class="nx">console</span><span class="p">.</span><span class="nx">error</span><span class="p">(</span><span class="nx">err</span><span class="p">);</span>
        <span class="k">return</span><span class="p">;</span>
    <span class="p">}</span>
    <span class="c1">// 参数1: 资源</span>
    <span class="c1">// 参数2: 资源所在的bundle 默认: resources</span>
    <span class="c1">// 参数3: 资源加载批次名 可以不传 默认 ""</span>
    <span class="nx">AssetPool</span><span class="p">.</span><span class="nx">add</span><span class="p">(</span><span class="nx">prefab</span><span class="p">,</span> <span class="nx">resources</span><span class="p">,</span> <span class="dl">"</span><span class="s2">batchName</span><span class="dl">"</span><span class="p">);</span>
<span class="p">});</span>
</code></pre></div></div>

<h2 id="一些特点">一些特点</h2>

<ul>
  <li>支持 Cocos Creator 3.7+</li>
  <li>没有其他依赖，就几个文件</li>
  <li>自动管理引用计数，不用担心内存泄漏</li>
  <li>同一个资源加载多次和一次效果一样，释放一次卸载</li>
</ul>

<h2 id="开源免费">开源免费</h2>

<p>代码放 GitHub 上了，ISC 协议，随便用：</p>

<ul>
  <li>GitHub: https://github.com/Gongxh0901/kunpocc-assets</li>
  <li>NPM: https://www.npmjs.com/package/kunpocc-assets</li>
</ul>

<p>也有商城版本，同样免费</p>
<ul>
  <li>商城: <a href="https://store.cocos.com/app/detail/8285">https://store.cocos.com/app/detail/8285</a></li>
</ul>

<p>有 bug 或者建议欢迎提 issue，我看到会回复。</p>

<h2 id="最后">最后</h2>

<p>这个库主要是我自己项目在用，觉得还挺顺手的，就分享出来了。</p>

<p>如果你也觉得 Creator 的资源加载 API 不太好用，可以试试这个。代码不多，看源码也很快。</p>

<p>有问题随时交流。</p>]]></content><author><name>gongxh</name></author><category term="博客文章" /><category term="Cocos Creator" /><category term="资源加载" /><category term="游戏开发" /><summary type="html"><![CDATA[CocosCreator资源加载]]></summary></entry><entry><title type="html">Creator插件集合</title><link href="https://me.bitgong.cn/posts/Creator%E6%8F%92%E4%BB%B6%E9%9B%86%E5%90%88/" rel="alternate" type="text/html" title="Creator插件集合" /><published>2025-09-24T07:26:00+00:00</published><updated>2025-09-24T07:26:00+00:00</updated><id>https://me.bitgong.cn/posts/Creator%E6%8F%92%E4%BB%B6%E9%9B%86%E5%90%88</id><content type="html" xml:base="https://me.bitgong.cn/posts/Creator%E6%8F%92%E4%BB%B6%E9%9B%86%E5%90%88/"><![CDATA[<h1 id="游戏开发工具集合">游戏开发工具集合</h1>

<p>这是我在游戏开发中积累的一系列常用工具，主要基于 Cocos Creator 开发。这些工具涵盖了ECS、行为树、界面属性绑定、EXCEL转JSON、调试工具、内存查看等多种可视化工具。</p>

<h2 id="完整工具列表">完整工具列表</h2>

<h3 id="1-调试与内存分析插件">1. 调试与内存分析插件</h3>
<p>一个即插即用的 Cocos Creator 调试与性能分析工具，实时编辑属性、定位节点、分析资源内存占用。</p>

<p><strong>商店地址</strong>: <a href="https://store.cocos.com/app/detail/7752">Cocos Store - 调试与内存分析插件</a><br />
<strong>支持版本</strong>: Cocos Creator 2.4.0+
<strong>演示视频</strong>: <a href="https://www.bilibili.com/video/BV195KZzSE6d/">Cocos Creator 调试与内存分析插件演示</a>
<strong>Cocos官方推荐</strong>: <a href="https://mp.weixin.qq.com/s/JoQG8H-9CO0P2M8Hn1iEqQ">微信公众号</a>。</p>

<h3 id="2-实体配置插件">2. 实体配置插件</h3>
<p>一个专为 Cocos Creator 3.7+ 设计的实体配置管理插件，通过可视化编辑和数据驱动方案，彻底简化游戏实体配置流程，提升开发效率。</p>

<p><strong>商店地址</strong>: <a href="https://store.cocos.com/app/detail/7311">Cocos Store - 实体配置插件</a><br />
<strong>支持版本</strong>: Cocos Creator 3.7+</p>

<h3 id="3-行为树插件">3. 行为树插件</h3>
<p>一个专为 Cocos Creator 3.7+ 设计的可视化行为树编辑器，通过可视化编辑和数据驱动方案，彻底简化游戏行为树配置流程，提升开发效率。</p>

<p><strong>商店地址</strong>: <a href="https://store.cocos.com/app/detail/8201">Cocos Store - 行为树插件</a><br />
<strong>支持版本</strong>: Cocos Creator 3.7+</p>

<h3 id="4-界面属性绑定插件">4. 界面属性绑定插件</h3>
<p>一个为 Cocos Creator 3.7+ 和 FairyGUI 设计的界面属性绑定插件，通过可视化编辑绑定代码和UI组件。</p>

<p><strong>商店地址</strong>: <a href="https://store.cocos.com/app/detail/7213">Cocos Store - 界面属性绑定插件</a><br />
<strong>支持版本</strong>: Cocos Creator 3.7+</p>

<h3 id="5-excel转json插件">5. Excel转JSON插件</h3>
<p>一个 Cocos Creator 3.7+ 的Excel转JSON插件。</p>

<p><strong>商店地址</strong>: <a href="https://store.cocos.com/app/detail/7407">Cocos Store - Excel转JSON插件</a><br />
<strong>支持版本</strong>: Cocos Creator 3.6+</p>

<hr />

<p>这套工具集合致力于为 Cocos Creator 开发者提供高效、易用的游戏开发解决方案。无论是独立开发者还是团队项目，都能为您的游戏开发之旅提供强有力的支持。</p>

<p>项目持续优化中，敬请期待更多功能和改进！</p>]]></content><author><name>gongxh</name></author><category term="游戏开发工具" /><category term="Cocos Creator" /><category term="插件" /><category term="ECS" /><category term="工具集" /><category term="实体组件" /><category term="可视化编辑" /><category term="行为树" /><summary type="html"><![CDATA[游戏开发工具集合]]></summary></entry><entry><title type="html">比特小队</title><link href="https://me.bitgong.cn/posts/%E6%AF%94%E7%89%B9%E5%B0%8F%E9%98%9F/" rel="alternate" type="text/html" title="比特小队" /><published>2025-08-06T00:00:00+00:00</published><updated>2025-08-06T00:00:00+00:00</updated><id>https://me.bitgong.cn/posts/%E6%AF%94%E7%89%B9%E5%B0%8F%E9%98%9F</id><content type="html" xml:base="https://me.bitgong.cn/posts/%E6%AF%94%E7%89%B9%E5%B0%8F%E9%98%9F/"><![CDATA[<h1 id="比特小队一场硬核又有趣的星际冒险">《比特小队》：一场硬核又有趣的星际冒险</h1>

<p>《比特小队》是一款由KunpoGames旗下bit工作室倾力打造的Roguelike+TPS（第三人称射击）独立游戏。它将玩家带入一个充满未知与挑战的随机宇宙，你需要探索各个星球，获取强力的道具和装备，战胜不同类型的敌人，最终拯救危在旦夕的星系。</p>

<video style="width: 100%; border-radius: 8px; margin-top: 1em; margin-bottom: 1em;" controls="" playsinline="" preload="none" loop="">
  <source src="/assets/videos/game-projects/bit-team.mp4" type="video/mp4" />
  您的浏览器不支持视频播放。
</video>

<!-- 本地开发备用方案：如果视频播放有问题，可以使用以下链接直接下载观看 -->
<p style="margin-top: 0.5em; text-align: center; font-size: 0.9em; color: #666;">
  <a href="/assets/videos/game-projects/bit-team.mp4" target="_blank">📹 如果视频无法播放，点击这里直接观看</a>
</p>

<h3 id="一款充满挑战的游戏">一款充满挑战的游戏</h3>

<p>拯救星系是一项艰难的任务。游戏融合了高随机性的Roguelike元素和硬核的第三人称射击玩法。每一次冒险，你面对的关卡、敌人、道具都是全新的。你需要做好充分的准备，并拥有足够的耐心，才能在一次次的挑战中不断变强，收获游戏丰富有趣的核心内容。</p>

<h3 id="一款停不下来的游戏">一款停不下来的游戏</h3>

<p>除了紧张刺激的战斗，星系中还隐藏着无数的秘密。那些细小的线索就散落在各个角落，每个角色、每个NPC都有着自己的背景故事。游戏拥有海量的武器、道具、成就等待你去收集和解锁。这不仅仅是一场战斗，更是一场发现之旅。</p>

<h3 id="核心玩法与特色">核心玩法与特色</h3>

<ul>
  <li><strong>高度随机的Roguelike体验</strong>：平行宇宙空间，丰富的元素，无尽的循环，以及多样的结局……每一次游戏都是独一无二的旅程。</li>
  <li><strong>爽快的双摇杆射击</strong>：流畅的360度自由射击操作，让你在枪林弹雨中尽情穿梭。</li>
  <li><strong>海量的内容与深度</strong>：超过140种武器、200种道具、100多种怪物、16名特色各异的角色和160项成就，提供了无穷的搭配和玩法流派。</li>
  <li><strong>独特的伪像素美术风格</strong>：复古而又精致的画面，搭配搞怪有趣的角色设计，构建了一个充满魅力的游戏世界。</li>
</ul>

<h3 id="玩家社区反响">玩家社区反响</h3>

<p>自发布以来，《比特小队》在玩家社区中收获了大量积极的评价。更是获得了9.4分的高分评价，玩家们普遍称赞其为“国产良心之作”，对其“不逼氪金”的付费模式大加赞赏。丰富的游戏内容和极高的可玩性，让许多玩家沉迷其中，并乐于将其与《以撒的结合》等经典独立游戏进行比较。</p>

<h3 id="研发历程一段好玩耐玩的探索之旅">研发历程：一段“好玩、耐玩”的探索之旅</h3>

<p>《比特小队》的诞生并非一蹴而就，它凝聚了团队数年的心血与坚持。</p>

<ul>
  <li><strong>2015年11月</strong>: 游戏原型诞生，确立了Rougelike+TPS的核心玩法。</li>
  <li><strong>2016年2月</strong>: 宏观设计与程序架构确定，以“平行宇宙”作为世界观载体。</li>
  <li><strong>2016年3月</strong>: 项目正式立项，以“好玩、耐玩”为根本开发准则。</li>
  <li><strong>2016年6月</strong>: Alpha1版本完成，首次进行小范围核心玩家测试。</li>
  <li><strong>2016年7月</strong>: 美术风格重大革新，重制了大量游戏资源。</li>
  <li><strong>2016年8月</strong>: Alpha2版本完成，进行了首次万人级玩家测试。</li>
  <li><strong>2016年11月</strong>: Alpha3版本完成，游戏初具规模，包含三大主题关卡。</li>
  <li><strong>2017年3月</strong>: Alpha4版本测试，新增数十种武器、道具及怪物。</li>
  <li><strong>2017年6月</strong>: Alpha5版本开启了历时最长的一次玩家内测，共计23天。</li>
  <li><strong>2017年11月</strong>: Alpha6版本时，游戏内容已接近首个上线版本。</li>
  <li><strong>2018年1月</strong>: 进行了第7次玩家测试，为上线做最后打磨。</li>
  <li><strong>2018年2月</strong>: 春节期间进行精英玩家测试，并根据反馈快速迭代，新增了大量内容。</li>
  <li><strong>2018年4月26日</strong>: <strong>《比特小队》iOS版正式上线！</strong></li>
  <li><strong>2018年6月-7月</strong>: 安卓版筹备不删档内测，并陆续新增角色、大量武器道具及全新地图“比蒙”。</li>
  <li><strong>2018年11月-12月</strong>: 新增成员UMA，并发布首个节日主题版本“圣诞SP”。</li>
  <li><strong>2019年1月</strong>: 新增成员埃米纳基，并发布春节SP版本。</li>
  <li><strong>2019年4月26日</strong>: <strong>《比特小队》上线一周年！</strong></li>
  <li><strong>2019年6月15日</strong>: TapTap版本上线一周年，新增皮肤、每日挑战等多个新系统。</li>
  <li><strong>2019年12月6日</strong>: 新增支线星球——404实验室。</li>
</ul>

<p>这段漫长而详细的研发历程，不仅记录了游戏的成长，也见证了开发团队与玩家社区之间的紧密互动。正是这种不断的打磨和优化，才最终铸就了《比特小队》这款深受玩家喜爱的优秀作品。</p>]]></content><author><name>gongxh</name></author><category term="游戏项目" /><category term="Roguelike" /><category term="TPS" /><category term="IndieGame" /><category term="Kunpo" /><summary type="html"><![CDATA[深入了解国产Roguelike精品游戏《比特小队》的创作理念、核心玩法与详细研发历程。这是一款充满挑战、秘密和无尽循环的星际冒险。]]></summary></entry><entry><title type="html">网络模块</title><link href="https://me.bitgong.cn/posts/%E7%BD%91%E7%BB%9C%E6%A8%A1%E5%9D%97/" rel="alternate" type="text/html" title="网络模块" /><published>2025-06-07T16:00:00+00:00</published><updated>2025-06-07T16:00:00+00:00</updated><id>https://me.bitgong.cn/posts/%E7%BD%91%E7%BB%9C%E6%A8%A1%E5%9D%97</id><content type="html" xml:base="https://me.bitgong.cn/posts/%E7%BD%91%E7%BB%9C%E6%A8%A1%E5%9D%97/"><![CDATA[<h1 id="kunpocc-net专为小游戏和原生androidios平台开发的网络库">kunpocc-net：专为小游戏和原生(Android、iOS)平台开发的网络库</h1>

<h2 id="它解决什么问题">它解决什么问题</h2>

<p>如果你在开发小游戏，特别是需要同时支持微信、支付宝、字节跳动等多个平台，你会发现一个头疼的问题：每个平台的网络API都不完全一样。</p>

<p>微信小游戏用<code class="language-plaintext highlighter-rouge">wx.request</code>和<code class="language-plaintext highlighter-rouge">wx.connectSocket</code>，支付宝用<code class="language-plaintext highlighter-rouge">my.request</code>和<code class="language-plaintext highlighter-rouge">my.connectSocket</code>，字节跳动又是另一套API。写一次代码要适配多个平台，需要大量的if-else判断。</p>

<p>kunpocc-net就是为了解决这个问题——<strong>用统一的API，让你的网络代码在所有平台上都能跑</strong>。</p>

<h2 id="主要功能">主要功能</h2>

<h3 id="1-http请求模块">1. HTTP请求模块</h3>
<ul>
  <li>封装XMLHttpRequest，支持GET/POST/PUT/HEAD</li>
  <li>统一的API接口，不用管底层平台差异</li>
  <li>支持JSON、文本、二进制数据响应</li>
  <li>完整的错误处理和超时控制</li>
</ul>

<h3 id="2-websocket模块">2. WebSocket模块</h3>
<ul>
  <li>抹平各小游戏平台WebSocket API的差异</li>
  <li>统一的连接、收发消息、断开接口</li>
  <li>支持文本和二进制数据传输</li>
  <li>自动处理平台兼容性</li>
</ul>

<h3 id="3-网络文件读取">3. 网络文件读取</h3>
<ul>
  <li>简化网络文件加载流程</li>
  <li>统一的文件读取API</li>
</ul>

<h2 id="快速开始">快速开始</h2>

<h3 id="安装">安装</h3>
<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>npm <span class="nb">install </span>kunpocc-net
</code></pre></div></div>

<h3 id="http请求示例">HTTP请求示例</h3>

<div class="language-typescript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">import</span> <span class="p">{</span> <span class="nx">HttpManager</span><span class="p">,</span> <span class="nx">IHttpEvent</span> <span class="p">}</span> <span class="k">from</span> <span class="dl">'</span><span class="s1">kunpocc-net</span><span class="dl">'</span><span class="p">;</span>

<span class="c1">// 定义请求回调</span>
<span class="kd">const</span> <span class="nx">loginEvent</span><span class="p">:</span> <span class="nx">IHttpEvent</span> <span class="o">=</span> <span class="p">{</span>
    <span class="na">name</span><span class="p">:</span> <span class="dl">"</span><span class="s2">login</span><span class="dl">"</span><span class="p">,</span>
    <span class="na">onComplete</span><span class="p">:</span> <span class="p">(</span><span class="nx">response</span><span class="p">)</span> <span class="o">=&gt;</span> <span class="p">{</span>
        <span class="nx">console</span><span class="p">.</span><span class="nx">log</span><span class="p">(</span><span class="dl">'</span><span class="s1">登录成功:</span><span class="dl">'</span><span class="p">,</span> <span class="nx">response</span><span class="p">.</span><span class="nx">data</span><span class="p">);</span>
    <span class="p">},</span>
    <span class="na">onError</span><span class="p">:</span> <span class="p">(</span><span class="nx">response</span><span class="p">)</span> <span class="o">=&gt;</span> <span class="p">{</span>
        <span class="nx">console</span><span class="p">.</span><span class="nx">log</span><span class="p">(</span><span class="dl">'</span><span class="s1">登录失败:</span><span class="dl">'</span><span class="p">,</span> <span class="nx">response</span><span class="p">.</span><span class="nx">error</span><span class="p">);</span>
    <span class="p">}</span>
<span class="p">};</span>

<span class="c1">// 发送POST请求</span>
<span class="nx">HttpManager</span><span class="p">.</span><span class="nx">post</span><span class="p">(</span>
    <span class="dl">"</span><span class="s2">https://api.example.com/login</span><span class="dl">"</span><span class="p">,</span>
    <span class="p">{</span> <span class="na">username</span><span class="p">:</span> <span class="dl">"</span><span class="s2">test</span><span class="dl">"</span><span class="p">,</span> <span class="na">password</span><span class="p">:</span> <span class="dl">"</span><span class="s2">123456</span><span class="dl">"</span> <span class="p">},</span>
    <span class="dl">"</span><span class="s2">json</span><span class="dl">"</span><span class="p">,</span>
    <span class="nx">loginEvent</span>
<span class="p">);</span>

<span class="c1">// 发送GET请求</span>
<span class="nx">HttpManager</span><span class="p">.</span><span class="kd">get</span><span class="p">(</span>
    <span class="dl">"</span><span class="s2">https://api.example.com/userinfo</span><span class="dl">"</span><span class="p">,</span> 
    <span class="p">{</span> <span class="na">userId</span><span class="p">:</span> <span class="mi">123</span> <span class="p">},</span>
    <span class="dl">"</span><span class="s2">json</span><span class="dl">"</span><span class="p">,</span>
    <span class="nx">loginEvent</span>
<span class="p">);</span>
</code></pre></div></div>

<h3 id="websocket连接示例">WebSocket连接示例</h3>

<div class="language-typescript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">import</span> <span class="p">{</span> <span class="nx">Socket</span> <span class="p">}</span> <span class="k">from</span> <span class="dl">"</span><span class="s2">kunpocc-net</span><span class="dl">"</span><span class="p">;</span>

<span class="c1">// 创建连接</span>
<span class="kd">const</span> <span class="nx">socket</span> <span class="o">=</span> <span class="k">new</span> <span class="nx">Socket</span><span class="p">(</span><span class="dl">"</span><span class="s2">wss://game-server.com/ws</span><span class="dl">"</span><span class="p">,</span> <span class="p">{</span>
    <span class="na">binaryType</span><span class="p">:</span> <span class="dl">"</span><span class="s2">arraybuffer</span><span class="dl">"</span><span class="p">,</span>
    <span class="na">timeout</span><span class="p">:</span> <span class="mi">5000</span>
<span class="p">});</span>

<span class="c1">// 监听连接事件</span>
<span class="nx">socket</span><span class="p">.</span><span class="nx">onopen</span> <span class="o">=</span> <span class="p">()</span> <span class="o">=&gt;</span> <span class="p">{</span>
    <span class="nx">console</span><span class="p">.</span><span class="nx">log</span><span class="p">(</span><span class="dl">"</span><span class="s2">连接成功</span><span class="dl">"</span><span class="p">);</span>
<span class="p">};</span>

<span class="nx">socket</span><span class="p">.</span><span class="nx">onmessage</span> <span class="o">=</span> <span class="p">(</span><span class="nx">data</span><span class="p">)</span> <span class="o">=&gt;</span> <span class="p">{</span>
    <span class="nx">console</span><span class="p">.</span><span class="nx">log</span><span class="p">(</span><span class="dl">"</span><span class="s2">收到消息:</span><span class="dl">"</span><span class="p">,</span> <span class="nx">data</span><span class="p">);</span>
<span class="p">};</span>

<span class="nx">socket</span><span class="p">.</span><span class="nx">onclose</span> <span class="o">=</span> <span class="p">(</span><span class="nx">code</span><span class="p">,</span> <span class="nx">reason</span><span class="p">)</span> <span class="o">=&gt;</span> <span class="p">{</span>
    <span class="nx">console</span><span class="p">.</span><span class="nx">log</span><span class="p">(</span><span class="dl">"</span><span class="s2">连接断开:</span><span class="dl">"</span><span class="p">,</span> <span class="nx">code</span><span class="p">,</span> <span class="nx">reason</span><span class="p">);</span>
<span class="p">};</span>

<span class="c1">// 发送消息</span>
<span class="nx">socket</span><span class="p">.</span><span class="nx">send</span><span class="p">(</span><span class="dl">"</span><span class="s2">Hello Server</span><span class="dl">"</span><span class="p">);</span>

<span class="c1">// 发送二进制数据</span>
<span class="nx">socket</span><span class="p">.</span><span class="nx">sendBuffer</span><span class="p">(</span><span class="nx">buffer</span><span class="p">);</span>

<span class="c1">// 主动断开</span>
<span class="nx">socket</span><span class="p">.</span><span class="nx">close</span><span class="p">(</span><span class="mi">1000</span><span class="p">,</span> <span class="dl">"</span><span class="s2">正常关闭</span><span class="dl">"</span><span class="p">);</span>
</code></pre></div></div>

<h2 id="技术特点">技术特点</h2>

<p><strong>简单实用</strong>：API设计简洁，上手快。一个HttpManager.post()就能发请求，一个new Socket()就能建连接。</p>

<p><strong>跨平台兼容</strong>：自动识别运行平台，无需手动适配。在微信、支付宝、字节跳动小游戏，以及普通Web环境都能正常工作。</p>

<p><strong>类型安全</strong>：完整的TypeScript类型定义，IDE有完整的代码提示和类型检查。</p>

<p><strong>零依赖</strong>：不依赖任何第三方库，打包后体积小。</p>

<p><strong>生产可用</strong>：已在多个游戏项目中使用，稳定可靠。</p>

<h2 id="适用场景">适用场景</h2>

<ul>
  <li>小游戏网络通信（特别是需要多平台发布的项目）</li>
  <li>Web游戏开发</li>
  <li>需要简化HTTP请求管理的项目</li>
  <li>需要WebSocket长连接的实时游戏</li>
</ul>

<h2 id="项目信息">项目信息</h2>

<ul>
  <li><strong>版本</strong>: 0.0.2</li>
  <li><strong>作者</strong>: gongxh</li>
  <li><strong>开源协议</strong>: ISC</li>
  <li><strong>NPM包</strong>: <a href="https://www.npmjs.com/package/kunpocc-net">kunpocc-net</a></li>
  <li><strong>GitHub</strong>: <a href="https://github.com/Gongxh0901/kunpocc-net">kunpocc-net</a></li>
  <li><strong>Gitee</strong>: <a href="https://gitee.com/gongxinhai/kunpocc-net">kunpocc-net</a></li>
</ul>

<p>如果你在小游戏开发中遇到网络API兼容问题，或者想简化HTTP请求代码，kunpocc-net应该能帮到你。</p>]]></content><author><name>gongxh</name></author><category term="开源项目" /><category term="网络模块" /><category term="小游戏" /><category term="原生" /><category term="Android" /><category term="iOS" /><summary type="html"><![CDATA[一个专为小游戏和原生(Android、iOS)平台开发设计的轻量级网络库]]></summary></entry><entry><title type="html">调试与内存分析插件</title><link href="https://me.bitgong.cn/posts/%E8%B0%83%E8%AF%95%E4%B8%8E%E5%86%85%E5%AD%98%E5%88%86%E6%9E%90%E6%8F%92%E4%BB%B6/" rel="alternate" type="text/html" title="调试与内存分析插件" /><published>2025-05-30T02:00:00+00:00</published><updated>2025-05-30T02:00:00+00:00</updated><id>https://me.bitgong.cn/posts/%E8%B0%83%E8%AF%95%E4%B8%8E%E5%86%85%E5%AD%98%E5%88%86%E6%9E%90%E6%8F%92%E4%BB%B6</id><content type="html" xml:base="https://me.bitgong.cn/posts/%E8%B0%83%E8%AF%95%E4%B8%8E%E5%86%85%E5%AD%98%E5%88%86%E6%9E%90%E6%8F%92%E4%BB%B6/"><![CDATA[<p>面向 Cocos Creator 的运行时调试与内存分析插件。聚焦两件事：实时查看/编辑节点树，一键洞察资源内存占用。不改引擎、零依赖、即插即用。</p>

<h2 id="-核心功能左手节点树右手看内存">🎯 核心功能：左手节点树，右手看内存</h2>

<h2 id="项目信息">项目信息</h2>
<ul>
  <li><strong>商店地址</strong>: <a href="https://store.cocos.com/app/detail/7752">Cocos Store - 调试与内存分析插件</a></li>
  <li><strong>支持版本</strong>: Cocos Creator 2.4.0+</li>
  <li><strong>演示视频</strong>: <a href="https://www.bilibili.com/video/BV195KZzSE6d/">Cocos Creator 调试与内存分析插件演示</a></li>
  <li><strong>Cocos官方推荐</strong>: <a href="https://mp.weixin.qq.com/s/JoQG8H-9CO0P2M8Hn1iEqQ">微信公众号</a>。</li>
</ul>

<h3 id="-左手功能实时节点树显示">👈 左手功能：实时节点树显示</h3>

<p>✅ 运行时自动展示完整节点树（展开/收起）
✅ 点击节点 → 游戏画面高亮定位
✅ 属性实时编辑（立即生效，无需编译）</p>

<p>支持的属性包括：</p>
<ul>
  <li><code class="language-plaintext highlighter-rouge">active</code>：激活状态</li>
  <li><code class="language-plaintext highlighter-rouge">name</code>：节点名称</li>
  <li><code class="language-plaintext highlighter-rouge">x, y</code>：位置坐标</li>
  <li><code class="language-plaintext highlighter-rouge">width, height</code>：尺寸大小</li>
  <li><code class="language-plaintext highlighter-rouge">anchorX, anchorY</code>：锚点设置</li>
  <li><code class="language-plaintext highlighter-rouge">scaleX, scaleY</code>：缩放比例</li>
  <li><code class="language-plaintext highlighter-rouge">rotation</code>：旋转角度</li>
  <li><code class="language-plaintext highlighter-rouge">opacity</code>：透明度(0-255)</li>
  <li><code class="language-plaintext highlighter-rouge">color.r/g/b</code>：RGB颜色值</li>
</ul>

<h3 id="-右手功能资源内存占用查看">👉 右手功能：资源内存占用查看</h3>

<p>✅ 按 Bundle / 资源类型 / 路径分组
✅ 支持按大小与类型筛选（B/KB/MB/GB）
✅ 默认按占用降序，快速定位“大户”资源</p>

<h2 id="-技术特色真正的即插即用">💎 技术特色：真正的即插即用</h2>

<h3 id="-兼容性强">🔥 兼容性强</h3>
<ul>
  <li><strong>Creator 2.x</strong> ✅ （测试版本：2.4.13）</li>
  <li><strong>Creator 3.x</strong> ✅ （测试版本：3.0.0、3.8.6）</li>
  <li><strong>浏览器</strong>：Chrome（其他浏览器未测试）</li>
</ul>

<h3 id="-零依赖设计">⚡ 零依赖设计</h3>
<ul>
  <li>纯<strong>TypeScript</strong>原生实现</li>
  <li>无任何外部框架依赖</li>
  <li>无需修改引擎，真正的即插即用</li>
</ul>

<h3 id="-ui与交互">🎨 UI与交互</h3>
<ul>
  <li>深色主题，抽屉式面板，默认收起不遮挡画面</li>
</ul>

<h3 id="-性能友好">📱 性能友好</h3>
<ul>
  <li>收起几乎零开销；展开状态 100ms 更新一帧</li>
</ul>

<h2 id="-实际应用场景">🎮 实际应用场景</h2>

<h3 id="场景一复杂-ui-调整">场景一：复杂 UI 调整</h3>
<p>直接点击节点高亮定位，现场改位置/尺寸/透明度，省去反复编译验证。</p>

<h3 id="场景二性能优化分析-">场景二：性能优化分析 📊</h3>
<p>项目运行时感觉卡顿，怀疑是资源加载过多导致的内存压力。传统方式下，你只能靠经验猜测或者写复杂的统计代码。</p>

<p>打开内存面板，按 Bundle/类型/路径盘点占用，优先处理 Top 资源包与贴图集。</p>

<h3 id="场景三新人培训协作-">场景三：新人培训协作 👥</h3>
<p>新人看节点树结构，逐个点击即见高亮，十分钟熟悉主界面结构。</p>

<h2 id="-如何获取使用">🎁 如何获取使用</h2>

<h3 id="creator-2x-集成">Creator 2.x 集成</h3>
<p>1) 下载 <code class="language-plaintext highlighter-rouge">kunpo-inspector.min.js</code>
2) 放入项目目录
3) 在编辑器设置为插件</p>

<h3 id="creator-3x-集成">Creator 3.x 集成</h3>
<p>1) 推荐下载 <code class="language-plaintext highlighter-rouge">kunpo-inspector.min.mjs</code>（或 <code class="language-plaintext highlighter-rouge">kunpo-inspector.min.ts</code>）
2) 直接放入 <code class="language-plaintext highlighter-rouge">assets/</code> 目录，无需插件配置</p>

<h3 id="使用说明">使用说明</h3>
<ul>
  <li>默认收起；左侧展开节点树，右侧展开内存面板</li>
  <li>建议仅在开发环境启用，发布前移除或通过宏屏蔽</li>
</ul>

<p>如有建议与问题，欢迎在商店评论区留言，我会优先处理高频诉求并持续打磨体验。</p>]]></content><author><name>gongxh</name></author><category term="游戏开发工具" /><category term="Cocos Creator" /><category term="调试" /><category term="内存分析" /><category term="工具" /><category term="TypeScript" /><summary type="html"><![CDATA[即插即用的 Cocos Creator 调试与性能分析工具，实时编辑属性、定位节点、分析资源内存占用。]]></summary></entry><entry><title type="html">kunpocc框架</title><link href="https://me.bitgong.cn/posts/kunpocc%E6%A1%86%E6%9E%B6/" rel="alternate" type="text/html" title="kunpocc框架" /><published>2025-05-06T04:00:00+00:00</published><updated>2025-05-06T04:00:00+00:00</updated><id>https://me.bitgong.cn/posts/kunpocc%E6%A1%86%E6%9E%B6</id><content type="html" xml:base="https://me.bitgong.cn/posts/kunpocc%E6%A1%86%E6%9E%B6/"><![CDATA[<h1 id="kunpocc我的cocos-creator游戏开发框架">kunpocc：我的Cocos Creator游戏开发框架</h1>

<h2 id="项目简介">项目简介</h2>

<p>kunpocc（简称kunpocc）是我开发的一个基于Cocos Creator的游戏开发框架。在用Cocos Creator做游戏项目时，经常会遇到一些重复性的开发工作，比如UI窗口管理、资源加载、平台API差异处理等。为了提高开发效率，我把这些常用功能封装成了一个框架。</p>

<p>这个框架已经在几个商业项目中使用。</p>

<h2 id="技术栈">技术栈</h2>

<ul>
  <li><strong>TypeScript</strong>: 完全使用TypeScript编写，提供完整的类型定义</li>
  <li><strong>Cocos Creator</strong>: 支持3.7+版本</li>
  <li><strong>FairyGUI</strong>: UI编辑器，提供强大的UI制作能力</li>
  <li><strong>NPM</strong>: 已发布到npm，方便安装使用</li>
</ul>

<h2 id="主要功能模块">主要功能模块</h2>

<h3 id="1-ui管理系统">1. UI管理系统</h3>

<p>这是框架的核心功能，基于FairyGUI构建，提供完整的窗口管理功能：</p>

<p><strong>主要特性</strong>：</p>
<ul>
  <li>装饰器语法简化UI开发</li>
  <li>自动资源加载和释放</li>
  <li>窗口层级管理</li>
  <li>多窗口组管理</li>
  <li>顶部资源栏（Header）复用</li>
</ul>

<p><strong>使用示例</strong>：</p>
<div class="language-typescript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">import</span> <span class="p">{</span> <span class="nx">Window</span><span class="p">,</span> <span class="nx">_uidecorator</span> <span class="p">}</span> <span class="k">from</span> <span class="dl">'</span><span class="s1">kunpocc</span><span class="dl">'</span><span class="p">;</span>
<span class="kd">const</span> <span class="p">{</span> <span class="nx">uiclass</span><span class="p">,</span> <span class="nx">uiprop</span><span class="p">,</span> <span class="nx">uiclick</span> <span class="p">}</span> <span class="o">=</span> <span class="nx">_uidecorator</span><span class="p">;</span>

<span class="p">@</span><span class="nd">uiclass</span><span class="p">(</span><span class="dl">"</span><span class="s2">Window</span><span class="dl">"</span><span class="p">,</span> <span class="dl">"</span><span class="s2">Home</span><span class="dl">"</span><span class="p">,</span> <span class="dl">"</span><span class="s2">MainWindow</span><span class="dl">"</span><span class="p">)</span>
<span class="k">export</span> <span class="kd">class</span> <span class="nx">MainWindow</span> <span class="kd">extends</span> <span class="nx">Window</span> <span class="p">{</span>
    <span class="p">@</span><span class="nd">uiprop</span> <span class="k">private</span> <span class="nx">btnStart</span><span class="p">:</span> <span class="nx">GButton</span><span class="p">;</span>
    <span class="p">@</span><span class="nd">uiprop</span> <span class="k">private</span> <span class="nx">txtTitle</span><span class="p">:</span> <span class="nx">GTextField</span><span class="p">;</span>

    <span class="p">@</span><span class="nd">uiclick</span>
    <span class="k">private</span> <span class="nx">onBtnStart</span><span class="p">():</span> <span class="k">void</span> <span class="p">{</span>
        <span class="nx">console</span><span class="p">.</span><span class="nx">log</span><span class="p">(</span><span class="dl">'</span><span class="s1">开始游戏</span><span class="dl">'</span><span class="p">);</span>
    <span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>

<h3 id="2-平台工具">2. 平台工具</h3>

<p>提供平台检测和小游戏API封装，支持：</p>
<ul>
  <li>微信小游戏</li>
  <li>抖音小游戏</li>
  <li>支付宝小游戏</li>
</ul>

<p><strong>平台检测示例</strong>：</p>
<div class="language-typescript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">import</span> <span class="p">{</span> <span class="nx">Platform</span> <span class="p">}</span> <span class="k">from</span> <span class="dl">'</span><span class="s1">kunpocc</span><span class="dl">'</span><span class="p">;</span>

<span class="k">if</span> <span class="p">(</span><span class="nx">Platform</span><span class="p">.</span><span class="nx">isWX</span><span class="p">)</span> <span class="p">{</span>
    <span class="nx">console</span><span class="p">.</span><span class="nx">log</span><span class="p">(</span><span class="dl">'</span><span class="s1">当前是微信小游戏</span><span class="dl">'</span><span class="p">);</span>
<span class="p">}</span> <span class="k">else</span> <span class="k">if</span> <span class="p">(</span><span class="nx">Platform</span><span class="p">.</span><span class="nx">isAndroid</span><span class="p">)</span> <span class="p">{</span>
    <span class="nx">console</span><span class="p">.</span><span class="nx">log</span><span class="p">(</span><span class="dl">'</span><span class="s1">当前是Android平台</span><span class="dl">'</span><span class="p">);</span>
<span class="p">}</span>
</code></pre></div></div>

<p><strong>小游戏API使用</strong>：</p>
<div class="language-typescript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">import</span> <span class="p">{</span> <span class="nx">MiniHelper</span> <span class="p">}</span> <span class="k">from</span> <span class="dl">'</span><span class="s1">kunpocc</span><span class="dl">'</span><span class="p">;</span>

<span class="c1">// 获取小游戏基础信息</span>
<span class="kd">const</span> <span class="nx">common</span> <span class="o">=</span> <span class="nx">MiniHelper</span><span class="p">.</span><span class="nx">common</span><span class="p">();</span>

<span class="c1">// 播放广告</span>
<span class="kd">const</span> <span class="nx">ad</span> <span class="o">=</span> <span class="nx">MiniHelper</span><span class="p">.</span><span class="nx">ad</span><span class="p">();</span>

<span class="c1">// 支付功能</span>
<span class="kd">const</span> <span class="nx">pay</span> <span class="o">=</span> <span class="nx">MiniHelper</span><span class="p">.</span><span class="nx">pay</span><span class="p">();</span>
</code></pre></div></div>

<h3 id="3-全局计时器">3. 全局计时器</h3>

<p>提供统一的计时器管理功能：</p>

<div class="language-typescript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">import</span> <span class="p">{</span> <span class="nx">GlobalTimer</span> <span class="p">}</span> <span class="k">from</span> <span class="dl">'</span><span class="s1">kunpocc</span><span class="dl">'</span><span class="p">;</span>

<span class="c1">// 启动定时器 (回调函数, 时间间隔秒, 重复次数: 0=执行一次, -1=无限重复)</span>
<span class="kd">const</span> <span class="nx">timerId</span> <span class="o">=</span> <span class="nx">GlobalTimer</span><span class="p">.</span><span class="nx">startTimer</span><span class="p">(()</span> <span class="o">=&gt;</span> <span class="p">{</span>
    <span class="nx">console</span><span class="p">.</span><span class="nx">log</span><span class="p">(</span><span class="dl">'</span><span class="s1">定时器执行</span><span class="dl">'</span><span class="p">);</span>
<span class="p">},</span> <span class="mi">1</span><span class="p">,</span> <span class="o">-</span><span class="mi">1</span><span class="p">);</span> <span class="c1">// 每秒执行一次，无限重复</span>

<span class="c1">// 停止定时器</span>
<span class="nx">GlobalTimer</span><span class="p">.</span><span class="nx">stopTimer</span><span class="p">(</span><span class="nx">timerId</span><span class="p">);</span>

<span class="c1">// 暂停和恢复</span>
<span class="nx">GlobalTimer</span><span class="p">.</span><span class="nx">pauseTimer</span><span class="p">(</span><span class="nx">timerId</span><span class="p">);</span>
<span class="nx">GlobalTimer</span><span class="p">.</span><span class="nx">resumeTimer</span><span class="p">(</span><span class="nx">timerId</span><span class="p">);</span>
</code></pre></div></div>

<h3 id="4-屏幕适配工具">4. 屏幕适配工具</h3>

<p>提供屏幕尺寸相关的静态属性：</p>

<div class="language-typescript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">import</span> <span class="p">{</span> <span class="nx">Screen</span> <span class="p">}</span> <span class="k">from</span> <span class="dl">'</span><span class="s1">kunpocc</span><span class="dl">'</span><span class="p">;</span>

<span class="c1">// 获取屏幕信息</span>
<span class="nx">console</span><span class="p">.</span><span class="nx">log</span><span class="p">(</span><span class="dl">'</span><span class="s1">屏幕宽度:</span><span class="dl">'</span><span class="p">,</span> <span class="nx">Screen</span><span class="p">.</span><span class="nx">ScreenWidth</span><span class="p">);</span>
<span class="nx">console</span><span class="p">.</span><span class="nx">log</span><span class="p">(</span><span class="dl">'</span><span class="s1">屏幕高度:</span><span class="dl">'</span><span class="p">,</span> <span class="nx">Screen</span><span class="p">.</span><span class="nx">ScreenHeight</span><span class="p">);</span>
<span class="nx">console</span><span class="p">.</span><span class="nx">log</span><span class="p">(</span><span class="dl">'</span><span class="s1">设计分辨率:</span><span class="dl">'</span><span class="p">,</span> <span class="nx">Screen</span><span class="p">.</span><span class="nx">DesignWidth</span><span class="p">,</span> <span class="nx">Screen</span><span class="p">.</span><span class="nx">DesignHeight</span><span class="p">);</span>
<span class="nx">console</span><span class="p">.</span><span class="nx">log</span><span class="p">(</span><span class="dl">'</span><span class="s1">安全区域:</span><span class="dl">'</span><span class="p">,</span> <span class="nx">Screen</span><span class="p">.</span><span class="nx">SafeWidth</span><span class="p">,</span> <span class="nx">Screen</span><span class="p">.</span><span class="nx">SafeHeight</span><span class="p">);</span>
</code></pre></div></div>

<h3 id="5-热更新功能">5. 热更新功能</h3>

<p>提供热更新的基础功能：</p>

<div class="language-typescript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">import</span> <span class="p">{</span> <span class="nx">HotUpdateManager</span> <span class="p">}</span> <span class="k">from</span> <span class="dl">'</span><span class="s1">kunpocc</span><span class="dl">'</span><span class="p">;</span>

<span class="c1">// 需要先获取实例并初始化</span>
<span class="kd">const</span> <span class="nx">hotUpdate</span> <span class="o">=</span> <span class="nx">HotUpdateManager</span><span class="p">.</span><span class="nx">getInstance</span><span class="p">();</span>

<span class="c1">// 检查更新</span>
<span class="nx">hotUpdate</span><span class="p">.</span><span class="nx">checkUpdate</span><span class="p">().</span><span class="nx">then</span><span class="p">(</span><span class="nx">result</span> <span class="o">=&gt;</span> <span class="p">{</span>
    <span class="k">if</span> <span class="p">(</span><span class="nx">result</span><span class="p">.</span><span class="nx">code</span> <span class="o">===</span> <span class="nx">HotUpdateCode</span><span class="p">.</span><span class="nx">CHECK_FOUND_UPDATE</span><span class="p">)</span> <span class="p">{</span>
        <span class="nx">console</span><span class="p">.</span><span class="nx">log</span><span class="p">(</span><span class="dl">'</span><span class="s1">发现新版本</span><span class="dl">'</span><span class="p">);</span>
        <span class="c1">// 可以开始更新流程</span>
    <span class="p">}</span>
<span class="p">});</span>
</code></pre></div></div>

<h3 id="6-条件显示节点">6. 条件显示节点</h3>

<p>用于UI红点等条件显示功能，需要配合装饰器使用：</p>

<div class="language-typescript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">import</span> <span class="p">{</span> <span class="nx">_conditionDecorator</span><span class="p">,</span> <span class="nx">ConditionManager</span> <span class="p">}</span> <span class="k">from</span> <span class="dl">'</span><span class="s1">kunpocc</span><span class="dl">'</span><span class="p">;</span>

<span class="c1">// 条件系统需要先初始化</span>
<span class="nx">ConditionManager</span><span class="p">.</span><span class="nx">initCondition</span><span class="p">();</span>

<span class="c1">// 条件系统主要通过装饰器和条件类来管理UI节点的显示状态</span>
<span class="c1">// 具体使用方式需要定义条件类并配合UI节点</span>
</code></pre></div></div>

<h2 id="项目结构">项目结构</h2>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>src/
├── cocos/          # Cocos Creator适配层
├── condition/      # 条件显示模块
├── fgui/          # FairyGUI相关
├── global/        # 全局工具
├── hotupdate/     # 热更新
├── minigame/      # 小游戏平台
├── tool/          # 工具函数
└── ui/            # UI管理核心
</code></pre></div></div>

<h2 id="安装使用">安装使用</h2>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>npm <span class="nb">install </span>kunpocc
</code></pre></div></div>

<p>详细的使用文档和示例代码可以在项目的docs目录中找到。</p>

<h2 id="配套工具">配套工具</h2>

<p>框架还提供了一些配套的开发工具：</p>

<p><strong>kunpo-fgui插件</strong>: Cocos Creator插件，可以一键导出FairyGUI界面配置，减少手写代码量。</p>

<p><strong>独立模块</strong>：</p>
<ul>
  <li><a href="https://github.com/Gongxh0901/kunpo-ec">EC模块</a> - 实体组件系统</li>
  <li><a href="https://github.com/Gongxh0901/kunpocc-net">网络模块</a> - HTTP和WebSocket</li>
  <li><a href="https://github.com/Gongxh0901/kunpo-quadtree">四叉树</a> - 空间数据结构</li>
  <li><a href="https://github.com/Gongxh0901/kunpocc-behaviortree">行为树</a> - AI决策系统</li>
  <li><a href="https://github.com/Gongxh0901/kunpocc-assets">资源管理</a> - 资源加载管理</li>
  <li><a href="https://github.com/Gongxh0901/kunpocc-event">事件系统</a> - 全局事件管理</li>
</ul>

<h2 id="开源信息">开源信息</h2>

<ul>
  <li><strong>GitHub</strong>: <a href="https://github.com/Gongxh0901/kunpolibrary">https://github.com/Gongxh0901/kunpolibrary</a></li>
  <li><strong>Gitee</strong>: <a href="https://gitee.com/gongxh0901/kunpolibrary">https://gitee.com/gongxh0901/kunpolibrary</a></li>
  <li><strong>许可证</strong>: ISC License</li>
</ul>

<h2 id="项目特点">项目特点</h2>

<p>这个框架的设计原则是实用性优先。每个功能都是在实际项目中遇到问题后，总结出来的解决方案。代码结构清晰，文档相对完整，适合中小型游戏项目使用。</p>

<p>框架还在持续优化中，会根据实际使用中遇到的问题不断改进。如果你也在使用Cocos Creator开发游戏，欢迎试试这个框架，也欢迎提出改进建议。</p>

<hr />

<p><em>这个项目主要是为了解决我在游戏开发中遇到的实际问题。如果对其他Cocos Creator开发者也有帮助，那就更好了。</em></p>]]></content><author><name>gongxh</name></author><category term="开源项目" /><category term="Cocos Creator" /><category term="TypeScript" /><category term="游戏框架" /><category term="开源" /><category term="FairyGUI" /><summary type="html"><![CDATA[kunpocc是基于Cocos Creator的游戏开发框架，提供UI管理、平台适配、计时器等常用功能模块。]]></summary></entry><entry><title type="html">批量打包工具</title><link href="https://me.bitgong.cn/posts/%E6%89%B9%E9%87%8F%E6%89%93%E5%8C%85%E5%B7%A5%E5%85%B7/" rel="alternate" type="text/html" title="批量打包工具" /><published>2025-03-12T03:00:00+00:00</published><updated>2025-03-12T03:00:00+00:00</updated><id>https://me.bitgong.cn/posts/%E6%89%B9%E9%87%8F%E6%89%93%E5%8C%85%E5%B7%A5%E5%85%B7</id><content type="html" xml:base="https://me.bitgong.cn/posts/%E6%89%B9%E9%87%8F%E6%89%93%E5%8C%85%E5%B7%A5%E5%85%B7/"><![CDATA[<h1 id="批量打包工具解决多平台发布的重复劳动">批量打包工具：解决多平台发布的重复劳动</h1>

<h2 id="它解决什么问题">它解决什么问题</h2>

<p>如果你用Cocos Creator开发游戏，要发布到多个平台，你就知道有多痛苦了。</p>

<p>每次更新版本，需要：</p>
<ul>
  <li>iOS包：Creator构建 → 修改版本号 → Xcode打包 → 上传TestFlight</li>
  <li>Android包：Creator构建 → 生成热更新文件 → 修改版本号 → 签名打包 → 上传分发</li>
  <li>微信小游戏：Creator构建 → 上传CDN资源 → 上传小游戏包 → 设置体验版</li>
  <li>支付宝小游戏：Creator构建 → 上传CDN资源 → 上传小游戏包 → 设置体验版</li>
  <li>抖音小游戏：Creator构建 → 上传CDN资源 → 上传小游戏包 → 设置体验版</li>
  <li>鸿蒙包：Creator构建 → 生成热更新文件 → 修改版本号 → DevEco打包</li>
</ul>

<p>一个游戏6个平台，每个平台5-8步操作，手动操作一遍要1-2小时，而且容易出错。</p>

<p>这个工具就是为了<strong>把这些重复劳动自动化，一个命令搞定所有平台的打包发布</strong>。</p>

<h2 id="核心功能">核心功能</h2>

<h3 id="支持平台">支持平台</h3>
<ul>
  <li><strong>原生平台</strong>: Android、iOS、鸿蒙</li>
  <li><strong>小游戏平台</strong>: 微信、支付宝、抖音</li>
  <li><strong>多渠道</strong>: TapTap、小米、4399等Android渠道</li>
</ul>

<h3 id="自动化流程">自动化流程</h3>

<p><strong>原生平台打包:</strong></p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Creator构建 → 生成热更新manifest → 修改版本号/build号 → 平台打包 → 上传CDN → 飞书通知
</code></pre></div></div>

<p><strong>小游戏打包:</strong></p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Creator构建 → 上传远程资源到CDN → 上传小游戏后台 → 设置体验版 → 飞书通知+二维码
</code></pre></div></div>

<p><strong>热更新发布:</strong></p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Creator构建 → 生成新manifest文件 → 上传更新资源到CDN → 飞书通知
</code></pre></div></div>

<h2 id="快速开始">快速开始</h2>

<h3 id="1-安装依赖">1. 安装依赖</h3>
<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">cd </span>BuildTools
npm <span class="nb">install</span>
</code></pre></div></div>

<h3 id="2-配置文件">2. 配置文件</h3>
<p>修改<code class="language-plaintext highlighter-rouge">config.json</code>，主要配置：</p>

<div class="language-json highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">{</span><span class="w">
  </span><span class="nl">"creator"</span><span class="p">:</span><span class="w"> </span><span class="s2">"/Applications/Cocos/Creator/3.8.6/CocosCreator.app/Contents/MacOS/CocosCreator"</span><span class="p">,</span><span class="w">
  </span><span class="nl">"project"</span><span class="p">:</span><span class="w"> </span><span class="s2">"/Users/your-name/YourGame"</span><span class="p">,</span><span class="w">
  </span><span class="nl">"channels"</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="w">
    </span><span class="p">{</span><span class="w">
      </span><span class="nl">"channel"</span><span class="p">:</span><span class="w"> </span><span class="s2">"wechatgame"</span><span class="p">,</span><span class="w">
      </span><span class="nl">"name"</span><span class="p">:</span><span class="w"> </span><span class="s2">"微信小游戏"</span><span class="p">,</span><span class="w"> 
      </span><span class="nl">"platform"</span><span class="p">:</span><span class="w"> </span><span class="s2">"wechatgame"</span><span class="p">,</span><span class="w">
      </span><span class="nl">"config"</span><span class="p">:</span><span class="w"> </span><span class="s2">"./local/buildConfig_wechatgame.json"</span><span class="w">
    </span><span class="p">}</span><span class="w">
  </span><span class="p">],</span><span class="w">
  </span><span class="nl">"platforms"</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="w">
    </span><span class="p">{</span><span class="w">
      </span><span class="nl">"platform"</span><span class="p">:</span><span class="w"> </span><span class="s2">"android"</span><span class="p">,</span><span class="w">
      </span><span class="nl">"native"</span><span class="p">:</span><span class="w"> </span><span class="s2">"./native/engine/android/app"</span><span class="p">,</span><span class="w">
      </span><span class="nl">"build"</span><span class="p">:</span><span class="w"> </span><span class="s2">"./build/android/proj"</span><span class="w">
    </span><span class="p">}</span><span class="w">
  </span><span class="p">]</span><span class="w">
</span><span class="p">}</span><span class="w">
</span></code></pre></div></div>

<h3 id="3-环境变量">3. 环境变量</h3>
<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># 阿里云OSS（用于CDN上传）</span>
<span class="nb">export </span><span class="nv">OSS_ACCESS_KEY_ID</span><span class="o">=</span><span class="s2">"your-key-id"</span>
<span class="nb">export </span><span class="nv">OSS_ACCESS_KEY_SECRET</span><span class="o">=</span><span class="s2">"your-key-secret"</span>
<span class="nb">export </span><span class="nv">OSS_URL</span><span class="o">=</span><span class="s2">"your-oss-url"</span>
<span class="nb">export </span><span class="nv">OSS_REGION</span><span class="o">=</span><span class="s2">"oss-cn-hangzhou"</span>
<span class="nb">export </span><span class="nv">OSS_BUCKET</span><span class="o">=</span><span class="s2">"your-bucket"</span>

<span class="c"># 飞书通知机器人</span>
<span class="nb">export </span><span class="nv">FEISHU_ROBOT_WEBHOOK</span><span class="o">=</span><span class="s2">"your-webhook-url"</span>

<span class="c"># Android打包工具</span>
<span class="nb">export </span><span class="nv">APKSIGNER</span><span class="o">=</span><span class="s2">"/path/to/sdk/build-tools/30.0.3/apksigner"</span>
</code></pre></div></div>

<h3 id="4-使用方式">4. 使用方式</h3>

<p><strong>交互式打包:</strong></p>
<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>node BuildCommand.js
<span class="c"># 选择平台、输入版本号、选择debug/release</span>
</code></pre></div></div>

<p><strong>命令行打包:</strong></p>
<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># 打包微信小游戏 v1.2.0 release版</span>
node BuildJenkins.js <span class="nt">-p</span> wechatgame <span class="nt">-v</span> 1.2.0 <span class="nt">-b</span> 1 <span class="nt">-d</span> <span class="nb">false</span> <span class="nt">-n</span> <span class="nb">true</span>

<span class="c"># 热更新 Android v1.2.1</span>
node HotUpdateJenkins.js <span class="nt">-p</span> android <span class="nt">-v</span> 1.2.1 <span class="nt">-hot</span> 1001 <span class="nt">-d</span> <span class="nb">false</span> <span class="nt">-n</span> <span class="nb">true</span>
</code></pre></div></div>

<p><strong>集成Jenkins:</strong></p>
<div class="language-groovy highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// Jenkins Pipeline 示例</span>
<span class="n">pipeline</span> <span class="o">{</span>
    <span class="n">agent</span> <span class="n">any</span>
    <span class="n">parameters</span> <span class="o">{</span>
        <span class="n">choice</span><span class="o">(</span><span class="nl">name:</span> <span class="s1">'PLATFORM'</span><span class="o">,</span> <span class="nl">choices:</span> <span class="o">[</span><span class="s1">'wechatgame'</span><span class="o">,</span> <span class="s1">'android'</span><span class="o">,</span> <span class="s1">'harmonyos-next'</span><span class="o">],</span> <span class="nl">description:</span> <span class="s1">'选择平台'</span><span class="o">)</span>
        <span class="n">string</span><span class="o">(</span><span class="nl">name:</span> <span class="s1">'VERSION'</span><span class="o">,</span> <span class="nl">defaultValue:</span> <span class="s1">'1.0.0'</span><span class="o">,</span> <span class="nl">description:</span> <span class="s1">'版本号'</span><span class="o">)</span>
        <span class="n">booleanParam</span><span class="o">(</span><span class="nl">name:</span> <span class="s1">'IS_DEBUG'</span><span class="o">,</span> <span class="nl">defaultValue:</span> <span class="kc">false</span><span class="o">,</span> <span class="nl">description:</span> <span class="s1">'是否Debug版本'</span><span class="o">)</span>
    <span class="o">}</span>
    
    <span class="n">stages</span> <span class="o">{</span>
        <span class="n">stage</span><span class="o">(</span><span class="s1">'Build Game'</span><span class="o">)</span> <span class="o">{</span>
            <span class="n">steps</span> <span class="o">{</span>
                <span class="n">script</span> <span class="o">{</span>
                    <span class="n">sh</span> <span class="s2">"""
                        cd BuildTools
                        node BuildJenkins.js -p ${params.PLATFORM} -v ${params.VERSION} -b ${env.BUILD_NUMBER} -d ${params.IS_DEBUG} -n true
                    """</span>
                <span class="o">}</span>
            <span class="o">}</span>
        <span class="o">}</span>
    <span class="o">}</span>
    
    <span class="n">post</span> <span class="o">{</span>
        <span class="n">success</span> <span class="o">{</span>
            <span class="n">echo</span> <span class="s2">"构建成功: ${params.PLATFORM} v${params.VERSION}"</span>
        <span class="o">}</span>
        <span class="n">failure</span> <span class="o">{</span>
            <span class="n">echo</span> <span class="s2">"构建失败"</span>
        <span class="o">}</span>
    <span class="o">}</span>
<span class="o">}</span>
</code></pre></div></div>

<h2 id="技术特点">技术特点</h2>

<p><strong>配置化驱动</strong>: 通过JSON配置文件管理所有平台参数，不需要改代码。</p>

<p><strong>模块化设计</strong>: 每个平台一个独立模块，添加新平台只需实现对应接口。</p>

<p><strong>完整的构建链</strong>: 从Creator构建到最终发布的完整自动化。</p>

<p><strong>错误处理</strong>: 每步操作都有结果检查，构建失败会及时停止并通知。</p>

<p><strong>通知集成</strong>: 自动发送飞书通知，包含版本信息、下载链接、二维码。</p>

<h2 id="二次开发指南">二次开发指南</h2>

<h3 id="添加新平台">添加新平台</h3>

<ol>
  <li><strong>创建平台模块</strong>
    <div class="language-javascript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// platforms/YourPlatform.js</span>
<span class="kd">class</span> <span class="nx">BuildYourPlatform</span> <span class="p">{</span>
 <span class="kd">static</span> <span class="k">async</span> <span class="nx">build</span><span class="p">(</span><span class="nx">config</span><span class="p">,</span> <span class="nx">channel</span><span class="p">,</span> <span class="nx">version</span><span class="p">,</span> <span class="nx">buildCode</span><span class="p">,</span> <span class="nx">isDebug</span><span class="p">)</span> <span class="p">{</span>
     <span class="c1">// 实现平台特定的构建逻辑</span>
     <span class="k">return</span> <span class="nx">Result</span><span class="p">.</span><span class="nx">success</span><span class="p">();</span>
 <span class="p">}</span>
<span class="p">}</span>
</code></pre></div>    </div>
  </li>
  <li><strong>注册到主流程</strong>
    <div class="language-javascript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// AutoBuild.js</span>
<span class="k">this</span><span class="p">.</span><span class="nx">_platformFlowBindFuncs</span> <span class="o">=</span> <span class="p">{</span>
 <span class="p">[</span><span class="dl">'</span><span class="s1">your-platform</span><span class="dl">'</span><span class="p">]:</span> <span class="k">this</span><span class="p">.</span><span class="nx">buildYourPlatformFlow</span><span class="p">,</span>
 <span class="c1">// ... 其他平台</span>
<span class="p">}</span>
</code></pre></div>    </div>
  </li>
  <li><strong>添加配置</strong>
    <div class="language-json highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">{</span><span class="w">
  </span><span class="nl">"channels"</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="w">
 </span><span class="p">{</span><span class="w">
   </span><span class="nl">"channel"</span><span class="p">:</span><span class="w"> </span><span class="s2">"your-channel"</span><span class="p">,</span><span class="w">
   </span><span class="nl">"name"</span><span class="p">:</span><span class="w"> </span><span class="s2">"Your Platform"</span><span class="p">,</span><span class="w">
   </span><span class="nl">"platform"</span><span class="p">:</span><span class="w"> </span><span class="s2">"your-platform"</span><span class="p">,</span><span class="w"> 
   </span><span class="nl">"config"</span><span class="p">:</span><span class="w"> </span><span class="s2">"./local/buildConfig_your-platform.json"</span><span class="w">
 </span><span class="p">}</span><span class="w">
  </span><span class="p">]</span><span class="w">
</span><span class="p">}</span><span class="w">
</span></code></pre></div>    </div>
  </li>
</ol>

<h3 id="自定义构建步骤">自定义构建步骤</h3>

<p>继承基础构建类，重写需要的方法：</p>

<div class="language-javascript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">class</span> <span class="nx">CustomAndroidBuild</span> <span class="kd">extends</span> <span class="nx">BuildAndroid</span> <span class="p">{</span>
    <span class="kd">static</span> <span class="k">async</span> <span class="nx">beforeBuild</span><span class="p">()</span> <span class="p">{</span>
        <span class="c1">// 构建前的自定义操作</span>
        <span class="k">await</span> <span class="k">super</span><span class="p">.</span><span class="nx">beforeBuild</span><span class="p">();</span>
        <span class="c1">// 你的自定义逻辑</span>
    <span class="p">}</span>
    
    <span class="kd">static</span> <span class="k">async</span> <span class="nx">afterBuild</span><span class="p">()</span> <span class="p">{</span>
        <span class="c1">// 构建后的自定义操作  </span>
        <span class="k">await</span> <span class="k">super</span><span class="p">.</span><span class="nx">afterBuild</span><span class="p">();</span>
        <span class="c1">// 你的自定义逻辑</span>
    <span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>

<h3 id="扩展通知方式">扩展通知方式</h3>

<p>添加新的通知渠道：</p>

<div class="language-javascript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// notifications/YourNotification.js</span>
<span class="kd">class</span> <span class="nx">YourNotification</span> <span class="p">{</span>
    <span class="kd">static</span> <span class="k">async</span> <span class="nx">send</span><span class="p">(</span><span class="nx">title</span><span class="p">,</span> <span class="nx">content</span><span class="p">,</span> <span class="nx">isDebug</span><span class="p">)</span> <span class="p">{</span>
        <span class="c1">// 实现通知逻辑</span>
    <span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>

<h3 id="核心模块说明">核心模块说明</h3>

<ul>
  <li><code class="language-plaintext highlighter-rouge">AutoBuild.js</code>: 主控制器，协调各模块</li>
  <li><code class="language-plaintext highlighter-rouge">BuildCreator3.8.js</code>: Creator构建封装</li>
  <li><code class="language-plaintext highlighter-rouge">platforms/</code>: 各平台构建实现</li>
  <li><code class="language-plaintext highlighter-rouge">utils/</code>: 通用工具类（命令执行、文件操作、结果处理）</li>
  <li><code class="language-plaintext highlighter-rouge">oss/</code>: CDN上传模块</li>
  <li><code class="language-plaintext highlighter-rouge">NotificationFeishu/</code>: 飞书通知模块</li>
</ul>

<h3 id="开发建议">开发建议</h3>

<p><strong>遵循现有模式</strong>: 每个平台模块都返回<code class="language-plaintext highlighter-rouge">Result</code>对象，统一错误处理。</p>

<p><strong>配置优先</strong>: 尽量通过配置文件控制行为，减少硬编码。</p>

<p><strong>详细日志</strong>: 使用<code class="language-plaintext highlighter-rouge">Colors.js</code>输出带颜色的日志，方便调试。</p>

<p><strong>异常处理</strong>: 每个异步操作都要try-catch，构建失败要有明确提示。</p>

<h2 id="适用场景">适用场景</h2>

<ul>
  <li>Cocos Creator游戏多平台发布</li>
  <li>需要频繁更新版本的项目</li>
  <li>有多个Android渠道的游戏</li>
  <li>团队协作需要标准化构建流程的项目</li>
  <li>Jenkins集成自动化发布</li>
</ul>

<p>如果你也被多平台打包折磨，不妨试试。</p>]]></content><author><name>gongxh</name></author><category term="开源项目" /><category term="Cocos Creator" /><category term="游戏框架" /><category term="开源" /><category term="自动化" /><category term="工具集" /><summary type="html"><![CDATA[专为Cocos Creator多平台发布设计的自动化打包工具]]></summary></entry></feed>