一步一步開發Game服務器(四)地圖線程,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 }
View Code
當我線程裡面第一次添加定時器任務的時候加觸發定時器線程的初始化。
先看看效果

地圖運作方式怎麼樣的呢?
來一張圖片看看

在正常情況下一個地圖需要這些事情。然後大部分事情是需要定時器任務處理的,只有客戶端交互通信是不需要定時器任務處理。
封裝地圖信息類

![]()
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
[email protected]
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 }
View Code
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
[email protected]
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
[email protected]
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
[email protected]
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 }
View Code
就在初始化地圖線程的時候加入定時器任務
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,你的地圖能容納多少怪物,多少玩家,多少掉落物?換句話說也就是你設計的復雜度間接限制了你的地圖有多少場景對象。
那麼還有什麼需要注意的呢?
其實地圖最大的消耗在於尋路。高性能的尋路算法和人性化尋路算法一直是大神研究的對象,我也只能是借鑒他們的了。
這一章我只是簡單的闡述了地圖運行和任務等劃分和構成已經任務處理流程。
接下來我會繼續講解游戲服務器編程,一步一步的剖析。
文路不是很清晰。希望大家不要見怪。