肉丁土豆园地

安静的小博客里,属于我的编程时光
稳定编号系统 - 我的世界OCO指令系列

最近在搞一些我的世界指令。
其中有这么一个指令,需要编号系统,也就是 uid 。

本指令需要引用的模块:

意义

通常的编号系统中,当玩家进入或退出时,编号会从头重新分配。然而这样会导致编号不是很稳定。

比如编号的一个用途是用来传送特定玩家。按照通常的编号分配方法,如果你现在选中了一个玩家,马上要传送了。这个时候突然一个新玩家进来了,导致重新分配编号,有没有可能在你决定传送的下一秒钟,你选中玩家的编号就因为重新分配而改变,然后你就传送错人了呢?

对于这一类问题,最根本的解决方法,就是搞一个“稳定”的编号系统。当新玩家加入的时候,只为新玩家分配编号;而当有玩家退出时,也只尽量少地对玩家编号重新分配。

实现方法简述

我想到了一些稳定编号系统的实现,这里搭配图例说明:

稳定时的情况

比如现在有这么一个多人游戏,游戏里有 \(4\) 个人。
对于这个状况,用名为 ID 的计分板存储编号,每个人都被分配了从 \(0\) 开始数的编号,用 C-counterID 存储当前玩家数。

稳定时

玩家加入

首先,我们考虑来了一个人。
鉴于这个人可能曾经进入过游戏,身上可能带着曾经被排的号,我们就用 \(n\) 来表示。

来一人等着

我们不难发现,在其他人编号不改变的情况下给他分号,他只能是 \(4\) 号。

来一人变号

这样,一共就有 \(5\) 个玩家了。我们把 C-counterID 递增一次。

来一人完成

玩家退出

玩家还有可能退出。我们先考虑普通玩家退出的情况。假如我们的 \(1\) 号玩家退出了。

退一人刚刚

那么我们发现,让编号最大的人的编号 \(3\) 变为退出的人的编号 \(1\) ,是改变编号最少的重排方法。

退一人变号

同时,除了普通玩家退出,还有编号最大的人退出的情况。这里假如我们的 \(3\) 号玩家退出了。

退一人特殊

我们不难发现,这种情况只需要给 C-counterID 值递减一次就可以了。

退一人完成

错误处理

考虑到这个系统比较的复杂且容错性较低,这里提供一套处理编号错误的方法。

首先,显而易见的是对于这个编号系统,可能出现的错误有三种:

  • C-counterID 值出错,与房间实际玩家数量不相等。
  • 有两个人同时使用同一个编号。
  • 有一个应该被实用的编号空着,没有人使用。

简单的错误处理

最简单的错误处理方法就是重新排。我们只需要把所有人都踢了,重设 C-counterID 值,再让他们回来,就可以处理一切错误了。

不过实际上不需要把所有人都踢了。如果是通过标签标记哪些人需要被分配编号的话,只需要把所有人的标签都取消,再重新打上标签,就能得到一样的效果。

显而易见的是这种错误处理方法比较的暴力,相当于从头重新分配一次编号,破坏了稳定性。

不过,当你认为房间编号系统基本不会受到干扰而出现错误的话,这确实是一个很容易实施的方法。

稳定的错误处理

考虑到房间里无形的各种干扰,定时处理一次错误是比较完善的做法,于是我们需要更稳定的错误处理方法来保证每次错误处理不会有太大的稳定性开销。

这里,我想到一种最稳定的做法。就是首先把 C-counterID 变成正确的玩家数量,再寻找需要重新分配的编号的人——可以分为两种:

  • 和其他人使用同一个编号的人,也就是占用他人编号的人。
  • 编号大于等于 C-counterID 值的人

然后我们可以一个一个从这些人中抽人,挑出来插入到空编号里。

具体指令实现

在实际编写指令过程中,发现考虑到这个系统实在容错性不是很高,干脆可以把新玩家进入和之前玩家退出的情况当成一种错误情况,再对其进行处理,以此来排号。这样子可以避免异步指令出现的不可预料的繁多状态,并且由于不需异步等待,延迟可以为 \(0\) ,效率高。

用 JavaScript 伪代码可以表示成这样:

let roundNow = 0;
while (true) {
  let count = select('@e', 'inlist' in tags).count();
  let max = count - 1;
  select('@e')
    .addTag('new');
  select('@e', scores.round === roundNow)
    .removeTag('new');
  roundNow++;
  let idNow = 0;
  while (idNow < count) {
    let idEntity = select('@r', scores.id === idNow);
    idEntity.setScore('round', roundNow);
    if (!idEntity) {
      select('@r', 'new' in tags)
        .setScore('round', roundNow)
        .setScore('id', idNow)
        .removeTag('new');
    }
    idNow++;
  }
}

主体命令方块串:

// 统计数量
scoreboard players set ID-counter C-ne 0
execute @e[tag=ID-inlist] ~~~ scoreboard players add ID-counter C-ne 1
scoreboard players operation ID-max C-ne = ID-counter C-ne
scoreboard players remove ID-max C-ne 1
// 整理当前检测值
scoreboard players add ID-checking C-ne 1
scoreboard players operation ID-checking C-ne %= ID-counter C-ne
scoreboard players operation @e[tag=C-caller] C-temp = ID-counter C-ne
execute @e[tag=C-caller,scores={C-temp=0}] ~~~ scoreboard players set ID-checking C-ne -1
// 检查是否是新回合
scoreboard players operation @e[tag=C-caller] C-temp = ID-checking C-ne
tag @e[tag=C-caller,scores={C-temp=0}] add ID-newRound
// 标记编号越界的人
execute @e[tag=ID-newRound] ~~~ execute @e[tag=ID-inlist] ~~~ scoreboard players operation @s ID-round -= ID-round C-ne
execute @e[tag=ID-newRound] ~~~ tag @e[tag=ID-inlist] add ID-new
execute @e[tag=ID-newRound] ~~~ tag @e[scores={ID-round=0}] remove ID-new
// 回合递增
execute @e[tag=ID-newRound] ~~~ scoreboard players add ID-round C-ne 1
// 结束检查新回合
tag @e[tag=ID-newRound] remove ID-newRound
// 选择匹配当前检测编号的人
execute @e[tag=ID-inlist] ~~~ scoreboard players operation @s C-temp = @s ID
execute @e[tag=ID-inlist] ~~~ scoreboard players operation @s C-temp -= ID-checking C-ne
tag @e[scores={C-temp=0},tag=ID-inlist] add ID-checking
// 获得可以加入的人
tag @e[tag=C-caller] add ID-void
execute @e[tag=ID-checking] ~~~ tag @e[tag=ID-void] remove ID-void
execute @e[tag=ID-void] ~~~ tag @r[tag=ID-new,type=!egg] add ID-newing
tag @e[tag=ID-void] remove ID-void
// 给新加入的人赋编号
scoreboard players operation @e[tag=ID-newing] ID = ID-checking C-ne
// 让加入的人匹配编号检测
tag @e[tag=ID-newing] add ID-checking
// 完成加入
tag @e[tag=ID-newing] remove ID-newing
// 更新检测回合
scoreboard players operation @r[tag=ID-checking,type=!egg] ID-round = ID-round C-ne
// 结束检测
tag @e[tag=ID-checking] remove ID-checking

除了第一个命令方块为循环、无条件、延迟随意之外,其余命令均为连锁、保持开启、 \(0\) 延迟。

玩家进入退出检测框架中的命令串:

// 把玩家归到编号里
tag @e[type=player,tag=TE-new] add ID-inlist
// 编号新建
tag @e[tag=ID-inlist,tag=TE-new] add ID-new
scoreboard players reset @e[tag=ID-inlist,tag=TE-new] ID

均为连锁保持开启无延迟。

posted on 2024-02-05 17:47  肉丁土豆表  阅读(22)  评论(0编辑  收藏  举报