<?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>Godot on 山高月小</title><link>https://blog.dart.xin/categories/godot/</link><description>Recent content in Godot on 山高月小</description><generator>Hugo</generator><language>zh-cn</language><copyright>zwdart</copyright><lastBuildDate>Wed, 04 Nov 2026 09:56:14 +0800</lastBuildDate><atom:link href="https://blog.dart.xin/categories/godot/index.xml" rel="self" type="application/rss+xml"/><item><title>Godot AnimationTree</title><link>https://blog.dart.xin/godot/gd104-animationtree/</link><pubDate>Wed, 04 Nov 2026 09:56:14 +0800</pubDate><guid>https://blog.dart.xin/godot/gd104-animationtree/</guid><description>&lt;p&gt;&lt;code&gt;AnimationTree&lt;/code&gt; 是 Godot 动画系统的&lt;strong&gt;决策层&lt;/strong&gt;。它本身不包含任何动画数据，而是指向一个已有的 &lt;code&gt;AnimationPlayer&lt;/code&gt;，在运行时根据你的逻辑（状态切换、输入方向、速度值）动态决定“现在该播放什么、如何混合”。当你发现需要在多条动画之间&lt;strong&gt;平滑过渡&lt;/strong&gt;、&lt;strong&gt;按方向混合&lt;/strong&gt;，或在基础动画上&lt;strong&gt;叠加一次性动作&lt;/strong&gt;时，就该引入 &lt;code&gt;AnimationTree&lt;/code&gt; 了。&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id="-三种核心组织方式先选对大脑"&gt;🧭 三种核心组织方式：先选对“大脑”&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;AnimationTree&lt;/code&gt; 的根节点决定了它的调度方式，最常用的是以下三种：&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th style="text-align: left"&gt;根节点类型&lt;/th&gt;
&lt;th style="text-align: left"&gt;解决的问题&lt;/th&gt;
&lt;th style="text-align: left"&gt;典型场景&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style="text-align: left"&gt;&lt;strong&gt;&lt;code&gt;AnimationNodeStateMachine&lt;/code&gt;&lt;/strong&gt;&lt;/td&gt;
&lt;td style="text-align: left"&gt;离散状态的切换与过渡&lt;/td&gt;
&lt;td style="text-align: left"&gt;&lt;code&gt;idle&lt;/code&gt; ↔ &lt;code&gt;walk&lt;/code&gt; ↔ &lt;code&gt;jump&lt;/code&gt; ↔ &lt;code&gt;attack&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style="text-align: left"&gt;&lt;strong&gt;&lt;code&gt;AnimationNodeBlendSpace2D&lt;/code&gt;&lt;/strong&gt;&lt;/td&gt;
&lt;td style="text-align: left"&gt;连续方向/速度的混合&lt;/td&gt;
&lt;td style="text-align: left"&gt;8 方向移动、&lt;code&gt;walk&lt;/code&gt;→&lt;code&gt;run&lt;/code&gt; 的速度混合&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style="text-align: left"&gt;&lt;strong&gt;&lt;code&gt;AnimationNodeBlendTree&lt;/code&gt;&lt;/strong&gt;&lt;/td&gt;
&lt;td style="text-align: left"&gt;分层与叠加（配合 &lt;code&gt;OneShot&lt;/code&gt; 等节点）&lt;/td&gt;
&lt;td style="text-align: left"&gt;走路时开枪、受击闪烁叠加在基础动画上&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;实际项目中，&lt;strong&gt;状态机与混合空间通常是嵌套配合的&lt;/strong&gt;：状态机负责“当前处于什么大状态”（如 &lt;code&gt;Locomotion&lt;/code&gt;），而该状态内部的具体方向或速度变化，交给嵌套的 &lt;code&gt;BlendSpace2D&lt;/code&gt; 处理。&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id="-前置设置两步让树跑起来"&gt;⚙️ 前置设置：两步让树跑起来&lt;/h3&gt;
&lt;p&gt;在写任何代码之前，&lt;code&gt;AnimationTree&lt;/code&gt; 需要两个明确的配置，否则它不会驱动任何东西：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;anim_player&lt;/code&gt; 指向数据源&lt;/strong&gt;：在检查器中将 &lt;code&gt;AnimationTree&lt;/code&gt; 的 &lt;code&gt;anim_player&lt;/code&gt; 属性指向场景中已有的 &lt;code&gt;AnimationPlayer&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;active = true&lt;/code&gt;&lt;/strong&gt;：&lt;code&gt;AnimationTree&lt;/code&gt; 默认不激活，必须在检查器中勾选，或在 &lt;code&gt;_ready()&lt;/code&gt; 中设置 &lt;code&gt;active = true&lt;/code&gt;。&lt;/li&gt;
&lt;/ol&gt;
&lt;hr&gt;
&lt;h3 id="-示例一最简状态机--travel-事件驱动"&gt;🧩 示例一：最简状态机 —— &lt;code&gt;travel()&lt;/code&gt; 事件驱动&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;code&gt;AnimationTree&lt;/code&gt; 编辑器中创建 &lt;code&gt;AnimationNodeStateMachine&lt;/code&gt;，添加状态并连线）：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;span class="lnt"&gt;2
&lt;/span&gt;&lt;span class="lnt"&gt;3
&lt;/span&gt;&lt;span class="lnt"&gt;4
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;StateMachine
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;├── Idle
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;├── Walk
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;└── Jump
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;&lt;strong&gt;关键代码&lt;/strong&gt;：状态机播放控制器通过 &lt;code&gt;playback&lt;/code&gt; 参数获取，&lt;code&gt;travel()&lt;/code&gt; 会&lt;strong&gt;立即中断&lt;/strong&gt;当前状态并转向目标状态，并自动寻找最短路径。&lt;/p&gt;</description><content:encoded><![CDATA[<p><code>AnimationTree</code> 是 Godot 动画系统的<strong>决策层</strong>。它本身不包含任何动画数据，而是指向一个已有的 <code>AnimationPlayer</code>，在运行时根据你的逻辑（状态切换、输入方向、速度值）动态决定“现在该播放什么、如何混合”。当你发现需要在多条动画之间<strong>平滑过渡</strong>、<strong>按方向混合</strong>，或在基础动画上<strong>叠加一次性动作</strong>时，就该引入 <code>AnimationTree</code> 了。</p>
<hr>
<h3 id="-三种核心组织方式先选对大脑">🧭 三种核心组织方式：先选对“大脑”</h3>
<p><code>AnimationTree</code> 的根节点决定了它的调度方式，最常用的是以下三种：</p>
<table>
	<thead>
			<tr>
					<th style="text-align: left">根节点类型</th>
					<th style="text-align: left">解决的问题</th>
					<th style="text-align: left">典型场景</th>
			</tr>
	</thead>
	<tbody>
			<tr>
					<td style="text-align: left"><strong><code>AnimationNodeStateMachine</code></strong></td>
					<td style="text-align: left">离散状态的切换与过渡</td>
					<td style="text-align: left"><code>idle</code> ↔ <code>walk</code> ↔ <code>jump</code> ↔ <code>attack</code></td>
			</tr>
			<tr>
					<td style="text-align: left"><strong><code>AnimationNodeBlendSpace2D</code></strong></td>
					<td style="text-align: left">连续方向/速度的混合</td>
					<td style="text-align: left">8 方向移动、<code>walk</code>→<code>run</code> 的速度混合</td>
			</tr>
			<tr>
					<td style="text-align: left"><strong><code>AnimationNodeBlendTree</code></strong></td>
					<td style="text-align: left">分层与叠加（配合 <code>OneShot</code> 等节点）</td>
					<td style="text-align: left">走路时开枪、受击闪烁叠加在基础动画上</td>
			</tr>
	</tbody>
</table>
<p>实际项目中，<strong>状态机与混合空间通常是嵌套配合的</strong>：状态机负责“当前处于什么大状态”（如 <code>Locomotion</code>），而该状态内部的具体方向或速度变化，交给嵌套的 <code>BlendSpace2D</code> 处理。</p>
<hr>
<h3 id="-前置设置两步让树跑起来">⚙️ 前置设置：两步让树跑起来</h3>
<p>在写任何代码之前，<code>AnimationTree</code> 需要两个明确的配置，否则它不会驱动任何东西：</p>
<ol>
<li><strong><code>anim_player</code> 指向数据源</strong>：在检查器中将 <code>AnimationTree</code> 的 <code>anim_player</code> 属性指向场景中已有的 <code>AnimationPlayer</code>。</li>
<li><strong><code>active = true</code></strong>：<code>AnimationTree</code> 默认不激活，必须在检查器中勾选，或在 <code>_ready()</code> 中设置 <code>active = true</code>。</li>
</ol>
<hr>
<h3 id="-示例一最简状态机--travel-事件驱动">🧩 示例一：最简状态机 —— <code>travel()</code> 事件驱动</h3>
<p><strong>适用场景</strong>：由代码逻辑直接命令的状态切换，如跳跃、攻击、死亡。</p>
<p><strong>树的结构</strong>（在 <code>AnimationTree</code> 编辑器中创建 <code>AnimationNodeStateMachine</code>，添加状态并连线）：</p>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt">1
</span><span class="lnt">2
</span><span class="lnt">3
</span><span class="lnt">4
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">StateMachine
</span></span><span class="line"><span class="cl">├── Idle
</span></span><span class="line"><span class="cl">├── Walk
</span></span><span class="line"><span class="cl">└── Jump
</span></span></code></pre></td></tr></table>
</div>
</div><p><strong>关键代码</strong>：状态机播放控制器通过 <code>playback</code> 参数获取，<code>travel()</code> 会<strong>立即中断</strong>当前状态并转向目标状态，并自动寻找最短路径。</p>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt"> 1
</span><span class="lnt"> 2
</span><span class="lnt"> 3
</span><span class="lnt"> 4
</span><span class="lnt"> 5
</span><span class="lnt"> 6
</span><span class="lnt"> 7
</span><span class="lnt"> 8
</span><span class="lnt"> 9
</span><span class="lnt">10
</span><span class="lnt">11
</span><span class="lnt">12
</span><span class="lnt">13
</span><span class="lnt">14
</span><span class="lnt">15
</span><span class="lnt">16
</span><span class="lnt">17
</span><span class="lnt">18
</span><span class="lnt">19
</span><span class="lnt">20
</span><span class="lnt">21
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-gdscript" data-lang="gdscript"><span class="line"><span class="cl"><span class="kd">extends</span> <span class="nc">CharacterBody2D</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="nd">@onready</span> <span class="kd">var</span> <span class="n">anim_tree</span><span class="p">:</span> <span class="nc">AnimationTree</span> <span class="o">=</span> <span class="nx">$AnimationTree</span>
</span></span><span class="line"><span class="cl"><span class="nd">@onready</span> <span class="kd">var</span> <span class="n">state_machine</span><span class="p">:</span> <span class="nc">AnimationNodeStateMachinePlayback</span> <span class="o">=</span> <span class="n">anim_tree</span><span class="o">.</span><span class="nf">get</span><span class="p">(</span><span class="s2">&#34;parameters/StateMachine/playback&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="kd">func</span> <span class="nf">_physics_process</span><span class="p">(</span><span class="n">_delta</span><span class="p">:</span> <span class="kt">float</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="kt">void</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">    <span class="kd">var</span> <span class="n">input_dir</span> <span class="o">:=</span> <span class="nc">Input</span><span class="o">.</span><span class="nf">get_vector</span><span class="p">(</span><span class="s2">&#34;ui_left&#34;</span><span class="p">,</span> <span class="s2">&#34;ui_right&#34;</span><span class="p">,</span> <span class="s2">&#34;ui_up&#34;</span><span class="p">,</span> <span class="s2">&#34;ui_down&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="kd">var</span> <span class="n">target_state</span><span class="p">:</span> <span class="nc">String</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="k">if</span> <span class="nc">Input</span><span class="o">.</span><span class="nf">is_action_just_pressed</span><span class="p">(</span><span class="s2">&#34;jump&#34;</span><span class="p">):</span>
</span></span><span class="line"><span class="cl">        <span class="n">state_machine</span><span class="o">.</span><span class="nf">travel</span><span class="p">(</span><span class="s2">&#34;Jump&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">        <span class="k">return</span>  <span class="c1"># 这一帧不再处理移动状态，让跳跃优先</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="k">if</span> <span class="n">input_dir</span> <span class="o">!=</span> <span class="nc">Vector2</span><span class="o">.</span><span class="n">ZERO</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">        <span class="n">target_state</span> <span class="o">=</span> <span class="s2">&#34;Walk&#34;</span>
</span></span><span class="line"><span class="cl">    <span class="k">else</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">        <span class="n">target_state</span> <span class="o">=</span> <span class="s2">&#34;Idle&#34;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="c1"># 避免每帧重复 travel 同一状态（travel 到当前状态通常不会重置）</span>
</span></span><span class="line"><span class="cl">    <span class="k">if</span> <span class="n">state_machine</span><span class="o">.</span><span class="nf">get_current_node</span><span class="p">()</span> <span class="o">!=</span> <span class="n">target_state</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">        <span class="n">state_machine</span><span class="o">.</span><span class="nf">travel</span><span class="p">(</span><span class="n">target_state</span><span class="p">)</span>
</span></span></code></pre></td></tr></table>
</div>
</div><p><strong>要点</strong>：<code>travel()</code> 适合<strong>事件</strong>（一次性、需要立即打断当前动作的情况）；对于 <code>idle</code> ↔ <code>walk</code> 这类<strong>连续状态</strong>，下一节的“条件驱动”是更稳健的选择。</p>
<hr>
<h3 id="-示例二条件驱动状态切换--声明式过渡">🧩 示例二：条件驱动状态切换 —— 声明式过渡</h3>
<p><strong>适用场景</strong>：<code>idle</code> ↔ <code>walk</code> 这类由持续状态（速度是否为零）决定的切换，不希望用命令式的 <code>travel()</code> 每帧去“推”。</p>
<p><strong>树的结构</strong>：在状态机的过渡线（Transition）上，设置 <strong>Advance Condition</strong>，填入一个布尔参数名：</p>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt">1
</span><span class="lnt">2
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">Idle → Walk 的过渡线：Advance Condition = &#34;moving&#34;
</span></span><span class="line"><span class="cl">Walk → Idle 的过渡线：Advance Condition = &#34;not_moving&#34;
</span></span></code></pre></td></tr></table>
</div>
</div><p><strong>关键代码</strong>：每一帧写入条件参数，条件满足时状态机<strong>自动</strong>完成过渡。</p>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt"> 1
</span><span class="lnt"> 2
</span><span class="lnt"> 3
</span><span class="lnt"> 4
</span><span class="lnt"> 5
</span><span class="lnt"> 6
</span><span class="lnt"> 7
</span><span class="lnt"> 8
</span><span class="lnt"> 9
</span><span class="lnt">10
</span><span class="lnt">11
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-gdscript" data-lang="gdscript"><span class="line"><span class="cl"><span class="kd">extends</span> <span class="nc">CharacterBody2D</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="nd">@onready</span> <span class="kd">var</span> <span class="n">anim_tree</span><span class="p">:</span> <span class="nc">AnimationTree</span> <span class="o">=</span> <span class="nx">$AnimationTree</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="kd">func</span> <span class="nf">_physics_process</span><span class="p">(</span><span class="n">_delta</span><span class="p">:</span> <span class="kt">float</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="kt">void</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">    <span class="kd">var</span> <span class="n">input_dir</span> <span class="o">:=</span> <span class="nc">Input</span><span class="o">.</span><span class="nf">get_vector</span><span class="p">(</span><span class="s2">&#34;ui_left&#34;</span><span class="p">,</span> <span class="s2">&#34;ui_right&#34;</span><span class="p">,</span> <span class="s2">&#34;ui_up&#34;</span><span class="p">,</span> <span class="s2">&#34;ui_down&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="kd">var</span> <span class="n">is_moving</span> <span class="o">:=</span> <span class="n">input_dir</span><span class="o">.</span><span class="n">x</span> <span class="o">!=</span> <span class="mf">0.0</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="c1"># 两个条件互斥，且每帧都写入，避免“残留 true”把状态机卡死</span>
</span></span><span class="line"><span class="cl">    <span class="n">anim_tree</span><span class="o">.</span><span class="nf">set</span><span class="p">(</span><span class="s2">&#34;parameters/moving&#34;</span><span class="p">,</span> <span class="n">is_moving</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="n">anim_tree</span><span class="o">.</span><span class="nf">set</span><span class="p">(</span><span class="s2">&#34;parameters/not_moving&#34;</span><span class="p">,</span> <span class="ow">not</span> <span class="n">is_moving</span><span class="p">)</span>
</span></span></code></pre></td></tr></table>
</div>
</div><p><strong>两条铁律</strong>：每个条件<strong>每帧都要写入</strong>（残留的 <code>true</code> 会让状态机永远卡在一个状态）；条件之间应当<strong>互斥</strong>，否则过渡结果会取决于节点在编辑器中的顺序，行为不可预测。</p>
<hr>
<h3 id="-示例三blendspace2d-方向混合--8-方向移动">🧩 示例三：<code>BlendSpace2D</code> 方向混合 —— 8 方向移动</h3>
<p><strong>适用场景</strong>：角色需要根据输入方向，在多个方向的动画之间<strong>连续插值</strong>，而不是硬切 8 个独立状态。</p>
<p><strong>树的结构</strong>：在状态机内创建一个 <code>Locomotion</code> 状态，将该状态的内容设为 <code>BlendSpace2D</code>，并在 2D 平面上放置各方向的动画点：</p>
<table>
	<thead>
			<tr>
					<th style="text-align: left">动画</th>
					<th style="text-align: left">Blend 坐标 (x, y)</th>
			</tr>
	</thead>
	<tbody>
			<tr>
					<td style="text-align: left"><code>walk_down</code></td>
					<td style="text-align: left"><code>(0, 0.65)</code></td>
			</tr>
			<tr>
					<td style="text-align: left"><code>walk_up</code></td>
					<td style="text-align: left"><code>(0, -0.65)</code></td>
			</tr>
			<tr>
					<td style="text-align: left"><code>walk_left</code></td>
					<td style="text-align: left"><code>(-0.65, 0)</code></td>
			</tr>
			<tr>
					<td style="text-align: left"><code>walk_right</code></td>
					<td style="text-align: left"><code>(0.65, 0)</code></td>
			</tr>
			<tr>
					<td style="text-align: left"><code>walk_down_right</code></td>
					<td style="text-align: left"><code>(0.45, 0.45)</code></td>
			</tr>
	</tbody>
</table>
<p><strong>关键代码</strong>：每一帧将输入方向写入 <code>blend_position</code>；<strong>停止移动时也要写入 <code>Vector2.ZERO</code></strong>，否则角色会“停在最后一次的方向”上，看起来像 idle 动画出了 bug。</p>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt"> 1
</span><span class="lnt"> 2
</span><span class="lnt"> 3
</span><span class="lnt"> 4
</span><span class="lnt"> 5
</span><span class="lnt"> 6
</span><span class="lnt"> 7
</span><span class="lnt"> 8
</span><span class="lnt"> 9
</span><span class="lnt">10
</span><span class="lnt">11
</span><span class="lnt">12
</span><span class="lnt">13
</span><span class="lnt">14
</span><span class="lnt">15
</span><span class="lnt">16
</span><span class="lnt">17
</span><span class="lnt">18
</span><span class="lnt">19
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-gdscript" data-lang="gdscript"><span class="line"><span class="cl"><span class="kd">extends</span> <span class="nc">CharacterBody2D</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="nd">@onready</span> <span class="kd">var</span> <span class="n">anim_tree</span><span class="p">:</span> <span class="nc">AnimationTree</span> <span class="o">=</span> <span class="nx">$AnimationTree</span>
</span></span><span class="line"><span class="cl"><span class="nd">@onready</span> <span class="kd">var</span> <span class="n">state_machine</span><span class="p">:</span> <span class="nc">AnimationNodeStateMachinePlayback</span> <span class="o">=</span> <span class="n">anim_tree</span><span class="o">.</span><span class="nf">get</span><span class="p">(</span><span class="s2">&#34;parameters/playback&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="kd">const</span> <span class="n">SPEED</span> <span class="o">:=</span> <span class="mf">200.0</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="kd">func</span> <span class="nf">_physics_process</span><span class="p">(</span><span class="n">_delta</span><span class="p">:</span> <span class="kt">float</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="kt">void</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">    <span class="kd">var</span> <span class="n">input_dir</span> <span class="o">:=</span> <span class="nc">Input</span><span class="o">.</span><span class="nf">get_vector</span><span class="p">(</span><span class="s2">&#34;ui_left&#34;</span><span class="p">,</span> <span class="s2">&#34;ui_right&#34;</span><span class="p">,</span> <span class="s2">&#34;ui_up&#34;</span><span class="p">,</span> <span class="s2">&#34;ui_down&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="c1"># 方向混合：每一帧都写入，停止时写 Vector2.ZERO</span>
</span></span><span class="line"><span class="cl">    <span class="n">anim_tree</span><span class="o">.</span><span class="nf">set</span><span class="p">(</span><span class="s2">&#34;parameters/Locomotion/blend_position&#34;</span><span class="p">,</span> <span class="n">input_dir</span><span class="o">.</span><span class="nf">normalized</span><span class="p">())</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="c1"># 条件驱动 Idle ↔ Locomotion（假设过渡条件已设好）</span>
</span></span><span class="line"><span class="cl">    <span class="n">anim_tree</span><span class="o">.</span><span class="nf">set</span><span class="p">(</span><span class="s2">&#34;parameters/moving&#34;</span><span class="p">,</span> <span class="n">input_dir</span> <span class="o">!=</span> <span class="nc">Vector2</span><span class="o">.</span><span class="n">ZERO</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="n">anim_tree</span><span class="o">.</span><span class="nf">set</span><span class="p">(</span><span class="s2">&#34;parameters/not_moving&#34;</span><span class="p">,</span> <span class="n">input_dir</span> <span class="o">==</span> <span class="nc">Vector2</span><span class="o">.</span><span class="n">ZERO</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="n">velocity</span> <span class="o">=</span> <span class="n">input_dir</span> <span class="o">*</span> <span class="n">SPEED</span>
</span></span><span class="line"><span class="cl">    <span class="nf">move_and_slide</span><span class="p">()</span>
</span></span></code></pre></td></tr></table>
</div>
</div><p><code>BlendSpace2D</code> 的坐标值<strong>不需要是单位长度</strong>，节点在混合时会自动归一化，但代码写入的坐标系必须与编辑器中放置动画点的坐标系一致。</p>
<hr>
<h3 id="-示例四oneshot-一次性叠加--走路时开枪">🧩 示例四：<code>OneShot</code> 一次性叠加 —— 走路时开枪</h3>
<p><strong>适用场景</strong>：在基础动画（走/跑/待机）之上，<strong>短暂播放一个上半身或全身动作</strong>，播完后自动返回基础动画，不打断底层状态。</p>
<p><strong>树的结构</strong>：将根节点改为 <code>AnimationNodeBlendTree</code>，放入一个 <code>OneShot</code> 节点，将基础动画接到 <code>OneShot</code> 的 <code>in</code> 端口，一次性动作（如 <code>shoot</code>）接到 <code>shot</code> 端口。</p>
<p><strong>关键代码</strong>：通过 <code>request</code> 参数触发一次性动画；<code>OneShot</code> 会在播完后<strong>自动清除请求</strong>并回到 <code>in</code> 端口的基础动画。</p>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt"> 1
</span><span class="lnt"> 2
</span><span class="lnt"> 3
</span><span class="lnt"> 4
</span><span class="lnt"> 5
</span><span class="lnt"> 6
</span><span class="lnt"> 7
</span><span class="lnt"> 8
</span><span class="lnt"> 9
</span><span class="lnt">10
</span><span class="lnt">11
</span><span class="lnt">12
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-gdscript" data-lang="gdscript"><span class="line"><span class="cl"><span class="kd">extends</span> <span class="nc">CharacterBody2D</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="nd">@onready</span> <span class="kd">var</span> <span class="n">anim_tree</span><span class="p">:</span> <span class="nc">AnimationTree</span> <span class="o">=</span> <span class="nx">$AnimationTree</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="kd">func</span> <span class="nf">_process</span><span class="p">(</span><span class="n">_delta</span><span class="p">:</span> <span class="kt">float</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="kt">void</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">    <span class="k">if</span> <span class="nc">Input</span><span class="o">.</span><span class="nf">is_action_just_pressed</span><span class="p">(</span><span class="s2">&#34;shoot&#34;</span><span class="p">):</span>
</span></span><span class="line"><span class="cl">        <span class="c1"># 触发一次性射击动画，播完后自动回到走路/待机</span>
</span></span><span class="line"><span class="cl">        <span class="n">anim_tree</span><span class="o">.</span><span class="nf">set</span><span class="p">(</span><span class="s2">&#34;parameters/OneShot/request&#34;</span><span class="p">,</span> <span class="nc">AnimationNodeOneShot</span><span class="o">.</span><span class="n">ONE_SHOT_REQUEST_FIRE</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="k">if</span> <span class="nc">Input</span><span class="o">.</span><span class="nf">is_action_just_pressed</span><span class="p">(</span><span class="s2">&#34;melee&#34;</span><span class="p">):</span>
</span></span><span class="line"><span class="cl">        <span class="c1"># 另一种一次性动作，同样叠加在基础动画上</span>
</span></span><span class="line"><span class="cl">        <span class="n">anim_tree</span><span class="o">.</span><span class="nf">set</span><span class="p">(</span><span class="s2">&#34;parameters/MeleeOneShot/request&#34;</span><span class="p">,</span> <span class="nc">AnimationNodeOneShot</span><span class="o">.</span><span class="n">ONE_SHOT_REQUEST_FIRE</span><span class="p">)</span>
</span></span></code></pre></td></tr></table>
</div>
</div><p>如果需要在一次性动画<strong>尚未播完时强制打断</strong>，可以使用 <code>ONE_SHOT_REQUEST_ABORT</code>（硬切）或 <code>ONE_SHOT_REQUEST_FADE_OUT</code>（淡出中断）。</p>
<hr>
<h3 id="-参数路径速查">📋 参数路径速查</h3>
<p><code>AnimationTree</code> 的所有控制都通过<strong>字符串路径</strong>读写参数，路径由编辑器中的节点名称决定：</p>
<table>
	<thead>
			<tr>
					<th style="text-align: left">操作</th>
					<th style="text-align: left">路径写法</th>
			</tr>
	</thead>
	<tbody>
			<tr>
					<td style="text-align: left">获取状态机播放控制器</td>
					<td style="text-align: left"><code>&quot;parameters/StateMachine/playback&quot;</code>（或 <code>&quot;parameters/playback&quot;</code>，视层级而定）</td>
			</tr>
			<tr>
					<td style="text-align: left">触发状态机 <code>travel()</code></td>
					<td style="text-align: left"><code>state_machine.travel(&quot;StateName&quot;)</code></td>
			</tr>
			<tr>
					<td style="text-align: left">读写条件参数</td>
					<td style="text-align: left"><code>&quot;parameters/moving&quot;</code></td>
			</tr>
			<tr>
					<td style="text-align: left">设置 BlendSpace2D 位置</td>
					<td style="text-align: left"><code>&quot;parameters/Locomotion/blend_position&quot;</code></td>
			</tr>
			<tr>
					<td style="text-align: left">触发 OneShot</td>
					<td style="text-align: left"><code>&quot;parameters/OneShot/request&quot;</code></td>
			</tr>
	</tbody>
</table>
<p><strong>路径不匹配是 <code>AnimationTree</code> 最常见的“不工作”原因</strong>：检查器中的参数名必须与代码中的字符串<strong>完全一致</strong>（包括大小写和层级），否则设置会被静默忽略。</p>
<hr>
<h3 id="-三个高频陷阱">⚠️ 三个高频陷阱</h3>
<p><strong>① 条件残留</strong>：Advance Condition 对应的布尔参数如果某一帧没有被写入，它会保留上一帧的值。一个残留的 <code>true</code> 会让状态机永久卡死在某条过渡上。<strong>每个条件每帧都写入</strong>是唯一可靠的做法。</p>
<p><strong>② <code>travel()</code> 与条件混用时的竞争</strong>：<code>travel()</code> 是“排队”执行的，而条件过渡在下一帧就会生效。如果先用 <code>travel(&quot;Jump&quot;)</code>，但下一帧条件驱动又把状态拉回 <code>Locomotion</code>，跳跃动画可能根本来不及播放。<strong>事件用 <code>travel()</code>，持续状态用条件</strong>，不要用条件去抢 <code>travel()</code> 刚设置的目标。</p>
<p><strong>③ 忘记设置 <code>active = true</code></strong>：<code>AnimationTree</code> 创建后默认不激活，即使 <code>anim_player</code> 已正确指向，它也不会输出任何动画。</p>
<hr>
<h3 id="-学习路径小结">💎 学习路径小结</h3>
<p>从 <strong><code>travel()</code> 驱动的事件状态机</strong>开始，把 <code>idle → walk → jump</code> 跑通；然后引入 <strong>Advance Condition</strong> 替代 <code>travel()</code> 来管理 <code>idle ↔ walk</code>；再在一个 <code>Locomotion</code> 状态内部嵌入 <strong><code>BlendSpace2D</code></strong> 处理方向；最后用 <strong><code>OneShot</code></strong> 解决“在移动中叠加动作”的需求。每一步都只增加一个概念，而不是一次性把所有节点类型堆上去——<code>AnimationTree</code> 的复杂度来自<strong>参数路径的精确性</strong>和<strong>条件逻辑的严谨性</strong>，而不是节点本身的数量。</p>
]]></content:encoded></item><item><title>Godot AnimationPlayer</title><link>https://blog.dart.xin/godot/gd103-animationplayer/</link><pubDate>Wed, 04 Nov 2026 09:53:13 +0800</pubDate><guid>https://blog.dart.xin/godot/gd103-animationplayer/</guid><description>&lt;p&gt;&lt;code&gt;AnimationPlayer&lt;/code&gt; 是 Godot 动画系统的&lt;strong&gt;数据核心&lt;/strong&gt;。与“即用即弃”的 &lt;code&gt;Tween&lt;/code&gt; 不同，它把动画作为&lt;strong&gt;资源&lt;/strong&gt;保存在节点里，通过可视化的时间轴进行编排，可以在编辑器中精细调整节奏，再用代码决定何时播放哪一段。如果你的动画涉及多个属性、需要精确到帧的时序，或者包含音效、函数调用等非属性事件，&lt;code&gt;AnimationPlayer&lt;/code&gt; 是比 &lt;code&gt;Tween&lt;/code&gt; 更合适的选择。&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id="-先建立正确的场景结构"&gt;🧱 先建立正确的场景结构&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;AnimationPlayer&lt;/code&gt; 继承自&lt;strong&gt;裸 &lt;code&gt;Node&lt;/code&gt;&lt;/strong&gt;，而不是 &lt;code&gt;Node2D&lt;/code&gt; 或 &lt;code&gt;Node3D&lt;/code&gt;。这意味着它&lt;strong&gt;不传递变换&lt;/strong&gt;——如果把带有 2D/3D 变换的节点设为它的子节点，子节点不会继承父级的位移、旋转或缩放，视觉上会出现位置偏移或异常。&lt;/p&gt;
&lt;p&gt;正确的结构是让 &lt;code&gt;AnimationPlayer&lt;/code&gt; 与被动画的节点&lt;strong&gt;平级&lt;/strong&gt;：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;span class="lnt"&gt;2
&lt;/span&gt;&lt;span class="lnt"&gt;3
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;Player (CharacterBody2D)
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;├── Sprite2D
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;└── AnimationPlayer
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;&lt;code&gt;AnimationPlayer&lt;/code&gt; 可以通过&lt;strong&gt;轨道路径&lt;/strong&gt;驱动任意兄弟或子节点的属性：位置、旋转、缩放、&lt;code&gt;modulate&lt;/code&gt;、&lt;code&gt;visible&lt;/code&gt;、碰撞形状的 &lt;code&gt;disabled&lt;/code&gt; 状态，甚至方法调用和音频播放。&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id="-轨道类型不止是属性"&gt;🗂️ 轨道类型：不止是属性&lt;/h3&gt;
&lt;p&gt;在动画面板中点击“添加轨道”，你会看到多种类型。常用的是以下四种：&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th style="text-align: left"&gt;轨道类型&lt;/th&gt;
&lt;th style="text-align: left"&gt;作用&lt;/th&gt;
&lt;th style="text-align: left"&gt;典型用途&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style="text-align: left"&gt;&lt;strong&gt;属性轨道&lt;/strong&gt; (Property / Value)&lt;/td&gt;
&lt;td style="text-align: left"&gt;插值任意属性&lt;/td&gt;
&lt;td style="text-align: left"&gt;位置、旋转、透明度、着色器参数&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style="text-align: left"&gt;&lt;strong&gt;方法轨道&lt;/strong&gt; (Call Method)&lt;/td&gt;
&lt;td style="text-align: left"&gt;在精确时间点调用函数&lt;/td&gt;
&lt;td style="text-align: left"&gt;生成伤害判定、播放音效、切换状态&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style="text-align: left"&gt;&lt;strong&gt;音频轨道&lt;/strong&gt; (Audio Playback)&lt;/td&gt;
&lt;td style="text-align: left"&gt;将音频锁定在时间轴上&lt;/td&gt;
&lt;td style="text-align: left"&gt;脚步声与落脚点精确同步&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style="text-align: left"&gt;&lt;strong&gt;贝塞尔轨道&lt;/strong&gt; (Bezier)&lt;/td&gt;
&lt;td style="text-align: left"&gt;用曲线精细控制数值节奏&lt;/td&gt;
&lt;td style="text-align: left"&gt;非线性的 UI 动效、特殊缓动&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;&lt;strong&gt;方法轨道的关键配置&lt;/strong&gt;：默认的调用模式是 &lt;code&gt;CALL_MODE_CONTINUOUS&lt;/code&gt;，在关键帧前后拖拽播放头时可能&lt;strong&gt;每帧重复触发&lt;/strong&gt;。对于“伤害判定只该发生一次”的逻辑，必须在代码中将调用模式设为 &lt;code&gt;CALL_MODE_DISCRETE&lt;/code&gt;，确保函数在越过关键帧时&lt;strong&gt;只执行一次&lt;/strong&gt;。&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id="-代码控制播放混合与队列"&gt;▶️ 代码控制：播放、混合与队列&lt;/h3&gt;
&lt;h4 id="基础播放与状态驱动"&gt;基础播放与状态驱动&lt;/h4&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;span class="lnt"&gt;2
&lt;/span&gt;&lt;span class="lnt"&gt;3
&lt;/span&gt;&lt;span class="lnt"&gt;4
&lt;/span&gt;&lt;span class="lnt"&gt;5
&lt;/span&gt;&lt;span class="lnt"&gt;6
&lt;/span&gt;&lt;span class="lnt"&gt;7
&lt;/span&gt;&lt;span class="lnt"&gt;8
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-gdscript" data-lang="gdscript"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nd"&gt;@onready&lt;/span&gt; &lt;span class="kd"&gt;var&lt;/span&gt; &lt;span class="n"&gt;anim_player&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;AnimationPlayer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;$AnimationPlayer&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kd"&gt;func&lt;/span&gt; &lt;span class="nf"&gt;_physics_process&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;_delta&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;float&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="kt"&gt;void&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="kd"&gt;var&lt;/span&gt; &lt;span class="n"&gt;input_dir&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="nc"&gt;Input&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get_vector&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;ui_left&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;ui_right&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;ui_up&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;ui_down&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;input_dir&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="nc"&gt;Vector2&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ZERO&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;anim_player&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;play&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;walk&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;else&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;anim_player&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;play&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;idle&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;一个重要的细节：&lt;strong&gt;对正在播放的同一动画重复调用 &lt;code&gt;play()&lt;/code&gt; 不会重新开始&lt;/strong&gt;，也不会报错。因此可以在 &lt;code&gt;_physics_process&lt;/code&gt; 中放心地每帧调用 &lt;code&gt;play()&lt;/code&gt;，根据状态切换动画。&lt;/p&gt;</description><content:encoded><![CDATA[<p><code>AnimationPlayer</code> 是 Godot 动画系统的<strong>数据核心</strong>。与“即用即弃”的 <code>Tween</code> 不同，它把动画作为<strong>资源</strong>保存在节点里，通过可视化的时间轴进行编排，可以在编辑器中精细调整节奏，再用代码决定何时播放哪一段。如果你的动画涉及多个属性、需要精确到帧的时序，或者包含音效、函数调用等非属性事件，<code>AnimationPlayer</code> 是比 <code>Tween</code> 更合适的选择。</p>
<hr>
<h3 id="-先建立正确的场景结构">🧱 先建立正确的场景结构</h3>
<p><code>AnimationPlayer</code> 继承自<strong>裸 <code>Node</code></strong>，而不是 <code>Node2D</code> 或 <code>Node3D</code>。这意味着它<strong>不传递变换</strong>——如果把带有 2D/3D 变换的节点设为它的子节点，子节点不会继承父级的位移、旋转或缩放，视觉上会出现位置偏移或异常。</p>
<p>正确的结构是让 <code>AnimationPlayer</code> 与被动画的节点<strong>平级</strong>：</p>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt">1
</span><span class="lnt">2
</span><span class="lnt">3
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">Player (CharacterBody2D)
</span></span><span class="line"><span class="cl">├── Sprite2D
</span></span><span class="line"><span class="cl">└── AnimationPlayer
</span></span></code></pre></td></tr></table>
</div>
</div><p><code>AnimationPlayer</code> 可以通过<strong>轨道路径</strong>驱动任意兄弟或子节点的属性：位置、旋转、缩放、<code>modulate</code>、<code>visible</code>、碰撞形状的 <code>disabled</code> 状态，甚至方法调用和音频播放。</p>
<hr>
<h3 id="-轨道类型不止是属性">🗂️ 轨道类型：不止是属性</h3>
<p>在动画面板中点击“添加轨道”，你会看到多种类型。常用的是以下四种：</p>
<table>
	<thead>
			<tr>
					<th style="text-align: left">轨道类型</th>
					<th style="text-align: left">作用</th>
					<th style="text-align: left">典型用途</th>
			</tr>
	</thead>
	<tbody>
			<tr>
					<td style="text-align: left"><strong>属性轨道</strong> (Property / Value)</td>
					<td style="text-align: left">插值任意属性</td>
					<td style="text-align: left">位置、旋转、透明度、着色器参数</td>
			</tr>
			<tr>
					<td style="text-align: left"><strong>方法轨道</strong> (Call Method)</td>
					<td style="text-align: left">在精确时间点调用函数</td>
					<td style="text-align: left">生成伤害判定、播放音效、切换状态</td>
			</tr>
			<tr>
					<td style="text-align: left"><strong>音频轨道</strong> (Audio Playback)</td>
					<td style="text-align: left">将音频锁定在时间轴上</td>
					<td style="text-align: left">脚步声与落脚点精确同步</td>
			</tr>
			<tr>
					<td style="text-align: left"><strong>贝塞尔轨道</strong> (Bezier)</td>
					<td style="text-align: left">用曲线精细控制数值节奏</td>
					<td style="text-align: left">非线性的 UI 动效、特殊缓动</td>
			</tr>
	</tbody>
</table>
<p><strong>方法轨道的关键配置</strong>：默认的调用模式是 <code>CALL_MODE_CONTINUOUS</code>，在关键帧前后拖拽播放头时可能<strong>每帧重复触发</strong>。对于“伤害判定只该发生一次”的逻辑，必须在代码中将调用模式设为 <code>CALL_MODE_DISCRETE</code>，确保函数在越过关键帧时<strong>只执行一次</strong>。</p>
<hr>
<h3 id="-代码控制播放混合与队列">▶️ 代码控制：播放、混合与队列</h3>
<h4 id="基础播放与状态驱动">基础播放与状态驱动</h4>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt">1
</span><span class="lnt">2
</span><span class="lnt">3
</span><span class="lnt">4
</span><span class="lnt">5
</span><span class="lnt">6
</span><span class="lnt">7
</span><span class="lnt">8
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-gdscript" data-lang="gdscript"><span class="line"><span class="cl"><span class="nd">@onready</span> <span class="kd">var</span> <span class="n">anim_player</span><span class="p">:</span> <span class="nc">AnimationPlayer</span> <span class="o">=</span> <span class="nx">$AnimationPlayer</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="kd">func</span> <span class="nf">_physics_process</span><span class="p">(</span><span class="n">_delta</span><span class="p">:</span> <span class="kt">float</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="kt">void</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">    <span class="kd">var</span> <span class="n">input_dir</span> <span class="o">:=</span> <span class="nc">Input</span><span class="o">.</span><span class="nf">get_vector</span><span class="p">(</span><span class="s2">&#34;ui_left&#34;</span><span class="p">,</span> <span class="s2">&#34;ui_right&#34;</span><span class="p">,</span> <span class="s2">&#34;ui_up&#34;</span><span class="p">,</span> <span class="s2">&#34;ui_down&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="k">if</span> <span class="n">input_dir</span> <span class="o">!=</span> <span class="nc">Vector2</span><span class="o">.</span><span class="n">ZERO</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">        <span class="n">anim_player</span><span class="o">.</span><span class="nf">play</span><span class="p">(</span><span class="s2">&#34;walk&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="k">else</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">        <span class="n">anim_player</span><span class="o">.</span><span class="nf">play</span><span class="p">(</span><span class="s2">&#34;idle&#34;</span><span class="p">)</span>
</span></span></code></pre></td></tr></table>
</div>
</div><p>一个重要的细节：<strong>对正在播放的同一动画重复调用 <code>play()</code> 不会重新开始</strong>，也不会报错。因此可以在 <code>_physics_process</code> 中放心地每帧调用 <code>play()</code>，根据状态切换动画。</p>
<h4 id="常用控制方法速查">常用控制方法速查</h4>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt">1
</span><span class="lnt">2
</span><span class="lnt">3
</span><span class="lnt">4
</span><span class="lnt">5
</span><span class="lnt">6
</span><span class="lnt">7
</span><span class="lnt">8
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-gdscript" data-lang="gdscript"><span class="line"><span class="cl"><span class="n">anim_player</span><span class="o">.</span><span class="nf">play</span><span class="p">(</span><span class="s2">&#34;attack&#34;</span><span class="p">)</span>                <span class="c1"># 正向播放</span>
</span></span><span class="line"><span class="cl"><span class="n">anim_player</span><span class="o">.</span><span class="nf">play_backwards</span><span class="p">(</span><span class="s2">&#34;attack&#34;</span><span class="p">)</span>      <span class="c1"># 反向播放</span>
</span></span><span class="line"><span class="cl"><span class="n">anim_player</span><span class="o">.</span><span class="nf">queue</span><span class="p">(</span><span class="s2">&#34;idle&#34;</span><span class="p">)</span>                 <span class="c1"># 当前动画结束后自动播放下一个</span>
</span></span><span class="line"><span class="cl"><span class="n">anim_player</span><span class="o">.</span><span class="nf">stop</span><span class="p">()</span>                        <span class="c1"># 立即停止</span>
</span></span><span class="line"><span class="cl"><span class="n">anim_player</span><span class="o">.</span><span class="nf">pause</span><span class="p">()</span>                       <span class="c1"># 暂停在当前帧</span>
</span></span><span class="line"><span class="cl"><span class="n">anim_player</span><span class="o">.</span><span class="nf">play</span><span class="p">()</span>                        <span class="c1"># 从暂停位置恢复</span>
</span></span><span class="line"><span class="cl"><span class="n">anim_player</span><span class="o">.</span><span class="n">speed_scale</span> <span class="o">=</span> <span class="mf">2.0</span>             <span class="c1"># 加速播放</span>
</span></span><span class="line"><span class="cl"><span class="n">anim_player</span><span class="o">.</span><span class="nf">seek</span><span class="p">(</span><span class="mf">0.5</span><span class="p">)</span>                     <span class="c1"># 跳转到 0.5 秒处</span>
</span></span></code></pre></td></tr></table>
</div>
</div><p><code>queue()</code> 是串联动画的轻量方式：先播放 <code>attack</code>，<code>attack</code> 播完后自动进入 <code>idle</code>，不需要手动等待信号。</p>
<h4 id="平滑过渡利用混合时间">平滑过渡：利用混合时间</h4>
<p><code>play()</code> 的第二个参数控制混合时间，让两段动画之间<strong>交叉淡入淡出</strong>，而不是硬切换：</p>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt">1
</span><span class="lnt">2
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-gdscript" data-lang="gdscript"><span class="line"><span class="cl"><span class="c1"># 在 0.2 秒内从当前动画混合过渡到 run</span>
</span></span><span class="line"><span class="cl"><span class="n">anim_player</span><span class="o">.</span><span class="nf">play</span><span class="p">(</span><span class="s2">&#34;run&#34;</span><span class="p">,</span> <span class="mf">0.2</span><span class="p">)</span>
</span></span></code></pre></td></tr></table>
</div>
</div><p>如果不传这个参数，Godot 会使用动画面板中“编辑过渡”（Edit Transitions）里为每对动画预设的混合时间。对于 <code>idle ↔ walk</code> 这类频繁切换的状态，设置 <strong>0.1–0.2 秒</strong>的混合时间能显著提升手感。</p>
<hr>
<h3 id="-方法轨道把逻辑事件焊在时间线上">🎬 方法轨道：把逻辑事件焊在时间线上</h3>
<p>属性轨道管“视觉”，方法轨道管“事件”。例如在攻击动画的第 0.3 秒生成伤害判定：</p>
<p>在编辑器中添加一条<strong>方法轨道</strong>，路径指向挂有目标脚本的节点，在第 0.3 秒插入关键帧并填入方法名 <code>spawn_hitbox</code>。然后在代码中确保调用模式为单次触发：</p>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt">1
</span><span class="lnt">2
</span><span class="lnt">3
</span><span class="lnt">4
</span><span class="lnt">5
</span><span class="lnt">6
</span><span class="lnt">7
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-gdscript" data-lang="gdscript"><span class="line"><span class="cl"><span class="c1"># 在 _ready() 或动画准备阶段，将方法轨道设为 DISCRETE</span>
</span></span><span class="line"><span class="cl"><span class="kd">func</span> <span class="nf">_ready</span><span class="p">()</span> <span class="o">-&gt;</span> <span class="kt">void</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">    <span class="kd">var</span> <span class="n">anim</span> <span class="o">:=</span> <span class="n">anim_player</span><span class="o">.</span><span class="nf">get_animation</span><span class="p">(</span><span class="s2">&#34;attack&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="k">for</span> <span class="n">i</span> <span class="ow">in</span> <span class="n">anim</span><span class="o">.</span><span class="nf">get_track_count</span><span class="p">():</span>
</span></span><span class="line"><span class="cl">        <span class="k">if</span> <span class="n">anim</span><span class="o">.</span><span class="nf">track_get_type</span><span class="p">(</span><span class="n">i</span><span class="p">)</span> <span class="o">==</span> <span class="nc">Animation</span><span class="o">.</span><span class="n">TYPE_METHOD</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">            <span class="n">anim</span><span class="o">.</span><span class="nf">track_set_call_mode</span><span class="p">(</span><span class="n">i</span><span class="p">,</span> <span class="nc">Animation</span><span class="o">.</span><span class="n">CALL_MODE_DISCRETE</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">            <span class="k">break</span>
</span></span></code></pre></td></tr></table>
</div>
</div><p>被调用的函数必须存在于目标节点上：</p>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt">1
</span><span class="lnt">2
</span><span class="lnt">3
</span><span class="lnt">4
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-gdscript" data-lang="gdscript"><span class="line"><span class="cl"><span class="kd">func</span> <span class="nf">spawn_hitbox</span><span class="p">()</span> <span class="o">-&gt;</span> <span class="kt">void</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">    <span class="c1"># 生成伤害区域、播放特效等，只会执行一次</span>
</span></span><span class="line"><span class="cl">    <span class="kd">var</span> <span class="n">hitbox</span> <span class="o">:=</span> <span class="nb">preload</span><span class="p">(</span><span class="s2">&#34;res://hitbox.tscn&#34;</span><span class="p">)</span><span class="o">.</span><span class="nf">instantiate</span><span class="p">()</span>
</span></span><span class="line"><span class="cl">    <span class="nf">add_child</span><span class="p">(</span><span class="n">hitbox</span><span class="p">)</span>
</span></span></code></pre></td></tr></table>
</div>
</div><p>如果需要在播放中途<strong>动态改变调用的方法或参数</strong>，方法轨道的关键帧值本身是一个字典 <code>{ &quot;method&quot;: &quot;函数名&quot;, &quot;args&quot;: [...] }</code>，可以在运行时通过 <code>track_insert_key</code> 替换。</p>
<hr>
<h3 id="-动画库组织多个动画">📚 动画库：组织多个动画</h3>
<p>一个 <code>AnimationPlayer</code> 可以保存多条动画。出于复用性考虑，动画被注册在 <strong><code>AnimationLibrary</code></strong> 资源中。如果你直接新建动画而不指定库，它会进入默认的 <code>[Global]</code> 库，直接 <code>play(&quot;idle&quot;)</code> 即可。</p>
<p>当动画数量增多时，按用途分库会更清晰：</p>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt">1
</span><span class="lnt">2
</span><span class="lnt">3
</span><span class="lnt">4
</span><span class="lnt">5
</span><span class="lnt">6
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-gdscript" data-lang="gdscript"><span class="line"><span class="cl"><span class="c1"># 运行时加载额外的动画库</span>
</span></span><span class="line"><span class="cl"><span class="kd">var</span> <span class="n">combat_lib</span><span class="p">:</span> <span class="nc">AnimationLibrary</span> <span class="o">=</span> <span class="nb">load</span><span class="p">(</span><span class="s2">&#34;res://animations/combat_lib.tres&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="n">anim_player</span><span class="o">.</span><span class="nf">add_animation_library</span><span class="p">(</span><span class="s2">&#34;combat&#34;</span><span class="p">,</span> <span class="n">combat_lib</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># 播放指定库中的动画</span>
</span></span><span class="line"><span class="cl"><span class="n">anim_player</span><span class="o">.</span><span class="nf">play</span><span class="p">(</span><span class="s2">&#34;combat/slash&#34;</span><span class="p">)</span>
</span></span></code></pre></td></tr></table>
</div>
</div><p>分库的好处是<strong>按需加载</strong>：基础移动动画可以常驻，战斗动画库只在进入战斗时加载，减少内存占用。</p>
<hr>
<h3 id="-两个必须避开的坑">⚠️ 两个必须避开的坑</h3>
<p><strong>① 不要给根节点直接添加动画</strong></p>
<p>动画中的 <code>position</code> 是<strong>相对于父节点</strong>的坐标。如果直接给场景根节点打位置关键帧，当这个场景被实例化到另一个场景中时，动画会以<strong>新的父节点位置</strong>作为原点，导致节点“跳”到错误的位置。解决办法是给场景加一个空白根节点，把原来的内容包进去，动画只作用于内部的子节点。</p>
<p><strong>② 移动或重命名节点后，轨道可能失效</strong></p>
<p><code>AnimationPlayer</code> 的轨道路径是<strong>节点路径字符串</strong>。如果你在创建动画后移动或重命名了被动画的节点，轨道路径不会自动更新，播放时会出现“Node not found”错误，属性不再被驱动。双击轨道可以手动编辑路径，或者在调整场景结构后重新绑定轨道。</p>
<hr>
<h3 id="-完整可运行示例带过渡的移动状态机">🧩 完整可运行示例：带过渡的移动状态机</h3>
<p>将以下脚本挂在一个 <code>CharacterBody2D</code> 上，确保场景中有一个 <code>AnimationPlayer</code> 包含 <code>idle</code> 和 <code>walk</code> 两条动画：</p>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt"> 1
</span><span class="lnt"> 2
</span><span class="lnt"> 3
</span><span class="lnt"> 4
</span><span class="lnt"> 5
</span><span class="lnt"> 6
</span><span class="lnt"> 7
</span><span class="lnt"> 8
</span><span class="lnt"> 9
</span><span class="lnt">10
</span><span class="lnt">11
</span><span class="lnt">12
</span><span class="lnt">13
</span><span class="lnt">14
</span><span class="lnt">15
</span><span class="lnt">16
</span><span class="lnt">17
</span><span class="lnt">18
</span><span class="lnt">19
</span><span class="lnt">20
</span><span class="lnt">21
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-gdscript" data-lang="gdscript"><span class="line"><span class="cl"><span class="kd">extends</span> <span class="nc">CharacterBody2D</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="nd">@onready</span> <span class="kd">var</span> <span class="n">anim_player</span><span class="p">:</span> <span class="nc">AnimationPlayer</span> <span class="o">=</span> <span class="nx">$AnimationPlayer</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="kd">const</span> <span class="n">SPEED</span> <span class="o">:=</span> <span class="mf">200.0</span>
</span></span><span class="line"><span class="cl"><span class="kd">const</span> <span class="n">BLEND_TIME</span> <span class="o">:=</span> <span class="mf">0.15</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="kd">func</span> <span class="nf">_physics_process</span><span class="p">(</span><span class="n">_delta</span><span class="p">:</span> <span class="kt">float</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="kt">void</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">    <span class="kd">var</span> <span class="n">input_dir</span> <span class="o">:=</span> <span class="nc">Input</span><span class="o">.</span><span class="nf">get_vector</span><span class="p">(</span><span class="s2">&#34;ui_left&#34;</span><span class="p">,</span> <span class="s2">&#34;ui_right&#34;</span><span class="p">,</span> <span class="s2">&#34;ui_up&#34;</span><span class="p">,</span> <span class="s2">&#34;ui_down&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="k">if</span> <span class="n">input_dir</span> <span class="o">!=</span> <span class="nc">Vector2</span><span class="o">.</span><span class="n">ZERO</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">        <span class="n">velocity</span> <span class="o">=</span> <span class="n">input_dir</span> <span class="o">*</span> <span class="n">SPEED</span>
</span></span><span class="line"><span class="cl">        <span class="c1"># 如果当前不在 walk，用 0.15 秒混合进入</span>
</span></span><span class="line"><span class="cl">        <span class="k">if</span> <span class="n">anim_player</span><span class="o">.</span><span class="n">current_animation</span> <span class="o">!=</span> <span class="s2">&#34;walk&#34;</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">            <span class="n">anim_player</span><span class="o">.</span><span class="nf">play</span><span class="p">(</span><span class="s2">&#34;walk&#34;</span><span class="p">,</span> <span class="n">BLEND_TIME</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="k">else</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">        <span class="n">velocity</span> <span class="o">=</span> <span class="nc">Vector2</span><span class="o">.</span><span class="n">ZERO</span>
</span></span><span class="line"><span class="cl">        <span class="k">if</span> <span class="n">anim_player</span><span class="o">.</span><span class="n">current_animation</span> <span class="o">!=</span> <span class="s2">&#34;idle&#34;</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">            <span class="n">anim_player</span><span class="o">.</span><span class="nf">play</span><span class="p">(</span><span class="s2">&#34;idle&#34;</span><span class="p">,</span> <span class="n">BLEND_TIME</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="nf">move_and_slide</span><span class="p">()</span>
</span></span></code></pre></td></tr></table>
</div>
</div><p>这个示例的关键在于<strong>用 <code>current_animation</code> 判断是否需要切换</strong>，避免每帧重复调用 <code>play()</code> 导致混合反复重置。<code>BLEND_TIME</code> 让 <code>idle</code> 和 <code>walk</code> 之间平滑交叉淡化，而不是生硬跳变。</p>
<p><code>AnimationPlayer</code> 的强项是<strong>时间轴编排</strong>——一旦你把关键帧、方法轨道和混合时间配置好，代码端只需要关心“何时切换到哪条动画”，决策逻辑可以保持得非常薄。当你需要状态机或方向混合空间时，<code>AnimationTree</code> 才是下一步的升级，而 <code>AnimationPlayer</code> 始终是它背后的数据底座。</p>
]]></content:encoded></item><item><title>Godot Tween</title><link>https://blog.dart.xin/godot/gd102-tween/</link><pubDate>Wed, 04 Nov 2026 09:51:01 +0800</pubDate><guid>https://blog.dart.xin/godot/gd102-tween/</guid><description>&lt;p&gt;&lt;code&gt;Tween&lt;/code&gt; 是 Godot 中用于“在代码里顺手做一小段动画”的轻量级工具。它的核心用途很单纯：&lt;strong&gt;将一个属性从当前值平滑地插值到目标值&lt;/strong&gt;，创建后自动运行，完成后自动释放，非常适合 UI 反馈、简单的位移/透明度变化、镜头震动这类“即用即弃”的动效。&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id="-先建立正确的心智模型"&gt;🧱 先建立正确的心智模型&lt;/h3&gt;
&lt;p&gt;使用 &lt;code&gt;Tween&lt;/code&gt; 时，有几个前置规则需要先刻在脑子里：&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;① 必须用 &lt;code&gt;create_tween()&lt;/code&gt; 创建&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;code&gt;Tween.new()&lt;/code&gt; 是无效的，不能用来做补间。正确的入口只有两个：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;span class="lnt"&gt;2
&lt;/span&gt;&lt;span class="lnt"&gt;3
&lt;/span&gt;&lt;span class="lnt"&gt;4
&lt;/span&gt;&lt;span class="lnt"&gt;5
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-gdscript" data-lang="gdscript"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# 方式一：从节点创建，Tween 会绑定到该节点的生命周期&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kd"&gt;var&lt;/span&gt; &lt;span class="n"&gt;tween&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;create_tween&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# 方式二：从场景树创建，不绑定特定节点&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kd"&gt;var&lt;/span&gt; &lt;span class="n"&gt;tween&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;get_tree&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create_tween&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;&lt;strong&gt;② Tween 本质上是一条“时间线”&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;你往这条时间线上添加的每一个 &lt;code&gt;tween_property()&lt;/code&gt;、&lt;code&gt;tween_callback()&lt;/code&gt;、&lt;code&gt;tween_interval()&lt;/code&gt;，都是一个 &lt;strong&gt;Tweener&lt;/strong&gt;。默认情况下，&lt;strong&gt;这些 Tweener 会按添加顺序一个接一个地串行执行&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;③ 默认是串行，&lt;code&gt;set_parallel(true)&lt;/code&gt; 可以改为并行&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;这是控制 Tween 行为最关键的一个开关，后面会用具体示例展开。&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id="-核心-api你只需要先掌握这几个"&gt;✂️ 核心 API：你只需要先掌握这几个&lt;/h3&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th style="text-align: left"&gt;方法&lt;/th&gt;
&lt;th style="text-align: left"&gt;作用&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style="text-align: left"&gt;&lt;strong&gt;&lt;code&gt;tween_property(object, property, final_val, duration)&lt;/code&gt;&lt;/strong&gt;&lt;/td&gt;
&lt;td style="text-align: left"&gt;最核心的方法，在 &lt;code&gt;duration&lt;/code&gt; 秒内将 &lt;code&gt;property&lt;/code&gt; 插值到 &lt;code&gt;final_val&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style="text-align: left"&gt;&lt;strong&gt;&lt;code&gt;tween_callback(callable)&lt;/code&gt;&lt;/strong&gt;&lt;/td&gt;
&lt;td style="text-align: left"&gt;在时间线的某个点执行一个函数&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style="text-align: left"&gt;&lt;strong&gt;&lt;code&gt;tween_interval(time)&lt;/code&gt;&lt;/strong&gt;&lt;/td&gt;
&lt;td style="text-align: left"&gt;在时间线上插入一段纯等待&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style="text-align: left"&gt;&lt;strong&gt;&lt;code&gt;set_parallel(true)&lt;/code&gt;&lt;/strong&gt;&lt;/td&gt;
&lt;td style="text-align: left"&gt;让后续的 Tweener 并行执行，而不是串行&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style="text-align: left"&gt;&lt;strong&gt;&lt;code&gt;chain()&lt;/code&gt;&lt;/strong&gt;&lt;/td&gt;
&lt;td style="text-align: left"&gt;从并行模式切回串行模式&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style="text-align: left"&gt;&lt;strong&gt;&lt;code&gt;set_loops(n)&lt;/code&gt;&lt;/strong&gt;&lt;/td&gt;
&lt;td style="text-align: left"&gt;设置循环次数，&lt;code&gt;0&lt;/code&gt; 或不传参表示无限循环&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style="text-align: left"&gt;&lt;strong&gt;&lt;code&gt;set_ease()&lt;/code&gt; / &lt;code&gt;set_trans()&lt;/code&gt;&lt;/strong&gt;&lt;/td&gt;
&lt;td style="text-align: left"&gt;控制缓动曲线（加速/减速的节奏）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style="text-align: left"&gt;&lt;strong&gt;&lt;code&gt;.from(value)&lt;/code&gt;&lt;/strong&gt;&lt;/td&gt;
&lt;td style="text-align: left"&gt;指定起始值，不依赖属性的当前值&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style="text-align: left"&gt;&lt;strong&gt;&lt;code&gt;.as_relative()&lt;/code&gt;&lt;/strong&gt;&lt;/td&gt;
&lt;td style="text-align: left"&gt;将目标值视为相对偏移量&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;hr&gt;
&lt;h3 id="-常用可运行示例"&gt;🍳 常用可运行示例&lt;/h3&gt;
&lt;p&gt;以下示例均假设脚本挂在某个节点上，可以直接复制到 &lt;code&gt;_ready()&lt;/code&gt; 或按钮回调中运行。&lt;/p&gt;</description><content:encoded><![CDATA[<p><code>Tween</code> 是 Godot 中用于“在代码里顺手做一小段动画”的轻量级工具。它的核心用途很单纯：<strong>将一个属性从当前值平滑地插值到目标值</strong>，创建后自动运行，完成后自动释放，非常适合 UI 反馈、简单的位移/透明度变化、镜头震动这类“即用即弃”的动效。</p>
<hr>
<h3 id="-先建立正确的心智模型">🧱 先建立正确的心智模型</h3>
<p>使用 <code>Tween</code> 时，有几个前置规则需要先刻在脑子里：</p>
<p><strong>① 必须用 <code>create_tween()</code> 创建</strong></p>
<p><code>Tween.new()</code> 是无效的，不能用来做补间。正确的入口只有两个：</p>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt">1
</span><span class="lnt">2
</span><span class="lnt">3
</span><span class="lnt">4
</span><span class="lnt">5
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-gdscript" data-lang="gdscript"><span class="line"><span class="cl"><span class="c1"># 方式一：从节点创建，Tween 会绑定到该节点的生命周期</span>
</span></span><span class="line"><span class="cl"><span class="kd">var</span> <span class="n">tween</span> <span class="o">=</span> <span class="nf">create_tween</span><span class="p">()</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># 方式二：从场景树创建，不绑定特定节点</span>
</span></span><span class="line"><span class="cl"><span class="kd">var</span> <span class="n">tween</span> <span class="o">=</span> <span class="nf">get_tree</span><span class="p">()</span><span class="o">.</span><span class="nf">create_tween</span><span class="p">()</span>
</span></span></code></pre></td></tr></table>
</div>
</div><p><strong>② Tween 本质上是一条“时间线”</strong></p>
<p>你往这条时间线上添加的每一个 <code>tween_property()</code>、<code>tween_callback()</code>、<code>tween_interval()</code>，都是一个 <strong>Tweener</strong>。默认情况下，<strong>这些 Tweener 会按添加顺序一个接一个地串行执行</strong>。</p>
<p><strong>③ 默认是串行，<code>set_parallel(true)</code> 可以改为并行</strong></p>
<p>这是控制 Tween 行为最关键的一个开关，后面会用具体示例展开。</p>
<hr>
<h3 id="-核心-api你只需要先掌握这几个">✂️ 核心 API：你只需要先掌握这几个</h3>
<table>
	<thead>
			<tr>
					<th style="text-align: left">方法</th>
					<th style="text-align: left">作用</th>
			</tr>
	</thead>
	<tbody>
			<tr>
					<td style="text-align: left"><strong><code>tween_property(object, property, final_val, duration)</code></strong></td>
					<td style="text-align: left">最核心的方法，在 <code>duration</code> 秒内将 <code>property</code> 插值到 <code>final_val</code></td>
			</tr>
			<tr>
					<td style="text-align: left"><strong><code>tween_callback(callable)</code></strong></td>
					<td style="text-align: left">在时间线的某个点执行一个函数</td>
			</tr>
			<tr>
					<td style="text-align: left"><strong><code>tween_interval(time)</code></strong></td>
					<td style="text-align: left">在时间线上插入一段纯等待</td>
			</tr>
			<tr>
					<td style="text-align: left"><strong><code>set_parallel(true)</code></strong></td>
					<td style="text-align: left">让后续的 Tweener 并行执行，而不是串行</td>
			</tr>
			<tr>
					<td style="text-align: left"><strong><code>chain()</code></strong></td>
					<td style="text-align: left">从并行模式切回串行模式</td>
			</tr>
			<tr>
					<td style="text-align: left"><strong><code>set_loops(n)</code></strong></td>
					<td style="text-align: left">设置循环次数，<code>0</code> 或不传参表示无限循环</td>
			</tr>
			<tr>
					<td style="text-align: left"><strong><code>set_ease()</code> / <code>set_trans()</code></strong></td>
					<td style="text-align: left">控制缓动曲线（加速/减速的节奏）</td>
			</tr>
			<tr>
					<td style="text-align: left"><strong><code>.from(value)</code></strong></td>
					<td style="text-align: left">指定起始值，不依赖属性的当前值</td>
			</tr>
			<tr>
					<td style="text-align: left"><strong><code>.as_relative()</code></strong></td>
					<td style="text-align: left">将目标值视为相对偏移量</td>
			</tr>
	</tbody>
</table>
<hr>
<h3 id="-常用可运行示例">🍳 常用可运行示例</h3>
<p>以下示例均假设脚本挂在某个节点上，可以直接复制到 <code>_ready()</code> 或按钮回调中运行。</p>
<hr>
<h4 id="示例-1基础位移--让节点滑到指定位置">示例 1：基础位移 —— 让节点滑到指定位置</h4>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt">1
</span><span class="lnt">2
</span><span class="lnt">3
</span><span class="lnt">4
</span><span class="lnt">5
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-gdscript" data-lang="gdscript"><span class="line"><span class="cl"><span class="kd">func</span> <span class="nf">_ready</span><span class="p">()</span> <span class="o">-&gt;</span> <span class="kt">void</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">    <span class="kd">var</span> <span class="n">tween</span> <span class="o">=</span> <span class="nf">create_tween</span><span class="p">()</span>
</span></span><span class="line"><span class="cl">    <span class="n">tween</span><span class="o">.</span><span class="nf">tween_property</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="s2">&#34;position&#34;</span><span class="p">,</span> <span class="nc">Vector2</span><span class="p">(</span><span class="mi">400</span><span class="p">,</span> <span class="mi">300</span><span class="p">),</span> <span class="mf">1.0</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">        <span class="o">.</span><span class="nf">set_trans</span><span class="p">(</span><span class="nc">Tween</span><span class="o">.</span><span class="n">TRANS_CUBIC</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">        <span class="o">.</span><span class="nf">set_ease</span><span class="p">(</span><span class="nc">Tween</span><span class="o">.</span><span class="n">EASE_OUT</span><span class="p">)</span>
</span></span></code></pre></td></tr></table>
</div>
</div><p><code>set_trans(Tween.TRANS_CUBIC)</code> 让运动带有“起步快、收尾缓”的节奏感，<code>.set_ease(Tween.EASE_OUT)</code> 表示缓出。常用的组合是 <strong><code>TRANS_CUBIC</code> + <code>EASE_OUT</code></strong>，对大多数 UI 动效都很自然。</p>
<hr>
<h4 id="示例-2淡入--淡出ui-最常用">示例 2：淡入 / 淡出（UI 最常用）</h4>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt">1
</span><span class="lnt">2
</span><span class="lnt">3
</span><span class="lnt">4
</span><span class="lnt">5
</span><span class="lnt">6
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-gdscript" data-lang="gdscript"><span class="line"><span class="cl"><span class="kd">func</span> <span class="nf">fade_in</span><span class="p">(</span><span class="n">node</span><span class="p">:</span> <span class="nc">CanvasItem</span><span class="p">,</span> <span class="n">duration</span> <span class="o">:=</span> <span class="mf">0.3</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="kt">void</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">    <span class="n">node</span><span class="o">.</span><span class="n">modulate</span><span class="o">.</span><span class="n">a</span> <span class="o">=</span> <span class="mf">0.0</span>
</span></span><span class="line"><span class="cl">    <span class="nf">create_tween</span><span class="p">()</span><span class="o">.</span><span class="nf">tween_property</span><span class="p">(</span><span class="n">node</span><span class="p">,</span> <span class="s2">&#34;modulate:a&#34;</span><span class="p">,</span> <span class="mf">1.0</span><span class="p">,</span> <span class="n">duration</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="kd">func</span> <span class="nf">fade_out</span><span class="p">(</span><span class="n">node</span><span class="p">:</span> <span class="nc">CanvasItem</span><span class="p">,</span> <span class="n">duration</span> <span class="o">:=</span> <span class="mf">0.3</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="kt">void</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">    <span class="nf">create_tween</span><span class="p">()</span><span class="o">.</span><span class="nf">tween_property</span><span class="p">(</span><span class="n">node</span><span class="p">,</span> <span class="s2">&#34;modulate:a&#34;</span><span class="p">,</span> <span class="mf">0.0</span><span class="p">,</span> <span class="n">duration</span><span class="p">)</span>
</span></span></code></pre></td></tr></table>
</div>
</div><p>属性路径用 <code>&quot;modulate:a&quot;</code> 直接绑定到透明度分量，比操作整个 <code>Color</code> 更清晰。</p>
<hr>
<h4 id="示例-3串行序列--变红--缩小--释放">示例 3：串行序列 —— 变红 → 缩小 → 释放</h4>
<p>这是官方文档中的标准示例，展示了 Tween 的“时间线”本质：</p>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt">1
</span><span class="lnt">2
</span><span class="lnt">3
</span><span class="lnt">4
</span><span class="lnt">5
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-gdscript" data-lang="gdscript"><span class="line"><span class="cl"><span class="kd">func</span> <span class="nf">_ready</span><span class="p">()</span> <span class="o">-&gt;</span> <span class="kt">void</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">    <span class="kd">var</span> <span class="n">tween</span> <span class="o">=</span> <span class="nf">create_tween</span><span class="p">()</span>
</span></span><span class="line"><span class="cl">    <span class="n">tween</span><span class="o">.</span><span class="nf">tween_property</span><span class="p">(</span><span class="nx">$Sprite</span><span class="p">,</span> <span class="s2">&#34;modulate&#34;</span><span class="p">,</span> <span class="nc">Color</span><span class="o">.</span><span class="n">RED</span><span class="p">,</span> <span class="mf">1.0</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="n">tween</span><span class="o">.</span><span class="nf">tween_property</span><span class="p">(</span><span class="nx">$Sprite</span><span class="p">,</span> <span class="s2">&#34;scale&#34;</span><span class="p">,</span> <span class="nc">Vector2</span><span class="o">.</span><span class="n">ZERO</span><span class="p">,</span> <span class="mf">1.0</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="n">tween</span><span class="o">.</span><span class="nf">tween_callback</span><span class="p">(</span><span class="nx">$Sprite</span><span class="o">.</span><span class="n">queue_free</span><span class="p">)</span>
</span></span></code></pre></td></tr></table>
</div>
</div><p>三个步骤严格按顺序执行：先变红，再缩小到零，最后调用 <code>queue_free()</code> 释放节点。</p>
<hr>
<h4 id="示例-4延迟回调--两秒后执行某个函数">示例 4：延迟回调 —— 两秒后执行某个函数</h4>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt">1
</span><span class="lnt">2
</span><span class="lnt">3
</span><span class="lnt">4
</span><span class="lnt">5
</span><span class="lnt">6
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-gdscript" data-lang="gdscript"><span class="line"><span class="cl"><span class="kd">func</span> <span class="nf">_ready</span><span class="p">()</span> <span class="o">-&gt;</span> <span class="kt">void</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">    <span class="kd">var</span> <span class="n">tween</span> <span class="o">=</span> <span class="nf">create_tween</span><span class="p">()</span>
</span></span><span class="line"><span class="cl">    <span class="n">tween</span><span class="o">.</span><span class="nf">tween_callback</span><span class="p">(</span><span class="n">_on_delayed</span><span class="p">)</span><span class="o">.</span><span class="nf">set_delay</span><span class="p">(</span><span class="mf">2.0</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="kd">func</span> <span class="nf">_on_delayed</span><span class="p">()</span> <span class="o">-&gt;</span> <span class="kt">void</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">    <span class="nb">print</span><span class="p">(</span><span class="s2">&#34;2 秒后执行&#34;</span><span class="p">)</span>
</span></span></code></pre></td></tr></table>
</div>
</div><p><code>.set_delay(2.0)</code> 让回调在加入时间线后等待 2 秒再执行，很适合替代手动的计时器来做简单的延迟逻辑。</p>
<hr>
<h4 id="示例-5并行动画--面板滑入的同时淡入">示例 5：并行动画 —— 面板滑入的同时淡入</h4>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt">1
</span><span class="lnt">2
</span><span class="lnt">3
</span><span class="lnt">4
</span><span class="lnt">5
</span><span class="lnt">6
</span><span class="lnt">7
</span><span class="lnt">8
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-gdscript" data-lang="gdscript"><span class="line"><span class="cl"><span class="kd">func</span> <span class="nf">show_panel</span><span class="p">(</span><span class="n">panel</span><span class="p">:</span> <span class="nc">Control</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="kt">void</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">    <span class="n">panel</span><span class="o">.</span><span class="n">modulate</span><span class="o">.</span><span class="n">a</span> <span class="o">=</span> <span class="mf">0.0</span>
</span></span><span class="line"><span class="cl">    <span class="n">panel</span><span class="o">.</span><span class="n">position</span><span class="o">.</span><span class="n">x</span> <span class="o">=</span> <span class="o">-</span><span class="mi">200</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="kd">var</span> <span class="n">tween</span> <span class="o">=</span> <span class="nf">create_tween</span><span class="p">()</span><span class="o">.</span><span class="nf">set_parallel</span><span class="p">(</span><span class="kc">true</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="n">tween</span><span class="o">.</span><span class="nf">tween_property</span><span class="p">(</span><span class="n">panel</span><span class="p">,</span> <span class="s2">&#34;position:x&#34;</span><span class="p">,</span> <span class="mf">100.0</span><span class="p">,</span> <span class="mf">0.4</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">        <span class="o">.</span><span class="nf">set_trans</span><span class="p">(</span><span class="nc">Tween</span><span class="o">.</span><span class="n">TRANS_CUBIC</span><span class="p">)</span><span class="o">.</span><span class="nf">set_ease</span><span class="p">(</span><span class="nc">Tween</span><span class="o">.</span><span class="n">EASE_OUT</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="n">tween</span><span class="o">.</span><span class="nf">tween_property</span><span class="p">(</span><span class="n">panel</span><span class="p">,</span> <span class="s2">&#34;modulate:a&#34;</span><span class="p">,</span> <span class="mf">1.0</span><span class="p">,</span> <span class="mf">0.4</span><span class="p">)</span>
</span></span></code></pre></td></tr></table>
</div>
</div><p>调用 <code>set_parallel(true)</code> 之后，<strong>后续添加的所有 Tweener 会同时开始</strong>，而不是排队执行。这是 UI 弹出动画最常用的模式：位移、透明度、缩放同时进行，整体感觉会更利落。</p>
<hr>
<h4 id="示例-6并行结束后接回调--chain-的用法">示例 6：并行结束后接回调 —— <code>chain()</code> 的用法</h4>
<p>如果并行块结束后还需要执行一段串行逻辑，必须用 <code>chain()</code> 切回串行：</p>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt">1
</span><span class="lnt">2
</span><span class="lnt">3
</span><span class="lnt">4
</span><span class="lnt">5
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-gdscript" data-lang="gdscript"><span class="line"><span class="cl"><span class="kd">func</span> <span class="nf">popup_animation</span><span class="p">(</span><span class="n">node</span><span class="p">:</span> <span class="nc">Control</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="kt">void</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">    <span class="kd">var</span> <span class="n">tween</span> <span class="o">=</span> <span class="nf">create_tween</span><span class="p">()</span><span class="o">.</span><span class="nf">set_parallel</span><span class="p">(</span><span class="kc">true</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="n">tween</span><span class="o">.</span><span class="nf">tween_property</span><span class="p">(</span><span class="n">node</span><span class="p">,</span> <span class="s2">&#34;scale&#34;</span><span class="p">,</span> <span class="nc">Vector2</span><span class="p">(</span><span class="mf">1.1</span><span class="p">,</span> <span class="mf">1.1</span><span class="p">),</span> <span class="mf">0.2</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="n">tween</span><span class="o">.</span><span class="nf">tween_property</span><span class="p">(</span><span class="n">node</span><span class="p">,</span> <span class="s2">&#34;modulate:a&#34;</span><span class="p">,</span> <span class="mf">1.0</span><span class="p">,</span> <span class="mf">0.2</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="n">tween</span><span class="o">.</span><span class="nf">chain</span><span class="p">()</span><span class="o">.</span><span class="nf">tween_callback</span><span class="p">(</span><span class="n">_on_popup_ready</span><span class="p">)</span>
</span></span></code></pre></td></tr></table>
</div>
</div><p><code>chain()</code> 的作用就是把“并行模式”关掉，恢复为串行，从而让后面的 <code>tween_callback</code> 等到前面的并行动画全部完成后才触发。</p>
<hr>
<h4 id="示例-7无限循环的往返移动">示例 7：无限循环的往返移动</h4>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt">1
</span><span class="lnt">2
</span><span class="lnt">3
</span><span class="lnt">4
</span><span class="lnt">5
</span><span class="lnt">6
</span><span class="lnt">7
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-gdscript" data-lang="gdscript"><span class="line"><span class="cl"><span class="kd">func</span> <span class="nf">start_bobbing</span><span class="p">()</span> <span class="o">-&gt;</span> <span class="kt">void</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">    <span class="kd">var</span> <span class="n">start_x</span> <span class="o">:=</span> <span class="n">position</span><span class="o">.</span><span class="n">x</span>
</span></span><span class="line"><span class="cl">    <span class="kd">var</span> <span class="n">end_x</span> <span class="o">:=</span> <span class="n">position</span><span class="o">.</span><span class="n">x</span> <span class="o">+</span> <span class="mf">100.0</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="kd">var</span> <span class="n">tween</span> <span class="o">=</span> <span class="nf">create_tween</span><span class="p">()</span><span class="o">.</span><span class="nf">set_loops</span><span class="p">()</span> <span class="c1"># 无限循环</span>
</span></span><span class="line"><span class="cl">    <span class="n">tween</span><span class="o">.</span><span class="nf">tween_property</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="s2">&#34;position:x&#34;</span><span class="p">,</span> <span class="n">end_x</span><span class="p">,</span> <span class="mf">1.0</span><span class="p">)</span><span class="o">.</span><span class="nf">from</span><span class="p">(</span><span class="n">start_x</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="n">tween</span><span class="o">.</span><span class="nf">tween_property</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="s2">&#34;position:x&#34;</span><span class="p">,</span> <span class="n">start_x</span><span class="p">,</span> <span class="mf">1.0</span><span class="p">)</span><span class="o">.</span><span class="nf">from</span><span class="p">(</span><span class="n">end_x</span><span class="p">)</span>
</span></span></code></pre></td></tr></table>
</div>
</div><p><code>set_loops()</code> 不传参数表示无限循环。<code>.from()</code> 明确指定每段的起点，避免依赖属性当前值，让往返逻辑更可控。</p>
<hr>
<h4 id="示例-8相对位移--as_relative-的用法">示例 8：相对位移 —— <code>as_relative()</code> 的用法</h4>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt">1
</span><span class="lnt">2
</span><span class="lnt">3
</span><span class="lnt">4
</span><span class="lnt">5
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-gdscript" data-lang="gdscript"><span class="line"><span class="cl"><span class="kd">func</span> <span class="nf">shake</span><span class="p">()</span> <span class="o">-&gt;</span> <span class="kt">void</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">    <span class="kd">var</span> <span class="n">tween</span> <span class="o">=</span> <span class="nf">create_tween</span><span class="p">()</span>
</span></span><span class="line"><span class="cl">    <span class="n">tween</span><span class="o">.</span><span class="nf">tween_property</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="s2">&#34;position:x&#34;</span><span class="p">,</span> <span class="mf">10.0</span><span class="p">,</span> <span class="mf">0.05</span><span class="p">)</span><span class="o">.</span><span class="nf">as_relative</span><span class="p">()</span>
</span></span><span class="line"><span class="cl">    <span class="n">tween</span><span class="o">.</span><span class="nf">tween_property</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="s2">&#34;position:x&#34;</span><span class="p">,</span> <span class="o">-</span><span class="mf">20.0</span><span class="p">,</span> <span class="mf">0.05</span><span class="p">)</span><span class="o">.</span><span class="nf">as_relative</span><span class="p">()</span>
</span></span><span class="line"><span class="cl">    <span class="n">tween</span><span class="o">.</span><span class="nf">tween_property</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="s2">&#34;position:x&#34;</span><span class="p">,</span> <span class="mf">10.0</span><span class="p">,</span> <span class="mf">0.05</span><span class="p">)</span><span class="o">.</span><span class="nf">as_relative</span><span class="p">()</span>
</span></span></code></pre></td></tr></table>
</div>
</div><p><code>.as_relative()</code> 让目标值变成<strong>相对当前值的偏移量</strong>，非常适合做不需要关心绝对坐标的震动、抖动效果。</p>
<hr>
<h4 id="示例-9弹性缩放--按钮按压反馈">示例 9：弹性缩放 —— 按钮按压反馈</h4>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt">1
</span><span class="lnt">2
</span><span class="lnt">3
</span><span class="lnt">4
</span><span class="lnt">5
</span><span class="lnt">6
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-gdscript" data-lang="gdscript"><span class="line"><span class="cl"><span class="kd">func</span> <span class="nf">press_feedback</span><span class="p">(</span><span class="n">button</span><span class="p">:</span> <span class="nc">Control</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="kt">void</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">    <span class="kd">var</span> <span class="n">tween</span> <span class="o">=</span> <span class="nf">create_tween</span><span class="p">()</span>
</span></span><span class="line"><span class="cl">    <span class="n">tween</span><span class="o">.</span><span class="nf">tween_property</span><span class="p">(</span><span class="n">button</span><span class="p">,</span> <span class="s2">&#34;scale&#34;</span><span class="p">,</span> <span class="nc">Vector2</span><span class="p">(</span><span class="mf">0.9</span><span class="p">,</span> <span class="mf">0.9</span><span class="p">),</span> <span class="mf">0.1</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="n">tween</span><span class="o">.</span><span class="nf">tween_property</span><span class="p">(</span><span class="n">button</span><span class="p">,</span> <span class="s2">&#34;scale&#34;</span><span class="p">,</span> <span class="nc">Vector2</span><span class="o">.</span><span class="n">ONE</span><span class="p">,</span> <span class="mf">0.3</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">        <span class="o">.</span><span class="nf">set_trans</span><span class="p">(</span><span class="nc">Tween</span><span class="o">.</span><span class="n">TRANS_ELASTIC</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">        <span class="o">.</span><span class="nf">set_ease</span><span class="p">(</span><span class="nc">Tween</span><span class="o">.</span><span class="n">EASE_OUT</span><span class="p">)</span>
</span></span></code></pre></td></tr></table>
</div>
</div><p>先用很短的时间缩小，再用弹性缓动弹回原始大小，是按钮点击反馈中很常见的“Q 弹”手感。</p>
<hr>
<h4 id="示例-10控制生命周期--保存引用随时终止">示例 10：控制生命周期 —— 保存引用，随时终止</h4>
<p>Tween 创建后会自动运行，但如果你需要中途取消（比如玩家在动画途中离开了状态），应当保存引用并调用 <code>kill()</code>：</p>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt"> 1
</span><span class="lnt"> 2
</span><span class="lnt"> 3
</span><span class="lnt"> 4
</span><span class="lnt"> 5
</span><span class="lnt"> 6
</span><span class="lnt"> 7
</span><span class="lnt"> 8
</span><span class="lnt"> 9
</span><span class="lnt">10
</span><span class="lnt">11
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-gdscript" data-lang="gdscript"><span class="line"><span class="cl"><span class="kd">var</span> <span class="n">_move_tween</span><span class="p">:</span> <span class="nc">Tween</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="kd">func</span> <span class="nf">move_to</span><span class="p">(</span><span class="n">target</span><span class="p">:</span> <span class="nc">Vector2</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="kt">void</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">    <span class="k">if</span> <span class="n">_move_tween</span> <span class="ow">and</span> <span class="n">_move_tween</span><span class="o">.</span><span class="nf">is_valid</span><span class="p">():</span>
</span></span><span class="line"><span class="cl">        <span class="n">_move_tween</span><span class="o">.</span><span class="nf">kill</span><span class="p">()</span>  <span class="c1"># 先终止旧 Tween，避免多 Tween 争抢同一属性</span>
</span></span><span class="line"><span class="cl">    <span class="n">_move_tween</span> <span class="o">=</span> <span class="nf">create_tween</span><span class="p">()</span>
</span></span><span class="line"><span class="cl">    <span class="n">_move_tween</span><span class="o">.</span><span class="nf">tween_property</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="s2">&#34;position&#34;</span><span class="p">,</span> <span class="n">target</span><span class="p">,</span> <span class="mf">0.5</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="kd">func</span> <span class="nf">cancel_movement</span><span class="p">()</span> <span class="o">-&gt;</span> <span class="kt">void</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">    <span class="k">if</span> <span class="n">_move_tween</span> <span class="ow">and</span> <span class="n">_move_tween</span><span class="o">.</span><span class="nf">is_valid</span><span class="p">():</span>
</span></span><span class="line"><span class="cl">        <span class="n">_move_tween</span><span class="o">.</span><span class="nf">kill</span><span class="p">()</span>
</span></span></code></pre></td></tr></table>
</div>
</div><p>一个重要的实践规则：<strong>不要用多个 Tween 同时驱动同一个属性</strong>，否则行为会变得不可预测。在启动新 Tween 之前，先 <code>kill()</code> 旧的引用。</p>
<hr>
<h3 id="-缓动速查set_trans-与-set_ease-怎么选">🎚️ 缓动速查：<code>set_trans</code> 与 <code>set_ease</code> 怎么选</h3>
<table>
	<thead>
			<tr>
					<th style="text-align: left">场景</th>
					<th style="text-align: left">推荐组合</th>
			</tr>
	</thead>
	<tbody>
			<tr>
					<td style="text-align: left">UI 滑入 / 淡入</td>
					<td style="text-align: left"><code>TRANS_CUBIC</code> + <code>EASE_OUT</code></td>
			</tr>
			<tr>
					<td style="text-align: left">弹簧/弹性反馈</td>
					<td style="text-align: left"><code>TRANS_ELASTIC</code> + <code>EASE_OUT</code></td>
			</tr>
			<tr>
					<td style="text-align: left">自然往返（呼吸感）</td>
					<td style="text-align: left"><code>TRANS_SINE</code> + <code>EASE_IN_OUT</code></td>
			</tr>
			<tr>
					<td style="text-align: left">快速起步、缓慢收尾</td>
					<td style="text-align: left"><code>TRANS_QUAD</code> + <code>EASE_OUT</code></td>
			</tr>
			<tr>
					<td style="text-align: left">强调“砸下来”的感觉</td>
					<td style="text-align: left"><code>TRANS_BACK</code> + <code>EASE_IN</code></td>
			</tr>
	</tbody>
</table>
<p><code>set_ease</code> 和 <code>set_trans</code> 可以链式调用在单个 <code>tween_property</code> 上，也可以设置在 <code>create_tween()</code> 后作为整个 Tween 的默认值。</p>
<hr>
<h3 id="-总结">💎 总结</h3>
<p><code>Tween</code> 的学习曲线很短，关键是把三个概念分清楚：<strong>时间线是串行的</strong>（用 <code>set_parallel</code> 切换并行）、<strong>Tweener 是可链式配置的</strong>（<code>.from()</code>、<code>.set_trans()</code>、<code>.set_delay()</code> 都作用在单个步骤上）、<strong>Tween 对象本身是可终止的</strong>（保存引用，必要时 <code>kill()</code>）。把上面十个示例跑一遍，再组合出“并行位移+淡入，结束后回调”这类复合动效，你就能覆盖大多数日常需求了。</p>
]]></content:encoded></item><item><title>Godot Animation介绍</title><link>https://blog.dart.xin/godot/gd101-animation/</link><pubDate>Wed, 04 Nov 2026 09:44:43 +0800</pubDate><guid>https://blog.dart.xin/godot/gd101-animation/</guid><description>&lt;p&gt;Godot 的动画系统以&lt;strong&gt;简洁但极具深度&lt;/strong&gt;著称。理解它的关键在于建立一个清晰的层次观：&lt;strong&gt;&lt;code&gt;Tween&lt;/code&gt;、&lt;code&gt;AnimationPlayer&lt;/code&gt; 和 &lt;code&gt;AnimationTree&lt;/code&gt; 三者并非替代关系，而是分别针对不同场景、逐级递进的工具&lt;/strong&gt;。&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id="-先看全景你该在什么时候用什么"&gt;🗺️ 先看全景：你该在什么时候用什么？&lt;/h3&gt;
&lt;p&gt;社区里一条非常实用的“黄金法则”是：&lt;strong&gt;从 &lt;code&gt;AnimationPlayer&lt;/code&gt; 开始；当你需要动画之间的混合、过渡或分层时，再引入 &lt;code&gt;AnimationTree&lt;/code&gt;&lt;/strong&gt;。&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th style="text-align: left"&gt;工具&lt;/th&gt;
&lt;th style="text-align: left"&gt;最适合的场景&lt;/th&gt;
&lt;th style="text-align: left"&gt;关键特征&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style="text-align: left"&gt;&lt;strong&gt;&lt;code&gt;Tween&lt;/code&gt;&lt;/strong&gt;&lt;/td&gt;
&lt;td style="text-align: left"&gt;代码驱动的一次性短动画：按钮悬停、屏幕震动、UI 淡入、伤害闪烁&lt;/td&gt;
&lt;td style="text-align: left"&gt;纯代码创建，运行一次后自动释放&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style="text-align: left"&gt;&lt;strong&gt;&lt;code&gt;AnimationPlayer&lt;/code&gt;&lt;/strong&gt;&lt;/td&gt;
&lt;td style="text-align: left"&gt;需要在编辑器中“编排”的动画：精灵帧序列、复杂运动、过场动画、UI 过渡&lt;/td&gt;
&lt;td style="text-align: left"&gt;可视化时间轴，可复用，能驱动几乎任何属性&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style="text-align: left"&gt;&lt;strong&gt;&lt;code&gt;AnimationTree&lt;/code&gt;&lt;/strong&gt;&lt;/td&gt;
&lt;td style="text-align: left"&gt;需要&lt;strong&gt;状态管理&lt;/strong&gt;或&lt;strong&gt;混合&lt;/strong&gt;的角色动画：&lt;code&gt;idle→walk→run&lt;/code&gt;、8 方向移动、上下半身分层&lt;/td&gt;
&lt;td style="text-align: left"&gt;包裹 &lt;code&gt;AnimationPlayer&lt;/code&gt;，用状态机或混合树来调度播放&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;你的学习路径可以按照 &lt;strong&gt;&lt;code&gt;Tween&lt;/code&gt; → &lt;code&gt;AnimationPlayer&lt;/code&gt; → &lt;code&gt;AnimationTree&lt;/code&gt;&lt;/strong&gt; 的顺序推进，下面逐层展开。&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id="-第一层tween--代码里的一次性动效"&gt;🌱 第一层：&lt;code&gt;Tween&lt;/code&gt; —— 代码里的“一次性动效”&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;Tween&lt;/code&gt; 是三者中最轻量的。它不活在场景树里，而是由代码通过 &lt;code&gt;create_tween()&lt;/code&gt; 创建，专注于将某个属性从当前值平滑插值到目标值。&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;span class="lnt"&gt;2
&lt;/span&gt;&lt;span class="lnt"&gt;3
&lt;/span&gt;&lt;span class="lnt"&gt;4
&lt;/span&gt;&lt;span class="lnt"&gt;5
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-gdscript" data-lang="gdscript"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# 让节点在 1 秒内移动到 (500, 300)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kd"&gt;var&lt;/span&gt; &lt;span class="n"&gt;tween&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;create_tween&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;tween&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;tween_property&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;position&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;Vector2&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;500&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;300&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="mf"&gt;1.0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;set_ease&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Tween&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;EASE_OUT&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;set_trans&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Tween&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;TRANS_CUBIC&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;&lt;strong&gt;核心心智模型&lt;/strong&gt;：&lt;code&gt;Tween&lt;/code&gt; 是“即用即走”的。如果只是一个按钮弹一下、一个面板滑进来，用它最直接，不需要在场景里挂节点，也不需要打开动画面板。&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id="-第二层animationplayer--你的万能动画剪辑台"&gt;✂️ 第二层：&lt;code&gt;AnimationPlayer&lt;/code&gt; —— 你的万能动画剪辑台&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;AnimationPlayer&lt;/code&gt; 是整个动画系统的&lt;strong&gt;数据基础&lt;/strong&gt;。它本身是一个 &lt;code&gt;Node&lt;/code&gt;，内部可以存放多条&lt;strong&gt;动画片段&lt;/strong&gt;，每条动画由若干&lt;strong&gt;轨道&lt;/strong&gt;组成，轨道上放置&lt;strong&gt;关键帧&lt;/strong&gt;。&lt;/p&gt;
&lt;h4 id="关键概念轨道与关键帧"&gt;关键概念：轨道与关键帧&lt;/h4&gt;
&lt;p&gt;一条&lt;strong&gt;轨道&lt;/strong&gt;绑定到“某个节点的某个属性”（例如 &lt;code&gt;Sprite2D:position&lt;/code&gt;）。&lt;strong&gt;关键帧&lt;/strong&gt;记录该属性在某一时刻的值；两个关键帧之间，引擎会自动插值，让属性平滑变化。&lt;/p&gt;</description><content:encoded><![CDATA[<p>Godot 的动画系统以<strong>简洁但极具深度</strong>著称。理解它的关键在于建立一个清晰的层次观：<strong><code>Tween</code>、<code>AnimationPlayer</code> 和 <code>AnimationTree</code> 三者并非替代关系，而是分别针对不同场景、逐级递进的工具</strong>。</p>
<hr>
<h3 id="-先看全景你该在什么时候用什么">🗺️ 先看全景：你该在什么时候用什么？</h3>
<p>社区里一条非常实用的“黄金法则”是：<strong>从 <code>AnimationPlayer</code> 开始；当你需要动画之间的混合、过渡或分层时，再引入 <code>AnimationTree</code></strong>。</p>
<table>
	<thead>
			<tr>
					<th style="text-align: left">工具</th>
					<th style="text-align: left">最适合的场景</th>
					<th style="text-align: left">关键特征</th>
			</tr>
	</thead>
	<tbody>
			<tr>
					<td style="text-align: left"><strong><code>Tween</code></strong></td>
					<td style="text-align: left">代码驱动的一次性短动画：按钮悬停、屏幕震动、UI 淡入、伤害闪烁</td>
					<td style="text-align: left">纯代码创建，运行一次后自动释放</td>
			</tr>
			<tr>
					<td style="text-align: left"><strong><code>AnimationPlayer</code></strong></td>
					<td style="text-align: left">需要在编辑器中“编排”的动画：精灵帧序列、复杂运动、过场动画、UI 过渡</td>
					<td style="text-align: left">可视化时间轴，可复用，能驱动几乎任何属性</td>
			</tr>
			<tr>
					<td style="text-align: left"><strong><code>AnimationTree</code></strong></td>
					<td style="text-align: left">需要<strong>状态管理</strong>或<strong>混合</strong>的角色动画：<code>idle→walk→run</code>、8 方向移动、上下半身分层</td>
					<td style="text-align: left">包裹 <code>AnimationPlayer</code>，用状态机或混合树来调度播放</td>
			</tr>
	</tbody>
</table>
<p>你的学习路径可以按照 <strong><code>Tween</code> → <code>AnimationPlayer</code> → <code>AnimationTree</code></strong> 的顺序推进，下面逐层展开。</p>
<hr>
<h3 id="-第一层tween--代码里的一次性动效">🌱 第一层：<code>Tween</code> —— 代码里的“一次性动效”</h3>
<p><code>Tween</code> 是三者中最轻量的。它不活在场景树里，而是由代码通过 <code>create_tween()</code> 创建，专注于将某个属性从当前值平滑插值到目标值。</p>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt">1
</span><span class="lnt">2
</span><span class="lnt">3
</span><span class="lnt">4
</span><span class="lnt">5
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-gdscript" data-lang="gdscript"><span class="line"><span class="cl"><span class="c1"># 让节点在 1 秒内移动到 (500, 300)</span>
</span></span><span class="line"><span class="cl"><span class="kd">var</span> <span class="n">tween</span> <span class="o">=</span> <span class="nf">create_tween</span><span class="p">()</span>
</span></span><span class="line"><span class="cl"><span class="n">tween</span><span class="o">.</span><span class="nf">tween_property</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="s2">&#34;position&#34;</span><span class="p">,</span> <span class="nc">Vector2</span><span class="p">(</span><span class="mi">500</span><span class="p">,</span> <span class="mi">300</span><span class="p">),</span> <span class="mf">1.0</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="o">.</span><span class="nf">set_ease</span><span class="p">(</span><span class="nc">Tween</span><span class="o">.</span><span class="n">EASE_OUT</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="o">.</span><span class="nf">set_trans</span><span class="p">(</span><span class="nc">Tween</span><span class="o">.</span><span class="n">TRANS_CUBIC</span><span class="p">)</span>
</span></span></code></pre></td></tr></table>
</div>
</div><p><strong>核心心智模型</strong>：<code>Tween</code> 是“即用即走”的。如果只是一个按钮弹一下、一个面板滑进来，用它最直接，不需要在场景里挂节点，也不需要打开动画面板。</p>
<hr>
<h3 id="-第二层animationplayer--你的万能动画剪辑台">✂️ 第二层：<code>AnimationPlayer</code> —— 你的万能动画剪辑台</h3>
<p><code>AnimationPlayer</code> 是整个动画系统的<strong>数据基础</strong>。它本身是一个 <code>Node</code>，内部可以存放多条<strong>动画片段</strong>，每条动画由若干<strong>轨道</strong>组成，轨道上放置<strong>关键帧</strong>。</p>
<h4 id="关键概念轨道与关键帧">关键概念：轨道与关键帧</h4>
<p>一条<strong>轨道</strong>绑定到“某个节点的某个属性”（例如 <code>Sprite2D:position</code>）。<strong>关键帧</strong>记录该属性在某一时刻的值；两个关键帧之间，引擎会自动插值，让属性平滑变化。</p>
<p><code>AnimationPlayer</code> 远不止能做位移。它可以驱动<strong>检查器里几乎任何可插值的属性</strong>：颜色、透明度、碰撞形状的启用状态、着色器参数等等。</p>
<h4 id="不止是属性轨道类型全览">不止是属性：轨道类型全览</h4>
<p>除了最常用的属性轨道，<code>AnimationPlayer</code> 还支持多种专用轨道，这是它成为“剪辑台”的关键：</p>
<table>
	<thead>
			<tr>
					<th style="text-align: left">轨道类型</th>
					<th style="text-align: left">作用</th>
					<th style="text-align: left">典型用途</th>
			</tr>
	</thead>
	<tbody>
			<tr>
					<td style="text-align: left"><strong>属性轨道</strong> (Property)</td>
					<td style="text-align: left">插值节点属性</td>
					<td style="text-align: left">位置、旋转、缩放、颜色</td>
			</tr>
			<tr>
					<td style="text-align: left"><strong>方法轨道</strong> (Call Method)</td>
					<td style="text-align: left">在精确时间点调用函数</td>
					<td style="text-align: left">在攻击动画第 0.3 秒生成伤害判定、播放音效、触发状态切换</td>
			</tr>
			<tr>
					<td style="text-align: left"><strong>音频轨道</strong> (Audio Playback)</td>
					<td style="text-align: left">将音频锁定在时间轴上播放</td>
					<td style="text-align: left">让脚步声与脚落地精确同步</td>
			</tr>
			<tr>
					<td style="text-align: left"><strong>贝塞尔曲线轨道</strong> (Bezier)</td>
					<td style="text-align: left">用曲线精细控制数值变化节奏</td>
					<td style="text-align: left">非线性的 UI 动效、特殊的缓动需求</td>
			</tr>
			<tr>
					<td style="text-align: left"><strong>动画轨道</strong> (Animation)</td>
					<td style="text-align: left">播放其他动画</td>
					<td style="text-align: left">在一条时间轴上串联多个片段</td>
			</tr>
	</tbody>
</table>
<p><strong>方法轨道的一个重要细节</strong>：它默认的调用模式是 <code>CONTINUOUS</code>，在关键帧前后拖拽播放头时可能会反复触发。对于“伤害判定只该发生一次”的逻辑，应当在代码中将调用模式设为 <code>CALL_MODE_DISCRETE</code>，确保函数在越过关键帧时只执行一次。</p>
<h4 id="-一个必须避开的坑">⚠️ 一个必须避开的坑</h4>
<p>不要将带有 2D/3D 变换的节点设为 <code>AnimationPlayer</code> 的<strong>直接子节点</strong>。因为 <code>AnimationPlayer</code> 继承自裸 <code>Node</code>，不传递变换，这会导致子节点的视觉表现异常。正确的结构是让 <code>AnimationPlayer</code> 与 <code>Sprite2D</code> 等节点<strong>平级</strong>，而不是成为它的父节点。</p>
<hr>
<h3 id="-第三层animationtree--状态机与混合的大脑">🧠 第三层：<code>AnimationTree</code> —— 状态机与混合的大脑</h3>
<p>当你的动画需求从“播放一条片段”升级为“在多个片段之间智能切换和混合”时，<code>AnimationTree</code> 就该登场了。</p>
<p>它的基本结构是：<code>AnimationTree</code> 通过 <code>anim_player</code> 属性指向一个 <code>AnimationPlayer</code>，自身在运行时以<strong>参数</strong>的形式对外暴露控制接口。</p>
<h4 id="两种核心组织方式状态机-vs-混合空间">两种核心组织方式：状态机 vs 混合空间</h4>
<p><strong>① 状态机 (<code>AnimationNodeStateMachine</code>)</strong> —— 管理“离散状态”的切换</p>
<p>适合 <code>idle</code>、<code>walk</code>、<code>jump</code>、<code>attack</code> 这类<strong>互斥状态</strong>。你在编辑器中画出状态节点和过渡连线，通过代码驱动状态迁移。</p>
<p>最常用的两种驱动方式：</p>
<ul>
<li>
<p><strong><code>travel()</code></strong>：命令式切换。在 <code>_physics_process</code> 中根据游戏逻辑判断目标状态并调用。</p>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt">1
</span><span class="lnt">2
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-gdscript" data-lang="gdscript"><span class="line"><span class="cl"><span class="kd">var</span> <span class="n">playback</span> <span class="o">=</span> <span class="n">anim_tree</span><span class="o">.</span><span class="nf">get</span><span class="p">(</span><span class="s2">&#34;parameters/StateMachine/playback&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="n">playback</span><span class="o">.</span><span class="nf">travel</span><span class="p">(</span><span class="s2">&#34;Run&#34;</span><span class="p">)</span>
</span></span></code></pre></td></tr></table>
</div>
</div></li>
<li>
<p><strong>Advance Condition（推进条件）</strong>：声明式切换。在过渡线上设置一个布尔条件名，代码中设置该参数，条件满足时自动过渡。适合<strong>受击、死亡</strong>等事件驱动的切换。</p>
</li>
</ul>
<p>此外，<strong>Auto Advance</strong> 可以用于<strong>连招链</strong>：<code>Attack1 → Attack2</code> 无需条件，前一段播完自动进入下一段。</p>
<p><strong>② 混合空间 (<code>BlendSpace1D</code> / <code>BlendSpace2D</code>)</strong> —— 管理“连续变化”</p>
<p>适合<strong>方向</strong>和<strong>速度</strong>这类连续量。</p>
<ul>
<li>
<p><strong><code>BlendSpace1D</code></strong>：单一参数驱动。例如用速度值在 <code>walk</code> 和 <code>run</code> 之间连续混合。</p>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt">1
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-gdscript" data-lang="gdscript"><span class="line"><span class="cl"><span class="n">anim_tree</span><span class="p">[</span><span class="s2">&#34;parameters/BlendSpace1D/blend_position&#34;</span><span class="p">]</span> <span class="o">=</span> <span class="nb">inverse_lerp</span><span class="p">(</span><span class="n">walk_speed</span><span class="p">,</span> <span class="n">run_speed</span><span class="p">,</span> <span class="n">velocity</span><span class="o">.</span><span class="nf">length</span><span class="p">())</span>
</span></span></code></pre></td></tr></table>
</div>
</div></li>
<li>
<p><strong><code>BlendSpace2D</code></strong>：二维向量驱动。将 8 个方向的动画放置在平面上的对应坐标，用输入方向向量直接驱动，实现<strong>8 方向移动</strong>的自然混合。</p>
</li>
</ul>
<p><strong>实际项目中，两者是配合使用的</strong>：状态机负责“当前该处于什么大状态”（比如 <code>Locomotion</code> 状态），而该状态内部的 <code>walk/run</code> 切换或方向混合则交给混合空间处理。</p>
<hr>
<h3 id="-专项骨骼动画2d-与-3d">🦴 专项：骨骼动画（2D 与 3D）</h3>
<p><strong>2D 骨骼</strong>通过 <code>Skeleton2D</code> + <code>Bone2D</code> 层级实现。<code>AnimationPlayer</code> 对 <code>Bone2D</code> 的旋转属性打关键帧，即可驱动骨骼摆动。</p>
<p>如果需要<strong>程序化</strong>的肢体定位（比如让脚精确踩在斜坡上），可以使用 <code>SkeletonModification2DTwoBoneIK</code>。将 IK 目标点交给 <code>Marker2D</code> 或射线检测结果，骨骼链会自动解算姿态。</p>
<p><strong>3D 动画</strong>的导入非常直接：从 Blender 等 DCC 工具导出 FBX/glTF 时勾选动画，导入 Godot 后，动画会自动收纳在生成场景的 <code>AnimationPlayer</code> 节点中。之后 <code>AnimationTree</code> 指向这个 <code>AnimationPlayer</code> 即可照常使用。</p>
<hr>
<h3 id="-总结你的学习节奏">💎 总结：你的学习节奏</h3>
<ol>
<li><strong>先只用 <code>Tween</code></strong>：所有“代码里顺手做一下”的动效，都用它完成，熟悉插值、缓动和链式调用。</li>
<li><strong>再掌握 <code>AnimationPlayer</code></strong>：学会在动画面板里创建片段、添加属性轨道，并尝试<strong>方法轨道</strong>来把游戏逻辑事件绑定到时间线上。</li>
<li><strong>最后引入 <code>AnimationTree</code></strong>：从一个简单的 <code>idle/walk</code> 状态机开始，逐步加入 <code>BlendSpace2D</code> 处理方向移动。记住那条法则——<strong>能用 <code>AnimationPlayer</code> 直接 <code>play()</code> 解决的，就不要用状态机</strong>。</li>
</ol>
<p>Godot 动画系统的设计哲学是<strong>用可视化编辑器承载“编排”，用代码控制“决策”</strong>。当你把 <code>AnimationPlayer</code> 当作素材库、<code>AnimationTree</code> 当作决策层时，两者的分工就会变得非常清晰。</p>
]]></content:encoded></item></channel></rss>