Qiita Teams that are logged in
You are not logged in to any team

Log in to Qiita Team
Community
OrganizationAdvent CalendarQiitadon (β)
Service
Qiita JobsQiita ZineQiita Blog
22
Help us understand the problem. What is going on with this article?
@klriutsa

ゼルダの伝説でオブジェクト指向がほぼ理解できる記事

More than 1 year has passed since last update.

背景

世の中にはオブジェクト指向の解説が多くありますが大半が、コードのオブジェクト化で止まっている物が多く、
オブジェクト指向が一体なんの役に立つのかが全く理解できない人が多くいるのではないでしょうか?

前提としてオブジェクト指向についての筆者の意見を述べると、は以下のとおりです

  • オブジェクト指向はこうするべき、という答えは無い
  • 現実世界とは別
  • 開発を楽にするための設計方針がオブジェクト指向

そこで、Switchで話題になったゲームであるゼルダの伝説を題材に、日本では比較的ユーザーが多いRubyを使ってオブジェクト指向を解説してみます。
(※本来はKotlinのほうがうまく説明できるので、Kotlin編は後日投稿します)

説明するに当たり、ポリモーフィズム、カプセル化などの用語は一切使いません。

ゼルダの伝説 ブレス オブ ザ ワイルドとは

掛け算のあそびができることが特徴であり、物理法則と化学反応を組み合わせた遊び方ができます。
例)空を飛ぶ、燃やす、ものを投げる 等

以下が参考になります
https://game.watch.impress.co.jp/docs/news/1047403.html

実装する要件

以下の機能をRubyで実装してみます。

  • プレイヤーと武器と敵がある。武器(マスターソード、森人の剣、強化されたマスターソード)、敵(ボコブリン、ファイアチュチュ、ガーディアン)
    • 強化されたマスターソードというアイテムは存在しないのですが、説明のために入れておきます
  • プレイヤーが武器を使って敵を攻撃するとダメージを与える
  • プレイヤーがガーディアンをマスターソードで攻撃すると2倍のダメージを与えられる
  • プレイヤーがファイアチュチュを攻撃すると手持ちの武器が可燃性の場合燃えてなくなる
    • 森人の剣は燃える

スクリーンショット 2018-10-31 10.33.14.png

作成したプログラム

stage1.rbが最初の状態で、徐々にオブジェクト指向を取り入れてコードに変更を加えていっています。

実行例

$ ruby stage1.rb
"装備アイテム 1 マスターソード、2 森人の剣 3 強化されたマスターソード"
1
"攻撃対象。1 ボコブリン 2 ファイアチュチュ 3 ガーディアン"
1
#<Bokoburin:0x00007fb2e31b2608 @health=200>
"敵のHP170"
"プレイヤーの手持ち武器マスターソード"

最初の段階(stage1)

stage1.rb
class MasterSword
  attr_reader :name, :power

  def initialize
    @name = 'マスターソード'
    @power = 30
  end
end

class UpgradedMasterSword
  attr_reader :name, :power

  def initialize
    @name = 'マスターソード'
    @power = 60
  end
end

class ForestDwellersSword
  attr_reader :name, :power

  def initialize
    @name = '森人の剣'
    @power = 20
  end
end

class RedChuchu
  attr_accessor :health

  def initialize
    @health = 100
  end
end

class Bokoburin
  attr_accessor :health

  def initialize
    @health = 200
  end
end

class Guardian
  attr_accessor :health

  def initialize
    @health = 1000
  end
end

class Player
  attr_accessor :weapon

  def initialize
    @weapon = nil
  end
end

player = Player.new

p '装備アイテム 1 マスターソード、2 森人の剣 3 強化されたマスターソード'
input = gets
player.weapon = case input.chomp!
                when '1'
                  MasterSword.new
                when '2'
                  ForestDwellersSword.new
                when '3'
                  UpgradedMasterSword.new
                else
                  raise '1~3を入力してください'
                end

p '攻撃対象。1 ボコブリン 2 赤チュチュ 3 ガーディアン'
input = gets
enemy = case input.chomp!
        when '1'
          Bokoburin.new
        when '2'
          RedChuchu.new
        when '3'
          Guardian.new
        else
          raise '1~3を入力してください'
        end

p enemy

if enemy.class == Guardian && player.weapon.class == MasterSword || enemy.class == Guardian && player.weapon.class == UpgradedMasterSword
  enemy.health -= player.weapon.power * 2
else
  enemy.health -= player.weapon.power
end
if enemy.class == RedChuchu && player.weapon.class == ForestDwellersSword
  player.weapon = nil
end


p "敵のHP#{enemy.health}"
p "プレイヤーの手持ち武器#{player.weapon&.name}"

やったこと

  • 武器と敵のオブジェクト化
    • ただしオブジェクトはデータのみを持っている

このコードの問題点

神クラスのようなものが存在

スクリーンショット 2018-10-31 9.46.16.png

if enemy.class == Guardian && player.weapon.class == MasterSword || enemy.class == Guardian && player.weapon.class == UpgradedMasterSword
  enemy.health -= player.weapon.power * 2
else
  enemy.health -= player.weapon.power
end
if enemy.class == RedChuchu && player.weapon.class == ForestDwellersSword
  player.weapon = nil
end

このコードの問題点としては

  • 条件分岐が複雑でひと目で何をしているかわからない
  • 変更に弱い
    • 新しく条件を追加するのが大変
  • バグが発生しやすい

このようにコードのオブジェクト化のみでは、オブジェクトを使わないコードと大差が無いです。

オブジェクトにメソッドをもたせる(stage2.rb)

stage2.rb
class Sword
  attr_reader :name, :power
end

class MasterSword < Sword
  def initialize
    @name = 'マスターソード'
    @power = 30
  end
end

class UpgradedMasterSword < MasterSword
  def initialize
    @name = 'マスターソード'
    @power = 60
  end
end

class ForestDwellersSword < Sword
  def initialize
    @name = '森人の剣'
    @power = 20
  end
end

class Enemy
  attr_accessor :health

  def damage(player)
    @health -= player.weapon.power
  end
end

class RedChuchu < Enemy
  def initialize
    @health = 100
  end

  def damage(player)
    super
    if player.weapon.class == ForestDwellersSword
      player.weapon = nil
    end
  end
end

class Bokoburin < Enemy
  def initialize
    @health = 200
  end
end

class Guardian < Enemy
  def initialize
    @health = 1000
  end

  def damage(player)
    damage = player.weapon.power
    if player.weapon.is_a?(MasterSword)
      damage *= 2
    end
    @health -= damage
  end
end

class Player
  attr_accessor :weapon

  def initialize
    @weapon = nil
  end
end

player = Player.new

p '装備アイテム 1 マスターソード、2 森人の剣 3 強化されたマスターソード'
input = gets
player.weapon = case input.chomp!
                when '1'
                  MasterSword.new
                when '2'
                  ForestDwellersSword.new
                when '3'
                  UpgradedMasterSword.new
                else
                  raise '1~3を入力してください'
                end

p '攻撃対象。1 ボコブリン 2 ファイアチュチュ 3 ガーディアン'
input = gets
enemy = case input.chomp!
        when '1'
          Bokoburin.new
        when '2'
          RedChuchu.new
        when '3'
          Guardian.new
        else
          raise '1~3を入力してください'
        end

p enemy

enemy.damage(player)

p "敵のHP#{enemy.health}"
p "プレイヤーの手持ち武器#{player.weapon&.name}"

やったこと

  • 敵にdamageというメソッドをもたせた   * ダメージの条件は敵自身が知っている
  • クラスの継承を導入した

変更点

  • Enemyクラスにメソッドを作り、各クラスでオーバーライドする
class Enemy
  attr_accessor :health

  def damage(player)
    @health -= player.weapon.power
  end
end

class Guardian < Enemy
  def initialize
    @health = 1000
  end

  def damage(player)
    damage = player.weapon.power
    if player.weapon.is_a?(MasterSword)
      damage *= 2
    end
    @health -= damage
  end
end

このように変更することでstage1.rbで存在していた神クラスのようなものは以下1行になりました。

enemy.damage(player)

このように設計することで、新しい条件を持った敵や武器を追加することが簡単になりました。

条件の追加(stage3.rb)

以下の条件を新しく追加します

  • 武器を追加する
    • 木の枝 攻撃力1 可燃性
    • Swordクラスを継承する
    • 森人の槍 攻撃力20 可燃性
    • Spearクラスを継承する
stage3.rb
class Weapon
  attr_reader :name, :power
end

class Sword < Weapon

end

class Spear < Weapon

end

class MasterSword < Sword
  def initialize
    @name = 'マスターソード'
    @power = 30
  end
end

class UpgradedMasterSword < MasterSword
  def initialize
    @name = 'マスターソード'
    @power = 60
  end
end

class FlammableSword < Sword
  def burn(player)
    player.weapon = nil
  end
end

class ForestDwellersSword < FlammableSword
  def initialize
    @name = '森人の剣'
    @power = 20
  end
end

class TreeBranch < FlammableSword
  def initialize
    @name = '木の枝'
    @power = 1
  end
end

class FlammableSpear < Spear
  def burn(player)
    player.weapon = nil
  end
end

class ForestDwellersSpear < FlammableSpear
  def initialize
    @name = '森人の槍'
    @power = 20
  end
end

class Enemy
  attr_accessor :health

  def damage(player)
    @health -= player.weapon.power
  end
end

class RedChuchu < Enemy
  def initialize
    @health = 100
  end

  def damage(player)
    super
    if player.weapon.is_a?(FlammableSword) || player.weapon.is_a?(FlammableSpear)
      player.weapon.burn(player)
    end
  end
end

class Bokoburin < Enemy
  def initialize
    @health = 200
  end
end

class Guardian < Enemy
  def initialize
    @health = 1000
  end

  def damage(player)
    damage = player.weapon.power
    if player.weapon.is_a?(MasterSword)
      damage *= 2
    end
    @health -= damage
  end
end

class Player
  attr_accessor :weapon

  def initialize
    @weapon = nil
  end
end

player = Player.new

p '装備アイテム 1 マスターソード、2 森人の剣 3 強化されたマスターソード 4 木の枝 5 森人の槍'
input = gets
player.weapon = case input.chomp!
                when '1'
                  MasterSword.new
                when '2'
                  ForestDwellersSword.new
                when '3'
                  UpgradedMasterSword.new
                when '4'
                  TreeBranch.new
                when '5'
                  ForestDwellersSpear.new
                else
                  raise '1~3を入力してください'
                end

p '攻撃対象。1 ボコブリン 2 ファイアチュチュ 3 ガーディアン'
input = gets
enemy = case input.chomp!
        when '1'
          Bokoburin.new
        when '2'
          RedChuchu.new
        when '3'
          Guardian.new
        else
          raise '1~3を入力してください'
        end

p enemy

enemy.damage(player)

p "敵のHP#{enemy.health}"
p "プレイヤーの手持ち武器#{player.weapon&.name}"

やったこと

  • 燃えるという処理を武器クラスに持たせた
    • FlammableSword、FlammableSpear

変更点

class FlammableSword < Sword
  def burn(player)
    player.weapon = nil
  end
end

class FlammableSpear < Spear
  def burn(player)
    player.weapon = nil
  end
end

可燃性の武器をいう概念を作りました。

 ※ 本来は抽象クラスとして実装しますが、Rubyには抽象クラスという概念がないため行っていません。

このコードの問題点

条件分岐、クラス構造が複雑

  def damage(player)
    super
    if player.weapon.is_a?(FlammableSword) || player.weapon.is_a?(FlammableSpear)
      player.weapon.burn(player)
    end
  end

この分岐は良くないです。
例えば、燃える盾というものを追加しなければならないとき
FlammableShieldを更に条件に追加しなければならないです。
追加を忘れてしまうとバグの原因になります。

スクリーンショット 2018-10-31 10.05.33.png

クラスの関係図を図で表すと、上記のように、役割が被っているクラスがあります。
クラスの継承だけでは解決できません。

性質を導入する(stage4.rb)

stage4.rb
class Weapon
  attr_reader :name, :power
end

class Sword < Weapon

end

class Spear < Weapon

end

class MasterSword < Sword
  def initialize
    @name = 'マスターソード'
    @power = 30
  end
end

class UpgradedMasterSword < MasterSword
  def initialize
    @name = 'マスターソード'
    @power = 60
  end
end

module Flammable
  def burn(player)
    player.weapon = nil
  end
end

class ForestDwellersSword < Sword
  include Flammable

  def initialize
    @name = '森人の剣'
    @power = 20
  end
end

class TreeBranch < Sword
  include Flammable

  def initialize
    @name = '木の枝'
    @power = 1
  end
end

class ForestDwellersSpear < Spear
  include Flammable

  def initialize
    @name = '森人の槍'
    @power = 20
  end
end

class Enemy
  attr_accessor :health

  def damage(player)
    @health -= player.weapon.power
  end
end

class RedChuchu < Enemy
  def initialize
    @health = 100
  end

  def damage(player)
    super
    if player.weapon.include?(Flammable)
      player.weapon.burn(player)
    end
  end
end

class Bokoburin < Enemy
  def initialize
    @health = 200
  end
end

class Guardian < Enemy
  def initialize
    @health = 1000
  end

  def damage(player)
    damage = player.weapon.power
    if player.weapon.is_a?(MasterSword)
      damage *= 2
    end
    @health -= damage
  end
end

class Player
  attr_accessor :weapon

  def initialize
    @weapon = nil
  end
end

player = Player.new

p '装備アイテム 1 マスターソード、2 森人の剣 3 強化されたマスターソード 4 木の枝 5 森人の槍'
input = gets
player.weapon = case input.chomp!
                when '1'
                  MasterSword.new
                when '2'
                  ForestDwellersSword.new
                when '3'
                  UpgradedMasterSword.new
                when '4'
                  TreeBranch.new
                when '5'
                  ForestDwellersSpear.new
                else
                  raise '1~3を入力してください'
                end

p '攻撃対象。1 ボコブリン 2 ファイアチュチュ 3 ガーディアン'
input = gets
enemy = case input.chomp!
        when '1'
          Bokoburin.new
        when '2'
          RedChuchu.new
        when '3'
          Guardian.new
        else
          raise '1~3を入力してください'
        end

p enemy

enemy.damage(player)

p "敵のHP#{enemy.health}"
p "プレイヤーの手持ち武器#{player.weapon&.name}"

やったこと

  • 可燃性(Flammable)という性質をインターフェースで実装した   * Rubyではmoduleというものになる。

変更点

module Flammable
  def burn(player)
    player.weapon = nil
  end
end

class ForestDwellersSword < Sword
  include Flammable

  def initialize
    @name = '森人の剣'
    @power = 20
  end
end

class ForestDwellersSpear < Spear
  include Flammable

  def initialize
    @name = '森人の槍'
    @power = 20
  end
end

このように定義し、ファイアチュチュの処理を以下のように変更しました。

class RedChuchu < Enemy
  def initialize
    @health = 100
  end

  def damage(player)
    super
    if player.weapon.include?(Flammable)
      player.weapon.burn(player)
    end
  end
end

可燃性であれば燃やすという書き方に変えました。
これにより可燃性の武器を追加する場合、そのクラスに対してFlammableをincludeするだけで完了することになります。

さらなる仕様変更(stage5.rb)

以下の仕様変更を行います

  • 攻撃したらプレイヤーを感電させるエレキチュチュを追加。
  • 電気を通す導電性という性質を追加する   * 感電すると、武器を落とす
stage5.rb
class Weapon
  attr_reader :name, :power
end

class Sword < Weapon

end

class Spear < Weapon

end

module Flammable
  def burn(player)
    player.weapon = nil
  end
end

module Conductive
  def shock(player)
    player.weapon = nil
  end
end

class MasterSword < Sword
  include Conductive

  def initialize
    @name = 'マスターソード'
    @power = 30
  end
end

class UpgradedMasterSword < MasterSword
  include Conductive

  def initialize
    @name = 'マスターソード'
    @power = 60
  end
end

class ForestDwellersSword < Sword
  include Flammable

  def initialize
    @name = '森人の剣'
    @power = 20
  end
end

class TreeBranch < Sword
  include Flammable

  def initialize
    @name = '木の枝'
    @power = 1
  end

  def burn(player)
    p '火がついた'
  end
end

class ForestDwellersSpear < Spear
  include Flammable

  def initialize
    @name = '森人の槍'
    @power = 20
  end
end

class Enemy
  attr_accessor :health

  def damage(player)
    @health -= player.weapon.power
  end
end

class RedChuchu < Enemy
  def initialize
    @health = 100
  end

  def damage(player)
    super
    if player.weapon.class <= Flammable
      player.weapon.burn(player)
    end
  end
end

class YellowChuchu < Enemy
  def initialize
    @health = 100
  end

  def damage(player)
    super
    if player.weapon.class <= Conductive
      player.weapon.shock(player)
    end
  end
end

class Bokoburin < Enemy
  def initialize
    @health = 200
  end
end

class Guardian < Enemy
  def initialize
    @health = 1000
  end

  def damage(player)
    damage = player.weapon.power
    if player.weapon.class <= MasterSword
      damage *= 2
    end
    @health -= damage
  end
end

class Player
  attr_accessor :weapon

  def initialize
    @weapon = nil
  end
end

player = Player.new

p '装備アイテム 1 マスターソード、2 森人の剣 3 強化されたマスターソード 4 木の枝 5 森人の槍'
input = gets
player.weapon = case input.chomp!
                when '1'
                  MasterSword.new
                when '2'
                  ForestDwellersSword.new
                when '3'
                  UpgradedMasterSword.new
                when '4'
                  TreeBranch.new
                when '5'
                  ForestDwellersSpear.new
                else
                  raise '1~3を入力してください'
                end

p '攻撃対象。1 ボコブリン 2 ファイアチュチュ 3 ガーディアン 4 エレキチュチュ'
input = gets
enemy = case input.chomp!
        when '1'
          Bokoburin.new
        when '2'
          RedChuchu.new
        when '3'
          Guardian.new
        when '4'
          YellowChuchu.new
        else
          raise '1~3を入力してください'
        end

p enemy

enemy.damage(player)

p "敵のHP#{enemy.health}"
p "プレイヤーの手持ち武器#{player.weapon&.name}"

変更点

マスターソードに導電性を追加しました。

module Conductive
  def shock(player)
    player.weapon = nil
  end
end

class MasterSword < Sword
  include Conductive

  def initialize
    @name = 'マスターソード'
    @power = 30
  end
end

導電性の武器の場合、感電させるようにしました。

class YellowChuchu < Enemy
  def initialize
    @health = 100
  end

  def damage(player)
    super
    if player.weapon.class <= Conductive
      player.weapon.shock(player)
    end
  end
end

まとめ

オブジェクト指向における重要な要素

以下の要素をうまく使うことがオブジェクト指向だと思います。

  • クラス
    • 実態があるものを表現するときに使う
  • インターフェース
    • 性質などを表現するときに使う
    • Rubyではmodule
    • インターフェースにメソッド持たせるかで宗教戦争になりますが、私はメソッドをもたせても良いと思います。
  • 抽象クラス
    • 実態が無い概念を表現するときに使う
    • Rubyでは存在しない

Rubyはオブジェクト指向に向いている言語ではないため、抽象クラスの説明ができませんでしたが、

剣や槍という概念はゼルダの伝説に存在しますが、「剣」や「槍」といったアイテムはゲーム内には存在しません。
そういった実態はないが概念として存在するものが抽象クラスとなります。
SwordやEnemyクラスがそれに当たります。

オブジェクト指向とはなにか

  • オブジェクト指向は設計の方針を大まかに指し示したもの
  • オブジェクト指向の答えは無い
    • 例では、武器をクラス、可燃性をインターフェースにしましたが、それを逆にしたほうがうまく設計できるのであればそちらのほうが良いです。
    • 明確な正解はなく、どのようにして開発を楽にしてバグを少なくするかとしての手段としてオブジェクト指向を使います。
  • クラス、インターフェース、(抽象クラス)をつかって表現する

終わりに

最後に思ったことを書いておきます。

オブジェクト指向が必要なのか

フレームワークに則って開発を行う場合、オブジェクト指向はほとんど役に立たないとおもいます。
自分でフレームワークだったり、ゲームエンジンを自作するときにオブジェクト指向が生かされます。

ゼルダの伝説の実際のコードとの対比

実際のコードを見たことが無いのでわからないですが、一部分くらいは合っていてほしいなと言う感想です。
ただし、このコードは投げる、移動させるということが考慮されていないです。
それらを追加する場合、説明しきれなくなってしまうため、簡略化して実装しました。

22
Help us understand the problem. What is going on with this article?
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away

Comments

No comments
Sign up for free and join this conversation.
Sign Up
If you already have a Qiita account Login
22
Help us understand the problem. What is going on with this article?