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

        各位朋友,大家好。我是秦元培,欢迎大家关注我的博客,我的博客地址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的方式,角色控制器具备了更为优越的特性。

详细地说,角色控制器同意开发人员在受制于碰撞的情况下非常easy的进行运动,而不用处理刚体。

依照博主的理解就是角色控制器具备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系统,我们能够把它当做是一个预制的动画模板,它定义了角色详细的状态下应该採用什么样的动画,而这一切的载体就是Unity3DMecanim动画系统。也许我们对于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模型,通过该模型我们能够更easy地控制角色,只是恕博主直言,在国内假设採用这样的模型来做游戏的话。可能会不太适应我们自己的游戏。由于在《仙剑奇侠传五》中最初就是由于採用相似这样的的自己主动视角,导致游戏最初的游戏体验并非非常完美。再者像《仙剑奇侠传》、《古剑奇谭》这类中国传统风格的游戏在美学设计上更倾向于好看而不是真实,所以直接採用这样的模型会有点困难。只是还是那句话,我们不能由于某些客观的因素就停止学习啊,好了。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,玩家是能够跳跃的,那么利用角色控制器来实现这样的效果该怎么做呢?非常easy。这和模拟重力是一样的,即设定一个跳跃速度,假设玩家按下了空格键。则改变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


posted @ 2016-04-15 20:46  zfyouxi  阅读(3079)  评论(0编辑  收藏  举报