
今回は、JavaのGUI機能を使って、勇者をフィールド上に表示し、キーボードで動かせるようにします。
RPGを作るなら、やはり画面があった方が楽しくなります。
これまでの記事では、Chara、Hero、Slime などのクラスを作りながら、オブジェクト指向や継承について考えてきました。
今回は、JavaでGUIの画面を作り、最終的には、草原のマス目の上に勇者を表示し、キーボードの矢印キーで動かせるようにします。
今回作るもの
今回作る画面は、シンプルなRPGのフィールドです。
画面上に草原の画像を並べ、その中の1マスに勇者の画像を表示します。
そして、矢印キーを押すと、勇者の位置が変わります。
たとえば、右キーを押すと勇者が右に移動します。
下キーを押すと勇者が下に移動します。
本格的なRPGには、マップ、敵、アイテム、戦闘画面など、いろいろな要素があります。
しかし、最初から全部を作ろうとすると大変です。
まずは、
- 画面を表示する
- フィールドを表示する
- 勇者を表示する
- キーボードで勇者を動かす
ところまで作ります。
Swingで使う主なクラス
Javaで画面を持つアプリケーションを作るには、GUIの仕組みが必要です。
GUIとは、ボタンやウィンドウ、画像などを使って操作できる画面のことです。
Javaには、GUIを作るためのライブラリとして Swing が用意されています。
Swingを使うと、ウィンドウを表示したり、画像を表示したり、ボタンやラベルを配置したりできます。
今回使う主なSwingのクラスは、次の通りです。
| クラス | 役割 |
|---|---|
| JFrame | ウィンドウを表示する |
| JLabel | 文字や画像を表示する |
| ImageIcon | 画像ファイルを読み込む |
| GridBagLayout | 部品をマス目のように配置する |
| KeyListener | キーボード入力を受け取る |
Swingにはたくさんのクラスがありますが、今回は最小限のクラスだけ使うことにします。
RPGを作るという目的で考えると、
- JFrame はゲーム画面
- JLabel は勇者や草原の画像を表示する部品
- GridBagLayout はフィールドをマス目に並べる仕組み
- KeyListener は勇者を動かすための入力処理
と考えると分かりやすいです。
MVCアーキテクチャ
GUIアプリケーションを作る時に大切なのが「MVCアーキテクチャ」です。
MはModel、VはView、CはControllerです。
それぞれ、クラスの役割分担をさせましょうという考え方です。
これまでに作成した Hero や Slime は Model の役割を受け持ちます。
Model だけではアプリケーションは動きません。
Viewは、画面上に表示するためのオブジェクトです。Heroを画面上でどのように表示するのかを決める役割を持ちます。
例えば、Windowsのエクスプローラーでファイルを見る時に、ファイルやフォルダはModelです。
そして、メニューからファイルの表示方法を大きいアイコンや小さいアイコン、詳細などに変更できますよね。
これがファイルやフォルダのViewです。Viewのクラスを切り替えることで、表示方法を変更しているのです。
メニューにある「大きいアイコン」や「小さいアイコン」などがControllerです。
その考え方を前提にして、GUIアプリケーションを作っていきます。
まずは起動用のGuiAppクラスを作る
まず、GUIアプリケーションを起動するための GuiApp クラスを作ります。
import javax.swing.SwingUtilities;
public class GuiApp {
public static void main(String[] args) {
SwingUtilities.invokeLater(() -> {
RpgFrame frame = new RpgFrame();
frame.pack();
frame.setLocationRelativeTo(null);
frame.setVisible(true);
frame.requestFocusInWindow();
});
}
}
GuiApp の役割は、RPGの画面を作って表示することです。
RpgFrame frame = new RpgFrame();
ここで、RPG用の画面である RpgFrame を作っています。
このクラスは、RpgFrameを表示する役割を持つControllerにあたります。
frame.pack(); frame.setLocationRelativeTo(null); frame.setVisible(true);
pack()は、画面内の配置を自動調整してくれるメソッドです。
setLocationRelativeTo()は、画面の位置を指定するメソッドです。nullを引数に指定すると、ディスプレイの中央に画面を表示してくれます。
setVisivle(true)で画面を表示します。
Swingでは、画面の表示や更新を専用のスレッドで行うため、SwingUtilities.invokeLater() の中で画面を作るようにしています。
最初は細かい仕組みまで理解できなくても大丈夫です。
ここでは、Swingの画面表示は SwingUtilities.invokeLater() の中で行うお約束になっているのでそうしているだけです。
RPG用の画面RpgFrameクラスを作る
次に、RPG用の画面を表す RpgFrame クラスを作ります。画面を表示する機能を持つ、JFrameクラスを継承しています。
このクラスは、RPGのGUIに当たるViewの役割を持ちます。
import javax.swing.JFrame;
public class RpgFrame extends JFrame {
public RpgFrame() {
super("RPG");
setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
}
}
ただの JFrame ではなく、RPG用に使いやすくした画面として RpgFrame クラスを作っています。
このように、Javaが用意しているクラスを継承して、自分の目的に合ったクラスを作ることもできます。
前の記事では、Hero や Slime が Chara を継承しました。
今回は、RpgFrame が JFrame を継承しています。
どちらも、
既存のクラスの機能を受け継いで、新しいクラスを作る
という点では同じです。
勇者を表示するHeroLabelクラスを作る
次に、勇者の画像を表示するためのクラスを作ります。
HeroLabelは、HeroというModelに対応するViewクラスです。
画像を表示するには、JLabel と ImageIcon を使います。
ここでは、勇者の画像ファイルとして hero.png を用意しているものとします。
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));
}
}
HeroLabel は JLabel を継承しています。
JLabel は、文字や画像を表示するためのJavaの標準部品です。
つまり、HeroLabel は、勇者の画像を表示するためのラベルです。
画像ファイルは、次の部分で読み込んでいます。
ImageIcon icon = new ImageIcon("hero.png");
画像サイズをフィールドのマス目に合わせるために、getScaledInstance() で画像を拡大・縮小しています。
Image image = icon.getImage().getScaledInstance( RpgFrame.ICON_WIDTH, RpgFrame.ICON_HEIGHT, Image.SCALE_SMOOTH);
このように、勇者の表示に関する処理を HeroLabel にまとめておくと、コードが読みやすくなります。
草原を表示するFieldLabelクラスを作る
次に、草原を表示するための FieldLabel クラスを作ります。
ここでは、草原の画像ファイルとして grass.png を用意しているものとします。
import java.awt.Dimension;
import java.awt.Image;
import javax.swing.ImageIcon;
import javax.swing.JLabel;
public class FieldLabel extends JLabel {
public FieldLabel() {
ImageIcon icon = new ImageIcon("grass.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));
}
}
FieldLabel も JLabel を継承しています。
HeroLabel は勇者を表示するためのラベルです。
FieldLabel は草原を表示するためのラベルです。
どちらも画像を表示する部品ですが、役割が違います。
このように、画面に表示するものごとにクラスを分けると、コードの役割が分かりやすくなります。
RpgFrameにフィールドの大きさと勇者の位置を持たせる
次に、RpgFrame にフィールドの大きさと勇者の位置、アイコンのサイズを定数で持たせます。
public class RpgFrame extends JFrame {
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; // ヒーローのX座標
private int heroY = 0; // ヒーローのY座標
public RpgFrame() {
super("RPG");
setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
}
}
GridBagLayoutでフィールドをマス目のように並べる
次に、草原と勇者を画面に並べます。
フィールドをマス目のように並べるために、GridBagLayout を使います。
RpgFrame を次のように書き換えます。
import java.awt.Component;
import java.awt.Container;
import java.awt.GridBagConstraints;
import java.awt.GridBagLayout;
import javax.swing.JFrame;
public class RpgFrame extends JFrame {
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; // ヒーローのX座標
private int heroY = 0; // ヒーローのY座標
public RpgFrame() {
super("RPG");
setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
Container pane = getContentPane();
pane.setLayout(new GridBagLayout());
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 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);
}
}
}
少し長くなりましたが、やっていることはシンプルです。
drawField() メソッドで、フィールド全体を描画しています。
描画しようとしているマス目が勇者の位置なら HeroLabel を表示し、それ以外はFieldLabelを表示します。
最後の add() メソッドは、GridBagLayoutのマス目のどこに表示するのかを指定する機能をまとめています。
キーボード入力を受け取る
次に、矢印キーで勇者を動かせるようにします。
キーボード入力を受け取るために、KeyListener を使います。
RpgFrame を KeyListener に対応させます。
KeyListenerは、Javaが用意しているキー入力を受け取るためのインターフェースです。
RpgFrameクラスを宣言している部分に、implements KeyListener を追加することで、RpgFrameがキー入力を受け取れるようになります。
キー入力を受け取る役割は、Controllerとして振る舞うので、
RpgFrameというViewのクラスにControllerの機能を追加することになりますが、ここはちょっと手抜きしておきます。
public class RpgFrame extends JFrame implements KeyListener {
KeyListener を使うには、次の3つのメソッドをRpgFrameに追加する必要があります。
@Override
public void keyTyped(KeyEvent e) {
}
@Override
public void keyPressed(KeyEvent e) {
}
@Override
public void keyReleased(KeyEvent e) {
}
今回は、キーが押されたときに勇者を動かしたいので、keyPressed() に処理を書きます。
以下のコードをRpgFrameの最後の } の手前に追加します。
何かキーが押されたら、勇者の座標を移動し、画面を再描画しています。
@Override public void keyTyped(KeyEvent e) {
}
@Override public void keyPressed(KeyEvent e) {
if (e.getKeyCode() == KeyEvent.VK_LEFT) {
if (heroX > 0) {
heroX--;
}
}
if (e.getKeyCode() == KeyEvent.VK_RIGHT) {
if (heroX < FIELD_WIDTH - 1) {
heroX++;
}
}
if (e.getKeyCode() == KeyEvent.VK_UP) {
if (heroY > 0) {
heroY--;
}
}
if (e.getKeyCode() == KeyEvent.VK_DOWN) {
if (heroY < FIELD_HEIGHT - 1) {
heroY++;
}
}
drawField();
System.out.println("x=" + heroX + ", y=" + heroY);
}
@Override public void keyReleased(KeyEvent e) {
}
今回作ったクラスの役割
今回作ったクラスの役割を整理すると、次のようになります。
| クラス | 役割 |
|---|---|
| GuiApp | アプリケーションを起動する |
| RpgFrame | RPGの画面を作り、勇者の位置を管理する |
| HeroLabel | 勇者の画像を表示する |
| FieldLabel | 草原の画像を表示する |
このようにクラスごとに役割を分けると、プログラムが読みやすくなります。
クラス図で整理する
今回作ったクラスを、簡単なクラス図で整理してみます。
+----------------+
| GuiApp |
+----------------+
| |
+----------------+
| main(args) |
+----------------+
+----------------+
| JFrame |
+----------------+
△
|
+----------------+
| RpgFrame |
+----------------+
| ICON_WIDTH |
| ICON_HEIGHT |
| FIELD_WIDTH |
| FIELD_HEIGHT |
| heroX |
| heroY |
+----------------+
| drawField() |
| keyPressed(e) |
| keyTyped(e) |
| keyReleased(e) |
+----------------+
+----------------+
| JLabel |
+----------------+
△
|
+--------------------+
| |
+----------------+ +----------------+
| HeroLabel | | FieldLabel |
+----------------+ +----------------+
| | | |
+----------------+ +----------------+
| | | |
+----------------+ +----------------+
まとめ:勇者が動くとRPGらしくなる
今回は、JavaでRPGの画面を作り、勇者をフィールド上で動かしてみました。
まず、GuiApp でアプリケーションを起動しました。
次に、RpgFrame でRPG用の画面を作りました。
勇者の画像は HeroLabel、草原の画像は FieldLabel としてクラスを分けました。
そして、GridBagLayout を使って草原をマス目のように並べ、その中に勇者を表示しました。
最後に、KeyListener を使って矢印キーの入力を受け取り、勇者の座標を変更して画面を描き直しました。
今回の段階では、まだ敵もアイテムもありません。
しかし、勇者をフィールド上で動かせるようになると、次に作りたいものが見えてきます。
次回は、勇者が移動したときに敵と遭遇する処理を追加して、戦闘画面を表示してみます。