仿LOL項目開發第一天


---恢復內容開始---

仿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,行了,那么本節就到這里,下節繼續。。。。。。

Login界面UI下載鏈接

 

 仿LOL項目開發第二天鏈接地址

 

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM