個人的には、Minecraftを改造したいとずっと思っていました。しかし、そのためにはJavaをもう一度学ぶ必要があるように思われ、とても乗り気ではありませんでした。でもそれはどうやら間違っていたようです。
粘り強く努力したおかげで、Javaにまったく詳しくなくてもMinecraftを改造する方法が分かりました。とはいえ、PHPを使って望んでいるすべての改造のためにはコツや注意事項があります。
この記事はまだ完璧なものではありません。『JavaScriptの3D Minecraftエディターを構築する』では、きちんと整理された3D JavaScriptのMinecraftエディターについて紹介しています。興味のある人は併せて確認してください。
本記事に掲載したほとんどのコードがGithubにあります。JavaScriptはChromeの最新バージョンで確認し、PHPはPHP7.0で確認しました。ほかのブラウザー、バージョンの違うPHPで同じ動作をするか分かりませんが、コア概念は共通しています。
セットアップする
すぐに分かることですが、PHPとMinecraftサーバー間でやり取りをすることになります。改造機能がほしいなら、実行するスクリプトが必要になります。以下のような従来のビジーループが使えます。
while (true) {
// listen for player requests
// make changes to the game
sleep(1);
}
あるいは、もう少しおもしろいこともできます。
個人的にはAMPHPがとても好きになりました。AMPHPはHTTPサーバーやクライアント、イベントループなどを含む、PHPの非同期ライブラリーのコレクションです。よく知らなくても大丈夫です。分かりやすいように、じっくりと進めていきます。
イベントループとファイルの変更を監視する関数の作成から始めます。次のようにイベントループとファイルシステムライブラリーをインストールします。
composer require amphp/amp
composer require amphp/file
このあとイベントループを起動し、考えたとおりに動いているかを確認します。
require __DIR__ . "/vendor/autoload.php";
Amp\run(function() {
Amp\repeat(function() {
// listen for player requests
// make changes to the game
}, 1000);
});
ノンブロッキングであることを除けば無限ループと似ています。通常ならプロセスをブロックする操作を待ちますが、これなら同時操作を実行できます。
Promised Landへの寄り道
AMPHPは、ラッパーコードだけでなくpromiseベースのインターフェイスも提供しています。JavaScriptの概念としてすでに知っていると思いますが、以下に簡単な例を挙げます。
$eventually = asyncOperation();
$eventually
->then(function($data) {
// do something with $data
})
->catch(function(Exception $e) {
// oops, something went wrong!
});
Promiseはまだ持っていないデータを表現する方法で、最終的な値です。ファイルシステム操作やHTTPリクエストのように、時間がかかることがあります。
すぐに値を持つわけではないことに注意してください。フォアグラウンドで値を待つ代わりに(これは従来、プロセスをブロックすることになります)、バックグラウンドで待機します。バックグラウンドで待つ間にフォアグラウンドで有意義な作業ができます。
AMPHPはジェネレーターを使用してさらに一歩Promiseを進めます。端的に説明するように精一杯がんばりますので、おつきあいください。
ジェネレーターは、イテレーター構文を簡略化したものです。つまり、配列で定義されていない値を反復処理するために記述するコード量を減らします。さらに、値を生成する関数へ、値の生成中でもデータ送信できるようにします。パターン検知のためにジェネレーターをここで起動しましょう。
ジェネレーターは、オンデマンドで続きの配列項目を構築できます。Promiseは最終的な値を示します。したがってステップ(または動作)のリスト生成にジェネレーターを再利用でき、オンデマンドで実行されます。
コードを見ると分かりやすいかもしれませんね。
use Amp\File\Driver;
function getContents(Driver $files, $path, $previous) {
$next = yield $files->mtime($path);
if ($previous !== $next) {
return yield $files->get($path);
}
return null;
}
同期処理はどのように動作するか考えてみます。
- getContentsを呼び出す
- $files->mtime($path)を呼び出す(これはfilemtimeへのプロキシと考える)
- filemtimeが返るのを待つ
- $files->get($path)を呼び出す(これはfile_get_contentsへのプロキシと考える)
- file_get_contentsが返るのを待つ
Promiseでは、いくつかの新しいクロージャーでブロックを避けられます。
function getContents($files, $path, $previous) {
$files->mtime($path)->then(
function($next) use ($previous) {
if ($previous !== $next) {
$files->get($path)->then(
function($data) {
// do something with $data
}
)
}
// do something with null
}
);
}
Promiseはチェーンできるので、次のように減らせます。
function getContents($files, $path, $previous) {
$files->mtime($path)->then(
function($next) use ($previous) {
if ($previous !== $next) {
return $files->get($path);
}
// do something with null
}
)->then(
function($data) {
// do something with data
}
);
}
みなさんはどうか分かりませんが、まだ少し乱雑に見えます。では、ジェネレーターをどのように適応すればよいでしょうか。AMPHPはPromiseの評価にyieldキーワードを使用します。ここでgetContents関数をもう一度見てみます。
function getContents(Driver $files, $path, $previous) {
$next = yield $files->mtime($path);
if ($previous !== $next) {
return yield $files->get($path);
}
return null;
}
$files->mtime($path)はPromiseを返します。ルックアップの完了を待つ代わりに、yieldキーワードに遭遇したときに関数が実行を停止します。しばらくするとAMPHPにSTAT操作の完了が通知され、関数の実行が再開されます。
タイムスタンプが一致しない場合はfiles->get($path)が内容を取り出します。これは別のブロック操作なので、yieldが関数を再び中断します。ファイルが読み込まれると、AMPHPはファイルの内容を返す、この関数を再開します。
このコードは同期の代替に似ていますが、ノンブロッキングにするためにPromise(透過)とジェネレーターを使用します。
AMPHPはthenメソッドをサポートしておらず、Promises A+とはスペックが少し異なります。React/PromiseやGuzzle Promisesでは、ほかのPHPのように実装します。重要なことは、この簡潔な非同期構文をサポートするために、Promiseの最終的な性質と、どのようにジェネレーターとインターフェースで接続できるかを理解することです。
ログをリッスンする
『マインクラフトで始めるIoT! PHPとArduinoでゲームの世界を監視してみた』ではMinecraftについて書きましたが、内容は現実世界のアラームをトリガーするためにMinecraftの家のドアを使用するという内容でした。その中で、Minecraftサーバーからのデータ取得や、PHPにデータを渡すプロセスについて簡単に触れました。
今回はもう少し長く説明しますが、本質的には同じことです。では、プレイヤーのコマンドを識別するコードについて取り上げます。
define("LOG_PATH", "/path/to/logs/latest.log");
$files = Amp\File\filesystem();
// get reference data
$commands = [];
$timestamp = yield $filesystem->mtime(LOG_PATH);
// listen for player requests
Amp\repeat(function() use ($files, &$commands, &$timestamp) {
$contents = yield from getContents(
$files, LOG_PATH, $timestamp
);
if (!empty($contents)) {
$lines = array_reverse(explode(PHP_EOL, $contents));
foreach ($lines as $line) {
$isCommand = stristr($line, "> >") !== false;
$isNotRepeat = !in_array($line, $commands);
if ($isCommand && $isNotRepeat) {
// execute mod command
array_push($commands, $line);
print "executing: " . $line . PHP_EOL;
break;
}
}
}
}, 500);
参照ファイルのタイムスタンプの取得から始めます。ファイルが(getContents関数で)変更された場合に動作するようにします。また、実行済みのコマンドをすべて格納する空のリストも作成します。このリストは同じコマンドを2度実行するのを防ぐのに役立ちます。
/path/to/Logs/Latest.LogをMinecraftサーバーのログファイルのパスに置き換える必要があります。スタンドアロンのMinecraftサーバーでの実行を勧めますが、ルートディレクトリにLogs/Latest.Logが必要です。
Amp\repeatにクロージャーを500ミリ秒ごとに実行するよう指示しています。そのときファイルの変更を確認します。タイムスタンプが変更された場合、ログファイルの行を配列に分割し、最新のメッセージから読めるように逆順にします。
行に、プレイヤーが「>コマンド」を入力した場合に発生する「> >」が含まれている場合、コマンド命令が含まれていると見なします。
詳細な計画の作成
Minecraftの中でもっとも時間がかかるのは、大規模な建造物の構築です。もし、3D JavaScriptのビルダーを使って計画できればとても簡単になり、そのあと特別なコマンドを使用して実際に配置します。
カスタムブロックの配置のリストを生成するには、『JavaScriptの3D Minecraftエディターを構築する』で触れたビルダーの、わずかに修正されたバージョンを使用できます。
現時点では、このビルダーはブロックの配置のみができます。生成する配列構造は、最初のシーンがレンダリングされたあとに各ブロックが配置されるx、y、z座標です。これは、これまで取り組んできたPHPスクリプトにコピーできます。設計した構造を構築するための正確なコマンドを特定する方法を見つけ出す必要があります。
$isCommand = stristr($line, "> >") !== false;
$isNotRepeat = !in_array($line, $commands);
if ($isCommand && $isNotRepeat) {
array_push($commands, $line);
executeCommand($line);
break;
}
// ...later
function executeCommand($raw) {
$command = trim(
substr($raw, stripos($raw, "> >") + 3)
);
if ($command === "build") {
$blocks = [
// ...from the 3D builder
];
foreach ($block as $block) {
// ... place each block
}
}
}
コマンドを受信するたびに、executeCommand関数に渡せます。そこでは、2つ目の>から行の末尾まで抽出します。この時点ではbuildコマンドの識別だけが必要です。
サーバーと通信する
ログをリッスンするほか、サーバーとはどのように通信しているのでしょうか。スタンドアロンサーバーは(RCONと呼ばれる)管理者チャットサーバーを起動します。カウンターストライクのようなゲームの改造を可能にするものと同じ管理者チャットサーバーです。
誰かがすでにRCONクライアントを構築していることが分かっており、最近、すばらしいラッパーを書きました。以下と一緒にインストールします。
composer require theory/builder
ライブラリーの大きさについてはお詫びします。ライブラリーの自動テストを構築できるようにMinecraftのスタンドアロンサーバーのバージョンをインクルードしました。とても急いで書きました。
RCONの接続ができるように、スタンドアロンサーバーを設定する必要があります。サーバーのjarと同じフォルダのserver.propertiesファイルに次の行を追加します。
enable-query=true
enable-rcon=true
query.port=25565
rcon.port=25575
rcon.password=password
再起動後は、次の類似したコードを使用してサーバーに接続できるはずです。
$builder = new Client("127.0.0.1", 25575, "password");
$builder->exec("/say hello world");
完全な構造を構築するためにexecuteCommand関数を改良できます。
function executeCommand($builder, $raw) {
$command = trim(
substr($raw, stripos($raw, "> >") + 3)
);
if (stripos($command, "build") === 0) {
$parts = explode(" ", $command);
if (count($parts) < 4) {
print "invalid coordinates";
return;
}
$x = $parts[1];
$y = $parts[2];
$z = $parts[3];
$blocks = [
// ...from the 3D builder
];
$builder->exec("/say building...");
foreach ($blocks as $block) {
$dx = $block[0] + $x;
$dy = $block[1] + $y;
$dz = $block[2] + $z;
$builder->exec(
"/setblock {$dx} {$dy} {$dz} dirt"
);
usleep(500000);
}
}
}
新規および改良されたexecuteCommand関数は、コマンド<player_name>> buildに似たメッセージが「build」で始まるかどうか確認します。
ビルダーがノンブロッキングだった場合、usleep(500000)の代わりにyieldnew Amp\Pause(500)の使用を勧めます。また、呼び出す場所、つまりyieldexecuteCommand(...)を使って、executeCommandをジェネレーター関数として取り扱う必要があります。
すでに実行している場合、デザインが構築されるべき場所のx、y、z座標を取得するために、コマンドがスペースで分割されます。設計者が生成した配列を受け取り、各ブロックを配置します。
ここから先は?
いま作成したばかりのシンプルなMODのようなスクリプトの、たくさんの楽しい機能拡張を想像していると思います。設計者は、多くの異なる種類のブロックとその構成からの配置を作成するために拡張できます。
MODスクリプトを使えば、設計者が名づけた設計を送信し、プレイヤーはどのデザインを構築したいのかbuildコマンドが正しく指定できるように、JSON APIを介してアップデートを受信するように拡張できます。
(原文:Modding Minecraft with PHP – Buildings from Code!)
[翻訳:柴田理恵/編集:Livit]