韩版传奇 2 源码分析与 Unity 重制(二)客户端启动与交互流程

专题介绍

该专题将会分析 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 hereif (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; // 5000msList<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 NPCCallDefaultNPC(DefaultNPCType.Login);//Call Daily NPCif (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();elseGetMentor();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);//IntelligentCreaturefor (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();
}

下一步

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

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/577743.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

Linux 用户和用户组管理

Linux系统是一个多用户多任务的分时操作系统&#xff0c;任何一个要使用系统资源的用户&#xff0c;都必须首先向系统管理员申请一个账号&#xff0c;然后以这个账号的身份进入系统。 用户的账号一方面可以帮助系统管理员对使用系统的用户进行跟踪&#xff0c;并控制他们对系统…

将elementUI,NaiveUI的progress环形进度条设置为渐变色

需求 &#xff1a;进度条要有一个渐变效果。效果图&#xff1a; NaiveUI和elementUI的官方progress组件都是只能设置一种颜色&#xff0c;不符合需求所以改一下。 其实NaiveUI和elementUI设置进度条的实现方式基本一样都是使用svg渲染出两个path&#xff0c;第一个是底色&…

用手机做无人直播怎么做?

用手机进行无人直播已经成为了一种新兴的方式&#xff0c;给我们的生活带来了便利和创新。无人直播是指通过手机进行实时转播&#xff0c;而无需人工操作的一种直播形式。以下将介绍如何用手机实现无人直播。 首先&#xff0c;要实现手机无人直播&#xff0c;我们需要一个稳定…

<meta name=“Keywords“ content=““ >、<meta name=“Description“ content=““ > 等用法解释

今天在看网站代码&#xff0c;发现类似<meta name"Keywords" content"" >、<meta name"Description" content"" >这样的写法&#xff0c;不知道具体代表什么意思&#xff0c;于是上网搜了一下&#xff0c;下面是在网上找到…

Python电能质量扰动信号分类(三)基于Transformer的一维信号分类模型

目录 引言 1 数据集制作与加载 1.1 导入数据 1.2 制作数据集 2 Transformer分类模型和超参数选取 2.1 定义Transformer分类模型 2.2 定义模型参数 3 Transformer模型训练与评估 3.1 模型训练 3.2 模型评估 代码、数据如下&#xff1a; 往期精彩内容&#xff1a; 电…

[内功修炼]函数栈帧的创建与销毁

文章目录 1:什么是函数栈帧2:理解函数栈帧能解决什么问题呢3:函数栈帧的创建与销毁的解析3.1:什么是栈3.2:认识相关寄存器与汇编指令相关寄存器相关汇编指令 3.3 解析函数栈帧的创建和销毁3.3.1 预备知识3.3.2 详细解析一:调用main函数,为main函数开辟函数栈帧First:push前push…

RK3588平台开发系列讲解(AI 篇)RKNN-Toolkit2 模型的加载转换

文章目录 一、Caffe 模型加载接口二、TensorFlow 模型加载接口三、TensorFlowLite 模型加载接口四、ONNX 模型加载五、DarkNet 模型加载接口六、PyTorch 模型加载接口沉淀、分享、成长,让自己和他人都能有所收获!😄 📢 RKNN-Toolkit2 目前支持 Caffe、TensorFlow、Tensor…

MySQL报错:1054 - Unknown column ‘xx‘ in ‘field list的解决方法

我在操作MySQL遇到1054报错&#xff0c;报错内容&#xff1a;1054 - Unknown column Cindy in field list&#xff0c;下面演示解决方法&#xff0c;非常简单。 根据箭头指示&#xff0c;Cindy对应的应该是VARCHAR文本数字类型&#xff0c;字符串要用引号&#xff0c;所以解决方…

web架构师编辑器内容-HTML2Canvas 截图的原理

HTML2Canvas 截图的原理 目的&#xff1a;一个canvas元素&#xff0c;上面有绘制一系列的HTML节点 局限&#xff1a;canvas中没法添加具体的Html节点&#xff0c;它只是一张画布 通过canvas.getContext(‘2d’)可以拿到canvas提供的2D渲染上下文&#xff0c;然后在里面绘制形…

结构体-2-测试排名

22-结构体-2-测试排名 [命题人 : 外部导入] 时间限制 : 1.000 sec 内存限制 : 128 MB 题目描述 为了提升同学们的编程能力&#xff0c;老师们会在平时进行C语言的上机测试&#xff0c;了解班上同学的学习情况&#xff0c;对于一些测试成绩较差的同学&#xff0c;老师会进行督促…

使用tesla gpu 加速大模型,ffmpeg,unity 和 UE等二三维应用

我们知道tesla gpu 没有显示器接口&#xff0c;那么在windows中怎么使用加速unity ue这种三维编辑器呢&#xff0c;答案就是改变注册表来加速相应的三维渲染程序. 1 tesla gpu p40 p100 加速 在windows中使用regedit 来改变 核显配置&#xff0c; 让p100 p40 等等显卡通过核显…

【动态规划】11简单多状态 dp 问题_按摩师_C++(easy)

题目链接&#xff1a;leetcode按摩师 目录 题目解析&#xff1a; 算法原理 1.状态表示 2.状态转移方程 3.初始化 4.填表顺序 5.返回值 编写代码 题目解析&#xff1a; 题目让我们求按摩师找到最优的预约集合&#xff08;总预约时间最长&#xff09; 由题可得&#xff…

一体机定制_工控触控一体机安卓主板方案

工控一体机是一种集成化的硬件方案&#xff0c;采用了联发科MT8768八核芯片和12nm制程工艺。该芯片拥有2.0GHz的主频和IMG PowerVR GE8320图形处理GPU&#xff0c;具备强大的视频处理能力&#xff0c;并且兼容大部分的视频格式和解码能力。工控一体机搭载了Android 9.0操作系统…

合并的单元格如何填充连续的序号

希望你以后碰到合并的单元格&#xff0c;不在一个个输入序号&#xff0c;用以下操作帮你输入连续的序号。 一、操作过程如下 1.有一个基准的单元格在同一列&#xff0c;而且这个基准单元格必须得是序号为1的单元格的上面的一个单元格&#xff0c;这样的话后面才能自动递增&am…

单聊和群聊

TCP协议单聊 服务端&#xff1a; import java.awt.BorderLayout; import java.io.BufferedReader; import java.io.IOException; import java.io.InputStreamReader; import java.io.PrintWriter; import java.net.ServerSocket; import java.net.Socket; import java.util.Vec…

查看docker映射数据卷

要查看Docker容器已经运行的数据卷映射&#xff0c;可以使用以下命令&#xff1a; docker inspect -f {{range .Mounts}}{{.Source}} -> {{.Destination}}{{end}} <容器名称或ID>这个命令使用docker inspect命令以格式化的方式输出容器的详细信息。-f选项允许您指定G…

使用ArcMap进行实测数据处理

文章目录 题目流程 题目 实验名称&#xff1a;实测数据处理 实验目的及要求&#xff1a; 1. 掌握实测点数据转为矢量点数据方法 2. 掌握数据投影变换方法 3. 掌握点数据插值方法 流程 1&#xff0c;打开ArcMap软件&#xff0c;在左菜单栏上选中File&#xff0c;然后鼠标移…

『CV学习笔记』英伟达NVLink和NVSwitch介绍

英伟达NVLink和NVSwitch介绍 文章目录 一. 全球最大GPU背后秘密:NVSwitch如何实现NVIDIA DGX-2的超强功力?1.1. 单一GPU1.2. 双GPU(PCIe和NVLink)1.3. Super Crossbar将GPU连接在一起1.4. NVIDIA NVSwitch介绍二. NVLink和NVSwitch介绍和对比2.1. 什么是NVLink2.2. 什么是N…

我在代码随想录|写代码|简单题理解KMP算法

本篇提纲 什么是KMPKMP有什么用什么是前缀表为什么一定要用前缀表如何计算前缀表前缀表与next数组使用next数组来匹配时间复杂度分析构造next数组使用next数组来做匹配前缀表统一减一 C代码实现前缀表&#xff08;不减一&#xff09;C实现总结 什么是KMP? 说到KMP&#xf…

MFC或QT中,自绘控件的目的和实现步骤

MFC自绘控件的步骤 自绘控件的目的是为了能够自定义控件的外观、行为和交互方式&#xff0c;以满足特定的需求&#xff0c;同时增强应用程序的用户体验。 实现步骤如下&#xff1a; 1、创建一个继承自MFC控件基类&#xff08;如CButton、CStatic等&#xff09;的自定义控件类…