本ページの内容は、Bukkit WikiのPlugin Tutorialを和訳した物となります。(一部は省略しています)
最新ではない可能性があるため、より新しい情報を確認する場合は、本家を参照するようにして下さい。
目次
- 1 始めに
- 2 Javaの習得
- 3 Javaの開発環境
- 4 Plugin用プロジェクトを始めるために
- 5 onEnable()メソッドとonDisable()メソッド
- 6 イベントとリスナ
- 7 コマンド
- 8 堅牢なonCommandの記述
- 9 プラグインのConfiguration/Settings
- 10 権限
- 11 スケジューリングタスクとバックグラウンドタスク
- 12 ブロックの操作
- 13 プレイヤーインベントリの操作
- 14 アイテムの操作
- 15 HashMapの応用
- 16 Map・Set・Listの応用
- 17 データベース
- 18 プラグインの配布
- 19 ヒントとノウハウ
- 20 リクエストに応じて書かれた記事
- 21 プラグインのサンプル兼雛形
始めに
- Note: 当ページの内容は、Bukkit用MODの作成方法の内容とは異なるアプローチによるBukkitプラグイン開発手法の解説です。
- 重要: 当ページには、原文の翻訳ではない、日本語読者向けの独自の記述を行っている箇所が少量あります。そのような内容を含む節ではNoteにて通知しています。
- 重要: 当ページでは、技術的な解説やたとえ話の原意を理解し易くするために、一部意訳を行っています。
- 重要: 当ページは訳文であるため、訳文自体に対する文責は原著者にありません。当ページの内容を受けて原著者へ何らかのアクションを行う場合、その点を考慮して必ず原文にも目を通して下さい。
このチュートリアルは、こちらのスレッドからの転載です。当ページの原著者は、Adamki11s氏ですが、多くの編集を受けて今に至ります。
このチュートリアルを読み終えた後は、Adamki11s'氏の"Extras" からより深い情報を得る事が出来ます。Extras library forums thread.
Javaの習得
- Note: 当記事の一部は原文の訳ではありません、日本語読者向けに独自の内容を記述しています。
このチュートリアルの読者には、Java言語プログラミングの基礎がわかる方を対象としています。
Javaの基礎知識が少ない方は、下記の情報に触れてください。
Javaコーディング方法
- Oracleの記事 (現在のJavaの公式サイト)
- とほほのJava入門 - 基礎をみっちり解説している
- Javaのオブジェクト指向入門 - 継承や実装等のオブジェクト指向用語が分からない人向け
Javaがどうしても理解できない場合は、この他にも様々な資料があるので探して触れてください。
Javaの開発環境
- Note: 当記事の一部は原文の訳ではありません、日本語読者向けに独自の内容を記述しています。
プラグインを開発する(またはJavaを学ぶ)前に、開発環境(IDE (Integrated Development Environment))をインストールして下さい。
開発環境は、プラグインのコンパイルとデバッグを行うために使います。
Javaの主要なIDEには、Eclipse, Netbeans, IntelliJ IDEA の3つがあります。
EclipseはBukkit開発者の中では最も人気があり、IntelliJの方は業界内で広く利用されています。
あなたがJavaの初心者なら、当チュートリアルが解説に利用しているEclipseを利用する事をお勧めします。
Eclipseのお勧めのバージョンの配布元は、日本語 Eclipse / Pleiades All in One 日本語ディストリビューションです。
これはMergedocProjectが配布する拡張されたパッケージであり、Eclipseの一次配布元が提供するパッケージではありません。
どのIDEも気に入らなければ、BlueJも利用できます。
プラグイン開発が初めてであるなら、とにかく一度Eclipseを使ってみて、
良ければNetBeansを試してみると良いでしょう。
優良なEclipseの解説。お勧めのものより古いバージョンの解説ですが、ほぼ同様です。
Plugin用プロジェクトを始めるために
プロジェクトの作成
始める前にEclipseのワークスペースとファイルの設定を行う必要があります。
Eclipseを起動し、ファイル>新規>Java プロジェクトと選択して下さい。
プロジェクト名を入力し、新規プロジェクトウィザード画面の指示に従って、プロジェクトを作成してください。
Bukkit APIの参照
開発を始める前にbukkit APIライブラリをプロジェクトに追加する必要があります。 使用したい他のAPIも同じように追加することが可能です。
最新のBukkit APIはここからダウンロードが可能です。 Bukkit API - Development Snapshot
画面の左手にあるパッケージエクスプローラの(先ほど名前を付けた)プロジェクトを右クリックし、プロパティーを選択します。
開いた画面のJavaビルド・パスを選択し、ライブラリータブの中から、外部 Jar 追加ボタンを押して、ダウンロードしたBukkit APIを指定します。
BukkitのJavadocの利用方法
Eclipseを用いたJavaプログラミングの経験がある方なら、
Eclipseのエディタ上でクラスやメソッドにマウスカーソルを重ねた時に、
クラスやメソッドに関するドキュメントがポップアップで表示される事をご存知でしょう。
これは、Oracleのウェブサイトから得る事ができるJavadocの内容が表示されています。
Bukkitもまた、BukkitのAPIが提供するクラスやメソッドの各々のコードに同種のドキュメントを含んでおり、ポップアップで表示させる事ができます。
Eclipse上で、Bukkitのクラスやメソッドにマウスを重ねたタイミングでポップアップを表示できるようにするためには、下記の手順を行います。
- プロジェクトエクスプローラ上で、Bukkitのjarファイルを右クリックして、メニューを開く。
- "プロパティ"を選択し、表示されるポップアップ左側の"Javadoc ロケーション"項目を選択する。
- "Javadoc URL"の下部にあるテキストボックスに、"http://jd.bukkit.org/apidocs/" (ダブルクォートは除く)を貼り付ける。
- "検証"ボタンを押下し、URLがJavadocとして正しく識別される事をチェックしてから、OKボタンを押す。
Plugin開発の開始
Javaのクラスを格納するための'パッケージ'を作成する必要があります。 既存の"src"フォルダを右クリックし、新規 > パッケージを選択します:
パッケージ名は下記の方法で命名して下さい:
- ドメインを持っているなら、そのドメインの逆引き名にしましょう。
- ドメイン名がi-am-a-bukkit-developer.comの場合、パッケージ名はcom.i-am-a-bukkit-developerとなります。source
- ドメイン名を持っていない場合、下記の選択肢があります。お勧め順です。
- Option 1 githubやsourceforgeのようなソース管理サイトにアカウント登録します。
- githubの場合: こちらから作成したユーザページのURLを元に、com.github.<username>と命名します。
- Option 2 emailアドレスを元に命名します。(<username>@gmail.comであればcom.gmail.<username>とします)
- Option 3 重複しなさそうなパッケージ名を指定します(他者の開発物と重複する可能性が高いので非推奨)
下記の名前で始まるような、他者の開発物と重複するパッケージで命名すべきではありません:
- org.bukkit
- net.bukkit
- com.bukkit
ベースとなるパッケージ名(例: com.github.<username>)を決めたら、末尾にプラグイン名称(例: TestPlugin)を追加したものを、パッケージ名称としましょう。つまり、com.github.<username>.TestPluginになります。
プラグイン開発のためのプロジェクトに、クラスファイルを追加する準備ができました。メインのクラス名は、プラグインの名称と同一にすると良いでしょう(つまりこの例ではTestPlugin.javaが良いでしょう)。パッケージを右クリックし、 新規 > クラスを選択して追加します。
以上で、プロジェクトとメインファイルの準備ができました。次に、bukkitがplugin.ymlファイルを参照できるようにします。このファイルは、Javaプログラムがプラグインとして動作するための必須情報を記述するファイルであり、これ無しではプラグインは動作しません。ソースフォルダ以外(この例では"src"以外)のプロジェクトフォルダを右クリックし、新規 > ファイルを選択し、ファイル名を"plugin.yml"としてOKを押下します。Eclipseは、作成した空のplugin.ymlを、標準テキストエディタで開きます。
(Hint: ワークスペースのファイル構成を維持したければ、テキストエディタを閉じてplugin.ymlファイルをワークスペースへドラッグ・ドロップして移動させてください。当ファイルに必須の内容は、メインクラスへの参照と、プラグインのコマンドの情報です。最もシンプルなplugin.ymlは、次のような内容になります :
name: <PluginName>
main: <packagename>.<MainClass>
version: <Version Number>
- Note: メインクラスへの参照として記入されたパッケージ名が、<pluginname>.<pluginname>のように、パッケージ名がプラグイン名を含んでいる事がありますが、間違いではありません。
- Note: クラスの名称には、大文字小文字の区別があることに留意してください。
onEnable()メソッドとonDisable()メソッド
このファンクションは、プラグインが有効/無効になったときに呼ばれます。
デフォルトでは、プラグインは自動的に読み込まれたときに、イベントを登録やデバッグ出力を行うことが出来ます。
onEnable()は、プラグインがBukkitから読み込まれるときに最初に呼ばれ、プラグインを実行するために必須です。
onEnable()とonDisable()の基本
前のセクションで作成したメインクラスに、onEnable()とonDisable()のメソッドを作成します。
public void onEnable(){ } public void onDisable(){ }
現時点では、このメソッドは何も行いません。また、エラーが発生する事にも気付くでしょう。このエラーは、メインクラスが、プラグインの機能を継承(extends)する必要がある事を示しています。当クラスの定義文を、下記のように変更しましょう。
これを・・・
Class <classname> {}
このように変更する。
Class <classname> extends JavaPlugin {}
すると、前述の追加コードが赤の下線でハイライト表示され、何かが間違っていることを通知してくるはずです。このハイライト部にマウスを重ねると、下記のようなポップアップが表示されるので、「'JavaPlugin'をインポートします(org.bukkit.plugin.java)」を選択します。
もしくは、
import org.bukkit.plugin.java.JavaPlugin;
をコード上部の定義部に記述する事でも同様の事が行えます。
Loggerを利用したメッセージ出力
始めに、プラグインが実際に動作するかどうかを確認するため、シンプルなメッセージをサーバコンソールに表示させてみましょう。この処理として、ログ出力機能(Logger)をメインクラスに定義して初期化します。
Logger log = this.getLogger();
その後、onEnable()メソッドに、プラグインが動作している事を通知するメッセージを出力する処理を記述します。
log.info("Your plugin has been enabled.");
onDisable()メソッドについても同等の記述を行います。ここまでのコーディングによって、メインクラスは下記の様な内容になっているはずです。
package me.<yourname>.<pluginname>; import java.util.logging.Logger; import org.bukkit.plugin.java.JavaPlugin; public class <classname> extends JavaPlugin { Logger log; public void onEnable(){ log = this.getLogger(); log.info("Your plugin has been enabled!"); } public void onDisable(){ log.info("Your plugin has been disabled."); } }
イベントとリスナ
- 新しいEventSystemの使い方を参照して下さい
コマンド
onCommand()メソッド
さて、どのようにイベントを登録しいつ発生するかについて理解しました。 しかし、コマンドを利用して何かを起こしたい場合はどのようなデータ型を利用すればよいのでしょうか?それには、onCommand()メソッドを利用します。 このメソッドは、プレイヤーが文字"/"を入力する度に実行されます。 具体的には、"/do something"とコマンドを実行した場合にonCommand()が呼び出されます。 今のところ、onCommand()はまだプログラムしていないので何も起こらないでしょう。
コマンド名は、Bukkitや他のプラグインが提供しているものや、 提供するであろうものとの重複を避け、 ユニークなものとなるように考慮して下さい。 具体的には、"give"コマンドは既にいくつかのプラグインで利用されています。 もし独自に"give"コマンドを実装した場合は、 "give"コマンドを実装している他のプラグインとの互換性は無くなります。
onCommand()は、常にboolean型の値としてtrue,falseのどちらかを戻り値として返さねばなりません。 trueを返した場合は、情報表示のためのイベントは発生しません。 falseを返した場合は、プラグインファイルを"usage: property"に戻し、コマンドを実行したプレイヤーに、コマンドの利用方法を通知するメッセージを表示します。
onCommand()を利用する際は、4つのパラメータを登録しなければなりません。
- CommandSender sender - コマンドの発行元
- Command cmd - 実行されたコマンドの内容
- String commandLabel - 利用されたコマンドエイリアス
- String[] args - コマンドの引数を格納した配列(例:/hello abc defコマンドが入力された場合の内容は、args[0]がabc、args[1]がdefとなる)
コマンドの設定
public boolean onCommand(CommandSender sender, Command cmd, String commandLabel, String[] args){ // プレイヤーが「/basic」コマンドを投入した際の処理... if(cmd.getName().equalsIgnoreCase("basic")){ // 何かの処理 return true; // コマンドが実行された場合は、trueを返して当メソッドを抜ける。 } return false; // コマンドが実行されなかった場合は、falseを返して当メソッドを抜ける。 }
onCommand()をコーディングする際は、上記の例のように、メソッドの最終ステップにfalseをreturnする行を記述するのが良い方法です。
falseが返る事で、plugin.yml(下記参照)内に記述されたメッセージが表示され、
onCommand()処理が正しく動作しなかった事をメッセージから検知できるため、動作確認の助けになるからです。
逆に、メソッドの最後でtrueを返す処理構造にしてしまった場合は、onCommand()内の各処理に対して、
処理結果チェック処理と結果が不正である場合にfalseを返すような、同じ処理を何度も記述する必要が出てきますので、大変な無駄となります。
なお、コード「.equalsIgnoreCase("basic")」のパラメタ「basic」は、大文字小文字の区別はありません。
plugin.ymlへのコマンドの追加
コマンドを用意する際は、plugin.ymlファイルにも定義を追加する必要があります。plugin.ymlの最後に次の行を追加します。
commands: basic: description: This is a demo command. usage: /<command> [player] permission: <plugin name>.basic permission-message: You don't have <permission>
- basic - コマンド名
- description - コマンドの説明文
- usage - onCommand()がfalseを返した際に、コマンド実行ユーザに向けて表示されるメッセージの内容。コマンドの使い方を明解に書いてください。
- permission - 当コマンドの動作に必要なプラグインの設定を、コマンド実行ユーザに知らせるメッセージ。(主に、コマンド実行に必要なpermissionを書きます)
- permission-message - コマンド実行権限を持たないユーザがコマンドを実行した場合に、その旨を同ユーザに知らせるメッセージ。
- Note: ymlファイルには、1個タブを2個のスペースで記述する必要があります。タブ文字は構文エラーとなるため利用できません。
コンソールコマンドとプレイヤーコマンド
察しの良い人は、CommandSender sender
パラメタに着目しているかもしれません。 CommandSender
クラスは、プラグイン開発者に2つの点で有用な、Bukkitが提供するインタフェースです。
CommandSenderインタフェースの実装: Player
と ConsoleCommandSender
(正確には Player
もインタフェース)です。
プラグインを作る際は、次の2点に注意しながら作る事が非常に良い方法です。
- プラグインが動作している事をサーバコンソールで確認しながら作る事
- ログインプレイヤー向けのコマンドが、実際にログインしているプレイヤーのみ実行できる事
プラグインは、senderがプレイヤーではない場合(例えばコンソールからプラグインのコマンドが投入された場合)であっても、サーバコンソールには明解でシンプルな動作結果を返します。(例えば天気を変えるコマンドの実行結果は、ゲーム内では無くサーバコンソール上のメッセージから確認すれば間違いありません)
記述例:
public boolean onCommand(CommandSender sender, Command cmd, String label, String[] args) { Player player = null; if (sender instanceof Player) { player = (Player) sender; } if (cmd.getName().equalsIgnoreCase("basic")){ // If the player typed /basic then do the following... // do something... return true; } else if (cmd.getName().equalsIgnoreCase("basic2")) { if (player == null) { sender.sendMessage("this command can only be run by a player"); } else { // do something else... } return true; } return false; }
この例では、basic コマンドはログインプレイヤーとサーバコンソールのどちらからでも実行できます。しかし、basic2 コマンドは、ログインプレイヤーしか実行できません。 一般的に、コマンドはプレイヤーとサーバコンソールの両者に実行を許可しますが、投入されたコマンドの実行可否をチェックする必要があるコマンドについては、ログインプレイヤー向けのコマンドとして実装されるべきでしょう。
つまり、プレイヤーに依存する処理(テレポートコマンドやアイテムを与えるコマンド等は、対象となるプレイヤーが必要です)を行うコマンドが、プレイヤーコマンドとして実装されるべきものと言えます。
凝ったコマンドを作りたい場合、コマンドのパラメタ(上記例では args
パラメタ)を利用して独自の処理実装を行うことができます。例えば、プレイヤー名を指定したテレポートコマンドのみが、サーバコンソールから実行できる実装にする等が考えられます。
- 訳者補記: コマンド実装方法には、プレイヤー向け・サーバコンソール向けの2種類があり、それぞれの区別はどのような考え方で行うべきかについて説明しています。
CommandExecutorのクラス分割
上記の例では、onCommand()メソッドをプラグインのメインクラスに記述していました。小さなプラグインでは良い方法ですが、大きなプラグインに拡張していくのであれば、適切なクラスを作成してそちらに配置するべきです。幸い、難しい事ではありません:
- プラグインのpackage内に、MyPluginCommandExecutor のような名前で新規のコマンド実行クラスを作成する(当然、MyPluginはあなたのプラグイン名に合わせて変えましょう)。
- MyPluginCommandExecutorに、BukkitのCommandExecutorインタフェースを実装(implements)させる。
- プラグインのonEnable()メソッドの処理で、MyPluginCommandExecutorクラスのインスタンスを生成する。
- MyPluginCommandExecutorインスタンスをパラメタとして、処理
getCommand("basic").setExecutor(myExecutor);
を実行させる。
- Note:
"basic"
は実行されたコマンドであり、myExecutor
はMyPluginCommandExecutorインスタンスです。
とっても良い具体例:
MyPlugin.java (the main plugin class):
private MyPluginCommandExecutor myExecutor; @Override public void onEnable() { // .... myExecutor = new MyPluginCommandExecutor(this); getCommand("basic").setExecutor(myExecutor); // ... }
MyPluginCommandExecutor.java:
public class MyPluginCommandExecutor implements CommandExecutor { private MyPlugin plugin; public MyPluginCommandExecutor(MyPlugin plugin) { this.plugin = plugin; } @Override public boolean onCommand(CommandSender sender, Command command, String label, String[] args) { // ... implementation exactly as before ... } }
どのように、メインプラグインのインスタンスがMyPluginCommandExecutorのコンストラクタを実行するかに注目しましょう。
この節で紹介した方法により、メインのonCommand()メソッドが巨大で複雑になったとしても簡単に整理する事ができ、結果として、プラグインのメインクラスを複雑化させずに、処理分割する事ができます。
- Note: プラグインが複数のコマンドを持つ場合、個々のコマンドに対応するCommandExecutorをコーディングする必要があります。
- 訳者補記: 積極的に、大きな処理は小さく分割していきましょう。そのための仕組みをBukkitが用意しています。という事を教示しています。
堅牢なonCommandの記述
onCommand()メソッドを記述する際、パラメタの利用に関して、
決め付けて掛かってはいけない事がありますので念頭において下さい。
これらに留意した処理を記述する事で、堅牢なonCommand()をコーディングする事ができます。
senderの内部データ型をチェックする
senderの内部データがPlayer型であるとは限らない事を念頭において、チェックを行って下さい。
処理例:
public boolean onCommand(CommandSender sender, Command cmd, String commandLabel, String[] args){ if ((sender instanceof Player)) { // doSomething } else { sender.sendMessage(ChatColor.RED + "You must be a player!"); return false; } Player player = (Player) sender; return false; }
コマンドのパラメタ長をチェックする
senderインスタンスは、妥当なパラメタを持っているとは限りません。パラメタ長をチェックして下さい。
処理例:
public boolean onCommand(CommandSender sender, Command cmd, String commandLabel, String[] args){ if (args.length > 4) { sender.sendMessage(ChatColor.RED + "Too many arguments!"); return false; } if (args.length < 2) { sender.sendMessage(ChatColor.RED + "Not enough arguments!"); return false; } }
プレイヤーがオンラインである事を確認する
特定のプレイヤーのPlayerインスタンスを利用したい場合、
必ずそのプレイヤーがオンラインである必要があります。
オンラインであるかどうかをチェックして下さい。
処理例:
public boolean onCommand(CommandSender sender, Command cmd, String commandLabel, String[] args){ Player other = (Bukkit.getServer().getPlayer(args[0])); if (other == null) { sender.sendMessage(ChatColor.RED + args[0] + " is not online!"); return false; } return false; }
オンラインではないプレイヤーのインスタンスに操作を加えたい場合は、一般的にはOfflinePlayer
クラスを利用して行う事ができます。
プラグインのConfiguration/Settings
- 新しいConfigurationの使い方を参照して下さい
権限
Bukkitのパーミッション(権限)APIの利用は、簡単ではありません。
プレイヤーが特定の権限を持っているかどうかを調べる処理は次のようになります:
if(player.hasPermission("some.pointless.permission")) { //Do something }else{ //Do something else }
権限がセットされているか、いない(Javaのnullと同等)かを調べる処理は次のようになります:
boolean isPermissionSet(String name)
なぜグループの概念が存在しないかと思った方もいるでしょうが、 そもそも、権限にはグルーピングの概念は不要なのです。
元々、グループの主な用途はチャットメッセージをフォーマッティングする事にありました。
これは権限の機能を利用してもっと簡単に行えます。
例えば、チャットプラグインの設定において、権限とプレフィクスの関連を定義する事が該当します。
具体的には、権限"someChat.prefix.admin"をプレフィクス[Admin]に対応させる定義を行い、
プレイヤーがチャットで発言する度に、プレイヤー名の先頭に[Admin]が付くようになる機能が挙げられます。
他にも、グループに所属する複数のユーザに、
メッセージを送信するような機能を実現するために利用する事が考えられます。
この例を、権限を利用した処理で記述すると以下のようになります:
for(Player player: getServer().getOnlinePlayers()) { if(player.hasPermission("send.recieve.message")) { player.sendMessage("You were sent a message"); } }
さて、依然として
「グループを利用せずに、複数のプレイヤーに権限をセットする良い方法は何なのか?」が疑問かと思いますが・・・
BukkitのAPI自体は、グループの概念を提供していません。
グループの概念を利用するためには、permissionsBukkitのような
グループ権限の機能を提供するプラグインを利用する事になります。
とどのつまり、このAPIはインタフェース(Interface)であり、実装(Implementation)ではないのです。
権限の設定
権限を利用して細かな制御を行いたい場合は、
デフォルト権限と、子権限の設定をplugin.ymlに追記する事を検討して下さい。
この2種類の設定は、オプション(必須ではなく完全に任意で利用される)項目ではありますが、お勧めします。
下記は、plugin.ymlの最後に追加する形で設定する権限です:
permissions: doorman.*: description: Gives access to all doorman commands children: doorman.kick: true doorman.ban: true doorman.knock: true doorman.denied: false doorman.kick: description: Allows you to kick a user default: op doorman.ban: description: Allows you to ban a user default: op doorman.knock: description: Knocks on the door! default: true doorman.denied: description: Prevents this user from entering the door
まず、プラグインが利用する各権限を、permissionsノードの子ノードとして定義します。
それぞれの権限はオプションとして、概要(description)、デフォルト値、子ノードを持ちます。
デフォルト権限
プレイヤーが権限を持たない場合、 hasPermission はデフォルトではfalseを返します。
plugin.yml中のデフォルトのノードに対して、下記の4つのどれか1つを設定する事で、
この挙動を変更する事ができます:
- true - デフォルトで、権限を持つ(=hasPermissionがtrueを返す)。
- false - デフォルトで、権限を持たない(=hasPermissionがfalseを返す)。
- op - プレイヤーがOPであれば、権限を持つ(=hasPermissionがtrueを返す)。
- not op - プレイヤーがOPでなければ、権限を持つ(=hasPermissionがtrueを返す)。
子権限
恐らく今までに、* 権限を利用してサブ権限を割り当てた事があると思います。
これは、変更されたBukkit APIと、子権限の定義によって実現した機能であり、
高い柔軟性を提供しています。
下記はその実装例です:
permissions: doorman.*: description: Gives access to all doorman commands children: doorman.kick: true doorman.ban: true doorman.knock: true doorman.denied: falsedoorman.*権限は、いくつかの子権限を含んでいます。 doorman.*権限がtrueである場合に、 その子権限はplugin.ymlに定義された値(trueかfalse)をデフォルト権限として機能します。 doorman.*権限がfalseである場合は、 その子権限のデフォルト権限は、全て反転(trueが定義値ならfalseになる)された状態で機能します。
独自の権限設定
独自に権限の仕組みを提供するプラグインを開発したい場合、 権限プラグインの開発方法を参考にして下さい。
スケジューリングタスクとバックグラウンドタスク
現在、Minecraftサーバはゲームロジックのほとんどがシングルスレッドで稼動しています。
このため、発生する個々の処理はごく小さいものにする必要があります。
プラグイン中に複雑なコードが存在して、それが適切に処理されないような場合は、
ゲームロジックに多大なラグや処理遅延を発生させる原因となります。
幸運なことに、Bukkitはプラグインに対してスケジューリングのためのコーディング方法を提供しています。
どこかのタイミングで一度だけRunnableタスクを実行したり、
小さなタスクに分けた定期的な繰り返しであったり、
新規に独立したスレッドを派生させたり・・・
といった複数の方法で、長大なタスクをゲームロジックと並行で処理させる事ができます。
詳しくは、スケジューラのプログラミング中の、同期タスクと非同期タスクのスケジューリング方法に関する解説を参考にして下さい。
ブロックの操作
ブロックを生成する簡単な方法は、既存のブロックを取得して変更する事です。
例えば、あなたの5ブロック上方にあるブロックを変更したい場合、
まずはそのブロックを取得した上で、変更を加えることになります。
PlayerMoveイベントの処理中でこれを行う例を示します:
public void onPlayerMove(PlayerMoveEvent evt) { Location loc = evt.getPlayer().getLocation(); // プレイヤーからその位置を得る World w = loc.getWorld(); // 位置からワールドを得る loc.setY(loc.getY() + 5); // 位置のY座標を+5する Block b = w.getBlockAt(loc); // ワールド上の指定位置のブロックを得る b.setTypeId(1); // ブロックのタイプIDに1をセットする }
このコードの処理は、プレイヤーが移動する(=PlayerMoveイベントが発生する)度に、
プレイヤーの5ブロック上方のブロックがStone(TypeIdが1のブロック)に変化する動作として見えるでしょう。
ブロック取得までの流れは・・・
- 取得したプレイヤーの位置から、ワールドを取得する
- 位置のY座標を+5する
w.getBlockAt(loc);
で、ワールドにおける位置に存在するブロックを得る
となります。
位置(loc)中のブロックインスタンス(b)に対して、
IDを変えたり、ブロックデータ自体を変更する事もできます。
また、ブロックの種類を表すデータはbyte型であるため、
byte型にキャストしたデータを与えた指定ができます。
例えば、b.setData((byte)3);
です。
建物の生成や、一定のアルゴリズムに従ったブロック生成処理などを行う事ができます。
例えば、固体ブロックによる立方体を生成する処理は、
3階層にネストしたforループ処理によって記述できます。
public void generateCube(Location point, int length){ // public visible method generateCube() with 2 parameters point and location World world = point.getWorld(); int x_start = point.getBlockX(); // Set the startpoints to the coordinates of the given location int y_start = point.getBlockY(); // I use getBlockX() instead of getX() because it gives you a int value and so you dont have to cast it with (int)point.getX() int z_start = point.getBlockZ(); int x_lenght = x_start + length; // now i set the lenghts for each dimension... should be clear. int y_lenght = y_start + length; int z_lenght = z_start + length; for(int x_operate = x_start; x_operate <= x_length; x_operate++){ // Loop 1 for the X-Dimension "for x_operate (which is set to x_start) //do whats inside the loop while x_operate is //<= x_length and after each loop increase //x_operate by 1 (x_operate++ is the same as x_operate=x_operate+1;) for(int y_operate = y_start; y_operate <= y_length; y_operate++){// Loop 2 for the Y-Dimension for(int z_operate = z_start; z_operate <= z_length; z_operate++){// Loop 3 for the Z-Dimension Block blockToChange = world.getBlockAt(x_operate,y_operate,z_operate); // get the block with the current coordinates blockToChange.setTypeId(34); // set the block to Type 34 } } } }
このメソッドは、辺の長さと開始点の指定を受けて、任意のサイズと位置を持つ直方体を生成する処理です。
ブロックの削除処理の場合も、このロジックを真似て同様のアルゴリズムで実装する事ができます。
ただし、セットするブロックのIDはゼロ(=air)になりますね。
プレイヤーインベントリの操作
この節では、プレイヤーのインベントリ操作を特に扱っていますが、
チェストのインベントリの操作にも適用できますので、必要なら応用して下さい(゚∀゚)
インベントリ操作のシンプルな例:
public void onPlayerJoin(PlayerJoinEvent event) { Player player = event.getPlayer(); // Joinしたプレイヤー PlayerInventory inventory = player.getInventory(); // プレイヤーのインベントリ ItemStack diamondstack = new ItemStack(Material.DIAMOND, 64); // 山積みのダイヤモンド! if (inventory.contains(diamondstack)) { inventory.addItem(diamondstack); // プレイヤーインベントリに山積みのダイヤモンドを加える player.sendMessage(ChatColor.GOLD + "よく来たな!もっとダイヤモンドをくれてやろう、このとんでもない成金め!!"); } }
さて、onPlayerJoin()メソッドの先頭で
player, inventory、diamondstack変数を用意して下さい。
inventoryはプレイヤーのインベントリで、diamondstackは(アイテムとしての)64個のダイヤモンドです。
次に、プレイヤーのインベントリがダイヤモンドを含んでいるかをチェックします。
プレイヤーがダイヤモンドをインベントリに所持している場合、
inventory.addItem(diamondstack)
処理にて
同プレイヤーのインベントリに別のスタックを与え、黄金色のメッセージを送信します。
インベントリ操作は全然難しくありません。
ダイヤモンドのスタックを削除したい場合でも単純に、
inventory.addItem(diamondstack)
をinventory.remove(diamondstack)
に置き換えるだけです。
(メッセージにも少しイタズラしておきましょう)
アイテムの操作
アイテムはスタックという単位で操作します。
スタックデータに対する全ての操作は、ItemStackクラスの仕様を参照して下さい。
エンチャント
アイテムに対するエンチャントに触れる前に、Item Code と EIDを見てから以下の解説を読んでください。
エンチャントは、Enchantmentクラスが受け持っている機能ですが、
Enchantmentクラス自体は抽象クラスであるため、インスタンス化(new Enchantment())出来ません。
エンチャントはEnchantmentWrapperクラスから利用する必要があるからです。
元々エンチャントが出来ないアイテムに対して、独自にエンチャントを付与する処理は書けません。
Bukkitサーバ自体が、エンチャント不可能なアイテムに対して、エンチャント情報を付与する仕組みを備えていないからです。
つまり、Fire AspectedなStick(火のエンチャントが付いた木の棒)はあり得ません。
int itemCode = 280; // StickのItemID int effectId = 20; // 効果Fire AspectのEID int enchantmentLevel = 100; // エンチャントのレベル ItemStack myItem = new ItemStack(itemCode); // 木の棒のインスタンスを生成する Enchantment myEnchantment = new EnchantmentWrapper(effectId); // エンチャント効果のインスタンスを生成する myItem.addEnchantment(myEnchantment, enchantmentLevel); // 木の棒にFireAspectを付与する(ただし付与は成功しない)
HashMapの応用
- Note: このセクションは原文が独特な書き回しだったため意訳を行っています。
複数のプレイヤーが発するアクションやイベントを処理するためには、
イベントの発生や状態を保持するための領域として単一の変数を利用していたのでは不十分です。
例を挙げると、
私が作った古いプラグインのZones(今はRegionsに改名して処理も改善しています)では、
プラグインがサーバ上で実際にどのように振舞うかに関して考慮し切れていなかったために、
多くのエラーに直面しました。
具体的には、プレイヤーが特定の領域内に居たかどうかをチェックする処理で
チェック結果の格納先として単一のboolean値を利用したため、
複数のプレイヤーそれぞれに対するチェックを個別に保持する事が出来ずに膨大なエラーを引き起こしたのです。
HashMapはこのような問題を解決する素晴らしい方法です。
HashMapは、データを「キーと値のペア」の集まりとして保管できるデータ保持機能です。
これを利用するメリットは、HashMapが下記の仕様を備えている事です。
- 1つのキーに対して、必ず1つの値が対応する
- 同じ内容のキーが、同一のインスタンス内に重複して存在できない
つまり、キーがプレイヤー、値がプレイヤーに紐付く情報の保管先になるようにHashMapを利用する事で、
プレイヤーとプレイヤーに紐付く値が強制的にペア(対)で管理できますし、
プレイヤーがインスタンス内で重複し得ない(ゲーム内で同一のプレイヤーは重複し得ない)事を保証できるのです。
具体的には、"TaroYamada"がキーとなり、"False"が値となります。
また、後からキー"TaroYamada"に対応する値を"True"に変更する事もできます:
import java.util.HashMap; public class Sample{ public Sample(){ HashMap<String, Boolean> playerFlags = new HashMap<String, Boolean>(); playerFlags.put("TaroYamada", false); playerFlags.put("TaroYamada", true); playerFlags.put("GorillaMatsui", true); } }
- Note: 理解のために独自に追加したコードです。
HashMapの定義
public Map<Key, DataType> HashMapName = new HashMap<Key, Datatype>(); //Example syntax //Example Declaration public Map<Player, Boolean> pluginEnabled = new HashMap<Player, Boolean>(); public Map<Player, Boolean> isGodMode = new HashMap<Player, Boolean>();
このコードは、以降のHashMapsの説明で使うので覚えてください。 さて、例としてプラグインが有効・無効化する時に、天候を切り替える簡単な処理を作りましょう。 まずはonCommand()の中に、前述の説明と同様にプレイヤーの状態に応じて、プレイヤー名を送信する処理を記述します。
onCommand()には下記を記述します。 内部で呼んでいるメソッドの名前は変えても良いですが、意味が通じるシンプルなものにして下さい。
Player player = (Player) sender; togglePluginState(player);
上記のコードは、senderをPlayer型にキャストしたものをパラメタとしてtogglePluginState()に与えています。 では、togglePluginState()を実装しましょう。
public void togglePluginState(Player player){ if(pluginEnabled.containsKey(player)){ if(pluginEnabled.get(player)){ pluginEnabled.put(player, false); player.sendMessage("Plugin disabled"); } else { pluginEnabled.put(player, true); player.sendMessage("Plugin enabled"); } } else { pluginEnabled.put(player, true); //If you want plugin enabled by default change this value to false. player.sendMessage("Plugin enabled"); } }
このコードが何をやっているかというと、 まず、HashMapのpluginEnabledがplayerをキーとして持っているかと、 次に、値がtrueとfalseのどちらなのかを調べています。
playerがキーに含まれており、かつ 値がtrueであれば、falseにリセットしてプレイヤーへメッセージを送信します。 falseであれば、trueにリセットして同様にメッセージを送信します。
playerがキーに含まれていない場合、 今処理をplayerが初めて行った当処理の実行とみなして、 HashMapへプレイヤーと値のペアを追加し、メッセージを送信します。
まだあるHashMapsの用途
HashMap(よく似たMapも)は、データの集合体です。 集合の中から、一意なキーを使って対応する値を参照する処理を、高速・効率的に行います。
これに類する処理は付き物ですが、Mapはこれらを解決します。 Mapの理想的な用途の例はいくつもあります。 下記を見れば分かる通り、 用途は個々のプレイヤーに対応したデータである事にこだわる必要もありません。 必要に応じて、対象を置き換えて他の用途にも活用して下さい。
値の参照
public Map<String, Integer> wool_colors = new HashMap<String, Integer>(); // Run this on plugin startup (ideally reading from a file instead of copied out row by row): wool_colors("orange", 1); wool_colors("magenta", 2); wool_colors("light blue", 3); ... wool_colors("black", 15); // Run this in response to user commands - turn "green" into 13 int datavalue = 0; if (wool_colors.containsKey(argument) datavalue = wool_colors.get(argument); else { try { datavalue = Integer.parseInt(argument); } catch (Exception e) { ; } }
HashMapデータのセーブ/ロード
HashMapがどのように動作するかを学びましたが、 次は恐らく、HashMapのデータを読み書きする方法を知りたくなるかと思います。 もし次のような要求があれば、HashMapが適しています。
- 管理者に手作業でデータ編集をさせたくない
- バイナリ形式でデータを保管したい(YAMLで扱うには複雑すぎるデータ)
- ブロック等のオブジェクトの引き当てに、任意の文字列を使いたい
これらは、HasMapの定義(HashMap<Player,Boolean>)を、任意のデータ型に変更することでシンプルに行えます。 引き続き、前節で出てきた"pluginEnabled"インスタンスの例を使って説明をします。 このコードは、与えられたパスに存在するファイルに対して、HashMapの値を保存します。
public void save(HashMap<Player,Boolean> pluginEnabled, String path) { try{ ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream(path)); oos.writeObject(pluginEnabled); oos.flush(); oos.close(); //Handle I/O exceptions }catch(Exception e){ e.printStackTrace(); } }
これも非常に簡単で、ロード処理も他の例とそっくりな記述をします。 ObjectOutputStreamの代わりにObjectInputStream、 FileOutputStreamの代わりにFileInputStream、 writeObject()の代わりにreadObject()を使っています。 そして、最後にHashMapをreturnしています。
public HashMap<Player,Boolean> load(String path) { try{ ObjectInputStream ois = new ObjectInputStream(new FileInputStream(path)); Object result = ois.readObject(); //you can feel free to cast result to HashMap<Player,Boolean> if you know there's that HashMap in the file return (HashMap<Player,Boolean>)result; }catch(Exception e){ e.printStackTrace(); } }
このAPIを、HashMap, ArrayLists, Blocks, Players...その他全てのオブジェクトの、セーブ/ロードに利用できます。もし利用する場合は、私(Tomsik68)の著作である事を明記して下さい。
/** SLAPI = Saving/Loading API * API for Saving and Loading Objects. * @author Tomsik68 */ public class SLAPI { public static void save(Object obj,String path) throws Exception { ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream(path)); oos.writeObject(obj); oos.flush(); oos.close(); } public static Object load(String path) throws Exception { ObjectInputStream ois = new ObjectInputStream(new FileInputStream(path)); Object result = ois.readObject(); ois.close(); return result; } }
APIを使った実装方法: 一部だけ抜粋
public class Example extends JavaPlugin { private ArrayList<Object> list = new ArrayList<Object>(); public void onEnable() { list = (ArrayList<Object>)SLAPI.load("example.bin"); } public void onDisable() { SLAPI.save(list,"example.bin"); } }
- SLAPIとJavaのObjectOutputStreamに関する補足:Integer, String, HashMapといったJavaのデータ型について理解しているのであれば、この処理は未編集で動作します。Bukkitが提供するデータ型に対しても同様です。しかし、独自のデータクラスを自作してこのテクニックを適用したい場合は、JavaのSerializableインタフェースについて読んで下さい。それにより、簡単に少量の変更で動かす事ができます。コードを丁寧に解析する必要はありません。
Map・Set・Listの応用
HashMapクラスのもうひとつの機能は、Javaの持つ多くのデータ構造を提供する事にあります。
しかし、クラスの種類によってMapは時に適切ではないケースも存在します。
こちらに、そのようなケースについて詳しく記述するためのページとして、Javaのデータ構造クラスを用意しています。
データベース
時にフラットファイルは、データベースとして利用するには不足があるでしょう。 Linux/Mac/Windowsのマシンにおいて、もっとも汎用的なデータベースはSQL(Structured Query Language)です。
ソフトウェアとしてのSQLは、個々のデータを格納するセルと、それらを識別するための列およびヘッダから成るデータベースを生成します。 会計用ソフトウェア(たとえばエクセルのような)の表が、より便利になったモノを思い浮かべてください。 SQLは、この表形態のデータに、データ同士の整合性の確保を目的とした独自のルール付けが行え、 かつ、フラットなファイルよりも高速で高機能なデータ利用のための方法も提供します。
Bukkitは、SQLの標準的な機能を利用するための仕組みを提供しています。 残念なことに、SQLには複数の種類が存在し、それぞれの設定方法と利用方法が少しずつ異なっています。 各自のニーズに応じて、好きな種類のSQLを選んで利用してください。(プラグインについても、利用するデータベースの種類に応じて複数の接続・利用方法が利用できます)
SQLite
Alta189は、ダウンロード・インポートによって導入や移動が容易に可能なSQLiteを、Bukkitのプラグインで利用するためのライブラリを使う方法のチュートリアル、fantastic SQLite tutorialを書きました。
SQLの構文については、次に紹介する動画を一度見てみる事をお勧めします。簡単な内容なので短時間で済みます。SQL Tutorials @W3Schools
SQLiteは、その稼動のためにサーバを構築する必要がないため、シンプルさにおいて非常に優れています。 新規にデータベースとその中のテーブルを作成する手順は少量です。また、データのバックアップも、1個のデータベースファイルをバックアップするだけです。ただし、データ間の整合性の確保や柔軟性、データ件数が100万を超えるような膨大なデータの扱いにおいては、多少弱い面もあります。
とはいえSQLiteは、SQLデータベースを利用する新規プラグインの開発作業を、迅速で簡単にしてくれるメリットがあります。また、サーバ向けの大規模なSQLの予習のためにも有用です。
MySQL
もうひとつ、人気があるSQLにMySQLというものがあります。 SQLiteよりもサーバ寄りの用途をもった種類のSQLで、多くの有名な企業の業務システムや、一日に100万アクセスを捌くようなウェブサイトが、この機能に依存しています。ただし、チューニング可能な範囲や機能が多岐に渡るため、セキュリティ上のリスクが、データベース管理者のMySQLへの習熟度に大きく左右されます。
プラグインからMySQLを利用する処理のコーディング自体は、小規模なSQLiteからMByte単位のOracleのデータベースに対するものと、大差ありません。しかし、サーバ用途のデータベースの管理作業は、どんどん増えていくものです。使い続ける内に、データベース利用者のアカウントや権限の設定作業を行う事になっていくでしょう。また、データのバックアップやバックアップからの復旧(Rollbackと呼ぶ)作業のために、SQL文のスクリプトを書く事にもなるはずです。
プラグインの配布
- Note: 当セクションは、日本語圏の読者向けの資料として外部の紹介サイトを独自に追加しています。
プラグインのコーディング後に行うべき事は、サーバへインストールするためのJarファイルを、ソースコードから作成する事です。これはどのように行えば良いでしょう?
- まずは、サーバのセットアップを参考にしながらローカルマシン上にBukkitサーバを起動可能な状態で設置して下さい。(とりあえずは[minecraft公式サーバ]と[bukkitサーバ]を取得して、bukkitサーバを起動コマンドで起動可能な状態にするだけで良い)
- 次に、プラグインをJar形式でエクスポートするための画面を開きます。(Eclipseを例にした場合、プラグインのプロジェクトを選択した状態で、パッケージエクスプローラ上のプロジェクトの右クリックか、ファイル > エクスポートの選択で表示される機能)
- 開いたエクスポート画面上で"Java", "JARファイル"を選択し、次へボタンを押下し、下記のような画面に遷移します。
- 画面左側のメニューで、プラグインのソースコードが存在するフォルダ(この例ではsrc)が選択されている事を確認します。
- 画面右側のメニューで、ファイル名がドットで始まるファイルの選択を解除します(この例では.classpath, .gitgnore, .projectの3ファイル)。これらはEclipseが利用するファイルでありプラグインには不要です。
- プラグインを動作させるために重要な事ですが、plugin.ymlが選択されている事も確認します。
- 最後に、任意の場所にJARファイルをエクスポート(書き出し)します。
プラグインのコードとplugin.ymlに不備が無ければ、エクスポートしたJarファイルはすぐにBukkitプラグインとして動作します。Jarファイルを、Bukkitサーバの"plugins"フォルダの中に配置し、Bukkitサーバを起動し、プラグインの動作確認をしてみましょう。なお、ローカルマシン上で起動するBukkitサーバへは、Minecraftクライアントのマルチプレイヤーサーバの接続先IPアドレスに"localhost"を指定して接続する事でログインできます。
もしプラグインが上手く動かず、それがどうしても自分で解決できない場合は、当Wikiやその原文をもう一度よく読み、それでも駄目ならBukkitプラグイン開発者フォーラム(英語圏), bukkitdev Bukkit公式サイトの開発者向けIRCチャンネル(英語圏), マインクラフト非公式日本ユーザフォーラムの関連トピック(日本語圏), 当WikiのMOD制作関連IRCチャンネル(日本語圏)をたずねてみて下さい。有用なプラグインが作れたら、dev.bukkitに登録し、プラグインを広く公開する事を検討してみて下さい。他のBukkitユーザ・開発者に貢献する事ができます。
ヒントとノウハウ
CraftBukkit APIは、すばらしい機能をたくさん提供しています。 下記に、面白い効果を実現するコードを示します。
プレイヤーに着火する
プレイヤーを燃やすコマンド:
public boolean onCommand(CommandSender sender, Command cmd, String label, String[] args){ if(cmd.getName().equalsIgnoreCase("ignite")){ Player s = (Player)sender; Player target = s.getWorld().getPlayer(args[0]); // 誰がコマンドを実行したか // "/ignite notch"コマンドが打たれた場合、targetインスタンスは"notch"を対象とします。 // Note: 最初の引数は[0]で、[1]ではないので、arg[0]がプレイヤー名を格納しています。 target.setFireTicks(10000); return true; } return false; }
プレイヤー"Notch"がオンラインである場合のみ/ignite Notchコマンドは動作し、Notchが燃やされます。
プレイヤーを殺す
同じ要領で、プレイヤーを殺害するコマンドの例を紹介します。 onCommand()メソッドに記述します:
public boolean onCommand(CommandSender sender, Command cmd, String label, String[] args){ if(cmd.getName().equalsIgnoreCase("KillPlayer")){ Player player = (Player)sender; Player target = player.getWorld().getPlayer(args[0]); target.setHealth(0); } return false; }
上記の拡張版として、プレイヤーを爆死させる処理を下記に示します:
float explosionPower = 4F; //This is the explosion power - TNT explosions are 4F by default Player target = sender.getWorld().getPlayer(args[0]); target.getWorld().createExplosion(target.getLocation(), explosionPower); target.setHealth(0);
爆発を起こす
このコードは、TNTやクリーパーの爆発と同様の音とヴィジュアルを再現します。 これは、TNTの爆発効果は無効化しつつ、音とヴィジュアル効果を発生させる処理に転用できます。
public void onExplosionPrime(ExplosionPrimeEvent event){ Entity entity = event.getEntity(); if (entity instanceof TNTPrimed){ TNTPrimed tnt = (TNTPrimed) entity; event.getEntity().getWorld().createExplosion(tnt.getLocation(), 0); } }
リクエストに応じて書かれた記事
mavenを利用したプラグイン開発
gitのリポジトリ上にあるBukkitPluginArchetypeをcloneし、それをビルドする:
git clone git://github.com/keyz182/BukkitPluginArchetype.git cd BukkitPluginArchetype mvn clean install
作成するプラグインのフォルダに移動して下記のコマンドを実行します:
mvn archetype:generate -DarchetypeCatalog=local
プロンプトに表示されたリストから下記を選択します:
uk.co.dbyz:bukkitplugin (bukkitplugin)
GroupIDには筆頭(プラグインのトップ階層)としたいJavaパッケージ名を指定し、ArtifactIDにはパッケージ名の末端の名称(Jarファイルのような成果物の名称として利用されます)を入力します。そして、確認メッセージにY<enter>で応答します。
例:
Define value for property 'groupId': : uk.co.dbyz.mc Define value for property 'artifactId': : plugin Define value for property 'version': 1.0-SNAPSHOT: Define value for property 'package': uk.co.dbyz.mc:
ArchetypeIDに指定した文字列と同名のフォルダが生成され、配下にsrcフォルダとpom.xmlファイルが配置されます。
src/main/java/<package>フォルダに存在する<archetypeid>CommandExecuter.javaファイルを開き、コード
//Do Something
の部分に書きのコードを記述します:
Player player = (Player) sender; player.setHealth(1000);
ベースのフォルダに移動して下記を実行します:
mvn clean package
ダウンロード処理が走りますが、他の作業を並行して行っても大丈夫です。 clean package処理が完了すると、targetフォルダの中に<archetypeid>-1.0-SNAPSHOT.jarファイルが生成されます(これがビルドされたプラグインのJarファイルです)。このファイルをBukkitのpluginsフォルダへコピーして、Bukkitサーバを起動して下さい。
ゲーム内で/<archetypeid>コマンドを実行すると、そのプレイヤーのHealthが全快します。
プラグインのサンプル兼雛形
この内容について質問がある場合、遠慮なくAdamki11sかBukkitDevのIRCチャンネル(当Wikiの原文を掲載しているサイトのIRCチャンネルです)で聞いてください。
- Note: 当ページは訳文であるため、訳文自体に対する文責は原著者にありません。その点を考慮して必ず原文にも目を通してから質問して下さい。