Unity

調整とVisualStudioのCopilotの使用感

Unity

いやぁ、年末だからなのか、クソテレビは煽るのに忙しいようで。
そんなに人柱が欲しいんでしょうか? まあ、煽ってる側が人柱になればいいのでは? クローンで腐るほどいるんだから。



微調整も含め、音声部分の調整次第で、具体的に言うとその音声を再生完了まで待つかどうかによってタイミングが変わる。
という確認が地味ながら大変だったりします。テキストベースだったら単純に秒数でどうとでもなりますけどね。

あと、微妙に音声のところで気になるところがあって、加工漏れがあったから随時対応です。
一応消したと思ったんだけど、微妙に残ってる箇所があった。息継ぎ箇所とか意図的に消してますよ。いいお声を生かすために。

…まあ、NGボイスとか微妙に残ってたのもあって、終わり部分のごにょごにょ感がかわいい。あ、見なかったことに。

音声とテキストベースの違いがこうもあるとは。
大変だけど、本当にいい勉強です。それと、メルトぐらいの喋る速度の方が、E-moteの口パクと相性がいいですね。口パクが潰れなくていい。これ、極めて重要。早い感じだと口パクが対応しきれないとか、調整がきついです。

よくよく見たら、宴側でタイトル画面でもBGM鳴らせるようだから、そのまま遷移できるように変更しました。
ついでに、万が一の保険もかけてスクリプトも改修。以前は無音でしたが、これでコンフィグで音量いじっても即座に確認が取れます。UtageUguiTitleでタイトル画面、Bootの方で起動直後。


今後次第ですが、キャラによって音量調整もどうなるかは、やれるかどうか…。
やってることがちょっとおかしくなってきたな…。

VisualStudioのCopilotも優秀だとか聞いたので、ちょっと使ってみました。
ChatGPTで作ったスクリプトを何回か直してもらいましたが、エラーも収まりました。

宴と相性はいいかもしれません。
ソースコードを参照して全部直してる感じですので。その分思考時間はありますけども。

AIにやらせてその分祈りながら待つ時間が増えたとかも見ましたけど。お祈りゲーム? 就活?

直った部分を確認して、参照の仕方とかはよく見てます。
状況で散らばってるスクリプトを集約とかも方法かも。集約できれば楽ですけど、正直新規で組み直しとかじゃない限りは結局初手が面倒という。

クリックパーティクルで違う制御を試みたらエラー吐いてて、それを直してもらったのがきっかけでした。
まあ、ちゃんとその試み自体が機能してるかは分かりません。よく分からん。

うん、既にその弊害はあったんですけどね。
でも、これは宴の仕様みたいで、少し面倒な感じです。オートモードでVoiceコマンド実行中止まってるのを確認した。

オートモードでテストしてなかったからまるで気づきませんでした。
そう考えると、少し方法を考えないと。

それ以外に、せっかくだからAndroidでの動画の再生もできるかやってましたが、やっぱりこれは無理でした。
再生はできるようになりつつあったけど、音声が出ない、再生速度が異様に速い、2個目の動画がすぐに終わるとか、とにかく安定性がまるでない。

本当、なんなんですかね、この謎の仕様というか、弊害は。どうやっても無理ゲーである。
動画二つだとどうしても安定性が皆無だったから、単体に変更しました。2個、連続再生が安定せず。

一応、動くようになったのでソースコード置いておきます。Androidではこれから確認します…。
エディタ上では普通に再生してる。前は映りもしません。Android以外なら映すのは比較的簡単です。

using UnityEngine;
using UnityEngine.UI;
using UnityEngine.Video;
using System.Collections;
using System.IO;
using UnityEngine.Networking;
using UnityEngine.EventSystems;
using Utage;

public class StreamingVideoPlayer : MonoBehaviour
{
    public VideoPlayer videoPlayer;
    public RawImage rawImage;
    public string fileName = "Emote_rogo1280x720.mp4";

    // RenderTexture used at runtime (released on destroy)
    RenderTexture runtimeRenderTexture;

    bool hasStarted = false;
    public bool allowSkipByClick = true; // click/touch to skip
    public bool openTitleOnSkip = true; // open title UI when skipped/finished

    IEnumerator Start()
    {
        string videoPath = System.IO.Path.Combine(Application.streamingAssetsPath, fileName);
        // ensure RawImage initially disabled until texture is ready
        if (rawImage != null)
        {
            rawImage.enabled = false;
        }

        // hook end event
        if (videoPlayer != null)
        {
            videoPlayer.loopPointReached += OnVideoFinished;
        }

        // パスの組み立て
        string streamingAssetPath = PathCombineStreamingAssets(fileName);

        string url = streamingAssetPath;

        // Android の場合、StreamingAssets は apk 内に梱包されているため直接 VideoPlayer に渡せないことがある。
        // そのため一旦 Application.persistentDataPath にコピーしてから再生する。
        if (Application.platform == RuntimePlatform.Android)
        {
            string persistentPath = Path.Combine(Application.persistentDataPath, fileName);

            // 既にコピー済みならスキップ
            if (!File.Exists(persistentPath))
            {
                using (var uwr = UnityWebRequest.Get(streamingAssetPath))
                {
                    // ダウンロードしてファイルに保存
                    uwr.downloadHandler = new DownloadHandlerBuffer();
                    yield return uwr.SendWebRequest();

#if UNITY_2020_1_OR_NEWER
                    if (uwr.result != UnityWebRequest.Result.Success)
#else
                    if (uwr.isNetworkError || uwr.isHttpError)
#endif
                    {
                        Debug.LogError($"StreamingVideoPlayer: failed to load video from StreamingAssets: {uwr.error}");
                        yield break;
                    }

                    try
                    {
                        File.WriteAllBytes(persistentPath, uwr.downloadHandler.data);
                    }
                    catch (System.Exception ex)
                    {
                        Debug.LogError($"StreamingVideoPlayer: failed to write video file to persistentPath: {ex}");
                        yield break;
                    }
                }
            }

            // ローカルファイルパスを VideoPlayer に渡す(Android では file:// が必要)
            url = "file://" + persistentPath;
        }
        else
        {
            // 他プラットフォームでは streamingAssetPath を使う。
            // ネイティブファイルの場合、file:// を付けたほうが確実
            if (!streamingAssetPath.StartsWith("http://") && !streamingAssetPath.StartsWith("https://"))
            {
                url = "file://" + streamingAssetPath;
            }
        }

        // VideoPlayer 設定
        videoPlayer.source = VideoSource.Url;
        videoPlayer.url = videoPath;
        videoPlayer.url = url;
        // start with APIOnly so Prepare will create the internal texture
        videoPlayer.renderMode = VideoRenderMode.APIOnly;
        videoPlayer.waitForFirstFrame = true;
        videoPlayer.skipOnDrop = false;
        videoPlayer.playOnAwake = false;
        videoPlayer.audioOutputMode = VideoAudioOutputMode.None;
        videoPlayer.audioOutputMode = VideoAudioOutputMode.Direct;

        // Prepare 開始
        videoPlayer.Prepare();

        // 完全に準備が終わるまで待つ
        while (!videoPlayer.isPrepared)
        {
            yield return null;
        }

        // テクスチャを設定
        // --- Create a RenderTexture and switch to RenderTexture mode ---
        // Some platforms (notably Android) render better when using a RenderTexture target.
        int texW = (int)videoPlayer.width;
        int texH = (int)videoPlayer.height;
        if (texW <= 0 || texH <= 0)
        {
            // fallback to texture size if width/height not set yet
            if (videoPlayer.texture != null)
            {
                texW = videoPlayer.texture.width;
                texH = videoPlayer.texture.height;
            }
        }

        if (texW > 0 && texH > 0)
        {
            // release previous if any
            if (runtimeRenderTexture != null)
            {
                if (videoPlayer.targetTexture == runtimeRenderTexture)
                {
                    videoPlayer.targetTexture = null;
                }
                runtimeRenderTexture.Release();
                Destroy(runtimeRenderTexture);
            }

            runtimeRenderTexture = new RenderTexture(texW, texH, 0, RenderTextureFormat.Default);
            runtimeRenderTexture.Create();

            videoPlayer.renderMode = VideoRenderMode.RenderTexture;
            videoPlayer.targetTexture = runtimeRenderTexture;

            // assign render texture to RawImage
            if (rawImage != null)
            {
                rawImage.texture = runtimeRenderTexture;
                // Ensure RawImage is visible and using default material
                rawImage.enabled = true;
                rawImage.color = Color.white;
                rawImage.material = null;
                // Resize to match video aspect if possible
                try { rawImage.SetNativeSize(); } catch { }
            }
        }
        else
        {
            // fallback: use API texture directly
            if (rawImage != null)
            {
                rawImage.texture = videoPlayer.texture;
                rawImage.enabled = true;
                rawImage.color = Color.white;
                rawImage.material = null;
                try { rawImage.SetNativeSize(); } catch { }
            }
        }

        // 1フレーム目を待ってから再生(確実に準備された状態)
        videoPlayer.Play();
        while (videoPlayer.frame <= 0)
        hasStarted = true;
        // wait until first frame is rendered
        int waitFrames = 0;
        while (videoPlayer.frame <= 0 && waitFrames < 300)
        {
            waitFrames++;
            yield return null;
        }

        Debug.Log($"再生開始 frame={videoPlayer.frame} tex={videoPlayer.texture} targetTex={(videoPlayer.targetTexture!=null?videoPlayer.targetTexture.width+"x"+videoPlayer.targetTexture.height:"null")} rawTex={(rawImage!=null && rawImage.texture!=null?rawImage.texture.width+"x"+rawImage.texture.height:"null")} waitFrames={waitFrames}");

        if (rawImage != null && rawImage.texture == null)
        {
            Debug.LogWarning("StreamingVideoPlayer: RawImage.texture is null after play. Check Canvas/RawImage settings.");
        }
    }

    void Update()
    {
        if (!hasStarted || !allowSkipByClick) return;

        // desktop click or touch begin
        if (Input.GetMouseButtonDown(0) || (Input.touchCount > 0 && Input.touches[0].phase == TouchPhase.Began))
        {
            StopAndCleanup();
            if (openTitleOnSkip) OpenTitle();
        }
    }

    void OnVideoFinished(VideoPlayer vp)
    {
        // video finished normally
        StopAndCleanup();
        if (openTitleOnSkip) OpenTitle();
    }

    void StopAndCleanup()
    {
        try
        {
            if (videoPlayer != null)
            {
                if (videoPlayer.isPlaying) videoPlayer.Stop();
                videoPlayer.targetTexture = null;
            }

            if (rawImage != null)
            {
                rawImage.texture = null;
                rawImage.enabled = false;
            }

            if (runtimeRenderTexture != null)
            {
                runtimeRenderTexture.Release();
                Destroy(runtimeRenderTexture);
                runtimeRenderTexture = null;
            }
        }
        finally
        {
            hasStarted = false;
        }
    }

    void OpenTitle()
    {
        // Clear Utage UI input state before opening title to avoid leftover input triggering immediate interactions
        try
        {
            var engine = FindObjectOfType<AdvEngine>();
            if (engine != null && engine.UiManager != null)
            {
                engine.UiManager.ClearPointerDown();
                engine.UiManager.IsInputTrig = false;
            }
        }
        catch { }

        // Find title view even if inactive
        var titles = Resources.FindObjectsOfTypeAll<UtageUguiTitle>();
        if (titles != null && titles.Length > 0)
        {
            // Open the first title found
            titles[0].Open();
        }
        else
        {
            Debug.LogWarning("StreamingVideoPlayer: UtageUguiTitle not found to open on skip.");
        }

        // Additional safety: clear EventSystem selection and briefly disable InputUtil to avoid immediate click propagation
        if (EventSystem.current != null)
        {
            EventSystem.current.SetSelectedGameObject(null);
        }
        StartCoroutine(ReenableInputAfterDelay());
    }

    IEnumerator ReenableInputAfterDelay()
    {
        // disable global input for a short time to avoid propagation
        InputUtil.EnableInput = false;
        // wait two frames
        yield return null;
        yield return null;
        InputUtil.EnableInput = true;

        // ensure UiManager cleared again
        var engine = FindObjectOfType<AdvEngine>();
        if (engine != null && engine.UiManager != null)
        {
            engine.UiManager.ClearPointerDown();
            engine.UiManager.IsInputTrig = false;
        }
    }

    void OnDestroy()
    {
        if (videoPlayer != null)
        {
            videoPlayer.loopPointReached -= OnVideoFinished;
        }
         if (runtimeRenderTexture != null)
         {
             if (videoPlayer != null && videoPlayer.targetTexture == runtimeRenderTexture)
             {
                 videoPlayer.targetTexture = null;
             }
             runtimeRenderTexture.Release();
             Destroy(runtimeRenderTexture);
             runtimeRenderTexture = null;
         }
     }

        Debug.Log("再生開始");
    // StreamingAssets のプラットフォーム依存パスを組み立てて返す
    string PathCombineStreamingAssets(string filename)
    {
        // Application.streamingAssetsPath はプラットフォームによって形式が異なる
        // そのまま UnityWebRequest に渡せる形式にする
        string basePath = Application.streamingAssetsPath;

        // Android の場合は jar:file://...!/assets/... のようなパスになる。UnityWebRequest は扱えるのでそのまま使う。
        // Windows/Mac の場合はファイルパスを返す
        if (basePath.EndsWith("/"))
        {
            return basePath + filename;
        }
        return basePath + "/" + filename;
    }
}



クリスマスケーキは面倒だけど買いに行ったら、ケーキのお釣りで1,111円でした。
ジャニーズのクソスノーマンは大嫌いだが(暴行事件の犯罪者で在日韓国人だから凶暴)、ケーキ自体は雪だるまをモチーフにした求肥(ぎゅうひ)で包まれたものです。

会計見てさすがに「あっ」と思いましたね。
再会に向けて戻ってくると、違う器で戻ってくると思えたので期待しておきましょう。カルマとか課題は残ってないのが救いか。

大事な人となら食べたいと思ってるようなもので、それで面倒だけで買いに出かけたら、会計で知らずとそうなった。

しかも、伝えたい言葉が出来た、それに対して曲の歌詞で返信してきてる辺りが、確定で戻ってくると。
今は存在領域が異なっているだけ。
来年以降に何かあれば嬉しい。是が非でも生き残る。死んでるわけじゃないから星の海は泳いでくるかは不明だけど。

じゃないと、またクソみたいな世界で再度生き直しになるしで、たまったもんじゃない…。
お互いにそれは望んでないはずだし、今世で終わりにするはず。多分ですが、こういう状態になった人ほど人間として再転生はかなり低い気がします。自分は確定でもう降りてこないと分かってるけど。輪廻の外側に行きます。

クソみたいな使命感で、てめーの成長すら放棄してる人も中にはいる上に、さらには一部を見下してるのもいるから。
多分、そういう感情自体がエゴになるんでしょうけどね。使命感はクソです。ハイヤーセルフだとか言っても、止まってる。

死別してたらもっと違ったのかもしれないが、生きてても自我が強烈な抵抗で完全無視してる以上、そりゃ魂も見捨てるでしょうね。多分、魂側は怒ってる。最悪は、自ら死に追いやるかと思ったよ…。文字通り引導。

なんとなく死んだと思ったことは、完全に終演という意味だったのかも。影も消えたが、魂も完全に離れたと。
思い返すと、相手は散々試されてたな。結果で魂だけが浮上して離れていった。加護を失った人間は悲惨でしょう。
謝罪しても歩み寄れない人間がいるとは、良い学びになりました…。本当、「え…?」という感じです。

心に致命傷は負ったが、引きずるような想いは微塵もないのが救いか。
普通の人間は引きずって失恋だなんだと騒ぎ散らす。執着しすぎ。所詮は自我レベルでしかない。

自分にとっては、良い意味でクリスマスプレゼントになりました。
期待していよう、そして「おかえり」と「ありがとう」を伝えたい。

向こうも曲の歌詞で返信してきて「ただいま」と言いたいのと「おかえり」を聞きたいと返してきてるので。
どういう形でも戻ってきたら第2部のスタートです。

なお、家族は週末にでもケーキ食べるかと思ってたようですが、あえて言おう。

元旦に誕生日なのに、ほぼ間隔なしでケーキになるじゃねーか。
まあいいや。自分にとって、本当にメッセージ性があるクリスマスプレゼントだったし、いずれ今の環境自体終わるし。

コメント