1月社内ゲームジャムの成果物「Bound×Boundary」
社内ゲームジャム成果物「Bound×Boundary」
概要
- GGJ(グローバルゲームジャム)に参加したかったのだけど金曜日に外せない用事があって代わりに土日で社内ゲームジャムをやった
- 今回の成果物は2人プレイの引っ張りゲー
- RITUAL(GGJテーマ)要素は何も無い…. でもコレは儀式だと言い張るよ!
ゲーム説明
スクリーンショット
GGJ出れなかった代わりに社内でゲームジャムやりました! 来年こそGGJ出たいぞい (作ったのは軌跡が障害物になる2人プレイ引っ張りゲー) #ggj16jp #mfgamejam pic.twitter.com/dI8PqLo4Nx
— 青木とと(ˊᗜˋ*) (@lycoris102) 2016年1月31日
資料
bound×boundary from lycoris102
www.slideshare.net
システム
- 2人で遊ぶ
- プレイヤーは円を引っ張って離すことで円を動かすことが出来る
- 円は引っ張った距離に応じて勢いが増す
- 円は移動際に軌跡を描く
- 停止したらプレイヤー交代する
- 片方のプレイヤーはもう片方のプレイヤーの軌跡が壁のような扱いになる
目的
- 正直、このゲームの「目的」は残念ながらブレブレ
- (現状で特にスコアやゴールを設けていない)
- 最初は資料にもある通り、協力ゲーを目指していた
- 先に進む/ライフを維持する/味方を妨害しないのジレンマをテクニックで解決する
- 最終的にはPvPのゲームにして「相手を妨害する」「自分のスコアを稼ぐ」を両立させるのが良いかなとか考えている
実装について
UnityにおけるXZ平面の2Dゲーム
- 俯瞰型のボードゲームやタワーディフェンス等のゲームは奥行きが地面/床である(XZ平面)
- それに対してUnityにおける「2D」の基本的な定義はXY平面を指す
- すなわち画面の下部に対して重力が働く一般的な横スクロールアクションゲームのイメージ
- なので跳ね返り等の物理判定を入れるためにRigidbody2Dをアタッチすると画面下部に落ちて行く
- 主な対応方法として、3D実装/Rigidbodyを使用しつつ、カメラ向きを変更する方法がメジャーな認識
- 今回は少し変わったアプローチとして「Rigidbody2Dを使用しつつ、重力を0にしつつ、一部の物理挙動をコードでカバーする」というものを取った
- 今思えば、3Dで実装すれば良かったと後悔している….
Unityにおける重力量
- “Edit” -> “ProjectSettings” -> “Physics2D” を開く
- 上部にある “Gravity” の項目で重力加速度を設定出来る
- デフォルトでy軸方向に対して “-9.81” が指定されている
- 月面のゲームを作りたければ意図的にここの値を下げても良い
- もしくはアタッチした Rigidbody2D の “GravityScale” を調整すると良いだろう
- デフォルトでy軸方向に対して “-9.81” が指定されている
- 今回は (何を思ったのか) Gravity の指定を (0, 0) にした
- つまり無重力となり、主に以下のような現象が起こる
- 摩擦が働かずに減速しない
- 物質にぶつかっても反発しない
- つまり無重力となり、主に以下のような現象が起こる
- すなわち、これらの現象を自前で用意する必要がある
- この時点で諦めておけば良かったのだが、とある事情により突き進むことに
- 以下のようなコードをプレイヤーにアタッチすることで減速/反発させるようにした
- XXX 反発のコードが雑すぎて情けないけど自戒の意を込めて晒す (反射ベクトル使おう…)
IEnumerator Deceleration() { while (true) { if (this.GetComponent<Rigidbody2D>().IsSleeping() == false) { this.GetComponent<Rigidbody2D>().velocity = new Vector2( this.GetComponent<Rigidbody2D>().velocity.x * friction, this.GetComponent<Rigidbody2D>().velocity.y * friction ); } yield return null; } } void OnCollisionEnter2D(Collision2D coll) { if (this.GetComponent<Rigidbody2D>().IsSleeping() == false) { for (int i = 0; i < coll.contacts.Length; i++) { Bound(coll.contacts[i].point); } } } // XXX 反射ベクトルを使おう void Bound(Vector2 hitPoint) { if (System.Math.Abs(hitPoint.x - this.transform.localPosition.x) < 0.1f) { this.GetComponent<Rigidbody2D>().velocity = new Vector2( this.GetComponent<Rigidbody2D>().velocity.x, this.GetComponent<Rigidbody2D>().velocity.y * -1 ); } if (System.Math.Abs(hitPoint.y - this.transform.localPosition.y) < 0.1f) { this.GetComponent<Rigidbody2D>().velocity = new Vector2( this.GetComponent<Rigidbody2D>().velocity.x * -1, this.GetComponent<Rigidbody2D>().velocity.y ); } }
追記(20160211)
- 反射ベクトル、すごい簡単に求められます!
- collison から法線が取得可能
- veclocityと法線を
Vector2.Refrect
に通せば反射ベクトルが取得可能
void OnCollisionEnter2D(Collision2D coll) { if (this.GetComponent<Rigidbody2D>().IsSleeping() == false) { for (int i = 0; i < coll.contacts.Length; i++) { Vector2 refrectVec = Vector2.Reflect(this.GetComponent<Rigidbody2D>().velocity, coll.contacts[0].normal); this.GetComponent<Rigidbody2D>().velocity = refrectVec; } } }
軌跡の描画
- Vectrosityというアセットを使用した
- スクリプト経由で線や円、ベジェ曲線を描画することができるアセット
- 基本的な使い方は以下のような感じ
// 線として結びつけたい座標の配列を作成 List<Vector2> linePoints = new List<Vector2>() { this.transform.localPosition, targetPosition }; // 第一引数にHierarchyに表示されるName // 第二引数に座標 // 第三引数にテクスチャ // 第四引数に線の大きさ(太さ) // 第五引数に線の連続性の指定 (未指定可) VectorLine line new VectorLine("LineName", linePoints, texture, 1.0f, LineType.Continuous); line.Draw();
line.collider = true;
するだけで描画した線に対してColliderを生成することが出来る- (がuGUIを使用し始めたバージョンからWorld座標/Screen座標を考慮する必要がありそのまま使用するのはもしかしたら少し面倒かもしれない…)
- 今回は線とコライダの位置がずれてしまったので自前でコライダを別途用意することにした
- (コライダ機能を使いたくてRigidbody2D貫き通したのに、完全に本末転倒な感じだった)
軌跡に対する跳ね返りの実装
- EdgeCollider2D (指定した座標間に対してコライダを生成) をスクリプト側で操作する
- 衝突したときに座標を追加し、静止時に有効にしてあげる
using UnityEngine; using System.Collections; using System.Collections.Generic; public class PlayerLocusLineCollider : MonoBehaviour { [SerializeField] EdgeCollider2D lineCollider; private List<Vector2> lineColliderPoints = new List<Vector2>(); public void Set(Vector2 position) { lineColliderPoints.Add(position); } public void UpdateCollider() { lineCollider.enabled = true; lineCollider.points = lineColliderPoints.ToArray(); } public void Reset() { lineColliderPoints = new List<Vector2>(); lineCollider.enabled = false; } void OnCollisionEnter2D(Collision2D coll) { Set(this.transform.localPosition); } }
iTween
- アニメーションや演出をスクリプトから実装する上での言わずと知れた便利アセット
- 何気に使用するのは始めて
- ブロックが消える時のフェードアウトの演出実装に使用した
using UnityEngine; using System.Collections; public class StageWall : MonoBehaviour { void OnCollisionEnter2D(Collision2D coll) { if (coll.gameObject.tag == "Player") { this.GetComponent<BoxCollider2D>().enabled = false; iTween.ValueTo(gameObject, iTween.Hash( "from", 1.0f, "to", 0.0f, "time", 0.5f, "onupdate", "Fadeout", "oncomplete", "Delte" )); } } void Delete() { Destroy(this.gameObject); } void Fadeout(float alpha) { Color currentColor = this.GetComponent<SpriteRenderer>().color; this.GetComponent<SpriteRenderer>().color = new Color( currentColor.r, currentColor.g, currentColor.b, alpha ); } }
スクリプト構成
- 今までは割と一つのクラスにメソッドを詰め込みがちだったが役割に応じてスクリプトを分割した
- 可読性が上がって良い
- ちなみにきっかけはテラシュールブログさんの以下の記事
- UnityのサンプルプロジェクトTank!がrole毎にスクリプトを分割していて独立して動くため、流用性があって良いという話
// 今までは Player.cs に全ての挙動を詰め込みがちだった ├── Player │ ├── Player.cs // ステータス管理・イベント発火 │ ├── PlayerAuxiliaryLine.cs // 引っ張る時の補助線描画 │ ├── PlayerLocusLine.cs // 移動時の軌跡の描画 │ ├── PlayerLocusLineCollider.cs // 軌跡のコライダの管理 │ └── PlayerMove.cs // 移動