<?xml version="1.0" encoding="utf-8" standalone="yes"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
  <channel>
    <title>游戏开发 on 方糖猫Blog</title>
    <link>https://ftmao.com/blog/tags/%E6%B8%B8%E6%88%8F%E5%BC%80%E5%8F%91/</link>
    <description>AI 视频、AI Agent 与工程实践笔记</description>
    <generator>Hugo</generator>
    <language>zh-CN</language>
    <lastBuildDate>Sat, 05 Sep 2020 21:00:00 +0800</lastBuildDate><atom:link href="https://ftmao.com/blog/tags/%E6%B8%B8%E6%88%8F%E5%BC%80%E5%8F%91/index.xml" rel="self" type="application/rss+xml" /><item>
      <title>《princes》：用 Cocos Creator 写一款餐厅经营放置小游戏</title>
      <link>https://ftmao.com/blog/posts/princes-restaurant-game-intro/</link>
      <pubDate>Sat, 05 Sep 2020 21:00:00 +0800</pubDate>
      <guid>https://ftmao.com/blog/posts/princes-restaurant-game-intro/</guid>
      <description>&lt;h1 id=&#34;princes用-cocos-creator-写一款餐厅经营放置小游戏&#34;&gt;《princes》：用 Cocos Creator 写一款餐厅经营放置小游戏&lt;/h1&gt;
&lt;p&gt;继《去赶海吧》之后，把更早做的一款餐厅经营小游戏也整理开源了，照例写篇文章介绍玩法和实现上的技术点。仓库名叫 princes（历史遗留命名，内容其实是餐厅），代码里还住着不少从其他项目移民过来的残留，这次一并&amp;quot;考古&amp;quot;出来。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;开源地址：&lt;a href=&#34;https://gitee.com/soulnew/princes&#34;&gt;https://gitee.com/soulnew/princes&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;技术栈：Cocos Creator 2.3.4 + JavaScript&lt;/li&gt;
&lt;li&gt;平台：微信小游戏（竖屏 720×1280）&lt;/li&gt;
&lt;li&gt;规模：39 个脚本，约 4200 行代码，单主场景&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;游戏核心玩法一句话概括：&lt;strong&gt;开一家餐厅，顾客自动上门排队等位、走到餐桌点餐、用餐完留下金币，你点击收金币，再用金币解锁更多食物、餐桌和顾客皮肤&lt;/strong&gt;——放置 + 点击收菜的经典经营循环。&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&#34;一玩法介绍&#34;&gt;一、玩法介绍&lt;/h2&gt;
&lt;h3 id=&#34;11-核心循环&#34;&gt;1.1 核心循环&lt;/h3&gt;
&lt;p&gt;玩家几乎不直接操作角色，核心是&amp;quot;看店 + 点击收钱 + 花钱解锁&amp;quot;：&lt;/p&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;pre tabindex=&#34;0&#34;&gt;&lt;code class=&#34;language-mermaid&#34; data-lang=&#34;mermaid&#34;&gt;graph LR
    A[顾客自动上门&amp;lt;br/&amp;gt;每15秒补1个] --&amp;gt; B{有空餐桌?}
    B --&amp;gt;|有| C[A*寻路走到餐桌]
    B --&amp;gt;|没有| D[门口排队等位]
    D --&amp;gt;|有空桌释放| C
    C --&amp;gt; E[头顶弹气泡点单]
    E --&amp;gt; F[用餐5秒&amp;lt;br/&amp;gt;点击气泡可加速]
    F --&amp;gt; G[桌边产出一堆金币]
    G --&amp;gt; H[玩家点击拾取]
    H --&amp;gt; I[解锁新食物/餐桌/顾客]
    I --&amp;gt; A&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;&lt;p&gt;几个关键设计：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;餐桌是稀缺资源&lt;/strong&gt;：场上最多 10 个顾客（&lt;code&gt;maxPlayer = 10&lt;/code&gt;），没桌子的顾客会在门口按队列序号排开站位，有空桌释放时队首补位、全队前移；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;点单匹配&lt;/strong&gt;：每个顾客皮肤有自己的&amp;quot;可点菜单&amp;quot;（&lt;code&gt;foodList&lt;/code&gt;），随机点一道——这道菜已解锁就正常产金币，没解锁就显示&amp;quot;没有这道菜&amp;quot;的图标，等于告诉你&amp;quot;该去商店解锁了&amp;quot;；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;巡游消费&lt;/strong&gt;：顾客吃完饭不直接走，而是从酒柜、古董花瓶、海滩桌等 6 类装饰家具里洗牌出一份&amp;quot;参观清单&amp;quot;，逐件走过去参观，每件停留几秒再留下一笔金币——买装饰家具不只是好看，是真的能多赚钱；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;邀请按钮&lt;/strong&gt;：等不及可以点&amp;quot;邀请&amp;quot;立刻来一位顾客，这个入口同时服务每日任务和成就。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&#34;12-四屏地图与翻页&#34;&gt;1.2 四屏地图与翻页&lt;/h3&gt;
&lt;p&gt;餐厅不是一张单屏地图，而是四个房间：&lt;strong&gt;海滩（left1）、大厅（mid）、剧院（right1）、花园（down1）&lt;/strong&gt;。顾客在房间之间穿行时走&amp;quot;传送门&amp;quot;格子——比如大厅到海滩，先寻路到 &lt;code&gt;(0,11)&lt;/code&gt; 这个门，切换父节点到海滩场景节点，再从海滩的入口格 &lt;code&gt;(12,11)&lt;/code&gt; 继续寻路。&lt;/p&gt;
&lt;p&gt;相机不是跟随式的，而是&lt;strong&gt;手动拖拽翻页&lt;/strong&gt;：单轴锁定滑动，滑过屏宽 1/10 左右翻页、屏高 1/14 上下翻页。门口还有两个服务员（&lt;code&gt;waiterPlayer&lt;/code&gt;），有顾客进门就弹出欢迎气泡 5 秒——不做实际传菜逻辑，纯氛围组。&lt;/p&gt;
&lt;h3 id=&#34;13-成长线解锁三件套&#34;&gt;1.3 成长线：解锁三件套&lt;/h3&gt;
&lt;p&gt;金币花在三个商店面板上：&lt;/p&gt;
&lt;table&gt;
	&lt;thead&gt;
			&lt;tr&gt;
					&lt;th&gt;商店&lt;/th&gt;
					&lt;th&gt;内容&lt;/th&gt;
					&lt;th&gt;解锁的意义&lt;/th&gt;
			&lt;/tr&gt;
	&lt;/thead&gt;
	&lt;tbody&gt;
			&lt;tr&gt;
					&lt;td&gt;食物&lt;/td&gt;
					&lt;td&gt;54 道菜，价格 100~153 递增&lt;/td&gt;
					&lt;td&gt;顾客点单能匹配上，用餐才产金币&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;家具&lt;/td&gt;
					&lt;td&gt;大厅 6 组餐桌（每组 3 级升级）+ 各房间散桌 + 装饰家具&lt;/td&gt;
					&lt;td&gt;新餐桌 = 多一个接待座位；装饰家具 = 巡游产金币点&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;顾客&lt;/td&gt;
					&lt;td&gt;50 种皮肤&lt;/td&gt;
					&lt;td&gt;新皮肤进入上门顾客的随机池&lt;/td&gt;
			&lt;/tr&gt;
	&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;顾客皮肤的名字很有梗：学士、水电工、警长、老板、小青龙、法师、熊猫、海贼、管道工、侦探、伯爵、外星人、厨神、牙医、流浪歌手、开膛手、屠夫、王少爷、腹肌男、特工K、机器人……基本是把能想到的梗都塞进去了。&lt;/p&gt;
&lt;h3 id=&#34;14-成就与每日任务&#34;&gt;1.4 成就与每日任务&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;15 条成就&lt;/strong&gt;分 5 种类型：解锁指定物品、解锁 N 种某类物品、服务 N 个顾客、累计金币（500/5000/10000/30000）、邀请 N 次，奖励统一 5000 金币；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;每日任务&lt;/strong&gt;从 7 条里随机洗牌取 6 条（服务顾客 2 次、累计 800 金币、邀请 2 次、招待指定顾客 2 次等），按本地日期键 &lt;code&gt;年_月_日&lt;/code&gt; 翻日重置，还会随机指定&amp;quot;今天要招待某位顾客&amp;quot;。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;两个系统都有红点提醒，领取后自动检查链上下一档成就。&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&#34;二技术亮点&#34;&gt;二、技术亮点&lt;/h2&gt;
&lt;p&gt;项目不大（4000 行出头），但有几个点是自己撸出来的轮子，值得展开。&lt;/p&gt;
&lt;h3 id=&#34;21-手写-a--传送门式跨屏寻路&#34;&gt;2.1 手写 A* + &amp;ldquo;传送门&amp;quot;式跨屏寻路&lt;/h3&gt;
&lt;p&gt;寻路没有用任何库，&lt;code&gt;AStar.js&lt;/code&gt;（217 行）是手写的标准实现：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;地图是 &lt;code&gt;position.json&lt;/code&gt; 里一个 &lt;strong&gt;21×13 的一维数组&lt;/strong&gt;（273 个元素，&lt;code&gt;0&lt;/code&gt; 可走，其他障碍），&lt;code&gt;screenManager&lt;/code&gt; 按屏幕尺寸算出格子边长，做格坐标到像素坐标的换算；&lt;/li&gt;
&lt;li&gt;&lt;code&gt;AStarStep&lt;/code&gt; 封装 &lt;code&gt;g/h/position/last&lt;/code&gt;，&lt;code&gt;f&lt;/code&gt; 是 &lt;code&gt;g+h&lt;/code&gt; 的 getter；启发函数用曼哈顿距离，八方向扩展，斜走 14、直走 10（经典的 √2×10 近似）；&lt;/li&gt;
&lt;li&gt;拿到格子路径后，把每一格转成 &lt;code&gt;cc.moveTo(1, 像素坐标)&lt;/code&gt; 拼成 &lt;code&gt;cc.sequence&lt;/code&gt; 播放——&lt;strong&gt;每格固定走 1 秒&lt;/strong&gt;，移动节奏完全由动画序列驱动，不需要 &lt;code&gt;update&lt;/code&gt; 里手动插值。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;跨屏是这套寻路最有意思的部分——四个房间不是一张大地图，而是各自独立的节点树，靠 6 对&amp;quot;门坐标&amp;quot;缝合：&lt;/p&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;pre tabindex=&#34;0&#34;&gt;&lt;code class=&#34;language-mermaid&#34; data-lang=&#34;mermaid&#34;&gt;graph LR
    A[当前房间] --&amp;gt;|寻路到门格 mid_left1| B[门 0,11]
    B --&amp;gt;|切换父节点| C[目标房间]
    C --&amp;gt;|从入口格 left1_mid 继续寻路| D[最终目标]&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;&lt;p&gt;大地图拆成小地图 + 门对，每个房间的寻路空间始终只有 21×13，复杂度和内存都可控，是小工程里很实用的拆法。&lt;/p&gt;
&lt;h3 id=&#34;22-自研虚拟列表-listview&#34;&gt;2.2 自研虚拟列表 ListView&lt;/h3&gt;
&lt;p&gt;Cocos 2.x 官方没有现成的虚拟列表，商店、成就、每日任务的长列表就手写了一个（&lt;code&gt;ListView.js&lt;/code&gt;，380 行，项目内最大文件）：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;NodePool 复用&lt;/strong&gt;：划出视野的 item 回池，进入视野的从池里取或 instantiate；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;增量填充&lt;/strong&gt;：&lt;code&gt;_filledIds&lt;/code&gt; 记录当前树上已填充的索引，&lt;code&gt;update()&lt;/code&gt; 每帧用 content 的滚动偏移算起始 item 索引，变了才触发 &lt;code&gt;updateView&lt;/code&gt;，只回收越界的、只补缺失的；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Adapter 模式&lt;/strong&gt;：&lt;code&gt;ListAdapter&lt;/code&gt; 持有数据和 item 脚本名，&lt;code&gt;updateView&lt;/code&gt; 里 &lt;code&gt;item.getComponent(componentName).setData(data)&lt;/code&gt;——Android ListView 的味道很浓，View 和数据彻底分离；&lt;/li&gt;
&lt;li&gt;支持多列网格（&lt;code&gt;oneLineItemNumber&lt;/code&gt;）、上拉/下拉回调、监听 &lt;code&gt;refreshScollView&lt;/code&gt; 事件做局部刷新。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;家具面板没用这个 ListView，而是手写了一个更简的裁剪：视野内 &lt;code&gt;opacity=255&lt;/code&gt;、视野外 &lt;code&gt;opacity=0&lt;/code&gt;。同一项目里两种方案并存，算是&amp;quot;够用就好&amp;quot;和&amp;quot;造个轮子&amp;quot;的对照组。&lt;/p&gt;
&lt;h3 id=&#34;23-餐桌分配一个微缩排队系统&#34;&gt;2.3 餐桌分配：一个微缩排队系统&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;realTimeData.js&lt;/code&gt; 里藏着一个小小的服务台排队模型：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;activeFurnitureData&lt;/code&gt; 是&amp;quot;空闲桌池&amp;rdquo;，顾客来了随机抽一张（&lt;code&gt;splice&lt;/code&gt; 取出），用完 &lt;code&gt;push&lt;/code&gt; 回去；&lt;/li&gt;
&lt;li&gt;没桌时顾客进 &lt;code&gt;waitTableQuenue&lt;/code&gt;，按 &lt;code&gt;index/maxPlayer&lt;/code&gt; 比例在门口插值站位——队伍有多长，人就排多密；&lt;/li&gt;
&lt;li&gt;空桌释放时遍历等待队列，队首补位、全队回调前移。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;没有定时器轮询，全靠事件驱动，几十行代码把&amp;quot;等位&amp;quot;这个经营游戏的核心张力做出来了。&lt;/p&gt;
&lt;h3 id=&#34;24-运行时程序化动画两帧图集走天下&#34;&gt;2.4 运行时程序化动画：两帧图集走天下&lt;/h3&gt;
&lt;p&gt;所有角色（50 个顾客皮肤 + 2 个服务员）的走路动画都是&lt;strong&gt;运行时代码创建&lt;/strong&gt;的。&lt;code&gt;AnimationBase.js&lt;/code&gt; 只有 23 行：&lt;/p&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-javascript&#34; data-lang=&#34;javascript&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;let&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;clip&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;cc&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;AnimationClip&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;createWithSpriteFrames&lt;/span&gt;(&lt;span style=&#34;color:#a6e22e&#34;&gt;frames&lt;/span&gt;, &lt;span style=&#34;color:#ae81ff&#34;&gt;1&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#a6e22e&#34;&gt;clip&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;wrapMode&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;cc&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;WrapMode&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;Loop&lt;/span&gt;;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
  &lt;/div&gt;&lt;p&gt;美术只需要给每个皮肤画两帧（&lt;code&gt;icon&lt;/code&gt;/&lt;code&gt;icon2&lt;/code&gt;），代码动态建 clip 循环播放，&amp;ldquo;两帧鬼畜踏步&amp;quot;反而有一种魔性的喜感。50 套皮肤 = 100 帧图 + 一段 23 行的代码，动画资源维护成本为零。这个思路和《去赶海吧》的皮肤动画一脉相承。&lt;/p&gt;
&lt;h3 id=&#34;25-金币堆对象池--聚合拾取&#34;&gt;2.5 金币堆：对象池 + 聚合拾取&lt;/h3&gt;
&lt;p&gt;金币是这个游戏最高频的创建/销毁对象，处理上花了最多心思（git 历史里&amp;quot;金币堆积优化&amp;quot;&amp;ldquo;金币拾取动画效果修改&amp;quot;好几个 commit 都在打磨这块）：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;金币走 &lt;code&gt;cc.NodePool&lt;/code&gt;（上限 50），生成时随机旋转、1 秒散落动画到桌边随机位置；&lt;/li&gt;
&lt;li&gt;同一格子的金币记入 &lt;code&gt;nearGroup&lt;/code&gt; &lt;strong&gt;按堆聚合&lt;/strong&gt;，面额累加进 &lt;code&gt;valueMap&lt;/code&gt;——十份饭钱堆成一坨，而不是十个独立节点；&lt;/li&gt;
&lt;li&gt;池满时 &lt;code&gt;getGoldByMoveNearby&lt;/code&gt; 从邻居堆&amp;quot;借&amp;quot;一个实体过来复用；&lt;/li&gt;
&lt;li&gt;点击一堆 → 整堆回收，合成一个 &amp;ldquo;+总额&amp;rdquo; 数字标签飞向屏幕左上角的 HUD，飞行时长按距离等比计算，落地才到账。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;性能和手感都照顾到了：节点数有上限，拾取有&amp;quot;整堆收进腰包&amp;quot;的爽感。&lt;/p&gt;
&lt;h3 id=&#34;26-事件驱动与成就三态流转&#34;&gt;2.6 事件驱动与成就三态流转&lt;/h3&gt;
&lt;p&gt;全局只有一个 50 行的事件中心（&lt;code&gt;eventuality.js&lt;/code&gt;，出自 Crockford《JavaScript 语言精粹》里的 &lt;code&gt;eventuality&lt;/code&gt; 函数），只有 &lt;code&gt;fire/on/removeListener&lt;/code&gt; 三个方法，所有跨模块通信——&lt;code&gt;loadover&lt;/code&gt;、&lt;code&gt;addGold&lt;/code&gt;、&lt;code&gt;unLock&lt;/code&gt;、&lt;code&gt;serverPlayer&lt;/code&gt;、&lt;code&gt;invite&lt;/code&gt;、&lt;code&gt;arriveAtGate&lt;/code&gt;、&lt;code&gt;showDialog&lt;/code&gt;——都走它。&lt;/p&gt;
&lt;p&gt;成就/每日任务系统（两个 Manager 合计 660 行）建立在这个事件中心上，数据按三态流转：&lt;/p&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;pre tabindex=&#34;0&#34;&gt;&lt;code class=&#34;language-mermaid&#34; data-lang=&#34;mermaid&#34;&gt;graph LR
    A[unachieveMap&amp;lt;br/&amp;gt;未达成 按类型分桶] --&amp;gt;|事件计数达标| B[waitPayMap&amp;lt;br/&amp;gt;达成待领取]
    B --&amp;gt;|点击领取| C[已领取&amp;lt;br/&amp;gt;写入存档]
    C --&amp;gt;|自动检查| D[链上下一档成就]&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;&lt;p&gt;事件一来，只检查对应桶里的成就，不用全表扫描；领取后自动把下一档（服务 10 人 → 服务 50 人 → ……）挪进未达成桶继续监听。&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&#34;三整体架构一览&#34;&gt;三、整体架构一览&lt;/h2&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;pre tabindex=&#34;0&#34;&gt;&lt;code class=&#34;language-mermaid&#34; data-lang=&#34;mermaid&#34;&gt;graph TD
    subgraph 场景层
        L[loading.fire&amp;lt;br/&amp;gt;微信分包加载] --&amp;gt; G[game.fire&amp;lt;br/&amp;gt;主场景 103节点]
    end
    subgraph 入口与全局
        G --&amp;gt; GAME[game.js&amp;lt;br/&amp;gt;顾客生成/对象池]
        GL[global.js&amp;lt;br/&amp;gt;playerData/event/realTimeData]
        GAME --&amp;gt; GL
    end
    subgraph 玩法层
        GAME --&amp;gt; P[player.js&amp;lt;br/&amp;gt;顾客AI: 排队/寻路/点单/巡游]
        GAME --&amp;gt; W[waiterPlayer.js&amp;lt;br/&amp;gt;迎宾氛围组]
        GAME --&amp;gt; FM[FurnitureManager&amp;lt;br/&amp;gt;家具摆放/餐桌注册]
        GAME --&amp;gt; GM[GoldManager&amp;lt;br/&amp;gt;金币堆/拾取]
        P --&amp;gt; AS[AStar.js&amp;lt;br/&amp;gt;21×13格子寻路]
        P --&amp;gt; SM[screenManager&amp;lt;br/&amp;gt;格子坐标系/跨屏门]
    end
    subgraph 系统层
        AM[AchievementManager&amp;lt;br/&amp;gt;15条成就]
        DM[DailyTaskManager&amp;lt;br/&amp;gt;每日任务随机6条]
        LV[ListView&amp;lt;br/&amp;gt;自研虚拟列表]
        GUI[guide.js&amp;lt;br/&amp;gt;HUD与五个面板]
    end
    subgraph 数据层
        PD[player-data.js&amp;lt;br/&amp;gt;6份json配置+localStorage存档]
        RT[realTimeData.js&amp;lt;br/&amp;gt;餐桌队列/对象池/运行时状态]
    end
    GL --&amp;gt; AM
    GL --&amp;gt; DM
    GL --&amp;gt; PD
    GL --&amp;gt; RT
    GUI --&amp;gt; LV&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;&lt;p&gt;数据分三层：&lt;code&gt;defines.js&lt;/code&gt; 常量表（配置路径 / 存档键 / 事件名）、&lt;code&gt;player-data.js&lt;/code&gt; 持久层（6 份 json 配置 + localStorage 存档，配置是 Excel 手填导出的）、&lt;code&gt;realTimeData.js&lt;/code&gt; 运行时状态（对象池、餐桌队列、zIndex 发号器）。首次进游戏送 400 金币，默认解锁每类第一个条目。&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&#34;四找茬彩蛋&#34;&gt;四、找茬彩蛋&lt;/h2&gt;
&lt;p&gt;和《去赶海吧》一样，代码里的瑕疵原样保留，欢迎找茬：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;em&gt;&lt;em&gt;A&lt;/em&gt; 代价函数传错参&lt;/em&gt;*（&lt;code&gt;AStar.js:87&lt;/code&gt;）：算邻居格移动代价时，&lt;code&gt;_costToMove(borderPosition, finish)&lt;/code&gt; 把终点当成上一个节点传了进去，导致大量格子被按斜走 14 计费、g 值普遍虚高。好在曼哈顿 h 也偏小，错误互相稀释，路还是能找对，只是已经不是标准 A* 了。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;全项目调了一个不存在的方法&lt;/strong&gt;：事件中心只实现了 &lt;code&gt;removeListener&lt;/code&gt;，但十几处 &lt;code&gt;onDestroy&lt;/code&gt; 里都在调 &lt;code&gt;global.event.off(...)&lt;/code&gt;——调用即 TypeError，只是主场景从不销毁所以没炸过。就算有 &lt;code&gt;off&lt;/code&gt;，传进去的也是新建箭头函数，永远匹配不上原监听器。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;死代码雷区&lt;/strong&gt;（&lt;code&gt;utils/pathSearch.js&lt;/code&gt;）：另一套从未被实例化的 A*，里面有 &lt;code&gt;this.year = &#39;2002&#39;&lt;/code&gt; 调试残留、原型方法引用闭包变量（调用即 ReferenceError）、严格模式下裸赋值，是从网上教程抄来没跑通的草稿。&lt;code&gt;paopao.js&lt;/code&gt; 和 &lt;code&gt;food.js&lt;/code&gt; 则是从泡泡龙/三消项目移民来的——&lt;code&gt;food.js&lt;/code&gt; 的 &lt;code&gt;onLoad&lt;/code&gt; 会调一个不存在的方法，挂上节点必崩，所以它谁也没挂。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;拼写宇宙&lt;/strong&gt;：&lt;code&gt;createPalyer&lt;/code&gt;、&lt;code&gt;walktoTabel&lt;/code&gt;、&lt;code&gt;distence&lt;/code&gt;、&lt;code&gt;memuPanel&lt;/code&gt; 全文通行；最惨的是 &lt;code&gt;awarddiamand&lt;/code&gt;（成就表）和 &lt;code&gt;awarddiamond&lt;/code&gt;（家具表）两种拼法并存，&lt;code&gt;AchievementItem.getAward&lt;/code&gt; 只认前者，家具表里的钻石奖励永远读不到。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;每日任务污染成就存档&lt;/strong&gt;（&lt;code&gt;DailyTaskManager.js:220&lt;/code&gt;）：每日服务计数误写进了成就用的总计数键；&lt;code&gt;payAward&lt;/code&gt; 的 switch 里还有两个永远进不去的 &lt;code&gt;undefined&lt;/code&gt; case。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;另外 &lt;code&gt;defines.js&lt;/code&gt; 里挂着 9 个根本不存在的配置路径（&lt;code&gt;engineerNameConfig&lt;/code&gt;……&amp;ldquo;工程师&amp;quot;暴露了这是从另一个项目复制来的），food.json 里 54 道菜的名字是&amp;quot;食物1&amp;quot;到&amp;quot;食物54&amp;rdquo;——明显的占位数值没来得及填。&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&#34;五写在最后&#34;&gt;五、写在最后&lt;/h2&gt;
&lt;p&gt;从 git 历史能还原出开发顺序：跨屏寻路 → 桌子排队 → 成就系统 → 每日任务 → 钻石 → 金币特效与堆积优化 → 层级修正——先把核心玩法立住，再一层层包外围系统，最后打磨手感。&lt;/p&gt;
&lt;p&gt;这是一个典型的个人开发者&amp;quot;麻雀型&amp;quot;项目：4000 行代码里同时住着写得相当讲究的虚拟列表、排队模型、跨屏寻路，和成堆的复制粘贴残留、拼写错误与失效代码。亮点和找茬各占一半，反而更适合做&amp;quot;真实项目代码考古&amp;quot;的样本——毕竟真实项目从来不是教科书。&lt;/p&gt;
&lt;p&gt;源码在 &lt;a href=&#34;https://gitee.com/soulnew/princes&#34;&gt;https://gitee.com/soulnew/princes&lt;/a&gt;，欢迎 Star，也欢迎提 Issue 交流。&lt;/p&gt;
</description>
    </item>
    <item>
      <title>教你打麻将</title>
      <link>https://ftmao.com/blog/posts/beimi-chess-card-game-summary/</link>
      <pubDate>Wed, 24 Jun 2020 14:00:00 +0800</pubDate>
      <guid>https://ftmao.com/blog/posts/beimi-chess-card-game-summary/</guid>
      <description>&lt;h1 id=&#34;教你打麻将&#34;&gt;教你打麻将&lt;/h1&gt;
&lt;p&gt;文章面向&lt;strong&gt;从未接触过联网游戏开发&lt;/strong&gt;的读者，所以前半部分会把&amp;quot;联网棋牌游戏到底是怎么跑起来的&amp;quot;讲清楚，中间穿插真实代码层面的例子；后半部分专门开一章讲四川麻将的新手实战策略——毕竟做麻将游戏的人，自己先得会打麻将。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;服务端：Java + Spring Boot + Netty（netty-socketio）+ Hazelcast + MySQL&lt;/li&gt;
&lt;li&gt;客户端：Cocos Creator / JavaScript&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h2 id=&#34;一先搞明白联网棋牌游戏到底是怎么回事&#34;&gt;一、先搞明白：联网棋牌游戏到底是怎么回事&lt;/h2&gt;
&lt;h3 id=&#34;11-单机游戏和联网游戏的本质区别&#34;&gt;1.1 单机游戏和联网游戏的本质区别&lt;/h3&gt;
&lt;p&gt;单机游戏里，所有逻辑都在你自己的设备上运行，规则由你的设备说了算。联网游戏不行——四个人打麻将，&lt;strong&gt;谁摸了什么牌、谁点了炮，必须有一个&amp;quot;裁判&amp;quot;说了算&lt;/strong&gt;，否则每个人的手机都各算各的，立刻就乱套了。&lt;/p&gt;
&lt;p&gt;这个&amp;quot;裁判&amp;quot;就是&lt;strong&gt;服务器&lt;/strong&gt;。联网棋牌游戏的基本模型是：&lt;/p&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;pre tabindex=&#34;0&#34;&gt;&lt;code class=&#34;language-mermaid&#34; data-lang=&#34;mermaid&#34;&gt;graph TB
    subgraph 玩家设备
        C1[玩家A的手机&amp;lt;br/&amp;gt;Cocos Creator 客户端]
        C2[玩家B的手机]
        C3[玩家C的手机]
        C4[玩家D的手机]
    end
    S[游戏服务器&amp;lt;br/&amp;gt;贝密：Spring Boot + Netty]
    DB[(MySQL&amp;lt;br/&amp;gt;账号/战绩/房卡)]
    CACHE[(内存缓存 Hazelcast&amp;lt;br/&amp;gt;房间/牌局实时状态)]

    C1 &amp;lt;--&amp;gt;|WebSocket 长连接| S
    C2 &amp;lt;--&amp;gt; S
    C3 &amp;lt;--&amp;gt; S
    C4 &amp;lt;--&amp;gt; S
    S --&amp;gt; CACHE
    S -.异步落盘.-&amp;gt; DB&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;&lt;p&gt;几个关键点：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;牌在服务器手里&lt;/strong&gt;。洗牌、发牌都在服务器上做，客户端只负责&amp;quot;显示&amp;quot;和&amp;quot;发指令&amp;quot;。你在手机上点&amp;quot;打出三万&amp;quot;，本质是给服务器发了一条消息：&lt;code&gt;doplaycards: &amp;quot;12&amp;quot;&lt;/code&gt;，服务器校验&amp;quot;现在确实轮到你、你手里确实有三万&amp;quot;之后，才广播给全桌人。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;每个人看到的信息不同&lt;/strong&gt;。服务器把同一局牌的状态，按&amp;quot;你能看到什么&amp;quot;过滤后分别发给四个人——你只能看到自己的手牌和别人打出来的牌。这叫&lt;strong&gt;信息不对称&lt;/strong&gt;，也是棋牌游戏和射击游戏最大的不同：状态同步必须分人定制。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;防作弊的第一道防线就是&amp;quot;服务器权威&amp;quot;&lt;/strong&gt;。客户端想赖牌（打出一张自己没有的牌），服务器直接拒绝。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&#34;12-为什么用-websocket-而不是-http&#34;&gt;1.2 为什么用 WebSocket 而不是 HTTP&lt;/h3&gt;
&lt;p&gt;没接触过联网游戏的人容易问：发消息不就是调接口吗，用 HTTP 不行吗？&lt;/p&gt;
&lt;p&gt;不行，因为 HTTP 是&amp;quot;问一句答一句&amp;quot;，服务器没法&lt;strong&gt;主动&lt;/strong&gt;找你。而棋牌游戏里大量事件是服务器主动推的：轮到你出牌了、有人碰了你的牌、房间人齐了……这些都需要服务器&lt;strong&gt;随时&lt;/strong&gt;推消息给客户端。WebSocket 是一条建立后一直保持的双向通道，双方随时可以说话，延迟低、开销小。贝密用的 &lt;code&gt;netty-socketio&lt;/code&gt; 就是在 Netty 上实现了 Socket.IO 协议（跑在 WebSocket 上），客户端 Cocos Creator 里直接用 socket.io 的 JS 库连。&lt;/p&gt;
&lt;h3 id=&#34;13-房卡模式一张虚拟门票&#34;&gt;1.3 房卡模式：一张&amp;quot;虚拟门票&amp;quot;&lt;/h3&gt;
&lt;p&gt;贝密 0.8.0 主打&lt;strong&gt;房卡模式&lt;/strong&gt;，这也是 2016-2018 年棋牌游戏爆发的典型商业模式：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;你在商城购买&amp;quot;房卡&amp;quot;（虚拟道具，存在 MySQL 里）；&lt;/li&gt;
&lt;li&gt;你想和朋友打牌，就&amp;quot;开房&amp;quot;——消耗房卡，服务器生成一个 &lt;strong&gt;6 位数字房号&lt;/strong&gt;（贝密里由 &lt;code&gt;GameEngine.creatGameRoom&lt;/code&gt; 生成）；&lt;/li&gt;
&lt;li&gt;朋友在大厅输入房号进房（客户端发 &lt;code&gt;searchroom&lt;/code&gt; 事件），人齐开局；&lt;/li&gt;
&lt;li&gt;打完约定的局数，服务器出结算单，房间销毁。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;和陌生人随机撮合的&amp;quot;大厅模式&amp;quot;相比，房卡模式本质上卖的是&amp;quot;熟人社交的场地&amp;quot;，技术上则多了一个&amp;quot;按房号精确找房&amp;quot;的入口。&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&#34;二贝密的整体架构拆解&#34;&gt;二、贝密的整体架构拆解&lt;/h2&gt;
&lt;h3 id=&#34;21-架构总览&#34;&gt;2.1 架构总览&lt;/h3&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;pre tabindex=&#34;0&#34;&gt;&lt;code class=&#34;language-mermaid&#34; data-lang=&#34;mermaid&#34;&gt;graph LR
    subgraph 客户端
        A[Cocos Creator&amp;lt;br/&amp;gt;dizhu / majiang 模块]
    end
    subgraph 服务端 com.beimi
        B[GameEventHandler&amp;lt;br/&amp;gt;Socket 事件入口]
        C[GameEngine&amp;lt;br/&amp;gt;游戏总调度]
        D[自研状态机&amp;lt;br/&amp;gt;BeiMiStateMachine]
        E[Board 牌局内存模型&amp;lt;br/&amp;gt;MaJiangBoard / DuZhuBoard]
        F[Action/Task&amp;lt;br/&amp;gt;出牌·结算·计时任务]
    end
    subgraph 存储层
        G[(Hazelcast&amp;lt;br/&amp;gt;房间/牌局/玩家/撮合队列)]
        H[(cache2k&amp;lt;br/&amp;gt;过期回调当计时器)]
        I[Disruptor RingBuffer&amp;lt;br/&amp;gt;异步事件]
        J[(MySQL JPA)]
        K[(Elasticsearch)]
    end

    A &amp;lt;--&amp;gt;|WebSocket /game namespace| B
    B --&amp;gt; C --&amp;gt; D --&amp;gt; E
    D --&amp;gt; F
    E --&amp;gt; G
    F --&amp;gt; H
    C --&amp;gt; I
    I --&amp;gt; J
    I --&amp;gt; K&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;&lt;p&gt;各层职责一句话概括：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;GameEventHandler&lt;/strong&gt;：所有 &lt;code&gt;@OnEvent&lt;/code&gt; 端点都在这里，负责把客户端消息翻译成人话（比如把 &lt;code&gt;&amp;quot;12,13&amp;quot;&lt;/code&gt; 解析成字节数组），然后交给 GameEngine；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;GameEngine&lt;/strong&gt;：总调度，加房、开局、出牌、杠碰胡、离房、解散房间都从这儿过；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;自研状态机&lt;/strong&gt;：驱动&amp;quot;房间&amp;quot;这个对象的生命周期（后面细讲）；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Board 内存模型&lt;/strong&gt;：一局牌的完整状态（四家的手牌、桌面牌、轮到谁、倍率），只活在 Hazelcast 内存缓存里；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;存储分层&lt;/strong&gt;：实时状态全在 Hazelcast，MySQL 只存账号/战绩/房卡流水，而且是通过 Disruptor &lt;strong&gt;异步&lt;/strong&gt;写入——牌局进行中的一秒钟都不等数据库。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&#34;22-一次出牌的完整旅程以麻将为例&#34;&gt;2.2 一次出牌的完整旅程（以麻将为例）&lt;/h3&gt;
&lt;p&gt;这是最核心的一条链路，用实际代码里的事件名走一遍：&lt;/p&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;pre tabindex=&#34;0&#34;&gt;&lt;code class=&#34;language-mermaid&#34; data-lang=&#34;mermaid&#34;&gt;sequenceDiagram
    participant P1 as 玩家A客户端
    participant H as GameEventHandler
    participant E as GameEngine
    participant B as MaJiangBoard
    participant T as 计时器(cache2k)
    participant P2 as 其他玩家客户端

    P1-&amp;gt;&amp;gt;H: emit(&amp;#34;doplaycards&amp;#34;, &amp;#34;12&amp;#34;)
    H-&amp;gt;&amp;gt;E: takeCardsRequest(token, card)
    E-&amp;gt;&amp;gt;B: 出牌校验 + 从手牌移除
    E-&amp;gt;&amp;gt;T: remove(roomid) 取消A的出牌倒计时
    E--&amp;gt;&amp;gt;P2: 广播 &amp;#34;command&amp;#34;: takecards
    E-&amp;gt;&amp;gt;E: checkMJCard 遍历其他家&amp;lt;br/&amp;gt;查 杠/碰/吃/胡
    alt 有人能杠碰胡
        E-&amp;gt;&amp;gt;T: 挂5秒等待任务(DEAL事件)
        P2-&amp;gt;&amp;gt;H: emit(&amp;#34;selectaction&amp;#34;, &amp;#34;peng&amp;#34;)
        H-&amp;gt;&amp;gt;E: actionEventRequest 执行碰
    else 无人响应
        E-&amp;gt;&amp;gt;B: dealRequest 下家抓牌
        E--&amp;gt;&amp;gt;P2: 广播 &amp;#34;command&amp;#34;: dealcard
        E-&amp;gt;&amp;gt;T: 挂8秒出牌任务&amp;lt;br/&amp;gt;(超时自动打)
    end&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;&lt;p&gt;&lt;strong&gt;举例说明&lt;/strong&gt;：假设你打出一张&amp;quot;五筒&amp;quot;，服务器干的事依次是——&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;校验：轮到你、你有五筒、这张牌合法；&lt;/li&gt;
&lt;li&gt;把你 8 秒的出牌倒计时从计时器缓存里删掉（你不主动出，超时会由 &lt;code&gt;CreateMJPlayCardsTask&lt;/code&gt; 强制替你出）；&lt;/li&gt;
&lt;li&gt;广播给全桌：&amp;ldquo;A 打了五筒&amp;rdquo;；&lt;/li&gt;
&lt;li&gt;挨个问另外三家：你能碰吗？能杠吗？能胡吗？（定缺了筒子的那家直接跳过——这就是四川麻将的规则在代码里的体现，&lt;code&gt;GameUtils.processMJCard&lt;/code&gt; 里定缺花色不参与判断）；&lt;/li&gt;
&lt;li&gt;如果 B 能碰，给 B 弹&amp;quot;碰/过&amp;quot;按钮，牌局挂起 5 秒等他决定；B 点了&amp;quot;碰&amp;quot;，就执行碰、轮到 B 出牌；&lt;/li&gt;
&lt;li&gt;如果三家都&amp;quot;过&amp;quot;，下家自动摸一张牌，服务器先替他检查一次&amp;quot;摸上来的牌能不能暗杠/自摸&amp;quot;，然后通知他出牌，重新挂上 8 秒倒计时。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;整个过程中，客户端只做两件事：把服务器的消息渲染成动画，把玩家的点击发给服务器。&lt;/p&gt;
&lt;h3 id=&#34;23-房间的状态机流转&#34;&gt;2.3 房间的状态机流转&lt;/h3&gt;
&lt;p&gt;房间是有生命的：创建、等人、人齐、开局、结算、销毁。贝密没有用 Spring StateMachine，而是&lt;strong&gt;自研了一个极简状态机&lt;/strong&gt;（&lt;code&gt;core/statemachine&lt;/code&gt; 包），每种游戏一个 Bean：&lt;/p&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;pre tabindex=&#34;0&#34;&gt;&lt;code class=&#34;language-mermaid&#34; data-lang=&#34;mermaid&#34;&gt;stateDiagram-v2
    [*] --&amp;gt; NONE
    NONE --&amp;gt; CRERATED: ENTER 创建房间
    CRERATED --&amp;gt; WAITTING: JOIN 玩家陆续进房
    WAITTING --&amp;gt; READY: ENOUGH 人齐
    WAITTING --&amp;gt; WAITTING: AUTO 超时补AI机器人
    READY --&amp;gt; BEGIN: RAISEHANDS 全员准备
    BEGIN --&amp;gt; LASTHANDS: 发牌完成
    LASTHANDS --&amp;gt; PLAY: 庄家打出首张
    PLAY --&amp;gt; PLAY: DEAL/PLAYCARDS&amp;lt;br/&amp;gt;摸牌出牌循环
    PLAY --&amp;gt; END: ALLCARDS 有人胡牌/牌摸完
    END --&amp;gt; [*]: 结算发战报&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;&lt;p&gt;状态机里每个&amp;quot;事件&amp;quot;对应一串 Action（比如麻将的链路是 &lt;code&gt;EnterAction → JoinAction → EnoughAction → SelectAction(定缺) → MJRaiseHandsAction → DealMJCardAction → PlayMJCardsAction → AllCardsAction&lt;/code&gt;），业务代码只需要一句 &lt;code&gt;game.change(room, event, intervalSeconds)&lt;/code&gt;，状态机负责找到对应 Action 执行，并由 Action 顺手创建下一个延时任务，形成闭环。&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&#34;三断线重连联网游戏逃不掉的工程问题&#34;&gt;三、断线重连：联网游戏逃不掉的工程问题&lt;/h2&gt;
&lt;p&gt;新手最容易低估的就是这块。手机上打牌，来个电话、进个电梯，连接说断就断。贝密的处理方式是&amp;quot;托管 + 快照恢复&amp;quot;：&lt;/p&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;pre tabindex=&#34;0&#34;&gt;&lt;code class=&#34;language-mermaid&#34; data-lang=&#34;mermaid&#34;&gt;flowchart TD
    A[玩家网络断开] --&amp;gt; B{服务器收到&amp;lt;br/&amp;gt;OnDisconnect}
    B --&amp;gt;|玩家正在对局中| C[标记 OFFLINE&amp;lt;br/&amp;gt;进入托管状态]
    C --&amp;gt; D[计时器超时后&amp;lt;br/&amp;gt;auto=true 自动出牌]
    D --&amp;gt; E{玩家重连?}
    E --&amp;gt;|emit recovery| F[GameEngine 查缓存&amp;lt;br/&amp;gt;发现该房间有进行中的 Board]
    F --&amp;gt; G[下发 RecoveryData 全量快照:&amp;lt;br/&amp;gt;自己的手牌/桌面牌/轮到谁/剩余倒计时25s]
    G --&amp;gt; H[客户端按快照重建牌桌画面]
    E --&amp;gt;|一直没回来| I[AI托管打到本局结束]&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;&lt;p&gt;&lt;strong&gt;举例&lt;/strong&gt;：你出牌出了半局断网了，服务器不会暂停牌局等你（其他三个人不答应），而是把你标记为 OFFLINE，由超时任务替你&amp;quot;摸什么打什么&amp;quot;。30 秒后你连回来了，客户端发 &lt;code&gt;recovery&lt;/code&gt;，服务器把当前牌局的完整快照发给你，你的手机根据快照把桌面重新画出来，无缝接上。注意重连期间你是被托管的——所以掉线久了回来，往往牌已经很难看了，这是规则的一部分。&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&#34;四四川麻将新手致胜策略一个结构化的决策框架&#34;&gt;四、四川麻将新手致胜策略：一个结构化的决策框架&lt;/h2&gt;
&lt;p&gt;做了这么久的麻将游戏，经常被问&amp;quot;新手怎么提高胜率&amp;quot;。零散的口诀（&amp;ldquo;记牌&amp;quot;&amp;ldquo;防点炮&amp;rdquo;）新手记不住也用不上，所以这一章我把四川麻将的策略整理成一个&lt;strong&gt;结构化框架&lt;/strong&gt;：先确立唯一的目标函数，再按&amp;quot;战略层 → 信息层 → 战术层 → 概率层&amp;quot;四个层级组织所有决策，最后给出血战到底的特有推论和新手错误清单。逻辑主线只有一句话：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;麻将的每一步都是在信息不完备下做期望收益最大化的决策。四个层级分别回答：往哪走（战略）、知道什么（信息）、这巡打哪张（战术）、什么时候切换路线（概率）。&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3 id=&#34;40-规则即策略先立规则再推结论&#34;&gt;4.0 规则即策略：先立规则，再推结论&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;定缺&lt;/strong&gt;：开局后每人必须选一门花色（万/筒/条）作为&amp;quot;缺门&amp;rdquo;，胡牌时手里不能有这门牌，摸到缺门的牌必须打掉。牌墙只有 108 张（三门花色，无风牌）。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;血战到底&lt;/strong&gt;：一家胡牌不结束牌局，剩下的人继续打，直到牌摸完或只剩一家没胡。点炮只有点炮者付分，最后没胡的人还要给已听牌的人&amp;quot;查叫&amp;quot;赔分。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;从这两条规则可以直接推出三条战略推论，整章都是它们的展开：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;信息压缩&lt;/strong&gt;：定缺把每家的&amp;quot;有效牌域&amp;quot;从三门砍到两门，而且是&lt;strong&gt;公开信息&lt;/strong&gt;。读牌成本比别的麻将低一个量级——新手也能读。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;时间价值&lt;/strong&gt;：牌局不因一人胡牌而结束，每张牌的价值随牌墙减少而衰减。&lt;strong&gt;&amp;ldquo;还剩几巡&amp;quot;是所有决策的隐含变量&lt;/strong&gt;。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;止损优先&lt;/strong&gt;：点炮只由点炮者一人付分、没胡要赔查叫——&lt;strong&gt;落后的代价是确定的，追上的收益是概率性的&lt;/strong&gt;。这决定了&amp;quot;不跟随到最后&amp;quot;在数学上往往是对的。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;三条推论的推导关系：&lt;/p&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;pre tabindex=&#34;0&#34;&gt;&lt;code class=&#34;language-mermaid&#34; data-lang=&#34;mermaid&#34;&gt;flowchart LR
    R1[规则一: 定缺&amp;lt;br/&amp;gt;缺门公开] --&amp;gt; C1[推论一: 信息压缩&amp;lt;br/&amp;gt;读牌成本低]
    R2[规则二: 血战到底&amp;lt;br/&amp;gt;胡牌不终局] --&amp;gt; C2[推论二: 时间价值&amp;lt;br/&amp;gt;剩几巡是隐含变量]
    R2 --&amp;gt; C3[推论三: 止损优先&amp;lt;br/&amp;gt;点炮独赔+查叫保底]
    C1 --&amp;gt; S[决定 4.2 信息层怎么读]
    C2 --&amp;gt; S2[决定 4.4 何时切换路线]
    C3 --&amp;gt; S3[决定 4.5 防守与保底策略]&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;&lt;h3 id=&#34;41-战略层起手评估与路线选择&#34;&gt;4.1 战略层：起手评估与路线选择&lt;/h3&gt;
&lt;p&gt;拿到起手牌、定缺之前，先给牌打分。不需要精确，三个数就够：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;向听数&lt;/strong&gt;：还差几张牌能听牌。数一下你手里有几个成型搭子/刻子/对子，13 张牌需要 4 副 + 1 将，差几副就是几向听。新手只要会区分&amp;quot;两向听以内（好牌）&amp;ldquo;和&amp;quot;三向听以上（差牌）&amp;quot;。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;对子数&lt;/strong&gt;：4 对以上 → 七对/大对子路线候选；0~1 对 → 别惦记对子路线。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;同花色集中度&lt;/strong&gt;：9 张以上同花色 → 清一色路线候选。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;定缺选择&lt;/strong&gt;是第一个战略决策，原则按优先级排：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;缺张数最少的那门&lt;/strong&gt;——缺门每张都是必打的废牌，张数越少浪费的巡数越少；&lt;/li&gt;
&lt;li&gt;张数相近时，&lt;strong&gt;缺结构最差的那门&lt;/strong&gt;（孤张多、没搭子的）；&lt;/li&gt;
&lt;li&gt;还在纠结时，&lt;strong&gt;缺端张多的那门&lt;/strong&gt;（1、9 多），因为端张成搭能力弱。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;strong&gt;举例&lt;/strong&gt;：起手万子 2 张（三万、八万，无关联）、筒子 5 张（两个搭子）、条子 6 张（一个刻子一个边搭）。毫无疑问题缺万——但如果万子是 2 张而筒子是 3 张但全是散牌，就该缺筒子：定缺看的是&amp;quot;废牌数量&amp;rdquo;，不是&amp;quot;张数&amp;rdquo;。&lt;/p&gt;
&lt;p&gt;定缺公布后立即做第二步战略判断：&lt;strong&gt;看三家的缺门，决定做大还是快跑&lt;/strong&gt;。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;三家中有两家缺你这门 → 你要做的花色没人抢，牌墙里全剩给你，&lt;strong&gt;做大的绿灯&lt;/strong&gt;；&lt;/li&gt;
&lt;li&gt;三家都不缺你这门 → 四个人抢一门牌，你的关键张会被飞速消耗，&lt;strong&gt;果断转快跑&lt;/strong&gt;。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;至此，三条路线已经清晰：&lt;/p&gt;
&lt;table&gt;
	&lt;thead&gt;
			&lt;tr&gt;
					&lt;th&gt;路线&lt;/th&gt;
					&lt;th&gt;触发条件&lt;/th&gt;
					&lt;th&gt;目标&lt;/th&gt;
					&lt;th&gt;核心动作&lt;/th&gt;
			&lt;/tr&gt;
	&lt;/thead&gt;
	&lt;tbody&gt;
			&lt;tr&gt;
					&lt;td&gt;快跑&lt;/td&gt;
					&lt;td&gt;牌型一般、向听小、花色被抢&lt;/td&gt;
					&lt;td&gt;最速听牌、先胡锁定收益&lt;/td&gt;
					&lt;td&gt;牌效优先，不留废搭&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;做大&lt;/td&gt;
					&lt;td&gt;起手 9 张+同花色/4 对+，且无竞争&lt;/td&gt;
					&lt;td&gt;清一色/大对子/七对&lt;/td&gt;
					&lt;td&gt;忍受慢听牌，屯关键张&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;防守&lt;/td&gt;
					&lt;td&gt;有人已听牌且自己手牌差&lt;/td&gt;
					&lt;td&gt;不点炮、保查叫底线&lt;/td&gt;
					&lt;td&gt;只打熟张，听牌优先于胡牌&lt;/td&gt;
			&lt;/tr&gt;
	&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;把起手到定路线的全过程画成一张图：&lt;/p&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;pre tabindex=&#34;0&#34;&gt;&lt;code class=&#34;language-mermaid&#34; data-lang=&#34;mermaid&#34;&gt;flowchart TD
    A[拿到13张起手牌] --&amp;gt; B[三个数打分:&amp;lt;br/&amp;gt;向听数 / 对子数 / 同花色集中度]
    B --&amp;gt; C{定缺: 缺哪门?}
    C --&amp;gt;|第一优先| C1[缺张数最少的门]
    C --&amp;gt;|张数相近| C2[缺结构最差的门&amp;lt;br/&amp;gt;孤张多没搭子]
    C --&amp;gt;|还纠结| C3[缺端张多的门&amp;lt;br/&amp;gt;1和9成搭弱]
    C1 &amp;amp; C2 &amp;amp; C3 --&amp;gt; D[公布定缺&amp;lt;br/&amp;gt;观察三家缺门]
    D --&amp;gt; E{有人抢我要做的花色?}
    E --&amp;gt;|两家以上缺我这门&amp;lt;br/&amp;gt;绿灯| F{起手支持做大?&amp;lt;br/&amp;gt;9张+同花色 或 4对+}
    F --&amp;gt;|是| G[做大路线&amp;lt;br/&amp;gt;清一色/大对子/七对]
    F --&amp;gt;|否| H[快跑路线&amp;lt;br/&amp;gt;最速听牌]
    E --&amp;gt;|三家都抢我这门| H
    E --&amp;gt;|牌型差 三向听以上| I[保守推进&amp;lt;br/&amp;gt;随时准备转防守]
    style G fill:#ffe6cc
    style H fill:#d4edda
    style I fill:#f8d7da&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;&lt;p&gt;路线不是选定就不变——&lt;strong&gt;每一巡都可以用新信息重估&lt;/strong&gt;，4.4 会给出切换的量化标准。&lt;/p&gt;
&lt;h3 id=&#34;42-信息层记牌与读牌&#34;&gt;4.2 信息层：记牌与读牌&lt;/h3&gt;
&lt;p&gt;信息层回答&amp;quot;我知道什么&amp;quot;。它分两半：&lt;strong&gt;记牌&lt;/strong&gt;是对已发生事实的压缩存储，&lt;strong&gt;读牌&lt;/strong&gt;是从事实反推未知信息。&lt;/p&gt;
&lt;h4 id=&#34;421-记牌分层记忆法&#34;&gt;4.2.1 记牌：分层记忆法&lt;/h4&gt;
&lt;p&gt;让新手背下三家打过的每张牌不现实。按重要性只记三类：&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;第一层：记&amp;quot;张数&amp;quot;不记&amp;quot;顺序&amp;quot;。&lt;/strong&gt; 每门花色每张牌只有 4 张。你不需要记住&amp;quot;上家第 3 巡打了七万&amp;quot;，只需在脑子里维护计数：&amp;ldquo;七万已见 3 张&amp;rdquo;。重点盯的是&lt;strong&gt;你自己将牌和搭子两端的关键牌&lt;/strong&gt;——你手里 6、8 万等卡 7 万，&amp;ldquo;7 万见了几张&amp;quot;直接决定这个搭子的死活：见 3 张以上，搭子基本死了，趁早拆。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;第二层：记&amp;quot;缺门&amp;quot;和&amp;quot;花色倾向&amp;rdquo;。&lt;/strong&gt; 定缺后，对手打出的&lt;strong&gt;非缺门&lt;/strong&gt;花色才是有效信息。下家缺条，从第 2 巡起一直打筒子、摸到万子全留——一句话记住&amp;quot;下家只留万&amp;quot;即可，不用记他打的每一张。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;第三层：记&amp;quot;序盘的反常中张&amp;quot;。&lt;/strong&gt; 序盘（前 4~5 巡）正常人先打孤张边张（1、9）。有人序盘就打 4、5、6 中张，说明这门要么极好（搭子齐了中张多余）要么整门不要，结合缺门大概率能分辨。&lt;/p&gt;
&lt;p&gt;口诀：&lt;strong&gt;数自己的关键张，记别人的花色倾向，盯序盘的反常中张。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;三层记忆的工作量递减、收益递增，像一座金字塔——新手从塔底练起：&lt;/p&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;pre tabindex=&#34;0&#34;&gt;&lt;code class=&#34;language-mermaid&#34; data-lang=&#34;mermaid&#34;&gt;flowchart BT
    L1[第三层: 序盘反常中张&amp;lt;br/&amp;gt;一两个反常信号即可&amp;lt;br/&amp;gt;精力占比 ~10%] --&amp;gt; L2[第二层: 缺门与花色倾向&amp;lt;br/&amp;gt;每家一句话: 他只留X&amp;lt;br/&amp;gt;精力占比 ~30%]
    L2 --&amp;gt; L3[第一层: 关键张计数&amp;lt;br/&amp;gt;自己搭子两端的牌各见几张&amp;lt;br/&amp;gt;精力占比 ~60%]
    L3 --&amp;gt; R[收益: 搭子死活判断&amp;lt;br/&amp;gt;直接决定拆不拆]
    L2 --&amp;gt; R2[收益: 知道谁在收哪门&amp;lt;br/&amp;gt;直接决定防什么]
    L1 --&amp;gt; R3[收益: 序盘读出牌型方向]&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;&lt;h4 id=&#34;422-读牌他要什么不要什么打什么危险&#34;&gt;4.2.2 读牌：他要什么、不要什么、打什么危险&lt;/h4&gt;
&lt;p&gt;读牌的本质是&lt;strong&gt;用别人的&amp;quot;打出&amp;quot;反推他的&amp;quot;留下&amp;quot;&lt;/strong&gt;——人留下什么牌，才会打出什么牌。按牌局阶段读不同的东西：&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;序盘读&amp;quot;结构&amp;quot;&lt;/strong&gt;：缺门 + 反常中张 + 第一碰，拼出每家的大致牌型方向。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;中盘读&amp;quot;进度&amp;quot;&lt;/strong&gt;：看连续动作。上家碰了&amp;quot;二筒&amp;quot;后摸打筒子以外的牌，说明他筒子上还有货；碰完后打的那张牌的&lt;strong&gt;邻张&lt;/strong&gt;往往是他不需要的（碰二筒后打五万，他大概率没有 3-4 万、4-6 万的搭子）；碰了二筒后打的还是筒子，几乎可断定他在做筒子清一色或已听牌。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;尾盘读&amp;quot;节奏&amp;quot;&lt;/strong&gt;：一个人&lt;strong&gt;连续摸牌就打（摸打）&lt;/strong&gt;，说明手牌已成型，打出的就是刚摸的；反过来&lt;strong&gt;想了很久才打出一张&lt;/strong&gt;，说明这张是从手里&amp;quot;拆&amp;quot;出来的，他在这张牌附近有搭子变动，其邻张不再安全。&lt;/p&gt;
&lt;p&gt;由此得到防点炮 checklist，中后盘（牌墙剩 1/3 以内）每打一张前过一遍：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;这张牌见过几张？&lt;/strong&gt; 见 2~3 张的熟张相对安全；一张没见的生张风险最高。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;有没有人明显在收这门？&lt;/strong&gt; 对家在做万子，你手里的生张五万就是高危，宁可拆搭子打熟张。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;他是不是单钓/卡张？&lt;/strong&gt; 碰了三副、手牌只剩一两张的人，听的牌范围很窄，往往就在他碰的那几副的邻张上——那一片中张全是雷区。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;strong&gt;完整例子&lt;/strong&gt;：对家定缺条子，碰了&amp;quot;三筒&amp;quot;，摸打两巡后思考良久切出&amp;quot;八万&amp;quot;。推断：他在做筒子（碰三筒+留筒），八万是万子里的多余搭子；&lt;strong&gt;你手里五、六、七万从此相对安全&lt;/strong&gt;（他不要万子），但&lt;strong&gt;任何生张筒子都是高危&lt;/strong&gt;，尤其一、二、四筒（三筒邻张）。&lt;/p&gt;
&lt;p&gt;读牌的推理链条固定是&amp;quot;观察 → 反推 → 行动&amp;quot;三段，常见信号对照如下：&lt;/p&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;pre tabindex=&#34;0&#34;&gt;&lt;code class=&#34;language-mermaid&#34; data-lang=&#34;mermaid&#34;&gt;flowchart LR
    subgraph 观察[他打出/做了什么]
        O1[缺门公布后&amp;lt;br/&amp;gt;一直打筒留万]
        O2[碰二筒后&amp;lt;br/&amp;gt;又打出筒子]
        O3[碰二筒后&amp;lt;br/&amp;gt;打出五万]
        O4[连续摸打]
        O5[长考后拆出八万]
    end
    subgraph 反推[他留下/在想什么]
        I1[在做万子清一色]
        I2[筒子听牌或做筒]
        I3[万子3-4/4-6搭子不存在&amp;lt;br/&amp;gt;→ 万字邻张变安全]
        I4[手牌已成型&amp;lt;br/&amp;gt;打的就是刚摸的]
        I5[八万附近有搭子变动&amp;lt;br/&amp;gt;→ 邻张不安全]
    end
    subgraph 行动[我该怎么办]
        A1[生张万子列为高危]
        A2[生张筒子绝不打]
        A3[手里4万6万可放心打]
        A4[按他已听牌对待&amp;lt;br/&amp;gt;进入防守]
        A5[七万九万扣住观察]
    end
    O1 --&amp;gt; I1 --&amp;gt; A1
    O2 --&amp;gt; I2 --&amp;gt; A2
    O3 --&amp;gt; I3 --&amp;gt; A3
    O4 --&amp;gt; I4 --&amp;gt; A4
    O5 --&amp;gt; I5 --&amp;gt; A5&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;&lt;h3 id=&#34;43-战术层牌效这一巡到底打哪张&#34;&gt;4.3 战术层：牌效——这一巡到底打哪张&lt;/h3&gt;
&lt;p&gt;战术层是执行：在路线已定、信息已有的前提下，每张出牌都按同一套优先级评估。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;出牌优先级（无安全压力时）&lt;/strong&gt;：缺门牌（必打）→ 孤立端张（无邻张的 1、9）→ 孤立中张 → 多余搭子中进张最少的那张 → 拆搭子。&lt;strong&gt;核心原则是&amp;quot;每一巡都最大化有效进张总数&amp;quot;&lt;/strong&gt;：假设搭子 A（4-5 万）有 8 张潜在进张、搭子 B（1-2 筒）只有 3 张潜在进张，要拆就先拆 B——哪怕 B 看起来更&amp;quot;连得顺&amp;quot;。注意&amp;quot;潜在进张&amp;quot;还要减去已见张数，见 3 张的两面搭子不如没见过的卡张。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;碰/杠决策&lt;/strong&gt;是新手最容易犯错的地方，默认答案是&amp;quot;&lt;strong&gt;不碰&lt;/strong&gt;&amp;quot;，除非满足以下至少一条：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;碰了立刻听牌，或碰的是你的将牌方向（对子路线）；&lt;/li&gt;
&lt;li&gt;你在做清一色/大对子，碰能加速成型；&lt;/li&gt;
&lt;li&gt;这张牌不碰就被下家吃碰走、你再也等不到。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;因为碰有隐性成本：&lt;strong&gt;暴露信息&lt;/strong&gt;（全桌知道你在收这门）、&lt;strong&gt;破坏门清弹性&lt;/strong&gt;（碰完之后手牌变窄，改路线更难）、&lt;strong&gt;改变摸牌顺序&lt;/strong&gt;（可能把本该你摸的关键张让给下家）。杠同理：明杠多摸一张且血战里&amp;quot;杠上花&amp;quot;有额外收益，但同样暴露信息，尾盘还可能给别人开&amp;quot;杠上炮&amp;quot;。&lt;strong&gt;起手做大时积极碰杠，快跑路线时能不碰就不碰。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;听口选择&lt;/strong&gt;遵循两个数字：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;听口宽度&lt;/strong&gt;：两面听（8 张潜在）&amp;gt; 卡张/边张（4 张）&amp;gt; 对倒/单钓（2~4 张）。能选宽不选窄；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;剩余张数&lt;/strong&gt;：减去已见的。听见过 2 张的两面（剩 4~6 张），通常不如听没见过的卡张（剩 4 张但还有别人扣牌的空间）。尾盘再加一条修正：&lt;strong&gt;听熟张&lt;/strong&gt;——别人敢打的牌，你才能吃到点炮；听生张只能指望自摸。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;把出牌优先级和碰杠决策各画成一张图，实战时按图索骥即可：&lt;/p&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;pre tabindex=&#34;0&#34;&gt;&lt;code class=&#34;language-mermaid&#34; data-lang=&#34;mermaid&#34;&gt;flowchart TD
    A[这巡要出牌] --&amp;gt; B{手里有缺门牌?}
    B --&amp;gt;|有| C[打缺门牌]
    B --&amp;gt;|没有| D{有孤立端张?&amp;lt;br/&amp;gt;无邻张的1/9}
    D --&amp;gt;|有| E[打孤立端张]
    D --&amp;gt;|没有| F{有孤立中张?}
    F --&amp;gt;|有| G[打孤立中张]
    F --&amp;gt;|没有| H{搭子进张数比较}
    H --&amp;gt;|拆进张最少的搭子| I[打出该搭子中&amp;lt;br/&amp;gt;价值更低的一张]
    C &amp;amp; E &amp;amp; G &amp;amp; I --&amp;gt; J[打完前最后过一遍&amp;lt;br/&amp;gt;4.2.2 防点炮checklist]&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;pre tabindex=&#34;0&#34;&gt;&lt;code class=&#34;language-mermaid&#34; data-lang=&#34;mermaid&#34;&gt;flowchart TD
    P[别人打出一张牌&amp;lt;br/&amp;gt;我可以碰/杠] --&amp;gt; Q{碰了立刻听牌&amp;lt;br/&amp;gt;或是对子路线?}
    Q --&amp;gt;|是| R[碰]
    Q --&amp;gt;|否| S{我在做大&amp;lt;br/&amp;gt;且碰能加速成型?}
    S --&amp;gt;|是| R
    S --&amp;gt;|否| T{不碰就永远&amp;lt;br/&amp;gt;等不到这张?}
    T --&amp;gt;|是| R
    T --&amp;gt;|否| U[不碰, 默认答案]
    U -.成本.-&amp;gt; V1[暴露信息: 全桌知道你收这门]
    U -.成本.-&amp;gt; V2[破坏门清弹性: 手牌变窄]
    U -.成本.-&amp;gt; V3[改变摸牌顺序: 可能放走关键张]
    style U fill:#d4edda
    style R fill:#fff3cd&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;&lt;h3 id=&#34;44-概率层期望值决策做大快跑防守的切换点&#34;&gt;4.4 概率层：期望值决策——做大、快跑、防守的切换点&lt;/h3&gt;
&lt;p&gt;这是整个框架的&amp;quot;调度器&amp;quot;，回答&amp;quot;什么时候换路线&amp;quot;。先把 4.3 的直觉量化成一个可心算的模型。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;基本概率感觉&lt;/strong&gt;：任一时刻，你某方向的&amp;quot;期望进展&amp;quot; ≈ 剩余进张数 × 剩余可摸次数。你手里 4-5 万等 3/6 万，若都少见，剩余进张约 6-8 张；牌墙剩 40 张、四家轮流摸，你大约还能摸 8-10 次，摸中概率相当可观。但如果 3/6 万已见 3 张，进张只剩 1~2 张，期望直接砍到四分之一以下——&lt;strong&gt;搭子的价值不看牌面，看剩余进张数&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;切换到&amp;quot;做大&amp;quot;的条件&lt;/strong&gt;（需同时满足）：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;起手结构支持：9 张以上同花色，或 4 对以上；&lt;/li&gt;
&lt;li&gt;进张供给充足：三家缺门不与你抢这门（4.1 的绿灯）；&lt;/li&gt;
&lt;li&gt;时间充足：牌墙过半之前。做牌是&amp;quot;用听牌速度换番数&amp;quot;，起手离目标越近、剩余巡数越多，换得越划算；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;血战规则加成&lt;/strong&gt;：一家胡牌不结束牌局，早胡者只是先锁定小收益，晚胡的大牌仍能兑现——大牌的&amp;quot;被截胡损失&amp;quot;比推倒胡小得多，所以四川麻将天然鼓励做牌。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;切换到&amp;quot;快跑/止损&amp;quot;的条件&lt;/strong&gt;（满足任一即执行）：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;进张枯竭&lt;/strong&gt;：关键牌期望进张 &amp;lt; 2，每一巡等待都是负期望——马上换听、拆死搭子；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;有人已听牌且你生张密集&lt;/strong&gt;：此时你的期望收益为负（点炮赔分 + 被查叫），最优解不是&amp;quot;我也赶紧听&amp;quot;，而是&lt;strong&gt;只打熟张把点炮概率压到最低&lt;/strong&gt;，保住查叫底线。这就是&amp;quot;不跟随到最后&amp;quot;——追不上就不再投入，及时止损；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;牌墙见底（剩 15 张以内）还没听牌&lt;/strong&gt;：做牌的时间价值归零，唯一目标是&lt;strong&gt;听牌防查叫&lt;/strong&gt;——往剩余张数最多的熟张方向听，只听一张也比&amp;quot;做了一半的大牌&amp;quot;值钱。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;一个简化的期望算账例子&lt;/strong&gt;（假设平胡 1 番 = 2 分，清一色 4 番 = 16 分）：你听牌了，可以选听口 A——平胡两面听，剩余进张 6 张；或改听口 B——清一色单钓，剩余进张 2 张。若剩余可摸次数约 6 次、点炮胡与自摸混合，A 的期望收益约是正 6~8 分；B 做成的概率约一半不到，做成了 16 分、做不成还要承担被查叫赔 2 分——期望约 6 分，且方差巨大。&lt;strong&gt;结论：番数翻倍抵不过进张减半，除非时间足够早或你已经落后必须搏。&lt;/strong&gt; 新手默认选宽听口，永远是长期正期望。&lt;/p&gt;
&lt;p&gt;把&amp;quot;什么时候切换路线&amp;quot;画成一张象限图——横轴是你的剩余进张，纵轴是牌局剩余时间，每巡定位一次自己落在哪个象限：&lt;/p&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;pre tabindex=&#34;0&#34;&gt;&lt;code class=&#34;language-mermaid&#34; data-lang=&#34;mermaid&#34;&gt;flowchart TB
    UP[纵轴 ↑ 剩余时间多] --- TOP
    subgraph TOP[ ]
        direction LR
        LEFT2[进张少] ~~~ A[换听调整&amp;lt;br/&amp;gt;时间够但方向死了&amp;lt;br/&amp;gt;例: 卡七万已见三张]
        A ~~~ B[做大&amp;lt;br/&amp;gt;时间够且进张足&amp;lt;br/&amp;gt;例: 起手清一色九张]
        B ~~~ RIGHT2[进张多]
    end
    TOP --- BOT
    subgraph BOT[ ]
        direction LR
        LEFT[进张少] ~~~ C[防守保底&amp;lt;br/&amp;gt;先听牌防查叫&amp;lt;br/&amp;gt;例: 剩十二张没听牌]
        C ~~~ D[快跑&amp;lt;br/&amp;gt;进张还在抢时间&amp;lt;br/&amp;gt;例: 两面听六张剩半局]
        D ~~~ RIGHT[进张多]
    end
    BOT --- DOWN[纵轴 ↓ 剩余时间少]
    style A fill:#fff3cd
    style B fill:#ffe6cc
    style C fill:#f8d7da
    style D fill:#d4edda&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;&lt;p&gt;两条边界记死：&lt;strong&gt;进张 &amp;lt; 2 立刻换方向（左半区行动），剩 15 张只保听牌（下半区行动）&lt;/strong&gt;。右下象限是血战到底最常见的好局面——进张还在但时间不多，别犹豫，最快胡法锁收益。&lt;/p&gt;
&lt;h3 id=&#34;45-血战到底的三条特有推论&#34;&gt;4.5 血战到底的三条特有推论&lt;/h3&gt;
&lt;p&gt;规则特殊，策略就有别处用不上的推论：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;先胡是&amp;quot;锁仓&amp;quot;，不是&amp;quot;胜利&amp;quot;&lt;/strong&gt;。你第一个平胡跑了，只是锁定 2 分；剩下三人继续打，大牌仍在你走后兑现。所以领先时&lt;strong&gt;快跑锁收益&lt;/strong&gt;，落后时反而要&lt;strong&gt;做大搏翻盘&lt;/strong&gt;——这和推倒胡&amp;quot;先胡为王&amp;quot;的直觉相反。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;查叫把尾盘变成了保底竞赛&lt;/strong&gt;。没胡的人要给听牌者赔分，所以尾盘的及格线是&amp;quot;听牌&amp;quot;而不是&amp;quot;胡牌&amp;quot;。最后 15 张，一切决策让位于&amp;quot;听上&amp;quot;。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;点炮一人付，防守成本低&lt;/strong&gt;。别人自摸三家分摊、你点炮你独赔——你打熟张&amp;quot;放生&amp;quot;别人自摸，损失远小于你点炮。所以防守期打熟张不仅是避险，还是&lt;strong&gt;把损失转移给全桌分摊&lt;/strong&gt;的理性选择。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;三种结算方式的赔付流向对比，一看就懂为什么&amp;quot;防守打熟张&amp;quot;是理性选择：&lt;/p&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;pre tabindex=&#34;0&#34;&gt;&lt;code class=&#34;language-mermaid&#34; data-lang=&#34;mermaid&#34;&gt;flowchart LR
    subgraph 点炮[你点炮: 你独赔]
        Y1[你 -8分] --&amp;gt; H1[胡牌者 +8分]
        O1[另两家 0分]
    end
    subgraph 自摸[对手自摸: 三家分摊]
        Y2[你 -3分] --&amp;gt; H2[胡牌者 +9分]
        O2[另两家各 -3分] --&amp;gt; H2
    end
    subgraph 查叫[流局查叫: 没听赔听牌]
        Y3[你没听 -2分] --&amp;gt; H3[听牌者 +2分]
    end
    style Y1 fill:#f8d7da
    style Y2 fill:#fff3cd
    style Y3 fill:#d4edda&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;&lt;p&gt;同样是对手胡牌，你点炮亏 8 分、放生他自摸只亏 3 分——&lt;strong&gt;防守期每打一张熟张，赚的就是这个差价。&lt;/strong&gt;&lt;/p&gt;
&lt;h3 id=&#34;46-新手常见错误清单&#34;&gt;4.6 新手常见错误清单&lt;/h3&gt;
&lt;p&gt;对照检查，改掉一条涨一截胜率：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;只记自己牌，不看池子&lt;/strong&gt;——搭子死活取决于关键张已见几张，不看池子等于闭眼打牌；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;见碰就碰&lt;/strong&gt;——暴露信息、锁死手牌结构，快跑路线尤其要忍住；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;死抱死搭子&lt;/strong&gt;——7 万已见 3 张还抱着 6、8 万不拆，每巡都在付租金；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;做大不看缺门&lt;/strong&gt;——三家都抢你这门还硬做清一色，供给断了；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;尾盘还想着翻盘&lt;/strong&gt;——剩 15 张没听牌，唯一 KPI 是听牌防查叫；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;不跟随到最后反着用&lt;/strong&gt;——该止损时硬追生张点炮，该搏的时候（落后、时间早）却提前保守。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&#34;47-一页纸决策框架&#34;&gt;4.7 一页纸决策框架&lt;/h3&gt;
&lt;p&gt;把整章压缩成一张每巡都走的决策图：&lt;/p&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;pre tabindex=&#34;0&#34;&gt;&lt;code class=&#34;language-mermaid&#34; data-lang=&#34;mermaid&#34;&gt;flowchart TD
    A[每一巡] --&amp;gt; B{信息更新:&amp;lt;br/&amp;gt;关键张已见数/&amp;lt;br/&amp;gt;三家缺门/谁听牌了}
    B --&amp;gt; C{阶段判断}
    C --&amp;gt;|序盘: 定缺+定路线| D{起手支持做大?&amp;lt;br/&amp;gt;9张+同花色/4对+&amp;lt;br/&amp;gt;且三家不抢这门}
    D --&amp;gt;|是| E[做大路线:&amp;lt;br/&amp;gt;积极碰杠, 屯关键张]
    D --&amp;gt;|否| F[快跑路线:&amp;lt;br/&amp;gt;牌效优先, 能不碰不碰]
    C --&amp;gt;|中盘| G{有人已听牌&amp;lt;br/&amp;gt;且我生张密集?}
    G --&amp;gt;|是| H[防守: 只打熟张&amp;lt;br/&amp;gt;放生自摸, 不点炮]
    G --&amp;gt;|否| I{我的关键进张&amp;lt;br/&amp;gt;剩余&amp;gt;=4?}
    I --&amp;gt;|是| J[继续推进&amp;lt;br/&amp;gt;选进张最多的打]
    I --&amp;gt;|否| K[拆死搭子&amp;lt;br/&amp;gt;换听熟张方向]
    C --&amp;gt;|尾盘: 剩15张内| L{已听牌?}
    L --&amp;gt;|否| M[唯一目标: 听牌防查叫&amp;lt;br/&amp;gt;听剩张最多的熟张]
    L --&amp;gt;|是| N[宽听口优先&amp;lt;br/&amp;gt;不为一番改窄]&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;&lt;p&gt;四个层级的关系：&lt;strong&gt;战略层定方向，信息层供弹药，战术层管执行，概率层负责喊停。&lt;/strong&gt; 新手先把战术层的&amp;quot;进张最大化&amp;quot;练成肌肉记忆，再逐步叠加记牌和读牌，最后学会用期望值喊停——这条学习路径本身也是按依赖关系排的。&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&#34;五贝密的技术亮点&#34;&gt;五、贝密的技术亮点&lt;/h2&gt;
&lt;p&gt;最后回到工程，挑几个我认为值得讲的点。&lt;/p&gt;
&lt;h3 id=&#34;51-用-byte-编码整副牌一副麻将只要几十个字节&#34;&gt;5.1 用 byte 编码整副牌：一副麻将只要几十个字节&lt;/h3&gt;
&lt;p&gt;贝密把每张麻将牌编码成一个 &lt;code&gt;byte&lt;/code&gt;：万/筒/条各占 0~107（每门 36 个编号 = 9 个面值 × 4 张），牌面值 = &lt;code&gt;(card % 36) / 4&lt;/code&gt;，花色 = &lt;code&gt;card / 36&lt;/code&gt;；风牌用负数。这样 108 张牌的牌墙就是一个 &lt;code&gt;byte[108]&lt;/code&gt;，序列化后不到 80 字节。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;举例&lt;/strong&gt;：编号 12 这张牌，&lt;code&gt;12 / 36 = 0&lt;/code&gt;（万子），&lt;code&gt;(12 % 36) / 4 = 3&lt;/code&gt;（第 4 个面值）——就是&amp;quot;四万&amp;quot;。一个除法一个取模就解出了花色和面值，不用查表、不用对象，广播出牌消息时网络上跑的就是 &lt;code&gt;&amp;quot;12,13&amp;quot;&lt;/code&gt; 这样的字符串。&lt;/p&gt;
&lt;h3 id=&#34;52-胡牌判断不用-3n2-递归用分堆--贪心摘顺子&#34;&gt;5.2 胡牌判断：不用 3N+2 递归，用&amp;quot;分堆 + 贪心摘顺子&amp;quot;&lt;/h3&gt;
&lt;p&gt;经典麻将胡牌算法是 3N+2 递归枚举。贝密的 &lt;code&gt;GameUtils.processMJCard&lt;/code&gt; 走了一条不同的路：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;把 14 张手牌按 &lt;code&gt;key = 面值 + 9 × 花色&lt;/code&gt; 放进字典计数，&lt;strong&gt;先查定缺&lt;/strong&gt;——缺门花色还在手里，直接不能胡（四川规则前置拦截）；&lt;/li&gt;
&lt;li&gt;按计数把牌分成三堆：&lt;code&gt;others&lt;/code&gt;（单张）、&lt;code&gt;pairs&lt;/code&gt;（对子）、&lt;code&gt;kezi&lt;/code&gt;（刻子）；&lt;/li&gt;
&lt;li&gt;对单张堆排序后&lt;strong&gt;贪心摘顺子&lt;/strong&gt;：同花色且 key 连续的 k/k+1/k+2 三张直接移除；&lt;/li&gt;
&lt;li&gt;终止条件：单张摘空且恰好剩 1 对将（或 7 对——七对牌型天然被&amp;quot;pairs 堆 = 14 张&amp;quot;覆盖）；&lt;/li&gt;
&lt;li&gt;对子多于 1 对时，把多余对子拆开当顺子候选再摘；全无刻子时枚举刻子拆开试。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;源码 &lt;code&gt;main()&lt;/code&gt; 里作者还放了性能测试：&lt;strong&gt;500 万次胡牌判断&lt;/strong&gt;直接计时。要说明的是，这是个简化实现——作者自己在注释里承认某些&amp;quot;三连对&amp;quot;式的特殊牌型没完整处理。对在线棋牌的实际负载来说，这种&amp;quot;足够快、覆盖主流牌型&amp;quot;的取舍是合理的。&lt;/p&gt;
&lt;h3 id=&#34;53-斗地主牌型识别频次直方图一把梭&#34;&gt;5.3 斗地主牌型识别：频次直方图一把梭&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;ActionTaskUtils.identification(byte[])&lt;/code&gt; 的做法很干净：先统计同点数张数的分布（几个单张、几个对子、几个三张、几个炸弹），再用 &lt;code&gt;switch(种类数, 最大点数)&lt;/code&gt; 一把识别全部牌型——单张、对子、三带一/二、顺子、连对、飞机带翅、四带二、炸弹、王炸。比较两手牌大小的 &lt;code&gt;allow()&lt;/code&gt; 则只有三条规则：王炸最大、炸弹管一切非炸弹、同牌型比最大点数。AI 托管出牌也复用这套识别，找&amp;quot;能压住上家的最小牌&amp;quot;。&lt;/p&gt;
&lt;h3 id=&#34;54-自研极简状态机统一驱动两种游戏&#34;&gt;5.4 自研极简状态机统一驱动两种游戏&lt;/h3&gt;
&lt;p&gt;斗地主和麻将的流程差异巨大（抢地主 vs 定缺、出牌规则完全不同），但贝密用一个抽象统一了：&lt;code&gt;BeiMiStateMachine&lt;/code&gt; + 按事件名查 transition 表 + Action 链。新增一种游戏 = 写一组 Action + 一份状态配置，调度、计时、广播的基础设施全部复用。&lt;/p&gt;
&lt;h3 id=&#34;55-把缓存过期回调当游戏计时器用&#34;&gt;5.5 把缓存过期回调当游戏计时器用&lt;/h3&gt;
&lt;p&gt;这是我觉得最&amp;quot;巧&amp;quot;的设计。棋牌游戏里到处是倒计时：出牌 8 秒、杠碰胡决策 5 秒、抢地主超时自动放弃、房间超时补机器人。贝密没用 Quartz、没开线程池，而是用一个 &lt;code&gt;sharpExpiry&lt;/code&gt; 的 &lt;strong&gt;cache2k 缓存&lt;/strong&gt;当计时器：key 是 roomid，value 是任务对象，&lt;strong&gt;到期自动触发过期监听器执行任务&lt;/strong&gt;。玩家主动操作了？&lt;code&gt;remove(roomid)&lt;/code&gt; 就把倒计时取消了；要重置？put 一个新任务进去。零额外线程，语义严丝合缝。&lt;/p&gt;
&lt;h3 id=&#34;56-实时状态与持久化彻底解耦&#34;&gt;5.6 实时状态与持久化彻底解耦&lt;/h3&gt;
&lt;p&gt;牌局进行中的所有状态（房间、Board、玩家手牌、撮合队列）全在 &lt;strong&gt;Hazelcast&lt;/strong&gt; 分布式内存里，MySQL 只在结算时被&lt;strong&gt;异步&lt;/strong&gt;写入——通过 &lt;strong&gt;Disruptor&lt;/strong&gt; 的 RingBuffer 把变更事件丢给消费者，JPA 和 Elasticsearch 双写（ES 用于运营后台的战绩检索）。注释里还有一条重要的工程纪律：&amp;ldquo;只有一个地方对 GameRoom 二次写入，避免分布式锁&amp;rdquo;——用设计约束换掉了锁的开销。另外，房卡模式建房时的局数等规则赋值用了 &lt;strong&gt;Drools&lt;/strong&gt; 规则引擎，规则写在 &lt;code&gt;.drl&lt;/code&gt; 文件里，运营改规则不用改代码。&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&#34;六总结与反思&#34;&gt;六、总结与反思&lt;/h2&gt;
&lt;p&gt;回顾这个项目，对新手最有价值的三个认知是：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;联网游戏 = 服务器权威 + 按人过滤的状态分发&lt;/strong&gt;。理解了&amp;quot;客户端只是显示器和遥控器&amp;quot;，就看懂了所有联网游戏的骨架。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;棋牌游戏的复杂度不在算法，在工程&lt;/strong&gt;。胡牌判断几百行就写完了，真正难的是断线重连、超时托管、并发房间这些&amp;quot;无聊&amp;quot;的部分——它们决定了玩家半夜打牌掉线时骂不骂人。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;做游戏的人必须会玩游戏&lt;/strong&gt;。第四章的四川麻将策略——分层记牌、由&amp;quot;打出&amp;quot;反推&amp;quot;留下&amp;quot;、用剩余进张 × 剩余巡数做期望决策——不只是给玩家的攻略，也是当初我们校验 AI 托管和出牌提示是否合理的标尺。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;当然也有粗糙之处，如实记录：README 宣称的德州扑克在 0.8.0 里只有枚举占位并未实现；胡牌算法是简化版；源码里散落的 &lt;code&gt;System.out.println&lt;/code&gt; 和作者自注的 bug 注释（比如多人同时可碰时先碰者胜出的竞态问题）都还在。但作为一个学习联网游戏开发的开源样本，它把一个房卡棋牌系统该有的零件——长连接、房间状态机、计时器、托管、异步落盘——都真实地摆在了那里，这比完美更有价值。&lt;/p&gt;
</description>
    </item>
    <item>
      <title>《去赶海吧》：用 Cocos Creator 写一款「大鱼吃小鱼」io 微信小游戏</title>
      <link>https://ftmao.com/blog/posts/fishpick-cocos-io-game-intro/</link>
      <pubDate>Wed, 26 Feb 2020 14:00:00 +0800</pubDate>
      <guid>https://ftmao.com/blog/posts/fishpick-cocos-io-game-intro/</guid>
      <description>&lt;h1 id=&#34;去赶海吧用-cocos-creator-写一款大鱼吃小鱼io-微信小游戏&#34;&gt;《去赶海吧》：用 Cocos Creator 写一款「大鱼吃小鱼」io 微信小游戏&lt;/h1&gt;
&lt;p&gt;最近把自己做的一款微信小游戏整理开源了，写篇文章介绍一下玩法和实现上的一些技术点，希望能给同样在做休闲小游戏的朋友一些参考。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;开源地址：&lt;a href=&#34;https://gitee.com/soulnew/fishPick&#34;&gt;https://gitee.com/soulnew/fishPick&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;技术栈：Cocos Creator 2.3.4 + TypeScript&lt;/li&gt;
&lt;li&gt;平台：微信小游戏&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;游戏叫《去赶海吧》，核心玩法一句话概括：&lt;strong&gt;摇杆控制赶海人在海里吃海鲜攒贝壳，分数即体型，大鱼吃小鱼，活到最后吃鸡&lt;/strong&gt;。&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&#34;一玩法介绍&#34;&gt;一、玩法介绍&lt;/h2&gt;
&lt;h3 id=&#34;11-核心循环&#34;&gt;1.1 核心循环&lt;/h3&gt;
&lt;p&gt;玩家通过全屏虚拟摇杆控制角色游动，摄像机始终跟随主角。地图上持续刷新各种海洋生物，吃到就得分（贝壳），分数直接决定体型和战斗力：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;两个玩家相遇，分高者吃掉分低者&lt;/strong&gt;——这是 io 类游戏的经典规则；&lt;/li&gt;
&lt;li&gt;被吃的玩家死亡后会&lt;strong&gt;爆出金币&lt;/strong&gt;（一半的分数拆成最多 10 条金币鱼散布在周围），形成&amp;quot;击杀后抢遗产&amp;quot;的爽点；&lt;/li&gt;
&lt;li&gt;分数变成负数会直接死亡，所以海里的&amp;quot;毒蛇&amp;quot;（-2 分）和&amp;quot;海胆&amp;quot;（-1 分）是要躲着走的。&lt;/li&gt;
&lt;/ul&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;pre tabindex=&#34;0&#34;&gt;&lt;code class=&#34;language-mermaid&#34; data-lang=&#34;mermaid&#34;&gt;graph LR
    A[摇杆控制游动] --&amp;gt; B[吃海鲜攒贝壳]
    B --&amp;gt; C{遇到其他玩家?}
    C --&amp;gt;|我分高| D[吃掉对方]
    C --&amp;gt;|我分低| E[触发逃跑 AI / 被吃]
    D --&amp;gt; F[对方爆金币]
    F --&amp;gt; B
    B --&amp;gt; G[实时排行榜 Top5]&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;&lt;h3 id=&#34;12-鱼的种类与概率设计&#34;&gt;1.2 鱼的种类与概率设计&lt;/h3&gt;
&lt;p&gt;海里的鱼不只是&amp;quot;分&amp;quot;，还混入了道具和陷阱，让取舍更有策略性：&lt;/p&gt;
&lt;table&gt;
	&lt;thead&gt;
			&lt;tr&gt;
					&lt;th&gt;类型&lt;/th&gt;
					&lt;th&gt;效果&lt;/th&gt;
					&lt;th&gt;概率（实现值）&lt;/th&gt;
			&lt;/tr&gt;
	&lt;/thead&gt;
	&lt;tbody&gt;
			&lt;tr&gt;
					&lt;td&gt;加速道具&lt;/td&gt;
					&lt;td&gt;速度 ×1.5，持续 10 秒&lt;/td&gt;
					&lt;td&gt;5%&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;护盾道具&lt;/td&gt;
					&lt;td&gt;免疫被吃 / 免疫负分鱼，10 秒&lt;/td&gt;
					&lt;td&gt;5%&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;鱼、虾、蟹&lt;/td&gt;
					&lt;td&gt;+1 分&lt;/td&gt;
					&lt;td&gt;约 35%&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;水母、海星&lt;/td&gt;
					&lt;td&gt;+2 分&lt;/td&gt;
					&lt;td&gt;20%&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;乌龟&lt;/td&gt;
					&lt;td&gt;+3 分&lt;/td&gt;
					&lt;td&gt;10%&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;海豚&lt;/td&gt;
					&lt;td&gt;+4 分&lt;/td&gt;
					&lt;td&gt;5%&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;毒蛇&lt;/td&gt;
					&lt;td&gt;&lt;strong&gt;-2 分&lt;/strong&gt;&lt;/td&gt;
					&lt;td&gt;10%&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;海胆&lt;/td&gt;
					&lt;td&gt;&lt;strong&gt;-1 分&lt;/strong&gt;&lt;/td&gt;
					&lt;td&gt;10%&lt;/td&gt;
			&lt;/tr&gt;
	&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;负分鱼 10 秒后会自动消失，道具的剩余时间通过节点透明度渐变来提示，玩家一眼就能看懂状态。&lt;/p&gt;
&lt;h3 id=&#34;13-四种游戏模式&#34;&gt;1.3 四种游戏模式&lt;/h3&gt;
&lt;p&gt;主菜单提供四个入口，共用同一套核心机制：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;无尽模式&lt;/strong&gt;：死了可以用激励视频复活，AI 每 5 秒重生补齐，刷分为主；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;挑战模式&lt;/strong&gt;：&amp;ldquo;吃鸡&amp;quot;玩法——剩余玩家数低于阈值后进入 10 秒倒计时，活到最后获胜；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;贪吃蛇模式&lt;/strong&gt;：吃到的鱼变成尾巴跟随身后，撞到别人的尾巴即死；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;人挤人模式&lt;/strong&gt;：群体 Steering Behaviors 的实验玩法，20 个跟随者 + 1 个领队挤作一团。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;挑战模式和贪吃蛇模式开局前有一个&lt;strong&gt;伪联机匹配面板&lt;/strong&gt;：&amp;ldquo;搜索附近玩家…&amp;ldquo;的省略号动画，12 个头像从 288 张头像图集中不重复随机抽取、按 0~6 秒随机延迟逐个亮起——营造真人匹配的感觉，实际对手全是 AI。这是小团队没有联机服务器时的一个低成本体验方案。&lt;/p&gt;
&lt;h3 id=&#34;14-ai-设计&#34;&gt;1.4 AI 设计&lt;/h3&gt;
&lt;p&gt;AI 不是无头苍蝇，每个 AI 出生时会随机分到三种性格之一：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;进攻型&lt;/strong&gt;：主动出击追杀人；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;吃鱼型&lt;/strong&gt;：闷头吃海鲜发育；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;闲逛型&lt;/strong&gt;：随机游走为主。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;配合两个细节参数避免行为抖动：被追时有 2 秒&amp;quot;逃跑冷却&amp;rdquo;（不会在追/逃之间反复横跳），追击最长持续 7 秒（避免死追不放）。AI 找鱼也不搜全图，而是在自己屏幕范围内找&lt;strong&gt;鱼最多的格子&lt;/strong&gt;再过去吃——这个格子划分和碰撞检测用的是同一套空间网格，下面会讲。&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&#34;二技术亮点&#34;&gt;二、技术亮点&lt;/h2&gt;
&lt;p&gt;整个项目是单场景架构（主场景约 400 个节点），28 个 TypeScript 脚本，代码量不大，但性能优化和 AI 上的几个点值得展开说说。&lt;/p&gt;
&lt;h3 id=&#34;21-空间哈希网格把碰撞检测从-on-降为局部查询&#34;&gt;2.1 空间哈希网格：把碰撞检测从 O(n) 降为局部查询&lt;/h3&gt;
&lt;p&gt;地图上同时存在几百条鱼，如果每帧让每条鱼和所有玩家做碰撞检测，微信小游戏的低端机上肯定扛不住。做法是把地图划分为 &lt;strong&gt;20×20 的网格&lt;/strong&gt;：&lt;/p&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;pre tabindex=&#34;0&#34;&gt;&lt;code class=&#34;language-mermaid&#34; data-lang=&#34;mermaid&#34;&gt;graph TD
    A[鱼出生/移动] --&amp;gt; B[按坐标计算所在格子&amp;lt;br/&amp;gt;key = x_y]
    B --&amp;gt; C[挂到对应桶 BlockManager]
    D[碰撞检测] --&amp;gt; E[只查玩家 AABB 覆盖的格子&amp;lt;br/&amp;gt;getCoveredBlock]
    E --&amp;gt; F[与桶内鱼做精确检测]
    C -.鱼移动跨格.-&amp;gt; G[updatePosofFish 迁移桶]&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;&lt;ul&gt;
&lt;li&gt;碰撞检测时只查询玩家包围盒覆盖的几个格子里的鱼，而不是全图扫描；&lt;/li&gt;
&lt;li&gt;鱼移动跨格时从一个桶迁移到另一个桶；&lt;/li&gt;
&lt;li&gt;这套网格还顺手解决了另外两个需求：&lt;strong&gt;视野裁剪&lt;/strong&gt;（视野外格子里的鱼直接隐藏）和上面提到的 &lt;strong&gt;AI 找鱼最密集区域&lt;/strong&gt;。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;再配合&lt;strong&gt;降频采样&lt;/strong&gt;——每 4 帧才做一次全量碰撞检测——CPU 占用就控制得很好了。&lt;/p&gt;
&lt;h3 id=&#34;22-对象池向-gc-抖动说不&#34;&gt;2.2 对象池：向 GC 抖动说不&lt;/h3&gt;
&lt;p&gt;鱼、金币、飘分数字这些高频创建销毁的节点全部走对象池：&lt;code&gt;Map&amp;lt;string, Node[]&amp;gt;&lt;/code&gt; 按预制名分桶，全局上限 500。小游戏平台上 GC 卡顿是卡顿的主要来源之一，对象池是性价比最高的优化。&lt;/p&gt;
&lt;h3 id=&#34;23-steering-behaviors教科书式的群体行为实现&#34;&gt;2.3 Steering Behaviors：教科书式的群体行为实现&lt;/h3&gt;
&lt;p&gt;&amp;ldquo;人挤人&amp;quot;模式的背后是把 Reynolds 那套经典的 Steering Behaviors 完整实现了一遍（&lt;code&gt;Gamer.ts&lt;/code&gt;，约 400 行）：&lt;/p&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;pre tabindex=&#34;0&#34;&gt;&lt;code class=&#34;language-mermaid&#34; data-lang=&#34;mermaid&#34;&gt;graph TD
    subgraph Steering Behaviors
        A[Seek 追踪] --&amp;gt; H[AccumulateForce&amp;lt;br/&amp;gt;力累加 + 截断]
        B[Flee 逃跑] --&amp;gt; H
        C[Arrive 减速到达] --&amp;gt; H
        D[Wander 随机游走] --&amp;gt; H
        E[Alignment / Cohesion / Separation&amp;lt;br/&amp;gt;群体三规则] --&amp;gt; H
        F[Pursuit 带预测提前量的追击] --&amp;gt; H
        H --&amp;gt; I[EnforceNonPenetrationConstraint&amp;lt;br/&amp;gt;防穿透约束]
        I --&amp;gt; J[最终速度/朝向]
    end&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;&lt;p&gt;其中 Wander 用的是&amp;quot;jitter + 圆半径 + 投影距离&amp;quot;的经典做法，Pursuit 会根据目标速度算提前量。所有行为输出的力先累加、再按 max_force 截断，最后加一个防穿透的分离约束——这套结构在后面的项目里基本可以原样搬走。&lt;/p&gt;
&lt;h3 id=&#34;24-屏外敌人指示器io-游戏的标配-ux&#34;&gt;2.4 屏外敌人指示器：io 游戏的标配 UX&lt;/h3&gt;
&lt;p&gt;强敌在屏幕外时，玩家需要感知威胁方向。实现方式是给每个屏外 AI 分配一个&lt;strong&gt;钉在屏幕边缘的箭头 + 分数标签&lt;/strong&gt;：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;用 &lt;code&gt;atan2&lt;/code&gt; 计算敌人相对主角的方向角；&lt;/li&gt;
&lt;li&gt;按屏幕对角线角度判断箭头该落在上下左右哪条边上；&lt;/li&gt;
&lt;li&gt;求射线与屏幕边界的交点作为箭头位置；&lt;/li&gt;
&lt;li&gt;8 种随机颜色区分不同敌人。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;代码不长，但体验提升非常明显——被高分玩家接近时能提前看到箭头逼近，紧张感就出来了。&lt;/p&gt;
&lt;h3 id=&#34;25-运行时程序化动画&#34;&gt;2.5 运行时程序化动画&lt;/h3&gt;
&lt;p&gt;美术资源只有每张皮肤 2 帧的游动图，动画是运行时用 &lt;code&gt;cc.AnimationClip.createWithSpriteFrames&lt;/code&gt; &lt;strong&gt;动态创建&lt;/strong&gt;的 2 帧循环。换皮肤只需要换图集里的起始帧索引（100/102/104…），不用为每套皮肤手工做 Animation 资源。6 套皮肤 = 12 帧图 + 一段代码，维护成本几乎为零。&lt;/p&gt;
&lt;h3 id=&#34;26-事件总线与模块解耦&#34;&gt;2.6 事件总线与模块解耦&lt;/h3&gt;
&lt;p&gt;全局只有一个 50 行的事件总线（on/off/fire）加一个聚合单例，音乐、广告、面板切换、碰撞结果全部走事件通信。单场景多面板的结构下，这种轻量解耦比引入框架更实用。&lt;/p&gt;
&lt;h3 id=&#34;27-微信小游戏生态全套集成&#34;&gt;2.7 微信小游戏生态全套集成&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;分包加载&lt;/strong&gt;：字体、头像、音频、图集拆成 4 个分包，loading 场景按包显示进度；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;广告变现&lt;/strong&gt;：Banner + 激励视频复活；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;社交&lt;/strong&gt;：&lt;code&gt;wx.showShareMenu&lt;/code&gt; 分享、好友排行榜（开放数据域 + &lt;code&gt;wx.postMessage&lt;/code&gt; 通信）、&lt;code&gt;wx.login&lt;/code&gt; 登录；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;轻量后端&lt;/strong&gt;：服务器只负责登录、存分数、拉排行三个接口，玩法完全单机，部署成本极低。&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h2 id=&#34;三整体架构一览&#34;&gt;三、整体架构一览&lt;/h2&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;pre tabindex=&#34;0&#34;&gt;&lt;code class=&#34;language-mermaid&#34; data-lang=&#34;mermaid&#34;&gt;graph TD
    subgraph 场景层
        L[loading.fire&amp;lt;br/&amp;gt;分包加载 + 剧情动画] --&amp;gt; F[FirstPannel.fire&amp;lt;br/&amp;gt;单场景多面板]
    end
    subgraph 玩法层
        F --&amp;gt; GM[gameManager&amp;lt;br/&amp;gt;刷鱼/模式管理/降频碰撞]
        GM --&amp;gt; P[Player / BasePlayer&amp;lt;br/&amp;gt;得分/体型/道具/死亡爆币]
        GM --&amp;gt; Fish[Fish&amp;lt;br/&amp;gt;概率产出]
        GM --&amp;gt; Snake[贪吃蛇尾巴 lerp 跟随]
    end
    subgraph 数据与系统层
        GM --&amp;gt; BM[BlockManager&amp;lt;br/&amp;gt;20×20 空间网格]
        GM --&amp;gt; CM[CollideManager]
        GM --&amp;gt; PM[PoolManager&amp;lt;br/&amp;gt;对象池上限500]
        GM --&amp;gt; SM[ScoreManager&amp;lt;br/&amp;gt;实时排行]
        GM --&amp;gt; EV[CustomEventListener&amp;lt;br/&amp;gt;事件总线]
    end
    subgraph 平台层
        EV --&amp;gt; WX[分包/广告/分享/好友排行/登录]
        EV --&amp;gt; SRV[轻量后端&amp;lt;br/&amp;gt;login / saveScore / getRank]
    end&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;&lt;hr&gt;
&lt;h2 id=&#34;四写在最后&#34;&gt;四、写在最后&lt;/h2&gt;
&lt;p&gt;这个项目验证了一个想法：&lt;strong&gt;没有联机服务器，用&amp;quot;伪匹配 + 性格化 AI&amp;quot;也能撑起 io 游戏的核心体验&lt;/strong&gt;。代码里当然也留下了一些遗憾，比如数值全部硬编码没有配置表、&lt;code&gt;BasePlayer.scoreChange()&lt;/code&gt; 里有一条永远执行不到的 else-if 分支（&lt;code&gt;score&amp;gt;=10&lt;/code&gt; 先命中，后面的大体型缩放被挡住了）——这些都在代码里原样保留着，权当给读者的&amp;quot;找茬彩蛋&amp;rdquo;。&lt;/p&gt;
&lt;p&gt;源码在 &lt;a href=&#34;https://gitee.com/soulnew/fishPick&#34;&gt;https://gitee.com/soulnew/fishPick&lt;/a&gt;，欢迎 Star，也欢迎提 Issue 交流。&lt;/p&gt;
</description>
    </item>
    <item>
      <title>《游戏开发物理学》学习笔记：从运动学到刚体碰撞的新手实战指南</title>
      <link>https://ftmao.com/blog/bookstore/game-physics-bourg-notes/</link>
      <pubDate>Thu, 16 Jan 2020 11:00:00 +0800</pubDate>
      <guid>https://ftmao.com/blog/bookstore/game-physics-bourg-notes/</guid>
      <description>&lt;h1 id=&#34;游戏开发物理学学习笔记从运动学到刚体碰撞的新手实战指南&#34;&gt;《游戏开发物理学》学习笔记：从运动学到刚体碰撞的新手实战指南&lt;/h1&gt;
&lt;blockquote&gt;
&lt;p&gt;参考书籍：《Physics for Game Developers》（David M. Bourg，O&amp;rsquo;Reilly，中译名《游戏开发物理学》）
目标读者：想自己做物理效果（而不是只会调引擎参数）的游戏开发者、图形学/仿真方向的新手。
阅读建议：这篇文章帮你建立&lt;strong&gt;主干知识框架&lt;/strong&gt;：先数学，再运动学 → 动力学 → 载具模拟 → 实时积分 → 碰撞 → 粒子，最后给出避坑清单和主流引擎对照。公式只保留实战中真正会用的。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr&gt;
&lt;h2 id=&#34;一这本书到底讲什么&#34;&gt;一、这本书到底讲什么？&lt;/h2&gt;
&lt;p&gt;很多新手做游戏的物理效果，路径是这样的：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;用 Unity/UE 拖一个 Rigidbody，调调质量、摩擦力，能跑就行；&lt;/li&gt;
&lt;li&gt;遇到「球穿过墙了」「车开起来像肥皂盒」「碰撞后能量爆炸越弹越高」就抓瞎；&lt;/li&gt;
&lt;li&gt;回头发现：不是引擎不行，是自己不懂底层的物理模型。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Bourg 这本书的价值就在于：&lt;strong&gt;它把物理引擎内部在干什么，用程序员能实现的粒度讲了一遍&lt;/strong&gt;。它不追求物理学的严谨完备，而是追求「在 60 FPS 的游戏循环里，用可承受的计算量，得到看起来正确、玩起来稳定的结果」。&lt;/p&gt;
&lt;p&gt;全书知识地图：&lt;/p&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;pre tabindex=&#34;0&#34;&gt;&lt;code class=&#34;language-mermaid&#34; data-lang=&#34;mermaid&#34;&gt;mindmap
  root((游戏开发物理学))
    数学基础
      向量与运算
      矩阵与坐标变换
      四元数
    运动学 Kinematics
      位移 速度 加速度
      抛体运动
    动力学 Dynamics
      牛顿定律
      力与力矩
      流体阻力
    载具模拟
      飞机
      舰船
      气垫船
      汽车
    实时模拟
      数值积分
      游戏循环
      时间步长
    碰撞
      碰撞检测
      碰撞反应 冲量
    粒子系统&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;&lt;p&gt;&lt;strong&gt;一句话总纲&lt;/strong&gt;：游戏物理 = 用力计算加速度，用数值积分把加速度变成速度和位置，用碰撞检测发现问题，用冲量修正速度。整本书就是在把这句话的每个环节讲细。&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&#34;二核心数学基础速成&#34;&gt;二、核心数学基础速成&lt;/h2&gt;
&lt;p&gt;物理引擎 90% 的代码是向量、矩阵和四元数运算。这一关不过，后面全看不懂。&lt;/p&gt;
&lt;h3 id=&#34;21-向量物理量的通用语言&#34;&gt;2.1 向量：物理量的通用语言&lt;/h3&gt;
&lt;p&gt;位置、速度、力、动量，全部是向量。必须熟练的运算：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;加减&lt;/strong&gt;：力的合成（$F = F_1 + F_2$）、相对速度；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;点积&lt;/strong&gt;（结果是一个数）：&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;$$
\mathbf{a} \cdot \mathbf{b} = |\mathbf{a}||\mathbf{b}|\cos\theta
$$&lt;/p&gt;
&lt;p&gt;实战用途：求投影（比如速度沿碰撞法线方向的分量）、判断两个方向夹角是锐角还是钝角（点积大于 0 还是小于 0）。碰撞反应里「物体是否在相互靠近」就是靠点积判断的。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;叉积&lt;/strong&gt;（结果是一个垂直向量，仅 3D；2D 中退化为一个标量）：&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;$$
|\mathbf{a} \times \mathbf{b}| = |\mathbf{a}||\mathbf{b}|\sin\theta
$$&lt;/p&gt;
&lt;p&gt;实战用途：&lt;strong&gt;力矩&lt;/strong&gt; $\tau = \mathbf{r} \times \mathbf{F}$（力作用点偏离质心会产生转动）、2D 碰撞冲量里的旋转项。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;归一化&lt;/strong&gt;：$\hat{\mathbf{v}} = \mathbf{v} / |\mathbf{v}|$，只要方向不要大小（碰撞法线必须归一化，忘记这步是经典 bug）。&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;避坑：2D 里叉积 $\mathbf{r} \times \mathbf{F}$ 是标量 $r_x F_y - r_y F_x$，正负号代表顺时针/逆时针，和引擎的坐标系朝向约定有关，写之前先定好「逆时针为正」并全程统一。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3 id=&#34;22-矩阵与坐标变换&#34;&gt;2.2 矩阵与坐标变换&lt;/h3&gt;
&lt;p&gt;旋转、缩放、坐标系切换都靠矩阵。新手只需要先建立两个直觉：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;矩阵乘法 = 变换的复合，&lt;strong&gt;顺序不可交换&lt;/strong&gt;（先旋转再平移 ≠ 先平移再旋转）；&lt;/li&gt;
&lt;li&gt;每个刚体都活在两个坐标系里：&lt;strong&gt;局部坐标系&lt;/strong&gt;（物体自己的，转动惯量、几何形状在这里算最方便）和&lt;strong&gt;世界坐标系&lt;/strong&gt;（所有物体一起碰撞、渲染的地方）。物理计算每天的工作就是在这两个系之间来回变换。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&#34;23-四元数3d-姿态的正确打开方式&#34;&gt;2.3 四元数：3D 姿态的正确打开方式&lt;/h3&gt;
&lt;p&gt;3D 中表示朝向有三种方式：&lt;/p&gt;
&lt;table&gt;
	&lt;thead&gt;
			&lt;tr&gt;
					&lt;th&gt;方式&lt;/th&gt;
					&lt;th&gt;优点&lt;/th&gt;
					&lt;th&gt;缺点&lt;/th&gt;
			&lt;/tr&gt;
	&lt;/thead&gt;
	&lt;tbody&gt;
			&lt;tr&gt;
					&lt;td&gt;欧拉角（roll/pitch/yaw）&lt;/td&gt;
					&lt;td&gt;直观，3 个数&lt;/td&gt;
					&lt;td&gt;&lt;strong&gt;万向锁&lt;/strong&gt;（gimbal lock），插值不平滑&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;旋转矩阵&lt;/td&gt;
					&lt;td&gt;直接用于向量变换&lt;/td&gt;
					&lt;td&gt;9 个数冗余，数值误差积累会「变形」&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;&lt;strong&gt;四元数&lt;/strong&gt;&lt;/td&gt;
					&lt;td&gt;4 个数紧凑，插值平滑，无万向锁&lt;/td&gt;
					&lt;td&gt;不直观，乘法规则要背&lt;/td&gt;
			&lt;/tr&gt;
	&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;游戏物理的标准做法：&lt;strong&gt;内部存储和积分用四元数，给玩家看的时候转欧拉角&lt;/strong&gt;。四元数必须经常归一化，否则数值漂移会让物体悄悄缩放变形。&lt;/p&gt;
&lt;h3 id=&#34;24-微积分只需要两个直觉&#34;&gt;2.4 微积分：只需要两个直觉&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;导数&lt;/strong&gt; = 变化率。速度是位置的导数，加速度是速度的导数。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;积分&lt;/strong&gt; = 累积。已知加速度，累加得到速度；累加速度得到位置。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;游戏里你永远不会真的去解符号积分——而是用数值积分一步步逼近（见第六章）。理解「导数是斜率、积分是累加」就够了。&lt;/p&gt;
&lt;h3 id=&#34;25-单位制与坐标系开工前先立规矩&#34;&gt;2.5 单位制与坐标系：开工前先立规矩&lt;/h3&gt;
&lt;blockquote&gt;
&lt;p&gt;避坑（这是新手第一大坑）：开工前必须白纸黑字定下来——&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;长度单位是&lt;strong&gt;米&lt;/strong&gt;还是&lt;strong&gt;像素&lt;/strong&gt;？重力加速度是 9.81 m/s² 还是 981 px/s²？混用必出灵异现象。&lt;/li&gt;
&lt;li&gt;2D 屏幕坐标 &lt;strong&gt;y 轴向下&lt;/strong&gt;，而物理教科书 y 轴向上。直接照抄公式会把「下落」写成「飞天」。&lt;/li&gt;
&lt;li&gt;角度用&lt;strong&gt;弧度&lt;/strong&gt;，所有三角函数接口都是弧度，传角度进去结果毫无明显错误提示。&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;
&lt;hr&gt;
&lt;h2 id=&#34;三运动学kinematics只描述运动不问原因&#34;&gt;三、运动学（Kinematics）：只描述运动，不问原因&lt;/h2&gt;
&lt;p&gt;运动学关心「物体怎么动」，不关心「为什么动」（那是动力学的事）。&lt;/p&gt;
&lt;h3 id=&#34;31-三个基本量&#34;&gt;3.1 三个基本量&lt;/h3&gt;
&lt;p&gt;$$
\text{速度 } \mathbf{v} = \frac{d\mathbf{x}}{dt}, \qquad \text{加速度 } \mathbf{a} = \frac{d\mathbf{v}}{dt}
$$&lt;/p&gt;
&lt;p&gt;匀加速运动（加速度恒定）下的三个实用公式：&lt;/p&gt;
&lt;p&gt;$$
v = v_0 + at
$$&lt;/p&gt;
&lt;p&gt;$$
x = x_0 + v_0 t + \frac{1}{2} a t^2
$$&lt;/p&gt;
&lt;p&gt;$$
v^2 = v_0^2 + 2a(x - x_0)
$$&lt;/p&gt;
&lt;h3 id=&#34;32-抛体运动游戏里最值钱的运动学&#34;&gt;3.2 抛体运动：游戏里最值钱的运动学&lt;/h3&gt;
&lt;p&gt;抛体（炮弹、篮球、跳跃的角色）的核心思想一句话：&lt;strong&gt;水平方向和竖直方向互相独立，分开算&lt;/strong&gt;。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;水平：无加速度，匀速 $x = v_0 \cos\theta \cdot t$&lt;/li&gt;
&lt;li&gt;竖直：受重力，$y = v_0 \sin\theta \cdot t - \frac{1}{2} g t^2$&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;由这两个式子可以推出（落回同一高度时）：&lt;/p&gt;
&lt;p&gt;$$
\text{射程 } R = \frac{v_0^2 \sin 2\theta}{g}, \qquad \text{最高点 } H = \frac{v_0^2 \sin^2\theta}{2g}
$$&lt;/p&gt;
&lt;p&gt;两个直接能用在游戏里的结论：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;45 度抛射射程最远&lt;/strong&gt;（不计空气阻力时）；&lt;/li&gt;
&lt;li&gt;已知初速度和目标距离，可以反解出抛射角——这就是「抛物线命中目标」的数学基础。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;典型问题：炮弹打移动目标的提前量。&lt;/strong&gt; 设目标当前位置 $\mathbf{p}$、速度 $\mathbf{v}_t$，炮弹速度 $s$，求解命中时间 $t$：&lt;/p&gt;
&lt;p&gt;$$
|\mathbf{p} + \mathbf{v}_t t| = s t
$$&lt;/p&gt;
&lt;p&gt;两边平方得到一个关于 $t$ 的一元二次方程，取最小正根，瞄准点就取 $\mathbf{p} + \mathbf{v}_t t$。AI 炮塔、技能预判全是这个套路。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;避坑：二次方程可能无解（目标跑得太快炮弹追不上），代码里必须处理判别式小于 0 的情况，否则 AI 会对着永远打不中的目标开火。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr&gt;
&lt;h2 id=&#34;四作用力与动力学dynamics运动的原因&#34;&gt;四、作用力与动力学（Dynamics）：运动的原因&lt;/h2&gt;
&lt;h3 id=&#34;41-牛顿三定律的程序员版本&#34;&gt;4.1 牛顿三定律的程序员版本&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;惯性定律&lt;/strong&gt;：不受力的物体保持匀速直线运动——所以你不用写代码让它「继续动」，它不减速只是因为没人让它减速；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;核心公式&lt;/strong&gt; $\mathbf{F} = m\mathbf{a}$：所有物理引擎的心脏。&lt;strong&gt;先把作用在物体上的所有力加起来（合力），除以质量得到加速度，然后交给积分器&lt;/strong&gt;；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;作用力与反作用力&lt;/strong&gt;：碰撞反应、火箭推力的理论基础。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;旋转版本的对应关系（这对应表值得背下来）：&lt;/p&gt;
&lt;table&gt;
	&lt;thead&gt;
			&lt;tr&gt;
					&lt;th&gt;直线运动&lt;/th&gt;
					&lt;th&gt;旋转运动&lt;/th&gt;
			&lt;/tr&gt;
	&lt;/thead&gt;
	&lt;tbody&gt;
			&lt;tr&gt;
					&lt;td&gt;质量 $m$&lt;/td&gt;
					&lt;td&gt;转动惯量 $I$&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;力 $\mathbf{F}$&lt;/td&gt;
					&lt;td&gt;力矩 $\tau = \mathbf{r} \times \mathbf{F}$&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;加速度 $\mathbf{a} = \mathbf{F}/m$&lt;/td&gt;
					&lt;td&gt;角加速度 $\alpha = \tau / I$&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;速度 $\mathbf{v}$&lt;/td&gt;
					&lt;td&gt;角速度 $\omega$&lt;/td&gt;
			&lt;/tr&gt;
	&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;转动惯量的直觉：质量分布离转轴越远越难转（花样滑冰收手加速）。2D 里 $I$ 就是一个标量（如均匀圆盘 $I = \frac{1}{2}mr^2$，细杆绕中心 $I = \frac{1}{12}mL^2$）；3D 里是一个 3×3 张量矩阵，通常直接在&lt;strong&gt;局部坐标系&lt;/strong&gt;下预计算并求逆，避免每帧重复。&lt;/p&gt;
&lt;h3 id=&#34;42-游戏里最常见的几种力&#34;&gt;4.2 游戏里最常见的几种力&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;重力&lt;/strong&gt;：$F = mg$，恒定向下，人人有份；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;弹簧力&lt;/strong&gt;：胡克定律 $F = -kx$（$k$ 是劲度系数），悬挂、软绳、镜头跟随弹性都靠它；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;摩擦力&lt;/strong&gt;：静摩擦（小于阈值时完全抵消切向力）与动摩擦 $F = \mu N$（$\mu$ 摩擦系数，$N$ 法向压力）；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;流体阻力&lt;/strong&gt;（空气/水）：低速近似线性 $F = -k_1 v$，高速用二次项：&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;$$
F_d = -\frac{1}{2} \rho v^2 C_d A , \hat{\mathbf{v}}
$$&lt;/p&gt;
&lt;p&gt;$\rho$ 是流体密度，$C_d$ 阻力系数，$A$ 是迎流面积。飞机、舰船、弹道下坠感全靠它。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;避坑：二次阻力在速度很大时力也爆炸式增大，直接积分容易数值不稳定（物体来回震荡甚至飞出屏幕）。实战技巧是给阻力加速度设上限，或改用半隐式积分。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr&gt;
&lt;h2 id=&#34;五载具物理本书的招牌四章&#34;&gt;五、载具物理：本书的招牌四章&lt;/h2&gt;
&lt;p&gt;Bourg 用飞机、舰船、气垫船、汽车四个例子，展示同一套框架（合力 → 加速度 → 积分）如何套出完全不同的手感。掌握套路比记住每个参数重要。&lt;/p&gt;
&lt;h3 id=&#34;51-飞机四力平衡&#34;&gt;5.1 飞机：四力平衡&lt;/h3&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;pre tabindex=&#34;0&#34;&gt;&lt;code class=&#34;language-mermaid&#34; data-lang=&#34;mermaid&#34;&gt;flowchart LR
    T[推力 Thrust&amp;lt;br/&amp;gt;发动机] --&amp;gt;|向前| P((飞机))
    D[阻力 Drag&amp;lt;br/&amp;gt;空气摩擦与压差] --&amp;gt;|向后| P
    L[升力 Lift&amp;lt;br/&amp;gt;机翼上下压差] --&amp;gt;|向上| P
    W[重力 Weight&amp;lt;br/&amp;gt;mg] --&amp;gt;|向下| P&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;&lt;p&gt;核心公式是升力和阻力，共享同一个「动压」结构：&lt;/p&gt;
&lt;p&gt;$$
L = \frac{1}{2} \rho v^2 S C_L, \qquad D = \frac{1}{2} \rho v^2 S C_D
$$&lt;/p&gt;
&lt;p&gt;其中 $S$ 是机翼面积，$C_L$ 升力系数。关键知识点：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;$C_L$ 随&lt;strong&gt;迎角&lt;/strong&gt;（机翼与气流的夹角）增大而增大，但超过临界迎角后骤减——这就是&lt;strong&gt;失速&lt;/strong&gt;，飞机升力瞬间消失往下掉。模拟飞行手感，失速是必须实现的；&lt;/li&gt;
&lt;li&gt;滚转、俯仰、偏航三个通道各自独立输入力矩，组合出飞行姿态；&lt;/li&gt;
&lt;li&gt;升力正比于 $v^2$：速度不够就拉不起来，这是「起飞需要跑道」的物理原因。&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;避坑：完整的空气动力学模型极复杂，游戏里几乎没人真做 CFD。标准做法是&lt;strong&gt;查表/分段线性&lt;/strong&gt;：把 $C_L$ 和 $C_D$ 做成关于迎角的查找表，手感调好就是胜利。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3 id=&#34;52-舰船浮力与流体阻力的天下&#34;&gt;5.2 舰船：浮力与流体阻力的天下&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;浮力&lt;/strong&gt;：阿基米德原理，浮力等于排开水的重量。简化模拟常用「浮力采样点」——在船底布若干采样点，每个点按入水深度产生向上的力，自然涌现出船的横摇和纵摇；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;稳性&lt;/strong&gt;：浮心与重心的相对位置决定船会不会翻。重心太高、浮心太低 = 头重脚轻，一晃就翻；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;阻力&lt;/strong&gt;：水的密度约为空气的 800 多倍，所以舰船阻力以&lt;strong&gt;粘性阻力&lt;/strong&gt;为主，低速近似线性即可；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;转弯&lt;/strong&gt;：舵产生侧向水动力，船不是「拐过去」而是「漂着转过去」，模拟时要给船体侧向很大的阻力、纵向较小的阻力，才有「船感」。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&#34;53-气垫船低摩擦的特殊手感&#34;&gt;5.3 气垫船：低摩擦的特殊手感&lt;/h3&gt;
&lt;p&gt;气垫船悬在气垫上，&lt;strong&gt;与地面的摩擦几乎为零&lt;/strong&gt;，带来两个标志性的操控特性：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;推力方向和运动方向可以长期不一致——船头朝东，船还在往西漂；&lt;/li&gt;
&lt;li&gt;转向靠舵/侧推产生力矩，但&lt;strong&gt;转弯时船身会侧滑&lt;/strong&gt;，直到横向水/气阻力把侧向速度消耗掉。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;实现上就是在普通刚体模型里把接触摩擦调得极小，再加分方向差异很大的阻力系数（横向阻力大、纵向阻力小）。「阻力系数各向异性」是气垫船和所有水上载具手感的灵魂。&lt;/p&gt;
&lt;h3 id=&#34;54-汽车一切手感都在轮胎上&#34;&gt;5.4 汽车：一切手感都在轮胎上&lt;/h3&gt;
&lt;p&gt;汽车的驱动力链路：&lt;strong&gt;发动机扭矩 → 变速箱/挡位 → 驱动轮 → 轮胎与地面的摩擦 → 牵引力&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;最重要的概念是&lt;strong&gt;抓地力有限&lt;/strong&gt;：轮胎能提供的摩擦力有上限（摩擦圆/摩擦椭圆），加速、刹车、转向消耗的抓地力共享同一个预算。转向打满还猛踩油门，抓地力透支——打滑、漂移就这么来的。&lt;/p&gt;
&lt;p&gt;简化模型三件套：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;驱动力 $F = \text{engineForce}$，随速度衰减（极速的来源：驱动力 = 阻力时不再加速）；&lt;/li&gt;
&lt;li&gt;阻力 = 滚动阻力 + 空气阻力（二次项主导高速）；&lt;/li&gt;
&lt;li&gt;转向时按当前速度把车头方向「往速度方向拉」，拉伸强度代表抓地力——强度低就是漂移手感。&lt;/li&gt;
&lt;/ol&gt;
&lt;blockquote&gt;
&lt;p&gt;避坑：很多新手给汽车直接用「速度 = 车头方向 × 油门」，车像轨道车一样瞬间转向，毫无惯性。正确做法是&lt;strong&gt;转向改的是车头朝向，速度方向靠轮胎力逐渐跟上&lt;/strong&gt;，这个「跟不上」就是车感的来源。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr&gt;
&lt;h2 id=&#34;六实时模拟数值积分与时间步长&#34;&gt;六、实时模拟：数值积分与时间步长&lt;/h2&gt;
&lt;p&gt;前面所有公式给出的是「这一瞬间的加速度」。游戏是一帧一帧跑的，核心问题是：&lt;strong&gt;怎么从加速度一步步推出位置和姿态？&lt;/strong&gt;&lt;/p&gt;
&lt;h3 id=&#34;61-游戏物理主循环&#34;&gt;6.1 游戏物理主循环&lt;/h3&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;pre tabindex=&#34;0&#34;&gt;&lt;code class=&#34;language-mermaid&#34; data-lang=&#34;mermaid&#34;&gt;flowchart TD
    A[读取输入] --&amp;gt; B[计算所有作用力与力矩&amp;lt;br/&amp;gt;重力/推力/阻力/弹簧...]
    B --&amp;gt; C[求合力 F 与合力矩 τ&amp;lt;br/&amp;gt;a = F/m, α = τ/I]
    C --&amp;gt; D[数值积分&amp;lt;br/&amp;gt;更新速度 v、角速度 ω]
    D --&amp;gt; E[再积分&amp;lt;br/&amp;gt;更新位置 x、姿态 q]
    E --&amp;gt; F[碰撞检测]
    F --&amp;gt; G{有碰撞?}
    G --&amp;gt;|是| H[碰撞反应&amp;lt;br/&amp;gt;施加冲量修正速度]
    G --&amp;gt;|否| I[渲染]
    H --&amp;gt; I
    I --&amp;gt; A&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;&lt;h3 id=&#34;62-三种积分方法对比&#34;&gt;6.2 三种积分方法对比&lt;/h3&gt;
&lt;table&gt;
	&lt;thead&gt;
			&lt;tr&gt;
					&lt;th&gt;方法&lt;/th&gt;
					&lt;th&gt;公式（以位置为例）&lt;/th&gt;
					&lt;th&gt;精度&lt;/th&gt;
					&lt;th&gt;速度&lt;/th&gt;
					&lt;th&gt;适用&lt;/th&gt;
			&lt;/tr&gt;
	&lt;/thead&gt;
	&lt;tbody&gt;
			&lt;tr&gt;
					&lt;td&gt;显式欧拉&lt;/td&gt;
					&lt;td&gt;$x(t+h) = x(t) + v(t),h$&lt;/td&gt;
					&lt;td&gt;差，能量发散&lt;/td&gt;
					&lt;td&gt;最快&lt;/td&gt;
					&lt;td&gt;不推荐单独用&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;&lt;strong&gt;半隐式欧拉&lt;/strong&gt;（Symplectic Euler）&lt;/td&gt;
					&lt;td&gt;先 $v(t+h) = v(t) + a,h$，再 $x(t+h) = x(t) + v(t+h),h$&lt;/td&gt;
					&lt;td&gt;一阶但&lt;strong&gt;能量稳定&lt;/strong&gt;&lt;/td&gt;
					&lt;td&gt;快&lt;/td&gt;
					&lt;td&gt;&lt;strong&gt;游戏物理默认选择&lt;/strong&gt;&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;RK4&lt;/td&gt;
					&lt;td&gt;四次采样加权平均&lt;/td&gt;
					&lt;td&gt;四阶，很准&lt;/td&gt;
					&lt;td&gt;4 倍开销&lt;/td&gt;
					&lt;td&gt;需要精确的离线仿真&lt;/td&gt;
			&lt;/tr&gt;
	&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;半隐式欧拉只比显式欧拉改了一个顺序（&lt;strong&gt;用新速度更新位置&lt;/strong&gt;），稳定性却有质的飞跃——这是书里最划算的「一行代码的智慧」。&lt;/p&gt;
&lt;h3 id=&#34;63-时间步长实时模拟的头号工程问题&#34;&gt;6.3 时间步长：实时模拟的头号工程问题&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;固定时间步&lt;/strong&gt;：物理每 1/60 秒（或 1/120 秒）推进一步，渲染帧率高时插值、低时多补几步。优点：结果可复现（回放、联机同步必须用它）；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;可变时间步&lt;/strong&gt;：直接用上一帧的耗时当 $h$。优点简单；缺点致命——帧率波动直接改变物理行为，同一台机器跑两遍结果都不一样。&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;避坑（ spiral of death，死亡螺旋）：帧率突然下降 → 单帧物理步数暴增 → 物理计算更慢 → 帧率进一步下降。解法：给每帧物理步数设上限（如最多 5 步），宁可游戏慢动作也不让模拟爆炸。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;blockquote&gt;
&lt;p&gt;避坑（隧道效应 tunneling）：步长太大 + 速度太快 → 子弹一帧飞过了整面墙，两帧采样点都在墙外，碰撞检测永远发现不了。解法：小步长、射线扫掠检测（swept collision / CCD），或对高速物体单独细分步长。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr&gt;
&lt;h2 id=&#34;七刚体旋转从粒子到真正的物体&#34;&gt;七、刚体旋转：从粒子到真正的「物体」&lt;/h2&gt;
&lt;p&gt;粒子只有位置；刚体多了&lt;strong&gt;姿态&lt;/strong&gt;（orientation），一切都变复杂了。&lt;/p&gt;
&lt;p&gt;3D 姿态更新的标准流程（四元数版）：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;存姿态四元数 $q$ 和角速度向量 $\omega$；&lt;/li&gt;
&lt;li&gt;每步积分：&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;$$
q(t+h) = q(t) + \frac{h}{2}, \tilde{\omega} , q(t)
$$&lt;/p&gt;
&lt;p&gt;其中 $\tilde{\omega} = (0, \omega_x, \omega_y, \omega_z)$ 是纯四元数形式的角速度（注意乘法顺序，别写反）；
3. &lt;strong&gt;归一化 $q$&lt;/strong&gt;（不做的后果：物体慢慢变大/缩小）；
4. 需要转动向量时，用 $q$ 把向量从局部系旋转到世界系。&lt;/p&gt;
&lt;p&gt;力矩链路：外力作用点相对质心的偏移 $\mathbf{r}$ 产生力矩 $\tau = \mathbf{r} \times \mathbf{F}$，角加速度 $\alpha = I^{-1}\tau$（3D 中 $I^{-1}$ 要用当前姿态把局部系的逆惯量张量变换到世界系），积分得 $\omega$。这就是为什么打在边缘的一拳会让物体边飞边转。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;避坑：3D 转动惯量张量一定要在&lt;strong&gt;局部坐标系&lt;/strong&gt;预计算并求逆；每帧变换到世界系用 $I^{-1}_{world} = R I^{-1}_{local} R^T$。直接在世界系里积分惯量是错误的经典写法。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr&gt;
&lt;h2 id=&#34;八碰撞检测先粗后精&#34;&gt;八、碰撞检测：先粗后精&lt;/h2&gt;
&lt;h3 id=&#34;81-两阶段流水线&#34;&gt;8.1 两阶段流水线&lt;/h3&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;pre tabindex=&#34;0&#34;&gt;&lt;code class=&#34;language-mermaid&#34; data-lang=&#34;mermaid&#34;&gt;flowchart LR
    subgraph 粗阶段 Broad Phase
        A1[遍历所有物体对] --&amp;gt; A2[包围球 / AABB 快速排除]
        A2 --&amp;gt; A3[空间划分&amp;lt;br/&amp;gt;网格 / 四叉树 / BVH]
    end
    subgraph 窄阶段 Narrow Phase
        B1[候选物体对] --&amp;gt; B2[精确求交&amp;lt;br/&amp;gt;SAT / 顶点-面 / 边-边]
        B2 --&amp;gt; B3[输出碰撞信息&amp;lt;br/&amp;gt;接触点 / 法线 / 穿透深度]
    end
    A3 --&amp;gt; B1&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;&lt;p&gt;关键思想：$n$ 个物体两两检测是 $O(n^2)$，先用粗阶段把 99% 不可能相碰的对子排除掉，窄阶段只处理剩下的少数。&lt;/p&gt;
&lt;h3 id=&#34;82-常用包围体&#34;&gt;8.2 常用包围体&lt;/h3&gt;
&lt;table&gt;
	&lt;thead&gt;
			&lt;tr&gt;
					&lt;th&gt;包围体&lt;/th&gt;
					&lt;th&gt;特点&lt;/th&gt;
					&lt;th&gt;典型场景&lt;/th&gt;
			&lt;/tr&gt;
	&lt;/thead&gt;
	&lt;tbody&gt;
			&lt;tr&gt;
					&lt;td&gt;包围球&lt;/td&gt;
					&lt;td&gt;测试只需比较圆心距与半径和，最快&lt;/td&gt;
					&lt;td&gt;子弹、粒子、粗略筛选&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;AABB（轴对齐包围盒）&lt;/td&gt;
					&lt;td&gt;各轴区间重叠判断，简单快速&lt;/td&gt;
					&lt;td&gt;静态场景、粗阶段&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;OBB（有向包围盒）&lt;/td&gt;
					&lt;td&gt;贴合物体旋转，测试较贵&lt;/td&gt;
					&lt;td&gt;车辆、细长物体&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;凸多边形/凸包 + SAT&lt;/td&gt;
					&lt;td&gt;&lt;strong&gt;分离轴定理&lt;/strong&gt;：存在一条轴使两形体的投影不重叠 ⟺ 不相交&lt;/td&gt;
					&lt;td&gt;2D 精确检测、3D 凸体&lt;/td&gt;
			&lt;/tr&gt;
	&lt;/tbody&gt;
&lt;/table&gt;
&lt;blockquote&gt;
&lt;p&gt;避坑：SAT 的轴候选是「两个形体所有面的法线」（3D 还要加边叉积），漏掉一组轴就会在特定角度漏检。另外 SAT 只适用于&lt;strong&gt;凸体&lt;/strong&gt;，凹体要先拆成多个凸体。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3 id=&#34;83-碰撞检测必须输出的三样东西&#34;&gt;8.3 碰撞检测必须输出的三样东西&lt;/h3&gt;
&lt;p&gt;窄阶段不是只回答「碰没碰」，而是要给出碰撞反应需要的输入：&lt;strong&gt;接触点&lt;/strong&gt;（冲量作用在哪里）、&lt;strong&gt;法线&lt;/strong&gt;（冲量沿哪个方向）、&lt;strong&gt;穿透深度&lt;/strong&gt;（位置修正多少）。缺了任何一个，后面的碰撞反应都写不出来。&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&#34;九碰撞反应冲量法&#34;&gt;九、碰撞反应：冲量法&lt;/h2&gt;
&lt;p&gt;碰撞瞬间持续时间极短、力极大，直接模拟力不现实。游戏物理的标准做法是&lt;strong&gt;冲量法&lt;/strong&gt;：把碰撞看成瞬间改变速度的一次「踢」。&lt;/p&gt;
&lt;h3 id=&#34;91-核心推导思路&#34;&gt;9.1 核心推导思路&lt;/h3&gt;
&lt;p&gt;设两物体碰撞前的&lt;strong&gt;相对速度沿法线分量&lt;/strong&gt;为 $v_r \cdot \mathbf{n}$，碰撞后变为 $-e$ 倍（$e$ 是恢复系数：$e=1$ 完全弹性，$e=0$ 完全非弹性）——这就是牛顿碰撞定律。把「冲量同时改变两物体的线速度和角速度」代入，解出法向冲量大小：&lt;/p&gt;
&lt;p&gt;$$
j = \frac{-(1+e), \mathbf{v}_{rel} \cdot \mathbf{n}}
{\dfrac{1}{m_1} + \dfrac{1}{m_2} +
\dfrac{(\mathbf{r}_1 \times \mathbf{n})^2}{I_1} +
\dfrac{(\mathbf{r}_2 \times \mathbf{n})^2}{I_2}}
$$&lt;/p&gt;
&lt;p&gt;（这是 2D 版本，$\mathbf{r}_1$、$\mathbf{r}_2$ 是接触点相对两质心的偏移；3D 版本把平方项换成矩阵形式 $\mathbf{n} \cdot [(\mathbf{r} \times \mathbf{n}) I^{-1} \times \mathbf{r}]$。）&lt;/p&gt;
&lt;p&gt;然后：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;线速度：$\mathbf{v}_1 \mathrel{-}= \frac{j}{m_1}\mathbf{n}$，$\mathbf{v}_2 \mathrel{+}= \frac{j}{m_2}\mathbf{n}$&lt;/li&gt;
&lt;li&gt;角速度：$\omega_1 \mathrel{-}= \frac{(\mathbf{r}_1 \times j\mathbf{n})}{I_1}$，$\omega_2 \mathrel{+}= \frac{(\mathbf{r}_2 \times j\mathbf{n})}{I_2}$&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;打偏的一撞让物体旋转起来，就是这两个 $\mathbf{r} \times$ 项在起作用。&lt;/p&gt;
&lt;h3 id=&#34;92-摩擦冲量&#34;&gt;9.2 摩擦冲量&lt;/h3&gt;
&lt;p&gt;法向冲量解决「弹开」，摩擦冲量解决「蹭」。算出切向相对速度后施加切向冲量 $j_t$，并钳制在库仑摩擦锥内：&lt;/p&gt;
&lt;p&gt;$$
|j_t| \le \mu , j
$$&lt;/p&gt;
&lt;p&gt;不做这个钳制，摩擦会反过来给物体加速——又一个「能量爆炸」来源。&lt;/p&gt;
&lt;h3 id=&#34;93-实战避坑清单碰撞反应篇&#34;&gt;9.3 实战避坑清单（碰撞反应篇）&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;穿透与抖动&lt;/strong&gt;：数值误差让物体每帧陷进去一点又弹出来。解法：允许一个微小穿透量（slop），只对超出部分做位置修正（Baumgarte 稳定化），修正量再打个 0.2 左右的折扣；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;能量爆炸&lt;/strong&gt;：单次冲量解多接触点会互相打架，恢复系数别用满 1，摩擦记得钳制，必要时加阻尼；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;迭代求解&lt;/strong&gt;：多个接触点/约束时，对速度约束循环迭代 4-10 次（顺序冲量法），直到收敛——Box2D 就是这么做的；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;静止接触&lt;/strong&gt;：物体叠放静止时，冲量趋近于零但不等于零，抖动主要来自这里。slop + 位置修正 + 休眠（速度长期低于阈值就把物体标记为睡眠，跳过模拟）三件套缺一不可；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;衝量只改速度&lt;/strong&gt;：碰完物体还插在里面是正常的，位置交给下一帧的位置修正或积分自然分离，别试图一帧内又改速度又硬掰位置（除非做的是专门的位置投影）。&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h2 id=&#34;十粒子系统刚体的降维打击&#34;&gt;十、粒子系统：刚体的「降维打击」&lt;/h2&gt;
&lt;p&gt;粒子 = 没有旋转、没有形状（通常渲染成点或公告板）、数量极大的迷你刚体。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;模拟上只做：合力（重力、风、阻力）→ 半隐式欧拉积分 → 简单碰撞（对地面反弹或直接死亡）；&lt;/li&gt;
&lt;li&gt;表现上靠&lt;strong&gt;生成器&lt;/strong&gt;（发射速率、初速度锥角、寿命随机分布）和&lt;strong&gt;衰减&lt;/strong&gt;（颜色、大小、透明度随寿命变化）；&lt;/li&gt;
&lt;li&gt;典型用途：火花、烟雾、爆炸、尾迹、简单的流体/喷泉近似。&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;避坑：粒子系统的瓶颈几乎从不在物理，而在&lt;strong&gt;渲染和内存&lt;/strong&gt;。对象池复用粒子、避免每帧 new/delete，CPU 模拟 + GPU 渲染分工明确，是入门就该养成的习惯。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr&gt;
&lt;h2 id=&#34;十一2d-刚体模拟器把全书串起来的最小实战&#34;&gt;十一、2D 刚体模拟器：把全书串起来的最小实战&lt;/h2&gt;
&lt;p&gt;书中后半部分的 2D 刚体模拟示例，是最适合新手的毕业项目。最小可用版本只需要这些模块：&lt;/p&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;pre tabindex=&#34;0&#34;&gt;&lt;code class=&#34;language-mermaid&#34; data-lang=&#34;mermaid&#34;&gt;flowchart TD
    subgraph 数据结构
        D1[RigidBody2D&amp;lt;br/&amp;gt;位置/角度/线速度/角速度&amp;lt;br/&amp;gt;质量/转动惯量]
        D2[Contact&amp;lt;br/&amp;gt;接触点/法线/穿透深度]
    end
    subgraph 每帧流程
        S1[施加重力等外力] --&amp;gt; S2[半隐式欧拉积分]
        S2 --&amp;gt; S3[粗阶段: AABB 排除]
        S3 --&amp;gt; S4[窄阶段: SAT 求接触]
        S4 --&amp;gt; S5[冲量求解 迭代若干次]
        S5 --&amp;gt; S6[位置修正 slop + Baumgarte]
        S6 --&amp;gt; S7[休眠检测]
    end
    D1 --&amp;gt; S1
    S4 --&amp;gt; D2
    D2 --&amp;gt; S5&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;&lt;p&gt;动手顺序建议：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;先做单刚体自由落体 + 地面反弹（恢复系数可调），验证积分正确；&lt;/li&gt;
&lt;li&gt;加旋转：给一个方块偏心施加力，看是否边飞边转；&lt;/li&gt;
&lt;li&gt;加碰撞：圆 vs 圆（最简单）→ 凸多边形 SAT；&lt;/li&gt;
&lt;li&gt;加摩擦冲量和迭代求解；&lt;/li&gt;
&lt;li&gt;加位置修正和休眠，一堆方块能稳稳叠起来——恭喜，你写出了一个迷你 Box2D。&lt;/li&gt;
&lt;/ol&gt;
&lt;hr&gt;
&lt;h2 id=&#34;十二典型问题-faq&#34;&gt;十二、典型问题 FAQ&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;Q1：物体速度越来越快最后飞出世界（数值发散）？&lt;/strong&gt;
检查三件事：是否用了显式欧拉（换半隐式）；步长是否过大（弹簧系统等刚度大的力要求更小的步）；阻力/弹簧是否缺上限或阻尼。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Q2：子弹穿墙？&lt;/strong&gt;
隧道效应。高速物体用扫掠检测（上一帧位置到这一帧位置打一条射线/扫掠体），或对其使用更小的子步长（CCD）。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Q3：箱子叠在一起不停抖动、慢慢沉进地里？&lt;/strong&gt;
缺 slop 容差和位置修正折扣；恢复系数设成了接近 1（静止接触应该用 0 或极小）；缺少休眠机制。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Q4：联机/回放时物理结果不一致？&lt;/strong&gt;
使用了可变时间步或浮点运算顺序不一致。固定时间步 + 固定迭代次数 + 固定遍历顺序是确定性模拟的前提；跨平台完全一致还要锁定浮点模式或用定点数（如《王者荣耀》等帧同步游戏的做法）。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Q5：斜坡上的箱子自己往下滑 / 不会滑？&lt;/strong&gt;
检查摩擦实现：切向冲量有没有做库仑钳制 $\mu j$；法线是否归一化；静摩擦与动摩擦有没有分开处理。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Q6：四元数积分后物体悄悄变形/缩放？&lt;/strong&gt;
忘记每步归一化四元数。这是四元数模拟里出现率最高的 bug。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Q7：2D 里物体往天上「掉」？&lt;/strong&gt;
屏幕 y 轴向下而公式按 y 轴向上写的，重力符号反了。&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&#34;十三避坑总清单速查表&#34;&gt;十三、避坑总清单（速查表）&lt;/h2&gt;
&lt;table&gt;
	&lt;thead&gt;
			&lt;tr&gt;
					&lt;th&gt;坑&lt;/th&gt;
					&lt;th&gt;症状&lt;/th&gt;
					&lt;th&gt;解法&lt;/th&gt;
			&lt;/tr&gt;
	&lt;/thead&gt;
	&lt;tbody&gt;
			&lt;tr&gt;
					&lt;td&gt;单位混用（米/像素）&lt;/td&gt;
					&lt;td&gt;一切数值都「差不多但不对」&lt;/td&gt;
					&lt;td&gt;开工前定死单位制，重力常数跟着改&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;y 轴朝向&lt;/td&gt;
					&lt;td&gt;运动方向全部镜像&lt;/td&gt;
					&lt;td&gt;定好坐标约定，重力符号随之确定&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;显式欧拉&lt;/td&gt;
					&lt;td&gt;能量越来越多，逐渐爆炸&lt;/td&gt;
					&lt;td&gt;半隐式欧拉起步&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;可变时间步&lt;/td&gt;
					&lt;td&gt;帧率一掉物理就变，不可复现&lt;/td&gt;
					&lt;td&gt;固定时间步 + 步数上限&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;隧道效应&lt;/td&gt;
					&lt;td&gt;高速物体穿墙&lt;/td&gt;
					&lt;td&gt;扫掠检测 / CCD / 子步长&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;法线没归一化&lt;/td&gt;
					&lt;td&gt;碰撞反应大小诡异&lt;/td&gt;
					&lt;td&gt;所有法线使用前 normalize&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;四元数不归一化&lt;/td&gt;
					&lt;td&gt;物体缓慢缩放变形&lt;/td&gt;
					&lt;td&gt;每步积分后归一化&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;摩擦不钳制&lt;/td&gt;
					&lt;td&gt;摩擦反而加速物体&lt;/td&gt;
					&lt;td&gt;库仑摩擦锥约束&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;穿透硬修正&lt;/td&gt;
					&lt;td&gt;抖动、弹跳不止&lt;/td&gt;
					&lt;td&gt;slop + Baumgarte 折扣修正&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;无休眠&lt;/td&gt;
					&lt;td&gt;性能浪费 + 静止抖动&lt;/td&gt;
					&lt;td&gt;速度阈值休眠&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;死亡螺旋&lt;/td&gt;
					&lt;td&gt;卡顿后彻底卡死&lt;/td&gt;
					&lt;td&gt;每帧物理步数设上限&lt;/td&gt;
			&lt;/tr&gt;
	&lt;/tbody&gt;
&lt;/table&gt;
&lt;hr&gt;
&lt;h2 id=&#34;十四主流游戏框架与物理引擎&#34;&gt;十四、主流游戏框架与物理引擎&lt;/h2&gt;
&lt;p&gt;上面的原理，主流引擎都已内置实现；学原理的意义在于&lt;strong&gt;调得动、查得准、必要时能绕过&lt;/strong&gt;。&lt;/p&gt;
&lt;table&gt;
	&lt;thead&gt;
			&lt;tr&gt;
					&lt;th&gt;引擎/框架&lt;/th&gt;
					&lt;th&gt;物理方案&lt;/th&gt;
					&lt;th&gt;备注&lt;/th&gt;
			&lt;/tr&gt;
	&lt;/thead&gt;
	&lt;tbody&gt;
			&lt;tr&gt;
					&lt;td&gt;&lt;strong&gt;Unity&lt;/strong&gt;&lt;/td&gt;
					&lt;td&gt;PhysX（3D）、Box2D（2D）&lt;/td&gt;
					&lt;td&gt;Rigidbody / Rigidbody2D，FixedUpdate 即固定时间步&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;&lt;strong&gt;Unreal Engine&lt;/strong&gt;&lt;/td&gt;
					&lt;td&gt;Chaos（新版）、PhysX（旧版）&lt;/td&gt;
					&lt;td&gt;Chaos 支持破坏、布料等大型模拟&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;&lt;strong&gt;Godot&lt;/strong&gt;&lt;/td&gt;
					&lt;td&gt;自研 Godot Physics，可换 Jolt&lt;/td&gt;
					&lt;td&gt;开源，2D/3D 分离&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;&lt;strong&gt;Box2D&lt;/strong&gt;&lt;/td&gt;
					&lt;td&gt;2D 物理库（C++）&lt;/td&gt;
					&lt;td&gt;本书思想的工业化实现，愤怒的小鸟、无数 2D 游戏在用&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;&lt;strong&gt;Bullet&lt;/strong&gt;&lt;/td&gt;
					&lt;td&gt;3D 物理库&lt;/td&gt;
					&lt;td&gt;开源老牌，影视与游戏双用&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;&lt;strong&gt;Jolt&lt;/strong&gt;&lt;/td&gt;
					&lt;td&gt;3D 物理库&lt;/td&gt;
					&lt;td&gt;《地平线》系列使用，近年性能口碑极好&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;&lt;strong&gt;PhysX&lt;/strong&gt;&lt;/td&gt;
					&lt;td&gt;3D 物理库（NVIDIA）&lt;/td&gt;
					&lt;td&gt;Unity/UE 底层，支持 GPU 加速&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;&lt;strong&gt;Cocos Creator&lt;/strong&gt;&lt;/td&gt;
					&lt;td&gt;内置 2D（Box2D/builtin）、3D 可选 PhysX/cannon.js&lt;/td&gt;
					&lt;td&gt;国内小游戏主流&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;&lt;strong&gt;LayaAir&lt;/strong&gt;&lt;/td&gt;
					&lt;td&gt;集成 Bullet（编译为 wasm）&lt;/td&gt;
					&lt;td&gt;国内 3D H5 主流&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;&lt;strong&gt;Rapier&lt;/strong&gt;&lt;/td&gt;
					&lt;td&gt;Rust 编写，可编译 wasm&lt;/td&gt;
					&lt;td&gt;Web/独立游戏新势力，确定性模拟支持好&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;&lt;strong&gt;MuJoCo / PyBullet&lt;/strong&gt;&lt;/td&gt;
					&lt;td&gt;科研仿真&lt;/td&gt;
					&lt;td&gt;机器人、强化学习训练场景&lt;/td&gt;
			&lt;/tr&gt;
	&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;选型直觉：2D 游戏默认 Box2D 系；3D 商用项目跟着引擎走（Unity 用 PhysX、UE 用 Chaos）；网页端想要确定性（帧同步）看 Rapier；做机器人/RL 仿真看 MuJoCo。&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&#34;十五学习路径建议&#34;&gt;十五、学习路径建议&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;第一周&lt;/strong&gt;：把向量运算（点积、叉积、归一化）练到不用查，手写一个 2D 抛体模拟（重力 + 半隐式欧拉 + 地面反弹）；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;第二周&lt;/strong&gt;：给抛体加空气阻力和风，做「炮弹打移动目标」的提前量解算；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;第三周&lt;/strong&gt;：实现 2D 刚体（加旋转和转动惯量），偏心受力看旋转；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;第四周&lt;/strong&gt;：SAT 碰撞检测 + 冲量法碰撞反应 + 摩擦，让方块能撞、能停、能叠；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;之后&lt;/strong&gt;：挑一个载具（汽车最容易出成就感）做手感调优；有 3D 需求再啃四元数和惯量张量；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;贯穿始终&lt;/strong&gt;：每实现一个特性，先想「这一步在哪个引擎里对应什么参数」，原理和工具互相印证。&lt;/li&gt;
&lt;/ol&gt;
&lt;hr&gt;
&lt;h2 id=&#34;结语&#34;&gt;结语&lt;/h2&gt;
&lt;p&gt;《游戏开发物理学》最大的价值不是那些公式，而是一个思维框架：&lt;strong&gt;任何炫酷的物理效果，拆到最后都是「合力 → 积分 → 碰撞 → 冲量」这条流水线的变体&lt;/strong&gt;。飞机和汽车的差别只在于往「合力」里塞了什么力；刚体和粒子的差别只在于有没有旋转那一半。&lt;/p&gt;
&lt;p&gt;先把这条流水线在小项目里跑通，再去用引擎——你会发现所有参数突然都认识了。&lt;/p&gt;
</description>
    </item>
    <item>
      <title>Cocos Creator 2.x 实战笔记：动画、事件、向量、资源与避坑指南</title>
      <link>https://ftmao.com/blog/posts/cocos-creator-guide/</link>
      <pubDate>Sat, 28 Dec 2019 14:00:00 +0800</pubDate>
      <guid>https://ftmao.com/blog/posts/cocos-creator-guide/</guid>
      <description>&lt;h1 id=&#34;cocos-creator-2x-实战笔记动画事件向量资源与避坑指南&#34;&gt;Cocos Creator 2.x 实战笔记：动画、事件、向量、资源与避坑指南&lt;/h1&gt;
&lt;blockquote&gt;
&lt;p&gt;本文基于 Cocos Creator 2.x（&lt;code&gt;cc.*&lt;/code&gt; API）整理，覆盖从动作动画、输入事件、位置向量、场景管理、音效、资源加载到摄像机跟随等高频知识点。虽然 3.x 已全面转向 TypeScript 与新的组件系统，但 2.x 的核心思想——&lt;strong&gt;节点树 + 组件 + 生命周期 + Action 系统&lt;/strong&gt;——仍然是理解引擎设计的最佳入口。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr&gt;
&lt;h2 id=&#34;一高层次思想cocos-creator-在组织什么&#34;&gt;一、高层次思想：Cocos Creator 在组织什么？&lt;/h2&gt;
&lt;p&gt;Cocos Creator 的核心理念可以概括为一句话：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;“节点（Node）负责层级与变换，组件（Component）负责行为与表现。”&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;pre tabindex=&#34;0&#34;&gt;&lt;code class=&#34;language-mermaid&#34; data-lang=&#34;mermaid&#34;&gt;graph TD
    A[Scene 场景] --&amp;gt; B[Canvas]
    B --&amp;gt; C[Node 节点]
    C --&amp;gt; D[Transform 变换]
    C --&amp;gt; E[Sprite 渲染]
    C --&amp;gt; F[Script 脚本]
    C --&amp;gt; G[AudioSource 音效]
    C --&amp;gt; H[PhysicsCollider 物理]
    D --&amp;gt; I[position / rotation / scale]
    F --&amp;gt; J[onLoad / start / update]&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;&lt;h3 id=&#34;11-节点树--变换层级&#34;&gt;1.1 节点树 = 变换层级&lt;/h3&gt;
&lt;p&gt;每个 &lt;code&gt;cc.Node&lt;/code&gt; 都有父节点，子节点的变换会继承父节点。理解这一点才能避免“为什么我改了坐标没效果？”这类问题。&lt;/p&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;pre tabindex=&#34;0&#34;&gt;&lt;code&gt;World Space（世界坐标系）
   │
   ├── Canvas (Design Resolution: 720x1280)
   │      ├── Player (position: 0, -400)
   │      │      ├── Head (position: 0, 80)    ← 子节点相对父节点
   │      │      └── Gun  (position: 60, 0)
   │      └── Enemy&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;&lt;h3 id=&#34;12-组件生命周期&#34;&gt;1.2 组件生命周期&lt;/h3&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;pre tabindex=&#34;0&#34;&gt;&lt;code class=&#34;language-mermaid&#34; data-lang=&#34;mermaid&#34;&gt;stateDiagram-v2
    [*] --&amp;gt; onLoad: 场景初始化
    onLoad --&amp;gt; onEnable: 节点激活
    onEnable --&amp;gt; start: 第一次 update 前
    start --&amp;gt; update: 每帧调用
    update --&amp;gt; lateUpdate: update 之后
    lateUpdate --&amp;gt; update: 下一帧
    update --&amp;gt; onDisable: 节点禁用
    onDisable --&amp;gt; onEnable: 重新激活
    onDisable --&amp;gt; onDestroy: 销毁节点&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;避坑指南 #1&lt;/strong&gt;：&lt;code&gt;onLoad&lt;/code&gt; 中适合获取节点引用和初始化数据；&lt;code&gt;start&lt;/code&gt; 中适合访问其他组件（此时场景中所有节点的 &lt;code&gt;onLoad&lt;/code&gt; 都已执行完毕）。不要把耗时的初始化放在 &lt;code&gt;update&lt;/code&gt; 里。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr&gt;
&lt;h2 id=&#34;二动作系统action让节点动起来&#34;&gt;二、动作系统（Action）：让节点“动起来”&lt;/h2&gt;
&lt;p&gt;Action 是 Cocos Creator 2.x 中非常优雅的动画封装。它基于&lt;strong&gt;组合模式&lt;/strong&gt;：简单动作可以拼接成复杂序列。&lt;/p&gt;
&lt;h3 id=&#34;21-跳跃动画位移--缓动曲线&#34;&gt;2.1 跳跃动画：位移 + 缓动曲线&lt;/h3&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-javascript&#34; data-lang=&#34;javascript&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#75715e&#34;&gt;// 跳跃上升
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;var&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;jumpUp&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;cc&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;moveBy&lt;/span&gt;(&lt;span style=&#34;color:#66d9ef&#34;&gt;this&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;jumpDuration&lt;/span&gt;, &lt;span style=&#34;color:#a6e22e&#34;&gt;cc&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;v2&lt;/span&gt;(&lt;span style=&#34;color:#ae81ff&#34;&gt;0&lt;/span&gt;, &lt;span style=&#34;color:#66d9ef&#34;&gt;this&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;jumpHeight&lt;/span&gt;))
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    .&lt;span style=&#34;color:#a6e22e&#34;&gt;easing&lt;/span&gt;(&lt;span style=&#34;color:#a6e22e&#34;&gt;cc&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;easeCubicActionOut&lt;/span&gt;());
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#75715e&#34;&gt;// 下落
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;var&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;jumpDown&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;cc&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;moveBy&lt;/span&gt;(&lt;span style=&#34;color:#66d9ef&#34;&gt;this&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;jumpDuration&lt;/span&gt;, &lt;span style=&#34;color:#a6e22e&#34;&gt;cc&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;v2&lt;/span&gt;(&lt;span style=&#34;color:#ae81ff&#34;&gt;0&lt;/span&gt;, &lt;span style=&#34;color:#f92672&#34;&gt;-&lt;/span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;this&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;jumpHeight&lt;/span&gt;))
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    .&lt;span style=&#34;color:#a6e22e&#34;&gt;easing&lt;/span&gt;(&lt;span style=&#34;color:#a6e22e&#34;&gt;cc&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;easeCubicActionIn&lt;/span&gt;());
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#75715e&#34;&gt;// 不断重复
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;return&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;cc&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;repeatForever&lt;/span&gt;(&lt;span style=&#34;color:#a6e22e&#34;&gt;cc&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;sequence&lt;/span&gt;(&lt;span style=&#34;color:#a6e22e&#34;&gt;jumpUp&lt;/span&gt;, &lt;span style=&#34;color:#a6e22e&#34;&gt;jumpDown&lt;/span&gt;));&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
  &lt;/div&gt;&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;pre tabindex=&#34;0&#34;&gt;&lt;code class=&#34;language-mermaid&#34; data-lang=&#34;mermaid&#34;&gt;xychart-beta
    title &amp;#34;跳跃高度随时间变化&amp;#34;
    x-axis [0, 0.25, 0.5, 0.75, 1.0]
    y-axis &amp;#34;高度&amp;#34; 0 --&amp;gt; 100
    line [0, 80, 100, 80, 0]&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;避坑指南 #2&lt;/strong&gt;：&lt;code&gt;moveBy&lt;/code&gt; 是&lt;strong&gt;相对移动&lt;/strong&gt;，&lt;code&gt;moveTo&lt;/code&gt; 是&lt;strong&gt;绝对移动&lt;/strong&gt;。跳跃一般用 &lt;code&gt;moveBy&lt;/code&gt;，因为落地位置取决于起跳点。如果误用 &lt;code&gt;moveTo&lt;/code&gt;，角色会跳到固定世界坐标。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3 id=&#34;22-缩放与回调序列动作&#34;&gt;2.2 缩放与回调：序列动作&lt;/h3&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-javascript&#34; data-lang=&#34;javascript&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#a6e22e&#34;&gt;cc&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;sequence&lt;/span&gt;(
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#a6e22e&#34;&gt;cc&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;scaleTo&lt;/span&gt;(&lt;span style=&#34;color:#a6e22e&#34;&gt;dua&lt;/span&gt;, &lt;span style=&#34;color:#ae81ff&#34;&gt;0.8&lt;/span&gt;, &lt;span style=&#34;color:#ae81ff&#34;&gt;0.8&lt;/span&gt;),
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#a6e22e&#34;&gt;cc&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;scaleTo&lt;/span&gt;(&lt;span style=&#34;color:#a6e22e&#34;&gt;dua&lt;/span&gt;, &lt;span style=&#34;color:#ae81ff&#34;&gt;1&lt;/span&gt;, &lt;span style=&#34;color:#ae81ff&#34;&gt;1&lt;/span&gt;),
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#a6e22e&#34;&gt;cc&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;callFunc&lt;/span&gt;(() =&amp;gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#a6e22e&#34;&gt;paopaoNode&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;destroy&lt;/span&gt;();
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    })
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;);&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
  &lt;/div&gt;&lt;h3 id=&#34;23-旋转&#34;&gt;2.3 旋转&lt;/h3&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-javascript&#34; data-lang=&#34;javascript&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;var&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;rotationTo&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;cc&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;rotateTo&lt;/span&gt;(&lt;span style=&#34;color:#ae81ff&#34;&gt;1&lt;/span&gt;, &lt;span style=&#34;color:#ae81ff&#34;&gt;0&lt;/span&gt;, &lt;span style=&#34;color:#ae81ff&#34;&gt;180&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;this&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;paiNode&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;runAction&lt;/span&gt;(&lt;span style=&#34;color:#a6e22e&#34;&gt;rotationTo&lt;/span&gt;);&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
  &lt;/div&gt;&lt;h3 id=&#34;24-左右晃动以锚点为原点&#34;&gt;2.4 左右晃动（以锚点为原点）&lt;/h3&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-javascript&#34; data-lang=&#34;javascript&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;var&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;seq&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;cc&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;sequence&lt;/span&gt;(
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#a6e22e&#34;&gt;cc&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;rotateTo&lt;/span&gt;(&lt;span style=&#34;color:#ae81ff&#34;&gt;0.2&lt;/span&gt;, &lt;span style=&#34;color:#ae81ff&#34;&gt;15&lt;/span&gt;),
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#a6e22e&#34;&gt;cc&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;rotateTo&lt;/span&gt;(&lt;span style=&#34;color:#ae81ff&#34;&gt;0.4&lt;/span&gt;, &lt;span style=&#34;color:#f92672&#34;&gt;-&lt;/span&gt;&lt;span style=&#34;color:#ae81ff&#34;&gt;15&lt;/span&gt;),
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#a6e22e&#34;&gt;cc&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;rotateTo&lt;/span&gt;(&lt;span style=&#34;color:#ae81ff&#34;&gt;0.4&lt;/span&gt;, &lt;span style=&#34;color:#ae81ff&#34;&gt;15&lt;/span&gt;),
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#a6e22e&#34;&gt;cc&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;rotateTo&lt;/span&gt;(&lt;span style=&#34;color:#ae81ff&#34;&gt;0.4&lt;/span&gt;, &lt;span style=&#34;color:#f92672&#34;&gt;-&lt;/span&gt;&lt;span style=&#34;color:#ae81ff&#34;&gt;15&lt;/span&gt;),
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#a6e22e&#34;&gt;cc&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;rotateTo&lt;/span&gt;(&lt;span style=&#34;color:#ae81ff&#34;&gt;0.4&lt;/span&gt;, &lt;span style=&#34;color:#ae81ff&#34;&gt;15&lt;/span&gt;),
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#a6e22e&#34;&gt;cc&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;rotateTo&lt;/span&gt;(&lt;span style=&#34;color:#ae81ff&#34;&gt;0.2&lt;/span&gt;, &lt;span style=&#34;color:#ae81ff&#34;&gt;0&lt;/span&gt;),
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#a6e22e&#34;&gt;cc&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;delayTime&lt;/span&gt;(&lt;span style=&#34;color:#ae81ff&#34;&gt;2&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;this&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;node&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;runAction&lt;/span&gt;(&lt;span style=&#34;color:#a6e22e&#34;&gt;cc&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;repeatForever&lt;/span&gt;(&lt;span style=&#34;color:#a6e22e&#34;&gt;seq&lt;/span&gt;));&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
  &lt;/div&gt;&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;pre tabindex=&#34;0&#34;&gt;&lt;code class=&#34;language-mermaid&#34; data-lang=&#34;mermaid&#34;&gt;graph LR
    A[初始 0°] --&amp;gt; B[右偏 +15°]
    B --&amp;gt; C[左偏 -15°]
    C --&amp;gt; D[右偏 +15°]
    D --&amp;gt; E[左偏 -15°]
    E --&amp;gt; F[回正 0°]
    F --&amp;gt; G[停顿 2s]
    G --&amp;gt; A&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;避坑指南 #3&lt;/strong&gt;：旋转方向遵循引擎约定，正数为&lt;strong&gt;顺时针&lt;/strong&gt;，负数为&lt;strong&gt;逆时针&lt;/strong&gt;。&lt;code&gt;rotateTo&lt;/code&gt; 会尽量选择最短路径到达目标角度；如果需要连续多圈旋转，请用 &lt;code&gt;rotateBy&lt;/code&gt;。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3 id=&#34;25-点击时按钮放大&#34;&gt;2.5 点击时按钮放大&lt;/h3&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-javascript&#34; data-lang=&#34;javascript&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#75715e&#34;&gt;// 在按钮的触摸开始/结束时分别放大与恢复
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#a6e22e&#34;&gt;buttonNode&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;on&lt;/span&gt;(&lt;span style=&#34;color:#a6e22e&#34;&gt;cc&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;Node&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;EventType&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;TOUCH_START&lt;/span&gt;, () =&amp;gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#a6e22e&#34;&gt;buttonNode&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;runAction&lt;/span&gt;(&lt;span style=&#34;color:#a6e22e&#34;&gt;cc&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;scaleTo&lt;/span&gt;(&lt;span style=&#34;color:#ae81ff&#34;&gt;0.1&lt;/span&gt;, &lt;span style=&#34;color:#ae81ff&#34;&gt;1.1&lt;/span&gt;, &lt;span style=&#34;color:#ae81ff&#34;&gt;1.1&lt;/span&gt;));
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;});
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#a6e22e&#34;&gt;buttonNode&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;on&lt;/span&gt;(&lt;span style=&#34;color:#a6e22e&#34;&gt;cc&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;Node&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;EventType&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;TOUCH_END&lt;/span&gt;, () =&amp;gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#a6e22e&#34;&gt;buttonNode&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;runAction&lt;/span&gt;(&lt;span style=&#34;color:#a6e22e&#34;&gt;cc&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;scaleTo&lt;/span&gt;(&lt;span style=&#34;color:#ae81ff&#34;&gt;0.1&lt;/span&gt;, &lt;span style=&#34;color:#ae81ff&#34;&gt;1.0&lt;/span&gt;, &lt;span style=&#34;color:#ae81ff&#34;&gt;1.0&lt;/span&gt;));
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;});&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
  &lt;/div&gt;&lt;hr&gt;
&lt;h2 id=&#34;三输入事件键盘触摸与鼠标&#34;&gt;三、输入事件：键盘、触摸与鼠标&lt;/h2&gt;
&lt;h3 id=&#34;31-键盘事件&#34;&gt;3.1 键盘事件&lt;/h3&gt;
&lt;p&gt;注册：&lt;/p&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-javascript&#34; data-lang=&#34;javascript&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#a6e22e&#34;&gt;cc&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;systemEvent&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;on&lt;/span&gt;(&lt;span style=&#34;color:#a6e22e&#34;&gt;cc&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;SystemEvent&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;EventType&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;KEY_DOWN&lt;/span&gt;, &lt;span style=&#34;color:#66d9ef&#34;&gt;this&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;onKeyDown&lt;/span&gt;, &lt;span style=&#34;color:#66d9ef&#34;&gt;this&lt;/span&gt;);&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
  &lt;/div&gt;&lt;p&gt;实现监听函数：&lt;/p&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-javascript&#34; data-lang=&#34;javascript&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#a6e22e&#34;&gt;onKeyDown&lt;/span&gt;(&lt;span style=&#34;color:#a6e22e&#34;&gt;event&lt;/span&gt;) {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#66d9ef&#34;&gt;switch&lt;/span&gt;(&lt;span style=&#34;color:#a6e22e&#34;&gt;event&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;keyCode&lt;/span&gt;) {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#66d9ef&#34;&gt;case&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;cc&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;macro&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;KEY&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;a&lt;/span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;:&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            &lt;span style=&#34;color:#66d9ef&#34;&gt;this&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;accLeft&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;true&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            &lt;span style=&#34;color:#66d9ef&#34;&gt;break&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#66d9ef&#34;&gt;case&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;cc&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;macro&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;KEY&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;d&lt;/span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;:&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            &lt;span style=&#34;color:#66d9ef&#34;&gt;this&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;accRight&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;true&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            &lt;span style=&#34;color:#66d9ef&#34;&gt;break&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    }
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
  &lt;/div&gt;&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;避坑指南 #4&lt;/strong&gt;：第三个参数 &lt;code&gt;this&lt;/code&gt; 不要漏！否则回调里的 &lt;code&gt;this&lt;/code&gt; 会指向 &lt;code&gt;cc.systemEvent&lt;/code&gt;，导致 &lt;code&gt;this.accLeft&lt;/code&gt; 为 &lt;code&gt;undefined&lt;/code&gt;。建议在 &lt;code&gt;onDestroy&lt;/code&gt; 中移除监听，避免场景切换后事件泄漏。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3 id=&#34;32-触摸--鼠标事件&#34;&gt;3.2 触摸 / 鼠标事件&lt;/h3&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-javascript&#34; data-lang=&#34;javascript&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;this&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;node&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;on&lt;/span&gt;(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#39;touchstart&amp;#39;&lt;/span&gt;, &lt;span style=&#34;color:#66d9ef&#34;&gt;this&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;touchStart&lt;/span&gt;, &lt;span style=&#34;color:#66d9ef&#34;&gt;this&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;this&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;node&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;on&lt;/span&gt;(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#39;touchend&amp;#39;&lt;/span&gt;,   &lt;span style=&#34;color:#66d9ef&#34;&gt;this&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;touchEnd&lt;/span&gt;,   &lt;span style=&#34;color:#66d9ef&#34;&gt;this&lt;/span&gt;);&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
  &lt;/div&gt;&lt;p&gt;枚举与事件名对照：&lt;/p&gt;
&lt;table&gt;
	&lt;thead&gt;
			&lt;tr&gt;
					&lt;th&gt;枚举&lt;/th&gt;
					&lt;th&gt;字符串事件名&lt;/th&gt;
					&lt;th&gt;说明&lt;/th&gt;
			&lt;/tr&gt;
	&lt;/thead&gt;
	&lt;tbody&gt;
			&lt;tr&gt;
					&lt;td&gt;&lt;code&gt;cc.Node.EventType.TOUCH_START&lt;/code&gt;&lt;/td&gt;
					&lt;td&gt;&lt;code&gt;touchstart&lt;/code&gt;&lt;/td&gt;
					&lt;td&gt;手指按下&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;&lt;code&gt;cc.Node.EventType.TOUCH_MOVE&lt;/code&gt;&lt;/td&gt;
					&lt;td&gt;&lt;code&gt;touchmove&lt;/code&gt;&lt;/td&gt;
					&lt;td&gt;手指移动&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;&lt;code&gt;cc.Node.EventType.TOUCH_END&lt;/code&gt;&lt;/td&gt;
					&lt;td&gt;&lt;code&gt;touchend&lt;/code&gt;&lt;/td&gt;
					&lt;td&gt;手指抬起（在节点内）&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;&lt;code&gt;cc.Node.EventType.TOUCH_CANCEL&lt;/code&gt;&lt;/td&gt;
					&lt;td&gt;&lt;code&gt;touchcancel&lt;/code&gt;&lt;/td&gt;
					&lt;td&gt;系统中断（如来电）&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;&lt;code&gt;cc.Node.EventType.MOUSE_DOWN&lt;/code&gt;&lt;/td&gt;
					&lt;td&gt;&lt;code&gt;mousedown&lt;/code&gt;&lt;/td&gt;
					&lt;td&gt;鼠标按下&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;&lt;code&gt;cc.Node.EventType.MOUSE_ENTER&lt;/code&gt;&lt;/td&gt;
					&lt;td&gt;&lt;code&gt;mouseenter&lt;/code&gt;&lt;/td&gt;
					&lt;td&gt;鼠标进入节点&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;&lt;code&gt;cc.Node.EventType.MOUSE_MOVE&lt;/code&gt;&lt;/td&gt;
					&lt;td&gt;&lt;code&gt;mousemove&lt;/code&gt;&lt;/td&gt;
					&lt;td&gt;鼠标移动&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;&lt;code&gt;cc.Node.EventType.MOUSE_LEAVE&lt;/code&gt;&lt;/td&gt;
					&lt;td&gt;&lt;code&gt;mouseleave&lt;/code&gt;&lt;/td&gt;
					&lt;td&gt;鼠标离开节点&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;&lt;code&gt;cc.Node.EventType.MOUSE_UP&lt;/code&gt;&lt;/td&gt;
					&lt;td&gt;&lt;code&gt;mouseup&lt;/code&gt;&lt;/td&gt;
					&lt;td&gt;鼠标抬起&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;&lt;code&gt;cc.Node.EventType.MOUSE_WHEEL&lt;/code&gt;&lt;/td&gt;
					&lt;td&gt;&lt;code&gt;mousewheel&lt;/code&gt;&lt;/td&gt;
					&lt;td&gt;滚轮滚动&lt;/td&gt;
			&lt;/tr&gt;
	&lt;/tbody&gt;
&lt;/table&gt;
&lt;hr&gt;
&lt;h2 id=&#34;四位置向量-vec22d-游戏的数学基础&#34;&gt;四、位置向量 Vec2：2D 游戏的数学基础&lt;/h2&gt;
&lt;h3 id=&#34;41-生成与设置位置&#34;&gt;4.1 生成与设置位置&lt;/h3&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-javascript&#34; data-lang=&#34;javascript&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;var&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;pos&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;cc&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;v2&lt;/span&gt;(&lt;span style=&#34;color:#a6e22e&#34;&gt;randX&lt;/span&gt;, &lt;span style=&#34;color:#a6e22e&#34;&gt;randY&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#a6e22e&#34;&gt;newStar&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;setPosition&lt;/span&gt;(&lt;span style=&#34;color:#a6e22e&#34;&gt;pos&lt;/span&gt;);&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
  &lt;/div&gt;&lt;h3 id=&#34;42-距离长度与运算&#34;&gt;4.2 距离、长度与运算&lt;/h3&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-javascript&#34; data-lang=&#34;javascript&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#75715e&#34;&gt;// 计算当前节点与玩家之间的距离
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;var&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;dist&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;this&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;node&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;position&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;sub&lt;/span&gt;(&lt;span style=&#34;color:#a6e22e&#34;&gt;playerPos&lt;/span&gt;).&lt;span style=&#34;color:#a6e22e&#34;&gt;mag&lt;/span&gt;();
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#75715e&#34;&gt;// 返回向量长度
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;var&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;len&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;dir&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;mag&lt;/span&gt;();
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#75715e&#34;&gt;// 自加（修改原向量）
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#a6e22e&#34;&gt;v&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;addSelf&lt;/span&gt;(&lt;span style=&#34;color:#a6e22e&#34;&gt;cc&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;v2&lt;/span&gt;(&lt;span style=&#34;color:#ae81ff&#34;&gt;5&lt;/span&gt;, &lt;span style=&#34;color:#ae81ff&#34;&gt;5&lt;/span&gt;));
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#75715e&#34;&gt;// 返回新向量（不修改原向量）
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;var&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;vnew&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;v&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;add&lt;/span&gt;(&lt;span style=&#34;color:#a6e22e&#34;&gt;cc&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;v2&lt;/span&gt;(&lt;span style=&#34;color:#ae81ff&#34;&gt;5&lt;/span&gt;, &lt;span style=&#34;color:#ae81ff&#34;&gt;5&lt;/span&gt;));
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#75715e&#34;&gt;// 缩放
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;var&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;v&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;cc&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;v2&lt;/span&gt;(&lt;span style=&#34;color:#ae81ff&#34;&gt;10&lt;/span&gt;, &lt;span style=&#34;color:#ae81ff&#34;&gt;10&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#a6e22e&#34;&gt;v&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;mul&lt;/span&gt;(&lt;span style=&#34;color:#ae81ff&#34;&gt;5&lt;/span&gt;);   &lt;span style=&#34;color:#75715e&#34;&gt;// 返回 Vec2 {x: 50, y: 50}
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#a6e22e&#34;&gt;console&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;log&lt;/span&gt;(&lt;span style=&#34;color:#a6e22e&#34;&gt;v&lt;/span&gt;); &lt;span style=&#34;color:#75715e&#34;&gt;// 仍打印 Vec2 {x: 10, y: 10}
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#75715e&#34;&gt;// 判断相等
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;var&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;res&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;v&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;equals&lt;/span&gt;(&lt;span style=&#34;color:#a6e22e&#34;&gt;cc&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;v2&lt;/span&gt;(&lt;span style=&#34;color:#ae81ff&#34;&gt;10&lt;/span&gt;, &lt;span style=&#34;color:#ae81ff&#34;&gt;10&lt;/span&gt;)); &lt;span style=&#34;color:#75715e&#34;&gt;// true
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
  &lt;/div&gt;&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;避坑指南 #5&lt;/strong&gt;：&lt;code&gt;addSelf&lt;/code&gt;、&lt;code&gt;mulSelf&lt;/code&gt;、&lt;code&gt;subSelf&lt;/code&gt; 等方法会&lt;strong&gt;修改原向量&lt;/strong&gt;；不带 &lt;code&gt;Self&lt;/code&gt; 的版本会返回新向量。在 &lt;code&gt;update&lt;/code&gt; 中计算方向时，若不想意外修改 &lt;code&gt;this.node.position&lt;/code&gt;，请使用非 &lt;code&gt;Self&lt;/code&gt; 版本。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3 id=&#34;43-线性插值lerp&#34;&gt;4.3 线性插值（Lerp）&lt;/h3&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-javascript&#34; data-lang=&#34;javascript&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;var&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;v&lt;/span&gt;  &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;cc&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;v2&lt;/span&gt;(&lt;span style=&#34;color:#ae81ff&#34;&gt;10&lt;/span&gt;, &lt;span style=&#34;color:#ae81ff&#34;&gt;10&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;var&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;v1&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;cc&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;v2&lt;/span&gt;(&lt;span style=&#34;color:#ae81ff&#34;&gt;5&lt;/span&gt;, &lt;span style=&#34;color:#ae81ff&#34;&gt;5&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;var&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;v3&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;cc&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;v2&lt;/span&gt;(&lt;span style=&#34;color:#ae81ff&#34;&gt;0&lt;/span&gt;, &lt;span style=&#34;color:#ae81ff&#34;&gt;0&lt;/span&gt;); &lt;span style=&#34;color:#75715e&#34;&gt;// 存储结果
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#a6e22e&#34;&gt;v&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;lerp&lt;/span&gt;(&lt;span style=&#34;color:#a6e22e&#34;&gt;v1&lt;/span&gt;, &lt;span style=&#34;color:#ae81ff&#34;&gt;0.5&lt;/span&gt;, &lt;span style=&#34;color:#a6e22e&#34;&gt;v3&lt;/span&gt;);  &lt;span style=&#34;color:#75715e&#34;&gt;// 在 v 与 v1 之间取 50%
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#a6e22e&#34;&gt;console&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;log&lt;/span&gt;(&lt;span style=&#34;color:#a6e22e&#34;&gt;v3&lt;/span&gt;);      &lt;span style=&#34;color:#75715e&#34;&gt;// Vec2 {x: 7.5, y: 7.5}
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
  &lt;/div&gt;&lt;h3 id=&#34;44-坐标转换&#34;&gt;4.4 坐标转换&lt;/h3&gt;
&lt;table&gt;
	&lt;thead&gt;
			&lt;tr&gt;
					&lt;th&gt;方法&lt;/th&gt;
					&lt;th&gt;原点&lt;/th&gt;
					&lt;th&gt;用途&lt;/th&gt;
			&lt;/tr&gt;
	&lt;/thead&gt;
	&lt;tbody&gt;
			&lt;tr&gt;
					&lt;td&gt;&lt;code&gt;convertToWorldSpaceAR(point)&lt;/code&gt;&lt;/td&gt;
					&lt;td&gt;节点锚点&lt;/td&gt;
					&lt;td&gt;局部坐标 → 世界坐标&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;&lt;code&gt;convertToNodeSpaceAR(point)&lt;/code&gt;&lt;/td&gt;
					&lt;td&gt;节点锚点&lt;/td&gt;
					&lt;td&gt;世界坐标 → 局部坐标&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;&lt;code&gt;convertToWorldSpace(point)&lt;/code&gt;&lt;/td&gt;
					&lt;td&gt;节点左下角&lt;/td&gt;
					&lt;td&gt;不建议，除非 UI 特殊需求&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;&lt;code&gt;convertToNodeSpace(point)&lt;/code&gt;&lt;/td&gt;
					&lt;td&gt;节点左下角&lt;/td&gt;
					&lt;td&gt;不建议&lt;/td&gt;
			&lt;/tr&gt;
	&lt;/tbody&gt;
&lt;/table&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;pre tabindex=&#34;0&#34;&gt;&lt;code class=&#34;language-mermaid&#34; data-lang=&#34;mermaid&#34;&gt;graph LR
    subgraph 世界坐标系
        direction TB
        A[World Origin] --&amp;gt; B[Player]
    end
    subgraph 局部坐标系
        direction TB
        C[Anchor Point] --&amp;gt; D[(100, 100)]
    end
    B --&amp;gt;|convertToWorldSpaceAR| D
    D --&amp;gt;|convertToNodeSpaceAR| B&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;避坑指南 #6&lt;/strong&gt;：Cocos 编辑器默认以&lt;strong&gt;锚点（Anchor）&lt;strong&gt;为原点，因此&lt;/strong&gt;优先使用带 AR 的方法&lt;/strong&gt;。左下角为原点的方法主要用于兼容旧项目或特殊 UI 对齐。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr&gt;
&lt;h2 id=&#34;五场景与生命周期&#34;&gt;五、场景与生命周期&lt;/h2&gt;
&lt;h3 id=&#34;51-重新加载场景&#34;&gt;5.1 重新加载场景&lt;/h3&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-javascript&#34; data-lang=&#34;javascript&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#a6e22e&#34;&gt;cc&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;director&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;loadScene&lt;/span&gt;(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#39;game&amp;#39;&lt;/span&gt;);&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
  &lt;/div&gt;&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;避坑指南 #7&lt;/strong&gt;：&lt;code&gt;loadScene&lt;/code&gt; 会销毁当前场景所有节点。如果需要在切换前保存数据（如分数、关卡进度），请先写入本地存储，再调用加载。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3 id=&#34;52-暂停与恢复&#34;&gt;5.2 暂停与恢复&lt;/h3&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-javascript&#34; data-lang=&#34;javascript&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#75715e&#34;&gt;// 暂停游戏逻辑，但渲染和 UI 响应仍在
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#a6e22e&#34;&gt;cc&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;director&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;pause&lt;/span&gt;();
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#75715e&#34;&gt;// 恢复
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#a6e22e&#34;&gt;cc&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;director&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;resume&lt;/span&gt;();&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
  &lt;/div&gt;&lt;p&gt;常用于弹出结算面板或暂停菜单。&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&#34;六透明度音效与组件&#34;&gt;六、透明度、音效与组件&lt;/h2&gt;
&lt;h3 id=&#34;61-透明度渐变实现缓慢消失&#34;&gt;6.1 透明度渐变（实现缓慢消失）&lt;/h3&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-javascript&#34; data-lang=&#34;javascript&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#a6e22e&#34;&gt;update&lt;/span&gt;(&lt;span style=&#34;color:#a6e22e&#34;&gt;dt&lt;/span&gt;) {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#66d9ef&#34;&gt;this&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;node&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;opacity&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;-=&lt;/span&gt; &lt;span style=&#34;color:#ae81ff&#34;&gt;100&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;*&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;dt&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#66d9ef&#34;&gt;if&lt;/span&gt; (&lt;span style=&#34;color:#66d9ef&#34;&gt;this&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;node&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;opacity&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;&amp;lt;=&lt;/span&gt; &lt;span style=&#34;color:#ae81ff&#34;&gt;0&lt;/span&gt;) {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#66d9ef&#34;&gt;this&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;node&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;destroy&lt;/span&gt;();
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    }
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
  &lt;/div&gt;&lt;h3 id=&#34;62-音效&#34;&gt;6.2 音效&lt;/h3&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-javascript&#34; data-lang=&#34;javascript&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#a6e22e&#34;&gt;properties&lt;/span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;:&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#a6e22e&#34;&gt;scoreAudio&lt;/span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;:&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#66d9ef&#34;&gt;default&lt;/span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;:&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;null&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#a6e22e&#34;&gt;type&lt;/span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;:&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;cc&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;AudioClip&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    }
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#75715e&#34;&gt;// 播放音效（false 表示不循环）
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#a6e22e&#34;&gt;cc&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;audioEngine&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;playEffect&lt;/span&gt;(&lt;span style=&#34;color:#66d9ef&#34;&gt;this&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;scoreAudio&lt;/span&gt;, &lt;span style=&#34;color:#66d9ef&#34;&gt;false&lt;/span&gt;);&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
  &lt;/div&gt;&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;避坑指南 #8&lt;/strong&gt;：背景音乐请用 &lt;code&gt;playMusic&lt;/code&gt;，音效用 &lt;code&gt;playEffect&lt;/code&gt;。微信小游戏对同时播放的音频数量有限制，过量的 &lt;code&gt;playEffect&lt;/code&gt; 可能导致后续音效被丢弃。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3 id=&#34;63-getcomponent获取组件&#34;&gt;6.3 getComponent：获取组件&lt;/h3&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-javascript&#34; data-lang=&#34;javascript&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#a6e22e&#34;&gt;newStar&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;getComponent&lt;/span&gt;(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#39;Star&amp;#39;&lt;/span&gt;).&lt;span style=&#34;color:#a6e22e&#34;&gt;game&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;this&lt;/span&gt;;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
  &lt;/div&gt;&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;避坑指南 #9&lt;/strong&gt;：&lt;code&gt;getComponent&lt;/code&gt; 是 &lt;code&gt;Node&lt;/code&gt; 的方法，JS 中的方法和属性都挂在组件上。不要把脚本逻辑直接写在场景或节点上，而要写在组件脚本里。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr&gt;
&lt;h2 id=&#34;七屏幕包围盒与资源加载&#34;&gt;七、屏幕、包围盒与资源加载&lt;/h2&gt;
&lt;h3 id=&#34;71-获取视图大小&#34;&gt;7.1 获取视图大小&lt;/h3&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-javascript&#34; data-lang=&#34;javascript&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;var&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;winSize&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;cc&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;director&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;getWinSize&lt;/span&gt;();&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
  &lt;/div&gt;&lt;h3 id=&#34;72-包围盒&#34;&gt;7.2 包围盒&lt;/h3&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-javascript&#34; data-lang=&#34;javascript&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;var&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;first_xMax&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;bgList&lt;/span&gt;[&lt;span style=&#34;color:#ae81ff&#34;&gt;0&lt;/span&gt;].&lt;span style=&#34;color:#a6e22e&#34;&gt;getBoundingBox&lt;/span&gt;().&lt;span style=&#34;color:#a6e22e&#34;&gt;xMax&lt;/span&gt;;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
  &lt;/div&gt;&lt;p&gt;&lt;code&gt;getBoundingBox()&lt;/code&gt; 返回&lt;strong&gt;父节坐标系&lt;/strong&gt;下的轴向对齐包围盒（AABB），包含 &lt;code&gt;xMin&lt;/code&gt;、&lt;code&gt;xMax&lt;/code&gt;、&lt;code&gt;yMin&lt;/code&gt;、&lt;code&gt;yMax&lt;/code&gt;、&lt;code&gt;width&lt;/code&gt;、&lt;code&gt;height&lt;/code&gt; 等属性。&lt;/p&gt;
&lt;h3 id=&#34;73-加载图片&#34;&gt;7.3 加载图片&lt;/h3&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-javascript&#34; data-lang=&#34;javascript&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#a6e22e&#34;&gt;cc&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;loader&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;load&lt;/span&gt;(
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    { &lt;span style=&#34;color:#a6e22e&#34;&gt;url&lt;/span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;:&lt;/span&gt; &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;res/raw-assets/resource/texture/gold.png&amp;#34;&lt;/span&gt;, &lt;span style=&#34;color:#a6e22e&#34;&gt;type&lt;/span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;:&lt;/span&gt; &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#39;png&amp;#39;&lt;/span&gt; },
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    (&lt;span style=&#34;color:#a6e22e&#34;&gt;err&lt;/span&gt;, &lt;span style=&#34;color:#a6e22e&#34;&gt;texture&lt;/span&gt;) =&amp;gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#66d9ef&#34;&gt;var&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;fra&lt;/span&gt;   &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;this&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;spr2&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;getComponent&lt;/span&gt;(&lt;span style=&#34;color:#a6e22e&#34;&gt;cc&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;Sprite&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#66d9ef&#34;&gt;var&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;sframe&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;new&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;cc&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;SpriteFrame&lt;/span&gt;(
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            &lt;span style=&#34;color:#a6e22e&#34;&gt;texture&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            &lt;span style=&#34;color:#66d9ef&#34;&gt;new&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;cc&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;rect&lt;/span&gt;(&lt;span style=&#34;color:#ae81ff&#34;&gt;20&lt;/span&gt;, &lt;span style=&#34;color:#ae81ff&#34;&gt;20&lt;/span&gt;, &lt;span style=&#34;color:#ae81ff&#34;&gt;100&lt;/span&gt;, &lt;span style=&#34;color:#ae81ff&#34;&gt;120&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        );
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#a6e22e&#34;&gt;fra&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;spriteFrame&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;sframe&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    }
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;);&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
  &lt;/div&gt;&lt;p&gt;&lt;code&gt;cc.rect(20, 20, 100, 120)&lt;/code&gt; 表示从图片的 &lt;code&gt;(20, 20)&lt;/code&gt; 开始，取宽 100、高 120 的矩形区域，即图片中 &lt;code&gt;(20, 20)&lt;/code&gt; 到 &lt;code&gt;(120, 140)&lt;/code&gt; 的内容。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;避坑指南 #10&lt;/strong&gt;：微信小游戏对图片尺寸有限制，&lt;strong&gt;长宽都不能超过 2048&lt;/strong&gt;。超出此限制会导致加载失败或显示异常，导入资源前请先检查尺寸。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3 id=&#34;74-加载-prefab&#34;&gt;7.4 加载 Prefab&lt;/h3&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-javascript&#34; data-lang=&#34;javascript&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#a6e22e&#34;&gt;cc&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;loader&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;loadRes&lt;/span&gt;(&lt;span style=&#34;color:#e6db74&#34;&gt;`prefab/game08/&lt;/span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;${&lt;/span&gt;&lt;span style=&#34;color:#a6e22e&#34;&gt;randIdx&lt;/span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;}&lt;/span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;`&lt;/span&gt;, &lt;span style=&#34;color:#a6e22e&#34;&gt;cc&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;Prefab&lt;/span&gt;, (&lt;span style=&#34;color:#a6e22e&#34;&gt;err&lt;/span&gt;, &lt;span style=&#34;color:#a6e22e&#34;&gt;res&lt;/span&gt;) =&amp;gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#66d9ef&#34;&gt;this&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;drawing&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;cc&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;instantiate&lt;/span&gt;(&lt;span style=&#34;color:#a6e22e&#34;&gt;res&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#66d9ef&#34;&gt;this&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;drawing&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;group&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;drawing&amp;#34;&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;});&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
  &lt;/div&gt;&lt;hr&gt;
&lt;h2 id=&#34;八本地存储&#34;&gt;八、本地存储&lt;/h2&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-javascript&#34; data-lang=&#34;javascript&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#75715e&#34;&gt;// 写入
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#a6e22e&#34;&gt;cc&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;sys&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;localStorage&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;setItem&lt;/span&gt;(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;finishLevel&amp;#34;&lt;/span&gt;, &lt;span style=&#34;color:#66d9ef&#34;&gt;this&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;curLevel&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#75715e&#34;&gt;// 读取（注意转换类型，localStorage 只存字符串）
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;var&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;level&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; parseInt(&lt;span style=&#34;color:#a6e22e&#34;&gt;cc&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;sys&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;localStorage&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;getItem&lt;/span&gt;(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;finishLevel&amp;#34;&lt;/span&gt;) &lt;span style=&#34;color:#f92672&#34;&gt;||&lt;/span&gt; &lt;span style=&#34;color:#ae81ff&#34;&gt;0&lt;/span&gt;);&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
  &lt;/div&gt;&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;避坑指南 #11&lt;/strong&gt;：&lt;code&gt;localStorage&lt;/code&gt; 只能存储字符串。存储对象时请先 &lt;code&gt;JSON.stringify&lt;/code&gt;，读取后再 &lt;code&gt;JSON.parse&lt;/code&gt;。微信小游戏有本地存储上限，重要数据建议结合云端存档。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr&gt;
&lt;h2 id=&#34;九摄像机跟随&#34;&gt;九、摄像机跟随&lt;/h2&gt;
&lt;h3 id=&#34;91-主角简单跟随&#34;&gt;9.1 主角简单跟随&lt;/h3&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-javascript&#34; data-lang=&#34;javascript&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#75715e&#34;&gt;// this.node 为 Camera 节点
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#a6e22e&#34;&gt;update&lt;/span&gt;(&lt;span style=&#34;color:#a6e22e&#34;&gt;dt&lt;/span&gt;) {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#66d9ef&#34;&gt;var&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;targetPos&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;this&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;player&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;position&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#66d9ef&#34;&gt;this&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;node&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;position&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;targetPos&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
  &lt;/div&gt;&lt;h3 id=&#34;92-主角自然跟随平滑插值&#34;&gt;9.2 主角自然跟随（平滑插值）&lt;/h3&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-javascript&#34; data-lang=&#34;javascript&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#a6e22e&#34;&gt;update&lt;/span&gt;(&lt;span style=&#34;color:#a6e22e&#34;&gt;dt&lt;/span&gt;) {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#66d9ef&#34;&gt;var&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;targetPos&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;this&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;player&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;position&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#66d9ef&#34;&gt;var&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;currentPos&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;this&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;node&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;position&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#a6e22e&#34;&gt;currentPos&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;lerp&lt;/span&gt;(&lt;span style=&#34;color:#a6e22e&#34;&gt;targetPos&lt;/span&gt;, &lt;span style=&#34;color:#ae81ff&#34;&gt;0.1&lt;/span&gt;, &lt;span style=&#34;color:#a6e22e&#34;&gt;currentPos&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#66d9ef&#34;&gt;this&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;node&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;position&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;currentPos&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
  &lt;/div&gt;&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;pre tabindex=&#34;0&#34;&gt;&lt;code class=&#34;language-mermaid&#34; data-lang=&#34;mermaid&#34;&gt;graph LR
    A[Camera 当前位置] --&amp;gt;|lerp 0.1| B[Player 当前位置]
    B --&amp;gt; C[下一帧继续插值]
    C --&amp;gt; D[最终重合]&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;&lt;hr&gt;
&lt;h2 id=&#34;十调试绘制与更新策略&#34;&gt;十、调试、绘制与更新策略&lt;/h2&gt;
&lt;h3 id=&#34;101-cclog&#34;&gt;10.1 cc.log&lt;/h3&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-javascript&#34; data-lang=&#34;javascript&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#a6e22e&#34;&gt;cc&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;log&lt;/span&gt;(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;只在发布时勾选调试模式才起效&amp;#34;&lt;/span&gt;);&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
  &lt;/div&gt;&lt;h3 id=&#34;102-graphics-画图&#34;&gt;10.2 Graphics 画图&lt;/h3&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-javascript&#34; data-lang=&#34;javascript&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;var&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;g&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;this&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;node&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;getComponent&lt;/span&gt;(&lt;span style=&#34;color:#a6e22e&#34;&gt;cc&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;Graphics&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#a6e22e&#34;&gt;g&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;moveTo&lt;/span&gt;(&lt;span style=&#34;color:#ae81ff&#34;&gt;0&lt;/span&gt;, &lt;span style=&#34;color:#ae81ff&#34;&gt;0&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#a6e22e&#34;&gt;g&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;lineTo&lt;/span&gt;(&lt;span style=&#34;color:#ae81ff&#34;&gt;100&lt;/span&gt;, &lt;span style=&#34;color:#ae81ff&#34;&gt;100&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#a6e22e&#34;&gt;g&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;stroke&lt;/span&gt;();&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
  &lt;/div&gt;&lt;h3 id=&#34;103-打包更新&#34;&gt;10.3 打包更新&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;发布时勾选 &lt;strong&gt;MD5 Cache&lt;/strong&gt;，避免浏览器/微信缓存旧资源。&lt;/li&gt;
&lt;li&gt;热更新需配合 &lt;code&gt;cc.assetManager.loadBundle&lt;/code&gt; 或官方热更新方案。&lt;/li&gt;
&lt;/ul&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-javascript&#34; data-lang=&#34;javascript&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#a6e22e&#34;&gt;cc&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;assetManager&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;loadBundle&lt;/span&gt;(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#39;level2&amp;#39;&lt;/span&gt;, (&lt;span style=&#34;color:#a6e22e&#34;&gt;err&lt;/span&gt;, &lt;span style=&#34;color:#a6e22e&#34;&gt;bundle&lt;/span&gt;) =&amp;gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#a6e22e&#34;&gt;bundle&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;loadDir&lt;/span&gt;(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#39;prefabs&amp;#39;&lt;/span&gt;, (&lt;span style=&#34;color:#a6e22e&#34;&gt;err&lt;/span&gt;, &lt;span style=&#34;color:#a6e22e&#34;&gt;assets&lt;/span&gt;) =&amp;gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#75715e&#34;&gt;// ...
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    });
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;});&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
  &lt;/div&gt;&lt;hr&gt;
&lt;h2 id=&#34;十一物理关节旋转关节-revolutejoint&#34;&gt;十一、物理关节：旋转关节 RevoluteJoint&lt;/h2&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-javascript&#34; data-lang=&#34;javascript&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#75715e&#34;&gt;// cc.RevoluteJoint 可约束两个刚体围绕一个点旋转
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;var&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;joint&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;nodeA&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;addComponent&lt;/span&gt;(&lt;span style=&#34;color:#a6e22e&#34;&gt;cc&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;RevoluteJoint&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#a6e22e&#34;&gt;joint&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;connectedBody&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;nodeB&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;getComponent&lt;/span&gt;(&lt;span style=&#34;color:#a6e22e&#34;&gt;cc&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;RigidBody&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#a6e22e&#34;&gt;joint&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;anchor&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;cc&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;v2&lt;/span&gt;(&lt;span style=&#34;color:#ae81ff&#34;&gt;0&lt;/span&gt;, &lt;span style=&#34;color:#ae81ff&#34;&gt;0&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#a6e22e&#34;&gt;joint&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;connectedAnchor&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;cc&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;v2&lt;/span&gt;(&lt;span style=&#34;color:#ae81ff&#34;&gt;0&lt;/span&gt;, &lt;span style=&#34;color:#ae81ff&#34;&gt;50&lt;/span&gt;);&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
  &lt;/div&gt;&lt;p&gt;常用于门铰链、钟摆、车轮等效果。&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&#34;十二帧率与屏幕适配&#34;&gt;十二、帧率与屏幕适配&lt;/h2&gt;
&lt;h3 id=&#34;121-锁定帧率&#34;&gt;12.1 锁定帧率&lt;/h3&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-javascript&#34; data-lang=&#34;javascript&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;let&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;fps&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; (&lt;span style=&#34;color:#a6e22e&#34;&gt;cc&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;sys&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;os&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;==&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;cc&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;sys&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;OS_IOS&lt;/span&gt;) &lt;span style=&#34;color:#f92672&#34;&gt;?&lt;/span&gt; &lt;span style=&#34;color:#ae81ff&#34;&gt;40&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;:&lt;/span&gt; &lt;span style=&#34;color:#ae81ff&#34;&gt;40&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#a6e22e&#34;&gt;cc&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;game&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;setFrameRate&lt;/span&gt;(&lt;span style=&#34;color:#a6e22e&#34;&gt;fps&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;if&lt;/span&gt; (&lt;span style=&#34;color:#a6e22e&#34;&gt;cc&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;sys&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;platform&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;==&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;cc&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;sys&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;WECHAT_GAME&lt;/span&gt;) {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#a6e22e&#34;&gt;wx&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;setPreferredFramesPerSecond&lt;/span&gt;(&lt;span style=&#34;color:#a6e22e&#34;&gt;fps&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
  &lt;/div&gt;&lt;h3 id=&#34;122-分辨率适配&#34;&gt;12.2 分辨率适配&lt;/h3&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-javascript&#34; data-lang=&#34;javascript&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;let&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;frameSize&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;cc&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;view&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;getFrameSize&lt;/span&gt;();
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;let&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;scrall&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;frameSize&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;width&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;/&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;frameSize&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;height&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#a6e22e&#34;&gt;cc&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;view&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;setResolutionPolicy&lt;/span&gt;(&lt;span style=&#34;color:#a6e22e&#34;&gt;cc&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;ResolutionPolicy&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;EXACT_FIT&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#a6e22e&#34;&gt;cc&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;view&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;setOrientation&lt;/span&gt;(&lt;span style=&#34;color:#a6e22e&#34;&gt;cc&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;macro&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;ORIENTATION_LANDSCAPE&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#a6e22e&#34;&gt;cc&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;Canvas&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;instance&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;fitWidth&lt;/span&gt;  &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;false&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#a6e22e&#34;&gt;cc&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;Canvas&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;instance&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;fitHeight&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;false&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;let&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;isFitWidth&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;scrall&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;&amp;lt;&lt;/span&gt; &lt;span style=&#34;color:#ae81ff&#34;&gt;1.77&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#a6e22e&#34;&gt;cc&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;Canvas&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;instance&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;fitWidth&lt;/span&gt;  &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;isFitWidth&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#a6e22e&#34;&gt;cc&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;Canvas&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;instance&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;fitHeight&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;!&lt;/span&gt;&lt;span style=&#34;color:#a6e22e&#34;&gt;isFitWidth&lt;/span&gt;;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
  &lt;/div&gt;&lt;h3 id=&#34;123-常用设计分辨率&#34;&gt;12.3 常用设计分辨率&lt;/h3&gt;
&lt;table&gt;
	&lt;thead&gt;
			&lt;tr&gt;
					&lt;th&gt;分辨率&lt;/th&gt;
					&lt;th&gt;比例&lt;/th&gt;
					&lt;th&gt;适用场景&lt;/th&gt;
			&lt;/tr&gt;
	&lt;/thead&gt;
	&lt;tbody&gt;
			&lt;tr&gt;
					&lt;td&gt;720 × 1280&lt;/td&gt;
					&lt;td&gt;9:16&lt;/td&gt;
					&lt;td&gt;竖屏手游最常见&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;640 × 960&lt;/td&gt;
					&lt;td&gt;2:3&lt;/td&gt;
					&lt;td&gt;早期 iPhone 经典&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;1080 × 1920&lt;/td&gt;
					&lt;td&gt;9:16&lt;/td&gt;
					&lt;td&gt;高清竖屏&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;1280 × 720&lt;/td&gt;
					&lt;td&gt;16:9&lt;/td&gt;
					&lt;td&gt;横屏手游&lt;/td&gt;
			&lt;/tr&gt;
	&lt;/tbody&gt;
&lt;/table&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;pre tabindex=&#34;0&#34;&gt;&lt;code class=&#34;language-mermaid&#34; data-lang=&#34;mermaid&#34;&gt;graph TD
    A[设计分辨率 720x1280] --&amp;gt; B[实际屏幕分辨率]
    B --&amp;gt; C{选择适配策略}
    C --&amp;gt;|FIT_WIDTH| D[宽度铺满，高度可能裁剪/留黑]
    C --&amp;gt;|FIT_HEIGHT| E[高度铺满，宽度可能裁剪/留黑]
    C --&amp;gt;|EXACT_FIT| F[拉伸铺满，可能变形]
    C --&amp;gt;|SHOW_ALL| G[等比缩放，全部可见，可能留黑]
    C --&amp;gt;|NO_BORDER| H[等比缩放，铺满屏幕，可能裁剪]&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;&lt;hr&gt;
&lt;h2 id=&#34;十三网络同步colyseus&#34;&gt;十三、网络同步：Colyseus&lt;/h2&gt;
&lt;p&gt;对于多人实时游戏，Colyseus 是 Cocos Creator 常用的 Node.js 状态同步框架。&lt;/p&gt;
&lt;table&gt;
	&lt;thead&gt;
			&lt;tr&gt;
					&lt;th&gt;同步方式&lt;/th&gt;
					&lt;th&gt;核心思想&lt;/th&gt;
					&lt;th&gt;适用场景&lt;/th&gt;
			&lt;/tr&gt;
	&lt;/thead&gt;
	&lt;tbody&gt;
			&lt;tr&gt;
					&lt;td&gt;状态同步&lt;/td&gt;
					&lt;td&gt;服务端权威，客户端插值预测&lt;/td&gt;
					&lt;td&gt;MMO、卡牌、休闲对战&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;帧同步&lt;/td&gt;
					&lt;td&gt;只同步输入，客户端各自演算&lt;/td&gt;
					&lt;td&gt;RTS、MOBA、格斗&lt;/td&gt;
			&lt;/tr&gt;
	&lt;/tbody&gt;
&lt;/table&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;避坑指南 #12&lt;/strong&gt;：帧同步对浮点数一致性和随机种子要求极高。Cocos 2.x 默认使用 JavaScript 浮点运算，跨平台（iOS/Android/微信）可能出现不一致，需统一数学库与随机算法。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr&gt;
&lt;h2 id=&#34;十四避坑清单速查表&#34;&gt;十四、避坑清单速查表&lt;/h2&gt;
&lt;table&gt;
	&lt;thead&gt;
			&lt;tr&gt;
					&lt;th&gt;编号&lt;/th&gt;
					&lt;th&gt;避坑点&lt;/th&gt;
			&lt;/tr&gt;
	&lt;/thead&gt;
	&lt;tbody&gt;
			&lt;tr&gt;
					&lt;td&gt;1&lt;/td&gt;
					&lt;td&gt;初始化放 &lt;code&gt;onLoad&lt;/code&gt;，跨组件访问放 &lt;code&gt;start&lt;/code&gt;&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;2&lt;/td&gt;
					&lt;td&gt;&lt;code&gt;moveBy&lt;/code&gt; 相对移动，&lt;code&gt;moveTo&lt;/code&gt; 绝对移动，别混用&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;3&lt;/td&gt;
					&lt;td&gt;旋转方向：正顺负逆；多圈旋转用 &lt;code&gt;rotateBy&lt;/code&gt;&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;4&lt;/td&gt;
					&lt;td&gt;注册事件必须传 &lt;code&gt;this&lt;/code&gt;，切场景时记得 &lt;code&gt;off&lt;/code&gt;&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;5&lt;/td&gt;
					&lt;td&gt;向量运算区分 &lt;code&gt;Self&lt;/code&gt; 与非 &lt;code&gt;Self&lt;/code&gt; 版本&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;6&lt;/td&gt;
					&lt;td&gt;坐标转换优先用带 &lt;code&gt;AR&lt;/code&gt; 的方法&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;7&lt;/td&gt;
					&lt;td&gt;&lt;code&gt;loadScene&lt;/code&gt; 前保存必要数据&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;8&lt;/td&gt;
					&lt;td&gt;音乐用 &lt;code&gt;playMusic&lt;/code&gt;，音效用 &lt;code&gt;playEffect&lt;/code&gt;&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;9&lt;/td&gt;
					&lt;td&gt;逻辑写在组件脚本里，不要直接挂在节点上&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;10&lt;/td&gt;
					&lt;td&gt;微信小游戏图片尺寸不能超过 2048×2048&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;11&lt;/td&gt;
					&lt;td&gt;&lt;code&gt;localStorage&lt;/code&gt; 只存字符串，对象需 JSON 序列化&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;12&lt;/td&gt;
					&lt;td&gt;帧同步注意浮点一致性与随机种子&lt;/td&gt;
			&lt;/tr&gt;
	&lt;/tbody&gt;
&lt;/table&gt;
&lt;hr&gt;
&lt;h2 id=&#34;十五总结&#34;&gt;十五、总结&lt;/h2&gt;
&lt;p&gt;Cocos Creator 2.x 的开发可以归纳为以下流程：&lt;/p&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;pre tabindex=&#34;0&#34;&gt;&lt;code class=&#34;language-mermaid&#34; data-lang=&#34;mermaid&#34;&gt;graph LR
    A[搭建场景节点树] --&amp;gt; B[挂载脚本组件]
    B --&amp;gt; C[处理输入事件]
    C --&amp;gt; D[在 update 中更新逻辑]
    D --&amp;gt; E[使用 Action 或逐帧修改属性实现动画]
    E --&amp;gt; F[加载资源 / 切换场景 / 保存数据]&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;&lt;p&gt;掌握 &lt;strong&gt;节点-组件模型、生命周期、Action 组合、Vec2 运算、坐标转换、屏幕适配&lt;/strong&gt; 这六大核心后，2D 游戏开发中大部分问题都能迎刃而解。希望这份笔记能成为你项目中的速查手册。&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&#34;参考&#34;&gt;参考&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&#34;https://docs.cocos.com/creator/manual/zh/getting-started/quick-start.html&#34;&gt;Cocos Creator 官方文档 - 快速上手&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&#34;https://forum.cocos.org/t/topic/74316&#34;&gt;Cocos Creator 官方论坛 - Colyseus 状态同步&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&#34;https://blog.csdn.net/qq_31967569/article/details/102624728&#34;&gt;CSDN - Cocos Creator 帧同步 + TypeScript 定义&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
</description>
    </item>
    
  </channel>
</rss>
