在之前的几篇​​Blog​​总,我们已经系统学习了自动寻路插件Navmesh的相关概念和细节。然而,如果要做一个场景精美的手游,需要用到各种复杂的场景地形,而不仅仅是平地上的自动寻路。今天我们将通过一个完整的复杂的实例,来贯穿各个细节。我们将实现一个复杂的场景,角色可以在里面攀爬,跳跃,爬坡。是不是感觉很像当年的CS游戏呢?本案例将会用得一些基本的动画函数,大家可以先结合文档有个大概的了解。本实例是在官方的范例上加工而成。

 

  • 步骤

1.在场景中摆放各种模型,包括地板,斜坡,山体,扶梯等

2.为所有的模型加上Navigation Static和OffMeshLink Generatic(这个根据需要,例如地板与斜坡相连,斜坡就不需要添加OffMeshLink)

3.特殊处理扶梯,需要手动添加Off Mesh Link,设置好开始点和结束点

4.保存场景,烘焙场景

5.添加角色模型,为其加Nav Mesh Agent组件

6.为角色添加一个新脚本,AgentLocomotion.cs,用来处理自动寻路,已经角色动画变换。代码比较长,大家可以结合注释来理解


using UnityEngine; using System.Collections;  public class AgentLocomotion : MonoBehaviour {     private Vector3 target;//目标位置     private NavMeshAgent agent;     private Animation anim;//动画     private string locoState = "Locomotion_Stand";     private Vector3 linkStart;//OffMeshLink的开始点     private Vector3 linkEnd;//OffMeshLink的结束点     private Quaternion linkRotate;//OffMeshLink的旋转     private bool begin;//是否开始寻路      // Use this for initialization     void Start()     {         agent = GetComponent<NavMeshAgent>();         //自动移动并关闭OffMeshLinks,即在两个隔离障碍物直接生成的OffMeshLink,agent不会自动越过         agent.autoTraverseOffMeshLink = false;         //创建动画         AnimationSetup();         //起一个协程,处理动画状态机         StartCoroutine(AnimationStateMachine());     }      void Update()     {         //鼠标左键点击         if (Input.GetMouseButtonDown(0))         {             //摄像机到点击位置的的射线             Ray ray = Camera.main.ScreenPointToRay(Input.mousePosition);             RaycastHit hit;             if (Physics.Raycast(ray, out hit))             {                 //判断点击的是否地形                 if (hit.collider.tag.Equals("Obstacle"))                 {                     begin = true;                     //点击位置坐标                     target = hit.point;                 }             }         }         //每一帧,设置目标点         if (begin)         {             agent.SetDestination(target);         }     }      IEnumerator AnimationStateMachine()     {         //根据locoState不同的状态来处理,调用相关的函数         while (Application.isPlaying)         {             yield return StartCoroutine(locoState);         }     }      //站立     IEnumerator Locomotion_Stand()     {         do         {             UpdateAnimationBlend();             yield return new WaitForSeconds(0);         } while (agent.remainingDistance == 0);         //未到达目标点,转到下一个状态Locomotion_Move         locoState = "Locomotion_Move";         yield return null;     }      IEnumerator Locomotion_Move()     {         do         {             UpdateAnimationBlend();             yield return new WaitForSeconds(0);             //角色处于OffMeshLink,根据不同的地点,选择不同动画             if (agent.isOnOffMeshLink)             {                 locoState = SelectLinkAnimation();                 return (true);             }         } while (agent.remainingDistance != 0);         //已经到达目标点,状态转为Stand         locoState = "Locomotion_Stand";         yield return null;     }      IEnumerator Locomotion_Jump()     {         //播放跳跃动画         string linkAnim = "RunJump";         Vector3 posStart = transform.position;          agent.Stop(true);         anim.CrossFade(linkAnim, 0.1f, PlayMode.StopAll);         transform.rotation = linkRotate;          do         {             //计算新的位置             float tlerp = anim[linkAnim].normalizedTime;             Vector3 newPos = Vector3.Lerp(posStart, linkEnd, tlerp);             newPos.y += 0.4f * Mathf.Sin(3.14159f * tlerp);             transform.position = newPos;              yield return new WaitForSeconds(0);         } while (anim[linkAnim].normalizedTime < 1);         //动画恢复到Idle         anim.Play("Idle");         agent.CompleteOffMeshLink();         agent.Resume();         //下一个状态为Stand         transform.position = linkEnd;         locoState = "Locomotion_Stand";         yield return null;     }     //梯子     IEnumerator Locomotion_Ladder()     {         //梯子的中心位置         Vector3 linkCenter = (linkStart + linkEnd) * 0.5f;         string linkAnim;         //判断是在梯子上还是梯子下         if (transform.position.y > linkCenter.y)             linkAnim = "Ladder Down";         else             linkAnim = "Ladder Up";          agent.Stop(true);          Quaternion startRot = transform.rotation;         Vector3 startPos = transform.position;         float blendTime = 0.2f;         float tblend = 0f;          //角色的位置插值变化(0.2内变化)         do         {             transform.position = Vector3.Lerp(startPos, linkStart, tblend / blendTime);             transform.rotation = Quaternion.Lerp(startRot, linkRotate, tblend / blendTime);              yield return new WaitForSeconds(0);             tblend += Time.deltaTime;         } while (tblend < blendTime);         //设置位置         transform.position = linkStart;         //播放动画         anim.CrossFade(linkAnim, 0.1f, PlayMode.StopAll);         agent.ActivateCurrentOffMeshLink(false);         //等待动画结束         do         {             yield return new WaitForSeconds(0);         } while (anim[linkAnim].normalizedTime < 1);         agent.ActivateCurrentOffMeshLink(true);         //恢复Idle状态         anim.Play("Idle");         transform.position = linkEnd;         agent.CompleteOffMeshLink();         agent.Resume();         //下一个状态Stand         locoState = "Locomotion_Stand";         yield return null;     }      private string SelectLinkAnimation()     {         //获得当前的OffMeshLink数据         OffMeshLinkData link = agent.currentOffMeshLinkData;         //计算角色当前是在link的开始点还是结束点(因为OffMeshLink是双向的)         float distS = (transform.position - link.startPos).magnitude;         float distE = (transform.position - link.endPos).magnitude;          if (distS < distE)         {             linkStart = link.startPos;             linkEnd = link.endPos;         }         else         {             linkStart = link.endPos;             linkEnd = link.startPos;         }         //OffMeshLink的方向         Vector3 alignDir = linkEnd - linkStart;         //忽略y轴         alignDir.y = 0;         //计算旋转角度         linkRotate = Quaternion.LookRotation(alignDir);          //判断OffMeshLink是手动的(楼梯)还是自动生成的(跳跃)         if (link.linkType == OffMeshLinkType.LinkTypeManual)         {             return ("Locomotion_Ladder");         }         else         {             return ("Locomotion_Jump");         }     }      private void AnimationSetup()     {         anim = GetComponent<Animation>();          // 把walk和run动画放到同一层,然后同步他们的速度。         anim["Walk"].layer = 1;         anim["Run"].layer = 1;         anim.SyncLayer(1);          //设置“跳跃”,“爬楼梯”,“下楼梯”的动画模式和速度         anim["RunJump"].wrapMode = WrapMode.ClampForever;         anim["RunJump"].speed = 2;         anim["Ladder Up"].wrapMode = WrapMode.ClampForever;         anim["Ladder Up"].speed = 2;         anim["Ladder Down"].wrapMode = WrapMode.ClampForever;         anim["Ladder Down"].speed = 2;          //初始化动画状态为Idle         anim.CrossFade("Idle", 0.1f, PlayMode.StopAll);     }     //更新动画融合     private void UpdateAnimationBlend()     {         //行走速度         float walkAnimationSpeed = 1.5f;         //奔跑速度         float runAnimationSpeed = 4.0f;         //速度阀值(idle和walk的临界点)         float speedThreshold = 0.1f;          //速度,只考虑x和z         Vector3 velocityXZ = new Vector3(agent.velocity.x, 0.0f, agent.velocity.z);         //速度值         float speed = velocityXZ.magnitude;         //设置Run动画的速度         anim["Run"].speed = speed / runAnimationSpeed;         //设置Walk动画的速度         anim["Walk"].speed = speed / walkAnimationSpeed;          //根据agent的速度大小,确定animation的播放状态         if (speed > (walkAnimationSpeed + runAnimationSpeed) / 2)         {             anim.CrossFade("Run");         }         else if (speed > speedThreshold)         {             anim.CrossFade("Walk");         }         else         {             anim.CrossFade("Idle", 0.1f, PlayMode.StopAll);         }     } }



效果图如下,点击任何一个地点,角色都可以自动寻路过去。中间可能经过不同的障碍物,我们可以看到角色如我们所预料的一样,可以跳跃下来,可以爬楼梯,最终到达目标点。

Unity 自动寻路Navmesh之跳跃,攀爬,斜坡_寻路



  • 总结


今天的这个例子比较复杂,要根据寻路网格的类型,来处理角色的动作是普通寻路,还是攀爬,抑或跳跃。这个例子应该是比较接近真实项目了。大家在实际项目中如果还有更加复杂的寻路,欢迎探讨