跟随在游戏中也很常见,特别在一些RPG游戏里面,他实现起来也不太难,下面就用一个例子来说明这个问题。如下图所示,红球是我们的主角,而点击蓝球NPC1和绿球NPC2之后,蓝球和绿球就自动跟随红球了,玩家则演变成控制一个队列。然后在屏幕的左上角显示当前的队列,玩家点击相应的名字,则取消跟随了,这个球就恢复原来的状态了。
同时可以看到,我们的主角红球实现了《【Unity3D】自动寻路》(点击打开链接),并且在带队的时候,始终朝向自动寻路的重点。
这里例子当然完全可以将简陋的Plane换成3D场景,球体换成相应的主角FBX模型,并且UI弄得更好。
一、场景布置
1、3D部分和《【Unity3D】刚体自动寻路的抖动问题和运动边界、空气墙的制作》(点击打开链接)基本一样,在一个Plane上放好3个球体Sphere,并且给这3个不同的球体上不同的纯色材质。同时在4周整4个空气墙,防止球的掉落。注意给球体都上刚体rigidbody属性,并且取消勾选Is Kinematic,而主控的红球勾选Is Kinematic。然后给红球上Nav Mesh Agent组件,完成自动寻路的烘培。
对于红球的速度和加速度分别设置为20和10,让自动寻路更加平稳。
2、而2D的UI部分,在一个Panel里面,分列3个宽和高几乎相同的按钮即可,当然你也可以根据你游戏里面,一个队列,最大允许NPC数量来设置按钮,比如这里一个队列最多就3个球,随意我就摆3个按钮。
二、脚本编写
在主角红球Hero上面摆定如下的Hero.cs:
using UnityEngine;
using UnityEngine.UI;//用到了UI
using System.Collections;
using System.Collections.Generic;//用到了容器类List
public class Hero : MonoBehaviour
{
public GameObject Plane;//地板
public GameObject NPC1;//蓝球
public GameObject NPC2;//绿球
public GameObject Panel;//左上角,显示当前队列里面有多少跟随着的面板
private NavMeshAgent navMeshAgent;//自动寻路的组件
private List<GameObject> FollowQueue;//用于存放跟随者的List
private List<GameObject> FollowCanelButtonQueue;//存放左上角取消跟随按钮的List
void Start()
{
navMeshAgent = gameObject.GetComponent<NavMeshAgent>();//初始化navMeshAgent
FollowQueue = new List<GameObject>();
FollowCanelButtonQueue = new List<GameObject>();
FollowQueue.Add(gameObject);//队列里面必有可控主角红球
for (int i = 0; i < 3; i++)
{//获取左上角取消跟随按钮,放进List
GameObject button = GameObject.Find("Button" + i);
FollowCanelButtonQueue.Add(button);
}
}
void Update()
{
if (Input.GetMouseButtonDown(0))//鼠标左键点下
{
Ray mRay = Camera.main.ScreenPointToRay(Input.mousePosition);//住摄像机向鼠标位置发射射线
RaycastHit mHit;
if (Physics.Raycast(mRay, out mHit))//射线检验
{
if (mHit.collider.gameObject == Plane && !navMeshAgent.hasPath)//如果当前的红球已经完成自动寻路停下来了,可以让其进行下一次寻路了。
{
navMeshAgent.SetDestination(mHit.point);//mHit.point就是射线和plane的相交点,实为碰撞点
gameObject.transform.LookAt(navMeshAgent.destination);//让红球面向寻路的终点
/*控制 跟随队列FollowQueue中 球的移动*/
for (int i = 1; i < FollowQueue.Count; i++)
{
GameObject follower = FollowQueue[i];
GameObject follower_leader = FollowQueue[i - 1];
/*让追随者follower始终面向寻路的终点*/
follower.transform.position = follower_leader.transform.position - (navMeshAgent.destination - gameObject.transform.position).normalized;
}
}
/*根据点击对象,开始相应的跟随处理*/
if (mHit.collider.gameObject == NPC1 && !FollowQueue.Contains(NPC1))
{
start_follow(NPC1);
}
if (mHit.collider.gameObject == NPC2 && !FollowQueue.Contains(NPC2))
{
start_follow(NPC2);
}
}
}
/*左上角的跟随队列信息UI*/
if (FollowQueue.Count > 1)//如果队列中的对象少于1就没必要显示了
{
Panel.SetActive(true);
for (int i = 0; i < 3; i++)//将队列中的按钮中的Text换成队列中对象的名字
{
GameObject button = FollowCanelButtonQueue[i];
if (i < FollowQueue.Count)
{
button.SetActive(true);
button.transform.Find("Text").GetComponent<Text>().text = FollowQueue[i].name;
}
else//之后的button就不要显示了
{
button.SetActive(false);
}
}
}
else
{
Panel.SetActive(false);
}
}
/*开始跟随的处理*/
private void start_follow(GameObject next_follower)
{
/*将跟随者压入跟随队列*/
FollowQueue.Add(next_follower);
GameObject follower = FollowQueue[FollowQueue.Count - 1];
GameObject follower_leader = FollowQueue[FollowQueue.Count - 2];
follower.rigidbody.isKinematic = true;//跟随者开始不受物理引擎影响,这句主要是为了避免自动寻路的鬼畜问题
follower.transform.position = follower_leader.transform.position + follower.transform.position.normalized;//让追随者靠近被被追者
follower.transform.parent = follower_leader.transform;//让追随者follower和被追随者follower_leader一起位移
}
/*取消追随的处理*/
public void canel_follow(int i)
{
FollowQueue[i].rigidbody.isKinematic = false;//跟随者重新受物理引擎影响
FollowQueue[i].transform.parent = null;//解除和被追随者follower_leader一起位移
FollowQueue.Remove(FollowQueue[i]);//在跟随队列中解除
}
}
并且在除第0个button外,设置点击事件,事件都是Hero.cs中的public void canel_follow(int i),传入参数分别是1~n(这里是n是2),用以与跟随序列FollowQueue中的对象对应起来,则大功告成。
上述脚本中有几个难点的。
(1)首先是用户点击某一个球的时候,要拉近可控主角,应该说是此时追随序列中最后一个球和用户点击的球的距离。在private void start_follow(GameObject next_follower)中的follower.transform.position = follower_leader.transform.position + follower.transform.position.normalized;。
normalized就是矢量归一化,也就是将矢量各分量除以向量的模长,用于即按比例缩短到单位长度,保证方向不变。
也就是方向向量(1,1),因为模长是根号2,所以归一化之后为(根号2/2,根号2/2)
这句的意思是这样的,如下图所示,刚好用于拉近两个东西的距离。
(2)在移动过程中的让追随者follower始终面向寻路的终点Update()函数中的follower.transform.position = follower_leader.transform.position - (navMeshAgent.destination - gameObject.transform.position).normalized;。意思是这样的:
(3)至于取消队列的管理倒是比较简单了。