Unity 程序化动画:还原塞尔达旷野之息 守护者 (六足)

又整了个小Demo,感觉程序化动画还挺好玩。先上效果图,使用到的所有模型均来源于网络。

72bdfddda30c4aa895c8ffc30f6a568b.gif

程序化动画生成守护者移动

实现思路:守护者共6条腿,初始化先激活两条腿14可移动,每移动完一条腿顺序激活下一条腿移动并取消激活移动完的腿(1移动完激活2并关闭1);守护者身上带有一个Controller包含了6指示器,分别往地上发射射线寻找对应腿的落点,找到落点让腿移动;Controller带着6各指示器去追林克,然后守护者的躯体去追Controller。

805d2601ecde477d8e2e099156ba0cd3.png

球球是Controller,带6个指示器,去追林克,身体去追Controller

6条腿需要用IK来实现,本文直接使用了Final IK,图省事,没有自己写IK脚本。给每条腿的根节点都挂上CCD IK,然后设置好子关节,本文每隔1个关节设置了一个子关节,不然计算起来太耗时,帧率太低。对应放进去的关节要设置一下Rotation Limit这里使用了Angle限制。

dd4c4d1f06cb4cb7a592f679cab7dd2f.png

腿部IK如何实现?首先需要给脚底寻找落点,这里特地没有把Foot设置为CCD IK的最后一个关节,而是把接近脚踝的位置设置为CCD IK的最后一个关节,这样设置好,可以我们自己手动设置脚的落点,保证始终落在垂直落点的方向上,而脚踝和脚底的距离大约0.25-0.35,这也是为什么后面脚本中会出现normal * 0.25f的原因,因为相当于找到踝关节落点,然后我们让ankle(子物体包含了脚底)始终LookAt这个ankle.position + info.normal,这就相当于我们自己实现了简化版的落脚点GrounderIK。

4337abf64a254811adb8cf408f7dfccf.png

接下来给每个脚底挂上IKFootSolver脚本。

c2b8bf00569046709b437987145876c9.png

代码写的稀烂,凑合看吧。思路:从指示器发射一条射线,如果落点落在了角度60度以内的环形范围内,并且当前leg允许移动且每次指示器找到的落点距离上一次落点大于设定的distance,就可以计算脚底应该落在那里,发送给ccdIK.solver.IKPosition。如果当脚底落在60度的环形范围之外,例如:落在了两个红色区域内圈外圈,分别对应两个红点重新寻找落点,同理绿色和蓝色范围;下图表示指示器落在了距离大于环形外圈的地方,则对应靠外的红点,以该红点位置向下发射射线,重新寻找落点。。。这样守护者的脚才不会伸的太直或太弯。

8854e3385b9548dea18a74b71ab41a20.png

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免费),在上面代码中已经写了,如何实现闪烁?

5629bfe79c104662936bed2c3ac347f0.gif

这里用到了Shader Graph自己写了一个简单的闪烁效果。实际上下图的shader graph是闪烁的,但是截图没有闪烁。仅供参考。

643bdf92de7c4ae19aeb30db5d730534.png

根据这个Shader Graph创建一个材质赋给激光就行了。

结束。

 

  • 3
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值