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

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

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

設計

機能要件

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

非機能要件

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

機能要件

  • 体力や空腹ゲージは最大化されること
    • コマンドを実行したら、体力と空腹値に20を設定する
  • 敵を倒すと点数が手に入ること
    EntityのSpawnの仕組みを使って敵を出現させる
    Entityが倒れたときのイベントを使って、点数を設定する。

点数加算のマルチプレイヤー対応

今のままでは、プレイヤーが上書きされ続けるので、仮に同じワールドにいるプレイヤーで最後にコマンドを実行したプレイヤーのみにしか、点数の加算が実行されないようになってしまっている。

実装

まず、「enemydown」のファイルの中に「新規」→「パッケージ」で新たに出てきたファイル名に「.data」と追加し「data」フォルダを作り、その中に「PlayerScore」というクラスを追加する。

そして、そのクラスの中になんのクラスなのかを解説するジャバドッグを書き込んでおく

ここでは、

package plugin.enemydown.data;

/**
 * EnemyDownのゲームを実行する際のスコア情報を扱うオブジェクト。
 * プレイヤー名、合計点数、日時などの情報を持つ。
 */

public class PlayerScore {

}

を入力しておく。

そこで「EnenyDownCommand」クラスから

  private Player player;
  private int score;

をコピーしてきてペーストする

package plugin.enemydown.data;

import org.bukkit.entity.Player;

/**
 * EnemyDownのゲームを実行する際のスコア情報を扱うオブジェクト。
 * プレイヤー名、合計点数、日時などの情報を持つ。
 */

public class PlayerScore {
  private Player player;
  private int score;

}

ここでは、playerとしているが、とにかくplayerの名前がほしいということになるので

「private Player player;」のところを

private String playerName;

と変更する。
Stringを使うと文字列を扱うことになるため、名前の文字を取ってくることができるようになるという意味かと???

そして、「EnenyDownCommand」に戻り

  private Player player;
  private int score;

の部分を

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

に書き換える。
「= new ArrayList<>()」をつけることによって、空の状態を作り、「Null」にならないようにしておく。

ここでは、書き換えてしまったので、情報がなくなりコンパイルエラーが起こっているので、それを解消していく。

そこで「if(sender instanceof Player player){」の下に
PlayerScore playerScore = new PlayerScore();

を追加。(リストに入れるオブジェクトを作る)
新しくできたスコアに名前をつけるみたいな感じ?(正しいかわからないが・・・)

ここで、次に進もうとすると、他のクラスで定義したものが取ってこれないとなる。
それは、「PlayerScore」クラスに定義したものが「private」で定義しているからによるものなので
対策として「PlayerScore」クラスの中で、「右クリック」→「生成」→「getterとsetter」をクリックし
出てきた候補を両方選択して「ok」を」クリックすると

 public String getPlayerName() {
    return playerName;
  }

  public void setPlayerName(String playerName) {
    this.playerName = playerName;
  }

  public int getScore() {
    return score;
  }

  public void setScore(int score) {
    this.score = score;
  }

が出てくる。
(これを出すことによって「puburic」なので、ほかのくらすでもあつかえるようになる。

しかし、二行のコードに多数ののコードを記述することになるため、鬱陶しくなるため、教材に用意されている

	compileOnly 'org.projectlombok:lombok:1.18.26'
    annotationProcessor 'org.projectlombok:lombok:1.18.26'

    testCompileOnly 'org.projectlombok:lombok:1.18.26'
    testAnnotationProcessor 'org.projectlombok:lombok:1.18.26'

を左のサイドバーの階層にある「build.gradle」の真ん中に出てくる

dependencies {
compileOnly "org.spigotmc:spigot-api:1.21-R0.1-SNAPSHOT"
}

の中にペーストする。

dependencies {
compileOnly "org.spigotmc:spigot-api:1.21-R0.1-SNAPSHOT"

    compileOnly 'org.projectlombok:lombok:1.18.26'
    annotationProcessor 'org.projectlombok:lombok:1.18.26'

    testCompileOnly 'org.projectlombok:lombok:1.18.26'
    testAnnotationProcessor 'org.projectlombok:lombok:1.18.26'
}

として、

すると、右上に動悸するかどうかの表示が出るのでクリックする。

そうすると「lombok」というものが使えるようになる。
これをするとコードを簡単に整理できるようになる。

そこで、「PlayerScore」クラスの中に、

@Getter
@Setter

を入力することによって、先程のコードを自動生成してくれるようになる。

package plugin.enemydown.data;

import lombok.Getter;
import lombok.Setter;
import org.bukkit.entity.Player;

/**
 * EnemyDownのゲームを実行する際のスコア情報を扱うオブジェクト。
 * プレイヤー名、合計点数、日時などの情報を持つ。
 */
@Getter
@Setter
public class PlayerScore {

  private String playerName;
  private int score;

}

となる。

そこで、再度、「EnenyDownCommand」クラスに戻り、

ここでエラー発生!!
「PlayerScore playerScore = new PlayerScore();」の下に「playerScore.setPlayerName(player.getName());」を打ち込もうとすると、「.set」を打ち込んだ時点で、エラーになり候補が出てこなくなりました。

何度打ち込んでも色々消しても、解消されまでんでしたが、「build.gradle」の中にペーストした、コードのバージョンが気になり、検索して

dependencies {
compileOnly "org.spigotmc:spigot-api:1.21-R0.1-SNAPSHOT"

    compileOnly 'org.projectlombok:lombok:1.18.30'
    annotationProcessor 'org.projectlombok:lombok:1.18.30'

    testCompileOnly 'org.projectlombok:lombok:1.18.30'
    testAnnotationProcessor 'org.projectlombok:lombok:1.18.30'
}

としたら、プラグインの導入を推薦されるので導入し、再度入力してみると、

できた!!!

それが、正解かわからないが、ここでは、そのまま進むことにする。

続いて、その下に「this.player = player;」をけして、「playerScoreList.add(playerScore);」を入力。
(スコアーをスコアーリストに入れなさい??ということ??)

そこで

    if(sender instanceof Player player){
      PlayerScore playerScore = new PlayerScore();
      playerScore.setPlayerName(player.getName());
      playerScoreList.add(playerScore);

となりますが、このままでは、ずっとリストが増え続けるような指示になってしまう。

それを解決するために

まず、これの上に

      if(playerScoreList.isEmpty()){
        
      }

(もし、「playerScoreList」が空だった場合に)

      if(playerScoreList.isEmpty()){
        PlayerScore playerScore = new PlayerScore();
        playerScore.setPlayerName(player.getName());
        playerScoreList.add(playerScore);
      }

とする。
(一件追加される)

そして、その下に

        else{for(PlayerScore playerScore : playerScoreList){
          
        }
       }

を入力するが、上の部分を同じ名前になってしまうので

「PlayerScore playerScore = new PlayerScore();」を「PlayerScore newPlayer = new PlayerScore();」に変更。

ここで変更するときは、「playerScore」を右クリックで名前を変更しないと、関連するところが代わってくれないので、必ず、右クリックしてから「名前の変更」をクリックするようにする。

ここで、これを使ってループを回す。

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

をついきして「!」をつけて、「一件目、二件目と名前が一致しなかった場合」という条件をつける。

もう一回、その人を追加するという指示を出すので

    if(sender instanceof Player player){
      if(playerScoreList.isEmpty()){
        PlayerScore newPlayer = new PlayerScore();
        newPlayer.setPlayerName(player.getName());
        playerScoreList.add(newPlayer);
      }else {
        for(PlayerScore playerScore : playerScoreList){
          if(!playerScore.getPlayerName().equals(player.getName())){
            PlayerScore newPlayer = new PlayerScore();
            newPlayer.setPlayerName(player.getName());
            playerScoreList.add(newPlayer);
          }
        }
      }

となる。
(これで新規のユーザーしかリストに入っていかないようになる)

ここで、上下に同じ処理ができているので、その部分をメソッド抽出して簡素化しておく
(片方の同じところを全部指定して右クリック「リファクタリング」→「メソッドの抽出」)

そして名前を「addNewPlayer(player)」として

同じものがありますよとのアドバイスが出るので、指示通り「置換」をすると

    if(sender instanceof Player player){
      if(playerScoreList.isEmpty()){
        addNewPlayer(player);
      }else {
        for(PlayerScore playerScore : playerScoreList){
          if(!playerScore.getPlayerName().equals(player.getName())){
            addNewPlayer(player);
          }
        }
      }

となる。

そこで、下にメソッドができているので、ジャバドッグを入力して説明書きを追加しておく。

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

これは、メソッドをまとめるため下に移動!!(やはりね!!)

ここからは、エラーが出ているところを修正していく。

まず、「(Objects.isNull(this.player)){」は、もともと、フィールドに持っていた「player」を取ってきていたが、
「private List<PlayerScore> playerScoreList = new ArrayList<>();」に代わっているので、

if (playerScoreList.isEmpty())

となる。

ここで、「if」が連続しているので改善できるため、「if」の部分にカーソルをあて、左の電球のようなもののリストに出てくる「連続する’if’ステートメントのマージ」をクリックすると

 if (Objects.isNull(player) || playerScoreList.isEmpty()) {
      return;
    }

となります。(すっきり!!!)
「||」=「または」という意味。

次に

    if (this.player.getName().equals(player.getName())){
      score += 10;
      player.sendMessage("敵を倒した! 現在のスコアは" + score + "点");
    }

の部分にもエラーが出ているので修正をしていく。

ここでも、上でしたように、「for文」で解決していく

まず、上部から

for(PlayerScore playerScore : playerScoreList){
      
    }

をコピーしてきて

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

として

取ってきたプレイヤーリストのプレイヤーが、コマンドを実行したプレイヤーの名前リストと一致する場合ということ??

そして、

playerScore.setScore(playerScore.getScore() + 10);

すでにコマンドを実行したプレーヤーのスコアを設定する(.setScore)が、それは、今設定されているスコアに対して10点足しなさいという指示。

そして、メッセージを出すために

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

とする。

しかし「score」のところは、エラーが出ているので、

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

とする。

そこで、最後の不必要な部分は、消去しときます。

ここで、実証!!

成功!!!

全体的なコードとしては、
「EnenyDownCommand」

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.Location;
import org.bukkit.Material;
import org.bukkit.World;
import org.bukkit.command.Command;
import org.bukkit.command.CommandExecutor;
import org.bukkit.command.CommandSender;
import org.bukkit.entity.EntityType;
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.data.PlayerScore;

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

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

  @Override
  public boolean onCommand(CommandSender sender, Command command, String label, String[] args) {
    if(sender instanceof Player player){
      if(playerScoreList.isEmpty()){
        addNewPlayer(player);
      }else {
        for(PlayerScore playerScore : playerScoreList){
          if(!playerScore.getPlayerName().equals(player.getName())){
            addNewPlayer(player);
          }
        }
      }

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

      initPlayerStatus(player);

      world.spawnEntity(getEnemySpawnlocation(player, world), getEnemy());
    }
      return false;
  }

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

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

「PlayerScore」

package plugin.enemydown.data;


import lombok.Getter;
import lombok.Setter;
import org.bukkit.entity.Player;

/**
 * EnemyDownのゲームを実行する際のスコア情報を扱うオブジェクト。
 * プレイヤー名、合計点数、日時などの情報を持つ。
 */
@Getter
@Setter
public class PlayerScore {

  private String playerName;
  private int score;

}

となりました。

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

コメント

コメントする

CAPTCHA


目次