Translation notice
This page was translated with machine translation and may contain inaccuracies. If you can help improve it, please open an issue or submit a pull request.

How to adapt single-player games to multiplayer - taking Xu Muxian's Pipes as an example
This article will be based on Xu Muxian's version of Pipes data pack for multi-player adaptation. As a general example, the function of this data pack is only used as a background and is not a core factor for multi-player compatibility. This article is mainly based on idea analysis and simple modifications to the original data pack for multi-person adaptation. The content is relatively simple.
Game introduction
There should be quite a few Pipes articles in this issue of Feature, so I won’t go into details about the functions and gameplay of data pack here. Simply put, we need to adapt the generation-display-interaction-decision process to a multi-player environment.
analyze
Front-end interaction
The dialog-based interaction solution designed by Xu Muxian is naturally suitable for compatibility with multiple people, because the dialog is only displayed to a single player. Similar solutions include display solutions based on the chat bar or title. If you use a display entity, you need to pay attention to the visibility and interaction permissions of the display entity. We will continue to use the dialog solution here.
Instance storage
The game being played by each player should be abstracted into an independent game instance, and the dialog itself is only used as a rendering and interaction tool, so we need to maintain a game instance for each player. The game instance contains information such as the current level, player position, game status, etc.
data pack status quo
This version of Pipes is not suitable for multiple people mainly because there is only one global variable slot, and all operations of the player are based on this global variable. So we have two options:
The data pack has been greatly changed to implement an independent variable slot for each player and operate within the slot.
Fine-tune the data pack, each player stores its own game instance separately, and clones its own instance to the global variable slot before responding to player operations.
Let’s choose the latter solution first.
accomplish
data structure
Let’s first check the data structure. The original article uses a column-major two-dimensional array.gridto store the map,
each of themgrid[x][y]All have the following data:
index:The index of the node, counting from 1, calculated as
<index>=<x>+<y>*<width>+1。parent_x:The parent node of this node
coordinate。 parent_y:The parent node of this node
coordinate。 side:The connection status of the node in the four directions. There are 4 elements in the array, representing the four directions of left, up, right, and down in turn, among which
0bis not connected,1bfor connection.source:Whether the node is the root node.
state:The status of the node,
0bFor nothing,1bFor irrigation,2bas a warning,3bTo be visited.visited:Whether the node has been visited during the generation phase has no practical effect during the game phase.
x:The node's
coordinate。 y:The node's
coordinate。
stored instorage pipes:grid gridWithin, this is the core variable of the game instance, and there are
storage pipes:grid dialogUsed to store custom text for the frontend
(score_holder) #record pipes.varUsed to store play time
These three data will form a game instance. But for convenience, we only consider the compatibility of the first data.
independent storage
In order to achieve multi-player compatibility, we only need to use
function pipes:prim/Create a map and addstorage pipes:grid gridCopy the contents into your own cache.
We can allocate storage space to the player according to the player uid and use macros to access it. Since we only access it once when the player operates, this macro does not bring much consumption.
When each player creates a game instance, we use the following command to automatically assign consecutive uids to the players. Players that have obtained uids will not be assigned repeatedly.
# game:new_game.mcfunction
execute unless score @s pipes.uid matches -2147483648..2147483647 store result score @s pipes.uid run scoreboard players add #Pointer pipes.uid 1Then we create the map and copy it to cache
# game:new_game.mcfunction
$scoreboard players set #width pipes.var $(w)
$scoreboard players set #height pipes.var $(h)
function pipes:prim/
function pipes:upset/
execute store result storage pipes:cache uid int 1.0 run scoreboard players get @s pipes.uid
function game:_copy_grid_to_cache with storage pipes:cache# game:_copy_grid_to_cache
$execute unless data storage pipes:cache players[{uid:$(uid)}] run data modify storage pipes:cache players append value {uid:$(uid)}
$data modify storage pipes:cache players[{uid:$(uid)}].grid set from storage pipes:grid gridUse the built-in renderer to render puzzles in the cache
# game:display
execute store result storage pipes:cache uid int 1.0 run scoreboard players get @s pipes.uid
function game:_copy_cache_to_grid with storage pipes:cache
function pipes:display/
scoreboard players enable @s pipes.trigger
scoreboard players enable @s pipes.operation# game:_copy_cache_to_grid
$data modify storage pipes:grid grid set from storage pipes:cache players[{uid:$(uid)}].gridNow a basic function of creating a new game example is ready. Different players can pass
function game:new_game {w:11,h:11}
function game:displayto create and display your own games without interfering with each other

Further package pull and upload as functions to facilitate later calls.
# game:pull_from_cahce
execute store result storage pipes:cache uid int 1.0 run scoreboard players get @s pipes.uid
function game:_copy_cache_to_grid with storage pipes:cache# game:push_to_cache
execute store result storage pipes:cache uid int 1.0 run scoreboard players get @s pipes.uid
function game:_copy_grid_to_cache with storage pipes:cacheresponse operation
Every time the player clicks on the tile, we need
Pull player cache
Perform original operation
Upload player cache
This means we need to simply modify the rotation trigger link
Enterpipes:operation/trigger/
# pipes:operation/trigger/
#旋转管道
advancement revoke @s only pipes:operation
execute store result storage pipes:grid macro.tile_index int 1.0 run scoreboard players get @s pipes.operation
function pipes:operation/trigger/tile with storage pipes:grid macro
scoreboard players reset @s pipes.operation
scoreboard players enable @s pipes.operation
#解题判定
function pipes:operation/tarjan/
#显示操作后的图
function pipes:display/
#音效
playsound item.book.page_turn player @sWe can just add the pull and upload operations before the entire file.
# pipes:operation/trigger/
function game:pull_from_cache
#旋转管道
advancement revoke @s only pipes:operation
execute store result storage pipes:grid macro.tile_index int 1.0 run scoreboard players get @s pipes.operation
function pipes:operation/trigger/tile with storage pipes:grid macro
scoreboard players reset @s pipes.operation
scoreboard players enable @s pipes.operation
#解题判定
function pipes:operation/tarjan/
#显示操作后的图
function pipes:display/
#音效
playsound item.book.page_turn player @s
function game:push_to_cacheA more robust approach is to override advancementpipes:operation, redirect it to functiongame:operation
# pipes:operation
{
"criteria": {
"rotate": {
"conditions": {
"player": [
{
"condition": "minecraft:any_of",
"terms": [
{
"condition": "minecraft:entity_scores",
"entity": "this",
"scores": {
"pipes.operation": {
"min": 1
}
}
},
{
"condition": "minecraft:entity_scores",
"entity": "this",
"scores": {
"pipes.operation": -1
}
}
]
}
]
},
"trigger": "minecraft:tick"
}
},
"rewards": {
"function": "game:operation"
}
}# game:operation
function game:pull_from_cache
function pipes:operation/trigger/
function game:push_to_cacheCustom frontend
We have not directly modified the original code, which means that what we provide is only an independent compatibility layer, and we can customize our dialog front end later.

just nowgame:displayThe original rendering method is called infunction pipes:display/
You only need to modify this method to your own rendering function and matching dialog interface to create a custom front end.
The custom front end provided below cuts off the multi-level menu of the original package and only retains the game itself.
# game:display
function game:pull_from_cache
# template from original pack
data modify storage pipes:grid cache.processing_data set from storage pipes:grid grid
data modify storage pipes:grid dialog.body.contents[2] set value [""]
data modify storage pipes:grid cache.processing_data_cache set value []
function pipes:display/height
data modify storage pipes:grid dialog.body.contents[2][-1] set value "\n\n\n\n"
execute unless entity @s[tag=pipes.win] run data modify storage pipes:grid dialog.body.contents[3] set value ""
execute if entity @s[tag=pipes.win] run function pipes:display/win
# injection: remove buttons
data modify storage pipes:grid dialog.actions set value [\
{action: {type: "minecraft:run_command", command: "function game:new_game {w:5, h:5}"}, label:{"translate":"dialog.pipes.game.new_game"}}\
]
function pipes:display/show with storage pipes:grid
data remove storage pipes:grid cache.processing_data
scoreboard players enable @s pipes.trigger
scoreboard players enable @s pipes.operationTo fully customize the frontend, you need to override it in the same waypipes:tirggeradvancement. Here are just simple examples.

Summarize
This article does not modify the original data pack, but implements a minimally intrusive compatibility layer by overwriting only one advancement file, thereby modifying a game that was originally only compatible with a single player into a version that is compatible with multiple players.

On the one hand, this is due to the good data management of the original data pack, that is, a clear global variable slot, which has nothing to do with the player, allowing us to use the existing architecture to the greatest extent to develop the compatibility layer. On the other hand, thanks to the clear interactive response architecture, we can inject the upload and pull operations of the player cache with minimal coverage.