【Unity/C#】脱初心者C#コード記述法

Unity/C#

はじめに

最近作ったゲームと、ゲーム制作を始めたばかりの時に作ったゲームでは、コードの可読性が全く違い、とても高揚しました。

それは、書道で綺麗な字が書けるようになっていくように…

それは、水泳でどんどんフォームが綺麗になっていくように….

それは、空手で所作のキレが鋭くなっていくように…

…まー、どれもやったことないんですけどねww

綺麗にコードが書けるようになると、プログラミングがより楽しくなり、制作効率も上がることが体感出来たので、綺麗に書けるコツをまとめて記事にしました。

省略できるものは省略する

this

省略可能な記述の代表格。thisとは文字通り現在のクラスそのものの事を指します。

round.text = this.rountCount.ToString();

古めの海外のアセットなどのコードでたまに見かけますが、プログラム内のグローバル変数などを参照する時にわざわざthisを記述する必要はないです。

round.text = rountCount.ToString(); //thisは不要

gameObject

gameObjectは、現在のスクリプトがアタッチされているオブジェクトそのものを指します。

var _lpos = gameObject.transform.localPosition;
var _comp = gameObject.GetComponent<AdMobAdaptiveBanner>();

プログラム側で勝手に判断してくれるのでわざわざ記述する必要はないです。

var _lpos = transform.localPosition; //gameObjectは不要
var _comp = GetComponent<AdMobAdaptiveBanner>(); //gameObjectは不要

private

privateやpublicといったものはアクセス修飾子と呼ばれています。変数や関数がどのアクセスレベルまで許可するのかを定義するものです。

private void VsModeRun() {
    gameManager.gameMode = (int)MODE.vs;
    nextScene.NextSceneRun(menu);
}

アクセス修飾子は何も記述しなければ勝手に「private」と判断するので、記述する必要はありません。

(ただし、私はMonoBehaviour経由のAwake,Start,Update,FixUpdateなど区別するために、自分で作った関数にはあえてprivateを付けています。)

//privateは不要
void VsModeRun() {
    gameManager.gameMode = (int)MODE.vs;
    nextScene.NextSceneRun(menu);
}

new クラス名();

インスタンス化の記述は以下のようなことが多いと思います。

WeaponClass weaponClass = new WeaponClass();
List<WeaponClass> weaponList = new List<WeaponClass>();

この場合、newの後のクラス名を省くと、自動的に代入する変数の型が指定されるので「 = new();」の形でOKです。

WeaponClass weaponClass = new();
List<WeaponClass> weaponList = new();

無駄にpublicを使わない

初心者講座で、こういう解説見たことありませんか?

インフルエンサー
インフルエンサー

この変数はUnityの画面でインスペクターにドラッグ&ドロップで取得したいのでpublicと記述しましょう。

そもそもprivateとpublicの使い分けは、インスペクターに表示させること(シリアライゼーション)が目的ではなくクラス外からの呼び出しの可不可を定義する記述です。

[SerializeField]を使う

インスペクター上でオブジェクトやコンポーネントを取得する目的であれば、Attribute(属性)の「SerializeField」を使うのが美しい記述方法だと言えます。

[SerializeField]
Transform gameArea; 

また、逆にクラス外からの呼び出しはしたいけど、インスペクターには表示させる必要がない場合は、Attribute(属性)の「HideInInspector」を記述します。

[HideInInspector]
public Transform gameArea;

マジックナンバーは使わない

以下は昔、私が実際に書いたコードです。

private void TournamentDataLoad() {
    for (int i = 0; i < 12; i++) {
          //~省略~
    }
}

まずはこのコードを見て、for文の12が何の数字か分かりますか?私は一瞬分かりませんでしたw(ちなみに12は1年間の月の数を表してる)

このように変数ではなく数字を直接使用したものをマジックナンバーと呼びます。

const/enumを使う

const

constは定数を定義する記述です。

const int monthMaxCount = 12;

private void TournamentDataLoad() {
    for (int i = 0; i < monthMaxCount; i++) {

          //~省略~
    }
}

この方法で記述をすると、「monthMaxCount」という変数は常に12であると定義されます。

for内のmonthMaxCountを見れば、

なるほど!「月の最大数」と i を比較してるんやな!

…と、一発で認識できるわけです。

さらに、アップデートなどでコードを修正する場合、定数を定義した値を変更すれば、使用している場所をひとつひとつ修正する必要はありません

後述の項目⑥でも紹介しますが、例えばステージ数の上限やキャラクターの移動速度など、ゲーム全体で使用する値を定数で定義しておくことで、管理がとても楽になります。

enum

enum(列挙型)は0,1,2,3,4,5…と続く値を自動で割り当てることができます。

enum MONTH {
    january,
    february,
    march
}

この場合、january=0,february=1,march=2が入ります。ただし、配列変数として使用する場合はint型へのキャストが必要になります。

(int)MONTH.february

この形式で列挙型をint型として使用できます。さらにMONTH.februaryと記述するので、

MONTH.februaryということは、「月の中の1月を意味する変数」なんやな!

…と、一発で理解できるわけです。

コードを縦に伸ばさない

パソコンの画面というものは基本的に横長です。コードが縦長になると見ずらいです。

無駄なコードを省くクセを付けておくと、縦長なコードを緩和することができます。

{}だけで改行しない

Visual Studioの初期設定だと、

public void CustomIdCheck() 
{
    playerGameData.customId = PlayerPrefs.GetString("customId", "none");
    if (playerGameData.customId == "none")
    {
        playerGameData.customId = randomIdCreate.GetId();
        PlayerPrefs.SetString("customId", playerGameData.customId);
        PlayerPrefs.Save();
    }
}

上記のように関数の記述の後に「{}(中括弧)」と改行が自動入力されますが、この改行に特に意味はないので、

public void CustomIdCheck(){
    playerGameData.customId = PlayerPrefs.GetString("customId", "none");
    if (playerGameData.customId == "none") {
        playerGameData.customId = randomIdCreate.GetId();
        PlayerPrefs.SetString("customId", playerGameData.customId);
        PlayerPrefs.Save();
    }
}

このような形式で記述します。プログラミングとは変数と関数で成り立っているので、1行とはいえ膨大な量になります。

{}をそもそも使用しない

if文やfor文、foreach文の処理が1行で事足りる場合があります。

if (dispMessage >= messageList.Count)
{
    Destroy(gameObject);
}

{ }を省略して、以下のようにもっとスッキリさせることができます。

if (dispMessage >= messageList.Count) Destroy(gameObject);

returnを有効的に使う

例として、攻撃タイプ(_type)が「Slash」の場合のみ適用されパワー(_power)が10以下はダメージがゼロになり、20以上ならダメージが2倍になり、それ以外は等倍の値を返すSlashDamageCulという関数を用意しました。

private int SlashDamageCul(string _type, int _power) {
    int _damage = 0;
    if (_type == "Slash") {
        //パワーが10以下はダメージゼロ
        //パワーが20以上はダメージ2倍
        //それ以外は等倍
        if (_power <= 10) {
            _damage = 0;
        }
        else if (_power >= 20) {
            _damage = _power * 2;
        } else {
            _damage = _power;
        }
    }
    return _damage;
}

ポイントは_typeが”Slash”以外は処理する必要がない点と、_powerを比較するifの条件分岐の後に他の処理はないという点です。

returnを使用することでif文以下の処理は通らず、コードがスマートなります。(ついでにコメントも移動…)

private int SlashDamageCul(string _type, int _power) {
    int _damage = 0;
    if (_type != "Slash") return _damage;
    if (_power <= 10) return _damage = 0;          //パワーが10以下はダメージゼロ
    if (_power >= 20) return _damage = _power * 2; //パワーが20以上はダメージ2倍
    return _power;
}

変数と関数を整理する

同型の変数や関数を区別し、整理することで制作効率が上がります。

region/endregion

region/endregionは初心者講座などではほぼ取り扱われませんが、とても便利な記述方法です。

以下のコードでは、グローバル変数を定義していますが、このスクリプトを触るたびにこの量のコードをスクロールしていくと、とても制作効率が悪いです。

[SerializeField]
Transform target;
[SerializeField]
GameObject scenarioPanelPrefab;
[SerializeField]
GameObject newRuleInfoPanalPrefab;
[SerializeField]
SEPlay sePlay;
[SerializeField]
Text[] texts;

GameManager gameManager;
NextScene nextScene;
Database database;

enum MENU {
    Scenario,
    DeckEdit,
    Trade,
    Clollection,
    Rule
}
enum SCENE {
    Title,
    Menu,
    Battle,
    Clollection,
    Rule,
    DeckEdit,
    Trade
}
enum CONTENTS {
    basic,
    weakness,
    combo,
    collection,
    match,
    male,
    female,
    fire,
    sum,
    water,
    leaf,
    less,
    raid,
    trade,
    sacrifice
}

const int nomal = 0;
const int extra = 1;
const int lv_basic = 0;
const int lv_wakness = 5;
const int lv_combo = 5;
const int lv_collection = 10;
const int lv_match = 15;
const int lv_male = 15;
const int lv_female = 20;
const int lv_fire = 25;
const int lv_sum = 25;
const int lv_water = 30;
const int lv_leaf = 35;
const int lv_less = 40;
const int lv_raid = 40;
const int lv_trade = 45;
const int lv_sacrifice = 50;

大きく分けて、SerializeField・Private・Enum・Constになっています。

それぞれに以下の形式でregionとendregionを記述します。

#region 〇〇

//~各グローバル変数~

#endregion
#region SerializeField
[SerializeField]
Transform target;
[SerializeField]
GameObject scenarioPanelPrefab;
[SerializeField]
GameObject newRuleInfoPanalPrefab;
[SerializeField]
SEPlay sePlay;
[SerializeField]
Text[] texts;
#endregion

#region Private   
GameManager gameManager;
NextScene nextScene;
Database database;
#endregion

#region Enum
enum MENU {
    Scenario,
    DeckEdit,
    Trade,
    Clollection,
    Rule
}
enum SCENE {
    Title,
    Menu,
    Battle,
    Clollection,
    Rule,
    DeckEdit,
    Trade
}
enum CONTENTS {
    basic,
    weakness,
    combo,
    collection,
    match,
    male,
    female,
    fire,
    sum,
    water,
    leaf,
    less,
    raid,
    trade,
    sacrifice
}
#endregion

#region Const
const int nomal = 0;
const int extra = 1;
const int lv_basic = 0;
const int lv_wakness = 5;
const int lv_combo = 5;
const int lv_collection = 10;
const int lv_match = 15;
const int lv_male = 15;
const int lv_female = 20;
const int lv_fire = 25;
const int lv_sum = 25;
const int lv_water = 30;
const int lv_leaf = 35;
const int lv_less = 40;
const int lv_raid = 40;
const int lv_trade = 45;
const int lv_sacrifice = 50;
#endregion

こうすることで、region 〇〇~endregionをエディタ上で畳むことができます。畳んだ状態でも〇〇の部分は表示されたままなので、視認性を損なうことはありません。

また、変数だけではなく関数にも使用可能です。

グローバル変数とローカル変数を区別する

クラス内のどこでも使用できる変数がグローバル変数で、関数内でのみ使えるのがローカル変数です。

では、以下の関数でどれがグローバル変数でどれがローカル変数か分かりますか?

private void PanelCreate(int num) {
    sePlay.SEPlayRun((int)SOUND.tap);
    var prefab = Instantiate(newRuleInfoPanalPrefab, target);
    prefab.GetComponent<NewRuleInfoPanel>().DataSet(num);
    database.playerData.informationFlg[num] = 1;
}

関数の引数である「num」関数内で宣言した「prefab」が該当します。

変数名に目印を付ける

プログラムを解読すれば分かりますが、わざわざコードを読まなくても一発で視認できる方法の簡単な手段として、変数名に目印を付けことが挙げられます。

私の場合は、ローカル変数の頭には「_(アンダーバー)」を付けるようにしています。

右手でShiftキー押しながら隣の_キーを押すだけだから楽なんやさ。

private void PanelCreate(int _num) {
    sePlay.SEPlayRun((int)SOUND.tap);
    var _prefab = Instantiate(newRuleInfoPanalPrefab, target);
    _prefab.GetComponent<NewRuleInfoPanel>().DataSet(_num);
    database.playerData.informationFlg[_num] = 1;
}

だいぶコードが見やすくなったと思います。

namespace/usingを使う

ゲーム全体で共有したいクラスがあると思います。例えば、「武器(weapon)」というクラスは装備画面、戦闘時、入手時、ショップでの売買など様々な場面で使用したいはずです。

初心者講座とかでこういうの聞いたことないですか?

インフルエンサー
インフルエンサー

一番上にusing UnityEngine;とか最初から書いてありますが、それはそういうものなので、今回は触れません。

…私は触れちゃいます。

namespace

別名・名前空間(そのまんま)。こう聞くとなんか難しそうと思われるかもしれませんが、仕組みは超シンプルです。

「namespace 名前{}」という形式で記述します。以下の場合は「MyGeneral」という名前空間「WeaponClass」というクラス入っている状態です。

namespace MyGeneral {
    public class WeaponClass{
        public string weaponName;
        public int weaponAtk;
        public int weaponValue;
    }
}

using

外部のクラスから呼び出す時は「using 名前空間;」を一番上に追記することで、名前空間内のクラスを呼び出せます。

using UnityEngine;
using UnityEngine.UI;
using MyGeneral;

public class TitlePanel : MonoBehaviour {
    WeaponClass weaponClass = new();

    void Start() {
        texts[weaponName].text = weaponClass.weaponName;
    }
}

さいごに

この記事ではC#のコードの記述方法をいくつか紹介しました。

私もまだまだ勉強中の身ではありますが、皆様が「プログラムなんて動けばいい」から脱却し、より美しく、より効率良く、より楽しくコードを書けるようになることを願っています。

ゲ制最高!

コメント

タイトルとURLをコピーしました