ゲームを作りながら学ぶ Java実践 リファクタリング③

プレイヤーの周囲の敵を消すのを、出現させた敵に限定する

リファクタリング

  private void gamePlay(Player player, PlayerScore nowPlayer) {
    Bukkit.getScheduler().runTaskTimer(main,Runnable->{
      if (nowPlayer.getGameTime() <= 0){
        Runnable.cancel();
        player.sendTitle("ゲームが終了しました。",
            nowPlayer.getPlayerName() + " 合計 " + nowPlayer.getScore() + "点",
            0,60,0);
        nowPlayer.setScore(0);
        List<Entity> nearbyEnemies = player.getNearbyEntities(50, 0, 50);
        for (Entity enemy : nearbyEnemies){
          switch (enemy.getType()) {
            case ZOMBIE, HUSK, SKELETON -> enemy.remove();
          }
        }
        return;
      }
      player.getWorld().spawnEntity(getEnemySpawnlocation(player), getEnemy());
      nowPlayer.setGameTime(nowPlayer.getGameTime() - 5);
    },0,5*20);
  }

の部分をリファクタリングしていく。

まず、全体的に名前を合わせるために「nowPlayer」を右クリックして「リファクタリング」→「名前の変更」で
「nowPlayerScore」と変更する。

  private void gamePlay(Player player, PlayerScore nowPlayerScore) {
    Bukkit.getScheduler().runTaskTimer(main,Runnable->{
      if (nowPlayerScore.getGameTime() <= 0){
        Runnable.cancel();
        player.sendTitle("ゲームが終了しました。",
            nowPlayerScore.getPlayerName() + " 合計 " + nowPlayerScore.getScore() + "点",
            0,60,0);

        nowPlayerScore.setScore(0);

        List<Entity> nearbyEnemies = player.getNearbyEntities(50, 0, 50);
        for (Entity enemy : nearbyEnemies){
          switch (enemy.getType()) {
            case ZOMBIE, HUSK, SKELETON -> enemy.remove();
          }
        }
        return;
      }
      player.getWorld().spawnEntity(getEnemySpawnlocation(player), getEnemy());
      nowPlayerScore.setGameTime(nowPlayerScore.getGameTime() - 5);
    },0,5*20);
  }

全体的に「nowPlayer」が「nowPlayerScore」に変更される。

まず、「nowPlayerScore.setScore(0);」の部分をリファクタリングしていく。

なぜここを「リファクタリング」するかというと

現在では、タイプ別でしか敵を倒したときに点数が入るとしかしていないので、ここで記述しているような形で「コマンドを実行したプレイヤーを登録してゲームを終了したときに点数を0にする」としていると、終了したあとに敵を倒して、点数を数多く稼いでおいたうえでコマンドを実行したら、点数が入った状態からスタートできてしまい、不正が起きてします。
ちなみに、バグとも呼べる。

よって、

「getPlayerScore」の時点で0にする必要がある。
(終了時にスコアを0にするのではなく、ゲーム開始時に0にする必要がある。)

先に、「GAME_TIME」を20秒に撮るようにしたので、その下に「playerScore.setScore(0);」を記述する。

この事により、ゲームスタート時にスコアが0になるようになったので、「nowPlayerScore.setScore(0);」はいらなくなる。

        List<Entity> nearbyEnemies = player.getNearbyEntities(50, 0, 50);
        for (Entity enemy : nearbyEnemies){
          switch (enemy.getType()) {
            case ZOMBIE, HUSK, SKELETON -> enemy.remove();
          }

次に、この部分をリファクタリングしていく。

これは、「周りの指定した敵をけす」というふうになっているが、実際は「自分が出現させた敵を消す」とするほうが正解なので、

まず、出現させた敵のリストを作るために、入れ物を作るために

「private List<PlayerScore> playerScoreList = new ArrayList<>();」の下に

「private List<Entity>spawnEntityList = new ArrayList<>();」を記述する。
(出現させたEntityのリストを作る。)

そこで、このリストに出現させたEntityを「spawnEntityList」に「add(リストに登録)」させるために

「player.getWorld().spawnEntity(getEnemySpawnlocation(player), getEnemy());」の部分に追加記述して

「spawnEntityList.add( player.getWorld().spawnEntity(getEnemySpawnlocation(player), getEnemy()));」とする。

ここでは、Entityを追加するという処理とEntityを出現させるという処理の2つをしていることになってしまっているので、「player.getWorld().spawnEntity(getEnemySpawnlocation(player), getEnemy())」の部分をを指定して「リファクタリング」→「変数の導入」で名前を「spawnEntity」としておく。

      Entity spawnEntity = player.getWorld().spawnEntity(getEnemySpawnlocation(player), getEnemy());
      spawnEntityList.add(spawnEntity);

とする。

ここで、「spawnEnemyList」ができているので、その中のリストのものを消せばよいとなるので、

        for (Entity enemy : spawnEntityList){
          enemy.remove();
        }

を追加記述する。

そこで、これをきれいにする方法があるので、「for」のところにカーソルをあて、左側の電球マークのようなものをクリックし、「ストリーム ‘forEach()’ を含むループを折りたたむ」をクリック。

spawnEntityList.forEach(Entity::remove);

となります。

このことにより

        List<Entity> nearbyEnemies = player.getNearbyEntities(50, 0, 50);
        for (Entity enemy : nearbyEnemies){
          switch (enemy.getType()) {
            case ZOMBIE, HUSK, SKELETON -> enemy.remove();
          }
        }

の部分がいらなくなり消去できるようになる。

ここでのリファクタリングとしては、

  private void gamePlay(Player player, PlayerScore nowPlayerScore) {
    Bukkit.getScheduler().runTaskTimer(main,Runnable->{
      if (nowPlayerScore.getGameTime() <= 0){
        Runnable.cancel();

        player.sendTitle("ゲームが終了しました。",
            nowPlayerScore.getPlayerName() + " 合計 " + nowPlayerScore.getScore() + "点",
            0,60,0);

        spawnEntityList.forEach(Entity::remove);
        return;
      }
      Entity spawnEntity = player.getWorld().spawnEntity(getEnemySpawnlocation(player), getEnemy());
      spawnEntityList.add(spawnEntity);
      nowPlayerScore.setGameTime(nowPlayerScore.getGameTime() - 5);
    },0,5*20);
  }

こうなります。

ここで実証!!

動作自体は成功!!

しかし「enemySpawnCommand」が実行できません!

エラーを調べていくと、「enemySpawnCommand」が利用できないとでているので、様々調べてみると、この「enemySpawnCommand」クラスが認識されていないとの指摘が出てきました。
そこで、すべてのファイルを見ていると、「Main」ファイルにコマンドを呼び込むような記述を見つけたので、
読み込ませるためのコードを調べて「getCommand(“enemyspawn”).setExecutor(new EnemySpawnCommand());」を追記することにしました。

そこで「Main」クラスは、

package plugin.enemydown;

import java.net.http.WebSocket.Listener;
import org.bukkit.Bukkit;
import org.bukkit.event.EventHandler;
import org.bukkit.event.entity.EntityDeathEvent;
import org.bukkit.plugin.java.JavaPlugin;
import plugin.enemydown.command.EnemyDownCommand;
import plugin.enemydown.command.EnemySpawnCommand;

public final class Main extends JavaPlugin {

    @Override
    public void onEnable() {
        EnemyDownCommand enemyDownCommand = new EnemyDownCommand(this);
        Bukkit.getPluginManager().registerEvents( enemyDownCommand, this);
        getCommand("enemyDown").setExecutor( enemyDownCommand);
        getCommand("enemyspawn").setExecutor(new EnemySpawnCommand());
    }
}

となりました。

そして、実証!!

「enemyspawn」で敵を出現させることができるようになりました。
そこで出来を倒したら、追加で点数が入り、続いて「enemydown」をしたときに点数がクリアになり

成功!!!

ここで、実装予定していないで実装した方が良い機能要件を追加する。

設計(項目追加)

機能要件

  • 体力や空腹ゲージは最大化されること
    • 前提条件を合わせるため
  • 一定のエリア内でしか敵は発生しないこと
    • エリア外で発生しても倒せない
  • 敵の種類はランダムであること
  • 装備や武器はプレイするたびにおなじになること
    • 今の装備を取得して保存しといて、ゲームが始まったら指定の装備にして、終わったらもとの装備に戻す等
    • 対象のプレイヤーのインベントリの中身を直接書き換えることで実現する。
    • コマンド実行時に差し替えて、最終的にはコマンド実行前の装備の状況を保存しておき、ゲーム終了後戻したい
  • 時間制限を設定できること
  • スコア(合計点数)ボードが表示できること
    • 画面上にバーンと出るようにする
    • スコアボードにするなど
  • 敵を倒すと点数が手に入ること
  • 敵の種類によって手に入る点数が異なること
    • ゲーム性を高めるため
    • ゲーム性をしてどうあるべきか
  • 時間制限が来たらエリア内の敵は消滅すること
    • 終わったことをはっきりさせるため
  • 特殊効果や状態異常はゲーム開始時に無効化されること
    • ゲーム終了時にも無効化されること
      (これは、私も毒って死んでentityを変更することとした原因となりました。)
  • 時間制限が来たら合計の点数が保存されること
    • できればデータベースなんかに保存して取り出せるようにしておきたい
  • 保存する情報はスコアとプレイヤー名と日時
  • 新しい情報が入った場合は上書きではなく、すべて保存すること
    • 追加されていく

非機能要件

  • コマンドでゲームを開始できる
    • コマンドでできる方が簡単だから
  • プラグインを導入すればSpigotを使っていればどのサーバーでも導入できる
  • 複数のプレイヤーが同時に実行しても動作すること(?)
  • ゲーム中のエリア内のブロックは何があっても破壊されない
    • ゲームモードの変更をしなくてもプラグインで制御できればというもの
  • ゲーム中のオプションでプレイヤーの強さ、敵の種類をある程度コントロールできる
  • 敵の出現数が一定数を超えたときにゲームが重たくならないようにする。
    • マシンのスペックは人によって違うので考慮する
    • 敵の数を制御する
    • 超えたら前に出現したものから消える

  • プログラムへの変更を加えずに、時間やスコアの項目などの設定値をある程度変更できる
    • お客さんはプログラムをいじることができない前提
    • プログラムをいじるのではなく、プログラム内の設定などのできることのモードを作っておく

機能要件

  • 体力や空腹ゲージは最大化されること
    • コマンドを実行したら、体力と空腹値に20を設定する
  • 敵を倒すと点数が手に入ること
    EntityのSpawnの仕組みを使って敵を出現させる
    Entityが倒れたときのイベントを使って、点数を設定する。
  • 時間制限ができること
    • スケジューラーを使って、一定周期で敵を出現させる。
    • 一定時間が経過したらその敵を出現させる処理を停止させる。
    • マルチプレイヤーでも対応できるように、プレイヤーごとにゲーム時間を保持すること。
    • 実行時に毎回ゲーム時間が設定されること。
  • スコア(合計点数)ボードが表示できること
    • sendTitleを使って画面いっぱいにスコア情報を表示する。
    • スコアを表示したら、プレイヤーのスコアを初期化すること。
  • 敵の種類によって手に入る点数が異なること
    • 敵を倒す時点での点数計算のところで、倒した敵によって処理を分岐させて、点数を変動させる。
  • 時間制限が来たらエリア内の敵は消滅すること
    • プレイヤーの周囲にいるエンティティーの情報を取得してきて、そのエンティティーを削除する。
    • 削除対象のエンティティーは、出現させた敵に限定すること。
    • 前提条件として、室内で行うことを想定しているので、敵を削除すれば問題は発生しない想定。

最終コード

全体としてコードは、

「EnemyDownCommand」

package plugin.enemydown.command;

import java.net.http.WebSocket.Listener;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import java.util.SplittableRandom;
import org.bukkit.Bukkit;
import org.bukkit.Location;
import org.bukkit.Material;
import org.bukkit.World;
import org.bukkit.command.CommandSender;
import org.bukkit.entity.Entity;
import org.bukkit.entity.EntityType;
import org.bukkit.entity.LivingEntity;
import org.bukkit.entity.Player;
import org.bukkit.event.EventHandler;
import org.bukkit.event.entity.EntityDeathEvent;
import org.bukkit.inventory.ItemStack;
import org.bukkit.inventory.PlayerInventory;
import plugin.enemydown.Main;
import plugin.enemydown.data.PlayerScore;

/**
 * 制限時間内にランダムで出現する敵を倒して、スコアを獲得するゲームを起動するコマンドです。
 * スコアは敵によって変わり、倒せた敵の合計によってスコアが変動します。
 * 結果はプレイヤー名、点数、日時などで保存されます。
 */
public class EnemyDownCommand extends BaseCommand implements Listener, org.bukkit.event.Listener {

  public static final int GAME_TIME = 20;

  private Main main;
private List<PlayerScore> playerScoreList = new ArrayList<>();
private List<Entity>spawnEntityList = new ArrayList<>();

  public EnemyDownCommand(Main main) {
    this.main = main;
  }

  @Override
  public boolean onExecutePlayerCommand(Player player) {
    PlayerScore nowPlayerScore = getPlyerScore(player);

    initPlayerStatus(player);

    gamePlay(player, nowPlayerScore);
    return true;
  }

  @Override
  public boolean onExecuteNPCCommand(CommandSender sender) {
    return false;
  }

  @EventHandler
  public void onEnemyDeath(EntityDeathEvent e){
    LivingEntity enemy = e.getEntity();
    Player player = enemy.getKiller();
    if (Objects.isNull(player) || playerScoreList.isEmpty()) {
      return;
    }

    for(PlayerScore playerScore : playerScoreList){
      if (playerScore.getPlayerName().equals(player.getName())){
        int point = switch (enemy.getType()) {
          case ZOMBIE -> 10;
          case SKELETON, HUSK -> 20;
          default -> 0;
        };

        playerScore.setScore(playerScore.getScore() + point);
        player.sendMessage("敵を倒した! 現在のスコアは" + playerScore.getScore() + "点");
      }
    }
  }

  /**
   * 現在実行しているプレイヤーのスコア情報を取得する。
   *
   * @param player コマンドを実行したプレイヤー
   * @return 現在実行しているプレイヤー情報
   */
  private PlayerScore getPlyerScore(Player player) {
    PlayerScore playerScore = new PlayerScore(player.getName());
    if(playerScoreList.isEmpty()){
      playerScore = addNewPlayer(player);
    }else {
      playerScore  = playerScoreList.stream()
          .findFirst()
          .map(PS -> PS.getPlayerName().equals(player.getName())
          ? PS
          : addNewPlayer(player)).orElse(playerScore);
    }
    playerScore.setGameTime(GAME_TIME);
    playerScore.setScore(0);
    return playerScore;
  }

  /**
   * 新規のプレイヤー情報をリストに追加します。
   *
   * @param player コマンドを実行したプレイヤー
   * @return 新規プレイヤー
   */
  private PlayerScore addNewPlayer(Player player) {
    PlayerScore newPlayer = new PlayerScore(player.getName());
    playerScoreList.add(newPlayer);
    return newPlayer;
  }

  /**
   * ゲームを始める前にプレイヤーの状態を設定する。
   * 体力と空腹度を最大にして、装備はネザライト一式になる。
   *
   * @param player コマンドを実行したプレイヤー
   */
  private void initPlayerStatus(Player player) {

    player.setHealth(20);
    player.setFoodLevel(20);

    PlayerInventory inventory = player.getInventory();
    inventory.setHelmet(new ItemStack(Material.NETHERITE_HELMET));
    inventory.setChestplate(new ItemStack(Material.NETHERITE_CHESTPLATE));
    inventory.setLeggings(new ItemStack(Material.NETHERITE_LEGGINGS));
    inventory.setBoots(new ItemStack(Material.NETHERITE_BOOTS));
    inventory.setItemInMainHand(new ItemStack(Material.NETHERITE_SWORD));
  }

  /**
   * ゲームを実行します。規定の時間内に敵を倒すとスコアが加算されます。合計スコアを時間経過後に表示します。
   *
   * @param player コマンドを実行したプレイヤー。
   * @param nowPlayerScore  プレイヤースコア情報。
   */
  private void gamePlay(Player player, PlayerScore nowPlayerScore) {
    Bukkit.getScheduler().runTaskTimer(main,Runnable->{
      if (nowPlayerScore.getGameTime() <= 0){
        Runnable.cancel();

        player.sendTitle("ゲームが終了しました。",
            nowPlayerScore.getPlayerName() + " 合計 " + nowPlayerScore.getScore() + "点",
            0,60,0);

        spawnEntityList.forEach(Entity::remove);
        return;
      }
      Entity spawnEntity = player.getWorld().spawnEntity(getEnemySpawnlocation(player), getEnemy());
      spawnEntityList.add(spawnEntity);
      nowPlayerScore.setGameTime(nowPlayerScore.getGameTime() - 5);
    },0,5*20);
  }

  /**
   *敵の出現場所を取得します。
   * 出現エリアはX軸とZ軸は、自分の一からプラス、ランダムで−10〜9の値がsettingされます。
   * y軸はプレイヤーと同じ位置になります。
   *
   * @param player コマンドを実行したプレイヤー
   */

  private Location getEnemySpawnlocation(Player player) {
    Location playerLocation = player.getLocation();
    int randomX= new SplittableRandom().nextInt(20) - 10;
    int randomZ= new SplittableRandom().nextInt(20) - 10;

    double x = playerLocation.getX()+randomX;
    double y = playerLocation.getY();
    double z = playerLocation.getZ()+randomZ;

    return new Location(player.getWorld(), x, y, z);
  }

  /**
   * ランダムで敵を抽出して、その結果の敵を取得します。
   * <p>
   * return 敵
   */
  private EntityType getEnemy() {
    List<EntityType> enemyList =  List.of(EntityType.ZOMBIE, EntityType.SKELETON, EntityType.HUSK);
    int random= new SplittableRandom().nextInt(enemyList.size());
    return enemyList.get(random);
  }
}
よかったらシェアしてね!
  • URLをコピーしました!
  • URLをコピーしました!

コメント

コメントする

CAPTCHA


目次