ぐーるらいふ

底辺。

【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

今回の感想

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

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

【Unity】攻撃判定タイミングをグラフィカルに作りたい 【Editor】

いきなりですが

GitHubにコード一式あげてます。ちょっと長いので、面倒な方はソースを見ると早いと思います。

github.com

格闘ゲームを作ろうとしてます。
格闘ゲームに限らずアクション要素のあるゲームには攻撃判定というものが大抵あります。

http://www.capcom.co.jp/blog/sf4/img_upload/t11151JRI_02.jpg

この攻撃判定ですが、「常に出続けてOK」なゲームもあれば、
「一時的にだけ表示してほしい」というゲームもあります。
アクションゲームの攻撃で「剣を振る」なども一時的に攻撃判定が出てほしいアクションだと思います。

攻撃判定の制御

パンチ Animation

start ---(攻撃判定発生中) ----->> end

キック Animation

start -------------(攻撃判定発生中) ----->> end

格闘ゲームで考えるとパンチならパンチ、キックならキックのアニメーションがあり、
攻撃判定が発生するタイミングとその長さをそれぞれ設定しなければなりません。

  • アニメーションの数が少ない
  • 一人で開発している、

などといった場合は
一つ一つ再生をスクリプトで拾って

10frame後に30frame間だけ判定を出す

などといったコードを埋め込んでいく事でも実現は可能です。
が、多人数で作っていたり、大規模になったりするとその限りではありません。

グラフィカルに当たり判定を制御したい!

Unity使ってるぞ!Editorだって作れちゃうんだ。
せっかくだからEditorで攻撃判定を制御するツールを作っちゃおうじゃないか。

事前準備

今回もまた以下のAssetsを使用して説明します。


f:id:ghoul_life:20170711171702p:plain
https://www.assetstore.unity3d.com/jp/#!/content/33478

f:id:ghoul_life:20170711171719p:plain
https://www.assetstore.unity3d.com/jp/#!/content/33083

以上のお二方は前回から引き続き。
また、Animationの共通化については以下の記事を参考にしてください。

ghoul-life.hatenablog.com

キャラクターに攻撃判定コライダーをつけよう

f:id:ghoul_life:20170720192725p:plain

f:id:ghoul_life:20170720192810p:plain

こんな感じで手足にCapsuleColliderと以下のスクリプトを付与します。
このCapsuleColliderが攻撃判定の大きさになります。

プログラマー作業ではなく、
ゲームデザイナーに位置や大きさなどの調整を任せる
といった分業も出来るでしょう。

また、タックルやヘッドバットといった別の箇所にも攻撃判定を付ける
といった事も可能です。

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

[RequireComponent(typeof(CapsuleCollider))]
public class HitCollider : MonoBehaviour {

    public HitType hitType;
    private CapsuleCollider collider;

    public void Disable()
    {
        if (collider == null)
        {
            collider = GetComponent<CapsuleCollider>();
        }
        collider.enabled = false;
    }

    public void Enable()
    {
        if (collider == null)
        {
            collider = GetComponent<CapsuleCollider>();
        }
        collider.enabled = true;
    }

    // colliderをSceneビューで表示してみる…。多少強引な計算があるので、
    // colliderの正確な位置とサイズを取る方法があれば…。
    void OnDrawGizmos()
    {
        if (collider && collider.enabled)
        {

            Vector3 offset = 
                transform.right * (collider.center.x / collider.height / 2) + 
                transform.up * (collider.center.y / collider.height / 2) + 
                transform.forward * (collider.center.z / collider.height / 2);

            Vector3 size = new Vector3(
                (collider.radius / 0.5f) * collider.transform.lossyScale.x,
                (collider.height / 2) * collider.transform.lossyScale.y, 
                (collider.radius / 0.5f) * collider.transform.lossyScale.z
                );

            Quaternion dir;
            switch (collider.direction)
            {
                case 0:
                    dir = Quaternion.Euler(Vector3.forward * 90);
                    break;
                case 1:
                    dir = Quaternion.Euler(Vector3.up * 90);
                    break;
                case 2:
                    dir = Quaternion.Euler(Vector3.right * 90);
                    break;
                default:
                    dir = Quaternion.Euler(Vector3.zero);
                    break;
            }
            Gizmos.color = new Color(1, 0, 0, 0.5f);
            Gizmos.DrawMesh(GetPrimitiveMesh(PrimitiveType.Capsule), collider.transform.position + offset, transform.rotation * dir, size);

        }
    }

    private Mesh GetPrimitiveMesh(PrimitiveType type)
    {

        GameObject gameObject = GameObject.CreatePrimitive(type);
        Mesh mesh = gameObject.GetComponent<MeshFilter>().sharedMesh;
        DestroyImmediate(gameObject);

        return mesh;

    }

}

スクリプトはコライダーのON/OFFを制御するのに使います。
HitTypeは後述。

攻撃判定種別を付けよう

今回は格闘ゲームを題材にしているため、
攻撃判定は複数あることにします。

  • 左手
  • 右手
  • 左足
  • 右足

の4つとします。
ゲームによっては、ひじやひざ、または頭突きや肩なんてものもあるかもしれません。
が、今回はシンプルにこの4つにします。
これを定義しているのが、HitTypeです。

public enum HitType
{
    LEFT_HAND = 1,
    RIGHT_HAND = 2,
    LEFT_LEG = 4,
    RIGHT_LEG = 8
}

こんなただのenumです。
値はbit演算のためにこのようにしてます。
その1で対応したHitColliderに左手なら左手、右足なら右足といったHitTypeをセットしておきます。

まずはAnimationClipを再生するEditorを作る

Editor沼に足を踏み入れたような気がします。
AnimationClipなんて、Inspectorの再生窓使えば見れるやんけ!
という声が聞こえてきそうですが、それをあえて自分で作ってみる。

(EditorWindowの作り方の説明などはいくらでも出てくるので割愛)

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEditor;

// 単純なサンプルエディタ
public class AnimClipSampleEditor : EditorWindow
{

    // アニメーションクリップを再生させるオブジェクト
    private Animator animObject;
    // 再生したいアニメーションクリップ
    private AnimationClip animClip;

    [MenuItem("Window/AnimClipSampleEditor")]
    static void Open()
    {
        GetWindow<AnimClipSampleEditor>();
    }

    private float value;

    void OnGUI()
    {
        GUILayout.BeginHorizontal();
        GUILayout.Label("character : ", GUILayout.Width(110));
        animObject = (Animator)EditorGUILayout.ObjectField(animObject, typeof(UnityEngine.Animator), true);
        GUILayout.EndHorizontal();
        EditorGUILayout.Space();

        GUILayout.BeginHorizontal();
        GUILayout.Label("animation clip : ", GUILayout.Width(110));
        animClip = (AnimationClip)EditorGUILayout.ObjectField(animClip, typeof(UnityEngine.AnimationClip), true);
        GUILayout.EndHorizontal();
        EditorGUILayout.Space();

        if (animObject == null || animClip == null)
        {
            GUILayout.Label("Please Setting character and animation clip");
            EditorGUILayout.Space();
            EditorGUI.BeginDisabledGroup(true);

        }

        GUILayout.BeginHorizontal();
        value = EditorGUILayout.Slider(new GUIContent("TimeLine"), value, 0, 1, GUILayout.Width(300));
        GUILayout.EndHorizontal();

        if (animObject == null || animClip == null)
        {
            EditorGUI.EndDisabledGroup();
        }

        //--------------------------------------------------------------------

        if (animObject != null && animClip != null)
        {
            animClip.SampleAnimation(animObject.gameObject, value);
        }

        Repaint();
    }

}

そして適当にキャラクターをシーンに配置して、
AnimationClipをセットすれば、
スライダーでアニメーションを動かすことが出来ます。

f:id:ghoul_life:20170720193802p:plain

攻撃判定を付与するには

色々なアプローチがあります。

  • 自力Scriptアプローチ、
  • AnimatorからStateBehaviorでUpdateのframeを見る

などなんでも良いのですが、
今回はAnimationClipからEventを呼び出すアプローチで行きたいと思います。

AnimationClipにはEventを付与することが出来ます。

f:id:ghoul_life:20170720195028p:plain

これによって、AnimationClipの最後にEventを付与して、アニメーションの終了判定を行ったり
していた過去もあるかもしれません。

攻撃判定を開始するタイミングでEventを発行して、終了タイミングで再度Eventを発行すれば良さそうです。

HitTypeの判定について

攻撃判定を開始した時

  • 「左手の攻撃判定開始」 -> 「判定終了」
  • 「右足の攻撃判定開始」 -> 「判定終了」
  • 「左手と右手の攻撃判定開始」 -> 「判定終了」

といった命令が送られてくると考えます。

こうしました。

イベントを送る側(Animation Clip)

    private int CreateHitEvent()
    {
        int result = 0;

        if (toggleLeftHand) result = (result | (int)HitType.LEFT_HAND);
        if (toggleRightHand) result = (result | (int)HitType.RIGHT_HAND);
        if (toggleLeftLeg) result = (result | (int)HitType.LEFT_LEG);
        if (toggleRightLeg) result = (result | (int)HitType.RIGHT_LEG);

        return result;
    }
    
    void OnGUI(){
        //~~
        if (GUILayout.Button("Add Hit Event")){
            var serialied = new SerializedObject(animClip);
            serialied.Update();

            var events = new List<AnimationEvent>();

            onHitEventStart = new AnimationEvent();
            onHitEventStart.time = startTime;
            onHitEventStart.functionName = "OnHitEventStart";
            onHitEventStart.intParameter = CreateHitEvent();
            events.Add(onHitEventStart);

            onHitEventEnd = new AnimationEvent();
            onHitEventEnd.time = endTime;
            onHitEventEnd.functionName = "OnHitEventEnd";
            events.Add(onHitEventEnd);

            // Animation ClipにEventを付与します
            AnimationUtility.SetAnimationEvents((AnimationClip)serialied.targetObject , events.ToArray());
            EditorUtility.SetDirty(serialied.targetObject);

            serialied.ApplyModifiedProperties();
        }
        //~~
    }

イベントを受ける側(Character)

    // hitEvtValue = 0x1111 , 0x1001 といった値が来る
    public void OnHitEventStart(int hitEvtValue)
    {
        // 該当するcolliderをON
        foreach (var hit in hitColliders)
        {
            if (((int)hit.hitType & hitEvtValue) != 0)
            {
                hit.Enable(); // HitColliderをON
            }
        }

    }

ビット演算を使い、そのフラグが立っていたら判定をONというシンプルな作りです。
他にも渡したいものがあればObjectに情報をまとめて渡すなんてのもアリなんですが、
とりあえず思いつかなかったので最小限にしてます。

あとはこれをグラフィカルにEditor化すればOK

必要なパーツは大体揃ったので、後はウィンドウにまとめて値をEditorから操作します。

f:id:ghoul_life:20170721103214p:plain

どうしてもGUI系はコードだらっと長いので、githubのコードを参考にしてください。

github.com

MinMaxSliderの範囲が攻撃判定の範囲ということにしました。
ハンドルが持ちにくく、もう少し見た目が変えられるといいんですが、
GUIStyleとか頑張って作ればいけるのかな。

攻撃判定発生タイミングはAnimationClip側に紐付いているため、
キャラクターを変えても、AnimationClipに攻撃判定発生タイミングは変わらず、
共通で扱うことが出来ます。

f:id:ghoul_life:20170721124224g:plain

そして、攻撃判定の大きさや位置はキャラクター側に紐付いているため、
キャラクター個別に設定することが出来ます。


f:id:ghoul_life:20170721123414g:plain

こうしてEditor沼へと落ちていくのだった

これで作業の分離が出来ました。(?)
出来たのか?

ゲームデザイナーやキャラクターデザイナー
攻撃判定位置や発生フレームを丸投げして、
自分はロジックを組む…という分担が出来ます。

「困難は群れで分け合え」

と、かばんさんも言っていました。

自分は完全に個人なので、自分で作って自分で使う!
という無駄な作業かもしれないのですが、勉強だと思って…。

注意点として

fbxの内側にあるAnimationClipをそのまま使おうとすると、
Eventを上手く付与出来ないかもしれません。(保存したのに反映されないなど)
その場合はfbxからAnimationClipを取り出すと、Editが自由になるため
Eventの付与も出来るはずです。
こちらも参考に!神。

baba-s.hatenablog.com

【Unity】[Tips] character animationのスピード変えたかった

Tipsだよ。本当に一言で終わるよ。

やりたかったことを察してほしい

animatorの再生時に速度を落としたりなんだりしたいな、と
思っていたのでまずは直感でこのように実装した。

   public void OnBtnSpeed100()
    {
        animator.speed = 1.0f;
        animator.SetTrigger("Hikick");
    }

    public void OnBtnSpeed50()
    {
        animator.speed = 0.5f;
        animator.SetTrigger("Hikick");
    }

    public void OnBtnSpeed10()
    {
        animator.speed = 0.1f;
        animator.SetTrigger("Hikick");
    }

この結果がこれ。

え、合ってないんかい!?

docs.unity3d.com



正解はこれ

AnimatorStateにはSpeedパラメータも渡すことが出来る

AnimatorのparameterにfloatでSpeedを追加して

f:id:ghoul_life:20170713153130p:plain

その後AnimationStateのSpeedの所にチェックを入れて
Multiplierの所にSpeedを指定して

f:id:ghoul_life:20170713153139p:plain

    public void OnBtnSpeed100()
    {
        animator.SetFloat("Speed", 1.0f);
        animator.SetTrigger("Hikick");
    }

    public void OnBtnSpeed50()
    {
        animator.SetFloat("Speed", 0.5f);
        animator.SetTrigger("Hikick");
    }

    public void OnBtnSpeed10()
    {
        animator.SetFloat("Speed", 0.1f);
        animator.SetTrigger("Hikick");
    }

他に影響なく値を変えることも出来るし!いいですね。

これだけ。
なんてこと無いけど面白かったのでブログに残す…。

【Unity】3Dモデルのアニメーションを別の3Dモデルで使い回したい

3Dキャラ同士同じアニメ使えるやろ

  • Unity chanは3Dキャラやん
  • Unity chanにアニメーションあるやん
  • 他の3Dキャラにも同じアニメーションいけるやろ
  • ペタッと
  • ちーん

これとか
f:id:ghoul_life:20170711171210p:plain

こうとか
f:id:ghoul_life:20170711171217g:plain

見たことありませんかね?
「合っとるやろ!?なんでお前は言うことを聞かないんや!?もうええ!」
とsteamを起動していたんですが、いい加減に向き合おうと思いました。

格闘ゲーム作りたくなったんです

パンチとかキックとか良いですよね。カッコイイですよね。
でも自分はバーチャファイターや鉄拳は凄いヘタでして…。
2D格闘世代なので、SFやKOFやGGとかの方が馴染みがあります。
闘神伝トバルNo.1はそっと胸の奥に閉まっておきました。

使用したAsset

ファイティングユニティちゃん 無料体験アセット
f:id:ghoul_life:20170711171702p:plain
https://www.assetstore.unity3d.com/jp/#!/content/33478


Jeremyくん 無料
f:id:ghoul_life:20170711171719p:plain
https://www.assetstore.unity3d.com/jp/#!/content/33083


この二方を利用して、アニメーションの使い回しをしてみます。

Avator RigのAnimation Typeをおさらい

Avator RigのAnimation Typeは以下の3つがある

  • Humanoid
  • Generic
  • Legacy
Humanoid

ボーン構造などの基本的な構造の統一化を図り、リターゲッティングと呼ばれる
さまざまなモデルに同じアニメーションが共有できる仕組みが扱える。
docs.unity3d.com
docs.unity3d.com


Generic

ヒューマノイドアニメーションと言われ、動物などで主に利用される事を想定している。
Mechanimで扱うことは出来るが、共有して扱うことは出来ない。
docs.unity3d.com

Legacy

これはそのまま旧アニメーションでそのオブジェクトに紐づくアニメーション。
Mechanim非対応のアニメーションの場合はコレになるのかな。
docs.unity3d.com

結論

つまり、HumanoidのTypeにしてあげれば共通して使うことが出来るはずだ。

設定を見直そう

Unity chanのmodelのRigを見る

Animation Type = Generic -> Humanoid に変更して Apply

JeremyくんのmodelのRigを見る

Animation Type = Generic -> Humanoid に変更して Apply


f:id:ghoul_life:20170711165803p:plain

あれ、まだ駄目?モデルだけではなく、Animation Clipも編集しよう。
ポチポチとAnimation ClipのRigを見て

Animation Type = Generic -> Humanoid に変更して Apply

f:id:ghoul_life:20170711165812p:plain

Unity chan , Jeremy共に同じAnimator Controllerでいいはずだ。
FightingUnityChan_freeをセット。

f:id:ghoul_life:20170711165918p:plain

実装

こんなスクリプトをButtonで呼び出すようにして

  public Animator player;
  public Animator enemy;

  public void Punch()
  {
      player.SetBool("Jab", true);
      enemy.SetBool("Jab", true);
  }

  public void Kick()
  {
      player.SetBool("SAMK", true);
      enemy.SetBool("SAMK", true);
  }

  // Fighting Unitychan側には無い、Jeremy側のアニメーション。
  // 逆も出来るはずだ、というテスト。
  public void Victory()
  {
      player.SetBool("Victory", true);
      enemy.SetBool("Victory", true);
  }

実行すると

f:id:ghoul_life:20170711165449g:plain

OK!!
(後で気づいたんですが、Jeremyくんパンツ履いてなかった。)

さぁ、steamを起動しよう!

書く(読む)と長いんだけど、やると本当にすぐです。五分ぐらいで終わっちゃう。
ちょっとググったら答えはすぐに出るんだけど、自分なりに調査した結果をメモっておく、って事で。

steamを起動する時は、熱々のピザとキンキンに冷えたコーラを忘れずにね!

【Unity】初心者でもCinemachineを使いたい

まずはこれを見てくれ。

複数のカメラが切り替わるとグッとゲームらしいです。
これ作るの面倒なんだよね~、なんて方。
ご安心ください。簡単に作れますよ。そうCinemachineならね。

Cinemachine?

UnityがAssetストアで無料で公開しているAssetで、統合カメラシステムです。

https://www.assetstore.unity3d.com/jp/#!/content/79898

Unity 2017.01.から使用可能になる予定となっていて、
今使いたい場合は、β版のUnityが必要です。

unity3d.com

早速使ってみる

Unity Betaを準備してAsset StoreからImportしたら
Examplesから各Sceneを開いて機能を確かめたら良い!
そして、公式のModule Examplesを見るんだ!

Module Examples – Cinemachine

と言いたい所ですがそれは置いておいて
「自分で使う場合はどうやるんだ?」
って所を書こうと思います。

まずは動かしてみよう

CinemachineをImportしているプロジェクトで画面上部メニューから

Cinemachine > Create Virtual Camera

f:id:ghoul_life:20170706184343p:plain

を押すと、Main Cameraに Cinemachine Brainが追加される。
さらにCinemachine Virtual Cameraが追加されたCM vcam1がSceneに追加される。

f:id:ghoul_life:20170706184129p:plain

もうこれだけでCinemachineが動いています。スゴイ。当たり前かw


見たまんまをあえて書きますと、

Main Cameraの位置に関わらず、CM vcam1の位置からカメラ表示するようになっている

Main Cameraの位置を動かそうとしても追加されたCM vcam1の位置が上書きされて動かせなくなっているはず。
また、sub cameraを作っているのではなく、スクリプト制御しているということもわかります。

ちょっと遊んでみる

もう一度

Cinemachine > Create Virtual Camera

を選択して、2つ目のVirtual Cameraを追加しよう。
ちょっと位置を調整して切り替わりのテストをしてみます。
こんな感じで切り替わりがわかる形にしました。(真上からの視点)

f:id:ghoul_life:20170706190409p:plain

切り替わり

1つ目のカメラをCM vcam1
2つ目のカメラをCM vcam2
とします。

CM vcam1 -> CinemachineVirtualCamera enable ON
CM vcam2 -> CinemachineVirtualCamera enable OFF

の状態で実行して、
CM vcam2のenableをON/OFFしてみよう。

f:id:ghoul_life:20170706190551g:plain

CinemachineVirtualCameraのenableやGameObjectのActiveを切り替えるだけで
自動でカメラが切り替わってくれる事がわかりました。

念のために優先度もテストしておこう

CM vcam1 -> priority 11
CM vcam2 -> priority 10

この状態で CM vcam2 のenableをON/OFF切り替えてみても
何も反応しないということがわかります。
想定通り、優先度の値が大きい方が優先になるということですね。

まとめると

この事から

  • GameObjectのActiveかCinemachineVirtualCameraのenableをON/OFFするだけで動く
  • Priorityが大きい方が優先になる
  • Priorityが同じ場合はONにした方が動く

ということがわかった。

ゲームに応用してみよう

Cinemachineがちょっとは理解できた。結構単純な仕組みで動いていそうだ。
ゲームで使いたい場合は、単純なFollow Modeなどでも十分使えるが、
もうちょっと応用してみて、アクションゲームやADVゲーム風のカメラワークを
作ってみたい。

仕様
  • カメラ自体は動かない
  • カメラはキャラクターを見る(LookAt)
  • カメラごとに範囲を指定
  • 指定範囲にプレイヤーが入ったらそのカメラに切り替わる
  • 切り替わる時のアニメーションは個別に設定
  • カメラ追加も出来るだけ容易に
実装

特に実装は要らないかなと思っていたのですが、
こんなのだけは作る必要がありました。

範囲に入れば優先度を上げ、範囲から出れば優先度を下げるだけ。

このスクリプトを各vcamにセットして、
SphereColliderもAddComponentします。(Is Trigger ONを忘れずに)
SphereColliderのRadiusで範囲指定して、実行すればもうそれだけでOK!

(二回目。これはこの範囲指定vcamを利用して作ってます。範囲に入るとカメラが切り替わっている)

カメラの追加はEditorから作るようにして、一気に必要なComponentの追加、
設定項目の設定なども一気にやっちゃうとさらに良さそう。

    public void AddColliderVirtualCamera()
    {
        var go = new GameObject();
        var sphereCollider = go.AddComponent<SphereCollider>();
        var cinema = go.AddComponent<Cinemachine.CinemachineVirtualCamera>();
        var cinemaCollider = go.AddComponent<CinemaCollider>();

        sphereCollider.isTrigger = true;
    }

Inspectorでボタン押下作れると便利。神、お世話になってます。
baba-s.hatenablog.com

個別にTweenアニメーション指定も出来る…だと?

Cinemachine Brainを改めて見てみる。
Default BlendというEase In Outという表示がセットされている。

f:id:ghoul_life:20170706193635p:plain

これはカメラとカメラを切り替える時のTweenアニメーションを指定している。
ここを個別に設定するには、Custom Blendsを使う。
Custom Blendsはほとんど直感で使えてしまうぐらい簡単だ。

Create Asset -> でMain Camera Blends.assetを作成して、
From , To , Style , Timeを追加していくだけだ。

Fromは「どこから」
Toは「どこまで」
Styleは「どのように」
Timeは「どれくらい」

だ。

f:id:ghoul_life:20170706193650p:plain

FromやToには
「ANY CAMERA」
という項目があり、
これはそのまんま、「いずれかのカメラ」を指す。

StyleのTweenアニメーションもいくつか用意されている

  • Cut -> 即座にパッと切り替わる
  • Linear -> 一定速度で切り替わる
  • Ease In Out -> 開始時と終了時に速度が変化する

など。この辺りは好きに触ってみてほしい。

これをガシガシ追加していけば個別にアニメーションを設定出来る

cinemachine神

ブログに起こすと凄く長くなったが、実際に触ると簡単だし!
でもきっと本気でカットシーン作ろうとしたら凄い苦労するんだろうなぁ。

また、今回作ったサンプルでぐりぐり走ってると結構カメラが激しく動くんでちょっと酔ってしまいました。
大きめなフィールドにしたり、移動速度を調整したり、カメラの切り替わり速度を調整すればその辺は改善できそう。
使うときはいい感じに調整してみて。

よくわからんメモ

CinemachineCollider.cs
といういかにも!なスクリプトがcinemachine側に用意されているのですが、
どうも使い方がよく分からず…自分の思っていた機能ではない?

API Reference無いかなぁ…。

【Unity】UnityのSceneを劇的ビフォーアフターしたい

概要

f:id:ghoul_life:20170703194328p:plain

こうしたい。

いいらいとさんがちょっと教えてくれた。
こういうこと教えてくれる人は少ないです。貴重な御方です。


なるほど、自分もやってみよう!

元のデータを用意

テキトーにSceneを作る。
テキトーにオブジェクトを配置してみる。
ここでは

  • Cube
  • Sphere
  • Cylinder

の3つをFloorに見立てたCubeの上に配置しました。

f:id:ghoul_life:20170703194359p:plain

ダサすぎワロタ。
これをオシャレに変えてみましょう。

まずは空から変えていこう

unityはSkyboxという空間の内側にSceneは描画されます。
そのSkyboxから変えていこう
Shaderで二色のカラーをグラデーションした感じでいかがでしょう。

神がいた。
github.com
もうこれこのままでええやん…?完成形や。

Lerp(A,B,C)はAからBに向かって変化した時に、Cの値で保管する関数

これを利用したMaterialを作って
Windows > Lighting > Setting

f:id:ghoul_life:20170703194442p:plain

f:id:ghoul_life:20170703194450p:plain

SkyboxのMaterialに作ったものをぽいっと設定すると
(眩しすぎるならIntensity Multiplierを減らしてみて)

f:id:ghoul_life:20170703194532p:plain

なんということでしょう、先程まで殺風景だった背面があっという間にオシャレなグラデーションの空間に!

ああ、次はカメラだ

これだけでもオシャレさはあるが、まだいける。
次は
Post Processing Stack(無料)
https://www.assetstore.unity3d.com/jp/#!/content/83912
を使う。
Unity5.5以上が対象になってしまうので、それ以前の人は
Import PackageからImage Effectで似たような機能が使えるはずだ。
(間違ってたらゴメンナサイ)

別で作ったProfileの値でPost Effect(色々描画して、最後にかけるエフェクト)を調整出来る

○Profileの作成
Project -> Create -> Post Processing Profileで作成

f:id:ghoul_life:20170703194633p:plain

○Post Efffectの設定
Post Processing BehaviorスクリプトをMain Cameraにアタッチ

○Post Effect profileの反映
作成したPostProcessing Profileを Post Processing Behavior 紐付けて準備完了だ。

PostProcessing Profileの設定

f:id:ghoul_life:20170703194656p:plain

  • Antialiasing
  • Ambient Occlution
  • Depth of Field
  • Bloom
  • Color Grading

の5つをONにしただけでこんな感じ。

f:id:ghoul_life:20170703194833p:plain

まるで豆腐のように美味しそうだったオブジェクトが、
あっという間に食べられなくなりました。

後はお好みでパラメータをいじったり、
point lightを入れてみたりするとそれだけでグッとそれっぽい。

f:id:ghoul_life:20170703194732p:plain

いかがでしょうか

オブジェクトには触らずに

  • Skybox
  • PostProcess

この2つを触るだけでグッと良くなるのがUnity。本当に劇的。
まだパラメータが一杯あるのでまだまだ出来ることはありそう。

【Unity】第三回unity1week「積む」に挑戦 後編 (開発編)

ghoul-life.hatenablog.com

ちゃんとゲームらしく

タイトル -> チュートリアル -> ゲーム -> 結果

と画面を作る。
必要な素材も大体ここで描く。
fireAlpacaで適当に描く。

他のゲームもいきなりゲーム画面のやつばかりやんけ!と
甘えから今まで一回もチュートリアルなんて作ってこなかったが、
今回はちゃんと作ろう。少しでもゲームをプレイして貰いたいし。
説明文に頑張って書く!という最終手段もあるけど。

開発

開発はスイスイ進んだ。流石に三回目ともなれば色々と積み重ねたものがある。
2DベースでuGUIでゲームを作るというあんまりやってはならない事かもしれないが、
それで行く事にした。

f:id:ghoul_life:20170627154507p:plain


各パーツと捉えてこれらは全て別々のCanvasにしている。
Canvasを複数作ってON/OFFをスッと切り替えて、画面遷移を制御するようにして、
順番やsortOrder , positionを細かく自由に変更可能にしていた。

縦書き

unityはデフォルトで縦書きに対応はしていない。
が、先人達がすでに挑戦してくれていたのを拝借。

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
using UnityEngine.EventSystems;
using System.Linq;

public class VerticalText : UIBehaviour, IMeshModifier
{
    private Text textComponent;
    private List<char> nonRotatableCharacters; // 回転させない文字リスト
    private List<char> adjustCharacters; // 位置を調整する文字リスト

    private void InitLimitCharacters()
    {
        if (nonRotatableCharacters == null)
        {
            nonRotatableCharacters = new List<char>();
            nonRotatableCharacters.Add('ー');
        }

        if (adjustCharacters == null)
        {
            adjustCharacters = new List<char>();
            adjustCharacters.Add('ァ');
        }
        
    }

    public new void OnValidate()
    {
        //base.OnValidate(); -> WebGLビルド時のみエラーになる。iOSやaOSは平気なのに…何故?
        textComponent = this.GetComponent<Text>();

        var graphics = base.GetComponent<Graphic>();
        if (graphics != null)
        {
            graphics.SetVerticesDirty(); // 更新有りと伝える
        }
    }

    public void ModifyMesh(Mesh m) { }
    public void ModifyMesh(VertexHelper verts)
    {
        var stream = ListPool<UIVertex>.Get();
        verts.GetUIVertexStream(stream);

        modify(ref stream);

        verts.Clear();
        verts.AddUIVertexTriangleStream(stream);

        ListPool<UIVertex>.Release(stream);
    }

    void modify(ref List<UIVertex> stream)
    {
        if (textComponent == null)
        {
            textComponent = this.GetComponent<Text>();
        }

        var characters = textComponent.text.ToCharArray();
        if (characters.Length == 0)
        {
            return;
        }

        InitLimitCharacters();

        float angle = 90.0f;
        // 頂点を云々する。1テキスト6頂点
        // 6文字ずつ進む
        for (int i = 0, streamCount = stream.Count; i < streamCount; i += 6)
        {
            int index = i / 6;
            if (IsNonrotatableCharactor(characters[index]))
            {
                continue;
            }

            // 文字の中央を取得(上なら[i+1])
            var center = Vector2.Lerp(stream[i].position, stream[i + 3].position, 0.5f);

            if (IsAdjustCharactor(characters[index]))
            {
                center = center + new Vector2(-2.0f , 2.0f);
            }

            // 頂点を回す
            for (int r = 0; r < 6; r++)
            {
                var element = stream[i + r];

                var pos = element.position - (Vector3)center; ;
                var newPos = new Vector2(
                    pos.x * Mathf.Cos(angle * Mathf.Deg2Rad) - pos.y * Mathf.Sin(angle * Mathf.Deg2Rad),
                    pos.x * Mathf.Sin(angle * Mathf.Deg2Rad) + pos.y * Mathf.Cos(angle * Mathf.Deg2Rad));
                element.position = (Vector3)(newPos + center);
                


                stream[i + r] = element;
            }
        }
    }

    bool IsNonrotatableCharactor(char character)
    {
        return nonRotatableCharacters.Any(x => x == character);
    }

    bool IsAdjustCharactor(char character)
    {
        return adjustCharacters.Any(x => x == character);
    }
}

OnValidate
https://docs.unity3d.com/ja/540/ScriptReference/MonoBehaviour.OnValidate.html

ModifyMesh
https://docs.unity3d.com/ja/current/ScriptReference/UI.IMeshModifier.ModifyMesh.html

簡単に言えばUIの更新イベントフックして、そこで回転させている。
縦書きにするとァやー(長音符)の位置が少しズレてしまい、違和感を感じるので、
それを調整するようにしました。
また、ListPoolはUnityのソースから拝借。

神様ありがとうございます!
http://madnesslabo.net/utage/?page_id=6042

注意点としては、
WebGLビルドした時に

error CS0117: `UnityEngine.EventSystems.UIBehaviour' does not contain a definition for `OnValidate'

とbase.OnValidate()が怒られる。
これなんでだろー?iOSやaOSビルドは問題が無かった。
とりあえずコメントアウトしていて、特に影響なかったのでそのまま…。

チュートリアル

一番苦労したのはマスタとかコメントに書いたけど、
実装側でちょっと困ったのはこれ。
長くなっちゃったので別記事で

ghoul-life.hatenablog.com

また1つ勉強になった。

リリース

後は体裁を整えたりデバッグしたりで日曜日夜に提出。

締め切りは守る。
これゼッタイ。
みんな決められた時間内で平等に頑張ってる。(と思っている)

感想

変なゲームを好きに作ろうとしたら
被ったー!盛大に被ったー!しかも結構多いw

もうすでに三回目になり、
付いていくのもギリギリですが、
毎回本当に勉強になるなぁ。
少しずつ出来ることが広がっていくのを感じる。

今まではなんかゴチャ混ぜ感があって、
適当に詰め込んだ感じがあったけど今回は割りとまとまった方だと思う。
ゲーム部分をもっと詰めたいな。なんか本とか読んでみよう。

そろそろTweet機能を付けてみてもええか…。