ゲームを作りながら学ぶ Java実践⑧ 

マインクラフト ゲーム開発

  * 要件の中には、検証してみないとできるかわからない要件も含む

設計

機能要件

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

非機能要件

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

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

機能要件

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

敵の種類によって加算する点数を変更

実装

まず、スコアの計算をしているのは、

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

    for(PlayerScore playerScore : playerScoreList){
      if (playerScore.getPlayerName().equals(player.getName())){
        playerScore.setScore(playerScore.getScore() + 10);
        player.sendMessage("敵を倒した! 現在のスコアは" + playerScore.getScore() + "点");
      }
    }
  }

の部分なので、「playerScore.setScore(playerScore.getScore() + 10);」の「+10」の部分を変更していくことになる。
(毎回10点ではなくなる。)

そこで、一旦変数に受けて変数の値を受けるようにする。

「10」のところにカーソルをあてて、右クリック「リファクタリング」→「変数の導入」で

「i」と出たところを「point」に変更。

そこで、まず「int point = 0;」として、最初は0店としておく

ここで、何を倒したかがほしいので、「Player player = e.getEntity().getKiller();」の「e.getEntity()」は、よく使うようになるので、ここにカーソルをあて「リファクタリング」→「変数の導入」で変数を「enemy」とする。

    LivingEntity enemy = e.getEntity();
    Player player = enemy.getKiller();
    if (Objects.isNull(player) || playerScoreList.isEmpty()) {
      return;
    }

そこで、分岐をしていく

「enemy.getType()」で敵の種類がとれてくる。

「enemy」の種類は、下の方にあるメソッドで定義していたのでそれ「EntityType.ZOMBIE」をコピーしてきて

        int point = 0;
        if (EntityType.ZOMBIE.equals(enemy.getType())){
          point = 10;
        }

として、「もし、entityのタイプがZOMBIE」だったら10点取する。

次に

else if (EntityType.SKELETON.equals(enemy.getType())){
          point = 20;
        }

とき記述し、「もし、entityのタイプがSKELETON」だったら、20点とする。

ここで敵をもう一体くらい増やすために、下のメソッドに敵を追加して、

  /**
   * ランダムで敵を抽出して、その結果の敵を取得します。
   * <p>
   * @return 敵
   */
  private EntityType getEnemy() {
    List<EntityType> enemyList =  List.of(EntityType.ZOMBIE, EntityType.SKELETON, EntityType.WITCH);
    int random= new SplittableRandom().nextInt(3);
    return enemyList.get(random);
  }
}

としておく。

ここで「extInt(3)」に変更するのを忘れないようする!!

しかし、毎回変えるのは、忘れることもあるので、「(3)」のところを変更して、

int random= new SplittableRandom().nextInt(enemyList.size());

として、毎回変えないで良いようにしておく。

ここでは、末尾の「 ) 数が一つ少ないが、エラーが出ていないのでそのまま進む。

そして、スコアのところに戻って三体目のentityを登録するために

      if (playerScore.getPlayerName().equals(player.getName())){
        int point = 0;
        if (EntityType.ZOMBIE.equals(enemy.getType())){
          point = 10;
        }else if (EntityType.SKELETON.equals(enemy.getType())){
          point = 20;
        }else if (EntityType.WITCH.equals(enemy.getType())){
          point = 20;
        }

としておく。

ここで、「if」や「else」が多すぎるので、「if (EntityType.ZOMBIE.equals(enemy.getType())){」のところの「if」にカーソルをあて、左に出てきた電球マークのようなものをクリックし、「ifをswitchで置換」をクリックし、

        int point = switch (enemy.getType()) {
          case ZOMBIE -> 10;
          case SKELETON -> 20;
          case WITCH -> 20;
          default -> 0;
        };

としておく(switch構文)

「default -> 0;」初期値は「0」という意味。
こうしておくことにより、敵が増えたときも書きやすくなる。

ここで、「case WITCH -> 20;」の「20」の部分が黄色い波線になっているが、そこにカーソルをあてると
「マージ」ということができるので、そうするとおなじ点数のものが簡略化されて

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

こうなり、更にシンプルになる。

ここで、実証!!

成功!!!

しか〜し!!

「WHICH」は強すぎます。

毒のダメージを与えられて何回も死にました。

よって、私は「WHICH」ではなく、「HUSK」に変更!!

そこで再度「reload」して試してみるが、何回しても「WHICH」が出てきます。

原因!!

下のメソッドを「WHICH」から「HUSK」に変更していませんでした。

そこで、メソッドも変更して実行したら、成功しました。

最終的にコードはこうなりました。

「EnemyDownCommand」クラス

import plugin.enemydown.data.PlayerScore;

public class EnemyDownCommand implements CommandExecutor, Listener, org.bukkit.event.Listener {

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

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

  @Override
  public boolean onCommand(CommandSender sender, Command command, String label, String[] args) {
    if(sender instanceof Player player){
     PlayerScore nowPlayer = getPlyerScore(player);

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

      initPlayerStatus(player);

      Bukkit.getScheduler().runTaskTimer(main,Runnable->{
        if (nowPlayer.getGameTime() <= 0){
          Runnable.cancel();
          player.sendTitle("ゲームが終了しました。",
              nowPlayer.getPlayerName() + " 合計 " + nowPlayer.getScore() + "点",
              0,30,0);
          nowPlayer.setScore(0);
          return;
        }
        world.spawnEntity(getEnemySpawnlocation(player, world), getEnemy());
        nowPlayer.setGameTime(nowPlayer.getGameTime() - 5);
      },0,5*20);


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

  /**
   * 新規のプレイヤー情報をリストに追加します。
   *
   * @param player コマンドを実行したプレイヤー
   * @return 新規プレイヤー
   */
  private PlayerScore addNewPlayer(Player player) {
    PlayerScore newPlayer = new PlayerScore();
    newPlayer.setPlayerName(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));
  }

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

  private Location getEnemySpawnlocation(Player player, World world) {
    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(world, 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


目次