动画
动画:
轻量级补间动画节点:
var tw = GetTree().CreateTween(); // 比AnimationPlayer轻量灵活。
tw.TweenProperty(GetNode("Sprite"), "modulate", Colors.Red, 1.0f); // 参数为节点对象、属性名、最终值、每次变化值。
tw.TweenProperty(GetNode("Sprite"), "scale", Vector2.Zero, 1.0f).SetTrans(Tween.TransitionType.Bounce);
tw.TweenProperty(GetNode("Sprite"), "position:x", 200.0f, 1.0f);
tw.TweenProperty(GetNode("Sprite"), "position", Vector2.Right * 300.0f, 1.0f).AsRelative().FromCurrent().SetTrans(Tween.TransitionType.Expo);
tw.TweenCallback(Callable.From(GetNode("Sprite").QueueFree)); // 结束时释放。
动画节点:添加AnimationPlayer,切至底部Animation面板,点“动画”-> 新建:动画名 -> 添加轨道:属性轨道 -> 选节点属性position等 ->调整属性后右键“插入关键帧”,两个关键帧之间属性要有些变化。
// 重复、循环播放
var ap = GetNode("player/AnimationPlayer");
//GD.Print(String.Join(",", ap.GetAnimationList()));
ap.GetAnimation("idle").LoopMode = LoopModeEnum.Linear;
ap.Play("idle"); // LoopMode会影响Play行为。
动画树(AnimationTree)支持更高级的多动画混合、过渡处理等,AnimationPlayer则只支持一个简单的两动画混合过渡功能set_blend_time
动画 -
播放规律:动画没播完,再次play同一个动画不会从头播放,如果play不同的动画则会中断当前动画,马上进行切换。
动画时长:animationPlay.GetAnimation("aName").Length; // 返回值为秒数
存储动画:ResourceSaver.Save(ap.GetAnimation("name"), "d:/" + DateTime.Now.ToString("yyyy-MM-dd_hh-mm-ss") + ".anim"); // 明文用*.tres
Blender自定义BlendShape用于动画轨道:Animation Editor -> Add Track -> Blend Shape Track; 属性Blend Shapes值后🔑图标用来添加至动画轨道。
var node = GetNode("rain_rig/RIG-rain/Skeleton3D/GEO-rain_head");
node.Set("blend_shapes/mouthFunnel", 0.5);
GD.Print(node.Get("blend_shapes/mouthFunnel"));
或 代码构造Blend Shapes关键帧动画:
var anim = new Animation(); anim.AddTrack(Animation.TrackType.BlendShape); anim.Length = 3;
anim.TrackSetPath(0, new NodePath("RIG-rain/Skeleton3D/GEO-rain_head:mouthFunnel")); // 无需“blend_shapes/”前缀
anim.BlendShapeTrackInsertKey(0, 1, 0.1f); anim.BlendShapeTrackInsertKey(0, 2, 0.5f); anim.BlendShapeTrackInsertKey(0, 3, 0.9f);
var ap = GetNode("AnimationPlayer"); ap.GetAnimationLibrary("").AddAnimation("test", anim); ap.Play("test");
最常见姿态有3个:idle、walk和run; 何时用状态机? _PhysicsProcess:
if (direction != Vector3.Zero)
{
if (ap.CurrentAnimation != "1H_Melee_Attack_Chop") { ap.Play("Walking_A"); }
}
if (!ap.IsPlaying()) { ap.Play("Idle"); }
动画树(AnimationTree)支持更高级的多动画混合、过渡处理等:
用法 - 通过IDE可视化工具编排动画播放顺序及过渡关系,代码单纯触发播放(AnimationTree内部或外部节点字段表达式)。
AnimationTree.tree_root设为AnimationNodeStateMachine,anim_player选中同场景的AnimationPlay;
如果动画在模型文件内,可通过AnimationLibrary方式导入后再用。
可视化“动画树”窗口应点右键直接“添加动画”,而不是“添加StateMachine”,且不要连线。
其他 -
AnimationNodeTransition是简单版AnimationNodeStateMachine,可用于切换Flag状态:animationTree.Set("parameters/Transition/transition_request", "state_2"); // 空字符串为清除状态。
AnimationTree设为循环播放但不影响原动画 - animationNodeAnimation.LoopMode = Animation.LoopModeEnum.Linear;
触发 - GetNode("AnimationTree").Get("parameters/playback").As().Travel("idle"); // 将当前状态按照编排顺序切换为入参状态。
或 根据变量字段而变化 https://www.bilibili.com/video/BV1Ye411R7Bo/
动画混合 - AnimationNodeBlendTree之"Edit Filters"只混合选取的骨骼部位(与导入时骨骼映射无关),作用是去In(未勾)存Blend(勾中)方向动画。
// 从 12 秒处开始播放子动画。
animationTree.Set("parameters/TimeSeek/seek_request", 12.0);
当Blend2执行at.Set("parameters/Blend2/blend_amount", 0.5)时前方的AnimationNodeOneShot会自动播一次in端口的动画;
AnimationNodeBlendTree调用:
var at=GetNode("AnimationTree"); at.Set("active", false);at.Set("active", true);
at.Set("parameters/Blend2/blend_amount", 0.5);
at.AnimationFinished += (StringName x) => { GD.Print("AnimationFinished"); };
注意 - AnimationTree的active为true时,其anim_player指定的AnimationPlayer.Play(...)等调用会失效,且animation_finished等信号转移到了AnimationTree,可复制一份AnimationPlayer来解决:
var ap2 = new AnimationPlayer();
foreach (var name in apInTree.GetAnimationLibraryList())
{ ap2.AddAnimationLibrary(name, apInTree.GetAnimationLibrary(name)); }
AddChild(ap2); ap2.Play("Walking_A");
// 指定new出来的AnimationPlayer,AnimationTree可视化中的AnimationNodeAnimation允许直输动画名。
GetNode("AnimationTree").AnimPlayer = ap2.GetPath();
动态创建AnimationTree:
var at = new AnimationTree();
at.Name = "AnimationTree";
at.AnimPlayer = ap2.GetPath();
var bt = new AnimationNodeBlendTree();
at.TreeRoot = bt;
AddChild(at);
var ana = new AnimationNodeAnimation();
ana.Animation = "Walking_A";
bt.AddNode("Walk", ana);
var anaAttack = new AnimationNodeAnimation();
anaAttack.Animation = "1H_Melee_Attack_Chop";
bt.AddNode("Attack", anaAttack);
var b2 = new AnimationNodeBlend2();
bt.AddNode("Blend2", b2);
// ConnectNode属于链式,可后接AnimationNodeOneShot等多步处理
bt.ConnectNode("Blend2", 0, "Walk");
bt.ConnectNode("Blend2", 1, "Attack");
// output已内置,无需AddNode
bt.ConnectNode("output", 0, "Blend2");
//at.Active = false;
// 遵守原动画循环设定,支持“首个循环播放”混合“第二个播一次”
at.Set("parameters/Blend2/blend_amount", 0.5);
骨骼:
作用 - 使物体具有肢体活动的能力。
var sk=GetNode("player/Skeleton/Skeleton3D");
//var c = sk.GetBoneCount(); GD.Print(c);
// 3D模型节点“在编辑器中打开”->“新建继承”(即存为场景)-> 选中Skeleton3D骨架节点右键“创建物理骨架”(即含碰撞体的PhysicalBone3D)
// 射线碰到骨骼体后判断命中:raycast.collider.bone_name == "爆头骨骼名"
sk.PhysicalBonesStartSimulation(); // PhysicalBone3D.bone_name指定3D模型资源内定义的骨骼部位名。
骨骼映射/导入重定向(Retargeting) - https://docs.godotengine.org/zh-cn/4.x/tutorials/assets_pipeline/retargeting_3d_skeletons.html
导入时选中Skeleton3D节点,点击“骨骼映射->BoneMap->Profile:SkeletonProfileHumanoid”,窗口Filtered Tracks所示为映射后的标准骨头部位名。
骨骼名 - Upper Arm即上臂,Forearm或Lower Arm为前臂;Upper Arm即大腿,Lower Leg为小腿
有限状态机(Finite State Machine):
N种状态下处于单一状态 - 比如老写法currentState = enumState.IDLE;switch (currentState) { case enumState.IDLE: ... }
FSM写法(适合于状态多于3种的情况) - 将enumState的每个状态分散到单个(基于StateBase)子类中单独控制。
Godot+C#状态机实例(相同状态时应改为跳过) - https://github.com/spaceyjase/sr-6 https://www.bilibili.com/video/BV1z34y1A7rg/
3D移动人物FSM - https://www.bilibili.com/video/BV1de411i7Lh/ C#非移动人物版 - https://www.youtube.com/watch?v=Kcg1SEgDqyk
多层次状态机原理 - https://zhuanlan.zhihu.com/p/662567305
并发状态机 - 即两个状态机分别负责腿部动作(站立,下蹲,奔跑)和手部动作(持枪,空手,瞄准)
把攻击动画的控制骨骼限制在上身,下身依然执行的是老动画walk。
并发状态机参考示例 - https://info.congci.com/main/infomations/articles/8df94b5f-c8cd-11ee-904e-592f6ee49b9d