オブジェクト指向とかデザインパターンとか開発プロセスとかツールとか

satoshi's ソフトウェア開発

js



当サイトはアフィリエイト広告を利用してます。

Java UML オブジェクト指向

JavaでRPGを作る|戦闘画面で勇者とスライムを戦わせよう

投稿日:


前回は、勇者がフィールドを歩いていると、一定の確率でスライムと遭遇するようにしました。

敵と遭遇すると、戦闘画面が表示されます。

ただ、前回の段階では、まだ戦闘画面を表示しているだけでした。

勇者とスライムのアイコンは表示されますが、攻撃するわけでもなく、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としては大きな一歩です。

次回は、戦闘メッセージをもう少し分かりやすくしたり、「逃げる」ボタンを追加したりして、戦闘画面を少しずつ改良していきます。

-Java, UML, オブジェクト指向
-, ,

Copyright© satoshi's ソフトウェア開発 , 2026 All Rights Reserved Powered by STINGER.