Java

オブジェクト指向でRPGを作ってみる – 継承

更新日:

勇者とスライムには共通しているところがいくつかあります。

オブジェクト指向プログラミングでは、共通部分をまとめたクラスを作り、その共通部分を「継承」したクラスを作ることができます。

勇者とスライムの共通部分をCharaクラスにまとめます。
変数hpは、Braveクラスの数か所でも参照しているので、継承先のサブクラスでもアクセスできるように、protectedで宣言しておきます。

public class Chara {
  protected int hp;
  private String name;
  public String getName() {
    return name;
  }
  public void setName(String name) {
    this.name = name;
  }
  public int getHp() {
    return hp;
  }
  public void damage(int v) {
    hp -= v;
  }
}

スライムのクラスは大半が共通部分に吸収されているので、以下のように変わります。

public class Slime extends Chara {
  public Slime() {
    hp = 3;
    setName("スライム");
  }
  public int getGold() {
    return 1;
  }
}

勇者クラスは独自の実装がたくさんありますので、あまり小さくはなりません。

import java.util.ArrayList;
import java.util.List;

public class Brave extends Chara {
  private int mp = 10;
  private int gold = 0;
  private Arms arms;
  private List<Item> items = new ArrayList<>();
  public Brave() {
    hp = 10;
    setName("ヨシヒコ");
  }
  public void setArms(Arms arms) {
    this.arms = arms;
  }
  public void addItem(Item item) {
    items.add(item);
  }

  private int maxHp() {
    return 20;
  }

  public boolean isMaxHp() {
    return maxHp() == hp;
  }

  private int maxMp() {
    return 20;
  }

  public boolean isMaxMp() {
    return maxMp() == mp;
  }

  public void introduce() {
    System.out.println();
    System.out.println("名前:" + getName());
    System.out.println("HP:" + hp + " MP:" + mp);
    if (arms == null) {
      System.out.println("武器: なし");
    } else {
      System.out.println("武器:" + arms.getName());
    }
    System.out.println("アイテム:");
    for (Item item: items) {
      System.out.println(item.getName());
    }
  }

  public Item getItem(String name) {
    for (Item item : items) {
      if (item.getName().equals(name)) return item;
    }
    return null;
  }

  public boolean isUsable(Item item) {
    return item.isUsable(this);
  }

  public void useItem(Item item) {
    if (!isUsable(item)) {
      System.out.println(item.getName() + "は、使えません。");
      return;
    }
    if (items.contains(item)) {
      System.out.println(item.getName() + "を使った。");
      item.effect(this);
      items.remove(item);
    } else {
      System.out.println(item.getName() + "を持ってなかった!");
    }
  }

  public void recoverHp(int hp) {
    this.hp += hp;
    if(this.hp > maxHp()) this.hp = maxHp();
  }
  
  public void recoverMp(int mp) {
    this.mp += mp;
    if(this.mp > maxMp()) this.mp = maxMp();
  }
  public void addGold(int gold) {
    this.gold += gold;
    System.out.println(getName() + "は、" + this.gold + "ゴールドを持っています。");
  }
}

新しいモンスターを追加する

継承を使って新しいモンスター「ドラキー」を追加してみましょう。

まずモデルクラスを作成します。

Slimeクラスをマネをして作ればいいので簡単です。

public class Dracky extends Chara {
  public Dracky() {
    hp = 11;
    setName("ドラキー");
  }
  public int getGold() {
    return 2;
  }
}

ビューにあたる DrackyLabel も作ります。

こちらも SlimeLabel と同様に作ればいいので簡単です。

import java.awt.Image;

public class DrackyLabel extends IconLabel {
  private static Image image = null;
  public DrackyLabel() {
    super();
  }

  @Override
  String getImageFilename() {
    return "dracky.png";
  }

  @Override
  Image getImage() {
    return image;
  }

  @Override
  void setImage(Image image) {
    DrackyLabel.image = image;
  }
}

スライムとドラキーを作ると、勇者とモンスターは分けて考えたほうが直感的にわかりやすいような気がするので、Monsterクラスを作成して、SlimeとDrackyはMonsterを継承するように変更します。

モンスターは勇者と区別するために形式的に作っておくだけなので、最初は何もなくていいと思います。

後でモンスター共通の実装があったときはこちらに追加します。

public class Monster extends Chara {
  
}
public class Slime extends Monster {
  public Slime() {
    hp = 3;
    setName("スライム");
  }
  public int getGold() {
    return 1;
  }
}
public class Dracky extends Monster {
  public Dracky() {
    hp = 11;
    setName("ドラキー");
  }
  public int getGold() {
    return 2;
  }
}

RpgFrameで敵と遭遇する部分を変更します。

現状は敵はスライムだけですが、ここを確率1/2でスライムとドラキーが出現するようにします。

  public void keyPressed(KeyEvent e) {
    if (e.getKeyCode() == KeyEvent.VK_LEFT) {
      if (braveX > 0) braveX--;
    }
    if (e.getKeyCode() == KeyEvent.VK_RIGHT) {
      if (braveX < FieldWidth - 1) braveX++;
    }
    if (e.getKeyCode() == KeyEvent.VK_UP) {
      if (braveY > 0) braveY--;
    }
    if (e.getKeyCode() == KeyEvent.VK_DOWN) {
      if (braveY < FieldHeight - 1) braveY++;
    }
    drawField();
    System.out.println("x=" + braveX + ", y=" + braveY);
    if (isEncount()) {
      System.out.println("敵と遭遇しました。");
      BattleFrame bf = new BattleFrame();
      bf.setBrave(rpg.getBrave());
      Monster m = rpg.getMonster();
      bf.setMonster(m);
      bf.setMonsterLabel(getMonsterLabel(m));
      SwingUtilities.invokeLater(new WindowInvoker(bf));
    }
  }
  private IconLabel getMonsterLabel(Monster m) {
    IconLabel l = null;
    if (m instanceof Slime) {
      l = new SlimeLabel();
    }
    if (m instanceof Dracky) {
      l = new DrackyLabel();
    }
    return l;
  }

モンスターはRpgクラスから取得します。

public class Rpg {
  private Brave brave;
  public Rpg() {
    brave = new Brave();
    brave.setName("ヨシヒコ");
  }
  public Brave getBrave() {
    return brave;
  }
  public Monster getMonster() {
    double v = Math.random();
    Monster m = null;
    if (v < 0.5) {
      m = new Slime();
    } else {
      m = new Dracky();
    }
    return m;
  }
}

BattleFrameでもスライム固定だったところをモンスターに変更します。

import java.awt.Container;

import javax.swing.BoxLayout;
import javax.swing.JButton;
import javax.swing.JFrame;
import javax.swing.JPanel;

public class BattleFrame extends JFrame {
  private Brave brave;
  private Monster monster;
  private BraveLabel braveLabel;
  private IconLabel monsterLabel;
  public BattleFrame() {
    super("戦闘");
    Container pane = getContentPane();
    pane.setLayout(new BoxLayout(pane, BoxLayout.Y_AXIS));
    JPanel iconsPanel = new JPanel();
    iconsPanel.setLayout(new BoxLayout(iconsPanel, BoxLayout.X_AXIS));
    JPanel buttonsPanel = new JPanel();
    buttonsPanel.setLayout(new BoxLayout(buttonsPanel, BoxLayout.Y_AXIS));
    pane.add(iconsPanel);
    pane.add(buttonsPanel);
    braveLabel = new BraveLabel();
    monsterLabel = getMonsterLabel();
    iconsPanel.add(braveLabel);
    iconsPanel.add(monsterLabel);
    JButton battleButton = new JButton("こうげき");
    battleButton.addActionListener(new BattleAction(this));
    buttonsPanel.add(battleButton);
    JButton escapeButton = new JButton("にげる");
    escapeButton.addActionListener(new EscapeAction());
    buttonsPanel.add(escapeButton);
  }

  public Brave getBrave() {
    return brave;
  }

  public void setBrave(Brave brave) {
    this.brave = brave;
  }

  public Monster getMonster() {
    return monster;
  }

  public void setMonster(Monster monster) {
    this.monster = monster;
  }

  public BraveLabel getBraveLabel() {
    return braveLabel;
  }

  public IconLabel getMonsterLabel() {
    return monsterLabel;
  }

  public void setMonsterLabel(IconLabel label) {
    this.monsterLabel = label;
  }
}

実行すると、敵と遭遇した時にNullPointerExceptionが発生してしまいました。

これを回避するために、以下のように周世します。

import java.awt.Container;

import javax.swing.BoxLayout;
import javax.swing.JButton;
import javax.swing.JFrame;
import javax.swing.JPanel;

public class BattleFrame extends JFrame {
  private Brave brave;
  private Monster monster;
  private BraveLabel braveLabel;
  private IconLabel monsterLabel;
  private JPanel iconsPanel;
  public BattleFrame() {
    super("戦闘");
    Container pane = getContentPane();
    pane.setLayout(new BoxLayout(pane, BoxLayout.Y_AXIS));
    iconsPanel = new JPanel();
    iconsPanel.setLayout(new BoxLayout(iconsPanel, BoxLayout.X_AXIS));
    JPanel buttonsPanel = new JPanel();
    buttonsPanel.setLayout(new BoxLayout(buttonsPanel, BoxLayout.Y_AXIS));
    pane.add(iconsPanel);
    pane.add(buttonsPanel);
    braveLabel = new BraveLabel();
    iconsPanel.add(braveLabel);
    JButton battleButton = new JButton("こうげき");
    battleButton.addActionListener(new BattleAction(this));
    buttonsPanel.add(battleButton);
    JButton escapeButton = new JButton("にげる");
    escapeButton.addActionListener(new EscapeAction());
    buttonsPanel.add(escapeButton);
  }

  public Brave getBrave() {
    return brave;
  }

  public void setBrave(Brave brave) {
    this.brave = brave;
  }

  public Monster getMonster() {
    return monster;
  }

  public void setMonster(Monster monster) {
    this.monster = monster;
  }

  public BraveLabel getBraveLabel() {
    return braveLabel;
  }

  public IconLabel getMonsterLabel() {
    return monsterLabel;
  }

  public void setMonsterLabel(IconLabel label) {
    this.monsterLabel = label;
    iconsPanel.add(monsterLabel);
  }
}

BattleActionでコンパイルエラーが出ていますので、それを修正します。

import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;

import javax.swing.JOptionPane;

public class BattleAction implements ActionListener {
  private BattleFrame battleFrame;
  public BattleAction(BattleFrame bf) {
    battleFrame = bf;
  }
  @Override
  public void actionPerformed(ActionEvent e) {
    Brave brave = battleFrame.getBrave();
    Monster monster = battleFrame.getMonster();
    System.out.println(brave.getName() + "のこうげき");
    System.out.println(monster.getName() + "のこうげき");
    monster.damage(1);
    brave.damage(1);
    BraveLabel bl = battleFrame.getBraveLabel();
    IconLabel sl = battleFrame.getMonsterLabel();
    bl.setText(brave.getName() + " HP:" + brave.getHp());
    sl.setText(monster.getName() + " HP:" + monster.getHp()); 
    if (monster.getHp() <= 0) {
      int gold = monster.getGold();
      String msg = monster.getName() + "をやっつけた!\n";
      msg += gold + "ゴールドを獲得した!";
      brave.addGold(gold);
      JOptionPane.showMessageDialog(
        battleFrame, msg,
        "勝利", JOptionPane.INFORMATION_MESSAGE);
      battleFrame.setVisible(false);
    }
    if (brave.getHp() <= 0) {
      JOptionPane.showMessageDialog(
        battleFrame, brave.getName() + "はしんでしまった。",
        "敗北", JOptionPane.INFORMATION_MESSAGE);
      battleFrame.setVisible(false);
    }
  }
}

24行目でコンパイルエラーが出ていますので、これを解決するためにMonsterクラスにgetGold()メソッドを追加します。

public class Monster extends Chara {
  public int getGold() {
    return 0;
  }
}

さらにゴーストを追加する

モンスターを増やせるようになったので、さらにゴーストを追加してみましょう。

ゴーストは、英語で Ghost で、HPは13、ドロップするゴールドは3です。

public class Ghost extends Monster {
  public Ghost() {
    hp = 13;
    setName("ゴースト");
  }
  public int getGold() {
    return 3;
  }
}

ビューにあたる GhostLabel を作ります。

import java.awt.Image;

public class GhostLabel extends IconLabel {
  private static Image image = null;
  public GhostLabel() {
    super();
  }

  @Override
  String getImageFilename() {
    return "ghost.png";
  }

  @Override
  Image getImage() {
    return image;
  }

  @Override
  void setImage(Image image) {
    GhostLabel.image = image;
  }
}

Rpgクラスの getMonster() で一定確率でゴーストを出現させるようにします。

public class Rpg {
  private Brave brave;
  public Rpg() {
    brave = new Brave();
    brave.setName("ヨシヒコ");
  }
  public Brave getBrave() {
    return brave;
  }
  public Monster getMonster() {
    double v = Math.random();
    Monster m = null;
    if (v < 0.4) {
      m = new Slime();
    } else if (v < 0.7) {
      m = new Dracky();
    } else {
      m = new Ghost();
    }
    return m;
  }
}

RpgFrame では、ゴーストのビューを生成するようにします。

  private IconLabel getMonsterLabel(Monster m) {
    IconLabel l = null;
    if (m instanceof Slime) {
      l = new SlimeLabel();
    }
    if (m instanceof Dracky) {
      l = new DrackyLabel();
    }
    if (m instanceof Ghost) {
      l = new GhostLabel();
    }
    return l;
  }

「にげる」を実装する

戦闘画面に「にげる」ボタンがありますが、まだ実装がありません。

「にげる」ボタンを押したら、ダイアログを表示して戦闘面を閉じるようにしましょう。

import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;

import javax.swing.JOptionPane;

public class EscapeAction implements ActionListener {
  private BattleFrame battleFrame;
  public EscapeAction(BattleFrame bf) {
    battleFrame = bf;
  }

  @Override
  public void actionPerformed(ActionEvent e) {
    System.out.println("にげる");
    Brave b = battleFrame.getBrave();
    String msg = b.getName() + "は、逃げ出した。";
    JOptionPane.showMessageDialog(
      battleFrame, msg,
      "逃げた", JOptionPane.INFORMATION_MESSAGE);
    battleFrame.setVisible(false);
  }
  
}

-Java
-, ,

Copyright© UMLとJavaで学ぶオブジェクト指向プログラミング入門 , 2024 All Rights Reserved Powered by STINGER.