使用程序生成的迷宫,在Minecraft中进行建设。(走)

はじめに

みなさんはMinecraftをプレイしていて、「自動的に建物が建ったらいいのになぁ」なんて思ったことはありませんか?僕はあります。そこで、マイクラの建築にプログラムを持ち込んでみようと思ったのが、この記事を書くに至ったきっかけです。
今回、マイクラ内で自動的に迷路を建築するプログラムをGoで作成し実際に動作させたので、それを行うまでの手順を紹介します。
また、迷路を建築する際に、プログラムからコマンドを実行しているので、迷路建築以外の目的でコマンドをプログラムから実行したいと考えている方の参考にもなるかもしれません。

Building maze

概述

プログラムに自動的に建物を建ててもらうには、当然建物の設計図が必要です。しかし、自分で設計図をなんらかの形で作成するのでは、それなりの手間もかかりますし、なんなら普通にマイクラ内で自分で建てたほうが早い場合もあるかもしれません。
かと言って、機械学習等を用いてプログラムによって建物デザインの作成から行い、建築してくれるプログラムを作り上げるほどの技量は僕にはありません。

そこで、確立されたアルゴリズムによって生成でき、ゲーム内に建てることで実際に遊べる迷路ならできるのではないかと考え、実際に作成しました。

本文介绍了在实际玩Minecraft游戏中,目标是创建一个能够自动建造随机生成的迷宫的程序,并详细解释了每个步骤。

迷路的制作

在Minecraft的世界中使用程序来创建迷宫,大致按照以下两个步骤进行实施。

    • プログラム上で迷路の設計図を生成する

 

    設計図をもとにMinecraftの世界にブロックを配置する

制作设计图

最終目标是在Minecraft中构建一个迷宫建筑程序,但首先我们会在程序中尝试表示迷宫。

■ ■ ■ ■ ■ ■ ■ ■ ■ ■ ■ ■ ■ ■ ■ ■ ■ ■ ■ ■ ■ 
■ □ □ □ □ □ ■ □ □ □ □ □ □ □ ■ □ □ □ □ □ ■ 
■ □ ■ ■ ■ □ ■ ■ ■ ■ ■ ■ ■ □ ■ ■ ■ ■ ■ □ ■ 
■ □ ■ □ □ □ □ □ ■ □ □ □ □ □ ■ □ □ □ □ □ ■ 
■ □ ■ ■ ■ ■ ■ ■ ■ □ ■ ■ ■ □ ■ ■ ■ ■ ■ □ ■ 
■ □ □ □ □ □ □ □ □ □ ■ □ □ □ □ □ ■ □ ■ □ ■ 
■ □ ■ ■ ■ □ ■ ■ ■ □ ■ ■ ■ ■ ■ ■ ■ □ ■ □ ■ 
■ □ ■ □ □ □ ■ □ □ □ □ □ □ □ □ □ □ □ ■ □ ■ 
■ □ ■ ■ ■ ■ ■ ■ ■ ■ ■ ■ ■ ■ ■ ■ ■ □ ■ □ ■ 
■ □ □ □ □ □ □ □ □ □ ■ □ □ □ □ □ ■ □ ■ □ ■ 
■ ■ ■ ■ ■ □ ■ ■ ■ □ ■ ■ ■ ■ ■ □ ■ □ ■ □ ■ 
■ □ □ □ □ □ ■ □ □ □ □ □ ■ □ □ □ □ □ □ □ ■ 
■ ■ ■ ■ ■ ■ ■ ■ ■ ■ ■ ■ ■ ■ ■ ■ ■ ■ ■ ■ ■ 

这是一个用黑白方块表示的迷宫。首先在Minecraft内,我们将尝试将其作为迷宫建筑的设计图。

今回迷路を作成する上で利用したのは、壁伸ばし法と呼ばれるアルゴリズムです。この手順で迷路を生成することで、ループや閉じた領域が生まれないようにできます。

在墙延展法中,我们通过以下步骤来创建迷宫。同时,在实施过程中,我参考了以下页面。

 

    1. 生成一个由二维数组组成的迷宫,使其的宽度和高度都为大于等于5的奇数。

 

    1. 设置外围为墙壁,其他位置为通道。

 

    1. 记录坐标x和y都是偶数的点作为墙壁的起点,将其列为起点列表。

 

    1. 随机选择起点列表中的一个坐标,如果该坐标是通道,则进行第5步的墙壁扩展处理。重复此过程直到所有起点变成墙壁。

 

    1. 进行墙壁扩展处理。

将指定坐标设置为墙壁。
在该位置可以扩展的方向上(该方向相邻的单元格是通道且该方向的下两个单元格不是当前正在扩展的墙壁方向)。
如果扩展方向的下两个单元格是已存在的墙壁,则结束墙壁的扩展。
如果是通道,则继续从该单元格向下扩展。
如果四周都是正在扩展的墙壁,则回退正在扩展的墙壁,直到找到可以扩展的坐标,然后重新开始墙壁的扩展。

這樣寫下來,實際上我們這次實施的程式碼中,這部分的摘錄如下。

通过这个CreateMaze函数,以迷宽和迷高作为参数传入,并返回一个被视为迷宫的int型二维切片。通过这个函数,我们现在可以在Minecraft中准备迷宫的设计图了!

type Cell struct {
	X int
	Y int
}

func isCurrentWall(s []Cell, n Cell) bool {
	for _, v := range s {
		if n == v {
			return true
		}
	}
	return false
}

func (c *Cell) isEmpty() bool {
	empty := Cell{}
	return *c == empty
}

func CreateMaze(height int, width int) ([][]int, error) {
	if height%2 == 0 {												// サイズの奇数合わせ
		height--
	}
	if width%2 == 0 {
		width--
	}
	if height < 5 || width < 5 {									// サイズのチェック
		return nil, fmt.Errorf("size is too small")
	}

	maze := make([][]int, height)									// 二次元スライス初期化
	for i := 0; i < height; i++ {
		maze[i] = make([]int, width)
	}

	var startCells []Cell
	for i, v := range maze {										// 周囲の壁化と起点取得
		for j := range v {
			if i == 0 || j == 0 || i == height-1 || j == width-1 {
				maze[i][j] = -1
			} else {
				if i%2 == 0 && j%2 == 0 {
					startCells = append(startCells, Cell{j, i})
				}
			}
		}
	}

	for len(startCells) != 0 {										// 迷路生成(起点リストを回す)
		rand.Seed(time.Now().UnixNano())
		r := rand.Intn(len(startCells))
		s := startCells[r]

		if maze[s.Y][s.X] != 0 {									// その起点が既に壁の場合リストから除外
			var tmp []Cell
			for i := 0; i < len(startCells); i++ {
				if i != r {
					tmp = append(tmp, startCells[i])
				}
			}
			startCells = tmp
			continue
		}

		currentWall := []Cell{s}

		for {														// 起点から壁伸ばし処理
			d := Cell{0, 0}
			for {                                                   // 進む方向決め
				if maze[s.Y-1][s.X] != 0 && isCurrentWall(currentWall, Cell{s.X, s.Y - 2}) &&
					maze[s.Y][s.X+1] != 0 && isCurrentWall(currentWall, Cell{s.X + 2, s.Y}) &&
					maze[s.Y+1][s.X] != 0 && isCurrentWall(currentWall, Cell{s.X, s.Y + 2}) &&
					maze[s.Y][s.X-1] != 0 && isCurrentWall(currentWall, Cell{s.X - 2, s.Y}) {
                                                                    // どこにも進めないならバックする
					if len(currentWall) > 3 {
						s = currentWall[len(currentWall)-2]
						currentWall = currentWall[:len(currentWall)-2]
					} else {
						currentWall = []Cell{}
					}
					break
				}

				switch rand.Intn(4) {
				case 0:
					{
						d = Cell{0, -1}
					}
				case 1:
					{
						d = Cell{1, 0}
					}
				case 2:
					{
						d = Cell{0, 1}
					}
				case 3:
					{
						d = Cell{-1, 0}
					}
				}
				if maze[s.Y+d.Y][s.X+d.X] == 0 && !isCurrentWall(currentWall, Cell{s.X + 2*d.X, s.Y + 2*d.Y}) { 
                                                                    // 進める方向なら方向決め終わり
					break
				}
			}
			if d.isEmpty() {										// どこにも進めなければdはEmpty
				continue
			}

			currentWall = append(currentWall, Cell{s.X + d.X, s.Y + d.Y}) // 壁に当たっても当たらなくても1マスは進む
			if maze[s.Y+2*d.Y][s.X+2*d.X] != 0 {					// 壁に当たったら
				break												// 壁の拡張終了
			} else {
				s = Cell{s.X + 2*d.X, s.Y + 2*d.Y}                  // 2マス進めて次のループ
				currentWall = append(currentWall, s)
			}
		}
		for i, v := range currentWall {								// 壁を確定
			maze[v.Y][v.X] = i + 1
		}
	}
	return maze, nil
}

Minecraft内に迷路を建築する

迷路の設計図ができたので、今度はMinecraftの世界にこれを建築したいと思います。

しかし、できた設計図を見ながら直接ブロックを置いていくのではなく、これもプログラムを用いて行いたいので、まずはプログラムでMinecraftの世界に手を加える方法について考えていきます。

RCON

我认为在玩Minecraft时,会使用命令作为一个工具来减轻工作负担。它可以方便地铺设大范围的方块,获得想要的物品等,非常实用。

在Minecraft的服务器程序中,有一个可以执行该命令的功能,并且通过使用RCON功能,可以远程执行命令。
因此,我们计划通过从程序中操作RCON来间接地对Minecraft的世界进行干涉。

RCON是一种使用TCP进行通信的严格意义上的通信协议,但具体的规范我没有进行调查。然而,通过在指定格式下进行TCP通信,可以从远程执行命令以与服务器进行交互。由于本次操作使用了一个代替的库来执行这部分功能,因此并不需要与通信相关的知识。

这次使用的库在这里。

 

在使用时,

go get github.com/willroberts/minecraft-client

可以在Go程序中使用。

另外,这是Go的模块,但以下页面也列出了其他语言的库。

 

具体的使用方法如下(摘自README)。

// Create a new client and connect to the server.
client, err := minecraft.NewClient(minecraft.ClientOptions{
	Hostport: "127.0.0.1:25575",
	Timeout: 5 * time.Second, // Optional, this is the default value.
})
if err != nil {
	log.Fatal(err)
}
defer client.Close()

// Send some commands.
if err := client.Authenticate("password"); err != nil {
	log.Fatal(err)
}
resp, err := client.SendCommand("seed")
if err != nil {
	log.Fatal(err)
}
log.Println(resp.Body) // "Seed: [1871644822592853811]"

这里显示的端口号和密码是在Minecraft服务器的server.properties文件中按以下方式记录:

enable-rcon=true
rcon.password=password
rcon.port=25575

按照上述的使用方法,通过将要执行的命令作为字符串参数传递给SendCommand方法,命令将被发送到在client实例初始化时指定的连接目标。如果想要看到执行结果,则可以通过输出返回值来查看。

這次,我們將把事先準備好的迷宮設計圖與Minecraft內的座標相對應,透過放置方塊的指令來進行建築。

实施

所以,我编写了一个程序来实际放置方块。
参数坐标是生成迷宫所需的直方体对角线上的两个点的坐标,material 是方块素材,client 是生成 RCON 操作模块的实例。
作为要发送的命令,我们使用 setblock 命令,在放置方块的坐标和方块名称之间用空格隔开。此外,我们还会在没有方块的位置放置空气方块。因此,即使在建造迷宫的直方体内已经有方块,所有方块也会被替换掉。

安直に3重のfor文で迷路データのスライスを回しているので、サイズの大きい迷路は重くてかなり時間がかかると思います。参考として、冒頭のスクショのような30 x 30 x 3程度のサイズであれば問題なく生成できました。(環境によると思いますが)
そのため、巨大な迷路を生成する際はアルゴリズムに工夫が必要になりますが、ゲーム内にブロックを配置する際の手法としては同じものが使えるのではないかと思います。

func BuildMaze(x1 int, y1 int, z1 int, x2 int, y2 int, z2 int, material string, client *minecraft.Client) {
	sortPositions(&x1, &y1, &z1, &x2, &y2, &z2)

	length := int(math.Abs(float64(z2 - z1)))
	width := int(math.Abs(float64(x2 - x1)))
	height := int(math.Abs(float64(y2 - y1)))

	m, err := CreateMaze(length, width)
	if err != nil {
		log.Fatal(err)
	}

	for i, v := range m {
		for j, vv := range v {
			if vv != 0 {
				for k := 0; k < height; k++ {
					time.Sleep(3)
					if vv != 0 {
						_, err = client.SendCommand(fmt.Sprintf(
							"setblock %d %d %d %s", x1+j, y1+k, z1+i, material))
					} else {
						_, err = client.SendCommand(fmt.Sprintf(
							"setblock %d %d %d %s", x1+j, y1+k, z1+i, "minecraft:air"))
					}
					if err != nil {
						log.Fatal(err)
					}
				}
			}
		}
	}
}

只要Minecraft服务器已经启动并正确配置,你可以根据需要在主函数等地方调用这个函数来生成迷宫。
至于服务器端的设置,除了可以作为常规多人服务器使用外,还需要在server.properties文件中进行以下配置,启用RCON,并进行密码和端口的设置。

enable-rcon=true
rcon.password=password
rcon.port=25575

最後に

今回の記事は、自分が調べながら実装して、少し期間を開けてから覚えていることを記事としてまとめたものであることや、コード全体を載せていないということがあるので、真似をして実装する際に必要な情報が抜けている、わかりづらいなどの点があると思います。なので、質問事項や訂正点等ございましたらお気軽にご指摘いただけると嬉しいです。

谢谢您一直以来的观看。

广告
将在 10 秒后关闭
bannerAds