jarlyyn 发表于 2025-5-20 14:44:15

杰哥瞎扯蛋之我的地图数据结构

俗话说的好,要想富,先修路。
要想玩MUD,特别是地图大的MUD,早早的开始积累地图数据是很重要的一点。

正好前一阵折腾Avaloniaui,做了一个地图数据管理器,正好整理了一下我的地图数据的格式。

发出来抛砖引玉一下。

注意,我分享的是 地图数据 的数据结构, 不是地图数据,也不是 怎么抓地图/定位/走迷宫,分享后连个属于折腾WIZ和玩家。

jarlyyn 发表于 2025-5-20 14:50:52

首先,我们要明确一点,在北侠的环境下,各种地图信息格式百花齐放。

有文本流的,有脚本流的,有数据库流的,还有脚本专有格式流的。

对于我而言,一直是文本流的拥护者。

第一,便于维护/批量维护

第二,便于进行版本管理,通过对比能看看两个版本之间变动了那些信息。设置我还准备 做过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之争)

jarlyyn 发表于 2025-5-20 15:08:42

核心概念。

对于地图来说,我们的用途主要是路径规划,房间查询,信息查询。

核心概念主要有4个

Room/Key房间/主键

每个房间,是一个抽象的概念,房间名描述之类属于附属信息,甚至是快照信息。只有为一的主键才是核心。

Key必须唯一,应该有实际意义,但不该有业务信息。

比如,扬州->广场,可以叫yzgc,但不该叫yz或者yz-(xxx任务信息)

因为业务可能会变,实际意义不随着业务逻辑变,要变也是信息重新录入。

这样,写代码是只要关心有一个key,不要关心其他的,其他的不属于核心功能。

Exit/出口

出口是房间和房间之间的单向通行的信息。

每个出口包括目的地,指令,条件,耗时。

特别的,以我的经验的话,迷宫是需要代码做一些特殊指令的,不应该通过地图信息来处理。

另外,出口是不包含起点信息的。

因为理论上有一些飞行指令,可以在符合条件(比如室外)的房间,都飞到固定点去(北侠好像只有养飞禽才能,我没用过)。

Cost 耗时

耗时时每一个出口的费用的评估。理论上是最小是1的整数。

寻路就是按照最小的耗时去找路线。有些房间之间虽然指令少,但可能有BUSY等限制,可能绕一下更合理。

条件 Condtion

这是比较核心的一个数据。

有些路径,有门派限制,有些路径,有性别限制。

比如我记得明教女休息室,只有 门派明教,性别女才能进去。

那这时候,我们可以通过给出口加上调教,比如 有 明教 标签 和 女性标签,才能使用,就能利用这个地图数据了。

既然有条件,我们还应该有 白名单和黑名单的概念。即只有某个标签能进入,和只有某个标签不能进。

比如有些房间,丐帮的进去会被踢,那我们就不进去。

这就是基本的标签/条件模式。

更复杂点。

很多地方,能不能通行不光光是 有/没有。

比如要一定的轻功/内力/技能才能通行。

这时候我们可以在代码里生成N个不同的标签,但这太丑了,更不好维护。

所以我们的标签是带数值的标签。

默认的标签和条件就是数值1.

指定数值的话

force:100 指你有100级force
这样你能进入 force:40的出口,但不能进force:200的出口。

这样,配合标签,数值,取反几个操作,我们能解决绝大部分的差异化路线规划的问题了。


jarlyyn 发表于 2025-5-20 15:13:35

好,开始来细节了。

首先,就是房间信息的结构

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 15:15:53

本帖最后由 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是耗时

直观界面大概是这样


jarlyyn 发表于 2025-5-20 15:25:01

好,最重要的数据体层结构有了。

接下去我们要看应用的数据了。

一般来说,我不建议机器里使用的数据全部是写死的魔数,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列表了。

房间列表之类同理。


jarlyyn 发表于 2025-5-20 15:29:25

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这种别面来记录她的位置,那么她哪怕拆迁到美国德州,我们也不怕。

jarlyyn 发表于 2025-5-20 15:32:23

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里,真没什么用


jarlyyn 发表于 2025-5-20 15:39:58

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的出没位置,可以不用搜索整个区域。

当然,这个数据必须配合动态生成路径使用,不然没啥价值



jarlyyn 发表于 2025-5-20 15:47:09

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合并后,再生成动态路径即可。

页: [1] 2 3
查看完整版本: 杰哥瞎扯蛋之我的地图数据结构