---恢復內容開始---
仿LOL項目開發第一天
by---草帽
項目源碼研究群:539117825
最近看了一個類似LOL的源碼,頗有心得,所以今天呢,我們就來自己開發一個類似於LOL的游戲demo。
可能項目持續的時間會比較久,主要是現在還在上學,所以基本上是在擠出一點課余時間來寫的博客。
如果項目更新慢,還請各位諒解。
這個項目呢,大家可以跟着我的步驟一起做。博客上我會盡量的詳細的教大家如何制作一款商業游戲。
OK,回歸正題。現在我們來做游戲的前期准備工作:
1.Unity3d--->版本5.0以上,我用的5.3.1版本
2.Eclipse---->版本隨意,但是jdk的版本要1.7以上
3.php+mysql+apache,可以去網上搜下:WampServer,里面集成了這些工具。
正式開始:
1.打開Unity5,新創建一個項目,取名為LOLGameDemo:

2.創建文件夾用來存放各種資源,比如Resources,Scripts,Scenes等,然后導入插件NGUI。
這里我用的是3.9.0版本的。
3.制作一個新的場景,我們取名為Login,存放在Scenes文件目錄下,為什么取名為Login,就像LOL一樣,我們一打開游戲是不是就是登陸界面。可能有些童鞋會問,不是還有更新嗎,沒有錯,我們把更新部分的代碼,集成到了Login場景中。

4.編寫腳本,這是我們程序的第一個腳本,第一個腳本通常來做什么?
沒錯就是驅動其他腳本的執行,比如檢測更新,資源加載等等等等。
那么,我們在Scripts文件下創建:LOLGameDriver.cs驅動腳本,然后在Hierachy窗口創建一個空物體,取名為LOLGameDriver,來存放這個腳本。


打開編輯腳本:
由於是驅動器,在整個游戲中,肯定只需用到一個,所以我們得設計成單例。
using UnityEngine;
using System.Collections;
/// <summary>
/// 驅動腳本
/// </summary>
public class LOLGameDriver : MonoBehaviour
{
/// <summary>
/// 靜態單例屬性
/// </summary>
public static LOLGameDriver Instance
{
get;
set;
}
void Awake ()
{
//如果單例不為空,說明存在兩份的單例,就刪除一份
if (Instance != null)
{
Destroy(this.gameObject);
return;
}
Instance = this;//初始化單例
DontDestroyOnLoad(this.gameObject);
Application.runInBackground = true;//可以在后台運行
Screen.sleepTimeout = SleepTimeout.NeverSleep;//設置屏幕永遠亮着
}
void Start ()
{
}
void Update ()
{
}
}
5.編寫檢測版本更新的情況。
在編寫代碼之前,我們先來制作登陸界面,不然運行的時候空白的界面顯得不好看。
這里我創建了一個Temp來存放臨時的Textures,因為我們界面用到的只是圖集,並不是這些textures,所以制作完圖集之后,就可以直接刪了。

我們打開NGui的制作圖集的工具,然后制作Login.altas圖集,存放在新建的Altas文件夾下面:

關於登錄界面的Textures,我會在文章的最后部分提供鏈接。
制作玩圖集之后,我們開始拼湊界面。不論怎么說制作界面是最煩的時候,也是最浪費時間。

這個是我隨手搭建的登陸界面:這里主要分兩塊
1.LoginFrame---->就是整體的框架,不包括右邊有用戶名輸入的UI
2.Login------>這個是右邊有用戶名輸入框的UI

為什么要分這兩部分,你想想看,如果LOL游戲要更新的時候,是不是就只有背景圖片,並沒有用戶名輸入框那個UI。所以我們要獨立出用戶名那個UI,動態來加載他。
那么,我們就要把它制作成Prefab。
在Resources文件下,創建Guis文件目錄,然后拖拽Login到文件中。

具體界面怎么制作,你們自己搞,我這里就不再詳細的講解。
OK,那么接下來,我們開始編寫程序。
回到LOLGameDriver腳本內,新建一個public方法,取名為TryInit();主要是用來檢測是否有網絡和版本更新。
首先是網絡是否可行的檢測:
我們新建一個類:CheckTimeout.cs專門用來檢測網絡是否良好。
在寫之前,我們考慮下,這個類是用來檢測網絡性能,而其中需要有下載功能,所以違背了類的單一職責原則,我們設計的類的時候,盡量不要讓他太過於復雜。
所以處理下載功能的,我們專門寫個DownloadMgr.cs來處理。
我們新建一個下載類,由於你的下載管理器也肯定是只在內存中存在一份,所以我們設計成單例:
using UnityEngine;
using System.Collections;
using System;
using System.Net;
public class DownloadMgr
{
private static DownloadMgr m_oInstance;
private WebClient m_oWebClient;
public static DownloadMgr Instance
{
get
{
if (m_oInstance == null)
{
m_oInstance = new DownloadMgr();
}
return m_oInstance;
}
}
public DownloadMgr()
{
this.m_oWebClient = new WebClient();
}
/// <summary>
/// 異步下載網頁文本
/// </summary>
/// <param name="url"></param>
/// <param name="AsynResult"></param>
/// <param name="onError"></param>
public void AsynDownLoadHtml(string url, Action<string> AsynResult, Action onError)
{
Action action = () =>
{
string text = DownLoadHtml(url);
if (string.IsNullOrEmpty(text))
{
if (onError != null)
{
onError();
}
}
else
{
if (AsynResult != null)
{
AsynResult(text);
}
}
};
//開始異步下載
action.BeginInvoke(null, null);
}
/// <summary>
/// 下載網頁的文本
/// </summary>
/// <param name="url"></param>
/// <returns></returns>
public string DownLoadHtml(string url)
{
try
{
return this.m_oWebClient.DownloadString(url);
}
catch (Exception e)
{
Debug.LogException(e);
return string.Empty;
}
}
}
只要在CheckTImeout類里面調用DownloadMgr的AsynDownloadHtml()方法,就可以進行異步下載,然后初始化帶參的委托。我們看看CheckTimeout.cs代碼:
using UnityEngine;
using System.Collections;
using System;
/// <summary>
/// 檢測網絡是否超時類
/// </summary>
public class CheckTimeout
{
/// <summary>
/// 是否網絡超時,這里使用百度做測試
/// </summary>
/// <param name="AsynResult"></param>
public void AsynIsNetworkTimeout(Action<bool> AsynResult)
{
TryAsynDownloadHtml("http://www.baidu.com", AsynResult);
}
private void TryAsynDownloadHtml(string url, Action<bool> AsynResult)
{
DownloadMgr.Instance.AsynDownLoadHtml(url, (text) =>
{
if (string.IsNullOrEmpty(text))
{
AsynResult(false);
}
else
{
AsynResult(true);
}
}, () => { AsynResult(false); });
}
}
再回到LOLGameDriver腳本:在TryInit()方法里面編寫代碼:
public void TryInit()
{
//說明網絡可以
if (Application.internetReachability != NetworkReachability.NotReachable)
{
CheckTimeout checkTimeout = new CheckTimeout();
checkTimeout.AsynIsNetworkTimeout((result) =>
{
//網絡良好
if (result)
{
//開始更新檢測
}
else //說明網絡錯誤
{
//開始消息提示框,重試和退出
}
});
}
}
可能讀者看到這樣的代碼,就是委托比較多的代碼,頭就很暈。其實委托很簡單,你們只要記住,委托就是方法指針。用來干嘛,解耦和用的。
你看如果不用委托,是不是LOLGameDriver得注入到CheckTimeout和DownloadMrg里面充當依賴,但是委托直接把LOLGameDriver的里面的匿名委托當做方法指針傳遞到DownloadMrg里面執行。
OK,講完網絡監測之后,我們來講講版本檢測更新。
我們在LOLGameDriver的網絡良好的判斷里面,新增一個方法:DoInit();
那么我們知道,所謂的版本更新,無非就是服務端的版本信息和客戶端版本信息的對照。
那么客戶端的版本信息保存在哪里?沒錯就是Application.persistentDataPath這個持久文件路徑。
所以我們新建一個類,專門管理這些與系統有關的路徑:SystemConfig.cs:
using UnityEngine;
using System.Collections;
/// <summary>
/// 系統參數配置
/// </summary>
public class SystemConfig
{
public readonly static string VersionPath = Application.persistentDataPath + "/version.xml";
}
里面存放的是本地版本信息的xml文件路徑:VersionPath
因為涉及到版本控制,所以我們得有個VersionManager單例來管理。
新建一個VersionManager.cs腳本:
有沒有突然發現,幾乎所有的管理器都是單例模式的,你我們每個管理器都需要寫個的單例,那不是特別的麻煩,所以呢,這里教大家一個小技巧:繼承單例。
我們寫個抽象單例的父類,放在命名空間:Game下面。
using System;
using System.Threading;
namespace Game
{
public class Singleton<T> where T : new()
{
private static T s_singleton = default(T);
private static object s_objectLock = new object();
public static T singleton
{
get
{
if (null == Singleton<T>.s_singleton)
{
object obj;
Monitor.Enter(obj = Singleton<T>.s_objectLock);
try
{
if (null == Singleton<T>.s_singleton)
{
Singleton<T>.s_singleton = ((default(T) == null) ? Activator.CreateInstance<T>() : default(T));
}
}
finally
{
Monitor.Exit(obj);
}
}
return Singleton<T>.s_singleton;
}
}
protected Singleton()
{
}
}
}
這個單例是多線程安全的。
所以我們的VersionManager就直接繼承該抽象類,注意需要引用Game命名空間:
using UnityEngine;
using System.Collections;
using Game;
public class VersionManager : Singleton<VersionManager>
{
}
OK,正式進入VersionManager代碼的編寫,我們先來分析一下:
1.VersionManager的初始化,主要處理事件的注冊和監聽。
2.VersionManager加載本地版本信息xml,封裝成版本信息類VersionManagerInfo來管理。
3.檢查網絡情況,開始下載服務器版本信息,也封裝成VersionManagerInfo類的實例來管理。
4.對比服務器和客戶端版本信息,如果一致無需更新,如果不一致,則下載資源,界面顯示下載進度,完成之后,解壓縮到游戲文件夾內完成更新。
先是第一步初始化,因為我們還沒涉及到什么事件,所以我們先寫個Init()初始化方法,等以后用到再在里面寫,所以寫個空的Init()。
public void Init()
{
}
第二步:加載本地版本信息,因為我們的版本信息需要進行對照,所以創建一個版本信息類來管理方便點,所以創建一個VersionManagerInfo類:
public class VersionManagerInfo
{
/// <summary>
/// 游戲程序版本號,基本上我們不會替換游戲程序,除非非得重新下載客戶端
/// </summary>
public VersionCodeInfo ProgramVersionCodeInfo;
/// <summary>
/// 游戲資源版本號
/// </summary>
public VersionCodeInfo ResourceVersionCodeInfo;
public string ProgramVersionCode
{
get
{
return ProgramVersionCodeInfo.ToString();
}
set
{
ProgramVersionCodeInfo = new VersionCodeInfo(value);
}
}
public string ResourceVersionCode
{
get
{
return ResourceVersionCodeInfo.ToString();
}
set
{
ResourceVersionCodeInfo = new VersionCodeInfo(value);
}
}
/// <summary>
/// 資源包列表
/// </summary>
public string PackageList { get; set; }
/// <summary>
/// 資源包地址
/// </summary>
public string PackageUrl { get; set; }
/// <summary>
/// 資源包md5碼列表
/// </summary>
public string PackageMd5List { get; set; }
/// <summary>
/// 資源包字典key=>url,value=>md5
/// </summary>
public Dictionary<string, string> PackageMd5Dic = new Dictionary<string, string>();
public VersionManagerInfo()
{
ProgramVersionCodeInfo = new VersionCodeInfo("0.0.0.1");
ResourceVersionCodeInfo = new VersionCodeInfo("0.0.0.0");
PackageList = string.Empty;
PackageUrl = string.Empty;
}
}
VersionCodeInfo.cs:
/// <summary>
/// 版本號
/// </summary>
public class VersionCodeInfo
{
/// <summary>
/// 版本號列表
/// </summary>
private List<int> m_listCodes = new List<int>();
/// <summary>
/// 初始化版本號
/// </summary>
/// <param name="version"></param>
public VersionCodeInfo(string version)
{
if (string.IsNullOrEmpty(version))
{
return;
}
string[] versions = version.Split('.');
for (int i = 0; i < versions.Length; i++)
{
int code;
if (int.TryParse(versions[i], out code))
{
this.m_listCodes.Add(code);
}
else
{
Debug.LogError("版本號不是數字");
this.m_listCodes.Add(code);
}
}
}
/// <summary>
/// 比較版本號,自己大返回1,自己小返回-1,一樣返回0
/// </summary>
/// <param name="codeInfo"></param>
/// <returns></returns>
public int Compare(VersionCodeInfo codeInfo)
{
int count = this.m_listCodes.Count < codeInfo.m_listCodes.Count ? this.m_listCodes.Count : codeInfo.m_listCodes.Count;
for (int i = 0; i < count; i++)
{
if (this.m_listCodes[i] == codeInfo.m_listCodes[i])
{
continue;
}
else
{
return this.m_listCodes[i] > codeInfo.m_listCodes[i] ? 1 : -1;
}
}
return 0;
}
/// <summary>
/// 重寫ToString()方法,輸出版本號字符串
/// </summary>
/// <returns></returns>
public override string ToString()
{
StringBuilder sb = new StringBuilder();
foreach (var code in this.m_listCodes)
{
sb.AppendFormat("{0}.", code);
}
//移除多余出來的.號
sb.Remove(sb.Length - 1, 1);
return sb.ToString();
}
}
ok,我們回到VersionManager中,定義一個LocalVersion屬性,類型是VersionManagerInfo類型。
public VersionManagerInfo LocalVersion { get; private set; }
然后在LoadLocalVersion()方法里面初始化,怎么初始化,我們需要讀取version.xml里面的內容,然后初始化。因為我們程序剛開始是不存在SystemConfig.VersionPath的文件,所以呢,我們要自己寫個xml文件,放在Resource下面,然后在保存到SystemConfig.VersionPath路徑上去。
public void LoadLocalVersion()
{
//如果已經存在本地版本文件
if (File.Exists(SystemConfig.VersionPath))
{
}
else
{
LocalVersion = new VersionManagerInfo();//默認版本的初始狀態0.0.0.0
TextAsset ver = Resources.Load("version") as TextAsset;
if (ver != null)
{
UnityTools.SaveText(SystemConfig.VersionPath, ver.text);
}
}
}
所以,創建一個xml文件,命名為version.xml放在Resources根目錄下面。(其實這個xml是打包的時候自動生成的,這里我簡化下,先不講打包)

<?xml version="1.0" encoding="utf-8"?> <root> <ProgramVersionCode>0.0.0.1</ProgramVersionCode> <ResourceVersionCode>0.0.0.0</ResourceVersionCode> <PackageList></PackageList> <PackageUrl></PackageUrl> <PackageMd5List></PackageMd5List> </root>
這個xml標簽的名字要和VersionManagerInfo類的屬性名一致。
UnityTools.SaveText(SystemConfig.VersionPath, ver.text);
這個方法我抽象出來到工具類里面去,功能是將string內容保存到一個文本文件內。
/// <summary>
/// 保存文本到指定文件路徑
/// </summary>
/// <param name="filePath"></param>
/// <param name="textContent"></param>
public static void SaveText(string filePath, string textContent)
{
//如果不存在該目錄就創建
if (!Directory.Exists(GetDirectoryName(filePath)))
{
Directory.CreateDirectory(GetDirectoryName(filePath));
}
//如果已經存在該文件就刪除
if (File.Exists(filePath))
{
File.Delete(filePath);
}
//創建文件並寫入內容
using (FileStream fs = new FileStream(filePath, FileMode.Create))
{
using (StreamWriter sw = new StreamWriter(fs))
{
sw.Write(textContent);
sw.Flush();
sw.Close();
}
fs.Close();
}
}
/// <summary>
/// 取得該文件所在的目錄文件夾
/// </summary>
/// <param name="filePath"></param>
/// <returns></returns>
public static string GetDirectoryName(string filePath)
{
return filePath.Substring(0, filePath.LastIndexOf('/'));
}
OK,那么加載本地版本資源完成之后,進行第三步:檢查網絡情況,開始下載服務器版本信息
我們知道,下載服務器版本信息,肯定涉及到界面的同步,比如下載更新消息提示框,下載進度條等等。那么,如果把這些界面都放在VersionManager或者DownloadMrg類里面處理,不符合類的單一職責,也不符合mvc模式,所以呢。
之前我們講過,用委托來處理,把界面的處理直接通過委托傳遞到VersionManager或者DownloadMrg類里面。
我們回到LOLGameDriver類的DoInit()方法:
public void DoInit()
{
VersionManager.singleton.Init();
VersionManager.singleton.LoadLocalVersion();
CheckVersion(CheckVersionFinished);
}
CheckVersion(Action finished):
private void CheckVersion(Action finished)
{
//添加一個解壓文件界面提示回調
Action<bool> fileDecompress = (finish) =>
{
if (finish)
{
//正在更新本地文件,原本是界面上顯示提示消息,以后再講,這里只是打印看看
Debug.Log("正在更新本地文件");
}
else
{
Debug.Log("數據讀取中");
}
};
Action<int, int, string> taskProgress = (total, index, fileName) =>
{
//正在下載更新文件
Debug.Log(string.Format("正在下載更新文件({0}/{1}:{2})", index + 1, total, fileName));
};
Action<int, long, long> progress = (ProgressPercentage, TotalBytesToReceive, BytesReceive) =>
{
//處理進度條
Debug.Log(string.Format("進度:{0}%" ,ProgressPercentage));
};
Action<Exception> error = (ex) =>
{
Debug.Log(ex);
};
//界面提示版本檢查中
Debug.Log("版本檢查中...");
VersionManager.singleton.CheckVersion(fileDecompress, taskProgress, progress, finished, error);
}
這里我將這些委托直接定義在方法內部,其實我們可以自己在外部定義這些方法的,其實都是一樣的。
Ok,寫到在運行程序試試。唉!發現有報錯誤:

他說Application.persistentDataPath這個方法得在主線程里面執行,也就是說我們把它放在另外一個線程里面執行了。想想,我們哪里有用到另外一個線程。哦,對了,在Donwload的時候,我們異步下載一個網頁資源。
也就是這個委托出現錯誤,他是在另外一個線程里面執行,然后調用LOLGameDriver.TryInit()->DoInit()->VersionManager.singleton.LoadLocalVersion();
所以他取得Application.persistentDataPath是在
action.BeginInvoke(null, null);
線程下面執行的。那么如何解決這個問題呢?關鍵是這個委托放在Update,Awake,Start或者協程里面執行,且只執行一次。
對了,之前看過我博客的童鞋可以很快想到--->時間定時器
我們在LOLGameDriver類下面寫個Tick方法,執行時間定時器的Tick計時:
private void Tick()
{
TimerHeap.Tick();
}
然后在Awake里面,不斷的重復執行這個Tick,實際上就是一個協程。
InvokeRepeating("Tick", 1, 0.02f);
然后在創建一個添加委托執行的接口,Invoke(Action action):
public static void Invoke(Action action)
{
TimerHeap.AddTimer(0, 0, action);
}
將這個委托添加到定時器里面執行,默認為0秒之后執行,無重復(0=無重復)執行。
OK,我們只需要修改一處就可以了:

將紅色代碼注釋,然后添加藍色代碼就ok了。運行,觀察打印信息:

OK,行了,那么本節就到這里,下節繼續。。。。。。
