
前回は、勇者がフィールドを歩いていると、一定の確率でスライムと遭遇するようにしました。
敵と遭遇すると、戦闘画面が表示されます。
ただ、前回の段階では、まだ戦闘画面を表示しているだけでした。
勇者とスライムのアイコンは表示されますが、攻撃するわけでもなく、HPが減るわけでもありません。
これでは、まだ「戦闘画面っぽいもの」が出ているだけです。
今回は、戦闘画面に攻撃ボタンを追加して、勇者とスライムを実際に戦わせてみます。
今回作るもの
今回作るのは、かなりシンプルな戦闘です。
勇者が攻撃する。
スライムのHPが減る。
スライムが生きていれば反撃する。
勇者のHPが減る。
この流れを、攻撃ボタンを押すたびに実行します。
RPGの戦闘では、魔法やアイテムや会心の一撃などがありますが、まずは攻撃だけ作ります。
- 攻撃する
- ダメージを受ける
- HPが減る
- HPが0になったら倒れる
という流れだけを作ります。
最初から全部作ろうとすると大変なので、今回も必要なものだけを少しずつ追加していきます。
戦闘には攻撃力が必要になる
前回までの Chara クラスには、名前とHPがありました。
public class Chara {
String name;
int hp;
public void attack() {
System.out.println(name + "の攻撃!");
}
public void damage(int point) {
hp -= point;
if (hp < 0) {
hp = 0;
}
}
}
ここまででも、「攻撃!」と表示することはできます。
でも、今回は実際にスライムのHPを減らしたいです。
そのためには、勇者がどれくらいのダメージを与えるのかを決める必要があります。
そこで、Chara クラスに攻撃力を表す attackPower を追加します。
public class Chara {
String name;
int hp;
int attackPower;
public void attack() {
System.out.println(name + "の攻撃!");
}
public void damage(int point) {
hp -= point;
if (hp < 0) {
hp = 0;
}
}
}
attackPower は、攻撃したときに相手に与えるダメージ量として使います。
最初から攻撃力を入れていたわけではありません。
「戦闘でHPを減らしたい」
「そのためにはダメージ量が必要」
「だから攻撃力を追加する」
という流れです。
このように、必要になったタイミングでフィールドを追加していくと、クラスが無駄に大きくなりにくくなります。
attack() に攻撃相手を渡す
今の attack() メソッドは、ただメッセージを表示するだけです。
public void attack() {
System.out.println(name + "の攻撃!");
}
でも、今回やりたいのは、相手にダメージを与えることです。
そのためには、
誰を攻撃するのか
を attack() メソッドに渡す必要があります。
そこで、attack() を次のように変更します。
public void attack(Chara target) {
target.damage(attackPower);
}
target は、攻撃する相手です。
勇者がスライムを攻撃するなら、target にはスライムを渡します。
hero.attack(slime);
このように書くと、勇者がスライムを攻撃します。
attack() メソッドの中では、相手の damage() メソッドを呼び出しています。
target.damage(attackPower);
つまり、「自分の攻撃力に応じた分だけ、相手にダメージを与える」という処理になります。
HPが残っているか調べる
戦闘では、HPが0になったら倒れたことにしたいです。
そこで、Chara クラスに isAlive() メソッドを追加します。
public boolean isAlive() {
return hp > 0;
}
HPが0より大きければ生きています。
HPが0なら倒れています。
これで、戦闘中に次のような判定ができます。
if (!slime.isAlive()) {
System.out.println("スライムをたおした!");
}
ここまでをまとめると、Chara クラスは次のようになります。
public class Chara {
String name;
int hp;
int attackPower;
public void attack(Chara target) {
target.damage(attackPower);
}
public void damage(int point) {
hp -= point;
if (hp < 0) {
hp = 0;
}
}
public boolean isAlive() {
return hp > 0;
}
}
これで、キャラクター共通の戦闘処理が少し増えました。
勇者もスライムも、どちらも Chara を継承しています。
そのため、attack()、damage()、isAlive() は、勇者でもスライムでも使えます。
Hero と Slime に初期値を持たせる
次に、勇者とスライムの初期値を設定します。
勇者とスライムは、どちらも名前、HP、攻撃力を持っています。
ただし、値は違います。
そこで、それぞれのコンストラクタで初期値を設定します。
public class Hero extends Chara {
public Hero() {
name = "勇者";
hp = 30;
attackPower = 10;
}
}
public class Slime extends Chara {
public Slime() {
name = "スライム";
hp = 20;
attackPower = 5;
}
}
勇者はHP30、攻撃力10。
スライムはHP20、攻撃力5。
かなり単純な設定ですが、最初はこれで十分です。
HPや攻撃力のような項目は Chara にあります。
でも、実際の値は Hero や Slime で設定しています。
共通する部分は親クラスにまとめて、違いは子クラスで記述する。
ここでも継承が自然に使えています。
BattleFrame に勇者とスライムを持たせる
次は戦闘画面です。
戦闘画面では、勇者とスライムのHPを表示し、攻撃ボタンを押したら戦闘を進めたいです。
そのため、BattleFrame に Hero と Slime を持たせます。
public class BattleFrame extends JFrame {
private Rpg rpg;
private Hero hero;
private Slime slime;
private JLabel heroHpLabel;
private JLabel slimeHpLabel;
private JLabel messageLabel;
private JButton attackButton;
public BattleFrame(Rpg rpg) {
super("戦闘");
this.rpg = rpg;
this.hero = rpg.getHero();
this.slime = rpg.getSlime();
setDefaultCloseOperation(JFrame.DISPOSE_ON_CLOSE);
createScreen();
pack();
setLocationRelativeTo(null);
}
}
hero と slime は、戦闘で使うキャラクターです。
private Hero hero; private Slime slime;
HP表示用のラベルも用意します。
private JLabel heroHpLabel; private JLabel slimeHpLabel;
メッセージ表示用のラベルと、攻撃ボタンも用意します。
private JLabel messageLabel; private JButton attackButton;
これらをフィールドに配置しておき、攻撃ボタンが押されたときに、HPやメッセージを更新します。
戦闘画面を作る
画面を作る処理は、createScreen() メソッドにまとめます。
private void createScreen() {
setLayout(new BorderLayout());
messageLabel = new JLabel("スライムがあらわれた!", JLabel.CENTER);
add(messageLabel, BorderLayout.NORTH);
JPanel charaPanel = new JPanel(new GridLayout(2, 2));
charaPanel.add(new HeroLabel());
charaPanel.add(new SlimeLabel());
heroHpLabel = new JLabel(hero.name + " HP: " + hero.hp, JLabel.CENTER);
slimeHpLabel = new JLabel(slime.name + " HP: " + slime.hp, JLabel.CENTER);
charaPanel.add(heroHpLabel);
charaPanel.add(slimeHpLabel);
add(charaPanel, BorderLayout.CENTER);
attackButton = new JButton("攻撃");
add(attackButton, BorderLayout.SOUTH);
}
画面の上にはメッセージを表示します。
messageLabel = new JLabel("スライムがあらわれた!", JLabel.CENTER);
中央には、勇者とスライムのアイコンを表示します。
charaPanel.add(new HeroLabel()); charaPanel.add(new SlimeLabel());
その下に、それぞれのHPを表示します。
heroHpLabel = new JLabel(hero.name + " HP: " + hero.hp, JLabel.CENTER); slimeHpLabel = new JLabel(slime.name + " HP: " + slime.hp, JLabel.CENTER);
そして、画面の下に攻撃ボタンを置きます。
attackButton = new JButton("攻撃");
これで、戦闘画面らしい見た目になってきました。
攻撃ボタンを押したときの処理
次に、攻撃ボタンを押したときの処理を書きます。
- 勇者が攻撃する
- スライムが反撃する
- HPが減る
- 倒れたら止まる
攻撃ボタンが押された時に動作するのは Controller です。
今は攻撃だけですが、後で魔法や道具を使ったり、逃げたりするのを追加することを考えると、それぞれの役割を持った別のクラスにした方がよさそうです。
Java では、ボタンが押された時に、そのイベントを受取るインターフェースとして ActionListener が用意されています。
ActionListener の実装として、BattleAction を作成します。
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
public class BattleAction implements ActionListener {
@Override
public void actionPerformed(ActionEvent e) {
System.out.println("攻撃");
}
}
attackButton を作った後に、ボタンのイベントを受け取れるように、以下の処理を追加します。
attackButton.addActionListener(new BattleAction());
これで、攻撃ボタンを押した時に、コンソールに「攻撃」の文字が出るようになるはずです。
この中に「攻撃」の実際の処理を追加していきます。
BattleAction でも、勇者とスライムを取得できるように、コンストラクタで Rpg を渡しておきます。
攻撃後に BattleFrame のメッセージなどを更新する必要があるので、BattleFrame も渡しておきます。
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) {
System.out.println("攻撃");
Hero hero = rpg.getHero();
Slime slime = rpg.getSlime();
hero.attack(slime);
battleFrame.updateLabels();
if (!slime.isAlive()) {
return;
}
slime.attack(hero);
battleFrame.updateLabels();
}
}
最初に、勇者とスライムを Rpg から取得しておきます。
Hero hero = rpg.getHero(); Slime slime = rpg.getSlime();
まず、勇者がスライムを攻撃します。
hero.attack(slime);
そのあと、表示を更新します。
battleFrame.updateLabels();
スライムがまだ生きていれば、今度はスライムが勇者に反撃します。
slime.attack(hero);
BattleFrame の表示を更新します。
battleFrame.updateLabels();
かなり単純ですが、これで
勇者が攻撃する
スライムが反撃する
HPが減る
倒れたら止まる
という流れができました。
表示を更新する
表示を更新するために、updateLabels() メソッドを作ります。
public void updateLabels() {
heroHpLabel.setText(hero.name + " HP: " + hero.hp);
slimeHpLabel.setText(slime.name + " HP: " + slime.hp);
if (!slime.isAlive()) {
messageLabel.setText(slime.name + "をたおした!");
return;
}
if (!hero.isAlive()) {
messageLabel.setText(hero.name + "はたおれた。");
return;
}
messageLabel.setText(hero.name + " は " + slime.name + "と戦っている!");
}
戦闘でHPが変わったあと、このメソッドを呼べば、画面上のHP表示とメッセージも変わります。
ここでは、画面全体を作り直しているわけではありません。
HPを表示している JLabel と、メッセージを表示している JLabel の文字だけを書き換えています。
この方が処理も分かりやすく、画面の部品も管理しやすくなります。
BattleFrame の全体コード
ここまでをまとめると、BattleFrame は次のようになります。
import java.awt.BorderLayout;
import java.awt.GridLayout;
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 Hero hero;
private Slime slime;
private JLabel heroHpLabel;
private JLabel slimeHpLabel;
private JLabel messageLabel;
private JButton attackButton;
public BattleFrame(Rpg rpg) {
super("戦闘");
this.rpg = rpg;
this.hero = hero;
this.slime = slime;
setDefaultCloseOperation(JFrame.DISPOSE_ON_CLOSE);
createScreen();
attackButton.addActionListener(new BattleAction(rpg, this));
pack();
setLocationRelativeTo(null);
}
private void createScreen() {
setLayout(new BorderLayout());
messageLabel = new JLabel("スライムがあらわれた!", JLabel.CENTER);
add(messageLabel, BorderLayout.NORTH);
JPanel charaPanel = new JPanel(new GridLayout(2, 2));
charaPanel.add(new HeroLabel());
charaPanel.add(new SlimeLabel());
heroHpLabel = new JLabel(hero.name + " HP: " + hero.hp, JLabel.CENTER);
slimeHpLabel = new JLabel(slime.name + " HP: " + slime.hp, JLabel.CENTER);
charaPanel.add(heroHpLabel);
charaPanel.add(slimeHpLabel);
add(charaPanel, BorderLayout.CENTER);
attackButton = new JButton("攻撃");
add(attackButton, BorderLayout.SOUTH);
}
public void updateLabels() {
heroHpLabel.setText(hero.name + " HP: " + hero.hp);
slimeHpLabel.setText(slime.name + " HP: " + slime.hp);
if (!slime.isAlive()) {
messageLabel.setText(slime.name + "をたおした!");
return;
}
if (!hero.isAlive()) {
messageLabel.setText(hero.name + "はたおれた。");
return;
}
messageLabel.setText(hero.name + " は " + slime.name + "と戦っている!");
}
}
これで、攻撃ボタンを押すたびに、勇者とスライムが戦うようになります。
スライムのHPが0になれば、スライムを倒したことになります。
勇者のHPが0になれば、勇者が倒れたことになります。
今回作ったクラスの役割
今回の戦闘で関係するクラスを整理すると、次のようになります。
| クラス | 役割 |
|---|---|
| Chara | キャラクター共通の情報と処理を持つ |
| Hero | 勇者の初期値を持つ |
| Slime | スライムの初期値を持つ |
| BattleFrame | 戦闘画面を表示し、戦闘の流れを管理する |
| HeroLabel | 勇者のアイコンを表示する |
| SlimeLabel | スライムのアイコンを表示する |
| BattleAction | 攻撃ボタンが押された時の処理を実行する |
Chara は、キャラクター共通の戦闘処理を持っています。
Hero と Slime は、それぞれの初期値を持っています。
BattleFrame は、勇者とスライムを画面に表示し、攻撃ボタンが押されたときに戦闘を進めます。
このように、戦闘のデータや処理と、画面表示の処理を少しずつ組み合わせていくと、RPGらしい動きになっていきます。
まとめ
今回は、戦闘画面で勇者とスライムを実際に戦わせてみました。
まず、Chara に attackPower を追加しました。
次に、attack(Chara target) を作り、攻撃相手にダメージを与えられるようにしました。
また、HPが残っているかを調べるために isAlive() も追加しました。
Hero と Slime には、それぞれHPと攻撃力の初期値を設定しました。
そして、BattleFrame に勇者とスライムのアイコン、HP表示、攻撃ボタンを配置しました。
攻撃ボタンを押すと、勇者がスライムを攻撃し、スライムが生きていれば反撃します。
これで、ただ戦闘画面を表示するだけではなく、実際に戦闘が進むようになりました。
まだかなり単純な戦闘ですが、RPGとしては大きな一歩です。
次回は、戦闘メッセージをもう少し分かりやすくしたり、「逃げる」ボタンを追加したりして、戦闘画面を少しずつ改良していきます。