おもちゃラボ

Unityで遊びを作ってます

【Unity】バネの動きを実現する3つの方法

Unityでバネのアニメーションを作る場合、大きく分けて3つの方法があります。

バネの挙動をシミューレートできるSpring2DコンポーネントはPhysicsの物理挙動に従って計算するため、リアルな挙動にしたい場合はこれを使うと良いでしょう。ちょっとバネバネアニメーションを付けたい、という場合には大げさで取り回しが悪かったりします・・・

また、Animationを自分で組み立てる方法もあります。この場合、バネの強度や移動方向など、値が決め打ちになってしまうので、挙動を変えたい場合にはすこし手間が大きい方法です。

最後に、もっとも柔軟性の高い方法として、バネの挙動をスクリプトでシミュレートする方法があります。スクリプトでバネの動きを実現するのは意外と簡単なので、オススメの方法です。

では、それぞれの方法を使う方法を紹介します。

Spring Joint 2Dを使う方法

Spring Joint 2Dコンポーネントを使う場合は、バネに従って動かしたいオブジェクトにこのコンポーネント(Physics2D → Spring Joint 2D)をアタッチします。Spring Joint 2Dをアタッチすると、自動的にPhysics 2Dコンポーネントもアタッチされます。

f:id:nn_hokuson:20170522205248j:plain:w300

Spring Joint 2Dコンポーネントをアタッチすると、シーンビューには次のように緑の線で仮想のバネが表示されます。

f:id:nn_hokuson:20170522213037p:plain:w400

バネを別のオブジェクトに固定するには「Connected Rigid Body」を設定します。ジョイントを固定する座標は「Anchor」、自然長は「Distance」、強度は「Frequency」で設定します。よく使用するのはこれらのパラメータです。ここでは、原点に接続された長さ1のバネをシミュレートします。今回は、Anchor=(0, 0)、Distance=1、Frequency=3に設定しました。

f:id:nn_hokuson:20170522205320p:plain:w300

これ以外のパラメータについては、Unityのサイトを参照してください。

docs.unity3d.com

ゲームを実行すると、次のようにボールが原点に向かってバネ振動します。

f:id:nn_hokuson:20170522213647g:plain

また、上のように原点とボールの間に線を描きたい場合は、次のスクリプトをボールにアタッチしてください。GizmoクラスのDrawLineメソッドを使ってシーンビューに線を描画します。

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class BallController : MonoBehaviour 
{
    void OnDrawGizmos ()
    {
        Gizmos.DrawLine (transform.position, Vector3.zero);
    }
}

スクリプトをアタッチ後、Gameビューの「Gizmos」をクリックして有効化することで、ゲームビューでも線を描画することができます。

f:id:nn_hokuson:20170522213816p:plain:w450

Animationを使う方法

バネのAnimationを自分で組み立てる場合は、Animatorをボールにアタッチします。ボールを選択した状態で、メニューバーから「Window」→「Animation」を選択してください。Animationウインドウが開くので「spring@ball」という名前でAnimation Clipを保存します。

f:id:nn_hokuson:20170522205337p:plain

続いてタイムラインでバネのアニメーションを組み立てます。バネの動作なので時間がたつに従って振幅が減衰するカーブを作成します。今回は次のように、x軸方向にのみ振動するバネを作成しました。

f:id:nn_hokuson:20170522205344p:plain

実行結果はこちらです。アニメーションを自分で作るのはセンスが必要ですね(笑)

f:id:nn_hokuson:20170523203123g:plain

こちらも分かりやすいように、原点とボールの間にGizmoを使って線を描画しています。この方法は上の「Spring Joint 2D」の項目を参照ください。

バネの挙動をスクリプトで書く方法

最後にスクリプトでバネの挙動をシミュレートする方法を紹介します。バネの挙動はイージングの動作と似ています。イージングではターゲット座標と現在の座標の差分が「ボールの移動速度」になりますが、バネではこの差分が「ボールの加速度」になります。

f:id:nn_hokuson:20170523203828p:plain:w300

つまり、ターゲット座標が遠ければ遠いほど大きな力で、ターゲット座標へ向かうことになり、ターゲット座標に近づくにつれて、かかる力も小さくなります。

f:id:nn_hokuson:20170523204225p:plain:w300

イージングでは、ターゲット座標と現在の座標の差分は速度に変換されるため、目標地点を行き過ぎることは決してありませんが、バネの場合は加速度に変換されるので、基本的にはオーバーシュートして、また戻ってを繰り返します。この挙動がバネっぽさに繋がるのですね。

ボールの動きを制御するC#スクリプト(BallController.cs)を作成し、ボールオブジェクトにアタッチします。BallControllerのプログラムは次のようになります。

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

public class BallController : MonoBehaviour {

    Vector3 targetPos;
    Vector3 acc, vel, pos;

    void Start () 
    {
        acc = vel = Vector3.zero;
        targetPos = pos = new Vector3(0, 0, 0);
    }

    void OnDrawGizmos ()
    {
        Gizmos.DrawLine (this.pos, this.targetPos);
    }

    void Update () 
    {
        if (Input.GetMouseButton (0))
        {
            this.pos = Camera.main.ScreenPointToRay(Input.mousePosition).origin;
            this.pos.z = 0;
        }
        else
        {
            Vector3 diff = this.targetPos - this.pos;
            this.acc = diff * 0.1f;
            this.vel += this.acc;
            this.vel *= 0.9f;
            this.pos += this.vel;        
        }
        transform.position = this.pos;
    }
}

完成図はこんな感じで、ボールをドラッグすると移動でき、マウスを離すと原点に向かってビヨンビヨンします。原点とボールが仮想のバネでつながれているイメージですね。

f:id:nn_hokuson:20170523204523g:plain

どうでしょうか?スクリプトでバネを実現する方法が一番気持ち良い動きになっている気がしませんか?慣れもあるとは思いますが、細かい動きまで微調整できるのでオススメですよ!

【Unity】uGUIの文字を一文字ずつ別々に動かす

uGUIを使って文字列を表示するにはTextを使います。このTextを使って一文字ずつランダムに動かす方法を紹介します。UIの文字でも場所によっては動いている方が楽しいですね〜

f:id:nn_hokuson:20170521083830g:plain

UIのTextを一文字ずつ動かす方法

UGUIのTextはModifyMeshメソッドを使うことで、文字を構成するポリゴンにアクセスすることが出来ます。各ポリゴンの頂点座標を配列として得られるので、その頂点座標を一文字ごとに、違った方向に動かすことで文字がバラバラの動きをします。

f:id:nn_hokuson:20170521085157p:plain:w500

文字を構成するポリゴンは4頂点ではなく、6頂点で構成されるので注意が必要です。次のような並びになっているようです。

f:id:nn_hokuson:20170521085442p:plain:w180

したがって、頂点配列の頂点を6個ごとにばらばらの方向に動かすことで上のような文字の動きが実現できます。

ランダムに動かす方法はなんでも良いのですが、ここでは半径Rの円周上をランダムに指すベクトルを方向ベクトルとして使っています。

f:id:nn_hokuson:20170521085744p:plain:w230

では、具体的な実装方法を紹介します。

uGUIでTextを配置する

まずはuGUIでTextを配置します。ヒエラルキービューから「Create」→「UI」→「Text」を選択し、インスペクタから適当な文字を設定して下さい。文字の設定はText欄から行います。その他にも、FontやFont Size、Colorなども調整します。

f:id:nn_hokuson:20170521090146j:plain

テキストを動かすスクリプトを作成する

続いて、文字を動かすためのスクリプトを作成します。プロジェクトビューで右クリックして「Create」→「C# Script」を選択し、「TextController」という名前で保存して下さい。

スクリプトが作成できたら次のプログラムを入力して下さい。

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

public class TextController : BaseMeshEffect {

    float time = 0;
    float radius = 1.5f;

    public override void ModifyMesh ( UnityEngine.UI.VertexHelper vh)
    {
        if (!IsActive())
            return;

        List<UIVertex> vertices = new List<UIVertex>();
        vh.GetUIVertexStream(vertices);

        TextMove(ref vertices);

        vh.Clear();
        vh.AddUIVertexTriangleStream(vertices);
    }

    void TextMove( ref List<UIVertex> vertices )
    {
        for (int c = 0; c < vertices.Count; c += 6)
        {
            float rad = Random.Range(0,360) * Mathf.Deg2Rad;
            Vector3 dir = new Vector3 (radius * Mathf.Cos (rad), radius * Mathf.Sin (rad), 0);

            for(int i = 0; i < 6; i++)
            {
                var vert       = vertices [c+i];
                vert.position  = vert.position + dir;
                vertices [c+i] = vert;
            }
        }
    }

    void Update()
    {
        time += Time.deltaTime;
        if (time > 0.05f)
        {
            time = 0;
            base.GetComponent<Graphic> ().SetVerticesDirty ();
        }
    }
}

uGUIのTextはBaseMeshEffectクラスのModifyMeshメソッドをオーバーライドすることで、テキストの表示に使われているポリゴンの頂点にアクセス出来るようになります。

このあたり、Unity5.1、5.2、5.3とどんどん仕様が変わっているようなので、バージョンによってはうまく動かないかもしれません。上のプログラムはUniy5.6で動作を確認しています。

toriden.hatenablog.com

取り出した頂点をTextMoveメソッドの引数に渡しています。TextMoveメソッドの中では6頂点ごとにランダムなベクトルを定義して、頂点座標をベクトル方向に移動させています。

最後にUpdateメソッドから逐次SetVerticesDirtyメソッドを呼ぶことで、文字が動くアニメーションを再生することが出来ます。

おまけ(もっと、もにょもにょ動かす)

上の例では、文字を構成する6頂点はまとめて同じ方向に動かしていましたが、これを1頂点ごとに方向も長さもランダムな方向に動かすと、さらにもにょっとした動きになりました。

f:id:nn_hokuson:20170521090514p:plain:w250

こんな感じです。綺麗かどうかは置いておいて(笑)

f:id:nn_hokuson:20170521090308g:plain

TextMoveメソッドの中は次のようになっています。頂点2と頂点3、頂点0と頂点5は同じ座標を指すので、同じ方向のベクトルを与えています。

            Vector3[] dirs = new Vector3[6];

            for (int i = 0; i < 6; i++)
            {
                float rad = Random.Range (0, 360) * Mathf.Deg2Rad;
                float r = radius + 6.5f * Random.value;
                Vector3 dir = new Vector3 (r * Mathf.Cos (rad), r * Mathf.Sin (rad), 0);

                dirs [i] = dir;
            }

            dirs [3] = dirs [2];
            dirs [5] = dirs [0];

            for(int i = 0; i < 6; i++)
            {
                var vert       = vertices [c+i];
                vert.position  = vert.position + dirs[i];
                vertices [c+i] = vert;
            }

まとめ

ModifyMeshメソッドをオーバーライドすることで、uGUIのTextの各頂点座標にアクセスする方法を紹介しました。動かし方はまだまだバリエーションが出せると思うので、是非試してみて下さいね〜

[asin:4839956405:detail]

【Unity】Macで使えるNative Pluginを作る(OpenCV編)

UnityでOpenCVを使いたい場合は、OpenCV for Unityを使う方法と、自分で使いたい関数だけをNative Pluginとして用意する方法があります。ここでは、アセットを使わず、Native PluginからOpenCVの機能を使う方法を紹介します。

UnityでOpenCVを使ったNative Pluginを作る手順は次のようになります。

Homebrewをインストール

コマンドラインで次のコマンドを打ってHomebrewをインストールします

ruby -e "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install)"

パッケージを最新にするため、Homebrewをアップデートします。

brew update

OpenCVをインストールする

brew install -v cmake
brew tap homebrew/science
brew install opencv3

これで/usr/local/Cellar/opencv3以下に、OpenCVのライブラリ一式がインストールされました。

XcodeでNative Pluginを作る

続いて、Unityから受け取った画像をNative Pluginでグレースケールに変換して、それをUnityに返すプログラムを作ります。

f:id:nn_hokuson:20170512201403j:plain:w500

Unityで使うMacのNative PluginはBundleとして作成します。Xcodeを起動してMacOS>Framewrok & Library>Bundleを選択してください。「Product Name」は「TestPlugin」にしておきます。

f:id:nn_hokuson:20170512192724p:plain:w400

XCodeからOpenCVが使えるようにパスを通しておきましょう。ナビゲーションビューでTestPluginを選択し、「Build Settings」→「Header Search Path」の欄に「/usr/local/Cellar/opencv3/3.2.0/include/」と入力します(バージョンは適宜書き換えてください)。

f:id:nn_hokuson:20170512200032j:plain

次に「Build Phasesを」選択し、Link Binary With Librariesの欄に使用するライブラリをドラッグ&ドロップします。とりあえず次のものをドラッグ&ドロップしておきましょう。

・libopencv_core.3.2.0.dlyb
・libopencv_highgui.3.2.0.dlyb
・libopencv_imageproc.3.2.0.dlyb

 続いてNative Pluginとして使うためのプログラム(Plugin.cpp)とヘッダファイル(Plugin.pch)を作成します。Unityから使えるようにヘッダファイルは「extern "C"」でくくっておいてください。

#include "Plugin.pch"
#include <opencv2/opencv.hpp>

void conv(unsigned char* arr, int w, int h)
{
    cv::Mat img(cv::Size(w, h), CV_8UC4, cv::Scalar(0, 0, 0));
    cv::Mat gray(cv::Size(w, h), CV_8UC1, cv::Scalar(0, 0, 0));

    // char* -> Mat
    int i = 0;
    for(int y = 0; y < img.rows; ++y){
        for(int x = 0; x < img.cols; ++x){
            for(int c = 0; c < img.channels(); ++c){
                img.data[ y * img.step + x * img.elemSize() + c ] = arr[i++];
            }
        }
    }
    
    // RGBA -> GRAY
    cv::cvtColor(img, gray, CV_RGBA2GRAY);
    cv::cvtColor(gray, img, CV_GRAY2RGBA);
    
    // Mat -> char*
    i = 0;
    for(int y = 0; y < img.rows; ++y){
        for(int x = 0; x < img.cols; ++x){
            for(int c = 0; c < img.channels(); ++c){
                arr[i++] = img.data[ y * img.step + x * img.elemSize() + c ];
            }
        }
    }
}
extern "C" {
    void conv(unsigned char* arr, int w, int h);
}

ここではグレースケールに変換するconv関数を作っています。最終的にはこのconv関数をUnity側から呼び出す事になります。引数にはピクセルの配列と画像の縦幅・横幅を受け取っています。Unity側ではcv::Mat型は使えないため、ここではunsigned charの配列として、画像のピクセルデータをやり取りしています。conv関数の内部ではOpenCVを使ってグレースケールに変換した後、再度ピクセル配列に書き戻しています。

メニューバーから「Product」→「Build」を選択し、プロジェクトをビルドするとProductsフォルダにTestPlugin.bundleが生成されます。

f:id:nn_hokuson:20170512193700p:plain:w250

UnityからNative Pluginを叩いて実行する

Unityでプロジェクトを作成し、Assetフォルダの下にPluginフォルダを作ってください。作成したPluginフォルダの中に先程の「TestPlugin.bundle」を入れます。このとき、XcodeのProductsフォルダからUnityに直接ドラッグ&ドロップすることが出来ます。

f:id:nn_hokuson:20170512194037p:plain:w500

最後にこのバンドルをNative Pluginとして使用するためのスクリプトを作成します。ImageProcessing.csという名前でスクリプトを作り、次のプログラムを入力して下さい。

public class ImageProcessing : MonoBehaviour {

    [DllImport ("TestPlugin0")]
    private static extern void conv(IntPtr img, int w, int h);

    private Color32[] pixels_;
    private GCHandle pixels_handle_;
    private IntPtr pixels_ptr_ = IntPtr.Zero;
    private Texture2D mainTexture;

    void Start () {

        // ピクセルデータを取り出す
        Texture2D mainTexture = (Texture2D) GetComponent<Renderer> ().material.mainTexture;
        mainTexture.filterMode = FilterMode.Point;
        pixels_ = mainTexture.GetPixels32();
        pixels_handle_ = GCHandle.Alloc(pixels_, GCHandleType.Pinned);
        pixels_ptr_ = pixels_handle_.AddrOfPinnedObject();
    
        int w = mainTexture.width;
        int h = mainTexture.height;

        // Native Pluginを呼び出す
        conv (pixels_ptr_, w, h);

        // ピクセルデータからテクスチャを作る
        Texture2D change_texture = new Texture2D (w, h, TextureFormat.RGBA32, false);
        change_texture.filterMode = FilterMode.Point;
        change_texture.SetPixels32 (pixels_);
        change_texture.Apply ();

        GetComponent<Renderer> ().material.mainTexture = change_texture;
    }
}

このスクリプトではテクスチャからピクセルデータを取り出し、IntPtr型に変換してからNative Pluginにデータを渡しています。Native Pluginの中でグレースケール変換して、そのデータを再度Unity側でテクスチャにセットしています。

このスクリプトをPlaneオブジェクトにアタッチしてください。planeには適当な画像を配置しておきます。

f:id:nn_hokuson:20170512194449j:plain

実行すると、Planeに設定した画像が、グレースケールの画像に変換されました。なんか白黒にすると遺影みたいですが・・・(笑)

f:id:nn_hokuson:20170512194455p:plain:w400

プラグインのロードには注意が必要です!

Unityでは一度スクリプトからNative Pluginをロードすると、Unityを再起動するまでUnloadされない仕様になっています。

Once a native plugin is loaded from script, it's never unloaded. if you deselect a native plugin and it's already loaded, please restart Unity.

f:id:nn_hokuson:20170512194614p:plain:w350

つまり、Native Pluginのプログラムを更新して、古いものと置き換えたとしてもUnityを再起動するまでは古いプラグインが使われます・・・・ひどい

私は毎回、Pluginの名前を変えてからプロジェクトにドラッグ&ドロップしています。なぜUnityをだましだまし使わなくてはいけないのか・・・

まとめ

この記事では、Native PluginでOpenCVを使う方法を紹介しました。また、OpenCV for Unityは$95でAsset Storeで販売されています。Native Pluginなんて作るの面倒くさいよ、という方はこちらを使ってみて下さい。

f:id:nn_hokuson:20170512192332j:plain:w400

[asin:4877833986:detail]