杰哥瞎扯蛋之我的地图数据结构
俗话说的好,要想富,先修路。要想玩MUD,特别是地图大的MUD,早早的开始积累地图数据是很重要的一点。
正好前一阵折腾Avaloniaui,做了一个地图数据管理器,正好整理了一下我的地图数据的格式。
发出来抛砖引玉一下。
注意,我分享的是 地图数据 的数据结构, 不是地图数据,也不是 怎么抓地图/定位/走迷宫,分享后连个属于折腾WIZ和玩家。
首先,我们要明确一点,在北侠的环境下,各种地图信息格式百花齐放。
有文本流的,有脚本流的,有数据库流的,还有脚本专有格式流的。
对于我而言,一直是文本流的拥护者。
第一,便于维护/批量维护
第二,便于进行版本管理,通过对比能看看两个版本之间变动了那些信息。设置我还准备 做过diff的功能。
第三,便于编码处理。很多专有格式要搞个编码处理很蛋疼。
我一开始的预案是JSON/XML/类CSV选择一个。
但考虑到要做1比1 js/lua实现
XML第一个被排除了。
JSON的话,不方便按行做版本处理,而且做不同编码很麻烦,我也不高兴做。
所以我自己做了个带转义最多支持3层(实际写着写着到了5层)的类CSV格式。
人类能看,难写,方便脚本处理。
格式大概如下(非北侠数据):
HMM1.0>UTF8
Info>hell地图测试|1746810633|测试用数据
Room>0|中央广场||||e,59,,1;enter dong,1927,,1;n,22,,1;s,40,,1;w,1,,1|
Room>1|西大街||||e,0,,1;n,2,,1;s,5,,1;w,7,,1|
Room>10|财主大院||||n,11,,1;s,9,,1|
Room>100|石阶||||eu。,101,,1;wd,99,,1|
Room>1000|民宅||||s,999,,1|
Room>1001|黄土路||||n,1002,,1;se,999,,1|
Room>1002|黄土路||||ne,1003,,1;s,1001,,1|
Room>1003|渭汾流域||||n,1004,,1;ne,1712,,1;s,1025,,1;sw,1002,,1|
Room>1004|黄河||||s,1003,,1;w,1005,,1|
Room>1005|河套||||e,1004,,1;sw,1007,,1;w,1006,,1|
Room>1006|青城||||e,1005,,1;n!,2048,,1|
Room>1007|黄土高原||||ne,1005,,1;sw,1008,,1|第一行是文件信息,包括版本和编码(对,就是为了gbk和uft之争)
核心概念。
对于地图来说,我们的用途主要是路径规划,房间查询,信息查询。
核心概念主要有4个
Room/Key房间/主键
每个房间,是一个抽象的概念,房间名描述之类属于附属信息,甚至是快照信息。只有为一的主键才是核心。
Key必须唯一,应该有实际意义,但不该有业务信息。
比如,扬州->广场,可以叫yzgc,但不该叫yz或者yz-(xxx任务信息)
因为业务可能会变,实际意义不随着业务逻辑变,要变也是信息重新录入。
这样,写代码是只要关心有一个key,不要关心其他的,其他的不属于核心功能。
Exit/出口
出口是房间和房间之间的单向通行的信息。
每个出口包括目的地,指令,条件,耗时。
特别的,以我的经验的话,迷宫是需要代码做一些特殊指令的,不应该通过地图信息来处理。
另外,出口是不包含起点信息的。
因为理论上有一些飞行指令,可以在符合条件(比如室外)的房间,都飞到固定点去(北侠好像只有养飞禽才能,我没用过)。
Cost 耗时
耗时时每一个出口的费用的评估。理论上是最小是1的整数。
寻路就是按照最小的耗时去找路线。有些房间之间虽然指令少,但可能有BUSY等限制,可能绕一下更合理。
条件 Condtion
这是比较核心的一个数据。
有些路径,有门派限制,有些路径,有性别限制。
比如我记得明教女休息室,只有 门派明教,性别女才能进去。
那这时候,我们可以通过给出口加上调教,比如 有 明教 标签 和 女性标签,才能使用,就能利用这个地图数据了。
既然有条件,我们还应该有 白名单和黑名单的概念。即只有某个标签能进入,和只有某个标签不能进。
比如有些房间,丐帮的进去会被踢,那我们就不进去。
这就是基本的标签/条件模式。
更复杂点。
很多地方,能不能通行不光光是 有/没有。
比如要一定的轻功/内力/技能才能通行。
这时候我们可以在代码里生成N个不同的标签,但这太丑了,更不好维护。
所以我们的标签是带数值的标签。
默认的标签和条件就是数值1.
指定数值的话
force:100 指你有100级force
这样你能进入 force:40的出口,但不能进force:200的出口。
这样,配合标签,数值,取反几个操作,我们能解决绝大部分的差异化路线规划的问题了。
好,开始来细节了。
首先,就是房间信息的结构
public partial class Room
{
public const string EncodeKey = "Room";
public string Key { get; set; } = "";
//房间的名称,显示用
public string Name { get; set; } = "";
//房间的描述,显示用
public string Desc { get; set; } = "";
//房间的区域,筛选用
public string Group { get; set; } = "";
//标签列表,筛选用
public List<ValueTag> Tags = [];
//房间出口列表
public List<Exit> Exits { get; set; } = [];
public List<Data> Data { get; set; } = [];
}这里面,Key是核心,主键
Name是方便记忆的名字,Desc和代码完全无关,属于注释
Group,分组,基本就是城市了。
Tags就是我刚刚说的带数值的标签,是房间本身的属性,比如非战斗房间,室外房间等。
Exit是一个一个的出口,下一楼贴结构。
Data就是键值对,额外数据。方便代码里有需要时设置一些额外信息。
直观显示时这样
本帖最后由 jarlyyn 于 2025-5-20 03:18 PM 编辑
出口的数据结构是这样
public class Exit
{
//路径指令
public string Command { get; set; } = "";
//目标房间
public string To { get; set; } = "";
public List<ValueCondition> Conditions { get; set; } = [];
public int Cost { get; set; } = 1;
}比较简单
Command属于必填,指令
To是目标房间
Conditions是条件,环境(后面详解)的Tag符合条件,才能使用这个出口
Cost是耗时
直观界面大概是这样
好,最重要的数据体层结构有了。
接下去我们要看应用的数据了。
一般来说,我不建议机器里使用的数据全部是写死的魔数,Magic number。
当应该是预设好的变量。
因此,实际使用的信息,我这里分为4类
1. Marker标注,单点信息,就是一般地图里的图钉,可以理解为别名
2.Route线路,有顺序的房间列表,固定遍历路径。
3.Trace足迹,无顺序的房间集合,一般表示某些道具/NPC可能出现的房间
4.Region区域,无顺序的房间集合,和Trace却区别是这不是实际数据,而是一类房间的归类。
因为这些数据都是给脚本使用的,所以除了常规的Key之外,还有个特殊的字段,叫Message,消息。
举个简单的例子,Npc类的Marker,可以在Message里标准NPC的ID,Name,等,这样把NPC Group的Marker都取出来,就能直接得到NPC列表了。
房间列表之类同理。
Marker的结构
public partial class Marker
{
public string Key { get; set; } = "";
public string Value { get; set; } = "";
public string Desc { get; set; } = "";
public string Group { get; set; } = "";
public string Message { get; set; } = "";
}
很简单的结构,Message就是之前提过的传递给脚本的信息
Value就是对应的房间。
Group是你的标记的类型,比如NPC类
这类数据在北侠这种更新特别频繁的MUD里很重要。
比如挖花任务,NPC就从濠州拆迁过。
如果使用Marker这种别面来记录她的位置,那么她哪怕拆迁到美国德州,我们也不怕。
Route 固定路线
public partial class Route
{
public string Key { get; set; } = "";
public string Desc { get; set; } = "";
public string Group { get; set; } = "";
public string Message { get; set; } = "";
public List<string> Rooms = [];
}
理论上必须有,但我不太常用的一个数据结构了
Rooms是有顺序的房间Key,我为了避免混淆,特地把几个相似结构的房间信息起了不同的字段名。
说真的,固定遍历列表在一个地质变化激烈的MUD里,真没什么用
Trace 足迹
public partial class Trace
{
public string Key { get; set; } = "";
public string Group { get; set; } = "";
public string Desc { get; set; } = "";
public string Message { get; set; } = "";
public List<string> Locations { get; set; } = [];
}Locations是房间ID列表
这个其实是挺好玩的一个结构
他主要标记了可能出现的位置,比如要找行踪不定的托钵僧。
还可以用来记录一个区域内有那些房间的可以挖花。
对于他,我还特地做了一个API,可以给指定的Trace增加房间,这样方便在游街/任务的时候维护响应信息。
甚至在某些MUD里,可以记录任务NPC的出没位置,可以不用搜索整个区域。
当然,这个数据必须配合动态生成路径使用,不然没啥价值
Region 地区
public partial class Region
{
public string Key { get; set; } = "";
public string Group { get; set; } = "";
public string Desc { get; set; } = "";
public string Message { get; set; } = "";
public List<RegionItem> Items { get; set; } = [];
}不好玩,但实用的一个结构。
他和Trace都是无顺序房间集合,但这个的本质是一个查询,取出来后的房间需要缓存。
一个Region包含多个有顺序的RegionItem
public class RegionItem(RegionItemType type, string value, bool not)
{
public bool Not { get; set; } = not;
public RegionItemType Type { get; set; } = type;
public string Value { get; set; } = value;
}RegionItem很简单,3个属性
[*]Type 房间还是房间组(城市)
[*]Value 具体的值
[*]Not 添加还是排除
就是把一系列符合条件的房间,拼成一个房间集合
干说可能很难理解。
配合我那过时的北侠机器的一个数据来说明下吧
建康府=建康府*,建康府北城,建康府南城
就是把几个分组,和额外的房间,拼成一个实际使用的区域。
这样,扫街的代码只需要记录房间对应的Group就行。具体实际使用的路径,通过几个Group合并后,再生成动态路径即可。