惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

WordPress大学
WordPress大学
T
Threat Research - Cisco Blogs
美团技术团队
IT之家
IT之家
Apple Machine Learning Research
Apple Machine Learning Research
Microsoft Azure Blog
Microsoft Azure Blog
小众软件
小众软件
Engineering at Meta
Engineering at Meta
U
Unit 42
Cyber Security Advisories - MS-ISAC
Cyber Security Advisories - MS-ISAC
MongoDB | Blog
MongoDB | Blog
奇客Solidot–传递最新科技情报
奇客Solidot–传递最新科技情报
The Cloudflare Blog
Last Week in AI
Last Week in AI
M
MIT News - Artificial intelligence
G
Google Developers Blog
博客园 - 三生石上(FineUI控件)
Vercel News
Vercel News
The Register - Security
The Register - Security
Cyberwarzone
Cyberwarzone
F
Fortinet All Blogs
L
LINUX DO - 热门话题
C
Check Point Blog
Security Archives - TechRepublic
Security Archives - TechRepublic
Know Your Adversary
Know Your Adversary
S
Security Affairs
cs.AI updates on arXiv.org
cs.AI updates on arXiv.org
Webroot Blog
Webroot Blog
V2EX - 技术
V2EX - 技术
Threat Intelligence Blog | Flashpoint
Threat Intelligence Blog | Flashpoint
Martin Fowler
Martin Fowler
Exploit-DB.com RSS Feed
Exploit-DB.com RSS Feed
I
InfoQ
Cisco Talos Blog
Cisco Talos Blog
博客园 - 司徒正美
aimingoo的专栏
aimingoo的专栏
T
The Exploit Database - CXSecurity.com
博客园 - 【当耐特】
C
CERT Recently Published Vulnerability Notes
酷 壳 – CoolShell
酷 壳 – CoolShell
云风的 BLOG
云风的 BLOG
L
Lohrmann on Cybersecurity
T
Threatpost
腾讯CDC
Security Latest
Security Latest
K
Kaspersky official blog
freeCodeCamp Programming Tutorials: Python, JavaScript, Git & More
Stack Overflow Blog
Stack Overflow Blog
Help Net Security
Help Net Security
Forbes - Security
Forbes - Security

机核

游戏性能旗舰最强之选,一加 Ace 6 至尊版国补到手价2999元起 6元钱自己更换电动车刹车线 《生化危机9:安魂曲》编剧Haris Orkin专访 摸金游戏?音乐游戏!暗区新赛季这把能弹的琴有何来历? 好评国产武侠SRPG《息风谷战略》免费DLC现已推出 | 机核 GCORES 碎片 《生化危机:安魂曲》将于5月实装首个免费更新内容 | 机核 GCORES 新锐东方游戏,谱写世界新章! 沉浸式恋爱视觉小说游戏《心象演算》免费试玩版现已正式上线 | 机核 GCORES 互动影游《代号三国:龙起》上线!穿越三国与曹操并肩、与佳人同行、与权谋博弈! 《老头收集梦想生活》,游戏酒桌会6,录音笔VOL.689 | 机核 GCORES 破界·共生——《白日梦:无限世界》五大核心游戏特点解析 价格已到史低,锐龙5 9600X/锐龙7 9700X正适合抄底 时间循环之旅即刻启程!《归环》一周目测试今日开启 热门在线韩游变魂游,洛奇衍生作能否打破“花瓶”魔咒? LG UltraGear evo 全新高端显示器系列:当“5K”遇见“AI”,不止强大,更懂热爱 反套路三国互动影游《代号三国:龙起》今日上线! 愿望单登记人数突破10万!备受瞩目的“女儿养成游戏” 《梦幻魔法公主》今日于Steam平台上线!限时八折优惠中 《黑神话:悟空》全球音乐会2026巡演将于4月29日12时开票 | 机核 GCORES “Snowguelike”生存肉鸽挖矿新作《蛙穿雪境》公布发售日期,5月7日正式上线 | 机核 GCORES 重塑移动办公、AI创作新境!全新华硕灵耀Air系列、ProArt 骁龙版震撼首发,创芯未来 均分88:《Saros》媒体评分汇总 | 机核 GCORES 上海烛龙公布合作遗迹探险游戏《吉时已到》首支预告片 | 机核 GCORES 首个独立游戏《萝薇日记》已上线Steam! 烛龙新IP《吉时已到》首曝,打造国内首款中式合作遗迹探险游戏 喜加一:《暗黑破坏神Ⅳ》国服现已开启限时免费领取本体活动 | 机核 GCORES 新版《生化危机》电影定于9月18日上映,官方网站现已上线 | 机核 GCORES 《冲就完事模拟器2》“星球大战”联动DLC正式宣布 | 机核 GCORES SteamController将于5月4日发售,售价99美元 | 机核 GCORES 事已至此,内存这么用也算省钱了,“2+1”非对称双通道应用实测 505游戏母公司现已收购《明末:渊虚之羽》IP | 机核 GCORES 基石 手动杂谈12|格斗游戏也能讲好故事 | 机核 GCORES I Love You Mr Snowball 我的向日葵小姐 会比GTA6先发售吗?最硬核的生存游戏《DAYZ》要推新DLC了 周记02:在2026遇到新怪谈 什么硬件,能让游戏Loading界面快速消失? | 机核 GCORES GadioSpec《百年风云世界杯》免费试听集 | 机核 GCORES 百年风云世界杯Vol.1丨足球崛起 | 机核 GCORES 100年前,一群人提前替我们经历了AI恐惧 | 机核 GCORES 《呼啸山庄》2026 ——一辆当代艺术的大卡车冲撞了我的大脑 手游《天穗之咲稻姬:日之香巡灵传》宣布将于7月27日停服 | 机核 GCORES 集结梦之队,征战世界杯,《最佳球会ONLINE》上线Steam 山水绝景随心拼 休闲建造游戏《千里山河录》Steam商店页公开 巫师帽、法袍、魔杖,为什么它们是影视、游戏里的法师必备三件套? 《无鞘信使》-第一章 复古风自动战斗肉鸽《终结之终结》Steam商店页面现已上线 电脑里有一款不破不立的MMO,录音笔VOL.688 | 机核 GCORES 《生化危机》30周年纪念周边发售,这次是真的“保护伞” 可靠耐用+AI全能,惠普战66 2025锐龙版深度体验 《时之书:无尽终章》关卡“大航海时代”全球首次公开 经典名作《乌龙派出所》改编经营模拟游戏《乌龙派出所~阿两的商店街物语~》正式宣布支持简体中文 明日开冲,解锁反套路三国互动影游《代号三国:龙起》即将上线 独立游戏《这是我的宝藏!》已发售~ 降低难度不是唯一解,无压力死亡也是好体验 Netflix官宣新片《普通人》:讲述韩国现代史上的权力风暴 《生化危机:安魂曲》全球销量现已突破700万份 《绝地鸭卫》PC版5月15日正式发售 亡妻回忆录?女性向情感叙事游戏《S-mail》现已正式发售 全新酷黑风格,酷睿Ultra 200S PLUS的高性价比搭档!七彩虹BATTLE-AX B860M-PLUS S WIFI7 V20 超级黑刃主板测评 胖狗 索尼发布了港台地区PS5产品价格调整公告,将于5月1日起实施 二次元怪猎+性感美女!《碧蓝幻想:无尽黄昏》开启Beta公测 这款怪谈类型中式恐怖游戏居然更新了?! 《百日战纪 -最终防卫学园-》改编漫画将于今年冬季开启连载 《如被附身,请致电我们》:匈牙利黑色幽默恐怖小说 《竹屿山房杂部卷五》(译文) 大树 超越引擎 摄影分享丨四月 碎片杂记vol.72 当老式FPS与老式动画技术碰撞出新时代的火花—《神探杰克鼠》 想做独游,如何避免首个项目就褒姒? 在线多人动作游戏《OCTOPinbs》将于5月12日上线Steam! 《酒鬼女神的酒诡》确定将于2026年登陆Steam 《CRYMELIGHT》将于11月5日(周四)正式发售!4月25日开始预购! 钢铁国度MKI部落Evolution,蛮兵部分 【钢铁国度】部落-Primal MK I,蛮兵部分 这是一个高中生用ai跑出来的作品,我自称他为物理神话 钢铁国度MKI部落Metamorphosis,蛮兵部分上 钢铁国度MKI部落Metamorphosis,蛮兵部分下 INDIE Live Expo于4月25日举办:首发9款新作,超200款独立游戏亮相 暗黑卡牌策略新作《魔忌:穷鼠啮狸》发布全新中文试玩版和发售预告片,4月30日正式上线在即 战锤40K长篇小说:变节者・苦难主宰(三)(全书完) 战锤40K长篇小说:变节者・苦难主宰(二) 战锤40K长篇小说:变节者・苦难主宰(一) 英语语言学习丨短语专题1:短语的特点 唯一的EVE战争 【少前同人】【M200】战术人形会梦见音乐会吗 【昏迷3】即将发售,前作主角悉数到场!“恶灵”宋老师化身可操控角色 业内人士:游戏公司“十有八九”使用生成式AI,包括卡普空 欢庆一周年:《光与影:33号远征队》全新纪念艺术图、超值折扣与免费更新同步上线 才刚刚开始呢 【抽奖】《星际卡车司机》推出免费大型更新,四折平史低折扣进行中 四人合作FPS游戏《佣兵猎手》抢先体验重大更新 1 现已上线 Raw Fury新作《深馅地牢 Deep Dish Dungeon》将于今秋加入 XGP 塔防幸存者游戏《魔怪来袭》推出首个 DLC 《饿狼传说:群狼之城》1周年纪念!新DLC“沃尔夫冈·克劳萨”今日参战 猫狗相伴 欢乐闯关 双人合作平台跳跃游戏《猫狗同行》上线Steam商店页 《同行:月球逃脱》(Together: Moon Escape)上线将于明日上线Steam
韩版《传奇2》源码分析与 Unity 重制(二):客户端启动与交互流程
Sou1gh0st · 2023-03-20 · via 机核

专题介绍

该专题将会分析 LOMCN 基于韩版《传奇2》,使用 .NET 重写的传奇源码(服务端 + 客户端),分析数据交互、状态管理和客户端渲染等技术,此外笔者还会分享将客户端部分移植到 Unity 和服务端用现代编程语言重写的全过程。

系列文章

概览

在这一篇文章中,我们将从客户端入手,分析从 TCP 连接建立、登录鉴权、角色选择、开始游戏到游戏内交互的全过程。

客户端启动

WinForm 入口 Program.cs

与服务端类似,客户端也是一个 WinForm 应用程序,在 Application 启动后,会先跳转到 AMain 检查是否有热更新,随后再跳转到 CMain 开启客户端主逻辑:

// Program.cs [STAThread] private static void Main(string[] args) { // ... Application.EnableVisualStyles(); Application.SetCompatibleTextRenderingDefault(false); if (Settings.P_Patcher) Application.Run(PForm = new Launcher.AMain()); else Application.Run(Form = new CMain()); // ... }

监听事件循环

在 CMain 的构造函数中,我们监听了 Application Idle 事件作为事件循环:

// CMain.cs public CMain() { InitializeComponent(); ​ Application.Idle += Application_Idle; ​ // ... }

在 Application_Idle 中,我们通过 UpdateTime 更新客户端全局的时间戳,通过 UpdateEnviroment 处理网络数据,通过 RenderEnvironment 处理客户端渲染:

private static void Application_Idle(object sender, EventArgs e) { try { while (AppStillIdle) { UpdateTime(); UpdateEnviroment(); RenderEnvironment(); } ​ } catch (Exception ex) { SaveError(ex.ToString()); } }

客户端场景划分

在用户登录之前,UpdateEnviroment 发现连接实例为空不会做任何操作,因此我们先跳过这个函数来看 RenderEnvironment 的处理过程,这里实际上就是基于 Direct 3D 的客户端的渲染循环,请大家注意 MirScene.ActiveScene.Draw 这个调用,传奇通过 Scene 去区分不同的场景,例如登录页面、角色选择页面和游戏页面,每个页面都是一个独立的 Scene:

private static void RenderEnvironment() { try { if (DXManager.DeviceLost) { DXManager.AttemptReset(); Thread.Sleep(1); return; } ​ DXManager.Device.Clear(ClearFlags.Target, Color.CornflowerBlue, 0, 0); DXManager.Device.BeginScene(); DXManager.Sprite.Begin(SpriteFlags.AlphaBlend); DXManager.SetSurface(DXManager.MainSurface); ​ // Note here if (MirScene.ActiveScene != null) MirScene.ActiveScene.Draw(); ​ DXManager.Sprite.End(); DXManager.Device.EndScene(); DXManager.Device.Present(); } catch (Direct3D9Exception ex) { DXManager.DeviceLost = true; } catch (Exception ex) { SaveError(ex.ToString()); ​ DXManager.AttemptRecovery(); } }

那么当前的 ActiveScene 是在哪里设置的呢?实际上在 MirScene 初始化时它会被指定为 LoginScene:

public abstract class MirScene : MirControl { public static MirScene ActiveScene = new LoginScene(); // ... }

因此上面的 Draw 方法其实会将登录页面绘制出来,我们这里先跳过 GUI 相关的部分,直接来看一下当用户输入完账号密码后是如何建立连接和发起登录的。

TCP 连接建立

传奇中的每个 Scene 都是继承自 MirControl 的 UI 对象,MirControl 提供了 Shown 回调用于监听 UI 的展示,在 LoginScene 展示时我们会开启 TCP 连接:

public LoginScene() { // ... Shown += (sender, args) => { Network.Connect(); _connectBox.Show(); }; }

Network 是客户端的网络管理类,在 Connect 方法中我们会创建一个 TcpClient 对象并发起连接,服务端的信息通过配置获取:

public static void Connect() { if (_client != null) Disconnect(); ​ ConnectAttempt++; ​ _client = new TcpClient {NoDelay = true}; _client.BeginConnect(Settings.IPAddress, Settings.Port, Connection, null); }

与服务端的处理方式类似,在 BeginConnect 的异步回调中,我们会开启 receiveList 和 sendList 两个队列,然后通过 BeginReceive 接收服务端数据、处理成 Packet 并加入 receiveList 等待处理。在客户端每帧 Process 的过程中,我们会处理 receiveList 更改客户端状态,同时根据用户输入产生数据包加入到 sendList 发送到服务端。

第一个数据包

服务端发送 S.Connected

通过上面的分析我们知道客户端启动的第一步是发起 TCP 连接请求,服务端在对 Client 进行 Accept 时会创建 MirConnection 对象(如果对此没有印象可以参考第一篇文章),在 MirConnection 的构造方法中我们会向客户端发送 Connected 数据包,这便是客户端与服务端交流的第一个数据包啦:

public MirConnection(int sessionID, TcpClient client) { // ... _receiveList = new ConcurrentQueue<Packet>(); _sendList = new ConcurrentQueue<Packet>(); _sendList.Enqueue(new S.Connected()); _retryList = new Queue<Packet>(); ​ Connected = true; BeginReceive(); }

客户端处理 S.Connected

前面我们提到在 TCP 连接建立之前基于 Application Idle 的事件循环对 UpdateEnviroment 的调用会被忽略,而在连接建立之后这里会通过 Network.Process 处理服务端数据包和发送这一帧产生的数据包,数据包会被路由到 ActiveScene 进行处理,因此这里的 ProcessPacket 会调用到 LoginScene:

public static void Process() { // ... while (_receiveList != null && !_receiveList.IsEmpty) { if (!_receiveList.TryDequeue(out Packet p) || p == null) continue; MirScene.ActiveScene.ProcessPacket(p); } if (CMain.Time > TimeOutTime && _sendList != null && _sendList.IsEmpty) _sendList.Enqueue(new C.KeepAlive()); if (_sendList == null || _sendList.IsEmpty) return; TimeOutTime = CMain.Time + Settings.TimeOut; // 5000ms List<byte> data = new List<byte>(); while (!_sendList.IsEmpty) { if (!_sendList.TryDequeue(out Packet p)) continue; data.AddRange(p.GetPacketBytes()); } CMain.BytesSent += data.Count; BeginSend(data); }

在 LoginScene 的 ProcessPacket 中包含了对客户端初始化和账户相关的数据处理,由于当前数据包是 S.Connected 自然会进入到 ServerPacketIds.Connected 这个 case,随后客户端通过 SendVersion 发送数据完整性检查请求(这里会对 Executable 进行 hash):

public override void ProcessPacket(Packet p) { switch (p.Index) { case (short)ServerPacketIds.Connected: Network.Connected = true; SendVersion(); break; case (short)ServerPacketIds.ClientVersion: ClientVersion((S.ClientVersion) p); break; // ... default: base.ProcessPacket(p); break; } }

数据完整性检查与 Connected 数据包类似,首先客户端发送 hash 到服务端,服务端校验后将结果返回到客户端,这是一个初级的逆向对抗策略,可通过修改发送的 hash 或忽略返回的错误跳过。

客户端登录过程

在上述检查通过以后,客户端会展示账号密码输入页面,用户输入账号密码后点击登录会调用 Login 方法发起登录请求:

// LoginScene.cs private void Login() { OKButton.Enabled = false; Network.Enqueue(new C.Login {AccountID = AccountIDTextBox.Text, Password = PasswordTextBox.Text}); }

作为一款早期的游戏,传奇的密码采用了明文传输(囧),服务端收到 C.Login 数据包后,会尝试从 Account Database 中查询与之匹配的账户,如果校验失败会发送 S.Login 返回登录失败的原因,成功则发送 S.LoginSuccess:

// Envir.cs public void Login(ClientPackets.Login p, MirConnection c) { // ... if (!AccountIDReg.IsMatch(p.AccountID)) { c.Enqueue(new ServerPackets.Login { Result = 1 }); return; } if (!PasswordReg.IsMatch(p.Password)) { c.Enqueue(new ServerPackets.Login { Result = 2 }); return; } var account = GetAccount(p.AccountID); if (account == null) { c.Enqueue(new ServerPackets.Login { Result = 3 }); return; } // ... if (string.CompareOrdinal(account.Password, p.Password) != 0) { if (account.WrongPasswordCount++ >= 5) { account.Banned = true; account.BanReason = "Too many Wrong Login Attempts."; account.ExpiryDate = DateTime.Now.AddMinutes(2); c.Enqueue(new ServerPackets.LoginBanned { Reason = account.BanReason, ExpiryDate = account.ExpiryDate }); return; } c.Enqueue(new ServerPackets.Login { Result = 4 }); return; } account.WrongPasswordCount = 0; lock (AccountLock) { account.Connection?.SendDisconnect(1); account.Connection = c; } c.Account = account; c.Stage = GameStage.Select; account.LastDate = Now; account.LastIP = c.IPAddress; MessageQueue.Enqueue(account.Connection.SessionID + ", " + account.Connection.IPAddress + ", User logged in."); c.Enqueue(new ServerPackets.LoginSuccess { Characters = account.GetSelectInfo() }); }

相应地,在客户端侧也包含了对 Login 和 LoginSuccess 的处理:

// LoginScene.cs public override void ProcessPacket(Packet p) { switch (p.Index) { // ... case (short)ServerPacketIds.Login: Login((S.Login) p); break; case (short)ServerPacketIds.LoginSuccess: Login((S.LoginSuccess) p); break; default: base.ProcessPacket(p); break; } }

在登录失败时会调用到 private void Login(S.Login p) 这个重载方法展示登录失败原因(事实上出于安全考虑,登录失败的原因应当尽可能模糊):

// LoginScene.cs private void Login(S.Login p) { _login.OKButton.Enabled = true; switch (p.Result) { case 0: MirMessageBox.Show("Logging in is currently disabled."); _login.Clear(); break; case 1: MirMessageBox.Show("Your AccountID is not acceptable."); _login.AccountIDTextBox.SetFocus(); break; case 2: MirMessageBox.Show("Your Password is not acceptable."); _login.PasswordTextBox.SetFocus(); break; case 3: MirMessageBox.Show(GameLanguage.NoAccountID); _login.PasswordTextBox.SetFocus(); break; case 4: MirMessageBox.Show(GameLanguage.IncorrectPasswordAccountID); _login.PasswordTextBox.Text = string.Empty; _login.PasswordTextBox.SetFocus(); break; } }

在登录成功时会调用到 private void Login(S.LoginSuccess p) 这个重载方法切换到角色选择 Scene 等待用户的下一步操作,为了避免额外的数据交互,服务端在登录成功后会返回角色列表:

// LoginScene.cs private void Login(S.LoginSuccess p) { Enabled = false; _login.Dispose(); if(_ViewKey != null && !_ViewKey.IsDisposed) _ViewKey.Dispose(); SoundManager.PlaySound(SoundList.LoginEffect); _background.Animated = true; _background.AfterAnimation += (o, e) => { Dispose(); ActiveScene = new SelectScene(p.Characters); }; }

开始游戏

服务端同步角色数据

在用户选择完角色点击开始游戏后,客户端会发送包含角色选择信息的 C.StartGame 数据包到服务端:

// SelectScene.cs public void StartGame() { // ... Network.Enqueue(new C.StartGame { CharacterIndex = Characters[_selected].Index }); }

服务端在接收到 C.StartGame 后会读从数据库读取角色数据,随后新建一个 PlayerObject 调用 StartGame 方法:

// MirConnection.cs private void StartGame(C.StartGame p) { // ... CharacterInfo info = null; for (int i = 0; i < Account.Characters.Count; i++) { if (Account.Characters[i].Index != p.CharacterIndex) continue; info = Account.Characters[i]; break; } if (info == null) { Enqueue(new S.StartGame { Result = 2 }); return; } // ... Player = new PlayerObject(info, this); Player.StartGame(); }

在 PlayerObject 的 StartGame 方法中,服务端将角色添加到地图中,随后发送游戏开始和玩家数据到客户端:

// PlayerObject.cs public void StartGame() { Map temp = Envir.GetMap(CurrentMapIndex); if (temp != null && temp.Info.NoReconnect) { Map temp1 = Envir.GetMapByNameAndInstance(temp.Info.NoReconnectMap); if (temp1 != null) { temp = temp1; CurrentLocation = GetRandomPoint(40, 0, temp); } } if (temp == null || !temp.ValidPoint(CurrentLocation)) { temp = Envir.GetMap(BindMapIndex); if (temp == null || !temp.ValidPoint(BindLocation)) { SetBind(); temp = Envir.GetMap(BindMapIndex); if (temp == null || !temp.ValidPoint(BindLocation)) { StartGameFailed(); return; } } CurrentMapIndex = BindMapIndex; CurrentLocation = BindLocation; } temp.AddObject(this); CurrentMap = temp; Envir.Players.Add(this); StartGameSuccess(); //Call Login NPC CallDefaultNPC(DefaultNPCType.Login); //Call Daily NPC if (Info.NewDay) { CallDefaultNPC(DefaultNPCType.Daily); } }

随后在 StartGameSuccess 的调用中向客户端发送游戏开发和角色数据,这里的每个 Get 方法的作用都是将地图和角色数据同步到客户端:

// PlayerObject.cs private void StartGameSuccess() { Connection.Stage = GameStage.Game; // ... Enqueue(new S.StartGame { Result = 4, Resolution = Settings.AllowedResolution }); ReceiveChat(string.Format(GameLanguage.Welcome, GameLanguage.GameName), ChatType.Hint); // ... Spawned(); SetLevelEffects(); GetItemInfo(); GetMapInfo(); GetUserInfo(); GetQuestInfo(); GetRecipeInfo(); GetCompletedQuests(); GetMail(); GetFriends(); GetRelationship(); if ((Info.Mentor != 0) && (Info.MentorDate.AddDays(Settings.MentorLength) < DateTime.Now)) MentorBreak(); else GetMentor(); CheckConquest(); GetGameShop(); // ... } private void GetUserInfo() { string guildname = MyGuild != null ? MyGuild.Name : ""; string guildrank = MyGuild != null ? MyGuildRank.Name : ""; S.UserInformation packet = new S.UserInformation { ObjectID = ObjectID, RealId = (uint)Info.Index, Name = Name, GuildName = guildname, GuildRank = guildrank, NameColour = GetNameColour(this), Class = Class, Gender = Gender, Level = Level, Location = CurrentLocation, Direction = Direction, Hair = Hair, HP = HP, MP = MP, Experience = Experience, MaxExperience = MaxExperience, LevelEffects = LevelEffects, Inventory = new UserItem[Info.Inventory.Length], Equipment = new UserItem[Info.Equipment.Length], QuestInventory = new UserItem[Info.QuestInventory.Length], Gold = Account.Gold, Credit = Account.Credit, HasExpandedStorage = Account.ExpandedStorageExpiryDate > Envir.Now ? true : false, ExpandedStorageExpiryTime = Account.ExpandedStorageExpiryDate }; Info.Inventory.CopyTo(packet.Inventory, 0); Info.Equipment.CopyTo(packet.Equipment, 0); Info.QuestInventory.CopyTo(packet.QuestInventory, 0); //IntelligentCreature for (int i = 0; i < Info.IntelligentCreatures.Count; i++) packet.IntelligentCreatures.Add(Info.IntelligentCreatures[i].CreateClientIntelligentCreature()); packet.SummonedCreatureType = SummonedCreatureType; packet.CreatureSummoned = CreatureSummoned; Enqueue(packet); }

客户端开始游戏

客户端目前处于 SelectScene,在收到游戏启动成功的数据包 S.StartGame 后会根据返回数据调整分辨率并切换到 GameScene:

public void StartGame(S.StartGame p) { StartGameButton.Enabled = true; ​ switch (p.Result) { case 0: MirMessageBox.Show("Starting the game is currently disabled."); break; case 1: MirMessageBox.Show("You are not logged in."); break; case 2: MirMessageBox.Show("Your character could not be found."); break; case 3: MirMessageBox.Show("No active map and/or start point found."); break; case 4: ​ if (p.Resolution < Settings.Resolution || Settings.Resolution == 0) Settings.Resolution = p.Resolution; ​ switch (Settings.Resolution) { default: case 1024: Settings.Resolution = 1024; CMain.SetResolution(1024, 768); break; case 1280: CMain.SetResolution(1280, 800); break; case 1366: CMain.SetResolution(1366, 768); break; case 1920: CMain.SetResolution(1920, 1080); break; } ​ ActiveScene = new GameScene(); Dispose(); break; } }

在 GameScene 中客户端会处理来自服务端的角色信息、地图数据以及 NPC 和其他玩家数据等,例如在收到游戏开始时服务端发送的 S.UserInformation 后会创建当前玩家的角色:

// GameScene.cs public override void ProcessPacket(Packet p) { switch (p.Index) { // ... case (short)ServerPacketIds.UserInformation: UserInformation((S.UserInformation)p); break; // ... } } ​ private void UserInformation(S.UserInformation p) { User = new UserObject(p.ObjectID); User.Load(p); MainDialog.PModeLabel.Visible = User.Class == MirClass.Wizard || User.Class == MirClass.Taoist; Gold = p.Gold; Credit = p.Credit; ​ InventoryDialog.RefreshInventory(); foreach (SkillBarDialog Bar in SkillBarDialogs) Bar.Update(); }

下一步

到这里整个客户端的启动流程就分析完了,接下来的逻辑主要集中在服务端向客户端同步状态和客户端发送角色行为,在接下来的文章中我们将深入分析这些交互的处理过程。