【ゲーム開発のためのC#入門講座・応用学習編】メンバー変数について学ぼう【#4】

2022-01-184.0_C#応用学習編

おさらい

  • クラスとは、データと処理(メソッド)をひとまとめにした設計図
  • 「new」というキーワードを使ってクラス(設計図)から生み出したモノ(オブジェクト)のことをインスタンス(実体)と呼ぶ
  • クラス(設計図)からインスタンス(実体)を作成し、インスタンスの行動(メソッド)の積み重ねでプログラムを構築する、というのがオブジェクト指向の基本的な考え方
  • クラスという設計図を皆で再利用して、効率のよいプログラミングやゲーム開発を目指そう

これまではオブジェクト指向の考え方だったり、クラスの役割や扱い方だったり、概念的なものを中心に紹介してきました。ここからはクラスのより深い理解やさらに再利用しやすくする方法などを学んでいきます。

少しずつオブジェクト指向やクラスという存在に慣れていきましょう。

ということで、まずはメンバー変数です。

変数っていつ解放されるんだっけ?

関数で使用した変数分の領域がいつメモリから解放されるのか、覚えていますか?

ヒープメモリはガベージ・コレクションに任せるとして、スタックメモリはどうでしょうか?

そう、関数終了時にまとめてメモリが解放されるんでしたね。

もし忘れてしまっていたら、基礎強化編の内容を復習してみましょう。

さて、それでは前回も使用した下記のプログラムを見てみてください。

BattleCharacter link = new BattleCharacter();
link.Name = "リンク";
link.Level = 10;
link.HP = 25;
link.ATK = 10;
​
BattleCharacter goblin = new BattleCharacter();
goblin.Name = "ゴブリン";
goblin.Level = 5;
goblin.HP = 10;
goblin.ATK = 7;
​
while (true)
{
    goblin.HP -= link.Attack();
    if (goblin.HP <= 0)
    {
        break;
    }
    link.HP -= goblin.Attack();
    if (link.HP <= 0)
    {
        break;
    }
}
​
if (goblin.HP <= 0)
{
    Console.WriteLine("勝利!");
}
else
{
    Console.WriteLine("ゲームオーバー!");
}
​
public class BattleCharacter
{
    public string Name = "";
    public int Level = 0;
    public int HP = 0;
    public int ATK = 0;
​
    public int Attack()
    {
        return ATK + Level * 2 / 5;
    }
}

クラスに紐づく関数のことをメソッドと言うんでしたね。

ということは、メソッドで使用した変数分のメモリ領域は、メソッド終了時にまとめて解放され……あれ?

考えてみると、クラスにまとめた変数ってメソッドの中で生成してないですよね。

public class BattleCharacter
{
    // ↓そもそもメソッドの中で生成していない
    public string Name = "";
    public int Level = 0;
    public int HP = 0;
    public int ATK = 0;
​
    public int Attack()
    {
        return ATK + Level * 2 / 5;
    }
}

となると、このクラスにまとめた変数分のメモリはいつ解放されるのでしょう?

クラスにまとめた変数はズッ友

結論から言うと、クラスにまとめた変数は、そのクラスから生成されたインスタンスが解放されるまで消えません

/*
    この「link」というインスタンスに紐づく
    「Name」や「Level」、「HP」「ATK」といった変数は
    「link」というデータが解放されるまで消えない
*/
BattleCharacter link = new BattleCharacter();
link.Name = "リンク";
link.Level = 10;
link.HP = 25;
link.ATK = 10;

そのインスタンスに紐づく情報なのに、いちいちメソッドを実行する度に消えてしまったら困ってしまいますよね。少なくともそのインスタンス自体が不要になるまでは、残り続けてもらう必要があります。

そのため、クラスにまとめた変数はインスタンスが生成されると同時に生成され、インスタンスが解放されると同時に解放されます。

いわば運命共同体、ズッ友な訳です。

// ↓この「link」というインスタンスが不要になり、
//  どこからも参照されなくなってガベージ・コレクションに解放されるまで、
BattleCharacter link = new BattleCharacter();
// ↓紐づく変数は消えない。ズッ友だょ!
link.Name = "リンク";
link.Level = 10;
link.HP = 25;
link.ATK = 10;

だからインスタンスを生成した直後からいきなり値を変更することができたんですね。

BattleCharacter link = new BattleCharacter();
// ↓インスタンス生成直後からいきなり値を変更できる
link.Name = "リンク";
link.Level = 10;
link.HP = 25;
link.ATK = 10;

そしてそこで設定された値をメソッドの中でも参照することができたのです。

BattleCharacter link = new BattleCharacter();
// ↓メソッドを実行する前に設定しておいた下記の値を、
link.Name = "リンク";
link.Level = 10;
link.HP = 25;
link.ATK = 10;
​
public class BattleCharacter
{
    public string Name = "";
    public int Level = 0;
    public int HP = 0;
    public int ATK = 0;
​
    public int Attack()
    {
        // ↓メソッドの中でも参照することができる
        return ATK + Level * 2 / 5;
    }
}

このズッ友な変数のことを、クラスに「属する」というニュアンスからメンバー変数と呼びます。

メンバー変数はインスタンスに紐づいているものなので、インスタンス毎の値を読み書きします。

BattleCharacter link = new BattleCharacter();
link.Name = "リンク";
link.Level = 10;
link.HP = 25;
link.ATK = 10;
​
// ↓「link」が「Attack」というメソッドを実行した場合、
goblin.HP -= link.Attack();
​
public class BattleCharacter
{
    public string Name = "";
    public int Level = 0;
    public int HP = 0;
    public int ATK = 0;
​
    public int Attack()
    {
        // ↓この「ATK」や「Level」は「link」の値が参照される
        return ATK + Level * 2 / 5;
    }
}
BattleCharacter goblin = new BattleCharacter();
goblin.Name = "ゴブリン";
goblin.Level = 5;
goblin.HP = 10;
goblin.ATK = 7;
​
// ↓「goblin」が「Attack」というメソッドを実行した場合、
link.HP -= goblin.Attack();
​
public class BattleCharacter
{
    public string Name = "";
    public int Level = 0;
    public int HP = 0;
    public int ATK = 0;
​
    public int Attack()
    {
        // ↓この「ATK」や「Level」は「goblin」の値が参照される
        return ATK + Level * 2 / 5;
    }
}

つまりメンバー変数を使うことで、

  • インスタンスが消えるまで共有したい変数を定義できる
  • インスタンス毎に異なる値を利用して処理(メソッド)を実行することができる

という恩恵が得られるんですね。

関数の場合は渡された引数がすべてでしたが、メソッドの場合は引数の他に自分自身(インスタンス)が持つメンバー変数を活用することができます。だからわざわざ呼び名をちょっと変えているんですね。

なお、メソッド中で生成し、メソッドが終わると解放される変数のことはローカル変数といいます。

オブジェクト指向ではこのローカル変数とメンバー変数を使い分けてプログラムを構築していきます。

初期値は設定しなくてもいい

クラスの構文説明時にあたかも初期値が必須であるかのように記載しましたが、実際には初期値は設定しなくても構いません。

public class クラス名
{
    // ↓この初期値はなくてもいい
    public データ型 データ名 = 初期値;
    
    public 戻り値 メソッド名(引数)
    {
        ……処理内容……
    }
}

なので先程の「BattleCharacter」は下記のように定義し直すことができます。

public class BattleCharacter
{
    public string Name;
    public int Level;
    public int HP;
    public int ATK;
​
    public int Attack()
    {
        return ATK + Level * 2 / 5;
    }
}

じゃあ初期値って設定する意味がないのかというと、そんなことはないです。

例えば「Enemy」というクラスがあったとしましょう。

public class Enemy
{
    public string Name;
    public int HP;
    public int ATK;
    public int EXP;
}

そのクラスを使って、「ゴブリンA」と「ゴブリンB」というインスタンスを生成したとします。

Enemy enemyA = new Enemy();
enemyA.Name = "ゴブリンA";
​
Enemy enemyB = new Enemy();
enemyB.Name = "ゴブリンB";

この「ゴブリンA」と「ゴブリンB」は異なる存在ではありますが、ステータスや倒した時にもらえる経験値は同じです。

だとすると、そういう共通している項目までいちいちインスタンス毎に設定するのは面倒ですよね。

Enemy enemyA = new Enemy();
enemyA.Name = "ゴブリンA";
// ↓enemyBと同じ値を設定している。めんどい!
enemyA.HP = 10;
enemyA.ATK = 5;
enemyA.EXP = 1;
​
Enemy enemyB = new Enemy();
enemyB.Name = "ゴブリンB";
// ↓enemyAと同じ値を設定している。めんどい!
enemyB.HP = 10;
enemyB.ATK = 5;
enemyB.EXP = 1;

そういう「インスタンスが消えるまで共有したいけど、すべてのインスタンスで値は共通なんだよな」という場合は、それを初期値とすることで、個々に設定する手間を省くことができます。

public class Enemy
{
    public string Name;
    // ↓共通している値なら初期値にしておくことで、
    public int HP = 10;
    public int ATK = 5;
    public int EXP = 1;
}
​
Enemy enemyA = new Enemy();
// ↓共通していないところだけ設定すればOKになる
enemyA.Name = "ゴブリンA";
​
Enemy enemyB = new Enemy();
// ↓共通していないところだけ設定すればOKになる
enemyB.Name = "ゴブリンB";

初期値を設定するべきか否かは、インスタンス毎に異なる値を設定したいかどうかで判断するとよいでしょう。

Unityでの活躍ポイント

Unityでゲーム開発をする際も、例えばキャラクターの座標やステータス、装備情報などはそのキャラクターが存在する限り、消えて欲しくないですよね。また、その情報はキャラクター毎に個別に保持したいはずです。

その場合はメンバー変数を活用することで、簡単に実現することができます。

ただし、メンバー変数はどのタイミングでも値を更新することができるし、どのタイミングでも値を参照することができます。逆にいうと、どこで更新されて、どこで参照されているのか分かり辛いという難点があります。

その点、ローカル変数はそのメソッド内でしか更新も参照もされないため、問題が発生したとしても非常に対処しやすいです。

ズッ友が悪友にならないよう、

  1. まずはローカル変数で対処できないか検討する
  2. どうしてもクラスに紐づけた方がいいと判断される場合のみ、メンバー変数にする

という手順で活用することをオススメします。

実践演習

それでは実際にメンバー変数を使ってみましょう。

演習①メンバー変数を定義しよう!

仕様

下記のメンバー変数をクラス「BattleCharacter」に追加してください。
・int型「MaxHP」。初期値なし
・bool型「IsDead」。初期値はFalse

テンプレート
BattleCharacter link = new BattleCharacter();
link.Name = "リンク";
link.Level = 10;
link.HP = 25;
link.MaxHP = 25;
link.ATK = 10;
​
BattleCharacter goblin = new BattleCharacter();
goblin.Name = "ゴブリン";
goblin.Level = 5;
goblin.HP = 10;
goblin.MaxHP = 10;
goblin.ATK = 7;
​
while (true)
{
    goblin.HP -= link.Attack();
    if (goblin.HP <= 0)
    {
        goblin.IsDead = true;
        break;
    }
    link.HP -= goblin.Attack();
    if (link.HP <= 0)
    {
        link.IsDead = true;
        break;
    }
}
​
if (goblin.IsDead)
{
    Console.WriteLine("勝利!");
}
else
{
    Console.WriteLine("ゲームオーバー!");
}
​
public class BattleCharacter
{
    public string Name = "";
    public int Level = 0;
    public int HP = 0;
    public int ATK = 0;
    // ↓ここに追加のメンバー変数を定義しよう!
    
    // ここまで
​
    public int Attack()
    {
        return ATK + Level * 2 / 5;
    }
}

演習②メンバー変数を追いかける苦労を体験しよう!

下記プログラムの実行結果を予想してください。

Counter counter = new Counter();
counter.CountUp();
counter.CountUp2();
counter.CountUp();
counter.CountDown();
​
Console.WriteLine(counter.Count);
Console.WriteLine(counter.Return10());
​
public class Counter
{
    public int Count = 0;
    
    public void CountUp()
    {
        Count++;
    }
        public void CountUp2()
    {
        Count += 2;   
    }
    public void CountDown()
    {
        Count--;
    }
    public int Return10()
    {
        int count = 10;
        return count;
    }
}

答え合わせ

演習①の答え

BattleCharacter link = new BattleCharacter();
link.Name = "リンク";
link.Level = 10;
link.HP = 25;
link.MaxHP = 25;
link.ATK = 10;
​
BattleCharacter goblin = new BattleCharacter();
goblin.Name = "ゴブリン";
goblin.Level = 5;
goblin.HP = 10;
goblin.MaxHP = 10;
goblin.ATK = 7;
​
while (true)
{
    goblin.HP -= link.Attack();
    if (goblin.HP <= 0)
    {
        goblin.IsDead = true;
        break;
    }
    link.HP -= goblin.Attack();
    if (link.HP <= 0)
    {
        link.IsDead = true;
        break;
    }
}
​
if (goblin.IsDead)
{
    Console.WriteLine("勝利!");
}
else
{
    Console.WriteLine("ゲームオーバー!");
}
​
public class BattleCharacter
{
    public string Name = "";
    public int Level = 0;
    public int HP = 0;
    public int ATK = 0;
    // ↓ここに追加のメンバー変数を定義しよう!
    public int MaxHP;
    public bool IsDead = false;
    // ここまで
​
    public int Attack()
    {
        return ATK + Level * 2 / 5;
    }
}

演習②の答え

コンソール画面

3
10

どうでしょうか?

メソッド「Return10」の方はローカル変数である「count」を使っていたため、すぐに結果を予想することができたのではないでしょうか?

一方で、メンバー変数「Count」はそれまでのメソッドの内容をすべて追いかけないといくつが出力されるか予想できなかったと思います。

安易にメンバー変数を使うと地獄を見るぞ、ということを少しでも感じてもらえていれば幸いです。

改めて、Unityでの活躍ポイントに記載した運用方法をオススメします。

まとめ

  • クラスに属する変数のことをメンバー変数という
  • メンバー変数はメソッドの中でだけ利用されるローカル変数と違い、インスタンスが存在する限り共にあり続ける。ズッ友
  • メンバー変数を活用することで、インスタンス毎に共有しておきたい情報を管理することができる
  • ただし安易に使うと調査が大変で地獄を見ることになるので、ご利用は計画的に!

次回もクラスのさらなる詳細な使い方を学習していきます。お楽しみに!

それでは、今回もお疲れ様でした!

また次の記事でお会いしましょう!

Posted by 夕目紅