20191126のUnityに関する記事は9件です。

【Unity】Airtableを使って簡易的なDB管理をする方法【SQLは不要】

こんにちは!
ねこじょーかー(@nekojoker1234)と申します。

Unityでゲームを作ろうとしたときに、DBとしてデータを管理して、そこからデータを引っ張ってくる、といった処理も必要になってくると思います。

数個くらいのデータであれば「まぁ、if文で分岐すればいいか」となりますが、さすがに数十、数百となると手間が増えてやっていられないですよね。

しかし、ちゃんとしたDBを用意するとなると、サーバー・テーブル設計・SQLなどなど、考えることがたくさんあります。

そんな「大規模なDBはいらないけど、ソースコードにベタ書きも嫌だ!」という人のための記事です。

Airtableを使って簡易的なDB管理をする方法

Airtableに登録する

タイトルでネタバレしていますが、まずはAirtableに無料登録しましょう。

Airtable」は、スプレッドシートのような機能が無料で使えるサービスがあり、これをDB代わりに使用することができます。

登録手順はメールアドレスを登録するだけなので割愛しますね。
登録が終わったら、以下の画面が表示されます。

スクリーンショット 2019-11-26 21.18.18.png

テーブルを追加する

先ほどの画像で「Add a base」となっているところをクリックすると、新しいテーブルを作成することができます。

「Start From Scratch」を選択することで、完全に「空」のテーブルになります。

スクリーンショット 2019-11-26 21.23.37.png

レコードを追加する

基本的な使い方はExcelと同じなので、特に操作の説明はしません。

私は以下のようなテーブルを作成しました。

スクリーンショット 2019-11-26 21.26.58.png

一番上に表示されているのが「テーブル名」、左上に表示されているのが「シート名」です。

シート名はプログラム上で使うことになるので、コピーしておいてください。

APIキーを確認する

トップページ右上の「Account」からマイページに行くと、APIキーが「●●●●●●」と表示されているので、カーソルを合わせると具体的なAPIキーが表示されます。

このAPIキーはプログラムで使用するのでコピーしておきましょう。

BaseIDを確認する

まずは以下のページにアクセスしてください。
https://airtable.com/api

「The ID of this base is」となっているところがBaseIDなので、これをコピーしておきましょう。(灰色でぬりつぶしているところです)

スクリーンショット 2019-11-26 21.36.26.png

これでようやくAirtable側の準備が終わったので、次からはUnity側の準備に入ります。

AirtableClientをプロジェクトにインポート

以下で公開されているソースコードを、プロジェクトにインポートしましょう。
csファイルだけで大丈夫です。

https://github.com/yKimisaki/AirtableClient

Utf8Jsonをプロジェクトにインポート

AirtableClientをインポートすると、「Utf8Jsonがないよ!」と怒られるので、Utf8Jsonもインポートしておきます。

https://github.com/neuecc/Utf8Json

するとここで、「unsafeが許可されていません」的なエラーが出て困ることになるので、unityの設定を変更しましょう。

「Edit -> Project Setting」と進んでいき、「unsafe」と検索窓に入力して「Allow 'unsafe' code」を見つけます。

スクリーンショット 2019-11-26 21.48.28.png

このチェックをつけることで、エラーを解消することができます。

処理を実装する

では、実際の処理を実装していきましょう。

サンプルはこんな感じです。

var client = new AirtableClient("コピーしたAPIキー");
var @base = client.GetBase("コピーしたBaseID");
var allRows = @base.LoadTableAsync<"コピーしたシート名">().Result;

foreach (var row in allRows)
{
    // 1レコードずつ取り出す処理 
}

コピーしたシート名の型のエンティティが返ってくるので、シート名をクラス名にして、各列名をプロパティ名にします。

public class CharacterParam
{
    public string Name;
    public int Attack;
    public int Defence;
    public int HitPoint;
}

完成

めでたく完成です!

「応答なし」で固まってしまう場合は、以下のケースが考えられるので、チェックしてみてください!

  • APIキーが間違っている
  • BaseIDが間違っている
  • シート名とエンティティ名が一致していない
  • オフラインになっている

補足

最後に念のため、公式で提供されているAPIとドキュメントをリンクとして貼っておきます。

公式API
https://github.com/ngocnicholas/airtable.net

公式ドキュメント
https://github.com/ngocnicholas/airtable.net/wiki/Documentation

あわせて読みたい

筆者のブログ:https://nekojokerblog.com

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

VRCで座った人を踊らせる椅子をアバターに仕込む

前提知識

  • Unityの基本的な操作
  • 購入アセットのインストール方法
  • カスタムオーバーライドでオブジェクトの出し入れをするヤツ(出し入れする場合のみ)

必要なもの

以下のものをプロジェクトに用意しておいてください。

  • VRCSDK
  • お好きなアバター
  • 仕込むアニメーション
  • FinalIK(有料)

やりたいこと

  • Sitした他のプレイヤーを踊らせる

なぜやろうと思ったか

近頃Sitしたプレイヤーを踊らせるギミックが仕込まれたパブリックアバターをよく見かけるため。
ぜひその仕組みを知りたい…!という知的好奇心もあり。

手順

大まかな手順は以下の通り。

  1. 椅子の作成
  2. Animationファイルの編集
  3. アニメーションを椅子へ適用
  4. アニメーション椅子をアバターに仕込む

1. 椅子の作成

椅子はVRC_Stationを使うと作れます。
VRCSDKのパッケージの中に入っているサンプルの椅子を使っても良いですが、
アバターに仕込めないスクリプトは削除しましょう。

今回はCubeを使うことにしました。
1.PNG

Box ColliderはRemoveしましょう。
2.PNG

このような状態にしておきます。
3.PNG

2. Animationファイルの編集

次にAnimationファイル(踊らせるモーション)を編集し、以下の2つのスクリプトを無効にするキーを打ちます。
両方ともFinalIKに入っているスクリプトです。

  • VRIK (3点トラッキング用)
  • Full Body Biped IK (フルトラ用)

今回はCLAZYさんのRunner Action Animation Packをお借りしました。
(有料ですがBlack Fridayで半額です!クールなモーションが大量に入ってます!オススメ!)

まずは適当なGameObjectを作成します。
4.PNG

作成したGameObjectにVRIKとFull Body Biped IKを追加します。
以下のような状態になります。
5.PNG

GameObjectに踊らせるモーションのアニメーションファイルをドラッグ&ドロップします。
6.PNG

GameObjectを選択した状態でAnimationウィンドウを開き、VRIKとFull Body Biped IKを無効にするキーを打ちます。
黄色くなっているヤツは気にしなくて良いです。
7.PNG

以下のような状態になります。
アニメーションの最初から最後まで、2つのスクリプトがオフになるように設定してください。
8.PNG

なお、座っている間ループしていてほしいので、Loop Timeにチェックを入れてあります。
これはアニメーションファイルを選択した状態でInspectorから設定できます。
9.PNG

3. アニメーションを椅子へ適用

アニメーションを椅子に設定します。
Assets/VRCSDK/Example/Sample Assets/Animationから、AvaterControllerTPose.controllerをひったくってきます。
Ctrl+Dで複製などして使うと良いと思います。

ひったくってきました。
10.PNG

ひったくってきたAvaterControllerTPoseを選択した状態で、Animatorウィンドウを開いてください。
Entryから矢印が伸びてTPoseがつながっていると思います。
11.PNG

TPoseをクリックしてください。
Inspector内に出てくる設定画面のMotionを、手順2で編集したアニメーションに差し替えます。
12.PNG

手順1で作成したCubeのVRC_StationにAvaterControllerTPoseを設定します。
VRC_StationのAnimator ControllerのNoneってなってるところにAvaterControllerTPoseをドラッグ&ドロップします。
13.PNG

プレイヤーが座ったときのトランスフォームと離席したときのトランスフォームを設定します。
Cubeの子供に空のGameObjectを作成して、Locationという名前をつけましました。
これを設定したいと思います。
14.PNG

最終的にこのような状態になりました。
ScaleとPositionは適当に調整しました。
15.PNG

これで椅子の設定は終わりです。

4. アニメーション椅子をアバターに仕込む

アバターにアイテムを仕込むときと一緒の手順です。
私はアバターの直下に入れてます。
16.PNG

あとはマテリアルを作ってCubeを装飾したり、
カスタムオーバーライドを設定して出し入れしたり出来るように設定すれば完成です:clap:

所感

入れるモーションによっては視界が大変荒ぶるので、一度テスト用ワールドを作って自分で座ってみたほうが良いです。
あと、VRIKとFullBodyBipedIKをオフにするためだけにFinalIKを買うのか?と聞かれると何も言えねえです。
大いなる目的をもってFinalIKを購入したついでにでも試してみると良いです。

参考文献

https://vrcworld.wiki.fc2.com/wiki/VRC_Station

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

StandardShaderを含むVRMモデルをランタイムロードできない

概要

StandardShaderを含むVRMモデルをランタイムロードできない例と対策を記載します。

ビルドに含めたシーンにStandardShaderが使われているオブジェクトが一つも無いと、ビルド物にStandardShaderが含まれないようです。VRMを使ったツール的なアプリだと空シーンにモデルだけ読むといった作りになるので、忘れちゃうかもしれません。(私は忘れてました。

確認

UniVRM的にはStandardShaderは「UniVRMで使えるシェーダー」だそうですので、対応しとくのが正解だと思われます。https://vrm.dev/univrm/shaders/univrm_shaders/

対策

StandardShaderが付いたオブジェクトを追加して、非表示にしておきます。
※もっと良い方法がありそうです。

参考

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

UnityからVRChatが立ち上がらない時

UnityからVRChatが立ち上がらない時にやること

UnityからVRChatへavatarやworldをアップロードする際にエラーが出たとき
この方法で解決できました.

キャッシュとレジストリの削除

これだけで直りました.

①コマンドプロントを開く
 ・windowsの検索欄に「cmd」と入力し「コマンドプロンプト」を右クリック
 ・「管理者として実行」をクリック

②下記のコマンドを「enter」で実行する

rmdir /S /Q %USERPROFILE%\AppData\LocalLow\VRChat\vrchat && rmdir /S /Q %TEMP%\VRChat && REG DELETE HKCU\Software\VRChat\vrchat /va

③YESなので「y」を入力

これでダメだったときは他の方法を試す
参考URLへ

参考

VRChatが起動しない/SDKが動かない場合

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

Unity + ADX2におけるサウンドデータの読み込みと破棄

Unity for ADX2導入後のデータ読み込み

CRI ADX2 および ADX2 LEは、ゲーム開発環境においてサウンドの演出開発を今日曲にサポートするミドルウェアです。
イントロ付きループBGMのストリーミング再生や、Androidにおける再生遅延の対策機能などの性能的アドバンテージもあります。

ADX2を導入する際に考える必要があることとして、音声データハンドリングの要素があります。
Unity Audioはシーンに紐付いたAudio Clipを自動的にロードしたり、スクリプトから任意のタイミングでメモリに展開することができます。

ADX2の場合も、音声データのマウントやロードを明示的に行うことでパフォーマンスの改善ができます。
本記事では、当社で開発中のゲーム「デモリッション ロボッツ K.K.」の事例を使いながら解説します。
ADX2を全く初めて触る、という場合は以下の記事を先にご覧ください。

Unityのサウンド機能をADX2で強化する
https://qiita.com/Takaaki_Ichijo/items/16e6501fc07f5b3b3377

ADX2には製品版のADX2と、個人開発者向けの無償版ADX2 LEの2つのエディションがあります。
本記事では、無償版である「ADX2 LE」のv2.10.05を使用しています。

CRI ADX2 LE
https://game.criware.jp/products/adx2-le/

ADX2が出力するファイルの種類

ADX2では、waveファイルなど元の音声データをUnity Editorにインポートせず、別ツールである「Atom Craft」を使って再生用データを作ります。これはADX2のSDKに含まれています。

sound_desight-768x454.jpg

Atom Craftでは、専用コーデック「HCA」への圧縮のほか「この音はフェードインアウト」「この音はランダムにピッチが変わる」といったメタデータ的な情報を音に埋め込みます。

データの出力は、複数の音をまとめたバイナリファイルになります。たとえば「システムSE」「キャラクターAのセリフ」「ステージ1のBGM」のように、用途によって分け、ひと塊のデータとして出力します。ゲーム実行時にはこの塊単位でロードします。
UnityからADX2のランタイムを介してそれらのデータを読み込み、スクリプトで再生、というフローになります。

Atom Craftからは「ACF」「ACB」「AWB」の3種類のデータが出力されます。

  • 「ACF」ファイルは、ゲームプロジェクト1つにつき1個の「全体設定」ファイルです。ゲームの開始時に読み込まれます。
  • 「ACB」ファイルが音データを含むバイナリファイルで、いくつかの音をまとめて固めています。圧縮音声データをメモリに乗せて再生(Unity Audioで言うところのCompressedInMemory設定)するためのファイルです。
  • 「AWB」は、ストリーミング再生(Unity AudioにおけるStreaming設定)を使う場合に登場します。ストリーミング再生の場合もACBファイルが必要です。

ADX2 for Unityにおけるファイルハンドリング

ゲーム起動時に全部ロードする

CRI ADX2は、「Cri Atom」コンポーネントをシーンに配置して使います。デフォルトでは「CRIWARE」ゲームオブジェクトにアタッチされています。
これはゲーム中1つだけ存在しており、ADX2のシステムがロードした音声データを管理します。

CRIAtomコンポーネント.png

もっとも簡単な利用方法は、ゲーム中に使うサウンドデータの参照をすべてインスペクターで設定してしまうことです。
「Add CueSheet」をクリックすると、ACFファイル、AWBファイルのファイルパスを文字列で指定しておくことができます。

ゲーム機同時に読んじゃう設定.png

上の図ではシステム用音声データのacfと、BGMのacf/awbを起動時にロードする設定です。
(ファイルパスはルートを省略するとStreamingAssetsから読むようになります、この場合はAssets/StreamingAssets/audioにデータが入っている想定です)

ですが、ゲームの起動直後に必要な音というのはあまりありません。BGMやキャラクターのボイスデータ、技の効果音など、複数大量にある音声データはゲームの場面に合わせてロードとアンロードを行うべきです。

ゲーム起動後に任意のタイミングでロードする

さて本題です。CriAtom.csのインスペクターでacb, awbファイルを登録せず、ゲーム起動後に任意のタイミングでロードを行うコードを用意します。
具体的には、次のようなメソッド経由でロードします。

CRIAtomCueSheetLoad.cs
    private const string adx2FileDirectoryName = "ADX2Files";
    private CriAtomExAcb cueSheet;
    private CriAtomExPlayer criAtomExPlayer;

    private void Awake()
    {
        criAtomExPlayer = new CriAtomExPlayer();
    }

    private async Task LoadCueSheet(string cueSheetName)
    {
        CriAtom.AddCueSheetAsync(cueSheetName, Path.Combine(adx2FileDirectoryName, cueSheetName + ".acb"), "");

        await UniTask.WaitUntil(() => CriAtom.CueSheetsAreLoading == false);

        cueSheet = CriAtom.GetCueSheet(cueSheetName).acb;
    }

    public void Play(string cueName)
    {
        criAtomExPlayer.SetCue(cueSheet, cueName);
        criAtomExPlayer.Start();
    }

これは、ディレクトリAssets/StreamingAssets/ADX2Files/下に配置されているacbファイルをロードする非同期処理です。
開発中のゲーム「デモリッションロボッツKK」では、ゲーム中のメニューUIのボタン音などをこのファイルに固めており、ゲーム起動時のサークルロゴ表示中に読み込んでいます。

非同期のロード処理を呼んで、キューシートのロード処理が終わるまで待ちます。
* UniTask.WaitUntilはUniTaskの機能です。簡単に条件待ちができて便利。

コルーチンを利用して実装する場合は次のコードになります。

CRIAtomCueSheetLoadCoroutine.cs
    public IEnumerator LoadCueSheetCoroutine(string cueSheetName, string path)
    {
        CriAtom.AddCueSheetAsync(cueSheetName, path, "");

        while (CriAtom.CueSheetsAreLoading == true)
        {
            yield return null;
        }

        cueSheet = CriAtom.GetCueSheet(cueSheetName).acb;
    }

データの多重ロードに注意

ADX2 for Unityを使ったサウンドの再生は、標準のUnity Audioと異なり、シーンを破棄してもデータが自動でアンロードされません。
たとえばステージ毎に違うBGMを読み込んでいる場合などは、明示的なアンロードが必要です。
アンロードはひとつメソッドを呼ぶだけです。

UnloadAdx2Data.cs
CriAtom.RemoveCueSheet("StageBGM");

アンロードはキューシート名を指定します。この呼び出しをシーン破棄処理と同時に行うことが大事です。

「デモリッションロボッツKK」では、ステージのシーンにBGMのacb, awbデータの名前を保持しておき、ステージデータの初期化時にCriAtom.AddCueSheetAsyncを読んでいます。
ところが、最初はこのアンロード処理を忘れており、タイトル画面に戻って同じステージをやり直すたびに同じデータがメモリにどんどん読まれてしまっていました。
そうなってくるとメモリを圧迫するほか、CRI Atom Initializerで指定したファイルマウント数を超えるとエラーが出て音が再生できなくなります。使わなくなったBGM、ボイス、環境音などは適宜アンロードしておくとよいでしょう。

場面に応じた音声データのロードとアンロード

基本的に、繰り返し使う短い音はメモリに常駐させておくべきです。たとえばUIのボタン音や、キャラクターの足音、通常攻撃ヒット音などです。
これらは再生レスポンスが重要であるため、ロードを挟まずに再生できると理想的です。

逆に、ある場面で1度しか使わない音声データは、必要になる直前にロードして再生し終わったら破棄するとよいでしょう。カットシーン中のセリフデータなどがこれにあたります。
プレイヤーキャラクターの切り替えなど、プレイヤーの捜査によって常駐させておく音声データが変わる場面もありえます。

また、BGMや環境音など、再生レスポンスが気にならない音に関しては、ストリーミング再生の設定にしておきましょう。
ストレージから少しづつデータを読むストリーミング再生なら、CriAtomがずっとacbファイルを握っていても、メモリ使用量は大きくはなりません。

その他のADX2ファイルハンドリング

ADX2のデータとAsset Bundle

ADX2のデータは、UnityがSerializedしたデータではないのでStreamingAssets下に置き、バイナリファイルとして読み込んで再生できます。
png等の画像データを外部から読み込むときと同様です。そのため、Unityの外からデータを取得したい場合も、AssetBundleファイルに含めずにサーバー等から直接取得して使用できます。
外のサーバーからデータを読み込んで再生するサンプルについては次の記事にまとめています。

Unity + PlayFab + ADX2で、Asset Bundleを介さないサウンドデータの配信を実装する
https://qiita.com/Takaaki_Ichijo/items/bc6ef09fa55d496e5800

(研究課題)Resource Managerとの兼ね合い

Addressable Asset Systemの基盤であるリソースロードシステムResource Managerを使ってADX2にデータを渡したい、あるいは必要なデータを取ってきたいという要望を耳にはさみました。こちらも調査の上、記事を用意します。

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

[Unity] DEATH STRANDINGのカイラル通信っぽいシェーダ

はじめに

配達系国道制作ゲーム、DEATH STRANDING。このゲームに登場するカイラル通信による通話(広義のホログラムのようなもの)を模倣してみました。
『DEATH STRANDING Day-1 GAMEPLAY SESSION Vol.1』PlayStation® presents LIVE SHOW “TGS2019”の53分あたりに登場。

できたもの

0a1a7f68e69bef5ab94aa7db5956a3af.gif

構成要素

Unity ShaderLabでの実装です。
おおまかな構成要素は以下
1.半透明
2.リムライト
3.vertex shaderでx軸方向にモデルの頂点をランダムに動かす
4.上方向に移動するスキャンライン
5.モデル全体にランダムノイズ
6.モデル全体に青系のオーバーレイ
7.上から下にdissolve
8.1のノイズが大きくなる(dissolve時)

実装

流れとしては、1Pass目で深度書き込み、2Pass目でアウトライン、3Pass目でその他、といった感じです。

1.半透明

Tags { "Queue"="Transparent" }
LOD 100
//先に深度を書き込む
Pass{
  ZWrite ON
  ColorMask 0
}
//透明
ZWrite Off
Blend SrcAlpha OneMinusSrcAlpha 

QueueをTransparentに設定し、描画順を不透明オブジェクトの後に設定します。
1Pass目でモデルの深度情報を書き込んでおきます。これをしないとデプス値が正しく更新されず、モデルが綺麗に表示されません。
その後はZWriteをOffにすることで深度の計算を破棄し、Blendを記述することでアルファブレンディングを有効にします。

2.リムライト

2Pass目でモデルの頂点を法線方向にオフセットすることでアウトラインを出します。

v2f vert (appdata v){
  v2f o;
  v.vertex += float4(v.normal * 0.006f, 0);   
  o.vertex = UnityObjectToClipPos(v.vertex); 
  o.worldPos = mul(unity_ObjectToWorld, v.vertex).xyz;
  return o;
}

3Pass目のfragment shaderで視線ベクトルと頂点の法線ベクトルの内積をとることでさらにリムライトっぽくします。

//リムライト
half rim = 1.0-saturate(dot(i.viewDir, i.worldNormal));
fixed4 rimColor = _RimColor * pow (rim, _RimPower);

3.vertex shaderでx軸方向にモデルの頂点をランダムに動かす

3Pass目のvertex shaderで計算を行い、x軸方向またはz軸方向に頂点をランダムに動かします。
ランダムに時間で区切ってオフセットさせるとパッパッと切り替わるような動きになってしまいます。これを防ぐために経過時間をfloorに入れ整数部のみを取り出すことでrandamに入れた時一定時間は同じ値を取るようにし、fracで小数部分のみを取り出しオフセットのサイズ調整に当てています。こうすることでサイズ徐々に変わるウニョウニョした動きをさせることができます。

float offset = Graph(v.vertex.y);
float t = _Time.z*_NoiseSpeed;
float offsetTF = rand(floor(t)+v.vertex.z);
if(offsetTF>_NoiseRange) v.vertex.z += offset*(1-frac(t))*_NoiseSize;

また、ランダムな値の算出は有名な関数をちょっと弄ったもの、Graphは適当にランダムな凹凸が現れるような関数を作成しました。こちらのサイトでグラフを描画して良い感じのものを採用しました。

float rand(float co){
return frac(sin(dot(co,12.9898)) * 43758.5453);
}

float rand2 (fixed2 p) { 
return frac(sin(dot(p, fixed2(12.9898,78.233))) * 43758.5453);
}

float Graph( fixed x){
return pow(_NoiseSize*abs(sin(10*x)*(-sin(x*2)+1))*0.5,2.0)*_Alpha;
}

4.上方向に移動するスキャンライン

ランダムな太さのスキャンラインを上方向に移動させる方法が思いつかなかったので、白黒のシマシマのテクスチャを作成し、uvスクロールさせる方法を取りました。これがスキャンラインA。
上のgifのようなスキャンラインBは、y座標を定数倍して小数部をアルファ値とすることで実装しています。

//スキャンラインA
//float2 scan = i.worldPos + float2 (0, _Time.x*-2.0);
//float4 scanLine = tex2D(_ScanlineTex, scan);

//スキャンラインB
float scanLine = frac(i.worldPos.y*5.0-_Time.y);

5.モデル全体にランダムノイズ

3で使用したノイズ関数を利用します。floorはタイミング調整に便利です。
noiseを減算することでザラついた感じを表現できます。

//ノイズ
float noise = rand2(i.uv+floor(_Time.y*10.0));

6.モデル全体に青系のオーバーレイ

オーバーレイは加算や乗算、スクリーンなどと異なり条件分岐が必要なので少し面倒です。

//オーバーレイ
if (col.r < 0.5){
col.r = 2.0*col.r*_OverlayColor.r;
}else{
col.r = 1.0 - 2.0 * (1.0 - col.r) * (1.0 - _OverlayColor.r);
}

if (col.g < 0.5){
col.g = 2.0*col.g*_OverlayColor.g;
}else{
col.g = 1.0 - 2.0 * (1.0 - col.g) * (1.0 - _OverlayColor.g);
}

if (col.b < 0.5){
col.b = 2.0*col.b*_OverlayColor.b;
}else{
col.b = 1.0 - 2.0 * (1.0 - col.b) * (1.0 - _OverlayColor.b);
}

7.上から下にdissolve

条件付きコンパイルを用いて実装します。
キーワードを定義しておき、

#pragma shader_feature _ Dissolve_ON

定義されたキーワードによってコンパイルする内容を変更します。
バリアントを作成する方がif文で毎回参照するよりも効率が良いので(おそらく)、できるだけこちらをつかいたいです。

#ifdef Dissolve_ON
col.a=saturate(saturate(abs(-i.worldPos.y+_ObjectSize)-_T*_DissolveSpeed)*5.0)*_Alpha;
#else
col.a = _Alpha;
#endif    

dissolveにグラデーションをかけたいので、saturateを使って値を調整します。

8.1のノイズが大きくなる(dissolve時)

実際のムービーを見ていただけると分かるのですが、disolve時はノイズのサイズが大きくなり、密度が大きくなります。C#スクリプトから7のdissolveのキーワード切り替えと同時にノイズのサイズと密度を調整します。

void Dissolve(){
        m.SetFloat("_NoiseSize", 1.5f);
        m.SetFloat("_NoiseRange",0.96f);
        m.EnableKeyword("Dissolve_ON");
        dissolve = true;
}

参考

・半透明 http://nn-hokuson.hatenablog.com/entry/2018/01/23/202530
・オーバーレイ http://sylphylunar.seesaa.net/article/390331341.html
・スキャンライン https://twitter.com/minionsart/status/899628037360234496
・シェーダのバリアント作成 http://light11.hatenadiary.com/entry/2019/01/12/232533

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

【Unity】点が集まった素材をランダムに点滅させる

はじめに

以下のような細かい点が集まったテクスチャがあります。

red_light.png

このテクスチャを以下のように点滅させる方法を解説します。
result.gif

アプローチ

  1. 時間とsinを使って点を明滅させる
  2. 時間を点ごとにランダムにずらす

ランダムな色を持つテクスチャの作成

点の時間をランダムにズラすためには点ごとに0~1のランダムな色が格納されたテクスチャが必要になります。
今回はSubstanceDesignerを利用してこれを作成します。

以下のようなノードを組みます。
image.png

結果として以下のテクスチャを得ます。 点ごとに0~1のランダムな値が格納されたテクスチャです。
RandomBlink_output.png

点滅シェーダー(uGUIシェーダー)

以下のようなシェーダーを利用してテクスチャを点滅させます。
先ほど作成したランダムテクスチャを以下のシェーダーの_BlinkRandomTexに割り当てることにより、点がランダムに点滅するようになります。

BlinkTexture.shader
// テクスチャを点滅させる
Shader "UI/BlinkTexture"
{
    Properties
    {
        [PerRendererData] _MainTex("Sprite Texture", 2D) = "white" {}
        _Color("Tint", Color) = (1,1,1,1) // 色
        [Space]
        _BlinkRandomTex("Blink Texture", 2D) = "white" {} // 点滅のバラつきを設定するためのテクスチャ(r:点滅の間隔, g:点滅の速さ)
        _BlinkIntervalMin("Blink Interval Min", Float) = 3.0 // 点滅間隔の最小値
        _BlinkIntervalMax("Blink Interval Max", Float) = 6.0 // 点滅間隔の最大値
        _BlinkSmoothstepEdge1("Blink Smoothstep Edge 1", Range(0,1)) = 0.1 // 点滅の緩急を調整するパラメータ
        _BlinkSmoothstepEdge2("Blink Smoothstep Edge 2", Range(0,1)) = 0.9 // 点滅の緩急を調整するパラメータ

        _StencilComp("Stencil Comparison", Float) = 8
        _Stencil("Stencil ID", Float) = 0
        _StencilOp("Stencil Operation", Float) = 0
        _StencilWriteMask("Stencil Write Mask", Float) = 255
        _StencilReadMask("Stencil Read Mask", Float) = 255

        _ColorMask("Color Mask", Float) = 15

        [Toggle(UNITY_UI_ALPHACLIP)] _UseUIAlphaClip("Use Alpha Clip", Float) = 0
    }

        SubShader
    {
        Tags
    {
        "Queue" = "Transparent"
        "IgnoreProjector" = "True"
        "RenderType" = "Transparent"
        "PreviewType" = "Plane"
        "CanUseSpriteAtlas" = "True"
    }

        Stencil
    {
        Ref[_Stencil]
        Comp[_StencilComp]
        Pass[_StencilOp]
        ReadMask[_StencilReadMask]
        WriteMask[_StencilWriteMask]
    }

        Cull Off
        Lighting Off
        ZWrite Off
        ZTest[unity_GUIZTestMode]
        Blend SrcAlpha OneMinusSrcAlpha
        ColorMask[_ColorMask]

        Pass
    {
        Name "Default"
        CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#pragma target 2.0

#include "UnityCG.cginc"
#include "UnityUI.cginc"

#pragma multi_compile __ UNITY_UI_ALPHACLIP

        struct appdata_t
    {
        float4 vertex   : POSITION;
        float4 color    : COLOR;
        float2 texcoord : TEXCOORD0;
        UNITY_VERTEX_INPUT_INSTANCE_ID
    };

    struct v2f
    {
        float4 vertex   : SV_POSITION;
        fixed4 color : COLOR;
        float2 texcoord  : TEXCOORD0;
        float4 worldPosition : TEXCOORD1;
        UNITY_VERTEX_OUTPUT_STEREO
    };

    // 円周率PIの2倍
    #define PI2 6.28318530718

    fixed4 _Color;
    half _BlinkIntervalMin;
    half _BlinkIntervalMax;
    fixed _BlinkSmoothstepEdge1;
    fixed _BlinkSmoothstepEdge2;
    float4 _ClipRect;
    sampler2D _MainTex;
    sampler2D _BlinkRandomTex;

    v2f vert(appdata_t IN)
    {
        v2f OUT;
        UNITY_SETUP_INSTANCE_ID(IN);
        UNITY_INITIALIZE_VERTEX_OUTPUT_STEREO(OUT);
        OUT.worldPosition = IN.vertex;
        OUT.vertex = UnityObjectToClipPos(OUT.worldPosition);

        OUT.texcoord = IN.texcoord;

        OUT.color = IN.color * _Color;
        return OUT;
    }


    fixed4 frag(v2f IN) : SV_Target
    {
        fixed4 color = tex2D(_MainTex, IN.texcoord); // カラー
        fixed4 blinkRandom = tex2D(_BlinkRandomTex, IN.texcoord); // 点滅のバラツキ

#define BLINK_INTERVAL lerp(_BlinkIntervalMin, _BlinkIntervalMax, blinkRandom.r)
#define BLINK_ALPHA_SINE (0.5 + 0.5 * sin(PI2 * (_Time.y / BLINK_INTERVAL - BLINK_SHIFT)))
#define BLINK_ALPHA smoothstep(_BlinkSmoothstepEdge1, _BlinkSmoothstepEdge2, BLINK_ALPHA_SINE)
#define BLINK_SHIFT (blinkRandom.g)

        color.a *= UnityGet2DClipping(IN.worldPosition.xy, _ClipRect) 
                * BLINK_ALPHA; // 点滅

#ifdef UNITY_UI_ALPHACLIP
        clip(color.a - 0.001);
#endif

        return color;
    }
        ENDCG
    }
    }
}

点滅の実装箇所(フラグメントシェーダー)

フラグメントシェーダー内にて点滅を実装しています。

点滅
        fixed4 frag(v2f IN) : SV_Target
        {
            fixed4 color = tex2D(_MainTex, IN.texcoord); // カラー
            fixed4 blinkRandom = tex2D(_BlinkRandomTex, IN.texcoord); // 点滅のバラツキ

    #define BLINK_INTERVAL lerp(_BlinkIntervalMin, _BlinkIntervalMax, blinkRandom.r)
    #define BLINK_ALPHA_SINE (0.5 + 0.5 * sin(PI2 * (_Time.y / BLINK_INTERVAL - BLINK_SHIFT)))
    #define BLINK_ALPHA smoothstep(_BlinkSmoothstepEdge1, _BlinkSmoothstepEdge2, BLINK_ALPHA_SINE)
    #define BLINK_SHIFT (blinkRandom.g)

            color.a *= UnityGet2DClipping(IN.worldPosition.xy, _ClipRect)
                    * BLINK_ALPHA; // 点滅

    #ifdef UNITY_UI_ALPHACLIP
            clip(color.a - 0.001);
    #endif

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

【Unity】セーブとロード簡単に実装してくれるManagerクラスを作ってみた

UnityのPlayerPrefsが使いづらい…

Unityで使えるPlayerPrefsを使っている方は多いと思います、自分も初めて使った時は感動して多様しました(笑)
ですがPlayerPrefsを使っていくうちに「アレ?なんか使いづらくね?」と気づくわけです。

PlayerPrefs何が使いづらい?

  • まず大量のセーブに向いていない点です。例えば100個の値を保存したい場合100回PlayerPrefsを呼び出す必要性があるわけです。
  • int型、float型、string型しか保存できない。
  • どこに保存してあるかよくわからない。

などなど割と使いづらい点があるわけです。

使いづらいなら作るしかない

使いづらいなら作ればいいじゃない理論でどうせなので作っていきましょう。

欲しい機能

  • 1回のSaveで多数の値を保存できる
  • セーブする時にフォルダ名、ファイル名を指定できる
  • 様々な値を一括で保存できる
    これらの機能を最低限つけて作ります。

コード

SaveLoadManager.cs
using System.IO;
using System.Runtime.Serialization.Formatters.Binary;
using UnityEngine;

public static class SaveLoadManager
{
    //ベースとなるフォルダ名
    private const string BASE_FOLDER_NAME="/BaseSaveFolder/";
    //デフォルトのフォルダ名
    private const string DEFAULT_FOLDER_NAME = "SaveFolder";

    /// <summary>
    /// フォルダー名の基づいてファイルをセーブ及びロードする時に使用するパスを決める
    /// </summary>
    /// <param name="folderName">フォルダ名</param>
    /// <returns>パス</returns>
    static string CreateSavePath(string _folderName = DEFAULT_FOLDER_NAME)
    {
        string savePath;

        //使っているプラットフォームを確認(IPhoneかどうか確認)
        if (Application.platform==RuntimePlatform.IPhonePlayer)
        {
            savePath = Application.persistentDataPath + BASE_FOLDER_NAME;
        }
        //それ以外のプラットフォームの場合
        else
        {
            savePath = Application.persistentDataPath + BASE_FOLDER_NAME;
        }

#if UNITY_EDITOR
        savePath = Application.dataPath + BASE_FOLDER_NAME;
#endif

        // セーブパス + SaveManager/
        savePath = savePath + _folderName + "/";

        return savePath;
    }

    /// <summary>
    /// 保存するファイル名を決める
    /// </summary>
    /// <param name="_fileName">ファイル名</param>
    /// <returns>保存されるファイル名</returns>
    static string SaveFileName(string _fileName)
    {
        return _fileName + ".binary";
    }

    /// <summary>
    /// セーブする
    /// </summary>
    /// <param name="_saveObject"></param>
    /// <param name="_fileName">ファイルの名前</param>
    /// <param name="_folderName">フォルダの名前</param>
    public static void Save(object _saveObject,string _fileName,string _folderName=DEFAULT_FOLDER_NAME)
    {
        //セーブパスを決める
        string savePath = CreateSavePath(_folderName);

        //ファイル名を決める
        string saveFileName = SaveFileName(_fileName);

        //ディレクトリがあるか確認、なければ作成
        if (!Directory.Exists(savePath))
        {
            Directory.CreateDirectory(savePath);
        }

        //クラスをバイナリとして扱う
        BinaryFormatter formatter = new BinaryFormatter();

        //ファイル作成
        FileStream saveFile = File.Create(savePath+saveFileName);

        //オブジェクトをシリアル化しディスク上にファイル書き込み
        formatter.Serialize(saveFile,_saveObject);

        //書き込み終了
        saveFile.Close();
    }

    /// <summary>
    /// ロードする
    /// </summary>
    /// <param name="_fileName">ファイルの名前</param>
    /// <param name="_folderName">フォルダの名前</param>
    /// <returns></returns>
    public static object Load(string _fileName,string _folderName=DEFAULT_FOLDER_NAME)
    {
        //セーブパス(指定フォルダ名)を取得
        string savePath = CreateSavePath(_folderName);

        //バイナリファイル名を取得
        string saveFileName = savePath + SaveFileName(_fileName);

        //返すデータ
        object returnObject;

        //Saveディレクトリまたはバイナリファイルが存在しない場合
        if (!Directory.Exists(savePath))
        {
            Debug.LogError("ディレクトリが見つかりませんでした");
            return null;
        }
        if (!File.Exists(saveFileName))
        {
            Debug.LogError("ファイルが見つかりませんでした");
            return null;
        }

        BinaryFormatter formatter = new BinaryFormatter();

        //ファイル開く
        FileStream saveFile = File.Open(saveFileName,FileMode.Open,FileAccess.Read,FileShare.Read);

        //バイナリファイルをデシリアル化しオブジェクトに変換する
        returnObject = formatter.Deserialize(saveFile);

        saveFile.Close();

        return returnObject;
    }
}

これでSaveとLoadをどこでもできるようになりました。

使ってみよう

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

public class test :MonoBehaviour
{
    //外部からいじれるように
    //セーブしたい内容をクラスで用意
    [Serializable]
    public class Status
    {
        public string name;
        public float hp;
        public float exp;
        public int level;
    }
    [SerializeField]
    Status status = new Status();
     [SerializeField]
    Status kakunin = new Status();

    private void Update()
    {
        //セーブ
        if (Input.GetKeyDown(KeyCode.S))
        {
            SaveLoadManager.Save(status, "PlayerStatus","PlayerData");
        }

        //ロード
        if (Input.GetKeyDown(KeyCode.L))
        {
            status = (Status)SaveLoadManager.Load("PlayerStatus", "PlayerData");
        }
    }
}

これは使えるかテストするために用意した簡易的な物なのでもし実用的に使うつもりなのであればManagerクラスを作成してそこで管理させてあげるといいと思います。

実行結果

3anrw-fn0i3.gif

まとめ

まだまだ拡張の余地があると思うので今後もアップグレードしようと思います。

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

Unity UIの自分用まとめ

ピボット、アンカー、RectTransform、Canvas・・・色々な要素があるUI。いろんなサイトをまわってみたが、体系的にどの要素がどう作用するのか、順を追って解説してくれておりかつ私自身の感覚にマッチするまとめ方をしているものが見当たらなかった。マジ難しいので、TechAcacemyのメンターさんと話してなんとかイメージできた(かな?)内容をまとめておく。
※未熟なので、表現の誤りなどには目をつむっていただきたい。私流の言葉で表現しているので。

UI

Canvasという平面オブジェクトの子オブジェクト。Canvasとカメラ、スクリーンとの関係はまず3種類ある。

Canvasのスクリーンへの描画

Canvasとカメラとスクリーンの関係はCanvasオブジェクトをヒエラルキービューで選択してインスペクタに表示されるCanvas - Render Modeで設定できる。

デフォルトでOverlayが選択されており、他の選択肢としてはCameraWorldという順に並んでいる。

メモ
World→Camera→Overlayの順に仕様としての理解をしていった方が個人的にはわかりやすいと思った。
Overlayはマジで世界座標系上での表示をやめてほしい・・・意味ないし混乱するから。

World

Canvas以外のオブジェクトと同様、カメラを通して実際のCanvasオブジェクト(および子オブジェクトたるUI)の大きさと世界座標系における位置を考慮してレンダリング、スクリーンに表示するというモード。Canvasオブジェクトを動かしてカメラから見えなくなると当然Canvasオブジェクトの子オブジェクトであるUIはスクリーンに表示されない。

Camera

Worldと同じく実際のCanvasオブジェクトの大きさ、世界座標系における位置を考慮してレンダリングされる。違いとしては、カメラとCanvasが紐付けされ、Canvasの位置は必ずカメラの真正面に来る状態になる。カメラを動かすとCanvasも動き、逆も然り。

Overlay

World, Cameraとは大分毛色が異なり、実際のCanvasオブジェクトの大きさや世界座標系における位置といったものは一切考慮されず、カメラを通してレンダリングされることがなく、Canvasオブジェクトをそのままスクリーンに表示させるような形になる。すなわちクロマキー的なもの、プリクラのお絵かき機能のお絵かきに相当するもの、ポケモンGoのARモードで描画されるポケモンに相当するもの(背景は実際の風景であり、こちらは現実空間の物体がレンダリングされたもの)というようなイメージがわかりやすいか。

注意
カメラを通してレンダリングされるわけではないのに、シーンビュー上でOverlayのオブジェクトが世界座標系上に存在するのでマジで混乱する。UnityのUIがおかしいんだと割り切って気にしてはいけないのだが、初学者がそんなことを知るわけがなく・・・これはUnityのUIとして機能改善してほしい。最新バージョンだとされてるのかもしれないけど。

UI描画領域の大きさと位置

UIオブジェクトをヒエラルキービューで選択してインスペクタに表示される「Rect Transform」で設定する。
私自身の混乱を避けるため、今の段階ではAnchorsのMin、Maxがx,yそれそれで同じ値で設定する前提とする。
※違う値になると相対位置?みたいな感じになるらしい。

https://tech.pjin.jp/blog/2017/03/22/unity_ugui_rect-transform/

UI描画領域の大きさ

Width, Heightで設定する。

UI描画領域の位置

UIはCanvasの子オブジェクトであり、UI描画領域の位置はCanvasを基準に決定される。
決定に関係する設定はAnchors, Pivot, PosX, PosY, PosZである(把握している分だけでいえば)。
今の所、描画領域の位置の決定はPosX,Y,Zは0,0,0固定でAnchorsとPivotのみの設定でほぼ事足りるように思える。

Anchors

アンカー。UI描画領域の基準点(Pivot)をCanvasのどこに合わせるか?を示す座標値を設定する。
ただし、Anchorsの座標値はCanvasの大きさを0〜1のスケールで表した座標系となる。

Pivot

ピボット。UI描画領域の基準点であり、この基準点の座標値を設定する。
Pivotの座標値はUI描画領域の大きさを0〜1のスケールで表した座標系となる。

PosX,Y,Z

これが0,0,0である = AnchorsとPivotがぴったり合わさっている状態。この状態からPosX,Y,Zの方向へ設定値分並行移動させて最終的なUI描画領域の位置が決定される・・・というイメージ。座標値は世界座標系のスケール。

設定例

インスペクタでRect Transformを以下のように設定した場合のUI描画領域のCanvas上における位置を図示してみた。
※図の中での(x,y)はCanvasの0~1スケール座標値、[x,y]はUI描画領域の0〜1スケール座標値を表している。

RectTransform設定値
- (PosX, PosY, PosZ) = (0,0,0)
- (Width, Height) = (100, 100)
- Anchors(MinX, MinY, MaxX, MaxY) = (1, 1, 1, 1)
- Pivot(x,y) = (1,1)

IMG_5755.JPG

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