Unity3D鼠标控制角色移动
一直都有一颗文学逗比的心,很中二和玛丽苏的想写那种龙傲天的小说。所以这个寒假就非常想敲出个RPG游戏来抒发心中的这份狂热。一开始是想用Three.js来做,后来转用Unity3D来做了,毕竟相对简单一点。好多东西不用自己去写,也可以避免心中这份狂热不至于还没把基础框架搭建好就降为0度了。
角色移动的例子
控制角色移动,对于PC端而言就是键盘或者鼠标。其中键盘控制角色移动的是经典的fps游戏中wasd四个方向按键。而鼠标控制角色移动一般常见于MMORPG。我比较倾向于MMORPG风格控制角色移动,所以选择这一类。
Unity3D官方提供的是javascript写的一个基于键盘控制的小demo。对我而言,javascript虽然目前很火,但是还是觉得这门语言缺陷太严重,所以改用C#,期间碰到了一个很奇怪的问题:其中一个函数像是被注释掉一样死活不执行,所以就放在另一个方法执行体内了,看起来丑丑的。还有一个就是被MonoDevelop整得微醺。还是很赞Mono哒~
大致是借鉴了官方提供的例子,在此基础上做了一些小修改。
还是详细说一下具体思路,在文章中贴出完整代码(ps:肯定不完整的啦,动画还没加呢,只是能动了XD)
定义类
类中分为四部分:字段、属性、本地方法、继承方法
官方例子中,将角色分为几种状态,根据不同的状态赋予角色不同的运动速度和显示动画,为了之后方便操作,提供了可以在Unity3D直接进行修改的公有字段(这里很费解,为什么不是公有属性,可能大师的设计不是能理解的),这里就大致精简和照抄照搬了:
字段和属性
enum CharacterState : int{
Standing = 0,
Walking = 1,
Running = 2,
}
#region 公有字段
#region 动作动画
/// <summary>
/// 站立动画
/// </summary>
public AnimationClip Standing;
/// <summary>
/// 奔跑动画
/// </summary>
public AnimationClip Running;
/// <summary>
/// 步行动画
/// </summary>
public AnimationClip Walking;
#endregion
#region 参数设置
/// <summary>
/// 步行速度
/// </summary>
public float WalkSpeed = 0.3f;
/// <summary>
/// 奔跑速度
/// </summary>
public float RunSpeed = 0.8f;
/// <summary>
/// 重力加速度
/// </summary>
public float Gravity = 5.0f;
/// <summary>
/// 到达误差
/// </summary>
public float ArriveError = 1.0f;
/// <summary>
/// 速度平滑度
/// </summary>
public float SpeedSmoothing = 5.0f;
/// <summary>
/// 角色接受的最大坡度
/// </summary>
public float SlopeLimit = 45.0f;
/// <summary>
/// 角色旋转速度
/// </summary>
public float RotateSpeed = 20.0f;
/// <summary>
/// 空中阻力
/// </summary>
public float InAirControlAcceleration = 4.0f;
/// <summary>
/// 最大速度
/// </summary>
public float MaxSpeed = 2.0f;
#endregion
#endregion
然后就是私有字段了,大致就是角色类内的各种状态,这里先给“最后距离”这一个字段立个flag,因为它解决了一个坑了我很久的问题:
#region 私有字段
/// <summary>
/// 角色状态
/// </summary>
private CharacterState _characterState;
/// <summary>
/// 碰撞标签
/// </summary>
private CollisionFlags _collisionFlag;
/// <summary>
/// 垂直速度
/// </summary>
private float _verticalSpeed = 0.0f;
/// <summary>
/// 移动方向
/// </summary>
private Vector3 _moveDirection = Vector3.zero;
/// <summary>
/// 在空中的速度
/// </summary>
private Vector3 _inAirVelocity = Vector3.zero;
/// <summary>
/// 移动速率
/// </summary>
private float _moveSpeed = 0.0f;
/// <summary>
/// 目标位置
/// </summary>
private Vector3 _targetPosition = Vector3.zero;
/// <summary>
/// 目标是否有效
/// </summary>
private bool _isValidTarget = false;
/// <summary>
/// 控制器
/// </summary>
private CharacterController _controller;
/// <summary>
/// 最后距离
/// </summary>
private float _lastDistance = 0.0f;
#endregion
目前属性设置了三个,其中一个略显鸡肋。
这里解释说明一下:
Unity3D提供了CharacterController 角色控制器这样一种很好用的东西。
摘自网络的Physx参考:
character一般用于主角这类用户控制的物体,它不会受到scene的重力影响,不会被其他物体推。
程序中可以使用它的move方法移动它,当他碰到静态物体时,会停下来,遇到动态物体时会推开他,当然,这些都是可以通过activegroup来控制的。group最多有32组。因为他是一个NxU32,并通过每一位代表一个组。
move的一个参数用来告诉程序,character的当前状态。(collisionFlags)
当他遇到物体的时候,如果设置了回调函数,系统会通过回调函数通知程序。。(NxControllerDesc.callback)
character还有上楼梯模式,在某些高度的台阶,可以直接上去。(NxControllerDesc.stepOffset)
character还可以设置可以走上去的斜坡。(NxControllerDesc.slopeLimit)
由于character不受场景的重力影响,所以,用户要在move函数中自己添加重力因素,也就是说,character可以浮在空中,除非那里有其他activegroup物体。
这是我不用Three.js原因中很大一部分原因。就是避免重复造轮子但是学人家造轮子就是另一回事了。
IsGrounded正是借用CharacterController.move()返回的collisionFlag来判断是否到达地面。
IsTouchedWall 这个属性很鸡肋,没有任何必要去讲。当时是因为当角色在斜坡上横向移动时,会因为判定为悬空而在竖直方向上有个非常大的速度,导致移动速度非常快。所以加上这样的属性,但是效果并不明显,它是检测竖直方向上的速度,而非横向速度。所以是没用的鸡肋的。其实跟CharacterController.slopeLimit功能应该是一样的。
SetCharacterState 这个是个只写属性,主要是用于修改角色状态,以及变换角色动画,switch里边填充XD
#region 属性
/// <summary>
/// 是否跌落至地面
/// </summary>
/// <value><c>true</c> if this instance is grounded; otherwise, <c>false</c>.</value>
private bool IsGrounded { get { return (this._collisionFlag & CollisionFlags.CollidedBelow) != 0; } }
/// <summary>
/// 是否碰触到墙体
/// </summary>
/// <value><c>true</c> if this instance is touched wall; otherwise, <c>false</c>.</value>
private bool IsTouchedWall
{
get
{
if(this._moveSpeed != 0){
Vector3 newPosition = this.transform.position + (this.transform.rotation * Vector3.forward * this._moveSpeed);
newPosition.y = Terrain.activeTerrain.SampleHeight(newPosition);
float deltaHeight = newPosition.y - Terrain.activeTerrain.SampleHeight(this.transform.position);
float deltaLength = Vector3.Distance(newPosition, this.transform.position);
Debug.DrawLine(this.transform.position, newPosition);
float alpha = Mathf.Asin(deltaHeight / deltaLength) * Mathf.Rad2Deg;
if(alpha <= this.SlopeLimit){
return false;
}
else{
return true;
}
}
return false;
}
}
/// <summary>
/// 设置角色状态
/// </summary>
/// <value>The state of the set character.</value>
private CharacterState SetCharacterState
{
set
{
//惰性更新动画
if(this._characterState != value)
{
this._characterState = value;
}
switch(this._characterState)
{
default:
break;
}
}
}
#endregion
方法
本地方法提供两个(其实是三个,由于一个死活不执行,所以只好两个功能相近的方法合并为一个)。
第一个方法:UpdateMovementTarget
用于更新运动目标
方法执行一开始,首先判断当前的移动目标是否有效
如果有效,则判断是否到达目的点(即当前位置与目的位置的距离小于容差值),
如果到达目的点,则停止运动,
否则,判断是否开始与目的点距离变大,如果变大,则立即停止运动。否则记录最新的与目的点距离。
判断是否有鼠标右键点击中断(可以设置为公有字段),
如果传入鼠标右键点击中断,则从摄像机位置发射射线,与地面碰撞的点即为目标点,由于在平面移动时竖直方向上的值并不影响,且在之前容错时会导致错误,所以将目标点的高度设置为0,并记录距离。
计算角色的移动方向,这里不赘述。
之后根据之前的状态,来改变角色的速度。
第二个方法:ApplyGravity
这个方法主要用作重力加速度,再次吐槽IsTouchedWall的鸡肋。
/// <summary>
/// 更新运动方向
/// </summary>
private void UpdateMovementTarget()
{
Vector3 memoryPosition = this.transform.position;
memoryPosition.y = 0;
if(this._isValidTarget)
{
float tempDistance = Vector3.Distance(memoryPosition, this._targetPosition);
if(tempDistance <= this.ArriveError)
{
this._isValidTarget = false;
this._lastDistance = 0.0f;
}
else if(tempDistance >= this._lastDistance)
{
this._isValidTarget = false;
this._lastDistance = 0.0f;
}
else
{
this._lastDistance = tempDistance;
}
}
if(Input.GetMouseButtonDown(1))
{
this._moveDirection = Vector3.zero;
this._isValidTarget = false;
Ray targetRay = Camera.main.ScreenPointToRay(Input.mousePosition);
RaycastHit hitInfo;
if(Physics.Raycast(targetRay, out hitInfo, Mathf.Infinity))
{
if(hitInfo.collider.gameObject.tag == "Terrain")
{
this._targetPosition = hitInfo.point;
this._targetPosition.y = 0;
this._isValidTarget = true;
this._lastDistance = Vector3.Distance(memoryPosition, this._targetPosition);
}
}
}
Vector3 targetDirection = (this._targetPosition - this.transform.position).normalized;
if(this._isValidTarget)
{
this._moveDirection = Vector3.RotateTowards(this._moveDirection, targetDirection, this.RotateSpeed * Mathf.Deg2Rad * Time.deltaTime, 1000);
this._moveDirection.y = 0;
this._moveDirection = this._moveDirection.normalized;
}
if(!this.IsGrounded)
{
this._inAirVelocity += targetDirection.normalized * Time.deltaTime * this.InAirControlAcceleration;
}
else
{
this._inAirVelocity = Vector3.zero;
this._verticalSpeed = 0.0f;
}
if(this.IsTouchedWall)
{
this._isValidTarget = false;
}
if(this._isValidTarget){
float targetSpeed = Mathf.Min(this._moveDirection.magnitude, 1.0f);
float currentSmooth = this.SpeedSmoothing * Time.deltaTime;
if(Input.GetKey(KeyCode.LeftShift) || Input.GetKey(KeyCode.RightShift))
{
targetSpeed *= this.RunSpeed;
this.SetCharacterState = CharacterState.Running;
}
else
{
targetSpeed *= this.WalkSpeed;
this.SetCharacterState = CharacterState.Walking;
}
this._moveSpeed = Mathf.Lerp(this._moveSpeed, targetSpeed, currentSmooth);
}
else
{
this._moveSpeed = 0.0f;
this._moveDirection = Vector3.zero;
this.SetCharacterState = CharacterState.Standing;
}
}
/// <summary>
/// 重力加速度
/// </summary>
private void ApplyGravity()
{
if(this.IsGrounded || this.IsTouchedWall)
{
this._verticalSpeed = 0.0f;
this._inAirVelocity = Vector3.zero;
}
else
{
this._verticalSpeed -= this.Gravity * Time.deltaTime;
}
}
#endregion
接下来是继承方法,MonoBehavior默认提供两种继承方法:Start和Update.
Start执行时间在Update函数第一次被调用前调用。
Update执行时间为当MonoBehaviour启用时,其Update在每一帧被调用。
// Use this for initialization
void Start ()
{
//初始设置
this.SetCharacterState = CharacterState.Standing;
//目标无效
this._isValidTarget = false;
//初始化控制器
this._controller = GetComponent<CharacterController>();
}
// Update is called once per frame
void Update ()
{
//更新移动目标
this.UpdateMovementTarget();
//添加重力影响
this.ApplyGravity();
this._controller.slopeLimit = this.SlopeLimit;
//合成速度
Vector3 movement = this._moveDirection * this._moveSpeed + (new Vector3(0, this._verticalSpeed, 0)) + this._inAirVelocity;
if(movement.magnitude > this.MaxSpeed)
{
movement = movement.normalized * this.MaxSpeed;
}
//角色转向
if(this._moveDirection != Vector3.zero)
this.transform.rotation = Quaternion.LookRotation(this._moveDirection);
//角色移动
_collisionFlag = this._controller.Move(movement);
}
郑重声明:本站内容如果来自互联网及其他传播媒体,其版权均属原媒体及文章作者所有。转载目的在于传递更多信息及用于网络分享,并不代表本站赞同其观点和对其真实性负责,也不构成任何其他建议。