Java アプリケーションビルド github actions

イントロダクション

ノンプログラマが、文章でRPGを作成できるアプリケーションテキストRPGを作成しています。
現段階では一通り動くようになったので、いろんなストーリーを作成してプログラムが問題なく動くことを確認するためのテストを行う工程に入りました。

そのため次のことを行おうと考えております。

Githubから実行可能ファイルをダウンロードできるようにする。

Github Actions

作成したテキストRPGはMavenでビルドする形をとっています。なのでPOMファイルを使用してビルドする形ですが、これをGithubActionsで起動できるように設定したいところです。

操作手順

GithubActionsを使用してMavenビルドするワークフローを設定して、GithubPackagesに公開します。

  1. GithubActionsは下の部分をクリックするとアクセスできます。

  2. ワークフローの作成

  3. 推奨されているものを選択しました。

  4. ビルドを行う設定をYMLファイルで行い、コミットします。※デフォルト設定でよい

YMLファイルについて

参考にしたのは、さくらインターネットさんのサイトです。
そして、詳細を記述しているのはGithubのドキュメントです。

各項目の意味

今回作成したYMLファイル。「#」でコメントを記述します。
英語の部分は自動生成されたもので、日本語のものは自分が記述したものです。「name」「on」「jobs」が主要な項目のようです。

# This workflow will build a package using Maven and then publish it to GitHub packages when a release is created
# For more information see: https://github.com/actions/setup-java/blob/main/docs/advanced-usage.md#apache-maven-with-a-settings-path
# テストの実施は設定していない。今後必要になったら実施するように編集する必要がある。
# 【注意するポイント】
# PUSHしたときにビルドが走る。改修などは別ブランチを切ってやるようにする。
name: TextRPG Ver0.8

on:
  release:
    types: [created]

jobs:
  build:

    runs-on: ubuntu-latest
    permissions:
      contents: read
      packages: write

    steps:
    - uses: actions/checkout@v3
    - name: Set up JDK 11
      uses: actions/setup-java@v3
      with:
        java-version: '11'
        distribution: 'temurin'
        server-id: github # Value of the distributionManagement/repository/id field of the pom.xml
        settings-path: ${{ github.workspace }} # location for the settings.xml file

    - name: Build with Maven
      run: mvn -B package --file pom.xml

    - name: Publish to GitHub Packages Apache Maven
      run: mvn deploy -s $GITHUB_WORKSPACE/settings.xml
      env:
        GITHUB_TOKEN: ${{ github.token }}

GitHub アクションのワークフロー構文

Githubのドキュメントを参考にしています。

  • 「YAML を Y 分で学ぶ」も参考にするとよいらしいです。
  • ワークフロー ファイルは、.github/workflowsリポジトリのディレクトリに保存する必要があります。

name


ワークフローの名前。GitHub は、ワークフローの名前をリポジトリの [アクション] タブに表示します。を省略した場合name、GitHub はそれをリポジトリのルートからの相対ワークフロー ファイル パスに設定します。

run-name


ワークフローから生成されたワークフロー実行の名前。GitHub は、リポジトリの [アクション] タブのワークフロー実行のリストにワークフロー実行名を表示します。が省略されているか空白のみの場合run-name、実行名はワークフロー実行のイベント固有の情報に設定されます。たとえば、pushまたはpull_requestイベントによってトリガーされるワークフローの場合、コミット メッセージとして設定されます。

記述例

run-name: Deploy to ${{ inputs.deploy_target }} by @${{ github.actor }}

githubこの値には式を含めることができ、およびコンテキストを参照できますinputs。

on


ワークフローを自動的にトリガーするには、 を使用してonワークフローを実行できるイベントを定義します。使用可能なイベントのリストについては、「ワークフローをトリガーするイベント」を参照してください。

ワークフローをトリガーできる単一または複数のイベントを定義したり、タイム スケジュールを設定したりできます。特定のファイル、タグ、またはブランチの変更に対してのみワークフローの実行を制限することもできます。これらのオプションについては、次のセクションで説明します。

たとえば、次のワークフローは、pull_requestプル リクエスト ターゲティングのイベントが発生するたびに実行されます。

main( refs/heads/main)という名前のブランチ
mona/octocat( refs/heads/mona/octocat)という名前のブランチ
( )releases/のように名前が で始まるブランチreleases/10refs/heads/releases/10

指定のブランチを除くこともできるようです。

on:
  pull_request:
    # Sequence of patterns matched against refs/heads
    branches:    
      - main
      - 'mona/octocat'
      - 'releases/**'

MavenでJAR出力

POMファイルに以下の部分を追加する

    <build>
        <plugins>
            <!-- 実行可能jarファイル用のプラグイン -->
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-assembly-plugin</artifactId>
                <version>3.4.2</version>
                <configuration>
                    <finalName>test</finalName>
                    <descriptorRefs>
                        <!-- 依存するリソースをすべてjarに同梱する -->
                        <descriptorRef>jar-with-dependencies</descriptorRef>
                    </descriptorRefs>
                    <archive>
                        <manifest>
                            <!-- 起動するメインメソッドを指定する -->
                            <mainClass>jp.zenryoku.rpg.TextRpgMain</mainClass>
                        </manifest>
                    </archive>
                </configuration>
                <executions>
                    <execution>
                        <!-- idタグは任意の文字列であれば何でもよい -->
                        <id>make-assembly</id>
                        <phase>package</phase>
                        <goals>
                            <goal>single</goal>
                        </goals>
                    </execution>
                </executions>
            </plugin>
        </plugins>
    </build>

これで、以下の手順を実行するとJARファイルが出力される。

  1. IntelliJ IDEA で Maven プロジェクトを開きます (ファイル → 開く)。

  2. View → Tool Windows → Mavenに移動して、Maven ツール ウィンドウが表示されていることを確認します。

  3. ツリーでプロジェクトを展開し、ライフサイクルを展開して、パッケージをダブルクリックします。

  4. IntelliJ が Mavenpackageフェーズを実行し、下の別のウィンドウに出力が表示されます。

  5. JAR ファイルはtarget/your-app-1.0.jar!で入手できます。

出力されJARファイルは2種類ありますが、「jar-with-dependencies」が付いているほうが実行可能jarファイルです。

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);
   }
}

テキストRPG~JAXBを使ってXMLを読み取る~

JAXBを使ってXMLを読み取る

2023-02-26現在、テキストRPGを作成中です。
このゲームは、文字表現のみのRPGを作成できるアプリとして作成中です。
イメージとしては、ゲームブックをPC上で作成するようなものです。
以前作成したのは、制限が多く実行した後も見た目にカレントディレクトリが表示されたまま。。。など不適切なものが多かったので
javax.swingを使用して作り直している最中です。

XMLとは

「Extensible Markup Language」の略で、日本語では「拡張可能なマークアップ言語」

XMLファイルは、いろんな場所で使用されています。

  1. ScenBuilder(JavaFX用の画面作成ツール)

  2. SprintBoot: DI1コンテナの作成、MyBatisのようなORマッピング

  3. クラスの読み込みとXML出力

2,3の項目には作成した動画がありませんでした。。。

JAXBとは

AXBとは、JavaでXMLを扱うためのライブラリの名称である(参照先)

作成したクラスをXMLファイルの形で出力、XMLファイルからクラスを取得と双方向で操作が可能。

<使用するライブラリ一覧>※gradleは使ってないので記載しません。。。
java9以降では、ライブラリをインポートしないといけないものがあるようです。
JAXBのAPIをインポートするのも大事ですが、ランタイムも行う必要があります。一番下の「jaxb-runtime」がそれにあたります。

      <!-- JAXB -->
      <!-- https://mvnrepository.com/artifact/javax.xml.bind/jaxb-api -->
      <dependency>
          <groupId>javax.xml.bind</groupId>
          <artifactId>jaxb-api</artifactId>
          <version>2.3.1</version>
      </dependency>
      <!-- JAXB API only -->
      <dependency>
          <groupId>jakarta.xml.bind</groupId>
          <artifactId>jakarta.xml.bind-api</artifactId>
          <version>3.0.0</version>
      </dependency>
      <!-- JAXB RI, Jakarta XML Binding -->
      <dependency>
          <groupId>com.sun.xml.bind</groupId>
          <artifactId>jaxb-impl</artifactId>
          <version>3.0.0</version>
          <scope>runtime</scope>
      </dependency>
      <!-- https://mvnrepository.com/artifact/org.glassfish.jaxb/jaxb-runtime -->
      <dependency>
          <groupId>org.glassfish.jaxb</groupId>
          <artifactId>jaxb-runtime</artifactId>
          <version>2.3.6</version>
      </dependency>

出力するクラス(Worlds)

テキストRPGでの「世界」を保持するクラスWorldsクラスをJAXBで出力します。
世界クラスには、以下のようにフィールド変数が定義されています。そしてこれは、ユーザーがXMLファイルを定義するときに自由に記述できるものです。
つまり、ユーザーが世界クラスを生成する形をとっています。
下に記載しているのはWorldsクラスではなくWorldクラスです。

使用するクラス(アノテーション)

import javax.xml.bind.annotation.XmlRootElement;
@XmlRootElement( name="worlds")
/**
 * 世界を表すクラス。
 * 基本的にゲーム作成者の指定する、設定情報を保持する。
 */
@Data
public class World extends StoryConfig {
    /** img */
    private String imgUrl;
    /** 国 */
    List<Country> countries;
    /** 自然 */
    private Nature nature;
    /** 食物連鎖(説明、テキストファイルへのパス) */
    private String food_chain;
    /** 生物、Creture.xmlへのパス */
    private List<Creature> creatures;
    /** 地域 */
    private List<Region> regions;
    /** 魔法などの発動する仕組み、説明、テキストファイルへのパス */
    private String logic;
    /** 文明 */
    private List<Civilization> civilizations;
    /** 生活様式の説明、テキストファイルへのパス */
    private String life_style;
    /** 社会構造の説明、テキストファイルへのパス */
    private String social_structure;
    /** 組織の説明、テキストファイルへのパス */
    private String organization;
    /** 文化 */
    private List<Culture>  cultures;

    public World() {
        nature = new Nature();
        regions = new ArrayList<>();
        civilizations = new ArrayList<>();
        cultures = new ArrayList<>();
        creatures = new ArrayList<>();
        countries = new ArrayList<>();
    }
}

このクラスで、XMLで定義した内容「世界観」を保持します。
ゲームの最中で、この世界観を表示することができる形で実装したいと考えております。
※文字表現のみなので、そんなに難しくないと思っています。。。

出力したXMLファイル

上記のWorldクラスをリストにして持っているクラスWorldsクラスを出力しました。その結果です。

<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<worlds>
    <world>
        <description>世界の説明</description>
        <id>pppp</id>
        <name>テスト用の世界</name>
        <civilizations>
            <description>文明1の説明</description>
            <id>civil1</id>
            <name>文明1</name>
            <arts>文明1の芸術</arts>
            <character>文明1の文字</character>
            <structures>文明1の社会構造</structures>
            <technology>文明1の技術</technology>
        </civilizations>
        <civilizations>
            <description>文明2の説明</description>
            <id>civil2</id>
            <name>文明2</name>
            <arts>文明2の芸術</arts>
            <character>文明2の文字</character>
            <structures>文明2の社会構造</structures>
            <technology>文明2の技術</technology>
        </civilizations>
        <countries>
            <description>A国の説明</description>
            <id>country1</id>
            <name>A国</name>
        </countries>
        <countries>
            <description>B国の説明</description>
            <id>country2</id>
            <name>B国</name>
        </countries>
        <cultures>
            <description>文化1の説明</description>
            <id>culture1</id>
            <name>文化1</name>
            <habit>文化1の生活習慣</habit>
            <life_style>文化1の生活スタイル</life_style>
            <norm>文化1の規範</norm>
            <organization>文化1の組織</organization>
            <social_structure>文化1の社会構造</social_structure>
            <values>文化1の価値観</values>
            <view_of_world>文化1の世界観</view_of_world>
        </cultures>
        <nature>
            <description>自然の説明</description>
            <id>natureId</id>
            <name>自然名</name>
            <path></path>
            <climateList>
                <description>サンプル気候の説明</description>
                <id>D</id>
                <name>D気候</name>
            </climateList>
            <climateList>
                <description>サンプル気候の説明</description>
                <id>D</id>
                <name>D気候</name>
            </climateList>
            <climateList>
                <description>サンプル気候の説明</description>
                <id>D</id>
                <name>D気候</name>
            </climateList>
            <climateList>
                <description>サンプル気候の説明</description>
                <id>D</id>
                <name>D気候</name>
            </climateList>
            <food_chain>食物連鎖</food_chain>
        </nature>
    </world>
    <world>
        <description>世界の説明</description>
        <id>UpperWorld</id>
        <name>上の世界</name>
        <civilizations>
            <description>文明1の説明</description>
            <id>civil1</id>
            <name>文明1</name>
            <arts>文明1の芸術</arts>
            <character>文明1の文字</character>
            <structures>文明1の社会構造</structures>
            <technology>文明1の技術</technology>
        </civilizations>
        <civilizations>
            <description>文明2の説明</description>
            <id>civil2</id>
            <name>文明2</name>
            <arts>文明2の芸術</arts>
            <character>文明2の文字</character>
            <structures>文明2の社会構造</structures>
            <technology>文明2の技術</technology>
        </civilizations>
        <countries>
            <description>A国の説明</description>
            <id>country1</id>
            <name>A国</name>
        </countries>
        <countries>
            <description>B国の説明</description>
            <id>country2</id>
            <name>B国</name>
        </countries>
        <cultures>
            <description>文化1の説明</description>
            <id>culture1</id>
            <name>文化1</name>
            <habit>文化1の生活習慣</habit>
            <life_style>文化1の生活スタイル</life_style>
            <norm>文化1の規範</norm>
            <organization>文化1の組織</organization>
            <social_structure>文化1の社会構造</social_structure>
            <values>文化1の価値観</values>
            <view_of_world>文化1の世界観</view_of_world>
        </cultures>
        <nature>
            <description>自然の説明</description>
            <id>natureId</id>
            <name>自然名</name>
            <path></path>
            <climateList>
                <description>サンプル気候の説明</description>
                <id>D</id>
                <name>D気候</name>
            </climateList>
            <climateList>
                <description>サンプル気候の説明</description>
                <id>D</id>
                <name>D気候</name>
            </climateList>
            <climateList>
                <description>サンプル気候の説明</description>
                <id>D</id>
                <name>D気候</name>
            </climateList>
            <climateList>
                <description>サンプル気候の説明</description>
                <id>D</id>
                <name>D気候</name>
            </climateList>
            <food_chain>食物連鎖</food_chain>
        </nature>
    </world>
</worlds>

余談

JAXBでXMLからクラスを生成するとき、対象のクラスには、デフォルトコンストラクタが定義されていないとエラーになる。
<XMLの一部抜粋>

<config>
    <views>HP</views>
    <views>MP</views>
    <views>LV</views>
    <money>
        <key>NIG</key>
        <name>ニギ</name>
        <value>0</value>
    </money>
    <money>
        <key>GLD</key>
        <name>ゴールド</name>
        <value>1</value>
    </money>
    <element>
        <id>FIR</id>
        <name>火</name>
    </element>
    <element>
        <id>WIN</id>
        <name>風</name>
    </element>
    <element>
        <id>WAT</id>
        <name>水</name>
    </element>
    <element>
        <id>EAT</id>
        <name>土</name>
    </element>
</config>

<クラス>

package jp.zenryoku.rpg.data.config;

import lombok.Data;
/**
 * このゲームで使用する要素・エレメントを定義する。
 * 
 * @author (Takunoji)
 * @version (1.0)
 */
@Data
public class Element
{
    /** ID */
    private String id;
    /** 要素名 */
    private String name;

    /** デフォルトコンストラクタ */
    public Element() {
    }

    public Element(String id, String name) {
        this.id = id;
        this.name = name;
    }
}

出力する順序をつける

使用するクラス(アノテーション)

import javax.xml.bind.annotation.XmlType;
@XmlType(propOrder={"フィールド変数名1", "フィールド変数名2", "フィールド変数名3" ... })

出力したXML(ストーリーXML)

<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<story>
    <description>はじめのタイトル表示を行いニューゲーム、コンテニューを選択、実行する</description>
    <id>First</id>
    <name>はじめの</name>
    <path>config/stories/Story_0001.xml</path>
    <sceneNo>0</sceneNo>
    <sceneType>STORY</sceneType>
    <nextScene>1</nextScene>
    <canSelectNextScene>true</canSelectNextScene>
</story>

タグに属性をつける

Commandを指定するときに追加で属性をつけたくなり以下のように修正しました。

<commandStr kigo="ATK" level="1"/>

「レベル」という小目を追加しました。これは、このコマンドがレベルなんぼ?の時に使用可能か?を定義するためのものです。
これをJavaで表現するためには、以下のようにクラスを追加してやる必要がありました。

@XmlAccessorType(XmlAccessType.FIELD)
public class CommandStr {
    @XmlAttribute
    private String kigo;
    @XmlAttribute
    private int level;

    public CommandStr() {
    }

    public CommandStr(int level, String kigo) {
        this.level = level;
        this.kigo = kigo;
    }
}

JobクラスのcommandStrに追加する情報でした。

@Data
public class Job extends StoryConfig {

    private List<CommandStr> commandStr;
    private int level;
    private List<Command> commandList;
    private List<Params> paramList;

}

こんな感じです。これを読み込んでゲームを開始するというようなアプリがテキストRPGなのですが、まだまだ問題はヤマズミです。。。

Java Swing テキストRPGを作る ~Swingを使って画面を作る~

イントロダクション

テキストRPGを作成しようと思います。どちらかというとリメイクに近いのですが、前回作成したものが完成していないので。。。
兎にも角にも、Java Swingでの実装になりますので、クラスの扱い方の学習がしやすい、かつ、視覚的にクラス関係を理解できると思います。

IDE(開発ツール)はBlueJを使用しています。

Swingを使って画面を作る

以前、テキストRPGを作成しました。
Gitにアップしてあります。

しかし、これはコマンドプロンプト上で実行するもので「ゲーム」って感じのしないものでした。画面のリロードとかうまくいきません。。。
なので、いろいろと考えた末、Java Swingで実装したらよいと考えなおしました。

余談

実際、筆者はJavaのオブジェクト指向プログラミングという部分をSwingで学びました。つまり、クラスの扱い方を理解しました。
「オブジェクト指向」という言葉を使うと「staticおじさん」や「オブジェクト指向おじさん」よろしく。。。混沌の世界に足を踏み入れることになるので言葉を変えていきたいと思います。

クラスの扱い方を理解する

まとめると、筆者はSwingの実装を通してクラスの扱い方を理解しました。というところを言いたかった次第です。
そして、最近覚えたBlueJを使用して、テキストRPGを作成していきたいと思います。

画面を作る

Swingを使用して、画面を作成していきます。まずは、テキストRPGを実行して「テキスト」を表示する領域が必要になります。

初めのコード

この領域には次の機能が必要になります。

  1. 文字を表示する
  2. 文字をクリア(削除)する

これらを実現するためにプログラム的には、以下のものを使用します。

  • JFrame: アプリの表示する領域フレームを表現するクラス
  • JPanel: フレーム上にコンポーネントを配置するパネル
  • 各種コンポーネント: ラベル(JLabel)、テキストエリア(JTextArea)など

クラス継承について

クラスの継承関係を見てみるとわかりやすいです。

これは、JFrameクラスの親は、Frameクラス、そしてその親は。。。とそれぞれの継承関係を示しています。
つまり、クラスキャストも行うことができるということです。

JFrame frame = new JFrame();
Frame superFrame = (Frame) frame;
superFrame.XXXX;

言葉を変えると、親クラスが必要な時は、上記のようにキャストして使用することができます。
そして、親クラスのメソッドを呼び出すこともできます。

JFrame frame = new JFrame();
frame.addNotify(); // java.awt.Frameのメソッド

コードについて

作成したコードは、下のような表示を行います。どの部分がフレームなのか?も記述しました。

コード

TextRPGMainクラスは、JFrameを継承しているところに注意してください。

package jp.zenryoku.rpg;

import javax.swing.JFrame;
import javax.swing.JTextArea;
import javax.swing.JPanel;
import javax.swing.JLabel;
import java.awt.Container;
import java.awt.BorderLayout;
import java.awt.Dimension;
import java.awt.Toolkit;

/**
 * クラス TextRPGMain の注釈をここに書きます.
 * 
 * @author (Takunoji)
 * @version (1.0)
 */
public class TextRPGMain extends JFrame
{
    public static void main(String[] args) {
        // JFrameを継承しているのでJFrameクラスのメソッドを使える
        TextRPGMain main = new TextRPGMain();
        main.run("Text RPG");
    }

    public void run(String title) {
        // タイトルをセット
        setTitle(title);
        // 画面の表示位置と画面サイズをセット
        Dimension windowSize = Toolkit.getDefaultToolkit().getScreenSize();
        int xPos = (int) windowSize.getWidth() / 4;
        int yPos = (int) windowSize.getHeight() / 4;
        setBounds(xPos, yPos, xPos * 2, yPos * 2);
        // 画面を閉じたときアプリを終了する設定
        setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);

        // ラベル
        JLabel titleLabel = new JLabel("Text RPG");
        JTextArea textarea = new JTextArea();
        // テキストエリア
        textarea.setColumns(40);
        textarea.setRows(10);

        // ラベル用のパネル
        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);
    }
}

ちなみに、クラス図で見ると下のようになります。

メインメソッドを持っているクラスのみになります。

次は、クラスの拡張実装を行ってみようと思います。

クラス継承の実装

クラスの継承方法は下のように「extends クラス名」と書くだけです。

public class ChildClass extends ParentClass {
   ....
}

JLabelを拡張する

「拡張」という言葉に戸惑うかもしれません。ズバリ「JLabelを継承して新しいクラスを作成する」という意味です。

新しく「TitleLabel」クラスを追加します。このクラスは上記のTextRPGMainクラスのrun()メソッドで行っている処理を少なくするように実装しています。
別な言い方をすると「タイトルラベルの処理はTitleLabelに任せましょう。というところです。

では、どのようになるのか?というところです。

TitleLabelの実装

  1. TitleLabelクラスを作成します。
  2. JLabelを継承します。
  3. 現状はコンストラクタの実装のみで事足ります。

実際のコードです。

package jp.zenryoku.rpg;

import javax.swing.JLabel;
import java.awt.Dimension;
import java.awt.Color;

/**
 * クラス TitleLabel の注釈をここに書きます.
 * JLabelを拡張して、テキストRPGのタイトルをセットするラベルを作成する。
 * 
 * @author (Takunoji)
 * @version (1.0)
 */
public class TitleLabel extends JLabel
{
    public TitleLabel(String title, Dimension windowSize) {
        super(title);
        int width = (int) windowSize.getWidth() / 4;
        int height = (int) windowSize.getHeight() / 16;
        Dimension labelSize = new Dimension(width, height);
        setOpaque(true);
        setPreferredSize(labelSize);
        setBackground(Color.GREEN);
        setHorizontalAlignment(JLabel.CENTER);
    }
}
  1. JLabelを継承しているので、親クラス(JLabel)のコンストラクタを呼び出します。super(title);
  2. ラベルのサイズ(縦横の幅指定)をします。
  3. ラベルの領域がわかるように、緑色の背景を付けます。

ちなみに、ラベルのサイズは、毎回値を変更するのは、面倒なのでPCの画面サイズに合わせてサイズを変更するように実装しました。

そして、run()メソッドと今回作成したクラスの処理の関係を示します。

TextRPGMain#run()

JLabelを生成して、タイトルをセットしただけで、幅や背景などはセットしていませんでした。

// ラベル
JLabel titleLabel = new JLabel("Text RPG");

なので、この「TitleLabel」クラスを作成していなかったらTextRPGMainクラスにJLabelの処理を書くことになります。
このTextRPGMainクラスにJLabelの処理を書くことがプログラム的に美しくないのでTitleLabelを作成しタイトルラベルのことはこのクラスにコードを書きましょう。という風に考えてプログラムを作りました。

TextRPGMain#run()の修正

ズバリ下のように修正しました。1行です。
JLabel titleLabel = new JLabel("Text RPG");TitleLabel titleLabel = new TitleLabel("Text RPG", windowSize);
になりました。表示の結果は以下の通り
<元々の表示>

<修正後の表示>

次は、プログラム・コードを見てみましょう。
<元々の処理>

public void run(String title) {
    // タイトルをセット
    setTitle(title);
    // 画面の表示位置と画面サイズをセット
    Dimension windowSize = Toolkit.getDefaultToolkit().getScreenSize();
    int xPos = (int) windowSize.getWidth() / 4;
    int yPos = (int) windowSize.getHeight() / 4;
    setBounds(xPos, yPos, xPos * 2, yPos * 2);
    // 画面を閉じたときアプリを終了する設定
    setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);

    // ラベル
    JLabel titleLabel = new JLabel("Text RPG");
    // テキストエリア
    JTextArea textarea = new JTextArea();
    textarea.setColumns(40);
    textarea.setRows(10);

    // ラベル用のパネル
    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);
}

<修正後>

public void run(String title) {
    // タイトルをセット
   setTitle(title);
    // 画面の表示位置と画面サイズをセット
    Dimension windowSize = Toolkit.getDefaultToolkit().getScreenSize();
    int xPos = (int) windowSize.getWidth() / 4;
    int yPos = (int) windowSize.getHeight() / 4;
    setBounds(xPos, yPos, xPos * 2, yPos * 2);
    // 画面を閉じたときアプリを終了する設定
    setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);

    // ラベル
    TitleLabel titleLabel = new TitleLabel("Text RPG", windowSize);
    // テキストエリア
    JTextArea textarea = new JTextArea();
    textarea.setColumns(40);
    textarea.setRows(10);

    // ラベル用のパネル
    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);
}

こんな感じです。
次は、テキストエリアをタイトルラベルと同じように拡張しようと思います。

JTextAreaの拡張

まずは、現状のクラス作成状況を確認します。

次は、画面の白い部分「テキストエリア」を拡張して文字列の表示領域を作成します。

今回も、テキストエリアを担当するクラスを作成します。ネーミングセンスが問われますが、目的と役割を明確にすることを最優先にするので。。。

RpgTextクラスを作成

RpgTextAreaクラスとします。作成はまずJTextAreaを継承します。

import javax.swing.JTextArea;

/**
 * クラス RpgTextArea の注釈をここに書きます.
 * テキストRPGの表示する文字列をこの領域に出力(描画)する。
 * 背景は黒、イメージはドラ○エのような感じにしたい。
 * 
 * @author (Takunoji)
 * @version (1.0)
 */
public class RpgTextArea extends JTextArea
{
    public RpgTextArea() {

    }
}

そして、テキストの表示を担当するので、メインクラスに書いている次の部分が不要になります。

JTextArea textarea = new JTextArea();
textarea.setColumns(40);
textarea.setRows(10);

同様に、次のようにコードをRpgTextAreaに追加します。

public class RpgTextArea extends JTextArea
{
    public RpgTextArea() {
        setColumns(40);
        setRows(10);
    }
}

そして、TextRPGMain#run()を修正

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

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

        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);
    }

JPanelをRpgTextAreaに修正、不要なコードを削除しました。

この状態でプログラム実行すると下のようになります。

全く変わりません。その代わり、run()メソッドのコードの量は(少しですが)減りました。
ここから、テキストエリアのおしゃれをしていきます。
参照するのはJavaDocのJTextAreaです。
他にも次のクラスを参照します。

Fontクラスを見ると、フォントファイルを指定することでオリジナルのフォントも使えるようです。

<実行結果>

TextAreaのサイズ設定

画面のサイズ指定に関して、文字入力することも考えてPCの画面サイズから文字の数、行の数を設定するようにプログラムを組みました。
理論的なところが、はっきりと決まらなかったのですが、縦横の「~分の~」という形で実装しました。

public class RpgTextArea extends JTextArea
{
    /** コンストラクタ */
    public RpgTextArea(Dimension size) {
        int widthCol = (int) size.getWidth() / 19;
        int heightRow = (int) size.getHeight() / 28;
        System.out.println("width: " + widthCol);
        System.out.println("height: " + heightRow);
        setColumns(widthCol);
        setRows(heightRow);
        ...
    }
    ...
}

実行結果は、下のような形です。

とりあえずは、これで、画面が作成できたのでここでひと段落になります。

まとめ

クラスを継承すると親クラスのメソッドなどは、自分のクラス内のメソッドのように使用することができる。
なので、下のようなコードが書ける。

public class RpgTextArea extends JTextArea
{
    /** コンストラクタ */
    public RpgTextArea(Dimension size) {
        int widthCol = (int) size.getWidth() / 19;
        int heightRow = (int) size.getHeight() / 28;
        System.out.println("width: " + widthCol);
        System.out.println("height: " + heightRow);
        setColumns(widthCol);
        setRows(heightRow);
        // 背景の描画準備
        setOpaque(true);
        // フォントの設定
        setFont(createTextFont());
        // 背景を黒にする
        setBackground(Color.BLACK);
        // 白い文字の設定
        setForeground(Color.WHITE);
        // 白いボーダーの設定
        Border border = BorderFactory.createLineBorder(Color.GREEN);
        setBorder(BorderFactory.createCompoundBorder(border,
            BorderFactory.createEmptyBorder(10, 10, 10, 10)));

        setWrapStyleWord(true);
        setLineWrap(true);
    }
    ....
}

つまるところは、親クラスのpublic, (packaged, )protectedのメソッドを子クラスが使用することができるので「setXXX」のようなメソッドを直接呼び出すことができる。

今回は、コンストラクタのみを使用した形で実装しました。
次は、テキストの表示などを行っていきたいと思います。

次回 >>>