1. 程式人生 > >Unity3D自帶案例AngryBots分析(三)——怪物啟用、攻擊、動作邏輯控制分析,第一個怪物KamikazeBuzzer的攻擊特效的實現原理

Unity3D自帶案例AngryBots分析(三)——怪物啟用、攻擊、動作邏輯控制分析,第一個怪物KamikazeBuzzer的攻擊特效的實現原理

從Hierarchy檢視中可以看見,Enemies物件下面掛有很多子物件,很多都是Prefab。而點選這些子物件,其實發現它們的很多地方有很大的相同之處,就拿SimpleBuzzers來看,裡面的怪物KamikazeBuzzer都是相同的怪物Prefab,隨便點選一個,都可以看見包含KamikazeMovementMotor.js指令碼,BuzzerKamikazeControllerAndAi.js指令碼,Health.js指令碼,和DestroyObject.js指令碼,Sphere Collider,Rigidbody,Audio Source等。以下分析就以SimpleBuzzers來進行的:

怪物的啟用:

       每個SimpleBuzzer物件點選後,可以在Inspector中看到Transform、EnemyArea.js指令碼及Box Collider。而EnemyArea.js就是用於啟用怪物的指令碼。

        在編輯器模式下,當我們點選SimpleBuzzer*時,可以看見Scene檢視中會有相應的長方體框顯示,這個框就是Box Collider的邊界,標記了盒狀碰撞器的範圍,實際上也是怪物活動範圍(或者說看守範圍)。

#pragma strict
#pragma downcast
import System.Collections.Generic;

public var affected : List.<GameObject> = new List.<GameObject> ();//主要記錄受影響的物件,即在此範圍有事件發生時會採取動作的物件的集合

ActivateAffected (false);//初始化時將所有物件標記為未啟用狀態

function OnTriggerEnter (other : Collider) {//當有其它碰撞器碰撞到該觸發器上時
	if (other.tag == "Player")
		ActivateAffected (true);//如果碰撞器是人物所帶碰撞器時,將affected集合中所有的物件啟用(即人物入侵,怪物啟用採取動作)
}

function OnTriggerExit (other : Collider) {//當有其它碰撞器離開該觸發器時
	if (other.tag == "Player")
		ActivateAffected (false);//如果碰撞器是人物所帶碰撞器時,將affected集合中所有的物件設為非啟用狀態(即人物離開防範領地,不需要警戒狀態,該幹嘛幹嘛去)
}

function ActivateAffected (state : boolean) {
	for (var go : GameObject in affected) {//將affected集合中的所有物件設定好狀態
		if (go == null)
			continue;
		go.SetActive (state);//設定狀態
		yield;
	}
	for (var tr : Transform in transform) {
		tr.gameObject.SetActive (state);
		yield;
	}
}
          當物理引擎在每固定時間幀去檢測遊戲中所有碰撞器、觸發器等是否發生碰撞,如果發生碰撞就會將相應的Trigger訊息或Collision訊息傳送給受到碰撞的兩個物體,那麼這兩個物體上掛載的指令碼會處理相應的訊息事件。就像上面指令碼中寫的,是訊息處理事件,是觸發之後我們決定採取什麼行動都寫在訊息事件處理函式中。人物入侵,所有怪物處於啟用狀態,怪物身上掛著的指令碼就會開始執行了。

怪物的攻擊:

       會根據人物的位置,設定怪物的移動目標movementTarget始終為人物所在位置;並根據與人物之間的距離判斷是否在威脅範圍內;電弧定時器到時並在威脅範圍內並追到人物則發動攻擊,對目標生命值造成傷害;施放電弧展示及音效;然後隨機重置電弧發射的定時器。

#pragma strict

public var motor : MovementMotor;      //MovementMotor物件,儲存移動方向、朝向、移動目標
public var electricArc : LineRenderer; //線渲染器,用於怪物發射電弧的繪製
public var zapSound : AudioClip;       //聲頻剪輯,用於怪物攻擊產生電弧時伴隨的聲音
public var damageAmount : float = 5.0f;//受傷害的大小

private var player : Transform;         //人物的Transform
private var character : Transform;      //怪物的Transform
private var spawnPos : Vector3;         //怪物的產生點
private var startTime : float;            //啟動時間
private var threatRange : boolean = false;//怪物是否受到威脅,即人物是否在怪物的攻擊範圍內
private var direction : Vector3;         //儲存從怪物到人物的距離向量
private var rechargeTimer : float = 1.0f;//電弧顯示定時器
private var audioSource : AudioSource;   //聲源
private var zapNoise : Vector3 = Vector3.zero;//用於設定怪物對人物傷害的小隨機變數

function Awake () {
	character = motor.transform;     //怪物Transform賦值
	player = GameObject.FindWithTag ("Player").transform;//人物Transform賦值,通過FindWithTag來獲取
	
	spawnPos = character.position;   //怪物的位置
	audioSource = GetComponent.<AudioSource> ();//聲源賦值
}

function Start () {
	startTime = Time.time; 
	motor.movementTarget = spawnPos; //怪物的移動目標為怪物產生點
	threatRange = false;//攻擊範圍沒有受到侵犯,即人物不在怪物的攻擊範圍內	
}

function Update () {	
	motor.movementTarget = player.position; //怪物的移動目標始終為人物的位置
	direction = (player.position - character.position);//從怪物到人物的距離向量
	
	threatRange = false;//未受到威脅
	if (direction.magnitude < 2.0f) {//假如怪物和人物離得太近了
		threatRange = true;//怪物受到威脅
		motor.movementTarget = Vector3.zero;//不用移動了,原地呆著
	} 
	
	rechargeTimer -= Time.deltaTime;//電弧發射定時器減去上一幀花的時間
	//假如電弧顯示定時器到時了,並且怪物受到威脅,並且怪物的forward方向(前方)與怪物和人物的距離向量之間的角度比較小
	if (rechargeTimer < 0.0f && threatRange && Vector3.Dot (character.forward, direction) > 0.8f) {
		zapNoise = Vector3 (Random.Range (-1.0f, 1.0f), 0.0f, Random.Range(-1.0f, 1.0f)) * 0.5f;//使每次人物受到傷害有些小隨機		
		var targetHealth : Health = player.GetComponent.<Health> ();//人物生命值
		if (targetHealth) {
			var playerDir : Vector3 = player.position - character.position;//怪物到人物的距離向量
			var playerDist : float = playerDir.magnitude;//怪物到人物的距離
			playerDir /= playerDist;//歸一化攻擊向量			
			targetHealth.OnDamage (damageAmount / (1.0f + zapNoise.magnitude), -playerDir);//人物受到傷害
		}		

		DoElectricArc(); //施放電弧顯示	
		
		rechargeTimer = Random.Range (1.0f, 2.0f);//隨機重置電弧發射的定時器
	}
}

function DoElectricArc () {	
	if (electricArc.enabled)
		return;
	//播放聲音
	audioSource.clip = zapSound;
	audioSource.Play ();
	
	//設定怪物電弧為enabled
	electricArc.enabled = true;
	
	zapNoise = transform.rotation * zapNoise;//使每次電到人物的位置不同
	
	//顯示電弧,並繪製紋理(繪製多條連續線段來產生電弧效果)
	var stopTime : float = Time.time + 0.2;//電弧從現在開始持續0.2s
	while (Time.time < stopTime) {//如果沒有電弧顯示結束時間
		electricArc.SetPosition (0, electricArc.transform.position);//設定電弧一個端點
		electricArc.SetPosition (1, player.position + zapNoise);//設定電弧的另一個端點
		electricArc.sharedMaterial.mainTextureOffset.x = Random.value;//共享紋理設定
		yield;
	}
	
	//隱藏電弧
	electricArc.enabled = false;
}
  • 攻擊特效主要利用DoElectricArc函式來表達攻擊方式,函式裡播放了聲音效果,而且通過LineRenderer構造多條連續線段,來製造閃電弧效果。

怪物的動作邏輯:

       怪物的動作和人物動作控制邏輯差不多,都是繼承類MovementMotor,並通過一些引數及movementTarget 來改變怪物的運動的。

#pragma strict
class KamikazeMovementMotor extends MovementMotor {
	
	public var flyingSpeed : float = 5.0;//怪物向前飛的速度
	public var zigZagness : float = 3.0f;//怪物移動影響因子
	public var zigZagSpeed : float = 2.5f;//怪物之字形移動速度
	public var oriantationMultiplier : float = 2.5f;//怪物方向旋轉影響因子
	public var backtrackIntensity : float = 0.5f;//怪物回溯強度大小
	
	private var smoothedDirection : Vector3 = Vector3.zero;//怪物轉動方向平滑因子
			
	function FixedUpdate () {
		var dir : Vector3 = movementTarget - transform.position;//移動方向設定為從自身位置到目標位置
		var zigzag : Vector3 = transform.right * (Mathf.PingPong (Time.time * zigZagSpeed, 2.0) - 1.0) * zigZagness;//怪物之字形移動速度

		dir.Normalize ();//移動方向歸一化
		
		smoothedDirection = Vector3.Slerp (smoothedDirection, dir, Time.deltaTime * 3.0f);//獲取平滑的移動方向,防止變換突兀
		var orientationSpeed = 1.0f;//旋轉速度設定
				
		var deltaVelocity : Vector3 = (smoothedDirection * flyingSpeed + zigzag) - rigidbody.velocity;//速度差值
		if (Vector3.Dot (dir, transform.forward) > 0.8f)//移動方向和怪物現在的正前方夾角比較小的情況(即怪物只需稍微移動即可)
			rigidbody.AddForce (deltaVelocity, ForceMode.Force);//對怪物身上剛體施加外力作用
		else {//否則讓怪物向相反方向移動
			rigidbody.AddForce (-deltaVelocity * backtrackIntensity, ForceMode.Force);//反速度方向的力,遊戲中可以觀察到怪物有的時候前進攻擊,有的時候旋轉,有的時候會後退伴隨旋轉	
			orientationSpeed = oriantationMultiplier;
		}
		
		//使怪物旋轉到目標方向
		var faceDir : Vector3 = smoothedDirection;
		if (faceDir == Vector3.zero) {
			rigidbody.angularVelocity = Vector3.zero;//不旋轉的時候,設定剛體轉動角速度為zero
		}
		else { 
			var rotationAngle : float = AngleAroundAxis (transform.forward, faceDir, Vector3.up);//世界座標系中,將怪物的transform中儲存的前方,旋轉到要面朝方向所需轉動的角度
			rigidbody.angularVelocity = (Vector3.up * rotationAngle * 0.2f * orientationSpeed);//設定剛體角速度讓其轉起來
		}		
	
	}
	
	//方向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);
	}	
	
	function OnCollisionEnter (collisionInfo : Collision) {//產生碰撞無動作
	}
	
}
  • rigidbody.AddForce (deltaVelocity, ForceMode.Force);中為剛體施力的函式跟人物的不一樣,人物利用加速模式,而對怪物利用的是考慮質量的持續的力,在每個FixedUpdate呼叫中持續一段時間。這種模式取決於剛體的質量,這樣的話對於推或扭轉更大質量的物體就需要更大的力。
  • 為什麼物理因素作用下的運動變換都是在FixedUpdate函式中定義的,而沒有在Update函式中定義?由於機器不同其幀速率不同,會使每秒呼叫Update函式次數也會不同,即使在同一臺機器,不同秒幀速率也會因為場景需要渲染的三角面數量不同,而被呼叫次數不同,幀的間隔時間不一定。Update函式會使用該幀與上一幀的時間間隔,FixedUpdate函式會使用固定時間間隔,這樣兩者的時間差會導致每一幀出現誤差,最後模擬出來的物理現象與理論不符合。物理引擎對剛體的各種模擬都是以FixedUpdate函式的時間間隔來計算的,使用Update函式會出錯。