教程基於http://pixelnest.io/tutorials/2d-game-unity/ , 這個例子感覺還是比較經典的, 網上轉載的也比較多. 剛好最近也在學習U3D, 做的過程中自己又修改了一些地方, 寫篇文和大家一起分享下, 同時也加深記憶. 有什么紕漏的地方還請大家多包涵.
1.創建第一個場景
新建工程,在Project面板創建文件夾, 是為了更好的規划管理資源文件.
接着在Hierarchy面板上創建多個空對象(這樣的結構也是清晰了整個游戲的層次, 對象之間的關系一目了然), 這些空對象的Position保持(0,0,0)即可. 保存場景到Scenes文件夾中, 名稱為Stage1.
2.添加顯示背景
將背景圖片放入Textures文件夾, 確認這張圖片的紋理類型Texture Type為Sprite.
在場景中添加一個Sprite游戲對象,命名為Background1,設置Sprite屬性為剛才導入的背景圖片, 將它移動到0 - Background中, 設置Position為(0,0,0).
接着添加背景元素. 導入平台島嶼圖片到Textures文件夾, 選中Platforms圖片, 設置它的Sprite Mode為Multiple, 然后點擊Sprite Editor, 如下圖:
在彈出的Sprite Editor窗口中, 進行繪制每個平台島嶼的包圍矩形, 以便將紋理分隔成更小的部分. 然后分別命名為platform1和platform2.
創建一個新的Sprite對象, 設置它的Sprite為platform1. 然后同樣再創建一個Sprite對象, 設置Sprite為platform2. 將它們放置到1 - Middleground對象里, 並且確認他們的Z坐標為0. 設置完成后, 將這兩個對象從Hierarchy面板拖動到Project面板下的Prefabs文件夾, 保存為預制對象. 接着, 為了避免顯示順序問題, 修改下游戲對象的Z坐標, 如下所示:
Layer | Z Position |
0 - Background | 10 |
1 - Middleground | 5 |
2 - Foreground | 0 |
此時, 點擊Scene面板上的2D到3D視圖切換, 可以清除的看到層次:
3.創建玩家和敵人
導入主角圖片到Textures文件夾, 創建一個Sprite對象, 命名為Player, 設置其Sprite屬性為剛才導入的主角圖片. 將它放入2 - Foreground中, 設置Scale為(0.2,0.2,1). 接着, 為主角添加碰撞機, 點擊Add Component按鈕, 選擇Box Collider 2D, 設置Size為(10,10), 雖然大於實際區域, 但是已經比圖片小多了.
但是我更願意去使用Polygon Collider 2D來達到更精致的效果, 這里只是個例子, 大家可以自由選擇.
接着, 再為主角對象添加Rigidbody 2D剛體組件, 現在運行可以看到如下結果:
可以看到主角往下落了, 這是因為剛體帶有重力, 但在這個游戲中我們用不到重力, 將Gravity Scale設置為0即可. 另外, 不想因為物理而引起的主角旋轉, 則將Fixed Angles勾選上.
開始准備讓主角移動. 在Scripts文件夾中, 創建一個C#腳本, 名稱為PlayerScript, 實現讓方向鍵移動主角, 代碼如下:
using UnityEngine; using System.Collections; /// <summary> /// 玩家控制器和行為 /// </summary> public class PlayerScript : MonoBehaviour { #region 1 - 變量 /// <summary> /// 飛船移動速度 /// </summary> private Vector2 speed = new Vector2(50, 50); // 存儲運動 private Vector2 movement; #endregion // Update is called once per frame void Update() { #region 運動控制 // 2 - 獲取軸信息 float inputX = Input.GetAxis("Horizontal"); float inputY = Input.GetAxis("Vertical"); // 3 - 保存運動軌跡 movement = new Vector2(speed.x * inputX, speed.y * inputY); #endregion } void FixedUpdate() { // 4 - 讓游戲物體移動 rigidbody2D.velocity = movement; } }
這里以改變剛體的速率來達到主角移動的效果, 而不是通過直接改變transform.Translate, 因為那樣的話, 可能會不產生碰撞. 另外, 這里有人可能會疑問為什么實現移動的代碼要寫在FixedUpdate而不是Update中, 請看Update和FixedUpdate的區別: 傳送門.
現在將此腳本附加到主角對象上, 點擊運行, 方向鍵來控制移動.
接下來, 添加第一個敵人. 導入章魚敵人圖片到Textures文件夾, 創建一個Sprite對象, 命名為Poulpi, 設置Sprite為剛才導入的章魚圖片, 設置Scale為(0.4,0.4,1), 添加碰撞機(Polygon Collider 2D或Box Collider 2D都可以), 如果是Box Collider 2D, 設置Size為(4,4), 添加Rigidbody 2D組件, 設置Gravity Scale為0, 並且勾選Fixed Angles屬性. 設置完成后, 將對象保存為預制. 在這里只讓章魚簡單的往前行走, 創建一個腳本, 命名為MoveScript, 代碼如下:
using UnityEngine; using System.Collections; /// <summary> /// 當前游戲對象簡單的移動行為 /// </summary> public class MoveScript : MonoBehaviour { #region 1 - 變量 /// <summary> /// 物體移動速度 /// </summary> public Vector2 speed = new Vector2(10, 10); /// <summary> /// 移動方向 /// </summary> public Vector2 direction = new Vector2(-1, 0); private Vector2 movement; #endregion // Use this for initialization void Start() { } // Update is called once per frame void Update() { // 2 - 保存運動軌跡 movement = new Vector2(speed.x * direction.x, speed.y * direction.y); } void FixedUpdate() { // 3 - 讓游戲物體移動 rigidbody2D.velocity = movement; } }
將此腳本附加到章魚對象上, 現在運行嗎可以看到章魚往前移動, 如圖:
此時如果主角和章魚發生碰撞, 會互相阻塞對方的移動.
4.射擊
導入子彈圖片到"Textures"文件夾,創建一個Sprite游戲對象,命名為"PlayerShot",設置其"Sprite"屬性為剛才導入的圖片,設置"Scale"屬性為(0.75, 0.75, 1),添加"Rigidbody 2D"組件,其"Gravity Scale"屬性為0,並且勾選"Fixed Angles"屬性框,添加"Box Collider 2D"組件,其Size為(1, 1),並且勾選"IsTrigger"屬性。勾選"IsTrigger"屬性表示該碰撞體用於觸發事件,並將被物理引擎所忽略。意味着,子彈將穿過觸碰到的對象,而不會阻礙對象的移動,觸碰的時候將會引發"OnTriggerEnter2D"事件。創建一個腳本,命名為"ShotScript",代碼如下:
using UnityEngine; using System.Collections; /// <summary> /// 子彈行為 /// </summary> public class ShotScript : MonoBehaviour { #region 1 - 變量 /// <summary> /// 造成傷害 /// </summary> public int damage = 1; /// <summary> /// 子彈歸屬 , true-敵人的子彈, false-玩家的子彈 /// </summary> public bool isEnemyShot = false; #endregion // Use this for initialization void Start() { // 2 - 為避免任何泄漏,只給予有限的生存時間.[20秒] Destroy(gameObject, 20); } }
將此腳本附加到子彈對象上,然后將"MoveScript"腳本也附加到子彈對象上以便可以移動。保存此對象為預制。接着,讓碰撞產生傷害效果。創建一個腳本,命名為"HealthScript",代碼如下:
using UnityEngine; using System.Collections; /// <summary> /// 處理生命值和傷害 /// </summary> public class HealthScript : MonoBehaviour { #region 1 - 變量 /// <summary> /// 總生命值 /// </summary> public int hp = 1; /// <summary> /// 敵人標識 /// </summary> public bool isEnemy = true; #endregion /// <summary> /// 對敵人造成傷害並檢查對象是否應該被銷毀 /// </summary> /// <param name="damageCount"></param> public void Damage(int damageCount) { hp -= damageCount; if (hp <= 0) { // 死亡! 銷毀對象! Destroy(gameObject); } } void OnTriggerEnter2D(Collider2D otherCollider) { ShotScript shot = otherCollider.gameObject.GetComponent<ShotScript>(); if (shot != null) { // 判斷子彈歸屬,避免誤傷 if (shot.isEnemyShot != isEnemy) { Damage(shot.damage); // 銷毀子彈 // 記住,總是針對游戲的對象,否則你只是刪除腳本 Destroy(shot.gameObject); } } } }
將此腳本附加到Poulpi預制體上。現在運行,讓子彈和章魚碰撞,可以看到如下結果:
如果章魚的生命值大於子彈的傷害值,那么章魚就不會被消滅,可以試着通過改變章魚對象的"HealthScript"的hp值。
接着,我們來准備射擊。創建一個腳本,命名為"WeaponScript",代碼如下:
using UnityEngine; using System.Collections; /// <summary> /// 發射子彈 /// </summary> public class WeaponScript : MonoBehaviour { #region 1 - 變量 /// <summary> /// 子彈預設 /// </summary> public Transform shotPrefab; /// <summary> /// 兩發子彈之間的發射間隔時間 /// </summary> public float shootingRate = 0.25f; /// <summary> /// 當前冷卻時間 /// </summary> private float shootCooldown; #endregion // Use this for initialization void Start() { // 初始化冷卻時間為0 shootCooldown = 0f; } // Update is called once per frame void Update() { // 冷卻期間實時減少時間 if (shootCooldown > 0) { shootCooldown -= Time.deltaTime; } } /// <summary> /// 射擊 /// </summary> /// <param name="isEnemy">是否是敵人的子彈</param> public void Attack(bool isEnemy) { if (CanAttack) { if (isEnemy) { SoundEffectsHelper.Instance.MakeEnemyShotSound(); } else { SoundEffectsHelper.Instance.MakePlayerShotSound(); } shootCooldown = shootingRate; // 創建一個子彈 var shotTransform = Instantiate(shotPrefab) as Transform; // 指定子彈位置 shotTransform.position = transform.position; // 設置子彈歸屬 ShotScript shot = shotTransform.gameObject.GetComponent<ShotScript>(); if (shot != null) { shot.isEnemyShot = isEnemy; } // 設置子彈運動方向 MoveScript move = shotTransform.gameObject.GetComponent<MoveScript>(); if (move != null) { // towards in 2D space is the right of the sprite move.direction = this.transform.right; } } } /// <summary> /// 武器是否准備好再次發射 /// </summary> public bool CanAttack { get { return shootCooldown <= 0f; } } }
將這個腳本附加到主角對象上,設置其"Shot Prefab"屬性為"PlayerShot"預制體。打開"PlayerScript"腳本,在Update()方法里面,加入以下片段:
// Update is called once per frame void Update() { #region 射擊控制 // 5 - 射擊 bool shoot = Input.GetButtonDown("Fire1"); shoot |= Input.GetButtonDown("Fire2"); // 小心:對於Mac用戶,按Ctrl +箭頭是一個壞主意 if (shoot) { WeaponScript weapon = GetComponent<WeaponScript>(); if (weapon != null) { weapon.Attack(false); } } #endregion }
當收到射擊的按鈕狀態,調用Attack(false)方法。現在運行,可以看到如下結果:
接下來,准備創建敵人的子彈。導入敵人子彈圖片到"Textures"文件夾,選中"PlayerShot"預制體,按下Ctrl+D進行復制,命名為"EnemyShot1",然后改變其Sprite為剛才導入的圖片,設置其Scale為(0.35, 0.35, 1)。接着,讓章魚可以射擊。將"WeaponScript"腳本附加到章魚對象上,拖動"EnemyShot1"預制體到其"Shot Prefab"屬性,創建一個腳本,命名為"EnemyScript",用來簡單地每一幀進行自動射擊,代碼如下:
using UnityEngine; using System.Collections; /// <summary> /// 敵人通用行為 /// </summary> public class EnemyScript : MonoBehaviour { #region 變量 private WeaponScript weapon; #endregion void Awake() { // 只檢索一次武器 weapon = GetComponent<WeaponScript>(); } // Update is called once per frame void Update() { // 自動開火 if (weapon != null && weapon.CanAttack) { weapon.Attack(true); } } }
將此腳本附加到章魚對象上,現在運行,可以看到如下結果:
可以看到章魚向右射擊了子彈,這是因為代碼就是讓它那么做的。實際上,應該做到可以朝向任何方向射擊子彈。修改"WeaponScript"中的Attack方法,代碼為如下:
/// <summary> /// 射擊 /// </summary> /// <param name="isEnemy">是否是敵人的子彈</param> public void Attack(bool isEnemy) { if (CanAttack) { if (isEnemy) { SoundEffectsHelper.Instance.MakeEnemyShotSound(); } else { SoundEffectsHelper.Instance.MakePlayerShotSound(); } shootCooldown = shootingRate; // 創建一個子彈 var shotTransform = Instantiate(shotPrefab) as Transform; // 指定子彈位置 shotTransform.position = transform.position; // 設置子彈歸屬 ShotScript shot = shotTransform.gameObject.GetComponent<ShotScript>(); if (shot != null) { shot.isEnemyShot = isEnemy; } // 設置子彈方向 MoveScript move = shotTransform.gameObject.GetComponent<MoveScript>(); if (move != null) { // towards in 2D space is the right of the sprite //move.direction = move.direction; // 如果是敵人的子彈, 則改變方向和移動速度 if (shot.isEnemyShot) { move.direction.x = -1f; move.speed = new Vector2(15, 15); } else { move.direction.x = 1f; move.speed = new Vector2(10, 10); } } } }
可以適當調整子彈的移動速度,它應該快於章魚的移動速度。現在運行,如下圖所示:
目前,當主角和章魚碰撞時,僅僅只是阻礙對方的移動而已,在這里改成互相受到傷害。打開"PlayerScript"文件,添加如下代碼:
void OnCollisionEnter2D(Collision2D collision) { if (collision.gameObject.ToString().IndexOf("Poulpi") >= 0) { bool damagePlayer = false; // 與敵人發生碰撞 EnemyScript enemy = collision.gameObject.GetComponent<EnemyScript>(); if (enemy != null) { // 殺死敵人 HealthScript enemyHealth = enemy.GetComponent<HealthScript>(); if (enemyHealth != null) { enemyHealth.Damage(enemyHealth.hp); } damagePlayer = true; } // 玩家也受到傷害 if (damagePlayer) { HealthScript playerHealth = this.GetComponent<HealthScript>(); if (playerHealth != null) { playerHealth.Damage(1); } } } }
5.視差卷軸效果
為了達到這種視差卷軸的效果,可以讓背景層以不同的速度進行移動,越遠的層,移動地越慢。如果操作得當,這可以造成深度的錯覺,這將很酷,又是可以容易做到的效果。在這里存在兩個滾動:
- 主角隨着攝像機向前推進
- 背景元素除了攝像機的移動外,又以不同的速度移動
一個循環的背景將在水平滾動的時候,一遍又一遍的重復進行顯示。現有的層如下:
Layer | Loop | Position |
0 - Background | Yes | (0, 0, 10) |
1 - Middleground | No | (0, 0, 5) |
2 - Foreground | No | (0, 0, 0) |
接下來,實現無限背景。當左側的背景對象遠離了攝像機的左邊緣,那么就將它移到右側去,一直這樣無限循環,如下圖所示:
要做到檢查的對象渲染器是否在攝像機的可見范圍內,需要一個類擴展。創建一個C#文件,命名為"RendererExtensions.cs",代碼如下:
using UnityEngine; using System.Collections; /// <summary> /// 渲染擴展 /// </summary> public static class RendererExtensions { /// <summary> /// 檢查對象渲染器是否在攝像機的可見范圍內 /// </summary> /// <param name="renderer">渲染對象</param> /// <param name="camera">攝像機</param> /// <returns></returns> public static bool IsVisibleFrom(this Renderer renderer, Camera camera) { Plane[] planes = GeometryUtility.CalculateFrustumPlanes(camera); return GeometryUtility.TestPlanesAABB(planes, renderer.bounds); } }
接下來,可以開始實現不帶背景循環的滾動。創建一個腳本,命名為"ScrollingScript",代碼如下:
using UnityEngine; using System.Collections; using System.Collections.Generic; using System.Linq; /// <summary> /// 背景視差滾動腳本 /// </summary> public class ScrollingScript : MonoBehaviour { #region 變量 /// <summary> /// 滾動速度 /// </summary> public Vector2 speed = new Vector2(2, 2); /// <summary> /// 移動方向 /// </summary> public Vector2 direction = new Vector2(-1, 0); /// <summary> /// 相機是否運動 /// </summary> public bool isLinkedToCamera = false; /// <summary> /// 背景是否循環 /// </summary> public bool isLooping = false; /// <summary> /// 渲染對象名單 /// </summary> private List<Transform> backgroundPart; #endregion // Use this for initialization void Start() { // 只循環背景 if (isLooping) { // 獲取該層渲染器的所有子集對象 backgroundPart = new List<Transform>(); for (int i = 0; i < transform.childCount; i++) { Transform child = transform.GetChild(i); // 只添加可見子集 if (child.renderer != null) { backgroundPart.Add(child); } } // 根據位置排序 // Note: 根據從左往右的順序獲取子集對象 // 我們需要增加一些條件來處理所有可能的滾動方向。 backgroundPart = backgroundPart.OrderBy(t => t.position.x).ToList(); } } // Update is called once per frame void Update() { // 創建運動狀態 Vector3 movement = new Vector3(speed.x * direction.x, speed.y * direction.y, 0); movement *= Time.deltaTime; transform.Translate(movement); // 移動相機 if (isLinkedToCamera) { Camera.main.transform.Translate(movement); } // 循環 if (isLooping) { // 獲取第一個對象 // 該列表的順序是從左往右(基於x坐標) Transform firstChild = backgroundPart.FirstOrDefault(); if (firstChild != null) { // 檢查子集對象(部分)是否在攝像機前已准備好. // We test the position first because the IsVisibleFrom // method is a bit heavier to execute. if (firstChild.position.x < Camera.main.transform.position.x) { // 如果子集對象已經在攝像機的左側,我們測試它是否完全在外面,以及是否需要被回收. if (firstChild.renderer.IsVisibleFrom(Camera.main) == false) { // 獲取最后一個子集對象的位置 Transform lastChild = backgroundPart.LastOrDefault(); Vector3 lastPosition = lastChild.transform.position; Vector3 lastSize = (lastChild.renderer.bounds.max - lastChild.renderer.bounds.min); // 將被回收的子集對象作為最后一個子集對象 // Note: 當前只橫向滾動. firstChild.position = new Vector3(lastPosition.x + lastSize.x, firstChild.position.y, firstChild.position.z); // 將被回收的子集對象設置到backgroundPart的最后位置. backgroundPart.Remove(firstChild); backgroundPart.Add(firstChild); } } } } } }
在Start方法里,使用了LINQ將它們按X軸進行排序。
Layer | Speed | Direction | Linked to Camera |
0 - Background | (1, 1) | (-1, 0, 0) | No |
1 - Middleground | (2.5, 2.5) | (-1, 0, 0) | No |
Player | (1, 1) | (1, 0, 0) | Yes |
- 添加兩個天空背景到0 - Background
- 添加一些平台到2 - Middleground
- 添加更多的敵人到3 - Foreground,放置在攝像機的右邊
將"0 - Background"對象里的"ScrollingScript"組件的"Is Looping"屬性勾選,現在運行,就可以看到視差卷軸的效果,如下圖所示:
接下來,修改敵人腳本,讓敵人靜止不動,且無敵,直到攝像機看到它們。另外,當它們移出屏幕時,則立刻移除它們。修改"EnemyScript"腳本,代碼為如下:
using UnityEngine; using System.Collections; /// <summary> /// 敵人通用行為 /// </summary> public class EnemyScript : MonoBehaviour { #region 變量 /// <summary> /// 是否登場 /// </summary> private bool hasSpawn; private MoveScript moveScript; private WeaponScript weapon; #endregion void Awake() { // 只檢索一次武器 weapon = GetComponent<WeaponScript>(); // 當未登場的時候檢索腳本以禁用 moveScript = GetComponent<MoveScript>(); } // Use this for initialization void Start() { hasSpawn = false; // 禁止一切 // -- 碰撞機 collider2D.enabled = false; // -- 移動 moveScript.enabled = false; // -- 射擊 weapon.enabled = false; } // Update is called once per frame void Update() { // 檢查敵人是否登場 if (hasSpawn == false) { if (renderer.IsVisibleFrom(Camera.main)) { Spawn(); } } else { // 自動開火 if (weapon != null && weapon.enabled && weapon.CanAttack) { weapon.Attack(true); } // 超出攝像機視野,則銷毀對象 if (renderer.IsVisibleFrom(Camera.main) == false) { Destroy(gameObject); } } } /// <summary> /// 激活自身 /// </summary> private void Spawn() { hasSpawn = true; // 啟用一切 // -- 碰撞機 collider2D.enabled = true; // -- 移動 moveScript.enabled = true; // -- 射擊 weapon.enabled = true; } }
在游戲過程中,可以發現主角並不是限制在攝像機區域內的,可以隨意離開攝像機,現在來修復這個問題。打開"PlayerScript"腳本,在Update方法里面添加如下代碼:
#region 確保沒有超出攝像機邊界 var dist = (transform.position - Camera.main.transform.position).z; var leftBorder = Camera.main.ViewportToWorldPoint(new Vector3(0, 0, dist)).x; var rightBorder = Camera.main.ViewportToWorldPoint(new Vector3(1, 0, dist)).x; var topBorder = Camera.main.ViewportToWorldPoint(new Vector3(0, 0, dist)).y; var bottomBorder = Camera.main.ViewportToWorldPoint(new Vector3(0, 1, dist)).y; transform.position = new Vector3( Mathf.Clamp(transform.position.x, leftBorder, rightBorder), Mathf.Clamp(transform.position.y, topBorder, bottomBorder), transform.position.z ); #endregion
6.粒子效果
制作一個爆炸的粒子,用於敵人或者主角被摧毀時進行顯示。創建一個"Particle System",導入煙圖片到"Textures"文件夾,改變其"Texture Type"為"Texture",並且勾選"Alpha Is Transparent"屬性,附加這個紋理到粒子上,將其拖動到粒子對象上,更改其Shader為"Particles"→"Alpha Blended",接着更改一些屬性,如下所示:
Category | Parameter name | Value |
General | Duration | 1 |
General | Max Particles | 15 |
General | Start Lifetime | 1 |
General | Start Color | Gray |
General | Start Speed | 3 |
General | Start Size | 2 |
Emission | Bursts | 0 : 15 |
Shape | Shape | Sphere |
Color Over Lifetime | Color | 見下圖 |
Size Over Lifetime | Size | 見下圖 |
其中Color Over Lifetime要設置成在結束時,有個淡出的效果,如下圖所示:
Size Over Lifetime選擇一個遞減曲線,如下圖所示:
當調整完成后,取消勾選"Looping",現在粒子效果為如下:
保存成預制,命名為"SmokeEffect",放在"Prefabs/Particles"文件夾下。現在創建另一個粒子,火焰效果,使用默認材質即可。其他設置如下:
Category | Parameter name | Value |
General | Looping | false |
General | Duration | 1 |
General | Max Particles | 10 |
General | Start Lifetime | 1 |
General | Start Speed | 0.5 |
General | Start Size | 2 |
Emission | Bursts | 0 : 10 |
Shape | Shape | Box |
Color Over Lifetime | Color | 見下圖 |
其中Color Over Lifetime要設置成有一個黃色到橙色的漸變,最后淡出,如下圖所示:
粒子效果為:
保存成預制,命名為"FireEffect"。創建一個腳本,命名為"SpecialEffectsHelper",代碼如下:
using UnityEngine; using System.Collections; /// <summary> /// 從代碼中創建粒子特效 /// </summary> public class SpecialEffectsHelper : MonoBehaviour { /// <summary> /// Singleton /// </summary> public static SpecialEffectsHelper Instance; public ParticleSystem smokeEffect; public ParticleSystem fireEffect; void Awake() { // Register the singleton if (Instance != null) { Debug.LogError("Multiple instances of SpecialEffectsHelper!"); } Instance = this; } // Use this for initialization void Start() { } // Update is called once per frame void Update() { } /// <summary> /// 在給定位置創建爆炸特效 /// </summary> /// <param name="position"></param> public void Explosion(Vector3 position) { // 煙霧特效 instantiate(smokeEffect, position); // 火焰特效 instantiate(fireEffect, position); } /// <summary> /// 從預制體中實例化粒子特效 /// </summary> /// <param name="prefab"></param> /// <returns></returns> private ParticleSystem instantiate(ParticleSystem prefab, Vector3 position) { ParticleSystem newParticleSystem = Instantiate(prefab, position, Quaternion.identity) as ParticleSystem; // 確保它會被銷毀 Destroy(newParticleSystem.gameObject, newParticleSystem.startLifetime); return newParticleSystem; } }
這里創建了一個單例,可以讓任何地方都可以產生煙和火焰的粒子。將這個腳本附加到"Scripts"對象,設置其屬性"Smoke Effect"和"Fire Effect"為對應的預制體。現在是時候調用這個腳本了,打開"HealthScript"腳本文件,在OnTriggerEnter方法里面,更新成如下代碼:
/// <summary> /// 對敵人造成傷害並檢查對象是否應該被銷毀 /// </summary> /// <param name="damageCount"></param> public void Damage(int damageCount) { hp -= damageCount; if (hp <= 0) { // 爆炸特效 SpecialEffectsHelper.Instance.Explosion(transform.position); // 播放音效 SoundEffectsHelper.Instance.MakeExplosionSound(); // 死亡! 銷毀對象! Destroy(gameObject); } }
現在運行,射擊敵人,可以看到如下效果:
7.游戲音效
現在來添加一些聲音。將聲音資源放入"Sounds"文件夾,取消勾選每一個聲音的"3D sound"屬性,因為這是2D游戲。准備播放背景音樂,創建一個游戲對象,命名為"Music",其Position為(0, 0, 0),將背景音樂拖到這個對象上,然后勾選"Mute"屬性。因為音效總是在一定的時機進行播放,所以創建一個腳本文件,命名為"SoundEffectsHelper",代碼如下:
using UnityEngine; using System.Collections; /// <summary> /// 創建音效實例 /// </summary> public class SoundEffectsHelper : MonoBehaviour { /// <summary> /// 靜態實例 /// </summary> public static SoundEffectsHelper Instance; public AudioClip explosionSound; public AudioClip playerShotSound; public AudioClip enemyShotSound; void Awake() { // 注冊靜態實例 if (Instance != null) { Debug.LogError("Multiple instances of SoundEffectsHelper!"); } Instance = this; } public void MakeExplosionSound() { MakeSound(explosionSound); } public void MakePlayerShotSound() { MakeSound(playerShotSound); } public void MakeEnemyShotSound() { MakeSound(enemyShotSound); } /// <summary> /// 播放給定的音效 /// </summary> /// <param name="originalClip"></param> private void MakeSound(AudioClip originalClip) { // 做一個非空判斷, 防止異常導致剩余操作被中止 if (Instance.ToString() != "null") { // 因為它不是3D音頻剪輯,位置並不重要。 AudioSource.PlayClipAtPoint(originalClip, transform.position); } } }
這里我做了一個非空判斷, 因為按照原例中的程序, 當章魚死亡在攝像機邊界時, 會導致Script對象被銷毀! 從而引發程序異常. 我沒有找到被銷毀的准確原因, 所以暫時折中一下, 加了個判斷, 以確保程序能照常運行. 如果哪位大神有知道原因的話也歡迎告訴我來更新此文.
將此腳本附加到"Scripts"對象上,然后設置其屬性值,如下圖所示:
接着,在"HealthScript"腳本文件里,播放粒子效果后面,添加代碼:
SoundEffectsHelper.Instance.MakeExplosionSound();
在"WeaponScript"腳本文件里,Attack方法中,添加代碼:
if (isEnemy) { SoundEffectsHelper.Instance.MakeEnemyShotSound(); } else { SoundEffectsHelper.Instance.MakePlayerShotSound(); }
現在運行,就可以聽到聲音了。
8.菜單
創建簡單的菜單,以便游戲可以重新開始。導入背景圖片和LOGO圖片到"Textures"文件夾的子文件夾"Menu"。創建一個新的場景,命名為"Menu"。添加背景Sprite對象,其Position為(0, 0, 1),Size為(2, 2, 1)。添加LOGO的Sprite對象,其Position為(0, 2, 0),Size為(0.75, 0.75, 1)。添加一個空對象,命名為"Scripts",用來加載腳本。現在為這個啟動畫面,添加一個開始按鈕。創建一個腳本文件,命名為"MenuScript",代碼如下:
using UnityEngine; using System.Collections; /// <summary> /// Title screen script /// </summary> public class MenuScript : MonoBehaviour { // Use this for initialization void Start() { } // Update is called once per frame void Update() { } void OnGUI() { const int buttonWidth = 84; const int buttonHeight = 60; // 在開始游戲界面繪制一個按鈕 if ( // Center in X, 2/3 of the height in Y GUI.Button(new Rect(Screen.width / 2 - (buttonWidth / 2), (2 * Screen.height / 3) - (buttonHeight / 2), buttonWidth, buttonHeight), "開始游戲") ) { // On Click, load the first level. // "Stage1" is the name of the first scene we created. Application.LoadLevel("Stage1"); } } }
將此腳本附加到"Scripts"對象上。現在運行,可以看到如下效果:
但是,點擊按鈕會崩潰,因為沒有將Stage1場景添加進來。打開"File"→"Build Settings",將場景"Menu"和"Stage1"拖動到上面的"Scenes In Build"里面。再次運行,就可以看到按鈕正常切換場景了。當主角被摧毀時,需要可以重新開始游戲。創建一個腳本文件,命名為"GameOverScript",代碼如下:
using UnityEngine; using System.Collections; /// <summary> /// 開始或退出游戲 /// </summary> public class GameOverScript : MonoBehaviour { // Use this for initialization void Start() { } // Update is called once per frame void Update() { } void OnGUI() { const int buttonWidth = 120; const int buttonHeight = 60; // 在x軸中心, y軸1/3處創建"重來"按鈕 if (GUI.Button(new Rect(Screen.width / 2 - (buttonWidth / 2), (1 * Screen.height / 3) - (buttonHeight / 2), buttonWidth, buttonHeight), "再來一次")) { // 重新加載游戲場景 Application.LoadLevel("Stage1"); } // x軸中心, y軸2/3處創建"返回菜單"按鈕 if (GUI.Button(new Rect(Screen.width / 2 - (buttonWidth / 2), (2 * Screen.height / 3) - (buttonHeight / 2), buttonWidth, buttonHeight), "返回菜單")) { // 加載菜單場景 Application.LoadLevel("Menu"); } } }
在主角死亡的時候,調用這個腳本。打開"PlayerScript"文件,添加如下代碼:
void OnDestroy() { // Game Over. // Add the script to the parent because the current game // object is likely going to be destroyed immediately. transform.parent.gameObject.AddComponent<GameOverScript>(); }
現在運行,當死亡時,就會出現按鈕,如下圖所示:
9.代碼創建平台島嶼和敵人
現在, 一個簡單的橫版射擊小游戲已經有雛形了, 然后手動創建有限的島嶼和敵人畢竟有耗盡的時候. 這個時候我們可以在代碼中動態的去創建敵人和島嶼, 這樣只要玩家還存活, 就會一直有敵人出現, 有點無盡版的意思.
創建一個腳本文件,命名為"MakePlatformScript",代碼如下:
using UnityEngine; using System.Collections; /// <summary> /// 制造平台 /// </summary> public class MakePlatformScript : MonoBehaviour { /// <summary> /// 平台預設體1 /// </summary> public Transform platform1Prefab; /// <summary> /// 平台預設體2 /// </summary> public Transform platform2Prefab; // Use this for initialization void Start() { } // Update is called once per frame void Update() { if (transform.childCount < 4) { if (Random.Range(0, 2) > 0) { CreatePlatform1(); } else { CreatePlatform2(); } } } void CreatePlatform1() { var platformTransform = Instantiate(platform1Prefab) as Transform; platformTransform.position = new Vector3(Camera.main.transform.position.x + Random.Range(14, 23), Random.Range(-3, 3), 5); platformTransform.transform.parent = transform; } void CreatePlatform2() { var platformTransform = Instantiate(platform2Prefab) as Transform; platformTransform.position = new Vector3(Camera.main.transform.position.x + Random.Range(14, 23), Random.Range(-3, 3), 5); platformTransform.transform.parent = transform; } }
將MakePlatformScript附加到1 - Middleground, 設置它的預制體為對應的平台島嶼預制體.
接着繼續創建一個腳本文件,命名為"MakeEnemyScript",代碼如下:
using UnityEngine; using System.Collections; /// <summary> /// 制造敵人 /// </summary> public class MakeEnemyScript : MonoBehaviour { /// <summary> /// 敵人預設體 /// </summary> public Transform enemyPrefab; // Use this for initialization void Start() { } // Update is called once per frame void Update() { if (transform.childCount < 2) { CreateEnemy(); } } /// <summary> /// 創建敵人 /// </summary> void CreateEnemy() { var enemyTransform = Instantiate(enemyPrefab) as Transform; enemyTransform.position = new Vector3(Camera.main.transform.position.x + 15, Random.Range(-4, 4), 0); enemyTransform.transform.parent = transform; MoveScript move = enemyTransform.gameObject.GetComponent<MoveScript>(); if (move != null) { move.direction.x = -1f; move.speed = new Vector2(3, 3); } } }
將MakeEnemyScript附加到2 - Foreground, 同樣, 設置對應的預制體為章魚.
現在再次運行, 就會看到系統自動創建的章魚敵人和漂浮島嶼了!
到這里, 整個游戲就完成了, 推薦大家可以去頁首找原文鏈接去看看, 老外的原文解說的更詳細, 我也是在原文的基礎上自己再次整理.
最后附上程序源碼, 文中有紕漏或看不太明白的地方大家可以對照着源碼一起看, 歡迎留言指教.