Minecraftで「だるまさんがころんだ」ができるプラグインを作った

以前製作過程をアップロードしていましたが、全てあげ終えるまでにパワーアップしまくったので改めて。

※2019/7/18 追記
色々機能を増やしたので更新しました。

#開発環境
言語はJavaで、エディタはIntelliJ IDEAを使用しています。
Minecraftのバージョンは1.13.2です。
pluginの種類はspigotです。

#このプラグインでできること
このプラグインは、おそらくみんなやったであろう日本の昔ながらの遊び「だるまさんがころんだ」がMinecraft内で遊べるようになるプラグインです。

#このプラグインの説明や機能について
###このプラグインの説明
- 現実のだるまさんがころんだでは、鬼役が存在しますが、このプラグインには鬼がいません。その代わり、**ゴール**が存在します。だるまさんがころんだをしながら、そのゴールまで進んで行く、というのがこのプラグインでできる「だるまさんがころんだ」です。
- このプラグインの「だるまさんがころんだ」には、**ターン**なるものが存在します。このプラグインでは「だるまさんがころんだ」を言い終わって動いたかどうかの判定をしてから次の"だ"を言いはじめるまでを１ターンとしています。例えば３ターンというと、”だるまさんがころんだ”を言って動いたかの判定を行うという流れを３回行ってからゲームを終了します。このターン数は、ゲーム内からコマンドで設定できるので、ゲームの長さはその場で自由に決めることができます。１ターンはおおよそ１３秒(”だるまさんがころんだ”を言い終わるまでが１０秒、判定時間が３秒)です。
- もしプレイヤーが動いたかの判定中に動いてしまった場合、スタート地点に戻されます。セーブポイントを通過している場合は最後に通過したセーブポイントに戻されます。
- ゲームの参加者は、ゲームを開始するコマンドを打った時点でゲームモードがアドベンチャーのプレイヤーが自動的に参加者として認識され、参加者一覧に登録されます(参加者はターミナルに表示される)。それ以外のプレイヤーは参加者一覧には登録されていないので専用のコマンドを打たない限りは何をしても参加者としては認識されません。

###このプラグインの機能
以下の機能は全て、ゲーム中でかつ、参加者として認識されているプレイヤーにしか発動しません。
####参加者か否かわかりやすくするやつ
<img width="443" alt="スクリーンショット 2019-07-18 14.35.14.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/278363/2434e06c-1c7d-b7a1-9a34-af2d8e5dd679.png">
<img width="524" alt="スクリーンショット 2019-07-18 14.35.23.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/278363/7e1a67b5-a7e5-8de1-7dbd-c569724df98f.png">
ゲームのスタート時やゴールした時などにプレイヤーリストの名前の横に[観戦中]や[参加者]などの表示が出るようになりました。これにより、管理者が参加者や観戦者、誰がゴールしたかなどを一発で把握できるようになりました。

####ゴール
![2019-06-24_22.04.06.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/278363/0dcc5364-4839-5d76-3d1a-08ac87b3b19d.png)
金ブロックの上に石ボタンを設置した場合、それはゴールとなります。金ブロックの上面以外に石ボタンがある場合にはゴールにはなりません。
プレイヤーがゴールすると、ゲームモードがスペクテイターモードに変更され、、観戦モードになります。

```java
public void GoalEvent(PlayerInteractEvent event) {
        if (Daruma.check) {
            Player player = event.getPlayer();
            Block block = event.getClickedBlock();
            if (event.getAction() == Action.RIGHT_CLICK_BLOCK) {
                if ((block != null ? block.getType() : null) == Material.STONE_BUTTON) {
                    Location location = block.getLocation().clone();
                    location.add(0,-0.1,0);
                    if (player.getGameMode() == GameMode.ADVENTURE && Daruma.list.contains(player.getName()) && location.getBlock().getType().equals(Material.GOLD_BLOCK)) {
                        getServer().broadcastMessage(ChatColor.BLUE + player.getName() + ChatColor.WHITE + "さんがゴールしました！");
                        player.setPlayerListName("["+ChatColor.BLUE+"ゴール済"+ChatColor.WHITE+"]"+"["+ChatColor.RED+"観戦中"+ChatColor.WHITE+"]"+ChatColor.RED+player.getName());
                        if (player.hasMetadata(Events.DATA_KEY)) {
                            player.removeMetadata(Events.DATA_KEY, plugin);
                        }
                        getServer().broadcastMessage(clearTime(Daruma.time));
                        (player.getWorld()).playSound(player.getLocation(), Sound.ENTITY_PLAYER_LEVELUP, 2, 1);
                        Daruma.Goallist.add(player.getName()+"<"+clearTime(Daruma.time)+ChatColor.WHITE+">");
                        player.setGameMode(GameMode.SPECTATOR);
                        Daruma.list.remove(player.getName());
                        if(Daruma.list.isEmpty()){
                            getServer().broadcastMessage("参加者全員がゴールしたため、ゲームを終了します。");
                            for (Player target : Bukkit.getOnlinePlayers()) {
                                if (target.getGameMode() == GameMode.ADVENTURE || target.getGameMode() == GameMode.SPECTATOR) {
                                    target.sendTitle(ChatColor.RED + "終了！", "", 10, 15, 10);
                                    target.teleport(target.getWorld().getSpawnLocation());
                                    target.setGameMode(GameMode.ADVENTURE);
                                    target.setPlayerListName(target.getName());
                                    target.setLevel(0);
                                }
                            }
                            Daruma.game=false;
                            Daruma.check=false;
                        }
                    }
                }
            }
        }
    }
```
コードはこんな感じ。
もし金ブロックの上に石のボタンが置かれたものを参加者がクリックしたならば、ゲームモードを変更してクリアタイムを表示したと同時にランキング表にクリアタイムを登録、そのプレイヤーを参加者一覧から削除しプレイヤーリストに[ゴール済]を追加する、というものになっています。この時点でもし参加者が全員ゴールしていたならばゲームを自動的に終わらせるようになっています。

```java
int min,sec;
        min = (time%3600)/60;
        sec = time%60;
        String clearTime;
        clearTime = ChatColor.GREEN+"クリアタイム："+min+"分"+sec+"秒";
        return clearTime;
```
こんな感じのコードでクリアタイムを計算しています。
####セーブポイント
![2019-06-24_22.04.19.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/278363/a62af857-d1d7-d346-bd05-6ec4100658c3.png)
エメラルドブロックを置いた場合、それはセーブポイントとなります。自動セーブ機能なので、セーブポイントであるエメラルドブロックの上を通過した瞬間に自動でセーブされ、もし動いたかの判定中に動いてしまった場合スタート地点には戻されず、最後に通過したセーブポイントに戻されます。
自動セーブ機能にはメタデータを使用しており、セーブには５秒のクールダウンがあります。

```java

Player player = event.getPlayer();
            if (player.isOnGround()){
                Location location = event.getTo().clone();
                location.add(0, -0.1, 0);
                if (location.getBlock().getType().equals(Material.EMERALD_BLOCK) && Daruma.list.contains(player.getName())&&player.getLevel()<1) {
                    player.setLevel(5);
                    player.setMetadata(DATA_KEY, new FixedMetadataValue(plugin,player.getLocation().clone()));
                    player.sendMessage(ChatColor.AQUA + "セーブしました！");
                    player.playSound(player.getLocation(), Sound.BLOCK_NOTE_BLOCK_CHIME, 1, 24);
                    Timer timer = new Timer();
                    TimerTask task = new TimerTask() {
                        int i = 5;
                        @Override
                        public void run() {
                            if(i<1){
                                player.setLevel(0);
                                player.setExp(0);
                                timer.cancel();
                            }
                            player.setLevel(i);
                            i--;
                        }
                    };
                    timer.schedule(task,0,1000);
                }
            }
```
コードはこんな感じで、プレイヤーの移動先を取得して、そこにエメラルドブロックがあればプレイヤーの現在地点をメタデータに記録し、プレイヤーに持たせると言った感じになっています。
そのままだと連続で発動してしまうのでクールダウンを実装して連続発動を防いでいます。
####ランキング
![2019-06-24_13.48.11.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/278363/c09cda4e-ef2d-a8e7-6057-b3bf0545a400.png)
プレイヤーがゴールすると、サーバーにそのプレイヤーのクリアタイムが表示されます。ゲームが終わった後にも、ゲーム内からコマンドでゴールした人のクリアタイム一覧を表示することが可能です。

```java
String string;
            if (!(Daruma.Goallist.isEmpty())) {
                for (int i = 1; i <= Daruma.Goallist.size(); i++) {
                    string = i + "位：" + Daruma.Goallist.get(i - 1);
                    getServer().broadcastMessage(string);
                }
            } else {
                getServer().broadcastMessage("ゴールしたプレイヤーはいませんでした。");
            }
```

コードはこんな感じ。ランキング表に何かしら書かれていればランキング表を表示する、というものになっています。
ランキング表が空であれば、ランキング表は表示されません。
####ギミック
セーブポイントの応用で踏むと発動するギミック的なものを作りました。
####ジャンプパッド

```java
void jumpPad(PlayerMoveEvent event){
        if((Daruma.game)){
            Player player = event.getPlayer();
            if (player.isOnGround()){
                Location location = event.getTo().clone();
                location.add(0, -0.1, 0);
                if (location.getBlock().getType().equals(Material.REDSTONE_BLOCK) && Daruma.list.contains(player.getName())) {
                    player.sendMessage(ChatColor.AQUA+"ジャンプパッドに乗った！");
                    for(float o=0;o<360;o=(float)(o+0.5)){
                        (player.getWorld()).spawnParticle(Particle.CLOUD,(float) (location.getX()+Math.sin(Math.toRadians(o))*1), (float) (location.getY()), (float) (location.getZ()+Math.cos(Math.toRadians(o))*1), 1, 0, 0, 0, 0);
                    }
                    player.playSound(player.getLocation(), Sound.ITEM_FIRECHARGE_USE, 1, 1);
                    player.setVelocity(new Vector(0,1.75,0));
                }
            }
        }
    }
```
レッドストーンブロックを踏むと上に大きくジャンプするジャンプパッドを作りました。
そのままジャンプするだけだと面白くないのでplaysoundやspawnParticleなんかを使って演出もつけています。
####踏んじゃいけない床

```java
void dontstep(PlayerMoveEvent event){
        if((Daruma.game)){
            Player player = event.getPlayer();
            if (player.isOnGround()){
                Location location = event.getTo().clone();
                location.add(0, -0.1, 0);
                if (location.getBlock().getType().equals(Material.NETHER_WART_BLOCK) && Daruma.list.contains(player.getName())) {
                    (player.getWorld()).spawnParticle(Particle.FLAME, location.getX(), location.getY(), location.getZ(), 30, 0.35, 0.35, 0.35);
                    if (player.hasMetadata(DATA_KEY)) {
                        MetadataValue value = null;
                        List<MetadataValue> values = player.getMetadata(DATA_KEY);
                        for (MetadataValue v : values) {
                            if (Objects.requireNonNull(v.getOwningPlugin()).getName().equals(plugin.getName())) {
                                value = v;
                                break;
                            }
                        }
                        if (value == null) {
                            return;
                        }
                        Location location1 = (Location) value.value();
                        assert location1 != null;
                        player.teleport(location1);
                    } else {
                        player.teleport(Daruma.startpoint);
                    }
                    player.playSound(player.getLocation(), Sound.ENTITY_GHAST_HURT, 1, 1);
                }
            }
        }
    }
```
ネザーウォートブロックを踏むとセーブ地点もしくはスタート地点に飛ばされる踏んじゃいけない床を作りました。こちらも「そのプレイヤーが着地した地点から火花が飛び散る」と言った演出を設定しています。
~~実際やって見るとなんかロック●ンみたいだった~~
####スピードパッド
```java
void dashPad(PlayerMoveEvent event){
        if((Daruma.game)){
            Player player = event.getPlayer();
            if (player.isOnGround()){
                Location location = event.getTo().clone();
                location.add(0, -0.1, 0);
                if (location.getBlock().getType().equals(Material.DIAMOND_BLOCK) && Daruma.list.contains(player.getName())&&!(player.hasPotionEffect(PotionEffectType.SPEED))) {
                    player.sendMessage(ChatColor.AQUA+"ダッシュパッドに乗った！");
                    player.playSound(player.getLocation(),Sound.BLOCK_BEACON_AMBIENT,1,1);
                    player.addPotionEffect(new PotionEffect(PotionEffectType.SPEED,30,3),true);
                }
            }
        }
    }
```
ダイアモンドブロックを踏むと一定時間スピードが上がるダッシュパッドを作りました。
addPotionEffectで移動速度上昇のポーションエフェクトを与えています。
~~演出が思いつかなかったとか言っちゃいけない~~
####スロウパッド

```java
void slowPad(PlayerMoveEvent event){
        if((Daruma.game)){
            Player player = event.getPlayer();
            if (player.isOnGround()){
                Location location = event.getTo().clone();
                location.add(0, -0.1, 0);
                if (location.getBlock().getType().equals(Material.LAPIS_BLOCK) && Daruma.list.contains(player.getName())&&!(player.hasPotionEffect(PotionEffectType.SLOW))) {
                    player.playSound(player.getLocation(),Sound.ENTITY_SLIME_JUMP,1,1);
                    player.sendMessage(ChatColor.RED+"スロウパッドに乗った！");
                    player.addPotionEffect(new PotionEffect(PotionEffectType.SLOW,80,3),true);
                }
            }
        }
    }
```
ラピスラズリブロックを踏むと一定時間足が遅くなるスロウパッドを作りました。
仕組みはスピードパッドとだいたい同じです。
####ブラインドパッド

```java
void blindPad(PlayerMoveEvent event){
        if((Daruma.game)){
            Player player = event.getPlayer();
            if (player.isOnGround()){
                Location location = event.getTo().clone();
                location.add(0, -0.1, 0);
                if (location.getBlock().getType().equals(Material.COAL_BLOCK) && Daruma.list.contains(player.getName())&&!(player.hasPotionEffect(PotionEffectType.BLINDNESS))) {
                    player.playSound(player.getLocation(),Sound.ENTITY_SLIME_JUMP,1,1);
                    player.sendMessage(ChatColor.RED+"ブラインドパッドに乗った！");
                    player.addPotionEffect(new PotionEffect(PotionEffectType.BLINDNESS,80,255),true);
                }
            }
        }
    }
```
石炭ブロックを踏むと一定時間目が見えなくなるブラインドパッドを作りました。
仕組みはだいたいスピードパッドと同じです。

#最後に
ここまで作るのに半年ほどかかりました。ぬるぽだのフェイタルエラーだの吐き出されまくりながらやっとこさ完成したので自分としてはこのプラグインは自慢の作品です。
ゲームの処理の所々にスリープメソッドを使用していますが何も考えないでそれ使うとゲームそのものが止まってしまう（全ての処理を一つのスレッドで行うため）ので、ゲームが止まらないようにTimerクラスを使ったりとかして工夫するところとか難しかったです。もっといいやり方はあるだろうけどとりあえずはこれを使いました。
しかし、自分はまだ未熟できっとまだまだ改善できる点はあると思うので、そこらへん改善していきたいと思います。
もし「配布してほしい」などの声があれば、専用のコマンドなどの説明もつけた上で配布するつもりでもあります。
~~いいねとかストックとかTwitterとかで拡散とかしてくれると嬉しい~~
ここまで読んでいただき、ありがとうございました。
ソースは[こちら](https://github.com/broly318/Daruma-plugin)になります。