はじめに
プログラミングを学ぶ方が壁にぶつかるポイントは、いくつかあります。
条件分岐と繰り返し。
配列。
そして、クラスとメソッド。
特にクラスとメソッドは、「概念は分かるけど、一体なにが便利なんだ!?」という風に思う方がほとんどだと思います。例えば、
- 犬を表す、「Dog」クラス
- 年齢、性別、名前というプロパティ
- 歩く、「walk」メソッド
- Dogクラスは設計書。実際にはその設計書をもとに、実体化(インスタンス化)する
・・・なんて言われても、「なんのこっちゃ!?」となるに決まっています。この説明だと、分かるわけがないんですね。
もう少し身近な例で、かつ用途が分かる例での解説が、必要になってくるわけです。
そこで今回は、誰もが一度はやったことがあるであろう、「ポケモン」を使用して、解説していきます。
注意
この記事は、「一度はクラス・メソッドを勉強したことがある人」「でも、あんまりよくわからなかった人」がターゲットです。まだクラス・メソッドを勉強したことがない人は、一度は教材などから、勉強することをオススメします。
この記事は、ポケモンをやったことがない方が読んでも、さっぱりな内容になってます。
ポケモンをやったことがない方は、ぜひこちらのリンクに飛んでください。
https://www.pokemon.co.jp/ex/sword_shield/
https://www.pokemon.co.jp/ex/VCAMAP/ポケモンは、基本的に初代仕様です。
なるべくわかりやすくするため、一部無理やりな実装部分があります。また、あえて説明していないところもいくつかあります。
以下の内容より良い実装、リファクタリングできる場所は、非常に多数あります。細かい事は気にせず、ざっくりとした概念を捉えていただけると嬉しいです。
ポケモンをプログラミングするには?
突然ですが、「ポケモンをプログラミングしてください」と言われたとき、皆さんはどのように実装しますか?
もし、クラスをちゃんと理解していない方が頑張って実装しようとすると、以下のように書くんじゃないかと思います。
using System;
namespace Pokemon
{
class Program
{
static void Main(string[] args)
{
// 手持ちポケモン作成
string[] pokemons = new string[] { "ピカチュウ", "カイリュー", "ヤドラン", "ピジョン", "コダック", "コラッタ" };
// 手持ちポケモンのタイプ。上の配列と同じ要素番号で管理。TODO:複合タイプ
string[] types = new string[] { "でんき", "ドラゴン", "みず", "ひこう", "みず", "ノーマル" };
// 手持ちポケモンのレベル。上の配列と同じ要素番号で管理。
int[] levels = new int[] { 50, 62, 28, 57, 78, 18 };
// 以下、諸々の処理を頑張って書く・・・
}
}
}
・・・もうこの時点で、どこかで必ず破綻するのは、火の中水の中草の中、あの子のスカートの中を見るよりも明らかですね。
では、どのように実装するのがいいでしょうか?一緒に考えていきましょう。
すべてのポケモンに共通する情報を考える
ポケモンは多種多様います。その数、全部で151匹です。(byおじいちゃん博士)
それだけたくさんのポケモンがいますが、すべてのポケモンが、共通してもつ情報があります。まずはそれについて、考えていきましょう。
すべてのポケモンが共通してもつ情報(一例1)
どんなポケモンの種類でも、以下のような情報は、共通で持っています。
- 図鑑NO
- 名前
- ニックネーム
- レベル
- わざ1
- わざ2
- わざ3
- わざ4
- タイプ1
- タイプ2
このような情報は、すべてのポケモンが持っています。例外はありません。レベルの持たないポケモンなんていませんよね。遊戯王じゃあるまいし。
この内容を実装すると、以下のようになります。
using System;
using System.Collections.Generic;
using System.Text;
namespace Pokemon
{
public class Pokemon
{
/// <summary>
/// 図鑑番号
/// </summary>
public string ZukanNo;
/// <summary>
/// 名前
/// </summary>
public string Name;
/// <summary>
/// ニックネーム
/// </summary>
public string Nickname;
/// <summary>
/// レベル
/// </summary>
public int Level;
/// <summary>
/// わざ1
/// </summary>
public string Waza1;
/// <summary>
/// わざ2
/// </summary>
public string Waza2;
/// <summary>
/// わざ3
/// </summary>
public string Waza3;
/// <summary>
/// わざ4
/// </summary>
public string Waza4;
/// <summary>
/// タイプ1
/// </summary>
public string Type1;
/// <summary>
/// タイプ2
/// </summary>
public string Type2;
}
}
すべてのポケモンが共通してもつ情報(HP、攻撃力、素早さなど)
すべてのポケモンには、HP、攻撃力、素早さといった情報を持っています。バトルで使用するパラメータですね。
これらのパラメータも、同じようにすべてのポケモンが持っているのですが、このパラメータの持ち方は、ちょっとややこしいです。
ポケモンのHPなどのパラメータは、「種族値」「個体値」「レベル」といった要素(ステータス)で決まってきます。
レベルはさておき、種族値、個体値は以下の内容です。
- 種族値:ポケモンの種類によって決まってくる値。「ピカチュウは攻撃力低いけど、カイリキーは攻撃力高い!」というのは、この「種族値」の値で決まる。
- 個体値:同じポケモンの種類でも、その1体1体のポケモンによって、強さが変わる。「なんか、同じレベルなのに、ライチュウAより、ライチュウBの方が素早さが早いぞ!?」というのは、この「個体値」の値で決まる。
といったといった内容で、ゲームには表示されない、隠しパラメータになります。
(※本当は努力値とか性格とかも絡んでくるのですが、ここでは割愛します)
そのため、ゲーム画面で表示される「攻撃力:150」「素早さ:200」というステータスは、実際には「種族値」「個体値」「レベル」といった要素の計算式で、決まってきます。
※「???」となってしまった方がいるかもしれません。これらの内容は説明したい内容の本筋じゃないので、「攻撃力とかのステータスは、色んな値の組み合わせで決めるんだ!」とだけ理解してください。
これらの内容を、以下の図、コードでまとめてみます。※ステータスは、最大HP、攻撃力、素早さのみ記載してます。防御力、特殊は割愛してます。
using System;
using System.Collections.Generic;
using System.Text;
namespace Pokemon
{
public class Pokemon
{
// 割愛
/// <summary>
/// 最大HP種族値
/// </summary>
protected int syuzokuMaxHP;
/// <summary>
/// 攻撃力種族値
/// </summary>
protected int syuzokuKougeki;
/// <summary>
/// 素早さ種族値
/// </summary>
protected int syuzokuSubayasa;
/// <summary>
/// 最大HP個体値
/// </summary>
protected int kotaichiMaxHP;
/// <summary>
/// 攻撃力個体値
/// </summary>
protected int kotaichiKougeki;
/// <summary>
/// 素早さ個体値
/// </summary>
protected int kotaichiSubayasa;
/// <summary>
/// 最大HP取得
/// </summary>
/// <returns></returns>
public int GetMaxHP()
{
// この式はホンモノじゃないよ
return (int)Math.Floor((decimal)(syuzokuMaxHP + kotaichiMaxHP) * 2 + (decimal)(Level / 100)) + Level + 10;
}
/// <summary>
/// 攻撃力取得
/// </summary>
/// <returns></returns>
public int GetKougeki()
{
return getStatus(syuzokuKougeki, kotaichiKougeki);
}
/// <summary>
/// 素早さ取得
/// </summary>
/// <returns></returns>
public int GetSubayasa()
{
return getStatus(syuzokuSubayasa, kotaichiSubayasa);
}
/// <summary>
/// 種族値と個体値から、ステータスを取得(HP以外で使用)
/// ※ホンモノじゃないよ
/// </summary>
/// <param name="syuzokuchi">種族値</param>
/// <param name="kotaichi">個体値</param>
/// <returns></returns>
protected int getStatus(int syuzokuchi, int kotaichi)
{
return (int)Math.Floor((decimal)(syuzokuchi + kotaichi) * 2 + (decimal)(Level / 100)) + 5;
}
}
}
攻撃力などのステータスが、計算式で出されていますね。そう、これがメソッドです。
例えば「攻撃力をゲーム画面に表示する」という状況になったとき、このGetKougekiメソッドを実行します。それにより、それぞれのポケモンが持つ他の値を使用して、攻撃力を求めて画面表示するわけです。
この計算式は、基本的にどのポケモンでも共通して使用されます。
それぞれのポケモンが持つ情報
すべてのポケモンが共通してもつ情報を定義しました。
今度はそれぞれのポケモンの種類が持つ情報を定義したり、セットしていきます。
例えば、「プリン」というポケモンを考えていきます。
ちなみに僕は小学生時代、プリンが大好きすぎて、周りからドン引きされてました。ぴえん。
プリンというポケモンは、以下のような情報が固定で決まっています。
- 図鑑NO:039
- 名前:プリン
- タイプ1:ノーマル
- タイプ2:なし
- 種族値HP:115
- 種族値攻撃:45
- 種族値防御:20
- 種族値特殊:25
- 種族値素早さ:20
いつ、どこで捕まえたプリンでも、この情報は固定です。
この「プリン」というポケモンを実装する場合、きっと以下のような、Purinクラスで、実装を行います。
using System;
using System.Collections.Generic;
using System.Text;
namespace Pokemon
{
public class Purin : Pokemon
{
/// <summary>
/// 図鑑番号
/// </summary>
public string ZukanNo = "039";
/// <summary>
/// 名前
/// </summary>
public string Name = "プリン";
/// <summary>
/// タイプ1
/// </summary>
public string Type1 = "ノーマル";
/// <summary>
/// 最大HP種族値
/// </summary>
protected int syuzokuMaxHP = 115;
/// <summary>
/// 攻撃力種族値
/// </summary>
protected int syuzokuKougeki = 45;
/// <summary>
/// 素早さ種族値
/// </summary>
protected int syuzokuSubayasa = 20;
}
}
「class Purin : Pokemon」という書き方をしています。
これが、継承です。上記で作成した「Pokemon」というクラスをベースに、Purin用のクラスを作成しているわけですね。
他にも、ライチュウ、カイリキーといったポケモンも、以下のような実装となるはずです。
using System;
using System.Collections.Generic;
using System.Text;
namespace Pokemon
{
public class Raicyu : Pokemon
{
/// <summary>
/// 図鑑番号
/// </summary>
public string ZukanNo = "026";
/// <summary>
/// 名前
/// </summary>
public string Name = "ライチュウ";
/// <summary>
/// タイプ1
/// </summary>
public string Type1 = "でんき";
/// <summary>
/// 最大HP種族値
/// </summary>
protected int syuzokuMaxHP = 60;
/// <summary>
/// 攻撃力種族値
/// </summary>
protected int syuzokuKougeki = 90;
/// <summary>
/// 素早さ種族値
/// </summary>
protected int syuzokuSubayasa = 100;
}
public class Kairiki : Pokemon
{
/// <summary>
/// 図鑑番号
/// </summary>
public string ZukanNo = "068";
/// <summary>
/// 名前
/// </summary>
public string Name = "カイリキー";
/// <summary>
/// タイプ1
/// </summary>
public string Type1 = "かくとう";
/// <summary>
/// 最大HP種族値
/// </summary>
protected int syuzokuMaxHP = 90;
/// <summary>
/// 攻撃力種族値
/// </summary>
protected int syuzokuKougeki = 130;
/// <summary>
/// 素早さ種族値
/// </summary>
protected int syuzokuSubayasa = 55;
}
}
つまり、これらのクラスは、ポケモンの数だけ作成されていきます。
図に書くと、以下のようなものになります。
「とびだしてきた!」でインスタンス化
ここまでが、クラスの説明で、いわゆる「設計書」と呼ばれる部分です。
ここから、いよいよ実体化、インスタンス化していきます。
3番道路の草むらを歩いていると、草むらからプリンが飛び出してきます。
「あ! やせいの プリンが とびだしてきた!」
この瞬間、プリンが実体化します。
Purinクラスという設計書をもとに、実際にプリンが飛び出してくるわけです。
そして、このタイミングで、そのポケモンの「レベル」、「個体値」、「覚えている技」といった値が決定します。
レベルは、どの草むらで出会ったかによって、ある程度の範囲でランダムに決められます。
個体値の、一定の範囲でランダムに決められます。
これを実装すると、以下のようになるでしょうか。
////// Pokemon.cs
using System;
using System.Collections.Generic;
using System.Text;
namespace Pokemon
{
public class Pokemon
{
/// <summary>
/// コンストラクタ。とびだしてきた!時に実施される
/// </summary>
public Pokemon(int level)
{
////// レベル、個体値、覚えている技を設定する
// レベル
Level = level;
// 個体値。初代は0~15の範囲でランダムに決定する
Random r = new System.Random();
kotaichiMaxHP = r.Next(0, 16);
kotaichiKougeki = r.Next(0, 16);
kotaichiSubayasa = r.Next(0, 16);
//// TODO:とびだしてきた時に覚えている技を作成
}
}
}
////// Purin.cs
using System;
using System.Collections.Generic;
using System.Text;
namespace Pokemon
{
public class Purin : Pokemon
{
// コンストラクタ追加
public Purin(int level) : base(level)
{
}
}
}
////// Program.cs
using System;
namespace Pokemon
{
class Program
{
static void Main(string[] args)
{
// やせいの プリンが とびだしてきた! ----------------------------------------------------
// 1体目:レベル3のプリン
Purin purin1 = new Purin(3);
// 2体目:レベル5のプリン
Purin purin2 = new Purin(5);
// 3体目:レベル7のプリン
Purin purin3 = new Purin(7);
}
}
}
Pokemonクラスのコンストラクタで、「とびだしてきた!」時の処理を実装しています。
Program.csで、3番道路でプリンがとびだしてきた時に、インスタンス化します。
そのタイミングで、初期レベル、個体値、覚えている技が決定します。
これらの値は、草むらで出会ってから、ずっと使用されます。
「あるタイミングで急にレベル1になる」とか、「あるタイミングでは急に攻撃力の個体値がめっちゃ強くなった!」ということは、基本的には無いわけです。
この「とびだしてきた!」をきっかけにして、どんどんポケモンがインスタンス化していきます。
Purinクラスという設計書をもとに、どんどんプリンが量産化されていきます。桃源郷ですね。
イメージとしては、以下のようになるでしょうか。
いろんなケースでクラスとメソッドを考えてみる
ここからは、もう少し別のケースを見て、クラスのメソッドを理解していきましょう。
技を覚える・忘れる
ポケモンバトルによってレベルが上がると、ポケモンは技を覚えていきます。
その技について、「手持ちの技は4つまで」というルールがあります。
そのため、以下のような流れとなります。
- 現在のそのポケモンがもつ、技の数を確認する
- 技が3つ以下であれば、その技を覚えさせる(インスタンス化された「わざ」プロパティにセットする)
- 技が4つの場合、プレイヤーに技を忘れさせるか確認する
- 忘れさせない場合、終了する
- 忘れさせる場合、プレイヤーに忘れさせる技を選択させる
- 選択した技を忘れさせる(削除する)
- 新しく覚えた技をセットする
なので、「技を覚える」というメソッドを実装するとしたら、こんな内容になるはずです。
using System;
using System.Collections.Generic;
using System.Text;
namespace Pokemon
{
public class Pokemon
{
/// <summary>
/// 技を覚える
/// </summary>
/// <param name="wazaName"></param>
public void AddWaza(string wazaName)
{
// 技を覚えている数が3つ以下の場合、技をセットして終了
if(string.IsNullOrEmpty(Waza1) || string.IsNullOrEmpty(Waza2) || string.IsNullOrEmpty(Waza3) || string.IsNullOrEmpty(Waza4))
{
// setWazaメソッド:新しい技をセット
setWaza(wazaName);
return;
}
// 技が4つの場合、プレイヤーに技を忘れさせるか確認する
// askForgetWazaメソッド:技を忘れさせるか確認
// 忘れさせない場合、終了する
if (!askForgetWaza(wazaName))
{
return;
}
// 忘れさせる場合、プレイヤーに忘れさせる技を選択させる
// selectForgetWazaメソッド:忘れさせる技を選択して忘れさせる
selectForgetWaza(wazaName);
// setWazaメソッド:新しい技をセット
setWaza(wazaName);
}
/// <summary>
/// 新しい技をセット
/// </summary>
/// <param name="wazaName"></param>
protected void setWaza(string wazaName)
{
if (string.IsNullOrEmpty(Waza1))
{
Waza1 = wazaName;
}
else if (string.IsNullOrEmpty(Waza2))
{
Waza2 = wazaName;
}
else if (string.IsNullOrEmpty(Waza3))
{
Waza3 = wazaName;
}
else if (string.IsNullOrEmpty(Waza4))
{
Waza4 = wazaName;
}
return;
}
}
}
「技」の実装方法
上記のコードで、「技」について触れました。string wazaNameとして、技名文字列として実装しています。
でも、本当に「技」情報を、文字列として実装して、いいのでしょうか?
「そのポケモンが覚えている技」というものを、単に「技の名前文字列」(ハイドロポンプとか、かえんほうしゃとか)で管理しようとすると、ポケモンバトルの際に、やっぱり破綻します。プログラミング始めたての方が陥る現象です。
ポケモンには、さまざまな種類の「技」があります。
そして、その「技」には、さまざまな共通の情報があります。例えば、以下のような内容です。
- 名前
- タイプ
- 威力
- PP
- 攻撃・補助
- 物理・特殊
- 命中率
- 追加効果
そして、例えば「はたく」という技は、以下のような効果があります。
- 名前:はたく
- タイプ:ノーマル
- 威力:40
- PP:35
- 攻撃・補助:攻撃
- 物理・特殊:物理
- 命中率:100
- 追加効果:なし
「うたう」という技は、以下のような効果があります。
- 名前:うたう
- タイプ:ノーマル
- 威力:なし
- PP:15
- 攻撃・補助:補助
- 物理・特殊:なし
- 命中率:55
- 追加効果:相手を眠らせる
そう、つまり何が言いたいかというと、ポケモンの種類と同様に、技についても、クラスで実装できるということです。
Wazaクラスを実装するのであれば、以下のようになるでしょうか。
using System;
using System.Collections.Generic;
using System.Text;
namespace Pokemon
{
/// <summary>
/// 技クラス
/// </summary>
public class Waza
{
/// <summary>
/// 技名
/// </summary>
public string Name;
/// <summary>
/// タイプ
/// </summary>
public string Type;
/// <summary>
/// 威力
/// </summary>
public int Power;
/// <summary>
/// PP
/// </summary>
public int PP;
/// <summary>
/// 攻撃かどうか
/// </summary>
public bool IsAttack;
/// <summary>
/// 物理かどうか
/// </summary>
public bool IsButsuri;
/// <summary>
/// 命中率
/// </summary>
public int HitRate;
}
/// <summary>
/// はたくクラス
/// </summary>
public class Hataku : Waza
{
/// <summary>
/// 技名
/// </summary>
public string Name = "はたく";
/// <summary>
/// タイプ
/// </summary>
public string Type = "ノーマル";
/// <summary>
/// 威力
/// </summary>
public int Power = 40;
/// <summary>
/// PP
/// </summary>
public int PP = 35;
/// <summary>
/// 攻撃かどうか
/// </summary>
public bool IsAttack = true;
/// <summary>
/// 物理かどうか
/// </summary>
public bool IsButsuri = true;
/// <summary>
/// 命中率
/// </summary>
public int HitRate = 100;
}
/// <summary>
/// うたうクラス
/// </summary>
public class Utau : Waza
{
/// <summary>
/// 技名
/// </summary>
public string Name = "うたう";
/// <summary>
/// タイプ
/// </summary>
public string Type = "ノーマル";
/// <summary>
/// PP
/// </summary>
public int PP = 15;
/// <summary>
/// 攻撃かどうか
/// </summary>
public bool IsAttack = false;
/// <summary>
/// 物理かどうか
/// </summary>
public bool IsButsuri = false;
/// <summary>
/// 命中率
/// </summary>
public int HitRate = 55;
}
}
ポケモン側の実装も、このようになります。わざ1~4が、string型から、Wazaクラスに変わりました。
using System;
using System.Collections.Generic;
using System.Text;
namespace Pokemon
{
public class Pokemon
{
/// <summary>
/// わざ1
/// </summary>
//public string Waza1;
public Waza Waza1;
/// <summary>
/// わざ2
/// </summary>
//public string Waza2;
public Waza Waza2;
/// <summary>
/// わざ3
/// </summary>
//public string Waza3;
public Waza Waza3;
/// <summary>
/// わざ4
/// </summary>
//public string Waza4;
public Waza Waza4;
}
}
そのポケモンが持っている技の型も、Wazaクラスになるわけです。
こちらの方が、ポケモンバトルで技を使用する際に、圧倒的に使いやすくなります。
他にも、上記で書いた、「技を覚える」というメソッドも、Wazaクラスに変わることで、圧倒的に実装しやすくなるでしょう。
このように、「クラスで実装した方がいい」ものは、他にも非常に多数あります。
例を挙げれば、以下のようなものでしょうか。
- タイプ
- 技の追加効果(共通の追加効果は多数あるので)
- トレーナー
これらの内容を、どう実装すればいいだろう?ということを、プログラミングはしなくても、設計して考えてみることをオススメします。
ヌケニンで学ぶオーバーライド
これまでの説明で、「攻撃力などのステータスは、種族値や個体値やレベルなどで計算して決まる」「計算式は、すべてのポケモンで共通している」と書きました。
しかし、初代のポケモンではないのですが、ここで例外となるポケモンがいます。「ヌケニン」というポケモンです。
ヌケニンは、「レベルや個体値などに関わらず、HPが常に1」というポケモンです。HP1なんですよ。とんでもないポケモンですね。
では、このヌケニンを実現するために、どのような実装を行えばいいでしょうか?
ヌケニンの実装には、「オーバーライド」という方法が非常に役立ちます。
using System;
using System.Collections.Generic;
using System.Text;
namespace Pokemon
{
public class Nukenin : Pokemon
{
/// <summary>
/// 最大HP取得。ヌケニン専用
/// </summary>
/// <returns></returns>
public new int GetMaxHP()
{
// ヌケニンのHPは常に1
return 1;
}
}
}
このように、NukeninクラスのGetHPメソッドで、"return 1;"だけする処理を記載します。
そうすることで、ゲーム画面やバトルでHPを使用するとき、全ポケモン共通の計算式を使用せず、「常に1」ということを実現できます。
これが、オーバーライドです。共通のものがあっても、独自の処理を実装できるので、非常に便利です。
このような「独自の処理」が必要なものは、ポケモンには多数ありそうです。
例えば、以下のようなものです。それらの処理をどうやって実装するか?という所を、考えてみると良さそうです。
- わざ「フライングプレス」:「格闘」と「飛行」タイプの技として扱う
- わざ「フリーズドライ」:氷タイプの技だけど、水タイプの技にも効果抜群となる
- ポケモン「パッチール」の、見た目の模様:パッチールの模様は、全部で4,294,967,296通りある
まとめ:さらなる理解のために
以上で、ポケモンについての解説は終了です。
「よく分かった!」という人もいれば、「やっぱりさっぱり!」という人もいるかもしれません。
大切なのは、「自分で設計して、自分で実装してみる」ことです。まずは設計だけでも構いません。
そして、クラスやメソッドの実装の練習を行うためには、身近な、自分が愛着を持っている内容がオススメです。
「これらの内容を、自分でプログラミングするなら、どうやって設計するかな・・・?」ということを、自分で考えてみるのが、すごく大切です。
例を挙げれば、こんな内容でしょうか。
- マリオの敵キャラ
- メタルギアソリッドの武器
- ときメモの各キャラのステータスや高感度
- 遊戯王の各カード
そして、ここまで自分が書いたコードも、かなりの勢いで改善点があります。例えば、以下のような内容です。
- 種族値やタイプは、各ポケモンで固定だけれども、今の実装だと、何らかの理由で上書きをすることができてしまう。なので、これらの値は読み取り専用で実装するのが良い。
- 「やせいの ポケモンが とびだしてきた!」での、ポケモンのレベル・個体値・最初に覚える技の決定処理を、今はコンストラクタで実施している。しかし、コンストラクタで実装すると、例えば「セーブデータから『続きから』のデータを読み込んで、プリンの情報を取得する」という処理でも、この「とびだしてきた処理」が実行されてしまう。なので本当は、別の方法で「とびだしてきた処理」を実装した方が良い。
対戦機能、セーブデータから読み込み機能・・・といった機能を実装していくうちに、こういった問題にぶち当たることは、よくあります。そのたびにリファクタリングして、都度対応していく必要があります。
以前、入門者向けにブラックジャックの記事も書いたのですが、分かりやすい・取っつきやすい例から、自分で手を動かして、「あーでもないこーでもない」を繰り返し、理解・定着していくことが、何よりも重要です。
この記事を読んだ方が、少しでもクラスとメソッドについて理解が進んでくれていたら、すごく嬉しいです。
THE END