LoginSignup
15
9

More than 1 year has passed since last update.

プログラムで生成した迷路をMinecraftで建築する(Go)

Posted at

はじめに

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

2022-03-18_18.16.21.png
Building maze

概要

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

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

この記事では、実際にプレイ中のMinecraftの世界に、ランダムに生成された迷路が自動で建築されるプログラムを作成することを目標に、各手順を解説していきます。

迷路を作る

Minecraftの世界にプログラムで迷路を作るために、おおまかに以下の2つの手順で実装を行います。

  • プログラム上で迷路の設計図を生成する
  • 設計図をもとにMinecraftの世界にブロックを配置する

設計図を作る

最終目標はマイクラ内に迷路を建築するプログラムを作ることですが、まずはプログラム上で迷路を表現してみます。

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

これは黒と白の四角で表現された迷路です。マイクラ内で迷路を建築するための設計図としてまずはこれを作ってみます。

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

壁伸ばし法では以下のような手順で迷路を作成します。また、実装にあたり以下のページを参考にさせていただきました。

  1. 迷路を構成する2次元配列を、幅と高さが5以上の奇数になるよう生成する
  2. 外周を壁とし、それ以外を通路とする
  3. x, y座標が共に偶数となる座標を壁の起点としてリストアップする
  4. 起点リストからランダムで座標を取り出し、そこが通路の場合のみ、5.の壁伸ばし処理を行う。この処理を全ての起点が壁になるまで繰り返す
  5. 壁伸ばし処理を行う
    1. 指定座標を壁とする
    2. そこから拡張できる方向(その向きに隣り合っているセルが通路である、かつ、その向きの2つ隣のセルが現在拡張中の壁ではない方向)
    3. 拡張中の方向の2セル先が既存の壁の場合、壁の拡張を終了する
    4. 通路の場合、そのセルから続けて拡張する
    5. 四方が全て現在拡張中の壁である場合、拡張できる座標が見つかるまで現在拡張中の壁をバックして、壁の拡張を再開する

と、文字で書くとこのようになるのですが、実際に今回実装したコードでこの部分を抜粋したものが以下になります。

このCreateMaze関数では迷路の幅、高さを引数として受け、迷路に見立てたintの2次元スライスを返すものになります。この関数により、マイクラ内で作る迷路の設計図が用意できるようになりました!

create_maze.go
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内の座標と対応付けて、ブロックを置くコマンドで建築していきます。

実装

ということで、実際にブロックを置いていくプログラムを書きました。
引数の座標は、迷路を生成したい直方体の対角の2点の座標で、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のサーバが起動し適切に設定がされている状態で、この関数をmain関数などで適宜呼び出せば、迷路が生成されます。
サーバ側の設定としては、通常のマルチサーバとして利用可能な状態に加え、server.propertiesに以下のように書き加え、RCONを有効化、パスワードやポートの設定を行う事が必要です。

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

最後に

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

最後までご覧頂きありがとうございました。

15
9
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
15
9