20190512のUnityに関する記事は6件です。

【Unity】ProBuilderで生成したオブジェクトの中心の位置がずれる

問題

ProBuilderを使って生成したオブジェクトの中心の位置がずれる。

スクリーンショット 2019-05-12 午後10.57.29.png
スクリーンショット 2019-05-12 午後10.57.14.png

白いCubeはUnityのHierarchyで普通にCreateし、
グレーのCubeはProBuilder -> New Shape -> Build Cube で作成しました。
どちらも位置は(0, 0, 0)です。

確かにグレーのCubeは位置がずれています。

原因

スクリーンショット 2019-05-12 午後11.04.33.png
スクリーンショット 2019-05-12 午後11.04.41.png

グレーのCubeのCenterの位置とPivotの位置が異なっていることが原因です。

解決方法

PivotをCenterに一致させます。

対象のオブジェクトを選択 -> ProBuilder Windowを開く -> Center Pivot

スクリーンショット 2019-05-12 午後11.20.03.png

これでPivotがCenterに一致しました。
スクリーンショット 2019-05-12 午後11.21.53.png

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Unity+Node.jsで1対1の簡単なオンラインシューティングゲームを作った話 導入編

この記事に関わる記事一覧

導入編(現在の記事)
Node.js編(投稿予定)
Unity編(投稿予定)

はじめに

Node.jsを使ってみたく、実際に手を動かして何か作ってみるのが一番だと思い、
自分なりにプレイヤーのマッチングや同期の取り方を考えてやってみようと思いました。

Node.js(サーバー側)よりもUnity(クライアント側)で苦戦しました...

私はNode.js,リアルタイム通信の知識がそこまであるわけではないので、
素人なりにどう考えて実装していったかの記録を残していきたいと思い記事を書き始めました。

コードの書き方に正解はないと思うので、これから書いていく記事を通して
今回作成したゲームの作り方の概念だけ書いていこうかなと思います。

成果物

今回作成したアプリはAndroid,iosでリリースしました。
AppStore
PlayStore
Mac版
Windows版
Android版は実機で動きを確認できていないので、動かなかったら申し訳ございません。

動きを確認するためにはデバイスが二つ必要になります。(PCとスマホでも確認できます)
2019/5/11現在サーバー稼働中ですのでマッチング開始ボタンを押すと同時に押した人(5秒以内)とマッチングします。

操作方法

ドラッグ:移動
タップ :自機が向いている方向に弾を発射

スクリーンショット

IMG_5512.PNG

IMG_5514.PNG

通信プロトコル

今回はUDPを選択しました。
マルチ対戦で使える通信プロトコルは以下の3種類あると思います。

通信プロトコル 特徴
UDP 速い!!でも、ちゃんと届いたか、届く順番が保証されない
TCP UDPとは違い確実に相手に届ける。(UDPよりは遅い)
WebSocket 低コストのTCP?Webで使うソケット通信

WebSocketはよく分かっていないのですが、
Webブラウザで手軽に双方向通信ができる!!そんなイメージが私の中にあります。
詳しいことは調べていただければと思います...

今回作成するのは毎フレーム同期が必要なリアルタイムで動くシューティングゲームなのでUDPにしました。

同期の取り方

はじめの方は入力情報だけ同期すれば大丈夫だろうと考えていました。
しかし、実装していくうちに2つのクライアント間で状態が異なっていき
終いにはプレイヤーの位置が違う、勝敗結果が異なるということになってしまいました。

今回は自前で衝突応答、判定などを作るのが面倒くさかったので、Rigidbody2D使ってしまおうと
考えたのがよくありませんでした。

Unityの中身で物理の処理がどのタイミングで実行されているのかを把握するのが難しいので、
このくらいの簡単なゲームであれば、自分で衝突判定や衝突応答を書いてしまって実行されるタイミングを
管理できるようにした方が後ほど苦しまなくなると思います。

結果的には以下の3つで同期を取ることになりました。

  • 入力情報(バーチャルJoystic、発射)
  • 座標
  • 弾が当たったかどうかの判定

また今回は通信する情報をJson形式で扱いました。

位置と入力の同期

まず何も考えずにプレイヤーを動かしてと言われたら、
毎フレーム入力に応じてプレイヤーを動かす処理を書くと思います。

しかし、このように実装してしまっては同期は取れません。

プレイヤーAとBがいるとします。
プレイヤーAの端末では現在5フレーム目で、プレイヤーBの端末も現在5フレームであったとします。
しかし、プレイヤーAの端末からプレイヤーBの端末まで情報が届くのが同じ5フレーム内であるという保証はありません。
通信には遅延が付き物です。
また、端末によってフレームの更新速度も違うのでプレイヤーAの端末では5フレーム目でも、
プレイヤーBの端末は4フレーム目が実行中かもしれません。

自分の端末で自機を動かしていて、情報が到着次第相手機を動かすような実装では同期は取れません。
この状態で作っていって最後に同期が取れないと気づいた頃には後の祭りです。

詳しくはUnity編で書きたいと思うのでここではざっくりと書きます。

Queueというデータ構造を使って入力情報を毎フレーム入れていき、相手の入力情報が届き次第
Queueから自分の入力情報を取り出して相手の入力情報と一緒に実行します。

キューとは
オブジェクトをどんどん追加していけるデータ構造で
一番最初に追加した古いオブジェクトから順に取り出していきます。(先入れ先出し)

なので、入力した情報は数フレーム遅れて実行されることになります。
私は3フレーム以内に相手の情報が届かなかったら、自分の入力情報を追加することをやめて待つようにしました。
もし待たなければ、情報の到着が30フレーム遅れて到着した場合約0.5秒前の自分の操作が実行されてしまいます。

位置と入力の同期では以下のJSONで送り合いました。

{
    "type":"input",
    "own":"{\"id\":\"16aab79959a24f\",\"name\":\"Black\",\"port\":\"\",\"address\":\"\"}",
    "rival":"{\"id\":\"16aab79983f274\",\"name\":\"White\",\"port\":\"61374\",\"address\":\"***.***.**.**\"}",
    "requireNextFrame":12,
    "inputObjects":
    [
        "{\"frame\":13,\"axisX\":0.0,\"axisY\":0.0,\"isFire\":false,\"fireDirX\":0.0,\"fireDirY\":0.0,\"posX\":0.0,\"posY\":-5.0,\"rotZ\":0.0}",
        "{\"frame\":14,\"axisX\":0.0,\"axisY\":0.0,\"isFire\":false,\"fireDirX\":0.0,\"fireDirY\":0.0,\"posX\":0.0,\"posY\":-5.0,\"rotZ\":0.0}"
    ]
}
Key 内容
type このJSONは何の情報かを判別するためのもの(全てのJSONに付けた)
own 自分の情報のJSON文字列(UserObj)
rival 相手の情報のJSON文字列(UserObj)
requireNextFrame 現在何フレーム目からの相手の入力情報が欲しいか
inputObjects 位置情報・入力情報(配列)(InputObj)

C#でJSONをパースする都合で、UserObj,InputObjをJSON文字列にしています。

UserObj

Key 内容
id ユーザーの識別ID(使わなかった)
name ユーザー名
port ポート番号
address グローバルIPアドレス

自分(own)のアドレス、ポート番号は特に必要がなかったので入れませんでした。

InputObj

Key 内容
frame 何フレーム目の情報か
axisX バーチャルパッドのX軸(-1.0~1.0)
axisY バーチャルパッドのY軸(-1.0~1.0)
isFire 弾が発射されたか(false,true)
fireDirX 弾の発射方向(X方向)
fireDirY 弾の発射方向(Y方向)
posX 現在の位置X
posY 現在の位置Y
rotZ 現在の回転角度Z(degree)

InputObjを複数送る理由は、UDP通信だとパケットロストが発生して情報が届かない時があるからです。
例えば5フレーム目で送った入力情報が相手に届かず、6フレームの入力情報は相手に届いた場合でも、
6フレームの入力情報に5フレーム目の入力情報が含まれているので届けることができます。

弾が当たったかどうかの同期

位置・入力の同期が取れたことでプレイヤー同士の位置がずれることなくなって満足していました...

しかし、何回かプレイをしてみると2つの端末の間でHPの値が異なったのです。
多分以下の原因があると思います。

  • 計算の誤差があり弾が当たっていたり、当たらなかったり2つの端末で結果が異なった
  • 弾の位置の同期はしていなかったので物理演算の結果が異なっていた

本来弾の位置の同期もとったほうがいいとは思ったのですが、面倒くさかったので
ある弾がプレイヤーに当たった場合、JSONで相手に情報を送って
お互いの端末でその弾がプレイヤーに当たっていた場合ダメージ判定をするようにしました。

弾の当たった情報を以下のJSON形式で相手に送りました。

{
    "type":"hit-bullet",
    "bulletType":"BLUE",
    "fireFrame":152,
    "own":"{\"id\":\"16aabc7087860\",\"name\":\"Black\",\"port\":\"\",\"address\":\"\"}",
    "rival":"{\"id\":\"16aabc70728244\",\"name\":\"White\",\"port\":\"62167\",\"address\":\"***.***.**.**\"}"
}
Key 内容
type このJSONは何の情報かを判別するためのもの(全てのJSONに付けた)
bulletType 弾の色(BLUE,RED)
fireFrame 当たった弾が発射されたフレーム
own 自分の情報のJSON文字列(UserObj)
rival 相手の情報のJSON文字列(UserObj)

成果物で動きを確認していただければ分かると思うのですが、
お互いの端末で自機は赤で、相手機は青で表示されています。
なので、送るときはbulletTypeの色を逆にして送りました。

1フレームに打てる弾は1個なのでfireFrameはその弾固有のIDになります。

自分の端末で当たった弾の情報をListに追加していき、
相手から送られてきた弾の情報もListに追加していきます。
この二つのListを照らし合わせたときにbulletType,fireFrameが一致するものがあれば、
bulletTypeに応じて相手または自分へのダメージ処理を行うようにしました。

最後に

次回はNode.jsの実装について書いていきたいと思います。
ほとんど自分の備忘録みたいな感じで書いているので分かりにくいところが多かったと思います。
今回の記事を書き終えてみて、もっと情報を整理してプレイヤーの位置だけを同期するサンプルを作成して
ワークショップ形式で説明を書いたほうが分かりやすいかなと思いました。
このシリーズの記事を書き終えたらそっちの方も書いてみようかなと思います。

自分なりの方法でやっているので、間違っているところがあるとは思いますがご了承ください。
最後まで記事を読んでいただきありがとうございました!

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Unityちゃんの頂点データを取得するだけ

最初に

点群データを扱う研究をしてたので,3Dキャラクターの点群データもほしいと思ってやりました.
Unityも久しぶりに使うのであまり詳しくないです.
最終的には頂点数*次元数の行列を得ます.
参考にした記事 : http://backbone-studio.com/brog-unity01/

Unityちゃん

Asset StoreからUnityちゃんをダウンロードし,unitychan.prefabをHierarchyビューにドラッグ&ドロップします.
unitychan.prefabの構造を眺めてみると以下のようになっています.
Hierarchy2.jpg
mesh_rootという空オブジェクトの子オブジェクトにそれぞれのパーツが入っており,それぞれにSkinned Mesh Rendererという頂点情報が乗ったメッシュがアタッチされています.
ここから取得するとよさそうです.

コード

以下のコードを書きます.

PointCloud.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class ChildAccess : MonoBehaviour {

    public string filename = "test";
    void Start () {
        // 頂点数を数える
        int vtx_num = 0;
        foreach(Transform child in gameObject.transform){
            SkinnedMeshRenderer skin = child.GetComponent<SkinnedMeshRenderer>();
            vtx_num += skin.sharedMesh.vertices.Length;
        }
        Vector3[] vtx_posi_array = new Vector3[vtx_num];

        // 頂点座標を取得
        int count = 0;
        foreach(Transform child in gameObject.transform){
            SkinnedMeshRenderer skin = child.GetComponent<SkinnedMeshRenderer>();
            Mesh child_mesh = skin.sharedMesh;
            for(int i = 0; i < child_mesh.vertices.Length; i++){
                float x = child_mesh.vertices[i].x;
                float y = child_mesh.vertices[i].y;
                float z = child_mesh.vertices[i].z;
                vtx_posi_array[count] = new Vector3(x, y, z);
                count++;
            }
        }

        // csvファイルに書き込む
        try{
            filename = filename + ".csv";
            bool append = false;
            using(var sw = new System.IO.StreamWriter(@filename, append))
            {
                for(int i = 0; i < vtx_posi_array.Length; ++i){
                    sw.WriteLine("{0},{1},{2}", vtx_posi_array[i].x, vtx_posi_array[i].y, vtx_posi_array[i].z);
                }
            }
        }
        catch(System.Exception e)
        {
            Debug.Log(e.Message);
        }
    }
    void Update () {

    }
}

簡単に説明すると,mesh_rootの子オブジェクトを順番に走査しつつ頂点座標を取得.座標の配列に格納して,最後csvファイルで出力します.

このファイルをmesh_rootにアタッチして実行するだけです.最終的には11407*3の行列が取得できます.

おわりに

本当は頂点の色情報も加えて頂点数*(次元数+3色)の行列を得たかったんですが,上手くできませんでした.
なにか別の方法を試してもよさそうです.
あと他の3Dモデルもこのような構造だとこのファイル使い回せるんですがどうなんでしょう.

© UTJ/UCL

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【Unity】サーバーなどを用意せずにオフライン報酬を実装する方法

はじめに

はじめまして,Nわかです。
初めての投稿でもあるので、軽く自己紹介。
現在起業した友人のもとでUnityエンジニアとして,大学に通いつつ働かせてもらっています。
現在1つのスマホ向けカジュアルゲーム制作を任されています。リモートでOKとのことで,ほぼ家や授業の合間などに作業させてもらっています。ホンマありがたいです。

その担当したゲームの機能の一つに「オフライン報酬」というものがあります。これはゲームをプレイしていなくても,ゲーム内コインがゲームをやっていなかった時間分貰えるもらえるというものです。
ソシャゲの時間経過で回復する体力(ゲームプレイ時に消費)などが近しい機能でしょう。

この機能を実装するにあたって,結構ググりましたが,直接的な記事は見当たりませんでした。
自分用の備忘録として,そして似たような機能を実装する方の助けに少しでもなれればと思います。

仕様

今回実装するにあたり,決められていた仕様をざっくりと。

  1. サーバーとのやり取りなどはしない
  2. 1分当たりに獲得できる報酬の量は決まっており,その量は成長させられる
  3. アプリをキルして再開もしくはキルせず再開(いったん別アプリをアクティブにしていた)のどちらの場合においても,報酬が発生していればポップアップ表示を行い報酬が獲得できる

本当にざっくりと、ですが。

実装

それでは実際に実装していくわけですが,やることとして大まかに2つ項目があります。

報酬の計算

サーバーとのやり取りをしないとのことなので,ローカルで時間を取得して前回の時間との差分を出して報酬を計算してあげる必要があります。

    private void SetOfflineEarning()
    {
        double minutes = CalcSecond(DateTime.UtcNow, _endTime) / 60;
        _endTime = DateTime.UtcNow;
        _earnOffline = (int) minutes * _offlineEarning;
        Save();
    }

DateTime.UtcNowで現在のUTC時刻を取得可能。Save();では,今回の機能に関係あるものでは_endTimeのみ保存しています。また,記載していませんが,ゲーム開始時に_endTimeを読み込んでいます。
1分あたりの報酬なので,今回は60で割っています。

ちなみにCalcSecond()は以下の通り。DateTimeでのUTC時刻の差分を出し,トータル秒数を返却するメソッドです。

    private double CalcSecond(DateTime now, DateTime prevTime)
    {
        return (now - prevTime).TotalSeconds;
    }

これで_earnOfflineにオフライン報酬が代入されている状態になったので,これを全体の稼ぎを示すメンバにでも加算してあげればよいでしょう。

アプリから離れたことの検知

アプリ起動時だけでなく,他のアプリから戻ってきた際にもポップアップ表示をしてあげないといけないので,アプリから離れたことを検知してあげる必要があります。

Unityには以下のようなprivateメソッドが用意されています。
OnApplicationPause(bool pauseStatus)
これはアプリが他のアプリに移ったりホーム画面に戻ったりすることによって一時中断されたときや,逆にアプリケーションに戻ってきた際に呼ばれるものです。
つまり,pauseStatusfalseの際は戻ってきたということになるので,ここにオフライン報酬の処理を記述してあげれば良さそうです。
ここで1つ注意すべき点としては,GoogleのAdMobを導入している際です。リワード広告を表示するとこれもアプリケーションを離れた判定を受けます。つまりリワード広告を閉じて戻ってきた際にも上記メソッドが呼ばれてしまうのです。
よって,もしリワード広告を導入しているのであれば,リワード広告を表示していた場合は無視するようにするべきでしょう。

    private void OnApplicationPause(bool pauseStatus)
    {
        //ゲーム中は無視
        if (GameSceneManager.IsGameStart) return;
        //リワード広告後は無視
        if (_isRewarding) return;
        if (!pauseStatus) _isOpenOfflineEarning = true;
        OpenOfflineEarning();
    }

OpenOfflineEarning()は以下のようになっています。

    public void OpenOfflineEarning()
    {
        //起動時にはオフライン報酬を表示
        if (_isOpenOfflineEarning)
        {
            _isOpenOfflineEarning = false;
            //もし稼ぎが0ならなし
            if (CalcOfflineEarning() <= 0) return;
            SceneManager.LoadSceneAsync(SceneName.OfflineEarningScene, LoadSceneMode.Additive);
        }
    }

わざわざメソッドにする意味あったか?とか今見返すと色々思うところがありますが,置いておきます笑
先ほどのSetOfflineEarningは読み込んだ先のシーン内にあるシーン管理スクリプトのStart()内で実行しています。
CalcOfflineEarning()SetOfflineEarningで行っている計算の部分のみを行い値だけ返却するメソッドです。報酬が0の場合はポップアップ表示をする必要がなく計算結果を代入されては困るので,if文で判定を行っています。
一応コードも載せておきます。(同じようなことを記述していてばかばかしいですね,もっと簡潔に書けそうではあります。)

    public int CalcOfflineEarning()
    {
        double minutes = _timer.CalcSecond(DateTime.UtcNow, _endTime) / 60;
        return (int) minutes * _offlineEarning;
    }

追加ロードするシーンは以下のようにポップアップ表示用UIを用意しておき,稼げる金額等をスクリプト側で書き換えています。
image.png

まとめ

初投稿で文章が読みづらいところもあるかとは思いますが,ご容赦ください。
今回基本的には時間差分で報酬を計算,アプリケーションに戻ってきたことを判定し,その際にポップアップ表示用のシーンを追加ロードという形で実装しました。
もっと良い方法もあるかとは思いますが,少しでも参考になれば幸いです。

追記(2019-05-13)

今回の実装では特段チート対策(端末の時刻をいじるなどに対する対策)はしていません。
企画の方に確認したところ,対策しなくてもいいとのことだったので,とりあえず現状は放置です。そのうち対策したものも載せるかもしれません。

参考

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

ObjectPoolから出し入れするAnimatorの再生状態をシンクロさせてオークをたくさんダンスさせる

ezgif-1-abc5260e8697.gif

わかる
コードをよむ
わからない
つぎにすすむ

なにを言っているのか

ObjectPoolを用いて頻繁にHumanoidAnimatorで動くキャラの出現/消失を行います。
そのとき、新しく出したキャラも既に出ているキャラの動きに合わせたいです。

なにも考えずキャラを出し入れするとこんな感じになります。

fail.gif

統率の取れていないオークの群れなどお話になりませんね。
Animatordisableされると再生位置がリセットされます。そのため、ObjectPoolに回収されるとアニメーションが最初からになってしまいます。ObjectPoolの要素が足りなくなって新規作成されたキャラも同様です。

Animatorの再生位置をシンクロさせる

解説

動きを揃えるからにはまず基準を定めなければなりません。というわけではぴねこちゃんをオークリーダーに任命します。はぴねこちゃんにもオークにも同じAnimatorをセットします。
アニメーションはある程度長くて動きが大きいものならズレがわかりやすいので、ユニティちゃん Candy Rock Star ライブステージ!のダンスを使います。

Animatorのアニメーションを途中から再生するには以下のメソッドを使います。

Animator.Play
public void Play (int stateNameHash, int layer= -1, float normalizedTime= float.NegativeInfinity);

stateNameHashがアニメーションの種類、normalizedTimeはアニメーションの再生位置です。layerは特に変更しないので初期値の-1を入れます。
このふたつのパラメータをオークリーダーのAnimatorから取得できればよいわけです。取得の仕方は以下の通り。

var currentState = _masterAnimator.GetCurrentAnimatorStateInfo(0);
var stateHash = currentState.fullPathHash;
var normalizedYime = currentState.normalizedTime;

問題はこの値を取得するタイミングです。Animatorのアニメーションはいつ更新されているのか。イベント実行順を見てみます。

Unity公式 : イベント関数の実行順

monobehaviour_flowchart.png

PhysicsブロックとGame LogicブロックそれぞれにInternal Animation Updateの記載があります。この2つが実行される前にパラメータをセットしておけば大丈夫そうです。というわけでFixedUpdate
ただし、同じFixedUpdateのタイミングで「オークリーダーからstateを取得」→「各オークに通知」する必要があります。異なるComponent間の実行順は保証されていないので一工夫……しないでUniRxSubjectを使いましょう。

OrcSecurer
// オークリーダーのAnimator
[SerializeField] private Animator _masterAnimator;
// オークリーダーの初期位置
private Vector3 _masterStartPosition;

// 初期位置からの差分, stateNameHash, normalizedTime
public readonly Subject<(Vector3, int, float)> MasterInfo = new Subject<(Vector3, int, float)>();

void Start()
{
    _masterStartPosition = _masterAnimator.rootPosition;

    Observable.EveryFixedUpdate().TakeUntilDestroy(_masterAnimator.gameObject).Subscribe(l =>
    {
        var currentDiff = _masterAnimator.rootPosition - _masterStartPosition;
        var currentState = _masterAnimator.GetCurrentAnimatorStateInfo(0);

        MasterInfo.OnNext((currentDiff, currentState.fullPathHash, currentState.normalizedTime));
    });
}

後はこのクラスをSingletonにしておきます。そしてObjectPoolから取り出されたときに各オークはこのSubjectを購読すればいいわけです。

各オークの初期化時
OrcSecurer.Instance.MasterInfo.TakeUntilDisable(this).Subscribe(tuple =>
{
    _animator.gameObject.transform.position = _startPosition + tuple.Item1;
    _animator.Play(tuple.Item2, -1, tuple.Item3);
});

これで無事同じFixedUpdateのタイミングで「オークリーダーからstateを取得」→「各オークに通知」をすることができました。

説明するタイミングを逃しましたが、同時に流しているVector3は初期位置からApply Root Motionによってどれだけ移動したか……の差分です。アニメーションの再生位置をすっとばすことで、それまでアニメーションによって移動したはずの座標位置もすっとばされています。なので合わせて補正してあげないといけません。

また、もしUniRxが使えない場合は[DefaultExecutionOrder()]属性でオークリーダーからstateを取得するクラスの実行優先度を高めて先にstateを取得します。Singletonにしておくことで自由にアクセスできるようになるので、各オークにアタッチされたComponentから取得しましょう。1

主題はこれで終わりです。改めて最初のgifを見てください。ちゃんとシンクロしているのがわかる2と思います。

コード

OrcSecurer

オークリーダーはぴねこちゃんのstateを取得するクラス。同時にオークをObjectPoolから出し入れもする。デブクラス。
めんどくせえからやってないけどちゃんと別クラスに分割しましょう。

OrcSecurer
using System;
using System.Collections.Generic;
using UniRx;
using UniRx.Toolkit;
using UnityEngine;

public class OrcSecurer : SingletonMonoBehaviour<OrcSecurer>
{
    [SerializeField] private Animator _masterAnimator;
    [SerializeField] private AnimatorSynchronizer _orcPrefab;

    private const int AppearCount = 20;
    public readonly Subject<(Vector3, int, float)> MasterInfo = new Subject<(Vector3, int, float)>();

    private Vector3 _masterStartPosition;

    void Start()
    {
        _masterStartPosition = _masterAnimator.rootPosition;

        Observable.EveryFixedUpdate().TakeUntilDestroy(_masterAnimator.gameObject).Subscribe(l =>
        {
            var currentDiff = _masterAnimator.rootPosition - _masterStartPosition;
            var currentState = _masterAnimator.GetCurrentAnimatorStateInfo(0);

            MasterInfo.OnNext((currentDiff, currentState.fullPathHash, currentState.normalizedTime));
        });

        var currentAppearList = new List<AnimatorSynchronizer>(AppearCount);
        var orcPool = new OrcPool(_orcPrefab, this.transform);

        for (var count = 0; count < AppearCount; count++)
        {
            var orc = orcPool.Rent();
            orc.Ready();
            currentAppearList.Add(orc);
        }

        Observable.Interval(TimeSpan.FromMilliseconds(400)).TakeUntilDestroy(this).Subscribe(
            l =>
            {
                if (UnityEngine.Random.Range(0, 2) == 0 && currentAppearList.Count < AppearCount * 2)
                {
                    var orc = orcPool.Rent();
                    orc.Ready();
                    currentAppearList.Add(orc);
                    return;
                }

                var index = UnityEngine.Random.Range(0, currentAppearList.Count);
                var removeOrc = currentAppearList[index];
                currentAppearList.RemoveAt(index);
                orcPool.Return(removeOrc);
            },
            () => { orcPool.Dispose(); });
    }

    private class OrcPool : ObjectPool<AnimatorSynchronizer>
    {
        private readonly AnimatorSynchronizer _prefab;
        private readonly Transform _parent;

        public OrcPool(AnimatorSynchronizer prefab, Transform parent)
        {
            _prefab = prefab;
            _parent = parent;
        }

        protected override AnimatorSynchronizer CreateInstance()
        {
            return GameObject.Instantiate(_prefab, _parent);
        }

        protected override void OnBeforeRent(AnimatorSynchronizer instance)
        {
            instance.gameObject.transform.position = new Vector3(0, -10, 0);
            base.OnBeforeRent(instance);
        }

        protected override void OnBeforeReturn(AnimatorSynchronizer instance)
        {
            base.OnBeforeReturn(instance);
            instance.gameObject.transform.position = new Vector3(0, -10, 0);
        }
    }
}

AnimatorSynchronizer

各オークにアタッチされています。

AnimatorSynchronizer
using UniRx;
using UnityEngine;

public class AnimatorSynchronizer : MonoBehaviour
{
    [SerializeField] private Animator _animator;

    private Vector3 _startPosition;
    private readonly float[] SquarePoints = {-3f, -1.5f, 0, 1.5f, 3f};

    public void Ready()
    {
        SetupStartPosition(new Vector3(SquarePoints[Random.Range(0, SquarePoints.Length)], 0f, SquarePoints[Random.Range(0, SquarePoints.Length)]));
    }

    public void SetupStartPosition(Vector3 startPosition)
    {
        _startPosition = startPosition;

        OrcSecurer.Instance.MasterInfo.TakeUntilDisable(this).Subscribe(tuple =>
        {
            _animator.gameObject.transform.position = _startPosition + tuple.Item1;
            _animator.Play(tuple.Item2, -1, tuple.Item3);
        });
    }
}

余録

ObjectPool

protected override void OnBeforeRent(AnimatorSynchronizer instance)
{
    instance.gameObject.transform.position = new Vector3(0, -10, 0);
    base.OnBeforeRent(instance);
}

protected override void OnBeforeReturn(AnimatorSynchronizer instance)
{
    base.OnBeforeReturn(instance);
    instance.gameObject.transform.position = new Vector3(0, -10, 0);
}

なぜインスタンスに対してObjectPool出入りのときに変な座標をセットしているかというと、ObjectPoolから出てenableになったキャラが指定された座標に移動する前に一瞬だけ見えてしまうからです。かなりちらついて気になります。新しく生成されたキャラも座標は(0, 0, 0)なのでこちらも同様です。
出現の瞬間にプレイヤーから見えなければいいわけなので、ObjectPoolの中にいる間は地の底に沈めることで誤魔化しています。どうせdisableなのでどこにいてもいいでしょう。たぶん。

あと、書いていて思ったのですが、ObjectPoolは別クラスにするよりプーリングするクラスの中にサブクラスとして書いちゃったほうがわかりやすくていいかもしれません。行数が増えすぎるかもですが。

Animator

Unity歴1年ちょっと、ありものを使うばかりでAnimation関係を触ったことなかったんですが、なんか、これ、闇、深い……?
いろいろ便利にラッピングされているっぽくて、奥底に何が潜んでいるかわからない恐怖を感じる。

毎フレームパラメータをセットしているのが気持ち悪くて、というかそもそも最初のアニメーション開始位置さえ合わせちゃえば後はみんな同じなのでは? と思って最初の10フレームだけ補正をかけるなどしたのですが、じわじわずれてきます。
Updateにタイミングを変更してもだめ。UniRxではなくMonoBehaviourのコールバックを使ってもだめ。
ということは肉眼で検知できないレベルでオークリーダーとオークのダンスがずれている?3 謎。
あと、ステートマシーンによるアニメーションの遷移ですが、これみんなよく管理できてますね……つらい……なんだこれは……。

まとめ

自分で作る前にUnityでダンス動画とか作ってる人のブログ漁ったらよかったかも。同じことを既に実行済みの人がいるはず。
あと、MMDの動画を参考資料としていろいろ見たのですが、みんなとんでもねえ性癖でとんでもねえなって思いました。

おしまい。

素材

はぴねこちゃん4
HarpyCat

オーク5
POLYGON - Fantasy Rivals

ダンス
ユニティちゃんライブステージ! -Candy Rock Star-
© Unity Technologies Japan/UCL

背景
Farland Skies - Cloudy Crown

参考

Animatorでアニメーションを途中から再生する
これが知りたかった。

UniRxのObjectPoolを利用する
くわしい。

【Unity】Animatorの更新タイミングを変更する
これをなんかうまいことすればずれる問題も解決するような気がしないでもないが、毎フレームセットしたところでパフォーマンスへの負荷も大きくないので調べる気がない。


  1. たぶんうまくいくと思うけどあんまり自信ない 

  2. あなたも「わかる」側になりました 

  3. こんなザマじゃ女騎士サマに勝てねえよなぁ? 

  4. 「harpy」ってそらで書けたのですが、いったいどこで綴り覚えたんだっけ…… 

  5. セールで買ったけど使ってなかったからつい 

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【Unity】2つのテクスチャを合成するシェーダー

概要

2つのテクスチャを合成して表示ができるようになるシェーダーの作り方を紹介します

今回作成したコードはこちらに置いてあります
https://github.com/tkada/UnityTextureBlendShader

全体ブレンド

blend1.gif

今回はUnlitシェーダーをベースに作ります
いきなりソースコードです

Shader "Unlit/BlendShader"
{
    Properties
    {
        _MainTex ("Texture", 2D) = "white" {}
        _SubTex ("SubTexture", 2D) = "white" {}
        _Blend("Blend",Range (0, 1)) = 1
    }
    SubShader
    {
        Tags { "RenderType"="Opaque" }
        LOD 100

        Pass
        {
            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag
            // make fog work
            #pragma multi_compile_fog

            #include "UnityCG.cginc"

            struct appdata
            {
                float4 vertex : POSITION;
                float2 uv : TEXCOORD0;
            };

            struct v2f
            {
                float2 uv : TEXCOORD0;
                UNITY_FOG_COORDS(1)
                float4 vertex : SV_POSITION;
            };

            sampler2D _MainTex;
            sampler2D _SubTex;
            float _Blend;
            float4 _MainTex_ST;

            v2f vert (appdata v)
            {
                v2f o;
                o.vertex = UnityObjectToClipPos(v.vertex);
                o.uv = TRANSFORM_TEX(v.uv, _MainTex);
                UNITY_TRANSFER_FOG(o,o.vertex);
                return o;
            }

            fixed4 frag (v2f i) : SV_Target
            {
                // sample the texture
                fixed4 main = tex2D(_MainTex, i.uv);
                fixed4 sub = tex2D(_SubTex, i.uv);
                fixed4 col = main * (1-_Blend) + sub * _Blend;
                // apply fog
                UNITY_APPLY_FOG(i.fogCoord, col);
                return col;
            }
            ENDCG
        }
    }
}

キモとなるのはこの2行

//Sub Textureの色を取得
fixed4 sub = tex2D(_SubTex, i.uv);

//_Blendの値に応じて色をブレンドする
fixed4 col = main * (1-_Blend) + sub * _Blend;

これだけで2つのテクスチャを合成することができます

UV指定ブレンド

blend2.gif
最初の例では画像全体を合成しますが、特定の位置だけ合成したい場合もあると思います。
そのため、少しだけソースコードを変更します

Shader "Unlit/UVBlendShader"
{
    Properties
    {
        _MainTex ("Texture", 2D) = "white" {}
        _SubTex ("SubTexture", 2D) = "white" {}
        _Blend("Blend",Range (0, 1)) = 1

        //合成の始点・終点を設定
        _BlendStartU("Blend Start U",Range (0, 1)) = 0
        _BlendEndU("Blend End U",Range (0, 1)) = 1
        _BlendStartV("Blend Start V",Range (0, 1)) = 0
        _BlendEndV("Blend End V",Range (0, 1)) = 1
    }
    SubShader
    {
        Tags { "RenderType"="Opaque" }
        LOD 100

        Pass
        {
            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag
            // make fog work
            #pragma multi_compile_fog

            #include "UnityCG.cginc"

            struct appdata
            {
                float4 vertex : POSITION;
                float2 uv : TEXCOORD0;
            };

            struct v2f
            {
                float2 uv : TEXCOORD0;
                UNITY_FOG_COORDS(1)
                float4 vertex : SV_POSITION;
            };

            sampler2D _MainTex;
            sampler2D _SubTex;
            float _Blend;
            float4 _MainTex_ST;

            float _BlendStartU;
            float _BlendEndU;
            float _BlendStartV;
            float _BlendEndV;

            v2f vert (appdata v)
            {
                v2f o;
                o.vertex = UnityObjectToClipPos(v.vertex);
                o.uv = TRANSFORM_TEX(v.uv, _MainTex);
                UNITY_TRANSFER_FOG(o,o.vertex);
                return o;
            }

            fixed4 frag (v2f i) : SV_Target
            {
                // sample the texture
                fixed4 main = tex2D(_MainTex, i.uv);
                fixed4 sub = tex2D(_SubTex, i.uv);
                fixed4 col = main;

                //指定の範囲だったら合成する
                if(i.uv.x >= _BlendStartU && i.uv.y >= _BlendStartV)
                {
                    if(i.uv.x <= _BlendEndU && i.uv.y <= _BlendEndV)
                    {
                        col = main * (1-_Blend) + sub * _Blend;
                    }
                }

                // apply fog
                UNITY_APPLY_FOG(i.fogCoord, col);
                return col;
            }
            ENDCG
        }
    }
} 

こうすることで指定したUVの部分だけ合成することができます

Sub Textureをフィットさせる

blend3.gif

最後に合成範囲に応じてSub Textureのスケールを変更して全体が表示できるようにします

Shader "Unlit/FitUVBlendShader"
{
    Properties
    {
        _MainTex ("Texture", 2D) = "white" {}
        _SubTex ("SubTexture", 2D) = "white" {}
        _Blend("Blend",Range (0, 1)) = 1

        _BlendStartU("Blend Start U",Range (0, 1)) = 0
        _BlendEndU("Blend End U",Range (0, 1)) = 1
        _BlendStartV("Blend Start V",Range (0, 1)) = 0
        _BlendEndV("Blend End V",Range (0, 1)) = 1
    }
    SubShader
    {
        Tags { "RenderType"="Opaque" }
        LOD 100

        Pass
        {
            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag
            // make fog work
            #pragma multi_compile_fog

            #include "UnityCG.cginc"

            struct appdata
            {
                float4 vertex : POSITION;
                float2 uv : TEXCOORD0;
            };

            struct v2f
            {
                float2 uv : TEXCOORD0;
                UNITY_FOG_COORDS(1)
                float4 vertex : SV_POSITION;
            };

            sampler2D _MainTex;
            sampler2D _SubTex;
            float _Blend;
            float4 _MainTex_ST;

            float _BlendStartU;
            float _BlendEndU;
            float _BlendStartV;
            float _BlendEndV;

            v2f vert (appdata v)
            {
                v2f o;
                o.vertex = UnityObjectToClipPos(v.vertex);
                o.uv = TRANSFORM_TEX(v.uv, _MainTex);
                UNITY_TRANSFER_FOG(o,o.vertex);
                return o;
            }

            fixed4 frag (v2f i) : SV_Target
            {
                //合成範囲のスケールを算出
                float u_scale = _BlendEndU - _BlendStartU;
                float v_scale = _BlendEndV - _BlendStartV;

                //Sub TextureのUV座標を算出
                fixed2 pos = fixed2((i.uv.x - _BlendStartU) * 1/u_scale,
                                     (i.uv.y - _BlendStartV) * 1/v_scale);

                // sample the texture
                fixed4 main = tex2D(_MainTex, i.uv);
                fixed4 col = main;

                //指定の範囲だったら合成する
                if(i.uv.x >= _BlendStartU && i.uv.y >= _BlendStartV)
                {
                    if(i.uv.x <= _BlendEndU && i.uv.y <= _BlendEndV)
                    {
                        col = main * (1-_Blend) + sub * _Blend;
                    }
                }

                // apply fog
                UNITY_APPLY_FOG(i.fogCoord, col);
                return col;
            }
            ENDCG
        }
    }
}

こうすることでgifのようにSub Textureを合成範囲に合わせてスケールすることができます

応用編 if文を使わないようにする

ここまでの例では分かりやすくするためif文を使っていました
しかしシェーダーの高速化のためif文は使わない方がいいといわれています
下記の記事を参考にif文を使わないように書き換えてみました

参考:条件分岐のためにstep関数を使う時の考え方をまとめてみた

Shader "Unlit/FitUVBlendShader"
{
    Properties
    {
        _MainTex ("Texture", 2D) = "white" {}
        _SubTex ("SubTexture", 2D) = "white" {}
        _Blend("Blend",Range (0, 1)) = 1

        _BlendStartU("Blend Start U",Range (0, 1)) = 0
        _BlendEndU("Blend End U",Range (0, 1)) = 1
        _BlendStartV("Blend Start V",Range (0, 1)) = 0
        _BlendEndV("Blend End V",Range (0, 1)) = 1
    }
    SubShader
    {
        Tags { "RenderType"="Opaque" }
        LOD 100

        Pass
        {
            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag
            // make fog work
            #pragma multi_compile_fog

            #include "UnityCG.cginc"

            struct appdata
            {
                float4 vertex : POSITION;
                float2 uv : TEXCOORD0;
            };

            struct v2f
            {
                float2 uv : TEXCOORD0;
                UNITY_FOG_COORDS(1)
                float4 vertex : SV_POSITION;
            };

            sampler2D _MainTex;
            sampler2D _SubTex;
            float _Blend;
            float4 _MainTex_ST;

            float _BlendStartU;
            float _BlendEndU;
            float _BlendStartV;
            float _BlendEndV;

            v2f vert (appdata v)
            {
                v2f o;
                o.vertex = UnityObjectToClipPos(v.vertex);
                o.uv = TRANSFORM_TEX(v.uv, _MainTex);
                UNITY_TRANSFER_FOG(o,o.vertex);
                return o;
            }

            fixed4 frag (v2f i) : SV_Target
            {
                float u_scale = _BlendEndU - _BlendStartU;
                float v_scale = _BlendEndV - _BlendStartV;

                fixed2 pos = fixed2((i.uv.x - _BlendStartU) * 1/u_scale,
                                     (i.uv.y - _BlendStartV) * 1/v_scale);

                // sample the texture
                fixed4 main = tex2D(_MainTex, i.uv);
                fixed4 sub = tex2D(_SubTex, pos);

                //if文をstep関数を使って表現.条件がそろっていればconditionに1が入る
                float condition = step(i.uv.x, _BlendEndU) * 
                                    step(i.uv.y, _BlendEndV) *
                                    step(_BlendStartU, i.uv.x) *
                                    step(_BlendStartV, i.uv.y);

                //conditionが0ということは範囲外なので、_blendにかけ合わせることで合成しない範囲を表現する
                float blend = condition * _Blend;

                fixed4 col = main * (1-blend) + sub * blend;

                // apply fog
                UNITY_APPLY_FOG(i.fogCoord, col);
                return col;
            }
            ENDCG
        }
    }
}
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む