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

satoshi's ソフトウェア開発

js



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

Java UML オブジェクト指向

JavaでRPGを作る|オブジェクト指向で敵の種類を増やそう

投稿日:


前回は、戦闘画面に攻撃ボタンを追加し、勇者とスライムを戦わせるところまで作りました。

勇者が攻撃する。
スライムが反撃する。
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 として扱うようにしました。

そのため、敵がスライムでもゴーストでも、同じように攻撃や反撃の処理ができます。

これが、オブジェクト指向で作る大きなメリットです。

共通する情報や処理を親クラスにまとめておくと、新しい種類を追加しやすくなります。

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

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