1. 程式人生 > >Unity學習筆記1 簡易2D橫版RPG遊戲製作(一)

Unity學習筆記1 簡易2D橫版RPG遊戲製作(一)

這個教程是參考一個YouTube上面的教程做的,原作者的教程做得比較簡單,我先參考著做一遍,畢竟我也只是個初學者,還沒辦法完全自制哈哈。不過我之前也看過一個2D平臺遊戲的系列教程了,以後會整合起來,做出一個類似冒險島那樣的遊戲。

原視訊連結:點選開啟連結   這是個YouTube視訊連結,如果可以“友情訪問外網”的朋友可以自己看看。視訊全英無字幕,如果看起來有壓力的話那其實也可以不看。反正我已經將裡面的程式碼都整理在這裡了。

原本我是打算把這篇東西整理到自己的qq空間的,不過發空間很麻煩,就算了,新浪部落格也不方便,那就用CSDN吧,以後我的Unity學習筆記都會整理到這個部落格裡面。談不上是教程,只能說是自己研究過程的一些整理。歡迎大家一起交流,一起進步,如果這篇筆記可以幫到更多的朋友學習Unity,那就最好不過了。

好了,廢話不多說,馬上開始。這次是我嘗試著製作一個類似魂鬥羅和超級瑪麗那樣的橫版遊戲,以後學得比較多了會整理更復雜的教程。

一、角色移動

開啟Unity,我用的是4.3的版本,選擇建立2D遊戲或者3D遊戲其實都可以,我喜歡建立3D的,這樣方便一些。

修改攝像機的檢視,改成正交檢視。(3D才需要改,2D不用改,3D可以在兩種檢視模式下進行切換,比2D好些。)


(projection裡面,orthographic即是正交檢視,在3D情況下預設是Perspective,即透視檢視,2D則預設為orthographic

改完orthographic之後,可以修改size,在正交視圖裡面size

決定畫面的大小,size越大視野的範圍越大,每件物體相應的可見面積就會縮小。我把size設為5。如果覺得畫面視野還不夠大的話可以把size改得更小,比如43這樣。

剛開始的時候,畫面會比較暗,有兩種方法可以處理,第一種方法是在遊戲場景中放置光源。


如上圖,有好幾種light可以選擇。

另一種方法是在EditRenderSettings裡面進行設定


如圖,修改Ambient Light(環境光)的顏色,預設是深灰色,可以調成淺灰色或者白色(白色太亮,不推薦,淺灰色和淺黃色都可以選擇,環境光相當於全域性光線,如果選擇其他顏色,比如紫色或者綠色的話,則會讓整個畫面變得相當詭異。)

設定完成之後我們就可以開始製作啦:

首先,需要建立一個基本的地形和角色,由於是2D的平面遊戲,所以都使用GameObjectCreate Other裡面的Quad即可。(原視訊教程裡面是用quad的,不過我想以後可能會換成其他的)

我們先建立三個,第一個改名為Player,第二個就叫做Enemy,第三個叫做Floor,然後將Floor拉長,將其ScaleX變為100,想調得更大也可以。


(這裡還多了一個GameManager,因為是事後截的圖,所以多了一個,後面會用到,這個和前三個物體不同,這個是個空物體。)

接著我們新建幾個資料夾,分別是Prefabs(預設)、Materials(材質)、Scripts(指令碼)、Scenes(場景)、Textures(紋理)。

在選單欄FileSave Scene裡儲存當前場景,我命名為Scene1


然後在Materials資料夾裡面建立三個material,分別命名為PlayerEnemyFloor,將它們調整成自己喜歡的顏色,我將Player弄成天藍色,將Enemy弄成紅色,將Floor弄成土黃色。這個只是個人愛好啦,用於區分這些物體的,可以隨便弄。(Floor和Enemy都去掉Mesh Collider,加上Box Collider,另外我們還要在Enemy身上新增一個Rigidbody元件。)將Player上面的Mesh Collider去掉,加上這兩個:


然後建立第一個指令碼。這次我全部使用c#指令碼,所以後面有提到的指令碼,除非有特別提到是js,否則都是c#。指令碼是學習筆記中的重點內容,其他的倒不是特別重要。

(這次用CharacterController元件來控制角色的移動,所以在角色身上去掉Mesh Collider的元件,然後新增Character ControllerBox Collider元件。

這個指令碼命名為Controller2D。下面貼出內容:

    (腳本里面的中文都是我自己加進去的註釋,可以全部刪掉)

using UnityEngine;
  using System.Collections;
  
  public class Controller2D : MonoBehaviour {
  	//引用CharacterController
  	CharacterController characterController;
  	//重力
  	public float gravity = 10;
  	//水平移動的速度
  	public float walkSpeed = 5;
  	//彈跳高度
  	public float jumpHeight = 5;
  
  	//顯示角色當前正受到攻擊
  	float takenDamage = 0.2f;
  
  	// 控制角色的移動方向
  	Vector3 moveDirection = Vector3.zero;
  	float horizontal = 0;
  	// Use this for initialization
  	void Start () {
  		characterController = GetComponent<CharacterController>();
  
  	}
  	
  	// Update is called once per frame
  	void Update () {
  		//控制角色的移動
  		characterController.Move (moveDirection * Time.deltaTime);
  		horizontal = Input.GetAxis("Horizontal");
  		//控制角色的重力
  		moveDirection.y -= gravity * Time.deltaTime;
  		//控制角色右移(按d鍵和右鍵時)  在這裡不直接使用0而是用0.01f是因為使用0之後會持續移動,無法靜止
  		if (horizontal > 0.01f) {
  			moveDirection.x = horizontal * walkSpeed;
  		}
  		//控制角色左移(按a鍵和左鍵時)
  		if (horizontal < 0.01f) {
  			moveDirection.x = horizontal * walkSpeed;
  		}
  		// 彈跳控制
  		if (characterController.isGrounded) {
  			if(Input.GetKeyDown(KeyCode.Space)){
  				moveDirection.y = jumpHeight;
  			}
  		}
  	}
  
  
  	public IEnumerator TakenDamage(){
  		renderer.enabled = false;
  		yield return new WaitForSeconds(takenDamage);
  		renderer.enabled = true;
  		yield return new WaitForSeconds(takenDamage);
  		renderer.enabled = false;
  		yield return new WaitForSeconds(takenDamage);
  		renderer.enabled = true;
  		yield return new WaitForSeconds(takenDamage);
  		renderer.enabled = false;
  		yield return new WaitForSeconds(takenDamage);
  		renderer.enabled = true;
  		yield return new WaitForSeconds(takenDamage);
  	} 
  }

這個指令碼比較簡單,而且都已經做了註釋,就不解釋了。做完這個指令碼之後儲存,然後拖拽到Player身上,角色就可以活動了。後面的這個IEnumerator TakenDamage是用來控制角色被攻擊時進行閃爍的,這個本來是後面的內容,指令碼先放這裡了,就先提及一下。

二、相機跟隨

接下來解決的是角色跳躍和相機跟隨的問題。這個問題比較容易。而且由於筆記是我跟著一些視訊教程做了幾節之後再整理的,所以稍微有點“先進”。上面的腳本里面已經解決了角色的跳躍問題。

// 彈跳控制
  		if (characterController.isGrounded) {
  			if(Input.GetKeyDown(KeyCode.Space)){
  				moveDirection.y = jumpHeight;
  			}
  		}

很簡單,就是上面的這一小段程式碼。用空格鍵來進行反重力,當然,KeyCode是可以隨便設定的。為了方便,我又改成了這樣:

if (characterController.isGrounded) {
  			if(Input.GetKeyDown(KeyCode.Space)||Input.GetKeyDown(KeyCode.K)){
  				moveDirection.y = jumpHeight;
  			}
  		}

這樣就可以用空格鍵或者K鍵進行跳躍了,平時我自己的習慣是利用WSAD鍵位進行上下左右的移動,用JK鍵進行攻擊和跳躍。這種按鍵佈局和FC遊戲機的手柄最為相似。

Unity也可以用手柄的,不過這個我還不太會,現在也暫時不用研究這個。

接下來新建一個Camera2D的指令碼。(指令碼名字是可以隨你喜歡的,不過有時候指令碼的名字會影響到呼叫,所以必須儘量規範。)

using UnityEngine;
  using System.Collections;
  
  public class Camera2D : MonoBehaviour {
  
  	public Transform player;
  
  	public float smoothRate = 0.5f;
  
  	private Transform thisTransform;
  	private Vector2 velocity;
  
  	// Use this for initialization
  	void Start () {
  		thisTransform = transform;
  		velocity = new Vector2 (0.5f, 0.5f);
  	}
  	
  	// Update is called once per frame
  	void Update () {
  		Vector2 newPos2D = Vector2.zero;
  		//Mathf.SmoothDamp平滑阻尼,這個函式用於描述隨著時間的推移逐漸改變一個值到期望值,這裡用於隨著時間的推移(0.5秒)讓攝像機跟著角色的移動而移動
  		newPos2D.x = Mathf.SmoothDamp (thisTransform.position.x, player.position.x, ref velocity.x, smoothRate);
  		newPos2D.y = Mathf.SmoothDamp (thisTransform.position.y, player.position.y, ref velocity.y, smoothRate);
  	
  		Vector3 newPos = new Vector3 (newPos2D.x, newPos2D.y, transform.position.z);
  		//Vector3.Slerp 球形插值,通過t數值在from和to之間插值。返回的向量的長度將被插值到from到to的長度之間。time.time此幀開始的時間(只讀)。這是以秒計算到遊戲開始的時間。也就是說,從遊戲開始到到現在所用的時間。
  		transform.position = Vector3.Slerp (transform.position, newPos, Time.time);
  	}
  }
好了,這個就是攝像機的指令碼,這個指令碼不採用死死咬著角色不放的方法,而是採取緩慢跟隨的效果。攝像機跟隨的指令碼可以千變萬化,有空再研究其他的,這個不要太糾結。
三、遊戲控制器和紋理
現在角色會動了,攝像機也可以跟隨了。(不要問我為什麼場景這麼醜,指令碼才是關鍵,場景只需要更換貼圖什麼的,這個其實在現在來說並不是重點。)接著就是製作一個簡單的遊戲控制器,也就是前面的GameManager。這個東西這次是用來顯示一些紋理內容的,比如在畫面上顯示角色的生命值,以及當角色死掉之後重新開始等等……好了,現在建立一個空物體,然後命名為GameManager,建立一個同名指令碼扔上去,然後開啟這個指令碼,我們要進行編輯:
  using UnityEngine;
  using System.Collections;
  
  public class GameManager : MonoBehaviour {
  	//Controller2D指令碼的參量
  	public Controller2D controller2D;
  	//角色生命值
  	public Texture playersHealthTexture;
  	//控制上面那個Teture的螢幕所在位置
  	public float screenPositionX;
  	public float screenPositionY;
  	//控制桌面圖示的大小
  	public int iconSizeX = 25;
  	public int iconSizeY = 25;
  	//初始生命值
  	public int playersHealth = 3;
  	GameObject player;
  	//這個地方定義了私有變數player作為一個GameObject,然後用下面的FindGameObjectWithTag獲取它,這樣的話,在下面的傷害判斷時,就可以用player.renderer了。
  	void Start(){
  		player = GameObject.FindGameObjectWithTag("Player");
  	}
  
  	//OnGUI函式最好不要出現多次,容易造成混亂,所以我把要展示的東西都整合在這個裡面
  	void OnGUI(){
  
  		//控制角色生命值的心的顯示
  		for (int h =0; h < playersHealth; h++) {
  			GUI.DrawTexture(new Rect(screenPositionX + (h*iconSizeX),screenPositionY,iconSizeX,iconSizeY),playersHealthTexture,ScaleMode.ScaleToFit,true,0);
  		}
  	}
  
  	void PlayerDamaged(int damage){   //此處使用player.renderer.enabled來進行判斷,如果角色沒有在閃爍,也就是存在的狀態為真,那麼才會受到傷害,這樣可以避免角色連續受傷,還有另外一種方法是採用計時,這裡沒有采用那種方法。
  		if (player.renderer.enabled) {
  						if (playersHealth > 0) {
  								playersHealth -= damage;	
  						}
  
  						if (playersHealth <= 0) {
  								RestartScene ();	
  						}
  				}
  	}
  
  	void RestartScene(){
  		Application.LoadLevel (Application.loadedLevel);
  	}
  }

好了,內容已經粘貼出來。有三個地方我認為有必要稍微解釋一下,作為備忘的。第一個地方是在OnGUI函式裡面,for迴圈用來畫出playersHealthTexture的個數,這個東西在這裡就是我們想要展示出來的角色的生命值(我用愛心表示)

接著用Photoshop或者GraphicsGale畫一個透明的愛心。不用很大,我畫的是32乘以32畫素的,當然,要畫的很大也可以,不過長寬比必須等於一,而且最好保持2N次方的大小尺寸,比如32乘以32256乘以256這樣的尺寸。記得是弄成透明的png,不要儲存成白色pngPS的很簡單就不說了,在GraphicsGale裡面儲存成透明背景的有點特殊。所以我順便截一下圖:(首先,我們要畫完一個愛心……)

然後再點選左下角

的左上角的那個

然後就可以開啟當前這幅畫的屬性:


在透明度那裡打勾(預設是沒有打勾的),然後用下邊的那隻滴管筆點選你自己畫面中的背景顏色。我這裡是白色,所以點選之後就是白色的,然後你的作品的背景顏色是其他顏色的,那麼在這裡出現的就是其他顏色的。

然後就可以確定了。然後在選單欄選擇“檔案”→“另存為”就可以儲存透明背景的PNG圖片出來了。這個功能還是蠻有意思的,PS沒辦法這樣直接儲存成PNG,需要對背景稍微處理一下。個人覺得,如果只是畫畫素畫的話,GG確實比PS有一定的便利性。當然,我看的視訊教程裡面,那位兄臺用的是GIMP,其實和PS差不多,也是可以的,直接建立一張透明背景的圖片來畫畫。

畫完我們的愛心之後就匯入到Unity的專案裡面去,這個很簡單,就不多說了。直接拖拽放到Textures資料夾就可以了。重新命名為Heart(名字可以隨便,只要儘量不使用中文名就行)

Inspector面板修改如下,然後點選Apply


然後拖拽到GameManagerInspector面板的腳本里面的Players Health Texture位置:


執行測試:

成功!現在我們的角色有生命值了哈哈!

(在Game視窗中,我將畫面大小調成了16:9是因為我用的是膝上型電腦,對於大部分的膝上型電腦來說,都是這個長寬比的尺寸,如果不是的話可以在那裡點選之後選擇其他的,比如4:3等等,甚至可以進行自定義,這個就不多說了。

現在如果我們修改的值,就可以改變執行時在畫面上出現的愛心(生命值)的個數,當然,也可以在腳本里面對playersHealth變數進行直接修改。

由於OnGUI函式是每一幀都進行實時渲染的,所以一旦被敵人攻擊造成HP的傷害,那麼OnGUI函式就會自動減少當前的HP數量。同理,如果角色因為某些原因增加了HP,也是一樣的。

四、敵人與傷害計算

好了,HP設定好了,自然就要設定簡單的敵人以及傷害計算了。

此處用到的是這個函式:

  void PlayerDamaged(int damage){   //此處使用player.renderer.enabled來進行判斷,如果角色沒有在閃爍,也就是存在的狀態為真,那麼才會受到傷害,這樣可以避免角色連續受傷,還有另外一種方法是採用計時,這裡沒有采用那種方法。
  		if (player.renderer.enabled) {
  						if (playersHealth > 0) {
  								playersHealth -= damage;	
  						}
  
  						if (playersHealth <= 0) {
  								RestartScene ();	
  						}
  				}
  	}

這個在上面已經出現過了。

我參考的一個視訊教程中並沒有if (player.renderer.enabled)的判斷條件,我增加了這個是因為這樣可以控制Player在閃爍的過程中不會再被重複扣血,否則的話,如果我們的Player撞到了敵人,如果敵人和我們一直在做著相向運動的話,那麼我們會不明不白地被扣光HP然後掛掉的~~~~

(這裡補充一個小常識:HPhit point的縮寫,很多人可能誤以為HPhealth point吧,畢竟這個遊戲術語就是用來表示生命的,似乎表示健康點也沒什麼不對。我一開始也是這麼想的,後來看了一些教程和資料之後才發現,其實HP指的是“可以承受的打擊點數或次數”,這樣的話,用health point來解釋當然是解釋不通的。而mp這個是mana point,就沒什麼問題,就不解釋了。)

接下來我們要來對敵人進行編輯啦。上面已經做好了敵人的物體,我的這個敵人去掉了自帶的Mesh Collider元件,增添了Box ColliderRigidbody(剛體)元件。在Rigidbody裡去掉Use Gravity的勾選,在Constraints選項裡進行這樣的設定:

也就是凍結了敵人物體可能造成的不必要的三向旋轉(XYZ)和Z軸的位移。

接著在Box Collider裡面,我們將Is Trigger打勾,這樣的話就變成了一個觸發器,可以用於進行碰觸判斷。然後我們將SizeX值擴大到1.3,如圖:

我們這樣做的目的是讓這個物體的實際碰觸區域比它的實際體積大。為什麼要這樣做呢?其實這個是對Player物體身上的Character Controller元件的不足的一個補充。


由於Character Controller元件的碰觸範圍不是一個方形,而是一個圓形,為了避免物體插入Floor,就必須縮小圓圈,這樣的話,如果不放大敵人的碰觸區間,就會使得Player物體在於敵人進行碰撞時,需要和敵人的身體相嵌入近一半才會產生碰觸的效果,那樣就實在是太核突了……

接著新建一個Enemy2D指令碼,然後弄進以下內容:

using UnityEngine;
  using System.Collections;
  
  public class Enemy2D : MonoBehaviour {
  	//GameManager指令碼的參量
  	public GameManager gameManager;
  	//敵人移動的初始和停止位置,用於控制敵人在一定範圍內移動
  	float startPos;
  	float endPos;
  	//控制敵人向右移動的一個增量
  	public int unitsToMove = 5;
  	//敵人移動的速度
  	public int moveSpeed = 2;
  	//左右移動的布林值
  	bool moveRight = true;
  
  	void Awake(){
  		startPos = transform.position.x;
  		endPos = startPos + unitsToMove;
  	}
  	//此處這個Update函式用於控制敵人的左右移動,當向右移動到一定距離後就會反向移動,同理,左移一定距離之後也是。
  	void Update(){
  		        if (moveRight) {
  						rigidbody.position += Vector3.right * moveSpeed * Time.deltaTime;	
  				}
  				if (rigidbody.position.x >= endPos) {
  						moveRight = false;
  				}
  				if (moveRight==false) {
  						rigidbody.position -= Vector3.right * moveSpeed * Time.deltaTime;	
  				}
  				if (rigidbody.position.x <= startPos) {
  						moveRight = true;
  				}
  		}
  	
  
  	int damageValue = 1;
  	//這裡利用sendmessage函式使得角色與敵人自己碰撞時,傳送一個扣血的message到gamemanager函式之中,然後就會在每次碰撞時減掉一滴血。
  	void OnTriggerEnter(Collider col){
  		if (col.gameObject.tag == "Player") {
  			gameManager.SendMessage("PlayerDamaged",damageValue,SendMessageOptions.DontRequireReceiver);
  			gameManager.controller2D.SendMessage("TakenDamage",SendMessageOptions.DontRequireReceiver);
  		}
  	}
  }

儲存,弄到我們的Enemy身上。

現在,利用SendMessage函式,可以向GameManager物體(上面的指令碼)傳送訊息了,第一個傳送的訊息是“PlayerDamaged”,這個可以用來使PlayerHP造成傷害,角色的HP其實並沒有在角色自己身上,而是在GameManager物體上。第二個傳送的訊息是給Controller2D函式的,這個雖然是在Player身上,但是上面的GameManager已經捆綁了這個,所以同樣傳送給GameManager即可。這個是用來使角色受傷時進行閃爍的,使用的是IEnumerator介面。我們同樣用裡面的renderer.enabled判斷角色是否處於閃爍狀態。

五、AI移動

上面那個比較複雜的Enemy2D指令碼其實已經包含了這一節的內容了。我就只需要把那部分內容簡單地複製一下:

void Update(){
  		        if (moveRight) {
  				rigidbody.position += Vector3.right * moveSpeed * Time.deltaTime;	
  				}
  				if (rigidbody.position.x >= endPos) {
  						moveRight = false;
  				}
  				if (moveRight==false) {
  						rigidbody.position -= Vector3.right * moveSpeed * Time.deltaTime;	
  				}
  				if (rigidbody.position.x <= startPos) {
  						moveRight = true;
  				}
  		}

因為內容比較簡單,就不多做解釋了。這個教程裡面提到的AI移動其實是非常簡單的,以後還會繼續深入設計一些比較複雜的AI

六、角色攻擊

接下來這一講是關於player attack的。我們建立一個叫做PlayerAttack的指令碼,扔到我們的player身上。然後輸入以下內容:

using UnityEngine;
  using System.Collections;
  
  public class PlayerAttack : MonoBehaviour {
  
  	public Rigidbody bulletPrefab;
  	
  	// Update is called once per frame
  	void Update () {
  		if (Input.GetKeyDown (KeyCode.J)) {
  			BulletAttack();	
  		}
  	}
  	//按下攻擊按鍵時建立子彈的prefab,也就是bulletPrefab。
  	void BulletAttack(){
  		//下面的這句話非常經典,利用as Rigidbody將Instantiate的GameObject強制轉換為Rigidbody型別。
  		Rigidbody bPrefab = Instantiate (bulletPrefab, transform.position, Quaternion.identity)as Rigidbody;
  		bPrefab.rigidbody.AddForce (Vector3.right * 500);
  	}
  
  }

儲存,現在我們的Player就可以利用J鍵發射子彈了。不過這個子彈是一個prefab,我們需要自己設計一個出來才行。

首先我們新建一個quad,命名為Bullet。然後將XYZscale設定為0.3XY肯定要0.3,至於Z,對於一個平面物體來說,沒所謂。)然後新增Rigidbody元件和Box Collider元件,在Rigidbody裡面去掉Use Gravity,在ConstraintsFreeze Rotation裡面三項全打勾,在Freeze Position裡面的Z打勾。在Box Collider裡面的Is Trigger打勾,其他的不用設定。然後建立一個叫做Bullet的指令碼,扔到Bullet物體上。

然後我們把這個Bullet物體拖拽到Prefabs資料夾位置,在資料夾中就會自動生成一個叫做Bullet的預設。以後如果需要對預設進行修改的話,只需要重新造一個克隆物體出來修改,然後修改完點選Apply,就會影響到所有的預設體。

最後,我們把bulletprefab拉到PlayerPlayerAttack上。然後將PlayerEnemytag改為和他們的名字一樣的tag。現在我們的主人公會發射子彈了。

Bullet指令碼中進行以下新增:

using UnityEngine;
  using System.Collections;
  
  public class Bullet : MonoBehaviour {
  	//用於碰撞時摧毀兩個物體
  	void OnTriggerEnter(Collider other){
  		if (other.gameObject.tag == "Enemy") {
  			Destroy(gameObject);
  			Destroy(other.gameObject);
  		}
  	}
  }

儲存,現在我們的子彈可以破壞敵人了。不過敵人會被一擊消滅,而且子彈飛出畫面不會自行摧毀。這些問題就留著下一節解決。

七、角色攻擊2

接下來到第七講。現在我們需要讓我們的子彈變得會自動消失。也就是給發射過程設定一個自動摧毀的時間。避免出現子彈越來越多,而且永不消失的情況。首先我們需要在Bullet腳本里面增加一個FixedUpdate函式,這個函式和Update函式有什麼不同呢?這裡順便引用一段網上的話:

從字面上理解,它們都是在更新時會被呼叫,並且會迴圈的呼叫。

但是Update會在每次渲染新的一幀時,被呼叫。

FixedUpdate會在每個固定的時間間隔被呼叫,那麼要是Update FixedUpdate的時間間隔一樣,是不是就一樣呢?答案是不一定,因為Update受當前渲染的物體,更確切的說是三角形的數量影響,有時快有時慢,幀率會變化,update被呼叫的時間間隔就發生變化。

但是FixedUpdate則不受幀率的變化,它是以固定的時間間隔來被呼叫,那麼這個時間間隔怎麼設定呢?

Edit->Project Setting->time下面的Fixed timestep

現在這樣我們就理解這兩個的不同了,下面是新增的內容:

void FixedUpdate(){
  		Destroy (gameObject, 1.25f);
  	}

利用Destroy函式給出的時間,讓物體在1.25秒之後自行摧毀。

接下來我們修改一下PlayerAttack指令碼如下:

using UnityEngine;
  using System.Collections;
  
  public class PlayerAttack : MonoBehaviour {
  
  	public Rigidbody bulletPrefab;
  
  	float attackRate = 0.5f;
  	float coolDown;
  
  	// Update is called once per frame
  	void Update () {
  		if (Time.time >= coolDown) {
  			if (Input.GetKeyDown (KeyCode.J)){
  				BulletAttack ();	
  			}
  		}
  	}
  	//按下攻擊按鍵時建立子彈的prefab,也就是bulletPrefab。
  	void BulletAttack(){
  		//下面的這句話非常經典,利用as Rigidbody將Instantiate的GameObject強制轉換為Rigidbody型別。
  		Rigidbody bPrefab = Instantiate (bulletPrefab, transform.position, Quaternion.identity)as Rigidbody;
  		bPrefab.rigidbody.AddForce (Vector3.right * 500);
  		coolDown = Time.time + attackRate;
  	}
  }

現在增加了兩個變數,第一個是attackRate,第二個是coolDown,有了這兩個變數之後,在每次使用BulletAttack函式時,coolDown會自動等於當前時間加上attackRate0.5秒),而每一幀裡面,都需要進行判斷Time.time是否大於coolDown,如果小於的話就無法射擊,這樣就實現了冷卻的效果。

接下來需要考慮一個問題,那就是子彈的發射方向的問題,因為現在我們的子彈只能向右邊發射,很不科學。所以為了解決這個問題,我參考的原視訊教程裡面將PlayerAttack指令碼整個整合到Controller2D裡面了。然後在外面的Controller2D位置需要重新連線一次bulletprefab


接下來對Controller2D指令碼進行一些加工,使我們的子彈可以向左邊發射:

using UnityEngine;
  using System.Collections;
  
  public class Controller2D : MonoBehaviour {
  	//引用CharacterController
  	CharacterController characterController;
  	//重力
  	public float gravity = 10;
  	//水平移動的速度
  	public float walkSpeed = 5;
  	//彈跳高度
  	public float jumpHeight = 5;
  
  	//顯示角色當前正受到攻擊
  	float takenDamage = 0.2f;
  
  	// 控制角色的移動方向
  	Vector3 moveDirection = Vector3.zero;
  	float horizontal = 0;
  	//原PlayerAttack腳本里面的變數,把那個指令碼和當前指令碼合併,PlayerAttack指令碼已經刪除。
  	public Rigidbody bulletPrefab;
  	float attackRate = 0.5f;
  	float coolDown;
  	bool lookRight = true;
  	// Use this for initialization
  	void Start () {
  		characterController = GetComponent<CharacterController>();
  
  	}
  	
  	// Update is called once per frame
  	void Update () {
  		//控制角色的移動
  		characterController.Move (moveDirection * Time.deltaTime);
  		horizontal = Input.GetAxis("Horizontal");
  		//控制角色的重力
  		moveDirection.y -= gravity * Time.deltaTime;
  
  		if (horizontal == 0) {
  			moveDirection.x = horizontal;		
  		}
  
  
  		//控制角色右移(按d鍵和右鍵時)  在這裡不直接使用0而是用0.01f是因為使用0之後會持續移動,無法靜止
  		if (horizontal > 0.01f) {
  			lookRight = true;
  			moveDirection.x = horizontal * walkSpeed;
  		}
  		//控制角色左移(按a鍵和左鍵時)
  		if (horizontal < -0.01f) {
  			lookRight = false;
  			moveDirection.x = horizontal * walkSpeed;
  		}
  		// 彈跳控制
  		if (characterController.isGrounded) {
  			if(Input.GetKeyDown(KeyCode.Space)||Input.GetKeyDown(KeyCode.K)){
  				moveDirection.y = jumpHeight;
  			}
  		}
  		//原PlayerAttack的函式
  		if (Time.time >= coolDown) {
  			if (Input.GetKeyDown (KeyCode.J)){
  				BulletAttack ();	
  			}
  		}
  	}
  	//原PlayerAttack的函式,現在和當前指令碼合併了。
  	//按下攻擊按鍵時建立子彈的prefab,也就是bulletPrefab。
  	void BulletAttack(){
  		if (lookRight) {
  			//下面的這句話非常經典,利用as Rigidbody將Instantiate的GameObject強制轉換為Rigidbody型別。
  			Rigidbody bPrefab = Instantiate (bulletPrefab, transform.position, Quaternion.identity)as Rigidbody;
  			bPrefab.rigidbody.AddForce (Vector3.right * 500);
  			coolDown = Time.time + attackRate;
  				}
  		else {
  			//下面的這句話非常經典,利用as Rigidbody將Instantiate的GameObject強制轉換為Rigidbody型別。
  			Rigidbody bPrefab = Instantiate (bulletPrefab, transform.position, Quaternion.identity)as Rigidbody;
  			bPrefab.rigidbody.AddForce (-Vector3.right * 500);
  			coolDown = Time.time + attackRate;
  		}
  	}
  	
  	public IEnumerator TakenDamage(){
  		renderer.enabled = false;
  		yield return new WaitForSeconds(takenDamage);
  		renderer.enabled = true;
  		yield return new WaitForSeconds(takenDamage);
  		renderer.enabled = false;
  		yield return new WaitForSeconds(takenDamage);
  		renderer.enabled = true;
  		yield return new WaitForSeconds(takenDamage);
  		renderer.enabled = false;
  		yield return new WaitForSeconds(takenDamage);
  		renderer.enabled = true;
  		yield return new WaitForSeconds(takenDamage);
  	} 
  }

這個腳本里面主要增加了lookRight的布林值以及在發射子彈時的一些判定。另外用

if (horizontal == 0) {

moveDirection.x = horizontal;

}

這個if函式使角色在不移動的情況下保持靜止。

八、AI血值

接下來處理另一個問題,現在我們的敵人都是打一下就死掉的,但是很多橫版遊戲的敵人是非常彪悍的,並不是打一下就馬上掛掉。所以我們開始來給AI設定簡單的血值。

首先開啟Enemy2D指令碼,修改成下面這個樣紙:

using UnityEngine;
  using System.Collections;
  
  public class Enemy2D : MonoBehaviour {
  	//GameManager指令碼的參量
  	public GameManager gameManager;
  	//敵人移動的初始和停止位置,用於控制敵人在一定範圍內移動
  	float startPos;
  	float endPos;
  	//控制敵人向右移動的一個增量
  	public int unitsToMove = 5;
  	//敵人移動的速度
  	public int moveSpeed = 2;
  	//左右移動的布林值
  	bool moveRight = true;
  
  	//敵人的HP
  	int enemyHealth = 1;
  	//敵人的種類
  	public bool basicEnemy;
  	public bool advancedEnemy;
  
  	void Awake(){
  		startPos = transform.position.x;
  		endPos = startPos + unitsToMove;
  
  		if (basicEnemy) {
  			enemyHealth = 3;		
  		}
  
  		if (advancedEnemy) {
  			enemyHealth = 6;		
  		}
  	}
  	//此處這個Update函式用於控制敵人的左右移動,當向右移動到一定距離後就會反向移動,同理,左移一定距離之後也是。
  	void Update(){
  		        if (moveRight) {
  				rigidbody.position += Vector3.right * moveSpeed * Time.deltaTime;	
  				}
  				if (rigidbody.position.x >= endPos) {
  						moveRight = false;
  				}
  				if (moveRight==false) {
  						rigidbody.position -= Vector3.right * moveSpeed * Time.deltaTime;	
  				}
  				if (rigidbody.position.x <= startPos) {
  						moveRight = true;
  				}
  		}
  	
  
  	int damageValue = 1;
  	//這裡利用s