---恢复内容开始---

仿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项目开发第二天链接地址

仿LOL项目开发第一天的更多相关文章

  1. 仿LOL项目开发第六天

    仿LOL项目开发第六天 by草帽 OK,因为更新模块已经处理好了,接着开始登陆的编写.那么我们就需要状态机的管理. 所谓状态机就是在哪个状态执行那个状态的代码逻辑: 那么我们开始编写GameState ...

  2. 仿LOL项目开发第四天

    ---恢复内容开始--- 仿LOL项目开发第四天 by草帽 上节讲了几乎所有的更新版本的逻辑,那么这节课我们来补充界面框架的搭建的讲解. 我们知道游戏中的每个界面都有自己的一个类型:比如登陆界面,创建 ...

  3. 仿LOL项目开发第二天

    仿LOL项目开发第二天 by草帽 接着上节来讲,上节更新还没开始写代码逻辑,今天我们补充完整. 我们找到VersionManager脚本里面的CheckVersion方法: 首先我们想到检测版本,需要 ...

  4. 仿LOL项目开发第九天

    仿LOL项目开发第九天 by 草帽 OK,今天我们完全换了一种风格,抛弃了Unity3d的c#语法,我们来写写java的项目. 说到java服务器,当然有些人可能鄙视java的服务器速度太慢,但是相对 ...

  5. 仿LOL项目开发第八天

    仿LOL项目开发第八天 by 草帽 这节我们继续上节所讲的内容,上节我们初始化好了LoginWindow,当我们点击确认选择服务器按钮的时候,就发送服务器id给游戏服务器. 这里就开始涉及到客户端需要 ...

  6. 仿LOL项目开发第七天

    仿LOL项目开发第七天 by 草帽 不知不觉已经写到了第七篇这种类型的博客,但是回过头看看之前写的,发现都只能我自己能看懂. 我相信在看的童鞋云里雾里的,因为我基本上没怎么详细讲一个脚本怎么用?但是你 ...

  7. 仿LOL项目开发第五天

    仿LOL项目开发第五天 by草帽 今天呢,我们看下能开发什么内容,首先上节我们已经讲了UI框架的搭建,上节还遗留下很多问题,比如说消息的字符是代码里面自己赋值的. 那么就比较死板,按照正常的逻辑,那些 ...

  8. 仿LOL项目开发第三天

    仿LOL项目开发第二天 by草帽 昨个我们已经实现了下载功能,但是发现没有,下载的包是压缩的,没有解压开,那么Unity是识别不了的. 所以今个我们来讲讲如何实现解压文件. 还记得吗,我们在Downl ...

  9. 如何在项目开发中应用好“Deadline 是第一生产力”?

    我想也许你早就听说过"Deadline是第一生产力"这句话,哪怕以前没听说过,我相信看完本文后,再也不会忘记这句话,甚至时不时还要感慨一句:"Deadline是第一生产力 ...

随机推荐

  1. poj 2337(单向欧拉路的判断以及输出)

    Catenyms Time Limit: 1000MS   Memory Limit: 65536K Total Submissions: 11648   Accepted: 3036 Descrip ...

  2. Error: could not open `C:\Java\jre7\lib\i386\jvm.cfg

    打开eclipse时出现Error: could not open `C:\Program Files\Java\jre7\lib\i586\jvm.cfg’) 删除 c:\windows\syste ...

  3. centos7 修改时区

    Linux 系统(我特指发行版, 没说内核) 下大部分软件的风格就是不会仔细去考虑向后 的兼容性, 比如你上个版本能用这种程序配置, 没准到了下一个版本, 该程序已经不见了. 比如 sysvinit ...

  4. Java 8 对 List<List<String>> 排序

    Java 8 对 List<List> 排序 import java.util.ArrayList; import java.util.List; import java.util.str ...

  5. 【U3D】播放器设置(PlayerSettings)

    播放器设置 (Player Settings) 播放器设置 (Player Settings) 用于为您要在 Unity 中编译的最终游戏定义各项(特定于平台的)参数.例如,参数中的一些值用于您打开单 ...

  6. Anaconda 安装 OpenCV 遇到的问题

    1. 使用 pip install   安装 OpenCV 2. 对于 Ananconda 安装 OpenCV ,通常会遇到无法 import 的情况, 这是由于 anaconda 本身没有遵循 PE ...

  7. 【拓扑排序或差分约束】Guess UVALive - 4255

    题目链接:https://cn.vjudge.net/contest/209473#problem/B 题目大意:对于n个数字,给出sum[j]-sum[i](sum表示前缀和)的符号(正负零),求一 ...

  8. Windows群集之NLB

    转http://www.aixchina.net/Article/31746 网络负载平衡群集(Network Load balancing) 在Internet快速发展的今天,为了满足企业的高速发展 ...

  9. CodeForces - 1016D Vasya And The Matrix

    题面在这里! 很明显二进制每一位都是无关的,所以可以先把原问题简化:给矩阵中的每个位置填入0/1,使得特定行/列有奇数个1,其他行/列有偶数个1. 一个比较好想的方法是对行和列 列出 n+m 个异或方 ...

  10. [转]php-fpm - 启动参数及重要配置详解

    约定几个目录/usr/local/php/sbin/php-fpm/usr/local/php/etc/php-fpm.conf/usr/local/php/etc/php.ini 一,php-fpm ...