好友,你們好,歡迎關注個人博客。我是秦培,個人博客地址blog.csdn.net/qinyuanpei。數據庫
今天,我想分享的是,如今在移動平臺上的檢查點系統更受歡迎,機遊戲如《憤慨的小鳥》、《保衛蘿蔔》中對遊戲內容的組織形式,玩家可經過已解鎖的關卡(默認第一關是已解鎖的)獲取分數進而解鎖新的關卡,或者是經過付費購買解鎖新的關卡。那麼好了,在今天的文章中博主將帶領你們高速實現一個可擴展的關卡系統,這個實例的靈感來自博主近期的工做經歷,但願對你們學習Unity3D遊戲起到必定幫助性的做用。markdown
在本地配置一個Xml文件。在這個文件裏定義當前遊戲中關卡的相關信息。經過解析該文件並和UI綁定終於實現一個完整的關卡系統。網絡
首先咱們來定義一個關卡的基本結構:app
public class Level
{
/// <summary>
/// 關卡ID
/// </summary>
public string ID;
/// <summary>
/// 關卡名稱
/// </summary>
public string Name;
/// <summary>
/// 關卡是否解鎖
/// </summary>
public bool UnLock = false;
}
在這裏,咱們假定關卡的名稱和該關卡在Unity3D中場景名稱一致。當中最爲重要的一個屬性是UnLock。該值是一個布爾型變量,代表該關卡是否解鎖。因爲在遊戲中。僅僅有解鎖的場景是可以訪問的。編輯器
從關卡的基本結構Level可以定義出例如如下的配置文件,這裏使用Xml做爲配置文件的存儲形式:學習
spa
<?xml version="1.0" encoding="utf-8"?> <levels> <level id="0" name="level0" unlock="1" /> <level id="1" name="level1" unlock="0" /> <level id="2" name="level2" unlock="0" /> <level id="3" name="level3" unlock="0" /> <level id="4" name="level4" unlock="0" /> <level id="5" name="level5" unlock="0" /> <level id="6" name="level6" unlock="0" /> <level id="7" name="level7" unlock="0" /> <level id="8" name="level8" unlock="0" /> <level id="9" name="level9" unlock="0" /> </levels>
和關卡結構定義類似,這裏使用0和1來表示關卡的解鎖狀況。0表示未解鎖。1表示解鎖,可以注意到默認狀況下第一個關卡是解鎖的。這符合咱們在玩《憤慨的小鳥》這類遊戲時的直觀感覺。那麼好了,在完畢了關卡的結構定義和配置文件定義後,接下來咱們開始思考怎樣來實現一個關卡系統,因爲此處並不涉及到Unity3D場景中的詳細邏輯,所以咱們在關卡系統中基本的工做就是維護好主界面場景和各個遊戲場景的跳轉關係,咱們可以注意到這裏要完畢兩件事情,即第一要將配置文件裏的關卡以必定形式載入到主界面中,並告訴玩家哪些關卡是已解鎖的、哪些關卡是未解鎖的。當玩家點擊不一樣的關卡時可以獲得不一樣的響應,已解鎖的關卡可以訪問並進入遊戲環節,未解鎖的關卡則需要得到不少其它的分數或者是經過付費來解鎖關卡;第二是要對關卡進行編輯,當玩家得到了分數或者是支付必定的費用後可以解鎖關卡進入遊戲環節。.net
這兩點綜合起來就是咱們需要對關卡的配置文件進行讀寫,因爲咱們注意到一個關卡是否解鎖僅僅取決於unlock屬性。那麼好了,明確了這一點後咱們來動手編寫一個維護關卡的類。插件
這裏直接給出代碼。因爲從嚴格的意義上來講。這段代碼並非咱們此刻關注的重點。可能這讓你們感到難以適應,因爲文章明明就是在教咱們實現一個關卡系統。可是此刻博主卻說這部分不重要了,請你們稍安勿躁。因爲這裏有比代碼更爲深入的東西。設計
using UnityEngine;
using System.Collections;
using System.Collections.Generic;
using System.Xml;
public static class LevelSystem
{
/// <summary>
/// 載入Xml文件
/// </summary>
/// <returns>The levels.</returns>
public static List<Level> LoadLevels()
{
//建立Xml對象
XmlDocument xmlDoc = new XmlDocument();
//假設本地存在配置文件則讀取配置文件
//不然在本地建立配置文件的副本
//爲了跨平臺及可讀可寫,需要使用Application.persistentDataPath
string filePath = Application.persistentDataPath + "/levels.xml";
if (!IOUntility.isFileExists (filePath)) {
xmlDoc.LoadXml (((TextAsset)Resources.Load ("levels")).text);
IOUntility.CreateFile (filePath, xmlDoc.InnerXml);
} else {
xmlDoc.Load(filePath);
}
XmlElement root = xmlDoc.DocumentElement;
XmlNodeList levelsNode = root.SelectNodes("/levels/level");
//初始化關卡列表
List<Level> levels = new List<Level>();
foreach (XmlElement xe in levelsNode)
{
Level l=new Level();
l.ID=xe.GetAttribute("id");
l.Name=xe.GetAttribute("name");
//使用unlock屬性來標識當前關卡是否解鎖
if(xe.GetAttribute("unlock")=="1"){
l.UnLock=true;
}else{
l.UnLock=false;
}
levels.Add(l);
}
return levels;
}
/// <summary>
/// 設置某一關卡的狀態
/// </summary>
/// <param name="name">關卡名稱</param>
/// <param name="locked">是否解鎖</param>
public static void SetLevels(string name,bool unlock)
{
//建立Xml對象
XmlDocument xmlDoc = new XmlDocument();
string filePath=Application.persistentDataPath + "/levels.xml";
xmlDoc.Load(filePath);
XmlElement root = xmlDoc.DocumentElement;
XmlNodeList levelsNode = root.SelectNodes("/levels/level");
foreach (XmlElement xe in levelsNode)
{
//依據名稱找到相應的關卡
if(xe.GetAttribute("name")==name)
{
//依據unlock又一次爲關卡賦值
if(unlock){
xe.SetAttribute("unlock","1");
}else{
xe.SetAttribute("unlock","0");
}
}
}
//保存文件
xmlDoc.Save (filePath);
}
}
這裏咱們首先將關卡配置文件levels.xml放置在Resources文件夾下,這是因爲咱們可以使用Resources.Load()這樣的方式來載入本地資源。這樣的方式對於Unity3D來講有着得天獨厚的優點:
* 它使用相對於Resources文件夾的相對路徑,因此在使用的時候不用考慮是相對路徑仍是絕對路徑的問題
* 它使用名稱來查找一個本地資源。因此在使用的時候不用考慮擴展名和文件格式的問題
* 它可以是Unity3D支持的隨意類型。從貼圖到預製體再到文本文件等等。可以和Unity3D的API完美地結合
說了這麼多它的長處,咱們天然要痛心疾首地說說它的缺點。它的缺點是什麼呢?那就是不支持寫入操做。這固然不能責備Unity3D,因爲當Unity3D導出遊戲的時候會將Rsources文件夾下的內容壓縮後再導出,咱們固然不能要求在一個壓縮後的文件裏支持寫入操做啦,因此咱們是時候來總結下Unity3D中資源讀寫的常見方案了,那麼Unity3D中常見的資源讀寫方案由哪些呢?
一、Resources.Load:僅僅讀,當咱們的資源不需要更新且對本地存儲無容量要求的時候可以採用這樣的方式
二、AssetBundle:僅僅讀。當咱們的資源需要更新且對本地存儲有容量要求的時候可以採用這樣的方式
三、WWW:僅僅讀。WWW支持http協議和file協議,所以可以WWW來載入一個網絡資源或者本地資源
四、PlayerPrefs:可讀可寫,Unity3D提供的一種的簡單的鍵-值型存儲結構,可以用來讀寫float、int和string三種簡單的數據類型,是一種較爲鬆散的數據存儲方案
五、序列化和反序列化:可讀可寫,可以使用Protobuf、序列化爲Xml、二進制或者JSON等形式實現資源讀寫。
六、數據庫:可讀可寫,可以使用MySQL或者SQLite等數據庫對數據進行存儲實現資源讀寫。
好了,在瞭解了Unity3D中資源讀寫的常見方案後,咱們接下來來討論下Unity3D中的路徑問題:
一、Application.dataPath:這個路徑是咱們經常使用的一個路徑,可是咱們真的瞭解這個路徑嗎?我看這裏要打個大大的問號。爲何這麼說呢?因爲這個路徑在不一樣的平臺下是不同的。從官方API文檔中可以瞭解到這個值依賴於執行的平臺:
* Unity 編輯器:<project文件夾的路徑>/Assets
* Mac:<到播放器應用的路徑>/Contents
* IOS: <到播放器應用的路徑>/
using UnityEngine;
using System.Collections;
using System.Collections.Generic;
using UnityEngine.UI;
using System.Xml.Serialization;
public class Main : MonoBehaviour
{
//關卡列表
private List<Level> m_levels;
void Start ()
{
//獲取關卡
m_levels = LevelSystem.LoadLevels ();
//動態生成關卡
foreach (Level l in m_levels)
{
GameObject prefab=(GameObject)Instantiate((Resources.Load("Level") as GameObject));
//數據綁定
DataBind(prefab,l);
//設置父物體
prefab.transform.SetParent(GameObject.Find("UIRoot/Background/LevelPanel").transform);
prefab.transform.localPosition=new Vector3(0,0,0);
prefab.transform.localScale=new Vector3(1,1,1);
//將關卡信息傳給關卡
prefab.GetComponent<LevelEvent>().level=l;
prefab.name="Level";
}
//人爲解鎖第二個關卡
//在實際遊戲中玩家需要知足必定條件方可解鎖關卡
//此處僅做爲演示
LevelSystem.SetLevels ("level1", true);
}
/// <summary>
/// 數據綁定
/// </summary>
void DataBind(GameObject go,Level level)
{
//爲關卡綁定關卡名稱
go.transform.Find("LevelName").GetComponent<Text>().text=level.Name;
//爲關卡綁定關卡圖片
Texture2D tex2D;
if(level.UnLock){
tex2D=Resources.Load("nolocked") as Texture2D;
}else{
tex2D=Resources.Load("locked") as Texture2D;
}
Sprite sprite=Sprite.Create(tex2D,new Rect(0,0,tex2D.width,tex2D.height),new Vector2(0.5F,0.5F));
go.transform.GetComponent<Image>().sprite=sprite;
}
}
在這段腳本中,咱們首先載入了關卡信息,而後將關卡信息和界面元素實現綁定。從而實現一個簡單的關卡選擇界面,並人爲地解鎖了第二個關卡。好吧。假設這是一個正式遊戲的配置關卡配置文件。相信你們都知道怎麼免費玩解鎖的關卡了吧。哈哈!固然。我不推薦你們這樣作。因爲做爲一個程序猿,當你全身心地投入到一個項目中的時候,你就會明確完畢一款軟件或者遊戲需要投入多少精力。因此你們儘可能仍是不要想破解或者盜版這些這些事情,畢竟做爲開發人員可能他的出發點是想作出來一個讓你們都喜歡的產品,可是更現實的問題是開發人員同樣要生活,因此請善待他們吧。好了。言歸正傳,這裏的UI都是基於UGUI實現的,不要問我爲何不用NGUI,因爲我就是喜歡UGUI!
咱們知道咱們需要爲每個關卡的UI元素綁定一個響應的事件。所以咱們需要爲其編寫一個LevelEvent的腳本:
using UnityEngine;
using System.Collections;
using UnityEngine.UI;
using UnityEngine.EventSystems;
public class LevelEvent : MonoBehaviour
{
//當前關卡
public Level level;
public void OnClick()
{
if(level.UnLock){
//假設關卡的名稱即爲相應場景的名稱
//Application.LoadLevel(level.Name);
Debug.Log ("當前選擇的關卡是:"+level.Name);
}else{
Debug.Log ("抱歉!當前關卡還沒有解鎖!");
}
}
}
記得在本文開始的時候,博主提到了一個假設,就是關卡的名稱和其相應的遊戲名稱一致的假設,相信到此處你們都知道爲何了吧!爲了讓每個關卡的UI元素知道本身相應於哪一個關卡。咱們設置了一個level變量。這個變量的值在載入關卡的時候已經完畢了初始化,因此此時咱們可以在這裏知道每個關卡的詳細信息,從而完畢事件的響應。
好了。今天的內容就是這樣了。咱們來看看終於的效果吧。
可以注意到在第二次打開遊戲後,第二個關卡已經解鎖了,說明咱們在最開始設計的兩個目標都達到了,那麼內容就是這樣子啦,假設你們有什麼好的想法或者建議。歡迎在文章後面給我留言,謝謝你們!
2015年12月1日更新:
近期有朋友反映需要我給出IOHelper這個類的實現。可是其實這個類僅僅是對System.IO這個命名空間下的相關方法進行了簡單的封裝,我一直認爲不管學習什麼技術。咱們都要有一個完整的系統性的學習路線。
儘管Unity3D入門十分簡單,可說究竟它本質上仍然是C#的技術範疇,因此咱們不能把眼光侷限在Unity3D這個引擎和它豐富的插件上面,學插件、使用插件只是是邯鄲學步。真正能讓你成長的倒是本身掌握的知識。我知道我說這些,大部分人都不會看,因爲你認爲來看我博客是給我面子,直接抄你博客裏的代碼是看得起你。
呵呵,我寫這個博客的目的可不是爲了給懶人提供代碼庫,懶人真正的代碼庫是Github。罷了。給這些懶人給出源碼吧!
/* * Unity3D腳本(C#) * Author: * Date: */
using UnityEngine;
using System.Collections;
using System.IO;
public static class IOUntility
{
/// <summary>
/// 建立文件夾
/// </summary>
/// <param name="path">文件夾路徑</param>
public static void CreateFolder(string path)
{
if(!Directory.Exists(path)){
Directory.CreateDirectory(path);
}
}
/// <summary>
/// 建立文件
/// </summary>
/// <param name="filePath">文件路徑</param>
/// <param name="content">文件內容</param>
public static void CreateFile(string filePath,string content)
{
//文件流
StreamWriter writer;
//推斷文件文件夾是否存在
//不存在則先建立文件夾
Debug.Log (filePath);
string folder = filePath.Substring (0, filePath.LastIndexOf ("//"));
CreateFolder (folder);
//假設文件不存在則建立。存在則追加內容
FileInfo file=new FileInfo(filePath);
if(!file.Exists){
writer=file.CreateText();
}else{
file.Delete();
writer=file.CreateText();
}
//寫入內容
writer.Write(content);
writer.Close();
writer.Dispose();
}
/// <summary>
/// 推斷文件是否存在
/// </summary>
/// <param name="path">文件路徑</param>
public static bool isFileExists(string path)
{
FileInfo file=new FileInfo(path);
return file.Exists;
}
public static void DeleteFile(string fileName)
{
if(!File.Exists(fileName)) return;
File.Delete(fileName);
}
}