<?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>AnimationTree on 山高月小</title><link>https://blog.dart.xin/tags/animationtree/</link><description>Recent content in AnimationTree 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/tags/animationtree/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></channel></rss>