マインクラフト ゲーム開発
* 要件の中には、検証してみないとできるかわからない要件も含む
設計
機能要件
- 体力や空腹ゲージは最大化されること
- 前提条件を合わせるため
- 前提条件を合わせるため
- 一定のエリア内でしか敵は発生しないこと
- エリア外で発生しても倒せない
- エリア外で発生しても倒せない
- 敵の種類はランダムであること
- 装備や武器はプレイするたびにおなじになること
- 今の装備を取得して保存しといて、ゲームが始まったら指定の装備にして、終わったらもとの装備に戻す等
- 対象のプレイヤーのインベントリの中身を直接書き換えることで実現する。
- コマンド実行時に差し替えて、最終的にはコマンド実行前の装備の状況を保存しておき、ゲーム終了後戻したい
- 時間制限を設定できること
- スコア(合計点数)ボードが表示できること
- 画面上にバーンと出るようにする
- スコアボードにするなど
- 敵を倒すと点数が手に入ること
- 敵の種類によって手に入る点数が異なること
- ゲーム性を高めるため
- ゲーム性をしてどうあるべきか
- 時間制限が来たらエリア内の敵は消滅すること
- 終わったことをはっきりさせるため
- 終わったことをはっきりさせるため
- 時間制限が来たら合計の点数が保存されること
- できればデータベースなんかに保存して取り出せるようにしておきたい
- できればデータベースなんかに保存して取り出せるようにしておきたい
- 保存する情報はスコアとプレイヤー名と日時
- 新しい情報が入った場合は上書きではなく、すべて保存すること
- 追加されていく
- 追加されていく
非機能要件
- コマンドでゲームを開始できる
- コマンドでできる方が簡単だから
- コマンドでできる方が簡単だから
- プラグインを導入すればSpigotを使っていればどのサーバーでも導入できる
- 複数のプレイヤーが同時に実行しても動作すること(?)
- ゲーム中のエリア内のブロックは何があっても破壊されない
- ゲームモードの変更をしなくてもプラグインで制御できればというもの
- ゲームモードの変更をしなくてもプラグインで制御できればというもの
- ゲーム中のオプションでプレイヤーの強さ、敵の種類をある程度コントロールできる
- 敵の出現数が一定数を超えたときにゲームが重たくならないようにする。
- マシンのスペックは人によって違うので考慮する
- 敵の数を制御する
- 超えたら前に出現したものから消える
- プログラムへの変更を加えずに、時間やスコアの項目などの設定値をある程度変更できる
- お客さんはプログラムをいじることができない前提
- プログラムをいじるのではなく、プログラム内の設定などのできることのモードを作っておく
機能要件
- 体力や空腹ゲージは最大化されること
- コマンドを実行したら、体力と空腹値に20を設定する
- コマンドを実行したら、体力と空腹値に20を設定する
- 敵を倒すと点数が手に入ること
EntityのSpawnの仕組みを使って敵を出現させる
Entityが倒れたときのイベントを使って、点数を設定する。 - 時間制限ができること
- スケジューラーを使って、一定周期で敵を出現させる。
- 一定時間が経過したらその敵を出現させる処理を停止させる。
- マルチプレイヤーでも対応できるように、プレイヤーごとにゲーム時間を保持すること。
- 実行時に毎回ゲーム時間が設定されること。
- スコア(合計点数)ボードが表示できること
- sendTitleを使って画面いっぱいにスコア情報を表示する。
- スコアを表示したら、プレイヤーのスコアを初期化すること。
スコアの表示
実装
ーマルチプレイヤー対応ー
まず、プレイヤースコアの対して時間をもたせるようにする。
そこで「PlayerScore」クラスに
private int gameTime;
を追記する。
これにより「EnemyDownCommand」クラスの
private Main main;
private List<PlayerScore> playerScoreList = new ArrayList<>();
private int gameTime =20;
の「private int gameTime =20;」は消去してしまう。(しかし、エラーが出てしまうがそのまま進む)
そこで、「PlayerScore」をとってこなければいけなくなるので、
if(playerScoreList.isEmpty()){
addNewPlayer(player);
}else {
for(PlayerScore playerScore : playerScoreList){
if(!playerScore.getPlayerName().equals(player.getName())){
addNewPlayer(player);
}
}
}
の部分をメソッド抽出する。
全部指定して、「リファクタリング」→「メソッド抽出」でメソッド抽出をして
名前を
getplyerScore(player);
とする。
そこで、何も返さないようになっているので、いま、抽出されたメソッドへ移動し
private void getplyerScore(Player player) {
if(playerScoreList.isEmpty()){
addNewPlayer(player);
}else {
for(PlayerScore playerScore : playerScoreList){
if(!playerScore.getPlayerName().equals(player.getName())){
addNewPlayer(player);
}
}
}
}
を
private PlayerScore getPlyerScore(Player player) {
if(playerScoreList.isEmpty()){
addNewPlayer(player);
}else {
for(PlayerScore playerScore : playerScoreList){
if(!playerScore.getPlayerName().equals(player.getName())){
addNewPlayer(player);
}
}
}
}
と変更する。
そこで、「addNewPlayer(player);」で新規でプレイヤーが増えた場合に追加するようにしているので、
下の方のプレイヤー情報を追加しているメソッドに移り
private void addNewPlayer(Player player) {
PlayerScore newPlayer = new PlayerScore();
newPlayer.setPlayerName(player.getName());
playerScoreList.add(newPlayer);
}
を
private PlayerScore addNewPlayer(Player player) {
PlayerScore newPlayer = new PlayerScore();
newPlayer.setPlayerName(player.getName());
playerScoreList.add(newPlayer);
}
と書き換えて、「PlayerScore」を返させるようにする。
そして、説明文として
@return 新規プレイヤー
を追加して記述しておく。
/**
* 新規のプレイヤー情報をリストに追加します。
*
* @param player コマンドを実行したプレイヤー
* @return 新規プレイヤー
*/
private PlayerScore addNewPlayer(Player player) {
PlayerScore newPlayer = new PlayerScore();
newPlayer.setPlayerName(player.getName());
playerScoreList.add(newPlayer);
return newPlayer;
}
でこうしておいて、「playerScoreList」に足したうえで、足したプレイヤー情報を返してあげる。
そこで、もとのメソッドに戻り「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);
}
}
}
}
とする。
ここで、前にコマンドを実行しているプレイヤーと名前が一致した場合を追記していく
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;
}
}
}
}
とする。
そして、この式あにてはまらない場合(基本的にはないが)を返してあげる。
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;
}
ここでジャバドッグを追加して説明文を残しておく
ここで
public boolean onCommand(CommandSender sender, Command command, String label, String[] args) {
if(sender instanceof Player player){
getPlyerScore(player);
の部分に移り、
「getPlyerScore(player);」の部分を「PlayerScore nowPlayer = getPlyerScore(player);」と書き換える。
ここで、「nowPlayer」の中に「gameTime」を設定するために
下の「gameTime = 20;」を「nowPlayer.setGameTime(20);」と書き換える。
そして、エラーが出ている
Bukkit.getScheduler().runTaskTimer(main,Runnable->{
if (gameTime <= 0){
Runnable.cancel();
player.sendMessage("ゲームが終了しました。");
return;
}
の部分の「gameTime 」の部分を「nowPlayer.getGameTime()」へと書き換える。
そして、その下の部分の「gameTime -= 5;」のエラーが出ている部分は、設定しなければいけないので
nowPlayer.setGameTime(nowPlayer.getGameTime() - 5);
と書き換える。
(今の時間から5秒減らしたのが再設定されるようになる)
ここまでが、マルチプレイヤー対応するためのものとなる。
ースコアの再設定ー
スコアを初期化するために(再設定するために)
player.sendMessage("ゲームが終了しました。");
return;
ここに
「nowPlayer.setScore(0);」を追記して
if (nowPlayer.getGameTime() <= 0){
Runnable.cancel();
player.sendMessage("ゲームが終了しました。");
nowPlayer.setScore(0);
return;
}
としておく。
ースコアの表示ー
player.sendMessage("ゲームが終了しました。");
の部分を
player.sendTitle("ゲームが終了しました。",
nowPlayer.getPlayerName() + " 合計 " + nowPlayer.getScore() + "点",
0,30,0);
に書き換える。
「fadeIn:0, stay:30, fadeOut: 0」は、どのくらい表示させておくかというもの。
ここで実証!!

成功!!!
最終的にコードは、こうなりました。
「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<>();
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){
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 コマンドを実行したプレイヤー
* @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);
int random= new SplittableRandom().nextInt(2);
return enemyList.get(random);
}
}
コメント