Deprecated: parse_url(): Passing null to parameter #1 ($url) of type string is deprecated in /www/wwwroot/blog_qqvbc_com/usr/plugins/Access/Access_Core.php on line 339

Deprecated: parse_url(): Passing null to parameter #1 ($url) of type string is deprecated in /www/wwwroot/blog_qqvbc_com/usr/plugins/Access/Access_Core.php on line 392

Deprecated: parse_url(): Passing null to parameter #1 ($url) of type string is deprecated in /www/wwwroot/blog_qqvbc_com/usr/plugins/Access/Access_Core.php on line 394
Joyber 的博客

Cocos Creator 3.x 引擎系统进阶学习路线(12周)

前提:已掌握 TS 基础、Node/Component 生命周期、能独立做简单小游戏。本路线聚焦工程化、性能、内存、框架封装,不覆盖入门操作。
版本:Cocos Creator 3.8.x,目标平台微信小游戏。

第1周:资源系统进阶(生命周期与分包)

学习目标

  • 吃透 assetManager 的加载、缓存、释放链路,能定位内存泄漏
  • 掌握引用计数 addRef / decRef 的手动管理时机
  • 掌握分包加载与远程资源热更前置能力

核心知识点

  • resources.load / assetManager.loadAny 的加载管线与版本配置
  • 自动释放 vs 手动释放:场景切换后哪些资源会被自动回收
  • Asset.addRef() / decRef() 与内存泄漏的关系
  • 图集 SpriteAtlas 的依赖资源(SpriteFrame)引用链
  • 分包配置:主包 / 分包 / 远程包的加载策略
  • loadRemote 远程图片与缓存清理

练习 Demo:资源加载器封装

  • 写一个 ResManager 单例,封装 load / loadDir / release / releaseAll
  • 支持按模块(UI / 音效 / 场景)分组释放
  • 加载一个预制体 → 实例化 → 延迟销毁 → 主动释放资源,在微信开发者工具 Memory 面板观察纹理是否被回收

验收标准

  • 能解释为什么直接 destroy(node) 后纹理还在内存中
  • 能写出正确释放一个 Prefab 及其依赖纹理的代码
  • 配置一个分包,主包启动时不加载分包资源,进入对应界面时才加载
  • Demo 连续进出场景 10 次,内存曲线不持续上涨

第2周:节点组件架构与对象池

学习目标

  • 摆脱"一个脚本写到底",建立可维护的组件化架构
  • 掌握对象池解决频繁实例化开销

核心知识点

  • 节点树查找的性能代价:find() vs 缓存引用
  • 组件间通信的三种方式:直接 getComponent / 节点事件 / 全局事件总线
  • NodePool 对象池:创建、获取、回收、预热
  • 预制体实例化 instantiate 的开销与适用场景
  • 节点 active 切换 vs destroy 的性能差异

练习 Demo:子弹/掉落物对象池

  • 实现一个 ObjPool<T> 通用对象池组件
  • 生成 200 个移动的子弹,用池化方式复用,禁止每帧 instantiate
  • 对比池化前后帧率(在微信开发者工具 Performance 面板查看)

验收标准

  • 1 秒内生成 100 个对象不卡顿,GC 次数明显下降
  • 对象回收时正确重置状态(位置、旋转、透明度、事件监听)
  • 能说出直接 instantiate 在小游戏低端机上的问题

第3周:UI 框架与列表复用

学习目标

  • 搭出可复用的 UI 框架:弹窗栈、界面基类、自动适配
  • 掌握 ScrollView 高效复用(这是小游戏列表性能关键)

核心知识点

  • UI 基类设计:onOpen / onClose / onShow / onHide 生命周期
  • 弹窗管理器:弹窗栈、遮罩、模态队列、关闭回调
  • Widget 锚点适配与安全区(刘海屏)
  • ScrollView + Layout 普通模式 vs 列表虚拟滚动
  • GridLayout 与列表项预制体复用
  • Label 文字排版、RichText 性能开销

练习 Demo:完整 UI 框架 + 长列表

  • 实现 UIManager:open(prefabPath) / close(uiName) / closeAll
  • 实现一个 1000 条数据的排行榜列表,滚动不卡顿(用复用而非全量创建)
  • 做一个多层弹窗叠加:主界面 → 弹窗A → 弹窗B → 关闭B回到A

验收标准

  • 列表滚动 60fps(中低端机),节点数量保持在屏幕可见项附近
  • 弹窗打开关闭有统一动画和回调,不出现层级错乱
  • 切换不同分辨率设计稿,UI 不错位、不被刘海遮挡

第4周:动画系统进阶

学习目标

  • 掌握 Tween 链式编排与动画状态机,不依赖手动写帧动画
  • 理解 Animation 组件的状态控制与事件

核心知识点

  • tween 链式 API:sequence / parallel / repeat / delay / call
  • Tween 的 stop / pause / resume 与 target 引用管理
  • Animation 组件:play / crossFade / stop,动画状态切换
  • 动画事件(Animation Event)回调到脚本
  • Spine 骨骼动画基础(如角色需要):动画混合、插槽控制
  • 动效性能:避免每帧修改 color / position 触发过度渲染

练习 Demo:游戏开场动效编排

  • 用 Tween 编排:Logo 缩放入场 → 文字淡入 → 按钮弹起 → 可跳过
  • 实现一个角色待机/移动/受伤三个 AnimationClip 的状态切换
  • 给按钮做统一的按压缩放动效(可复用组件)

验收标准

  • 动效时序精确,能在中途跳过且不残留状态
  • 动画切换无突兀跳变(用 crossFade)
  • 关闭界面时正确停止 Tween,防止回调操作已销毁节点

第5周:输入系统与手势

学习目标

  • 脱离 Button 组件,处理复杂触摸手势与多点触控
  • 掌握坐标转换,适配不同分辨率

核心知识点

  • 触摸事件:TOUCH_START / MOVE / END / CANCEL
  • 事件冒泡与捕获,swallowTouches 的作用
  • 屏幕坐标 → UI 坐标 → 世界坐标的转换
  • 手势识别:点击、长按、滑动方向、双指缩放
  • 多点触控:touch id 跟踪
  • 微信小游戏:触摸事件与鼠标事件的差异

练习 Demo:拖拽拼图 / 摇杆控制

  • 实现一个可拖拽的物体,跟随手指,松手有回弹
  • 实现一个虚拟摇杆:控制角色移动方向和速度
  • 实现双指缩放一个图片

验收标准

  • 拖拽跟手,无明显延迟,快速滑动不丢失 touch
  • 摇杆有死区和最大半径,输出方向向量正确
  • 在不同分辨率手机上,触摸坐标映射正确

第6周:事件总线与架构模式

学习目标

  • 建立模块间解耦通信,避免节点互相引用导致的强耦合
  • 掌握常用游戏架构模式在 Cocos 中的落地

核心知识点

  • EventTarget 全局事件总线:on / off / emit / targetOff
  • 事件监听的生命周期:onLoad 监听,onDestroy 必须移除
  • 命令模式 / 消息队列:UI 事件 → 命令 → 逻辑处理
  • 单例管理器设计:GameManager / DataManager / AudioManager
  • MVC 思想:数据、视图、逻辑分离
  • 防止事件泄漏:组件销毁后仍收到事件导致报错

练习 Demo:全局事件通信

  • 实现一个 EventBus 单例
  • 战斗逻辑层发出 SCORE_CHANGE 事件,UI 层监听并刷新分数,两者不直接引用
  • 实现一个简单的命令队列:点击按钮 → 执行命令 → 广播结果

验收标准

  • 关闭任意界面后,该界面注册的事件全部移除(无残留监听)
  • 模块之间不出现 getComponent 跨层硬引用
  • 能解释为什么节点销毁后不移除监听会报错

第7周:音频系统与微信适配

学习目标

  • 掌握音频池与动态加载,解决微信小游戏音频常见问题
  • BGM 与音效分离管理

核心知识点

  • AudioSource 组件 vs audioManager 接口
  • 音效池:多个相同音效同时播放(如连续射击)
  • BGM 循环、淡入淡出、切换
  • 微信小游戏音频限制:必须由用户手势触发才能播放
  • 音频格式:mp3 vs wav 的包体与性能权衡
  • 静音开关与音量持久化

练习 Demo:音频管理器

  • 实现 AudioManager:playMusic / playSound / stopAll / setVolume
  • 音效对象池:同时播放多个射击音效不互相打断
  • 处理微信:首次点击屏幕后才解锁音频

验收标准

  • 连续快速点击按钮,音效叠加播放不卡顿
  • 切后台 BGM 暂停,回前台恢复
  • 重启游戏后音量设置保留

第8周:数据驱动与存档系统

学习目标

  • 用配置表驱动游戏数值,不硬编码
  • 掌握存档读写、版本迁移、数据保护

核心知识点

  • JSON 配置表加载:resources.load 加载 JsonAsset
  • 数据管理器:全局单例管理玩家数据
  • sys.localStorage 读写与序列化
  • 存档版本号:版本升级时旧存档迁移
  • 数据加密(可选):防止简单篡改
  • 脏数据标记:哪些数据变化了需要写盘(避免频繁 IO)

练习 Demo:玩家数据系统

  • 设计一个 PlayerData:金币、最高分、关卡进度、设置项
  • 实现 DataManager:load / save / reset / 自动存档
  • 做一个关卡配置表(JSON),从配置读取关卡参数,改配置不改代码

验收标准

  • 杀进程重进游戏,数据完整恢复
  • 修改配置表后,不改动脚本即可改变关卡数值
  • 存档损坏时有兜底(读默认值),不白屏

第9周:渲染性能优化

学习目标

  • 能定位并解决 DrawCall、过度绘制、纹理内存问题
  • 这是微信小游戏包体和帧率达标的关键

核心知识点

  • DrawCall 概念:为什么合图能减少 DrawCall
  • SpriteAtlas 图集规划:按界面/模块打包,避免全量图集
  • 动态合图 vs 静态图集的适用场景
  • 过度绘制:半透明遮罩叠加层数
  • 纹理格式:ETC2 / ASTC 压缩,包体与内存
  • 节点数量优化:不可见节点及时 active = false
  • 微信开发者工具:DrawCall、FPS、内存面板使用

练习 Demo:性能对比优化

  • 做一个有 50 个 Sprite 的界面,先散图后合图,对比 DrawCall
  • 找出一个过度绘制严重的界面(多层半透明),优化层数
  • 用 ASTC 压缩纹理,对比包体大小

验收标准

  • 能在开发者工具中读出当前场景 DrawCall 数
  • 同屏 100 个同图集 Sprite,DrawCall 保持低位
  • 主包纹理内存控制在微信限制内(通常 ≤ 150MB 运行内存)

第10周:物理系统进阶(2D)

学习目标

  • 掌握 2D 物理的碰撞过滤、传感器、物理调试
  • 理解物理与游戏逻辑的解耦

核心知识点

  • RigidBody2D 三种类型:dynamic / static / kinematic
  • Collider2D:Box / Circle / Polygon,传感器(isSensor)
  • 碰撞回调:onBeginContact / onEndContact,碰撞对象信息
  • 碰撞分组与掩码:group / category,避免不必要碰撞
  • 物理材质:摩擦、弹性
  • 物理调试绘制:PhysicsSystem2D.instance.debugDrawFlags
  • 物理更新时机:fixedUpdate vs update

练习 Demo:碰撞检测游戏

  • 实现一个角色(kinematic)与多个障碍物(static)碰撞
  • 用传感器触发"进入区域"事件,而非物理阻挡
  • 配置碰撞分组:玩家只与敌人碰撞,不与背景碰撞

验收标准

  • 碰撞回调能正确拿到双方节点并执行逻辑
  • 传感器触发不产生物理反弹
  • 开启调试绘制,碰撞体形状与视觉一致

第11周:网络层与实时通信

学习目标

  • 封装统一请求层,处理超时、重试、错误码
  • 了解 WebSocket 长连接基础

核心知识点

  • fetch / wx.request 的 Promise 封装
  • 请求拦截:统一加 token、loading、错误提示
  • 错误码处理:登录失效、网络断开、超时
  • WebSocket:连接、重连、心跳、消息分发
  • 协议选择:JSON vs protobuf(小游戏包体敏感)
  • 微信小游戏:必须 https / wss,域名白名单配置

练习 Demo:网络管理器

  • 封装 HttpManager:get / post,统一错误处理
  • 实现一个简单的 WebSocket 连接:断线自动重连 + 心跳
  • 请求一个远程配置接口,失败时有重试

验收标准

  • 断网时请求有友好提示,不卡死
  • WebSocket 断开后能自动重连
  • 所有请求走统一入口,不散落 wx.request

第12周:微信小游戏专项与发布

学习目标

  • 完成从开发到上传的全流程,处理平台特有限制
  • 掌握分包、广告、性能监控

核心知识点

  • 构建发布:微信小游戏构建选项、资源服务器配置
  • 主包 4M 限制:分包策略、远程资源
  • 微信登录:wx.login → code → 后端换 session
  • 激励视频广告:加载、播放、关闭回调、失败兜底
  • 插屏广告、Banner 广告
  • 开放数据域:好友排行榜(独立子域)
  • 性能监控:wx.getPerformance、错误上报
  • 审核注意事项:隐私协议、类目、内容合规

练习 Demo:可上传的完整小游戏

  • 选一个前面做过的 demo,配置分包:主包只放启动和首页,其余进分包
  • 接入一个激励视频广告:看广告复活 / 得奖励
  • 配置微信登录,获取 openid(可模拟)
  • 上传体验版,在真机测试

验收标准

  • 主包体积 ≤ 4M,分包按需加载
  • 广告加载失败时有兜底逻辑(不卡死游戏)
  • 真机测试:启动时间、帧率、内存无明显异常
  • 能走通上传 → 提审流程(不要求真过审)

配套交付物清单(每周产出)

周次产出物
1ResManager 封装 + 内存对比截图
2通用对象池 + 帧率对比
3UIManager + 可复用列表组件
4动效编排脚本 + 状态切换
5拖拽/摇杆组件
6EventBus + 模块解耦 demo
7AudioManager
8DataManager + 配置表
9优化前后 DrawCall 对比
10物理碰撞 demo
11HttpManager + WebSocket 封装
12可上传的微信小游戏工程

学习建议

  1. 每周一个可运行 demo,不要只看文档;每周末用微信开发者工具真机自测一次。
  2. 第 1、3、9、12 周是微信小游戏上线的硬骨头,建议多花时间。
  3. 遇到问题优先查 Cocos 官方 API 文档和微信小游戏官方文档,第三方博客版本可能滞后。

=AVERAGEIF(C:C,"餐后",D:D)

作用

单条件求平均值:在C列中,找到单元格内容等于餐后的行,对应D列的数值,计算这些数字的算术平均值。

语法

AVERAGEIF(条件区域, 条件, 求平均区域)
  • C:C:条件区域,用来判断的列(这里是C整列)
  • "餐后":判断条件,单元格文字必须完全匹配“餐后”才参与计算,文本条件要加英文双引号
  • D:D:要计算平均值的数据列

示例

C列是类型(餐前/餐后),D列是血糖数值

CD
餐后12.3
餐前8.1
餐后14.2

公式会筛选两行餐后,计算 (12.3+14.2)/2 = 13.25

注意点

  1. 条件文字区分空格,"餐后 "(后面带空格)和"餐后"判定为不相同
  2. D列中非数字内容会自动忽略,不参与平均
  3. 找不到满足条件的单元格,公式返回#DIV/0!除零错误

✅ 博客最常见两种:

1. particles.js(老博客最经典),升级版本叫 tsParticles(推荐新项目)

就是你描述效果:页面背景漂浮小点,近的点自动连线,鼠标移动附近粒子会吸附(grab抓取模式,粒子被鼠标吸过去,还拉出连线),Canvas实现。

  • onhover.mode: "grab":鼠标靠近,粒子吸附到光标,带连线,就是你看到的效果
  • line_linked:粒子互相之间近距离自动生成细线>

    很多Hexo、VuePress、VitePress博客早年全用这个。

2. canvas-nest.js(国内博客大量用,极简!)

国内博客圈非常流行,只有几百行原生JS,不需要任何依赖。
特点:背景小点、点之间连线;鼠标靠近,粒子向鼠标聚拢吸附。
很多sakura、hexo博客模板自带这个,代码极轻。

区别:canvas-nest功能少,只能做背景粒子;particles/tsParticles自定义能力极强。

简单区分

  • 只想快速给博客加背景粒子吸附效果,不想折腾配置:canvas-nest.js,一行引入直接用。
  • 需要精细调颜色、粒子数量、多种鼠标模式(吸附/排斥/点击爆炸):tsParticles(新版particles)

补充:和刚才模块网络图的区别

⚠️ 注意区分:

  1. particles.js / canvas-nest:背景粒子特效,粒子自由漂浮,鼠标吸附,用于博客首页背景;
  2. force-graph / d3-force:力导向图,是表达数据之间依赖关系(你前面Vite模块图),不是单纯装饰背景。

tsParticles 关键配置片段(实现鼠标吸附抓取效果)

"interactivity": {
  "events": {
    "onhover": {
      "enable": true,
      "mode": "grab" // grab=吸附抓取,鼠标和粒子之间拉出连线
    }
  },
  "modes": {
    "grab": {
      "distance": 140,
      "links": {
        "opacity": 0.5
      }
    }
  }
}

canvas-nest 最简引入

<script src="[https://cdn.jsdelivr.net/npm/canvas-nest.js/dist/canvas-nest.min.js](https://cdn.jsdelivr.net/npm/canvas-nest.js/dist/canvas-nest.min.js)"></script>

引入页面直接生效,无需额外配置。

你是打算加到Vue博客页面做背景吗?我可以直接给对应的Vue版本代码。

方案1:concat 拼接(按顺序首尾相连,最常用)

  1. 先新建 list.txt

    file 'audio1.mp3'
    file 'audio2.mp3'
    file 'audio3.mp3'
  2. ffmpeg命令

    ffmpeg -f concat -safe 0 -i list.txt -c:a libmp3lame output.mp3
  3. -safe 0:允许文件使用绝对路径
  4. -c:a libmp3lame:mp3编码

方案2:直接在命令行写3个文件,不用txt

ffmpeg -i audio1.mp3 -i audio2.mp3 -i audio3.mp3 -filter_complex "[0:a][1:a][2:a]concat=n=3:v=0:a=1" -c:a libmp3lame output.mp3
注意:此方式要求三个音频采样率、声道、采样位完全一致,否则会失真/失败。推荐优先用方案1。

方案3:混音(三个音频同时播放,叠加在一起,不是首尾拼接)

ffmpeg -i audio1.mp3 -i audio2.mp3 -i audio3.mp3 -filter_complex "[0:a][1:a][2:a]amix=inputs=3:duration=longest" -c:a libmp3lame output.mp3

常用可选参数

  • 指定码率 -b:a 128k

    ffmpeg -f concat -safe 0 -i list.txt -c:a libmp3lame -b:a 128k output.mp3

区分:

  • concat:一段播完接着下一段(拼接)
  • amix:三个声音一起响(混音叠加)

文章:https://juejin.cn/post/7114869372144844814
一个演示站:https://kingrayhan.github.io/javascript-drum-kit/

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>JS Drum kit</title>
    <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/normalize/5.0.0/normalize.min.css">
    <link rel="stylesheet" href="css/style.css">

    <!--Open graph meta-->
    <meta property="og:url"                content="" />
    <meta property="og:type"               content="article" />
    <meta property="og:title"              content="Javascript Drum Kit" />
    <meta property="og:description"        content="Play Drum by keyboard" />
    <meta property="og:image"              content="images/og.jpg" />

</head>
<body>
<h1 class="title">JavaScript Drum Kit</h1>
<div class="keys">
    <p class="note">Use keyboard to play drum</p>
    <div data-key="65" class="key">
        <kbd>A</kbd>
        <span class="sound">clap</span>
    </div>
    <div data-key="83" class="key">
        <kbd>S</kbd>
        <span class="sound">hihat</span>
    </div>
    <div data-key="68" class="key">
        <kbd>D</kbd>
        <span class="sound">kick</span>
    </div>
    <div data-key="70" class="key">
        <kbd>F</kbd>
        <span class="sound">openhat</span>
    </div>
    <div data-key="71" class="key">
        <kbd>G</kbd>
        <span class="sound">boom</span>
    </div>
    <div data-key="72" class="key">
        <kbd>H</kbd>
        <span class="sound">ride</span>
    </div>
    <div data-key="74" class="key">
        <kbd>J</kbd>
        <span class="sound">snare</span>
    </div>
    <div data-key="75" class="key">
        <kbd>K</kbd>
        <span class="sound">tom</span>
    </div>
    <div data-key="76" class="key">
        <kbd>L</kbd>
        <span class="sound">tink</span>
    </div>
</div>
<audio data-key="65" src="sounds/clap.wav"></audio>
<audio data-key="83" src="sounds/hihat.wav"></audio>
<audio data-key="68" src="sounds/kick.wav"></audio>
<audio data-key="70" src="sounds/openhat.wav"></audio>
<audio data-key="71" src="sounds/boom.wav"></audio>
<audio data-key="72" src="sounds/ride.wav"></audio>
<audio data-key="74" src="sounds/snare.wav"></audio>
<audio data-key="75" src="sounds/tom.wav"></audio>
<audio data-key="76" src="sounds/tink.wav"></audio>
<script src="app.js"></script>
</body>
</html>
// Play drum by keyboard press
window.addEventListener('keydown' , playDrum);
function playDrum(e) {
    const audio = document.querySelector(`audio[data-key="${e.keyCode}"]`);
    if(!audio) return;
    audio.currentTime = 0;
    audio.play();
    const key = document.querySelector(`.key[data-key="${e.keyCode}"]`);
    key.classList.add('playing');
};

// Remove .playing class after transition action over
const keys = document.querySelectorAll('.key');
keys.forEach( key => key.addEventListener('transitionend' , removePlayingClass ) );
function removePlayingClass(e)
{
    //if( e.propertyName != 'transform' ) return;
    this.classList.remove('playing');
}


// Play music by mouse click
keys.forEach( key => key.addEventListener('click' , playByMouse ) );
function playByMouse(e)
{
    const keyCode = this.getAttribute('data-key');
    const audio = document.querySelector(`audio[data-key="${keyCode}"]`);
    audio.currentTime = 0;
    audio.play();
    const key = document.querySelector(`.key[data-key="${keyCode}"]`);
    key.classList.add('playing');
}