本文是學習官方教程SURVIVAL SHOOTER TUTORIAL的筆記;
主要目的是用于記錄一些關鍵的代碼和步驟;
如果感興趣,推薦還是觀看官方的教程:教程鏈接
預覽
1. 控制角色移動
創(chuàng)建地板用于射線捕捉,把Layer設定為Floor
新建一個PlayerMovement腳本,綁定在角色上,并且添加以上屬性
public float speed = 6f;
Vector3 movement;
Animator anim;
Rigidbody playerRigidbody;
public int floorMask;
float camerRayLength = 100f;
void Awake (){
floorMask = LayerMask.GetMask ("Floor"); //綁定LayerMask
anim = GetComponent<Animator> ();
playerRigidbody = GetComponent<Rigidbody> ();
}
射線檢測是否和地面碰撞,并且讓角色旋轉
void Turing() {
//捕捉主攝像機和鼠標交集的射線
Ray camRay = Camera.main.ScreenPointToRay (Input.mousePosition);
RaycastHit floorHit;
//射線, out 射線點, 長度, 層級,以上幾個關鍵屬性如果產生交集就讓角色旋轉
if (Physics.Raycast (camRay, out floorHit, camerRayLength, floorMask)) {
Vector3 playerToMouse = floorHit.point - transform.position;
playerToMouse.y = 0f;
Quaternion newRotation = Quaternion.LookRotation (playerToMouse);
playerRigidbody.MoveRotation (newRotation);
}
}
捕捉輸入的h和v,移動角色
void Move(float h, float v) {
movement.Set (h, 0, v);
movement = movement.normalized * speed * Time.deltaTime;
playerRigidbody.MovePosition (transform.position + movement);
}
如果角色移動就播放移動動畫
//如果輸入的上下左右不等于0,則讓動畫狀態(tài)機的IsWalking屬性變?yōu)閠rue,具體的動畫切換在角色綁定的動畫狀態(tài)機中
void Animating (float h, float v){
bool walking = (h != 0f || v != 0f);
anim.SetBool ("IsWalking", walking);
}
FiexdUpdate輸入Input,并且統(tǒng)一調用以上幾個方法
void FixedUpdate(){
//具體的單詞參考Edit->ProjectSetting->Input面板
float h = Input.GetAxisRaw ("Horizontal");
float v = Input.GetAxisRaw ("Vertical");
Move (h, v); //移動角色
Animating (h, v); //判斷是否播放移動動畫
Turing (); //旋轉角色
}
ok 運行游戲試試看,此時玩家可以使用鍵盤移動了,并且會面朝鼠標指向的方向.
2. 控制攝像機跟隨玩家
新建一個CameraLookAt腳本,代碼如下
public class CameraLookAtPlayer : MonoBehaviour {
public Transform target; //用于編輯器中綁定玩家
public float smoothing = 5f; //用于計算順滑度
Vector3 offset;
void Start() {
//首先初始化的時候保存相機和玩家的相對位置
offset = transform.position - target.position;
}
//這里不要使用FixedUpdate, 移動端屏幕會有視覺卡頓
void Update () {
//計算出相機跟隨的位置
Vector3 targetCamPos = target.position + offset;
//設置相機的位置,這里用到了Vector3.Lerp,是一個差值計算,使得移動更柔和.但是會略微消耗計算量
//由于主攝像機只有1個,所以可以忽略這個計算量的消耗
transform.position = Vector3.Lerp (transform.position, targetCamPos, smoothing * Time.deltaTime);
}
}
在項目中設定好攝像機和玩家的距離
然后在把CamerLookAt腳本綁定在主攝像機上,并且把玩家設定到target屬性中
相機的設置
ok,運行一下游戲,此時攝像機會跟隨玩家移動,并且有一個緩慢加速的過程
3.創(chuàng)建一個敵人,自動尋路跟隨玩家
首先添加Navigation烘焙
- 首先把場景中需要烘焙的物件設置為static, 一些障礙物體要添加碰撞體
- 點擊Window->Navigation面板,設置烘焙屬性并且烘焙
創(chuàng)建一個怪物并且基礎設置
- 設置Rigidbody
- 添加一個膠囊碰撞體,CapsuleCollider組件,用于尋找玩家做碰撞
- 添加一個球碰撞體,SphereCollider組件,用于以后攻擊玩家做碰撞
- 添加一個尋路組件,NavMeshAgent,用于配合Navigation尋路
- 給怪物設置一個動畫控制機AminatorController,用于切換動畫.
給添加怪物尋路代碼
public class ZombunnyMovement_ym : MonoBehaviour {
Transform player;
NavMeshAgent nav;
void Awake(){
//因為怪物是生成器生成的,所以player需要在生成的時候遍歷一下場景,找到玩家
player = GameObject.FindGameObjectWithTag ("Player").transform;
//讀出導航組件
nav = GetComponent<NavMeshAgent> ();
}
void Update() {
//導航到目的地:玩家的坐標,這里注意,一定是transform.position,而不是transform
nav.SetDestination (player.position);
}
}
ok,此時運行游戲,怪物會一直朝著玩家移動
4.設置血條UI
首先設置UI如下圖效果
- 在場景中創(chuàng)建一個Canvas
- 里面創(chuàng)建一個空物體用來存放HealthUI
-
創(chuàng)建一個Slider,刪除手柄,只要進度條,用來顯示血量
Hierarchy中的層級
如圖,左下角有一個紅心,和一個Slider,Slider刪掉了手柄
聲明一下,這種UI不是我做的......
5.給玩家添加生命控制的腳本,和給敵人添加攻擊的腳本
首先是添加控制玩家生命的代碼
新建一個PlayerHealth腳本
using UnityEngine;
using System.Collections;
using UnityEngine.UI;
public class PlayerHealth_ym : MonoBehaviour {
public int startingHealth = 100;
public int currentHealth; //當前血量
public Slider healthSlider; //用來存放Slider,收到傷害修改它的值
public Image damageImage; //之前設置的收到傷害覆蓋全屏紅色閃爍的圖片
public AudioClip deathClip; //用來存放死亡的聲音
public float flashSpeed = 5f;
public Color flashColor = new Color (1f, 0f, 0f, 0.1f);
Animator anim;
AudioSource playAudio;
PlayerMovement_ym playMovement; //用來死亡的時候取消移動組件,防止玩家移動
bool isDead;
bool damaged;
void Awake () {
anim = GetComponent<Animator> ();
playAudio = GetComponent<AudioSource> ();
playMovement = GetComponent<PlayerMovement_ym> ();
currentHealth = startingHealth;
}
// Update is called once per frame
void Update () {
//如果受到傷害,就改變圖片顏色,否則,Lerp過渡顏色到空顏色
if(damaged) {
damageImage.color = flashColor;
} else {
damageImage.color = Color.Lerp (damageImage.color, Color.clear, flashSpeed * Time.deltaTime);
}
damaged = false;
}
//public方法,用于怪物攻擊玩家時調用
public void TakeDamage(int amount) {
damaged = true;
currentHealth -= amount;
healthSlider.value = currentHealth;
playAudio.Play ();
if(currentHealth <= 0 && !isDead) {
Death ();
}
}
//死亡時播放死亡聲音,設置死亡動畫,并且關閉玩家移動方法
void Death(){
isDead = true;
anim.SetTrigger ("Die");
playAudio.clip = deathClip;
playAudio.Play ();
playMovement.enabled = false;
}
}
接著給腳本的public屬性綁定物件,如圖:
接下來是給怪物添加攻擊腳本
新建一個Empty_Attack腳本:
using UnityEngine;
using System.Collections;
public class Empty_Attack_ym : MonoBehaviour {
public float timeBetweenAttacks = 0.5f; //攻擊間隔
public int attackDamage = 10; //攻擊力
public bool playerInRange; //玩家是否在攻擊范圍內
Animator anim;
GameObject player;
PlayerHealth_ym playerHealth;
float timer;
void Awake(){
player = GameObject.FindGameObjectWithTag ("Player");
playerHealth = player.GetComponent<PlayerHealth_ym> ();
anim = GetComponent<Animator> ();
}
//當碰撞體進入
void OnTriggerEnter (Collider other){
if(other.gameObject == player) {
playerInRange = true;
}
}
//當碰撞體離開
void OnTriggerExit (Collider other) {
if(other.gameObject == player) {
playerInRange = false;
}
}
void Update(){
timer += Time.deltaTime;
//當攻擊正在范圍內,并且timer的值大于攻擊間隔,進行攻擊
if(timer >= timeBetweenAttacks && playerInRange) {
Attack ();
}
//當玩家當前生命小于0, 怪物的動畫狀態(tài)機切換為玩家已死的狀態(tài)(此時播放開心的動畫);
if(playerHealth.currentHealth <= 0) {
anim.SetTrigger ("PlayerDie");
}
}
void Attack(){
timer = 0f;
if(playerHealth.currentHealth > 0) {
//調用玩家生命控制器的被攻擊的方法,傳入攻擊傷害值
playerHealth.TakeDamage (attackDamage);
}
}
}
ok,運行一下,玩家會被怪物打死...
6. 給玩家添加攻擊動作,和怪物生命腳本
首先配置資源
- 把玩家射擊的粒子預設組件拷貝到玩家的槍上
- 給玩家的槍添加一個Line Renderer組件
- 設置Parameters的StartWidth和EndWidth為0.07
- 設置Materials->Element0的材質為LineRenderMaterial
- 給玩家的槍添加一個Light組件,并且設置好亮度角度, 初始取消它,等待攻擊的時候再臨時激活
- 把怪物受到傷害的粒子組件拷貝到怪物身上
- 把怪物的Layer設置為Shootable,一會射線檢測的LayerMask用得到
開始添加玩家攻擊腳本PlayerShooting
代碼都有注釋, 就不需要額外說明了
using UnityEngine;
using System.Collections;
public class PlayerShooting_ym : MonoBehaviour {
public int damagePerShot = 20; //傷害
public float timeBetweenBullets = 0.15f; //攻擊間隔
public float range = 100f; //射線最大距離
float timer; //用于攻擊間隔計時
Ray shootRay; //子彈的射線
RaycastHit shootHit;
int shootableMask; //用于存放LayerMask
ParticleSystem gunParticles;
LineRenderer gunLine; //射線的渲染
AudioSource gunAudio;
Light gunLight; //槍上的燈光
float effectsDisplayTime = 0.2f; //粒子時長比例
void Awake() {
shootableMask = LayerMask.GetMask ("Shootable");
gunParticles = GetComponent<ParticleSystem> ();
gunLine = GetComponent<LineRenderer> ();
gunAudio = GetComponent<AudioSource> ();
gunLight = GetComponent<Light> ();
}
void Start () {
}
// Update is called once per frame
void Update () {
timer += Time.deltaTime;
//當點擊Fire1鍵時,并且攻擊間隔達到預設時,可以攻擊
if(Input.GetButton("Fire1") && timer >= timeBetweenBullets) {
Shoot ();
}
//當計時器大于槍的粒子時間時,取消射擊的樣式, 這里射擊樣式的時間是攻擊間隔 * 粒子時長比例
if(timer >= timeBetweenBullets * effectsDisplayTime) {
DisableEffects ();
}
}
// Shoot方法是關鍵
void Shoot() {
timer = 0f;
//激活聲音,燈光
gunAudio.Play ();
gunLight.enabled = true;
//停止之前的射擊粒子,重新播放
gunParticles.Stop ();
gunParticles.Play ();
//打開LineRenderer組件, 設置0點為gameobject默認位置
gunLine.enabled = true;
gunLine.SetPosition (0, transform.position);
//設置射線的位置
shootRay.origin = transform.position;
//設置射線的方向,正前方
shootRay.direction = transform.forward;
//用于捕捉射線,然后根據(jù)射線的射擊點進行做一些事情
//物理.射線捕捉(射線, 得到的點shootHit, 最大距離, 進行計算的Layer)
if(Physics.Raycast(shootRay, out shootHit, range, shootableMask)) {
//得到怪物的生命組件
ZombunnyHealth_ym enemyHealth = shootHit.collider.GetComponent<ZombunnyHealth_ym> ();
//如果怪物的生命組件不為空, 調用這個組件的TakeDamage方法進行扣血
if(enemyHealth != null) {
enemyHealth.TakeDamage (damagePerShot, shootHit.point);
}
//設置LineRenderer的第二個位置,為shootHit.point, 兩點確定一條直線
gunLine.SetPosition (1, shootHit.point);
}
else {
//如果沒有捕捉到射擊點
//設置LineRenderer的第二個位置為, 射線初始位置 + 射線方向(0, 0, 1) * 最大距離
gunLine.SetPosition (1, shootRay.origin + shootRay.direction * range);
}
}
void DisableEffects(){
//取消LineRenderer組件和燈光組件
gunLine.enabled = false;
gunLight.enabled = false;
}
}
給怪物添加生命組件ZombunnyHealth
public int startingHealth = 100; //初始生命
public int currentHealth; //當前生命
public float sinkSpeed = 2.5f; //下沉速度
public int scoreValue = 10; //此怪物殺死計多少分
public AudioClip deathClip; //保存死亡時播放的聲音
Animator anim;
AudioSource enemyAudio;
ParticleSystem hitParticles;
CapsuleCollider capsuleCollider; //capsulecollider是用來做移動碰撞捕捉
bool isDead;
bool isSinking;
void Awake(){
anim = GetComponent<Animator> ();
enemyAudio = GetComponent<AudioSource> ();
hitParticles = GetComponentInChildren<ParticleSystem> ();
capsuleCollider = GetComponent<CapsuleCollider> ();
currentHealth = startingHealth;
}
// Use this for initialization
void Start () {
}
// Update is called once per frame
void Update () {
//如果可以下沉, 設定下沉的動畫
//transform.Translate是位置變化,必須放在循環(huán)中,才能達到像動畫一樣的效果
if(isSinking){
//位置.變化((0, -1, 0) * 下沉速度)
transform.Translate (-Vector3.up * sinkSpeed * Time.deltaTime);
}
}
public void TakeDamage(int amount, Vector3 hitPoint) {
if(isDead) {
return;
}
//播放挨打的聲音
enemyAudio.Play ();
//設置粒子的位置等于攻擊點的位置
hitParticles.transform.position = hitPoint;
//播放粒子
hitParticles.Play ();
//扣血
currentHealth -= amount;
if(currentHealth <= 0) {
//調用死亡方法
Death ();
}
}
void Death() {
isDead = true;
//把碰撞體設置為isTrigger
capsuleCollider.isTrigger = true;
//設置動畫控制器的觸發(fā)"Dead"
anim.SetTrigger ("Dead");
//切換聲音片為死亡聲音
enemyAudio.clip = deathClip;
//播放聲音
enemyAudio.Play ();
}
//這個是公開方法, 實在模型動畫里的event調用的,當動畫播放到某一個時間時,會調用這個函數(shù)
public void StartSinking(){
//取消尋路組件
GetComponent<NavMeshAgent> ().enabled = false;
//取消剛體組件
GetComponent<Rigidbody> ().isKinematic = true;
//設置可以下沉, 一會在Updata里面就會調用下沉動畫
isSinking = true;
//添加修改靜態(tài)變量,然后更新UI的分數(shù)
SourceValue_ym.score += scoreValue;
//兩秒之后銷毀本物體
Destroy (gameObject, 2f);
}
}
給場景添加計分Text
-
在場景中間添加一個Text,如圖:
- 給Text添加一個SourceValue組件,用于改變Text的文字
using UnityEngine;
using System.Collections;
using UnityEngine.UI;
public class SourceValue_ym : MonoBehaviour {
public static int score;
Text text;
void Awake () {
text = GetComponent<Text> ();
score = 0;
}
// Update is called once per frame
void Update () {
//之前在ZombunnyHealth組件中已經添加了怪物死亡修改score的方法
text.text = "Score: " + score;
}
}
ok,現(xiàn)在運行一下游戲, 玩家可以射擊攻擊怪物了,并且怪物會被玩家打死, 而且有計分
一具尸體...
6. 創(chuàng)建怪物生成器
首先創(chuàng)建兩個新怪物,引用之前的邏輯
第一個怪物ZomBear:
- 把ZomBear的模型拖進Scene中
- 把Zombunny的所有組件拷貝到ZomBear上: 拷貝所有組件的方法擴展鏈接
- 因為兩個動畫狀態(tài)機和Avatar(化生)都是一樣的,所以動畫狀態(tài)機可以使用同一個
第二個怪物Hellephant:
- 把Hellephant的模型拖進Scene中
- 把Zombunny的所有組件拷貝到Hellephant上
- 因為兩個動畫的Avatar不一樣, 所以播放的動畫不能重用, 但是動畫邏輯可以重用
- 創(chuàng)建一個Animator Override Controller(動畫覆蓋控制器)
- 動畫控制器選ZombunnyAC, Original中的動畫片段選用Hellephant自己的動畫
-
這樣就覆蓋動畫片段, 并且使用原有的動畫邏輯了
Animator Override Controller 例子
修改一下兩個新怪物的屬性,需要修改的內容有:
- 移動速度
- 生命值
- 攻擊力, 攻擊間隔
- 收到傷害聲音, 死亡聲音
接下來創(chuàng)建怪物生成器的代碼
創(chuàng)建EnemyManager組件, 一會用來生成怪物
using UnityEngine;
using System.Collections;
public class EnemyManager_ym : MonoBehaviour {
public PlayerHealth_ym playerHealth; //玩家的血量
public GameObject enemy; //要創(chuàng)建的怪物
public float spawnTime = 3f; //創(chuàng)建間隔時間
public Transform[] spawnPoints; //創(chuàng)建位置
// Use this for initialization
void Start () {
//循環(huán)調用("方法名", 初始等待時間, 循環(huán)間隔時間)
InvokeRepeating ("Spawn", spawnTime * 0.7f, spawnTime);
}
void Spawn() {
if(playerHealth.currentHealth <= 0) {
return;
}
//隨機得到數(shù)組范圍內的一個整數(shù)
int spawnPointIndex = Random.Range (0, spawnPoints.Length);
//實例化一個物件(物件, 位置, 旋轉角度);
Instantiate (enemy, spawnPoints [spawnPointIndex].position, spawnPoints [spawnPointIndex].rotation);
}
}
- 在場景中創(chuàng)建一個空物體, 把坐標還原成(0, 0, 0), 取名為EnemyManager
- 在EnemyManager中創(chuàng)建三個空物體, 并且拖動到場景不同的位置中, 用于設定3種怪物的生成位置
- 在EnemyManager中綁定三個EnemyManager組件, 用于創(chuàng)建三種不同的怪物, 如圖:
這是官方的做法, 有優(yōu)有劣吧
ok, 可以出不同怪物了
ok, 現(xiàn)在運行一下游戲, 怪物從不同地方出來了, 可以好好的干一仗了
7. 最后,完善失敗場景
- 首先添加UI, GameOver Text提示,和灰藍色Mask, 如最后效果圖
- 放好位置之后, 把Text和Mask的顏色Alpha都設置為0, 平時讓玩家看不到.
- 然后給UI做動畫, 等待死亡就觸發(fā)它
- 選中整個HealthCanvas, 打開Window->Animation窗口, 新建一個動畫
- 此時系統(tǒng)會自動創(chuàng)建一個AnimatorController, 然后會要求取名新建一個AnimationClip
- 添加Text和Mask的顏色動畫, 縮放動畫
- 給系統(tǒng)生成的HealthCanvas動畫控制器做邏輯
- 做一個空動畫, 然后鏈接到之前那個UI的AnimationClip上
- 添加一個Trigger("GameOver") 觸發(fā)動畫
- 這類動畫記得把Exit With Time去掉勾選, 這樣一觸發(fā)就會立刻切換動畫狀態(tài)
最后添加GameOverManager組件給HealthCanvas:
using UnityEngine;
using System.Collections;
using UnityEngine.SceneManagement;
public class GameOverManager_ym : MonoBehaviour {
public PlayerHealth_ym playerHealth;
public float restartDelay = 3f; //重新開始的等待時間
Animator anim;
float restartTimer; ////重新開始的計時
void Start () {
anim = GetComponent<Animator> ();
}
void Update () {
if(playerHealth.currentHealth <=0) {
//觸發(fā)失敗動畫
anim.SetTrigger ("GameOver");
//計時
restartTimer += Time.deltaTime;
if (restartTimer >= restartDelay) {
//新的場景切換, 這個demo切換原來的場景就是重新開始游戲
SceneManager.LoadScene (0);
}
}
}
}
死亡效果
ok, 玩家被怪物打死之后會有提示, 并且在3秒之后就會重新開始游戲啦
至此, 整個教程已經結束