ゲームを作りながら学ぶ Java実践 リファクタリング④「変数の考え方も追記」

ゲーム終了後にスコアが入らないようにする

リファクタリング

  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() + "点");
      }
    }
  }

これでは、ゲームが終了したあとでも、敵を倒したらスコアが入ってしまうようになってしまう。
(ゲームが終わったあとの事後処理がメンテナンスできていない。)
(ゲーム以外のところに影響を与えてしまうのは、やはり、まずい!!)

まずは、

    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() + "点");
      }
    }

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

ここでやっていることは、
「playerScoreList」の中にある「playerScore」とenemyを倒したプレイヤーの名前が一致したら、スコアを加算して表示を出しなさいとしている。

ここでは、ループさせたいわけではなく、リストにあるプレイヤーと一致するかどうかがわかれば良い!

「for」文でループさせているため、対象を見つけてもリストにあるもの全体を探し続けてしまうので、それは無駄なので、対象を見つけたら、その対象だけ処理をすると変えていく。

まず、

    playerScoreList.stream()
        .filter(p -> p.getPlayerName().equals(player.getName()))
        .findFirst();

と記述し、

「stream」= フィルターの役目をして、リストの中からほしいものを拾い上げることを用意にしたいときに使う。

フィルターを掛けて、名前の一致しているものをとってきて、同じ名前のプレイヤーがいることはないので、最初に引っかかった名前のプレイヤーをとってくると指定する。

そこで「.findFirst();」を右クリック「リファクタリング」→「変数の導入」で

    Optional<PlayerScore> first = playerScoreList.stream()
        .filter(p -> p.getPlayerName().equals(player.getName()))
        .findFirst();

とする。

ここで、変数を導入するのはなぜかを再確認!!
私の理解としては、定義した値が変わった場合でも、その値に名前をつけておくことで、その名前「ここでは、first」を使用した箇所においては、すべて修正したら修正した値になるようにしておくこと。

「optional」=「null」を回避するものだが、「null」が入る可能性がありますといっているようなもの

しかし、ここでは、変数の受けを消して

   playerScoreList.stream()
        .filter(p -> p.getPlayerName().equals(player.getName()))
        .findFirst();

としておく。

「ifPresent」= 値が入っていたときだけ処理を行う。

そこで

    playerScoreList.stream()
        .filter(p -> p.getPlayerName().equals(player.getName()))
        .findFirst()
        .ifPresent(p -> {
          int point = switch (enemy.getType()) {
            case ZOMBIE -> 10;
            case SKELETON, HUSK -> 20;
            default -> 0;
          };

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

とし、「playerScore」の部分がエラーと出るので、ここは、名前を「p」としたのでエラーのところをすべて「p」に変える。

playerScore.setScore(playerScore.getScore() + point);
player.sendMessage(“敵を倒した! 現在のスコアは” + playerScore.getScore() + “点”);
を龍力するのは、「 });」の1個上の段ということを気をつける。

そこで

    for(PlayerScore playerScore : playerScoreList){
      if (playerScore.getPlayerName().equals(player.getName())){



      }
    }

が要らなくなるので消去。

    playerScoreList.stream()
        .filter(p -> p.getPlayerName().equals(player.getName()))
        .findFirst()
        .ifPresent(p -> {
          int point = switch (enemy.getType()) {
            case ZOMBIE -> 10;
            case SKELETON, HUSK -> 20;
            default -> 0;
          };

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

となります。

バグの修正

そこで

    boolean isSpawnEntity = spawnEntityList.stream()
        .anyMatch(entity -> entity.equals(enemy));

を記述する。

「.anyMatch()」= entityListの中にマッチするものが一つでもあれば、「true」か「false」(boolean)を返してくれる。

ここで、次に「if (Objects.isNull(player) || playerScoreList.isEmpty())」を書き換えていく。
(これは、コマンドを実行したプレイヤーがいない場合となっている)

ここで倒した敵が、出現させた的かどうかを判断出せるようにする。

「!isSpawnEntity」=「isSpawnEntity」でなかった場合。

    if (Objects.isNull(player) || !isSpawnEntity) {
      return;
    }

となり、「isSpawnEntity」でなかった場合は、スコアに加算させず「return(何もしない)」とする。

ここで式を簡略化するために

「spawnEntityList.stream()
.anyMatch(entity -> entity.equals(enemy))」で切り取って、

「isSpawnEntity」のところに上書きし、いらないところは消去して

  public void onEnemyDeath(EntityDeathEvent e){
    LivingEntity enemy = e.getEntity();
    Player player = enemy.getKiller();

    if (Objects.isNull(player) || !spawnEntityList.stream()
        .anyMatch(entity -> entity.equals(enemy))) {
      return;
    }

とする。
あとは、重複してると出るので、黄色いところにカーソルをあて置換をすると

    if (Objects.isNull(player) || spawnEntityList.stream()
        .noneMatch(entity -> entity.equals(enemy))) {
      return;
    }

となる。

もう一つ治すところがあり、

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

の部分の「cforEach(Entity::remove);」は、このままでは、「Entity」は、消しているが、「spawnEntityList」の中身は消していないので、リストの中には残った状態となっているので、このリストも消すようにしなければいけない。

そこで、「spawnEntityList = new ArrayList<>();」をしたに挿入し、「新しい空のリストにする」ということにする。

そして、毒状態もクリアにするために、

  private void initPlayerStatus(Player player) {

    player.removePotionEffect(PotionEffectType.POISON);
    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));
  }

「player.removePotionEffect(PotionEffectType.POISON);」を追加。

ここで、実証!!
(状態が戻るかどうかも確認するために敵を「HUSK」→「WHICH」に戻しておく。)

成功!!!

全体的にコードとしては、

「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.Optional;
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 org.bukkit.potion.PotionEffectType;
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) || spawnEntityList.stream()
        .noneMatch(entity -> entity.equals(enemy))) {
      return;
    }

    playerScoreList.stream()
        .filter(p -> p.getPlayerName().equals(player.getName()))
        .findFirst()
        .ifPresent(p -> {
          int point = switch (enemy.getType()) {
            case ZOMBIE -> 10;
            case SKELETON, WITCH -> 20;
            default -> 0;
          };

          p.setScore(p.getScore() + point);
          player.sendMessage("敵を倒した! 現在のスコアは" + p.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.removePotionEffect(PotionEffectType.POISON);
    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);
        spawnEntityList = new ArrayList<>();
        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.WITCH);
    int random= new SplittableRandom().nextInt(enemyList.size());
    return enemyList.get(random);
  }
}

となりました。

よかったらシェアしてね!
  • URLをコピーしました!
  • URLをコピーしました!

コメント

コメントする

CAPTCHA


目次