<?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>VAT on Last Stand</title><link>https://amatsuzero.github.io/LastStand/tags/vat/</link><description>Recent content in VAT on Last Stand</description><generator>Hugo</generator><language>zh-cn</language><lastBuildDate>Sun, 31 May 2026 09:00:00 +0800</lastBuildDate><atom:link href="https://amatsuzero.github.io/LastStand/tags/vat/index.xml" rel="self" type="application/rss+xml"/><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></channel></rss>