上一节,我们学习了如何在地图中建造防御塔、锁定射程内的敌人,并发射激光束攻击敌人。由于被吐槽敌人都是方块不好看,本节将先重点学习如何导入敌人模型,让画面好看些。
模型和动画(Models and Animations)
尽管可以在Unity编辑器中创建简单的动画,但通常会将它们与3D模型一起通过.fbx文件导入。你可以通过3DMax、Maya等程序中创建它们,也可以从Unity资产存储库等其他地方获取它们。在本文,我将从Unity的3D Game Kit中导入掷弹兵。
1.掷弹兵模型(Grenadier Model)
1.1.导入模型(Importing Model)
访问https://assetstore.unity.com/,搜索“3D Game Kit – Character Pack”,收藏并下载资源到Unity包管理器。不要导入整个3D游戏工具包,因为它太大了,会弄乱你的项目,只需导入Grenadier掷弹兵模型+依赖的Shaders目录即可。
掷弹兵对我们来说太大了。通过选择模型来缩小它,转到模型选项卡,将它的比例系数减少到0.25。对于我们最终使用的所有动画,你也需要这样做,因为否则模型将会分裂引发异常表现。
1.2.替换敌人(Replace Enemy Cube)
把掷弹兵模型拖到Enemy预置敌人中来取代立方体。添加TargetPoint和SphereCollider碰撞器到Grenadier_Sphere对象,因为那是它的质心。将碰撞器的半径设置为0.125,因为我们还没有像导入模型时那样的缩放参数。同时设置Grenadier模型的图层为Enemy层。
运行后可以看到方块被替换为了Grenadier模型:
调下相机位置和角度:
现在还有个问题,敌人的腿不动,是在平移,需要通过动画让他走起来。
2.动画(Animations)
我们需要给掷弹兵配置自己的动画。导入的所有动画资源均位于Grenadier/AnimationClips目录下,其名称前带有@。我们可以使用@GrenadierWalk动画进行运动,使用@GrenadierCloseRangeAttack进行intro和outro,以及使用@GrenadierDeath进行死亡。确保将所有这些动画的缩放系数设置为0.25。另外,请转到其Animation选项卡并删除Events下的所有条目,因为将其保留会导致错误。
2.1.行走动画(Walk Animation)
a.选择GrenadierWalk动画,播放看下效果:
b.改为原地行走
你会发现,它已经加入了向前运动,而我们只需要一个可以原地行走的动画。我们需要修改两处:
取消Grenadier模型-Animator-Apply Root Motion应用根运动选项的选中状态:
参考下图修改@GrenadierWalk/@GrenadierWalkFast动画的Animation属性:
单独播放这个动画就可以看到变为原地行走了
c.创建动画配置/控制器脚本
创建动画配置EnemyAnimationConfig.cs、控制器脚本EnemyAnimatorController.cs。创建EnemyAnimationConfig.cs对应的预制,配置Move属性为@GrenadierWalk。
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
// 敌人动画配置
[CreateAssetMenu]
public class EnemyAnimationConfig : ScriptableObject{
// 行走动画
[SerializeField]
AnimationClip move = default;
public AnimationClip Move => move;
}
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Animations;
using UnityEngine.Playables;
// 敌人动画控制器
[System.Serializable]
public struct EnemyAnimatorController {
// 可播放视图
PlayableGraph graph;
// 初始化Animator组件和动画配置
public void Configure(Animator animator, EnemyAnimationConfig config){
// 创建视图
graph = PlayableGraph.Create();
graph.SetTimeUpdateMode(DirectorUpdateMode.GameTime); //应设置更新模式
// 创建播放对象,传入视图和要播放的动画剪辑
var clip = AnimationClipPlayable.Create(graph, config.Move);
var output = AnimationPlayableOutput.Create(graph, "Enemy", animator);
output.SetSourcePlayable(clip); //将剪辑设置为输出源
}
// 播放
public void Play(float speed){
graph.GetOutput(0).GetSourcePlayable().SetSpeed(speed);
graph.Play();
}
// 停止
public void Stop(){
graph.Destroy(); // 如果需要复用,也可以调用Stop
}
}
d.播放和停止动画
// 敌人
public class Enemy : MonoBehaviour{
...
// 速度
public float Speed { get; private set;}
// 动画配置
[SerializeField]
EnemyAnimationConfig animationConfig = default;
// 动画控制器
EnemyAnimatorController animator = default;
void Awake(){
// 初始化敌人动画对象
animator.Configure(model.GetChild(0).gameObject.GetComponent<Animator>(), animationConfig); //注意这里的GetChild(0),把之前的Cube删掉,一定要确保Grenadier在Model下变第一个
}
// 初始化
public void Initialize(float scale, float pathOffset, float speed=1){
...
Speed = speed;
// 播放动画
animator.Play(Speed * Scale);
}
// 回收
public void Reclaim(){
animator.Stop(); //停止播放动画
OriginFactory.Reclaim(this);
}
...
}
e.运行可以看到腿开始交替行走了
2.2.消失动画(Death Animation)
添加其他动画方法和行走类似。
a.添加动画配置
// 敌人动画配置
[CreateAssetMenu]
public class EnemyAnimationConfig : ScriptableObject{
// 动画
[SerializeField]
AnimationClip move = default, attack = default, death = default;
public AnimationClip Move => move; //行走
public AnimationClip Attack => attack; //攻击
public AnimationClip Death => death; //死亡
...
}
b.实现多动画剪辑选择性播放
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Animations;
using UnityEngine.Playables;
// 敌人动画控制
[System.Serializable]
public struct EnemyAnimator {
// 可播放视图
PlayableGraph graph;
// 剪辑分类
public enum Clip { Move, Attack, Death }
// 混剪管理器
AnimationMixerPlayable mixer;
//AnimationClipPlayable = clipMove, clipAttack, clipDeath;
// 初始化Animator组件和动画配置
public void Configure(Animator animator, EnemyAnimationConfig config){
// 创建视图
graph = PlayableGraph.Create();
graph.SetTimeUpdateMode(DirectorUpdateMode.GameTime); //应设置更新模式
// 创建剪辑
int size = System.Enum.GetNames(typeof(Clip)).Length;
var clipMove = AnimationClipPlayable.Create(graph, config.Move);
var clipAttack = AnimationClipPlayable.Create(graph, config.Attack);
var clipDeath = AnimationClipPlayable.Create(graph, config.Death);
clipMove.Pause();
clipAttack.Pause();
clipDeath.Pause();
// 添加剪辑到可播放项中
mixer = AnimationMixerPlayable.Create(graph, size, true);
mixer.ConnectInput((int)Clip.Move, clipMove, 0);
mixer.ConnectInput((int)Clip.Attack, clipAttack, 0);
mixer.ConnectInput((int)Clip.Death, clipDeath, 0);
// 创建动画输出对象
var output = AnimationPlayableOutput.Create(graph, "EnemyOutput", animator);
output.SetSourcePlayable(mixer); //clipMove); // 将可播放项连接到输出
// 开始播放(因所有剪辑都被设置了暂停,所以实际并不会播放,需要执行剪辑.play才可以)
graph.Play();
Debug.Log("mixer.GetInputCount: " + mixer.GetInputCount());
}
// 行走
public void Move(float speed){
Debug.Log("Move! " + (int)Clip.Move);
// 单动画播放
//graph.GetOutput(0).GetSourcePlayable().SetSpeed(speed);
//graph.Play();
// 播放移动剪辑
mixer.SetInputWeight((int)Clip.Move, 1); // 设置剪辑播放权重
var clip = mixer.GetInput((int)Clip.Move); //返回对应剪辑
clip.SetSpeed(speed);
clip.Play(); //播放剪辑
}
// 攻击
public void Attack(){
Debug.Log("Attack! " + (int)Clip.Attack);
mixer.GetInput((int)Clip.Attack).Play();
}
// 死亡
public void Death(){
Debug.Log("Death! " + (int)Clip.Death);
// 播放死亡剪辑
//mixer.GetInput((int)Clip.Move).Pause();
mixer.SetInputWeight((int)Clip.Death, 1); // 设置剪辑播放权重
var clip = mixer.GetInput((int)Clip.Death);
clip.SetSpeed(5); //加速5倍播放
clip.Play(); //播放剪辑
// 销毁
//graph.Destroy(); // 如果需要复用,也可以调用Stop
}
}
c.敌人播放行走和死亡动画
// 敌人
public class Enemy : MonoBehaviour{
...
// 初始化
public void Initialize(float scale, float pathOffset, float speed=1){
...
// 播放行走动画
animator.Move(animationConfig.MoveAnimationSpeed * Speed * Scale);
}
// 更新敌人位置等信息(敌人死亡时返回false)
public bool GameUpdate(){
...
if(Health <= 10f){
animator.Death(); //提前播放死亡动画(播放动画如何阻塞enemy/graph的销毁需要研究下)
}
...
}
...
}
*注意:死亡动画的播放位置应该在Reclaim的时候,但是暂时没找到动画阻塞敌人销毁的方法,先临时在血量不足时播放,后边找到方法再改。
d.运行效果
yan 21.8.29
参考:
Tower Defense – Importing Models and Animations
【Unity3D】3D模型的使用——FBX的使用与Animation设置