仿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,来存放这个脚本。
打开编辑脚本:
由于是驱动器,在整个游戏中,肯定只需用到一个,所以我们得设计成单例。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 | 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来处理。
我们新建一个下载类,由于你的下载管理器也肯定是只在内存中存在一份,所以我们设计成单例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 | 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代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 | 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()方法里面编写代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | 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:
1 2 3 4 5 6 7 8 9 | 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下面。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 | 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命名空间:
1 2 3 4 5 6 7 | 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()。
1 2 3 4 | public void Init() { } |
第二步:加载本地版本信息,因为我们的版本信息需要进行对照,所以创建一个版本信息类来管理方便点,所以创建一个VersionManagerInfo类:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 | 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:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 | /// <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类型。
1 | public VersionManagerInfo LocalVersion { get ; private set ; } |
然后在LoadLocalVersion()方法里面初始化,怎么初始化,我们需要读取version.xml里面的内容,然后初始化。因为我们程序刚开始是不存在SystemConfig.VersionPath的文件,所以呢,我们要自己写个xml文件,放在Resource下面,然后在保存到SystemConfig.VersionPath路径上去。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | 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是打包的时候自动生成的,这里我简化下,先不讲打包)
1 2 3 4 5 6 7 8 | <? 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类的属性名一致。
1 | UnityTools.SaveText(SystemConfig.VersionPath, ver.text); |
这个方法我抽象出来到工具类里面去,功能是将string内容保存到一个文本文件内。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 | /// < 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()方法:
1 2 3 4 5 6 | public void DoInit() { VersionManager.singleton.Init(); VersionManager.singleton.LoadLocalVersion(); CheckVersion(CheckVersionFinished); } |
CheckVersion(Action finished):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 | 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的时候,我们异步下载一个网页资源。

1 |
也就是这个委托出现错误,他是在另外一个线程里面执行,然后调用LOLGameDriver.TryInit()->DoInit()->VersionManager.singleton.LoadLocalVersion();
所以他取得Application.persistentDataPath是在
1 | action.BeginInvoke( null , null ); |
线程下面执行的。那么如何解决这个问题呢?关键是这个委托放在Update,Awake,Start或者协程里面执行,且只执行一次。
对了,之前看过我博客的童鞋可以很快想到--->时间定时器
我们在LOLGameDriver类下面写个Tick方法,执行时间定时器的Tick计时:
1 2 3 4 | private void Tick() { TimerHeap.Tick(); } |
然后在Awake里面,不断的重复执行这个Tick,实际上就是一个协程。
1 | InvokeRepeating( "Tick" , 1, 0.02f); |
然后在创建一个添加委托执行的接口,Invoke(Action action):
1 2 3 4 | public static void Invoke(Action action) { TimerHeap.AddTimer(0, 0, action); } |
将这个委托添加到定时器里面执行,默认为0秒之后执行,无重复(0=无重复)执行。
OK,我们只需要修改一处就可以了:
将红色代码注释,然后添加蓝色代码就ok了。运行,观察打印信息:
OK,行了,那么本节就到这里,下节继续。。。。。。
【推荐】还在用 ECharts 开发大屏?试试这款永久免费的开源 BI 工具!
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 从二进制到误差:逐行拆解C语言浮点运算中的4008175468544之谜
· .NET制作智能桌面机器人:结合BotSharp智能体框架开发语音交互
· 软件产品开发中常见的10个问题及处理方法
· .NET 原生驾驭 AI 新基建实战系列:向量数据库的应用与畅想
· 从问题排查到源码分析:ActiveMQ消费端频繁日志刷屏的秘密
· C# 13 中的新增功能实操
· Ollama本地部署大模型总结
· 【杭电多校比赛记录】2025“钉耙编程”中国大学生算法设计春季联赛(4)
· langchain0.3教程:从0到1打造一个智能聊天机器人
· 用一种新的分类方法梳理设计模式的脉络