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的邊界,標記了盒狀碰撞器的範圍,實際上也是怪物活動範圍(或者說看守範圍)。
當物理引擎在每固定時間幀去檢測遊戲中所有碰撞器、觸發器等是否發生碰撞,如果發生碰撞就會將相應的Trigger訊息或Collision訊息傳送給受到碰撞的兩個物體,那麼這兩個物體上掛載的指令碼會處理相應的訊息事件。就像上面指令碼中寫的,是訊息處理事件,是觸發之後我們決定採取什麼行動都寫在訊息事件處理函式中。人物入侵,所有怪物處於啟用狀態,怪物身上掛著的指令碼就會開始執行了。#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; } }
怪物的攻擊:
會根據人物的位置,設定怪物的移動目標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函式會出錯。