リファクタリング②

StreamAPIを駆使したりしながら、ループ、条件分岐を減らす

ワールド情報の変数定義の簡略化

まず、最初のリファクタリングとして、

    //前提条件
    World world = player.getWorld();

の部分に着目する。

ここのフィールド上に定義するのは、この先何回も呼び出されるものを定義しておくものだが、ここでの「world」情報は、プレイヤーの周りの情報を取得するための一箇所しか使われておらず、変数でわざわざ持っておく必要がなく無駄に行数を使っていることになるので、インライン化してしまうほうが良いので、

変数を定義するとは、様々なプログラミングの場所で使われることが前提なので、他の人が仮にこのプログラムを見たときに、どこで、「world」情報が使われているのか複数あると思って探してしまい、不思議がってしまう恐れがある。
ルールに従った読みやすいコードにできるだけするようにすることが大切。
「なんのためにそうしたのか?」ということが、読み解けるようにする必要がある。

まず、

return new Location(world, x, y, z);

この部分は、

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

とすることができるので、(書き方は違うが、ここでは同じ意味となる。

そうすると

private Location getEnemySpawnlocation(Player player, World world) 

の部分の「World world」がいらなくなるので、削除する。

すると、

 world.spawnEntity(getEnemySpawnlocation(player), getEnemy());

の部分にあった引数が一個削除されるようになる。「world」がなくなっている。

そこで、「World world = player.getWorld();」の左側の「world」の部分を右クリックし、「リファクタリング」→「変数のインライン化」とすると上記のコードが

player.getWorld().spawnEntity(getEnemySpawnlocation(player), getEnemy());

こう変化する。
(今あった変数でとっていたワールド情報は消える、)

名前付けしての退避対応

つぎに、煩雑になっている

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);

の部分をリファクタリングしていく(ちょっと、煩雑になっているため退避しておく)

この部分を指定し、「リファクタリング」→「メソッド抽出」

名前を「gamePlay」(任意)としておく。

    gamePlay(player, nowPlayer);
    return true;
  }

  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);
  }

そこで、作成したメソッドは、下の方に持っていって、名前や解説をジャバドッグ書いておく。

「ジャバドッグ」

  /**
   * ゲームを実行します。規定の時間内に敵を倒すとスコアが加算されます。合計スコアを時間経過後に表示します。
   *
   * @param player コマンドを実行したプレイヤー。
   * @param nowPlayer  プレイヤースコア情報。
   */

そして、c自体に名前をつけるために

「public class EnemyDownCommand extends BaseCommand implements Listener, org.bukkit.event.Listener {」
の上にジャバドッグで名前をつける。

「ジャバドッグ」

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

コメントは、様々なところに書いて説明書きをすることができるが、プログラムが見にくくなるので適度な数にしておくほうが良い!!
書きすぎ注意!

「@Override」のところには、コメントは書かない。
コマンド名のところにカーソルをあてると解説が出てくるため。

書いたコードにカーソルをあてると、それは何なのかは、あらかた解説をしてくれる機能がある。

最終的にこうなって

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

    nowPlayer.setGameTime(20);

    initPlayerStatus(player);

    gamePlay(player, nowPlayer);
    return true;
  }

解説 : 「getPlyerScore(player)」プレイヤースコアをとってきて
     「nowPlayer.setGameTime(20);」プレイヤーのスコア情報にガーム時間を設定して
     「initPlayerStatus(player);」プレイヤーの初期化を行って
     「gamePlay(player, nowPlayer);」ゲームプレイをする。

時間定数の定義変更

「nowPlayer.setGameTime(20);」は、決まった値なので「毎回設定する必要があるのか?」という疑問が生まれてくるので、

20のところにカーソルををあて、「リファクタリング」→「定数の導入」をすると

 nowPlayer.setGameTime(GAME_TIME);

となり、上の部分に

public static final int GAME_TIME = 20;

が形成さあれる。

スコア情報コードの簡略化(IDEの力を借りる)

  /**
   * 現在実行しているプレイヤーのスコア情報を取得する。
   *
   * @param player コマンドを実行したプレイヤー
   * @return 現在実行しているプレイヤー情報
   */
  private PlayerScore getPlyerScore(Player player) {
    if(playerScoreList.isEmpty()){
      return addNewPlayer(player);
    }else {
      for(PlayerScore playerScore : playerScoreList){
        if(!playerScore.getPlayerName().equals(player.getName())){
          return addNewPlayer(player);
        }else{
          return playerScore;
        }
      }
    }
    return null;
  }

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

まずは、下の部分の「if」のところにカーソルをあて、左の電球マークをクリックし、「elseを?に変換」をクリックすると

        return !playerScore.getPlayerName().equals(player.getName())
            ? addNewPlayer(player)
            : playerScore;

となる。(三項演算子)

解説 : 「!playerScore.getPlayerName().equals(player.getName())」(条件文)プレイヤーの名前をとる。
     「? addNewPlayer(player)」一致しなかったら新規プレイヤーなのでリストに加える。
     「: playerScore;」一致した場合は、既存のプレイヤーで返す。

しかし、最初に「!」反転しているのでわかりにくいから、「!」のところにカーソルをあて、左の電球マークをクリックし、「?を反転」をクリックし

        return playerScore.getPlayerName().equals(player.getName()) 
            ? playerScore
            : addNewPlayer(player);

としておく。(意味としては、上記「解説」の反対となる)

ストリームAPI

次に、「for」文の簡略化として「for」のところにカーソルをあて、左の電球のところをクリックし、「ストリーム ‘findFirst()’ を含むループを折りたたむ」をクリック(ここは、動画と表記が違います。)

      return playerScoreList.stream().findFirst().map(playerScore 
          -> playerScore.getPlayerName().equals(player.getName())
              ? playerScore
              : addNewPlayer(player)).orElse(null);

となり(ストリーム化している : 「ストリームAPI」 )
これは、既存のコードの一番下にあった、「null」も条件の中に組み込んでくれるようになる。

解説 : -> playerScore.getPlayerName().equals(player.getName())
     ? playerScore
     : addNewPlayer(player)).orElse(null);
     の三項式に合致する最初の値を返す。

「GAME_TIME」の取り出し

まず、プレイヤーは、新規で返す場合もあれば、既存で返す場合もあるので、共通的にいれるものが必要になる。

そこで、受けることのできる、一次変数を定義する。

「PlayerScore playerScore = new PlayerScore();」

を入力し、

  private PlayerScore getPlyerScore(Player player) {
    PlayerScore playerScore = new PlayerScore();
    if(playerScoreList.isEmpty()){
      return addNewPlayer(player);
    }else {
      return playerScoreList.stream().findFirst().map(playerScore
          -> playerScore.getPlayerName().equals(player.getName())
              ? playerScore
              : addNewPlayer(player)).orElse(null);
    }
  }

こうしておいて、「return」するのではなく,「addNewPlayer」を一次変数で受けるために、「リファクタリング」→「変数の導入」で

    if(playerScoreList.isEmpty()){
      playerScore = addNewPlayer(player);
      return playerScore;

とする。(変数を撮るとき複数か?ここだけか?きかれるので、ここだけにする。)

そして、下の「playerScoreList」にも変数の導入をして、

      playerScore  = playerScoreList.stream().findFirst().map(playerScore
          -> playerScore.getPlayerName().equals(player.getName())
          ? playerScore
          : addNewPlayer(player)).orElse(null);
      return playerScore;

とし、上の部分と同じなので、「if」のところにカーソルをあて、「共通部分を抽出」をクリックすると

  private PlayerScore getPlyerScore(Player player) {
    PlayerScore playerScore = new PlayerScore();
    if(playerScoreList.isEmpty()){
      playerScore = addNewPlayer(player);
    }else {
      playerScore  = playerScoreList.stream().findFirst().map(playerScore
          -> playerScore.getPlayerName().equals(player.getName())
          ? playerScore
          : addNewPlayer(player)).orElse(null);
    }
    return playerScore;
  }

となるが、エラーが出ているので、エラーが出ているところの名前を変更する必要ンがあるので、そこを右クリックして「リファクタリング」→「名前の変更」で「PS(PlayerScore)」とする。

 private PlayerScore getPlyerScore(Player player) {
    PlayerScore playerScore = new PlayerScore();
    if(playerScoreList.isEmpty()){
      playerScore = addNewPlayer(player);
    }else {
      playerScore  = playerScoreList.stream().findFirst().map(PS
          -> PS.getPlayerName().equals(player.getName())
          ? PS
          : addNewPlayer(player)).orElse(null);
    }
    return playerScore;
  }

ここで「mull」を返すのは気持ち悪いのし、プレーヤーは必ず存在していないということはないので、「null」を「playerScore」に変更しておく。

ここで、最後に「return」する前に「GAME_TIME」をセットしておくため「playerScore.setGameTime(GAME_TIME);」を挿入する。

  private PlayerScore getPlyerScore(Player player) {
    PlayerScore playerScore = new PlayerScore();
    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);
    return playerScore;
  }

となる。(プレイヤーのスコアをとるときは、ゲーム時間も設定されてから返すとなる。)

これにより、のフィールドに入力していた、「GAME_TIME」が必要なくなる。

新規プレイヤーセットの簡素化

  private PlayerScore addNewPlayer(Player player) {
    PlayerScore newPlayer = new PlayerScore();
    newPlayer.setPlayerName(player.getName());
    playerScoreList.add(newPlayer);
    return newPlayer;
  }

これは、必ずここでプレイヤーネームを取得して、プレーヤーネームを設定している。

ということは、「PlayerScore」を作るときは絶対プライヤーネームが必要になるということなので、

  private PlayerScore addNewPlayer(Player player) {
    PlayerScore newPlayer = new PlayerScore(player.getName());
    playerScoreList.add(newPlayer);
    return newPlayer;
  }

としておいたほうが良くて、ここでエラーが出るので、そこにカーソルをあて「コンストラクターの作成」とでるので、

  public PlayerScore(String playerName) {
    this.playerName = playerName;
  }
}

とする。(必ずプレイヤースコアを設定するときは、プレイヤーネームを撮るようになる。)

すると、上部の「PlayerScore playerScore = new PlayerScore(player.getName());」の部分にエラーが出るので

  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);
    return playerScore;
  }

とする。

ここで、コードが正しいかどうか実証!!

成功!!!

全体的にコードは、こうなりました。

「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<>();

  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);
    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 nowPlayer  プレイヤースコア情報。
   */
  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);
  }

  /**
   *敵の出現場所を取得します。
   * 出現エリアは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


目次