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

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

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

設計

機能要件

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

非機能要件

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

機能要件

  • 体力や空腹ゲージは最大化されること
    • コマンドを実行したら、体力と空腹値に20を設定する
  • 敵を倒すと点数が手に入ること
    EntityのSpawnの仕組みを使って敵を出現させる
    Entityが倒れたときのイベントを使って、点数を設定する。
  • 時間制限ができること
    • スケジューラーを使って、一定周期で敵を出現させる。
    • 一定時間が経過したらその敵を出現させる処理を停止させる。

時間制限の実装

一から作るのは、ベテラン経験者でもかなり骨が折れる。
そこで、フレームワーク(spigot)が用意してくれているものをつかう。

実装

敵を出現させるコードとしては、「EnenyDownCommand」クラスの中の「world.spawnEntity(getEnemySpawnlocation(player, world), getEnemy());」となるので、
その上に記述していく。

まず、

Bukkit.getScheduler().runTaskTimer();

つぎに、「Main」クラスに移り、

「javaプラグイン」を「EnemyDownCommand」に引き渡すために、

「EnemyDownCommand enemyDownCommand = new EnemyDownCommand();」の引数に「this」を入れる。

するとエラーが出るので、そこにカーソルをあて、出てきた「コンストラクターの作成」をクリックすると
「EnemyDownCommand」クラスに自動的に

  public EnemyDownCommand(Main main) {
    
  }

が生成される。
こうしておくことで、「EnemyDownCommand」を受けるので、上の「private List<PlayerScore> playerScoreList = new ArrayList<>();」の部分の上に

private  Main main;

を追加し、次に

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

ここにゲッターせったー

とすることによって、「EnemyDownCommand」クラスが「pulugin」の情報を持って「Main」と同じことができるようになるので、これを、先に記述した「Bukkit.getScheduler().runTaskTimer();」の引数に渡してやれば良くなる。

    Bukkit.getScheduler().runTaskTimer(main, Runnable -> {

      });

よて、上記のように記述し、処理を証入するために下に記述してある。
「world.spawnEntity(getEnemySpawnlocation(player, world), getEnemy());」を挿入する。

「ー>」=ラムダ式

「Runnable -> 」の「{}」は省略することもできるが、できるのは処理が一行のときだけ。

      Bukkit.getScheduler().runTaskTimer(main, Runnable -> {
        world.spawnEntity(getEnemySpawnlocation(player, world), getEnemy());
      });

ここで、引数が必要になるので、
「delay」=時間をすらすのか?(ここでは、ずらさず0)
「period」= どのくらいの間隔で実行するか?(ここでは、5秒とする)

マインクラフトでは20チックで一秒となる。

なので、5秒にしたいときは「5 * 20」となる。

そこで、コードとしては、

このあと、問題発生!!

このあと、何度も「delay」を入力するが、候補が見つからない!!
何度も何度も最初からやったり、色々したが解決しない!!
そこで、様々調べた結果!!

解決策!!

そのまま数字を打てばよいのでした!!(数字を打てば「delay」は自然と出てきました)
情けない(T_T)

      Bukkit.getScheduler().runTaskTimer(main,Runnable->{
        world.spawnEntity(getEnemySpawnlocation(player, world), getEnemy());
      },0,5*20);

これで、5秒おきに敵を出現させることができるようになった。

しかし、これでは、永遠に敵が出現してしまう。

そこで、「Rummable」に対してキャンセルをかけるようにする。

まず、判断をするための時間を持つようにするが、これは、必ずフィールドに持つようにする。

そこで

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

とする。
そして

      Bukkit.getScheduler().runTaskTimer(main,Runnable->{
        if (gameTime <= 0){
          Runnable.cancel();
          return;
        }
        world.spawnEntity(getEnemySpawnlocation(player, world), getEnemy());
      },0,5*20);

こうすることで、「gameTime」がお以下のときは、止まる(Runnable.cancel();)という指示となる

      Bukkit.getScheduler().runTaskTimer(main,Runnable->{
        if (gameTime <= 0){
          Runnable.cancel();
          return;
        }
        world.spawnEntity(getEnemySpawnlocation(player, world), getEnemy());
        gameTime -= 5;
      },0,5*20);

として5秒ごとに敵が出現するので、5秒ごと減らしていく。

このままでは、時間が経過してしまって止まったままになってしまうので、実行したら時間が新しくなるようにする必要がある。
また、時間はプレイヤー単位である必要もある。(これは、後ほど)

そこで、「World world = player.getWorld();」の上に、

gameTime = 20;

を挿入。

これでできるが、ゲームが終わったどうかはっきりとはわからないので、メッセージを表示するようにする。

    Bukkit.getScheduler().runTaskTimer(main,Runnable->{
        if (gameTime <= 0){
          Runnable.cancel();
          player.sendMessage("ゲームが終了しました。");
          return;
        }

これで、実証!!

どんな機能を持っているか?ということを調べることにより、便利に実装できるようになるので、「こんなことをしたい!!」といった感じで、検索して使えるものを探すようにする。

成功!!!

全体としてコードは、こうなりました。

「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.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.Main;
import plugin.enemydown.data.PlayerScore;

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

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

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

  @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);
          }
        }
      }
      gameTime = 20;
      //前提条件
      World world = player.getWorld();

      initPlayerStatus(player);

      Bukkit.getScheduler().runTaskTimer(main,Runnable->{
        if (gameTime <= 0){
          Runnable.cancel();
          player.sendMessage("ゲームが終了しました。");
          return;
        }
        world.spawnEntity(getEnemySpawnlocation(player, world), getEnemy());
        gameTime -= 5;
      },0,5*20);


    }
      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);
  }
}
よかったらシェアしてね!
  • URLをコピーしました!
  • URLをコピーしました!

コメント

コメントする

CAPTCHA


目次