Java Swing テキストRPGを作る~ストーリーを進めるところまで実装~

イントロダクション

前回テキストRPGの作成を開始して、多少時間がかかりましたが、なんとか処理フローの基本的なところができました。
下の画像は、エメラルドグリーン?の部分です。

仕様などはWikiに記載しています。

色々と考えたのですが、コマンドプロンプトでの実装の時は、「入力」するしか選択肢がなかったけど、今回のSwing実装では、自由にできる。
なので、選択はポップアップで実装することにしました。選択肢は、Story_XXX.xmlファイルで定義、Selectクラスに反映し次のシーンへ移動するという形をとりました。

テキストRPGの作成

元々「文字表現のみでRPGの戦闘シーンを作成する」というのが目標だったのですが、作ってみたら「拡張してみたい!」と思うようになり。。。
下のように作ってみました。

<Ver0.5>

<Ver0.5Jar出力後>

<Ver8.5>

こんな感じで作ってきたら「コマンドプロンプトは使わない方が良いのでわ?」と思い、「ならばJava Swingでやるか!」と至った次第です。

プログラムの実装

ここまで、プログラムの設計ができてない状態だったので、ここにまとめたいと思います。
クラス図としては以下のようにします。(BlueJを使用)

そして、ストーリーを表示するところまで実装できました。

  1. TextRPGMainクラスはJFrameを継承して作成、メインメソッドを持っている
  2. ConfigGeneratorクラスでXMLファイルから設定クラスを生成する
  3. メインメソッドで画面を構築、XMLから取得したストーリーを表示
  4. 同様にXMLで設定した選択肢を選択し、次のシーンへ移動

以下のような形でInputSelectorをインスタンス化し、XMLから取得した選択肢をセット、ポップアップ表示して
その選択に応じて次のシーンを表示するという形の実装を行いました。

<TextRPGMain>

// はじめのシーン番号
int sceneNo = 0;
story = config.getScenes().get(sceneNo);
// 1. 文章の表示
textarea.setText(story.getStory());
// 2. 入力を受ける
InputSelector pop = new InputSelector(story, textarea, this);
pop.show(this, xPos - 30, yPos + 220);

<InputSelector>

/**
 * 選択肢のポップアップを作成する。
 * @param selects 配列の要素一つが選択肢一つに当たる
 */
public InputSelector(Story story, RpgTextArea textarea, TextRPGMain main) {
    super(story.getId());
    addSeparator();
    List<Select> selects = story.getSelects();
    for(Select sel : selects) {
        SelectMenu act = new SelectMenu(sel, textarea, main);
        JMenuItem menu = new JMenuItem(act);
        add(menu);
        addSeparator();
     }
}

<SelectMenu>

public void actionPerformed(ActionEvent event) {
   String selectedStr =  event.getActionCommand();

   try {
       Story story = ConfigGenerator.getInstance()
                       .getScenes().get(select.getNextScene());
       JMenuItem item = (JMenuItem) event.getSource();           
       Point pos = item.getComponent().getLocation();
       ((InputSelector) item.getParent()).setVisible(false);
       //item.finalize();
       textarea.setText(story.getStory());
       InputSelector popup = new InputSelector(story, this.textarea, main);
       popup.show(main, (int)(pos.getX() + 350.0), (int)(pos.getY() + 500.0));
   } catch (Exception e) {
       e.printStackTrace();
       System.exit(-1);
   }
}

実行結果としては下のような感じです。

処理概要

大まかな処理としては、次のような処理を行っています。

1.画面の表示

メインメソッドで、設定ファイルのロード(読み込み)を行い、run()を呼び出し、ゲームの実行を行っています。
ちなみにコンストラクタでは、何もしていません。デフォルトコンストラクタを定義しているだけです。

メインメソッドの処理

下のように、ConfigGeneratorクラスは、シングルトン実装なので、getInstance()でインスタンスを取得しています。
この時に、各設定ファイル、XMLファイルを読み込んでます。
そして、run()メソッドを起動してゲームを開始します。

public static void main(String[] args) {
    TextRPGMain main = new TextRPGMain();
    try {
        config = ConfigGenerator.getInstance();
        main.run("Text RPG");
    } catch(RpgException e) {
        e.printStackTrace();
    }
}

run()メソッドの処理

はじめに、この「TextRPGMain」クラスはJFrameを継承しているので、JFrameクラスとして動きます。
なので、画面の表示位置(setBounds()で指定)や、ラベル、テキストエリアの設定を行っています。
「playGame」はフィールド変数で、boolean型です。このフラグがTUREの間はゲームを続ける、という形で実装する予定でしたが、状況が変わり。。。
現状では、ごみコードになっています。後ほど削除する予定です。

そして、ポイントは、「Scene」クラスを取得してから、InputSelectorでポップアップを表示して処理が終わっているところです。
実は、このあとInputSelector内で生成する。SelectMenuクラスで再帰的に処理を行う形の実装をしました。

public void run(String title) throws RpgException {
    setTitle(title);
    Dimension windowSize = Toolkit.getDefaultToolkit().getScreenSize();
    int xPos = (int) windowSize.getWidth() / 4;
    int yPos = (int) windowSize.getHeight() / 5;
    setBounds(xPos, yPos, xPos * 2, yPos * 3);
    setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);

    TitleLabel titleLabel = new TitleLabel("Text RPG", windowSize);
    RpgTextArea textarea = new RpgTextArea(windowSize);

    JPanel titlePanel = new JPanel();
    titlePanel.add(titleLabel);

    JPanel textPanel = new JPanel();
    textPanel.add(textarea);

    Container contentPane = getContentPane();
    contentPane.add(titlePanel, BorderLayout.NORTH);
    contentPane.add(textPanel, BorderLayout.CENTER);

    setVisible(true);

    // ゲーム中
    playGame = true;

    Scene story = null;
    // はじめのシーン番号
    int sceneNo = 0;
    story = config.getScenes().get(sceneNo);
    // 1. 文章の表示
    textarea.setText(story.getStory());

    try {
        // 2. 入力を受ける
        InputSelector pop = new InputSelector(story, textarea, this);
        pop.show(this, xPos - 30, yPos + 220);
    } catch (RpgException e) {
        e.printStackTrace();
    }
}

InputSelectorの処理

上記のrun()メソッドで呼び出されるこのクラスのコンストラクタから見ていきます。
コンストラクタの引数にあるSceneクラスには、読み込んだXMLファイルから生成した情報がセットされています。
なので、このクラスから<select>タグに設定されている、選択肢を取得、ポップアップ表示する。というわけです。

/**
 * 選択肢のポップアップを作成する。
 * @param selects 配列の要素一つが選択肢一つに当たる
 */
public InputSelector(Scene story, RpgTextArea textarea, TextRPGMain main) throws RpgException {
    super(story.getId());
    addSeparator();
    List<Select> selects = story.getSelects();
    if (selects == null) {
        createSingleSelect(story, textarea, main);
        return;
    }
    for(Select sel : selects) {
        SelectMenu act = new SelectMenu(sel, textarea, main);
        JMenuItem menu = new JMenuItem(act);
        add(menu);
        addSeparator();
     }
}

private void createSingleSelect(Scene story, RpgTextArea textarea, TextRPGMain main) {
        Select sel = new Select(story.getNextScene(), "すすむ");
        SelectMenu act = new SelectMenu(sel, textarea, main);
        JMenuItem menu = new JMenuItem(act);
        add(menu);
}

<XML>

    <selects>
        <mongon>ニューゲーム</mongon>
        <nextScene>1</nextScene>
    </selects>
    <selects>
        <mongon>コンチニュー</mongon>
        <nextScene>-1</nextScene>
    </selects>

そして、選択された後、SelectクラスにあるactionPerformed()メソッドが動きます。
この時に、処理を再帰的に呼び出します。

  1. 設定クラスからシーンを取得
  2. テキストエリアに文字列の表示
  3. InputSelectorクラスの生成

メインメソッドにある。run()メソッドと同じ処理を呼び出しています。今後は、この処理をメソッドでラップしてし、同じメソッドを呼び出せばよい形を作るか?でもクラスを無駄に増やすことになりそう。。。と検討中です。

<SelectMenu>

public void actionPerformed(ActionEvent event) {
   String selectedStr =  event.getActionCommand();

   try {
       int sceneNo = select.getNextScene();
       Scene story = ConfigGenerator.getInstance()
                       .getScenes().get(sceneNo);
       if (story == null) {
           throw new RpgException("対応するストーリ番号がありまっせん。: " + sceneNo);
       }
       JMenuItem item = (JMenuItem) event.getSource();
       Point pos = item.getComponent().getLocation();
       ((InputSelector) item.getParent()).setVisible(false);

       // storyが未定義はPATH指定
       String st = story.getStory();
       if (st == null || "".equals(st)) {
           st = XMLUtil.loadText(story.getPath(), story.isCenter());
       }
       textarea.setText(story.getStory());
       InputSelector popup = new InputSelector(story, this.textarea, main);
       popup.show(main, (int)(pos.getX() + 350.0), (int)(pos.getY() + 500.0));
   } catch (Exception e) {
       e.printStackTrace();
       System.exit(-1);
   }
}

投稿者:

takunoji

音響、イベント会場設営業界からIT業界へ転身。現在はJava屋としてサラリーマンをやっている。自称ガテン系プログラマー(笑) Javaプログラミングを布教したい、ラスパイとJavaの相性が良いことに気が付く。 Spring framework, Struts, Seaser, Hibernate, Playframework, JavaEE6, JavaEE7などの現場経験あり。 SQL, VBA, PL/SQL, コマンドプロント, Shellなどもやります。

コメントを残す