Unityでインディゲーム道!

プログラム、Unity初心者がインディゲーム制作を目指して日々思うことなどを書き綴ります。

停止性問題について考える。Halting Problem

コンピュータに何が出来て、何が出来ないのか、についての話です。

*本記事は個人的な思索をまとめたものなので、内容や記述の正当性についての保証はしかねます!

 

 Unityも触っていますが、やはりプログラミング自体も面白く、そちらに多く時間を掛けている状況です。特にやはり、数学は避けられないということがはっきりしたので、自分のペースでいろいろ勉強しています。 

プログラマの数学第2版

プログラマの数学第2版

 

 数学ガールというのは名前自体は聞いたことはあったし、サンプルで軽く読んでみたら文章も良さげだったので、この本を購入。

 対象としては、ある程度プログラミング経験のある人向けな感じですが、ド文型の自分でも分かりやすい内容だったので良かったです。改めて、数学というのは自発的な興味を持ってやれば、面白いものだなと思いました。

 

 そんなワケでこの本の中でも扱っている停止性問題について、自分の頭の中の整理を兼ねて書いていきます。wikiにも停止性問題 - Wikipediaという記事がありますが、特に証明の部分が分かりにくいような気がするので、その補足を自分なりにしつつ、改めて内容の理解を深めようという狙いです。

 

コンピュータが出来るコト

 現代社会において、あらゆるモノや場所にコンピュータが存在しています。なので、何でも出来るように思えますが、コンピュータにも出来ないことがあるということです。出来ることと出来ないことの見極めは大事ですよね、出来ないこと以外は理論上全て出来るわけなので。

 

 コンピュータ上で扱えるのは数字のみであり、コンピュータで出来ることは計算できることにのみ限られます。

 コンピュータは0と1の世界とも言われますが、それは単に見た目の問題であり、人間の言う数字の5もコンピュータ上の101(二進法)も内容は同じです。要は数字で表現できるかどうかということが本質です。

 そして、計算可能かどうかというのは有限回数内に計算が止まるか、ということになります。ただし、人間社会においてはコンピュータの運用にはコストもありますし、時間も有限なので、『現実的な時間内に終わる』かどうか、という条件が付きます。

 

 というワケで計算可能かどうか、というコンピュータの働きにおいて重要な観点から出てくるのが『停止性問題』です。

 つまりは、『あるプログラムが止まるかどうかを判定するプログラム』は作ることが出来るか、という問題です。

 

 結論から言ってしまえば、そのようなプログラムは書くことが出来ません。それを証明するのには背理法を使うんですが、背理法というのも実際そこまでややこしいものではないような気がします。

 

反対を正しいと仮定する

 論理の世界というのは、我々人間社会とは違ってはっきりしていて、白黒付くものです。つまり、白じゃないなら黒だし、黒じゃないなら白という風に、はっきりと分かれて決着が付きます。

 これが人間社会だと白じゃないし黒でもないグレイかもしれないし、黒じゃないけど白でもない、みたいなことになってしまいがちです。

 

 これが正しい!と証明したいものの反対を正しいと仮定し、論理を積み上げていく。その過程で矛盾が出てくるなら、前提が間違っていたことになる。つまり、黒じゃない!だから、これが白であることは正しい!というのが背理法における論理の流れです。

 これは白黒はっきりしている論理の世界だから出来ることですよね。

 

Wikiの停止性問題を読み解く

 プログラムを関数とも言われます。関数はFunctionの頭文字を取って、f(x)という風に表記もされます。

 プログラムは処理の連なりですが、コンピュータにおける処理というのは、結局計算なので、一定の計算をする式という意味で関数です。入力を二倍にして返すプログラムは、y = 2xという関数として表現できます。何かをする、というのは計算なので特定の計算をするということで、一定の処理をするコードの塊を関数と呼ぶ、ということです。

 

 混乱しやすいのはプログラミングの中にも関数があるわけですが、それはミニプログラム、サブプログラムと言われたりもします。プログラミングにおける関数と数学における関数は、一応微妙に文脈は違う点には注意すべきかもしれません。

 

 そして、コンピュータ上で扱われるのは全てデータ、数字なので、プログラムもまた数字の集まり、データとも言えるわけです。

 

 この問題で証明したいのは『あるプログラムにとあるデータを入力した時に停止するかどうか』を判断するプログラムは書くことが出来ない、ということです。

これを背理法で証明するために次のように仮定します。

『入力された関数Aの引数を入力されたデータxにした時、止まるかどうかを判断できるプログラムは書くことが出来る!』

 

止まらない関数

これが停止性問題 - WikipediaでのH(A, x)という関数ないし、プログラムになります。プログラムAに対して、xを入力した時

A(x)

止まるかどうかを判定する、というもの。HというのはHalt(停止)の頭文字です。

 

 ここではより分かりやすくするのに、HaltCheck(A, x)という関数とします。

HaltCheck(A, x){

    if( Aは止まる ){

          YESを出力

    }

    else{  //Aが止まらないなら・・・

          NOを出力

    }

}

 YES, NOを出力する、ということですが、true, falseを返す、という意味での理解で大丈夫だと思います。

 

 ありえないことを想定するのが背理法とは言え、既に怪しい部分があります。止まるかどうかを確認して出力するのはまだ分かります。しかし、計算が止まらない状態をどう判断するのでしょうか。

 プログラムというのは処理の流れです。もし、その過程で止まらなくなる、計算が終わらない、つまり無限ループになってしまったら、プログラムはそこから抜け出せなくなってしまい、最終的な出力にまで進み、値を返すことは出来なくなります。それはHaltCheckという関数でも同じなはずです。なので、少なくとも内部で実際に関数Aにxを入力してみる、という方法で確認するというようなプログラムではないと思われます。

 

 しかし、あくまでこれはそのようなプログラムが書ける、という仮定の下での話であり、実際に出来るかどうか、出来るとしてどういうコードになるのかを考える必要はありません。ここが背理法のややこしい部分です。正しいと思い込んで論を進めます!

 

 次は、そのような魔法のプログラムHaltCheck( )が存在するとして、それを使った別のプログラムを考えてみよう!という内容です。

 

自分自身を入力する

 前述の通り、関数(またはプログラム)自体も本質的にはデータであるので、あらゆる入力データxのうちのひとつとして考えることが出来ます。

 つまり、ある関数に関数自身を入力する、というプログラムをHaltCheck( )を使って、書こうということです。

 

 

停止性問題 - WikipediaのM(A)のMはおそらくMyselfからきていると思うので、ここではMyselfCheck(A)という関数名にして扱っていきたいと思います。

 

 あらゆる関数やあらゆるデータを対象とするHaltCheck(A, x)からすると、MyselfCheck(A)の対象はプログラムAそれ自身に絞られるので、HaltCheck( )が書けるのであれば、無限ループと組み合わせることで書くことが出来ます。

MyselfCheck(A){

    if(HaltCheck(A, A) == NO){

        M(A)が停止する

    }

    else{

        while(true){ }

        // 無限ループ。M(A)は止まらない 

    }

}

 つまり、HaltCheck(A, A)という風にHaltCheckのふたつの引数にAを両方ぶち込むということでMyselfCheck(A)は実現可能なはずです。HaltCheckが可能なら。

 HaltCheckという関数を使い、プログラムAにAを入力した時にNOと出力されたら停止し、そしてYESと出力されたら、M(A)は止まらないというのがMyselfCheck(A)の動作になります。

 

 注意としてはYES, NOと止まるか止まらないかの関係がHaltCheckとは反対になっている点です。HaltCheckではAが止まる時にYESと出力し、止まらないと判断した時にはNOと出力することになっています。プログラミングとしては、単に論理をひっくり返すだけなのでこういうコードは書くことは可能です。

 

 ここまでが、『入力された関数に入力されたデータを引数にした時、止まるかどうかを判断できるプログラムは書くことが出来る!』と仮定した場合に導き出される論理の積み重ねです。

 

問題は次です。

 

MyselfCheckにMyselfCheckを入力

 MyselfCheckにMyselfCheckを入力したら、どうなるでしょうか。今までの積み重ねは論理上でのことであり、実際に確かめる術はないので、場合分けによって判断します。

MyselfCheck( MyselfCheck );

MyselfCheckが止まった

もし、MyselfCheckが止まったのなら、

HaltCheck(MyselfCheck, MyselfCheck)は、NOを出力したことになります。

 しかし、NOならばHaltCheckの内部ではMyselfCheck(MyselfCheck)を止まらないと判断した、ということになり、MyselfCheckが止まったことと矛盾します。

 

MyselfCheckが止まらない

もし、MyselfCheckが止まらないのなら、

HaltCheck(MyselfCheck, MyselfCheck)は、YESを出力。

 しかし、YESならばHaltCheckはMyselfCheck(MyselfCheck)を止まると判断したことになるので、これもまた矛盾。

 


 どちらの場合でも矛盾が生じてしまうので、つまり、前提とする仮定が間違っていたことになります。よって、あるプログラムが止まるかどうかを判定するプログラムは作ることが出来ない、ということです。

 

まとめ!

 というわけで、プログラマの数学を読んで学んだことを踏まえて、wikiにおける不足した記述を自分なりに補いつつ、論理を流れを辿ってみる、ということにチャレンジしてみました。

 数学的思考、論理的な考え方はやはり大事だと思いますし、自分で1から組み立てられるようになりたいですね。

 

 

オブジェクト指向をもっとシンプルに考える!変数と関数も。

オブジェクト指向プログラミングにおける初心者への説明はまどろっこしい。もっとシンプルに考えることは出来ないか、という試みです。とりあえず、変数と関数から行ってみます。

 

変数は箱のようなもので、箱じゃない

 変数は箱ではありませんし、そもそも変数と呼ぶことさえ抽象化であり、既存の概念を流用した例え話です。そうした方が人間の認識に都合がいいらしいので。ただ、問題なのは『箱のようなモノ』と言われて人が思い浮かべるのは、名札を貼り付けた箱だと言うことです。繰り返しますが、変数は箱じゃありません。
 プログラミングにおける変数とはメモリの一部分です。そして、型とはタイプになります。<T>のTはTypeのTです。


 変数の宣言とは、あるタイプのデータをコンピュータ上で扱いたいのでメモリの一部を使わせてほしい、とコンピュータに伝えることです。その結果、コンピュータはメモリの一部分をそのタイプに必要な分だけ確保し、プログラマの付けた名前と結びつけます。つまり宣言とはメモリの場所取りです。


 コンピュータ側は、それを箱のようなものどころか変数とすら思っていなくて、メモリ上のとあるアドレスの1区画にデータが保存されている、という認識です。人間が勝手に箱に例えているだけです。
 つまり、変数とは箱のようなモノという人間側の認識とはかなりのズレがあります。このズレこそポインタの難しさに繋がるのだと考えられます。

 

関数はミニプログラム

 また、関数とは再利用するためもの、と言われます。モジュール化、部品化という概念が出てきます。しかし、この説明も初心者にとって分かりづらい。なぜなら、再利用のメリットはある程度コードを自分で書いて初めて実感できることだからです。関数はもっとシンプルに言い表せます。


関数とは…


ある1つの仕事をする複数行のコードをまとめ、

明確で本質的な名前を与え、切り離したもの


です。

 関数の利点とは、他のコードから切り離し、その内容に沿った名付けをすることで、コードを読みやすくなるし、その結果名前を使って呼び出せばいいから再利用しやすくなる、ということです。つまり、順序が逆ですね。

 

 プログラムとは入力されたものを計算し、出力するものですが、関数も入力された引数を計算し、返すという性質を持っています。なので、ミニ・プログラムぐらいの認識がちょうどよい気がします。 (プログラムの中の小さなプログラム)

 

オブジェクト指向は普通の考え方

 そもそもオブジェクトとは何か?客体です。客体とは主観を持った他人です。つまり、オブジェクト指向とは仕事を分担して、それをその人に任せよう、というごくごく単純なものなのです。クラスは仕事を分割するためのものとなります。

 

 プログラミングにおける、様々な機能やテクニックは主に二種類に分けられます。コードを読みやすくするか、コードの量を減らして扱いやすくする、か。
 クラスとは、コードを幾分か分割し、読みやすく扱いやすくするための器です。ただ、どう分割するかが難しい。物事の本質を掴む能力が求められます。なので、オブジェクト志向が難しいのではなく、クラス設計が難しい、という方が正しいです。

 

 その上、分割したということはコードがバラバラになってしまっているので、それらをどう連携させるか?という問題も出てきます。そういう意味ではオブジェクト志向は完璧な方法論ではなく、リスクもあるということになります。しかし、大規模なプログラムに対しては、分割倒置法が現実的な対応策だというだけです。

 

 継承もコードを減らすための技術です。これも要は省略の技術です。ただ省略は理解してない人にとっては分かりにくい、というデメリットがあります。カプセル化は、コード群を意味のある、必然的なまとまりにして、関数のように他のコードと切り離して、分かりやすく管理しやすくしようというもの。

 情報隠蔽はバラバラになったコード同士が安全に楽に連携できるようにするためのものです。ポリモーフィズムとは違った形を持たせる、ということですが、要は関連性のある各クラスに共有されるメソッドに違った挙動を与える、ということです。

 

 法的手続きをしたい時は行政書士や弁護士に依頼します。あなたは法律知識を知る必要も実際に書類を用意する必要もありません。あなたはただ依頼して報酬を払えばいい。弁護士という仕事は社会のなかで1つの職業としてカプセル化されています。
 また、あなたは銀行でお金を引き落とせるが、金庫の中に入ることは出来ない。窓口かATMからしか引き出せません。これは機密情報がユーザーであっても安全のため隠蔽されているからです。
こう考えればオブジェクト志向って別に普通で順当な考え方ですよね。

 


 客体は主体を持った他人だと言いました。クラスを使って分割するだけでなく、オブジェクトに主体性を持たせることが大事です。つまり、自分の仕事を理解していて、必要な処理を自分で責任もって行うことが出来るということです。クラスの定義がしっかりしていて、必然性のあるメソッドが準備されているか、ということです。

 

操作するのではなく、命じよ

 クラスとはコードを分割するためのものです。しかし、同時にそのクラスに任せられた仕事を達成するような主体的な振る舞いを持たせなければならない。つまり、他のクラスとの関わりの中で、仕事の線引き(どっちが何をやる?)が大切になります。

 

ゲームで例えます。
 プレイヤーのキャラが敵キャラを攻撃をしました。敵にダメージを与えました。トリガーから検出した敵のコライダーを使って、Enemy classの参照を得たインスタンスを作ります。ダメージは互いの攻撃力と防御力を計算して出します。

これをコード上でどう処理すべきか。

 

 悪い例は、プレイヤークラスが敵クラスのプロパティから防御力を取得し、自分の攻撃力と合わせて計算。そうして出したダメージ量をHPから引く、という処理をプレイヤークラス内部で全てやってしまうことです。これはプレイヤークラスが敵キャラクラスのインスタンスを直接的に操作してしまっていますし、敵キャラクラスは自分では何もしない受動的な状態になってしまっています。

 

 そうではなく、敵キャラにaddDamageというようなプレイヤーから利用できるpublicなメソッドを用意しておいて、プレイヤーはただこのメソッドを呼び出し、自分の攻撃力を渡せばいい。

enemy.addDamage(Player.AttackPower)

 そうして、敵キャラクラスは渡された攻撃力を元にダメージ量を計算して、HPという風にダメージ処理という仕事をきちんとこなしています。

 これこそが各々の主体性が保たれた状態で、分業のあるべき姿です。プレイヤーは自分の攻撃力を伝えて、ダメージ計算するように敵キャラにお願いしてるだけです。

 

最後に

 このように説明すれば、オブジェクト指向が如何に自然で普通な考え方か分かります。別に難しくないっすよ。実際使いこなせるかはともかく。崇める必要も嫌悪する理由もまったくない。

 なんで、教本に書かれているような説明だとわかりにくいのでしょうか?もっとシンプルに考えるべきだと思います。

長々書きましたが、言いたいことはプログラマの皆さん、教えるの下手!

(そうじゃない方々は、救って頂きありがとうございました。)

 

ゲーム製作を学んで一番感動したこと。なぜマリオは壁の中で立てるのか。[ 当たり判定 ]


 ゲーム製作やプログラミングを現在も勉強中ですが、それ以前は当然、単なるゲームプレイヤーでした。今では、それに違う視点も加わることで、ゲームというものを多面的に見れるようになり、また別の面白さを味わえるようになっています。


 その中で、特に感動したのが当たり判定についてです。プレイヤーキャラと敵キャラが当たってダメージを食らう処理とか、そもそも地面に立てたりできるのすら、すべて当たり判定のおかげです。
 考えてみれば、ゲームというのはコンピュータ上におけるプログラムによるもので、実体はありません。画面に見えているのものは単純に画像であって、それだけでは幽霊のように見えているけど、触ったりすることが出来ない状態です。

 実体があるかのように感じさせるために、当たり判定は根幹的機能を果たします。当たり判定に基づいてレスポンスがあることで、ゲームプレイにおける感触や実感というものに繋がりますし、また逆に当たり判定が適当だったり、厳密すぎるとストレスに感じることもあるわけです。

マリオの当たり判定

 子供の頃の思い出として、スーパーマリオでの体験があります。マリオはキノコを食べると巨大化できますが、そうすると1マス分の穴に潜れなくなります。しかし、走りながらタイミングよくしゃがむと、しゃがみ状態で穴に滑り込めます。
 面白いのは、その状態で立つ事ができるということです。そして、穴を出るまでニューっと押し出されます。この押し出される、というのがポイントです。共感してくれる人は多いと思うんですが、このニューというのがすごく好きで、マリオを遊ぶときは毎回やっていました。今でもやります。

 現実においては、自分の身長よりも小さい穴の中で立つことはできないですよね、当然ですが。実体のある人間と壁はぶつかるからです。しかし、ゲームではそうではありません。当たり判定、とは言われますが、実は当たっていないんです。

当たり判定はなにするの?

 つまり、当たり判定とはゲーム上のオブジェクト同士が重なったら、『当たった』と判断し、重ならない状態まで押し戻すことで、実体があるかのように見せるのが本質だというのです。正直、こういう説明を目の前にして、最初は実感が湧きませんが、先ほど書いたマリオでの体験を思い返せば、至極納得がいきます。

 マリオが穴の中で立った瞬間、マリオと壁という2つのオブジェクトが重なり合い、その結果、動的オブジェクトであるマリオが壁の外まで押し出されるからです。こんな風に過去の体験の理由をその仕組みと共に理解が出来たのが、今までで一番感動した瞬間ですね。


 そもそも、キャラが地面に立つというのも、立っているのではなく、理論的には地面が常にキャラを押し返し続けている、ということになります。また、押し出す処理が必要ない場合は、重なった!という判断だけをするトリガーとして扱われます。これは敵味方のビームや空中を自由に浮遊してくる敵キャラなどがそうですね。

仕組みが分かればトラブルも!

 なんで、壁を取りぬけてしまうバグなどもこの仕組みが分かっていれば、何故起こるのか理解できます。つまり、重なっているという判断をする前に、フレームを跨いで壁を通りぬけてしまうと押し返せない、ということなのです。面白いですよね。ハイスピードなレースゲームなんかは、より速いレートで当たり判定を行うことで、すり抜けないようにするみたいですね。

 また、銃弾の玉をオブジェクトとして飛ばすとコレもまたすり抜けてしまうので、Unityでもそうですが、レイキャストを飛ばして、つまり空間上を線と線で結んで判断する、みたいな処理をするようです。

 また、当たり判定はある程度は大らかである方が、ゲームプレイ上いいようです。適当な当たり判定に憤慨する、ということは昔のゲームではよくありましたよね。

まとめ!

 というわけで、個人的に感動したことを交えつつ、当たり判定について書いてみました。Unityではコンポーネントとしてコライダーが準備されているので、ゲームオブジェクトにアタッチしてすぐ使えます。便利な世の中!

[参考文献]
ゲームの作り方 改訂版 Unityで覚える遊びのアルゴリズム on Amazon

リアルでの人助けと『ウィッチャー3』| 決断と見返り

f:id:miur-us:20180825231700p:plain 
 ひさびさの投稿になります。リハビリがてら、ちょっとした小話というか、ゲームに絡めた雑談を。


 昨日、スーパーに行った時の話。自転車を倒して買ったモノを散乱させたおばあさんに出くわしました。
 さすがに目の前だったので、知らんぷりも出来ません。自転車起こしたり、荷物拾うのを少しお手伝いしたんですけど、その時に思ったのが、あっこれ、ウィッチャーだとモンスターがおばあさんに化けてるか、山賊の仲間かもしれないやつだ!と。

 いや、すいません。悪気はないんです。でも、ウィッチャー3をやった人なら分かってくれると思います。そういう風にモノの考え方に影響を与えるゲームもないよな、って改めて考えさせられます。もう、三年ぐらい経ってるんですよね。

情けは自分のためならず

 ウィッチャーというのは、あくまで職業の一つであって、世間一般から忌避されています。主人公のゲラルトは決して"勇者様"ではない。なにより、物騒で陰惨な中世ヨーロッパな世界観なので、いかにして自分の身を守るか、ということが求められます。

 ウィッチャーでは、そうした自己防衛への意識もロールプレイに含められています。自分の身は自分で守れ。他人に構っている余裕はあるのか?ということです。


 ゲーム内の用意されているミニクエストは、基本的には人助けです。しかし、多くのクエストに共通しているのは、金銭や情報を得るためのビジネスだということ。そして、純粋な正義も悪もないし、善かれと思ってやったことが必ずしも良い結果をもたらすとは限りません。

 ここがウィッチャーの肝かもしれません。プレイヤーは決断を迫られますが、それか正しいのかどうかは、どんな選択にしろ明確に判断することが難しい。痛い目にあったり、裏目に出たりする。そういう構造になっています。

見返りを求めない

 で、話をおばあさんに戻しますけど、声をかける前に瞬間的にフッとそのようなことが思い浮かんだんです。
 
 でも、実際今回はものを拾うくらいなので全然いいんですけど、人助けやったつもりが善くない結果をもたらしたり、自分がひどい怪我をしてしまったり、っていうのは全然ありえますよね。
 だから、ウィッチャーでの経験を活かして、少しだけ考えてから行動する、というのもいいのかもしれませんね。とは、言ってもほぼ瞬間的におばあさんに大丈夫ですか?って声をかけたんですけどね。善いことしたつもりではいるんですけど、それが裏目に出ることもあるかもしれない、という意識はどこかで持っていた方がいいなと思いました。


 もし誰かを瞬間的に助けようとして、その結果、自分が取り返してのつかない怪我をしてしまったりした時、納得できるのでしょうか、それとも、後悔するのでしょうか。

 ウィッチャー3からは、それでも自分の信じた道を進め、自分の決断を受け止めろ、と教わりました。

 以上です。

カメラを揺らして、画面上の敵を振動で倒すブロックを作る!| Unity 2D ポリモーフィズム 継承

下から叩くと画面を揺らし、画面上にいる敵キャラを全部倒す、というブロックをUnity2Dで再現してみました!一気に敵を一掃できる爽快感溢れるアクションになりますが、これを上手くやるためにはカメラを揺らすだけでなく、ポリモーフィズム継承を上手く活用する必要があります。 

f:id:miur-us:20180601012939g:plain

 

敵キャラをまとめて処理

 敵キャラといっても、様々なキャラがいます。マリオで言えばクリボー、ノコノコをはじめとした多くのキャラクタがおり、多様な動きをしています。

 クラスを横断する一括処理を実現させるにはポリモーフィズムを使います。いろいろな敵キャラを"ひとまとめ"にし、一括して処理する必要があるからです。

 

 つまり、"衝撃波で攻撃する"という処理を各キャラに行うわけですが、、ひとつのアクション対してのそれぞれ違ったリアクションを取らせる、ということになります。それをシンプルにコーディングするには、継承ポリモーフィズムによる抽象化が必要です。

 

継承とポリモーフィズム

 継承とはコードの量を減らすためのアイディアで、ポリモーフィズム複雑な処理を人間的な感覚でシンプルに書き表すためのテクニックです。

f:id:miur-us:20180531230304p:plain

 敵キャラクターという概念があり、そこから様々な具体的なキャラクターに派生します。各々のキャラクタは違いがあるといえど、基本的なパラメータ、動作を共通しています。体力があるかもしれませんし、移動するでしょうし、プレイヤーへの攻撃動作もあるかもしれません。そうした共通部分をスーパークラス、基底クラスで定義します。

 それを継承した各々の具体的なキャラの独自性の定義は、そのキャラのクラスですれば良いわけです。そうすれば、各敵キャラに共通する部分は省略することが出来ます。

 

 ポリモーフィズムとは、要は共有されたメソッドの中身をそのクラスにあった内容にする、実装を変えるということです。つまり、移動する、Move()というメソッドがあったとして、あるキャラは歩いて移動するかもしれませんし、他のキャラは飛び跳ねる、または飛ぶ。魚系のキャラはそれが泳ぐ、ということになるかもしれません。今回の例だと、ボール型と花型の敵は動かないですが。

 

 また、"プレイヤーに踏まれる"という動作などについては、クリボーはペッタンコになってしまいますし、ノコノコは殻にひっこみます。トゲゾーは何も起きず逆にマリオがやられてしまいます。つまり、同じ動作に対して、違った反応、結果が定義されていることになります。

 ポリモーフィズム多態性)とは、違った形態を持たせるということです。

 

Unity上での継承

 Unityで継承する際には、UpdateなどのUnity固有のメソッドも各クラスに継承させる必要があります。この時、protectedを使います。親子関係にあるクラス間のみアクセス可能、となります。そして、継承クラスのUpdate内で、base.Update()とすることで、基底クラスのUpdateを実行します。

 

↓このクラスで定義した、プレイヤーがブロックを叩いた時の処理は継承されたクラス内のUpdate内にbase.Update()と記述することで引き継がれます。

f:id:miur-us:20180601003520p:plain

 衝撃波の出るブロックは原型となるBoxControllerクラスから派生しています。アイテムが出るボックスもありますし、いろいろあるボックスのうちのひとつです。

 どんなボックスでもプレイヤーがしたからジャンプで叩くと飛び跳ねるという動作を共有しています。継承してしまえば、各クラスごとにこのコードを書く必要がなくなります。繰り返しますが、継承とは余分な、重複するコードを無くし、読まなければいけないコードの量を減らすためのテクニックです。

f:id:miur-us:20180601004036p:plain

↓はShockBoxControllerスクリプトですが、base.Update()があります。

 f:id:miur-us:20180601004415p:plain

BoxController内のvirtual Updateでの処理が実行されることになります。

 

Unity上でのポリモーフィズム

  基底クラスでvirtualとして定義したメソッドを継承クラスでoverrideすることで、そのクラス独自の動作を出来るようにします。今回で言えば、ブロックを叩いた後に、敵がやられるわけですが、"どうやられるか"という反応をそれぞれ違ったものにするということです。

f:id:miur-us:20180601004846p:plain

 あるキャラは吹っ飛ぶかもしれませんし、あるキャラはバラバラになるかもしれません。まったく効かず、微動だにしないキャラもいるかもしれません。今回、衝撃波によるダメージを"DamagedDeath"というメソッドとして定義していますが、させたい動作をそれぞれコーディングすればいいわけです。 

f:id:miur-us:20180601004921p:plain

 Enemyクラスでは中身がありませんが、地を這う敵キャラは"飛び跳ねて下に落ちる"という処理をしています。花の敵キャラは"逆さまになって、まっすぐ下に落ちる"ようにしています。

f:id:miur-us:20180601005716p:plain

f:id:miur-us:20180601005722p:plain

死亡判定されると、まず回転させて、下に移動させるという処理で、動きを作っています。

 

まとめて処理する!(アップキャスト)

  さて、では各クラスでoverrideとして定義した"DamagedDeath"メソッドをどう活用すれば良いか!それには各キャラをおおざっぱに"敵キャラ"として認知する必要があります。

 UnityにはPhysics2Dに、ある地点の周辺のコライダーを集めるOverlapBoxAllメソッドがあります。つまり、これを使って画面上の敵キャラのコライダーを集めます。

f:id:miur-us:20180601004723p:plain

 このコライダーを使って、ボックスのクラス内にEnemyクラスのインスタンスを作ります。つまり、いろんな敵キャラを具体的な各々のクラスのインスタンスとして、生成するのではなく、漠然とした"敵キャラ"インスタンスとして生成するということで、柔軟性を持たせます。これこそ"抽象化"になります。詳細を無視して、概念的に物事を捉えるということです。

 

 つまり、様々なキャラをひとまとめに出来、大らかな同タイプのオブジェクト群として扱えるようになります。

enemy.DamagedDeath();

 なので、そうして集めた"敵キャラ"の集合に対して、ボックスは「衝撃波出すので、各々リアクション取ってね!」とお願いします。つまり、"DamagedDeath"が実行されます。つまり、各クラスで実装された異なる処理がなされるということになります。

 

カメラを揺らす!

 今回の件で更に重要なのは、視覚効果であるカメラの揺れです。これは何にでも使えるアイディアですよね。爆弾が爆発した時にも使えそうですし、いろいろ使い道があります。注意としては、やりすぎるとプレイヤーが酔ってしまうということですね。特に縦揺れはキツイ。

f:id:miur-us:20180601011601p:plain

変数など。動く幅、スピードなどの変数を宣言。

f:id:miur-us:20180601010845p:plain

 やることは多いので、揺れを起動するメソッドをShockActivationとして定義。叩いた時点でのカメラの位置の検出、ブロックを光らせるなど、様々なヘルパーメソッドを実行します。

 

 これには動く足場の件で学んだことを生かしています。まぁ、サイン波に沿ってカメラを一定時間揺らす、というだけのことなんですけどね。

 ブロックを叩いた瞬間のカメラの位置を中央"Center"として、そこから左右に揺れ動く、という処理にしています。叩いた瞬間を最大の揺れにして、時間経過ごとに揺れとスピードが弱まっていく、という処理にしています。それぞれ、speed, widthにしていますが、毎フレームごとにdeltaTime分を引いていく、という処理です。

f:id:miur-us:20180601010621p:plain

cam.transform.position = center +

           new Vector3("横揺れ", "縦揺れ", 0);

 縦揺れも加えていますが、揺れの周期も幅も横揺れよりも弱くしています。

 

より、リアルに。

 揺れている!という感じを更に感じさせるにはどうすればいいのか。プレイヤーが一定時間動けなくなるのがいいなと思いました。実際、そういうゲームは多いですよね、地面が揺れてるとプレイヤーは動けなくなる、みたいな。ロックマンのガッツマンとかもそうでしたね。

f:id:miur-us:20180601011008p:plain

 これは単純にブロック側のスクリプトがプレイヤーキャラ操作のスクリプトを一定時間、非アクティブにすることで実現しています。コレをやると、画面が揺れている間、プレイヤは動けなくなるので、あぁスゴイ揺れなんだなーという演出になります。FindGameObjectWithTagでプレイヤーとカメラのオブジェクトを探して、GetComponentから直接、オンオフにするのが楽ですが、少し長いのでStopPlayer()というヘルパーメソッドを作りました。

 

より、白熱した体験を!

 今回の動作を再現して思ったのは、やっぱゲームに爽快感を与えるには、こういう派手な仕掛けが必要なんだな!ということです。このアイディアの初出って、初代マリオブラザーズなんでしょうか?POWブロックを再現しました。

 こういう見た目が派手なものが大事ですよね、加えて敵キャラを一掃出来るのが楽しい。やっぱ、ゲームはやってて楽しくないと意味ないですよ!!

 

 

スーパードンキーコングでのバナナ、星の動きを再現してみた。

スーパードンキーコング・シリーズと言えば、レア社任天堂による、SFCの傑作2Dアクションゲーム三部作です。今回はその"バナナ"や☆の獲得後の動きを再現してみました。 

f:id:miur-us:20180531014126g:plain

 本シリーズのバナナは言わば、マリオにおけるコインのような存在で、100本集めると一機増えるというコレクトアイテムになっています。今回再現するのは、プレイヤーがバナナを回収した後に起きる動きです。

 

 プレイヤーがバナナに触ると、バナナが画面の左上、つまり獲得数が表示されている部分に向かって飛んでいき、表示と重なると数が更新されるというものです。 

  • プレイヤーがバナナに触れる。
  • バナナが左上に飛んでいく。
  • アイコン表示部と重なると、消える。
  • スコアが更新される。 

 マリオなどでは、コインが獲得後は消えてしまうことを考えると(獲得アニメはありますが)、特徴的な演出であると言えます。とても面白みのある動きです。

 

再現に向けて

 まずは見た目から、です。画像を用意します。しかし、バナナはめんどくさいので星にすることにしました。本作中のミニゲームでは星を集めるので、特に問題はないはずです。右の画像の通り。

 

 

コーディングで押さえるべきところ

 この動きは気の利いたものですが、やろうとしていること自体は単純です。プレイヤーが触れたら、目標となる地点へ星を動かせばいいだけです。

 

 上記のリストから、各段階のコードでの処理をまとめると・・・

  • トリガーでプレイヤーを検知する。
  • 検知後、フラグをオンにする。
  • 画面上の左上部に移動させる続ける。
  • アイコンとの距離を測る。
  • 一定の距離になったら星を消す。
  • 獲得数に基づき、スコア表示を更新する。

 注意点としては、スクリーン上の位置とゲーム中、つまりシーン中(ゲームワールド内)の位置は異なるということです。プレイヤーが移動し続けるのであれば、画面上の位置とゲーム内の座標はズレ続けます。つまり、GUIのアイコンはスクリーン上に、星はゲーム内のアイテムとして、違う次元の存在なのでズレを考慮しないとダメだということです。プログラミング的には、星の移動中はアイコンの位置を更新し続けないとダメだということになります。

 

 今回、対象となるスクリプトは星自体にアタッチする"CollectableStar"とゲーム全体を管理する"GameController"の二つになります。前者は当然、星の動きを管理するもので、後者は星の獲得スコアを管理します。

 

星の動き

 まずは画像をインポートします。Spriteフォルダで"Import New Asset"を実行し、星の画像を選択し取り込みます。

f:id:miur-us:20180531015312p:plain

 シーン内に引っ張って、コライダーをアタッチします。Physics 2D >> Circle Colliderを選択し、大きさ(半径)を適当に調整します。シビアすぎてもダメなので、大らかな大きさに設定してます。

 

 で、スクリプトである"CollectableStar"をアタッチします。ドンキーにおける星は、その場で回転しているので、それも再現します。

transform.Rotate()

 これを使うと、ゲームオブジェクトを回転させることが出来ます。ベクターの各軸、X, Y, Z軸それぞれ回転数をfloatで指定して、Update関数内で使用します。見たまま時計回りにするので、回転させるのはZ軸になります。Rotateは実は負担が大きい、とのことで、それなりに対策した方が良いらしいですが、ここでは省略します。

 

スコアの管理

f:id:miur-us:20180531015326p:plain

 本筋に移る前に、スコア関連について。星獲得スコアは、単純に数を数えるだけなのでintタイプの変数"score"になります。星側のスクリプトから直接、そのフィールド(クラス内に直接宣言された変数)を操作するのは良くないことなので、このスコアを管理する"GameController"内部にスコアを加算し、表示を更新するUpdateScoreなる関数を作り、星側のスクリプトが外から、その関数を呼び出すという形にしました。内容はシンプルで、スコアを加算してTextの中身をそれに合わせて更新する、という処理をするだけです。

 

テキストについて

 表示については、Textを使います。using UnityEngine.Textを書く必要があります。星の画像を表示するので、それはimageです。どちらもCanvas上に表示させます。

 

星を動かそう!

  さて、ではいよいよ本編です。星のコライダーはトリガー・モードで使用します。つまり、プレイヤーが触れたかどうかが分かればいいだけですので。 

 

f:id:miur-us:20180531015349p:plain

 プレイヤーの検知はおなじみ、OnTriggerEnter2Dを使います。2Dの時は、あらゆるものの後ろに2Dが付くことに注意です。"Tag"タグを使って判定するので、この場合Unity上でプレイヤーキャラに"Player"タグを設定しておく必要があります。

 

 触ったかどうかのフラグは"IsCollected"のbool値で管理します。集めたか?ということですね。OnTriggerEnter2Dでプレイヤーが触れたら判定し、これをtrueにします。trueになった時点から、移動のための処理が始まります。(Update()内部)

 

 

スターにアタッチするスクリプト

f:id:miur-us:20180531015415p:plain

f:id:miur-us:20180531015429p:plain

 Update内に移動のメソッドを書いておき、フラグが立ったらこれが実行されるようにします。つまりif文です。IsCollectedがtrueになったら、移動メソッドが実行されるようになり、星が動き始めるという算段です。

 

 問題は前述の通り、星がどこに向けて移動していくかということです。

 

 画面上の左上部分になりますが、そもそも我々が見ているものはスクリーンであり、スクリーンはゲーム内世界の一部を映し出したものに過ぎません。つまり、星やバナナからしてみれば、画面上の位置は分からないわけで、画面上の位置をシーン内の座標に変換しないとダメです。 

 スクリーンに映し出されているものは、カメラを通したものなのです。CameraクラスにおけるViewportToWorldを使い、変換します。

 

 スクリーン上の座標は0.0fから1.0fまでのfloatのx, y座標として表されます。つまり、今回の"左上"はVector(0.3f, 0.95f)です。今回のようなゲームは常にプレイヤーが動き続けるので、こうした座標の変換、計算を常に行う必要があります。IsCollectedがTrueになった瞬間からアイコンに届くまでの間、Updateメソッド内で移動の処理が毎フレームされ続けます。

 

 

 そして、指定の位置に近づいたらスコア加算のメソッドを実行します。厳密に言えば、GameControllerUpdateScoreメソッドを呼び出し、自分のゲームオブジェクトを非アクティブにすることで表示を消す、という処理です。

gameObject.SetActive(false);

 Destroyでもいいかと思いますが、Destroyは負担も大きいし、星自体の数も多いので、SetActiveを使うことにしました。画面から消える、という効果は同じなので。

 

  ちなみに星の点滅というか、色の変化の処理も書いてますが、それについては今回の本筋とは無関係なので省略します。

 

注意!

 今回のコードですと、画面の大きさの変化に対応できません。アイコンの座標を定数で表しているからです。なので、解像度が変わればズレてしまいます。なので、本当は画面の大きさから計算して、相対的なアイコンの位置を計算して割り出すという処理が必要なのですが、今回は省略しています。

 

まとめ!

  ということで、自分も大好きなゲームの動きを一部だけではあっても、再現することは楽しいなと思いました。こういうちょっとした演出が、ゲームに生命観を与えるのだなーと、また1つ勉強になりましたね。今後も名作ゲームの再現に挑戦していきたいと思います。あとSwitchでトロピカルフリーズやりたいなぁ。

  

 

オブジェクト指向とは単なる分割、縮小、連携のための手段だ!

 オブジェクト指向は思想のように捉えることは混乱を生みます。エレガントな手段!なんてものではなく、タイトルの通り、それなりの規模のプログラミングに対する妥当で、現実的な手段ではないのでしょうか。オブジェクト指向の本質を理解した気がするので、考えをまとめたいと思います!

 

むしろ補助的な機能

 オブジェクト指向に振り回されがちな初心者プログラマですが、自分も最初は掴み所がなくて困惑しました。クラスは設計図のようなもの・・・ようなものってなんだよ!?みたいな。

 Unityでやることを含め、最初の言語としてC#を選択する人が少くないと思いますが、C#オブジェクト指向の言語です。そのためオブジェクト志向な考え方にどっぷり浸かることになります。
 つまり、オブジェクト指向がプログラミングにおける本分のように錯覚してしまうわけですね、自分も最初そう思いましたし、同時に初歩的なこととオブジェクト指向的なもののが噛み合ってこないので、そこで要領を掴めなかったりもしました。
 
 それもそのはずで、オブジェクト指向はむしろ補助的な機能に他ならないわけで、実は初心者はそれほど気にすべき事柄ではないのです。

コーディングにおける二つの目的とは?

 プログラミングには、いろんな機能やテクニックがありますが、それらは主に二つの目的のために存在していると思われます。つまり・・・
可読性、保守のための分割 & コードを読む量を減らすための縮小、の二つです。

 例えば、関数やメソッドというのはこの2つが組み合わさったもの、と説明できます。
 複数行の処理を関数化することで、他のコードと切り離し、区別できるようにし、読みやすくします。また、その処理が繰り返し使われる場合、関数を呼び出せばよくなるので、総合的なコードの量は減ります。関数の再利用というものですね!

 For文なども繰り返し処理を効率的に書き表すためのテクニックですし、縮小のための道具と言えそうです。

クラスも単なる道具である。

 というわけで、クラスもこの分割&縮小のための道具に他なりません。オブジェクト指向は現実世界に近い形でコードを書けるエレガントな方法だー!みたいなのは結果的にそう解釈できるに過ぎず、本来の目的は大きなプログラムを分割して、読みやすく扱いやすくし、また大きくなりすぎないようにするためのものなはずです。

 カプセル化とはコードを意味のある塊にまとめる、ということであり、機能や分野ごとにまとめるというのはその方がわかりやすいから、ということになります。
 難しく考える必要はなく、ただ単に分けて管理しよう、別々に考えよう、というものに過ぎないはずで、言葉遊びに付き合う必要はありません。
 
 継承、ポリモーフィズムはコードの量を減らすためのものです。継承は関連あるクラスの重複部をひとまとめにするためのもの、ポリモーフィズムは各クラスをひとまとめにし、いろんな動作を共通のメソッドで表現し、やはりコードを少なく、簡潔にするためのテクニックです。

自分でデータ・タイプを作るための器

 分割&縮小はトラブル回避のための消極的な目的ではありますが、積極的にクラスを使う目的もあります。それはデータ・タイプを自分で作れるということです。

 データ・タイプとは、いわゆるInt, float, char, bool などのデータの形式や種類を表すものですが、基本的なタイプは単純なデータであり、そのデータしか扱えません。

 世の中にある様々なモノは複雑な構造、概念によって形成されており、そうした単純なタイプでは表現不可能です。正確に言えば、多くの単純なタイプが組み合わさって、初めて高度て複雑な表現ができます。
 要するにクラスとは、そうした数多くのデータタイプをひとまとめにし、抽象化するための入れ物になるわけです。抽象化とは、細かい事柄をざっくりと捉えることで、高度な理解、表現をするための手段です。

コンピュータ上にあるものは全てデータです。複雑で抽象的なものをデータとして定義するのが、クラス設計になります。つまり、そうした高度な概念のためにあるデータタイプを定義するためにクラスは存在します。


 また、クラスはメソッドも含めることができるので、振る舞いも表すことができ、クラスから作られるモノ(インスタンス)に主体性を与えることができます。これはクラスによる分業の面から見て、とても重要です。

 これにより複雑な構造を持つ、人間を含めた生き物などをコンピュータ上でデータとして、扱えるようになる、ということになります。
 つまり、高度で複雑なデータを表すタイプを自分で定義できるということです。

連携という、ネック

 分けた、ということはバラバラになったコード群が一つの仕事を成し遂げるために連携しなければならないということになります。どう連携するかの管理のためのアイデアがアクセス修飾子です。つまり、公開、非公開にすることでクラス間の連携を制限するというものです。

 連携はむしろクラスを利用するために支払わなければならないコストのようになります。なぜなら、上手く連携をするような構造になっていないと、かえってコードが複雑になってしまうからです。
 プログラミング言語が人間のためのものなのに、読みにくくなってしまうのと同じように、クラス分けのせいで、プログラム全体の構造が複雑になって、把握しにくくなるのは本末転倒です。
 しかし、クラス設計とは単純なコーディング以上に、物事の本質を見極める能力が求められ、難しいわけです。
 ここらへんがオブジェクト指向が必要以上に叩かれる原因なのではないでしょうか。しかし、オブジェクト指向は単なる方法論であり、使わなければならないなら慎重に使わざるを得ず、他にいい方法があるなら、その方法を使えばいいだけです。

まとめ

 というわけで、クラスは分割や縮小、連携、そして、自分でデータ・タイプを定義するためのもの、という説明を書いてみました。
 少し、長くなりましたが、かなり分かりやすく書けたかと思います。自分で言うのもなんですが。

 ポリモーフィズムに関しては、Unity上の実践含めて、また改めて記事を書きたいと思います。画面中の敵を全部・・・みたいな内容です!