
前回は、戦闘画面に攻撃ボタンを追加し、勇者とスライムを戦わせるところまで作りました。
勇者が攻撃する。
スライムが反撃する。
HPが減る。
HPが0になったら倒れる。
かなり単純な形ですが、RPGの戦闘らしい動きが作れました。
しかし、今のままだと敵はスライムだけです。
RPGを作るなら、スライム以外のまものも登場させたいところです。
そこで今回は、新しいまものとして ゴースト を追加します。
そして、単にゴーストを追加するのではなく、「オブジェクト指向で作っておくと、簡単に敵の種類を増やせる」ことを実感しましょう。
敵の種類を増やしてみる
これまで、まものとして Slime クラスを作ってきました。
public class Slime extends Chara {
public Slime() {
name = "スライム";
hp = 20;
attackPower = 5;
}
}
Slime は Chara を継承しています。
そのため、Chara にある name、hp、attackPower を使うことができます。
また、attack()、damage()、isAlive() も使えます。
新しく追加するゴーストも、まものの一種です。
名前、HP、攻撃力を持ち、攻撃したり、ダメージを受けたりします。
つまり、ゴーストも Chara を継承したクラスとして作れます。
Ghostクラスを作る
新しいまものとして、Ghost クラスを作ります。
public class Ghost extends Chara {
public Ghost() {
name = "ゴースト";
hp = 15;
attackPower = 8;
}
}
Ghost も Chara を継承しています。
そのため、Slime と同じように、名前、HP、攻撃力を持つことができます。
今回は、スライムよりHPは少し低く、攻撃力は少し高くしてみました。
name = "ゴースト";
hp = 15;
attackPower = 8;
このように、同じ Chara を継承していても、初期値を変えることで違うまものとして表現できます。
SlimeもGhostもCharaとして扱える
ここで大事なのは、Slime も Ghost も、どちらも Chara を継承していることです。
そのため、次のように Chara 型の変数に入れることができます。
Chara enemy; enemy = new Slime(); enemy = new Ghost();
enemy という変数には、スライムを入れることもできますし、ゴーストを入れることもできます。
このように、親クラスの型で子クラスのオブジェクトを扱えることは、オブジェクト指向の大きな特徴です。
敵の種類が増えても、Chara として扱うことで、戦闘処理を大きく変えずに済みます。
Rpgクラスのslimeをenemyに変更する
次に、Rpg クラスを修正します。
これまでは、敵としてスライムを持っていました。
private Slime slime;
しかし、これからはスライム以外の敵も登場します。
そこで、スライム専用の slime ではなく、敵を表す enemy に変更します。
private Chara enemy;
enemy の型は Chara です。
スライムもゴーストも Chara を継承しているので、どちらも enemy に入れられます。
enemy = new Slime(); enemy = new Ghost();
この変更により、Rpg は「今の敵がスライムかゴーストか」を細かく気にしなくてよくなります。
Rpg は、ただ「今戦っている敵」として enemy を持つだけです。
Rpgクラスの修正版
Rpg クラスは、次のようになります。
import java.util.Random;
public class Rpg {
public static final int FIELD_WIDTH = 10;
public static final int FIELD_HEIGHT = 8;
private Hero hero;
private Chara enemy;
private int heroX;
private int heroY;
private boolean inBattle;
private Random random = new Random();
public Rpg() {
hero = new Hero();
heroX = 0;
heroY = 0;
inBattle = false;
}
public Hero getHero() {
return hero;
}
public Chara getEnemy() {
return enemy;
}
public int getHeroX() {
return heroX;
}
public int getHeroY() {
return heroY;
}
public boolean isInBattle() {
return inBattle;
}
public boolean moveLeft() {
if (heroX > 0) {
heroX--;
return true;
}
return false;
}
public boolean moveRight() {
if (heroX < FIELD_WIDTH - 1) {
heroX++;
return true;
}
return false;
}
public boolean moveUp() {
if (heroY > 0) {
heroY--;
return true;
}
return false;
}
public boolean moveDown() {
if (heroY < FIELD_HEIGHT - 1) {
heroY++;
return true;
}
return false;
}
public boolean encounter() {
if (inBattle) {
return false;
}
int value = random.nextInt(100);
if (value < 20) {
createEnemy();
inBattle = true;
return true;
}
return false;
}
private void createEnemy() {
int monsterType = random.nextInt(2);
if (monsterType == 0) {
enemy = new Slime();
} else {
enemy = new Ghost();
}
}
public void endBattle() {
inBattle = false;
enemy = null;
}
}
大きな変更点は、敵を Chara 型の enemy として持つようにしたことです。
private Chara enemy;
そして、敵を取得するメソッドも getEnemy() にしました。
public Chara getEnemy() {
return enemy;
}
これまでは getSlime() でスライムを取得していました。
しかし、これからは敵がスライムとは限りません。
そのため、getEnemy() という名前にしています。
createEnemy()で敵を作る
敵を作る処理は、createEnemy() メソッドにまとめました。
private void createEnemy() {
int monsterType = random.nextInt(2);
if (monsterType == 0) {
enemy = new Slime();
} else {
enemy = new Ghost();
}
}
random.nextInt(2) は、0か1をランダムに返します。
0ならスライムを作り、1ならゴーストを作ります。
どちらの場合も、代入先は enemy です。
enemy は Chara 型なので、Slime も Ghost も入れることができます。
ここが、今回の重要なポイントです。
private Chara enemy;
このようにしておくと、敵の種類が増えても、戦闘画面や戦闘処理の多くをそのまま使えます。
BattleActionを修正する
次に、攻撃ボタンの処理を行う BattleAction を修正します。
これまでは、スライムを相手にしていました。
Slime slime = rpg.getSlime();
しかし、これからは敵がスライムとは限りません。
そこで、Chara 型の enemy を使います。
Chara enemy = rpg.getEnemy();
修正版の BattleAction は次のようになります。
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
public class BattleAction implements ActionListener {
private Rpg rpg;
private BattleFrame battleFrame;
public BattleAction(Rpg rpg, BattleFrame battleFrame) {
this.rpg = rpg;
this.battleFrame = battleFrame;
}
@Override
public void actionPerformed(ActionEvent e) {
Hero hero = rpg.getHero();
Chara enemy = rpg.getEnemy();
hero.attack(enemy);
if (enemy.isAlive()) {
enemy.attack(hero);
}
battleFrame.updateLabels();
}
}
まず、Rpg から勇者を取得します。
Hero hero = rpg.getHero();
次に、敵を取得します。
Chara enemy = rpg.getEnemy();
ここでは、敵がスライムかゴーストかを気にしていません。
どちらも Chara だからです。
勇者は敵を攻撃します。
hero.attack(enemy);
敵がまだ生きていれば、敵が勇者に反撃します。
if (enemy.isAlive()) {
enemy.attack(hero);
}
この処理も、敵がスライムでもゴーストでも同じです。
enemy は Chara 型なので、attack() や isAlive() を呼び出せます。
BattleFrameを修正する
次に、戦闘画面を表示する BattleFrame を修正します。
今までは、スライムのHPを表示していました。
これからは、敵のHPを表示します。
そのため、slimeHpLabel ではなく、enemyHpLabel に変更します。
private JLabel enemyHpLabel;
createScreen() では、敵の名前とHPを表示します。
enemyHpLabel = new JLabel(
rpg.getEnemy().name + " HP: " + rpg.getEnemy().hp,
JLabel.CENTER);
全体は次のようになります。
import java.awt.GridLayout;
import java.awt.event.WindowAdapter;
import java.awt.event.WindowEvent;
import javax.swing.JButton;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JPanel;
public class BattleFrame extends JFrame {
private Rpg rpg;
private JLabel heroHpLabel;
private JLabel enemyHpLabel;
private JLabel messageLabel;
private JButton attackButton;
public BattleFrame(Rpg rpg) {
super("戦闘");
this.rpg = rpg;
setDefaultCloseOperation(JFrame.DISPOSE_ON_CLOSE);
addWindowListener(new WindowAdapter() {
@Override
public void windowClosed(WindowEvent e) {
rpg.endBattle();
}
});
createScreen();
pack();
setLocationRelativeTo(null);
}
private void createScreen() {
JPanel panel = new JPanel(new GridLayout(3, 2));
messageLabel = new JLabel(rpg.getEnemy().name + "があらわれた!", JLabel.CENTER);
panel.add(messageLabel);
panel.add(new JLabel(""));
panel.add(new HeroLabel());
panel.add(createEnemyLabel());
heroHpLabel = new JLabel(
rpg.getHero().name + " HP: " + rpg.getHero().hp,
JLabel.CENTER);
enemyHpLabel = new JLabel(
rpg.getEnemy().name + " HP: " + rpg.getEnemy().hp,
JLabel.CENTER);
panel.add(heroHpLabel);
panel.add(enemyHpLabel);
add(panel);
attackButton = new JButton("攻撃");
attackButton.addActionListener(new BattleAction(rpg, this));
add(attackButton, java.awt.BorderLayout.SOUTH);
}
private ImageLabel createEnemyLabel() {
Chara enemy = rpg.getEnemy();
if (enemy instanceof Slime) {
return new SlimeLabel();
}
if (enemy instanceof Ghost) {
return new GhostLabel();
}
return new ImageLabel("unknown.png");
}
public void updateLabels() {
Hero hero = rpg.getHero();
Chara enemy = rpg.getEnemy();
heroHpLabel.setText(hero.name + " HP: " + hero.hp);
enemyHpLabel.setText(enemy.name + " HP: " + enemy.hp);
if (!enemy.isAlive()) {
messageLabel.setText(enemy.name + "をたおした!");
attackButton.setEnabled(false);
return;
}
if (!hero.isAlive()) {
messageLabel.setText(hero.name + "はたおれた。");
attackButton.setEnabled(false);
return;
}
messageLabel.setText(hero.name + "は" + enemy.name + "と戦っている!");
}
}
ここで、敵の画像を作るために createEnemyLabel() メソッドを作っています。
private ImageLabel createEnemyLabel() {
Chara enemy = rpg.getEnemy();
if (enemy instanceof Slime) {
return new SlimeLabel();
}
if (enemy instanceof Ghost) {
return new GhostLabel();
}
return new ImageLabel("unknown.png");
}
rpg.getEnemy() で、現在の敵を取得します。
敵が Slime の場合は、SlimeLabel を返し、敵が Ghost の場合は、GhostLabel を返します。
これで、敵の種類によって表示する画像を変えることができます。
ただし、敵が増えるたびに if 文を追加する必要があります。
ここは少し気になるところです。
今回はまず、敵の種類を増やすことを優先して、この形で作ります。
今後、敵の画像表示ももっと整理したくなったら、別の方法を考えていきます。
GhostLabelを作る
ゴーストの画像を表示するために、GhostLabel クラスを作ります。
public class GhostLabel extends ImageLabel {
public GhostLabel() {
super("ghost.png");
}
}
GhostLabel は ImageLabel を継承しています。
そのため、画像を読み込む処理や、サイズを変更する処理を書く必要はありません。
super("ghost.png") と書くだけで、ゴースト画像を表示できます。
このように、画像表示の共通処理を ImageLabel にまとめておくと、新しい画像ラベルを追加するのも簡単です。
スライムのときと同じように、ゴーストも短いクラスで追加できました。
敵の種類を増やしても戦闘処理は変わらない
今回の大きなポイントは、敵の種類を増やしても、戦闘処理の中心はほとんど変わらないことです。
BattleAction では、敵を Chara として扱っています。
Chara enemy = rpg.getEnemy();
そのため、敵がスライムでもゴーストでも、同じように攻撃できます。
hero.attack(enemy);
敵が生きていれば、同じように反撃できます。
enemy.attack(hero);
ここで、BattleAction は敵がスライムかゴーストかを気にしていません。
Chara として扱っているからです。
今回のクラスの関係
今回の変更後の関係を簡単に整理すると、次のようになります。
+-------------+
| Chara |
+-------------+
| name |
| hp |
| attackPower |
+-------------+
| attack() |
| damage() |
| isAlive() |
+-------------+
△
|
+----------------+----------------+
| | |
+---------+ +---------+ +---------+
| Hero | | Slime | | Ghost |
+---------+ +---------+ +---------+
+-------------+
| Rpg |
+-------------+
| hero |
| enemy |
| inBattle |
+-------------+
| getHero() |
| getEnemy() |
| encounter() |
| endBattle() |
+-------------+
+-------------------+
| BattleAction |
+-------------------+
| rpg |
| battleFrame |
+-------------------+
| actionPerformed() |
+-------------------+
Slime も Ghost も Chara を継承しています。
Rpg は、現在の敵を Chara enemy として持っています。
BattleAction は、その enemy に対して攻撃処理を行います。
敵の種類が増えても、BattleAction は Chara として扱えばよいので、戦闘処理を大きく変えずに済みます。
まとめ
今回は、新しいまものとしてゴーストを追加しました。
まず、Ghost クラスを作り、Chara を継承しました。
ゴーストは、名前、HP、攻撃力を持ちます。
また、Chara を継承しているので、attack()、damage()、isAlive() も使えます。
次に、Rpg クラスの slime を enemy に変更しました。
敵がスライムだけなら Slime 型でよかったのですが、敵の種類が増えるとスライム専用では困ります。
そこで、Chara 型の enemy として扱うようにしました。
これにより、enemy には Slime も Ghost も入れられるようになりました。
BattleAction でも、敵を Chara として扱うようにしました。
そのため、敵がスライムでもゴーストでも、同じように攻撃や反撃の処理ができます。
これが、オブジェクト指向で作る大きなメリットです。
共通する情報や処理を親クラスにまとめておくと、新しい種類を追加しやすくなります。