角色控制-点击移动以及自动寻路
角色控制-点击移动以及自动寻路
角色控制-点击移动以及自动寻路(一)
之前在D3D中做的人物控制里,在空间中两点之间行走是一个比较复杂的制作过程。跟大家分享一下我的制作过程,可以一起研究改进改进。
这里面需要解决几个问题:
1.如何取得目标地点
需要通过鼠标当前位置的屏幕二维坐标,逆矩阵变换到世界空间下,做从摄像机到这个点的射线。射线与地面mesh进行碰撞检测,获得一个交点,就是目标点。 2.如何移动角色
实时得出角色的移动方向向量,在两点之间做线性插值计算,移动的同时也要播放动画。 3.两点之间有障碍怎么办
这个过程是一个比较复杂的过程,可以通过两种办法,一种是做一种转向机制,先移动监测点(我自己起的,意思是一个前进向量和一个向下的碰撞向量的假象交点),监测到碰撞了,就将角色转向,再次监测,直到碰撞不到,向当前挪一步,再重复刚才的操作,直到绕过这个障碍物;另一种是俗称的A*算法,需要预先准备好一个二维图来标记哪里可以走,哪里不可以走,并设置权值,以便于计算最短路径。
4.Unity3D里的碰撞需要注意什么
射线的碰撞检测是比较耗费CPU资源的,推荐的是尽量少用,比如当做场景内物体的鼠标拾取时,对备选对象队列进行筛选后,再去逐一做检测。unity3d中也是一样,只不过没有
API,无法用自定义的结构去决定对哪些对象进行射线碰撞。所以提供像DX中提供的那种
要引入Layer的概念,具体的操作下节说。角色控制-点击移动以及自动寻路(二) 针对上一节中第四点--筛选待测物体来说一下U3D中Layer的用法。
U3D中的Layer可以简单地理解为一个集合,所有的GameObject都可以设置Layer,且只能设置一个Layer。
设置方法:
1.Edit->roject Setting->Tags(tag和layer在同一个设置面板上)
2.前几个Builtin Layer是系统默认的,不能修改,可以在后面几个Layer中,设置Layer的名称,比如"layers"
3.在Hierarchy面板中选择GameObject,在Inspector面板上面的Layer下拉框中选择刚才设置好的Layer
从此,该GameObject就属于这个Layer集合中的一员了。
Layer的应用:LayerMask
LayerMask可以在射线碰撞过程中屏蔽掉一些GameObject。
LayerMask的方法:
LayerMask.NameToLayer("layers") 返回该Layer的编号
LayerMask.LayerToName(8) 返回该Layer的名称
var layerMaskPlayersayerMask = 1 << LayerMask.NameToLayer("Players"); var layerMaskTerrainsayerMask = 1 << LayerMask.NameToLayer("Terrains"); var FinalMaskayerMask = (layerMaskPlayers.value | layerMaskTerrains.value); var hitt : RaycastHit;
var ray : Ray = mainCamera.ScreenPointToRay(Input.mousePosition); if( Physics.Raycast(ray, hitt, 600, FinalMask.value))
{ ... }
上面代码描述一个鼠标拾取规则,射线仅与Layer属于Players和Terrains的GameObject做碰撞。
LayerMask的使用是按位操作的,在设置新的Layer时可以看到一共只有32个,应该是因为记录Layer的变量是四个字节。LayerMask.NameToLayer方法返回的LayerId是所要移位的位数,比如在设置Layer时,Players这个Layer的ID是8,则
var layerMaskPlayers:LayerMask = 1 << LayerMask.NameToLayer("Players"); 最后将所有的可能按位或操作,就得出所有需要做射线碰撞的GameObject。 如果想取“除了XXXLayer”的都显示,则可以用取反操作。
layerMaskPlayers = ~layerMaskPlayers.value;
比如layerMaskPlayers = 1<<8;
则layerMaskPlayers = 100000000;
在双字长的情况下,~layerMaskPlayers.value = -257
二进制
示:11111111111111111111111011111111角色控制-点击移动以及自动寻路(三) 为鼠标点击的位置增加一个光圈。需求如下:
鼠标右键点击地面,射线与地面的交点处出现一个光圈(纹理动画)。
该纹理动画会旋转,跟游戏中一样。
玩家移动到该光圈处,或者进入了其他状态,光圈消失。
玩家点击新的目标点,原目标点光圈消失。
光圈要随着地形的起伏改变自己的朝向,这样自然。
制作过程:
首先create一个Plane,去掉这个Plane的Collider Component,否则玩家会和光圈点发生碰撞。并为这个Plane指定一个纹理贴图,shader方式选择Particles/Alpha Blended Premultiply 颜色相乘,显得光圈亮一些。注意光圈图片必须要有通道,才能进行透Alpha明处理。 (注:另一种可选的shader方式是FX/Flare,与前面的那种不同,这个绘制出来的纹理将会一直显示在前端,相当于关闭了屏幕的深度缓冲区后再绘制。效果就是:即使在障碍物后面,也能看到它,比较不太符合现实逻辑,但是考虑到地形的起伏,光圈平面有可能会嵌入到地形里面,所以有时这个又比较合适,具体是不是有更好的办法处理这个问题,稍后再细细追究,或者大家可以提出更好的意见供参考。主要是U3D中不好确定渲染过程,不能动态操纵何时关闭屏幕深度缓冲区,其实怎么关闭屏幕的深度缓冲区也不清楚。。。)
然后在全局空GameObject(空的对象,不做任何逻辑处理,只为了挂接全局的脚本--不依附于任何Object的脚本)中增加一个鼠标相关控制脚本。加上前面所说过的改变鼠标指针UI操作,整个脚本如下:
这个脚本要完成的工作是:
通过传进来的prefab(transform类型),将光圈点放置在指定位置,并且根据射线与地形交点的面片的法线,调整这个光圈的朝向。还有一个在创建时要注意的是,将光圈的坐标沿着Y轴正向提高了一个数值,是为了避免纹理动画与地面纹理重合造成撕裂现象。 其中定义的两个方法CreateDestinationTex和DestroyDestinationTex将会在人物控制脚本中调用。
注:虽然Instantiate方法在手册中显示返回的类型是Object,但实际上是返回跟prefab同类型的一个对象,比如prefab是个transform类型,那么Instantiate返回的就是一个transform类型的对象,在Destroy一个prefeb对象时,Destroy(...) 括号里只能写Object或GameObject,
如果是Transform,则会因为有其他Component依附于这个transform,而无法删除掉。
最后修改角色控制脚本,在其中获得鼠标控制脚本的对象:
private var _ScriptObj_cursorControl : Script_Cursor; _ScriptObj_cursorControl = GameObject.Find("GameObject_GlobalController").GetComponent(Script_Cursor); //获得鼠标图标脚本对象
然后在鼠标右键点击地面后,进入STATE_LERPWALK状态,同时调用鼠标控制脚本中的函数CreateDestinationTex在指定给位置创建一个光圈对象,而当不在STATE_LERPWALK状态下时,调用鼠标控制脚本中的函数DestroyDestinationTex删除该对象。 if (Input.GetMouseButtonUp (1) && _grounded)
{
......
//调用鼠标处理脚本
if(_ScriptObj_cursorControl != null)
//创建目标点光圈
_ScriptObj_cursorControl.CreateDestinationTex(_endPos,TextureOnFloor,_hit.normal);
......
}
if(_playerstate == PlayerState.STATE_LERPWALK && _grounded) {...}
else
{
if(_ScriptObj_cursorControl != null)
_ScriptObj_cursorControl.DestroyDestinationTex(); // 删除目标点光圈
else
Debug.Log("cursor scirpt error !");
}角色控制-点击移动以及自动寻路(四)
...
var _ray : Ray; //从摄像机发射到鼠标位置的射线
var _hit : RaycastHit ; //射线与目标网格的交点数据
var _startPos : Vector3; //当前位置,出发点
var _endPos : Vector3; //鼠标点击位置,目标点
var _lerp : float; //插值系数
var _deltaLerp : float; //插值系数递增步长
var _rotation : Quaternion; //从当前朝向(本地空间Z轴)转向目标方向的旋转四元数值 var _slerpRotation : float ; //旋转插值系数
var _deltaSlerpRotation : float; //旋转插值系数递增步长
var _moveDirectionLerpWalk : Vector3; //鼠标点击地面后,应该移动的方向 var _movePrecision : float; //当前点与目标点的距离误差
private var _lastDistance: float ; //暂时未制作自动寻路系统,地图上未划分网格,如果目标点玩家上不去,有可能会一直跑,玩家与目标点的距离会呈现“先缩小距离,后拉长距离”的结果,所以用此变量记录上一步的距离,当距离开始拉长时,就停下。 .....
enum PlayerState
{
STATE_READY = 0,
STATE_RUN = 1,
STATE_IDLE = 2,
STATE_ATTACK = 3,
STATE_JUMP = 4,
STATE_LERPWALK = 5,//增加一个状态,标示处于右键点击地面后的移动状态 }
.......
function Update()
{
......
//=====================================================================
====
//鼠标操作 - 右键按下后弹起
var layerPlayers :LayerMask = 1 << LayerMask.NameToLayer("Players"); //var layerTerrains :LayerMask = 1 << LayerMask.NameToLayer("Terrains");
//var finalLayer : LayerMask = (layerPlayers.value | layerTerrains.value); layerPlayers = ~layerPlayers.value; //对Players这个Layer的数值2进制位取反,表示除了Player
s以外,其他Layer都可以与射线进行碰撞
if (Input.GetMouseButtonUp (1) && _grounded) {
if(_cameraCurrent == null)
{
print(null);
return;
}
_ray = _cameraCurrent.ScreenPointToRay (Input.mousePosition); if (Physics.Raycast (_ray, _hit, Mathf.Infinity , layerPlayers.value)) {
if(_hit.collider.gameObject.tag == "Terrains")
{
//Debug.DrawLine (_ray.origin, _hit.point);
_playerstate = PlayerState.STATE_LERPWALK;
_lerp = 0;
_startPos = transform.position;
var tempVec = _hit.point;
tempVec.y = _startPos.y; //高度拉齐,为了算出方向向量
_endPos = _hit.point;
print("*****the endpos is : " + _endPos);
_lastDistance = Vector3.Distance(_startPos , _endPos); _deltaLerp = 1/Vector3.Distance(_startPos , _endPos); _slerpRotation = 0;
var relativePos = tempVec - _startPos;
_rotation = Quaternion.LookRotation(relativePos); //这个函数很方便,计算出从当前朝向转向
目标方向的旋转四元数
_moveDirectionLerpWalk = relativePos.normalized; _moveDirectionLerpWalk *= _speed;
//调用鼠标处理脚本
if(_ScriptObj_cursorControl != null) //创建目标点光圈
_ScriptObj_cursorControl.CreateDestinationTex(_endPos,TextureOnFloor,_hit.normal);
}
}
}
if(_playerstate == PlayerState.STATE_LERPWALK && _grounded) {
_lerp += _deltaLerp;
//if(_lerp >= 1)
//{
// _playerstate = PlayerState.STATE_IDLE;
// transform.rotation = _rotation;
// return;
//}
_slerpRotation += _deltaSlerpRotation;
if(_slerpRotation >= 1)
{
transform.rotation = _rotation;
}
//平滑旋转人物的朝向
transform.rotation = Quaternion.Slerp(transform.rotation, _rotation, _slerpRotation);
//插值虽然计算得准,但直接用插值就无法做碰撞了,这里只用来保存坐标 //先取出移动方向向量,用Move方法移动,可以顺便做碰撞 var position = Vector3.Lerp(_startPos , _endPos , _lerp); var _flagsLerp : CollisionFlags = _controller.Move(_moveDirectionLerpWalk * Time.deltaTime);
_animation.Play("run");
var distance = Vector3.Distance(transform.position,_endPos); //以当前坐标和目标坐标的距离作为结束条件,小于误差范围值则停下 if(distance <= _movePrecision)
_playerstate = PlayerState.STATE_IDLE;
//当距离由大变小,又开始由小变大时,也要停下
else if(distance >= _lastDistance)
_playerstate = PlayerState.STATE_IDLE;
//记录上一帧中,玩家与目标点的距离
else
_lastDistance = distance;
}
else
{
if(_ScriptObj_cursorControl != null) _ScriptObj_cursorControl.DestroyDestinationTex(); // 删除目标点光圈 else
Debug.Log("cursor scirpt error !");
}
//=====================================================================
====
......
}