今週のお題「space」 | Unity 1週間ゲームジャム #unity1week https://t.co/2uhr9rdqDr
お題でました!今回もよろしくお願いしますー!— naichi (@naichilab) November 12, 2017
Unity1Week GameJam お題「Space」に、「FIND SPACE」を応募しました。
ところが本業の仕事が忙しく、お題が発表された日曜夜から金曜日までは全く時間が取れず・・・。「Space」のテーマに合うゲームを寝る前に考えようとしてそのまま寝る、みたいな日々が続きました。
「Space」というと真っ先に宇宙が思い浮かびますが、今回は「空間」「領域」のイメージで考えて、シンプルにUnityの2Dで作ることにしました。
初めはテトリスのように、空いているスペースにぴったりのブロックを入れるような落下パズルを考えていたのですが、試行錯誤の上、ブロックをぴったりのスペースにドラッグ&ドロップする、という内容に落ち着きました。
金曜の夜にUnityプロジェクトを作り、土曜日に基本骨子を作り、日曜日にxRTechTokyoに参加しながら調整とWebGLへの書き出しを行い、帰宅後の締め切りの20時の少し手前でアップロード完了、というドタバタスケジュールでしたが何とか応募に間に合いました。
以下、メモも兼ねてポイントを記載します。
お題または選択肢となるブロックですが、1辺のブロックの数blockSize(可変にしていましたが今回は3で固定)を使って、下記のメソッドで生成を行っています。
ブロックの部品をparentObjectの子オブジェクトとしてまとめると同時に、配置の情報をintの二次元配列に格納しています。これは、ゲームの制約上、作成した選択肢は必ずユニークでなければならないため、比較をしやすくするためです。また、3×3すべてが空白のブロックは使いたくなかったため、isEmptyというチェック用メソッドを用意して、do~whileでチェックを通過するまでループする構造になっています。
GameObject createBlock(string name,out int[,] retMap) { int[,] map = new int[blockSize,blockSize]; GameObject parentObject = new GameObject (name); parentObject.transform.localScale = new Vector3 (blockSize, blockSize, 0); do { //ループした場合に前にInstantiateしたオブジェクトを一旦消去する foreach(Transform tsf in parentObject.transform) { Destroy(tsf.gameObject); } for (int i = 0; i < blockSize; i++) { for (int j = 0; j < blockSize; j++) { int rand = Random.Range (0, 2); if (rand == 0) { GameObject t = (GameObject)Instantiate (tile, new Vector2 (i, j), Quaternion.identity); t.transform.SetParent (parentObject.transform); map [i, j] = 1; } else { GameObject t = (GameObject)Instantiate (blackTilePrefab, new Vector2 (i, j), Quaternion.identity); t.transform.SetParent (parentObject.transform); map [i, j] = 0; } } } } while(isEmptyMap (map)); float offset = (float)(blockSize - 1) / (blockSize * 2f); //がんばった計算式 parentObject.AddComponent<boxCollider2D> (); parentObject.GetComponent<boxCollider2D> ().offset = new Vector2 (offset, offset); retMap = map; return parentObject; }
しかし、ここで一番苦労したのは、BoxCollider2Dのサイズと位置の制御についてです。
親オブジェクトにBoxCollier2Dをアタッチして、そのサイズを子オブジェクトの3×3の領域と全く同じにするのにいろいろ考えて、上記の計算式の結果のoffsetをBoxCollider2Dにセットすることにより位置合わせを行いました。
また、単純ですが見落としがちなこととして、Collider2D同士のあたり判定を行う場合には、少なくとも片方にRigidBody2Dがアタッチされていなければなりません。
小さいオブジェクトなら、マウスカーソルの位置をワールド座標に変換して、オブジェクトのpositionにすればドラッグでの移動は簡単に作れるのですが、今回のように大きいオブジェクトだと、オブジェクトのどこをクリックしてドラッグを開始したのかを考えないと、オブジェクトがワープしてしまいます。(オブジェクトの中心点がカーソルの位置になってしまうため)
なので、オブジェクトをクリックした位置と、オブジェクトの中心点の差分のベクトルを保存しておき、移動の際にその値を加算しました。
Vector3 diff = Vector3.zero; void OnMouseDown() { Vector3 clickPointScreen = Input.mousePosition; Vector3 clickPointWorld = Camera.main.ScreenToWorldPoint (clickPointScreen); diff = gameObject.transform.position - clickPointWorld; } void OnMouseDrag() { Vector3 mousePointScreen = Input.mousePosition; Vector3 targetPointWorld = Camera.main.ScreenToWorldPoint (mousePointScreen); targetPointWorld = targetPointWorld + diff; targetPointWorld.z = 0; this.transform.position = targetPointWorld; }
お題のブロックを選択肢のブロックにドラッグ&ドロップした際に、正しいブロックなのか間違いのブロックなのか判定する必要があります。判定はColliderを使用するのですが、ドロップ=OnMouseUpした瞬間にどのColliderなのかを判定することはできなさそうです(OnTriggerやOnStayなどのイベントで制御されているため)
なので、ドラッグされてOnTriggerStay2Dが発生しているときにColliderで判別してグローバル変数に入れておき、OnMouseUpが発生したときにその値を使用して判定を行っています。
enum CollisionStatus { None = -1, Target, Other } CollisionStatus colStatus; void OnTriggerStay2D(Collider2D col){ if (GameManager.Instance.targetBlockName == col.gameObject.name) { colStatus = CollisionStatus.Target; } else { colStatus = CollisionStatus.Other; } } void OnMouseUp() { if (colStatus == CollisionStatus.Target) { Debug.Log ("Collect!!"); GameManager.Instance.OnBlockDrop (true); } else if (colStatus == CollisionStatus.Other) { Debug.Log ("Miss!!"); GameManager.Instance.OnBlockDrop (false); } }
まず、WebGLへの書き出しはかなり時間がかかります。途中でプログレスバーが進まなくなってフリーズしたかと思うほどなのですが、気長に数分以上は待たないとだめそうです。@naichilabさんに教えてもらって助かりました。
そして書き出したhtmlで確認すると、画面の比率が大幅に違っていて焦りましたが、画面のサイズはPlayer Settings > Default Screen Height/Width で設定できます。これはhtml側の設定の話となります。
また、UnityRoomにアップロードする際にもHeight/Widthを設定できますので、そこで正しい値を入れれば基本的には問題なく動くはずです。
短い開発期間だったので、AssetStoreの素材を使わせていただきました。(いずれも無料Asset)
企画してくださっている@naichlabさん、ありがとうございます!
Unityを勉強して使えるようになってきても、1つの作品として完成・公開するには相当なパワーが必要になります。コアのゲーム部分だけを作るのは楽しいですが、例えばステージクリア処理、ゲームオーバー処理、ハイスコアの記録、ゲームバランスの調整、デバッグなど、ゲームのコア部分以外の作業が必ず必要になってきます。
Unity1Week GameJamは、期限を1週間に制限することにより、1つのゲームとして完成させるためのリソースの割り当て方を嫌が応にも勉強させてくれます。自作にこだわらないでAssetStoreを使ったり、本当ならもっと綺麗に書きたいプログラムをひとまずは動かす方法で、という割り切りができたり、人により様々だと思いますが、リソースを有効に活用する(せざるを得ない)経験というのはとても重要だと思います。
さて、次回は丸々一週間使えるように本業を終わらせておこう・・・。