1.玩家输入模块Player Input Module
按键输入原理
灵魂画师博老师:
Key (键盘输入)–> PI脚本(PlayerInput) --> signal(信号)
Joy (手柄输入) --> PI脚本(PlayerInput) --> signal(信号)
创建场景
在Hierarchy窗口创建Capsule(胶囊)和Plane(平面),并重置坐标值,Capsule的Position Y值抬高1
添加按键输入
在Capsule新建一个PalyerInput脚本
// Variable
public string keyUp = "w";
public string keyDown = "s";
public string keyLeft = "a";
public string keyRight = "d";
效果如下:
把输入的信号转换成电脑能读懂的signal
public float Dup;
public float Dright;
void Update ()
{
Dup = (Input.GetKey(keyUp)?1.0f:0) - (Input.GetKey(keyDown)?1.0f:0); //三目运算符
Dright = (Input.GetKey(keyRight)?1.0f:0) - (Input.GetKey(keyLeft)?1.0f:0);
}
2.输入衰减与使能旗飘 Damping and Enable Flag.
SmoothDamp平滑缓冲
代码如下:
// Variable
public string keyUp = "w";
public string keyDown = "s";
public string keyLeft = "a";
public string keyRight = "d";
public float Dup;
public float Dright;
private float targetDup; //目标值
private float targetDright;
private float velocityDup; //暂存值
private float velocityDright;
void Update ()
{
targetDup = (Input.GetKey(keyUp)?1.0f:0) - (Input.GetKey(keyDown)?1.0f:0); //三目运算符
targetDright = (Input.GetKey(keyRight)?1.0f:0) - (Input.GetKey(keyLeft)?1.0f:0);
Dup = Mathf.SmoothDamp(Dup,targetDup,ref velocityDup,0.1f);
Dright = Mathf.SmoothDamp(Dright, targetDright, ref velocityDright, 0.1f);
}
smoothdamp(平滑缓冲)东西不是僵硬的移动而是做减速缓冲运动到指定位置
Mathf.SmoothDamp(需要修改的参数, 目标值 ,ref 暂存值,平滑所需的时间);
添加关闭输入开关
public bool inputEnabled = true; //Flag
void Update ()
{
if (!inputEnabled)
{
targetDup = 0;
targetDright = 0;
}
}
3.导入模型Import Models
导入材质更改参数
-
把材质和模型导入Project文件夹中
-
添加Plane,并把地板贴图并拖入材质球中,Tiling属性设为9,9,地板大小也放大三倍(扩大地图)
-
capsule改名为PlayerHandle并放入人物模型,成为PlayerHandle子物件,remove(删除)掉Mesh Renderer(网格渲染器)和Mesh Filter(网格过滤器)
-
调整PlayerHandle和人物模型的位置及大小
更改材质密度大小
scene 中的gizmos设置(优化场景显示)
3d lcons 调整场景图标大小
selection Qutline 取消显示模型外边框
4.新增动画控制器Add Animator Controller
- 在Assets窗口中新建一个folder改名为Animator,并在Animator文件夹中新建一个Animator Controller名字就叫Actor
- 没有animator创建就在windows-animator窗口可以找到
- 放入idle和walk动画,并给人物模型的Animator的controller加入刚刚创建名为Actor的动画控制器
- 在Project窗口找到人物模型,在rig-Animation Type的属性改为Humanoid(关于这个的详细设置以后再更新)
- Hierarchy窗口的人物模型animator组件的Apply Root Motion(根运动)选项消勾
5.一维混合树1D Blend Tree
一个混合树可以包含多个动画
右键单击 Animator Controller 窗口上的空白区域。 从显示的上下文菜单中,选择 Create State > From New Blend Tree。 双击混合树 (Blend Tree) 以进入混合树视图 (Blend Tree Graph)。
把BlendTree改名为ground,双击Blend改名为forward,在inspector窗口加两个motion filed并放入动画
6.串联玩家控制与角色控制模块Connect PI and AC modules
在PlayerHandle添加一个名为ActorController的脚本
代码如下:
//ActorController
public GameObject modle; //挂载一个GameObject名为modle
public PlayerInput pi; //挂载一个pi叫PlayerInput
[SerializeField]//序列化的数据类型可以通过这个特性显示到编辑器上
private Animator anim; //声明一个animator组件,把ybot子物体拖进去
void Awake() //awake>enable>start
{
pi = GetComponent<PlayerInput>(); //得到pi的PlayerInput脚本
anim = model.GetConmponent<Animator>(); //得到modle的animator动画组件
}
void Update() //帧更新
{
anim.SetFloat("forward", pi.Dup); //取pi的Dup变量更改animator的forward的值
}
7.角色行走#1Actor walks#1
Dup和Dright值数范围是-1到1,而animator的forword数值变化值数范围是0到1
因此我们把Dup和Dright的值变为长度m,方向和由Dup和Dright的变化而变化
根据这幅图的原理我们把以前的代码更改一下:
//ActorController
void Update()
{
anim.SetFloat("forward", Mathf.Sqrt((pi.Dup * pi.Dright) + (pi.Dright * pi.Dright))); //图片的m值
model.transform.forward = pi.Dright * transform.right + pi.Dup * transform.forward; //旋转
}
然后我们把代码进行封装一下,更改PallyerInput脚本:
//PallyerInput
public float Dmag; //D向量模,用来更改动画forward的值
public Vector3 Dvec;
void Update()
{
Dmag = Mathf.Sqrt((Dup * Dright) + (Dright * Dright));
Dvec = Dright * transform.right + Dup * transform.forward;
}
然后我们把ActorController脚本改一下,漂漂亮亮的:
//ActorController
void Update()
{
anim.SetFloat("forward", pi.Dmag); //图片的m值
model.transform.forward = pi.Dvec //旋转
}
但是这个代码有个缺陷:停止按键会立刻朝着前方,这个我们下一课进行解决
8.角色行走#2Actor walks#2
我们是以Dup和Dright的大小作为方向的,但是松手后Dup和Dright的值数大小会组件变成0,forward向量的值不能为0导致模型方向重置,解决方法很简单在Dmag到0之前暂停这段代码执行即可:
//ActorController
void Update()
{
anim.SetFloat("forward", pi.Dmag); //图片的m值
if (pi.Dmag > 0.1f)
{
model.transform.forward = pi.Dvec //旋转
}
}
//ActorController
public float walkSpeed = 1.4f
private Rigidbody rb; //声明一个rigidbody组件
private Vector3 movingVec; //声明一个模型移动方向
void Awake()
{
rb = GetComponent<Rigidbody>();
}
void Update()
{
movingVec = pi.Dmag * model.transform.forward * walkSpeed; //每秒向model.transform.forward移动Dmag米
}
void FixedUpdate() //注意rigidbody一定要在fixedupdate里写,update是每帧执行一次,Fixedupdate是固定每0.02秒执行uici(Unity默认值)
{
//rb.position += movingVec * Time.fixedDeltaTime; //胶囊速度乘以时间
rb.velocity = new Vector3(movingVec.x, rb.velocity.y, movingVec.z);
}
完整代码如下:
//PallyerInput
public float Dmag; //D向量模,用来更改动画forward的值
public Vector3 Dvec;
void Update()
{
Dmag = Mathf.Sqrt((Dup * Dright) + (Dright * Dright));
Dvec = Dright * transform.right + Dup * transform.forward;
}
//ActorController
public float walkSpeed = 1.4f
public GameObject modle; //挂载一个GameObject名为modle
public PlayerInput pi; //挂载一个pi叫PlayerInput
[SerializeField]//序列化的数据类型可以通过这个特性显示到编辑器上
private Animator anim; //声明一个animator组件,把ybot子物体拖进去
private Rigidbody rb; //声明一个rigidbody组件
private Vector3 movingVec; //声明一个模型移动方向
void Awake() //awake>enable>start
{
pi = GetComponent<PlayerInput>(); //得到pi的PlayerInput脚本
anim = model.GetConmponent<Animator>(); //得到modle的animator动画组件
rb = GetComponent<Rigidbody>();
}
void Update() //帧更新
{
anim.SetFloat("forward", pi.Dmag); //图片的m值
if (pi.Dmag > 0.1f)
{
model.transform.forward = pi.Dvec //旋转
}
movingVec = pi.Dmag * model.transform.forward * walkSpeed; //每秒向model.transform.forward移动Dmag米
}
void FixedUpdate() //注意rigidbody一定要在fixedupdate里写,update是每帧执行一次,Fixedupdate是固定每0.02秒执行uici(Unity默认值)
{
rb.velocity = new Vector3(movingVec.x, rb.velocity.y, movingVec.z);
}
9.角色奔跑Actor run
在blend tree添加跑步动画
更改Threshold的值如果改不了就把Automate Thresholds选项消勾
因为需要实现跑步功能所以我们要添加新的按键,代码如下:
//PlayerInput
[Header("===== key settings =====")]//序列化,Inspector面板中显示文字
public string keyA;
public string keyB;
public string keyC;
public string keyD;
[Header("===== Output signals =====")]
// 1.pressing signal
public bool run;
void Update()
{
run = Input.Getkey(keyA);
}
//ActorConntroller
anim.SetFloat("forward", pi.Dmag * ((pi.run) ? 2.0f : 1.0f));
movingVec = pi.Dmag * model.transform.forward * walkSpeed *((pi.run) ? 2.7f : 1.0f); //三元运算符
10.线性插值与球形线性插值Lerp & Slerp
//ActorConntroller
//移动渐变
anim.SetFloat("forward", pi.Dmag * Mathf.Lerp(anim.GetFloat("forward"),(pi.run) ? 2.0f :1.0f),0.5f));
//转向渐变
Vector3 targetForward = Vector3.Slerp(model.transform.forward, pi.DVec, 0.5f);//渐变
model.transform.forward = targetForward;//利用slerp把current值转到目标值实现平滑的转身
11.椭圆映射法Elliptical Grid Mapping
论文地址:https://arxiv.org/ftp/arxiv/papers/1509/1509.06344.pdf
在游戏运行时我们会发现一个问题,玩家斜向移动速度会变快,为了修改这个我们用论文第五页公式。
代码如下:
//PlayerInput
private void SquareToCirclie(Vector2 input)
{
Vector2 output = Vector2.zero;
output.x = input.x * Mathf.Sqrt(1 - (input.y * input.y)/2.0f);
output.y = input.y * Mathf.Sqrt(1 - (input.x * input.x)/2.0f);
returm output;
}
Vector2 tempDAxis = SquareToCirclie (new Vector2(Dright,Dup));
float Dright2 = tempDAxis.x;
float Dup2 = tempDAxis.y;
12.一次性触发控制(Trigger Oonce Signal)
实现跳跃功能:
//PlayerInput
// 2. trigger once signal
public bool jump;
private bool lastJump;
void Update ()
{
bool newJump = Input.GetKey (keyB);
if (newJump != lastJump && newJump == true)//newJump == true,解释如下图
{
jump = true;
}
else
{
jump = false;
}
lastJump = newJump;
}
这种触发器按下会触发两次信号,所以我们要触发前面的信号:
13.新增跳跃动画Jump Animation
animator窗口添加Jump动画,详细操作我就不写了毕竟做了我们做很多次,所以直接上代码:
//ActorController
if(pi.jump)
{
anim.SetTrigger("jump");
}
14.巧妙地重置动画控制器触发Reset Triggers
我们会发现一个问题,跳跃键按两次会自动跳两次,所以我们在ground的Inspector窗口新建一个名为FSMClearSingnals的脚本:
新建脚本会出现以下预设:
- OnStateEnter:当一个转换开始并且状态机开始评估这个状态时
- OnStateUpdate:OnStateUpdate 在 OnStateEnter 和 OnStateExit 回调之间的每个更新帧上调用
- OnStateExit:当转换结束并且状态机完成评估此状态时
- OnStateMove:OnStateMove 在 Animator.OnAnimatorMove () 之后被调用。处理和影响根运动的代码应该在这里实现
- OnAnimatorIK:OnStateIK 在 Animator.OnAnimatorIK () 之后被调用。设置动画 IK(反向运动学)的代码应该在这里实现。
代码如下:
public string[] clearAtEnter;
public string[] clearAtExit;
override public void OnStateEnter(Animator animator, AnimatorStateInfo stateInfo, int layerIndex)
{
foreach (var signal in clearAtEnter)
{
animator.ResetTrigger(signal);//清除信号
}
override public void OnStateExit(Animator animator, AnimatorStateInfo stateInfo, int layerIndex)
{
foreach (var signal in clearAtExit)
{
animator.ResetTrigger(signal);
}
}
15.套出FSM On Enter&Exit方法FSM On Enter&Exit
在jump的Inspector窗口新建一个名为FSMOnEnter和FSMOnExit的脚本:
public string[] onEnterMessages;
override public void OnStateEnter(Animator animator, AnimatorStateInfo stateInfo, int layerIndex)
{
foreach (var msg in onEnterMessages)
{
//animator.gameObject.SendMessage(msg);
animator.gameObject.SendMessageUpwards(msg);//向上发信息
}
}
public string[] onExitMessages;
override public void OnStateExit(Animator animator, AnimatorStateInfo stateInfo, int layerIndex)
{
foreach (var msg in onEnterMessages)
{
//animator.gameObject.SendMessage(msg);
animator.gameObject.SendMessageUpwards(msg);//向上发信息
}
}
然后在ActorController脚本进行拦截:
//ActorController
public void OnJumpEnter()
{
print ("On jump enter");
}
public void OnJumpExit()
{
print ("On jump exit");
}
16.锁死平面移动Lock Planar
解决跳跃状态移动的bug
直接在上个脚本改:
//ActorController
private bool lockPlanar = false;//锁死平面移动
void Update()
{
if (lockPlanar == false)
{
movingVec = pi.Dmag * model.transform.forward * walkSpeed;
}
}
public void OnJumpEnter()
{
pi.inputEnabled = false;
lockPlanar = true;
//print ("On jump enter");
}
public void OnJumpExit()
{
//print ("On jump exit");
pi.inputEnablie = true;
lockPlanar = false;
}
17.跳跃向量Jump Thrust
//ActorController
public float jumpVelocity = 3.0f;//为了写活跳跃值
private Vector3 thrustVec; //新增跳跃冲量向量
void FixedUpdate()
{
rb.velocity = new Vector3(movingVec.x, rb.velocity.y, movingVec.z) + thrustVec;
thrustVec = Vector3.zero; //一帧触发然后变成0
}
public void OnJumpEnter()
{
pi.inputEnabled = false;
lockPlanar = true;
thrustVec = new Vector3 (0, jumpVelocity, 0);
//print ("On jump enter");
}
18.新增降落状态Add Fall State
在动画控制器里添加下落动画名字为fall,并调整好状态机,过程很简单就不详写了
19.新增落地侦测器Add On Ground Sensor
在这里傅老师用的是Physics.OverlapCapsule这个方法用来检测地面撞击,这个函数判定胶囊中有多少碰撞体:
Collider[] OverlapCapsule(Vector3 point0, Vector3 point1, float radius, int layerMask);
在PlayerHandle新建一个空对象,名为sensor。在sensor新建名为OnGroundSensor的脚本,代码如下:
//OnGroundSensor
public CapsuleCollider capcol; //把PlayerHandle拽进来
private Vector3 point1;
private Vector3 ponit2;
private float radius; //定义半径值
void Awake ()
{
radius = capcol.radius; //把PlayerHandle抓取半径值
}
void FixedUpdate ()
{
point1 = transform.position + transform.up * radius;
point2 = transform.position + transform.up * capcol.height - transform.up * radius;
Collider[] outputCols = Physics.OverlapCapsule(point1, ponit2, radius, LayerMask.GetMask("Ground"));//给地板加个Ground Layer
if (outputCols.Length != 0)
{
foreach (var col in outputCols)
{
print ("collision:" + col.name)
}
}
}
20.使用落地侦测器Use On Ground Sensor
我们上节课已经写好了落地检测,现在我们来使用它们:
//OnGroundSensor
if (outputCols.Length != 0)
{
//foreach (var col in outputCols)
//{
// print ("collision:" + col.name)
//}
SendMessageUpwards("IsGround");//向上传递信号
}
else
{
SendMessageUpwards("IsNotGround");
}
我们在ActorController脚本进行拦截:
//ActorController
public void IsGround()
{
anim.SetBoll ("isGround", true);
}
public void IsNotGround()
{
anim.SetBoll ("isGround", false);
}
添加一个布尔类型的isGround信号并在fall->ground添加这个信号:
调整动画优先级防止出现落地时优先播放fall动画,如果还是出现落地播放fall动画可以降低跳跃的高度;或者打开jump->fall动画的setting,启用interruption source下的current stage
21.重新修正降落状态Rearrange Fall State
这样更好的解决了下落的问题:
public void OnGroundEnter()//OnJumpExit改成OnGroundEnter
{
pi.inputEnablid = true;
lockPlanar = false;
}
22.失足掉落 Fall form Ground State
动画控制器ground->fall添加一个isGround=false触发条件:
为了防止移动下落没有抛物线的这种情况,我们加一个OnFallEnter:
public void OnFallEnter()
{
pi.inputEnablid = false;
lockPlanar = true;
}
出现跳跃发生Fall动画原因:跳跃值太高会产生下落效果,但是下落动画播放之后才会返回所以有下落的僵持效果,我们把Interruption Spirce改为Curremt state打断它即可
23.新增翻滚状态Add Roll State
在动画控制器添加下落root的动画,然后加个roll的trigger信号来检测控制下落,动画控制roll优先
//ActorController
if (rb.velocity.magnitude > 5.0f)
{
anim.SetTrigger("rool");
}
烘焙roll动画添加翻滚效果:
24.走路翻滚+跑步跳跃Walk Roll and Sprint Jump
走路翻滚:我们要fall从ground进入roll,所以我们添加动画条件
更改动画条件:
由于翻滚没有高度很是不美观,所以我们添加一个向上冲量:
//ActorController
public float rollVelocity = 3.0f;
public void OnRollEnter()
{
thrustVec = new Vector3 (0, rollVelocity, 0);
pi.inputEnable = false;
lockPlanar = true;
}
rool动画添加OnRollEnter来触发
25.新增后跳状态Add Jab State
forward速度在0-1.1之间为roll,1.1-2.1为jump,在0-0.1之间增加jab后跳动作
我们继续添加后跳Jab动画,添加条件,更改优先级,Has Exit Time 消勾,然后再拉回来驾轻就熟…
接下来我们给jab添加向后的冲量,所以我们给jab加之前写好的FSM On Enter脚本,挂上OnJabEnter脚本
添加代码如下:
//ActorController
public float jabVelocity = 3.0f;
public void OnJabEnter()
{
//thrustVec = new Vector3 (0, 0, -javaVelocity);//错误方法,一个是方向错误,瞬时冲量类似于瞬移
thrustVec = model.transform.forward * -jabVelocity;//添加一个负的向前向量的值
pi.inputEnabled = false;
lockPlanar = true;
}