200+篇教程总入口,欢迎收藏:
放牛的星星:[教程汇总+持续更新]Unity从入门到入坟——收藏这一篇就够了zhuanlan.zhihu.com本文重点内容:
1、记录动画
2、创建可以玩的敌人动画
3、混合动画
4、使用已有的模型和动画
这是有关创建简单的塔防游戏的系列教程的第六篇也是最后一部分。这是关于为敌人设置动画,包括录制新动画和导入现有资产。
本教程是CatLikeCoding系列的一部分,原文地址见文章底部。
本教程使用Unity 2018.4.9f1编写。
1 跳跃的敌人
到目前为止,我们的敌人只是在棋盘上滑动。这对于使用立方体和球体作为敌人的抽象游戏来说是没问题的,但即使是这样的敌人也可以通过让它们以更生动的方式移动而变得更有趣。我们可以通过基于时间的绝对正弦波来调整它们的垂直位置,从而使它们反弹,但一般的方法是使用动画剪辑(animation clip)。我们将使用animation,因为它允许更复杂的移动,也也可以导入现有的动画。
1.1 动画剪辑
通过记录对对象层次结构的调整,我们可以在Unity编辑器中创建动画剪辑。将中等敌人的立方体预制实例拖到场景中,或将一个单独的场景拖到动画录制(animation record)中。然后选择敌人模型的Cube子级,通过Window/ Animation / Animation打开动画窗口。
由于尚未对立方体设置动画,因此Animation 窗口将显示create按钮。按下它会将一个Animator组件附加到Cube,并创建两个资产,一个用于立方体的Animation Controller和一个动画剪辑,我们将其命名为Enemy Move。
动画剪辑资产包含动画的数据,它目前仍然是空的。选择它将显示一个默认启用的循环时间切换,这意味着它表示一个循环动画。这是正确的,因为只要敌人在运动,运动动画就应该重复。
添加到Cube的Animator组件具有对也创建的动画控制器资产的引用。
控制器是一种状态机,可能会变得非常复杂,但最初它仅具有直接进入我们创建的动画的进入状态,以及所有所有状态。你可以通过打开Animator 窗口来看到它们,过双击控制器或按其Open按钮来进行操作。
1.2 录制动画
要记录移动动画,请选择Cube,然后在Animation窗口中按红点记录按钮。我们将创建一个持续时间为一秒的简单反弹动画。将时间线移至0:30,代表半秒。然后将Cube的垂直位置从0.25增加到0.75。这将创建两个关键帧,一个关键帧位于0:00,而原始帧则位于0:30。之后,将时间线移至1:00,将垂直位置设置回0.25,然后再次按录制按钮以停止录制。
你可以通过按录制按钮右边稍微一点的播放动画按钮来预览动画。
立方体的位置在关键帧之间插入,导致它在0.25和0.75之间上下移动。我们可以把轨迹变成抛物线,让它看起来更有组织性。通过动画窗口底部的按钮从Dopesheet切换到Cuves。这向我们展示了用于在关键帧之间插入的曲线。你可以通过改变滚动条的大小来放大。然后在0:00点为Y选择关键点,并向上拖动它的切线,直到看起来合适为止。在1的位置对关键帧做同样的操作。因为运动应该是平缓完整的,所以切线不需要改变。
你可以用缩放调整来美化动画,但这已经足够让敌人看起来栩栩如生了。
1.3 配置动画
即使使用相同的3D模型,敌人也可以具有不同的动画。相反,具有不同模型的敌人可以具有相同的动画。因此,我们将可以通过单独的EnemyAnimationConfig资产类型为每个敌人配置动画剪辑,从而可以轻松共享配置。由于此时我们只有一个移动动画,因此这是目前唯一可存储的剪辑。
创建一个动画配置资产并将移动动画分配给它。
在此配置中向敌人添加一个可序列化的字段,然后为所有敌方预制件提供对我们单个动画配置资产的引用,因为我们的动画可同时用于立方体和球体。
2 播放动画
可以使用动画控制器为敌人设置动画,但是对于我们简单的敌人行为而言,它是一种笨拙而僵化的方法。除此之外,可能同时有许多敌人还活着,他们都需要自己的控制器,因此控制动画的逻辑应尽可能简单。最后,我们希望对每个敌人使用不同的动画,而它们都共享相同的逻辑。因此,我们将不依赖Unity的动画控制器而创建自己的动画控制器。仅需要Unity的动画控制器来记录动画。
2.1 Enemy动画器
Enemy可以自己处理动画,但是逻辑仍然相当复杂,所以我们将它隔离在一个单独的可序列化的EnemyAnimator结构类型中。它依赖于UnityEngine中的类型。动画和UnityEngine。Playables名称空间,我们会用到它们。
添加一个字段到Enemy上。
为了完成其工作,EnemyAnimator需要三种公共方法。首先配置以设置动画状态,为此我们需要一个Animator组件和动画配置。第二播放开始播放,第三停止播放停止。
在Enemy中,Recycle停止播放,然后在Initialize中开始播放。
我们也可以在Initialize中配置动画制作器,但只需要执行一次即可,因此让在Awake中进行更好。这样一来,如果在某个时候要重用敌人,就不会发生不必要的额外配置。
与其给所有敌方预制件一个Animator组件,不如在这里以代码的方式创建它。无论我们要的动画的是什么,都必须是模型的子节点,因此将其添加到该对象的第一个子节点吧。
2.2 Playable Graphs
通过可播放视图(Playable Graphs)来控制对象的动画状态,该视图以native代码而不是C#存在。可以通过PlayableGraph结构进行控制,该结构包含对native数据的引用。通过静态PlayableGraph.Create方法创建图形。所有Playables都是以类似的方式创建的。
最初,图视图是非激活的。我们可以通过调用Play来激活它,也可以通过调用Stop来停止它。但是,由于我们不会在本教程系列中重用敌人,因此应调用Destroy销毁原始视图数据,否则它会残留下来。
视图可以自我更新,但是我们可以告诉它们应该如何进行更新。我们需要链接到游戏时间的动画,该动画是通过在创建视图后调用DirectorUpdateMode.GameTime的SetTimeUpdateMode来配置的。
要播放动画剪辑,我们首先需要通过AnimationClipPlayable.Create创建其可播放的表示。还需要提供它所属的视图和动画剪辑作为参数。
然后,我们需要为该视图创建一个AnimationPlayableOutput,并带有一个附加名称和对用于动画的动画师组件的引用。通过SetSourcePlayable将剪辑设置为该输出的源。这将使我们的敌人弹跳起来。
中等立方体最终以锁定步幅进行动画处理,因为它们每秒刷新一次,这与动画持续时间相匹配。
为什么代码补全不能为Playables提供有用的文档?
Playables API主要由适用于通用Playable结构类型的扩展方法组成。因此,关于特定类型和方法的代码文档并不多。由于这种方法,在将Playables存储在变量中时,我也可以使用var。
2.3 调整动画速度
我们的移动动画每秒循环一次,并不适合所有敌人。对于某些人来说,它应该播放得更快,而对于其他人来说,它应该播放得更慢。通过在Play方法中添加速度参数来控制。通过GetOutput在索引零处获取视图的输出,通过GetSourcePlayable获取其可播放的源,并以提供的速度对其调用SetSpeed。
在Enemy.Initialize中提供所需的速度。更快的敌人需要更快的动画,因为它们会覆盖更多地面。而较大的敌人会走较大的步伐,因此需要较慢的动画速度。因此,我们使移动动画的速度等于敌人的速度除以其比例。
作为一个奖励,因为中等立方体并不是所有的都有完全相同的速度,它们不再以完全一致的步调动起来。
2.4 可视化 Playable Graphs
如果要直观地检查生成的可播放图形,可以通过“Window / Package Manager”导入PlayableGraph Visualizer程序包。它的当前版本是0.2.1,这是预览版本,因此你需要启用Advanced下的Show preview才能看到它。导入后,你可以通过Window / Analysis / PlayableGraph Visualizer打开可视化器并进入播放模式。你无法选择特定的视图实例,因为它们没有唯一的名称,但是足以看到视图结构。
这里会注意到的一件事是,虽然仅在播放模式下创建了视图,但它们在退出播放模式后仍然存在。当敌人在比赛出口被摧毁时,就会发生这种情况。我们可以通过在EnemyAnimator中添加一个销毁视图的公共Destroy方法来解决此问题。此时,我们还可以更改Stop,使其停止而不是销毁视图,以支持将来的重用。
向Enemy添加一个OnDestoy方法,该方法可以销毁animator以始终销毁图形。
3 进入和退出
跳跃的敌人看起来比滑动的更加生动,但是,当他们出生和到达目的地,动画突然出现和消失又有些冲突。我们可以通过添加一个intro和outro动画使它看起来更好。
3.1 动画
要创建额外的动画,请返回设置为动画录制的敌人实例。选择Cube,然后在当前设置为敌人移动的Animation窗口中打开下拉菜单。选择创建New Clip... 两次,创建一个Enemy Intro和Enemy Outro动画。
对于 Intro,请将0:00的比例和位置设置为零,并将其原始值设置为0:30。用另一种方法来解决Outro,但是这次持续时间是一秒钟。另外,通过将其垂直位置增加到1.25并将其Y旋转设置为360°,使其生动活泼。
动画控件还为新动画获得了额外的状态,这些状态与它的图形断开连接。这没什么问题,因为我们只使用动画控制器来记录动画。
向EnemyAnimationConfig添加对Intro和Outro动画的支持。
3.2 混合动画剪辑
为了支持多种动画,我们需要向EnemyAnimator添加一个动画混合器。给它一个AnimationMixerPlayable字段来追踪它。
现在,我们还需要在Configure中创建一个混合器。除了视图之外,还提供动画剪辑的数量(现在为3)作为其Create方法的参数。然后将混合器作为输出源。
每个剪辑在混合器中都有自己的固定索引。让我们用嵌套在EnemyAnimator中的枚举类型定义它们。将其公开,以便敌人以后可以访问。
向混合器中添加剪辑最简单的方法是通过在混合器上调用ConnectInput,将剪辑的索引和可播放的剪辑作为参数。第三个参数指定剪辑的输出索引,它总是0。在Configure中对所有三个剪辑执行此操作。
3.3 切换剪辑
混合器根据其权重混合所有剪辑,默认情况下为零。一次只能有一个活动剪辑,我们可以通过将其权重设置为1并将所有其他权重设置为零来实现。追踪当前活动的剪辑很方便,因此可以为其添加一个属性。公开获取者,以便Enemy也可以访问它。
现在,用更具体的PlayIntro方法替换Play方法。它不需要速度,而是使用混合索引(intro index)调用混合器上的SetInputWeight,将剪辑的权重设置为1,设置当前剪辑,并播放视图。
然后添加带有速度参数的PlayMove方法。它将当前剪辑的权重设置为零(以防你稍后在Intro和移动之间插入动画),而将移动剪辑的权重设置为1,设置速度,并更新当前剪辑。可以通过在混合器上使用适当的索引调用GetInput来检索特定剪辑的可播放句柄。
再添加一个PlayOutro方法,该方法可以切换到outro剪辑。
3.4 播放Intro
调整Enemy.Initialize,以便它调用PlayIntro而不是Play。
我们需要延迟移动直到Intro动画完成。EnemyAnimator可以通过抓取混合器的当前剪辑并调用IsDone来检查这个。通过属性公开它。
现在我们必须检查Enemy.GameUpdate的开头是否正在播放Intro剪辑。如果是这样,并且没有完成,请跳过该方法的其余部分,否则以速度调用PlayMove并继续进行。
这意味着我们延迟更新敌人的位置,所以必须确保在PrepareIntro中正确设置了它。
现在,我们得到了陷入重复的Intro动画中的敌人。解决此问题的第一步是禁用Intro动画剪辑的Loop Time选项。
但这还不够。因为我们自己创建了一个可播放的视图,所以如果需要检测何时完成,我们需要明确设置非循环剪辑的持续时间。在EnemyAnimator.Configure中,在简介剪辑上调用SetDuration,并提供其剪辑的长度作为参数。
敌人现在可以从出生到移动正常了,但是移动的一部分被跳过了,存在不连续性。发生这种情况是因为所有剪辑耗费时间都流逝,跟他们的权重没有关系。可以通过在Configure中创建移动剪辑并在PlayMove中播放它时暂停移动剪辑来解决此问题。
3.5 播放Outro
outro剪辑需要类似的处理。禁用其Loop Time选项,并在Configure中设置其持续时间。初始化还要暂停它,就像移动动画一样。
在PlayOutro中播放剪辑。
若要播放Outro,请在Enemy.GameUpdate中调用PlayOutro,在到达目的地时将其回收,调用PlayOutro。返回true,以使其不断更新。
现在我们还需要在GameUpdate开始时检查是否运行了outro。如果是,我们要么完成并可以回收,要么需要继续播放并返回true。
4 动画过渡
从intro到move的过渡是正确的,但是从move到outro的过渡存在问题。move和outro动画是否对齐取决于敌人的速度和行进距离,而这是可变的。解决这些动画之间的困难过渡的唯一方法是混合它们。
4.1 开始过渡
我们通过线性插值权重来在两个动画之间进行混合,前一个剪辑的权重从1减小,而当前剪辑的权重从零开始增大。为了追踪此过渡,EnemyAnimator还需要追踪先前的剪辑和过渡的进度。
添加一个BeginTransition方法,以下一个剪辑的枚举值作为参数。它需要使当前剪辑与前一个剪辑相同,设置新的当前剪辑,将过渡进程设置为零,并播放当前剪辑。
在PlayMove和PlayOutro中调用此方法以便使用适当的剪辑。除了设置移动速度,这就是他们现在要做的全部事情 了。
我们也把intro到move做了融合,因为不能保证它们会对齐,而现在展示的只是简单的敌人动画的情况。
4.2 进度化过渡
每次游戏的Update都需要进行过渡,因此请向EnemyAnimator添加公共GameUpdate方法。用时间乘以某种过渡速度来增加进度。过渡应该很快,所以我们使用5以表示持续0.2秒。
如果过渡完成,则将当前剪辑的权重设置为1。还要将前一个剪辑的权重设置为零并将其暂停。否则,使权重分别等于进度和1减去进度。
仅当正在进行过渡时才需要这样做。我们可以使用进度值为-1表示没有过渡。
在Enemy.GameUpdate的开始处调用animator的GameUpdate方法以启用过渡。
5 将死的敌人
Intro, move,outro动画现在可以正常工作并融合。下一步是为敌人死亡时添加动画。
5.1 死亡动画
为将死的敌人创建新的动画。像outro动画一样,将死的动画可以通过将其比例减小到零来销毁敌人。如果只是添加悬浮的旋转不太合适,给它一个更合适的动画,例如滚动。我们在半秒内将Z位置增加到0.5,同时将X旋转增加到90°。然后在下半秒将标度和Y位置降低到零。完成后,将其添加到EnemyAnimationConfig。
通过向枚举添加第四个值,在Configure中创建其剪辑,并添加开始适当过渡的PlayDying方法,也可以向EnemyAnimator添加对它的支持。
5.2 不再是瞬间死亡
当生命值降为0时,调用PlayDying并返回true,而不是立即回收敌人。由于死亡剪辑出现在outo剪辑之后,我们可以通过检查当前剪辑是否至少是outo剪辑而不是精确匹配来捕获。
5.3 只对移动敌人生效
塔不知道敌人的状态,所以会一直瞄准它,即使它已经死亡。这也同样适用于那些正在正在播放outro的敌人。而播放intro的敌人也不会立即死亡,尽管他们开始移动后可能马上就会死亡。为了从游戏玩法的角度保持简单和高效,让我们强制塔只瞄准和伤害移动的敌人。
我们可以通过禁用碰撞器来使其无法瞄准敌人。为此,请向Enemy添加一个碰撞器字段。可以通过编辑器使它可配置,但是让我们给它一个公共的setter属性,该属性只能被调用一次。
TargetPoint附加到具有碰撞器的同一个游戏对象上,因此当它Awake时,抓住该碰撞器并将其分配给敌人。
从播放intro开始,在__Enemy__ .Initialize中禁用碰撞器。
在播放dying或outro动画时,也请在GameUpdate中禁用碰撞器,并在播放移动动画时将其启用。
我们还需要确保塔停止跟踪那些不再有效的目标。给敌人一个属性,指示它是否是有效的目标(移动时就是这种情况)。
如果不是这种情况,则Tower.TrackTarget必须返回false。
6 导入模型和动画
尽管可以在Unity编辑器中创建简单的动画,但通常会将它们与3D模型一起导入。你可以在单独的程序中自己创建它们,也可以从资产存储库等其他地方获取它们。例如,我将从Unity的3D Game Kit中导入掷弹兵。
6.1 掷弹兵
转到资产商店,然后从Unity Technologies搜索3D Game Kit - Character Pack。下载然后导入。你只需导入掷弹兵模型及其依赖项即可。不要获取整个3D游戏工具包,因为它太大了,会弄乱你的项目。
掷弹兵对我们来说太大了。通过选择模型来缩小它,转到模型选项卡,将它的比例系数减少到0.25。对于我们最终使用的所有动画,你也需要这样做,因为否则模型将会分裂引发异常表现。
创建一个以掷弹兵为模型的预置敌人,来取代立方体或球体。在骨架层次中添加目标点和碰撞器到Grenadier球体对象,因为那是它的质心。将碰撞器的比例设置为0.125,因为我们还没有像导入模型时那样的缩放参数。
现在我们已经可以使用掷弹兵敌人了,例如简单地替换现有场景波中的立方体敌人。但这看起来相当愚蠢,因为掷弹兵以他们默认的T姿势来回弹跳,而且是悬浮的。
6.2 动画选择
给掷弹兵配置自己的动画。我们可以使用GrenadierWalk动画进行运动,使用GrenadierCloseRangeAttack进行intro和outro,以及使用GrenadierDeath进行死亡。所有资源均位于资源内的AnimationClips文件夹下,其名称前带有@。确保将所有这些资产的比例参数设置为0.25。另外,请转到其Animation选项卡并删除Events下的所有条目,因为将其保留会导致错误。
不幸的是,我们不能直接使用GrenadierWalk动画,因为它已经加入了向前运动,而我们只需要一个可以原地行走的动画。因此,复制该动画剪辑并选择它。我们要做的就是在Animation窗口的左侧找到Grenadier_Root:Position行,并通过其上下文菜单中的Remove Properties选项将其删除。
6.3 调整行走速度
掷弹兵的行走速度与它在游戏中的速度不匹配,这导致即使在向前移动时也会滑步。这是因为动画剪辑没有覆盖每秒一个单位的值。我们将通过在EnemyAnimationConfig中添加一个移动动画速度配置选项来弥补这一点,默认设置为1。
在Enemy.GameUpdate中将这个值计入移动速度。
在掷弹兵的示例下,我们需要加倍动画速度,使其排成一行。
请注意掷弹兵也有一个运行的动画。你可以为快速掷弹兵创建一个独立的预制件。
6.4 出现和消失
掷弹兵没有任何动画来缩小或扩大它。虽然可以编辑现有的动画来合并缩放,但这是一项烦人的工作,每次导入新的动画时都需要重新做。它更方便创建单独的动画出现和消失,并与现有的动画混合。
创建两个新动画,一个动画在0到1的范围内缩放,另一个动画进行相反的操作,都在半秒内完成。你可以为此使用立方体敌人设置的动画录制。然后将它们的配置选项添加到EnemyAnimationConfig。
为掷弹兵配置选择这些动画。请勿对立方体和球体执行此操作,因为它们已经出现并自行消失。
让EnemyAnimator分别追踪它是否有出现剪辑和消失剪辑,以实现最大的灵活性。再给它们添加到枚举里。
在Config中,如果我们至少有一个剪辑,则将剪辑的数量增加到六个。然后创建适当的可播放剪辑。
播放intro时,如果存在的话,也要全权重播放。这意味着两个剪辑的权重为1,只要它们不为相同的属性设置动画,它们就可以正常工作。因此,只有在导入的动画无法缩放其Root(通常不会缩放)的情况下,它才能正常工作。
移动开始时,我们不再需要出现的剪辑,因此可以在PlayMove中将其权重设置为零。
现在,当播放outro或dying的动画时,我们还需要播放消失剪辑(如果存在)。但是我们需要延迟该剪辑,因为我们假设消失的剪辑是最短的,所以两者都在同一时间结束。这是通过在剪辑上调用SetDelay的持续时间等于另一个剪辑的持续时间减去消失持续时间来完成的。
7 幸存和热重载
使用PlayableGraph的唯一问题是不可序列化。这不是构建中的问题,但是当热重新加载发生时,动画将在编辑器中停止。由于敌人依靠检测动画的结束来进行前进,因此他们可能会陷入困境。因此,这不仅仅是视觉上的故障。我们必须从热重载中恢复,以保持游戏正常运行。
7.1 重建 Playable Graph
EnemyAnimator是可序列化的,但是在热重载期间本机数据丢失后,其视图将无法工作。我们可以通过在视图上调用IsValid来检测到这一点。将其包装在公共属性中,以便敌人也可以检测到它。我们仅在编辑器中需要它,因此我们可以使代码成为条件语句。
要在热重载后恢复动画状态,我们需要创建一个新的视图。为此添加一个RestorAfterHotReload方法,该方法调用Configure,设置移动速度,将当前剪辑的权重设置为1,并播放该剪辑和视图。这不会恢复过渡效果,但是过渡纯粹是装饰性的,无论如何在热重载期间游戏都会冻结。
如果Enemy.GameUpdate无效,它现在需要先恢复动画,然后再执行其他任何操作。
7.2 恢复Clip时间
敌人现在会保留其动画,但其时间会恢复为零。为了保持时间,EnemyAnimator必须追踪它并在还原时进行设置。为了获得更高的精度,使用双精度而不是浮点数来跟踪时间。
为了使时间保持最新,需要在GameUpdate结束时对其进行检索。
7.3 出现和消失的重建
我们还可以还原出现动画。如果我们要还原intro剪辑,并且出现剪辑存在,请与当前剪辑同时激活出现剪辑。
消失剪辑的效果是一样的,但是只当我们恢复outro和dying的动画时。在这种情况下,消失延迟必须由当前剪辑时间来减少。如果延迟仍然是正的,那么这就是剩余的延迟。如果它是负的,那意味着消失的动画已经在播放它的时间等于负的延迟。
《塔防》教程系列到此结束。你可以将其用作自己的游戏的起点,也可以将其转变为其他东西。可以添加声音,GUI,保存/加载功能,更多塔类型,其他游戏瓦片内容等等。
如何让视图与敌人复用一起生效?
在再次Play视图之前,你需要将所有剪辑的时间设置为零并将其暂停。最后一个活动剪辑的权重也需要变为零。最后,需要通过在非循环剪辑上调用SetDone(false)来重置其完成状态。
本文翻译自 Jasper Flick的系列教程