又整了个小Demo,感觉程序化动画还挺好玩。先上效果图,使用到的所有模型均来源于网络。
程序化动画生成守护者移动
实现思路:守护者共6条腿,初始化先激活两条腿14可移动,每移动完一条腿顺序激活下一条腿移动并取消激活移动完的腿(1移动完激活2并关闭1);守护者身上带有一个Controller包含了6指示器,分别往地上发射射线寻找对应腿的落点,找到落点让腿移动;Controller带着6各指示器去追林克,然后守护者的躯体去追Controller。
球球是Controller,带6个指示器,去追林克,身体去追Controller
6条腿需要用IK来实现,本文直接使用了Final IK,图省事,没有自己写IK脚本。给每条腿的根节点都挂上CCD IK,然后设置好子关节,本文每隔1个关节设置了一个子关节,不然计算起来太耗时,帧率太低。对应放进去的关节要设置一下Rotation Limit这里使用了Angle限制。
腿部IK如何实现?首先需要给脚底寻找落点,这里特地没有把Foot设置为CCD IK的最后一个关节,而是把接近脚踝的位置设置为CCD IK的最后一个关节,这样设置好,可以我们自己手动设置脚的落点,保证始终落在垂直落点的方向上,而脚踝和脚底的距离大约0.25-0.35,这也是为什么后面脚本中会出现normal * 0.25f的原因,因为相当于找到踝关节落点,然后我们让ankle(子物体包含了脚底)始终LookAt这个ankle.position + info.normal,这就相当于我们自己实现了简化版的落脚点GrounderIK。
接下来给每个脚底挂上IKFootSolver脚本。
代码写的稀烂,凑合看吧。思路:从指示器发射一条射线,如果落点落在了角度60度以内的环形范围内,并且当前leg允许移动且每次指示器找到的落点距离上一次落点大于设定的distance,就可以计算脚底应该落在那里,发送给ccdIK.solver.IKPosition。如果当脚底落在60度的环形范围之外,例如:落在了两个红色区域内圈外圈,分别对应两个红点重新寻找落点,同理绿色和蓝色范围;下图表示指示器落在了距离大于环形外圈的地方,则对应靠外的红点,以该红点位置向下发射射线,重新寻找落点。。。这样守护者的脚才不会伸的太直或太弯。
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using RootMotion.FinalIK;
using UnityEngine.Serialization;
public class IKFootSolver : MonoBehaviour
{
public Transform indicator;
public Transform guardian;
private Vector3 arcPos1outer;
private Vector3 arcPos1inner;
private Vector3 arcPos2outer;
private Vector3 arcPos2inner;
private Vector3 arcPos3outer;
private Vector3 arcPos3inner;
private int terrainLayer = 1 << 3;
private Vector3 currentPosition;
public Vector3 newPosition;
private Vector3 oldPosition;
public Vector3 newOutRangePosition;
[SerializeField] private float stepDistance = 1.5f;
public float stepHeight;
[SerializeField] private float speed = 7f;
public float lerp = 1.01f;
public CCDIK ccdIK;
RaycastHit info;
RaycastHit Inner6PointRaycastInfo;
public Transform ankle;
public bool stopMove;
public bool inRange;
public bool callbackMoveNextLeg = true;
public int newOutRangeArea;
public int oldOutRangeArea;
public bool moveLeg;
private void Awake()
{
oldPosition = transform.position;
}
private void Start()
{
ccdIK.solver.IKPosition = currentPosition;
}
// Update is called once per frame
void Update()
{
ankle.LookAt(ankle.position + info.normal);
//transform.position = currentPosition;
Ray ray = new Ray(indicator.position, Vector3.down);
if (Physics.Raycast(ray, out info, 10, terrainLayer) && lerp == 1.01f)
{
//计算本体和腿的方向向量
Vector3 guarPos = new Vector3(guardian.position.x, 0, guardian.position.z);
Vector3 legPos = new Vector3(ccdIK.gameObject.transform.position.x, 0, ccdIK.gameObject.transform.position.z);
Vector3 dir1 = legPos - guarPos;
//计算射线点和本体的方向向量
Vector3 infoPos = new Vector3(info.point.x, 0, info.point.z);
Vector3 dir2 = infoPos - guarPos;
arcPos1outer = guardian.position + dir1.normalized * 6.6f + Vector3.up * 10;
arcPos1inner = guardian.position + dir1.normalized * 5.3f + Vector3.up * 10;
arcPos2outer = arcPos1outer + Quaternion.AngleAxis(-110, Vector3.up) * dir1.normalized * 2.8f;
arcPos2inner = arcPos1inner + Quaternion.AngleAxis(-105, Vector3.up) * dir1.normalized * 2.2f;
arcPos3outer = arcPos1outer + Quaternion.AngleAxis(110, Vector3.up) * dir1.normalized * 2.8f;
arcPos3inner = arcPos1inner + Quaternion.AngleAxis(105, Vector3.up) * dir1.normalized * 2.2f;
inRange = Vector3.Angle(dir1.normalized, dir2.normalized) <= 30 && dir2.magnitude >= 9.5f / 2 && dir2.magnitude <= 14 / 2;
//如果是范围内
if (inRange)
{
if (Vector3.Distance(newPosition, info.point) > stepDistance && !stopMove)
{
//Debug.Log("inRange--");
lerp = 0;
newPosition = info.point + 0.25f * info.normal;
inRange = true;
callbackMoveNextLeg = true;
}
}
else
{
if (Vector3.Distance(newOutRangePosition, info.point) > stepDistance && !stopMove)
{
if (Vector3.Angle(dir1.normalized, dir2.normalized) <= 15)
{
if (dir2.magnitude >= 7)
{
newOutRangeArea = 1;
Ray ray1 = new Ray(arcPos1outer, Vector3.down);
if (Physics.Raycast(ray1, out Inner6PointRaycastInfo, 20, terrainLayer))
{
//Debug.LogError("Cond1outer--");
lerp = 0;
newPosition = Inner6PointRaycastInfo.point + 0.25f * Inner6PointRaycastInfo.normal;
newOutRangePosition = info.point;
callbackMoveNextLeg = true;
}
}
else
{
newOutRangeArea = 2;
Ray ray1 = new Ray(arcPos1inner, Vector3.down);
if (Physics.Raycast(ray1, out Inner6PointRaycastInfo, 20, terrainLayer))
{
//Debug.LogError("Cond1inner--");
lerp = 0;
newPosition = Inner6PointRaycastInfo.point + 0.25f * Inner6PointRaycastInfo.normal;
newOutRangePosition = info.point;
callbackMoveNextLeg = true;
}
}
}
else if (Vector3.Angle(dir1.normalized, dir2.normalized) * Mathf.Sign(Vector3.Cross(dir2.normalized, dir1.normalized).y) > 15)
{
if (dir2.magnitude >= 7)
{
newOutRangeArea = 3;
Ray ray2 = new Ray(arcPos2outer, Vector3.down);
if (Physics.Raycast(ray2, out Inner6PointRaycastInfo, 20, terrainLayer))
{
//Debug.LogError("Cond2outer--");
lerp = 0;
newPosition = Inner6PointRaycastInfo.point + 0.25f * Inner6PointRaycastInfo.normal;
newOutRangePosition = info.point;
callbackMoveNextLeg = true;
}
}
else
{
newOutRangeArea = 4;
Ray ray2 = new Ray(arcPos2inner, Vector3.down);
if (Physics.Raycast(ray2, out Inner6PointRaycastInfo, 20, terrainLayer))
{
//Debug.LogError("Cond2inner--");
lerp = 0;
newPosition = Inner6PointRaycastInfo.point + 0.25f * Inner6PointRaycastInfo.normal;
newOutRangePosition = info.point;
callbackMoveNextLeg = true;
}
}
}
else if (Vector3.Angle(dir1.normalized, dir2.normalized) * Mathf.Sign(Vector3.Cross(dir2.normalized, dir1.normalized).y) < -15)
{
if (dir2.magnitude >= 7)
{
newOutRangeArea = 5;
Ray ray3 = new Ray(arcPos3outer, Vector3.down);
if (Physics.Raycast(ray3, out Inner6PointRaycastInfo, 20, terrainLayer))
{
//Debug.LogError("Cond3outer--");
lerp = 0;
newPosition = Inner6PointRaycastInfo.point + 0.25f * Inner6PointRaycastInfo.normal;
newOutRangePosition = info.point;
callbackMoveNextLeg = true;
}
}
else
{
newOutRangeArea = 6;
Ray ray3 = new Ray(arcPos3inner, Vector3.down);
if (Physics.Raycast(ray3, out Inner6PointRaycastInfo, 20, terrainLayer))
{
//Debug.LogError("Cond3inner--");
lerp = 0;
newPosition = Inner6PointRaycastInfo.point + 0.25f * Inner6PointRaycastInfo.normal;
newOutRangePosition = info.point;
callbackMoveNextLeg = true;
}
}
}
moveLeg = false;
}
}
}
if (lerp < 1)
{
Vector3 footPosition = Vector3.Lerp(oldPosition, newPosition, lerp);
footPosition.y += Mathf.Sin(lerp * Mathf.PI) * stepHeight;
//ankle.LookAt(ankle.position + info.normal);
currentPosition = footPosition;
lerp += Time.deltaTime * speed;
if (!stopMove)
{
ccdIK.solver.IKPosition = currentPosition;
}
}
else
{
lerp = 1.01f;
oldPosition = newPosition;
oldOutRangeArea = newOutRangeArea;
if (callbackMoveNextLeg)
{
ccdIK.solver.IKPosition = newPosition;
//Debug.Log("OnMoveNextLeg:" + this.gameObject.name + ": " + callbackMoveNextLeg);
EventHandler.CallMoveNextLeg(callbackMoveNextLeg, this.gameObject.name);
callbackMoveNextLeg = false;
}
//inCondition234 = false;
}
}
}
接下来是控制腿部的移动顺序,很简单,初始化设置两条腿能动,其他不能动,用事件顺序激活其他腿。GetMatchedLeg方法是找到对应的腿,14配对,25配对,36配对,腿1执行完了就激活腿2动,以此类推。
public static class EventHandler
{
public static event Action<bool,string> MoveNextLeg;
public static void CallMoveNextLeg(bool onFinishMoveLeg, string legName)
{
MoveNextLeg?.Invoke(onFinishMoveLeg, legName);
}
}
using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class GuardianLegMoveOrderController : MonoBehaviour
{
public IKFootSolver[] iKFootSolvers;
private IKFootSolver lastIKFootSovler;
private void Start()
{
for (int i = 0; i < iKFootSolvers.Length; i++)
{
if (i % 3 == 0)
{
iKFootSolvers[i].stopMove = false;
}
else
{
iKFootSolvers[i].stopMove = true;
}
}
lastIKFootSovler = iKFootSolvers[0];
EventHandler.MoveNextLeg += OnMoveNextLeg;
}
private void OnDestroy()
{
EventHandler.MoveNextLeg -= OnMoveNextLeg;
}
private void OnMoveNextLeg(bool onFinishMoveLeg, string legName)
{
//Debug.Log("CallOnMoveNextLeg:" + legName + ": " + onFinishMoveLeg);
if(onFinishMoveLeg)
{
iKFootSolvers[Convert.ToInt32(legName.Replace("Foot", "")) - 1].stopMove = true;
iKFootSolvers[GetMatchedLeg(legName.Replace("Foot", "")) - 1].stopMove = false;
}
}
private int GetMatchedLeg(string legName)
{
int leg = legName switch
{
"1" => 2,
"2" => 3,
"3" => 1,
"4" => 5,
"5" => 6,
"6" => 4,
};
return leg;
}
}
腿弄好了,接下来移动守护者的身体。
public class GuardianMovement : MonoBehaviour
{
public Vector3 guardianBodyPosition;
private GuardianLegMoveOrderController controller;
public Transform guardianBody;
public Transform target;
public float speed = 5f;
// Start is called before the first frame update
void Start()
{
controller = gameObject.GetComponent<GuardianLegMoveOrderController>();
}
// Update is called once per frame
void Update()
{
var dist1 = Vector3.Magnitude(controller.gameObject.transform.position - target.position);
//var dist2 = Vector3.Distance(controller.gameObject.transform.position, guardianBody.position);
if (target != null && dist1 > 12)
{
MoveIndicator();
MoveBody();
}
}
private void OnTriggerEnter(Collider other)
{
Debug.Log(other.gameObject.name);
if (other.gameObject.CompareTag("Player"))
{
target = other.gameObject.transform;
}
}
private void MoveIndicator()
{
var dir1 = target.position - controller.gameObject.transform.position;//目标方向
controller.gameObject.transform.Translate(dir1.normalized * Time.deltaTime * speed, Space.World);//向目标方向移动,normalized归一实现匀速移动
}
private void MoveBody()
{
var dir2 = controller.gameObject.transform.position - guardianBody.position;//目标方向
guardianBody.transform.Translate(dir2.normalized * Time.deltaTime * speed * 0.85f, Space.World);//向目标方向移动,normalized归一实现匀速移动
}
}
移动完守护者,就可以让守护者转动头部瞄准林克了
public class HeadTurnToTarget : MonoBehaviour
{
public Transform target;
public GuardianMovement guardianMovement;
public GameObject head;
public GameObject eye;
public GameObject targetHead;
public GameObject laser;
public float lerp;
public float targetAngle;
public float turnAngle;
private void Start()
{
guardianMovement = FindObjectOfType<GuardianMovement>();
laser.SetActive(false);
}
// Update is called once per frame
void Update()
{
target = guardianMovement.target;
if (target != null)
{
targetHead = GameObject.Find("mixamorig:Head").gameObject;
TurnHeadToTarget();
}
}
public void TurnHeadToTarget()
{
var dir = target.position - head.transform.position;
targetAngle = Vector3.Angle(dir.normalized, head.transform.up) * Vector3.Cross(dir.normalized, head.transform.up).y;
head.transform.eulerAngles += new Vector3(0, 0, targetAngle * Time.deltaTime * 2);
if (targetAngle <=1)
{
laser.SetActive(true);
LockTarget();
}
else
{
laser.SetActive(false);
}
//head.transform.eulerAngles = Vector3.Lerp(head.transform.eulerAngles, new Vector3(0, 0, head.transform.eulerAngles.z + targetAngle), 0.05f);
}
public void LockTarget()
{
var distance = (targetHead.transform.position - eye.transform.position).magnitude;
laser.transform.position = eye.transform.position;
laser.transform.localScale = new Vector3(0.02f, 0.02f, distance * 0.5f);
laser.transform.LookAt(targetHead.transform.position);
}
}
这里面用到了一个laser,是红色的一道闪烁的激光(Cylinder物体),根据守护者到林克的距离调整激光的长度,然后把激光位置调整到守护者眼睛处,并指向林克,这里激光的pivot使用adjust pivot插件调整到圆珠底部了(该插件unity asset store免费),在上面代码中已经写了,如何实现闪烁?
这里用到了Shader Graph自己写了一个简单的闪烁效果。实际上下图的shader graph是闪烁的,但是截图没有闪烁。仅供参考。
根据这个Shader Graph创建一个材质赋给激光就行了。
结束。