机器人WBW
本帖最后由 林扬骐 于 2021-11-9 00:10 编辑

[#1]零、背景
[#2]一、性质要求设计
[#3]二、迷宫机运作基本流程
[#4]三、迷宫线路的建造(最初版本)
[#5]四、迷宫线路建造优化   (节点选择、失败记录、密度控制、路线修改、概率修正、实体数量控制、一帧多步加速、断点续打)
[#6]五、结语



跑酷迷宫机发布帖:
https://www.mcbbs.net/thread-1229566-1-1.html

就算没人来看咱也得硬着头皮来讲讲原理是吧

https://www.bilibili.com/blackboard/newplayer.html?playlist=false&crossDomain=1&aid=291862168&page=1




零、背景
       俺其实有过迷宫机的经验,即1.9版本时的“3d迷宫机”,大概是那段时间做得最漂亮的一个作品吧……
       那之后其实就有造高级迷宫机的想法了,于是1.12就开始策划跑酷迷宫机,无奈学业正紧(当时初三,现在马上大二),只出来个半成品(纠结在tellraw以及各种设置设计)

       高一后试图重拾呢,那时候1.13版本出来了,指令格式大修,原本的很多指令基本上算作废了吧……倒也留下些设计(感谢当时@⊙v⊙ 的合作,虽然最终未果)
       后来高中就开始追小马,大一重新追神奇宝贝,而且还有WBW梦想,很少碰我的世界了,不过设计还偶尔挂念在心
       大概三四天前又重新回忆起来这个项目,在设计上大大简化周边功能而保留核心功能,也大大放宽对迷宫路线审查的要求,发现好像可以动手了,于是一天就把核心原型给搞定了(嗯,就用了一天,迷宫机就可以跑起来了),后面两三天进行调试优化细化,就有了现在0.6.0版本,算是第一阶段的最终版本吧_(:3JZ)_
实际上1.13后实现起来要比1.12前容易多了,多亏了进行了强大改进的execute以及局部坐标功能(1.12那时候是考虑用结构方块的!)
       总之,现在用起来还算不错的跑酷迷宫生成器终于做出来了,可喜可贺。

(最上边的目录可点现在不可点了(doge))



一、性质要求设计:

       跑酷迷宫机,生成的是迷宫,路线是跑酷。
       一个合格的跑酷迷宫机,至少要保证,仅用生存模式下的行走跳跃,便足以达到迷宫各处。
       然后是个人的设计,即使你走错路了,也还有路能让你重新到达原来的位置(即,单行道不能是死路,不小心掉下去了咱还得有路再爬上去……一般跑酷地图也都要这么设计吧)

       另外,迷宫机应当能比较合理地利用所提供的空间,即路线既不过于密集(否则有难度的路线统统被简单路线取代,过于“平坦”),也要尽量钻进封闭空间中能够钻进的角落(否则迷宫路线便不够复杂,无法称之为“迷宫”)——emm这个设计要求我用“超细空间(stick)”测试了好久……算是能生成长度比较可观的垂直路线而不至于“卡层”了……

       还有针对巨型迷宫生成,所用的步骤是非常多的,实体量过多会拖慢速度,所以也作了优化的~

       大概就是这样……迷宫生成方便,并且做出来的迷宫好玩就行嘛





二、迷宫机运作基本流程

       可以参考发布帖的“使用步骤”,不过那是用户端,这里我们讲一下“运营端”:

       1.init初始化,计分板,分数设定(初始设置)
       2.(用户已经自行创建好迷宫建造的范围)接受用户指令start,执行“第一步”(生成第一个node节点),激活循环step进行建造!(stepping为1时总循环tick会调用step)
       3.(循环中的每一步step)选择合适节点——用随机器随意选择路线——判断所选路线是否在当前方块环境下可用——若能,则放置方块,此步结束;若不能,记作一次失败,此步仍然结束(“不走回头步”,但对“失败者”会有一些处理)
       4.持续循环第3步,等待结束条件
       5.结束end,进行得分点/终点设置,清空实体,彻底结束
       _(:3JZ)_突然想起来好像结束没有提示消息emmmm可能是习惯了看积分栏了吗23333不过如果雪球全部消失了也是结束(好吧赶紧改)好了0.6.1加上了结束消息提示)

       下面重点细讲步骤3的各个部分(步骤3算是核心,其他的都勉强算周边功能了……周边功能这里就不细讲了,愿意的可以翻翻数据包内部)






三、迷宫线路的建造(最初版本)


迷宫的路线是人为设计好的,而这些路线将由“操作单元”来组成:

       node实体单元,即跑酷落脚点,变体是node oneway(单行道节点,无法原路到达前一个节点)
       path实体单元,即跑酷路径空间(一个路线必须要留出这样的空间,才能保证这条生成的节点是可到达的),变体是path hit(原意“碰头块”,实际表示该跑酷路径不论有或没有这一格空间,这条路径生成的节点都是可到达的_(:3JZ)_不过难度可能不同罢了……)
       (见于ways/_unit,文件里其实还有ladder与web单元,是备用做拓展路线用的,然而目前并没有用上,便只有“方块与空气”)


由于在“路径审验”通过后:
       node会在当前格放置结构空位(后面结构空位会被清理成空气),在脚下放置石砖(落脚点)
       path会在当前格放置结构空位(path hit则是概率性放置……)


因此在“路径审验”时要求:
       1. node与path当前所在格不能是“bedrock类”(人为设置的封闭空间的边界块,以及落脚块,都属于“bedrock类”)
       ——否则,node与path在“当前格”放置的结构空位,将会消除掉可能必要的落脚点,或者破坏人为设置的边界而“路线流出”。
       2. node脚下不能是“path类”(主要是结构空位,虽说也有梯 子和蜘蛛网……但实际上还没有设计含有这两者的路线……目前路线是纯方块的,大概各位愿意的话可以试着自定义一下)
       ——否则,node生成的落脚块将会消除掉可能必要的通行空间

       3.node oneway 必须保证 ~1 ~1 ~或 ~-1 ~1 ~或~ ~1 ~1或~ ~1 ~-1(即上面一格的前后左右四格)是“path类”
       ——否则,此node将可能生成一条死路
       (人物基本上是1x2x1的空间占位,这种位置是保证通路的“必要空位”,不论是上格通路(“腿一格”),平通路,坠落通路(均为“头一格”),一定会在这种位置留有空位)
       _(:3JZ)_其实这种单行道基本上不会导向“新路线”,基本上都通往已生成的路线,所以作用大概就是形成“回路”(左右手法则不再适用,嗯),来“坑害”那些方向感较差或者跑酷水平不足的人吧
       4.path hit  与path类似,不同的是path hit如果不合格,单独剔除此path hit 便足够,不需要整条路线废除



现在重新回到这句话,详解步骤
选择合适节点——用随机器随意选择路线——判断所选路线是否在当前方块环境下可用——若能,则放置方块,此步结束;若不能,记作一次失败,此步仍然结束


(上边的文件夹中fallb与jumpb文件夹其实并没有用起来了……目前相当于一个备份)

       1.选择合适节点:初代基本是随机选择节点
       2.用随机器随意选择路线:初代18类路线(见上图),每类路线一个代号(0-17)用随机器抽取(上图2_select)
       ——ps:路线的初始朝向也是随机器,4向,代号0~3;(路线是用 ^ ^ ^2这类局坐标表示,所以4朝向也就意味着一类路线能表示的路线乘以4)(上图1_rotation)
       ——此外,“折角”的方式也是用随机器的(l不拐,b首左拐,d首右拐,p尾右拐,q尾左拐五种,代号0~4)(上图3_turn)
       “折角”的实现方式是tag,execute统一朝向,还有局部坐标tp。比如,b首左拐的代码:
  1. <font size="3">#execute as @e positioned as @s run teleport @s ^ ^ ^3
  2. #上面这样可以实现一切生物以我的视线为准向前移动
  3. #execute align xyz run tp @e[dy=1,dx=0,dz=0] @p
  4. #这样的选择器可以精确到格子
  5. tag @e[tag=path] add turning
  6. tag @e[tag=node,tag=new] add turning
  7. execute align xyz run tag @e[y=0,dy=256,dx=0,dz=0,tag=turning] remove turning
  8. execute as @e[tag=turning,tag=maze] positioned as @s run teleport @s ^1 ^ ^-1</font>
复制代码


       3.判断所选路线是否在当前方块环境下可用:即前文“路径审验时”内容(上图4_live)
       4.“若能,则放置方块,此步结束……”:见前文“路径审验通过后”内容,简单而言即对应单元实体使用对应的方块放置程序(上图5_block)
         “……若不能,记作一次失败,此步仍然结束”:初代似乎并没有加入失败惩罚,只是单纯凭随机而已……(上图5b_bad)
(注意,是对单元路径进行的整体决定,一个单元格不符合要求,此步生成的路径便作废)






四、跑酷迷宫机线路建造方法的优化

       啊今天时间不早了,就明天再继续写吧……好的咱们继续
       迷宫机的优化,包括节点选择、失败记录、密度控制、路线修改、概率修正、实体数量控制、一帧多步加速、断点续打


节点选择:       初代“选择节点”是纯随机,但这样选择的话,先生成的节点就有更多机会被抽到,实际上导致迷宫前面节点密,后面节点散,简直就一个“小爆炸”形状。
       现在的节点选择类似于深度优先——如果一次路径生成成功了,则下一个节点便选择新生成的节点继续——如果一直成功,路线可以像贪吃蛇一样延伸,路线足以做得很长了。

失败记录:
       随机选择不可能总是成功抽到合适的路线,如果出现失败了,就需要考虑换一个节点重新生成路线了
       然而实际上也不是一出现失败便更换节点,那样的话迷宫路线还是可能“过于散乱”:
      
失败上限:如果节点生成的数次路线都不合格,则认为此处已经几乎不能生成路径,便删除此节点,放弃在此处的路线生成。
       失败宽容:如果节点所处位置实际上很容易选到合适的生成路线,那么抽到失败路线便是小概率事件。为了让“贪吃蛇”稍微走长一点,给一些宽容次数是有必要的。……宽容次数达到上限后,此节点并不删除,而是更换节点(把机会留给其他节点,但实际上此处节点仍然不排除生成路线的可能)
      


密度控制:
       拒绝重复:初版是允许“节点重复”的(node下方允许是“bedrock类”方块),但这样同样会导致“前密后散”,因为节点会跑回原来生成过路线的地方 ——更严重的是,这样子迷宫节点会反复在原地来回跑,导致使用了步数但实际上没有新的路线生成。后面便改成不允许节点重复了。
       过密清理如果一个节点周围全是其他节点,那么此处生成的路线便大概率是重复路线,再加上上文的“拒绝重复”,基本上认为此节点是不容易生成路线的,于是便根据密度来清理这样的节点——另外,节点密集的区域,被随机抽取的概率也更高,但实际上不容易生成路线,也就是浪费了一次随机抽取,所以也说要清理这种节点密集的区域。
       (“过密清理”的旧方案是用的“定时清理”,但是会把某些关键节点去掉,这样原本可以继续生成路线的地方,还没等到“建造机会”到来,便已经被清理,导致这里的路线不能继续生成)
       (“过密清理”的新方案是“建造区引领”,如果新节点成功生成,便暂时不进行过密清理,而在路线生成失败宽容达到上限,将要更换节点时,对周围进行过密清理——这样便更有针对性,能较大概率保留那些容易生成路线的节点)

路线修改:
       如果预留空间实在狭小,那么路线便很难生成——只要有一格不符合要求,随机选出的路线便不可用——可以说迷宫路线审验算是十分苛刻的,因此“钻空子路线”也很难生成。
       不过由于path hit的审验要求是:
与path类似,不同的是path hit如果不合格,单独剔除此path hit 便足够,不需要整条路线废除

       因此path hit代替部分path,便大大提高了一条路径的审验通过几率,迷宫路线也不至于一直失败了。
       修改后的路线如下图所示:



概率修正:

      初代版本生成的路线总有向下的趋势,因为向下的fall路线共有9条(其中4条单行线,但是单行线下降都不止一格),而向上的jump路线仅有4条,向下概率明显高于向上概率因此需要概率修正。
       值得注意的是,execute的score matches功能可以用100..199这种表示一个范围值,这样便方便许多,例如:
  1. execute if score @s randomer matches 650..699 run function parkour_maze:ways/fall/2b
复制代码
       如此,路线便不是以代号,而是以数字范围,用自定义的概率值来生成了~
       此外,移动仅1格的路线所占格子少,很容易通过,但是这样的路线不仅缺少挑战性(嗯),而且还会“铺地板”,并导致迷宫的上下穿插路线变得难以生成——这样的跑酷迷宫咱们是不太喜欢的所以咱把这种路线的出现概率调低了,反正迷宫路线密集后其实也很容易出现这种“一格路线”(节点相邻即是)


实体数量控制:

       node数量控制:初代版是不控制节点总数量的,所以只要迷宫够大(比如example中的hell,默认有五十万建造步),路线生成便很难出现失败,而且即使进行了“过密清理”,node实体数量还是能轻轻松松上几千,导致电脑性能严重下降。
       因此便有了“过千减五百”,对node实体隔一段时间计一次数,达到一定实体数量便进行随机实体清理,以控制node的实体数量。
       低密度免死金牌:前文也提到过,还要避免能创建路线的关键节点被误删,因此设计为,如果一个node周围没有其他node,在随机清理时便不被清理,可见于代码:
  1. #定期清理防止数量超限

  2. #免死金牌:5格内没有"另外的"node,保留
  3. execute as @e[tag=node] at @s unless entity @e[distance=0.5..5,tag=node] run tag @s add lonely

  4. #过1000减500……设定上限也是一次减500
  5. execute store result score node_count maze_settings if entity @e[tag=node,tag=maze]
  6. scoreboard players operation goal_plus500 maze_settings = goal_count maze_settings
  7. scoreboard players add goal_plus500 maze_settings 500

  8. execute if score node_count maze_settings > node_limit maze_settings if score node_count maze_settings > goal_plus500 maze_settings run kill @e[sort=random,tag=node,tag=!lonely,limit=500]
  9. #不过如果goal_count本身就超500……就1000过了还不会清……另外500可能用其他清法了……


  10. #免死金牌是一次性的
  11. tag @e[tag=node,tag=lonely] remove lonely

  12. scoreboard players operation cleanremain maze_settings = cleanloop maze_settings
复制代码


       clear数量控制:clear实体是用来在迷宫结束后把结构空位换成空气的实体,初代是在node被删除后用clear替换来实现范围界定的,但是这也意味着一个node就对应一个clear,而且是一种“历史记录”般的存在,数量甚至可以多于node的数量。
       新版本使用的方法是“网络信号塔”类似的方案,在一定范围内如果有clear存在,此node last便不生成新的clear,而如果范围内不存在clear,则在node last所在的位置生成clear(node last是最后生成路径成功的节点),如此便可以用较少的实体覆盖较大的范围了。
      代码见于ways/5_block(随机选择到的node节点执行的第5步,在第4步审验成功后放置方块的函数,clear的内容也放在里面)
  1. #clear的检测与放置(用于清理结构空位)
  2. execute unless entity @e[tag=clear,tag=maze,distance=..10] run function parkour_maze:ways/_unit/_clear
复制代码


一帧多步:

      控制了实体数量后,建造迷宫时电脑已经流畅了不少……然而这时却感受到1秒20游戏帧对于hell这种500000建造步的怪物来说真是“手无缚鸡之力”啊~
       所以咱们再把速度提高一些,用个自迭代让一帧执行多次step~
       现在tick调用step的代码部分是这样的:
  1. #状态
  2. execute if score stepping maze_settings matches 1 run scoreboard players operation multistep_remain maze_settings = multistep maze_settings
  3. execute if score stepping maze_settings matches 1 run function parkour_maze:tick/step
复制代码

       step最后自迭代用的代码是这样的:
  1. #一帧多步
  2. scoreboard players remove multistep_remain maze_settings 1
  3. execute if score multistep_remain maze_settings matches 1.. run function parkour_maze:tick/step
复制代码


断点续打:

      套用了3d打印机的一个说法嗯不过实际上指的是:如果迷宫正在建造,则即使退出游戏重进,或使用/reload,迷宫都可以继续建造。
      /reload仍然继续建造:方法其实挺简单的,init是/reload后自动执行的命令,原本init是执行分数初始化操作的,把这些东西转移到reload函数中,而让init仅在迷宫并未建造时调用reload就好了(不过init还是会先把maze_setting计分板创建好,否则execute unless的检测会出问题)
      【0.6.2版本为防止名字误解,把init和reload互换了一下,现在系统启动时调用的是reload,分数初始化则放到init里】
      重启游戏继续建造:这说起来有些诡异……原本step里是有这样一段代码用作自动停止的
  1. execute unless entity @e[tag=node,tag=maze] run scoreboard players set remain maze_settings -1
复制代码
       但是不知是关闭游戏时还是重启游戏后,这段代码似乎无视条件而直接运行了???就算有实体node,重启游戏后它还是会把分数改变,导致建造终止……个人推测是退出游戏一瞬间实体不能被探测到或者什么的……(俺也不晓得咋回事啦       而去掉这段指令后,退出游戏重进,迷宫就能继续建造了(此前已经把“/reload仍继续建造”做好了),算是运气吧?
       不过话说回来,现在迷宫建造,即使在后期路线生成一直失败的情况下,也不会把node实体全部清理掉,至少还会保留goal_count数量的node节点,所以这段代码也不适合加回去了各位如果建造迷宫真的卡带又等不及的话,用end函数吧~
  1. /function parkour_maze:end
复制代码





五、结语

       跑酷迷宫机总体设计其实还是简单的,而优化则需要实验后才能想得到、想得全。
       大概比较理想的方法便是,先做原型机让代码能跑起来,然后在实践中慢慢来优化吧。


       跑酷迷宫机实际上还有不少拓展空间的,比如设计新路线(甚至可以包括机关),而这些就看诸位的创造力了~
       最后还是希望地图制作者们愿意使用这个跑酷迷宫生成器吧,这样诸位的地图可以多一种元素,而跑酷迷宫机也才能有存在的意义,多谢



跑酷迷宫机发布帖:
https://www.mcbbs.net/thread-1229566-1-1.html













来自群组: The Command's Power

luckyxing
有实质图片的吗

唯记你温柔
全是文字没啥直观感受