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

推荐订阅源

T
Tailwind CSS Blog
C
CERT Recently Published Vulnerability Notes
P
Proofpoint News Feed
Vercel News
Vercel News
博客园 - 三生石上(FineUI控件)
IT之家
IT之家
Help Net Security
Help Net Security
月光博客
月光博客
N
News and Events Feed by Topic
Cloudbric
Cloudbric
博客园 - 司徒正美
L
LangChain Blog
Recent Commits to openclaw:main
Recent Commits to openclaw:main
freeCodeCamp Programming Tutorials: Python, JavaScript, Git & More
T
Tenable Blog
The Register - Security
The Register - Security
The Hacker News
The Hacker News
I
InfoQ
The Last Watchdog
The Last Watchdog
MyScale Blog
MyScale Blog
Schneier on Security
Schneier on Security
WordPress大学
WordPress大学
小众软件
小众软件
Threat Intelligence Blog | Flashpoint
Threat Intelligence Blog | Flashpoint
宝玉的分享
宝玉的分享
CTFtime.org: upcoming CTF events
CTFtime.org: upcoming CTF events
K
Kaspersky official blog
L
LINUX DO - 热门话题
N
News | PayPal Newsroom
F
Fortinet All Blogs
Exploit-DB.com RSS Feed
Exploit-DB.com RSS Feed
S
Security @ Cisco Blogs
Recorded Future
Recorded Future
大猫的无限游戏
大猫的无限游戏
H
Help Net Security
Google Online Security Blog
Google Online Security Blog
S
Schneier on Security
C
Cisco Blogs
N
News and Events Feed by Topic
V2EX - 技术
V2EX - 技术
Latest news
Latest news
PCI Perspectives
PCI Perspectives
T
The Blog of Author Tim Ferriss
P
Palo Alto Networks Blog
T
Tor Project blog
Project Zero
Project Zero
云风的 BLOG
云风的 BLOG
Webroot Blog
Webroot Blog
Attack and Defense Labs
Attack and Defense Labs
cs.CL updates on arXiv.org
cs.CL updates on arXiv.org

机核

游戏性能旗舰最强之选,一加 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-04-15 · via 机核

专题介绍

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

系列文章

概览

在这一篇文章中,我们将开始研究传奇服务端的地图逻辑,根据前面的分析我们知道,传奇采用了 Tilemap 的方式来实现地图,接下来就让我们看一下其中的细节。

地图数据加载

在传统的 Tilemap 渲染中,地图资源会以地图元数据 + 瓦片贴图的形式存在,传奇作为一款网络游戏,地图元数据存储在服务端,瓦片贴图存储在客户端。下面让我们来看一下地图元数据的加载及数据结构吧。

就像我们在第一篇文章中讲的,加载地图的逻辑也是在 MainEnvir 的 StartEnvir 阶段完成的,首先在 LoadDB 阶段从 Server.MirDB 加载地图列表,然后通过地图列表分别加载对应的地图文件:

// Envir.cs public bool LoadDB() { // ... var count = reader.ReadInt32(); MapInfoList.Clear(); for (var i = 0; i < count; i++) MapInfoList.Add(new MapInfo(reader)); // ... } ​ private void StartEnvir() { // ... LoadDB(); // ... for (var i = 0; i < MapInfoList.Count; i++) MapInfoList[i].CreateMap(); // ... }

在第一阶段 LoadDB 中,加载的是地图索引以及动态配置,例如刷怪点和传送点,其中的 FileName 指向的是第二阶段加载的地图元数据:

// MapInfo.cs public MapInfo(BinaryReader reader) { Index = reader.ReadInt32(); FileName = reader.ReadString(); Title = reader.ReadString(); MiniMap = reader.ReadUInt16(); Light = (LightSetting) reader.ReadByte(); ​ BigMap = reader.ReadUInt16(); ​ int count = reader.ReadInt32(); for (int i = 0; i < count; i++) SafeZones.Add(new SafeZoneInfo(reader) { Info = this }); ​ count = reader.ReadInt32(); for (int i = 0; i < count; i++) Respawns.Add(new RespawnInfo(reader, Envir.LoadVersion, Envir.LoadCustomVersion)); ​ count = reader.ReadInt32(); for (int i = 0; i < count; i++) Movements.Add(new MovementInfo(reader)); // mesh link ​ NoTeleport = reader.ReadBoolean(); NoReconnect = reader.ReadBoolean(); NoReconnectMap = reader.ReadString(); ​ NoRandom = reader.ReadBoolean(); NoEscape = reader.ReadBoolean(); NoRecall = reader.ReadBoolean(); NoDrug = reader.ReadBoolean(); NoPosition = reader.ReadBoolean(); NoThrowItem = reader.ReadBoolean(); NoDropPlayer = reader.ReadBoolean(); NoDropMonster = reader.ReadBoolean(); NoNames = reader.ReadBoolean(); Fight = reader.ReadBoolean(); Fire = reader.ReadBoolean(); FireDamage = reader.ReadInt32(); Lightning = reader.ReadBoolean(); LightningDamage = reader.ReadInt32(); MapDarkLight = reader.ReadByte(); count = reader.ReadInt32(); for (int i = 0; i < count; i++) MineZones.Add(new MineZone(reader)); MineIndex = reader.ReadByte(); NoMount = reader.ReadBoolean(); NeedBridle = reader.ReadBoolean(); NoFight = reader.ReadBoolean(); Music = reader.ReadUInt16(); ​ if (Envir.LoadVersion < 78) return; NoTownTeleport = reader.ReadBoolean(); if (Envir.LoadVersion < 79) return; NoReincarnation = reader.ReadBoolean(); }

在第二阶段 CreateMap 中,加载的是瓦片地图的元数据,这部分数据应该是美术和策划在地图编辑器中完成地图制作后导出的静态资源文件,后期无需再进行修改:

// MapInfo.cs public void CreateMap() { // ... Map map = new Map(this); if (!map.Load()) return; Envir.MapList.Add(map); // ... } ​ // Map.cs public bool Load() { // ... string fileName = Path.Combine(Settings.MapPath, Info.FileName + ".map"); if (File.Exists(fileName)) { byte[] fileBytes = File.ReadAllBytes(fileName); switch(FindType(fileBytes)) { case 0: LoadMapCellsv0(fileBytes); break; case 1: LoadMapCellsv1(fileBytes); break; // ... } } }

运行时的 Tilemap Cell 创建是在上述代码中的 LoadMapCellsX 中完成的,这里我们以 LoadMapCellsv0 为例,可以看到这里首先根据地图尺寸创建了 Cells,然后根据每个瓦片的属性为 Cell 填充属性:

private void LoadMapCellsv0(byte[] fileBytes) { int offSet = 0; Width = BitConverter.ToInt16(fileBytes, offSet); offSet += 2; Height = BitConverter.ToInt16(fileBytes, offSet); Cells = new Cell[Width, Height]; DoorIndex = new Door[Width, Height]; ​ offSet = 52; ​ for (int x = 0; x < Width; x++) for (int y = 0; y < Height; y++) {//total 12 if ((BitConverter.ToInt16(fileBytes, offSet) & 0x8000) != 0) Cells[x, y] = Cell.HighWall; //Can Fire Over. ​ offSet += 2; if ((BitConverter.ToInt16(fileBytes, offSet) & 0x8000) != 0) Cells[x, y] = Cell.LowWall; //Can't Fire Over. ​ offSet += 2; ​ if ((BitConverter.ToInt16(fileBytes, offSet) & 0x8000) != 0) Cells[x, y] = Cell.HighWall; //No Floor Tile. ​ if (Cells[x, y] == null) Cells[x, y] = new Cell { Attribute = CellAttribute.Walk }; ​ offSet += 4; ​ if (fileBytes[offSet] > 0) DoorIndex[x, y] = AddDoor(fileBytes[offSet], new Point(x, y)); ​ offSet += 3; ​ byte light = fileBytes[offSet++]; ​ if (light >= 100 && light <= 119) Cells[x, y].FishingAttribute = (sbyte)(light - 100); } }

地图对象管理

通过上述分析我们了解了瓦片地图的加载过程,其中包含动态部分和静态部分,静态部分包含了地图尺寸和瓦片类型,动态部分上面只涵盖了安全区、刷怪点、传送点和道具限制等这些基础属性,除此之外,地图上的玩家, 怪物和 NPC 等对象才是地图的核心数据。

让我们先以 NPC 为例,NPC 的加载是在地图加载的第二阶段,在完成了 LoadMapCellsv0 后会根据一阶段 MapInfo 中的 NPC 配置将 NPC 添加到特定的 Cell 中:

// Map.cs public bool Load() { // ... LoadMapCellsX(); // ... for (int i = 0; i < Info.NPCs.Count; i++) { NPCInfo info = Info.NPCs[i]; if (!ValidPoint(info.Location)) continue; AddObject(new NPCObject(info) { CurrentMap = this }); } }

这里的 ValidPoint 指的是该坐标点是否可通行,在后续处理人物行走等逻辑时也会用到,它的判断依据是瓦片属性是否等于 Walk:

// Map.cs public class Cell { // ... public bool Valid { get { return Attribute == CellAttribute.Walk; } } // ... } ​ public Cell GetCell(Point location) { return Cells[location.X, location.Y]; } ​ public bool ValidPoint(Point location) { return location.X >= 0 && location.X < Width && location.Y >= 0 && location.Y < Height && GetCell(location).Valid; }

上述代码在检查目标点合法后,会通过 AddObject 方法将 NPC 对象添加到地图中,传奇的瓦片地图上的每一个对象都需要继承 MapObject 来维护对象在地图上的状态:

// NPCObject.cs (Server) public sealed class NPCObject : MapObject { public override ObjectType Race { get { return ObjectType.Merchant; } } // ... } ​ // Map.cs public void AddObject(MapObject ob) { if (ob.Race == ObjectType.Player) { Players.Add((PlayerObject)ob); InactiveTime = Envir.Time; } if (ob.Race == ObjectType.Merchant) NPCs.Add((NPCObject)ob); ​ GetCell(ob.CurrentLocation).Add(ob); }

通过上述代码可知 Map 在处理 AddObject 时会单独维护 Player 与 NPC 的列表,随后获取到当前坐标点的 Cell 完成对象的添加逻辑,Cell 本身的逻辑很轻,只维护对象列表而不处理任何逻辑:

// Map.cs public class Cell { public static Cell LowWall { get { return new Cell { Attribute = CellAttribute.LowWall }; } } public static Cell HighWall { get { return new Cell { Attribute = CellAttribute.HighWall }; } } ​ public bool Valid { get { return Attribute == CellAttribute.Walk; } } ​ public List<MapObject> Objects; public CellAttribute Attribute; public sbyte FishingAttribute = -1; ​ public void Add(MapObject mapObject) { if (Objects == null) Objects = new List<MapObject>(); ​ Objects.Add(mapObject); } public void Remove(MapObject mapObject) { Objects.Remove(mapObject); if (Objects.Count == 0) Objects = null; } }

那么地图上会有哪些对象呢?我们通过查找所有继承 MapObject 的类可以得到:

  1. DecoObject - Decoration, 装饰物

  2. ItemObject - 物品

  3. MonsterObject - 怪物

  4. NPCObject - NPC

  5. PlayerObject - 玩家

  6. SpellObject - 技能

地图逻辑处理

在服务端启动时,已加载的运行时地图数据包含如下信息:

  1. 加载地图的安全区,刷怪点,传送点,道具技能的使用限制等配置;

  2. 根据配置创建 Tilemap Cells,填充属性

  3. 根据配置创建刷怪点对象 MapRespawn

  4. 根据配置添加 NPC 对象到指定的 Cell

  5. 根据配置创建安全区,安全区是通过无限循环的技能对象 SpellObject 表示的

  6. 根据配置创建挖矿点

显然此时在游戏中还缺少怪物和玩家对象,下面我们分别来看他们的创建时机。

怪物刷新

怪物的刷新数据包括从 DB 中读取的怪物刷新配置 RespawnInfo 以及基于它创建的刷怪点对象 MapRespawn,下面我们来看一下他们的核心属性:

// RespawnInfo.cs public class RespawnInfo { protected static Envir Envir { get { return Envir.Main; } } public int MonsterIndex; public Point Location; public ushort Count, Spread, Delay, RandomDelay; public byte Direction; public string RoutePath = string.Empty; public int RespawnIndex; public bool SaveRespawnTime = false; public ushort RespawnTicks; //leave 0 if not using this system! // ... }

每个 Info 中只存储了一个 MonsterIndex,因此不同的怪物刷新需要独立的 RespawnInfo,RoutePath 为刷怪点的坐标配置文件路径,文件中通过逗号+换行分隔的字符串表示一系列坐标,每行一个刷怪点坐标。

这里的 Delay 和 RespawnTicks 分别代表了两种怪物刷新系统,当没有配置 RespawnTicks 的时候 Delay 生效,否则 RespawnTicks 生效,这个后面我们会更加详细的进行分析。

上述刷怪点配置对象会在地图加载 Cell 完成后作为入参创建地图上的实际刷怪点控制对象 MapRespawn,它会完成从 RoutePath 读取坐标、绑定地图、绑定怪物等逻辑:

// Map.cs public class MapRespawn { protected static Envir Envir { get { return Envir.Main; } } public RespawnInfo Info; public MonsterInfo Monster; public Map Map; public int Count; public long RespawnTime; public ulong NextSpawnTick; public byte ErrorCount = 0; public List<RouteInfo> Route; // ... }

这里的 Count, RespawnTime, NextSpawnTick 和 ErrorCount 将作为该刷怪点的运行时状态用于控制刷怪逻辑。

根据之前的文章不难推断,刷怪这类地图逻辑肯定是在服务端 WorkLoop 的 Process 阶段处理的:

// Envir.cs - WorkLoop for (var i = 0; i < MapList.Count; i++) MapList[i].Process(); // Map.cs public void Process() { ProcessRespawns(); // ... } private void ProcessRespawns() { bool Success = true; for (int i = 0; i < Respawns.Count; i++) { MapRespawn respawn = Respawns[i]; if ((respawn.Info.RespawnTicks != 0) && (Envir.RespawnTick.CurrentTickcounter < respawn.NextSpawnTick)) continue; if ((respawn.Info.RespawnTicks == 0) && (Envir.Time < respawn.RespawnTime)) continue; if (respawn.Count < (respawn.Info.Count * Envir.SpawnMultiplier)) { int count = (respawn.Info.Count * Envir.SpawnMultiplier) - respawn.Count; for (int c = 0; c < count; c++) Success = respawn.Spawn(); } if (Success) { respawn.ErrorCount = 0; long delay = Math.Max(1, respawn.Info.Delay - respawn.Info.RandomDelay + Envir.Random.Next(respawn.Info.RandomDelay * 2)); respawn.RespawnTime = Envir.Time + (delay * Settings.Minute); if (respawn.Info.RespawnTicks != 0) { respawn.NextSpawnTick = Envir.RespawnTick.CurrentTickcounter + (ulong)respawn.Info.RespawnTicks; if (respawn.NextSpawnTick > long.MaxValue)//since nextspawntick is ulong this simple thing allows an easy way of preventing the counter from overflowing respawn.NextSpawnTick -= long.MaxValue; } } else { respawn.RespawnTime = Envir.Time + 1 * Settings.Minute; // each time it fails to spawn, give it a 1 minute cooldown if (respawn.ErrorCount < 5) respawn.ErrorCount++; else { if (respawn.ErrorCount == 5) { respawn.ErrorCount++; Logger.GetLogger(LogType.Spawn).Info($"Failed to spawn: " + $"mapindex: {respawn.Map.Info.Index}, " + $"mob info: index: {respawn.Info.MonsterIndex}, " + $"spawncoords ({respawn.Info.Location.X}:{respawn.Info.Location.Y}), " + $"range {respawn.Info.Spread}"); } } } } }

这里的刷怪处理逻辑分为两个子系统:

  1. 如果 RespawnInfo.RespawnTicks = 0,使用以分钟为单位延迟的刷怪系统,即根据 RespawnInfo.Delay 叠加当前时间 Envir.Time 计算下一次刷怪时间,并不断重复;

  2. 如果 RespawnInfo.RespawnTicks ≠ 0,使用一个独立的 RespawnTimer 进行 tick,并基于此控制刷怪间隔。

注意这里的 Envir.Time 是通过一个 StopWatch 不断在 WorkLoop 开始时更新的毫秒时间戳:

// Envir.cs - WorkLoop try { while (Running) { Time = Stopwatch.ElapsedMilliseconds; // ... } // ... }

在服务端启动过程中,MapRespawn.RespawnTime 和 MapRespawn.NextSpawnTick 都是 0(对于高版本的 DB,服务端也可以选择在 Shutdown 时将这两个数据序列化,并在下次启动时恢复),因此每个刷怪点都会触发一次刷新逻辑,即调用到 MapRespawn.Spawn。

刷怪的逻辑非常简单直接,主要包括以下几个步骤:

  1. 根据 MapRespawn 绑定的 MonterInfo,调用工厂方法 MonsterObject.GetMonster 创建 MonsterObject;

  2. 调用 MonsterObject 的 Spawn 方法,在随机的点位尝试创建 1 只怪物,最多重试 10 次;

  3. 将新创建的怪物添加到地图中,并刷新怪物状态。

// Map.cs - MapRespawn public bool Spawn() { MonsterObject ob = MonsterObject.GetMonster(Monster); if (ob == null) return true; return ob.Spawn(this); } // MonsterObject.cs public static MonsterObject GetMonster(MonsterInfo info) { if (info == null) return null; switch (info.AI) { case 1: case 2: return new Deer(info); case 3: return new Tree(info); // ... } } public bool Spawn(MapRespawn respawn) { Respawn = respawn; if (Respawn.Map == null) return false; for (int i = 0; i < 10; i++) { CurrentLocation = new Point(Respawn.Info.Location.X + Envir.Random.Next(-Respawn.Info.Spread, Respawn.Info.Spread + 1), Respawn.Info.Location.Y + Envir.Random.Next(-Respawn.Info.Spread, Respawn.Info.Spread + 1)); if (!respawn.Map.ValidPoint(CurrentLocation)) continue; respawn.Map.AddObject(this); CurrentMap = respawn.Map; if (Respawn.Route.Count > 0) Route.AddRange(Respawn.Route); RefreshAll(); SetHP(MaxHP); Spawned(); Respawn.Count++; respawn.Map.MonsterCount++; Envir.MonsterCount++; return true; } return false; }

这里再补充一点,上述基于 Timer 和 Tick 的刷怪点有哪些不同呢?通过分析代码我们可以看到基于 Tick 的模式在特定配置下,随着玩家数量的增加可以动态缩短 Tick 间隔从而提升刷怪速度:

// RespawnTimer.cs public void GetTickSpeed() { if (LastUsercount == Envir.PlayerCount) return; LastUsercount = Envir.PlayerCount; double bonus = 1.0; foreach (RespawnTickOption Option in Respawn) { if (Option.UserCount <= LastUsercount) bonus = Math.Min(bonus, Option.DelayLoss); } CurrentDelay = (long)Math.Round((BaseSpawnRate * 60000) * bonus); } ​ public void Process() { //by always rechecking tickspeed we reduce the chance of having respawns get silly on situations where usercount goes up or down fast (like say after a server reboot) GetTickSpeed(); ​ if (Envir.Time >= (LastTick + CurrentDelay)) { CurrentTickcounter++; if (CurrentTickcounter == long.MaxValue) //by using long instead of ulong here you basicaly have a huge safe zone on the respawn ticks of mobs { CurrentTickcounter = 0; } LastTick = Envir.Time; } }

玩家上线与下线

在第二篇文章中我们讲到服务端在与客户端完成鉴权、角色选择后会在收到 C.StartGame 的数据包后创建 PlayerObject,并调用 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); // ... }

相应地,在玩家断开连接后会调用 StopGame 从地图上移除:

// PlayerObject.cs public void StopGame(byte reason) { // ... Envir.Players.Remove(this); CurrentMap.RemoveObject(this); Despawn(); // ... }

下一步

到这里我们已经分析完了地图的加载和基础的地图对象维护逻辑,但其中还有很多细节没有深入,例如人物和怪物的移动,技能的释放,物品的掉落等,这些内容会在接下来的文章中一一分析。