Unity3D自带案例AngryBots分析(二)——人物动作控制逻辑

        Unity3D将自带demo放在C:\Users\Public\Documents\Unity Projects\4-0_AngryBots中,可以在open project中直接选择,或者从该目录中Assets双击AngryBots进入即可。

    在Hierarchy视图中选中Player及展开Player下面的子对象,选中各对象,可以在Inspector中看到相关对象上挂有的脚本及其他components。Transform是每个对象都有的,负责存储位置、方向、缩放因子,因而负责对象的平移、旋转、缩放等变换;Capsule Collider为人物添加了不可触发事件的无摩擦碰撞器;Rigidbody使人物置于物理因素作用下,没有空气阻力,设置扭矩和重力,防止快速移动穿越其它object设置的离散碰撞检测。该组件提供了按照物理属性来改变人物的Transform的能力,更具有真实效果,FreeMovementMotor.js中必须有Rigidbody,操作的就是刚体;player_root对象完成了人物整个身体骨骼的搭建;Skinned Mesh Renderer能展示阴影效果、设置了血条(healthbar_player)和人物外表(play-01-default)材质,影响顶点最大骨骼数auto,设置offscreen的范围,添加了main_player_lorez的Mesh。


人物动作控制脚本分析如下:

MovementMotor.js:包含三个变量

#pragma strict
@HideInInspector
public var movementDirection : Vector3;

@HideInInspector
public var movementTarget : Vector3;

@HideInInspector
public var facingDirection : Vector3;

 
  
  • #pragma strict为js中强制使用静态类型,禁用动态类型,输入变量时必须明确指定变量类型不能让编译器自己去推测类型。
  • @HideInInspector为编译器属性,作用是使变量不会显示在Inspector中,但是会被实例化。
  • movementDirection为角色世界坐标系中的移动方向,该向量长度应该保持0~1内。
  • movementTarget为在仅有目标的情况下使用,来驱动角色向目标前进。
  • facingDirection为角色在世界坐标系中面朝的方向。

    一般情况是,有了target则使用movementTarget来控制变换,而movementDirection和facingDirection一般都一起使用,两者合在一起驱动角色的运动。该脚本主要用于刚体控制物体的移动旋转时用到,使用起来更像接口,适用于完成人物、敌人、各种实体的运动控制。

FreeMovementMotor.js:

#pragma strict
@script RequireComponent (Rigidbody)//强制添加一个Rigidbody到这个object上,如果存在,则该Rigidbody不允许被删除。
class FreeMovementMotor extends MovementMotor {		
	public var walkingSpeed : float = 5.0;     //人物移动速度
	public var walkingSnappyness : float = 50; //人物移动影响因子
	public var turningSmoothing : float = 0.3; //人物旋转影响因子
	
	function FixedUpdate () {
		//处理人物移动
		var targetVelocity : Vector3 = movementDirection * walkingSpeed;//人物走动速度和移动方向融合得出移动速度
		var deltaVelocity : Vector3 = targetVelocity - rigidbody.velocity;//速度差值
		if (rigidbody.useGravity)
			deltaVelocity.y = 0;//重力会使人物不断降落,所以将y轴方向移动设置为0,则人物像我们期望的那样保持原高度。
		rigidbody.AddForce (deltaVelocity * walkingSnappyness, ForceMode.Acceleration);//对刚体施加外力,让其移动起来
		
		//设置人物朝向
		var faceDir : Vector3 = facingDirection;//朝向设置为facingDirection
		if (faceDir == Vector3.zero)
			faceDir = movementDirection;//朝向Zero情况下,将其设置为移动的方向(正常情况下朝向方向和移动方向并不一致)
		
		//使人物旋转到目标朝向
		if (faceDir == Vector3.zero) {
			rigidbody.angularVelocity = Vector3.zero;
		}
		else {//世界坐标系中,将人物的transform中存储的前方,旋转到要面朝方向所需转动的角度
			var rotationAngle : float = AngleAroundAxis (transform.forward, faceDir, Vector3.up);
			rigidbody.angularVelocity = (Vector3.up * rotationAngle * turningSmoothing);//设置刚体角速度,让其旋转起来
		}
	}
	
	//方向dirA绕轴axis旋转到方向dirB所需转动的角度
	static function AngleAroundAxis (dirA : Vector3, dirB : Vector3, axis : Vector3) {
	    //dirA和dirB在与轴垂直的平面上的投影,这样以便得到两者直接的角度
	    dirA = dirA - Vector3.Project (dirA, axis);
	    dirB = dirB - Vector3.Project (dirB, axis);
	   
	    //dirA和dirB之间角度的正值
	    var angle : float = Vector3.Angle (dirA, dirB);
	   
	    //根据dirA旋转到dirB叉乘正方向与axis方向,得出旋转角度的正负
	    return angle * (Vector3.Dot (axis, Vector3.Cross (dirA, dirB)) < 0 ? -1 : 1);
	}
	
}

  • 游戏中人物的朝向和人物的移动方向并不一致,玩游戏时就会发现,人物可以像螃蟹似得横着移动。
  • var deltaVelocity : Vector3 = targetVelocity - rigidbody.velocity; targetVelocity是我们指定的固定不变的速度,而rigidbody.velocity是刚体自身的速度(刚体涉及物理因素,所以该速度都是跟物理因素相关的,它跟人物的移动速度不是一个东西,对移动速度有影响)。因而移动速度受固定速度和物理因素影响的速度共同作用得到的。
  • rigidbody.AddForce (deltaVelocity * walkingSnappyness, ForceMode.Acceleration);中利用的是加速模式(ForceMode.Acceleration),该模式不论对象的质量大小是多少,都以相同的方式移动对象。walkingSnappyness是个因子,可以在Inspector中设置,涉及到物理因素的影响。
  • rigidbody.angularVelocity = (Vector3.up * rotationAngle * turningSmoothing);中turningSmoothing是转动平滑因子,Vector3.up是角速度的正方向,人物在世界坐标系中上方向为世界坐标系的y轴,rotationAngle为旋转角度,这样就使人物旋转到指定的朝向。

PlayerMoveController.js

#pragma strict

//拖进的对象
public var motor : MovementMotor;
public var character : Transform;
public var cursorPrefab : GameObject;
public var joystickPrefab : GameObject;

//摄像机设置
public var cameraSmoothing : float = 0.01;
public var cameraPreview : float = 2.0f;

//光标设置
public var cursorPlaneHeight : float = 0;
public var cursorFacingCamera : float = 0;
public var cursorSmallerWithDistance : float = 0;
public var cursorSmallerWhenClose : float = 1;

//私有成员变量
private var mainCamera : Camera;
//光标操作杆相关
private var cursorObject : Transform;
private var joystickLeft : Joystick;
private var joystickRight : Joystick;
//摄像机相关
private var mainCameraTransform : Transform;
private var cameraVelocity : Vector3 = Vector3.zero;
private var cameraOffset : Vector3 = Vector3.zero;
private var initOffsetToPlayer : Vector3;

//光标点(PC上鼠标位置,移动设备上操作杆控制) 
private var cursorScreenPosition : Vector3;
//人物移动平面
private var playerMovementPlane : Plane;

private var joystickRightGO : GameObject;
//屏幕在世界坐标系中旋转量及位置方向
private var screenMovementSpace : Quaternion;
private var screenMovementForward : Vector3;
private var screenMovementRight : Vector3;

function Awake () {		
	motor.movementDirection = Vector2.zero;//人物初始移动方向
	motor.facingDirection = Vector2.zero;//人物初始朝向
	
	//设置主摄像机变量
	mainCamera = Camera.main;
	mainCameraTransform = mainCamera.transform;
	
	// 确保人物存在,默认设置为该脚本所挂对象的transform
	if (!character)
		character = transform;
	//初始化摄像机位置和人物位置之间的距离
	initOffsetToPlayer = mainCameraTransform.position - character.position;
	
	#if UNITY_IPHONE || UNITY_ANDROID || UNITY_WP8 || UNITY_BLACKBERRY || UNITY_TIZEN//Iphone,Android,WP8,BlackBerry,Tizen平台
		if (joystickPrefab) {
			//创建做操作杆
			var joystickLeftGO : GameObject = Instantiate (joystickPrefab) as GameObject;//克隆操作杆预置
			joystickLeftGO.name = "Joystick Left";
			joystickLeft = joystickLeftGO.GetComponent. ();
			
			// 创建右操作杆
			joystickRightGO = Instantiate (joystickPrefab) as GameObject;//克隆操作杆预置(GameObject类型)
			joystickRightGO.name = "Joystick Right";
			joystickRight = joystickRightGO.GetComponent. ();//Joystick类型			
		}
	#elif !UNITY_FLASH//非flash平台
		if (cursorPrefab) {
			cursorObject = (Instantiate (cursorPrefab) as GameObject).transform;//光标位置获取
		}
	#endif
	
	//初始化摄像机位置和人物位置之间的距离,第一帧中会用到
	cameraOffset = mainCameraTransform.position - character.position;
	
	//设置初始鼠标位置为屏幕中央
	cursorScreenPosition = Vector3 (0.5 * Screen.width, 0.5 * Screen.height, 0);
	
	//人物移动平面初始化
	playerMovementPlane = new Plane (character.up, character.position + character.up * cursorPlaneHeight);
}

function Start () {
	#if UNITY_IPHONE || UNITY_ANDROID || UNITY_WP8 || UNITY_BLACKBERRY || UNITY_TIZEN
		//将操作杆的图标移动屏幕右侧
		var guiTex : GUITexture = joystickRightGO.GetComponent. ();
		guiTex.pixelInset.x = Screen.width - guiTex.pixelInset.x - guiTex.pixelInset.width;			
	#endif	
	
	//在Start函数中计算屏幕在世界坐标系中的旋转量和forward、right坐标轴,因为摄像机此时静态不会旋转	
	screenMovementSpace = Quaternion.Euler (0, mainCameraTransform.eulerAngles.y, 0);//绕y轴旋转方向同摄像机旋转方向
	screenMovementForward = screenMovementSpace * Vector3.forward;//初始化为世界坐标系forward方向
	screenMovementRight = screenMovementSpace * Vector3.right;//初始化为世界坐标系right方向
}

function OnDisable () {
	if (joystickLeft) //左操作杆失效
		joystickLeft.enabled = false;
	
	if (joystickRight)//右操作杆失效
		joystickRight.enabled = false;
}

function OnEnable () {
	if (joystickLeft) //左操作杆有效
		joystickLeft.enabled = true;
	
	if (joystickRight)//右操作杆有效
		joystickRight.enabled = true;
}

private var button8Down : boolean = false;
private var button9Down : boolean = false;
private var moveF : int;
private var moveB : int;

function Update () {

	if(GLOBAL.isJSConnected)
	{//跟iphone平台有关,用于设置移动方向
		if(Input.GetButtonDown("Joystick button 8"))
		{
			button8Down = true;
		}

		if(Input.GetButtonUp("Joystick button 8"))
		{
			button8Down = false;
		}

		if(Input.GetButtonDown("Joystick button 9"))
		{
			button9Down = true;
		}

		if(Input.GetButtonUp("Joystick button 9"))
		{
			button9Down = false;
		}

		// 逆转逻辑
		if(!button8Down)
		{
			moveF = 1;
		}
		else
		{
			moveF = 0;
		}

		if(button9Down)
		{
			moveB = 1;
		}
		else
		{
			moveB = 0;
		}
	}

	#if UNITY_ANDROID || UNITY_WP8 || UNITY_BLACKBERRY || UNITY_TIZEN
		motor.movementDirection = joystickLeft.position.x * screenMovementRight + joystickLeft.position.y * screenMovementForward;
	#elif UNITY_IPHONE
		if(GLOBAL.isJSConnected)
		{
			if (GLOBAL.isJSExtended)
				motor.movementDirection = Input.GetAxis("Horizontal") * screenMovementRight + Input.GetAxis("Vertical") * screenMovementForward;
			else
				motor.movementDirection = -moveB * (motor.facingDirection*2) + moveF * (motor.facingDirection*2);
		}
		else
		{//使用操作杆同Android等平台
			motor.movementDirection = joystickLeft.position.x * screenMovementRight + joystickLeft.position.y * screenMovementForward;
		}

	#else//PC上,根据Input设置的Horizontal、Vertical来综合获得移动方向
		motor.movementDirection = Input.GetAxis ("Horizontal") * screenMovementRight + Input.GetAxis ("Vertical") * screenMovementForward;//如果是Vertical上下方向返回(-1~1)的值则乘以screenMovementForward会获得世界坐标系中人物在垂直方向上的位移方向;Horizontal相同,不过是左右即水平方向(ASWD)
	#endif
				
	//要确保移动大小长度小于1,这样人物在对角方向上移动不会快于水平和垂直方向(如果对角方向移动速度快于水平和垂直方向不符合实际情况,正常情况人在各个方向移动速度应该是一致的)
	if (motor.movementDirection.sqrMagnitude > 1)
		motor.movementDirection.Normalize();
	
	
	//处理人物朝向和屏幕焦点问题
	
	playerMovementPlane.normal = character.up;//人物移动平面法向量为人物up轴
	playerMovementPlane.distance = -character.position.y + cursorPlaneHeight;//人物移动平面到世界坐标系原点的距离
		
	var cameraAdjustmentVector : Vector3 = Vector3.zero;//该变量用于存储利用鼠标或操作杆位置来调整摄像机位置
	
	#if UNITY_ANDROID || UNITY_WP8 || UNITY_BLACKBERRY || UNITY_TIZEN
	
		// 移动设备上使用手指操作,将其转换到平面移动空间
		motor.facingDirection = joystickRight.position.x * screenMovementRight + joystickRight.position.y * screenMovementForward;
				
		cameraAdjustmentVector = motor.facingDirection;//调整量设为移动方向		
	
	#else			
		#if (UNITY_XBOX360 || UNITY_PS3 || UNITY_IPHONE)
			//在这些平台上使用模拟棒
			var axisX : float;
			var axisY : float;
			if(GLOBAL.isJSConnected)
			{
				if (GLOBAL.isJSExtended)
				{
					axisX = Input.GetAxis("RightHorizontal");
					axisY = Input.GetAxis("RightVertical");
					motor.facingDirection = axisX * screenMovementRight + axisY * screenMovementForward;
				}
				else
				{
					axisX = Input.GetAxis("Horizontal");
					axisY = Input.GetAxis("Vertical");
					motor.facingDirection = axisX * screenMovementRight + axisY * screenMovementForward;
				}
			}
			else
			{
				motor.facingDirection = joystickRight.position.x * screenMovementRight + joystickRight.position.y * screenMovementForward;
				cameraAdjustmentVector = motor.facingDirection;
			}		
		
		#else
	
			//PC上光标位置为鼠标位置
			var cursorScreenPosition : Vector3 = Input.mousePosition;
						
			//找出从摄像机位置射出,经过鼠标位置的那条射线与人物所在移动平面的世界坐标系中点坐标
			var cursorWorldPosition : Vector3 = ScreenPointToWorldPointOnPlane (cursorScreenPosition, playerMovementPlane, mainCamera);
			
			var halfWidth : float = Screen.width / 2.0f;
			var halfHeight : float = Screen.height / 2.0f;
			var maxHalf : float = Mathf.Max (halfWidth, halfHeight);
			
			//获取相对的屏幕点坐标(光标坐标与摄像机光心即屏幕位置的差[世界坐标系中])			
			var posRel : Vector3 = cursorScreenPosition - Vector3 (halfWidth, halfHeight, cursorScreenPosition.z);		
			posRel.x /= maxHalf; 
			posRel.y /= maxHalf;//摄像机是微调的,按照一定比例来调整
						
			cameraAdjustmentVector = posRel.x * screenMovementRight + posRel.y * screenMovementForward;
			cameraAdjustmentVector.y = 0.0;	
									
			//人物的朝向为从人的位置指向世界坐标系中光标的位置
			motor.facingDirection = (cursorWorldPosition - character.position);
			motor.facingDirection.y = 0;			
			
			//鼠标呈现在屏幕上的光标的绘制
			HandleCursorAlignment (cursorWorldPosition);
			
		#endif		
	#endif
		
	//处理摄像机位置		
	//摄像机的target位置使其指向焦点
	var cameraTargetPosition : Vector3 = character.position + initOffsetToPlayer + cameraAdjustmentVector * cameraPreview;
	
	//摄像机平滑移动到目标位置
	mainCameraTransform.position = Vector3.SmoothDamp (mainCameraTransform.position, cameraTargetPosition, cameraVelocity, cameraSmoothing);
	
	//保存摄像机坐标与人物坐标之间的差,留作下帧中使用
	cameraOffset = mainCameraTransform.position - character.position;
}

public static function PlaneRayIntersection (plane : Plane, ray : Ray) : Vector3 {//获取射线与平面的交点
	var dist : float;
	plane.Raycast (ray, dist);//获取射线端点到平面交点的距离
	return ray.GetPoint (dist);//获取交点
}
//获取摄像机光心到屏幕上一点在世界坐标系中对应点形成射线与人物移动平面的交点
public static function ScreenPointToWorldPointOnPlane (screenPoint : Vector3, plane : Plane, camera : Camera) : Vector3 {
	//创建摄像机光心到屏幕上一点在世界坐标系中形成的射线
	var ray : Ray = camera.ScreenPointToRay (screenPoint);
	
	//找出射线与平面交点
	return PlaneRayIntersection (plane, ray);
}

function HandleCursorAlignment (cursorWorldPosition : Vector3) {
	if (!cursorObject)
		return;
	
	//处理光标位置
	
	//光标对象世界坐标系位置
	cursorObject.position = cursorWorldPosition;
	
	#if !UNITY_FLASH
		//隐藏鼠标的光标而显示游戏中的光标
		Screen.showCursor = (Input.mousePosition.x < 0 || Input.mousePosition.x > Screen.width || Input.mousePosition.y < 0 || Input.mousePosition.y > Screen.height);
	#endif
	
	
	//处理光标旋转	
	var cursorWorldRotation : Quaternion = cursorObject.rotation;
	if (motor.facingDirection != Vector3.zero)
		cursorWorldRotation = Quaternion.LookRotation (motor.facingDirection);//光标旋转到人物朝向
	
	// 计算光标屏幕空间旋转
	var cursorScreenspaceDirection : Vector3 = Input.mousePosition - mainCamera.WorldToScreenPoint (transform.position + character.up * cursorPlaneHeight);
	cursorScreenspaceDirection.z = 0;
	var cursorBillboardRotation : Quaternion = mainCameraTransform.rotation * Quaternion.LookRotation (cursorScreenspaceDirection, -Vector3.forward);
	
	//设置光标旋转
	cursorObject.rotation = Quaternion.Slerp (cursorWorldRotation, cursorBillboardRotation, cursorFacingCamera);
	
	
	//处理光标缩放
	
	//光标是放在世界坐标系中的,所以透视之后它呈现在屏幕上就变小了,为了弥补这点缺陷
	//让光标缩放因子为光标到摄像机平面距离的倒数
	var compensatedScale : float = 0.1 * Vector3.Dot (cursorWorldPosition - mainCameraTransform.position, mainCameraTransform.forward);
	
	//光标靠近人物时变小
	var cursorScaleMultiplier : float = Mathf.Lerp (0.7, 1.0, Mathf.InverseLerp (0.5, 4.0, motor.facingDirection.magnitude));
	
	//设置光标局部缩放因子
	cursorObject.localScale = Vector3.one * Mathf.Lerp (compensatedScale, 1, cursorSmallerWithDistance) * cursorScaleMultiplier;
	
}
        在PlayerMoveController.js脚本中,每帧调用Update函数,该函数中通过玩家的一些鼠标移动、按键或操作杆操作等来获取玩家想让人物角色移动或旋转的量,然后设置好人物的移动方向、朝向,摄像机随之调整及光标在屏幕上的调整显示等。而在下一帧的FixedUpdate函数调用时,FreeMovementMotor.js脚本中的计算就依据设置好的movementDirection和facingDirection来调整人物的位置朝向。如果这帧中玩家没有任何输入,则下一帧中仅仅会根据人物的刚体得出的物理因素影响进行移动旋转。

        还有重要的一点是,设置了光标的位置,人物到光标方向为人物朝向,玩家可以通过改变光标位置使得人物产生相应的移动旋转;摄像机也会随着光标进行画面的调整。

你可能感兴趣的:(游戏开发)