Unity 2-7 Stealth秘密行動
Abstract:向量運算;Animation動畫;Navigation尋路系統;Mecanim動畫系統
任務1&2&3:游戲介紹
&& 創建工程和游戲場景介紹 && 創建游戲環境
逃生游戲,過關條件為拿到鑰匙並從電梯處逃脫
被敵人/ 攝像頭/ 觸碰紅外線 -- 觸發警報
紅外線可以手動斷電
右上圖場景是美工創建好的
Import package->Custom package: StealthAssets.unitypackage
資源介紹:
Animation, Audio, Fonts, Gizmos, Materials, Models, Shaders, Textures
Animation中的.fdx文件是從3d軟件中導出的動畫模型
humanoid動畫可以應用在任意人形模型上
Audio:聲音文件
Fonts:字體
Gizmos:waypoint的圖標,用於敵人尋路的AI,用圖標表示路徑
Materials:材質
Models:模型(環境模型、物品模型等)
里面有一個文件夾Collision Meshes,存放collision的mesh,用於碰撞檢測
比如prop_cctvCam_collision: 一個椎體,用來檢測player是否進入了攝像頭視野
Shaders:更好的效果
Textures:模型的貼圖
創建游戲環境:
1. 新建文件夾Scenes,保存場景Stealth
2. 創建空物體env,用於存放有關環境的物體
3. 將Models->env_stealth_static拖入場景(env下)
Reset Transform
4. 給env_stealth_static添加碰撞器
添加Mesh Collider
在Mesh屬性中指定簡化的Models->Collision Meshes:
env_stealth_collision->env_stealth_collision_001
在scene模式下,可以看到的綠色網格就是剛才的collision
創建battleBus:
發現中間一塊有一個突起的mesh collider,而沒有物體存在
由於在env_stealth_static中沒有放入位於地圖中間的小車
因此手動添加prop_battleBus到地圖內 (作為env子物體),並旋轉到車頭朝向雜物,位置與Mesh Collider重合即可
任務4:添加環境燈光
給環境添加環境燈光:
在最外層創建空物體light (位置歸零),用於存放所有燈光
在light中創建Directional Light,顏色為暗一點的灰色,Intensity調小(0.1)
Main Camera背景顏色調為黑色,Clear Flags調為Solid color
給房間添加燈光:
在light中創建Point Light,顏色為暗橘黃色,
一個房間添加一個,在四周的牆上或過於陰暗的地方放置若干
任務5&6:添加警報燈 && 添加警報聲
實現警報燈:
添加Directional Light: AlarmLight,紅色,觸發警報的時候Intensity在0.1~0.5之間不斷變化
在AlarmLight上添加腳本AlarmController.cs
public bool isAlarmOn = false; // Flag
private float lowest/highestIntensity = 0.1/ 0.5; // 指定最高值最低值
private float targetIntensity; // 當前需要往lowest還是highest靠近
在Awake()中:
targetIntensity = highestIntensity; // 默認是從lowest變到highest的
isAlarmOn = false; // 確保剛開始警報未觸發
在Update()中實現警報燈閃爍的效果:
if(isAlarmOn) { // 警報開啟狀態
light.intensity=Mathf.Lerp(ligh.intensity, targetIntensity, Time.deltaTime*speed);
現在的警報實現了從lowestIntensity到highestIntensity的變亮過程
判斷接近 (Lerp是一個無限趨近的過程) targetIntensity后,改變target的值
if(Mathf.Abs(light.intensity - targetIntansity) < 0.05f) { // 達到目標值
if(targetIntensity == highestIntensity) {
targetIntensity = lowestIntensity;
} else { 同理; }
當警報消除的時候,需要將Intensity設為0(直接設置會顯得不自然,采用Lerp方法)
light.intensity = Mathf
但是(個人認為)這么寫需要一直執行intensity的賦值操作
為了讓外界訪問AlarmController.cs方便,采用單例模式
public static AlarmController _instance;
在Awake()中:_instance = this;
在外界直接訪問_instance即可
public class AlarmController : MonoBehaviour { public static AlarmController _instance; public bool isAlarmOn = false; private Light alarmLight; private float lowestIntensity = 0.1f; private float highestIntensity = 0.75f; private float targetIntensity; private float currentIntensity; private float intensityChangeSpeed = 5f; void Awake () { _instance = this; targetIntensity = highestIntensity; currentIntensity = lowestIntensity; isAlarmOn = false; alarmLight = GetComponent<Light>(); } void Update() { if (isAlarmOn) { if (Mathf.Abs(currentIntensity - targetIntensity) < 0.05) { if (targetIntensity.Equals(highestIntensity)) { targetIntensity = lowestIntensity; } else { targetIntensity = highestIntensity; } } currentIntensity = Mathf.Lerp(currentIntensity, targetIntensity, Time.deltaTime * intensityChangeSpeed); } else { // 警報處於關閉狀態 currentIntensity = Mathf.Lerp(currentIntensity, 0, Time.deltaTime * intensityChangeSpeed); } alarmLight.intensity = currentIntensity; } }
實現警報聲效果:
在env_stealth_static模型里會找到若干個喇叭: prop_magaphone
對這六個喇叭添加AudioSource組件,用於播放聲音 alarm_triggered
取消勾選play on awake
將Min Distance設置大一點 (5)
之后需要播放警報聲時,需要找到這六個喇叭,因此把他們設置為tag=Siren
任務7:游戲控制器GameController -- 控制整個游戲的運行
游戲控制器:
燈光、聲音、警報主角位置(比如主角位置暴露了,需要記錄看到主角的最新位置,讓機
器人朝着最新位置移動)
創建空物體GameController,添加GameController.cs腳本
需要控制警報燈:
public bool isAlarmOn = false;
在Update()中將isAlarmOn傳遞給任務6中的單例模式AlarmController
// 因為要隨時控制isAlarmOn的值
AlarmController._instance.isAlarmOn = this.isAlarmOn;
需要控制警報聲:
// 得到六個警報喇叭
private GameObject[] sirens = GameObject.FindGameObjectsWithTag("Siren");
兩個方法:分別控制警報聲的響起和停止
private void PlaySiren() {
foreach (GameObject siren in sirens) {
if(!siren.audio.isPlaying) { // 如果沒有播放
siren.audio.Play(); // 新版unity改用GetComponent AudioSource
}}}
private void StopSiren() { // 相似,但是不需判斷當前是否正在播放 }
在Update()中,控制警報聲的播放和停止
if(isAlarmOn) {
PlaySiren();
} else {
StopSiren();
}
public class GameController : MonoBehaviour { public bool isAlarmOn = false; private GameObject[] sirens; private void Awake() { isAlarmOn = false; sirens = GameObject.FindGameObjectsWithTag("Siren"); } private void Update() { AlarmController._instance.isAlarmOn = this.isAlarmOn; if (isAlarmOn) { PlaySiren(); } else { StopSiren(); }} private void PlaySiren() { foreach (GameObject siren in sirens) { if (!siren.GetComponent<AudioSource>().isPlaying) { siren.GetComponent<AudioSource>().Play(); }}} private void StopSiren() { foreach (GameObject siren in sirens) { siren.GetComponent<AudioSource>().Stop(); }} }
任務8&9:實時攝像機CCTV Camera && 攝像機的自動旋轉
任務10:攝像機的警報觸發功能
在Prefab中可以找到 prop_cctvCam
在環境中需要有三個cctv Camera,分別在
創建空物體Camera,用於放置所有cctvCamera
第一個camera放在bus的兩桶油下方
攝像機的旋轉(Joint)通過x和y的旋轉實現,設置為低頭60°
給攝像機添加燈光
在cctv_cam_body上添加Light組件,Light設為Spot(探照燈)
Light的cookie設置為texture: fx_cameraView_alp,顏色改為紅色
將cctv_collision添加進cctv_cam_body,調整位置
取消cctv_collision的renender渲染
給collision添加mesh collider碰撞器,用於碰撞檢測
將上面的cctv_Camera做成prefab
將其他兩個camera分別放置
攝像機的旋轉:
通過Animation的方式,控制joint部分的y軸旋轉
1. 新建Animator -- Window->Animation->Create CameraSweepAnimation.anim
2. Add Property: Transform->Rotation
3. 在對應時間點上添加KeyFrame,並設置Rotation.y的值(90~0~-90)
Sample為每秒的幀數
4. 因為左下角的攝像機不需要進行旋轉,因此不能apply to prefabs
在另一個需要旋轉的攝像機上添加Animator組件,並賦值,即可
攝像機的警報觸發:
思路:在左下角攝像機中完成警報觸發功能,並apply to prefabs
1. 在prop_cctvCam的collision子物體上添加腳本CctvCamCollision.cs
2. 將碰撞器設置為Trigger,因為不需要有物理碰撞效果
3. OnTriggerEnter(Collider other) {}
if(other.tag.Equals("Player")) { // 觸發警報 }
4. 觸發警報需要設置GameController.cs中的isAlarmOn
GameController設置為單例模式
public static GameController _instance;
_instance = this;
設置isAlarmOn:
GameController._instance.isAlarmOn = true;
5. 需要記錄當前警報觸發位置
在GameController.cs中
public Vector3 lastPlayerPos;
GameController._instance.lastPlayerPos = other.transform.position;
6. 使用OnTriggerStay()更好,因為當Player在其中移動的時候,會觸發位置更新
任務11:標簽的管理(代碼管理)
使用代碼進行標簽的管理(使用字符串的過程中很可能出現字符串打錯等情況)
創建腳本Tags.cs
// 注意:Tags不是作為一個組件存在的,只是存放了一些變量
public const string player = "Player";
// const -- 常量(tags不需要修改)
使用的時候:
if(other.tag == Tags.player) {}
還有其他tags:
"Siren"、"Enemy"等
任務12&13:添加激光警報裝置 && 警報的觸發和閃爍
fx_laserfence_laser:
調整大小、角度和位置(門柱上正好有孔,與laser一一對應)
1. 給laser添加Collider組件,用於碰撞檢測 -- BoxCollider,選擇Trigger
2. 給laser添加light組件,發光:PointLight、范圍變小、紅色、強度增大
3. 給laser添加Audio Source組件: Audio->laser_ham,發聲:
Spetial Blend、min/max distance、PlayOnAwake、Loop
4. 做成Prefab
5. 創建空物體lasers,放入另外五個激光警報(一共6個)即可
激光警報的觸發:
添加腳本LaserController.cs
OnTriggerStay() {
// 和攝像頭觸發警報一樣的操作
// 因此在GameController中寫成函數public void SwitchAlarmOn(Transform t)
if(tag....) GameController._instance.SwitchAlarmOn(other.transform);
Apply to prefabs.
激光警報的閃爍:
兩個激光(最長的那兩條)需要間隔閃爍,方便Player的通過
在LaserController中:
public bool isFlicker; public float onTime = 3f; public float offTime = 1.5f; private float timer = 0; private void Update() { if(isFlicker) { if(this.gameObject.GetComponent<Renderer>().enabled) { // 當前亮着 timer += Time.deltaTime; if(timer >= onTime) { this.gameObject.GetComponent<Renderer>().enabled = false; timer = 0; } } else { // 當前暗着 timer += Time.deltaTime; if(timer >= offTime) { this.gameObject.GetComponent<Renderer>().enabled = true; timer = 0; }}}}
(Collider也需要禁用和啟用) --
gameObject.GetComponent<BoxCollider>().enabled;
在Inspector中將兩個需要閃爍的激光的isFlicker屬性設置為true
兩個激光閃爍不同步,一個為(1.6, 2.8), 一個為(2.1, 2.5)(隨意)
任務14&15:主角和主角的動畫
添加主角:
Models->char_ethan
給Player添加碰撞器Capsule Collider
給Player添加剛體Rigidbody
因為主角不需要通過rigidbody進行移動(使用動畫控制),所以勾選IsKinematic
(在任務16中會發現這是一個bug)
主角在(x, z)平面上移動,且只圍繞y軸旋轉,所以Freeze Position: y; Rotation: x, z
主角移動的動畫:
在Humanoid中的動畫是人形動畫,可以使用
新建一個Animator Controller,名為PlayerController
打開Animator編輯器(選中PlayerController,Window->Animator)
1. 主角的狀態:
walk/ run (walk和run是同一個動作,只不過速度不同 -- 相同動畫)
sneaker
idle
death
2. 添加parameter
float: speed -- 移動速度
bool: sneaker -- 是否處於sneaker狀態(按下shift鍵)
3. 默認狀態:idle
將humanoid_idel->Idle動畫拖入狀態機(黃色為默認動畫)
4. 右鍵Create State -> From Blend Tree
(上面提到walk和run是同一個動作,用BlendTree實現)
命名為Locomotion (移動、運動)
雙擊進入編輯模式
Add Motion Field,添加Walk和Run動畫
在Parameter中選擇speed,用speed來控制walk和run的融合
uncheck Automate Thresholds (自動生成參數)
取消勾選后可以手動確定:參數是根據哪個值來確定的,這里選擇speed
這里表示1.555在Walk,speed增大漸漸進入run的狀態,5.667為run狀態
5. Idle->Locomotion
make transition: Conditions: speed > 0.1
Locomotion->Idle: speed < 0.1
6. Sneak: humanoid_sneak: sneak動畫拖入狀態機
Idle->Sneak:
make transition: Conditions: speed > 0.1 && sneak == true;
Sneak->Idle: speed < 0.1
7. Locomotion->Sneak:
speed > 0.1 && sneak == true;
Sneak->Locomotion:
speed > 0.1 && sneak == false;
由於player的移動是通過動畫實現的
因此如果覺得移動過慢/快,可以修改對應狀態的speed
8. Dying: humanoid_dying->Dying
添加parameter: bool dead
因為任何狀態下都會死亡 --
AnyState->Dying: make transition: dead = true;
任務16&17:控制主角的運動和移動
運行游戲,手動調整主角的速度,會發現主角出現了移動,主角的transform也發生變化
由於Player->Animator勾選了屬性Apply Root Motion,表示動畫會影響transform的值
思路:按下上下左右鍵時,設置speed即可
根據按鍵控制朝向
在char_ethan添加腳本PlayerController.cs
在Update()中實現:
// 得到按鍵的信息
float h = Input.GetAxis("Horizontal");
float v = Input.GetAxis("Vertical");
// 需要得到Animator來改變speed的值
private Animator anim = GetComponent...
(我的思路)
anim.SetFloat("Speed", (Mathf.Max(h,v) * 5.56f));
-- 結果為不適合
1. 數值變化太快,walk->run過於迅速
2. 而且延遲較大,Input.GetAxis()的值需要時間來歸零和增大
(正確思路 -- 利用差值)
if(Mathf.Max(Mathf.Abs(h), Mathf.Abs(v)) > 0.1f) {
// 開始移動
newSpeed = Mathf.Lerp(anim.GetFloat("speed"), maxSpeed, delta...)
} else { // 停止移動
// 相同思路,利用差值,但是將speedRate增大,因為需要很快停止移動
// 試驗過后發現還是不好控制,於是直接將speed設為0
// 但是從跑步到Idle的動畫很不平滑 -- 自行選擇
newSpeed = 0;
}
anim.SetFloat("speed", newSpeed);
出現bug -- Player會進行穿牆,就好像Collider沒有任何作用一樣
原因:任務14中給Player添加Rigidbody時勾選了IsKinemetic選項,取消勾選即可
控制Player的移動方向:
在需要移動的if條件中:
// 取得目標方向
Vector3 targetDir = new Vector3(h, 0, v);
// 取得當前方向
Vector3 currDir = transform.forward;
// 取得兩個方向的夾角
float angle = Vector3.Angle(targetDir, currDir);
// 使用勻速旋轉的方法
transform.Rotate(Vector3.up * angle * Time.deltaTime * rotateSpeed);
// rotateSpeed可設為5
到此為止player的移動功能實現了,但是
出現Bug --
1. 角色在停止控制后有的時候會繼續不聽旋轉
(個人推測是慣性?)
將角色的Rotation: y Freeze即可
2. 拐彎的時候會出現有的時候往大彎繞
比如:角色朝左,向下控制,角色會通過上右下而向下移動
angle的值也是從90變大到180再變小
細心觀察會發現,角色永遠是順時針旋轉
原因:Vector3.Angle()返回值沒有正負,小於180°
Rotate(...)方向為Vector3.up,因此永遠是順時針旋轉
而旋轉一些以后,angle值變大,需要繼續旋轉,直到angle值為0
解決方法 -- 想不出如何判斷使用Vector3.up還是Vector3.down
舍棄該方法
使用Quaternion和Slerp來解決
Quaternion targetQuaternion = Quaternion.LookRotation(targerDir);
// 四元數返回的是一個帶有方向的角度 (該函數默認以Vector3.up為上方)
transform.rotation = Quaternion.Slerp(transform.rotation,
targetQuaternion, Time.deltaTime * rotationSpeedRate);
private Animator animator; private float increaseSpeedRate = 2.5f; private float decreaseSpeedRate = 60f; private float rotateSpeedRate = 2; private float newSpeed; private const float maxSpeed = 5.66f; private void Awake() { animator = GetComponent<Animator>(); } void Update() { float h = Input.GetAxis("Horizontal"); float v = Input.GetAxis("Vertical"); if (Mathf.Max(Mathf.Abs(h), Mathf.Abs(v)) > 0.1f) { // 開始移動 newSpeed = Mathf.Lerp(animator.GetFloat("speed"), maxSpeed, Time.deltaTime * increaseSpeedRate); // 開始旋轉 // 取得目標方向 Vector3 targetDir = new Vector3(h, 0, v); Quaternion targetDirQuaternion = Quaternion.LookRotation(targetDir); transform.rotation = Quaternion.Slerp(transform.rotation, targetDirQuaternion, Time.deltaTime * rotateSpeedRate); } else { // 停止移動 newSpeed = Mathf.Lerp(animator.GetFloat("speed"), 0, Time.deltaTime * decreaseSpeedRate); // newSpeed = 0; } animator.SetFloat("speed", newSpeed); }
實現sneak緩慢行走:
if(Input.GetKeyDown(KeyCode.LeftShift)) {
anim.SetBool("sneak", true);
}
if(Input.GetKeyUp(KeyCode.LeftShift)) {
anim.SetBool("sneak", false);
}
會發現,從Idle時按住shift后再按方向鍵,剛開始一小小段時間會進行walk而不是sneak
解決方法 --
將動畫狀態機中Idle->Locomotion的condition加上sneaker=false;即可
此時sneak的速度有點慢
控制sneak的動畫播放速度即可
選中動畫狀態機中的sneak,將inspector中的speed加大即可
任務18:主角行走聲音和游戲背景音樂
添加背景音樂:
在GameController中添加AudioSource: music_normal,PlayOnAwake,Loop,2D
添加行走聲音:
在主角上添加AudioSource: player_footsteps
在PlayerController.cs中添加兩個方法
void PlayFootstepsAudio() {
if(!audio.isPlaying) {
audio.Play();
}}
void StopFootstepsAudio() {
audio.Stop();
}
在Locomotion的狀態下需要播放聲音,其他狀態不要
if(anim.GetCurrentAnimatorStateInfo(0).IsName("Locomotion")) {
// play audio
} else {
// stop audio
}
切換背景音樂:
當警報被觸發的時候,背景音樂會從music_normal切換至music_panic
思路:如果直接暫停播放會顯得比較突兀,因此使用對兩個Volume進行差值運算的方法
在GameController上添加另一個AudioSource: music_panic, Loop
在GameController.cs中:
定義兩個AudioSource來存放兩個聲音
在Update()中修改兩個AudioSource的Volume
在原有的isAlarmOn判斷下
musicNormal.volume=Mathf.Lerp(musicNormal.volume, 0, Time.deltaTime);
musicPanic.volume=Mathf.Lerp(musicPanic.volume, 1, Time.deltaTime);
!isAlarmOn時也類似
也可以添加一個musicFadeSpeed來控制漸變速度
Panic的背景聲音有點大,因此改為0.5f
任務19:添加自動門
自動門機制:當主角靠近門的時候會門會自動打開: door_generic_slide
門的模型都自帶兩個動畫 open和close
創建空物體,用於存放所有門
使用Animator狀態機進行門的控制
新建Animator,名為normalDoorAnimationController
將兩個動畫拖入狀態機 -- 默認為close狀態,設為default
添加Parameters: bool closing; // 當closing為true時,進行關閉,為false時進行開啟
closing初始值為true
添加狀態close和open之間的transition
給door_generic_slide->door_generic_slide_panel 添加BoxCollider來檢測碰撞
給door_generic_slide添加sphere collider,用於trigger開門動畫
在door上添加DoorController.cs腳本 -- 代碼控制門的開關
因為是控制門的開關,因此如果是Enemy或Player在觸發區內,保持開門狀態
OnTriggerStay(Collider other) {
if(other.tag == Tags.Player || ... == Tags.Enemy) {
anim.SetBool("closing", false);
}}
// 那如何關門呢?OnTriggerExit()? 不行,如果區域內有兩個人的話,怎么解決?
// 使用count來計數 -- 就不能用OnTriggerStay了
OnTriggerEnter(...) {
if(other.tag ... || ... ) {
count++;
}}
OnTriggerExit(...) { // 相同情況 count--; }
在Update中用count來判斷是否開關門
doorAnimator.SetBool("closing", (count<=0));
添加開關門的聲音:打開和關閉時播放聲音
在門上添加AudioSource: door_open
在Update中更改doorAnimator.closing時
// 播放聲音
if(anim.IsInTransition(0) {
// 如果在0 layer中正在進行某個Transition
audioDoorOpen.Play();
將門做成Prefab,並創建其他兩扇門
任務20:添加電梯門 -- 設置內側外側打開和關閉動畫
電梯的自動門是兩扇,往兩邊打開
電梯門分為外側和內側兩扇門(內側門即電梯,跟隨上下移動的)
添加外側門:door_exit_outer
相同的,分別給door_exit_outer_left和right添加上BoxCollider
給door_exit_outer添加上SphereCollider作為Trigger
AudioSource為door_open
外側門的控制代碼使用上一任務的DoorController.cs即可
Animator使用上一任務的NormalDoorController即可
-- 復制一個AnimatorController,並將狀態中的動畫(Motion)換成對應的動畫
door_exit_outer_close和door_exit_outer_open
添加內側門:prop_lift_exit
注意電梯門的方向 prop_lift_exit->door_exit_inner
在Lift上添加腳本LiftController.cs
// 讓inner door的z軸坐標跟隨outer door的x軸坐標變化即可
// 首先得到是個transform: inner_door和outer_door的
innerDoorLeft.position = new Vector3(outerDoorLeft.position.x,
innerDoorLeft.position.y, innerDoorLeft.position.z);
innerDoorRight.position = new Vector3(outerDoorRight.position.x,
innerDoorRight.position.y, innerDoorRight.position.z);
內側門的開關實現了,但是速度會比較快,因此采用另一種方法 -- Lerp
newInnerDoorLeftX = Mathf.Lerp(innerDoorLeft.position.x,
outerDoorLeft.position.x, Time.deltaTime);
再給內側門左右加上BoxCollider
任務21:對電梯門添加鑰匙控制
因為兩種門共用一個腳本DoorController.cs
使用一個bool keyRequired = false; 來判斷該門是否需要鑰匙🔑打開
在PlayerController.cs中使用bool hasKey=false; 來記錄當前是否擁有鑰匙
修改DoorController.cs中OnTriggerEnter部分:
如果該門需要key,且collider為Player,且身上有key,才進行count++
-- enemy在需要key的門前不會使其打開
if(keyRequired) {
if(other.tag == Tags.player) {
if(other.gameObject.getcomponent<...script...>().GetHasKey()) {
count++;
}}} else { // normal door }
對於OnTriggerExit() 也一樣需要相同的判斷,否則會平白無故count--;
-- 小小不算bug的bug -- 在Trigger內部修改hasKey,就再也不能開門了
邏輯,修改hasKey后出去, count--, count=-1, 再進去, count++, count=0;
將電梯外門的keyRequired勾選上即可
當沒有鑰匙的人接近了門的時候,需要播放AudioSource: door_accessDenied
判斷是否Player有鑰匙后
else {
GetComponents<AudioSource>()[1].Play();
任務22:主角拾起鑰匙的功能
Models->prop_key_card
放置在這個位置
給Key添加Light,藍色,intensity調大
創建Animator: KeyCardController
將key_card中自帶的默認動畫spin拖入狀態機即可 -- key的旋轉動畫
給Key添加Sphere Collider,用來作為Trigger,檢測主角是否到達拾起鑰匙的區域
給Key添加腳本KeycardController.cs
OnTriggerEnter(...) {
// Player進入觸發區時,拾起鑰匙
PlayerController player = other.GetComponent<PlayerController>();
player.hasKey = true;
// 播放得到鑰匙的聲音
-- 給Key添加一個AudioSource: keycard_pickUp
通過audioSource.Play() 能行嗎?不行,因為this已經被Destroy了
解決方法:AudioClip --
public AudioClip audio;
// 在transform.position處播放audioClip
AudioSource.PlayClipAtPoint(audio, transform.position);
// 銷毀鑰匙
Destroy(this.gameObject);
}
任務23:激光的開關控制
游戲機制:一個電閘開關控制一個激光,Player在電閘旁邊按下z來關閉對應激光
創建空物體switchUnits,用來存放所有(4個)開關
關閉開關后,激光會消失,開關上面鎖的顯示會改變(改變材質即可)
給SwitchUnit添加BoxCollider防止Player走進去
添加BoxCollider作為Trigger -- Sphere不合適,在牆外也能觸發
添加AudioSource: switch_deactivation -- 解鎖聲音
添加腳本SwitchUnitController.cs控制開關邏輯
OnTriggerStay() { -- 需要在觸發區域內
if(other.tag == Tags.player) { -- 如果是Player進入
if(Input.GetKeyDown(KeyCode.z)) { -- 如果按下了z鍵
// 關閉對應激光 -- public GameObject laser;
laser.SetActive(false);
// 播放聲音
audio.Play();
// 修改解鎖標志 -- 替換material -- public Material unlockedMaterial;
transform.Find("prop_switchUnit_screen").
GetComponent<MeshRenderer>().material = unlockedMaterial;
}}}
-- 小Bug:在觸發區域內多次按下z鍵,會進行多次的音效播放
解決方法(個人):
定義一個bool isSwitchValid = true
在OnTriggerStay最外層用if(isSwitchValid) 判斷即可
將其做成Prefab,添加其他四個SwitchUnit
private void OnTriggerStay(Collider other) { // 需要在觸發區域內 if (isSwitchValid) { if (other.tag == Tags.player) { // 如果是Player進入 if (Input.GetKeyDown(KeyCode.Z)) { // 按下z鍵 // 關閉對應激光 -- public GameObject laser; laser.SetActive(false); // 播放聲音 unlockedAudio.Play(); // 修改解鎖標志(替換material) - public Material unlockedMaterial; transform.Find("prop_switchUnit_screen"). GetComponent<MeshRenderer>().material = unlockedMaterial; isSwitchValid = false; }}}}
任務24&25&26:攝像機的跟隨移動 && 攝像機的視野問題 && 攝像機的緩動
按F聚焦Player,在Scene視圖下調整好合適的視角
選中MainCamera,GameObject->Align With View -- 將Camera設置為當前視角
攝像機的跟隨移動:
會利用很多Lerp差值運算
在攝像機上添加腳本 FollowPlayer
// 得到相機和Player之間位置的offset
private Transform player = GameObject.FindWithTag(Tags.player).transform;
private Vector3 offset = tranform.position - player.position;
offset = new Vector3(0, offset.y, offset.z); // 保持x方向值相等
// 在Update中更新Camera位置
transform.position = player.position + offset;
出現問題:若主角在下側牆的上方,則視野會被牆擋住
--改進相機的視野實現
攝像機的視野智能化調整:
解決思路:從Camera發射射線到Player,如果射線沒有接觸到Player,則旋轉
如何旋轉?得到Player正上方的距離為offset的點,在該點和Camera之間等距划分
三點,依次判斷這三點是否符合要求,若符合,調整Camera位置和方向
代碼實現:
// 起始點
Vector3 beginPos = player.position + offset;
// 終點 -- Player正上方點
Vector3 endPos = player.position + offset.magnitude * Vector3.up;
// 得到兩點之間的四等分點 (3個點) -- 用向量運算方法或差值方法均可
Vector3 pos1 = Vector3.Lerp(beginPos, endPos, 0.25f);
其余的為pos2 = ... 0.5f; pos3 = ... 0.75f;
存放在數組中:
Vector3[] posArray = new Vector3[5];
// 判斷在哪個點可以看到Player -- 用射線的方法
foreach(Vector3 pos in posArray) {
// 遍歷所有可能位置
RaycastHit hitInfo;
if(Physics.Raycast(pos, player.position - pos, out hitInfo) {
// 如果有碰撞的話
if(hitInfo.collider.tag == Tags.player) {
// 如果在視野內能看到Player -- 移動攝像機
transform.position = pos;
// 改變攝像機的朝向
transform.LookAt(player.position);
break;
}}}
現在,攝像機的視角問題解決了,但是攝像機需要進行緩動,不然會顯得卡頓 -- Lerp
攝像機的緩動:
將上面的transfrom.position = pos; 改為
transform.position = Vector3.Lerp(transform.position, pos, Time.deltaTime);
將上面的transform.LookAt(player); 改為
Quaternion currQuaternion = transform.rotation;
transform.LookAt(player);
transform.rotation = Quaternion.Slerp(currQuaternion, transform.rotation,
Time.deltaTime);
private Transform player; private Vector3 offset; private float cameraMovingSpeed = 2; private float cameraRotateSpeed = 10; private void Start() { player = GameObject.FindWithTag(Tags.player).transform; // 保持在x軸方向值相同 offset = transform.position - player.position; } private void Update() { // 得到起始點和終點 Vector3 beginPos = player.position + offset; Vector3 endPos = player.position + offset.magnitude * Vector3.up; // 得到四等分點 Vector3[] posArray = new Vector3[5]; float lerp = 0.25f; float currLerp = 0; for (int i = 0; i < posArray.Length; i++) { posArray[i] = Vector3.Lerp(beginPos, endPos, currLerp); currLerp += lerp; } // 判斷哪個點是適合的 foreach (Vector3 pos in posArray) { RaycastHit hitInfo; if(Physics.Raycast(pos, player.position - pos, out hitInfo)) Debug.DrawRay(pos, player.position - pos); print(hitInfo.collider); if(hitInfo.collider.tag == Tags.player) { // 在視野內能看到Player transform.position = Vector3.Lerp(transform.position, pos, Time.deltaTime * cameraMovingSpeed); // 改變攝像機的朝向 -- 使用Quaternion的差值進行轉向 Quaternion currQuaternion = transform.rotation; transform.LookAt(player); transform.rotation = Quaternion.Slerp(currQuaternion, transform.rotation, Time.deltaTime * cameraRotateSpeed); break; }}}}
后期運行發現bug -- 當Player走到cctv_Cam下的collider的時候,也會導致camera視角變化
將hitInfo.collider.tag == Tags.player修改為
hitInfo.colllider.tag == Tags.player || hitInfo.collider.tag == Tags.cctvCollider
任務27:Navigation自動尋路導航網格
機器人的自動導航系統 --
把env_stealth_static和battle_bus設置為static (Navigation bake的目標)
Window->Navigation->Bake->Bake
檢查一下自動生成的區域是否正確 -- 發現網布下方的通路沒有打開
減小Agent Radius (增大區域的面積,離突起物更近的地方也可被視作可用區域)
減小Agent Height
Slope=0;因為在該場景下,是平面的,無需設置坡度
StepHeight=0;高度差小於這個值的,視為通路
任務28:添加機器人
Enemy: char_robotGuard -- AI實現、動畫實現、射擊實現等等
1. 防止碰撞牆體等
添加碰撞器 Capsule Collider
添加Rigidbody
2. 實現導航功能 -- 判斷機器人當前應該去的方向
添加Nav Mesh Agent組件
Radius 自身寬度
Speed 移動速度
Acceleration 加速度
AngularSpeed 旋轉速度
等等
注:NavMeshAgent是可以控制運動的(直接設置position)
但是由於動畫也會控制運動,故不使用
3. 實現發現Player功能 -- 即機器人視野和聽覺
添加Sphere Collider作為Trigger
當Player在Trigger之內,會進行機器人視野的檢測
將機器人做成Prefab
任務29:設置機器人的狀態機(動畫效果)
創建Animator,名為EnemyController
BaseLayer -- Idle, Locomotion,
將動畫humanoid_idle拖入狀態機,set as default
創建BlendTree,名為Locomotion,表示機器人的運動
機器人運動動畫:
Walk/Run, Walk/RunLeft/RightShort/Medium/Wide
TurnOnSpotLeft/RightA/B/C/D
將這些動畫都添加如BlendTree的motion中
參數為speed和rotation,因此為2d的BlendTree
將BlendType改為2D Freeform Cartesian
Compute Positions = Speed and AngularSpeed,數值會自動生成
可以看出來,每個點代表一個動畫狀態,
PosX表示轉向速度,PosY表示行走速度
比如豎直中間兩個點上面的為Run,下面的為Walk
需要兩個Float Parameter變量分別對應AngularSpeed: PosX和Speed: PosY
發現Idle其實應該在Locomotion中,而不是單獨存在
刪除Idle狀態,將Idle動畫添加到Locomotion中
射擊動畫:
weapon_lower/weapon_raise/weapon_shoot
增加一個Layer,稱為Shoot
添加一個bool Parameter: playerInSight,用來判斷是否處於射擊狀態
添加一個Empty狀態作為默認狀態 default
transition: default -> weapon_raise : playerInSight = true
transition: weapon_raise -> weapon_shooting : 沒寫condition時表示接着播放
這里也可以寫上playerInSight=true作為條件,表示raise后如果還是true才射擊
如果為false了就不進行射擊了
transition: weapon_raise -> weapon_lower : playerInSight = false;
transition: weapon_shoot -> weapon_lower : playerInSight = false;
如果playerInSight依然為true,則會進行循環播放
transition: weapon_lower -> default : no condition
因為射擊動畫只控制了手的動作而沒有控制身體
因此需要創建一個身體遮罩
在Project中create->Avatar Mask,命名為EnemyShootMask
在Inspector中選擇Humanoid,將全身禁止只留下雙手和頭部
點擊狀態機的Shoot Layer旁邊的設置按鈕,將EnemyShootMask賦值
因為這一Layer權重比上一層高,所以Weight = 1,並選用Override
任務30:機器人的聽覺和視覺
聽覺:Sphere Collider
視覺:前方錐形范圍 110°
添加腳本EnemySight.cs
private bool isPlayerInSight = false;
private float viewFieldAngle = 110;
視覺:
當主角在SphereCollider范圍內 -- 在OnTriggerStay() 中
if(other.tag = player) {
// 得到兩個方向:enemy前方和enemy朝向player的方向
Vector3 forward = transform.forward;
Vector3 playerDir = other.transform.position - transform.position;
float tempAngle = Vector3.Angle(forward, playerDir);
// 判斷是否在錐形視野范圍之內
if(tempAngle <= viewFieldAngle * 0.5) {
playerInSight = true;
} else { playerInSight = false; }
}
當主角不在SphereCollider范圍內(不在聽覺范圍內) -- OnTriggerExit()
if(other.tag == player) {
playerInSight = false; // 出了這個范圍肯定是看不到了
}
聽覺:機制 -- 兩點之間可通路的最短距離(Navigation),即牆等障礙物是完全隔音的
聽到聲音后,會設置警報位置,讓Enemy去追蹤
存儲當前警報位置 -- 被發現的Player位置
public Vector3 alertPosition = Vector3.zero; // 需要被外界使用,默認為zero
在上面Player被看到時,playerInSight=true;后加上 alertPos=other.transform.position;
同理,playerInSight=false;后加上 alertPos=Vector3.zero; // 歸零
在TriggerStay中(因為Trigger就是表示可以聽到聲音的范圍)
// 判斷player是否處於Locomotion狀態
if(playerAnim.GetCurrentAnimatorStateInfo(0).IsName("Locomotion")) {
// 使用NavMeshAgent判斷最短路徑距離
// private NavMeshAgent navAgent = GetComponent<NavMeshAgent>();
// 因為navAgent.CalculatePath(targetPos, navMeshPah), 需要一個空Path傳入
-- calculate a path to a specified point and stored the resulting path
return a bool if there is a path
NavMeshPath navPath = new NavMeshPath();
// navPath.corners,為一個存放所有路徑拐點的Vector3數組
if(navAgent.CalculatePath(other.transform.position, navPath)) {
Vector3[] waypoints = new Vector3[navPath.cornors.Length+2];
// 給所有路徑點賦值
waypoints[0] = transform.position;
waypoints[waypoints.Length-1] = other.transform.position;
for(int i = 1; i < waypoints.Length - 1; i++) {
waypoints[i] = navPath.corners[i-1];
}
// 計算總長度
float totalDistance = 0;
for(int i = 1; i < waypoints.Length; i++) {
// 從第二個點開始,計算與前一個點之間的距離
totalDistance += (waypoint[i-1] - waypoint[i]).magnitude;
}
// 總長度與最大長度進行比較,判斷是否在最大距離之內
private SphereCollider sphereCollider = GetComponent<...>();
if(totalDistance <= sphereCollider.radius) { // 可以聽到
alertPosition = other.transform.position;
private void OnTriggerStay(Collider other) { if(other.tag == Tags.player) { // Player進入聽覺視覺觸發區域 // 得到兩個方向:enemy前方和enemy朝向player的方向 Vector3 forward = transform.forward; Vector3 playerDir = other.transform.position - transform.position; float tempAngle = Vector3.Angle(forward, playerDir); // 判斷是否在錐形視野范圍之內 if(tempAngle <= viewFieldAngle * 0.5f) { playerInSight = true; // 更新警報位置 alertPosition = other.transform.position; } else { playerInSight = false; alertPosition = Vector3.zero; } // 聽覺實現 -- 通路距離在給定范圍之內,則進行追蹤 if (playerAnim.GetCurrentAnimatorStateInfo(0).IsName("Locomotion")) { // 若當前Player狀態為Locomotion -- 有聲音發出 NavMeshPath navPath = new NavMeshPath(); if (navAgent.CalculatePath(other.transform.position, navPath)) { Vector3[] waypoints = new Vector3[navPath.corners.Length + 2]; waypoints[0] = transform.position; waypoints[waypoints.Length - 1] = other.transform.position; for (int i = 1; i < waypoints.Length - 1; i++) { waypoints[i] = navPath.corners[i - 1]; } // 計算路徑總長度 float totalDistance = 0; for (int i = 1; i < waypoints.Length; i++) { totalDistance += (waypoints[i-1]-waypoints[i]).magnitude; } // 判斷是否在最大距離內 -- 最大距離即為SphereCollider的半徑 if (totalDistance <= sphereCollider.radius) { alertPosition = other.transform.position; }}}}} private void OnTriggerExit(Collider other) { if(other.tag == Tags.player) { playerInSight = false; alertPosition = Vector3.zero; }}
運行,聽覺和視覺都正常運行
Bug1: 但是,因為SphereCollider的關系,Camera.FollowPlayer()中需要加上另一個條件
hitInfo.collider.tag == Enemy
Bug2: 機器人移動到門附近的時候,因為SphereCollider范圍很大,會觸發門的開關
而只有當CapsuleCollider進入的時候才會觸發門的開關
改為:if(tag... && other.GetType().ToString()=="UnityEngine.CapsuleColiider")
或: if (other.tag == Tags.enemy && other is CapsuleCollider) {
或: if (other.tag == Tags.enemy && other.isTrigger == false) {
即可
任務31:機器人發起警報的功能
警報機制:當Enemy看到Player時,通知其他機器人進行追蹤
看到或聽到都會更新警報發出位置
記得之前在GameController中有一個警報方法SwitchAlarmOn將isAlarmOn設置為true
那么,在EnemySight.cs中,
當isPlayerInSight為true時,加上一句:
GameController._instance.SwitchAlarmOn(other.transform); // 將player位置傳遞
實現了Enmey看到Player時,開啟警報
還需實現追蹤功能
機制:alertPos不等於Vector3.zero時,就進行追蹤
而且,GameController中的lastPlayerPos修改時 (其他警報器),也要進行追蹤
當lastPlayerPos修改時,更新
// (Awake()中)用來存儲之前的lastPlayerPos;
private Vector3 preLastPlayerPos = GameController._instance.lastPlayerPos;
// 進行檢測 -- Update()中
if(preLastPlayerPos != GameController._instance.lastPlayerPos) {
alertPosition = GameController._instance.lastPlayerPos;
preLastPlayerPos = alertPosition;
}
報錯: preLastPlayerPosition = GameController._instance.lastPlayerPos;
原因:GameController.cs中_instance = this的賦值在Awake()中
而preLastPlayerPos這句話的賦值也是在Awake()中且用到了_instance
因此可能會出現空指針的錯誤
解決方法:將preLastPlayerPos的賦值放在Start()中
現在可以通過cctvCam和laser觸發警報並修改alertPosition的值,
Bug -- 在cctvCam中移動不會更新lastPlayerPos:
cctvCamCollision.cs中的觸發條件改為OnTriggerStay() 而不是OnTriggerEnter()
Bug -- 在SphereCollider之內且playerInSight=false時,alertPosition會被強制設為zero
因為在兩個else情況下,把alertPosition設置為了Vector3.zero
解決方法:去掉OnTriggerStay中的設置即可,OnTriggerExit需要設置為zero
任務32&33:使用Navigation控制機器人的巡邏 && 行走的動畫
創建空物體waypoints,用來存儲巡邏的路徑點
在里面創建若干個空物體waypoint -- 用來表示路徑的下一個臨時目標位置
分別在Scene中設置position
但是空物體在環境中是沒有圖標表示的,因此不好觀察
在Inspector中物體名的左邊可以選擇icon->other->waypoint
注意:使用NavMeshAgent進行控制,是直接設置transform的
創建腳本EnemyMoveAI.cs
// 定義數組,存儲waypoints的位置
public Transform[] waypoints = new Transform[4];
private int index = 0; // 當前目標索引
巡邏的方法:
private void Patrolling() {
-- 機制:巡邏到達一個waypoint后,需要休息一段時間,再去下一個waypoint
-- private float patrolTimer = 0;
-- private float patrolWaitingTime = 3;
// 需要使用NavMeshAgent進行路徑導航
-- private NavMeshAgent navAgent = GetComponent<...>();
-- Awake()中,設navAgent.destination = waypoints[index].position; //index=0
// 檢測有沒到達目標位置
if(navAgent.remainingDistance < 0.5f) {
// 休息
navAgent.isStoped = true;
patrolTimer += Time.deltaTime;
if(patrolTimer >= patrolWaitingTime) {
// 休息完畢,下一個點
// 一共四個點,循環播放,index不能超過三
index++;
index %= 4;
navAgent.destination = waypoints[index].position;
navAgent.isStoped = false; // 開始進行下一個點的尋路
patrolTimer = 0;
}}
現在,機器人可以進行四個點的巡邏的,但是,沒有行走動畫的播放,僅僅是位置的移動
NavMeshAgent提供的尋路為直接修改transform的,我們利用它的指向功能
結合動畫,來實現動畫版的巡邏
將NavMeshAgent的位置控制取消
navAgent.updatePosition = false;
navAgent.updateRotation = false;
新建腳本EnemyAnimationController.cs -- 通過控制狀態機來控制行動
private NavMeshAgent navAgent = GetComponent<...>();
在Update()里
navMesh.desiredVelocity // 期望速度 -- 當前移動的速度和方向
思路:navMesh的期望速度設置給用來控制動畫狀態機的Speed和AngularSpeed
當休息時(即navAgent.isStopped = true)
if(navAgent.desiredVelocity = Vector3.zero) {
enemyAnim.SetFloat("speed", 0);
enemyAnim.SetFloat("angularSpeed", 0);
// 注意 重載 SetFloat(name, value, dampTime, deltaTime)
dampTime: 阻尼時間,在該時間內,將name逐漸設置為value,漸變過程
// 直接設置,動畫有時候會顯得突兀,這里先不用,看看效果不好再用
} else { -- 行走狀態
// 計算角度AngularSpeed
float angle = Vector3.Angle(transform.forward, navAgent.desiredVelocity);
// anim中的AngularSpeed為弧度值
float angleRad = Mathf.Deg2Rad * angle; // Dot = 2PI/360
// 算出弧度的方向(正負) -- 根據動畫來說,右轉為正
-- Vector3.Cross(a,b)
方向:應用左手定則,result向量(中指)分別垂直於a(拇指)和b(食指)
大小:為a和b大小的乘積
思路:通過Cross的方向即可得angleRad的正負:向上為正,向下為負
Vector crossResult = Vector3.Cross(...forward, ...desiredVelocity);
if(crossResult.y < 0) { angleRad = -angleRad; }
anim.SetFloat("angularSpeed", angularSpeed);
思路:當角度大於九十度時,沒必要往前走了,便設定為原地旋轉,
當角度小於九十度時,邊走邊旋轉
if(angle >= 90) {
anim.SetFloat("speed", 0);
} else {
// 計算當前速度
// ab為當前朝向,ac為desiredVelocity
// 當前速度設為ac在ab方向上的分速度ad即可
慢慢加快速度,動畫會平滑很多
-- Vector3.Project(vector, onNormal) :
返回vector在onNormal方向上的分向量projection
Vector3 projection = Vector3.Project(...desiredVelocity, ...forward);
anim.SetFloat("speed", projection.magnitude);
}
Bug -- NavMeshAgent的位置與機器人的位置分離了
Siki上沒有出現這個情況
同樣的問題:http://tieba.baidu.com/p/5313022190?traceid=
解決方法:在Update() 最后將NavMeshAgent的位置設置為transform的位置即可
navAgent.nextPosition = transform.position;
public class EnemyAnimationController : MonoBehaviour { public float speedDampTime = 0.3f; public float angularSpeedDampTime = 0.3f; private Animator enemyAnim; private NavMeshAgent navAgent; void Awake () { enemyAnim = GetComponent<Animator>(); navAgent = GetComponent<NavMeshAgent>(); } void Update () { // 如果navAgent.isStopped = true -- 休息狀態 if(navAgent.desiredVelocity == Vector3.zero) { enemyAnim.SetFloat("speed", 0, speedDampTime, Time.deltaTime); enemyAnim.SetFloat("angularSpeed", 0, angularSpeedDampTime, Time.deltaTime); } else { // 行走狀態 // 旋轉角度大小 float angle = Vector3.Angle(transform.forward, navAgent.desiredVelocity); float angleRad = angle * Mathf.Deg2Rad; // 旋轉角度方向 Vector3 crossResult = Vector3.Cross(transform.forward, navAgent.desiredVelocity); if(crossResult.y < 0) { // 左轉 angleRad = -angleRad; } enemyAnim.SetFloat("angularSpeed", angleRad, angularSpeedDampTime, Time.deltaTime); // 兩種情況,角度大於90時,原地旋轉;小於90時,邊走便旋轉 if(angle >= 90) { // 原地旋轉 enemyAnim.SetFloat("speed", 0, speedDampTime, Time.deltaTime); } else { // 計算speed -- desiredVelocity在當前方向forward上的分速度 Vector3 projection = Vector3.Project(navAgent.desiredVelocity, transform.forward); enemyAnim.SetFloat("speed", projection.magnitude, speedDampTime, Time.deltaTime); } } navAgent.nextPosition = transform.position; } }
任務34:機器人的追捕 AI
機器人的三種狀態:巡邏、追捕、射擊
巡邏:普通
追捕:alertPosition != Vector3.zero
射擊:EnemySight.isPlayerInSight == true;
EnemyMoveAI.cs中
追捕方法:
private void Chasing() {
nagAgent.speed = 5; // 追捕時速度快 (記住在Patrolling()中navAgent.speed=3;)
navAgent.destination = enemySight.alertPosition; (EnemySight sight = ...)
// 追捕時,目標距離大一些,就可進行射擊()
if(navAgent.remainingDistance < 2f) {
到達目標位置后,若看見Player就自動射擊(在Animator和Shooting()中控制)
如果沒看見,alertPosition也沒更新,就在原地停留一段時間,解除警報
-- public chaseWaitingTime = 5;
-- private chaseTimer = 0;
chaseTimer += Time.deltaTime;
if(chaseTimer > chaseWaitingTime) {
// 解除警報,回到巡邏位置
sight.alertPosition = Vector3.zero;
GameController._instance.lastPlayerPosition = Vector3.zero;
GameController._instance.isAlarmOn = false;
index = 0;
navAgent.destination = waypoints[index];
chaseTimer = 0; //計時器歸零
}
任務35:解決bug:視野檢測和自動門的開啟和關閉
Bug -- 視野檢測:若Player與Enemey之間有障礙物(牆),Enemy也是可見Player的
解決方法:若在視野viewFieldAngle內,則在Enemy眼睛處發射一條射線,方向為playerDir
Raycast的起點為transform.position + Vector3.up * 1.8f; 終點為alertPos
RaycastHit hitInfo;
Physics.Raycast(transform.position+Vector3.up*1.8f, playerDir, out hitInfo);
if(hitInfo.collider.tag == Tags.player) {
在視野內
}
if (tempAngle <= viewFieldAngle * 0.5f) { // 判斷是否有障礙物(如牆)擋住視線 RaycastHit hitInfo; Physics.Raycast(transform.position + Vector3.up * 1.8f, playerDir, out hitInfo); if(hitInfo.collider.tag == Tags.player) { playerInSight = true; // 更新警報位置 alertPosition = other.transform.position; GameController._instance.SwitchAlarmOn(other.transform); } else { playerInSight = false; } } else { playerInSight = false; }
Bug -- 門的控制在Player和Enemy同時進出的時候有問題
解決方法:將player和enemy分別判斷並各自count++/--即可
任務36&37:機器人的射擊動畫 && 傷害計算
射擊動畫:
void Update () {
if(enemySight.playerInSight) {
// 只要能看見Player,就射擊
Shoot();
} else if (enemySight.alertPosition != Vector3.zero) {
// 有動靜 -- 進行追捕
Chasing();
} else {
Patrolling();
}
}
在Shoot()中不處理動畫
navAgent.isStopped = true; // 停止導航
-- 調用EnemyAnimationController.cs來控制射擊動畫的播放
在Update()控制行走的代碼后,通過playerInSight的賦值來控制動畫
private EnemySight enemySight = GetComponent...
enemyAnim.SetBool("playerInSight", enemySight.playerInSight);
傷害計算:
在player上添加腳本PlayerHealth.cs
public float hp = 100; // 血量
private void TakeDamege(float damage) {
hp -= damage;
}
public bool isPlayerAlive() {
return (hp>0);
在EnemyMoveAI.cs中
在Update()判斷Shoot()或Chasing()時,加上判斷如果isPlayerAlive才進行該操作
檢測enemy狀態機中humanoid_weapon_shoot動畫的播放:播放一次進行一次傷害計算
動畫->Inspector->Curves->Shot:看曲線的變化
在狀態機中設置一個參數,用來記錄上面Shot曲線的當前值
添加Parameter float Shot, 這個值會自動被曲線Shot的當前值賦值
在Enemy上添加腳本EnemyShooting() -- 用來處理射擊有關的操作
在Update()中
if(anim.GetFloat("Shot") > 0.5f) { // 進行了一次射擊
Shooting();
} else {
hasShot = false; // 沒有開槍,則標志位重置
}
傷害機制:距離越近傷害越大,傷害有保底值
主角在SphereCollider范圍之內會進行射擊,則在最外側傷害最低
public float minDamage = 30;
private void Shooting() { // 計算傷害
-- private bool hasShot = false;
if(!hasShot) { // 進行傷害
-- 從PlayerHealth腳本獲得
// 計算傷害
float distance = Vector3.Distance(transform.position, playerHealth.transform.pos);
float damage = minDamage+ (100-minDamage)*(distance/sphereCollider.radius);
playerHealth.TakeDamage(damage);
hasShot = true;
}
Player死亡動畫:
回顧:在Player的動畫狀態機中的Parameter bool dead;
dead = true時 播放dead動畫
在PlayerHealth.cs中持有animator
在TakeDamage() 中判斷
if(!IsPlayerAlive()) {
anim.SetBool("dead", true);
}
Bug -- Player死亡以后,enemy還是會在盯着,沒有回去巡邏
解決方法:在EnemySight中觸發playerInSight的判斷條件里加上
&& playerHealth.isPlayerAlive()
而且Shooting()時把navAgent設為Stopped了
Enemy從Shooting轉為Patrolling時候需要重新設置開始
navAgent.isStopped = false;
// navAgent.destination = waypoints[index].position; // 不要也行
Bug -- Enemy在PlayerInSight=true的狀態下正在射擊,但Player跑出了射擊范圍
此時Enemy就會呆在原地不動
思路:與上面一個bug相似,Shooting后navAgent被stop了,下一幀轉為Chasing()時還是true
在Chasing()剛開始的時候,navAgent.isStopped = false;
Bug -- Player死亡以后,死亡動畫會不斷播放
思路:猜想是因為狀態機中Any State--dead=true-->Dead
AnyState也包括了Dead本身,於是有Dead->Dead的動畫
https://blog.csdn.net/ln_polaris/article/details/50724425
將transition屬性取消勾選Can Transition To Self即可
任務38&39:添加其他機器人 和 游戲失敗狀態 && 游戲勝利狀態
將robot apply給prefab
創建空物體Robots,用來存儲其他兩個敵人(一共三個)
創建對應的waypoints
游戲失敗狀態:
機制:死亡之后,等待4秒,重新加載Scene
IEnumerator ReloadScene() {
yield return new WaitForSeconds(4f);
SceneManager.LoadScene(0);
}
在TakeDamage()中
在播放死亡動畫之后
StartCoroutine(ReloadScene());
播放失敗音效
添加AudioSource組件: endgame
defeatAudio.Play();
游戲勝利狀態:
機制:勝利之后,啟動電梯,並重新加載Scene
prop_lift_exit
添加BoxCollider作為Trigger
編輯之前創建的腳本LiftController.cs
private float liftUpWaitingTime = 3;
private float liftUpTimer = 0;
private bool isPlayerInside = false;
分別在OnTriggerEnter/Exit()中控制isPlayerInside,記得判斷collider.tag為player
在Update()中:
if(isPlayerInside) { // player進入電梯
liftUpTimer += Time.deltaTime;
if(liftUpTimer > liftUpWaitingTime) { // 等待時間到,升起電梯
-- 電梯只會升起一次
// 播放電梯升起音效
liftUpAudio.Play();
}
發現,若干秒后電梯向上移動了,但是:
1. Player沒有隨着電梯上升而上升
原因:電梯地板沒有設置collider
給電梯添加Mesh Collider, 選擇mesh: prop_lift_exit)collision_001
原因:player的position.y在Inspector中Freeze,取消勾選
2. 電梯上升了一小段便停下了
原因未知,但是修改完bug1后這個bug自動消失了
電梯上升后,游戲勝利
機制:電梯上升后一段時間,宣布游戲勝利,重新載入Scene
private float gameWinTimer = 0;
private float gameWinWaitingTime = 4;
在電梯上升的代碼塊中:
gameWinTimer += Time.deltaTime;
if (gameWinTimer > gameWinTime) {
SceneManager.LoadScene(0);
}
public class LiftController : MonoBehaviour { ... private float liftUpWaitingTime = 3; private float liftUpTimer = 0; private float gameWinWaitingTime = 5; private float gameWinTimer = 0; private bool isPlayerInside = false; void Update () { ... // 升起電梯 if(isPlayerInside) { // Player在電梯內 liftUpTimer += Time.deltaTime; if(liftUpTimer > liftUpWaitingTime) { // 等待時間結束 transform.Translate(Vector3.up * Time.deltaTime); gameWinTimer += Time.deltaTime; if(gameWinTimer > gameWinWaitingTime) { SceneManager.LoadScene(0); }}}} private void OnTriggerEnter (Collider other) { if(other.tag == Tags.player){ isPlayerInside = true; }} private void OnTriggerExit (Collider other) { if(other.tag == Tags.player) { isPlayerInside = false; }}}
任務40:游戲提示設置 和 總結
創建UI->Text,字體為Sansation_Light,位置設置為左下角,顏色灰白色
創建腳本MessageShow.cs
void Start () { msg = GetComponent<Text>(); msg.text = "WASD to Move\n" + "Z to Switch Off\n" + "LeftShift to Sneak"; }