FiveHeartsの技術記事

実用的なムービーの実装方法

ゲーム開発において、動画を再生したい場面があると思います。
しかし動画のデータはゲームとっては非常に重く
必要な時にロードして不要になったら破棄するのが原則となる
『Addressable Asset System』を用いるのが良いです。
本記事では『Addressable Asset System』は
扱える前提で話を進めますので、
『Addressable Asset System』については別記事を
ご覧頂ければと思います。

まずはコード全文を掲載しますので、
あとで要所に解説を加える形で進めていきたいと思います。

				  
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
using System;
using UnityEngine.SceneManagement;
using Cysharp.Threading.Tasks;
using System.Threading;
using UnityEngine.Video;
using UnityEngine.AddressableAssets;
using UnityEngine.ResourceManagement.AsyncOperations;

public class MoviePlayer : MonoBehaviour
{
    private CancellationTokenSource cts;
    [SerializeField]
    private GameObject screen;
    [SerializeField]
    private AssetReference subject;
    [SerializeField]
    private GameObject loadtext;
    [SerializeField]
    private RenderTexture texture;
    [SerializeField]
    private float movietime;

    private AsyncOperationHandle<VideoClip> subjectHandle;

    private VideoClip loadsubject;
    // インスタンス化した対象オブジェクトのリスト
    [SerializeField]
    private List<VideoClip> instancedsubjectList = new List<VideoClip>();

    // Start is called before the first frame update
    private void Start()
    {
        SoundManager.instance.StopBGMtoMovie();

        VideoPlayer videoPlayer = screen.GetComponent<VideoPlayer>();

        Addressables.LoadAssetAsync<VideoClip>(subject).Completed += (obj) => {
            subjectHandle = obj;
            loadsubject = obj.Result;
            videoPlayer.source = VideoSource.VideoClip;	// 動画ソースの設定
            videoPlayer.clip = loadsubject;
            videoPlayer.Play();
            Destroy(loadtext);
            WaitStart();
        };
    }

    async void WaitStart()
    {
        //ユニタスクのキャンセルトークン生成
        cts = new CancellationTokenSource();
        CancellationToken token = cts.Token;

        await waitsecond(movietime, token);
        if (token.IsCancellationRequested)
        {
            Debug.Log("Canceled(movie auto end)");
            return;
        }

        //メモリ解放
        instancedsubjectList.Clear();
        Addressables.ReleaseInstance(subjectHandle);
        texture.Release();
        SceneManager.LoadScene("Title");
    }

    void Update()
    {
        //****************
        //ボタン入力の処理
        //****************
        //プレイヤーの処理
        if (Input.GetMouseButtonUp(0))
        {
            //メインカメラ上のマウスカーソルのある位置からRayを飛ばす
            Ray ray = Camera.main.ScreenPointToRay(Input.mousePosition);

            RaycastHit2D hit = Physics2D.Raycast((Vector2)ray.origin, (Vector2)ray.direction);
            if (hit.collider)
            {
                if (hit.transform.gameObject.name == "UIFilter")
                {
                    cts.Cancel();//待機中のUniTaskを止められるようにする

                    //メモリ解放
                    instancedsubjectList.Clear();
                    Addressables.ReleaseInstance(subjectHandle);
                    texture.Release();
                    SceneManager.LoadScene("Title");
                }
                
            }
        }
    }

    //引数秒待つ関数
    async UniTask waitsecond(float sec, CancellationToken token)
    {
        await UniTask.Delay(TimeSpan.FromSeconds(sec)); //sec秒待機
    }
}
				  
				

今回の実装は、主にゲームのOPムービーやEDムービーを想定しています。
そのため機能としては、
1,画面タッチで別画面に遷移する。
2、最後まで視聴したら別画面に遷移する。
の2つが必要になります。
まずUsingの説明はいいとして、メンバにCancellationTokenSourceが
定義されています。これはユーザーが画面をタップして動画を閉じた場合と
動画を最後まで見て遷移する場合が存在するので今回は必要不可欠です。
screenにはCanvas配下にあるRawImageをセットします。
subjectはAddressableにした動画をセットします。
loadtextは『ロード中』とか『TIPS』とかのオブジェクトをセットします。
これを配列にして、ランダムにすると市販のゲームでよくある奴になります。
textureには動画を投影しているRenderTextureを指定します。
movietimeには動画の全長+3秒くらいを設定します。
instancedsubjectListについては今回は使っていません。
(じゃあ書くなよというツッコミはさておき……)
Addressableアセットが複数あり、まとめてDestroyしたいときに使います
この時点で何言ってるかわからない人は別記事の
『・動画を再生してみよう』を参照してください。

				
// Start is called before the first frame update
private void Start()
{
	SoundManager.instance.StopBGMtoMovie();

	VideoPlayer videoPlayer = screen.GetComponent<VideoPlayer>();

	Addressables.LoadAssetAsync<VideoClip>(subject).Completed += (obj) => {
		subjectHandle = obj;
		loadsubject = obj.Result;
		videoPlayer.source = VideoSource.VideoClip;	// 動画ソースの設定
		videoPlayer.clip = loadsubject;
		videoPlayer.Play();
		Destroy(loadtext);
		WaitStart();
	};
}
				
				

まずはスタート関数です。
SoundManager.instance.StopBGMtoMovie();は
前画面で鳴っているBGMを止めている処理です。
シングルトンなので普通はinstanceですね。ココが分からない人は
『・音をまとめて管理しよう』を参照してください。
次はscreenに付いているVideoPlayerコンポーネントにアクセスします。
そしてAddressableで動画をロードします。
ラムダ式にすることでロード完了後に実行される処理を記述します。
ロードしてきた動画をセットして再生開始、ロード中の表示を破壊して
終了時に自動遷移させるためのタイマーを作動させます。

				
async void WaitStart()
{
	//ユニタスクのキャンセルトークン生成
	cts = new CancellationTokenSource();
	CancellationToken token = cts.Token;

	await waitsecond(movietime, token);
	if (token.IsCancellationRequested)
	{
		Debug.Log("Canceled(movie auto end)");
		return;
	}

	//メモリ解放
	instancedsubjectList.Clear();
	Addressables.ReleaseInstance(subjectHandle);
	texture.Release();
	SceneManager.LoadScene("Title");
}
				
				

次はウェイト関数です。
await で動画の長さだけ待ちます。
もしこのawaitがキャンセルされているなら
何も処理せずに関数を終了します。
動画を最後まで見た場合は各種メモリを開放し
タイトル画面に遷移しています。

				
void Update()
{
	//****************
	//ボタン入力の処理
	//****************
	//プレイヤーの処理
	if (Input.GetMouseButtonUp(0))
	{
		//メインカメラ上のマウスカーソルのある位置からRayを飛ばす
		Ray ray = Camera.main.ScreenPointToRay(Input.mousePosition);

		RaycastHit2D hit = Physics2D.Raycast((Vector2)ray.origin, (Vector2)ray.direction);
		if (hit.collider)
		{
			if (hit.transform.gameObject.name == "UIFilter")
			{
				cts.Cancel();//待機中のUniTaskを止められるようにする

				//メモリ解放
				instancedsubjectList.Clear();
				Addressables.ReleaseInstance(subjectHandle);
				texture.Release();
				SceneManager.LoadScene("Title");
			}

		}
	}
}

//引数秒待つ関数
async UniTask waitsecond(float sec, CancellationToken token)
{
	await UniTask.Delay(TimeSpan.FromSeconds(sec)); //sec秒待機
}
				
				

次はアップデート関数です。
まず前提としてUIFilterという名前の画面と同じサイズの
無色透明なオブジェクトを動画の手前に配置しておきます。
これにはBoxCollider2Dのコンポーネントをアタッチしておきます。
画面の任意部分のタッチ検出によく使う手法ですね
あとは画面がタッチされたらUniTaskをキャンセルして
その時点でメモリを解放しタイトル画面に遷移させます。
UniTaskは本来シーン遷移などでデストラクタが自動で呼ばれたとしても
止まるものではありません。
普通は関数の確実な処理待ちに使うことが多いのであまり実感しませんが
(実際にそのような使い方の場合ctsを必ずしも定義する必要性はない)
基本的にはctsを定義するのが正しい使い方になります。
今回の場合、もしctsを定義せずにユーザーが画面タッチで
動画を抜けてタイトル画面にもどり
すぐにゲーム本編を開始したとすると、
カウンターが生きているので時間満了後にタイトル画面に戻されます。
ユーザーにはバグに感じられることでしょう。

今回の記事はここまでです。
それではまた別の記事でお会いしましょう。