从此

Godot开发、游戏动画(Animation)

游戏动画、状态机、骨骼蒙皮


动画

动画:
  动画树(AnimationTree)支持更高级的多动画混合、过渡处理等,AnimationPlayer则只支持一个简单的两动画混合过渡功能set_blend_time


  轻量级补间动画节点:
    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)); // 结束时释放。


  动画节点:
    动画面板 - ALT+N  AnimationPlayer是通过AnimationLibrary列表(libraries)来存取动画的。
    AnimationPlayer全局(或默认)动画 - ap.GetAnimationLibrary(""); // 非全局则将入参改为具体动画库名字。
    动画时间刻度上选取标记点并“Add Marker Key”后,可到AnimationNodeAnimation的“Set Custom Timeline from Marker”中设置开始和结束Marker的动画范围。

    添加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行为。

    播放规律:动画没播完,再次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.tree_root设为AnimationNodeBlendTree,即 IDE->AnimationTree->“根”视图,anim_player选中AnimationPlay节点供tree_root可视化编排;
      若在未继承的模型场景内,可点“⋮ -> 编辑”直接输入“../AnimationPlayer”(全路径超过260字符可临时用:“../../Player/AnimationPlayer”),依然能被IDE感知到其动画列表;

      可视化“动画树”窗口->“根”->添加节点->“添加动画”,即用于连线最基本单位的AnimationNodeAnimation。
      如果动画在模型文件内,可通过AnimationLibrary方式导入后再用。

      其他 - 
        AnimationNodeTransition是简单版AnimationNodeStateMachine,可用于切换Flag状态:animationTree.Set("parameters/Transition/transition_request", "state_2"); // 空字符串为清除状态。


      触发 - 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);


根运动方向偏移:
  Quaternion rootMotionRotation = at.GetRootMotionRotation();
  GD.Print(rootMotionRotation);
  Transform3D currentTransform = player.GlobalTransform;
  currentTransform.Basis *= new Basis(rootMotionRotation);
  player.GlobalTransform = currentTransform;