見出し画像

UdonSharpを始めた話~その1

はじめまして、
逆星るぅと申します。

今日はVRchatの開発ツールであるVRCSDK3から正式採用されたUdonというノード型のプログラミング支援システムに初めて取り掛かった時に、私がつまづいた所や気付きを得たことなどを書いて行きたいと思います。

Udon自体はノードベースなのですが、そのオペレーション自体はC#の知識がある程度ないと使いこなすのは難しいらしく、C#初心者レベルの自分には挫折しそうな気配が濃厚だったので、テキストエディタでのコーディングが可能なUdonsharpから導入してみようと考えました。

自分の場合、チュートリアル的なものを進めていくより実際に何か一つ作ったほうが身になると思ったので、ちょうど8月にVket2022SummerというVRChat上で開催されるイベントのブース出展の抽選に受かったので、ここで頒布するUdonsharpを使用した何かアセット的なものを一つ作ることにしました。

今回制作するもの

以前、VRCSDK2で光線銃のようなアセットを作った記憶が有ったので、今回はそれを発展したレーザーライフルを作ることにしました。
今回の仕様としては…

  1. 銃をグリップし、トリガーを握っている間光線が発射される。

  2. 握り続けているとエネルギーが空になる。

  3. トリガーを離すとエネルギーが充電される。

  4. 銃のグリップを解除して再度グリップするとエネルギーが満充電にリロードされる。

  5. エネルギー残量がだいたい把握できるようにする。

この5つを実現したい目標としてアセットの作成を進めていきました。

今回はスクリプトの研究も兼ねていたので、Blenderで3Dモデルを作成する前にある程度Udonsharpのコーディングを進めていこうと思いました。

Unity C#と違うところ

色々調べてみるとUdonsharpは現バージョンではUnityのメソッドをそのまま使えないものがいくつか有るらしく、今回のアセット制作に関わってくる下記のものが使えないそうです。

Listが使えない

今回作成にあたって、本来はRaycastではなくパーティクルの当たり判定を拾うようなスクリプトを書こうと考えていましたが、パーティクルのヒット位置を検出し、リストアップする関数が使えないことが分かったので方式を変更しました。

Listについての参考出典:

Instantiateが使えない

オブジェクトインスタンスを出現させるメソッドですが、ベタ打ちしてもエラーしか出てこないのでこちらの代替としてはVRCInstantiateを使用しました。
ただこちらのメソッドについては現状同期が取れないらしく、他のユーザーからはアニメーションが見えないみたいです(今後サポートされるかな?

コルーチン系が使えない

詳しくは学習が追いついていないので不明な部分はありますが、
IEnumerator関数で宣言するような数秒に一度処理を行う必要がある場合に多用されるメソッドが一式使えないようです。
こちらの代替としてはUpdate関数の中に必要な処理を一通り書く方式にしました。

そのようにして何行かコードを書いていくと、「これ、実際にPrefabにアタッチしたらどう動くんだろうか」と気になってきたので、コーディングは一旦止めてモデリングの方を進めていくことにしました。

まぁ、このツイートにも書いてある通りVRC特有のメソッドについてもあまり理解できていない状況だったので。。
さらに言えば最初のときUpdate関数のスペルを”UpDate”と間違えていたので、Update関数が機能していないことに気づかず何これ使い方分からんと思って何も書き込まず空欄にしておりました。
(これに気づいていればこんなに悩むことは無かったのにねという、ちょっとした後悔です。)

モデリング

Blenderを使用して3Dモデルを作成

エフェクトはUnityでアレコレいじるので、平面画だけを描きました。

こんな感じのエフェクト

最終的に書き上げたもの

    [SerializeField]
    Animator motionAnimator = null; // モーションを読み込む
    [SerializeField]
    ParticleSystem particleUnitBeam = null;
    [SerializeField]
    AudioSource audioSource = null; // 音声を読み込む
    [SerializeField]
    AudioSource audioSourceEmpty = null; // 音声を読み込む
    [SerializeField]
    AudioSource audioSourceCharging = null; // 音声を読み込む
    [SerializeField]
    AudioSource audioSourceFullCharged = null; // 音声を読み込む
    [SerializeField]
    Transform laserSpawn = null; // ビーム発生位置を読み込む
    [SerializeField, Min(1)]
    float maxRange = 30; //Rayの最大射程
    [SerializeField] // ヒット対象のレイヤー記述、全てのオブジェクトが同じレイヤーに居ると再生した際の挙動がおかしなことに。
    LayerMask hitLayers = 0;
    float setTime = 0;
    float oneShot = 0.2f;
    float charging = 0.75f;
    [SerializeField, Min(0f)]
    float currentEnergy = 30f; //現在の残弾数
    [SerializeField, Min(0f)]
    float fullEnergy = 30f; //総残弾数
    [SerializeField]
    GameObject LaserMazzleFlashEffectPrefab = null; //マズルフラッシュ系エフェクト追加
    [SerializeField]
    GameObject laserHitEffectPrefab = null; //レーザーが衝突して火花が飛び散るエフェクト追加
    float timer = 3.0f; // beamHitEffectPrefabを破棄する時間
    float beamKill = 1.5f; // beamHitEffectPrefabを破棄する時間
    [SerializeField]
    RaycastHit hit;
    private bool TriggerFire; // VRCトリガーの状態
    private bool TriggerDrop; // VRCトリガーの状態
    private bool OnceSounding;
    void Start() {
        float currentEnergy = 30f;
        bool TriggerFire = false;
        bool TriggerDrop = true;
        bool OnceSounding = false;
        motionAnimator.Play("Power Indicator.laserrifle_empty");
    }
    void Update() {
        if ( TriggerFire == true && TriggerDrop == false ) {
            setTime += Time.deltaTime;
            if ( setTime > oneShot ) {
                currentEnergy --;
                if ( currentEnergy >= 1f ) { //現在の残弾数が1以上の場合
                    audioSource.Play(); // 音声を再生
                    audioSourceCharging.Stop(); // 音声を停止
                    particleUnitBeam.Play();
                    var lbfx = VRCInstantiate(LaserMazzleFlashEffectPrefab);
                    lbfx.transform.position = new Vector3( laserSpawn.position.x , laserSpawn.position.y , laserSpawn.position.z );
                    Destroy (lbfx, beamKill);
                    if ( currentEnergy < fullEnergy * 1 && currentEnergy > fullEnergy * 0.8 ) {
                        motionAnimator.Play("Power Indicator.laserrifle_energy_full");
                    } else if ( currentEnergy < fullEnergy * 0.8 && currentEnergy > fullEnergy * 0.65 ) {
                        motionAnimator.Play("Power Indicator.laserrifle_energy_stillFull");
                    } else if ( currentEnergy < fullEnergy * 0.65 && currentEnergy > fullEnergy * 0.4 ) {
                        motionAnimator.Play("Power Indicator.laserrifle_energy_lastHalf");
                    } else if ( currentEnergy < fullEnergy * 0.4 && currentEnergy > fullEnergy * 0 ) {
                        motionAnimator.Play("Power Indicator.laserrifle_energy_emptyAlart");
                    } else {
                    }
                    if (Physics.Raycast(laserSpawn.position, laserSpawn.forward, out hit, maxRange, hitLayers, QueryTriggerInteraction.Ignore)) {
                        if (hit.collider == null) {
                            Debug.Log ("状態:ヌルかプレイヤーに当たった");
                        } else {
                            var lfx = VRCInstantiate(laserHitEffectPrefab);
                            lfx.transform.position = new Vector3(hit.point.x, hit.point.y, hit.point.z);
                            // Debug.Log ("状態:レーザーがヒットした");
                            Debug.DrawRay(laserSpawn.position, laserSpawn.forward, Color.red, 1f);
                            Destroy (lfx, timer);
                        }
                    } else {
                        // Debug.Log ("状態:レーザーが射出されたがColliderには衝突しなかった");
                    }
                } else if ( currentEnergy < 0f ) { //残弾数が0の場合
                    motionAnimator.Play("Base Layer.laserrifle_on_empty_action");
                    motionAnimator.Play("Power Indicator.laserrifle_empty");
                    if ( OnceSounding == true ) {
                        audioSourceEmpty.Play(); // 音声を再生
                        OnceSounding = false;
                    }
                    currentEnergy = 0;
                    // Debug.Log ("状態:エネルギー切れ");
                }
                setTime = 0;
            }
        } else if ( TriggerFire == false && TriggerDrop == false ) {
            setTime += Time.deltaTime;
            if ( setTime > charging ) {
                currentEnergy ++;
                if ( currentEnergy < fullEnergy ) { //現在の残弾数がfullEnergy以下の場合
                    OnceSounding = true;
                    audioSourceCharging.Play(); // 音声を再生
                    // Debug.Log ("状態:チャージ中");
                    if ( currentEnergy < fullEnergy * 1 && currentEnergy > fullEnergy * 0.8 ) {
                        motionAnimator.Play("Power Indicator.laserrifle_energy_full");
                    } else if ( currentEnergy < fullEnergy * 0.8 && currentEnergy > fullEnergy * 0.65 ) {
                        motionAnimator.Play("Power Indicator.laserrifle_energy_stillFull");
                    } else if ( currentEnergy < fullEnergy * 0.65 && currentEnergy > fullEnergy * 0.4 ) {
                        motionAnimator.Play("Power Indicator.laserrifle_energy_lastHalf");
                    } else if ( currentEnergy < fullEnergy * 0.4 && currentEnergy > fullEnergy * 0 ) {
                        motionAnimator.Play("Power Indicator.laserrifle_energy_emptyAlart");
                    } else {
                    }
                } else if ( currentEnergy > 30f ) {
                    motionAnimator.Play("Power Indicator.laserrifle_energy_full");
                    audioSourceCharging.Stop(); // 音声を停止
                    if ( OnceSounding == true ) {
                        audioSourceFullCharged.Play(); // 音声を再生
                        OnceSounding = false;
                    }
                    currentEnergy = 30f;
                    Debug.Log ("状態:チャージ完了");
                }
                setTime = 0;
            }
        } else if ( TriggerFire == false && TriggerDrop == true ) {
            audioSourceCharging.Stop(); // 音声を停止
            currentEnergy = 0f;
        } else if ( TriggerFire == false && TriggerDrop == false ) {

        }
    }
    public override void OnPickup() {
        currentEnergy = 30f;
        TriggerDrop = false;
        TriggerFire = false;
        OnceSounding = false;
        motionAnimator.SetInteger("State", 0);
        motionAnimator.Play("Power Indicator.laserrifle_switchOn");
    }
    public override void OnPickupUseDown() {
        motionAnimator.SetInteger("State", 1);
        OnceSounding = true;
        TriggerDrop = false; 
        TriggerFire = true;
    }
    public override void OnPickupUseUp() {
        motionAnimator.SetInteger("State", 3);
        OnceSounding = true;
        TriggerDrop = false; 
        TriggerFire = false;
    }
    public override void OnDrop() {
        motionAnimator.SetInteger("State", 4);
        motionAnimator.Play("Power Indicator.laserrifle_empty");
        OnceSounding = true;
        TriggerDrop = true; 
        TriggerFire = false;
    }

うぅ…
多分ものすごく冗長的なコードになった可能性がありますが、
プログラミングいまいち不慣れな人間なのでご容赦いただければ…

今回のアセットの作成はUnity側でもステートマシンなど色々と触ったのですが、本エントリーの趣旨はあくまでUdonsharpについてなので省略します。

なお、今回載せたコードの解説については次回書こうと思いますので、
もう少々お待ちいただければと思います。

参考サイトなど出典:


この記事が気に入ったらサポートをしてみませんか?