
前回は、フィールドを歩いていると、たまに やくそう を拾えるようにしました。
拾ったやくそうは、Hero がアイテムとして保持します。
ただし、Hero は「やくそう」を直接持っているわけではありません。
前回の記事では、Hero は拾ったものを Item として保持するようにしました。
つまり、勇者は「やくそうを持っている」というより、Itemを持っているという形になっています。
今回は、この Item を戦闘中に使えるようにします。
今はアイテムがやくそうだけなので、アイテムを使うとHPが回復します。
ただし、やくそう専用の処理にしてしまうと、あとで別のアイテムを追加したときに困ります。
そこで今回は、useHerb() のようなやくそう専用のメソッドではなく、useItem(Item item) という形にします。
そして、アイテムごとの効果は、アイテム自身に持たせるようにします。
今回作るもの
今回作る内容は、次の通りです。
Itemにeffect(Chara target)メソッドを追加するHerbでHPを回復する処理を実装するCharaにHPを回復するrecover()メソッドを追加するHeroにuseItem(Item item)メソッドを追加する- 戦闘画面に「アイテム」ボタンを追加する
- アイテムボタンを押したら、持っているアイテムを使う
- やくそうを使った場合はHPを回復する
- 使ったアイテムはアイテム一覧から削除する
今回は、アイテムを「持つ」だけでなく、「使う」処理を追加します。
ここで大事なのは、やくそうの効果を Hero に直接書かないことです。
やくそうを使ったときに何が起きるかは、やくそう自身が知っている方が自然です。
そこで、アイテムの効果は Item 側に持たせます。
アイテムに効果を持たせる
前回作った Item クラスは、アイテム名だけを持つシンプルなクラスでした。
public class Item {
private String name;
public Item(String name) {
this.name = name;
}
public String getName() {
return name;
}
}
今回は、アイテムを使ったときの効果を表す effect() メソッドを追加します。
public class Item {
private String name;
public Item(String name) {
this.name = name;
}
public String getName() {
return name;
}
public void effect(Chara target) {
}
}
effect() は、アイテムを使ったときの効果を表すメソッドです。
public void effect(Chara target) {
}
引数の target は、アイテムの効果を受ける相手です。
今回で言えば、やくそうを使って回復する相手は勇者です。
そのため、やくそうを使うときは、勇者を target として渡します。
Item クラスの effect() は空のままです。
具体的な効果は、Item を継承した子クラスで実装します。
Herbクラスで回復処理を実装する
次に、やくそうを表す Herb クラスを修正します。
やくそうを使うとHPが回復するようにしたいので、effect() メソッドをオーバーライドします。
public class Herb extends Item {
private int recoveryPoint;
public Herb() {
super("やくそう");
recoveryPoint = 10;
}
@Override
public void effect(Chara target) {
target.recover(recoveryPoint);
}
}
recoveryPoint は、回復量です。
private int recoveryPoint;
今回は、やくそうを使うとHPが10回復することにします。
recoveryPoint = 10;
そして、effect() の中で、対象のHPを回復しています。
@Override
public void effect(Chara target) {
target.recover(recoveryPoint);
}
これで、やくそうを使ったときの効果は Herb 自身が持つようになりました。
Hero 側に「やくそうならHPを10回復する」と書く必要はありません。
もし今後、別のアイテムを追加したくなったら、そのアイテムのクラスで effect() を実装すればよくなります。
CharaにHPを回復する処理を追加する
次に、HPを回復する処理を考えます。
HPは Chara クラスにあります。
これまで、HPを減らす処理として damage() を作りました。
public void damage(int point) {
hp -= point;
if (hp < 0) {
hp = 0;
}
}
今回は、HPを回復する処理が必要です。
そこで、Chara に recover() メソッドを追加します。
また、HPが最大値を超えないようにするために、最大HPを表す maxHp も追加します。
public class Chara {
String name;
int hp;
int maxHp;
int attackPower;
public void attack(Chara target) {
target.damage(attackPower);
}
public void damage(int point) {
hp -= point;
if (hp < 0) {
hp = 0;
}
}
public void recover(int point) {
hp += point;
if (hp > maxHp) {
hp = maxHp;
}
}
public boolean isAlive() {
return hp > 0;
}
}
maxHp は、そのキャラクターの最大HPです。
int maxHp;
recover() では、HPを回復したあと、最大HPを超えないようにしています。
public void recover(int point) {
hp += point;
if (hp > maxHp) {
hp = maxHp;
}
}
これで、やくそうを何度使っても、HPが最大HPを超えて増えることはありません。
Heroとまものの初期値を修正する
Chara に maxHp を追加したので、Hero、Slime、Ghost の初期値も修正します。
まず、Hero です。
public class Hero extends Chara {
public Hero() {
name = "勇者";
maxHp = 30;
hp = maxHp;
attackPower = 10;
}
}
maxHp を30にして、現在のHPである hp には maxHp を入れています。
maxHp = 30; hp = maxHp;
こうしておくと、初期状態ではHPが満タンになります。
Slime も修正します。
public class Slime extends Chara {
public Slime() {
name = "スライム";
maxHp = 20;
hp = maxHp;
attackPower = 5;
}
}
Ghost も同じです。
public class Ghost extends Chara {
public Ghost() {
name = "ゴースト";
maxHp = 15;
hp = maxHp;
attackPower = 8;
}
}
これで、どのキャラクターも最大HPを持つようになりました。
Heroがアイテムを使えるようにする
次に、Hero にアイテムを使う処理を追加します。
前回、Hero はアイテム一覧を持つようにしました。
private List<Item> items = new ArrayList<>();
今回は、このアイテム一覧の中から指定されたアイテムを使えるようにします。
メソッド名は useItem(Item item) にします。
import java.util.ArrayList;
import java.util.List;
public class Hero extends Chara {
private List<Item> items = new ArrayList<>();
public Hero() {
name = "勇者";
maxHp = 30;
hp = maxHp;
attackPower = 10;
}
public void addItem(Item item) {
items.add(item);
}
public int getItemCount() {
return items.size();
}
public Item getItem(int index) {
return items.get(index);
}
public boolean useItem(Item item) {
if (items.contains(item)) {
item.effect(this);
items.remove(item);
return true;
}
return false;
}
}
useItem() は、アイテムを使うためのメソッドです。
public boolean useItem(Item item)
指定されたアイテムを持っていれば使います。
使えた場合は true を返します。
持っていなかった場合は false を返します。
まず、items.contains(item) で、そのアイテムを持っているかどうかを確認します。
if (items.contains(item)) {
持っている場合は、アイテムの効果を発動します。
item.effect(this);
ここで this を渡しているのは、アイテムの効果を受ける相手が勇者自身だからです。
やくそうの場合は、Herb の effect() が呼ばれ、勇者のHPが回復します。
そのあと、使ったアイテムを一覧から削除します。
items.remove(item);
最後に、アイテムを使えたことを表すために true を返します。
return true;
指定されたアイテムを持っていなかった場合は、false を返します。
return false;
最初のアイテムを取得する
今回は、戦闘画面の「アイテム」ボタンを押したら、持っているアイテムのうち最初の1個を使うことにします。
そのために、Hero に getItem(int index) を用意しています。
public Item getItem(int index) {
return items.get(index);
}
これで、次のように最初のアイテムを取得できます。
Item item = hero.getItem(0);
本格的なRPGなら、アイテム一覧を画面に表示して、どのアイテムを使うか選べるようにしたいところです。
ただ、今回はまずシンプルに、最初のアイテムを使う形にしておきます。
戦闘画面にアイテムボタンを追加する
次に、戦闘画面を修正します。
これまでの BattleFrame には、攻撃ボタンがありました。
private JButton attackButton;
ここに、アイテムボタンを追加します。
private JButton itemButton;
今は持っているアイテムがやくそうだけなので、「アイテム」ボタンを押すと、実質的にはやくそうを使うことになります。
しかし、設計上はやくそう専用のボタンではなく、アイテムを使うためのボタンとして作っておきます。
createScreen() では、攻撃ボタンとアイテムボタンを並べて表示します。
JPanel buttonPanel = new JPanel();
attackButton = new JButton("攻撃");
attackButton.addActionListener(new BattleAction(rpg, this));
itemButton = new JButton("アイテム");
itemButton.addActionListener(new ItemAction(rpg, this));
buttonPanel.add(attackButton);
buttonPanel.add(itemButton);
add(buttonPanel, BorderLayout.SOUTH);
ボタンを2つ並べたいので、JPanel を使っています。
JPanel buttonPanel = new JPanel();
攻撃ボタンはこれまで通りです。
attackButton = new JButton("攻撃");
attackButton.addActionListener(new BattleAction(rpg, this));
新しく、アイテムボタンを追加します。
itemButton = new JButton("アイテム");
itemButton.addActionListener(new ItemAction(rpg, this));
ItemAction は、アイテムボタンが押されたときの処理を行うクラスです。
ItemActionクラスを作る
次に、アイテムボタンの処理を行う ItemAction クラスを作ります。
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
public class ItemAction implements ActionListener {
private Rpg rpg;
private BattleFrame battleFrame;
public ItemAction(Rpg rpg, BattleFrame battleFrame) {
this.rpg = rpg;
this.battleFrame = battleFrame;
}
@Override
public void actionPerformed(ActionEvent e) {
Hero hero = rpg.getHero();
if (hero.getItemCount() == 0) {
battleFrame.setMessage("アイテムを持っていない!");
return;
}
Item item = hero.getItem(0);
boolean used = hero.useItem(item);
if (used) {
battleFrame.setMessage(hero.name + "は" + item.getName() + "を使った!");
} else {
battleFrame.setMessage(item.getName() + "を使えなかった!");
}
battleFrame.updateLabels();
}
}
ItemAction は、BattleAction と同じように ActionListener を実装しています。
public class ItemAction implements ActionListener {
コンストラクタでは、Rpg と BattleFrame を受け取ります。
public ItemAction(Rpg rpg, BattleFrame battleFrame) {
this.rpg = rpg;
this.battleFrame = battleFrame;
}
ボタンが押されたら、勇者を取得します。
Hero hero = rpg.getHero();
まず、アイテムを持っているかどうかを確認します。
if (hero.getItemCount() == 0) {
battleFrame.setMessage("アイテムを持っていない!");
return;
}
アイテムを持っていない場合は、メッセージを表示して処理を終了します。
アイテムを持っている場合は、最初のアイテムを取得します。
Item item = hero.getItem(0);
そして、勇者にそのアイテムを使わせます。
boolean used = hero.useItem(item);
アイテムを使えた場合は、メッセージを表示します。
battleFrame.setMessage(hero.name + "は" + item.getName() + "を使った!");
最後に、HP表示を更新します。
battleFrame.updateLabels();
BattleFrameにメッセージ変更用メソッドを追加する
ItemAction から戦闘画面のメッセージを変更したいので、BattleFrame に setMessage() メソッドを追加します。
public void setMessage(String message) {
messageLabel.setText(message);
}
これで、ItemAction から次のようにメッセージを変更できます。
battleFrame.setMessage("アイテムを持っていない!");
画面の部品である messageLabel を直接外から操作するのではなく、setMessage() を通して変更するようにしています。
BattleFrameの修正例
BattleFrame の主な修正部分は、次のようになります。
import java.awt.BorderLayout;
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;
private JButton itemButton;
public BattleFrame(Rpg rpg) {
super("戦闘");
this.rpg = rpg;
setDefaultCloseOperation(JFrame.DISPOSE_ON_CLOSE);
setLayout(new BorderLayout());
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, BorderLayout.CENTER);
JPanel buttonPanel = new JPanel();
attackButton = new JButton("攻撃");
attackButton.addActionListener(new BattleAction(rpg, this));
itemButton = new JButton("アイテム");
itemButton.addActionListener(new ItemAction(rpg, this));
buttonPanel.add(attackButton);
buttonPanel.add(itemButton);
add(buttonPanel, 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);
itemButton.setEnabled(false);
return;
}
if (!hero.isAlive()) {
messageLabel.setText(hero.name + "はたおれた。");
attackButton.setEnabled(false);
itemButton.setEnabled(false);
return;
}
}
public void setMessage(String message) {
messageLabel.setText(message);
}
}
今回追加したのは、主に itemButton です。
private JButton itemButton;
攻撃ボタンとアイテムボタンを並べるために、buttonPanel を使っています。
JPanel buttonPanel = new JPanel();
そして、アイテムボタンには ItemAction を設定しています。
itemButton = new JButton("アイテム");
itemButton.addActionListener(new ItemAction(rpg, this));
また、戦闘が終わったときには、攻撃ボタンだけでなく、アイテムボタンも押せないようにしています。
itemButton.setEnabled(false);
アイテムごとの効果をItem側に持たせる意味
今回の大事なポイントは、アイテムの効果を Hero に直接書かなかったことです。
たとえば、Hero の中に次のような処理を書いてしまうこともできます。
if (item instanceof Herb) {
recover(10);
}
しかし、この形にすると、新しいアイテムを追加するたびに Hero を修正することになります。
たとえば、毒消し草を追加したら、Hero に毒を治す処理を追加する。
力の種を追加したら、Hero に攻撃力を上げる処理を追加する。
これでは、Hero がアイテムごとの効果をすべて知ることになってしまいます。
そこで今回は、アイテムごとの効果はアイテム側に持たせました。
public void effect(Chara target) {
target.recover(recoveryPoint);
}
Hero は、アイテムを使うだけです。
item.effect(this);
やくそうならHPを回復する。
別のアイテムなら、別の効果を発動する。
その違いは、それぞれのアイテムクラスに任せます。
このようにしておくと、アイテムの種類が増えても、Hero の修正を少なくできます。
アイテムボタンはまだ簡単な作りにしておく
今回は、アイテムボタンを押したら、持っている最初のアイテムを使うようにしています。
Item item = hero.getItem(0);
今は、拾えるアイテムがやくそうだけなので、この形でも問題ありません。
ただし、今後アイテムの種類が増えた場合は、どのアイテムを使うか選べるようにしたくなります。
たとえば、アイテム一覧を表示して、使うアイテムを選択する画面を作ることが考えられます。
今回は、まずアイテムを使う仕組みを作ることを優先します。
アイテム選択画面は、必要になったタイミングで作っていきます。
やくそうを使ったあと反撃させるか
ここで、少し考えるポイントがあります。
やくそうを使ったあと、敵に反撃させるかどうかです。
RPGによっては、アイテムを使うと1ターンを消費します。
その場合、やくそうを使ったあと、敵が反撃してきます。
一方で、今回はまだ戦闘処理をシンプルにしたいので、アイテムを使っても敵の反撃はなしにしておきます。
まずは、次の処理に集中します。
- アイテムを使う
- やくそうならHPが回復する
- 使ったアイテムが1個減る
敵の反撃を入れる場合は、次回以降、ターン制の戦闘として整理した方が分かりやすくなります。
今回のクラスの役割
今回の主なクラスの役割は、次のようになります。
Item:アイテム共通の情報と効果を持つHerb:やくそうを表し、HPを回復する効果を持つChara:HPを回復するrecover()を持つHero:アイテムを保持し、使うBattleFrame:戦闘画面にアイテムボタンを表示するItemAction:アイテムボタンが押されたときの処理を行う
アイテムを持っているのは Hero です。
HPを回復する処理は Chara にあります。
やくそうの効果は Herb にあります。
アイテムを使う処理は Hero にあります。
アイテムボタンを押したときの処理は ItemAction にあります。
画面表示は BattleFrame が担当します。
まとめ
今回は、戦闘中にアイテムを使ってHPを回復できるようにしました。
まず、Item に effect(Chara target) メソッドを追加しました。
次に、Herb で effect() をオーバーライドし、対象のHPを回復するようにしました。
Chara には、最大HPを表す maxHp と、HPを回復する recover() メソッドを追加しました。
Hero には、指定されたアイテムを使う useItem(Item item) メソッドを追加しました。
useItem() では、指定されたアイテムを持っていれば、そのアイテムの effect() を呼び出します。
item.effect(this);
これにより、アイテムごとの効果は、それぞれのアイテムクラスに任せることができます。
戦闘画面には「アイテム」ボタンを追加しました。
ボタンが押されたら、ItemAction が勇者にアイテムを使わせます。
今はアイテムがやくそうだけなので、アイテムを使うとHPが回復します。
これで、フィールドで拾ったやくそうを、戦闘中に使えるようになりました。
RPGらしさが少し増してきました。
次回は、アイテムを使ったときにも敵が反撃するようにして、ターン制の戦闘に近づけていきます。