您的位置:首页 > 移动开发 > Unity3D

[Unity3D]Unity3D游戏开发之角色控制漫谈

2014-09-04 13:13 786 查看
各位朋友,大家好,我是秦元培,欢迎大家关注我的博客,我的博客地址blog.csdn.net/qinyuanpei。今天呢,我们来说说Unity3D中的角色控制,这篇文章并非关注于Unity3D中的某项具体内容,而是博主在经过大量的实践后所得的感悟。今天的文章从内容上可以分为2种模式、1个组件、1种模型,希望对大家学习Unity3D起到良好的推动作用。好了,下面我们就正式开始今天的文章吧。

一、2种模式
众所周知,角色控制有第一人称和第三人称两种情况,在RPG游戏中通常以第三人称的形式出现。而对于第三人称角色控制而言,通常有2种模式。
第一种模式中,角色在Z轴方向向前或者向后移动、绕自身Y轴旋转。当角色旋转时,摄像机会根据角色旋转的角度旋转到对应的位置,使摄像机始终正对着角色的背面,这样玩家在控制角色的过程中,只能看到玩家的背面。当角色移动时,摄像机会保持与玩家间的一定距离,然后跟随角色进行移动。采用这种模式的代表游戏是《仙剑奇侠传四》,这种模式的优点是操作简单,依靠四个方向键就可以完成角色的控制。这种模式的缺点同样很明显,因为摄像机始终面向角色背面,所以玩家无法看到角色的正面。从这个角度上来说,这种模式不能称之为真正的3D,因为玩家的视角是锁死的。那么在Unity3D中如何实现这种模式的角色控制呢?我们只需要为Main Camera添加一个SmoothFollow脚本,这样我们就可以使摄像机始终面向玩家的背面。对于角色这部分,我们只需要完成Z轴方向上的前进与后退,Y轴方向上的旋转即可。这部分脚本我们将放在后面来讲,因为这里需要用到一个重要的组件。



下面来介绍第二种模式,这种模式是现下网游中较为流行的模式,在这种模式下,玩家可以按照自身坐标系向着四个不同的方向移动,当玩家按下鼠标右键时,可以绕Y轴按照一定的角度旋转摄像机,在旋转的过程中,角色将旋转相应的角度。在移动的过程中,摄像机会保持与玩家间的一定距离,然后跟随角色进行移动。这种模式的代表作有《古剑奇谭》、《仙剑奇侠传五前传》等。这种模式的优点是玩家可以自由地对角色进行观察,是真正意义上的3D。可是它不是没有缺点啊,在控制角色的时候,玩家需要双手同时进行操作。这种模式在Unity3D中的实现同样需要SmoothFollow脚本,不过需要克服对旋转角度的追踪。此外,我们还需要一个相机控制的脚本,这样我们可以在360度地欣赏游戏中的美丽场景。同样地,脚本我们放在后面来讲。




二、1个组件
在Unity3D中有一个称为角色控制器(CharacterController)的组件,从这个名称,我们就可以知道它是一个用来控制角色的组件。虽然通过Transform的方式同样可以实现角色的控制,可是相比Transform的方式,角色控制器具备了更为优越的特性。具体地说,角色控制器允许开发者在受制于碰撞的情况下很容易的进行运动,而不用处理刚体。按照博主的理解就是角色控制器具备Rigidbody组件的部分属性,如果使用角色控制器就可以不用用Rigidbody组件了。事实上,角色控制器可以忽略场景中重力的影响,但是受制于碰撞。这句话怎么理解呢?博主举一个例子,比如我们需要使角色受制于碰撞的影响,所以我们可以为角色添加一个刚体(Rigidbody),可是我们同时不希望当角色和场景中的静态物体碰撞时被撞飞,固然我们可以通过限制角色在碰撞过程中的角度和位置变化来实现这样的效果,可是事实上如果角色和场景中的物体发生了碰撞,其碰撞的结果通常是不受玩家控制的,尤其是角色启用重力因素后,对于碰撞结果的控制会显得更加艰难。那么角色控制器就是为了满足这样一个需求而产生的。具体地说,通过角色控制器我们可以实现这样的需求:
1、重力控制 由于CharacterController不受场景的重力影响,所以,设计者可以自行添加重力因素。例如在CharacterController组件中有一个isGrounded的属性,该属性可以判断角色是否位于地面上。需要注意的是,CharacterController依赖于碰撞,即地面和角色都需要有碰撞体才可以,一个较为有效的方法是使用标准的碰撞体如Box、Sphere、Capsule来作为一个模型的父容器。具体地应用我们会在最后的脚本部分给出来。
2、爬楼梯 CharacterController.stepOffset属性使得角色具备了爬楼梯的能力,它是一个以米为单位的角色控制器的台阶偏移量,表示角色在垂直方向上台阶的高度。试想我们如果使用Transform方式实现这样的效果,恐怕我们需要费一番周折了,好在CharacterController可以帮助我们轻松地实现这样的功能,让玩家在游戏世界里更为自由和充满乐趣。
3、爬斜坡 和stepOffset属性类似,slopeLimit可以设置允许角色攀爬的最大角度。利用这一属性最为显著的实例是在起伏的地面上控制角色,如果我们使用Transform方式来移动角色,可能会出现角色从地面上穿过去这样的情况,显然CharacterController再次让我们的问题变得简单。
关于更多CharacterController的特性大家可以自行查阅API文档,
然而不过无可否认的是CharacterController让我们控制角色变得更为简单,这一点大家可以在具体的项目中得到较为深刻的体会。

三、1种模型
如果说角色控制器的出现让我们控制角色变得更为简单,那么下面要讲的Locomotion模型将会让我们控制动画变得更为简单,特别强调是在控制角色移动动画的时候。关于Locomotion系统,我们可以把它当做是一个预制的动画模板,它定义了角色具体的状态下应该采用什么样的动画,而这一切的载体就是Unity3D的Mecanim动画系统。或许我们对于Locomotion模型不甚了解,可是我们会在Unity3D官方的示例项目中找到它的身影。使用Locomotion模型需要导入相应的资源包,我们可以在Unity3D资源商店里下载一个名为Mecanim Locomotion System StartKit的资源包,这样我们就可以在项目中使用Locomotion模型了。从官方的介绍中博主得知该模型主要的用途是能够自动混合关键帧或捕捉动画的走和跑的循环并且调整腿部骨骼的运动来保证脚步能正确地落在地面上。这个模型能调整并改变动画的速率和曲线以任何速率、方向、曲率、步幅从一个简单平面到任何倾斜角度的地形。在英文中Locomotion是运动的意思,所以这是一个提供角色运动支持的东西。可是博主目前并没有发现它在适应不同地形方面的特性,所以这里我们实际上还是在说Mecamin动画系统。这里我们以该资源包里的示例项目来讲解Locomotion模型。如图,在Animator窗口中可以看到它是一个Mecanim动画:



如果大家熟悉Mecanim的话,可以很清楚地看出来它关联了多种动画状态。或许在不同的项目中,大家设计的Mecamim动画可能会有所不同,不过它的实质是一样的。我们继续来看这个资源包为我们提供的东西,在Script文件夹下我们可以看到一个Locomotion的脚本,这个脚本是我们使用Locomotion模型的前提,打开脚本我们会发现,这是对Unity3D Mecanim API的一种封装。

using UnityEngine;
using System.Collections;

public class Locomotion
{
    private Animator m_Animator = null;
    
    private int m_SpeedId = 0;
    private int m_AgularSpeedId = 0;
    private int m_DirectionId = 0;

    public float m_SpeedDampTime = 0.1f;
    public float m_AnguarSpeedDampTime = 0.25f;
    public float m_DirectionResponseTime = 0.2f;
    
    public Locomotion(Animator animator)
    {
        m_Animator = animator;

        m_SpeedId = Animator.StringToHash("Speed");
        m_AgularSpeedId = Animator.StringToHash("AngularSpeed");
        m_DirectionId = Animator.StringToHash("Direction");
    }

    public void Do(float speed, float direction)
    {
        AnimatorStateInfo state = m_Animator.GetCurrentAnimatorStateInfo(0);

        bool inTransition = m_Animator.IsInTransition(0);
        bool inIdle = state.IsName("Locomotion.Idle");
        bool inTurn = state.IsName("Locomotion.TurnOnSpot") || state.IsName("Locomotion.PlantNTurnLeft") || state.IsName("Locomotion.PlantNTurnRight");
        bool inWalkRun = state.IsName("Locomotion.WalkRun");

        float speedDampTime = inIdle ? 0 : m_SpeedDampTime;
        float angularSpeedDampTime = inWalkRun || inTransition ? m_AnguarSpeedDampTime : 0;
        float directionDampTime = inTurn || inTransition ? 1000000 : 0;

        float angularSpeed = direction / m_DirectionResponseTime;
        
        m_Animator.SetFloat(m_SpeedId, speed, speedDampTime, Time.deltaTime);
        m_Animator.SetFloat(m_AgularSpeedId, angularSpeed, angularSpeedDampTime, Time.deltaTime);
        m_Animator.SetFloat(m_DirectionId, direction, directionDampTime, Time.deltaTime);
    }	
}

如果我们需要对角色进行控制,只需要使用下面的代码:

/// <summary>
/// 
/// </summary>

using UnityEngine;
using System;
using System.Collections;
  
[RequireComponent(typeof(Animator))]  

//Name of class must be name of file as well

public class LocomotionPlayer : MonoBehaviour {

    protected Animator animator;

    private float speed = 0;
    private float direction = 0;
    private Locomotion locomotion = null;

	// Use this for initialization
	void Start () 
	{
        animator = GetComponent<Animator>();
        locomotion = new Locomotion(animator);
	}
    
	void Update () 
	{
        if (animator && Camera.main)
		{
            JoystickToEvents.Do(transform,Camera.main.transform, ref speed, ref direction);
            locomotion.Do(speed * 6, direction * 180);
		}		
	}
}

我们发现此时我们的脚本变得简单了许多,因为大量的脚本被转移到了Locomotion脚本中。而这就是博主想为大家介绍的Locomotion模型,通过该模型我们可以更容易地控制角色,不过恕博主直言,在国内如果采用这种模型来做游戏的话,可能会不太适应我们自己的游戏。因为在《仙剑奇侠传五》中最初就是因为采用类似这种的自动视角,导致游戏最初的游戏体验并不是很完美。再者像《仙剑奇侠传》、《古剑奇谭》这类中国传统风格的游戏在美学设计上更倾向于好看而不是真实,所以直接采用这样的模型会有点困难。不过还是那句话,我们不能因为某些客观的因素就停止学习啊,好了,Locomotion就先说到这里吧,下面我们来重点讲解脚本。
下面讲述如何使用角色控制器来控制角色,我们一起来看下面的脚本:

using UnityEngine;
using System.Collections;

public class PlayerController : MonoBehaviour {
	
	//移动速度
	public float MoveSpeed=1.5F;
	//奔跑速度
	public float RunSpeed=4.5F;
	//旋转速度
	public float RotateSpeed=30;
	//重力
	public float Gravity=20;
	//动画组件
	private Animator mAnim;
	//声音组件
	private AudioSource mAudio;
	//速度
	private float mSpeed;
	//移动方式,默认为Walk
	public TransportType MoveType=TransportType.Walk;
	//游戏管理器
	private GameManager mManager;
	//角色控制器
	private CharacterController mController;
	

	void Start () 
	{
	   //获取动画组件
	   mAnim=GetComponentInChildren<Animator>();
	   //获取声音组件
	   mAudio=GetComponent<AudioSource>();
	   //获取游戏管理器
	   mManager=GameObject.Find("GameManager").GetComponent<GameManager>();
	   //获取角色控制器
	   mController=GetComponent<CharacterController>();
	}

	void Update () 
	{
		//只有处于正常状态时玩家可以行动
		if(mManager.Manager_State==GameState.Normal)
		{
	        MoveManager();
	    }
	}
    
	//移动管理
	void MoveManager()
	{
		//移动方向
		Vector3 mDir=Vector3.zero;
		if(mController.isGrounded)
		{
	       if(Input.GetAxis("Vertical")==1)
	       {
		      SetTransportType(MoveType);
			  mDir=Vector3.forward * RunSpeed * Time.deltaTime;
	       }
	       if(Input.GetAxis("Vertical")==-1)
	       {
		      SetTransportType(MoveType);
			  mDir=Vector3.forward * -RunSpeed * Time.deltaTime;
	       }
	       if(Input.GetAxis("Horizontal")==-1)
	       {
		      SetTransportType(MoveType);
		      Vector3 mTarget=new Vector3(0,-RotateSpeed* Time.deltaTime,0);
		      transform.Rotate(mTarget);
	       }
	       if(Input.GetAxis("Horizontal")==1)
	       {
		      SetTransportType(MoveType);
		      Vector3 mTarget=new Vector3(0,RotateSpeed* Time.deltaTime,0);
		      transform.Rotate(mTarget);
	       }
	       if(Input.GetAxis("Vertical")==0 && Input.GetAxis("Horizontal")==0)
	       {
		      mAnim.SetBool("Walk",false);
		      mAnim.SetBool("Run",false);
	       }
	   }
		//考虑重力因素
		mDir=transform.TransformDirection(mDir);
		float y=mDir.y-Gravity *Time.deltaTime;
		mDir=new Vector3(mDir.x,y,mDir.z);
		mController.Move(mDir);

	   //使用Tab键切换移动方式
	   if(Input.GetKey(KeyCode.Tab))
	   {
		  if(MoveType==TransportType.Walk){
			MoveType=TransportType.Run;
		  }else if(MoveType==TransportType.Run){
			MoveType=TransportType.Walk;
		  }
	   }
	}

    
	//设置角色移动的方式
	public void SetTransportType(TransportType MoveType)
	{
	   switch(MoveType)
	   {
			case TransportType.Walk:
				MoveType=TransportType.Walk;
				mAnim.SetBool("Walk",true);
				mSpeed=MoveSpeed;
				break;
			case TransportType.Run:
				MoveType=TransportType.Run;
				mAnim.SetBool("Run",true);
				mSpeed=RunSpeed;
				break;
	   }
	}

}
以上这段脚本是博主在做的一个游戏中使用的代码,在这段脚本中你可以看到博主是如何利用CharacterController来控制角色的,即根据玩家的输入轴计算移动方向,如果玩家不在地面上(通过isGrounded属性来判断),则需要考虑重力因素,理论上角色从高处下落的时候应该是加速运动,根据物理学公式h=1/2gt^2,这里只是为了模拟重力,所以采用了简化的匀速运动,希望大家注意。这里还可以进一步扩展,比如我们所熟悉的经典射击游戏CS,玩家是可以跳跃的,那么利用角色控制器来实现这种效果该怎么做呢?很简单,这和模拟重力是一样的,即设定一个跳跃速度,如果玩家按下了空格键,则改变mDir中的y即可。这里我们使用的是第一种控制模式,即锁视角的控制模式,可能是因为博主最早接触的3D游戏是《仙剑奇侠传四》吧,所以博主更喜欢这样的控制模式。在这段脚本中有一个SetTransportType()的方法,用来切换角色移动的方式,主要是切换动画和移动速度。我知道很多朋友对不锁视角的控制模式可能更感兴趣,因为这是目前的主流,例如《新剑侠传奇》在宣传之初就以不锁视角、即时战斗作为主要的噱头,不过游戏发布后效果并没有预期的那样好,好在游戏***方敢于承担错误,及时将游戏回炉重铸,足以看出***方想做好游戏的诚意。不过,这种模式博主该没有研究出来,现在手上有了新模型,所以有时间的话博主会尝试这种新的模式,希望大家关注我的博客啊,博主会不定期地更新博客的。那么下面为大家分享一个从《UnityChan》找到的关于相机控制部分的脚本,可以实现对角色的自由观察。下面给出脚本:

//CameraController.cs for UnityChan
//Original Script is here:
//TAK-EMI / CameraController.cs
//https://gist.github.com/TAK-EMI/d67a13b6f73bed32075d
//https://twitter.com/TAK_EMI
//
//Revised by N.Kobayashi 2014/5/15 
//Change : To prevent rotation flips on XY plane, use Quaternion in cameraRotate()
//Change : Add the instrustion window
//Change : Add the operation for Mac
//

using UnityEngine;
using System.Collections;

namespace CameraController
{
	enum MouseButtonDown
	{
		MBD_LEFT = 0,
		MBD_RIGHT,
		MBD_MIDDLE,
	};

	public class CameraController : MonoBehaviour
	{
		[SerializeField]
		private Vector3 focus = Vector3.zero;
		[SerializeField]
		private GameObject focusObj = null;

		public bool showInstWindow = true;

		private Vector3 oldPos;

		void setupFocusObject(string name)
		{
			GameObject obj = this.focusObj = new GameObject(name);
			obj.transform.position = this.focus;
			obj.transform.LookAt(this.transform.position);

			return;
		}

		void Start ()
		{
			if (this.focusObj == null)
				this.setupFocusObject("CameraFocusObject");

			Transform trans = this.transform;
			transform.parent = this.focusObj.transform;

			trans.LookAt(this.focus);

			return;
		}
	
		void Update ()
		{
			this.mouseEvent();

			return;
		}

		//Show Instrustion Window
		void OnGUI()
		{
			if(showInstWindow){
				GUI.Box(new Rect(Screen.width -210, Screen.height - 100, 200, 90), "Camera Operations");
				GUI.Label(new Rect(Screen.width -200, Screen.height - 80, 200, 30),"RMB / Alt+LMB: Tumble");
				GUI.Label(new Rect(Screen.width -200, Screen.height - 60, 200, 30),"MMB / Alt+Cmd+LMB: Track");
				GUI.Label(new Rect(Screen.width -200, Screen.height - 40, 200, 30),"Wheel / 2 Fingers Swipe: Dolly");
			}

		}

		void mouseEvent()
		{
			float delta = Input.GetAxis("Mouse ScrollWheel");
			if (delta != 0.0f)
				this.mouseWheelEvent(delta);

			if (Input.GetMouseButtonDown((int)MouseButtonDown.MBD_LEFT) ||
				Input.GetMouseButtonDown((int)MouseButtonDown.MBD_MIDDLE) ||
				Input.GetMouseButtonDown((int)MouseButtonDown.MBD_RIGHT))
				this.oldPos = Input.mousePosition;

			this.mouseDragEvent(Input.mousePosition);

			return;
		}

		void mouseDragEvent(Vector3 mousePos)
		{
			Vector3 diff = mousePos - oldPos;

			if(Input.GetMouseButton((int)MouseButtonDown.MBD_LEFT))
			{
				//Operation for Mac : "Left Alt + Left Command + LMB Drag" is Track
				if(Input.GetKey(KeyCode.LeftAlt) && Input.GetKey(KeyCode.LeftCommand))
				{
					if (diff.magnitude > Vector3.kEpsilon)
						this.cameraTranslate(-diff / 100.0f);
				}
				//Operation for Mac : "Left Alt + LMB Drag" is Tumble
				else if (Input.GetKey(KeyCode.LeftAlt))
				{
					if (diff.magnitude > Vector3.kEpsilon)
						this.cameraRotate(new Vector3(diff.y, diff.x, 0.0f));
				}
				//Only "LMB Drag" is no action.
			}
			//Track
			else if (Input.GetMouseButton((int)MouseButtonDown.MBD_MIDDLE))
			{
				if (diff.magnitude > Vector3.kEpsilon)
					this.cameraTranslate(-diff / 100.0f);
			}
			//Tumble
			else if (Input.GetMouseButton((int)MouseButtonDown.MBD_RIGHT))
			{
				if (diff.magnitude > Vector3.kEpsilon)
					this.cameraRotate(new Vector3(diff.y, diff.x, 0.0f));
			}
				
			this.oldPos = mousePos;	

			return;
		}

		//Dolly
		public void mouseWheelEvent(float delta)
		{
			Vector3 focusToPosition = this.transform.position - this.focus;

			Vector3 post = focusToPosition * (1.0f + delta);

			if (post.magnitude > 0.01)
				this.transform.position = this.focus + post;

			return;
		}

		void cameraTranslate(Vector3 vec)
		{
			Transform focusTrans = this.focusObj.transform;

			vec.x *= -1;

			focusTrans.Translate(Vector3.right * vec.x);
			focusTrans.Translate(Vector3.up * vec.y);

			this.focus = focusTrans.position;

			return;
		}

		public void cameraRotate(Vector3 eulerAngle)
		{
			//Use Quaternion to prevent rotation flips on XY plane
			Quaternion q = Quaternion.identity;
 
			Transform focusTrans = this.focusObj.transform;
			focusTrans.localEulerAngles = focusTrans.localEulerAngles + eulerAngle;

			//Change this.transform.LookAt(this.focus) to q.SetLookRotation(this.focus)
			q.SetLookRotation (this.focus) ;

			return;
		}
	}
}
下面是演示效果:
1、Locomotion演示



2、角色控制器演示(为节省容量只好牺牲质量啦.......)



好了,感谢大家关注我的博客,今天的内容就是这样了,希望大家喜欢。

每日箴言:当我真心在追寻着我的梦想时,每一天都是缤纷的,因为我知道每一个小时都是在实现梦想的一部分。—— 保罗·科埃略



喜欢我的博客请记住我的名字:秦元培,我博客地址是blog.csdn.net/qinyuanpei。
转载请注明出处,本文作者:秦元培,本文出处:http://blog.csdn.net/qinyuanpei/article/details/39050631
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: