07-创建动画
1. 创建动画
1.1 选中我们的青蛙🐸
1.2 点击 Window
会打开 Animation 的窗口
上面显示了,如果你想 Frog 创建动画,你需要创建一个 Animator 控制器和 Animator Clip「动画片段」
1.3 创建 Animator
你可以直接点击 create 创建,它会自动创建和控制器。
- 添加 Animator 控制器
因为不仅仅是青蛙要动画,我们的小汽车也需要动画。
Animation 是一个片段,Animator Controller 是一个动画控制器。一个控制器 可以控制动画的多个片段,所以我们需要添加控制器 Animator Controller。
我们就可以看见 Animation 看起来像一个时间轴:
我们就可以把每个时间点显示的图片放在上面就可以了。
全选 20 张 idle 图片
拖入成功如下图:
我们可以看见,现在的动画不到一秒钟:
原因是我们现在的采样率的问题。
显示我们 Samples 。
我们可以看见,我们现在的采样率是一秒钟 60 张,而我们只有 20 张图片。
所以,我们可以把我们的采样率改成 20,这样我们就可以完整的显示一秒了。
这样就可以了。
这里我们使用按秒显示。
1.4 我们如何查看动画效果呢?
当然,你现在点击运行游戏,游戏的青蛙现在也是会动的。
但是我们现在还没实现跳跃的动画。
1.5 创建跳跃动画
当然,现在没有之前的 create 的窗口了:
那怎么办呢?
- 保存路径和之前一样
- 保存名称:
Frog_Jump.anim
青蛙已经可以动起来了。
跳跃我们是不需要循环播放的,取消即可。
1.6 显示 Animator
我们可以直接拖拽,摆放到你想摆放的位置。
- 鼠标滚轮可以调整大小;
- 鼠标滚轮按住,就可以调整位置;
左侧有 Layers,不同的 Layers 执行不同的图层。
不过,在我们的项目中,用不到,就不多讲了。
我们要用到的是 Parameters。
这个参数很重要,涉及一种动画,切换到另外一种动画状态的条件。
我们现在点击运行,你会发现 Frog_idle 一直在运行:
要从 Frog_idle 到 Frog_Jump 需要有一个状态的连线。「橘黄色代表默认执行的」
1.7 添加连线
这个就是把它设为动画图层的当前状态。
不过,我们还是要默认播放 Frog_idle。
我们需要添加的是 Frog_idle 到 Frog_jump:
但是我们现在还没有 Conditions。也就是它切换的条件。
所以,我们要添加我们的条件:
可以看见,我们可以添加四种切换条件。
我们使用出发的方式 Trigger:
每次,只要我 Trigger 一下,就会触发切换到这个状态。
添加 conditions:
那我们现在可以点击运行测试一下:
当时你认真观察👀会发现,它卡在最后一帧。那么我们要怎么解决这个问题呢?
——播放完后,要返回回来。
返回的条件是什么呢?
选择返回的那条连接线,我们不需要额外的 conditions。
- Has Exit Time:这个切换条件,是有固定的切换时间的。
- Exit Time:代表退出的时间,是一个向量化的时间。0 到 1 之间。
- 1 代表百分之百,完全播放完,才会切换到下一个条件。「所以,我们这里可以写成 1 ,代表百分之百完成跳跃之后,我们切换回 Frog_idle。
切换的时候,我们不需要两个动画的混合,所以下面的我们可以选择取消:
在有些 3D 动画是需要混合的,比如跑动切换回站立的时候。
这样有一个缓慢的过渡动画,看起来会比较真实。
那么跳跃回来的时候,我们需要完全播放完。那么 idel 过去的时候,我们可以不需要完全播放。
因为,我们有可能随时点一下 Jump 所以,Frog_idle 不需要完全播放完成。
应该在点击屏幕的时候,马上进行跳跃。——也就是不需要 Has Exit Time:
同时,我们也不需要过渡:
保存你的项目,我们点击 Play 测试一下。
你会发现,现在点击参数 Parameters 的 Jump 就能正常的进行动画的切换了。
那么你会知道,我们是需要代码来调用 Jump 而不是手动点击 Jump 的状态。
如何使用代码切换动画的状态呢?
2. 编写跳跃触发代码
我们要用到 Animator 就要导入我们的 Animator 组件。
// --snip---
public class PlayerController : MonoBehaviour
{
// 组件一般写在上面
// 一般用两种方法: 一种就是 public 但是不推荐,因为 public 实现,需要我们自己去 Unity 里面去拖拽
// private 我们可以实现获取 Frog 自身身上的 Rigidbody2D 组件
private Rigidbody2D rb;
private Animator anim;
// --snip---
private void Awake() // 会在 start 之前执行
{
rb = GetComponent<Rigidbody2D>(); // 获得自身身上的组件
anim = GetComponent<Animator>(); // 获取 Animator 组件
}
// --snip---
}
我们可以对我们的代码进行分区:
//--snip---
#region INPUT 输入回调函数
public void Jump(InputAction.CallbackContext context)
{
//--snip---
}
//--snip---
public void GetTouchPosition(InputAction.CallbackContext context)
{
}
#endregion
}
这样分区的好处是什么呢?——代码块可以折叠
完美😍折叠。
//---snip---
public class PlayerController : MonoBehaviour
{
//---snip---
#region INPUT 输入回调函数
private void TriggerJump()
{
//TODO:获得移动方向、播放动画
anim.SetTrigger("Jump"); // 和在 unity 里面设置的参数名称一致才可以,大小写要一致
// anim.SetBool();
// anim.SetFloat();
// anim.SetInteger();
}
}
那么什么时候触发动画呢?
——我们会在 Update 里面触发动画。
之前写了到了跳跃的目的之后是:isJump = false;
那么现在,我希望通过动画的完成,告诉我们的系统,isJump 的状态改变了。
所以下面的代码就可以不需要了:
// isJump 什么时候变成 flase 呢?
// FIXME:临时操作
// if (transform.position.y == destination.y)
if (destination.y - transform.position.y < 0.1f)
{
isJump = false;
}
删掉之后,我们现在要确定我们是可以跳跃的,那需要确定什么呢?
确定状态,也就是需要一个变量,让程序知道,我们现在是可以跳跃的。所以,我们添加一个变量和编写对应的代码:
private bool canJump;
// ---snip---
public class PlayerController : MonoBehaviour
{
// ---snip---
private bool canJump;
// ---snip---
private void Update()
{
if (canJump)
{
TriggerJump();
}
}
// ---snip---
#region INPUT 输入回调函数
public void Jump(InputAction.CallbackContext context)
{
if (context.performed && !isJump) // 要执行跳跃,那前提是当前的青蛙没有跳跃
{
moveDistance = jumpDistance;
destination = new Vector2(transform.position.x, transform.position.y + moveDistance);
// isJump = true;
canJump = true;
}
}
public void LongJump(InputAction.CallbackContext context)
{
// ---snip---
// canceled 取消了
if (context.canceled && buttonHeld && !isJump)
{
buttonHeld = false; // 把状态改回来
destination = new Vector2(transform.position.x, transform.position.y + moveDistance);
// isJump = true;
canJump = true;
}
}
// ---snip---
#endregion
/// <summary>
/// 触发执行跳跃动画
/// </summary>
private void TriggerJump()
{
//TODO:获得移动方向、播放动画
anim.SetTrigger("Jump"); // 和在 unity 里面设置的参数名称一致才可以,大小写要一致
// anim.SetBool();
// anim.SetFloat();
// anim.SetInteger();
}
#region Animation Event
public void JumpAnimationEvent() // 跳跃的动画事件
{
// 改变状态
isJump = true;
}
// 还需要另外一个状态,也就是跳跃结束的时候,状态改为 false
public void FinishJumpAnimationEvent()
{
isJump = false;
}
#endregion
}
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.InputSystem;
public class PlayerController : MonoBehaviour
{
// 组件一般写在上面
// 一般用两种方法: 一种就是 public 但是不推荐,因为 public 实现,需要我们自己去 Unity 里面去拖拽
// private 我们可以实现获取 Frog 自身身上的 Rigidbody2D 组件
private Rigidbody2D rb;
private Animator anim;
public float jumpDistance;
private float moveDistance; // 真实跳跃距离
private bool buttonHeld; // 代表是否长按
private Vector2 destination; // 用来存储计算的值
private bool isJump;
private bool canJump;
// 我们要在我们的游戏最最开始第一帧执行,那么有一个周期函数是在 start 函数之前执行的,也就是 Awake
// 「Unity 为我们提供好的周期代码函数」
private void Awake() // 会在 start 之前执行
{
rb = GetComponent<Rigidbody2D>(); // 获得自身身上的组件
anim = GetComponent<Animator>(); // 获取 Animator 组件
}
private void Update()
{
// isJump 什么时候变成 flase 呢?
// FIXME:临时操作
// if (transform.position.y == destination.y)
// if (destination.y - transform.position.y < 0.1f)
// {
// isJump = false;
// }
if (canJump)
{
TriggerJump();
}
}
// 我们前面说了,如果你想使用物理的话,我们需要在 FixedUpdate 里面执行
private void FixedUpdate()
// FixedUpdate 是固定每 0.02s 执行一次,它不会依照你系统的快慢来执行——所以它是一个非常稳定的物理系统
{
// 在这里吗我们要实现什么呢?——实现真实的移动
// rb.position = Vector2.Lerp(起始坐标, 最终的坐标); // Linearly interpolates between vectors a and b by t.// 通过 t 在向量 a 和 b 之间进行线性插值。
// rb.position = Vector2.Lerp(transform.position, 那么最终坐标是?); // 目前不知道最终坐标位置,先注释掉
if (isJump) // 如果正在跳跃,则进行计算
{
rb.position = Vector2.Lerp(transform.position, destination, 0.134f); // 目前不知道最终坐标位置,先注释掉
// 不过我们用 moveDistance,我们用现在的坐标+moveDistance不就是移动的目标坐标
}
}
#region INPUT 输入回调函数
public void Jump(InputAction.CallbackContext context)
{
// 创建一个默认的函数写法
// public 公开的,其它类都可以调用
// void 没有返回类型
// if (context.phase == InputActionPhase.Performed)
// 下面是简写
// if (context.performed && isJump == false)
if (context.performed && !isJump) // 要执行跳跃,那前提是当前的青蛙没有跳跃
{ // 这样只有在功能完全的输出,我们才有里面的内容
// Debug.Log("Jump! Hello..." + context);
// 也改成具体跳跃的距离,方便后期调试
moveDistance = jumpDistance;
// Debug.Log("JUMP!" + " " + moveDistance); // 可以先注释掉了,不然控制台太乱
destination = new Vector2(transform.position.x, transform.position.y + moveDistance);
// isJump = true;
canJump = true;
}
}
public void LongJump(InputAction.CallbackContext context)
{
if (context.performed && !isJump) // 要执行跳跃,那前提是当前的青蛙没有跳跃
{
moveDistance = jumpDistance * 2; // 小跳执行的话,那就是 jumpDistance
// Debug.Log("LONG JUMP!" + " " + moveDistance);
buttonHeld = true; // 一旦被长按了,我们的 buttonHeld 就为 true
}
// canceled 取消了
if (context.canceled && buttonHeld && !isJump) // 既要是被按下松开 context.canceled && 也要是 true buttonHeld
{
// 松掉空格「按键」
// TODO: 执行跳跃,而我们说了,要在松掉键盘,执行。那么把上面的 29 行代码,移动下来:
// Debug.Log("LONG JUMP!" + " " + moveDistance); // 可以先注释掉了,不然控制台太乱
buttonHeld = false; // 把状态改回来
destination = new Vector2(transform.position.x, transform.position.y + moveDistance);
// isJump = true;
canJump = true;
}
}
public void GetTouchPosition(InputAction.CallbackContext context)
{
}
#endregion
/// <summary>
/// 触发执行跳跃动画
/// </summary>
private void TriggerJump()
{
//TODO:获得移动方向、播放动画
anim.SetTrigger("Jump"); // 和在 unity 里面设置的参数名称一致才可以,大小写要一致
// anim.SetBool();
// anim.SetFloat();
// anim.SetInteger();
}
#region Animation Event
public void JumpAnimationEvent() // 跳跃的动画事件
{
// 改变状态
isJump = true;
}
// 还需要另外一个状态,也就是跳跃结束的时候,状态改为 false
public void FinishJumpAnimationEvent()
{
isJump = false;
}
#endregion
}
private bool canJump;
: 状态检测
if (canJump)
{
TriggerJump();
}
触发跳跃事件;
/// <summary>
/// 触发执行跳跃动画
/// </summary>
作用如下:
/// <summary>
/// 触发执行跳跃动画
/// </summary>
private void TriggerJump()
{
//TODO:获得移动方向、播放动画
anim.SetTrigger("Jump"); // 和在 unity 里面设置的参数名称一致才可以,大小写要一致
// anim.SetBool();
// anim.SetFloat();
// anim.SetInteger();
}
下面代码,游戏状态的改变:
#region Animation Event
public void JumpAnimationEvent() // 跳跃的动画事件
{
// 改变状态
isJump = true;
}
// 还需要另外一个状态,也就是跳跃结束的时候,状态改为 false
public void FinishJumpAnimationEvent()
{
isJump = false;
}
#endregion
2.1 添加第一个关键帧
选中我们的 Frog:
选择 Frog_jump ,我们有两种方法,在最开始的地方,其实就是第一帧。也就是时间线上面右键:
第二种方法:
- 选择第一帧
可以看见,出现了一个蓝色的线。
注意
不要多次点击,保证只有一个。
2.2 选中关键帧添加函数
2.3 添加第二个关键帧
在倒数第二帧,其实也就是青蛙马上落地了,添加第二个关键帧:
2.4 梳理逻辑
- 如果我们点了一下空格或者触摸了一下我们的屏幕;
- 我们的第一个状态是满足的:
if (context.performed && !isJump)
,因为 isJump 默认值是 false; - 满足 2 的状态,我们就可以跳跃:
canJump = true;
; - 所以,在 Update 函数里面,实时每一帧检测,一旦 canJump 为 true,就会触发我们的
TriggerJump()
; - 这个 TriggerJump 是做了什么呢?——设置了我们的 Animator ,播放跳跃的动画;
- 所以在 Animator 当中,我们就从 Frog_idle 的状态,切换到了 Trigger 的状态。
- 到了第一帧的时候,播放
JumpAnimationEvent()
,一旦我们的isJump = true;
,我们的FixedUpdate()
每 0.02s 都在监测这个状态。一旦我们的状态为 true,我们就执行移动的方法rb.position = Vector2.Lerp(transform.position, destination, 0.134f);
在落地的时候,这个状态改变了,这行rb.position = Vector2.Lerp(transform.position, destination, 0.134f);
也就不再执行。
3. 运行
我们会在运行的时候会发现我们的青蛙会一直跳跃,没有停止:
我们来排查一下 bug:
我们可以发现,它一共做了三次引用。其中两次都被赋值为 true,导致它无法停止。也就是动画一直被执行。它没有一个情况下,变成 false 的。
——所以,canJump 什么时候变成 false 呢?
——一旦你跳起来了,你的 canJump 就应该变成 false 也就是不能再跳跃了。
所以,我们在哪里修改最好呢?
也就是 TriggerJump 的时候,canJump 就改成 false;保证 canJump 只执行一次。
private void TriggerJump()
{
canJump = false;
//TODO:获得移动方向、播放动画
anim.SetTrigger("Jump"); // 和在 unity 里面设置的参数名称一致才可以,大小写要一致
// anim.SetBool();
// anim.SetFloat();
// anim.SetInteger();
}
我们都在保证一秒钟执行完成。不过我们目前还没有考虑多方向。
tips
给各位同学的一些提醒哈,如果你的青蛙的动画有轻微抖动情况,那么可以将动画帧图片的 pivot 统一即可。
欢迎关注我公众号:AI悦创,有更多更好玩的等你发现!
公众号:AI悦创【二维码】
AI悦创·编程一对一
AI悦创·推出辅导班啦,包括「Python 语言辅导班、C++ 辅导班、java 辅导班、算法/数据结构辅导班、少儿编程、pygame 游戏开发、Linux、Web全栈」,全部都是一对一教学:一对一辅导 + 一对一答疑 + 布置作业 + 项目实践等。当然,还有线下线上摄影课程、Photoshop、Premiere 一对一教学、QQ、微信在线,随时响应!微信:Jiabcdefh
C++ 信息奥赛题解,长期更新!长期招收一对一中小学信息奥赛集训,莆田、厦门地区有机会线下上门,其他地区线上。微信:Jiabcdefh
方法一:QQ
方法二:微信:Jiabcdefh
- 0
- 0
- 0
- 0
- 0
- 0