ぐーるらいふ

底辺。

【Unity】第四回 unity1week 「夏」の開発について【レールの作り方】

第四回 Unity一週間ゲームジャムに参加

https://unityroom.com/assets/unity1week-d28da101073cc56f5238563f033ad780466b71b1605362b9fc351e7135ed1c4b.png
Unity 1週間ゲームジャム | 無料ゲーム投稿サイト unityroom - Unityのゲームをアップロードして公開しよう

一回目から参加し続けてもう四回目にもなってしまった。
参加者もちょっと少なかったのかもしれない。
イカドラクエのせいだ。


結論から言えば、今回も結果は振るわなかったのだけど、
まだまだUnity学びたいし頑張ろうと思っている。

イデアについて

とりあえず

  • イカ
  • 花火
  • お化け

この辺りが夏の記号になりそうだなと漠然と考えた。

で、とりあえずスイカ割らせてみたり。

切り方はMeshCutterで数回ランダムに切っているだけ。
毎回結果が変わるのが面白い所。

だが、スイカ割りは特に人気で被りまくりそう。
花火も上に同じ。

最近気になっていたレール式ランゲームをちょっとやってみたかったので、
サーフィンを題材に今回はそれに。

レール式ランゲーム

決められたレールの上を走るランゲーム。
ジェットコースターやトロッコとかイメージすればOK。
例えば右方向へ向かって走るランゲームと比べて、
3Dを活かしたダイナミックな動きを出すことが容易に出来る。
また制御しやすい。(一回転を作ったり、この曲がり角で敵を出す!とか)

unityに落とし込む

  • ランダムにルートを生成する
  • 無限に走ることが出来ないとならない
  • 敵の配置
  • ゲームとしての体裁を整える

敵の配置やゲームとしての体裁を整える辺りは長くなりすぎるので割愛。
今回はルート生成についてコードレベルで紹介。

ルート生成のアルゴリズム

一言で言えばルートはpointの集合体です。
「次に向かうポイント」
をどんどん点で打っていけばOKです。

流れとしては

  • ポイントをざっくり置く
  • 滑らかに整える
  • デコレーションする
  • 描画する

という流れになります。

ポイントをざっくり置く

まずはじめにポイントをざっくりと置いていきます。

for (var i = 1; i < LINE_CNT; i++)
{
    var distance = Random.Range(1, 5); // 距離の力
    range = Random.Range(-3, 3) * 30; // ランダムな方向を向ける
    lastAngle = Mathf.Clamp(lastAngle + range, 0, 180); // 前回の角度から0度~180度の間でランダムな方向を出す

    float angle = lastAngle * Mathf.PI / 180;
    // 極座標の公式で計算する
    path.LinePoints[i] = path.LinePoints[i - 1] + new Vector3(Mathf.Cos(angle), 0, Mathf.Sin(angle)) * distance;
}

pointはランダムな方向と、角度から位置を計算します。
極座標の公式

線の長さ(r)と角度(θ)で到達点座標(x,y)が求まる
(x,y) = (r cosθ , r sinθ)

イメージとしてはこんな感じでまだガタガタな点が作られます。

f:id:ghoul_life:20170731181931p:plain

滑らかに整える

点を細かくして補完していきます。
この計算をどのようにするかによって補完の仕方が変わります。
http://d.hatena.ne.jp/nakamura001/20111117/1321539246
イージングのアルゴリズムを応用して、緩急を作り出したりしても面白い!

/// <summary>
/// 線を滑らかに補完する
/// </summary>
/// <param name="linePoints"></param>
/// <param name="smoothness"></param>
/// <returns></returns>
private Vector3[] SmoothCurve(Vector3[] linePoints , float smoothness)
{
	if (smoothness < 1.0f) smoothness = 1.0f;

	var pointsLength = linePoints.Length;
	var curvedLength = (pointsLength * Mathf.RoundToInt(smoothness)) - 1;
	var curvedPoints = new List<Vector3>(curvedLength);

	float t = 0.0f;
	List<Vector3> points;
	for (int i = 0; i < curvedLength + 1; i++)
	{
		t = Mathf.InverseLerp(0, curvedLength, i);
		points = new List<Vector3>(linePoints);
		for (int j = pointsLength - 1; j > 0; j--)
		{
			for (int k = 0; k < j; k++)
			{
				points[k] = (1 - t) * points[k] + t * points[k + 1];
			}
		}
		curvedPoints.Add(points[0]);
	}

	return (curvedPoints.ToArray());
}

青い線が補完後の線になります。
ちょっと滑らかさが強いかもしれませんね…。

f:id:ghoul_life:20170731182144p:plain

デコレーションする

一旦パスを作った後、ゲーム的な要素を追加します。
敵を置いたり、ループ作ったり。

基本的に完成したパスを再度舐めつつ、
ランダムに処理をしていけばOK

var EFFECT_IDX = 20; // 20個目までの点には効果を出さないように~とか。
for (var i = EFFECT_IDX ; i < path.LinePoints.Length ; i++)
{
    var pos = path.LinePoints[i];
    // 効果がランダムで発動
    switch (Random.Range(1, 5))
    {
    	// 障害物を出す、とか
    	//case XXX:
	    //     ~~~
	    //     break;
	}
}

描画する

無くてもいいのですが、
見せてもいいのです。

LineRendererにまとめて入れるだけ

_lineRenderer.positionCount = _linePoints.Length;
_lineRenderer.SetPositions(_linePoints);

あれ、SetVertexCount だったような…とちょっと気になるヤーツ。

https://docs.unity3d.com/ja/540/ScriptReference/LineRenderer.SetVertexCount.html

変わったようです。

処理速度について

もうここまででもいいんじゃね?って話ですが
ゲームにした場合、気になるのは処理速度です。
どうしても計算はちょっと長いのでストレートに処理していたらプチフリーズしてしまうでしょう。
そして、パスの終点まで到達した時に処理をしていたのでは、遅れてしまうでしょう。

  • 並列に処理する
  • 予め次を計算しておく

この2つを満たせばOKです。

並列に処理するのは何でもいいです。
Coroutineでも、Taskでも、Threadでも…。
慣れ親しんだCoroutine辺りでとりあえずいいのではないでしょうか。

次を用意しておく処理はこんな感じで

// indexが半分を過ぎたら。ここはもっと早くてもいいかも。ゲームスピードとの兼ね合い
var len = currentPath.LinePoints.Length;
if (index > 0.5f * len && isCreateNext)
{
    var lastPoint = currentPath.LastPoint; // 最後のposition
    var lastAngle = currentPath.LastAngle; // 最後の角度
    // 続きのサブを作成しておく
    StartCoroutine(CreateRailPath(lastPoint, lastAngle , subPath));

    isCreateNext = false; // 何度も作らせないために
}

// 最後まで来たらswapする
if (index > len - 1)
{
    // 予め作っておいたサブとカレントを切り替える
    var tmpPath = currentPath;
    currentPath = subPath;
    subPath = tmpPath;
    isCreateNext = true; // 次を作らせるフラグをON
    index = 0;
}

currentとsubを用意しておいて、
終わり際にswapする!というだけ。
後は進捗をチェックしつつ、次を用意させるだけです。

f:id:ghoul_life:20170731183130p:plain

進んでいくと

f:id:ghoul_life:20170731183153p:plain

交互に次が生成されるのがわかると思います。
この辺りだけgithubにまとめておきました。
よく分からない所はこちらを参考に。

github.com

今回の感想

というわけで第四回も終わりました。
今回も大変だった…。手が遅くて後手後手に…。
イデアは相変わらずヒドイもんで、
センスが腐ってんなと思います。

次こそはもうちょっと良いものを作りたい。