イカで星を塗るゲームの実装を雰囲気だけ書く
この記事はKMCアドベントカレンダー17日目の記事です。大遅刻しました。本当に申し訳ございません。
昨日の記事は __kypu さんの「世界ダブステップ史」の予定です。
自己紹介
どうも。KMC-ID: tenといいます。 つい1か月前まで代表という役職をしていましたが、ついに任期満了しました。 仕事ないっていいな~。 3回生ですが、後期から休学しており、よくわからない存在です。
概要
KMCでは、NFでゲーム等を展示しています。 私は今年のNFで「宇宙の黒はイカスミの黒」というゲームを制作し、展示しました。 冬コミにも出すよ!ぜひKMCのブースに来てね!
このゲームはUnityで制作しました。 このゲームの中には、「イカが墨を吐いて星を塗りつぶす」というアクションがあるのですが、 その箇所の実装がいろいろと面倒だったので、この記事ではそのことについて今回は書きます。
この実装は、Unity上での「多角形同士の差を取る」「Polygon Collider 2dによる画像の切り取り」が含まれます。 また、この記事はUnityとかC#とかの知識を結構前提としている気がします。
あといろいろあって、雰囲気だけで書くので、あんまりわからないと思います。はい。
どんなゲームなの
この「宇宙の黒はイカスミの黒」は、 主人公であるイカを操作して、星を避けつつ塗りつぶすゲームです。
イカスミを吐いて星を黒く塗りつぶすことでのみスコアを獲得できます。
しかし、星に当たるとゲームオーバーであり、塗りつぶした星にも当たり判定があります。
すなわち、覚えられる範囲で星を塗りつぶし、星を避けていくゲームです。
実装方針とか
実装すべき事の整理
さて、このゲームで実装すべきことを整理しましょう。
まず重要なものは「墨」と「星」です。 墨と星とが当たった際、墨の当たり判定がある分だけ、星が削られていきます。 星の削られた部分は、黒くなります。
実装方針
まず、墨は多角形です。 星は最初は円ですが、後々多角形に変換したほうが都合がいいので多角形とします。 Unityで多角形の当たり判定といえば、Polygon Collider 2dですね。今回の実装で大活躍します。
墨と星の衝突判定があったときに、墨と星それぞれのPolygon Collider 2dのpathを取得します。 そのpathの形状をもとに、多角形と多角形の差分を取り、新たな星のPolygon Collider 2dのpathを導出します。
画像の表示を変える部分は、星の画像の切り取りさえ出来れば、星のオブジェクトの下に黒い円の画像を置けば済むのでよいです。ちょっとOrder in Layerを頑張る必要があるけど。 問題は星の画像を切り取る部分で、普段Unity 2DではSprite Rendererを使う(と思う)のですが、その情報からMeshを生成し、UVとかをゴニョゴニョして切り取ります。この辺よくわかってない。
実装
では実装していきましょう。
GameObjectを準備する
まずはGameObjectを。
星の GameObject を用意
星のPrefabはこんな感じの階層構造になっています。 星の画像部分(Graphic)と、下の黒い円(Atari)は同じLayerに来ます。 星の生成時にOrder In Layerを、星の画像部分はn+1、下の黒い円はnとします。 次の星を生成する時は、n+3とn+2にする......という感じにすると、いい感じになります。
星の画像部分と黒い円の両方にCircle Collider 2dをアタッチします。 星の画像は画像の切り取りの判定に使い、黒い円はイカ本体との当たり判定(ゲームオーバー判定)に使います。
墨の GameObject を用意
Polygon Collider 2dをいい感じに設定するだけ。
Polygon Collider 2d を切り取る
星のPolygon Collider 2dから墨のPolygon Collider 2dを切り取る部分を実装します。
Collider 2dをワールド座標のpathに変換
Polygon Collider 2dにはGetPath(int i)
という関数があり、pathを取得できます。
ここで取得できるpathはワールド座標ではなくGameObject基準の座標なので、それをワールド座標に変換します。
変換するには、Polygon Collider 2dがアタッチされているGameObjectのTransformのTransformPoint(Vector3 position)
という関数を、pathの各点に適用すればよいです。
多角形を切り取る
墨と星をワールド座標でのpathで表すことができたので、星のpathを切り取ります。 ここで、多角形と多角形の差分の実装はとても面倒なので、ライブラリに頼ります。
Clipper - an open source freeware polygon clipping library
すごい。
しかし、このアルゴリズムはよく知らないけど、longで動いているらしい。 pathは当然floatで表されているので、float<->longの変換が必要になります。なんで。なんで。
floatとlongの変換をする
これ、単にfloatとlongをキャストするだけじゃダメなんですよね。 Unityのワールド座標で使われる数なんで、まあ-10.0~+10.0くらいで、小数点以下がすごい響く世界なんで、普通にlongに変換してはいけない。
単純にfloatでの値を1万倍くらいして、それをlongに変換する、っていう方法が1つあります。
今回の私の実装では、
- 墨と星のpathの中で、x,y座標がそれぞれ最大、最小であるものを覚えておく
- 最小のものをlongで-20000、最大のものをlongで+20000として、pathの座標を変換する
- Clipperに投げて変換後のpathを取得する
- さっきのやつの逆変換をする
変換はInverseLerpとかを使ってできますし、逆変換もそういう感じでできます。
この実装は、小さい範囲で変換するときに、より高い精度で変換できるという意図があります。 本当にこれでいいかは知らん。っていうかfloatでのライブラリ欲しい。
RevertTransformする
切り取られたあとの星のpathが取得できたので、これを新たな星のPolygon Collider 2dとします。
このpathはワールド座標なので、星の座標に変換し直します。
これは、TransformにはInverseTransformPoint(Vector3 position)
という、さっきの関数の逆関数があるので、それを用います。
星の画像を切り取る
SpriteRenderer を MeshRenderer に変換
Unity 2Dで普段画像を出すときはSprite Rendererを用いると思います。 しかしこれは、画像の切り取りをするのには向いていないか不可能です。 なので、Mesh Rendererという、別の画像を表示するものに変換しておきます。
Meshの話をよく知らないので、雰囲気だけで書きますね。
Mesh何もわからない人に説明するには、この記事とかがよさそうだった。
Polygon Collider 2dの形状をMeshに適用する
元の Polygon Collider 2d、元の Mesh、新しい Polygon Collider 2d を用いて、新しいMeshを作成します。
元のMeshとuvの形状が一致することから、uv座標の(0,0),(1,1)にあたる場所の、verticesでの座標を出します。 こういうコードができます。
var oldVertices = oldMesh.vertices; var olduv = oldMesh.uv; if (oldMesh.vertices.Length == 0) { return new Mesh (); } Vector2 oldVerticesMax = oldVertices[0]; Vector2 oldVerticesMin = oldVertices[0]; Vector2 olduvMax = olduv[0]; Vector2 olduvMin = olduv[0]; for (int i = 0; i < oldVertices.Length; i++) { oldVerticesMax = Vector2.Max (oldVerticesMax, oldVertices[i]); oldVerticesMin = Vector2.Min (oldVerticesMin, oldVertices[i]); olduvMax = Vector2.Max (olduvMax, olduv[i]); olduvMin = Vector2.Min (olduvMin, olduv[i]); } var one = SolveFormula (olduvMin, oldVerticesMin, olduvMax, oldVerticesMax, Vector2.one); var zero = SolveFormula (olduvMin, oldVerticesMin, olduvMax, oldVerticesMax, Vector2.zero);
/* f(a)=A f(b)=B f(x)=cx+D Ca+D=A Cb+D=B C(a-b)=A-B C+D/a=A/a C+D/b=B/b D/a-D/b=A/a-B/b Db-Da=Ab-Ba D(b-a)=Ab-Ba D(a-b)=Ba-Ab */ static Vector2 SolveFormula (Vector2 paramA, Vector2 valueA, Vector2 paramB, Vector2 valueB, Vector2 param) { var C = (valueA - valueB) / (paramA - paramB); var D = (valueB * paramA - valueA * paramB) / (paramA - paramB); return param * C + D; }
新しいPolygon Collider 2dから、新しいMeshを生成します。
Meshにはtrianglesという変数があります。三角形配列っていうらしい。 verticesの中の3頂点を繋いで三角形を作って、いくつかの三角形でMeshの内部を埋める、みたいなことしなきゃいけないらしい。 それをやってくれるコードがありまして、Triangulatorといいます。これに投げればよい。やったね。
Triangulator - Unify Community Wiki
var paths = new List<Vector2[]>(); for (int i = 0; i < polygonCollider.pathCount; i++) { paths .Add (polygonCollider.GetPath (i)); } if (paths.Count == 0) { return new Mesh (); } Vector2[] vertices2D = new Vector2[0]; int[] allIndicies = new int[0]; foreach (var path in paths) { int n = vertices2D.Length; if (path.Length != 0) { vertices2D = vertices2D.Concat (path).ToArray (); } Triangulator tr = new Triangulator (path); int[] indices = tr.Triangulate (); var ind = indices.Select (x => x + n); allIndicies = allIndicies.Concat (ind).ToArray (); }
Meshのverticesとuvを作ります。 uvの作成時に、先ほど出したoneとかzeroが登場します。
Vector3[] vertices = new Vector3[vertices2D.Length]; for (int i = 0; i < vertices.Length; i++) { vertices[i] = new Vector3 (vertices2D[i].x, vertices2D[i].y, 0); } Vector2[] uv = vertices2D.Select ( vec => new Vector2 (Mathf.InverseLerp (zero.x, one.x, vec.x), Mathf.InverseLerp (zero.y, one.y, vec.y)) ).ToArray (); Mesh msh = new Mesh (); msh.vertices = vertices; msh.triangles = allIndicies; msh.uv = uv; msh.RecalculateNormals (); msh.RecalculateBounds (); return msh;
これでMeshも生成できたので、星の切り取りができました。めでたしめでたし。
あとがき
勢いで実装したけど、UnityのMesh知らねーよ!って気分になりながら実装してた。 すでに存在してそうだけど、Unity用の幾何ライブラリ自前で実装したい気分になったな~。 自分でまとめて実装したほうが使いやすいと思うんだよねこういうの。
ここに宣伝が入る
KMCはコミックマーケット95(冬コミ)に出展します。 ゲーム・音楽CDと既刊の部誌を頒布しているはずです。 ぜひ2日目(日)は東タ31aにお越しください。
◎あなたのサークル「京大マイコンクラブ」は、日曜日 東地区“タ”ブロック-31a に配置されました。
— 京大マイコンクラブ (@KMC_JP) November 2, 2018
また、KMCにはいつでもどなたでも入部できます。 ゲーム制作したいぜ!って人は大歓迎です。 私とあれこれしましょう。
明日の記事は ikubaku さんの「Unity 2018.3の新機能」でした。