
前回は、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らしくなりました。
まだ戦闘画面に表示しているだけなので、攻撃やダメージ計算はできません。
次回は、戦闘画面に「攻撃」ボタンを追加し、勇者がスライムにダメージを与える処理を作っていきます。