たくのろじぃのメモ部屋

プログラミング(C#)の基礎やそれを応用した技術情報をメモしておくブログです。

【C#】Minecraft自動化 #3 範囲指定と湧きつぶし

はじめに

前回はプレイヤーの位置座標とブロックの配置方法についてやりました。今回は前回に引き続き、湧きつぶしをやりたいと思います。

湧きつぶしはフィールド上にモンスターがスポーンしないよう、暗い場所にたいまつなどの照明を一定間隔で置いていくことを言います。

お品書き

  1. 湧きつぶしの条件とロジック
  2. 範囲指定
  3. 湧きつぶしの実装

1. 湧きつぶしの条件とロジック

まず、モンスターがスポーンしない明るさは7以上です。たいまつを置いた場所の明るさは14で、これを中心に放射状へ明るさが減少していきます。1マス当たり、1ずつ暗くなっていきます。よって、たいまつを置いた地点から東西南北方向に6マス間隔で置いていけばいいですね。 こちらのサイトが参考になります。

n5v.net

ロジックはアルゴリズム(計算手法)という意味として考えていきますが、6マス間隔に置いていくならば余剰演算子を用いて条件分岐を行えば良さそうです。置きたい範囲を6で割ったとき、あまりがない時だけ設置するようにすれば6マスごとに置くことができます。これを指定範囲回数だけ繰り返せばいいわけですね。

例えば、プレイヤーの座標を始点としてx方向に置いていきたい場合を考えます。このときの始点は (x, y, z) = (10, 20, 10)として、終点を (30, 20, 10) とします。

int StartPoint = 10;
int EndPoint = 30;
int Range = EndPoint - SatrtPoint; //配置範囲

Command.Send("/Setblock ..."); //まずはプレイヤーの位置に置く
for (int x = 1; x <= Range; x++) // 範囲の値だけ繰り返す
    if(x % 6 == 0) // x 回目(座標)が6の倍数の時だけ配置
    Command.Send("/setblock ...");
End

こんな感じだと思います。あとは実際に動かしてみます。

2. 範囲指定

範囲を指定しないと永遠と配置し続け、ワールドの外に配置しようとしたときにプログラムがストップします。また、置きたくないところに置くのもどうかと思いますので...。

まずは簡単に、プレイヤーの相対座標からたいまつを置いてみます。と、その前に...

2.1 絶対座標

絶対座標は(x, y, z) = (0, 0, 0) を基準にした座標です。座標に関してはテレポートコマンドで例えたほうが分かりやすいと思います。

/tp 10 20 30 

というコマンドを投げるとワールドの基準 (0, 0, 0) から (10, 20, 30) の地点に移動します。つまり、入れた値で移動します。

2.2 相対座標

相対座標は (x, y, z) = (プレイヤーの座標) を基準にした座標です。例えばプレイヤーが (100, 100, 100) にいるとします。相対座標を用いる場合は "~" チルダ記号の右に移動したい分の値を入れます。

/tp ~10 ~20 ~30

というコマンドを投げると、プレイヤーを基準に (+10, +20, +30) されるので、結果としては (110, 120, 130) の座標へ移動します。

2.3 相対座標を用いた移動操作

では、実際に相対座標を用いて指定した座標へテレポートしてみます。コマンド入力できるように入力を受け付けます。プログラムはわかりやすくするために、あえて配列を使っていません。配列使ったほうがメモリ節約とコードの短縮ができていいので、慣れている人は読み換えてください。

ちなみに、コマンドを送る際は絶対座標形式で大丈夫です。一度、相対座標を取得して、そこに加算しているためです。チルダを付けると大変なことになります。

using CoreRCON;
using System;
using System.Net;
using System.Text.RegularExpressions;
using System.Threading.Tasks;

namespace ConsoleApp1
{
    class Program
    {
        static async Task Main(string[] args)
        {
            Console.WriteLine("Now Starting...");
            await minecraft();
        }

        static async Task minecraft()
        {
            var serveraddress = IPAddress.Parse("127.0.0.1"); //IPアドレスとして扱うための変換
            var serverpass = "minecraft"; //RCONでログインするためのパスワード
            ushort port = 25575; //サーバのポート番号

            string Player_Name = "Player_Name";
            string TPCommand = "/tp " + Player_Name + " ~ ~ ~"; //プレイヤーの位置情報を得るためのコマンド

            //CoreRCONを使ってMinecraftへ接続
            try
            {
                // Minecraft 接続環境(固定)
                var connection = new RCON(serveraddress, port, serverpass);
                var result = await connection.SendCommandAsync(TPCommand);

                //相対座標取得
                string GetNum = Regex.Replace(result, @"[^0-9,.]", ""); //座標数値のみ検索
                string[] StrArray = GetNum.Split(','); //カンマで区切られた文字を部分的に取り出して、配列に保持

                Console.WriteLine(result);//生のデータ

                double Position_x = double.Parse(StrArray[0]); //南北方向 北へ進むほど数値が小さくなる
                double Position_y = double.Parse(StrArray[1]); //高さ
                double Position_z = double.Parse(StrArray[2]); //東西方向 東へ進むほど数値が大きくなる

                Console.Write("Move x:");
                double Input_x = double.Parse(Console.ReadLine()); //x座標入力
                Console.Write("Move y:");
                double Input_y = double.Parse(Console.ReadLine()); //y座標入力
                Console.Write("Move z:");
                double Input_z = double.Parse(Console.ReadLine()); //z座標入力

                double Teleport_x = Position_x + Input_x; //移動後の座標x
                double Teleport_y = Position_y + Input_y; //移動後の座標y
                double Teleport_z = Position_z + Input_z; //移動後の座標z

                string Telepote_Command = "/tp " + Player_Name + " " + Teleport_x + " " + Teleport_y + " " + Teleport_z;

                result = await connection.SendCommandAsync(Telepote_Command);

                Console.WriteLine(Telepote_Command); //テレポート後のログ

                //完了コマンド(固定)
                Console.ForegroundColor = ConsoleColor.Green; //コンソール文字列に色付け
                Console.WriteLine("\nDone."); //ちょっとカッコつけて完了の表示
                Console.ForegroundColor = ConsoleColor.White; //色を戻す

            }
            catch (Exception e)
            {
                Console.WriteLine(e);
            }
            Console.ReadKey();
        }
    }
}

実行してみると、指定した分だけ移動していますね。

f:id:takunology:20190826193036p:plain

プレイヤーの相対座標とコンソール入力によって、これらの差の絶対値をとれば範囲が得られますね。この差の数だけ繰り返して、6の倍数のとき(6で割ってあまりが0のとき)にたいまつを配置していけば良さそうです。

3. 湧きつぶしの実装

湧きつぶしのロジックを考える上でのポイントはこんな感じです。

  • 範囲指定の分繰り返す
  • 繰り返し回数が6の倍数の時だけたいまつを設置する
  • それ以外のときは何もしない

これらを意識して実装してみます。まずは、範囲指定を求めます。先ほどのは相対座標からやってみましたが、今回は絶対座標からやってみたいと思います。相対座標でやりたい人は上のままで大丈夫です。

絶対座標で指定する場合、方向によって負の値が出ることがあります。それだとfor文で回すときに困りますので、絶対値にするための関数を作ります。本当は絶対値にするためのメソッドがあるのですが、せっかくなので作ってみます。

一発でコマンドを投げると悲惨なことになるかもしれないので、一度コンソールチェックを行います。

//省略
Console.Write("Range in current position to x:");
double Input_x = double.Parse(Console.ReadLine()); //x座標入力
Console.Write("Range in current position to y:");
double Input_y = double.Parse(Console.ReadLine()); //y座標入力
Console.Write("Range in current position to z:");
double Input_z = double.Parse(Console.ReadLine()); //z座標入力

Console.WriteLine("\nSetup to Put Torches Range in:");

int Range_x = (int)Absolute_Range(Position_x, Input_x); //移動後の座標x
int Range_y = (int)Absolute_Range(Position_y, Input_y); //移動後の座標y
int Range_z = (int)Absolute_Range(Position_z, Input_z); //移動後の座標z
//省略
static double Absolute_Range(double Position, double Input) // 絶対値にする関数
{
    if(Position - Input < 0)
    {
        return (Position - Input) * (-1) + 0.9; //四捨五入対策の +0.9
    }
}

これで範囲(絶対値)を求めることができます。また、6ブロックごとの配置には余剰演算子と2重 for文が使えるのでこのようにしました。これは6の倍数ごとなので、プレイヤーの立っている相対座標自体は含まれません。この分をどこかで修正してください。(ここでは省略します。)

for (int x = 1; x <= Range_x; x++) //6マス離れた後の座標 以降6マスごと
{
    if(x % 6 == 0)
    {
        for (int z = 1; z <= Range_z; z++)
        {
            if(z % 6 == 0)
            {
                Console.Write("Set ({0}, {1})\n", x, z);
            }
        }
    }
}

f:id:takunology:20190826213026p:plain

6の倍数ごとに設置されていますね。0 はプレイヤーの相対座標そのものです。自分の立っている場所から始めたいのでこのようにしました。

あとはこれをコマンド送信用に変更して、ログを見れるようにします。 コード全体としてはこんな感じです。前回のやつを修正したところがあります。

using CoreRCON;
using System;
using System.Net;
using System.Text.RegularExpressions;
using System.Threading.Tasks;

namespace ConsoleApp1
{
    class Program
    {
        static async Task Main(string[] args)
        {
            Console.WriteLine("Now Starting...");
            await minecraft();
        }

        static async Task minecraft()
        {
            var serveraddress = IPAddress.Parse("127.0.0.1"); //IPアドレスとして扱うための変換
            var serverpass = "minecraft"; //RCONでログインするためのパスワード
            ushort port = 25575; //サーバのポート番号

            string Player_Name = "Player_Name";
            string TPCommand = "/tp " + Player_Name + " ~ ~ ~"; //プレイヤーの位置情報を得るためのコマンド

            //CoreRCONを使ってMinecraftへ接続
            try
            {
                // Minecraft 接続環境(固定)
                var connection = new RCON(serveraddress, port, serverpass);
                var result = await connection.SendCommandAsync(TPCommand);

                //相対座標取得
                string GetNum = Regex.Replace(result, @"[^0-9-,.]", ""); //座標数値のみ検索
                string[] StrArray = GetNum.Split(','); //カンマで区切られた文字を部分的に取り出して、配列に保持

                Console.WriteLine(result);//生のデータ

                double Position_x = double.Parse(StrArray[0]); //南北方向 北へ進むほど数値が小さくなる
                double Position_y = double.Parse(StrArray[1]); //高さ
                double Position_z = double.Parse(StrArray[2]); //東西方向 東へ進むほど数値が大きくなる

                Console.Write("Range in current position to x:");
                double Input_x = double.Parse(Console.ReadLine()); //x座標入力
                Console.Write("Range in current position to y:");
                double Input_y = double.Parse(Console.ReadLine()); //y座標入力
                Console.Write("Range in current position to z:");
                double Input_z = double.Parse(Console.ReadLine()); //z座標入力

                Console.WriteLine("\nSetup to Put Torches Range in:");

                int Range_x = (int)Absolute_Range(Position_x, Input_x); //移動後の座標x
                int Range_y = (int)Absolute_Range(Position_y, Input_y); //移動後の座標y
                int Range_z = (int)Absolute_Range(Position_z, Input_z); //移動後の座標z

                string Item = "torch";

                Console.WriteLine("Range x:{0} Range y:{1} Range z:{2}", Range_x, Range_y, Range_z); //範囲の確認

                for (int x = 1; x <= Range_x; x++) //プレイヤーの相対座標に置く x方向
                {
                    if (x % 6 == 0)
                    {
                        Console.WriteLine("Set (0, {0})", x);
                        string SetToaches = "/setblock " + (Position_x + x) + " " + (Position_y) + " " + (Position_z) + " " + Item;
                        Console.WriteLine(SetToaches);
                        result = await connection.SendCommandAsync(SetToaches);
                        Console.WriteLine(result);
                    }
                }

                for (int z = 1; z <= Range_z; z++) //プレイヤーの相対座標に置く z方向
                {
                    if (z % 6 == 0)
                    {
                        Console.WriteLine("Set (0, {0})", z);
                        string SetToaches = "/setblock " + (Position_x) + " " + (Position_y) + " " + (Position_z + z) + " " + Item;
                        Console.WriteLine(SetToaches);
                        result = await connection.SendCommandAsync(SetToaches);
                        Console.WriteLine(result);
                    }
                }

                for (int x = 1; x <= Range_x; x++) //6マス離れた後の座標 以降6マスごとにたいまつを配置
                {
                    if(x % 6 == 0)
                    {
                        for (int z = 1; z <= Range_z; z++)
                        {
                            if(z % 6 == 0)
                            {
                                Console.Write("Set ({0}, {1})\n", x, z);
                                string SetToaches = "/setblock " + (Position_x + x) + " " + (Position_y) + " " + (Position_z + z) + " " + Item;
                                Console.WriteLine(SetToaches);
                                result = await connection.SendCommandAsync(SetToaches);
                                Console.WriteLine(result);
                            }
                        }
                    }
                }

                //完了コマンド(固定)
                Console.ForegroundColor = ConsoleColor.Green; //コンソール文字列に色付け
                Console.WriteLine("\nDone."); //ちょっとカッコつけて完了の表示
                Console.ForegroundColor = ConsoleColor.White; //色を戻す

            }
            catch (Exception e)
            {
                Console.WriteLine(e);
            }
            Console.ReadKey();
        }

        static double Absolute_Range(double Position, double Input) //絶対値に変換
        {
            if (Position - Input < 0)
            {
                return (Position - Input) * (-1) + 0.9;
            }
            return Position - Input;
        }
    }
}

ちなみに、このコードは相対座標自体が負になっているとうまく動きません。あくまで自分の環境に合わせただけなので、環境に合わせて書きかえてください。

これを実行してみるとこんな感じです。

f:id:takunology:20190827014452p:plain

きれいにたいまつが並んでますね。美しい...。 前回作ったプログラムでブロックを 100 x 100 つまり1万ブロック並べて、その上にたいまつを配置しています。

一応動画も載せておきます。動作確認にどうぞ。

youtu.be

おわりに

これで湧き潰しの自動化をC#で行うことができました。意外と簡単にできることが分かりました。が、このままではまずいです。平地なので問題なく置けてますが、もし障害物(建物や山などのブロック)があると、置き換えてしまうので地表に置けません。なので、地面をスキャンするロジックを実装する必要があります。

あと、あまりコマンドを投げすぎるとサーバが落ちることを学んだので、前回のプログラムを修正しました。前回のままだと、コマンドのタスクが完了する前に次のコマンドが実行されてしまい、処理落ちします。なので、Thread.Sleep(5) で5ミリ秒の遅延を加えました。くれぐれもご注意を。

参考にさせていただいた動画

"くらでべ" の動画です。ありがとうございます。

www.youtube.com

今回作成したコード

参考までにどうぞ。

github.com