注:当ブログでは広告を利用しています。

Unity継承の使い方:enumとエラー頻出で地獄を味わった件

おはよう。
今ここを開いているあなたはUnityを使っており、
継承で大きく迷っているのではないかと考えている。

迷っているというか、複数のエラーが出て悪戦苦闘というか。
私は二日以上費やし、やっと継承したプログラムを記述できた。

今回の記事が継承における悪戦苦闘を少しでも救えればいいなと思っている。

 

継承(Inheritance)と親子関係

継承は英語だとInheritanceと呼ぶ。

Unityにおける継承とはテンプレート(鋳型)だ。
数学で例えるなら掛け算だ。

敵を複数実装する場合、敵のスクリプトを一つずつ作り、
同じ型名や変数をその都度コピペして動かす方法もいいが、
段々慣れてくると、同じ記述なら一つに束ねたくなる

例えるならスピード調整の関数を敵のスクリプトに一つ一つコピペしている状態から、
大本に一つ置いて各スクリプトにコピペしなくてもいい対応術だ。

継承は「大本」クラスが基底にあり、進化した派生クラスがある。
実際に継承の作り方を見て行こう。

以下、基底クラスを「親」、派生クラスを「子」と定める。

私の場合、敵全体の進行管理を行うEnemyManager
個別の敵としてMarumanという名のスクリプト(.cs)を作った。

個別敵は今後、CanonE、WeaponBoyなど個別の敵プログラムを創る予定だ。
マリオ出言うところの「クリボー、ノコノコ、バブル」と抑えてほしい。

継承において基底クラスは普通にやるが、
派生クラスはMonoBehaviourの部分をEnemyManagerに変えればいい

//親(基底)クラス
public class EnemyManager : MonoBehaviour{

//スクリプトをズラズラ書く

}
//子(派生)クラス
public class Maruman : EnemyManager //MonoBehaviourでない。
{

//親スクリプトにて、変更したい部分だけを書く
//親スクリプトをコピペしたら、エラーの元

}

継承において必ず押さえてほしいルールがある。
ルールをつかまぬままやると、私のように2日以上苦しむから。

 

すべての基本は「親(基底クラス)」にあり

継承は親(基底クラス)が全ての基本設定だ。
子(派生クラス)は親の一部分を変えるだけに過ぎない

継承を深く調べるまで、私は基本ルールすらわかっていなかった
子に親と同じ関数を入れて、わけのわからないエラーを出しまくっていた。

エラーの一部として「The same field name is serialized multiple」、
「’XXX’ is inaccessible due to its protection level」、
そして「nullreferenceexception」だ。

これらが出ている場合、本来「親」がやるべき計算や処理を、
「子」にやらせているために起きている

もっと言うなら継承に対する私の捉え方が間違っていたため、
上記三つのエラーを起こしていた。

※強引に直す方法はあるが、本質ではないし重複している

親がやるべき部分として、次のルールを抑えてほしい。

 

親子間で同じ関数があると親が捨てられる(子優先)

public class EnemyManager : MonoBehaviour
{

//うんたらかんたら(変数や型名、関数など)

void FixedUpdate(){

//うんたらかんたらその2

}//FixedUpdate

} //public class EnemyManager

//-------------------------------//

public class maruman :EnemyManager
{
//うんたらかんたら

void FixedUpdate(){

//うんたらかんたらその3

}//FixedUpdate

}//class maruman

親にStart()関数、Update関数、FixedUpdate関数を起き、
子にもStart()関数、Update関数を置いて再生ボタンを押すと……

Unityは子のStart()関数、Update関数、そして親のFixedUpdate関数を採用し、
親のStart()関数、Update関数は捨てられる運命をたどる。

どうしても使う時の対策は後で書く。

これも私をおおいに悩ませる案件となった。
敵プログラムを入れるときenumを使って複数にわけているため、
どうしても親元に敵を入れるとしてもなあ……。

親には敵が入らないし……それで子にUpdate関数などを入れ、
nullreferenceexception(参照元に何も入っていない…入ってるのに)が起きた。

原因は継承に対する間違った思い込みだった。
他のサイトや動画を通し、正しくとらえなおしてエラーが出なくなった。

関連して注意がある。

 

二つのAwake関数は子Awakeのみ取る

継承先の二つのAwake

上記スクリプトは大きく間違っている。

二つのAwake(overrideは親スクリプトを使っている)があると、
子のAwake関数のみ取ってoverrideは無視される

ついやらかすミスなので、気を付けてほしい。

 

アタッチは子スクリプトのみでOK

Unity継承スクリプト

大本のEnemyManagerから子(継承)のMarumanを作った後、
インスペクターを見たらEnemyManagerにてpublic設定している型番と変数が、
子のスクリプトに丸ごとコピーされているではないか。

子スクリプトには親にないpublicを入れており
親元スクリプトの型番と変数をすべて出し切った後に、
子独自のpublic型番+変数が載っていた。

親子間で見た目が同じスクリプトになっている。
調べたら親元のEnemyManagerスクリプトを外し、
子だけをきちんと入れていればいい

 

継承に必要なprotected

unityとprotected

上に乗せたルールをきちんと踏まえたうえで、改めて継承を作っていく。
最初にアクセス修飾子(publicやprivateなど)を抑える。

継承においてprotectedが新しく登場する。

親プログラムのみで使うならprivate、
あるいはインスペクターからいろいろいじるならpublicがあればいいが、
public設定した型と変数はインスペクター上にずらすら出てくる

子にも同じ型と変数を当てる場合、publicのほかにprotectedがある。
potectedを入れると、インスペクター上に現れない。

protectedは継承したクラスなら使いまわせるため、
親のみで使うならprotectedはいらないが、
子でも同じ変数や型を使うなら宣言しておくべき

例えば私の場合、敵の動きでRigidbody2Dを使うため、次のコードに変えた。

protected Rigidbody2D rg2d;

//private Rigidbody2D rg2d;だと親しか使えない。
//子にprivate Rigidbody2D rg2d;を書くと、子にしか通じない。

親に書けば十分であり、子に同じ書き込みを行うと重複エラー(黄色い警告)が生じる

継承時の警告

親にprotectedを置けばいいので、
子にはほとんど型番と変数の書き込みをしなくて十分だ。

もちろん子のみ使う変数や型番は子に定義しておくべし。

参照:アクセス修飾子の使い方を学ぶ

 

子が変更したい関数overrideと上書きされるvirtual

nullreferenceexception

親クラスには不要でも子クラスには必要。
親が不要だから削っているが、削るとnullエラーが起きる……

親子間の解決に数時間費やした。

親は「一枚のスクリプトで、敵が最低限の処理をすべて行える状態」であり、
子は「ここを変えてほしい」程度でしかない。

敵の種類が少ないなら親一つで十分だ。

まずは親のスクリプトをきちんと形にしなければならない。
親だけでも動くスクリプトになってから、上書きする部分を、
とりあえず最低限の関数を入れておく

「上書きされる」オリジナルの関数をpublic virtual void 関数名()をおく。
関数名の部分は英単語に置き換えてほしい。私の場合……

public class EnemyManager : MonoBehaviour
{

public virtual void Enemys(){
// public virtualをきちんと書こう

//ここに敵を動かすor止める最低限の処理を書く。
//rb2dはすでにキャッシュ済(Start関数でGetComponent取得済)

rb2d.bodyType = RigidbodyType2D.Kinematic;
rb2d.constraints = RigidbodyConstraints2D.FreezeAll;

}//Enemys()

//そしてEnemys()を目的の関数(私の場合はFixedUpdate関数)に入れておく。

void FixedUpdate(){

//ズラズラ書いてるプログラムを省略

Enemys();

//ズラズラ書いてるプログラムを省略

} //Enemys();
}//EnemyManager class

public virtualと書いている部分は
「子(継承用)スクリプトさん、ここを上書きしていいからね」であり、
上書きは別のクラスでpublic override void Enemys(){}とおく。

別のクラスは同一スクリプトでもいいし、
私のように別のスクリプトを作って、継承(MonoBehaviourを置換)すればいい。

私の場合、複数の敵を作っている。
複数の敵を選ぶための処理としてenumを使っている。

enumを使っていようが、普通に上書きできた。
今から載せるスクリプトは子であるMarumanの中身だ。

public class Maruman : EnemyManager
{

[SerializeField] public enum Enemy_Moving{
Stop,
B,
}

[SerializeField] public Enemy_Moving enemy_Moving;

public override void Enemys(){
// public overrideをきちんと書こう

speedy = speed * axisH;
switch ( enemy_Moving ){

case Enemy_Moving.Stop: //敵が止まったまま
rb2d.bodyType = RigidbodyType2D.Kinematic;
rb2d.constraints = RigidbodyConstraints2D.FreezeAll;
break;

case Enemy_Moving.B: //左右に動くのみ
rb2d.bodyType = RigidbodyType2D.Dynamic;
rb2d.AddForce( new Vector2(Mathf.Round(speedy) , gravity ) );
break;
}//enemy_Moving
}//Enemys

敵はあと数種類作っているが省略させてもらった。

子にて私が使っている型名及び変数名の定義は
public enum Enemy_Moving
public Enemy_Moving enemy_Moving;
のみだ。

後で何かあった場合、追加するかもしれない。
子はこれだけでいいのであり、ここに親と同じ関数を入れるとエラーを起こす。

同じ関数を入れるとしても、public override void FixedUpdate()はエラーを起こす、
というより子のFixedUpdateが優先される。

何より親のFixedUpdate()の中に様々な関数や変数を放り込んでおり、
どれかがprivateなりvirtualを定義していない関数があるためだ。

敵Enemys()関数もFixedUpdate(){}の外に置いている。
直接スクリプトを見てもらった方が早い。

※大本スクリプトは長いので、関係ない部分を一部省略した。

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class EnemyManager : MonoBehaviour
{

//定義、Start関数を省略

void Update()
{
if (GameManager.gameplaying !="nowPlay" ) return;

//省略

} //Update()

void FixedUpdate()
{

if(MoveOrStop) rb2d.bodyType = RigidbodyType2D.Static;
else {
PitFallContact();
Enemys();
} //else

} //Fixed Update()

protected bool PitFallContact(){ //落とし穴判定,falseなら落下防止のプログラムを出す
startposition2 = transform.position + transform.right * 1f * this.transform.localScale.x;
endposition2 = startposition2 - Vector3.up * 1.1f;
return pitfalled = Physics2D.Linecast(startposition2, endposition2, GroundLayer);
} //PitFallContact()

//親のみで使う上、数字「1fや1.1f」を全ての敵に当てはめるならprotectedを外してもOK
//数字「1fや1.1f」を変数にし、敵別に設定して変えるなら必ずprotected(継承)を入れる

public virtual void Enemys(){
//Enemys関数を更新するため、必ずpublic virtualをつける
//基準を一つ、適当におく(自分の場合は完全停止状態)
rb2d.bodyType = RigidbodyType2D.Kinematic;
rb2d.constraints = RigidbodyConstraints2D.FreezeAll;

} // Enemys()

//ほかのスクリプトを省略

} // public class EnemyManager : MonoBehaviour

子はすでに作った通りだが、もう一度載せる。

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEditor;

public class Maruman : EnemyManager
{

[SerializeField] public enum Enemy_Moving{
Stop,
B,
}

[SerializeField] public Enemy_Moving enemy_Moving;

public override void Enemys(){

speedy = speed * axisH;
switch ( enemy_Moving ){

case Enemy_Moving.Stop:
rb2d.bodyType = RigidbodyType2D.Kinematic;
rb2d.constraints = RigidbodyConstraints2D.FreezeAll;
Debug.Log("F") ;//デバッグをいれて、enumが起動しているかテストしてみよう
break;

case Enemy_Moving.B: //動くのみ
rb2d.bodyType = RigidbodyType2D.Dynamic;
rb2d.AddForce( new Vector2(Mathf.Round(speedy) , gravity ) );
break;

}//enemy_Moving
}//Enemys

} //public class Maruman : EnemyManager

後はインスペクターから親のスクリプトを外せばOK.

Unityスクリプト継承

プレハブにおいて、アセットからオブジェクトを入れられるものは、
はじめのうちに入れておいた方がいい。

Unity継承スクリプト親元の処理

一応EnemyManagerスクリプトは空ボックスEnemyManagerに入れて置いた。
もちろんチェックを外しているので「ただある」のみで意味はない。

 

敵が床から落ちないようにする

先ほど書いたスクリプトPitFallContact()部分は上記動画を参考にした。
マリオでいうクリボーみたいに落ちるタイプと、
赤ノコノコみたいに落ちないタイプを創る参考にしてほしい。

私の場合、敵の足元にCirclecollder2Dを置いて、
さらにPysicsMatelial2Dを置いて、摩擦(Friction)を調整している。

摩擦、初速度、限界速度(設定以上の速度を出さないようにする、上記動画参照)
物理要素を考慮し、限界速度と摩擦係数で落ちるかどうかを決めている。

落下する敵は摩擦を0にし、落とさない敵は0.2くらいにおいているよ。

物理だと最大摩擦力(μN)を越えないと動かないため、
初速度を大きく、限界速度を初速度より低く設定しなければならない。

 

スクリプトの継承と分割は違う

一つのスクリプトがあまりにも長い時、スクリプトを分割する。
スクリプトの分割は別のスクリプトを使って、一枚のスクリプトに仕上げるのみだ。

継承は全体を載せた後、
「ここだけをこう変えてほしい」部分を別スクリプトに記載する。

継承と分割は大きく異なる。

なお分割のやり方public partial class EnemyManager : MonoBehaviour と
分割基にpartialを入れればいいのみだ。

もっと詳しい方法は上記動画(英語だが、動画を見ればできる)を見てほしい。

なお私はEnemyManager2を作り、
2ではPitFallContact()など色々な関数を放り込んでいる。

 

改めて抑える継承のやり方

継承は親が主な働きをするのであり、
子の働きは「親のここを変えてほしい」のみだ。

主な働きをすべて親がやるため、
継承する場合は子がなくても親だけで動けるよう、
きちんとスクリプトを創らねばならぬ。

  • 継承する型番は頭にprotectedかpublicのどちらかをつける
  • 親元にとりあえず上書きされる用の関数(public virtial ~)を置く
  • 子には上書きする関数(public override ~)を置く
  • 子にはStartやUpdate関数などを置かない(上書き更新を覚悟)

私が参照用として載せた動画では、HPの更新としてStart関数に値を置いている。

 

どうしても子でStartやUpdate関数を使う時

start関数と継承

記事を更新後、別の敵ファイルでどうしてもStart更新せねばならなくなった。

そこでStart関数の中に独自の関数を作ってしまう。
更新する時だけ、別ファイルに書き込みを入れればいいのでは?

EnemyManagerスクリプトに

void Start(){

//色々な初期設定を行う

StartPlus(); //ここに独自の関数を代入
}//Start
public virtual void StartPlus(){//新しく作った独自関数
}

そして別の敵スクリプト(ここではWindy.cs)に

public override void StartPlus(){

//初期更新するだけのスクリプトを記す

}

後はStartPlus();としてStart関数に入れればいい

同じようにUpdatePlus関数を作るなどして、
必要なファイルだけを入れてしまえばいいとわかった。

関数について、敵の種類によって使う奴と使わぬ奴がある。
上手く振り分ければスクリプトの軽量化にもつながるのではないか?

 

エラー1:The same field name is serialized multiple

The same field name is serialized multiple

エラー1は親子(継承する。した)スクリプトにて重複しているがために生じる。
だから重複元の「子」においてある型と変数名を消せばいいだけ。

The same field name is serialized multiple.This is not Supported

そして親にはprotectedをつける。publicでもいい
つけ終わったらUnityをいったん閉じてまた開く(再起動)

私の場合は重複を消して再起動したら、エラーが消えた。

 

エラー2:~is inaccessible due to its protection level

is inaccessible due to its protection level

エラー1と似た状態だ。
親スクリプトでprivateと書いてある部分を子でも使う時生じる

親のみ使う部分はprivateでも構わないが、
親子間で使う場合はprotectedに変更しておこう。

 

エラー3:継承でのnullreferenceexception

nullreferenceexception

最も厄介なnullエラー。
エラーをクリックすればスクリプト及び対象物がわかるが、
どうしてnullなのかよくわからない

私の場合、最初子にもstart関数を置いていた。
そこにrigidbody2dを入れていた。

子にもrigidbody2dを入れる場合、頭にbaseを置けばいい
baseは継承元を参照した状態であるため、子にも通用する。

base.Unity

応急処置であり本質はスクリプトを一から見直すところにある。

あくまでも親単体で成り立つスクリプトを作り、
「ここだけこう変えてほしい」部分を子で創るのみだ。

関数の入れ替えならStartもUpdateもいらない。

 

記事を読んでも継承が理解できない場合

Unity継承スクリプト

Unityの継承、理解できただろうか?
おそらく今一つ「???」な状態ではないだろうか。

私もいまだに「?」な部分が残っている。

他サイトの記事などを読んだとき、今ひとつピンとこなかった。
体で理解した時、他サイトの記事もよくわかるようになった。

他サイトはDebug.Logを中心に書いており、関数を書いていなかったからだ。

継承の意味を体で抑えれば、スクリプトの組み方もわかる。

一度パソコンから離れ、受験勉強のごとくサイトや本、
動画を基に継承の仕組みを学びなおしてほしい。

参照:Unity,C#における継承とは?初心者向けに解説

参照:【Unity/C#】継承やオーバーライドも使えると便利なのでぜひ

私の場合、とりあえず二つの記事を印刷し、
色々書き込みを入れながら読んで、理解できるようになった。

後は持っている本にもクラスについて書いてあった。

 

継承の応用:Update関数量を減らす

最後に継承の応用として、Update()関数を減らすコツを載せている。
Update関数が多いとメモリにも影響を与えるので、ぜひ活用してほしい。

お願い

めがびちゃんからお知らせ♪

お知らせ

megabe-0へ訪問した"本当"の理由

まさか記事の書き形一つでこうなるとは…

お願い1

Writer軽い自己紹介

ティラノスクリプトや小説家になろう、ピクシブ他で物語を書きながら、 「私が気になった事件」の裏側を作家の視点で書いているおっさん。

プロフィール画像は自画像でなく、Megabe-0ブログのマスコット、めがびちゃん。

 

雷が苦手で、光を見ると頭が固まる(元から固い)。 月初めは墓参りと神社参拝を行い、賽銭箱へ1万円を入れた際、とても気持ちがすっきりした。

 

■ 簡単な自分史 ■

0歳:釧路のある病院で生まれる。暇さえあれば母乳を吸って、ご飯を4膳食べても体重が落ちるほど、母のダイエットにものすごく貢献したらしい

 

3歳:行方不明になり、全裸で海を泳ごうとしたところ、いとこのお姉さんに発見され、この世へ留まる

 

8歳:自分のお金でおもちゃのカードを初めて買い、経済を知る。なぜか父親に怒られ、家出するがすぐに見つかる。

 

12歳:学校で給食委員長になる。委員長として初めて全校生徒の前にて演説する際、原稿用紙を忘れてアドリブで笑いを誘いながらも何とかやり過ごし、多くの生徒に名前と顔を覚えてもらう。また、運動会の騎馬戦では変なアドリブを行い、多くの笑いを誘った。

 

18歳:初めて好きな人ができたけれど、告白が恥ずかしくてついにできず、別れたことを今でも根に持っている(妻となる人にははっきり言えてよかった)

 

21歳:大学在学中、アルバイトを始める。人手不足かつとても忙しい日々を過ごしながら「どうせなら自分から楽しいことをしていきたいなあ⇒起業って選択肢があるのか」働き方の選択肢を見つける

 

27歳:自分で作った会社がうまくいかず、一度たたんで都落ち。実家でとことん自分を責める日が続く。「何をやっても駄目だな、お前は」など。自分を責めても自殺ができず、体中から毒素があふれ出て苦しい日々を送る。寝るのも怖かった日々。

 

28歳:「このままじゃいけない」決心を決め、小学校からの勉強をやり直す。高校の勉強で躓きながらも、学び直すうちに「自分は何もわかっていなかったんだなあ」大切な教えに気づかされる。 加えて、小説やイラストなど「今までの自分が手を出さなかった分野」に手を伸ばしてみた。

 

29歳:「定義」と「自己肯定」こそが生き方を決めると気づかされ、不安な日々が起きても、心が強くなったと感じる。でも子供の誘惑にはめっぽう弱くなる。

 

35歳:人生初の交通事故(物損)に出会う。冬道の運転で車を上下に大回転(スピンではない)を体型氏、何とか命を取り留め、なぜ生きているのかわからない状態に陥る。

自分の生き方はすべて自分が握っている。わずかな瞬間にしか現れない「自分の真実」を表に引きずり出し、ピンチからチャンスを生み出す発想や視点をブログやメルマガ他で提供中。