オブジェクト指向とかデザインパターンとか開発プロセスとかツールとか

satoshi's ソフトウェア開発

js



当サイトはアフィリエイト広告を利用してます。

Java UML オブジェクト指向

JavaでRPGを作る|敵と遭遇して戦闘画面を表示しよう

投稿日:


前回は、JavaでRPGの画面を作り、勇者をフィールド上で動かせるようにしました。

草原のマス目を表示し、勇者の画像を配置して、矢印キーで移動できるようにしました。

しかし、RPGらしくするなら、フィールドを歩いているときに敵と遭遇したいところです。

そこで今回は、勇者が移動したときに、一定の確率でスライムと遭遇する処理を追加します。

そして、敵と遭遇したら戦闘画面を表示してみます。

今回作るもの

今回作る内容は、次の通りです。

  • 勇者が移動したときに敵との遭遇判定を行う
  • 一定の確率でスライムと遭遇する
  • スライムと遭遇したら戦闘画面を表示する
  • 戦闘画面に勇者とスライムを表示する

前回までのRpgFrame

前回までに、RpgFrame では勇者の位置を heroX と heroY で管理していました。

そして、矢印キーが押されたときに、勇者の座標を変更していました。

今回は、この keyPressed() メソッドに、敵との遭遇判定を追加していきます。

敵との遭遇はいつ判定するか

RPGでは、フィールドを歩いているときに敵と遭遇することがあります。

そのため、敵との遭遇判定は、勇者が移動したあとに行うのが自然です。

たとえば、右キーを押して勇者が右に移動したあと、敵と遭遇するかどうかを判定します。

ただし、キーを押しただけで、実際には移動していない場合もあります。

たとえば、勇者がフィールドの左端にいるときに左キーを押しても、勇者は移動しません。

この場合は、敵との遭遇判定をしない方が自然です。

そこで、今回は「実際に移動した場合だけ」敵との遭遇判定を行うようにします。

移動したかどうかを判定する

まず、keyPressed() の中で、勇者が移動したかどうかを覚えておくための変数を用意します。

boolean moved = false;

勇者が実際に移動したら、moved を true にします。

keyPressed()メソッドは、以下のようになります。

    @Override public void keyPressed(KeyEvent e) {
        boolean moved = false;
        if (e.getKeyCode() == KeyEvent.VK_LEFT) {
            if (heroX > 0) {
                heroX--;
                moved = true;
            }
        }
        if (e.getKeyCode() == KeyEvent.VK_RIGHT) {
            if (heroX < FIELD_WIDTH - 1) {
                heroX++;
                moved = true;
            }
        }
        if (e.getKeyCode() == KeyEvent.VK_UP) {
            if (heroY > 0) {
           heroY--;
                moved = true;
            }
        }
        if (e.getKeyCode() == KeyEvent.VK_DOWN) {
            if (heroY < FIELD_HEIGHT - 1) {
                heroY++;
                moved = true;
            }
        }
        drawField();
        System.out.println("x=" + heroX + ", y=" + heroY);
    }

勇者が移動した場合だけ、drawField() で画面を描き直し、checkEncounter() で敵との遭遇判定を行います。

if (moved) {
    drawField();
    checkEncounter();
}

Randomを使って敵との遭遇を判定する

敵と遭遇するかどうかは、ランダムに決めることにします。

Javaで乱数を使うには、Random クラスを使います。

import java.util.Random;

RpgFrame に Random のフィールドを追加します。

private Random random = new Random();

そして、checkEncounter() メソッドを追加します。

private void checkEncounter() {
    int value = random.nextInt(100);

    if (value < 20) {
        System.out.println("スライムがあらわれた!");
    }
}

random.nextInt(100) は、0から99までの整数をランダムに返します。

今回は、value が20未満なら敵と遭遇したことにします。

0から99までの100通りのうち、0から19までの20通りで敵と遭遇します。

つまり、20%の確率でスライムと遭遇することになります。

この時点では、まだ戦闘画面は表示していません。

まずはコンソールに、「スライムがあらわれた!」と表示されれば成功です。

戦闘画面を表すBattleFrameクラスを作る

次に、敵と遭遇したときに表示する戦闘画面を作ります。

戦闘画面用のクラスとして、BattleFrame を作ります。

import javax.swing.JFrame;
import javax.swing.JLabel;

public class BattleFrame extends JFrame {
    public BattleFrame() {
        super("戦闘");

        JLabel messageLabel = new JLabel("スライムがあらわれた!");
        add(messageLabel);

        pack();
        setLocationRelativeTo(null);
    }
}

BattleFrame は JFrame を継承しています。

public class BattleFrame extends JFrame

前回作った RpgFrame も JFrame を継承していました。

RpgFrame はフィールド画面です。
BattleFrame は戦闘画面です。

どちらもウィンドウなので、JFrame を継承しています。

この段階では、戦闘画面には文字だけを表示しています。

まずは、敵と遭遇したときに別の画面が表示されることを確認します。

敵と遭遇したらBattleFrameを表示する

次に、checkEncounter() の中で BattleFrame を表示します。

private void checkEncounter() {
    int value = random.nextInt(100);

    if (value < 20) {
        System.out.println("スライムがあらわれた!");

        BattleFrame battleFrame = new BattleFrame();
        battleFrame.setVisible(true);
    }
}

これで、勇者が移動したときに一定の確率で戦闘画面が表示されます。

ここまでの RpgFrame は次のようになります。

import java.awt.Component;
import java.awt.Container;
import java.awt.GridBagConstraints;
import java.awt.GridBagLayout;
import java.awt.event.KeyEvent;
import java.awt.event.KeyListener;
import java.util.Random;

import javax.swing.JFrame;

public class RpgFrame extends JFrame implements KeyListener {
    public static final int ICON_WIDTH = 64;
    public static final int ICON_HEIGHT = 64;
    public static final int FIELD_WIDTH = 10;
    public static final int FIELD_HEIGHT = 8;

    private int heroX = 0;
    private int heroY = 0;

    private Random random = new Random();

    public RpgFrame() {
        super("RPG");
        setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);

        Container pane = getContentPane();
        pane.setLayout(new GridBagLayout());

        addKeyListener(this);
        setFocusable(true);

        drawField();
    }

    private void drawField() {
        Container pane = getContentPane();
        pane.removeAll();

        for (int y = 0; y < FIELD_HEIGHT; y++) {
            for (int x = 0; x < FIELD_WIDTH; x++) {
                if (x == heroX && y == heroY) {
                    add(pane, new HeroLabel(), x, y, 1, 1);
                } else {
                    add(pane, new FieldLabel(), x, y, 1, 1);
                }
            }
        }

        pane.revalidate();
        pane.repaint();
    }

    private void checkEncounter() {
        int value = random.nextInt(100);

        if (value < 20) {
            System.out.println("スライムがあらわれた!");

            BattleFrame battleFrame = new BattleFrame();
            battleFrame.setVisible(true);
        }
    }

    private static void add(
            Container pane,
            Component component,
            int x,
            int y,
            int width,
            int height) {

        GridBagConstraints constraints = new GridBagConstraints();
        constraints.fill = GridBagConstraints.BOTH;
        constraints.gridx = x;
        constraints.gridy = y;
        constraints.gridwidth = width;
        constraints.gridheight = height;

        pane.add(component, constraints);
    }

    @Override
    public void keyTyped(KeyEvent e) {
    }

    @Override
    public void keyPressed(KeyEvent e) {
        boolean moved = false;

        if (e.getKeyCode() == KeyEvent.VK_LEFT) {
            if (heroX > 0) {
                heroX--;
                moved = true;
            }
        }

        if (e.getKeyCode() == KeyEvent.VK_RIGHT) {
            if (heroX < FIELD_WIDTH - 1) {
                heroX++;
                moved = true;
            }
        }

        if (e.getKeyCode() == KeyEvent.VK_UP) {
            if (heroY > 0) {
                heroY--;
                moved = true;
            }
        }

        if (e.getKeyCode() == KeyEvent.VK_DOWN) {
            if (heroY < FIELD_HEIGHT - 1) {
                heroY++;
                moved = true;
            }
        }

        if (moved) {
            drawField();
            checkEncounter();
        }

        System.out.println("x=" + heroX + ", y=" + heroY);
    }

    @Override
    public void keyReleased(KeyEvent e) {
    }
}

これで、勇者が移動したときにランダムでスライムと遭遇し、戦闘画面が表示されるようになります。

スライム画像を表示するSlimeLabelクラスを作る

文字だけの戦闘画面でも動作確認はできます。

しかし、せっかくGUIでRPGを作っているので、戦闘画面にもスライムの画像を表示してみます。

まず、スライムを表示するための SlimeLabel クラスを作ります。

ここでは、スライムの画像ファイルとして slime.png を用意しているものとします。

import java.awt.Dimension;
import java.awt.Image;

import javax.swing.ImageIcon;
import javax.swing.JLabel;

public class SlimeLabel extends JLabel {
    public SlimeLabel() {
        ImageIcon icon = new ImageIcon("slime.png");
        Image image = icon.getImage().getScaledInstance(
                RpgFrame.ICON_WIDTH,
                RpgFrame.ICON_HEIGHT,
                Image.SCALE_SMOOTH);

        setIcon(new ImageIcon(image));
        setPreferredSize(new Dimension(
                RpgFrame.ICON_WIDTH,
                RpgFrame.ICON_HEIGHT));
    }
}

SlimeLabel も JLabel を継承しています。

前回作った HeroLabel は勇者を表示するためのクラスでした。

今回作る SlimeLabel は、スライムを表示するためのクラスです。

どちらも画像を表示するラベルですが、表示する対象が違います。

HeroLabelとSlimeLabelの共通点を見つける

前回の記事では、勇者の画像を表示するために HeroLabel クラスを作りました。

import java.awt.Dimension;
import java.awt.Image;

import javax.swing.ImageIcon;
import javax.swing.JLabel;

public class HeroLabel extends JLabel {
public HeroLabel() {
ImageIcon icon = new ImageIcon("hero.png");
Image image = icon.getImage().getScaledInstance(
RpgFrame.ICON_WIDTH,
RpgFrame.ICON_HEIGHT,
Image.SCALE_SMOOTH);

setIcon(new ImageIcon(image));
setPreferredSize(new Dimension(
RpgFrame.ICON_WIDTH,
RpgFrame.ICON_HEIGHT));
}
}

今回、スライムの画像を表示するために SlimeLabel クラスを作りましたが、これらのクラスを見比べると、かなり似ていることがわかります。

違うのは、読み込んでいる画像ファイル名だけです。

それ以外の処理は同じです。

このように、複数のクラスに同じような処理が出てきたら、共通部分を親クラスにまとめることを考えます。

前の記事では、Hero と Slime の共通部分を Chara クラスにまとめました。

今回は、HeroLabel と SlimeLabel の共通部分を、親クラスとして ImageLabel にまとめてみます。

画像表示の共通処理をImageLabelクラスにまとめる

HeroLabel と SlimeLabel は、どちらも画像を表示するためのラベルです。

そこで、画像を読み込んでサイズを変更し、ラベルに設定する処理を ImageLabel クラスにまとめます。

import java.awt.Dimension;
import java.awt.Image;

import javax.swing.ImageIcon;
import javax.swing.JLabel;

public class ImageLabel extends JLabel {
    public ImageLabel(String filename) {
        ImageIcon icon = new ImageIcon(filename);
        Image image = icon.getImage().getScaledInstance(
                RpgFrame.ICON_WIDTH,
                RpgFrame.ICON_HEIGHT,
                Image.SCALE_SMOOTH);

        setIcon(new ImageIcon(image));
        setPreferredSize(new Dimension(
                RpgFrame.ICON_WIDTH,
                RpgFrame.ICON_HEIGHT));
    }
}

ImageLabel は JLabel を継承しています。

そして、コンストラクタで画像ファイル名を受け取っています。

public ImageLabel(String filename)

この filename に "hero.png" や "slime.png" を渡せば、それぞれの画像を表示できます。

画像を読み込む処理、サイズを変更する処理、ラベルに設定する処理は、すべて ImageLabel にまとめました。

HeroLabelはImageLabelを継承する

次に、HeroLabel を書き換えます。

public class HeroLabel extends ImageLabel {
    public HeroLabel() {
        super("hero.png");
    }
}

かなり短くなりました。

ImageLabel のコンストラクタには画像ファイル名が必要なので、super("hero.png") を呼び出しています。

super("hero.png");

super() は、親クラスのコンストラクタを呼び出すための書き方です。親クラスのことをスーパークラスと呼ぶこともあります。

それを覚えておくと、分かりやすいですね。

SlimeLabelもImageLabelを継承する

同じように、SlimeLabel も ImageLabel を継承するようにします。

public class SlimeLabel extends ImageLabel {
    public SlimeLabel() {
        super("slime.png");
    }
}

こちらもかなり短くなりました。

FieldLabelもImageLabelを継承する

RpgFrameでは、草原を表示するためにFieldLabel を作りましたが、これも ImageLabel を継承するようにします。

public class FieldLabel extends ImageLabel {
    public FieldLabel() {
        super("slime.png");
    }
}

こちらもかなり短くなりました。

ImageLabelを作ると何がうれしいのか

ImageLabel を作ることで、画像表示に関する共通処理を1か所にまとめることができました。

これは、継承の分かりやすい使用例です。

共通する処理を親クラスにまとめ、子クラスでは違いだけを書く。

今回の場合、違いは画像ファイル名だけです。

そのため、HeroLabel と SlimeLabel では、親クラスに渡すファイル名だけを指定しています。

クラス図で整理する

今回のクラスを簡単なクラス図で整理すると、次のようになります。

+----------------+
| GuiApp         |
+----------------+
|                |
+----------------+
| main(args)     |
+----------------+

+----------------+
| JFrame         |
+----------------+
        △
        |
        +--------------------+
        |                    |
+----------------+   +----------------+
| RpgFrame       |   | BattleFrame    |
+----------------+   +----------------+
| heroX          |   |                |
| heroY          |   |                |
| random         |   |                |
| battle         |   |                |
+----------------+   +----------------+
| drawField()    |   |                |
| checkEncounter()|  |                |
| keyPressed(e)  |   |                |
+----------------+   +----------------+

+----------------+
| JLabel         |
+----------------+
        △
        |
+----------------+
| ImageLabel     |
+----------------+
        △
        |
        +--------------------+--------------------+
        |                    |                    |
+----------------+   +----------------+   +----------------+
| HeroLabel      |   | FieldLabel     |   | SlimeLabel     |
+----------------+   +----------------+   +----------------+
|                |   |                |   |                |
+----------------+   +----------------+   +----------------+
|                |   |                |   |                |
+----------------+   +----------------+   +----------------+

まとめ:移動すると敵と遭遇するようになった

今回は、勇者がフィールド上を移動したときに、敵と遭遇する処理を追加しました。

まず、勇者が実際に移動したかどうかを moved で判定しました。

移動した場合だけ、checkEncounter() を呼び出すようにしました。

checkEncounter() では、Random を使って乱数を作り、20%の確率でスライムと遭遇するようにしました。

そして、スライムと遭遇したら、BattleFrame を表示するようにしました。

さらに、SlimeLabel を作り、戦闘画面に勇者とスライムの画像を表示しました。

これで、前回よりもかなりRPGらしくなりました。

まだ戦闘画面に表示しているだけなので、攻撃やダメージ計算はできません。

次回は、戦闘画面に「攻撃」ボタンを追加し、勇者がスライムにダメージを与える処理を作っていきます。

-Java, UML, オブジェクト指向
-, , ,

Copyright© satoshi's ソフトウェア開発 , 2026 All Rights Reserved Powered by STINGER.