<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/"><channel><title>Cocos on Last Stand</title><link>https://amatsuzero.github.io/LastStand/tags/cocos/</link><description>Recent content in Cocos on Last Stand</description><generator>Hugo</generator><language>zh-cn</language><lastBuildDate>Wed, 08 Jul 2026 19:00:15 +0800</lastBuildDate><atom:link href="https://amatsuzero.github.io/LastStand/tags/cocos/index.xml" rel="self" type="application/rss+xml"/><item><title>3D球场项目 (一)</title><link>https://amatsuzero.github.io/LastStand/posts/cocostennis/part-one/</link><pubDate>Fri, 29 May 2026 08:54:50 +0800</pubDate><guid>https://amatsuzero.github.io/LastStand/posts/cocostennis/part-one/</guid><description>&lt;h2 id="介绍"&gt;介绍&lt;/h2&gt;
&lt;p&gt;2025 H2，我们开始进行技术规划。体育的现状是版权不够丰富，关注对于非版权比赛，只有两种方式:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;图文直播: 就只有一个气泡文字展示
&lt;img alt="图文直播气泡文字展示界面" loading="lazy" src="https://amatsuzero.github.io/LastStand/posts/cocostennis/part-one/text-bubble-live-broadcast.webp"&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;文字直播：类似消息板的文字滚动&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;这两个形式提现都很一般，完全激不起用户的任何兴趣。竞品如爱奇艺，对于非版权赛事，也是类似的效果：&lt;/p&gt;
&lt;p&gt;&lt;img alt="爱奇艺非版权赛事文字直播界面" loading="lazy" src="https://amatsuzero.github.io/LastStand/posts/cocostennis/part-one/iqiyi-text-broadcast.webp"&gt;&lt;/p&gt;
&lt;p&gt;我们脑暴了一个使用 3D 来展示的计划。&lt;/p&gt;
&lt;h2 id="规划阶段"&gt;规划阶段&lt;/h2&gt;
&lt;p&gt;最初我们计划在图文直播的基础上做一个增强，在特定情境下，如进球、得分等场景下，播放一个固定的 3D 动画。起初我们打算使用 &lt;a href="https://pag.io/"&gt;PAG&lt;/a&gt; 形式，这个是最简单的，而且我们之前也有了一定的了解，PAG 文件支持一定程度的自定义，PAG 里面的文字、颜色，图片等等都可以替换。但是交互性相对使用 3D 模型差一些，无法实现调整视角、旋转等等，这个方案技术挑战也不足，所以很快放弃了。&lt;/p&gt;
&lt;p&gt;我后来又想使用 &lt;a href="https://github.com/google/filament"&gt;Filament&lt;/a&gt; 来播 3D 模型，腾讯视频 NBA 直播间曾经使用这个项目做过球员专属的进场特效，支持用户调整视角，互动性更强。&lt;/p&gt;
&lt;p&gt;&lt;img alt="腾讯视频NBA球员进场3D动画特效" loading="lazy" src="https://amatsuzero.github.io/LastStand/posts/cocostennis/part-one/nba-player-entrance-animation.gif"&gt;&lt;/p&gt;
&lt;p&gt;但是我们想到了几个问题：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;谁来做这些动画呢？我们没有 3D 资产库，这意味着我们要让设计给每个球队设计一个球队，还有动画，我们的设计团队支持不了；&lt;/li&gt;
&lt;li&gt;做到最后还是只是一个播动画，虽然交互比 PAG 提高了，但还是很单调，这个想作为一个卖点，还是有点单薄；&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;我们决定扩展一下这个项目，不再是图文项目的一个增强项目，而是构建一个图文直播的平替：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;构建一套端到端的 AI 文生 3D 动画直播生成系统;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;实现输入一段自然语言文本描述（如剧情、场景、动作、情感等），或标准序列化的 event数据（如网球 opta 数据），自动生成对应的3D动画内容，包括：剧本情节、3D角色与场景模型，以及带有镜头、动作与交互，可在客户端高性能展示的完整动画展示；&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;我们最后选择网球来落地这个项目，是基于以下的几点考量：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;腾讯体育是釆买了 WTA 以及澳网 OPTA 数据的版权，相比原来的图文直播，3D 直播显然能给用户提供更好的临场感；&lt;/li&gt;
&lt;li&gt;与篮球、足球相比，网球球员较少，场面没有那么复杂，不需要投入很多的额外精力进行性能优化（虽然我们后面还是做了一些）；&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;实际上国外已经有这种动画直播了：&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;澳网 3D 动画直播：&lt;/strong&gt;&lt;/p&gt;
&lt;div style="position: relative; padding-bottom: 56.25%; height: 0; overflow: hidden;"&gt;
&lt;iframe allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share; fullscreen" loading="eager" referrerpolicy="strict-origin-when-cross-origin" src="https://www.youtube.com/embed/bSfcTeEvDpc?autoplay=0&amp;amp;controls=1&amp;amp;end=0&amp;amp;loop=0&amp;amp;mute=0&amp;amp;start=0" style="position: absolute; top: 0; left: 0; width: 100%; height: 100%; border:0;" title="YouTube video"&gt;&lt;/iframe&gt;
&lt;/div&gt;
&lt;p&gt;&lt;strong&gt;索尼体育 &lt;a href="https://www.sony.com/en/SonyInfo/technology/stories/entries/20240411/beyondsports/"&gt;NFL 直播方案&lt;/a&gt;：&lt;/strong&gt;&lt;/p&gt;</description></item><item><title>3D球场项目 (二)</title><link>https://amatsuzero.github.io/LastStand/posts/cocostennis/part-two/</link><pubDate>Sat, 30 May 2026 10:00:00 +0800</pubDate><guid>https://amatsuzero.github.io/LastStand/posts/cocostennis/part-two/</guid><description>&lt;p&gt;在&lt;a href="../part-one/"&gt;3D球场项目 (一)&lt;/a&gt;中，我们搞定了 3D 资产，也选定了落地框架。但是这不代表我们可以直接使用，这个作为的弹幕引擎的一部引入的 Cocos，是有一些定制逻辑在的。我们需要抽丝剥茧的先了解这个弹幕系统做了什么，再来制定具体的实施方案。&lt;/p&gt;
&lt;h2 id="视频弹幕引擎"&gt;视频弹幕引擎&lt;/h2&gt;
&lt;p&gt;腾讯视频的弹幕引擎作为一个悠久的项目，支撑了腾讯视频大量的互动活动。弹幕引擎框架也是几经迭代，从最早的原生版本，到 PAG，再到现在的 Cocos Creator版本。&lt;/p&gt;
&lt;p&gt;之所以想要使用 Cocos 来替换原来的方案，是因为有以下两大优势：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;能力强：&lt;/strong&gt; 支持完善的图形渲染、运动、物理、粒子、3D、光照等游戏引擎能力，充分利用游戏引擎所提供的这些能力，可以在弹幕上实现丰富的玩法。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;跨平台：&lt;/strong&gt; Cocos Creator 是一款高效、清凉、免费开源的跨平台图形引擎，支持所有主流平台支持，真正实现一次开发，全平台运行。框架迭代几乎都是使用 Cocos Creator 开发，少部分业务，如数据上报、客户端交互等，才需要做做少量适配工作，这极大的节省了人力;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;动态化：&lt;/strong&gt; 动态发布新特性和配置能力，更方便配合运营做活动，不需要等 app 发版铺量；&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;弹幕引擎的工程划分，大致如下：&lt;/p&gt;
&lt;div class="arch-diagram"&gt;
&lt;style&gt;
.arch-diagram { font-size: 13px; color: #222; margin: 1.2em 0; max-width: 100%; overflow-x: auto; }
.arch-diagram .layer {
background: #f0f4f8;
border: 1.5px solid #aab4c2;
border-radius: 8px;
padding: 14px 16px 16px;
margin: 10px 0;
position: relative;
}
.arch-diagram .layer-title {
font-weight: 600;
color: #334155;
margin-bottom: 10px;
font-size: 14px;
}
.arch-diagram .row { display: flex; gap: 10px; flex-wrap: wrap; align-items: center; }
.arch-diagram .node {
background: #fff;
border: 1px solid #8b8b8b;
border-radius: 18px;
padding: 6px 14px;
min-width: 60px;
text-align: center;
box-shadow: 0 1px 1px rgba(0,0,0,0.03);
flex: 1 1 auto;
white-space: nowrap;
}
.arch-diagram .node.wrap {
white-space: normal;
line-height: 1.35;
padding: 8px 12px;
}
.arch-diagram .group {
background: #fafbfc;
border: 1px dashed #9aa4b2;
border-radius: 8px;
padding: 12px;
flex: 1;
min-width: 200px;
display: flex;
flex-direction: column;
}
.arch-diagram .group-title {
font-weight: 600;
color: #475569;
margin-bottom: 8px;
font-size: 13px;
}
.arch-diagram .inner {
background: #fff;
border: 1px solid #c5ccd6;
border-radius: 6px;
padding: 10px;
margin-bottom: 8px;
}
.arch-diagram .inner:last-child { margin-bottom: 0; }
.arch-diagram .inner-title {
color: #475569;
font-size: 12px;
text-align: center;
margin-bottom: 6px;
}
.arch-diagram .arrow {
text-align: center;
color: #94a3b8;
font-size: 18px;
line-height: 1;
margin: 4px 0;
user-select: none;
}
.arch-diagram .app-row { display: flex; justify-content: center; }
.arch-diagram .app-row .node { flex: 0 0 auto; min-width: 120px; padding: 8px 24px; }
.arch-diagram .md-row { display: flex; gap: 12px; align-items: flex-start; flex-wrap: wrap; }
.arch-diagram .md-row &gt; .group:first-child { flex: 2.4 1 360px; }
.arch-diagram .md-row &gt; .group:last-child { flex: 1 1 200px; }
.arch-diagram .ce-row { display: flex; gap: 10px; align-items: flex-start; flex-wrap: wrap; }
.arch-diagram .ce-row .nodes { display: flex; gap: 10px; flex: 1 1 280px; flex-wrap: wrap; }
.arch-diagram .ce-row .ext-group { flex: 1 1 38%; min-width: 220px; }
&lt;pre&gt;&lt;code&gt;/* Mobile &amp;amp; tablet responsive: &amp;lt;= 900px */
@media (max-width: 900px) {
.arch-diagram { font-size: 12px; }
.arch-diagram .layer { padding: 10px 10px 12px; }
.arch-diagram .layer-title { font-size: 13px; margin-bottom: 8px; }
.arch-diagram .row { gap: 6px; }
.arch-diagram .row &amp;gt; .node {
flex: 1 1 calc(33.333% - 6px);
padding: 5px 6px;
min-width: 0;
font-size: 11.5px;
white-space: normal;
line-height: 1.3;
word-break: break-word;
}
.arch-diagram .node.wrap { padding: 6px 8px; line-height: 1.3; }
.arch-diagram .group { padding: 8px; min-width: 0; }
.arch-diagram .inner { padding: 8px; }
.arch-diagram .app-row .node { min-width: 0; padding: 6px 18px; flex: 0 0 auto; white-space: nowrap; }
.arch-diagram .md-row { flex-direction: column; gap: 8px; }
.arch-diagram .md-row &amp;gt; .group:first-child,
.arch-diagram .md-row &amp;gt; .group:last-child { flex: 1 1 auto; width: 100%; }
.arch-diagram .ce-row { flex-direction: column; gap: 8px; }
.arch-diagram .ce-row .nodes { flex: 1 1 auto; width: 100%; }
.arch-diagram .ce-row .ext-group { flex: 1 1 auto; width: 100%; min-width: 0; }
}
/* Extra small mobile: &amp;lt;= 420px */
@media (max-width: 420px) {
.arch-diagram .row &amp;gt; .node { flex: 1 1 calc(50% - 6px); }
}
@media (prefers-color-scheme: dark) {
.arch-diagram { color: #e2e8f0; }
.arch-diagram .layer { background: #1e293b; border-color: #475569; }
.arch-diagram .group { background: #0f172a; border-color: #475569; }
.arch-diagram .inner { background: #1e293b; border-color: #475569; }
.arch-diagram .node { background: #0f172a; border-color: #94a3b8; color: #e2e8f0; }
.arch-diagram .layer-title, .arch-diagram .group-title, .arch-diagram .inner-title { color: #cbd5e1; }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;/style&gt;&lt;/p&gt;</description></item><item><title>3D球场项目 (三)</title><link>https://amatsuzero.github.io/LastStand/posts/cocostennis/part-three/</link><pubDate>Sat, 30 May 2026 17:30:28 +0800</pubDate><guid>https://amatsuzero.github.io/LastStand/posts/cocostennis/part-three/</guid><description>&lt;p&gt;在&lt;a href="../part-two/"&gt;3D球场项目 (二)&lt;/a&gt;中，我们梳理了视频弹幕引擎的整体架构、引擎初始化流程。现在，3D 球场部分基本已经探索完毕，我们接下来就是要解决这个项目的数据驱动部分了。&lt;/p&gt;
&lt;p&gt;我们现在研究一下下发数据的来源：&lt;a href="https://theanalyst.com/"&gt;Opta&lt;/a&gt;，它是全球顶级的体育数据公司，1996 年创立于英国伦敦，现隶属于 Stats Perform，是行业公认的足球数据金标准。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;覆盖：20+ 运动、3900+ 赛事、每年 6 万 + 场比赛&lt;/li&gt;
&lt;li&gt;规模：7.2PB 历史数据、年采集超 10 亿数据点&lt;/li&gt;
&lt;li&gt;精度：单场足球2 万 + 事件点，事件识别准确率 99.8%&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;腾讯体育采买了 Opta WTA 网球比赛的数据，我们可以通过 Opta 的 API 来获取数据。&lt;/p&gt;
&lt;p&gt;根据他们官网的&lt;a href="https://documentation.statsperform.com/docs/rh/sdapi/Topics/basketball/opta-sdapi-basketball-api-match-events.htm"&gt;介绍&lt;/a&gt;，一个 MA3 网球赛事数据中 typeId 对应的意义如下：&lt;/p&gt;
&lt;details&gt;
&lt;summary&gt;点击展开 / 折叠 Event TypeId 完整对照表&lt;/summary&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Event TypeId&lt;/th&gt;
&lt;th&gt;Event Name&lt;/th&gt;
&lt;th&gt;Description&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;20&lt;/td&gt;
&lt;td&gt;STOP_GAME&lt;/td&gt;
&lt;td&gt;Indicates that the game has finished.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;41&lt;/td&gt;
&lt;td&gt;START_SET_1&lt;/td&gt;
&lt;td&gt;Start 1st Set&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;42&lt;/td&gt;
&lt;td&gt;START_SET_2&lt;/td&gt;
&lt;td&gt;Start 2nd Set&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;43&lt;/td&gt;
&lt;td&gt;START_SET_3&lt;/td&gt;
&lt;td&gt;Start 3rd Set&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;44&lt;/td&gt;
&lt;td&gt;START_SET_4&lt;/td&gt;
&lt;td&gt;Start 4th Set&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;45&lt;/td&gt;
&lt;td&gt;START_SET_5&lt;/td&gt;
&lt;td&gt;Start 5th Set&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;46&lt;/td&gt;
&lt;td&gt;STOP_SET&lt;/td&gt;
&lt;td&gt;Stop Set&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;47&lt;/td&gt;
&lt;td&gt;T_WO1&lt;/td&gt;
&lt;td&gt;w.o. Player 1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;48&lt;/td&gt;
&lt;td&gt;T_WO2&lt;/td&gt;
&lt;td&gt;w.o. Player 2&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;128&lt;/td&gt;
&lt;td&gt;SAFE&lt;/td&gt;
&lt;td&gt;Safe (No longer used)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;129&lt;/td&gt;
&lt;td&gt;DANGER&lt;/td&gt;
&lt;td&gt;Danger (Technical/connection issue at venue)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;132&lt;/td&gt;
&lt;td&gt;INJ_BREAK&lt;/td&gt;
&lt;td&gt;Game suspended - Player injured&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;133&lt;/td&gt;
&lt;td&gt;PLAYERS_COMING_OUT&lt;/td&gt;
&lt;td&gt;Players coming out&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;141&lt;/td&gt;
&lt;td&gt;GAME_ABOUT_TO_START&lt;/td&gt;
&lt;td&gt;Game about to start&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;149&lt;/td&gt;
&lt;td&gt;GAME_SUSPENDED&lt;/td&gt;
&lt;td&gt;Game suspended&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;160&lt;/td&gt;
&lt;td&gt;PLAYERS_WARMING_UP&lt;/td&gt;
&lt;td&gt;Players warming up&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;162&lt;/td&gt;
&lt;td&gt;SHAKE_HANDS_WITH_REFEREE&lt;/td&gt;
&lt;td&gt;Players shake hands with referee&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;163&lt;/td&gt;
&lt;td&gt;COIN_FLIPPING_FOR_FIRST_SERVER&lt;/td&gt;
&lt;td&gt;Coin flip to choose first server&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;164&lt;/td&gt;
&lt;td&gt;PLAYERS_TALKING_TO_REFEREE&lt;/td&gt;
&lt;td&gt;Player(s) talking to the referee&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;165&lt;/td&gt;
&lt;td&gt;REFEREE_CHECKING_POINTMARK&lt;/td&gt;
&lt;td&gt;Referee checking pointmark&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;166&lt;/td&gt;
&lt;td&gt;BOTH_PLAYERS_SEATED&lt;/td&gt;
&lt;td&gt;Both players seated&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;167&lt;/td&gt;
&lt;td&gt;PLAYER1_RECEIVING_TREATMENT&lt;/td&gt;
&lt;td&gt;Player 1 receiving treatment&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;168&lt;/td&gt;
&lt;td&gt;PLAYER2_RECEIVING_TREATMENT&lt;/td&gt;
&lt;td&gt;Player 2 receiving treatment&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;169&lt;/td&gt;
&lt;td&gt;PLAYERS_GOING_TO_COURT&lt;/td&gt;
&lt;td&gt;Players going back to court&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;170&lt;/td&gt;
&lt;td&gt;NEXT_GAME_ABOUT_TO_START&lt;/td&gt;
&lt;td&gt;Next game is about to start&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;180&lt;/td&gt;
&lt;td&gt;T_MORE_THAN_5&lt;/td&gt;
&lt;td&gt;Rally, over five shots&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;181&lt;/td&gt;
&lt;td&gt;T_MORE_THAN_10&lt;/td&gt;
&lt;td&gt;Rally over ten shots&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;182&lt;/td&gt;
&lt;td&gt;T_MORE_THAN_15&lt;/td&gt;
&lt;td&gt;Rally over fifteen shots&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;187&lt;/td&gt;
&lt;td&gt;T_POINT_UNDER_INVESTIGATION&lt;/td&gt;
&lt;td&gt;Point under investigation&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;188&lt;/td&gt;
&lt;td&gt;T_START_TIE_BREAK&lt;/td&gt;
&lt;td&gt;Start tie break&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;191&lt;/td&gt;
&lt;td&gt;T_POINT_STOPPED_BY_REF&lt;/td&gt;
&lt;td&gt;Point stopped by referee&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;197&lt;/td&gt;
&lt;td&gt;UPDATE_SCORE_START&lt;/td&gt;
&lt;td&gt;Start score update&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;198&lt;/td&gt;
&lt;td&gt;UPDATE_SCORE_FINISHED&lt;/td&gt;
&lt;td&gt;Score update finished&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;200&lt;/td&gt;
&lt;td&gt;SIDE_CHANGE&lt;/td&gt;
&lt;td&gt;Side change&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;206&lt;/td&gt;
&lt;td&gt;SHOT_CNT&lt;/td&gt;
&lt;td&gt;Shot count&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;216&lt;/td&gt;
&lt;td&gt;STATISTIC_VERIFICATION&lt;/td&gt;
&lt;td&gt;Statistics confirmation&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;240&lt;/td&gt;
&lt;td&gt;NEW_BALLS&lt;/td&gt;
&lt;td&gt;New balls&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;241&lt;/td&gt;
&lt;td&gt;UMPIRE_ON_COURT&lt;/td&gt;
&lt;td&gt;UMPIRE ON COURT&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;242&lt;/td&gt;
&lt;td&gt;WARMING_UP_VOLLEY&lt;/td&gt;
&lt;td&gt;Warming up - volley&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;243&lt;/td&gt;
&lt;td&gt;WARMING_UP_SERVICE&lt;/td&gt;
&lt;td&gt;Warming up - service&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;244&lt;/td&gt;
&lt;td&gt;ONE_MINUTE&lt;/td&gt;
&lt;td&gt;One minute&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;245&lt;/td&gt;
&lt;td&gt;TIME&lt;/td&gt;
&lt;td&gt;Time&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;246&lt;/td&gt;
&lt;td&gt;SHOT_DETAILS&lt;/td&gt;
&lt;td&gt;Shot details&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;256&lt;/td&gt;
&lt;td&gt;CLS&lt;/td&gt;
&lt;td&gt;Cancel last sent event&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;257&lt;/td&gt;
&lt;td&gt;CLR&lt;/td&gt;
&lt;td&gt;Clear events&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;258&lt;/td&gt;
&lt;td&gt;GCC&lt;/td&gt;
&lt;td&gt;Game conditions changed.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;261&lt;/td&gt;
&lt;td&gt;SCORER&lt;/td&gt;
&lt;td&gt;Event details for %RELATED_EVENT% in the %MIN%. minute changed&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;262&lt;/td&gt;
&lt;td&gt;BP&lt;/td&gt;
&lt;td&gt;Ball position event&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;264&lt;/td&gt;
&lt;td&gt;ODD&lt;/td&gt;
&lt;td&gt;Odds event (deprecated)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;266&lt;/td&gt;
&lt;td&gt;TIME_CORRECTION_EVENT&lt;/td&gt;
&lt;td&gt;Correct timestamp for a missed event&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;276&lt;/td&gt;
&lt;td&gt;START_GAME_CLOCK&lt;/td&gt;
&lt;td&gt;The (stopped) game clock is (re)started the game is running&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;277&lt;/td&gt;
&lt;td&gt;STOP_GAME_CLOCK&lt;/td&gt;
&lt;td&gt;The game clock is stopped. Time will not change until Start Game Clock is sent again&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;278&lt;/td&gt;
&lt;td&gt;ADJUST_GAME_CLOCK&lt;/td&gt;
&lt;td&gt;Game clock value is adjusted manually&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;279&lt;/td&gt;
&lt;td&gt;CSTAT&lt;/td&gt;
&lt;td&gt;A statistical value is cleared and an additional event with the correct action is sent (e.g. an invalid Ace Home event is replaced by a Service Winner Home)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;280&lt;/td&gt;
&lt;td&gt;CONF_PERIOD_SCORE&lt;/td&gt;
&lt;td&gt;Period score confirmed&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;282&lt;/td&gt;
&lt;td&gt;TIME_ADAPTION&lt;/td&gt;
&lt;td&gt;Time for event %RELATED_EVENT% was adapted by %SEC% seconds&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;285&lt;/td&gt;
&lt;td&gt;PLAYER_DATA_CONFIRMED&lt;/td&gt;
&lt;td&gt;Player data for %RELATED_EVENT% in the %MIN% confirmed&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;513&lt;/td&gt;
&lt;td&gt;SYS_MSG&lt;/td&gt;
&lt;td&gt;System Message&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;514&lt;/td&gt;
&lt;td&gt;SCOUT_IN_STADION&lt;/td&gt;
&lt;td&gt;Scout in Stadium&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;515&lt;/td&gt;
&lt;td&gt;CONNECTION_PROBLEMSSCOUT_OFFLINE&lt;/td&gt;
&lt;td&gt;Connection problems Scout offline&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;516&lt;/td&gt;
&lt;td&gt;CONNECTION_PROBLEMS&lt;/td&gt;
&lt;td&gt;Connection problems&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;517&lt;/td&gt;
&lt;td&gt;TRANSMISSION_ONLINE&lt;/td&gt;
&lt;td&gt;Transmission online&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;520&lt;/td&gt;
&lt;td&gt;LINEUP_CHANGED&lt;/td&gt;
&lt;td&gt;Line-up changed&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;524&lt;/td&gt;
&lt;td&gt;JERSEY_CHANGED&lt;/td&gt;
&lt;td&gt;Jersey colors updated&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;782&lt;/td&gt;
&lt;td&gt;GAME CANCELLED&lt;/td&gt;
&lt;td&gt;Automatic &amp;ldquo;Game cancelled&amp;rdquo; event after first &amp;ldquo;cancellation&amp;rdquo; System message is sent&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;1152&lt;/td&gt;
&lt;td&gt;T_SERVE1&lt;/td&gt;
&lt;td&gt;Service Player 1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;1153&lt;/td&gt;
&lt;td&gt;T_1ST_SERVICE1&lt;/td&gt;
&lt;td&gt;1st service Player 1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;1154&lt;/td&gt;
&lt;td&gt;T_2ND_SERVICE1&lt;/td&gt;
&lt;td&gt;2nd service Player 1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;1155&lt;/td&gt;
&lt;td&gt;T_NA1&lt;/td&gt;
&lt;td&gt;Net approach player 1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;1156&lt;/td&gt;
&lt;td&gt;T_W_FH1&lt;/td&gt;
&lt;td&gt;Winner (forehand) Player 1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;1157&lt;/td&gt;
&lt;td&gt;T_W_BH1&lt;/td&gt;
&lt;td&gt;Winner (backhand) Player 1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;1158&lt;/td&gt;
&lt;td&gt;T_FE_FH1&lt;/td&gt;
&lt;td&gt;Forced error (forehand) Player 1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;1159&lt;/td&gt;
&lt;td&gt;T_FE_BH1&lt;/td&gt;
&lt;td&gt;Forced error (backhand) Player 1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;1160&lt;/td&gt;
&lt;td&gt;T_UE_FH1&lt;/td&gt;
&lt;td&gt;Unforced error (forehand) Player 1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;1161&lt;/td&gt;
&lt;td&gt;T_UE_BH1&lt;/td&gt;
&lt;td&gt;Unforced error (backhand) Player 1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;1162&lt;/td&gt;
&lt;td&gt;T_HE1&lt;/td&gt;
&lt;td&gt;Hawkeye Player 1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;1163&lt;/td&gt;
&lt;td&gt;T_WBR1&lt;/td&gt;
&lt;td&gt;Warning by referee Player 1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;1164&lt;/td&gt;
&lt;td&gt;T_CBR1&lt;/td&gt;
&lt;td&gt;Cautioned by referee Player 1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;1165&lt;/td&gt;
&lt;td&gt;T_SRV_NET1&lt;/td&gt;
&lt;td&gt;Net Player 1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;1166&lt;/td&gt;
&lt;td&gt;T_SRV_OUT1&lt;/td&gt;
&lt;td&gt;Out Player 1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;1167&lt;/td&gt;
&lt;td&gt;T_SRV_FF1&lt;/td&gt;
&lt;td&gt;Foot fault Player 1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;1168&lt;/td&gt;
&lt;td&gt;T_SRV_DF1&lt;/td&gt;
&lt;td&gt;Double fault Player 1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;1169&lt;/td&gt;
&lt;td&gt;T_SRV_A1&lt;/td&gt;
&lt;td&gt;Ace Player 1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;1170&lt;/td&gt;
&lt;td&gt;T_SRV_SW1&lt;/td&gt;
&lt;td&gt;Service winner Player 1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;1171&lt;/td&gt;
&lt;td&gt;T_SRV_RW_FH1&lt;/td&gt;
&lt;td&gt;Return winner (forehand) Player 1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;1172&lt;/td&gt;
&lt;td&gt;T_SRV_IN1&lt;/td&gt;
&lt;td&gt;Serve in Player 1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;1173&lt;/td&gt;
&lt;td&gt;T_POINT1&lt;/td&gt;
&lt;td&gt;Point Player 1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;1174&lt;/td&gt;
&lt;td&gt;T_CONF1&lt;/td&gt;
&lt;td&gt;Confirm Point Player 1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;1175&lt;/td&gt;
&lt;td&gt;T_GAME1&lt;/td&gt;
&lt;td&gt;Game Player 1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;1176&lt;/td&gt;
&lt;td&gt;T_SET1&lt;/td&gt;
&lt;td&gt;Set Player 1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;1177&lt;/td&gt;
&lt;td&gt;T_BL1&lt;/td&gt;
&lt;td&gt;Baseline Player 1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;1178&lt;/td&gt;
&lt;td&gt;T_SRV_NR1&lt;/td&gt;
&lt;td&gt;Net/Retake Player 1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;1179&lt;/td&gt;
&lt;td&gt;T_W1&lt;/td&gt;
&lt;td&gt;Winner Player 1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;1180&lt;/td&gt;
&lt;td&gt;T_FE1&lt;/td&gt;
&lt;td&gt;Forced error Player 1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;1181&lt;/td&gt;
&lt;td&gt;T_UE1&lt;/td&gt;
&lt;td&gt;Unforced error Player 1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;1182&lt;/td&gt;
&lt;td&gt;T_SRV_RW_BH1&lt;/td&gt;
&lt;td&gt;Return winner (backhand) Player 1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;1183&lt;/td&gt;
&lt;td&gt;T_V_FH1&lt;/td&gt;
&lt;td&gt;Volley (forehand) Player 1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;1184&lt;/td&gt;
&lt;td&gt;T_V_BH1&lt;/td&gt;
&lt;td&gt;Volley (backhand) Player 1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;1185&lt;/td&gt;
&lt;td&gt;T_BB1&lt;/td&gt;
&lt;td&gt;Break ball Player 1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;1186&lt;/td&gt;
&lt;td&gt;T_B1&lt;/td&gt;
&lt;td&gt;Break Player 1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;1187&lt;/td&gt;
&lt;td&gt;T_HES1&lt;/td&gt;
&lt;td&gt;Hawk eye successful Player 1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;1188&lt;/td&gt;
&lt;td&gt;T_ITO1&lt;/td&gt;
&lt;td&gt;Injury Timeout Player 1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;1189&lt;/td&gt;
&lt;td&gt;T_START_SRV1&lt;/td&gt;
&lt;td&gt;Start Service Player 1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;1190&lt;/td&gt;
&lt;td&gt;T_WARNING1&lt;/td&gt;
&lt;td&gt;1st offence - warning Player 1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;1191&lt;/td&gt;
&lt;td&gt;T_PENALTY_POINT1&lt;/td&gt;
&lt;td&gt;2nd offence - penalty point Player 1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;1192&lt;/td&gt;
&lt;td&gt;T_PENALTY_GAME1&lt;/td&gt;
&lt;td&gt;3rd offence - penalty game Player 1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;1193&lt;/td&gt;
&lt;td&gt;T_DISQUALIFICATION1&lt;/td&gt;
&lt;td&gt;Disqualification Player 1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;1194&lt;/td&gt;
&lt;td&gt;T_UE_NET1&lt;/td&gt;
&lt;td&gt;Unforced error net Player 1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;1195&lt;/td&gt;
&lt;td&gt;T_FE_NET1&lt;/td&gt;
&lt;td&gt;Forced error net Player 1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;1196&lt;/td&gt;
&lt;td&gt;T_SUCCESSFUL_NET_APPROACH1&lt;/td&gt;
&lt;td&gt;Successful net approach Player 1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;1197&lt;/td&gt;
&lt;td&gt;T_TIME_VIOLATION1&lt;/td&gt;
&lt;td&gt;Time violation Player 1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;1198&lt;/td&gt;
&lt;td&gt;T_PENALTY_POINT1&lt;br&gt;T_UNSUCCESSFUL_NET_APPROACH1&lt;/td&gt;
&lt;td&gt;Unsuccessful net approach Player 1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;1199&lt;/td&gt;
&lt;td&gt;T_AT_NET1&lt;/td&gt;
&lt;td&gt;At net Player 1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;1200&lt;/td&gt;
&lt;td&gt;T_AT_BASELINE1&lt;/td&gt;
&lt;td&gt;At baseline Player 1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;1201&lt;/td&gt;
&lt;td&gt;T_SET_BALL1&lt;/td&gt;
&lt;td&gt;Set ball Player 1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;1202&lt;/td&gt;
&lt;td&gt;T_MATCH_BALL1&lt;/td&gt;
&lt;td&gt;Match ball Player 1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2176&lt;/td&gt;
&lt;td&gt;T_SERVE2&lt;/td&gt;
&lt;td&gt;Service Player 2&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2177&lt;/td&gt;
&lt;td&gt;T_1ST_SERVICE2&lt;/td&gt;
&lt;td&gt;1st service Player 2&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2178&lt;/td&gt;
&lt;td&gt;T_2ND_SERVICE2&lt;/td&gt;
&lt;td&gt;2nd service Player 2&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2179&lt;/td&gt;
&lt;td&gt;T_NA2&lt;/td&gt;
&lt;td&gt;Net approach player 2&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2180&lt;/td&gt;
&lt;td&gt;T_W_FH2&lt;/td&gt;
&lt;td&gt;Winner (forehand) Player 2&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2181&lt;/td&gt;
&lt;td&gt;T_W_BH2&lt;/td&gt;
&lt;td&gt;Winner (backhand) Player 2&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2182&lt;/td&gt;
&lt;td&gt;T_FE_FH2&lt;/td&gt;
&lt;td&gt;Forced error (forehand) Player 2&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2183&lt;/td&gt;
&lt;td&gt;T_FE_BH2&lt;/td&gt;
&lt;td&gt;Forced error (backhand) Player 2&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2184&lt;/td&gt;
&lt;td&gt;T_UE_FH2&lt;/td&gt;
&lt;td&gt;Unforced error (forehand) Player 2&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2185&lt;/td&gt;
&lt;td&gt;T_UE_BH2&lt;/td&gt;
&lt;td&gt;Unforced error (backhand) Player 2&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2186&lt;/td&gt;
&lt;td&gt;T_HE2&lt;/td&gt;
&lt;td&gt;Hawkeye Player 2&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2187&lt;/td&gt;
&lt;td&gt;T_WBR2&lt;/td&gt;
&lt;td&gt;Warning by referee Player 2&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2188&lt;/td&gt;
&lt;td&gt;T_CBR2&lt;/td&gt;
&lt;td&gt;Cautioned by referee Player 2&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2189&lt;/td&gt;
&lt;td&gt;T_SRV_NET2&lt;/td&gt;
&lt;td&gt;Net Player 2&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2190&lt;/td&gt;
&lt;td&gt;T_SRV_OUT2&lt;/td&gt;
&lt;td&gt;Out Player 2&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2191&lt;/td&gt;
&lt;td&gt;T_SRV_FF2&lt;/td&gt;
&lt;td&gt;Foot fault Player 2&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2192&lt;/td&gt;
&lt;td&gt;T_SRV_DF2&lt;/td&gt;
&lt;td&gt;Double fault Player 2&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2193&lt;/td&gt;
&lt;td&gt;T_SRV_A2&lt;/td&gt;
&lt;td&gt;Ace Player 2&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2194&lt;/td&gt;
&lt;td&gt;T_SRV_SW2&lt;/td&gt;
&lt;td&gt;Service winner Player 2&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2195&lt;/td&gt;
&lt;td&gt;T_SRV_RW_FH2&lt;/td&gt;
&lt;td&gt;Return winner (forehand) Player 2&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2196&lt;/td&gt;
&lt;td&gt;T_SRV_IN2&lt;/td&gt;
&lt;td&gt;Serve in Player 2&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2197&lt;/td&gt;
&lt;td&gt;T_POINT2&lt;/td&gt;
&lt;td&gt;Point Player 2&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2198&lt;/td&gt;
&lt;td&gt;T_CONF2&lt;/td&gt;
&lt;td&gt;Confirm Point Player 2&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2199&lt;/td&gt;
&lt;td&gt;T_GAME2&lt;/td&gt;
&lt;td&gt;Game Player 2&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2200&lt;/td&gt;
&lt;td&gt;T_SET2&lt;/td&gt;
&lt;td&gt;Set Player 2&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2201&lt;/td&gt;
&lt;td&gt;T_BL2&lt;/td&gt;
&lt;td&gt;Baseline Player 2&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2202&lt;/td&gt;
&lt;td&gt;T_SRV_NR2&lt;/td&gt;
&lt;td&gt;Net/Retake Player 2&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2203&lt;/td&gt;
&lt;td&gt;T_W2&lt;/td&gt;
&lt;td&gt;Winner Player 2&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2204&lt;/td&gt;
&lt;td&gt;T_FE2&lt;/td&gt;
&lt;td&gt;Forced error Player 2&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2205&lt;/td&gt;
&lt;td&gt;T_UE2&lt;/td&gt;
&lt;td&gt;Unforced error Player 2&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2206&lt;/td&gt;
&lt;td&gt;T_SRV_RW_BH2&lt;/td&gt;
&lt;td&gt;Return winner (backhand) Player 2&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2207&lt;/td&gt;
&lt;td&gt;T_V_FH2&lt;/td&gt;
&lt;td&gt;Volley (forehand) Player 2&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2208&lt;/td&gt;
&lt;td&gt;T_V_BH2&lt;/td&gt;
&lt;td&gt;Volley (backhand) Player 2&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2209&lt;/td&gt;
&lt;td&gt;T_BB2&lt;/td&gt;
&lt;td&gt;Break ball Player 2&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2210&lt;/td&gt;
&lt;td&gt;T_B2&lt;/td&gt;
&lt;td&gt;Break Player 2&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2211&lt;/td&gt;
&lt;td&gt;T_HES2&lt;/td&gt;
&lt;td&gt;Hawk eye successful Player 2&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2212&lt;/td&gt;
&lt;td&gt;T_ITO&lt;/td&gt;
&lt;td&gt;Injury Timeout Player 2&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2213&lt;/td&gt;
&lt;td&gt;T_START_SRV2&lt;/td&gt;
&lt;td&gt;Start Service Player 2&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2214&lt;/td&gt;
&lt;td&gt;T_WARNING2&lt;/td&gt;
&lt;td&gt;1st offence - warning Player 2&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2215&lt;/td&gt;
&lt;td&gt;T_PENALTY_POINT2&lt;/td&gt;
&lt;td&gt;2nd offence - penalty point Player 2&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2216&lt;/td&gt;
&lt;td&gt;T_PENALTY_GAME2&lt;/td&gt;
&lt;td&gt;3rd offence - penalty game Player 2&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2217&lt;/td&gt;
&lt;td&gt;T_DISQUALIFICATION2&lt;/td&gt;
&lt;td&gt;Disqualification Player 2&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2218&lt;/td&gt;
&lt;td&gt;T_UE_NET2&lt;/td&gt;
&lt;td&gt;Unforced error net Player 2&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2219&lt;/td&gt;
&lt;td&gt;T_FE_NET2&lt;/td&gt;
&lt;td&gt;Forced error net Player 2&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2220&lt;/td&gt;
&lt;td&gt;T_SUCCESSFUL_NET_APPROACH2&lt;/td&gt;
&lt;td&gt;Successful net approach Player 2&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2221&lt;/td&gt;
&lt;td&gt;T_TIME_VIOLATION2&lt;/td&gt;
&lt;td&gt;Time violation Player 2&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2222&lt;/td&gt;
&lt;td&gt;T_AT_NET2&lt;/td&gt;
&lt;td&gt;Unsuccessful net approach Player 2&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2223&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;At net Player 2&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2224&lt;/td&gt;
&lt;td&gt;T_AT_BASELINE2&lt;/td&gt;
&lt;td&gt;At baseline Player 2&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2225&lt;/td&gt;
&lt;td&gt;T_SET_BALL2&lt;/td&gt;
&lt;td&gt;Set ball Player 2&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2226&lt;/td&gt;
&lt;td&gt;T_MATCH_BALL2&lt;/td&gt;
&lt;td&gt;Match ball Player 2&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;/details&gt;
&lt;p&gt;&lt;strong&gt;PBP = Play-by-Play（逐回合 / 逐事件数据）&lt;/strong&gt;，就是把一场比赛里每一次事件都按时间线秒级记录下来的最细粒度原始数据流。口语里也叫 “逐帧数据 / 流水账数据”。网球赛事的数据，都是以这个形式下发的，一段数据可能下发的数据是这样的：&lt;/p&gt;</description></item><item><title>3D球场项目 (四)</title><link>https://amatsuzero.github.io/LastStand/posts/cocostennis/part-four/</link><pubDate>Sat, 30 May 2026 20:23:44 +0800</pubDate><guid>https://amatsuzero.github.io/LastStand/posts/cocostennis/part-four/</guid><description>&lt;p&gt;项目终于来到了落地环节。
在&lt;a href="../part-three/"&gt;3D球场项目 (三)&lt;/a&gt;里，我们用一段离线
PBP 数据 + 一次 LLM 调用，把&amp;quot;导演 Agent&amp;quot;的雏形跑了起来。Demo 阶段的脚本是离线生成、整段
导入的；那时的 LLM 既负责&amp;quot;想象画面&amp;quot;，又顺手把动作、镜头、音效、解说词都打包了。&lt;/p&gt;
&lt;p&gt;但项目一旦从 Demo 推到线上，导演 Agent 立刻面对三件事：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Opta 数据是按 Point 持续推过来的，&lt;strong&gt;不会一次性把一场比赛喂给大模型&lt;/strong&gt;；&lt;/li&gt;
&lt;li&gt;大模型 &lt;strong&gt;会失败、会延迟&lt;/strong&gt;，一旦卡住，球员就只能停在 idle；&lt;/li&gt;
&lt;li&gt;客户端会断流、会重连，&lt;strong&gt;需要一个&amp;quot;刚才那一拍发生了什么&amp;quot;的回放接口&lt;/strong&gt;。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;上一章末尾提到的三个坑——player 卡 idle、两人一直来回发球、长链断了客户端不知道
怎么补——本质都来自这三件事。线上版本的 livecast 服务（基于 tRPC-Go）做了一次完整的
工程化：导演 Agent 不再是单一 LLM，而是 &lt;strong&gt;确定性脚本层 + LLM 解说层 + 双重下发保险&lt;/strong&gt;
的组合。本篇就讲这套组合怎么搭起来。&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id="一网球数据是怎么进来的"&gt;一、网球数据是怎么进来的&lt;/h2&gt;
&lt;p&gt;NBA 那边的 PBP 是 &lt;strong&gt;单条&lt;/strong&gt; 投递：上游每发生一个篮球事件，就经 Kafka 推一条消息过来。
但网球数据天然是 &lt;strong&gt;按回合聚合&lt;/strong&gt; 的——Opta 在一个 Point（一个回合）结束之后才会一次性
把这一拍涉及的所有事件下发，所以服务端也用了批量消费通道：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-gdscript3" data-lang="gdscript3"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;Kafka topic: pbp_flow
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#960050;background-color:#1e0010"&gt;└─&lt;/span&gt; FlowConsumer&lt;span style="color:#f92672"&gt;.&lt;/span&gt;BatchHandle(msgs []&lt;span style="color:#f92672"&gt;*&lt;/span&gt;sarama&lt;span style="color:#f92672"&gt;.&lt;/span&gt;ConsumerMessage)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#960050;background-color:#1e0010"&gt;└─&lt;/span&gt; livecastSvc&lt;span style="color:#f92672"&gt;.&lt;/span&gt;ProcessBatchPBPFlow(payloads [][]byte)
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;进入 &lt;code&gt;ProcessBatchPBPFlow&lt;/code&gt; 之后，先按 &lt;code&gt;mid&lt;/code&gt;（match id）做二级分组、过滤掉非网球的批次，
再交给真正的 pipeline。两道闸门决定一场比赛的事件能不能走 3D 流水线：&lt;/p&gt;</description></item><item><title>3D球场项目 (五)</title><link>https://amatsuzero.github.io/LastStand/posts/cocostennis/part-five/</link><pubDate>Sat, 30 May 2026 22:40:08 +0800</pubDate><guid>https://amatsuzero.github.io/LastStand/posts/cocostennis/part-five/</guid><description>&lt;p&gt;&lt;a href="../part-four/"&gt;3D球场项目 (四)&lt;/a&gt; 把服务端推到了线上：
导演 Agent 把每个 Point 切成 &lt;code&gt;SERVE / RALLY / CONCLUSION&lt;/code&gt; 三段，配齐了动作、机位、
位置、比分、解说，按 &lt;code&gt;seq&lt;/code&gt; 单调下发到客户端。本篇回到客户端，看 Cocos 这一侧拿到 Script
之后，是怎么把它变成屏幕上一拍接一拍的比赛画面的。&lt;/p&gt;
&lt;p&gt;整个客户端改造分四层落地，本篇只讲和 3D 球场主干业务直接相关的内容：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;CocosEngine 层&lt;/strong&gt;：场景加载架构升级（进度 / 传参 / Delegate&lt;sup id="fnref:1"&gt;&lt;a href="#fn:1" class="footnote-ref" role="doc-noteref"&gt;1&lt;/a&gt;&lt;/sup&gt; 回调 / JSBridge&lt;sup id="fnref:2"&gt;&lt;a href="#fn:2" class="footnote-ref" role="doc-noteref"&gt;2&lt;/a&gt;&lt;/sup&gt;
内存语义）；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;MagicDanmaku 层&lt;/strong&gt;：3D 球场的核心业务代码（数据流、状态机、动画 / 球轨迹 / 相机 /
观众）；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;MagicDanmaku iOS 层&lt;/strong&gt;：场景加载回调的演化，以及 &lt;code&gt;CocosPlayer&lt;/code&gt; 在调用链里的角色；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;业务层&lt;/strong&gt;：腾讯体育 App 的 SwiftUI&lt;sup id="fnref:3"&gt;&lt;a href="#fn:3" class="footnote-ref" role="doc-noteref"&gt;3&lt;/a&gt;&lt;/sup&gt; 视图把上面三层包成一个标准组件。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;每一层和业务流程脱钩的内容（性能优化、稳定性 / crash 修复、构建工程化）统一抽到了
&lt;a href="../part-six/"&gt;3D球场项目 (六)：客户端工程化与优化&lt;/a&gt;。&lt;/p&gt;
&lt;p&gt;下面这张图先把四层的位置关系和数据流向摆出来，读者可以把它当作整篇文章的导航——
&lt;strong&gt;箭头是数据流方向&lt;/strong&gt;（服务端 Script 自顶向下灌进引擎、引擎事件自底向上回到 SwiftUI），
每个节点旁标了对应章节：&lt;/p&gt;
&lt;div class="arch-diagram arch-pf5"&gt;
&lt;style&gt;
.arch-pf5 { font-size: 13px; color: #222; margin: 1.2em 0; max-width: 100%; overflow-x: auto; background: #f0f4f8; border: 1.5px solid #aab4c2; border-radius: 8px; padding: 14px 16px 16px; }
.arch-pf5 .layer {
background: #f0f4f8;
border: 1.5px solid #aab4c2;
border-radius: 8px;
padding: 14px 16px 16px;
margin: 10px 0;
position: relative;
}
.arch-pf5 .layer-title {
font-weight: 600;
color: #334155;
margin-bottom: 4px;
font-size: 14px;
}
.arch-pf5 .layer-sub {
color: #64748b;
font-size: 12px;
margin-bottom: 10px;
}
.arch-pf5 .row { display: flex; gap: 10px; flex-wrap: wrap; align-items: center; }
.arch-pf5 .node {
background: #fff;
border: 1px solid #8b8b8b;
border-radius: 18px;
padding: 6px 14px;
min-width: 60px;
text-align: center;
box-shadow: 0 1px 1px rgba(0,0,0,0.03);
flex: 1 1 auto;
white-space: nowrap;
}
.arch-pf5 .node.wrap {
white-space: normal;
line-height: 1.35;
padding: 8px 12px;
}
.arch-pf5 .node.entry {
background: #fef3c7;
border-color: #d97706;
color: #78350f;
font-weight: 600;
}
.arch-pf5 .node.event {
background: #ede9fe;
border-color: #7c3aed;
color: #4c1d95;
}
.arch-pf5 .group {
background: #fafbfc;
border: 1px dashed #9aa4b2;
border-radius: 8px;
padding: 12px;
flex: 1;
min-width: 200px;
display: flex;
flex-direction: column;
}
.arch-pf5 .group-title {
font-weight: 600;
color: #475569;
margin-bottom: 8px;
font-size: 13px;
}
.arch-pf5 .arrow {
text-align: center;
color: #94a3b8;
font-size: 18px;
line-height: 1;
margin: 4px 0;
user-select: none;
}
.arch-pf5 .arrow.down::before { content: "▼ 推数据"; font-size: 12px; color: #d97706; }
.arch-pf5 .arrow.up::before { content: "▲ 回事件"; font-size: 12px; color: #7c3aed; }
.arch-pf5 .arrow.bidi::before { content: "▲▼ 双向"; font-size: 12px; color: #475569; }
.arch-pf5 .app-row { display: flex; justify-content: center; }
.arch-pf5 .app-row .node { flex: 0 0 auto; min-width: 140px; padding: 8px 24px; }
.arch-pf5 .legend {
display: flex; gap: 14px; justify-content: center; align-items: center;
margin-bottom: 10px; font-size: 12px; color: #475569; flex-wrap: wrap;
}
.arch-pf5 .legend .node { flex: 0 0 auto; min-width: 0; padding: 4px 12px; font-size: 12px; }
&lt;pre&gt;&lt;code&gt;@media (max-width: 900px) {
.arch-pf5 { font-size: 12px; }
.arch-pf5 .layer { padding: 10px 10px 12px; }
.arch-pf5 .layer-title { font-size: 13px; }
.arch-pf5 .row { gap: 6px; }
.arch-pf5 .row &amp;gt; .node {
flex: 1 1 calc(33.333% - 6px);
padding: 5px 6px;
min-width: 0;
font-size: 11.5px;
white-space: normal;
line-height: 1.3;
word-break: break-word;
}
.arch-pf5 .group { padding: 8px; min-width: 0; }
.arch-pf5 .app-row .node { min-width: 0; padding: 6px 18px; flex: 0 0 auto; white-space: nowrap; }
}
@media (max-width: 420px) {
.arch-pf5 .row &amp;gt; .node { flex: 1 1 calc(50% - 6px); }
}
@media (prefers-color-scheme: dark) {
.arch-pf5 { color: #e2e8f0; background: #0f172a; border-color: #475569; }
.arch-pf5 .layer { background: #1e293b; border-color: #475569; }
.arch-pf5 .group { background: #0f172a; border-color: #475569; }
.arch-pf5 .node { background: #0f172a; border-color: #94a3b8; color: #e2e8f0; }
.arch-pf5 .node.entry { background: #78350f; border-color: #fbbf24; color: #fef3c7; }
.arch-pf5 .node.event { background: #4c1d95; border-color: #a78bfa; color: #ede9fe; }
.arch-pf5 .layer-title, .arch-pf5 .layer-sub, .arch-pf5 .group-title, .arch-pf5 .legend { color: #cbd5e1; }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;/style&gt;&lt;/p&gt;</description></item><item><title>3D球场项目 (六)：客户端工程化与优化</title><link>https://amatsuzero.github.io/LastStand/posts/cocostennis/part-six/</link><pubDate>Sun, 31 May 2026 09:00:00 +0800</pubDate><guid>https://amatsuzero.github.io/LastStand/posts/cocostennis/part-six/</guid><description>&lt;p&gt;&lt;a href="../part-five/"&gt;3D球场项目 (五)&lt;/a&gt; 把客户端的核心业务流
讲完了：Native 推送 → DataManager 队列 → ScenePlayer 章节调度 → GameController 状态机
→ Handler → 渲染层。功能闭环跑通之后，真正的硬仗是 &lt;strong&gt;&amp;ldquo;工程化&amp;quot;和&amp;quot;性能&amp;rdquo;&lt;/strong&gt;——前者保证
项目能稳定上线（编译跑通、退出不崩、Swift 调用顺手），后者保证在中低端机上画面流畅。&lt;/p&gt;
&lt;p&gt;第一版上线时遇到了两类问题：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;稳定性 / 工程化&lt;/strong&gt;：modulemap 编译报错、lipo 架构冲突、A11 设备启动 crash、
Cocos 退出期 OpenAL 资源竞争 crash……这些都不是&amp;quot;画面卡&amp;quot;，但每一个都能让用户体验
归零；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;性能&lt;/strong&gt;：中端 iPhone 在比赛回合渲染期间，GPU 和 CPU 都偶尔抖到 80%+，帧率掉到
30 fps 出头。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;本篇按这两类拆成两部分，所有改动都遵循一条原则：&lt;strong&gt;不动玩法，只把工程问题和帧时间
解决掉&lt;/strong&gt;。&lt;/p&gt;
&lt;h2 id="架构改动一览"&gt;架构改动一览&lt;/h2&gt;
&lt;p&gt;&lt;a href="../part-two/"&gt;3D球场项目 (二)&lt;/a&gt; 介绍过弹幕引擎的四层
架构（腾讯视频 APP / MagicDanmakuiOS / MagicDanmaku / cocos-engine）。这一阶段
&lt;strong&gt;每一层都动了&lt;/strong&gt;——3D 球场不是单点改造，而是一次自上而下的纵贯。下面这张图是把
PartTwo 那张原始架构图重画一遍，&lt;strong&gt;改动的节点用蓝色&lt;/strong&gt;、&lt;strong&gt;新增的节点用绿色&lt;/strong&gt;，
没动的节点保持白色：&lt;/p&gt;
&lt;div class="arch-diagram arch-diff"&gt;
&lt;style&gt;
.arch-diff { font-size: 13px; color: #222; margin: 1.2em 0; max-width: 100%; overflow-x: auto; background: #f0f4f8; border: 1.5px solid #aab4c2; border-radius: 8px; padding: 14px 16px 16px; }
.arch-diff .layer {
background: #f0f4f8;
border: 1.5px solid #aab4c2;
border-radius: 8px;
padding: 14px 16px 16px;
margin: 10px 0;
position: relative;
}
.arch-diff .layer-title {
font-weight: 600;
color: #334155;
margin-bottom: 10px;
font-size: 14px;
}
.arch-diff .row { display: flex; gap: 10px; flex-wrap: wrap; align-items: center; }
.arch-diff .node {
background: #fff;
border: 1px solid #8b8b8b;
border-radius: 18px;
padding: 6px 14px;
min-width: 60px;
text-align: center;
box-shadow: 0 1px 1px rgba(0,0,0,0.03);
flex: 1 1 auto;
white-space: nowrap;
}
.arch-diff .node.changed {
background: #dbeafe;
border-color: #2563eb;
color: #1e3a8a;
}
.arch-diff .node.added {
background: #dcfce7;
border-color: #16a34a;
color: #14532d;
font-weight: 600;
}
.arch-diff .node.wrap {
white-space: normal;
line-height: 1.35;
padding: 8px 12px;
}
.arch-diff .group {
background: #fafbfc;
border: 1px dashed #9aa4b2;
border-radius: 8px;
padding: 12px;
flex: 1;
min-width: 200px;
display: flex;
flex-direction: column;
}
.arch-diff .group-title {
font-weight: 600;
color: #475569;
margin-bottom: 8px;
font-size: 13px;
}
.arch-diff .inner {
background: #fff;
border: 1px solid #c5ccd6;
border-radius: 6px;
padding: 10px;
margin-bottom: 8px;
}
.arch-diff .inner:last-child { margin-bottom: 0; }
.arch-diff .inner-title {
color: #475569;
font-size: 12px;
text-align: center;
margin-bottom: 6px;
}
.arch-diff .arrow {
text-align: center;
color: #94a3b8;
font-size: 18px;
line-height: 1;
margin: 4px 0;
user-select: none;
}
.arch-diff .app-row { display: flex; justify-content: center; }
.arch-diff .app-row .node { flex: 0 0 auto; min-width: 120px; padding: 8px 24px; }
.arch-diff .md-row { display: flex; gap: 12px; align-items: flex-start; flex-wrap: wrap; }
.arch-diff .md-row &gt; .group:first-child { flex: 2.4 1 360px; }
.arch-diff .md-row &gt; .group:last-child { flex: 1 1 200px; }
.arch-diff .ce-row { display: flex; gap: 10px; align-items: flex-start; flex-wrap: wrap; }
.arch-diff .ce-row .nodes { display: flex; gap: 10px; flex: 1 1 280px; flex-wrap: wrap; }
.arch-diff .ce-row .ext-group { flex: 1 1 38%; min-width: 220px; }
.arch-diff .legend {
display: flex;
gap: 14px;
justify-content: center;
align-items: center;
margin-bottom: 10px;
font-size: 12px;
color: #475569;
flex-wrap: wrap;
}
.arch-diff .legend .node {
flex: 0 0 auto;
min-width: 0;
padding: 4px 12px;
font-size: 12px;
}
&lt;pre&gt;&lt;code&gt;@media (max-width: 900px) {
.arch-diff { font-size: 12px; }
.arch-diff .layer { padding: 10px 10px 12px; }
.arch-diff .layer-title { font-size: 13px; margin-bottom: 8px; }
.arch-diff .row { gap: 6px; }
.arch-diff .row &amp;gt; .node {
flex: 1 1 calc(33.333% - 6px);
padding: 5px 6px;
min-width: 0;
font-size: 11.5px;
white-space: normal;
line-height: 1.3;
word-break: break-word;
}
.arch-diff .node.wrap { padding: 6px 8px; line-height: 1.3; }
.arch-diff .group { padding: 8px; min-width: 0; }
.arch-diff .inner { padding: 8px; }
.arch-diff .app-row .node { min-width: 0; padding: 6px 18px; flex: 0 0 auto; white-space: nowrap; }
.arch-diff .md-row { flex-direction: column; gap: 8px; }
.arch-diff .md-row &amp;gt; .group:first-child,
.arch-diff .md-row &amp;gt; .group:last-child { flex: 1 1 auto; width: 100%; }
.arch-diff .ce-row { flex-direction: column; gap: 8px; }
.arch-diff .ce-row .nodes { flex: 1 1 auto; width: 100%; }
.arch-diff .ce-row .ext-group { flex: 1 1 auto; width: 100%; min-width: 0; }
}
@media (max-width: 420px) {
.arch-diff .row &amp;gt; .node { flex: 1 1 calc(50% - 6px); }
}
@media (prefers-color-scheme: dark) {
.arch-diff { color: #e2e8f0; background: #0f172a; border-color: #475569; }
.arch-diff .layer { background: #1e293b; border-color: #475569; }
.arch-diff .group { background: #0f172a; border-color: #475569; }
.arch-diff .inner { background: #1e293b; border-color: #475569; }
.arch-diff .node { background: #0f172a; border-color: #94a3b8; color: #e2e8f0; }
.arch-diff .node.changed { background: #1e3a8a; border-color: #60a5fa; color: #dbeafe; }
.arch-diff .node.added { background: #14532d; border-color: #4ade80; color: #dcfce7; }
.arch-diff .layer-title, .arch-diff .group-title, .arch-diff .inner-title { color: #cbd5e1; }
.arch-diff .legend { color: #cbd5e1; }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;/style&gt;&lt;/p&gt;</description></item><item><title>Cocos 引擎 iOS 渲染管线深度解析：从 CADisplayLink 到屏幕呈现</title><link>https://amatsuzero.github.io/LastStand/posts/cocosengine-ios-rendering/</link><pubDate>Wed, 08 Jul 2026 19:00:15 +0800</pubDate><guid>https://amatsuzero.github.io/LastStand/posts/cocosengine-ios-rendering/</guid><description>&lt;h1 id="cocos-引擎-ios-渲染管线深度解析从-cadisplaylink-到屏幕呈现"&gt;Cocos 引擎 iOS 渲染管线深度解析：从 CADisplayLink 到屏幕呈现&lt;/h1&gt;
&lt;h2 id="引言"&gt;引言&lt;/h2&gt;
&lt;p&gt;在移动端图形开发中，理解渲染管线的底层实现是走向高级工程师的必经之路。Cocos Creator 作为主流的跨平台游戏引擎，其 iOS Metal 后端的渲染实现采用了现代化的 FrameGraph 架构，结合脏状态追踪、资源池化等优化手段，是一份极佳的学习样本。&lt;/p&gt;
&lt;p&gt;本文基于对 Cocos 引擎 &lt;code&gt;gfx-metal&lt;/code&gt; 模块的源码分析，梳理从 &lt;code&gt;CADisplayLink&lt;/code&gt; 帧回调触发到最终 GPU 呈现的完整渲染链路，并深入解读各阶段的关键实现细节。&lt;/p&gt;
&lt;h2 id="整体架构概览"&gt;整体架构概览&lt;/h2&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;strong&gt;IOSPlatform&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;IOSPlatform.mm&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;iOS 平台入口，持有 &lt;code&gt;CADisplayLink&lt;/code&gt; 驱动渲染循环&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;RenderPipeline&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;RenderPipeline.cpp&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;管线编排器，遍历相机并调度 Flow/Stage&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;ForwardFlow&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;ForwardFlow.cpp&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;前向渲染流程，决定渲染策略&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;ForwardStage&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;ForwardStage.cpp&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;前向渲染阶段，收集渲染对象并填充 UBO&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;RenderQueue&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;RenderQueue.cpp&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;渲染队列，对可渲染对象排序&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;FrameGraph&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;FrameGraph.cpp&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;帧图调度器，负责 Pass 编译、排序、合并和执行&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;CCMTLDevice&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;MTLDevice.mm&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Metal 设备抽象，管理 Swapchain 和 Queue&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;CCMTLSwapchain&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;MTLSwapchain.mm&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;交换链，管理 &lt;code&gt;CAMetalLayer&lt;/code&gt; 和 drawable&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;CCMTLCommandBuffer&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;MTLCommandBuffer.mm&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;命令缓冲区封装，核心渲染编码入口&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;CCMTLRenderCommandEncoder&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;MTLRenderCommandEncoder.h&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;渲染编码器，含脏状态追踪优化&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h2 id="完整渲染时序图"&gt;完整渲染时序图&lt;/h2&gt;
&lt;p&gt;以下 Mermaid 时序图展示了从屏幕刷新信号到最终呈现的五个阶段：&lt;/p&gt;</description></item></channel></rss>