Java テキストRPG(戦闘シーンのみ)を再作成する2~設計からやり直す~

イントロfダクション

前回作成したものは、ちゃんと設計していませんでした。とうかアイディアだけが先走り、収拾がつかなくなった次第です。

今回作成するのもの

TesxtRPGをだんだんレベルアップしていく方向で作成しようと考えています。イメージとしては市価のような感じでレベルアップしていこうと考えています。

Lv1: 戦闘シーンのみ
Lv2: レベルアップ・ステータスの表示を追加
Lv3: タイトル表示~ストーリー(シーン)の表示を追加

  1. タイトル表示
  2. ストーリーを開始(シーン切り替えを実装)
  3. 登場する敵はラスボスのみ
    Lv4: ミニストーリーをRPGにする。
  4. イベント発生から、シーンの切り替えの実装
  5. 装備の変更を実装する
    Lv5: ミニストーリーをRPGにする2
  6. 一つの冒険を実装する、ゴールは隣町の近所に住む魔王を倒す
  7. エンディングを実装する

そして、今回実装するのはLv1です。
ここでは、戦闘シーンに必要な登場人物(モンスターを含む)を実装することに重きを置きます。

成果物

<Eclipseでの実行>

<コマンドプロンプトでの実行>

Lv1:TextRpgゲーム

まずは、戦闘シーンをイメージして、フローチャートを作成します。

<作成したフローチャート>

実際には、テストケースを作成しながらこのフローチャートを作成しました。

大まかに、下のように行う処理のイメージを図にする感じです。

フローチャート

ゲームの開始から、終了までの流れを図にしたものです。

作成するときに考えるのは「どのように、ゲームを進めるか?」です。具体的には以下のように考えました。

フローチャートを作る

<ステップ1>
テキストRPG(コンソール表示)で戦闘シーンをどのように進めるか?
まず、頭に浮かんだのは某有名ゲームソフトです。
戦闘シーンの開始時には「XXXが現れた!」と表示されます。ならばこちらもそのように。。。
そのほか、戦闘シーンのイメージはこのゲームソフトです。
そして、初期表示を行い、入力受付。。。と処理が続きます。

<ステップ2>
入力後どのような動きを行うか?今回は戦闘シーンのみなので例外的に「bye」と入力したらアプリケーションを終了するように実装します。
他は、入力⇒画面の更新(HPが減ったり、モンスターがダメージを受けたり)を行います。

クラス図を作る

実際に作成したクラス図は下のものです。赤くなっているものは今回使用しないもの(クラス)です。

フローチャートと同様にクラス図も作成します。
フローチャートを参考に、登場する人物(クラス)を用意します。
今回のプログラム起動方法としては、LWJGLを参考にしてやりました。つまり、①メインメソッドのクラス(GameMain)から②今回作成するゲームを実行するクラス(TextRpgGameEngin)を呼び出し実行します。
ここでのポイントとしては、②のクラスはThreadクラスを継承しマルチすれっと処理に対応できるようにすることです。

そうすることで、ゲーム(アプリケーション)を起動するスレッドと、例えばDBサーバーを起動するスレッドをマルチスレッドで起動したり、ほかにもアイディアがあればそのような実装ができます。

テストケースから作成する

フローチャートとクラス図からそれぞれのクラスに対するテストケースを作成していきます。

テストケースを実行することを考えると、最終的に呼び出されるクラス、「XXXUtilsクラス」がテストケースを実装するのに楽だろうと思われるので、ここら辺から攻めていきました。

フローチャートでも初めにある『「XXXが現れた!」を表示する』という処理から実装しました。あとはフローチャートの処理順にテストケースを作っていきました。初めからフローチャートの順序か。。。

作成したもの

Githubにアップロード(PUSH)しています。

Lv2の実装前に

現状で、実装するのに調査が必要だったものや、てこずったものに関して記載したいと思います。

JUnitでのコンソール出力

今回作成したゲーム(アプリ)は画面表示と言いながらコンソール出力になります。なので、テスト時には出力内容を確認する必要があり、出力先を変更し確認するために取得する必要がありました。

<解決策>
具体的には下のコードになるのですが、手順としては以下のようになります。

  1. 標準出力の出力先を変更する。
  2. 出力した文字列を取得する。

列挙するとシンプルな感じですが、実装もシンプルでした。
使用したの部品(Java API)配下の者です。

  • org.junit.platform.commons.logging.Logger
  • java.io.ByteArrayOutputStream
  • java.io.PrintStream

上記の部品を使用して、標準出力をPrintStreameに変更、PrintStreamにはByteArrayOutputStreamを渡して作成=出力先がByteArrayOutputStreamになる。

テスト実行時の出力する文言に関しては、org.junit.platform.commons.logging.Loggerへ出力しました。

/** ログ出力 */
private static final Logger LOG = LoggerFactory.getLogger(FirstJankenMainTest.class);
/** 標準出力確認 */
private static final ByteArrayOutputStream console = new ByteArrayOutputStream();

テストクラスの起動ですが、これは@BeforeClassを使用しました。

  1. テストクラスの起動時にテスト対象クラスをインスタンス化
    @BeforeClass
    public static void initClass() {
    target = new ConsoleUtils();
    // 標準出力の出力先を変更する
    System.setOut(new PrintStream(console));
    }
  2. 各テストケースの実行前に出力した文字列をリセット
    /**
    * テストの準備
    */
    @Before
    public void init() {
    // フィールド変数
    console.reset();
    }
    
  3. テストを実行した結果をassertEqualsで比較

    /**
    * ステータス表示のテスト:2桁
    */
    @Test
    public void testPrintBattleStatus1() {
    // プレーヤーは一人
    Player player = new Player("test");
    // 名前の文字数は、全角は4文字、半角8文字まで
    player.setLevel(10);
    player.setHP(20);
    player.setMP(10);
    target.printBattleStatus(player);
    LOG.info(() -> SEP + console.toString());
    
    String expect = "**** test ****\r\n" +
            "  Lv: 10     *\r\n" +
            "  HP: 20     *\r\n" +
            "  MP: 10     *\r\n" +
            "**************" + SEP + SEP;
    assertEquals(expect, console.toString());
    }
    

こんな風に実装しました。

コマンドの取得

Playerクラスで実装したメソッドの中に「たたかう」などのコマンド実行時に起動するメソッドがあります。そして、コンストラクタなど、コマンド実行以外で使用するメソッドも定義していあります。
「はて?どうやってコマンド用のメソッドのみを取得しようか?」と考えたところ「アノテーションがあ~るじゃないですか!」とひらめきました。

早速実装しました。参考サイトはこちらです。

  1. 自作のアノテーションを作成。
    /**
    * 行動を表すアノテーション
    * 。
    * @author 実装者の名前
    */
    @Target(ElementType.METHOD)
    @Retention(RetentionPolicy.RUNTIME)
    public @interface Command {
    /** 表示順 */
    public int index();
    /** 実行選択肢 */
    public String commandName();
    }
  2. コマンド用のメソッドにアノテーションをつける
    /**
    * 攻撃コマンド。
    *
    * @return 武器の攻撃力
    */
    @Command(index=1, commandName="たたかう")
    public int attack() {
    return mainWepon.getOffence();
    }
    
  3. アノテーションをつけたメソッドを取得。
    ※コンソール出力するめそっどですが、取得した内容を出力しています。

    /**
    * プレーヤーの行動選択肢を一覧表示する。
    *
    * @param player
    * @return コマンドマップ(indexでソート済み)
    */
    public Map<Integer, String> printCommandList(Player player) {
    Method[] mess = player.getClass().getDeclaredMethods();
    // 並び替え機能付きのMAP
    Map<Integer, String> commandMap = new TreeMap<>();
    for(Method mes : mess) {
        Command ano = mes.getAnnotation(Command.class);
        if (ano != null) {
            // マップにインデックス(順番)とコマンド名を登録
            commandMap.put(ano.index(), ano.commandName());
        }
    }
    // 並び替え後に表示
    commandMap.forEach((Integer index, String value) -> {
        System.out.println(index + ": " + value);
    });
    return commandMap;
    }

こんな感じで実装しました。

コンソールのクリア

[こちらの記事にも記載]()しましたが、以下のコードでコマンドの実行がでるようです。

Rumtime.getRuntime().exec(コマンド);

しかし、IOExceptionが出力され。。。調べてみるとWindowsでは下のようなコードで実行するとよいみたいです。

new ProcessBuilder("cmd", "/c", "cls").inheritIO().start().waitFor();

Eclipseで実行してみたところ、文字化けた文字が出力されるだけで、意味なかったのですが、JARファイルを作成し、コマンドプロンプトで実行してみたところ問題なく動きました。

今後は、Lv2の実装を進めていきたいと思います。
そして、今後はマルチスレッド実装がちゃんと動く必要があるので、サンプルを作成、実行してみました。

でわでわ。。。

関連ページ一覧

EclipseセットアップWindows版

Eclipse セットアップ

  1. Java Install Eclipse〜開発ツールのインストール〜
  2. TensorFlow C++環境〜EclipseCDTをインストール〜
  3. Setup OpenGL with JavaJOGLを使う準備 for Eclipse
  4. Eclipse Meven 開発手順〜プロジェクトの作成〜
  5. Java OpenCV 環境セットアップ(on Mac)
  6. Eclipse SceneBuilderを追加する
  7. JavaFX SceneBuilder EclipseSceneBuilder連携~

Java Basic一覧

  1. Java Basic Level 1 〜Hello Java〜