
前回までの記事では、勇者やスライムなどのキャラクターをクラスとして作ってきました。
今回は、少しRPGらしくするために、勇者をフィールド上で動かしてみます。
文字だけのプログラムでもオブジェクト指向の考え方は学べますが、RPGを作るなら、やはり画面があった方が楽しくなります。
そこで今回は、JavaのGUI機能を使って、草原のフィールドを表示し、その上で勇者を動かしてみます。
ただし、ここで大事なことがあります。
画面を表示するクラスに、ゲームの状態や処理を全部書いてしまうと、あとからプログラムが分かりにくくなります。
たとえば、勇者の位置、敵との遭遇、戦闘、HPの変化などを、すべて画面クラスに書いてしまうと、画面クラスがどんどん大きくなってしまいます。
そこで今回は、次のように役割を分けて作ります。
| クラス | 役割 |
|---|---|
| Rpg | ゲームの状態を管理する |
| RpgFrame | ゲームの状態を画面に表示する |
| Hero | 勇者の情報を持つ |
| HeroLabel | 勇者の画像を表示する |
| FieldLabel | フィールド画像を表示する |
| GuiApp | アプリケーションを起動する |
Rpg は、勇者がどこにいるかを管理します。
RpgFrame は、その Rpg の状態を見て、画面を描画します。
このように、ゲームの状態を持つクラスと、画面を表示するクラスを分けておくと、あとから敵や戦闘を追加しやすくなります。
MVCを少し意識して作る
今回のプログラムでは、MVCの考え方を少し意識します。
MVCとは、プログラムの役割を大きく次の3つに分けて考える方法です。
| 名前 | 役割 |
|---|---|
| Model | データや処理を管理する |
| View | 画面に表示する |
| Controller | 入力を受け取り、Modelを操作する |
今回のプログラムでは、Rpg が Model の役割を持ちます。
RpgFrame は、Rpg の状態を画面に表示するので View の役割を持ちます。
また、RpgFrame はキー入力も受け取るので、Controller に近い役割も少し持っています。
本格的にMVCを分けようとすると、Controller用のクラスを別に作ることもできます。
ただ、最初から細かく分けすぎると、かえって分かりにくくなります。
今回はまず、
ゲームの状態は Rpg に持たせる
画面表示は RpgFrame に任せる
キー入力を受けたら Rpg に命令する
という形で作っていきます。
JavaでGUIを作るためのSwingとは
今回は、画面を作るために Swing を使います。
Swing は、JavaでGUIアプリケーションを作るための仕組みです。
GUIとは、ウィンドウ、ボタン、画像、ラベルなどを使って操作できる画面のことです。
普段使っているアプリの多くは、文字だけではなく、ボタンや画像を使って操作できるようになっています。
Swingを使うと、Javaでもそのような画面を作ることができます。
今回使う主なクラスは、次の通りです。
| クラス | 役割 |
|---|---|
| JFrame | ウィンドウを表示する |
| JLabel | 文字や画像を表示する |
| ImageIcon | 画像を読み込む |
| GridBagLayout | 部品をマス目のように配置する |
| KeyListener | キーボード入力を受け取る |
Swingにはたくさんのクラスがありますが、最初から全部覚える必要はありません。
今回は、RPGのフィールド画面を作るために必要なものだけを使います。
今回作るもの
今回は、次のような画面を作ります。
- 草原のフィールドを表示する
- フィールド上に勇者を表示する
- 矢印キーで勇者を上下左右に動かす
- 勇者がフィールドの外に出ないようにする
まだ敵は出てきません。
戦闘もありません。
まずは、勇者がフィールド上を移動できるところまで作ります。
Rpgクラスを作る
まず、ゲームの状態を管理する Rpg クラスを作ります。
今回は、勇者の位置を管理します。
public class Rpg {
public static final int FIELD_WIDTH = 10;
public static final int FIELD_HEIGHT = 8;
private Hero hero;
private int heroX;
private int heroY;
public Rpg() {
hero = new Hero();
heroX = 0;
heroY = 0;
}
public Hero getHero() {
return hero;
}
public int getHeroX() {
return heroX;
}
public int getHeroY() {
return heroY;
}
public void moveLeft() {
if (heroX > 0) {
heroX--;
}
}
public void moveRight() {
if (heroX < FIELD_WIDTH - 1) {
heroX++;
}
}
public void moveUp() {
if (heroY > 0) {
heroY--;
}
}
public void moveDown() {
if (heroY < FIELD_HEIGHT - 1) {
heroY++;
}
}
}
FIELD_WIDTH は、フィールドの横のマス数です。
public static final int FIELD_WIDTH = 10;
FIELD_HEIGHT は、フィールドの縦のマス数です。
public static final int FIELD_HEIGHT = 8;
今回は、横10マス、縦8マスのフィールドにします。
勇者の位置を管理する
heroX と heroY は、勇者の位置です。
private int heroX; private int heroY;
今回は、フィールドをマス目として考えます。
左上を (0, 0) とします。
右に進むと heroX が増えます。
下に進むと heroY が増えます。
たとえば、勇者が左上にいるときは、次のような状態です。
heroX = 0 heroY = 0
右に1マス動くと、次のようになります。
heroX = 1 heroY = 0
下に1マス動くと、次のようになります。
heroX = 1 heroY = 1
このように、勇者の位置を数値で管理します。
移動メソッドを作る
勇者を動かすために、次の4つのメソッドを作っています。
moveLeft() moveRight() moveUp() moveDown()
左に動く処理は、次のようになります。
public void moveLeft() {
if (heroX > 0) {
heroX--;
}
}
heroX が0より大きいときだけ、左に動かします。
もし heroX が0なら、すでに一番左にいます。
その状態でさらに左へ動くと、フィールドの外に出てしまいます。
そのため、heroX > 0 のときだけ heroX-- しています。
右に動く処理は、次のようになります。
public void moveRight() {
if (heroX < FIELD_WIDTH - 1) {
heroX++;
}
}
フィールドの横幅は FIELD_WIDTH です。
ただし、座標は0から始まります。
横幅が10なら、使える x の値は、0から9までです。
そのため、右端は FIELD_WIDTH - 1 になります。
上に動く処理は、次のようになります。
public void moveUp() {
if (heroY > 0) {
heroY--;
}
}
下に動く処理は、次のようになります。
public void moveDown() {
if (heroY < FIELD_HEIGHT - 1) {
heroY++;
}
}
このように、フィールドの外に出ないかどうかの判定も `Rpg` に書いています。
これは、勇者の位置を管理しているのが `Rpg` だからです。
RpgFrame は、「勇者がどこにいるか」や「移動できるかどうか」を判断しません。
その判断は Rpg に任せます。
moveHeroではなく上下左右のメソッドにする理由
移動処理は、次のように作ることもできます。
public void moveHero(int dx, int dy) {
heroX += dx;
heroY += dy;
}
この形にすると、移動量を引数で指定できます。
ただし、今回はこの形にはしません。
なぜなら、次のような呼び出しもできてしまうからです。
rpg.moveHero(2, 0); rpg.moveHero(1, 1); rpg.moveHero(100, 0);
今回作りたいのは、矢印キーを押したら上下左右に1マスずつ動くRPGです。
2マス動いたり、斜めに動いたり、いきなり100マス動いたりする必要はありません。
そこで、今回は移動方向ごとにメソッドを分けています。
rpg.moveLeft(); rpg.moveRight(); rpg.moveUp(); rpg.moveDown();
この方が、できることがはっきりします。
「左に動く」「右に動く」「上に動く」「下に動く」という意味も、メソッド名から分かりやすくなります。
必要以上に自由なメソッドを作らないことも、プログラムを分かりやすくするためには大切です。
HeroLabelクラスを作る
次に、勇者の画像を表示するための HeroLabel クラスを作ります。
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(48, 48, Image.SCALE_SMOOTH);
setIcon(new ImageIcon(image));
}
}
HeroLabel は、勇者の画像を表示するためのクラスです。
JLabel は、文字や画像を表示できるSwingのクラスです。
今回は、JLabel を継承して、勇者専用のラベルを作っています。
public class HeroLabel extends JLabel {
画像ファイルには、`hero.png` を使うことにします。
ImageIcon icon = new ImageIcon("hero.png");
画像サイズは、48×48ピクセルにしています。
Image image = icon.getImage().getScaledInstance(48, 48, Image.SCALE_SMOOTH);
最後に、読み込んだ画像をラベルに設定します。
setIcon(new ImageIcon(image));
これで、HeroLabel を画面に追加すると、勇者の画像が表示されます。
FieldLabelクラスを作る
次に、フィールドの画像を表示する FieldLabel クラスを作ります。
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(48, 48, Image.SCALE_SMOOTH);
setIcon(new ImageIcon(image));
}
}
FieldLabel は、草原の画像を表示するためのクラスです。
今回は、grass.png という画像を使うことにします。
勇者がいない場所には、この FieldLabel を表示します。
HeroLabel と FieldLabel は、どちらも画像を表示するためのクラスです。
少し似ていますが、今回はまだそのままにしておきます。
今後、スライムなどの画像も表示するようになると、画像表示の共通処理を親クラスにまとめたくなるかもしれません。
そのときに、あらためて整理していきます。
RpgFrameクラスを作る
次に、画面を表示する RpgFrame クラスを作ります。
RpgFrame は、Rpg の状態を見て画面を描画します。
import java.awt.GridBagConstraints;
import java.awt.GridBagLayout;
import java.awt.event.KeyEvent;
import java.awt.event.KeyListener;
import javax.swing.JFrame;
public class RpgFrame extends JFrame implements KeyListener {
private Rpg rpg;
public RpgFrame(Rpg rpg) {
super("RPG");
this.rpg = rpg;
setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
setLayout(new GridBagLayout());
drawField();
addKeyListener(this);
setFocusable(true);
pack();
setLocationRelativeTo(null);
}
}
RpgFrame は、Rpg をフィールドとして持っています。
private Rpg rpg;
コンストラクタで Rpg を受け取っています。
public RpgFrame(Rpg rpg) {
this.rpg = rpg;
}
ここが大事なところです。
RpgFrame の中で new Rpg() しても、プログラムは動きます。
しかし、画面クラスがゲーム本体を勝手に作ってしまうと、あとから戦闘画面などを追加するときに扱いにくくなります。
そこで、Rpg は外から渡すようにしています。
RpgFrame は、渡された Rpg の状態を画面に表示します。
フィールドを描画する
次に、フィールドを描画する 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();
}
画面上にHeroLabelやFieldLabelを追加するためのadd()メソッドを用意します。
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);
}
}
まず、画面上にある部品をすべて削除します。
getContentPane().removeAll();
そのあと、二重の for 文でフィールド全体を描画します。
x と y は、フィールド上の座標です。
GridBagConstraints を使って、部品を配置する位置を指定します。
もし現在の座標が勇者の位置なら、HeroLabel を表示し、それ以外の場所では FieldLabel を表示します。
if (x == rpg.getHeroX() && y == rpg.getHeroY()) {
add(pane, new HeroLabel(), x, y, 1, 1);
} else {
add(pane, new FieldLabel(), x, y, 1, 1);
}
ここで重要なのは、RpgFrame が勇者の位置を自分で持っていないことです。
勇者の位置は、Rpg から取得しています。
rpg.getHeroX() rpg.getHeroY()
つまり、RpgFrame は Rpg の状態を見て、その状態を画面に表示しているだけです。
最後に、画面を更新します。
revalidate(); repaint();
revalidate() は、部品の配置をやり直すための処理です。
repaint() は、画面を再描画するための処理です。
キー入力で勇者を動かす
次に、上下左右の矢印キーで勇者を動かします。
@Override
public void keyPressed(KeyEvent e) {
if (e.getKeyCode() == KeyEvent.VK_LEFT) {
rpg.moveLeft();
}
if (e.getKeyCode() == KeyEvent.VK_RIGHT) {
rpg.moveRight();
}
if (e.getKeyCode() == KeyEvent.VK_UP) {
rpg.moveUp();
}
if (e.getKeyCode() == KeyEvent.VK_DOWN) {
rpg.moveDown();
}
drawField();
}
各キーが押されたら、Rpg に移動の情報を伝えます。
ここでも、RpgFrame が直接 heroX や heroY を変更していないことに注目してください。
RpgFrame は、キー入力を受け取って、Rpg に「左へ動いて」「右へ動いて」と伝えているだけです。
実際に勇者の位置を変えるのは Rpg です。
そのあと、drawField() を呼び出して、画面を描き直します。
drawField();
このようにすると、処理の役割が分かりやすくなります。
- 勇者の位置を管理するのは Rpg
- キー入力を受け取るのは RpgFrame
- 画面を描き直すのも RpgFrame
という分担です。
KeyListenerの残りのメソッド
KeyListener を使う場合は、keyPressed() 以外のメソッドも書く必要があります。
KeyListener インターフェースで用意しているメソッドは、implementsしたクラスでは、すべてを用意しないといけないという Java のお約束です。
今回は使わないので、中身は空にしておきます。
@Override
public void keyReleased(KeyEvent e) {
}
@Override
public void keyTyped(KeyEvent e) {
}
RpgFrameの全体コード
ここまでをまとめると、RpgFrame は次のようになります。
import java.awt.GridBagConstraints;
import java.awt.GridBagLayout;
import java.awt.event.KeyEvent;
import java.awt.event.KeyListener;
import javax.swing.JFrame;
public class RpgFrame extends JFrame implements KeyListener {
private Rpg rpg;
public RpgFrame(Rpg rpg) {
super("RPG");
this.rpg = rpg;
setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
setLayout(new GridBagLayout());
drawField();
addKeyListener(this);
setFocusable(true);
pack();
setLocationRelativeTo(null);
}
private void drawField() {
getContentPane().removeAll();
for (int y = 0; y < Rpg.FIELD_HEIGHT; y++) {
for (int x = 0; x < Rpg.FIELD_WIDTH; x++) {
if (x == rpg.getHeroX() && y == rpg.getHeroY()) {
add(pane, new HeroLabel(), x, y, 1, 1);
} else {
add(pane, new FieldLabel(), x, y, 1, 1);
}
}
}
revalidate();
repaint();
}
@Override
public void keyPressed(KeyEvent e) {
if (e.getKeyCode() == KeyEvent.VK_LEFT) {
rpg.moveLeft();
}
if (e.getKeyCode() == KeyEvent.VK_RIGHT) {
rpg.moveRight();
}
if (e.getKeyCode() == KeyEvent.VK_UP) {
rpg.moveUp();
}
if (e.getKeyCode() == KeyEvent.VK_DOWN) {
rpg.moveDown();
}
drawField();
}
@Override
public void keyReleased(KeyEvent e) {
}
@Override
public void keyTyped(KeyEvent e) {
}
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);
}
}
}
この RpgFrame は、勇者の位置を自分では管理していません。
勇者の位置は Rpg にあります。
RpgFrame は、Rpg の状態を見て画面を描き、キー入力があれば Rpg に命令しています。
アプリケーションを起動する
最後に、アプリケーションを起動するクラスを作ります。
import javax.swing.SwingUtilities;
public class GuiApp {
public static void main(String[] args) {
SwingUtilities.invokeLater(() -> {
Rpg rpg = new Rpg();
RpgFrame frame = new RpgFrame(rpg);
frame.setVisible(true);
});
}
}
まず、Rpg オブジェクトを作ります。
Rpg rpg = new Rpg();
次に、その Rpg を RpgFrame に渡します。
RpgFrame frame = new RpgFrame(rpg);
これで、RpgFrame は Rpg の状態を参照しながら画面に表示できるようになります。
RpgFrame の中で new Rpg() するのではなく、外で作った Rpg を渡しているところがポイントです。
このようにしておくと、あとから戦闘画面を作るときにも、同じ Rpg を渡すことができます。
BattleFrame battleFrame = new BattleFrame(rpg);
このようにすれば、フィールド画面と戦闘画面が、同じゲームの状態を扱えるようになります。
## 今回のクラスの関係
今回のクラスの関係を簡単に表すと、次のようになります。
+---------+
| GuiApp |
+---------+
|
| 作る
v
+-------------+
| Rpg | ← Model
+-------------+
| hero |
| heroX |
| heroY |
+-------------+
| moveLeft() |
| moveRight() |
| moveUp() |
| moveDown() |
+-------------+
^
| 状態を見る
| 操作する
|
+------------------+
| RpgFrame | ← View + 入力受付
+------------------+
| rpg |
+------------------+
| drawField() |
| keyPressed() |
+------------------+
Rpg はゲームの状態を持ちます。
RpgFrame は、その状態を見て画面を描きます。
キーが押されたら、RpgFrame は Rpg に「勇者を動かして」と伝えます。
このように分けておくと、あとから敵や戦闘を追加するときも、プログラムを整理しやすくなります。
ModelとViewを分けるメリット
今回のような小さなプログラムなら、RpgFrame に全部書いても動きます。
RpgFrame に heroX や heroY を持たせて、キー入力で直接値を変更しても、勇者を動かすことはできます。
でも、それを続けると、あとから困ります。
- 敵との遭遇を追加したい。
- 戦闘画面を追加したい。
- HPを表示したい。
- アイテムを使えるようにしたい。
- ゲームオーバーを判定したい。
このような処理をすべて RpgFrame に書いていくと、RpgFrame が画面表示のクラスなのか、ゲーム本体なのか分からなくなります。
そこで、ゲームの状態やルールは Rpg に置きます。
画面表示は RpgFrame に任せます。
この分け方をしておくと、プログラムが少し大きくなっても、どこに何を書くべきか分かりやすくなります。
まとめ
今回は、JavaのSwingを使って、勇者をフィールド上で動かしてみました。
ただ画面に勇者を表示するだけでなく、ゲームの状態を管理する Rpg クラスと、画面を表示する RpgFrame クラスを分けて作りました。
Rpg は、勇者の位置を管理します。
RpgFrame は、Rpg の状態を見て画面を描画します。
矢印キーが押されたら、RpgFrame は Rpg に勇者を動かすように伝えます。
そして、変更された Rpg の状態をもとに、画面を描き直します。
このように、ModelとViewを分けておくと、プログラムが大きくなっても整理しやすくなります。
最初から完璧なMVCを作る必要はありません。
しかし、ゲームの状態と画面表示を分けて考えることは、基礎の段階でもとても大切です。
次回は、勇者がフィールドを歩いていると、一定の確率で敵と遭遇するようにしてみます。
敵と遭遇したら、戦闘画面を表示するところまで作っていきます。