一步一步开发Game服务器(四)地图线程
时隔这么久 才再一次的回归正题继续讲解游戏服务器开发。
开始讲解前有一个问题需要修正。之前讲的线程和定时器线程的时候是分开的。
但是真正地图线程与之前的线程模型是有区别的。
为什么会有区别呢?一个地图肯定有执行线程,但是每一个地图都有不同的时间任务。
比如检测玩家身上的buffer,检测玩家的状态值。这种情况下如何处理呢?很明显就需要定时器线程。
我的处理方式是创建一个线程的时候根据需求创建对应的 timerthread
直接上代码其他不BB
1 using System; 2 using System.Collections.Generic; 3 using System.Linq; 4 using System.Text; 5 using System.Threading; 6 using System.Threading.Tasks; 7 8 namespace Sz.ThreadPool 9 { 10 /// <summary> 11 /// 线程模型 12 /// </summary> 13 public class ThreadModel 14 { 15 /// <summary> 16 /// 17 /// </summary> 18 public bool IsStop = false; 19 /// <summary> 20 /// ID 21 /// </summary> 22 public int ID { get; private set; } 23 /// <summary> 24 /// 已分配的自定义线程静态ID 25 /// </summary> 26 public static int StaticID { get; private set; } 27 28 string Name; 29 30 /// <summary> 31 /// 初始化线程模型, 32 /// </summary> 33 /// <param name="name"></param> 34 public ThreadModel(String name) 35 : this(name, 1) 36 { 37 38 } 39 40 /// <summary> 41 /// 初始化线程模型 42 /// </summary> 43 /// <param name="name">线程名称</param> 44 /// <param name="count">线程数量</param> 45 public ThreadModel(String name, Int32 count) 46 { 47 lock (typeof(ThreadModel)) 48 { 49 StaticID++; 50 ID = StaticID; 51 } 52 this.Name = name; 53 if (count == 1) 54 { 55 System.Threading.Thread thread = new System.Threading.Thread(new System.Threading.ThreadStart(Run)); 56 thread.Name = "< " + name + "线程 >"; 57 thread.Start(); 58 Logger.Info("初始化 " + thread.Name); 59 } 60 else 61 { 62 for (int i = 0; i < count; i++) 63 { 64 System.Threading.Thread thread = new System.Threading.Thread(new System.Threading.ThreadStart(Run)); 65 thread.Name = "< " + name + "_" + (i + 1) + "线程 >"; 66 thread.Start(); 67 Logger.Info("初始化 " + thread.Name); 68 } 69 } 70 } 71 72 System.Threading.Thread threadTimer = null; 73 74 /// <summary> 75 /// 任务队列 76 /// </summary> 77 protected List<TaskModel> taskQueue = new List<TaskModel>(); 78 /// <summary> 79 /// 任务队列 80 /// </summary> 81 private List<TimerTask> timerTaskQueue = new List<TimerTask>(); 82 83 /// <summary> 84 /// 加入任务 85 /// </summary> 86 /// <param name="t"></param> 87 public virtual void AddTask(TaskModel t) 88 { 89 lock (taskQueue) 90 { 91 taskQueue.Add(t); 92 } 93 //防止线程正在阻塞时添加进入了新任务 94 are.Set(); 95 } 96 97 /// <summary> 98 /// 加入任务 99 /// </summary> 100 /// <param name="t"></param> 101 public void AddTimerTask(TimerTask t) 102 { 103 t.RunAttribute["lastactiontime"] = SzExtensions.CurrentTimeMillis(); 104 if (t.IsStartAction) 105 { 106 AddTask(t); 107 } 108 lock (timerTaskQueue) 109 { 110 if (threadTimer == null) 111 { 112 threadTimer = new System.Threading.Thread(new System.Threading.ThreadStart(TimerRun)); 113 threadTimer.Name = "< " + this.Name + " - Timer线程 >"; 114 threadTimer.Start(); 115 Logger.Info("初始化 " + threadTimer.Name); 116 } 117 timerTaskQueue.Add(t); 118 } 119 timerAre.Set(); 120 } 121 122 /// <summary> 123 /// 通知一个或多个正在等待的线程已发生事件 124 /// </summary> 125 protected ManualResetEvent are = new ManualResetEvent(false); 126 127 /// <summary> 128 /// 通知一个或多个正在等待的线程已发生事件 129 /// </summary> 130 protected ManualResetEvent timerAre = new ManualResetEvent(true); 131 132 /// <summary> 133 /// 线程处理器 134 /// </summary> 135 protected virtual void Run() 136 { 137 while (!this.IsStop) 138 { 139 while ((taskQueue.Count > 0)) 140 { 141 TaskModel task = null; 142 lock (taskQueue) 143 { 144 if (taskQueue.Count > 0) 145 { 146 task = taskQueue[0]; 147 taskQueue.RemoveAt(0); 148 } 149 else { break; } 150 } 151 152 /* 执行任务 */ 153 //r.setSubmitTimeL(); 154 long submitTime = SzExtensions.CurrentTimeMillis(); 155 try 156 { 157 task.Run(); 158 } 159 catch (Exception e) 160 { 161 Logger.Error(Thread.CurrentThread.Name + " 执行任务:" + task.ToString() + " 遇到错误", e); 162 continue; 163 } 164 long timeL1 = SzExtensions.CurrentTimeMillis() - submitTime; 165 long timeL2 = SzExtensions.CurrentTimeMillis() - task.GetSubmitTime(); 166 if (timeL1 < 100) { } 167 else if (timeL1 <= 200L) { Logger.Debug(Thread.CurrentThread.Name + " 完成了任务:" + task.ToString() + " 执行耗时:" + timeL1 + " 提交耗时:" + timeL2); } 168 else if (timeL1 <= 1000L) { Logger.Info(Thread.CurrentThread.Name + " 长时间执行 完成任务:" + task.ToString() + " “考虑”任务脚本逻辑 耗时:" + timeL1 + " 提交耗时:" + timeL2); } 169 else if (timeL1 <= 4000L) { Logger.Error(Thread.CurrentThread.Name + " 超长时间执行完成 任务:" + task.ToString() + " “检查”任务脚本逻辑 耗时:" + timeL1 + " 提交耗时:" + timeL2); } 170 else 171 { 172 Logger.Error(Thread.CurrentThread.Name + " 超长时间执行完成 任务:" + task.ToString() + " “考虑是否应该删除”任务脚本 耗时:" + timeL1 + " 提交耗时:" + timeL2); 173 } 174 task = null; 175 } 176 are.Reset(); 177 //队列为空等待200毫秒继续 178 are.WaitOne(200); 179 } 180 Console.WriteLine(DateTime.Now.NowString() + " " + Thread.CurrentThread.Name + " Destroying"); 181 } 182 183 /// <summary> 184 /// 定时器线程处理器 185 /// </summary> 186 protected virtual void TimerRun() 187 { 188 ///无限循环执行函数器 189 while (!this.IsStop) 190 { 191 if (timerTaskQueue.Count > 0) 192 { 193 IEnumerable<TimerTask> collections = null; 194 lock (timerTaskQueue) 195 { 196 collections = new List<TimerTask>(timerTaskQueue); 197 } 198 foreach (TimerTask timerEvent in collections) 199 { 200 int execCount = timerEvent.RunAttribute.GetintValue("Execcount"); 201 long lastTime = timerEvent.RunAttribute.GetlongValue("LastExecTime"); 202 long nowTime = SzExtensions.CurrentTimeMillis(); 203 if (nowTime > timerEvent.StartTime //是否满足开始时间 204 && (nowTime - timerEvent.GetSubmitTime() > timerEvent.IntervalTime)//提交以后是否满足了间隔时间 205 && (timerEvent.EndTime <= 0 || nowTime < timerEvent.EndTime) //判断结束时间 206 && (nowTime - lastTime >= timerEvent.IntervalTime))//判断上次执行到目前是否满足间隔时间 207 { 208 //提交执行 209 this.AddTask(timerEvent); 210 //记录 211 execCount++; 212 timerEvent.RunAttribute["Execcount"] = execCount; 213 timerEvent.RunAttribute["LastExecTime"] = nowTime; 214 } 215 nowTime = SzExtensions.CurrentTimeMillis(); 216 //判断删除条件 217 if ((timerEvent.EndTime > 0 && nowTime < timerEvent.EndTime) 218 || (timerEvent.ActionCount > 0 && timerEvent.ActionCount <= execCount)) 219 { 220 timerTaskQueue.Remove(timerEvent); 221 } 222 } 223 timerAre.Reset(); 224 timerAre.WaitOne(5); 225 } 226 else 227 { 228 timerAre.Reset(); 229 //队列为空等待200毫秒继续 230 timerAre.WaitOne(200); 231 } 232 } 233 Console.WriteLine(DateTime.Now.NowString() + "Thread:<" + Thread.CurrentThread.Name + "> Destroying"); 234 } 235 } 236 }
当我线程里面第一次添加定时器任务的时候加触发定时器线程的初始化。
先看看效果
地图运作方式怎么样的呢?
来一张图片看看
在正常情况下一个地图需要这些事情。然后大部分事情是需要定时器任务处理的,只有客户端交互通信是不需要定时器任务处理。
封装地图信息类
1 using System; 2 using System.Collections.Generic; 3 using System.Linq; 4 using System.Text; 5 using System.Threading.Tasks; 6 using Sz.MMO.GameServer.IMapScripts; 7 using Sz.MMO.GameServer.TimerMap; 8 using Sz.MMO.GameServer.TimerMonster; 9 10 11 /** 12 * 13 * @author 失足程序员 14 * @Blog http://www.cnblogs.com/ty408/ 15 * @mail 492794628@qq.com 16 * @phone 13882122019 17 * 18 */ 19 namespace Sz.MMO.GameServer.Structs.Map 20 { 21 /// <summary> 22 /// 23 /// </summary> 24 public class MapInfo<TPlayer, TNpc, TMonster, TDropGoods> : IEnterMapMonsterScript, IEnterMapNpcScript, IEnterMapPlayerScript, IEnterMapDropGoodsScript 25 { 26 /// <summary> 27 /// 为跨服设计的服务器id 28 /// </summary> 29 public int ServerID { get; set; } 30 /// <summary> 31 /// 地图模板id 32 /// </summary> 33 public int MapModelID { get; set; } 34 /// <summary> 35 /// 地图id 36 /// </summary> 37 public long MapID { get; set; } 38 39 /// <summary> 40 /// 地图分线处理 41 /// </summary> 42 Dictionary<int, MapLineInfo<TPlayer, TNpc, TMonster, TDropGoods>> mapLineInfos = new Dictionary<int, MapLineInfo<TPlayer, TNpc, TMonster, TDropGoods>>(); 43 44 public MapInfo(string name, int mapModelId, int lineCount = 1) 45 { 46 47 this.MapID = SzExtensions.GetId(); 48 this.MapModelID = mapModelId; 49 Logger.Debug("开始初始化地图: " + name + " 地图ID:" + MapID); 50 51 for (int i = 1; i <= lineCount; i++) 52 { 53 MapLineInfo<TPlayer, TNpc, TMonster, TDropGoods> lineInfo = new MapLineInfo<TPlayer, TNpc, TMonster, TDropGoods>(name + "-" + i + "线"); 54 55 mapLineInfos[i] = lineInfo; 56 } 57 Logger.Debug("初始化地图: " + name + " 地图ID:" + MapID + " 结束"); 58 } 59 60 } 61 62 #region 地图分线 class MapLineInfo<TPlayer, TNpc, TMonster, TDropGoods> : IEnterMapMonsterScript, IEnterMapNpcScript, IEnterMapPlayerScript, IEnterMapDropGoodsScript 63 /// <summary> 64 /// 地图分线 65 /// </summary> 66 /// <typeparam name="TPlayer"></typeparam> 67 /// <typeparam name="TNpc"></typeparam> 68 /// <typeparam name="TMonster"></typeparam> 69 /// <typeparam name="TDropGoods"></typeparam> 70 class MapLineInfo<TPlayer, TNpc, TMonster, TDropGoods> : IEnterMapMonsterScript, IEnterMapNpcScript, IEnterMapPlayerScript, IEnterMapDropGoodsScript 71 { 72 public MapThread MapServer { get; set; } 73 74 public int ServerID { get; set; } 75 76 public int LineID { get; set; } 77 78 public int MapModelID { get; set; } 79 80 public long MapID { get; set; } 81 82 public MapLineInfo(string name) 83 { 84 Players = new List<TPlayer>(); 85 Monsters = new List<TMonster>(); 86 Npcs = new List<TNpc>(); 87 DropGoodss = new List<TDropGoods>(); 88 MapServer = new Structs.Map.MapThread(name); 89 } 90 91 /// <summary> 92 /// 地图玩家 93 /// </summary> 94 public List<TPlayer> Players { get; set; } 95 96 /// <summary> 97 /// 地图npc 98 /// </summary> 99 public List<TNpc> Npcs { get; set; } 100 101 /// <summary> 102 /// 地图怪物 103 /// </summary> 104 public List<TMonster> Monsters { get; set; } 105 106 /// <summary> 107 /// 地图掉落物 108 /// </summary> 109 public List<TDropGoods> DropGoodss { get; set; } 110 } 111 #endregion 112 }
Structs.Map.MapInfo<Player, Npc, Monster, Drop> map = new Structs.Map.MapInfo<Player, Npc, Monster, Drop>("新手村", 101, 2);
这样就创建了一张地图。我们创建的新手村有两条线。也就是两个线程
这样只是创建地图容器和地图线程而已。
如何添加各个定时器呢?
1 using System; 2 using System.Collections.Generic; 3 using System.Linq; 4 using System.Text; 5 using System.Threading.Tasks; 6 using Sz.MMO.GameServer.IMapScripts; 7 8 9 /** 10 * 11 * @author 失足程序员 12 * @Blog http://www.cnblogs.com/ty408/ 13 * @mail 492794628@qq.com 14 * @phone 13882122019 15 * 16 */ 17 namespace Sz.MMO.GameServer.TimerMap 18 { 19 /// <summary> 20 /// 21 /// </summary> 22 public class MapHeartTimer : ThreadPool.TimerTask 23 { 24 25 int serverID, lineID, mapModelID; 26 long mapID; 27 28 /// <summary> 29 /// 指定1秒执行一次 30 /// </summary> 31 public MapHeartTimer(int serverID, int lineID, long mapID, int mapModelID) 32 : base(1000 * 10) 33 { 34 this.serverID = serverID; 35 this.lineID = lineID; 36 this.mapID = mapID; 37 this.mapModelID = mapModelID; 38 } 39 40 /// <summary> 41 /// 42 /// </summary> 43 public override void Run() 44 { 45 46 Logger.Debug("我是地图心跳检查器 执行线程:" + System.Threading.Thread.CurrentThread.Name); 47 Logger.Debug("我是地图心跳检查器 检查玩家是否需要复活,回血,状态"); 48 //var scripts = Sz.ScriptPool.ScriptManager.Instance.GetInstances<IMapHeartTimerScript>(); 49 //foreach (var item in scripts) 50 //{ 51 // item.Run(serverID, lineID, mapID, mapModelID); 52 //} 53 } 54 55 } 56 } 57 58 59 60 61 62 using System; 63 using System.Collections.Generic; 64 using System.Linq; 65 using System.Text; 66 using System.Threading.Tasks; 67 using Sz.MMO.GameServer.IMonsterScripts; 68 69 70 /** 71 * 72 * @author 失足程序员 73 * @Blog http://www.cnblogs.com/ty408/ 74 * @mail 492794628@qq.com 75 * @phone 13882122019 76 * 77 */ 78 namespace Sz.MMO.GameServer.TimerMonster 79 { 80 /// <summary> 81 /// 82 /// </summary> 83 public class MonsterHeartTimer: ThreadPool.TimerTask 84 { 85 86 int serverID, lineID, mapModelID; 87 long mapID; 88 89 /// <summary> 90 /// 指定1秒执行一次 91 /// </summary> 92 public MonsterHeartTimer(int serverID, int lineID, long mapID, int mapModelID) 93 : base(1000 * 10) 94 { 95 this.serverID = serverID; 96 this.lineID = lineID; 97 this.mapID = mapID; 98 this.mapModelID = mapModelID; 99 } 100 101 /// <summary> 102 /// 103 /// </summary> 104 public override void Run() 105 { 106 Logger.Debug("怪物心跳检查器 执行线程:" + System.Threading.Thread.CurrentThread.Name); 107 Logger.Debug("怪物心跳检查器 检查怪物是否需要复活,需要回血,是否回跑"); 108 //var scripts = Sz.ScriptPool.ScriptManager.Instance.GetInstances<IMonsterHeartTimerScript>(); 109 //foreach (var item in scripts) 110 //{ 111 // item.Run(serverID, lineID, mapID, mapModelID); 112 //} 113 } 114 } 115 } 116 117 118 119 using System; 120 using System.Collections.Generic; 121 using System.Linq; 122 using System.Text; 123 using System.Threading.Tasks; 124 125 126 /** 127 * 128 * @author 失足程序员 129 * @Blog http://www.cnblogs.com/ty408/ 130 * @mail 492794628@qq.com 131 * @phone 13882122019 132 * 133 */ 134 namespace Sz.MMO.GameServer.TimerMonster 135 { 136 /// <summary> 137 /// 138 /// </summary> 139 public class MonsterRunTimer: ThreadPool.TimerTask 140 { 141 142 int serverID, lineID, mapModelID; 143 long mapID; 144 145 /// <summary> 146 /// 指定1秒执行一次 147 /// </summary> 148 public MonsterRunTimer(int serverID, int lineID, long mapID, int mapModelID) 149 : base(1000 * 5) 150 { 151 this.serverID = serverID; 152 this.lineID = lineID; 153 this.mapID = mapID; 154 this.mapModelID = mapModelID; 155 } 156 157 /// <summary> 158 /// 159 /// </summary> 160 public override void Run() 161 { 162 Logger.Debug("怪物移动定时器任务 执行线程:" + System.Threading.Thread.CurrentThread.Name); 163 Logger.Debug("怪物移动定时器任务 怪物随机移动和回跑"); 164 //var scripts = Sz.ScriptPool.ScriptManager.Instance.GetInstances<IMonsterHeartTimerScript>(); 165 //foreach (var item in scripts) 166 //{ 167 // item.Run(serverID, lineID, mapID, mapModelID); 168 //} 169 } 170 } 171 }
就在初始化地图线程的时候加入定时器任务
1 public MapInfo(string name, int mapModelId, int lineCount = 1) 2 { 3 4 this.MapID = SzExtensions.GetId(); 5 this.MapModelID = mapModelId; 6 Logger.Debug("开始初始化地图: " + name + " 地图ID:" + MapID); 7 8 for (int i = 1; i <= lineCount; i++) 9 { 10 MapLineInfo<TPlayer, TNpc, TMonster, TDropGoods> lineInfo = new MapLineInfo<TPlayer, TNpc, TMonster, TDropGoods>(name + "-" + i + "线"); 11 //添加地图心跳检测器 12 lineInfo.MapServer.AddTimerTask(new MapHeartTimer(ServerID, i, MapID, MapModelID)); 13 //添加怪物移动定时器 14 lineInfo.MapServer.AddTimerTask(new MonsterRunTimer(ServerID, i, MapID, MapModelID)); 15 //添加怪物心跳检测器 16 lineInfo.MapServer.AddTimerTask(new MonsterHeartTimer(ServerID, i, MapID, MapModelID)); 17 18 mapLineInfos[i] = lineInfo; 19 } 20 Logger.Debug("初始化地图: " + name + " 地图ID:" + MapID + " 结束"); 21 }
其实所有的任务定时器处理都是交给了timer线程,timer线程只负责查看该定时当前是否需要执行。
而具体的任务执行移交到线程执行器。线程执行器是按照队列方式执行。保证了timer线程只是一个简单的循环处理而不至于卡死
同样也保证了在同一张地图里面各个单元参数的线程安全性。
来看看效果。
为了方便我们看清楚一点,我把地图线程改为以一条线。
这样就完成了各个定时器在规定时间内处理自己的事情。
需要注意的是这里只是简单的模拟的一个地图处理各种事情,最终都是由一个线程处理的。那么肯定有人要问了。
你一个线程处理这些事情能忙得过来嘛?
有两点需要注意
1,你的每一个任务处理处理耗时是多久,换句话说你可以理解为你一秒钟能处理多少个任务。
2,你的地图能容纳多少怪物,多少玩家,多少掉落物?换句话说也就是你设计的复杂度间接限制了你的地图有多少场景对象。
那么还有什么需要注意的呢?
其实地图最大的消耗在于寻路。高性能的寻路算法和人性化寻路算法一直是大神研究的对象,我也只能是借鉴他们的了。
这一章我只是简单的阐述了地图运行和任务等划分和构成已经任务处理流程。
接下来我会继续讲解游戏服务器编程,一步一步的剖析。
文路不是很清晰。希望大家不要见怪。
跪求保留标示符 /** * @author: Troy.Chen(失足程序员, 15388152619) * @version: 2021-07-20 10:55 **/ C#版本代码 vs2010及以上工具可以 java 开发工具是netbeans 和 idea 版本,只有项目导入如果出现异常,请根据自己的工具调整 提供免费仓储。 最新的代码地址:↓↓↓ https://gitee.com/wuxindao 觉得我还可以,打赏一下吧,你的肯定是我努力的最大动力