パスワードを忘れた? アカウント作成

yumeさんのトモダチの日記みんなの日記も見てね。 スラドのRSSを取り込んでみよう。

14397031 journal
日記

yumeの日記: Unity制作 メデューサ・ゲーム #9 1

日記 by yume

●タイトル画面

タイトル画面と言っても、とりあえずは「なんかキーを押したらゲームが始まる画面」でよい。
一応タイトル画面であることがわかるように、仮のタイトルロゴを入れておく。実際にタイトルロゴを使うかは未定。
とにかくなんでも入力したらゲームスタート(現状はいきなりStage 1から始まる)
後々ここでやりたいこともあるが、一旦OK

--

●ESCキーでメニュー画面。あと一時停止

ESCキーを押すと:
・音量調整
・ゲーム終了
みたいなのが出るといい。
現状は音もないから、とりあえずはゲーム終了だけがあればいいが、仮のメニューも作っておこう。
メニューの外枠が必要だが、枠のサイズごとにでかいスプライトを作ってたら大変だな。
たぶん9スライスみたいな機能があるだろう。探したらバッチリある。

そんで、uiCanvasオブジェクトを作る。これはキャンバスオブジェクトで、スクリーン上に出てくるもの。
uiCanvasの子オブジェクトとして
・inGameMenu(うっすらとした黒)
        ・MenuWindow(メニュー背景ボックス)
        ・ボタンを二つ(音量・終了)
を置く。
uiCanvasにはMenuManagerスクリプトを置き、このスクリプトがエスケープキーの入力によって、inGameMenuのアクティブ状態を切り替える。

Unity既存のUIコンポーネントは、触った時にフェードインしながら色を変えたりとかができるようになっているので、自分でこの辺りを作る必要が無い(細かく変えようと思ったらやっぱり作らなきゃあかんけど)。

さて、タイトル画面などでは問題ないが、ゲーム中はポーズ(一時停止)としておきたい。
簡単な方法としては、Time.timeScaleを0にする、などがあるそうだ。
TimeScaleはFixedUpdate関数などの処理速度を変えるが、Update関数は止まったりはしないらしい。
対策として、すべてのUpdate関数にTimeScaleが0かどうかチェックする、などがあるようだが、ちょっと難がありそうだ。
notargs.comのポーズ処理の方法を導入してみよう。
ただし、このスクリプトは3D向けに作ってあるので、RigidBody周りを2Dに変える。
このコンポーネントが入ったオブジェクトに、ポーズさせたい対象のオブジェクトすべて子として入れてしまえば、ポーズさせれば停止するようだ。
今回はプレイヤーと兵士、あとカメラも入れておく。
テストすると……経路探索系の動きがおかしい。ポーズするたびに停止するのはいいが、ポーズ解除で再び加速するような動きになっている。ふーむ。
Astarプロジェクト公式曰く、
canMoveをtrueにすれば移動、Falseにすれば停止するようだ。
一時停止メソッドのどこかに、それを入れておけばうまくいくだろうか?
一時停止メソッドは、読んでみるに:
・RigidBodyの速度を保存し、RigidBodyをSleepさせる(アクティブ/非アクティブとは違うみたい? > RigidBody2Dはアクティブを切り替えられない代わりにSleepがあるようだ)
・アクティブかつ無視リストにないMonoBehaviour(とにかくUnityのMonoBehaviourを継承するすべてのコンポーネント=FixedUpdateとかで動くやつ)を非アクティブにする
で、戻すときは逆にアクティブにしたり、速度を入れてやったりしているようだ。

ここに、
・AIPathならcanMoveだけをFalseにする。
・MonoBehaviour停止リストから、AIPath型のものは除外する
という二つのコードを追加する。

こうすれば、
・RigidBodyはSleep
・AIPathはアクティブなままだがcanMoveだけFalse
・それ以外のすべてのMonoBehaviour継承コンポーネントは非アクティブ
となる。

このために型の比較が必要だが、調べたところisとかTypeOfとかでチェックできるようだ

MonoBehaviourとはなんぞや、というと大雑把にUnityのUpdateやらなんやらにひっかかる型、およびそれを継承する型のことのようだ。

アセットを使うと自分が理解していない要素が当然出てくるので、いじるときちょっと怖い。
が、とりあえずこれはこれでうまく動いてくれた。

次はテキスト描写シーンを作りたい。

14393794 journal
日記

yumeの日記: Unity制作 メデューサ・ゲーム #8

日記 by yume

●捕獲システム
昨日の続き。
テストAIで経路探索は成功したので、
・兵士がRaycastでメデューサを見つける
・見つけたら「発見状態」で経路探索開始
とつなげて、兵士の発見・経路探索は完成。

--

●ステージ

ゲームオーバー処理はデバッグメッセージになってるので、ゲームオーバー処理を作る。

……の前に、まずは「ステージ」という概念を作ったほうがよさそうだ。
Unityにはすでに「シーン」という概念があるので、これを使ってみよう。
まずはさっと調べてみる。

公式
Qiita @lycoris102 ざっくりとまとめ
note @鈴木ジョン@ゲーム開発 シーン偏移でデータの受け渡し
>これはシーン間で持っておきたい変数の扱い方のようだが、グローバルな何かを使ったりする方法もあるらしい。
簡単なゲームならそれでいいだろうけど、フラグ管理とかやるとやっぱりグローバルなオブジェクトが必要に思える。

各シーンにGameManagerを置き、そのステージインデックスをシーン偏移時に引き渡す。
ステージ上のすべての兵士の数をカウントしておき、メデューサがすべての兵士を石化させると次のステージに偏移する。

--

●ゲームオーバー
これはすぐリトライすればよい、という形にしておき、後々エフェクトなどは作りたいが、一旦即座にシーンをリセットする。

こうなる。

コンティニューにしろ、ステージ移動にしろ、画面偏移の効果が必要だから、どんな効果にするかちょっと考えておこう。
(肝心のステージも)

それはさておき、次はタイトル画面を作ろう。

14389080 journal
日記

yumeの日記: Unity制作 メデューサ・ゲーム #7

日記 by yume

●メデューサ移動システム
現状はマウスクリックしたポイントに向かってvelocityを決めて、フレームごとに徐々にそこに近づいていく、という方式だが
少し変えて「マウスをクリックしている間、マウスカーソルへ向かう」という方式にする

private void MoveTowardAim()
{
        towardPosition = aimPosition - myBody.position;
        towardPosition = towardPosition.normalized;
        myBody.velocity = towardPosition * speed;
}

●捕獲システム
兵士がメデューサを捕捉し、近づいて、捕獲するまでの動作。

兵士とメデューサをつなぐ線が、壁でさえぎられないとき:
・兵士はメデューサを「発見」した状態になる。

兵士は発見状態の時:
・メデューサへの経路を取得し、そこへ向かう。

兵士がメデューサに触れたとき:
・メデューサの動きを止める、移動不可能状態にする
・1秒数える
・ゲームオーバー処理

ただし:
・1秒数え終わるまでに石化してしまった場合は中断

だいたいの部分は簡単にできたが、問題は経路探索だ。
A*アルゴリズムだとかなんとかいろいろあるらしいというのは知っているが……。

arongranberg.comにUnity向けのA*経路探索アセットがある。
有料だが、無料で試せるバージョンもあるらしい。2D経路探索の動画解説もあるのでこれを一旦試してみよう。

かなりややこしいが、読んだ感じ:
・指定のサイズ(ex. 40*24)平面グリッドを作っておく。
・シーンのコライダーなどをもとに、平面グリッドを「通行不可領域」と「通行可能領域」に分ける。
・このアセットのAIコンポーネントをアタッチすると、指定したターゲット探索メソッドに基づいて、ターゲットを選んでそこへ移動する。
ということを大雑把にやればいいようだ。既存のwallは先にLineRendererで線だけを設計し、ゲーム実行時にそこからメッシュとコライダーを生成するという形だったが、これだとちょっとやりづらいので、「PolygonCollider2Dからメッシュをエディタ上でリアルタイムに生成」するやつを入れた。
仮のキャラクターにAIを入れて、サンプルのターゲットへ向かうスクリプトで試す。
そうするとこう。いい感じだ。

14382334 journal
日記

yumeの日記: Unity制作 メデューサ・ゲーム #6

日記 by yume

●乗算

可視領域ポリゴンを、一旦乗算ブレンドにしておく。
公式曰く、
・Blend DstColor Zero // 乗算

うーん、うまくいかないな。Photoshop上でテストしたやつよりもずっと暗いし、何より下のやつが全部消えてる。
一方、Unityのビルトインシェーダー「Legacy Shaders/Particles/Multiplyだとうまくいく。
公式より、ビルトインシェーダーをダウンロードできるらしい。
これをいじってみよう……と思ったが、あまりにもわからない要素が多すぎる。
このシェーダーはパーティクル用らしい。それでも動くならいいか、とも思ったがもう少し調べよう。

teratail.com @Bongoがスプライト乗算シェーダーのサンプルを書いてくれている。
これをベースに、ステンシルに関するコードを追加する……。するとうまく動いた。

●アート
とりあえずキャラクターの基礎アートを描く。
二等身くらいのキャラクターなら顔と髪型のパーツがでかいので、メデューサの特徴を強調しやすいだろう。
ゲームに疑似的な高さを導入するにしても、デフォルメレベルが高ければ不自然な印象は受けづらそうだ。
Adobe illustratorで簡単にベースキャラクターを描き、さらにメデューサと兵士を見分けられる程度に変えておく。
あまり描き込んで、後々プロポーション変えたくなったら大変なので、仮のスプライトだ。
ついでに、メデューサのマウスの角度に応じてスプライトの向き(x軸のスケールを1・-1で切り替えて疑似的に向きを表現)する。

するとこうなる。

絵が入るだけでなんだかそれっぽくなるなぁ。

14340324 journal
日記

yumeの日記: Unity制作 メデューサ・ゲーム #5

日記 by yume

●メデューサ・ゲーム

●Unityのバージョン

テキトーに最新版2020.1.4f1を使っていたが、長期サポートの対象はLTSバージョンである2019.4.9f1だから、ゲームをリリースするつもりならそっちのほうがよいらしい。
まだ要素も少ないので、一旦新しいプロジェクトをLTSバージョンで作り、そっちにインポートしておく。

って、引っ越したらすぐに2019.4.10f1が来たぞ……。でもマイナーバージョンアップは問題ないらしい。

--

●視界マスク

視界に入っているものは普通に描画し、視界外のものは描画処理を変えるやつを作りたい。
例えば、視界内は普通に表示して、視界外は真っ黒、とか任意のスプライトで覆うとかそういうの。
とは言ったものの、実際どうすればいいのか見当つかず。

Code Monkeyがまさにやりたいことを動画化してくれている。
しかし、スクリプト可能なレンダリングパイプラインを使う、と言っていきなりなんか見たことのないものをいじり始めた。
公式にもあるので、これも使える要素のはずなんだが、この概念が無い。
Code Monkeyではパッケージファイルもダウンロードできるらしいので、試しに登録してダウンロードしてみる。
インポートできたものの、いまいち使い方がわからない。めっちゃエラー出てるし。シーンも空なので実行しようがないな。

うーん、これは一旦止めよう。

--

wgld.org曰く、
・ステンシルバッファを使えば、あるモノのある部分を描画するかどうかを決められる
>結果として型抜きのような表現が可能になる

の前に、シェーダーについて調べておこう
samurai blog @sato曰く、
・Shaderは描画方法を記述したプログラム
・Unityではデフォルトで、Standard Shaderが実行されている
・Shaderはマテリアルの属性として選ぶ
・自作のShaderはCreate-shader で作る。種類がいろいろある
>試しにUnlit shaderを作れとある。 UnlitはLightの影響を受けないShaderらしい。

記事の続きでは、
・ShaderLabはUnityでShaderを記述する言語
>GLSLとかとは違うんだ
・コードをコピーしたら、真っ白なマテリアルが表示される。色を変えるとちゃんとかわる。
>なんかコード上にへんなゴミ(a____)があってエラーが出たが、コメントアウトしたらまぁ動いた。

うーんと、多分このサンプルコードでは、ただ色情報だけを持ってきておいて、フラグメントシェーダーにその色を返す。
頂点シェーダーでは、そのメッシュの頂点座標とかを入れて、
フラグメントシェーダーでは、拾っておいた色をすべてのピクセルに置いて実行している。

ということだと思う。シェーダーは全然理解できてないんだけどたぶん。

Sebastian Lagueが、視界のステンシルシェーダーについての動画を投稿している。
内容を追いかけると、だいたいこういう仕組みらしい:
・StencilMaskと、StencilObjectという二つのシェーダーを作る。
・StencilMaskは、Stencil{ Ref 1 }と書いてStencilメソッド?のRefを1にする。
・StencilObjectは、Ref 1のところだけ描画する
・オブジェクト用マテリアルと、視界領域用マテリアルを作り、それぞれにシェーダーをはめる。
・可視領域に「StencilMask」を、すべてのオブジェクトに「StencilObject」をあてる。
これで、視界領域のメッシュはマスクになり、オブジェクトは「通常は見えないが、マスクにかかったところだけは見える」ということになる。

とりあえずBackGroundだけにこのマテリアルを反映して、試してみよう。

なるほど。でも後々やりたいことを考えると、できたら逆にしたいな。「マスク」領域は「隠されて(マスクされ)」)、「でっかい板」がカメラの目の前にあって、その板がマスク領域だけ隠される。他のオブジェクトは普通に表示されるだけだが、板の後ろに隠れているところは見えない。そういう感じがいい。

もう一個調べよう

はるのゲーム開発メモ曰く、
・ベースとなるシェーダーを作り
・Write側とRead側に、それぞれ微妙に書き換えたシェーダーを渡す
・するとWrite側の範囲だけが描画される

このコードを見ると:
Write側は
Stencil{
        ref 2
        Comp always
        Pass replace
}

Read側は
Stencil{
        ref 2
        Comp equal
        Pass keep
}

公式曰く、
・Comp hogehogeのhogehogeが:
・always 常に表示
・Equal Refが等しいときだけ表示
・NotEqual Refが等しくないときだけ表示
他いろいろ。

ということは、現状は「Writeの範囲にある、WriteとRead両方を描画」ということになっているのを
「Writeの範囲外にある、Readのみを描画」にできるんじゃないか?
Write側を Comp Never
Read側を Comp NotEqualにすれば……。
だめか。これだけではだめで、
そのあとの
Pass replace
のあたりがミソだね。
Comp は「ref値とバッファ(現在値)を比較して、」
Comp equal は「ref値とバッファが等しいならステンシルテスト合格」
Pass は「ステンシルテストに合格した場合、バッファ値をどうするか」
Replace 「バッファをrefに置き換える」

この他に、
Fail なんてのものある。失敗時にバッファをどうするか
Fail replace なら「パステストに失敗した場合はバッファをrefに置き換える」
つまり……。

Write側:
//自分の領域はすべて描画せず、自分の領域すべてのバッファを2にする。
Stencil{
        ref 2
        Comp Never
        Fail replace
}

Read側:
//バッファが2「ではない」領域全てで描画。
Stencil{
        ref 2
        Comp notequal
        Pass keep
}

とすれば、ReadオブジェクトをWriteオブジェクトで隠すことができるんじゃないか。ではこれを可視領域とでっかい板に反映する。

えーと、可視領域は板より手前にないといけないから……。
・カメラ
・可視領域
・板
・その他の全部
という順にすればいいね。あと可視領域がプレイヤー本体とわずかな周囲を描画できるように、可視領域の深度に小さな球体(円形でいいんだけどないから)を置いて、そいつもWriteオブジェクトとする。

そうすっと……。うまくいくぞ!!

ってことは、例えばこのマテリアルをSpriteRendererに乗せることで、黒い板じゃなくて任意のスプライトでマスクできるかな。

うーん、さっき作ったShaderがSpriteを書くようにできてないみたいだ。
なので、新たなShaderを作って、Stencilの部分だけ追加する。

すると……。うまくいった!

14334820 journal
日記

yumeの日記: Unity制作 メデューサ・ゲーム #4 1

日記 by yume

●メデューサ・ゲーム
昨日の続き。

公式曰く、
・Mesh.Clear()でメッシュの情報を削除する
・このメソッドにfalseを渡して実行しないと、頂点レイアウト(Triangles)は削除しない
そもそもMesh.Clearはしていなかった(Meshに頂点座標とレイアウトは毎フレーム新たに渡していたので)が、このメソッドを頭につけるとうまく動くようになった。

これでエラーは消えたが、まだ問題は残る。左向き問題だ。
現状では、プレイヤー原点から左はRadianでいうと-π/πの境界線になっている。
以前の360度メッシュの場合、この境界線をまたぐ頂点(一番最初の頂点と、一番最後の頂点)をつなぐことで、一周させていた。

仮に今Playerが右を向いていて、頂点は全部で5つ。
1:0.785(左端)
2:0.2
3:0
4:-0.2
5:-0.785(右端)
という順なら、時計回りで頂点をソートできたことになる。この場合は大きい順にすればいい。
左をまたがないすべての場合は、これで問題なく動く。現状のソートでこれが実現できている。

左をまたぐ、左向きでならどうか。現状は:
1:3.1415
2:3.0
3:2.355(右端)
4:-2.355(左端)
5:-3.0
こういう順にソートされている。このままでは、メッシュをつなげるメソッドは、0(プレイヤー原点)-1-2、0-2-3、0-3-4とつなげてしまう。

理想的には:
1:-2.355(左端)
2:-3.0
3:3.1415
4:3.0
5:2.355(右端)
こういう順に並べば、時計回りに頂点をソートできたことになる。
しかしどうやればこういう順に並べられるだろう。

視野角は0.785なら:
右(0)に向くとき、 左端は 0.785、 右端は-0.785。
左(π)に向くとき、 左端は-2.3565、 右端は 2.3565。
下(-π/2)に向くとき、 左端は-0.785、 右端は-2.355。
上(π/2)に向くとき、 左端は 2.355、 右端は 0.785。
左をまたぐと左右の大小が入れ替わってしまう。言い換えると、右端>左端の時、左をまたいでいる。

左をまたぐとき、リストから「右端の点」を探し、そのIndexをとる。
最初から右端IndexまでのリストAと、Indexから最後までのリストBを作り、
visibleVertex = リストB+リストA;
とすればいけるか?

//左をまたぐとき、ソートを右端基準で入れ替える
if (rightRad > leftRad)
{
        int rightIndex = visibleVertex.FindLastIndex(
                delegate (VisibleVertex x)
                {
                        float y = rightRad;
                        return x.Radian == y;
                });

        int rightToEndCount = visibleVertex.Count - (rightIndex+1);

        //コピー用配列を作り、0~右端までをコピー。
        //元のリストの0~右端までを削除
        //元のリスト += コピー用配列(0~右端まで)
        VisibleVertex[] copyArray = new VisibleVertex[rightIndex+1];
        visibleVertex.CopyTo(0, copyArray, 0, rightIndex+1);
        visibleVertex.RemoveRange(0, rightIndex+1);
        visibleVertex.AddRange(copyArray);
}

なんか複雑だが、とにかく動くぞ。

邪眼も視野内じゃないと動かないようにして、これでできた。

14330637 journal
日記

yumeの日記: Unity制作 メデューサ・ゲーム #3

日記 by yume

●メデューサ・ゲーム

●メデューサの視野角を制限する
メデューサは現状では、360度の視野角(計算的には-π~π)を持っている。これを「前を向いているなら、後ろは見えない」というように、数値によって制限された視野角にしたい。
流れとしては:
・弧度viewingAngleを持っておく
・マウスポジションから2D座標aimPositionをとる
・aimPositionに対する弧度をとる(-π~+π)

え~と、それから……。

既存のFOVメソッドでは:
・ポリゴン頂点に向かって弧度goalRadianをとる
・goalRadianに向かってRaycastする
という仕組みなので、このRaycastする前に、goalRadianが「視野角内か」を判定し、視野角外なら無視すればよいはず。

例えば、aimPositionが0、viewingAngleが0.785だとしたら、goalRadianが(-0.785)~(0.785)の範囲にあるか調べればよい。
言い換えると

if (goalRadian >= (aimRadian - (viewingAngle)) && goalRadian <= (aimRadian + (viewingAngle)))
{
//do something
}

で、この範囲なら……。いや、これじゃだめかな。
例えばaimRadianが3.0の時は、最小値は2.215だが、最大値が3.785とかになってしまう。弧度は-π~+πの範囲だ。
この場合の正しい範囲は 最小値2.215、最大値-0.645だが……。 2.215より大きくて-0.645より小さい数字、なんてことになる。

いや、aimRadianからの差を求めればいいのか?
例えば 3.0の時、goalRadianが1.0だったら、その差は2.0、viewingAngleは0.785だから、差の方が大きい=視野角外
同3.0の時、goalRadianが-3.0だったら……。計算上の差は6.0、でもほんとの差は約0.283。ダメじゃん。ムムム……。

hakuhin.jp曰く、
角度の差を求めるには
・角度Aから角度Bを引く(sub)
・subを0~360の範囲に丸める(sub -= Math.floor(sub / 360.0) * 360.0)
・角度差が-180~180の範囲に収めるように丸める(if(sub>180.0) sub -= 360.0;

ふーむ?
メデューサ・ゲームの場合、与えられた差を何であれ-π~πの範囲に丸められるといいんだろうか。
実際のRadianの差は、π以下になるはずだ(180度真後ろでちょうどπなので)
言い換えると、πを超える差が出ている場合、それは左向きの角度をまたいでいるということだから、
本当の差は……。差 - 2π?
6.0 - 2π = -0.283。あ、これでいいかも。

if(sub > Math.PI)
{
        sub = (float) sub - (Math.PI * 2);
}

かな。ちゃんと絶対値にしなきゃならないが。
視野角が0.785だとして、
goalが-2.0、aimが1.0の時……差は3.0。視野外。あってる。
goalが3.14、aimが0.0の時……差は3.14。視野外。OK
goalが3.0、aimが-0.2の時……差は3.2。丸めて3.08。視野外。OK
goalが0.1、aimが0.0の時……差は0.1、視野内。OK
goalが-2.0、aimが3.0の時……差は5.0、丸めて1.283。視野外。OK。
goalが-3.0、aimが3.0の時……差は6.0、丸めて0.283。視野内。OK。
いけそう。
前回のvisibleVertexの取得の前に、

//弧度差チェック
float radDiff = Math.Abs(ownerControll.aimRadian - goalRadian);
if (radDiff > (float)Math.PI) radDiff = Math.Abs(radDiff - (float)Math.PI * 2);
//弧度差がviewingAngleを超えてるなら、無視。
if (radDiff > viewingAngle) continue;

を追加。

このままだと、メッシュがうまく形成できないだろうから、視界の範囲のふちにそれぞれRayを飛ばし、衝突点を取る必要があるだろう。
それも追加して、いざ動かすと
Mesh.vertices is too small. The supplied vertex array has less vertices than are referenced by the triangles array.
というエラーが出た。これは頂点が少なすぎるというときに出るエラーのようだ。
メッシュの頂点を視界外に持っていくとエラーが出ているっぽい。
うーん、どこかで何かを間違っているのか、アルゴリズムそのものに瑕疵があるのか……。

今日はもう遅いので、思いついたところを並べて明日考える。

・右上に視界を持って行ってスタートすると、ひとまずエラーは出ないが、視点を動かして頂点ポイントを視界からひとつでも外すとエラーがでる。
その時、エラーが出る前のMeshのverticesは30、Trianglesは84。(厳密にいえば代入前のリスト)
エラーが出た時点でのverticesは27、Trianglesは75。3の倍数ずつ減ってるから、おかしくないような……。
・視界外ポイントを除外する部分をコメントアウトすると動く(もちろん視野の概念も消える)
その時のverticesは51、Trianglesは147。

んー、メッシュ作成メソッドでは(前回から「最後のつなげる三角形」は消した)

//頂点リストをクリア
meshPoints.Clear();
meshTriangles.Clear();

//プレイヤー原点を入力
meshPoints.Add(ownerBody.transform.position);
meshTriangles.Add(0);

//最初の三角形の頂点をとる
meshPoints.Add(visibleVertex[0].EndPoint);
meshTriangles.Add(1);
meshPoints.Add(visibleVertex[1].EndPoint);
meshTriangles.Add(2);

//次以降の三角形をとる。
for (int i = 2; i < visibleVertex.Count; i++)
{
        //三角形の頂点番号を打っておく
        meshTriangles.Add(0);
        meshTriangles.Add(i);

        //頂点をとる。頂点番号を打つ。
        meshPoints.Add(visibleVertex[i].EndPoint);
        meshTriangles.Add(i + 1);

        //メッシュに代入
        FOVMesh.vertices = meshPoints.ToArray();
        FOVMesh.triangles = meshTriangles.ToArray();
        FOVMesh.RecalculateNormals();
}

meshPointsがverticesに、meshTrianglesがtrianglesに代入されるわけで、
meshPointsは:
・プレイヤー原点
・最初の2点
・次、頂点1点ごとに1点。
meshTrianglesは:
・プレイヤー原点
・最初の2点
・頂点1点ごとに3点。

えーと、meshPoints総数は:
・壁ポリゴンの頂点総数は16(background含め)
・衝突点はその3倍の48
・さらに、視界のふちの2とプレイヤー原点1
合計51。あってる。
Triangle総数は:
プレイヤー原点 1
最初の2点 2
その後、3番目以降頂点ごとに3
3 + (51-3)*3 = 147
あってる。
エラーが出た時点では27:75
3+(27-3)*3 = 75
あってるじゃん? なぜだろう。

エラーが出るのはメッシュ作成メソッドだが、メッシュ作成メソッドに使うリストは単なるリストで、しかもソート済み。
頂点取得メソッドで弧度差チェックを除外するとエラーが出ない。ソートはメソッドのすべてが終わった最後に行ってるから、ソート前のリストがすでにおかしいというだろうか。
やはり弧度差チェックメソッドがおかしい?
しかし視界から外れた瞬間にエラーってどういうことだ。マウスの座標をとるタイミングがおかしい?

14328669 journal
日記

yumeの日記: Unity制作 メデューサ・ゲーム #2 1

日記 by yume

●補習
昨日のコレだが:

public int playerLayer = 9;
int layerMask = ~(1 << playerLayer); //Exclude layer 9

++C++;曰く、

・<<は左シフト(これはまぁ覚えてた)
・~は補数
例えば二進数000111の0と1を逆転し、111000にする。

で、例えば

int D (1 << 5) //32
int rD = ~D //-33

となるようだ。ここではintを使っているが、bitに直すと
0b11111111111111111111111111011111
になり、符号付10進数であるintは、これを-33と読むらしい。
たぶんUnityのLayerMaskのとらえ方はすごく賢くて、intで与えられた数字を二進数に読み替えている(厳密にいえば、機械はそもそも二進数で読んでいるんだろうけど)んだろう。
だから、0b11111111111111111111111111011111は「5番目だけが0、ほかは1」、つまりレイヤーマスク5以外すべてOKとして扱えるようだ。

--

●メデューサ・ゲーム

石化の続き
石化について実装したい機能をまとめて考える
・石化進行中は視覚的にそれがわかるようにしたい
・メデューサに見られていない間は、石化の進行が止まるのではなく、徐々に元に戻していきたい。
うーむ、現状では直接GetEvilSeeingメソッドを見られるたびに実行するが、そうではなく、見られているときは「邪視状態」をTrueにして、
邪視状態中は徐々に石化が進行する、ということにしようか。

private void CheckEvilSeeing()
{
        if (evilSeeing)
        {
                evilDamage += Time.deltaTime;
                if (evilDamage > resistPower)
                {
                        evilDamage = 0.0f;
                        Debug.Log("石化しちゃった!");
                }
                evilSeeing = false;
        }
        else
        {
                evilDamage = Mathf.Min(0.0f, evilDamage - Time.deltaTime);
        }
        evilSeeing = false;
}

このメソッドをFixedUpdateメソッドで実行すれば、毎Fixedフレームごとに、evilSeeing状態ならば石化ダメージを負い、evilSeeingでないなら石化ダメージを徐々に回復する。
フレームの最後で状態をfalseにしておけば、「メデューサに見られたフレーム」だけevilSeeing状態になるはず。

evilDamageに応じて、一旦スプライトの色を青色に変えていこう。evilDamageが0なら正常な色。

そんで、実際の石化処理を行う。
これは石化したSoldierをDestroyするか、「石化状態」というべき状態に切り替えてしまえばよさそうだが、どっちがよいだろう。
Destroyする場合:
・メデューサ側でStart時にキャラクターの数を数え、それを配列にしているが、この配列が可変にならなければならないので、Listにする必要がある。
・ListからDestroyされるタイミングで削除する。
・Destroy後に、自分の石化した状態のSpriteだけを持つGameObjectをそこに置く。

Destroyしない場合:
・自分の状態を「石化状態」にする。
・様々なコンポーネント(RigidBody2Dとか)を無効化する。
・Spriteを「石化状態」に変える。

ん~。後々Soldierが後から増えたりする場合は、どちらにせよリスト化する必要はでてくるし、「様々なコンポーネントを無効化」が後々大変そうなので、
ここはDestroy + Listから削除 + 死体オブジェクトを残す方式にしよう。

メデューサが持つcharactersリストには、個々のGameObject情報が入っている。そこにはインスタンスIDもある。
Soldierが破壊される直前に、メデューサのリストから自分のインスタンスIDを削除するよう命令しよう。
ただ、それをFixedUpdate中にやるといろいろややこしそうだ。
公式曰く、
どうやらイベントの中には「OnDestroy」なるものがある。ここでもろもろの処理をすればよいだろう。

で、リストから自分のインタンスIDに合致するものを削除する、だが
リスト自体はGameObject型のリストで、GameObject型からインスタンスIDをGetする関数はあるものの、
直接リストからRemoveするメソッドがないかな。Remove()はGameObjectそのものを指定しなきゃならないし。

public void DeleteFromList(int instanceID)
{
        for (int i = 0; i < characters.Count; i++)
        {
                int checkID = characters[i].GetInstanceID();
                if (checkID == instanceID)
                {
                        characters.RemoveAt(i);
                        return;
                }
        }
}

で、これを石化時にSoldier側から呼び出す。呼び出す場合はまずメデューサを名前で検索してから……。
なんだか回りくどいな。例えばGameManagerみたいなオブジェクトがいて、すべてのCharacterのリストはそいつが持ってて、すべてのキャラクターはそいつへの参照をもって生まれたらもうちょいスマートかな。でもとりあえずこれでいこう。

これで動くかな?と思ったがダメ。エラー:
NullReferenceException: Object reference not set to an instance of an object
EvilEye.EvilEyeSeeing () (at Assets/Script/EvilEye.cs:38)
リスト削除メソッドの起動を、Destory時ではなくFixedUpdate中に移すとうまくいった。
さてなぜだろう。
OnDestroyでの処理の流れは:
・FixedUpdate(Player):Soldier[i]のevilSeeingをTrueに。
・FixedUpdate(Soldier[i]):evilSeeingがTrueなので、石化ダメージを追加。
・同:石化ダメージが抵抗力を超えたので、SoldierをDestroy
・OnDestroy(Soldier[i]):
・デバッグログを流し
・メデューサの名前で検索
・メデューサのリストを含むスクリプトを取得
・スクリプトにDeletFromListを実行させる

ところが、OnDestroyのデバッグログが流れる直前にエラーがでる。

ん~。例えば、あるフレームにおいて、SoldierのDestroyが実行された後に、PlayerのFixedUpdateが実行されて、そのあとにSoldierのOnDestroyが実行される、という順序になった場合、Player側では「すでにDestroyされたオブジェクトを探す」ということをやろうとしてしまうか。
一方、OnDestroyを使わない場合は
SoldierのFixedUpdateでリストからの削除も同時にされ、そのあとPlayerのFixedUpdateが走ってもすでにリストにはないので、問題はない。
逆に、PlayerのFixedUpdateで検索した後に、Soldier側から削除命令が出ても、やはり問題はない……か。
なら順序を問わない方法がいいか。

それで、OnDestroy時にStatueオブジェクトを生成する。
Qiita @Teach曰く、
・Instantiate(obj, this.transform.position, Quaternion.identity); みたいな感じでその場に生成できる。

生成はできたが、Unityになんか怒られる。

テラシュールブログ曰く、
・OnDestroy中にInstatiateするとゴミが残る。
・シーン停止時に、すべてのオブジェクトでUnityがOnDestroyを実行するため
だそうだ。回避策は、OnApplicationQuitかどうかを判別するか、単にOnDestroyでなんか生成するのをやめればよい。やめよう。

良い感じ。

14320905 journal
日記

yumeの日記: Unity制作 メデューサ・ゲーム #1

日記 by yume

●メデューサ・ゲーム

寝る前に簡単にゲームのルールを考えた:
・ゲームは2Dで、上から俯瞰したような(見取り図のような)視点、または疑似的な角度のついた(クロノ・トリガーみたいな)視点。
・ステージ上にはメデューサ(プレイヤー)、兵士、猫、壁がある。
・メデューサは:
        ・2次元の視界を持つ。視界は壁によってさえぎられる(前回までに作ったもの)
        ・マウスクリックで目標点を指定し、そこまで一定の速度で歩く。
        ・視界に兵士か猫を3秒間入れ続けると、それを「石化」させる。

・兵士は:
        ・メデューサよりも足が速い。
        ・壁にさえぎられていない直線状ならば、メデューサの座標をとらえて接近する。移動速度はメデューサよりも早い。
        ・メデューサに触れると、1秒後にメデューサを「捕獲」する。

・猫は:
        ・動かない、または、時々気まぐれな位置にゆっくりと少しだけ移動する。

・勝利条件は2種類(または、ステージによって異なる):
        ・ステージ上のすべての兵士を石化させる
        ・ステージの所定の位置まで移動する

・敗北条件は2種類:
        ・敵に「捕獲」される
        ・猫を「石化」させてしまう

まずは兵士と猫のベースとなるキャラクターの基盤、Characterプレハブを作ろう。
・壁に対する当たり判定
・移動速度
・メデューサに見られているか、現時点で連続で何秒見られているか。

まずはPlayer同様にRigidBody2D、Collider、SpriteRendererコンポーネントをアタッチ。
このCharacterプレハブをもとに、さらにSoldierのプレハブを作りたい。
公式曰く、
プレハブをいじったものを新たにプレハブに登録すると、
オリジナルプレハブ(新たな独立したプレハブ)か、プレハブバリアント(元のプレハブの影響を受けるプレハブ)を作れるらしい。便利。

さて、Characterは実際に壁にぶつかったりできるようになったが、プレイヤーにぶつかったときはボールのごとく跳ね返るだけだ。
プレイヤーにぶつかった場合は単に衝突するのではなく、何らかの処理(Soldierであれば「捕獲」アクションの開始など)にしたい。

Qiita @nutti曰く、
・衝突時の処理はOnColliosionEnter

公式曰く、
・オブジェクトのコライダーが他のコライダーに衝突したとき呼び出される。


private void OnCollisionEnter2D(Collision2D collision)
{
        if (collision.gameObject.tag == Player) //Playerは定数
        {
                Debug.Log(collision.gameObject.name); //ここでなんかする
        }
}

こんな感じだろう。デバッグにもログが返ってくる。Soldierの場合は「捕獲」アクションを開始することだ。
具体的な捕獲アクションは置いておいて、次に、視界に入ったときの「石化」処理を書く。
「視界」は現在、描画はMesh Rendererで行われているが、概念としては単なる座標の集合だ。
使えそうなコライダーは
・MeshCollider
・PolygonCollider2D
だが、どちらを使うべきだろう……。
公式曰く、
MeshCollider.sharedMeshで衝突検出のためのメッシュを指定できる。
すでにメッシュは視界のために生成しているわけだから、これをそのまま反映すればいいんだろうか。一旦そのようにしてみよう。

……。ダメだ。Meshは指定して、Soldier側は「とにかくなんでもCollisionでログを出す」としたが動いていないように見える。
うーん、編集画面を見る限りは、どうやらコライダー自体は作れているようなんだけど……。
Mesh Colliderは2D向けではないのかもしれない。じゃあPolygonColliderで……まてよ。

そもそも、「視界内に入っているCharacter」とは、言い換えると「壁にさえぎられない直線状」であればそれでよいのでは……。
それでいいな。それでいいわ。

・メデューサはすべてのCharacter(=自分を除くすべての動くやつ)への方向に毎フレームRayCastする
・RayCastが壁に当たったら、その分は無視
・Characterに当たったら、当たり続ける限りそいつに「石化メソッド」を実行させる。
・「石化メソッド」は、CharactoerにevilDamageを追加する
・evilDamageがresistPowerを超えたとき、そのキャラクターは石化する

視界がつながった時間をちゃんと算出しなければならない。
Qiita @Nagatch曰く、
・Time.deltaTimeには最後のフレームからの経過時間(ms)が格納されている。

これを足し続ければいいか。さて実際にRayCastを飛ばして困ったのは、
前回はLinecastして当たる対象は「レイヤーマスク」でPolyWallレイヤーにあるものだけを指定したが、
今回は壁とCharacter両方を検出しなければならない。
まぁ、Characterのレイヤーを「PolyWall」にすればいいわけだが、ちょっとマヌケっぽいやり方だ。

stackoverrun.com @Programmer曰く、

public int playerLayer = 9;
int layerMask = ~(1 << playerLayer); //Exclude layer 9

で「9」を除いたintを返せる、ということらしいがこの書き方は初めてみた。
今はちょっと時間がないので明日調べよう。

今日の成果。

日記を読み返したりしてふと気づいたが、Update()とFixedUpdate()が2D Visibilityスクリプトの中で正しく使い分けできえてなかったような。
視界関係はUpdate()に入っていたので、FixedUpdateに移す……。すると前回のメッシュのあらぶりが完璧に消えた。よかった~~。

14316600 journal
日記

yumeの日記: Unity学習 #10 2D Visibility 完成

日記 by yume

2D Visibility

●可視領域多角形の角度をとる。角度を基準に頂点をソートする

必要な頂点の座標はすべて取れたが、
プレイヤーを原点としたこの座標に対する角度を得る必要がある。
その前に、まずそのためのクラスを作っておく。

public class VisibleVertex
{
        //可視多角形領域の頂点とプレイヤーからの角度
        public float Radian;
        public Vector2 EndPoint;

        public VisibleVertex(float radian, Vector2 endPoint)
        {
                Radian = radian;
                EndPoint = endPoint;
        }
}

これで、可視領域に必要な座標と角度を保持する型ができた。
これまでは単にVector2リストに入れていた終端点リストを、このリストに置き換える。
そして、終端点を求めるタイミングで、角度も同時に求めているので、その角度も同時に入れておく。

for (int i = 0; i < polyVertexs.Count; i++)
{
        float goalRadian = GetRadian(position2D, polyVertexs[i]);
        RaycastHit2D myHitPoly;
        Vector2 checkRay;

        //-0.00001度のチェック
        checkRay = RadianToPoint(goalRadian - 0.00001f, viewRange);
        myHitPoly = Physics2D.Linecast(position2D, checkRay, polyWallLayer);
        visibleVertex.Add(new VisibleVertex(goalRadian - 0.00001f, myHitPoly.point));

        //+-0度のチェック
        checkRay = RadianToPoint(goalRadian, viewRange);
        myHitPoly = Physics2D.Linecast(position2D, checkRay, polyWallLayer);
        visibleVertex.Add(new VisibleVertex(goalRadian, myHitPoly.point));

        //+0.00001度のチェック
        checkRay = RadianToPoint(goalRadian + 0.00001f, viewRange);
        myHitPoly = Physics2D.Linecast(position2D, checkRay, polyWallLayer);
        visibleVertex.Add(new VisibleVertex(goalRadian + 0.00001f, myHitPoly.point));
}

次に、visibleVertexのリストを、radianを基準に並べ替える。
smdn曰く、
・複数型のソートは普通の.Sort()ではダメ
・Comparisonデリゲートの形でSortメソッドに渡すとよい。
>デリゲートってなんだ。
e-Wards曰く、
・デリゲートとは、委任・委譲などを意味する英単語
・オブジェクト指向プログラミングにおいて、あるオブジェクトが一部の処理を他のオブジェクトに任せる仕組みを意味する。
ピンとこないけど、実際のサンプルコードを見てみる:

static int CompareAccount(Account x, Account y)
{
    if (x.ID > y.ID)
        return 1; // xのIDがyのIDより大きい→xはyより大きい
    else if (x.ID < y.ID)
        return -1; // xのIDがyのIDより小さい→xはyより小さい
    else // if (x.ID == y.ID)
        return 0; // xのIDとyのIDが等しい→xはyと等しい
}

Account型は string nameと int IDを持っている。
このメソッドでは、複合型クラスAccountのID xと ID yを比較して結果を返すようだ。
Sortメソッドは、この「比較メソッド」の結果を引数として受け取れるらしい。

list.Sort(CompareAccount);

で、ID順に並ぶそうだ。
俺の場合、radian順に並べばよいわけだから

static int CompareRadian(VisibleVertex x, VisibleVertex y)
{
        //ソート用。Radianで比較
        if (x.Radian > y.Radian)
                return 1;
        else if (x.Radian < y.Radian)
                return -1;
        else
                return 0;
}

を準備して
visibleVertex.Sort(CompareRadian);
を毎フレーム実行すればいいか。

ん~これ、ちゃんと動くけど、ソートできてるかどうかがわかんないな……。
えーと、今はGLで色のついた線を1本ずつ描画してるから、その線を描画するたびにちょっとずつ赤から青へ変化させていけば、
正しくソートできているなら、綺麗なグラデーションになるはず。

……。なってる!

ただ、これではソートの順番が逆だ。これは比較メソッドのReturnの正負を逆にすればOK。

--

●ソートされた頂点リストをもとに、メッシュを描画する。

難解なメッシュづくりに戻ってきた。
昨日の資料によれば:
・どんな多角形でも三角形の組み合わせで作ることができる。
・三角形は3つの頂点と、そのつなげる順番で定義できる。
・正しく時計回りに頂点の順番を設定しないとダメ。

いま、ソートによって、頂点座標はプレイヤーから見て左から時計回りの順に並んでいる。
・0番はプレイヤーの原点としておく。
・1番はリストの[0]番を取る。
・2番はリストの[1]番を取る。
0,1,2の順で繋げると……。時計回りだ。次に:
・3番はリストの[2]番を取る。
0,2,3の順で繋げると……。やはり時計回りだ。
・4番はリストの[3]番を取る。
0,3,4の順で繋げると……。当然時計回りだ。
いけそうな気がしてきた。


private void Update()
{
        //頂点リストをクリア
        meshPoints.Clear();
        meshTriangles.Clear();

        //プレイヤー原点を入力
        meshPoints.Add(myFOV.ownerBody.transform.position);
        meshTriangles.Add(0);

        //最初の三角形の頂点をとる
        meshPoints.Add(myFOV.visibleVertex[0].EndPoint);
        meshTriangles.Add(1);
        meshPoints.Add(myFOV.visibleVertex[1].EndPoint);
        meshTriangles.Add(2);

        //次以降の三角形をとる。
        for (int i = 2; i < myFOV.visibleVertex.Count; i++)
        {
                //三角形の頂点番号を打っておく
                meshTriangles.Add(0);
                meshTriangles.Add(i);

                //頂点をとる。頂点番号を打つ。
                meshPoints.Add(myFOV.visibleVertex[i].EndPoint);
                meshTriangles.Add(i+1);
        }

        //最後の三角形をとる(最初と最後をつなげる三角形)
        meshPoints.Add(myFOV.visibleVertex[0].EndPoint);
        meshTriangles.Add(0);
        meshTriangles.Add(myFOV.visibleVertex.Count);
        meshTriangles.Add(myFOV.visibleVertex.Count+1);

        //作ったリストを実際のMeshに代入して、描画してもらう。
        FOVMesh.vertices = meshPoints.ToArray();
        FOVMesh.triangles = meshTriangles.ToArray();
        FOVMesh.RecalculateNormals();
}

……。動く!!!
できる気がしなかったけど、できたぞ!!

--

●マテリアルを作る

できたはいいが、現状だとピンクのメッシュ(マテリアル未定義だとピンクになる)が視界を表現しているので、
むしろ可視領域がピンクで染まって何も見えない感じになっている。
せめて可視領域は半透明で白っぽくしてやろう。
公式曰く、
Unity上でマテリアルも作ることができるらしい。
YTTMWORK曰く、
・マテリアルとは、色や光沢、テクスチャによって物質の材質を表現する概念
>3DCGとかと同じだよね。
・色はAlbedoをいじると決定できる
・Alpha値もここ。
>とりあえず、白の半透明のマテリアルを定義。
一旦これでいいか。しかしテストするとピンク色ではなくなったものの、めっちゃ暗いグレーになる。
Qiita @ReoNagai曰く、
・マテリアルに光が当たってないので暗くなっている
・シーンにLight -> Directional Lightを入れると全面ライトアップしてくれる。
>白くはなったが、なんかまだグレーっぽいし透明でもないな。
・Directional Lightの位置と向きを調整(画面に垂直に)
>真っ白にはなったが、透明ではない。
KKLOG曰く、
・MaterialのRendering ModeをFadeにすればよい。

そんで、もう仮の線は要らないので消す。
するとこうなる。

たまにメッシュがあらぶってる。
不透明なマテリアルで表示する分にはあまり気にならなかったが、
半透明にするとメッシュ同士が重なったり、隙間が空いたりした部分が見えやすい。

それと、今は半透明の白いマテリアルをメッシュ領域に乗せている感じだが、
本当は、例えば黒い半透明ですべての領域を乗算して暗くして、
可視領域だけを本来の明るさで描画するなどしたい。
座標と多角形の定義はできているから、この辺りはシェーダーにやってもらうといいんだろうか。
あるいは、LightをMeshで定義した形で照らしたりできる?
まぁ、それはおいおいやっていこう。

次は、この要素(動く・視界がある)だけで遊べるミニゲームを作ってみたい。

typodupeerror

アレゲはアレゲを呼ぶ -- ある傍観者

読み込み中...