サークル獏の佐藤敏 Unityとか備忘録

サークル獏の佐藤敏がUnityとかで知ったTipsを書いておく備忘録です。

乱暴に始めるUnityEditor拡張

乱暴に、レイアウトも雑に始めてしまえ

UnityEditor拡張は沼だとか言われるし、入門ページでも結構導入とかレイアウト操作を丁寧に書いているので難しそうに見える。
なので、乱暴ながら一応使い道がありそうな奴を。

f:id:VinSatoo:20180809133334p:plain:w300
Cubeを置いただけのUnityプロジェクト。

  • Editor拡張は「Editor」という名前のフォルダの中に作らないといけないので、フォルダ作成。
  • スペルミスしないように注意

 
 

スクリプトを書く

f:id:VinSatoo:20180809133639p:plain:w300
Editorフォルダの中に適当な名前でC#スクリプトを作る。
んで、そのファイルの中に以下をコピペして保存。

using UnityEngine;
using UnityEditor; //Editor拡張には必要

public class TestEditor : EditorWindow
{
    [MenuItem("Window/PosMove")]


    public static void ShowWindow() {
        EditorWindow.GetWindow(typeof(TestEditor));
    }

    void OnGUI() {
        if(GUI.Button(new Rect(20.0f, 20.0f, 120.0f, 20.0f), "X+")) { //ボタンの位置と名前を決める
            var obj = Selection.activeGameObject; //選択中オブジェクト
            if (obj != null) {
                var pos = obj.transform.position;
                pos.x += 0.1f;
                obj.transform.position = pos; //Xを0.1プラスした
            }
        } else if (GUI.Button(new Rect(20.0f, 50.0f, 120.0f, 20.0f), "X-")) { //ボタンの位置と名前を決める Y位置だけ1つめのボタンより下にずらした
            var obj = Selection.activeGameObject; //選択中オブジェクト
            if (obj != null) {
                var pos = obj.transform.position;
                pos.x -= 0.1f;
                obj.transform.position = pos;  //Xを0.1マイナスした
            }
        }
    }
}

 
 
f:id:VinSatoo:20180809141204p:plain:w300
Unityに戻りコンパイルが行われる。すると、Windowメニューにプログラムの6行目に書かれた名前の「PosMove」という項目ができている。

f:id:VinSatoo:20180809141104p:plain:w300
で、それを選ぶとこのように「TestEditor」という新しいウィンドウが現れる(変な場所に出ることもあるのでよく探して)。
X+を押すと選択中の物体のXが0.1ずつプラスされていく。X-はその逆。 
 
 

何が嬉しいのか?

この程度ならCubeを操作パネルからマウスで動かしても同じだ。

  • しかし、0.1ずつ微調整したいなんて時には使い道がある。
  • そして最大の違いは、SceneウィンドウでもGameウィンドウでも(つまりゲーム進行中でもSceneに戻らずに)使えるという点だ。
  • しかもスクリプトで書けるので複雑なこともできる。
  • 例えば「他のオブジェクトと合わせる」「Yだけ0にリセット(≒床に下ろす)」とか。

 
 
よくある「Gameという空オブジェクトにGameManagerコンポーネントがくっついていて、そこがゲーム進行の司令塔」なんて場合ならEditor拡張のボタンを押したときの処理に

var game = GameObject.Find("Game").GetComponent<GameManager>()
game.PlayBGM("エンディング");
game.ChangeScene("エンディングシーン");

とかすることでデバッグを便利にすることもできる。
 
 

乱暴に始めた後はレベルアップを

もちろん今回のはEditor拡張の初歩の初歩で、本当は細かいレイアウト機能や、ユーザーに数字を入力させてボタンを押したときに活用する機能など色々なものがある。
その辺はすぐれた入門ページがたくさんあるので、そちらで勉強を。
anchan828.github.io
caitsithware.com
qiita.com


ただ、乱暴に「ボタンを置いて、それを押したらなんか処理してくれる」だけでもEditor拡張は普通に便利だからどんどん活用していこう。
どうせ使うのは自分だ。

派生クラスのインスタンスを全部生成してDictionaryに入れるとか

どーんと派生クラスの一覧を得る便利なコード

esprog.hatenablog.com
@es_programさんのこういった記事がある。
 
 

自分用の使い道を考える

で、使い方として考えたのが、例えば「色々なボイス反応を返すクラスのインスタンスを全部自動生成して、辞書からアクセスできるようにする」っていう形。

あまり良い使い方でもないかもしれないが、調教SLGとかだと「パラメータで色々分岐するのでExcelの表みたいなので完全管理は難しい」ケースがあるのでこんな感じが割と使い勝手良かったりする。
(本来ならLuaとかのスクリプトに担当させて、いちいちコンパイルが走らないようにするのがカシコイのかもしれない)

以下のスクリプトのTestScriptをUnityで適当なempty objectとかにアタッチして実行すると、こんな実行結果が得られる。
f:id:VinSatoo:20180808133902p:plain

もっといいやり方があってもご愛敬。
 
 

スクリプト部分

using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using UnityEngine;

abstract public class VoiceResItem //抽象クラス
{
    public string name { get; set; }
    public virtual string GetResponse(int obey, bool damaged) {
        return "";
    }
}

public class VoiceHateResItem : VoiceResItem //派生クラス
{
    public VoiceHateResItem() {
        name = "憎悪";
    }

    public override string GetResponse(int obey,bool damaged) {
        if (damaged)
            return "うっ……くっ……!";
        switch (obey) {
            case 0:
                return "大嫌い!";
            case 1:
                return "きらい。";
            default:
                return "そういうことやめてよ";
        }
    }
}

public class VoiceLoveResItem : VoiceResItem //派生クラス
{
    public VoiceLoveResItem() {
        name = "好き";
    }

    public override string GetResponse(int obey, bool damaged) {
        if (damaged)
            return "私は……っ";
        switch (obey) {
            case 0:
                return "何よ…";
            case 1:
                return "別にイヤじゃないっていうか";
            default:
                return "好き。";
        }
    }
}

public class VoiceResManager //それらを管理
{
    Dictionary<string, VoiceResItem> dic;

    public VoiceResManager() {
        dic = new Dictionary<string, VoiceResItem>();

        var classesList = Assembly
.GetAssembly(typeof(VoiceResItem))
.GetTypes()
.Where(t => {
    return t.IsSubclassOf(typeof(VoiceResItem)) && !t.IsAbstract;
});

        foreach (Type type in classesList) {
            MakeDic((VoiceResItem)Activator.CreateInstance(type));
        }

    }

    void MakeDic(VoiceResItem resItem) {
        dic[resItem.name] = resItem;
    }

    public string GetResponse(string key, int obey, bool damaged) {
        if (dic.ContainsKey(key)) {
            return dic[key].GetResponse(obey, damaged);
        }

        Debug.Log("エラー");
        return "";
    }
}

public class TestScript : MonoBehaviour
//Unityで実際に使ってみる用 適当なEmpty Objectにでもアタッチして実行
{
    VoiceResManager voiceResManager;
    void Start() {
        voiceResManager = new VoiceResManager();
        for (int i = 0; i < 3; i++) {
            Debug.Log(voiceResManager.GetResponse("憎悪", i,false));
            Debug.Log(voiceResManager.GetResponse("好き", i, false));
        }
        Debug.Log(voiceResManager.GetResponse("憎悪", 0,true));
        Debug.Log(voiceResManager.GetResponse("好き", 2, true));
        Debug.Log(voiceResManager.GetResponse("普通", 1,true));
    }
}

Unityでカメラが近づくと角度によりモデル(の一部)が突然消える

カメラが近づくとモデルが突然消えた……

f:id:VinSatoo:20180813150726p:plain:w150
こうだったのが突然……
 
f:id:VinSatoo:20180813150522p:plain:w150
ギャー
 

どうすればモデルが消えなくなるか

Unityでカメラが近づきすぎるとモデルが消えるパターンとしてはまずカメラに設定してある「NearClip(CameraのClipping PlanesのNear)」が大きすぎるというのがある。特にVRなどでは顔を近づけてもギリギリまで描画して欲しいので、NearClipは小さめにする。

が、NearClipを小さめにしていても、モデル(の一部)が突然消える現象がある。
カメラが遠い場合は発生しないが、ある程度近づくとカメラの角度により突然消える。しかも再登場するときはDynamicBoneなどがボヨンボヨンするので、表示が消えていただけでなく動作自体が止まっていることがうかがえる。

これは、以下のブログの記述にある通り、「skinnedMeshRenderer」の「update whenoffscreen」をONにすればよい。

testaa.ryorika.com
 
 

より適切な解決方法

「update whenoffscreen」をONにするのは簡単でよいのだが、名前の通り「見えないときも処理を行う」ことになってしまうのでキャラがたくさん出てくるゲームなどでは問題になる。
で、より適切な解決方法としては「skinnedMeshRenderer」の「Bounds」の「Extent」を大きくすれば良いとのこと。



(onotchiさん、izmさん、説明頂きありがとうございますm(__)m )
 
 

なんでそんなことになるのか

じゃあなんで「Bounds」の値が適切じゃない=カメラの中にいるはずなのに非表示になるような値になっちゃってるのかというと、こういうことらしい。


(さすが@hecomiさん)

docs.unity3d.com

オブジェクトのビジビリティはメッシュの Bounds によって決定されます (言い換えると、全体のバウンディングボリュームはあらゆる有効なカメラの視界角外にある必要があります)。ただし、アニメーションしているメッシュの実際のバウンディングボリュームは、アニメーションの再生によって変化します (例えば、キャラクターが手を頭上に伸ばすと、ボリュームの高さが増えます)。Unity は、すべてのアタッチされたアニメーションが最大のバウンディングボリュームになるときを考慮しますが、すべての使用可能な状況を予想してバウンディングボリュームを計算することは、不可能な場合があります。

以下のような状況では、ボーンや頂点が事前計算されたバウンディングボリュームの外に押し出されてしまい、問題が生じます。

ランタイム時にアニメーションを追加する場合

要するに、動きのあるモデルの場合は最初に想定したモデルのサイズよりも大きい形になってしまう。
だからカメラの中にいるのに外にいると判断されることがあると。
 
 

「Update When Offscreen」にも使い所あり?

Extentを編集するのもモデルが増えてくると大変だし、どれくらいの数字が適切なのかも難しいし、

これらの例では、2 つの解決方法があります。

Bounds (境界) を修正してメッシュの可能なバウンディングボリュームに適合するように修正します。
Update When Offscreen をスキンに対して有効にし、常にスキンメッシュをレンダリングします。
通常は、パフォーマンスに与える影響も少ないため最初の方法を選択します。ただし、パフォーマンスを重視する必要がない場合や、バウンディングボリュームを予測できない場合 (例えばラグドール物理を使用する場合) に、2 番目の方法を選択します。

とUnity公式さんも言っているので、Unityで動画を撮るだけみたいなケースでは「Update When Offscreen」を使うのも良さそうだ。

不要パーツを消す「何もしない透明なUnityシェーダー」

バッドノウハウっぽいけれど、3D初心者は初心者らしく初心者的なノウハウを書いていこうかと。

既存のモデルを使うとき、「このパーツは要らないなあ」とか思うことがある。

  • そのパーツがUnity上で独立したGameObjectになっていれば非表示するだけで済む。
  • だが、1つのMeshの中に複数パーツがまとめられているとそうもいかない。
  • Blenderとかに行ってMeshをいじる必要がある。
  • ヘタするとその過程で色々壊れて面倒なことになる。

で、「何もしない透明なUnityシェーダー」を適用してしまう。最初からマテリアルが分けられていればこのシェーダーを使ったマテリアルを適用するだけで済む。
仮に必要なパーツと要らないパーツが同じマテリアルの間にまたがっていても、Blenderでマテリアルの塗り分けをするだけならモデルが壊れる可能性は低い。
(もちろんチャンとMeshを消すのと比べて重くなることは予想されるので、速度にシビアな局面では使えないと思われる)

Shader "Custom/VoidShader" {
SubShader{
Colormask 0 Zwrite Off
Pass{}
}
}

多分もっと簡単な書き方があるのだろうけど、とりあえずこれで動く。
より軽い書き方があったら教えて下さい。

よりシンプルなのを教えて頂いたので、そっちで。

引っ張ってきたUnityシェーダーがinvalid subscript uv2 エラー

Unityで以前作ったStandardを改変したシェーダーを別のプロジェクトに持ち込んだら、


invalid subscript 'uv2' at /Program Files/Unity/Editor/Data/CGIncludes/UnityStandardMeta.cginc(21) (on d3d11)

というエラーが出た。
これは、そのシェーダーをProjectウィンドウで右クリックして「Reimport」すればいいらしい。

https://www.reddit.com/r/Unity3D/comments/8kjzdf/standard_shader_invalid_subscript_uv2_error/