まだまだUnityを触り始めて日が浅いですがunityroomさんの1週間ゲームジャムに参加させて頂いたので、今回はその制作過程をメモがてらに振り返ってみようと思います。
Unity 1週間ゲームジャム | 無料ゲーム投稿サイト unityroom - Unityのゲームをアップロードして公開しよう
ブログとかろくに書いたことないので見づらかったらすいません。
制作したゲーム「テクノフライト」のURLはこちらです
テクノフライト -Techno Flight- | 無料ゲーム投稿サイト unityroom - Unityのゲームをアップロードして公開しよう
水曜日
twitterでUnity1週間ゲームジャムが始まっているのを知りました。正直初めは「あちゃー、もう始まっちゃってるのか。いまからだと完成するかわからないしECSも覚えたいし」とかなり参加に後ろ向きだった気がします。
でも誰かが言ってた「やらない理由を見つけるのは簡単」という言葉を思い出して、とりあえずアイディア出しから始めました。
アイディア出し
血液をギリギリまで抜くゲーム
初めに思いついたのが限界ギリギリまで血液を抜いて最後にタバコを投げ入れるというゲームでした。(元ネタがわからない人は福本先生の『アカギ』を見よう!)ボツにした理由は後述します。
風船をギリギリまで膨らませるゲーム
次に思いついたのが風船をギリギリまで膨らませるゲーム。バラエティとかで実際にあるゲームですね。
この2つはすぐにボツにしたわけではなくて、「絵を自分で描かないならそこまで時間がかからなさそう」という理由で保留にしました。
鳥を使おう
そのあと、ECSの勉強用に使おうと思ってダウンロードしていた鳥。これを何かに使えないだろうかと考えたのが今回の「テクノフライト」です。
シェーダー制作
鳥を動かすアニメーションはモデルと一緒に付いてたんですが、前からやろうと思ってた「頂点シェーダーで羽ばたき表現」を作るいい機会だと思って時間もないのにシェーダーを作り始めました。
新しいスタンダードサーフェイスシェーダーを作り、必要なパラメーターと頂点シェーダーを追加しました。(汎用性はあまりありません)
Shader "Custom/Crow" {
Properties {
_Color ("Color", Color) = (1,1,1,1)
_MainTex ("Albedo (RGB)", 2D) = "white" {}
_Glossiness ("Smoothness", Range(0,1)) = 0.5
_Metallic ("Metallic", Range(0,1)) = 0.0
//追加プロパティ
_BodySize("BodySize", float) = 0.0
_FlapSpeed ("FlapSpeed", float) = 0.0
_FlapOffset("FlapOffset", float) = 0.0
_FlapScaleX("FlapScaleX", float) = 0.0
_FlapScaleY("FlapScaleY", float) = 0.0
_FlapAngle("FlapAngle", float) = 0.0
}
SubShader {
Tags { "RenderType"="Opaque" }
LOD 200
CGPROGRAM
// 頂点シェーダを追加
// Use shader model 3.0 target, to get nicer looking lighting
//GPUインスタンシング可能
sampler2D _MainTex;
struct Input {
float2 uv_MainTex;
};
half _Glossiness;
half _Metallic;
//追加のプロパティ
half _BodySize;
half _FlapScaleX;
half _FlapScaleY;
half _FlapAngle;
//インスタンスごとのプロパティ
UNITY_INSTANCING_BUFFER_START(Props)
UNITY_DEFINE_INSTANCED_PROP(fixed4, _Color)
UNITY_DEFINE_INSTANCED_PROP(half, _FlapSpeed)
UNITY_DEFINE_INSTANCED_PROP(half, _FlapOffset)
UNITY_INSTANCING_BUFFER_END(Props)
//頂点シェーダ
void vert(inout appdata_full v)
{
//flap:羽ばたき
float flap = abs(v.vertex.x) * (sin(_Time.y * UNITY_ACCESS_INSTANCED_PROP(Props, _FlapSpeed) + UNITY_ACCESS_INSTANCED_PROP(Props, _FlapOffset)));
//体の部分は動かさない
flap *= step(_BodySize/100, abs(v.vertex.x));
//足元は動かさない(多少強引)
flap *= step(0.0, v.vertex.y);
v.vertex.x += (v.vertex.x < 0)? flap*flap * _FlapScaleX : -flap*flap * _FlapScaleX;
v.vertex.y += _FlapScaleY * flap * sin(_FlapAngle * PI / 180);
v.vertex.z += _FlapScaleY * flap * cos(_FlapAngle * PI / 180);
}
void surf (Input IN, inout SurfaceOutputStandard o) {
// テクスチャとカラーの計算を加算合成に変更
fixed4 c = (tex2D (_MainTex, IN.uv_MainTex) + UNITY_ACCESS_INSTANCED_PROP(Props, _Color) * 2)/3;
o.Albedo = c.rgb;
o.Metallic = _Metallic;
o.Smoothness = _Glossiness;
o.Alpha = c.a;
}
ENDCG
}
FallBack "Diffuse"
}
木曜日
プレイヤーの動き
次はとりあえずプレイヤーが操作する親鳥の動作部分を書き始めました。完成したものがこちら。
プレイヤーに前方にある球を操作させて、その球との位置の差にgameSpeedをかけたものをそのまま親鳥の速度にしてます。(良い子は加速度のほうを変えよう!)
次に仲間の鳥の動きはこちらのサイトを参考に、Boidsというアルゴリズムを使いました。
ただ、そのままだと初期位置付近で群れて親鳥についていかないので、
Vector3 Alignment(Bird bird)
{
Vector3 vec = Vector3.zero;
int count = 0;
foreach (var other in _birdArray)
{
if (other._birdController.birdState != BirdState.Boid || other._gameObject == bird._gameObject) continue;
if ((bird._transform.position - other._transform.position).magnitude < _neighborRange)
{
vec += other._rigidbody.velocity;
count++;
}
}
vec = (count > 0) ? vec / count : _targetRigidbody.velocity;
vec = (vec + _targetRigidbody.velocity) / 2;
vec.z = _targetRigidbody.velocity.z;
return vec - bird._rigidbody.velocity;
}
こんな感じで親鳥だけは距離関係なく比重大きめに影響するように変更してます。(後半に出てくる_targetRigidbodyが親鳥のRigidbodyです)
あとプレイしてて親鳥が見えにくかったので、親鳥の前後は避けるように分離のアルゴリズムのほうを変更して調整しました。
Vector3 Separation(Bird bird)
{
Vector3 diff = bird._transform.position - new Vector3(_targetTransform.position.x, _targetTransform.position.y, bird._transform.position.z);
Vector3 vec = (diff.magnitude < _separationRange) ? diff.normalized / diff.magnitude * diff.magnitude : Vector3.zero;
int count = 1;
foreach (var other in _birdArray)
{
if (other._birdController.birdState != BirdState.Boid || other._gameObject == bird._gameObject) continue;
diff = bird._transform.position - other._transform.position;
if (diff.magnitude < _separationRange)
{
vec += diff.normalized / diff.magnitude * diff.magnitude;
count++;
}
}
return (count > 0) ? vec / count : vec;
}
最後に各アルゴリズムの重みやパラメーターを調整して完成!
金曜日
ステージの作成
まず最初に考えたのが穴の空いた壁を何枚か用意しておき、それをランダムに並べていくというもの。で、実際にProBuilderを使って作業を始めて思ったのが、
「ProBuilderの使い方わからねぇ……」「穴を開けたいのに出っ張りしか作れない……」(※使い方が悪いだけで、決して機能が足りないわけではありません)
ということでCubeを4つ上下左右に配置して穴の空いた1枚の壁に見せるという現在の形に。これだとテクスチャは貼りにくくなりますが代わりに変形させやすくなるので一長一短ですね。
ステージを自動生成にするためにプロセスを考えます。
- まず、穴の位置を範囲内から決めて
- 上下左右にあるCubeを穴のふちまで拡大する
1は問題ありません。2も漠然とLeanTweenを使えば簡単かなーと思いながらとりあえずLeanTweenをインポートして、拡大していくパターンをいくつか書いていきました。しかしこれだと変形速度を変形にかかる時間で設定しているため、Time.timeScale以外で加速したときにうまくぎりぎりまで変形してくれません。
そこで壁からプレイヤーまでの距離を測り、それに応じて変形するようにしてみました。これなら毎回どんな速度で近づいても必ずぎりぎりで通ることになります。
void FixedUpdate()
{
float t = Mathf.Clamp01(1 - (transform.position.z - _playerTransform.position.z - _secondDistance) / (_firstDistance - _secondDistance));
if (t > 0) MoveWall(t);
}
_firstDistanceが変形を開始する距離、_secondDistanceが変形を完了する距離で、それらを元に変形率を計算してtに入れています。
これが結構便利で、ratio[]という配列の要素を壁の数だけ用意して、
_cubes[0].transform.localPosition = new Vector3(Mathf.Lerp(-_wallSize.x / 2, (-_wallSize.x / 2 + _holePos.x) / 2, ratio[0]), 0f, 0f);
_cubes[0].transform.localScale = new Vector3(Mathf.Lerp(0f, _wallSize.x / 2 + _holePos.x, ratio[0]), _wallSize.y, _wallSize.z);
_cubes[1].transform.localPosition = new Vector3(Mathf.Lerp(_wallSize.x / 2, (_wallSize.x / 2 + (_holePos.x + GameManager.Instance.holeSize)) / 2, ratio[1]), 0f, 0f);
_cubes[1].transform.localScale = new Vector3(Mathf.Lerp(0f, _wallSize.x / 2 - (_holePos.x + GameManager.Instance.holeSize), ratio[1]), _wallSize.y, _wallSize.z);
_cubes[2].transform.localPosition = new Vector3(0f, Mathf.Lerp(-_wallSize.y / 2, (-_wallSize.y / 2 + _holePos.y) / 2, ratio[2]), 0f);
_cubes[2].transform.localScale = new Vector3(_wallSize.x, Mathf.Lerp(0f, _wallSize.y / 2 + _holePos.y, ratio[2]), _wallSize.z);
_cubes[3].transform.localPosition = new Vector3(0f, Mathf.Lerp(_wallSize.y / 2, (_wallSize.y / 2 + (_holePos.y + GameManager.Instance.holeSize)) / 2, ratio[3]), 0f);
_cubes[3].transform.localScale = new Vector3(_wallSize.x, Mathf.Lerp(0f, _wallSize.y / 2 - (_holePos.y + GameManager.Instance.holeSize), ratio[3]), _wallSize.z);
こんな感じで各壁をMathf.Lerpで穴の最終サイズまで変形させることにすると……、
case 1:
ratio[0] = Mathf.Clamp01(t * t);
ratio[1] = Mathf.Clamp01(t * t);
ratio[2] = Mathf.Clamp01(t * t);
ratio[3] = Mathf.Clamp01(t * t);
break;
例えばこんな感じでCubicのイージングが出来たり、
case 13:
ratio[0] = (t < 0.2f) ? Mathf.Clamp01(t * 10) : (t < 0.8f) ? 0 : 1;
ratio[1] = (t < 0.6f) ? Mathf.Clamp01(t * 10 - 4f) : (t < 0.8f) ? 0 : 1;
ratio[2] = Mathf.Clamp01(t * 10 - 6f);
ratio[3] = (t < 0.4f) ? Mathf.Clamp01(t * 10 - 2f) : (t < 0.8f) ? 0 : 1;
break;
こんな変則的な動きを作るのも比較的短いコードで出来ます。
壁通過時の加速
壁と壁の距離を調整しているときに、「もうちょっと距離を縮めたいんだけどなんかしっくりこないなぁ」とか考えて、何を思ったのか通過後に加速するように変更。(上の画像のように壁通過後は親鳥を動かす目標地点が前のほうに移動する)
意外としっくりきたので採用。
土曜日
音選び
頭の中にこういう音にしたいというイメージはあるんですが実際に探すとなると時間がかかってしまいます。今回も効果音ラボさんと魔王魂さんの音源を一通り聴き比べて選びました。(力技)
BGMに関してはRezっぽさがほしかったのでテクノ系のBGMから探しました。
UI制作
正直UIはまだ全然わかりません。わからないのでTextMeshProを使って書いたテキストをそれっぽく配置しました。点滅させるのはアニメーションを使っています。
ランキングはnaichさんのサンプルをそのまま使用させて頂きました。
背景とか
背景に関してはカメラから一定の距離にQuadで作った板を置いて色を変えてるだけです。四隅の線も細長いQuadです。この辺りもう少しやりようがあったんじゃないかという思いはありましたが、とりあえず動くようにしようという感じでそれっぽいのを置いておきました。
背景のアルファを変更するのに唯一LeanTweenを使っています。
クリア時演出
GameManagerでGameStateを定義してステート管理をしているのですが、その中にMovieステートを作ってカメラと親鳥のMovie中の動きをそれぞれ追加しました。この辺りはもっといろんなコード見て勉強したいところです。
テストプレイ
恐怖の時間です。目立ったバグはその都度確認して修正していましたが、WebGLでの動作確認はまた別です。
毎回発生するのがWebGL特有の最初の1回だけ発生するラグスパイク。今回はこれをなんとかしたいと思い、まずは現状を確認しました。
- ゲーム開始直後、背景のアルファを変更するときに発生するラグ
- 最初に鳥が壁に衝突するときに発生するラグ
- 最初にランキング画面をロードする時のラグ
この3つのうち、3つ目のラグに関してはプレイに直接影響はなさそうなのでとりあえずそのままにしました。(非同期でシーンをロードすれば解決出来るかもしれません)
1つ目のラグはおそらくマテリアルのインスタンスが新しく生成されるときに発生するラグだと思います。とりあえず起動直後(Start内)にアルファ変更を入れて先にインスタンス化することで(多分)解決しました。
2つ目のラグはOnCollisionEnterが最初に呼び出されるときに発生しているような感じだったので、起動直後に鳥を何匹か壁に打ち付けてみたら解決しました。(タイトル画面で集まってくるのは壁に当たらなかった鳥で、実は後ろのほうで何匹か壁に衝突してます)
アップロード
とりあえず完成したのでアップロードしてtwitterでタグ検索して他の人の進捗なんかを見ることに。しかし、「なんだこのクオリティは……!」「演出かっこいい!!」「一体普段何を食べていたらそんなアイディアが出てくるんだ!?」「やっぱりまだ参加は早かったんだ……」と明らかに精神的ダメージを受けていました。
製作中にtwitterを見なくて本当によかった。
やりたいけど出来なかったこと
- かすり時スローや、かすり時得点などのかすり判定(簡単に試してみたけど複数コライダーの挙動がよくわからなくて断念)
- スピード感を出す背景演出(シェーダーを使う?)
- 鳥の羽ばたきをバラバラにする(ためしに設定してみたらSetPass Callが3桁に跳ね上がったので断念。モデルの頂点数を減らせば出来たかも?)
- かっこいいリザルト画面(時間とセンスと経験が足りなかった)
- クオンタイズ(出来れば通過音が自動でBGMに合うタイミングで鳴る、というのをやりたかった)
終わりに
正直公開されるのが怖かったんですが、公開されてみると思ったより好評で「参加してよかったなぁ」と思いました。
他の人の作品から吸収出来ることも多くて、それも楽しかったです。まだ150作品しかプレイ出来ていませんが、残りも全部プレイして評価していきたいと思います。(ランキングがあればランキングも)
拙い文章でしたが、ここまで読んでくれてありがとうございます。よかったらゲームのほうも遊んでやってください。それでは(`・ω・´)ノシ
※Unity始めて2ヶ月になるくらいの初心者なので参考になる部分は少ないかもしれません。間違っている部分やこういう作り方のほうがオススメだよ!といったところがあればアドバイスして頂けると助かります。