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

仿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. Eclipse的SVN插件与本地SVN客户端关联不上

    问题:当我们用SVN客户端把代码更新到本地,并导入到eclipse之后,却发现我们的SVN插件并没有起作用(没有代码入库.修改等小图标的显示,也没有check in,update等功能菜单).如果我们 ...

  2. 邂逅Sass和Compass之Compass篇

    本文主要讲解Compass的内容,众所周知Compass是Sass的工具库,如果对Sass不甚了解的同学可以移步 邂逅Sass和Compass之Sass篇 Sass本身只是一个“CSS预处理器”,Co ...

  3. copy深浅拷贝

    我们在很多方法里都看到copy()方法,这是对变量的复制,赋值,下面来看一下实例: 复制方法调用的是copy模块中的方法: import copy copy.copy()         #前拷贝 c ...

  4. 解决 .net HttpClient 调用时出现的 "A task was cancelled" 错误

    近日在系统中集成ElasticClient客户端,自动创建索引.删除索引,发现通过 ElasticClient 的 LowerLevelClient 无法正确返回结果,但是索引已成功创建或删除. 并会 ...

  5. java 获取路径的各种方法

    (1).request.getRealPath("/");//不推荐使用获取工程的根路径 (2).request.getRealPath(request.getRequestURI ...

  6. STL容器 -- Map

    核心描述: map 就是从键(key) 到 值(value) 的一个映射.且键值不可重复,内部按照键值排序. 头文件: #include <map> 拓展: multimap 是一个多重映 ...

  7. Python并发编程-队列

    队列 IPC = Inter-Process Communication 队列 先进先出 队列的几种方法 #put() #full() #get() #empty() #get-nowait() fr ...

  8. java变量的命名使用规则

    1.环境变量通常是指在操作系统中,用来指定操作系统运行时需要的一些参数 2.变量名以字母.下划线或者美元符(4上面的¥)开头,不能以数字开头,后面跟字母.下划线.美元符.数字,变量名对大小写敏感,无长 ...

  9. ironic简介

    转:https://doodu.gitbooks.io/openstack-ironic 简介 Bare Metal Servcie 裸机服务 -- 'bear betal' ironic简介 如今O ...

  10. Lisp em SCU - 4490 (强大的map用法)

    Time Limit: 1000 MS Memory Limit: 131072 K Description There are two lists and they may be intersect ...