
そして、少し脱線してグーグルプレイでゲームを書きましょうか? そして、私が普段記事を書いているような巨大で圧倒的なゴミではなく、私の心にシンプルで甘い何かを。
実際、すべてが非常に簡単です。最終的に開発者アカウントを登録し、実際に試してみたいと思います。 これらの行を書いている時点では、単一の記述クラスも単一の描画ピクセルもありません。 実際、この記事は本当の開発者向けログです。
記事
- 最初の部分。 23個のポリゴンからの口ひげを生やしたシューティングゲーム。
- 第二部。 多角形の腹を持つ口ひげを生やしたシューティングゲーム。
目次
- アイデア
- マイクロプロトタイプ
- 最初のカード
- 設定と視覚スタイルの選択
- 3Dへの移行
- 手続き生成
- 2Dの正多角形
- 3Dの正多角形
- 影
- 頂点シェーダーの影
- CPUでのシャドウ生成
- シャドウの最適化
- 前半のまとめ
レベル1.1 アイデア
最初のカリヤキマリヤキ。
紙の上でこの円を参照してください? 彼から始めます。 私には、どんなゲーム(まあ、どんな仕事でも)が同様のサークルで始められると思われます。 彼は数秒後に何になりますか? ホイールで? 帽子? 惑星? このサークルの意味を想像しながら、落書きを描いています。 帽子!
厳しいおじが道を歩いており、私たちは彼を上から見ています。 重度-彼は銃で撃つ方法を知っているからです。 街を歩き回り、口ひげを吹き、強盗に銃弾を発射した。
この空白は、私の頭の中で長い間回転してきた単なる画像です。 しかし、私は間違いなくクリムゾンランドのようなゲームをしたくありません。 そして、私はいつも好きではなかった2つのジョイスティックを持つGUI。 不要なOccamのかみそりをすべて切り離し、出力で次の概念を取得します。
レベル:家、木枠、樽のある小さな町。
キャラクター:メインキャラクター(シューティングゲーム)、盗賊、通行人。
ゲームは一時停止し、プレイヤーのアクションを待ちます。 プレーヤーは任意の方向にスワイプします。 この時点で:
1.ゲームの時間が進み始めます。
2.主人公はプレイヤーが指示した方向に撃ちます。
3.主人公は指示された方向に動き始めます。
0.5秒が経過すると、ゲームの時間が再び停止します。 プレーヤーは、すべての盗賊を倒し、できるだけ少ない通行人を負傷させる必要があります。
自動撮影と停止時間のこの組み合わせは、私がとても気に入っていました。
- 自動撮影:
- 管理を簡素化します(2つのジョイスティックの代わりに1回スワイプ)。
- 興味深いメカニズムを追加します(通行人を撃たないように、行き先を確認する必要があります)。
- 停止時間:
- ゲームプレイに変動性を追加します(異なるレベルを実行できます:遅い物思いに沈んだパズルまたはスマートシューティングゲーム)。
- 自動発砲の難しさを和らげます。
- 私(レベルデザイン)とプレイヤーの両方のダイナミクスを制御できます。
徐々に、アイデアは視覚化され、詳細が大きくなりすぎます。 今日はこれで十分です。
Todo:小さなプロトタイプを作成し、時間をかけずに移動/撮影することの楽しさを確認します。
レベル1.2 マイクロプロトタイプ
Unity3Dのおかげで、プロトタイピングは非常に簡単です。 BoxCollider2Dでいくつかの壁を追加し、RigidBody2DとCircleCollider2Dで丸いスプライト(プレーヤー、通行人、盗賊)を追加します。 箇条書き-同じスプライト、小さい、赤のみ、飛行経路用のRigidBody2D 、 CircleCollider2DおよびTrailRenderer 。
私は自分のClockクラスで時間管理を行っていますが、他のすべてのクラス(プレーヤー、弾丸など)は、Time.DeltaTimeではなく、そこからの時間差を使用します。
using UnityEngine; using System.Collections; public class Clock : MonoBehaviour { [SerializeField, Range(0, 2)] float stepDuration; [SerializeField] AnimationCurve stepCurve; float time = -1; float timeRatio = 0; float defaultFixedDeltaTime = 0; static Clock instance; public static Clock Instance { get { return instance; } } void Start() { instance = this; defaultFixedDeltaTime = Time.fixedDeltaTime; } void OnDestroy() { if (instance == this) instance = null; } public bool Paused { get { return time < 0; } } public float DeltaTime { get { return timeRatio * Time.deltaTime; } } public float FixedDeltaTime { get { return timeRatio * Time.fixedDeltaTime; } } public void Play() { if (!Paused) return; time = 0; timeRatio = Mathf.Max(0, stepCurve.Evaluate(0)); UpdatePhysicSpeed(); } public void Update() { if (Paused) return; time = Mathf.Min(time + Time.unscaledDeltaTime, stepDuration); if (time >= stepDuration) { timeRatio = 0; time = -1; UpdatePhysicSpeed(); return; } timeRatio = Mathf.Max(0, stepCurve.Evaluate(time / stepDuration)); UpdatePhysicSpeed(); } void UpdatePhysicSpeed() { Time.timeScale = timeRatio; Time.fixedDeltaTime = defaultFixedDeltaTime * timeRatio; } }
最も基本的なプロトタイプは1時間半で完成し、バグがいっぱいです。
- プレイヤーと弾丸の移動は、速度ではなく、transform.positionを介して行われるため、プレイヤーは壁に寄りかかってソーセージを使用します。
- 時間を停止しても物理現象は停止しません(fixedDeltaTimeは変更されません)。したがって、一時停止モードでは、キャラクターはわずかに移動します(相互にプッシュされます)。
しかし、このバージョンでも、移動して撮影することはすでに面白いです。 もちろん、最初のプロトタイプは完全に表現不可能です。
最初のプレイ可能なプロトタイプ
ただし、翌日にはすでにタスクが表示されます。
チップ:
- 「反射」壁を追加します。そこから弾丸が跳ね返ります。
- 弾丸との衝突でボットの破壊を追加します(方法はわかりません)。
- 柔軟な時間管理を追加します(「時間の経過の速度」の期間と曲線がありますが、これは不便です)。
修正:
- transform.positionではなく、速度を変更するために動きをやり直します。
- 壁での弾丸の作成を禁止します(プレイヤーは壁に寄りかかって撃ち、弾丸はすぐにプレイヤーを殺します)。
Todo:発明したチップでテスト学習レベルを作ります。
レベル1.3 最初のカード
通りを歩きながら、テストレベルの計画を立てました。
- 安全なエリア。 プレイヤーは歩くことを学び、すべての弾丸は壁に行きます。
- 非アクティブな敵との狭い廊下。 プレイヤーはホールを下り、攻撃方法を理解します。
- 非アクティブな盗賊と数人の通行人による廊下の拡大。 延長-ターンの後、プレイヤーが誤って通行人に入ることはできません。
- 鏡の代わりに潜望鏡の形の回転-ターンの背後にある反射壁-非アクティブな敵。 プレーヤーは壁を撃ち、リバウンドの仕組みを確認します。
- 鏡張りの壁、敵、通行人がいる廊下。 プレイヤーは廊下を慎重に通り過ぎ、通行人にぶつからないように(または、必要に応じてそれらを撃ちます);
- サンドボックス
ステージにオブジェクトを投げ、反射する壁を黄色で塗り直します。 次のようになります。
トレーニングレベルの種類
別のレイヤーで反射壁を作成すると、障害物と衝突する弾丸のコードは次のようになります。
void OnCollisionEnter2D(Collision2D coll) { int layer = 1 << coll.gameObject.layer; if (layer == wall.value) Destroy(gameObject); else if (layer == human.value) { Destroy(gameObject); var humanBody = coll.gameObject.GetComponent<Human>(); if (humanBody != null) humanBody.Kill(); return; } else if (layer == wallMirror.value) { Vector2 normal = Vector2.zero; foreach (var contact in coll.contacts) normal += contact.normal; direction = Vector2.Reflect(direction, normal); } }
バグを修正し、最終日に発明したチップを追加します。 Clockクラスを作り直しました。以前は、コースは実際の秒のstepDurationであり、時間速度の係数はstepCurveカーブによって決定されました。 曲線は、コースをスムーズに開始および終了するために必要です。
Clock.csの古い設定
ただし、ストロークの継続時間を変更した場合にのみ、開始/終了の継続時間も変更されます(曲線の縦座標が1に等しくない場合)。 また、ストロークの長さが短すぎる場合、「オン」時間は鋭すぎるように見え、1秒程度の場合、遅すぎるように見えます(曲線が移動の長さ全体にわたって「伸びる」ため)。 コースの開始と終了、および開始/終了の継続時間に個別の曲線を追加します。
プレーヤーを監視し、プレーヤーの軌跡を視覚化するカメラを追加します。
目的も管理も説明せずに、プロトタイプを数人の知人に静かに見せること。 誰もが経営陣を理解することができましたが、気づいていない問題があります。 私自身のために、プレイテストの結論を書き留めました。
- 狙うのは難しいです。 指の最後の2つの位置の間のデルタを使用します。より多くの値を取得する必要があります。
- 指は画面をスワイプするのにうんざりします。 コースの期間、レベルのデザインによって決まります。
- 時々、行われたスワイプが消えます。 人々は、動きがまだ終わっていないときにスワイプを始め、現在の動きが止まっている間に指を離します。 なぜなら コース中に方向を変えることはできません。ジェスチャーは無駄になります。 それはcなコードによって解決されます(まだどのコードかわかりません)。
- 弾丸によって破壊されるオブジェクトを作成するのはクールです。 彼らは接近する必要がありますが、直線ではありません。
- Ricochetは興味を追加します:狂気の弾丸嵐を手配できます。
- プレイヤーは通行人と敵を区別しません。 アートで扱われます。
プロトタイプの準備が整っているので、ゲームプレイを見せることができます!
Todo:設定、グラフィックスを決定します。
レベル1.boss [100hp]。 設定と視覚スタイルの選択
私の最初の「ボス」であることが判明したのはこの段階でした、私は本当にそうは思いませんでした。 私は次のことを計画しました:人気のあるゲーム設定のトピックでインターネットをサーフィンし、インスピレーションのための参考文献とアートを探し、ピクセルアートのレベルを描き始めます。
いくつかのグーグルの後、私はビクトリア朝のイングランドの側近に立ち寄ることに決めました。 シダ、死のカルト、悲観的なドック。 木材、金属、蒸気、油。
私は最初のスプライトを描いて問題を見つけようとします。 ゲーム内のすべてのオブジェクトは回転できます。 しかし、ご存じのとおり、ピクセルはありません。
各スプライトの360オプションのレンダリングは、明らかにオプションではありません。 幸いなことに、スプライトがその軸を中心に自由に回転する場合、モードは「不正なピクセルアート」ではありません。 この場合、必然的に現れるエイリアシング階段で何かをし、あちこちで略奪的な角のある銃口とちらつきを出す必要があります。 我慢して言うことができます:「これは私のスタイルです!」、ホットラインマイアミのクリエイターがやったように(そしてそれがやった!)。 アンチエイリアスを接続することができます:「香りの良い石鹸を長生き!」。
いずれにせよ、エイリアシングとラダー、またはアンチエイリアシング後のファジーエッジのいずれかが私に起こりました。

ピクセルアートテスト
私はピクセルアートをマークします(ごめん、友達!)そして、単純化、単純化!
Todo:適切な視覚スタイルを選択します。
レベル1.boss [75hp]。 3Dへの移行
紙の街! Wildfireの世界に少し似ていますが 、よりシンプルです。 粗い紙の高貴な白い縁、床のペンキの斑点、これらは面白い帽子のキャラクターです:

円筒形の人々
実際、私はゲーム開発で3Dを使用したことはなく、数年前に3Dエディタを最後に開きました。 しかし、私は多くが照明と影によって決定されることを知っています。 特にテクスチャが白い紙である場合、悪い光の傷を本当に隠すことはできません。
夜は最初のオブジェクトである牛乳のパッケージのモデリングに費やします。 私は標準的なシェーダー、照明を扱っています。
結論は簡単です:私は引っ張りません。 モデリングに多くの時間を費やし、標準的なツールを使用して美しい写真を撮ることはできません。 焙煎照明は役立ちますが、飛行中に焙煎するため、多くのレベルの小さなおもちゃを作りたかったのです。 ボスはまだ敗北していないようですが......

シンプルな照明付きミルクバッグ
レベル1.boss [15hp]。 手続き型生成。
私の長所と短所を思い出します。 通常、プロジェクトにアートを描画できない場合は、それを行うスクリプトを作成します。 3Dが悪いのはなぜですか? 手続き型生成です! 基本的なプリミティブは、実際には低ポリです。 ゲームプレイの違いを視覚的にエンコードする、鮮やかで対照的な色。
レベルを作成するために必要なプリミティブを決定する必要があります。 シリンダーとキューブ、おそらく五角形...うーん、1つのコードで生成できるのはそれだけです。 働くために!
Todo:単純なプリミティブ生成を実装します。
レベル2.1 2Dの正多角形
これまでのところ、レベルには十分な通常のポリゴンがあります。 最初に、2Dで試してみることにし、カメラを直交モードにして、2つの部分から要素を作成しました。
- 本体(単なる正多角形)は白です。
- 色付きのオブジェクトのタイプを示す色付きのリング(衝突時の弾丸の動作)。
すべてのポリゴンにリングの一定の半径を使用すると、これらの多彩な輪郭が得られます。

異なる厚さの輪郭
実際には、「リング」の外側部分と内側部分の側面間の距離を同じにする必要があり、側面ではなくコーナーで作業します。 多角形の角度が小さいほど、外接円と内接円の半径が大きく異なり、したがって、辺間の距離と角間の距離が大きく異なります。
$ inline $ 1-cos \ frac {\ pi} {edges} $ inline $ -問題を解決します。
角度が小さいほど、輪郭が広くなります。

同じ厚さの輪郭
少しのステンシルマジックにより、他のポリゴンの内側にリングが表示されず、このようなうさぎが得られます。

バニー
そして回転しました!
体に標準のセルラーテクスチャを追加し、色を拾い上げましたが、最終的に抵抗することができず、お気に入りの影をつなげました(既に何らかの形で書きました)。

シンプルできれい。
私はスクリーンを少女と共有し、合理的なフィードバックを得ました。高いオブジェクトから低いオブジェクトに落ちる影には歪み、よじれがあります。 私は同意します、私は常にこれを現実の世界で見ています。 私は紙に描いて、これらの歪みがどのように見えるかを理解しようとします。 そして、私は理解しています:カメラが直交している場合、どのような歪みですか?

パースペクティブカメラを使用した左影、直交を使用した右影
私の美しい影は、マップの平らな外観を強調するだけであることがわかりました。 3Dで戻ってくる時間です。
レベル2.2。 3Dの正多角形
正直なところ、3Dでの手続き生成は、私にとってまったく新しい経験です。 一方、2Dと違いはありません。
まず、特定のポリゴンの設定を決定しました。
- height-高さ ;
- エッジ - エッジの数。
- size -Vector2D。ポリゴンのサイズを設定したり、軸の1つに沿ってストレッチしたりできます。
- isCircle-これはシリンダーですか? その場合、面の数は半径に基づいて自動的に設定され、size.yはsize.xと等しくなります。
また、一般的な設定では、ゲームオブジェクトの1つのタイプで同じになります。
- 「天井」の色。
- 境界線の色;
- ボーダー幅。
- シリンダー内の面の最小数。
- シリンダーの面の数と周囲の1単位の比率。
次に、これらのポリゴンを作成します。 それぞれを3つのメッシュに分割しました。
- 本体-上底;
- ボーダー-上部ベースの色付きリング;
- 側面-側面;
低いベースを生成しても意味がありません。なぜなら、 オブジェクトをx軸またはy軸に沿って回転させることはできず、カメラは常に地図の上にあります。

このようなポリゴンを取得します
最適化の時間:
まず、特定の角度で回転する単位ベクトルを常に計算します。
1つのパブリックメソッドでAnglesCacheクラスを作成します。
namespace ObstacleGenerators { public class AnglesCache { public Vector3[] GetAngles(int sides); } }
次に、重要なパラメーター(辺の数、色、円であるかどうかなど)を使用するキーとして、3種類すべてのメッシュをキャッシュします。 色を一番上に保ちます。これにより、メッシュに1つのマテリアルを使用できるようになり、その結果、動的なバッチ処理が可能になります。
確かに、ボーダーとステンシルには問題があります。ステンシルを使用してボーダーを結合していましたが、ボリュームがあるため、このアプローチでは不十分な結果になります。

より高い円柱の境界は描画されません。 下は低いシリンダーのベースです
ステンシルバッファーの使用を停止します。 これで、すべての境界線が必ず描画されます。

ステンシルバッファーなし
最後に、ボーダーシェーダーのZTest設定をOn ( LEqual )からLessに変更します。 同じ高さのシリンダーの下部に境界線が描画されなくなりました。 その結果、高さの異なるオブジェクトで正しく機能するきちんとしたボーダーマージが得られます。

ZTestの設定による境界線のマージ
最後に、最後の仕上げ:
- 世界座標を円柱のベースのUV座標として使用します。 すべてのオブジェクトには、継ぎ目のない共通のテクスチャがあります。
- ハイライトされた境界線の色で側面をペイントします。
- _fixed3 _LightPositionを側面のシェーダーに追加し、側面を少し明るくします( Guroを着色する古典的な方法 )。 ここで、ところで、isCircleフラグはオブジェクトで役立ちました。設定されていない場合、各三角形は一意の頂点を持ち、設定されている場合、頂点は共通です。 その結果、法線は補間され、isCircleの滑らかな表面が得られます。
照明、スムージング、シェーダー、ワールドUV座標。 (明度を上げるために照明をより強くねじります)
最後の仕上げ-目的の形状のPolygonCollider2Dポリゴンを生成します。
合計:物理学ときちんとしたローポリスタイルの3次元ポリゴン。
Todo:影。
レベル3.1。 影
もちろん、今では以前の2次元の影は機能しません。

平らな影は奇妙に見える オブジェクトのボリュームを考慮しないでください
そして、次のようになります。

よりリアルな影。
「さて、問題は何ですか?」 -お願いします。 「Unity3Dには素晴らしい影があります!」
確かにあります。 シャドウマッピングアルゴリズムは、 シャドウの構築にのみ使用されます。 簡単に言うと、光源からシーンを見ると、表示されているすべてのオブジェクトが照らされており、何かで閉じられているオブジェクトは陰になっています。 光源の座標にカメラを配置し、シーンをレンダリングすることにより、シャドウマップを作成できます(光源までの距離のデータがzバッファーに表示されます)。 問題は遠近感の歪みです。 オブジェクトが光源から遠くなるほど、シャドウマップのテクセルに対応する画面ピクセルが多くなります。
つまり 影は「ピクセルパーフェクト」ではありません。これはチップではありません。さらに重要なのは、非常に高速です。 通常、テクスチャのある複雑なオブジェクトに影が重ねられるため、歪みの問題はありません。その結果、品質のわずかな低下は目立ちません。 しかし、私は非常に明るいテクスチャ、非常に少ないポリゴンを持っているので、影の低品質がはっきりと見えます。
ただし、良い解決策があります。 「 シャドウボリューム 」と呼ばれるアルゴリズムが呼び出され、以前の記事で行った2次元のシャドウに非常によく似ています。
光源から影を落とす何らかのメッシュがあるとします。
- そのシルエットの面(メッシュの照らされた部分と照らされていない部分を分離する面)を見つけます。
- 光源からそれらを「引き出します」(各面が2つの三角形に変わります)。
- これらのすべての細長い面を描画します(カラーバッファーには何も書き込まず、zバッファーからのみ読み取りますが、ステンシルに書き込みます)。
3.1。 三角形の法線がカメラ(正面)に向けられている場合:ステンシルバッファーに1つ追加します。
3.1。 三角形の法線がカメラから遠ざかる方向(後ろ)の場合:ステンシルバッファから1を引きます。
影を1回(正面の三角形を横切る)と1つ(「左」(背面の三角形を横切る))に「入れた」場合、ステンシルの値は等しくなります。 $インライン$ -1 + 1 = 0 $インライン$ ピクセルが点灯します。 影に何度も入った場合(前と前との間に、zバッファーにデータが描画され記録されたある種の三角形がある場合)-ピクセルは影にあり、明るくする必要はありません。
そのため、オブジェクトからシャドウメッシュを取得し、シェーダーを通過し、必要なデータをステンシルに追加してから、ステンシルがゼロ以外の値を持つシャドウを描画する必要があります。 シェーダーで解決するタスクのように聞こえます!
Todo:シェーダーでのシャドウ生成。
レベル3.2。 頂点シェーダーの影
ジオメトリシェーダーは使用しませんでした。GLバージョンが古いため、一部のデバイスを失いたくありません。 したがって、すべての潜在的な面は、各ポリゴンに対して事前にベイク処理する必要があります。
32角の円柱があるとします。 各面は、合計2つの三角形と4つの頂点に変わります。
合計面-側面32、2つのベースのそれぞれに32、合計96。
したがって、96 * 2 = 192個の三角形とシリンダーあたり384個の頂点。 かなりたくさん。
実際、さらに多く:最初は、側面のどれが光から影への移行(前面)であるか、およびどの側面が影から光への移行(背面)であるかを知りません。 したがって、側面ごとに2つの三角形を作成する必要はありませんが、4つ(法線の反対方向に2つ)にすると、後でカルバックまたはカルフロントを使用して必要な三角形を正しく切断できます。
したがって、32 * 4 = 128の面、256の三角形、および512の頂点。 本当にたくさん。
メッシュの作成は非常に簡単で、これに焦点を合わせません。
しかし、シェーダーは非常に好奇心が強いです。
自分の判断:すべての顔を描く必要はなく、シルエットの顔(光と影を共有する顔)だけを描く必要があります。 したがって、頂点シェーダーの各頂点が必要です。
- 前の顔の位置を見つけます。
- 現在の面と前の面を通る線のA、B、Cの値を計算します。
- ポリゴンの中心が線のどちら側にあるかを決定します。
- 光源が線のどちら側にあるかを判断します。
- 次のファセットに対して手順1〜4を繰り返します。
- 値を比較します-ライトの面の1つ(中心と光源が異なる半平面にある場合)とシャドウのもう1つ-この頂点はシルエットであり、描画する必要があります。
- 現在の頂点を拡張する必要があるかどうか、またはそれが円柱自体にあるかどうかを調べます。
- ストレッチする必要がある場合は、ワールド座標で頂点の位置を見つけ、光源から方向を取得し、この方向に特定の距離(たとえば、100単位)に移動します。
これらのすべての計算では、大量のデータを上部に保存する必要があります。
前と次の頂点への座標(またはオフセット)、フラグ-現在の頂点をオフセットするかどうか。
働くことを想像してください!
ただし、このシャドウ作成方法には致命的な欠陥が多く含まれているため、悲しくなります。
- 膨大な数の頂点と三角形。 ほとんどの場合、影は2つの側面で構成されます。半分は下面、半分は上面です。 32倍の石炭シリンダーと無限遠の光源の場合、 $インライン$ 16 * 4 = 64 $インライン$ ポイントと $インライン$(16 * 2 + 2)* 2 = 68 $インライン$ 三角形。 代わりに、256個の三角形と512個の頂点をビデオカードに与えます。
- バッチ処理は機能しません。 頂点の影を計算するには、隣接する頂点(位置と法線)に関する情報を何らかの方法で保存する必要があります。 したがって、データはメッシュ空間のローカル座標に関連付けられます。 バッチ処理を行うと、多くのメッシュが結合され(座標系がワールドメッシュに変更されます)、頂点には隣接ポイントの位置に関する情報がなくなります。
多数のピークが選択した方法の不快な結果であることが判明しましたが、壊れたバッチ処理が最後の釘に当たりました。モバイルデバイスのシャドウに対する100〜200の呼び出し呼び出しは受け入れられない結果です。 どうやら、シャドウの計算をCPUに変換する必要があります。 しかし、それは見かけほど悪いですか? :)
Todo: CPU.
Level 3.2. cpu
.
:
1.1。 ;
1.2。 ;
1.3。 ;
1.4。 , — , (lightToShadowIndex);
1.5.1 , , (shadowToLightIndex);
:
2.1。 lightToShadowIndex shadowToLightIndex 2 (, 4-, 2 , 2 — , );
:
3.1 shadowToLightIndex lightToShadowIndex 2 ;
- :
4.1 shadowToLightIndex lightToShadowIndex;
:
, ( ).
: , .
, . , 60fps 10 , 600 . ( — 6 10 ).
Todo: , 60fps nexus 5.
Level 3.3.
:
— , . , .
:
. AnglesCache, . :
using UnityEngine; using System.Collections.Generic; namespace ObstacleGenerators { public class AnglesCache { List<Vector2[]> cache; const int MAX_CACHE_SIZE = 100; public AnglesCache () { cache = new List<Vector2[]>(MAX_CACHE_SIZE); for (int i = 0; i < MAX_CACHE_SIZE; ++i) cache.Add(null); } public Vector2[] GetAngles(int sides) { if (sides < 0) return null; if (sides > MAX_CACHE_SIZE) return GenerateAngles(sides); if (cache[sides] == null) cache[sides] = GenerateAngles(sides); return cache[sides]; } public float AngleOffset { get { return Mathf.PI * 0.25f; } } Vector2[] GenerateAngles(int sides) { var result = new Vector2[sides]; float deltaAngle = 360.0f / sides; float firstAngle = AngleOffset; var matrix = Matrix4x4.TRS(Vector2.zero, Quaternion.Euler(0, 0, deltaAngle), Vector2.one); var direction = new Vector2(Mathf.Cos(firstAngle), Mathf.Sin(firstAngle)); for (int i = 0; i < sides; ++i) { result[i] = direction; direction = matrix.MultiplyPoint3x4(direction); } return result; } } }
:
( ). , c .
:
Transform.TransformPoint transform.localToWorldMatrix MultiplyPoint3x4.
Vector3 Vector2 ( , ), , :
Vector2 v2; Vector3 v3; // , v2.x = v3.x; v2.y = v3.y; // v2.Set(v3.x, v3.y); // , v2 = v3;
, , , .
:
. , , :
- (size.x == size.y);
- .
, — . — .
:
, : size.x == size.y ;
- :
direction = lightPosition - obstacleCenter;
- LCT (L — , C — , T — , L) LC, CT (). deltaAngle — LC CT;
- directionAngle — ( OX) LC;
- firstAngle , secondAngle — LT:
firstAngle = directionAngle - deltaAngle; secondAngle = directionAngle + deltaAngle;
- , — 360° / edges, — z:
fromLightToShadow = Mathf.FloorToInt(firstAngle / pi2 * edges + edges) % edges; fromShadowToLight = Mathf.FloorToInt(secondAngle / pi2 * edges + edges) % edges;
- , . , firstAngle : , . , , (fromLightToShadow fromShadowToLight), :
if (linesCache[fromLightToShadow].HalfPlainSign(lightPosition) < 0) fromLightToShadow = (fromLightToShadow + 1) % edges; if (linesCache[fromShadowToLight].HalfPlainSign(lightPosition) >= 0) fromShadowToLight = (fromShadowToLight + 1) % edges;
. — (Acos, Atan2) . — . , . , :
.
bool CanUseFastSilhouette(Vector2 lightPosition) { if (size.x != size.y || edgesList != null) return false; return (lightPosition - (Vector2)transform.position).sqrMagnitude > size.x * size.x; } bool FindSilhouetteEdges(Vector2 lightPosition, Vector3[] angles, out int fromLightToShadow, out int fromShadowToLight) { if (CanUseFastSilhouette(lightPosition)) return FindSilhouetteEdgesFast(lightPosition, angles, out fromLightToShadow, out fromShadowToLight); return FindSilhouetteEdges(lightPosition, out fromLightToShadow, out fromShadowToLight); } bool FindSilhouetteEdgesFast(Vector2 lightPosition, Vector3[] angles, out int fromLightToShadow, out int fromShadowToLight) { Vector2 center = transform.position; float radius = size.x; Vector2 delta = center - lightPosition; float deltaMagnitude = delta.magnitude; float sin = radius / deltaMagnitude; Vector2 direction = delta / deltaMagnitude; float pi2 = Mathf.PI * 2.0f; float directionAngle = Mathf.Atan2(-direction.y, -direction.x) - anglesCache.AngleOffset - transform.rotation.eulerAngles.z * Mathf.Deg2Rad; float deltaAngle = Mathf.Acos(sin); float firstAngle = ((directionAngle - deltaAngle) % pi2 + pi2) % pi2; float secondAngle = ((directionAngle + deltaAngle) % pi2 + pi2) % pi2; fromLightToShadow = Mathf.RoundToInt(firstAngle / pi2 * edges - 1 + edges) % edges; fromShadowToLight = Mathf.RoundToInt(secondAngle / pi2 * edges - 1 + edges) % edges; return true; }
:
cpu, . , , 32 , , 42 36 ( 512 256 gpu).
:
, . — "" . , .
:
x y ( ) — c , .
bounding box:
Mesh.RecalculateBounds — . AABB .
:
- 4 : circleCenter — lightPosition, ;
- 4 — 4-, z ( 8 — , );
- .
- ( , z == 0)
- 16 — , ;
- AABB .
, , .

( )

( )

Bounding box ( )
, , .
おわりに
, , . )
, , , . , , :
- ;
- ;
- .
:
- , , ;
- — ;
- 3 — , ;
- — .
- (, Gizmos), .
, ! )