Unityでインディゲーム道!

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

Switch版ダークソウル・リマスターをクリアして 【ゲーム感想】

f:id:miur-us:20190424013455j:plain

写真はニンテンドーストアより引用

 デモンズ・ソウルが話題になった頃、自分はコンソール・ゲームから離れていた時期で、羨ましく遠くから眺めていたのを覚えています。いわゆる死にゲーというのはネット上のフリーゲームなどで流行はしていたもののメジャーの商業作品でそれを前面に出すというのは当時としては狂気の沙汰であり、それを成し遂げたデモンズ、ダクソシリーズの功績はゼロ年代以降では計り知れないです。

 

 確かデモンズは始まって直ぐに死ぬんですよね。で、ソウル体になって冒険が始まる・・・とかそんな流れだったはずです。(デモンズ・リマスターはぜひやってみたいなぁ)ネット上で話題になっていたのを良く覚えています。当時のゲーム・シーンでは高難易度のゲームはもはや売れない、という風潮でしたね、特に和ゲー。

 

3の世界から1へ 

 私自身、PC版のダクソ3でようやくダクソ・デビューをしたニワカですが、ダクソの本質は単に難しいだけのゲームではないということだと考えています。むしろ、単に難しいゲームというのは、和洋問わず昔からいくらでもあったし、当時でも特に洋ゲーにはいくつもあったでしょう。

 ダクソのいいところは、実際難しいし、嫌らしいが、丁寧に計算されたレベルデザインにこそあるのだ、と今回ダークソウル・リマスターをやって改めて思いました。何回も死ぬけど、もう一回やれば行ける!と思わせてくれる作りになってますよね。

 

 3で初めてダクソをやって感じたことは、むしろかなり昔ながらのゲームである、ということです。実際、ダクソシリーズはウィザードリィドラクエなどの名作レトロゲーの持つ面白さを現代に蘇らせるという理念があったことを最近知ったので、やはりなーと思いました。

 特に3はファミコンの面白かったゲームを再構成する、という側面が強かったように思います。理由は後述。 

 

 今回、3の世界から時代を遡って1の世界にやってきたのですが、いろいろ感動しましたね。おそらく1,2,3と通してプレイするのが一番感動すると思うのですが、これはこれで楽しかったです。

体験版での動作確認

 発売前に体験版が出て動作確認も兼ねてやってみたのですが、正直かなりモッサリな感じだったし、当然3と比べるとシステム的にも未成熟な部分が見受けられたので不安にはなりました。

 グラはむしろ全然綺麗だなと思いました。何にせよ、体験版は体験版でしかないので、あまり気にせずに製品版を待つことにしました。(買ってプレイを始められたのは2019になってからでしたが・・・) 

製品版は体験版が嘘みたいに何の問題もなかったので良かったです。

むしろ携帯モードの方が綺麗?

 Switch版の大きな魅力な一つは携帯モードでのプレイ。そんなに期待はしていなかったのですが、十分綺麗だし何の問題も感じなかったです。携帯機でダクソが出来る感動と言ったら!

 多少、FPSが落ちてガクガクすることはありましたが、PS3版の動画を確認するとPS3版よりかは良くなっているように感じられますし、実際プレイ上で困ることはありませんでした。心配されていた病み村も問題なし!技術の進化ってスゴイ!

 

 最初はテレビモード中心でやっていたのですが、携帯モードで遊び通すことができるか?と思い、途中からは携帯モードだけでプレイするようにし、そしてちゃんと1週目をクリアすることが出来ました。 

ダクソとダクソ3の比較

 ダクソ3は正直、プレイしたのがかなり前ではありますが、動画で見たりもしていましたし、記憶を呼び覚ましつつ、1との比較をしてみたいと思います。(2は未プレイ。2は今のところやる予定はありません)

グウィンとの邂逅

 まず、単純に1でラスボスのグウィンと対戦できて感動しました。3のラスボス、王たちの化身はシリーズファンにとってはものすごい感動するラスボスなんだよ!という話は聞いていたのですが、確かにそりゃ泣くよなと納得しました。やっぱ、あの曲がいいですよね、悲しい感じの。

 当然、3でシリーズ初挑戦だった自分は、王たちの化身に感動することはなかったのですが、シンプルに強くて正統派のカッコいいラスボスだなぁとは思ってました。1でグウィンと戦ったことで、また3をやりたくなりましたね。シリーズファンも初見も楽しめるように出来てた3はやはり集大成だったのだなぁと思います。 

やっぱアノール・ロンドはいい!

 3でもアノール・ロンドは好きだったのですが、1でアノール・ロンドに到達できた時はものすごく感動しました。シリーズファンが3のアノールロンドに着いた時は感動し、その衰退ぶりに打ちひしがれたと聞いていますが、逆行することでまた違う感動を味わうことが出来ましたね。3のアノール・ロンドは荒廃しまくっていたので、綺麗なアノール・ロンドがとても新鮮でした。

 エルドリッチと戦った場所でオーンスタインとスモウと戦ったのはなんか不思議な感じでしたね。オーンスタイン戦のBGMはグウィン戦BGMの次に好きです。 

システムの差

 シリーズ集大成である3と1を直接比べれば当然、3の方が洗練されているに決まっていますし最初は違和感がありましたが、慣れてしまえばほとんど問題ありませんでした。一部のマップなどでカメラがガクンと過剰反応してしまう箇所はあったのですが、基本的にはゲームのシステム上の問題を感じられる部分はなく、そもそも完成度の高いシリーズであったのだなと再確認。

 崖から落ちることがあるのは、そういうゲームだから!これ!ただ、狭い場所に強敵を置いておくという嫌らしさは1の方が強かったかなぁという気がします。

マップ構成の差

 1と3の大きな違いはマップ構成にあると思います。1はわりとシームレスというか、各地域が実は絡み合いながらきちんと繋がっていて、一つの大きな世界、巨大マップを形成しているという構成です。各地を行ったり来たりすることで行動範囲を拡げつつ、ボスが各地域に点在しているので、そこに殴りこみに行くというゲームになっていると思います。メトロイドヴァニア風?

 なので、どちらかというと1はオープンワールド的な構成になっており、割と自由度の高いゲーム進行が出来ます。実際、3で既にダクソをプレイ済みだったので、1は好きにやってやるぞ!という態度で臨んでいたので、クラーグの前(つまり二つ目の鐘を鳴らす前)に四人の公王を倒す、というプレイングをしました。(死ぬほど苦労しました)

 

 それに対し、3はどちらかと言うとボス戦に重点を置いていて、そこまでの道中はあくまでボスまでの道のり、つまり、昔ながらのアクションゲームにおけるステージである、という感が強いように思います。

 実際、ボスのデザインに関しては3が圧倒的に洗練されていますし、ボスにリソースを注ぎ込まれているように思います。3は最初から転送できますしね。なので、いい意味でファミコン感が強いのが3の魅力じゃないでしょうか。初代の悪魔城ドラキュラとかロックマンに近い感じですね。

 

 3は3で好きですけど、1のマップはくまなく探索してクリアすると、ドコと何処が繋がっているかの土地勘が身について、自分の庭のようにわかるようになるし、周回プレイが楽しいマップなのかなーと思いました。マップデザインは1が好きですかね。

 

Switchでやるゲームとして

 インディ系のソウルライク作品はいくつもありますし、自分はホロウナイト(Hollow Knight)はやりましたが、やはり本物は別格だと思いますし、やり応えのあるゲームをやりたい人はぜひやって欲しいゲームです。

 

 高難易度ゲーとしては親切ですが、あえて説明しないことを良しとする設計ではあるので、ハマッてしまうこともあると言えばあります。そんな時はネットなどで正規進行ルートだけでも調べてやればいいと思います。

 ただ、ダクソというのは初見の場所を手探りしつつ、緊張感と絶望感を味わいながら打開していくゲームだと思うので、基本的には攻略を見ないで楽しんで欲しいゲームです。というわけで2週目以降も楽しく遊んで行きます!

 

あとダクソ3をぜひスイッチでやりたい・・・!!

 

 

けものフレンズを知らない人に『けもフレ2炎上』について説明したい

 もう二度とけものフレンズについて書かないだろうと思っていましたが、自分なりの考えを書きたいので、ブログの趣旨からは逸脱しますが失礼します。

[2019/04/18 : 追記]

更にややこしい事態になっているのに笑うしかない、という状況ですが、それも含めて事情の解説、その理解が困難になっていますので、この記事で書いたように『下手に】近づかない方がいい』という指針に変わりありません。

[追記おわり]

 

  けものフレンズについては、本ブログでも記事を書いたりしましたし、楽しみました。で、まぁいろいろありましたよね。悲しかったし、あまりに醜く揉めまくってたので、距離をおいたりしました。『嫌なら見るな』というやつです。ただ、人間というものは好奇心には勝てないもので、けもフレ2をたまたま見てしまったんですよね。

 

 いろいろを書いてしまうと長くなってしまうので、端的になぜ多くのけもフレファンがけものフレンズ2に対して怒っているか、12話のどこが問題なのかだけを外部の人に伝わるように書いてみたいと思います。

 

 たかがアニメ、という主張も分かりますが、その陰湿さが現実のいじめでも使われるような手口なので、たかがアニメといえど、そういう内容の放送はどうかなと思ったので。

表面上はよくある、駄作な美少女アニメ

 けものフレンズ2は一部の表現を除いて、その構造はいわゆる中身のない美少女アニメです。美少女キャラがキャーキャーやるやつです。

 一期との落差で、より質が低いように見えます。よくある質の低い美少女アニメが、不幸にも高い注目度を浴びてしまったため過剰に低評価されている、という見方も出来なくはなく、けもフレ2とは本来は多くの人に見過ごされる程度の作品だったと言えます。

 

一部の表現と最終話を除いては。

 

分かる人にしか分からない罵倒、嫌がらせ

けものフレンズ2はよく前作のコンセプトを否定している、と言われます。しかし、正直、前作のストーリーやコンセプトを否定したりする作品はそれなりにあります。ガンダムZガンダムとかも、まぁこれは監督が同じなので比べられませんが。

 ぶっちゃけると、ビジネス上、客層を一新するために客を振るいにかけて、客を入れ替える、というのはありますよね。リニューアルであったり、リブート、作風を変えたりすることで。

 そういう意味では、けもフレ2はけものフレンズを否定し、その流れを断つためのものだったのでしょう。ただ、原作は動物達であり、動物達が主役であることは、1も2も同じであるはずです。

 

 問題は、その一期ファンを突き放すための表現、作劇があまりに不快で陰湿であるということだと思います。 一期を楽しんだ人ほど、精神的ダメージを食らうように計算して作られています。性質が悪いのは、一見ただの美少女アニメかのように装っている点です。

 一期を良く知らない人にとっては、キャラがかわいいだけの凡百の美少女アニメにしか見えません。いい大人がしょうもないアニメにいちいち怒ってんじゃないよ、という意見が出てもしょうがないかなと思います。

 

 動物を原作とするコンセプトなので、そもそもけものフレンズは題材として非常にナイーブです。ただ、単に中身のないドタバタギャグならよかったのですが、特に9話では、ヒトが感情移入しやすいイエイヌが不条理に酷い目に会う、救いも無い、という展開で、実際に気分をがかなり害された人も多かったようです。こういう描写上の問題も多いのです。

 

一期を楽しんだ人の琴線に触れるように

 12話、つまりけものフレンズ2の最終話における問題は、まさに外側から見ると、何の変哲もない表現だけど、分かる人には強烈な精神攻撃になっている、という陰湿さです。

 

 たかがアニメをいじめや家庭内暴力などと同列に扱うつもりはないです。しかし、12話で使われている手法はこれらの手口に近いものです。

 

 例えば、いじめっ子はいじめを遊んでやったと正当化したり、いじめていることにすら自覚的でなかったりします。また、家庭内暴力を行っている親は見えにくい部分に傷を負わせたり、外部に対しては良い親であるかのように振舞ったりします。

 繰り返しますが、たかがアニメをこれらの犯罪と同レベルに扱うつもりはありません。しかし、このように陰湿なやり方の精神攻撃を含むアニメは表現の自由を脅かすような存在だ、と言ってしまうのは大げさでしょうか。特定の層へ向けての強い悪意、私怨を含んでしまうのは、やはりまずいと思います。

 

f:id:miur-us:20190408002808j:plain

問題のシーン。(ニコニコ動画よりキャプチャ)

 いくらクソアニメと言われても、そうした精神攻撃のためのシーンの演出はかなり的確なんですよ。問題のシーンはそうした一期ファンに対するダメージを与えるための演出は正確で、さすがプロの演出は違うなぁと今では感心しています。

 夕焼けのシーンは特に感傷的になりやすいですし、嫌でも一期のシーンを想起させられる内容、ここぞとばかりに前作OP曲の『ようこそジャパリパークが流れるし、前作キャラを仲間はずれにしたような絵も最後出てくるし・・・。 

 あそこで、『ようこそジャパリパークを流したのも作為的で、この曲が流れた時に不快感を覚えさせるための仕掛けだと考えられます。不快な動画を見ながら、もし、自分の好きな音楽を聴かせられたら、その曲を聴く度にその不快な動画のことが頭に浮かんでしまうようになる、みたいなことです。

 で、アニメの最後に前監督のメタファーとも言える、カバン(前作主要キャラ)を排除したキャラクター集合絵が表示されて、けものフレンズ2は幕を閉じます。

 

 あれ、私の勘違いかな?というような嫌がらせ方を受けたある方はそれなりにいると思うんですけど、そういうのって、こっちからも抗議しづらいし、難しいですよね。ただ、今回の騒動はその類のものなので、一見なんともないように見えても、見てきたファンにとってはそうではないということだけ、ご理解いただけたらと思います。

 

 実際、特に12話でかなりの精神的な苦痛を受けた方が多かったようです。自分も脳が理解を拒否する、ということを初めて体験しました。今ではそこまでして一期を否定したかったんだーと割と冷静です(強がり)。

 一期が好きな人は感受性高くて、物語を読み取る力が強い人が多いと思うので、なおさら仕込まれた毒に反応してしまった、という感じでしょうか。

 

最後に

 以上、なるべく簡潔に今回の騒動についてまとめてみました。外から見れば、特に問題があるように見えないし、騒いでいる人が単にヒステリックになっているように見える、という判断をされかねないな、と思ったので書いてみました。

 

 ネット上でとにかく暴れたい人、騒ぎに乗じて好き放題やりたい人が集まってしまっている、というのも事実かもしれません。なので、興味のない方、これからもけものフレンズに近寄るつもりもない方は、そのまま距離をとって無視しておくのがよいと思います。

 

配列の浅いコピーと深いコピーについて | 配列の宣言、代入

 配列とは、タイプが同一の変数の集まりを扱うためのデータタイプです。

 

 配列(array)はいくつかの制限があります。宣言時に指定した要素数で固定されますし、そのせいで自由にデータの挿入が出来なかったり。C#では、例えばListの方が使いやすいので、無理に配列を使わなくてもいいかもしれません。

*追記*Listでも本記事と同様な現象は起きます。


 しかし、今回はそんな配列について、思わぬ事態に遭遇したのでまとめてみたいと思います。配列に配列を代入したら、どうなるのか?

 

浅いコピー、深いコピー

 タイトルの浅いコピー深いコピーというのは、Shallow Copy, Deep Copyから来ています。
 変数は変数に代入することで、内容をコピーすることが出来ます。

int a = 12, b = 8;
a = b;

// a >> 8, b >> 8
 これは単純なintタイプの変数ですが、この時はaの値は上書きされ、8となります。ただ、その後にbに2を代入すると、aは8、bは2という風に別々の値になります。別々の変数なので当然ですが。

b = 2;

// a >> 8, b >> 2

 しかし、配列も同様に、単純な代入でコピーをしようとすると、不思議なことが起こります。

配列のコピー

int[ ] array1 = {1, 2, 3, 4, 5}

int[ ] array2 = array1;

 配列の場合、ある配列に対して別の配列を代入すると、たしかに内容、各要素の値は同じな配列となるので、コピーできたかのように見えます。しかし、その後が問題です。

array1[1] = 0;

// array1 >> 1,0,3,4,5

// array2 >> 1,0,3,4,5

 array1の値を変更すると、なぜかarray2の値も変更されます。これこそが、浅いコピーによる不可思議な現象です。


 コピーの元々の目的とは、値が同じだが独立した別々の変数、配列を得る、というもの。しかし、shallow copyでは、メモリ上に存在する一つのデータ列を2つの配列が共有する、という現象が起きてしまいます。

array1 >> {1, 2, 3, 4, 5} << array2

 

配列とポインタ

 実は、配列は本質的にポインタと同じ構造となっています。ポインタとは、ある変数のメモリ上のアドレス値を格納するための変数です。C#では、基本的にはあまり触れられませんが、CやC++では初心者殺しで有名です。

 

 実は、配列は複数のデータを格納しているわけではなく、その一番先頭の値が保存されているアドレス値を格納していますので、仕組みは実はポインタと同じになります。

 配列の各要素のデータはメモリ上に連続的に、保存されています。アドレス値を利用したインデクシング(indexing)によって、各要素の値を出し入れできるというのが、配列の仕組みです。

 

array << アドレス番号が記録されている

[1]     << そのタイプのバイト数×添え字の分だけアドレス番号をずらす

array[2] << arrayがintの配列なら、intは4バイトなので、2=8 arrayのアドレスがもし100なら、三番目の要素は100+8 = 108番地のメモリに記録されていることになり、そのアドレスのメモリに格納されているデータをコンピュータが読み込む、ということになります。


 こうした仕組みはC#であっても同じで、つまり配列に配列を直接代入しても、アドレス値がコピーされるだけで、同じ要素を持った別の配列は作られません。

 なので、浅いコピーに対する深いコピーとは、プログラマが明示的に同じで要素数を持った別の配列を宣言して、一つずつコピー先の要素に対応したコピー元の要素の値を代入していく、ということになります。

int [ ] array2 = new int[ array1.Length ]

for(int i= 0; i < array1.Length; i++){

    array2[i] = array1[i];

}
 こうすれば、同じ値を持った別個の配列が作られるので、片方の操作がもう片方に反映されることはありません。

 

きっかけ、倉庫番

 倉庫番というゲームをC#のコンソールアプリで作っていて、この事態に遭遇しました。コンソールアプリはCUIで、文字しか表示できません。しかし、ローグという偉大なゲームもCUIから誕生していますし、単純な昔ながらのゲームを原始的に再現するのは練習になるだろうと思ったので、挑戦しました。ただし、ノーヒントではなく、Python版の解説には目を通してます。


 ルールとしては、プレイヤーは荷物や箱を押すことしか出来なく(引くことは出来ない)、指定の場所(床)に全ての箱を移動させたらクリアです。ゲームが積むこともあるので、レベルリセット機能も必要になります。

 

 プログラムの流れとしては、まずマップを読み込み、画面に表示する。そして、プレイヤーの入力を受け付けた後、プレイヤーや箱を移動させ、コンソールの表示をConsole.Clearで消して、入力後のマップを直ぐに描画する、というもので、これをゲームクリアするまでループします。

 

 また、この手のマス目の2Dゲームは、マップをテキストで表すことが出来ます。実際、その方式の練習も兼ねていました。応用すれば、3Dゲームでも同様にステージやダンジョンを単純なテキストデータで表現、保存することも可能なようです。


プログラム内部では、charの配列として扱います。

char[ ][ ] map = {

new char[ ] = {'#', '#', '#', '#', '#'},

new char[ ] = {'#', ' ', ' ', ' ', '#'},

new char[ ] = {'#', '#', '#', '#', '#'}

} //こんな感じです

 2Dマップなので、二次元配列でなければいけません。二次元配列とは、配列の配列です。しかし、ジャグ配列を採用しました。二次元配列とちがい、各行の要素数を自由に変更できるからです。

 また、各面を管理するのに、配列の配列(マップ)を配列(各面)として、まとめる必要があり、大きさの違うマップをより簡潔にまとめるのにも適していました。

 

二つのマップを利用する

 そんなわけで、ひとまず岩を押す機能は置いておいて、移動の実現を優先しました。移動キーを押したら、その先のマスをチェックして、壁でなかったらプレイヤーの文字をそのマスに表示させ、プレイヤーのいたマスには、マップの元の床を表示させる、という入れ替え方式です。


 これには、その面のマップ構造を表す配列と、現在のマップ状態を表す配列の2つの配列が必要でした。それで、最初はマップを読み込んだ時点で、今回扱った浅いコピーをやってしまったのでした。

 

 単純な移動だけだと、問題は顕在化しなかったのですが、岩を押す機能を加えたとたんに、奇妙な動作をするようになりました。プレイヤーが増えたり、岩が増えたりです。最初はとにかく原因不明で焦りました。


 それで、とりあえず2つの配列を同時に画面に映そう、とやってみたら、見事に操作にあわせて、2つのマップがシンクロして動きました。

 

 マップ構造の配列は、変わってしまっては駄目です。プレイヤーや岩が移動した後に、元々その床が何だったのかを参照するデータベースだからです。繰り返しますが、浅いコピーによって、独立した2つの配列に出来ていなかったのが原因。移動だけの時は上手くいっていた(ように見えていた)ので、原因究明が難しかったです。

 

 原因が分かったので、先程書いたようにきちんと深いコピーで配列を作り、無事問題は解決されました。

 

配列には気をつけよう!

 というわけで、配列の宣言、代入やあるいはポインタとしての性質について、書いてみました。C#使いであっても、ある程度ポインタの知識は必要ですね。

 ただ、難しく考えすぎず、変数は箱ではなくて、メモリ上の一区画、と捉えて、そのメモリの番地、住所がアドレス、だと考えればいいと思います。

 

 

Unityでのオープン・ワールドにおけるBGMの自然な切り替えとFMODについて

 オープンワールド・ゲームにおいて、BGMを自然に切り替える、クロスフェードする方法をUnity上で再現してみたので、その経緯と結果をまとめます。そして、自分なりにやってみた後で、FMODという画期的なオーディオソフトに出会ったので、それについても書いてみます。

 

 オープン・ワールドという概念の定義については様々な議論がありますが、ここでは境目のないフィールドが広がり、ロードを挟むことなく各地のロケーション、村やダンジョンなどの建造物へとシームレスに出入りできるマップデザイン、とします。なので、厳密にはオープン・フィールドという言葉の方が適しているのかもしれません。

 

自然な移り変わり

 ゲームにおける音楽の重要性について、今更ながら語ることはないでしょう。しかし、昨今のゲームにおいては、そのオープンな構造や自由な進行に対応できる、柔軟なオーディオ・コントロールが求められます。

 例えば、オープンフィールドにおいては、全体のフィールドと各地のロケーションに隔たりがないので、もしそのエリアの専用BGMがあるなら、二つの音楽を自然に切り替えなければならないからです。反対にステージ制、エリア制なら、単純にそのレベルをロードし終えた後で、BGMを再生さえすればいいことになります。

 

 実際、最近のゲームのほとんどがこれに対応しています。村に入ったら、村の人々の声が聞こえるようになり、音楽が鳴りだします。不気味なエリアに入ると不気味な音楽に自然に切り替わり、水の中に入ると、フィールド音楽とシンクロする水中版アレンジに瞬時に切り替わる、というのもあります。

 そうした状況や変化に柔軟に対応する、インタラクティヴな音楽表現には前々から興味がありました。そして、そのような機能を持った多くのゲームを自分でもやってきて、これはどういう風にやっているんだろうと感じていたので、チャレンジしたというわけです。

 

エリアをコライダーで囲む 

 四方に広がるメインフィールドにポツンと村や城、あるいは敵のアジトがあるかもしれません。そのロケーションを円形のコライダーで囲みます。今回はカプセル・コライダー(Capsule Collider)をトリガーモードで使用しています。円形であることで、どの方向から入っても同質の処理が出来るし、ある程度高さのある建物にも対応したかったからです。

 まず、プレイヤーがそのエリア内にいるということを検出します。 下図は、その状態をフィールドの真上から見た図です。

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

真上からの図

 単純な音楽の切り替えということなら、 このコライダーがプレイヤーを検出した瞬間に、メインの音楽を止めてエリア音楽をスタートさせればいいことになります。しかし、それだと不自然なブツ切りになってしまいます。理想としては、二つの音楽が鳴り続けながら、プレイヤーが進むにつれて音量がスムーズに切り替わっていく、という動作を目指します。 

 

プレイヤーの位置(割合)を計算する

 自分が考えた方法は、そのコライダー内に音楽を切り替える区域を設定し、プレイヤーがその区間のどこにいるかで、メイン音楽とエリア音楽の音量バランスを変える、というものです。

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

プレイヤーの位置から、音量バランスを決める

 上図の赤い帯部分がそうです。ここを抜けて、完全にエリア内(図では白い円の中)に入ってしまえばエリア内の音楽に完全に切り替わることになります。

 

プレイヤーが赤い帯内にいる時に、どの位置にいるのかの割合を計算します。  

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

円の中心に対する距離から計算する

 二次元上の円の中心に対する距離なので、3Dゲームの場合はエリアを真上から見下ろした二次元で考える必要があります。つまり、Y軸の高さを除外した、X軸とZ軸による二次元平面におけるプレイヤーの座標を計算するということです。

 なので、Vector3からVector2に変換します。

Vector2 playerPos2D = new Vector2(player.pos.x, player.pos.z );

 という感じで、2DだとY軸は縦ですが、そこに3Dにおける奥行きであるZ軸の値を代入します。すると図のような真上から見たときの二次元平面でのプレイヤーの座標が得られます。

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

3D座標を2Dに変換し、相対的な位置を割り出す

  まずは赤帯部分の長さです。コライダーの円の半径と内円の半径の差で出せます。図だと黄色線ピンク線の差ですね。そして、プレイヤーの赤帯部分内における相対的な位置は、プレイヤーと円の中心までの長さ(水色線)からピンク線の長さを引いて、赤帯の長さで割ればいいです。

 そうすれば内円から測って、赤帯内の何%の位置に立っているかを計算できます。

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

外円からの割合をエリアBGM、内円からの割合をメイン音楽の音量に

 エリア外のどの位置から入ってきても、同様な処理をするのには、このように中心からの距離で計算するのがいいはずです。というわけで、赤帯内にいる時はこのように計算した値から、図のように音量バランスを設定しています。

 

スクリプトについて

 今回、二つのAudio Sourceの音量をクロスフェードをする、ということでAreaMusicFadeCrossというスクリプトを作成しました。

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

AreaMusicFadeCrossのメンバー変数

 これをカプセルコライダーをアタッチしたゲームオブジェクトに追加して、メインBGMエリアBGMのAudio Sourceをセットするだけで使えます。

f:id:miur-us:20190205000624j:plain

トリガーモードをアクティブにするのを忘れずに!!

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

Start関数

 Start()内にて、プレイヤーやコライダーの参照を得たり、そこからコライダーの半径をgetしたりします。

 ここでは半径の70%を内円としているので、areaThreshold = colliderRadius * 0.7fとなっています。半径からそれを引けば、赤帯区域の長さが計算できるので、faderCrossDist赤帯の長さです。

 

Unityのオーディオソース(Audio Source)

f:id:miur-us:20190204234756j:plain

メイン音楽のAudio Source

 Unityにはオーディオソースというコンポーネントが用意されています。これにオーディオクリップ(wavやMP3などの音声ファイル)を読み込み、音を鳴らすわけですが、ボリューム・プロパティがありますので、これを使います。

AudioSource mainBGM = GetComponent<AudioSource>();

mainBGM.volume = 0.0f;

って感じでボリュームを変更できます。

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

 volumeの値は0.0f~1.0fまでの値を取るので、これは0%~100%と考えていいです。なので、フレーム毎にプレイヤーの位置を計算して、その割合をそのままボリュームの値に設定してもいいのですが、それだと変化が急すぎてしまいます。

 なので、Math.Lerpを使って緩やかに推移させていく、という処理にします。この関数を上手く使えば、線形補完によって、緩やかに値を目標値にまで変化させることができます。

 

余韻を持たせる

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

プレイヤーはコライダーから出たら・・・

 位置によって、バランスを変化させるということが出来ました。しかし、ボリュームの変更にMath.Lerpを使っていることもあって、このままだとエリアのコライダーを出た瞬間にBGMがぶつ切りになってします。(あるいはクロスフェードが途中で止まる)

 ある程度の余韻を持たせて、コライダーを出てからも緩やかにフェードクロスが続き、しばらく経った後で完全にメイン音楽に切り替わる、という風にしたいです。そのためには単にエリアのコライダーにいるというフラグだけでは不十分です。エリアから出た後でも、しばらくの間クロスフェードさせたいからです。 

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

Coroutineで処理を遅らせる

 なので、IsFadeCrossingという変数を作っています。そして、コルーチンによる遅延処理を行います。エリアから出た5秒後にIsFadeCrossingをfalseにするという処理です。これで、エリアを出た後でも、IsFadeCrossingがtrueの間は、Update関数内でクロスフェード処理が続行することになります。

 

コルーチンのキャンセル

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

StopCoroutine()

 ただし、一つ問題があります。出た後にまた直ぐエリア内に入ると、コルーチン処理がエリア内にいた状態で実行されてしまいます。

 なので、コルーチンが実行されている間にまたエリア内に入った場合は、StopCoroutineを使ってコルーチンを停止させます。

 

挑戦を振りかえって

 今回のやり方だと、Mixerを挟んでいないのでシンプルで分かりやすいですが、より高度なことをやろうとするとミキサーを挟まないと駄目なんでしょうね。

 ミキサーの場合は、パラメータ操作にややこしい部分もありますが、ダッキング機能(他の音が鳴ると音量を下げる機能)スナップショット機能もあるので、上手く使いこなせれば、今回チャレンジした以上のことも出来そうです。ここらへんはUnityのミキサー周りをよくわかっていないので、なんとも言えません。

 

FMODとの出会い

f:id:miur-us:20190205003529j:plain

FMODのトップ画面(公式デモ・プロジェクト)

 というわけで、とりあえず目標となる動作を自分なりに実現できたのですが、その後、FMODというUnityやアンリアル・エンジンでも使えるオーディオ・ソフトと出会い、そのあまりの性能の良さに感動し、この記事を書くきっかけにもなりました。

 

 FMODというのは、インタラクティブなメディアにおける柔軟なオーディオ管理を実現するためのソフトで、3Dオーディオやゲーム内の状況、プレイヤーの操作に対応したオーディオ操作をより高度にスムーズに行うことのできるソフトです。

https://www.fmod.com/

 自分もまだ触り始めたばかりなのですが、DAWや動画作成ソフトなどを触ったことのある人なら、すぐに動かせるようになると思います。直感的で使いやすい!!しかも、ホビイストや個人製作者(予算制限あり)ならタダ!で使うコトができます!はっきり言って、タダで使っていいクオリティではないので、ゲーム・オーディオなどに興味ある人はぜひ触ってみるべきです!

 

FMODで出来ること

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

トラックをレイヤーで分けてコントロールする

 今回のエリア内に入ると音が鳴り始める、はもちろんですが、それ以上に高度なオーディオ処理が、簡単に実現できるようです。例えば、楽曲内のセクションごとのループだとか、進行具合をパラメータとして、曲の進行をコントロールしたりすることも可能です。上写真ではAreaというパラメータで、各レイヤーの音量を変化させています。

 サウンドや音楽は全てFMOD内では全てイベントとして扱えますが、様々な音声を組み合わせることが出来るし、それらをソフト上のエフェクタで加工することも出来ます。

f:id:miur-us:20190205004211j:plain

いくつかのサンプルから、ひとつをランダムに再生できる

 また、いくつかのサウンドをまとめて、その内のどれかをランダムに再生するということも簡単に出来ますし、再生の度にピッチや音量を微妙にランダマイズさせることもできます。

 Unityで自分でスクリプトを書いて、同様のことを出来なくはないですが、それよりはるかに手軽で素早く出来ます。

 

 また、デモプロジェクトとして、なんと!あの有名インディゲームCelestaのFMODプロジェクトが配布されています。元々はMOD開発者向けのためのようですが、FMOD初心者にとっては格好の教材なので、これも研究したいと思います!

 

 今回の記事でチャレンジしたこと、あるいはその他の様々なことが、このソフトがあれば実現可能となるのでかなり興奮しており、ガンガン触っていきたいな、という感じです。

 

 

停止性問題について考える。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