7080 + 1

ゲームプログラミングの記事を書いてます。

【Unity】イントロ+ループ再生を実装する

またUnityの記事です。今回は難易度低め。

BGMを流すとき、イントロ+ループというのを前提にしたBGMって結構あると思います。
Unityではそれを再生する機能が標準でない(Why?)ので、簡単に出来るのを実装してみました。

必要なものは
・イントロとループで分けたAudioClip

だけです。わざわざ分けるのめんどいですが実装が簡単な分こっちに手間がかかるということで...

ソースは以下です。改良なり再配布なりご自由にどうぞ。
(再生、停止の最低限の機能しかないです)

public class BGMPlayer : MonoBehaviour {

    [SerializeField] AudioClip bgmIntroAudioClip;
    [SerializeField] AudioClip bgmLoopAudioClip;

    AudioSource introAudioSource;
    AudioSource loopAudioSource;

    void Start()
    {
        introAudioSource = gameObject.AddComponent <AudioSource>();
        loopAudioSource = gameObject.AddComponent <AudioSource>();

        introAudioSource.clip = bgmIntroAudioClip;
        introAudioSource.loop = false;
        introAudioSource.playOnAwake = false;

        loopAudioSource.clip = bgmLoopAudioClip;
        loopAudioSource.loop = true;
        loopAudioSource.playOnAwake = false;
    }

    public void PlayBGM()
    {
        if (introAudioSource == null || loopAudioSource == null) {
            return;
        }

        introAudioSource.Play ();
        loopAudioSource.PlayScheduled (AudioSettings.dspTime + bgmIntroAudioClip.length);
    }

    public void StopBGM()
    {
        if (introAudioSource == null || loopAudioSource == null) {
            return;
        }

        if (introAudioSource.isPlaying) {
            introAudioSource.Stop ();
        } else if (loopAudioSource.isPlaying) {
            loopAudioSource.Stop ();
        }
    }
}
解説

インスペクタにイントロとループのAudioClipをそれぞれ設定すれば再生出来ます。
今回のやりかたは、AudioSourceをイントロとループでそれぞれ用意して、イントロの再生が終わった瞬間からループを再生する。
という仕組みになっています。

PlayScheduled()を使うことで、再生を遅延させることができます。AudioSetttings.dspTimeはTime.timeとは別に、オーディオを再生するための正しい時間みたいなものです。
時間指定で音を再生する場合はこれ使ったほうがいいと思います。

別の方法

他の方法として、AudioSourceは1つで、イントロの再生が終わると同時にClipを入れ替えるという方法もありますが、切り替えの瞬間にどうしてもノイズが走ってしまいます。
フレーム単位でしか切り替えられないので当然っちゃ当然なんですが、解決方法イマイチわからず...

一番はAudioSourceもAudioClipも1つだけで、ループを開始してほしい時間を指定すれば動いてくれるスクリプトなんですが、色々試した結果上記の方法が一番実装は楽そうでした。
多分サンプルレートとかも考慮して組まないと駄目な気がします。

蛇足

今回の調べて思ったんですが、Unityのマニュアルとリファレンスの音周りがほとんど翻訳されてないのが軽く衝撃でした。

【Unity】スプレッドシートのデータをスクリプトから取得する【OAuth】

久しぶりの更新です。
仕事柄、最近はUnityばかり触ってます。のでUnityの記事です。

ゲームで使う値をエクセルじゃなくてスプレッドシートで管理したかったので、色々調べました。
全体の流れをメモ代わりに書きますが、詳しく触れていないところはわからないところです。

スプレッドシートにアクセスにするには、2つの方法があります。
Google API Keyを使ってアクセスする
OAuth認証をしてアクセストークンをもらい、それでアクセスする

1つ目はすぐに出来ます。APIKeyさえあればアクセス出来ますが、シートの共有をオンにしておく必要があります。
共有するのが嫌な人はOAuth認証を使いましょう。APIKeyのほうは割愛します。

OAuth認証に必要なもの

OAuth認証によるアクセスでは、以下のものが必要になります。
・OAuthクライアントID
・OAuthクライアントシークレット
OAuth認証コード
・アクセストーク
・リフレッシュトーク
スプレッドシートのID

実際にアクセスするのに必要なのはアクセストークンとスプレッドシートのIDのみです。
アクセストークンを取得するのにOAuth認証コードとクライアントシークレットもしくはリフレッシュトークンが必要になり、
OAuth認証コードやリフレッシュトークンを取得するにはOAuthクライアントIDが必要になります。
結構ややこしい。

注意なのは、アクセストークンには有効期限があり、切れるとそのトークンではアクセスできなくなることです。
その時はリフレッシュトークンを使って、アクセストークンを再発行することになります。
リフレッシュトークンに有効期限はない(多分)ので、どこかに保存しておきましょう。

スプレッドシートのIDは、スプレッドシートのURLの一部になります。
https://docs.google.com/spreadsheets/d/[この部分]/edit#gid=0

手順

OAuthのクライアントIDとクライアントシークレットをAPIConsoleから作る

当たり前ですが、Googleアカウントが必要になります。
Google API Consoleとググるとトップにサイトが出てきます。
そこで認証情報→認証情報を作成→OAuthクライアントの作成を選んでください。

f:id:atori708:20170215001707p:plain

アプリケーションの種類を聞かれるので、その他を選んで、名前は適当につけてください。
これでOAuthのクライアントIDとクライアントシークレットを取得できました。

Unityが登場するのはまだ先です。

OAuthのクライアントIDとScopeから、認証コードを取得する

認証コードを取得するには、クライアントIDとScopeが必要になります。
Scopeは、特定のサービスに、どの権限でアクセスするかを教えるものです。
今回はスプレッドシートの読み書きをしたいので、
https://www.googleapis.com/auth/spreadsheets
がScopeになります。

Scopeの種類は以下に書いてあるので、スプレッドシート以外のサービスにもアクセスできます。
OAuth 2.0 Scopes for Google APIs  |  Google Identity Platform  |  Google Developers

コードを取得するには、
https://accounts.google.com/o/oauth2/v2/auth にクエリをたくさんつけてリクエストを投げます。

クエリは、

?scope=https://www.googleapis.com/auth/spreadsheets
&redirect_uri=http://localhost
&response_type=code
&client_id=OAuthクライアントID

になります。
全部ひとつなぎにしてURL欄に入力してください。

URLが正しく入力できていると以下の画面に飛ぶので、許可を選んでください。
もしGoogleのアカウントにログインする画面に飛んだら、OAuthクライアントを作ったアカウントでログインしてください。

f:id:atori708:20170215001833p:plain

許可を押すとredirect_uriで設定した先へ飛んでいきます。
localhostにしたので何もないですが、URLのcode=~以下がOAuthの認証コードになるので保存しておきましょう。

認証コードからアクセストークンとリフレッシュトークンを取得する

ここからUnityでの作業になります。
UnityのWWWを使って、OAuthからスプレッドシートのアクセストークンをもらいましょう。
https://www.googleapis.com/oauth2/v4/token にフォームデータ付きでリクエストを送信します。

WWWForm form = new WWWForm ();
form.AddField ("code", OAuthの認証コード);
form.AddField ("client_id", OAuthクライアントID);
form.AddField ("client_secret", クライアントシークレット);
form.AddField ("redirect_url", "http://localhost");
form.AddField ("grant_type", "authorization_code");
form.AddField ("access_type", "offline");
form.headers.Add ("Content-Type", "application/x-www-form-urlencoded");
Dictionary<string, string> headers = form.headers;
byte[] rawData = form.data;
WWW www = new WWW ("https://www.googleapis.com/oauth2/v4/token", rawData, headers);

レスポンスはwww.textにJSON形式で入って返ってきます。

こんなJSONが返ってきたら成功です。

{
 "access_token": "アクセストークン",
 "token_type": "Bearer",
 "expires_in": 3600,
 "refresh_token": "リフレッシュトークン"
}

expires_inがアクセストークンの有効期限で、スプレッドシートは1時間のようです。
リフレッシュトークンは数に上限があるそうなので、なるべく同じのを使い続けましょう。

Unityからでなくても、curlコマンドを使うと同じことが出来ます。
その方法は他の人が書いてるので割愛。

アクセストークンからスプレッドシートにアクセスする

このシートのデータを取ってきてみましょう。
テイルズオブベルセリアのキャラです。面白かったです。

f:id:atori708:20170215001651p:plain

ここまで来ると簡単で、
https://sheets.googleapis.com/v4/spreadsheets/スプレッドシートのID/values/取得するセルの範囲?AccessToken=アクセストークン
にPOSTすると、セルの中身がJSON形式で返って来ます。

WWW www = new WWW ("https://sheets.googleapis.com/v4/spreadsheets/スプレッドシートのID/values/取得するセルの範囲?AccessToken=アクセストークン");	

今回はセルの範囲をA1:B4にしました。

{
  "range": "'シート1'!A1:B4",
  "majorDimension": "ROWS",
  "values": [
    [
      "名前",
      "性別"
    ],
    [
      "ベルベット",
      "女"
    ],
    [
      "ライフィセット",
      "男"
    ],
    [
      "ロクロウ",
      "男"
    ]
  ]
}

無事取ってこれたでしょうか。
後はJSONをMiniJSONとか使って保存すればOKですね!

リフレッシュトークンからアクセストークンを取得する

先程も書きましたが、アクセストークンには有効期限があるので、もし切れていたら、リフレッシュトークンを使って再発行する必要があります。
https://www.googleapis.com/oauth2/v4/tokenにリクエストを飛ばすと取得できます。

WWWForm form = new WWWForm ();
form.AddField ("refresh_token", リフレッシュトークン);
form.AddField ("client_id", OAuthクライアントID);
form.AddField ("client_secret", OAuthクライアントシークレット);
form.AddField ("grant_type, "refresh_token");
Dictionary<string, string> headers = form.headers;
byte[] rawData = form.data;
WWW www = new WWW ("https://www.googleapis.com/oauth2/v4/token", rawData);

これで成功していたら、アクセストークンがJSON形式で返ってきます。
アクセストークン期限が切れていなくても再発行可能のようです。
10個くらい連続で取得してみたのですが、全部使えました。

アクセストークンが有効かどうか調べる

有効かどうか、また有効期限も調べられます。

WWW www = new WWW("https://www.googleapis.com/oauth2/v3/tokeninfo?access_token=アクセストークン");

無効だった場合

{
 "error_description": "Invalid Value"
}

※もともと存在しない適当な文字列入れても同じエラーが返ってきます。

有効だった場合(例)

{
 "azp": "247046462806-7efg42ut1tau4n2ku081gn81nebma2ab.apps.googleusercontent.com",
 "aud": "247046462806-7efg42ut1tau4n2ku081gn81nebma2ab.apps.googleusercontent.com",
 "scope": "[]https://www.googleapis.com/auth/spreadsheets[]",
 "exp": "1485228014",
 "expires_in": "3584",
 "access_type": "offline"
}

Unityの記事と言いましたが、認証の仕方は他でも使えると思います。
間違っていたり、質問や不明点がありましたらコメントでお願いします。

WindowsなんだからWindowsAPIも知る必要がある

学生時代の自分のDirectXのプログラムを見ていると、先生からもらったサンプルをそのまま意味もわからず使っているケースがあります。 特にWindowsAPI周りが意味不明で、この関数呼んでる意味あんのか?と思ったりすることもあります。

しかし調べてみると、意味のある処理が非常に多かったので、忘れないようにメモしておこうと思います。 ちなみに、独自解釈が多々入っているので、間違っている可能性があります。その時はご指摘いただければと思います。

ValidateRect(HWND, CONST RECT*)

過去の自分のプログラムのコメントを見ると、「WM_PAINTを呼ばないようにする」と書いてありました。 WM_PAINTというのは、WindowsAPIで毎フレーム呼ばれる、描画メッセージです。つまり、DirectX使わなくても、WindowsAPIだけで画面描画することはできます。 ですがそれでテクスチャつけて3Dモデル動かして・・・なんてやってられません。描画をDirectXだけでやりたいなら、このメッセージが飛ばないようにして、処理を少しでも軽くしてやりましょう。 第一引数にウィンドウハンドル、第二引数にNULLを渡してやれば、そのウィンドウではWM_PAINTは呼ばれなくなるみたいです。(確かめてない)

GetClientRect(HWND, RECT*)

これ意外と大事です。Windowsのウィンドウには、外枠がありますよね。ウィンドウのタイトル表示してたり、閉じるボタン、最小化ボタンがあるところです。 左右と下にもわずかですが枠があります。CreateWindow()呼ぶときに幅と高さを指定しますが、これはその枠を含めたサイズでウィンドウを作ります。

ですが実際にゲームの描画が行われるのはその枠を覗いた領域のみです。これを「クライアント領域」と言います。この関数はそれを取得できます。(枠がないクライアント領域のみのウィンドウを作ることも可能です) ウィンドウのサイズそのままでDirectXの描画を行うと、きっと小さなズレが生まれるでしょう。ゲームを作る上で、クライアント領域を覚えておくのは必須といえます。

今回は2つ紹介しました。全部WindowsAPIです。使い方は全て調べていただければ普通に出てきます。 DirectXの学習も大切ですが、こういった部分は一度設定するとそのまま変えないので、忘れがちになると思います。 また何か紹介できるものがあれば書いていきたいと思います。

OSのバージョンを調べる(VerifyVersionInfo())

OSのバージョン取得方法には、GetVersionEx()と、VerifyVersionInfo()の2つがあります。 しかし両者には違いがあります。

前者は現在自分の使っているOSのバージョンを知ることが出来ます。
後者は現在自分の使っているOSと、引数で指定したバージョンを比較します。

言葉だけではわかりにくいですね。でも安心してください。前者はWindows8.1では使えません。 古い形式だとコンパイラに怒られてしまいます。
私はなんとか前者が使えないかと四苦八苦してました。

なので、Windows8.1の人は何も考えずにVarifyVersionInfo()を使いましょう。 GetVersionEx()の使い方はご自分でお調べください。

それでは、まずはVerifyVersionInfo()を使うための準備です。

    OSVERSIONINFOEX OSver;
    ULONGLONG condition = 0;
    OSver.dwOSVersionInfoSize = sizeof(OSVERSIONINFOEX);
    OSver.dwMajorVersion = 6;
    OSver.dwMinorVersion = 2;
    VER_SET_CONDITION( condition, VER_MAJORVERSION, VER_GREATER_EQUAL);
    VER_SET_CONDITION( condition, VER_MINORVERSION, VER_GREATER_EQUAL);

OSVERSIONINFOEXに設定しているのは、この構造体のサイズと、Windowsのバージョン情報です。 このバージョンと、自分の使っているOSのバージョンを比較するわけです。サイズは必ず必要です。 dwMajorVersionに6、dwMinorVersionに2を渡しているので、Windows8.0を表しています。 Majorは6のままで、Minorを1にするとWindows7、3にするとWindows8.1を表します。

VER_SET_CONDITION()というのは、何の値をどのように比較するのか、というのをconditionに設定しています。 OSVERSIONINFOEXにはたくさんのメンバがあります。OSのバージョン以外にも、ServicePackのバージョンとか、なんか色々です。で、現在はVER_MAJORVERSIONVER_MINORVERSIONを比較します。比較の仕方がVER_GREATER_EQUAL、要は比較演算子<=で比較するわけです。

つまり、メジャーバージョン、マイナーバージョンの比較は<=で比較するように設定します。これをマイナーバージョンだけは==で比較、とか自由に設定できるわけです。

さて、それではVerifyVersionInfo()を実際に使ってみるとこうなります。

if( VerifyVersionInfo( &OSver, VER_MAJORVERSION | VER_MINORVERSION, condition))
{
    // Windows8.0以上の時の処理
}
else
{
    // それ以外の処理
}

第二引数で、何と何を比較するのかをパイプ演算子で設定します。 これを正しく設定しないと、先程用意したconditionの設定が無駄になります。 先程作ったOSVerconditionの設定だと、自分の使っているOSがWindows8.0以上なら trueを、それ以外ならfalseを返します。

IDXGIDebug::ReportLiveObjects()を使うまでの道のり その2

前回の続きです。

結論から言います。出来ました。
奮闘記を書いて教えを請う予定でしたが、出来てしまったのでやり方だけ書きます。

前回も言いましたが、このメソッドはというか、このインターフェース自体が Windows8以降じゃないと使えないとMSDNに書いてありました。

なので、まずはOSのバージョンによって分岐しておく必要があります。 俺の作ったプログラムはWindows8上でしか動かねえぜ!!というのなら不要な処理です。

バージョンの調べ方は自分の書いた記事がありますので、そちらをどうぞ。

IDXGIDebug::ReportLiveObjects()を呼び出す

if( VerifyVersionInfo( &OSver, VER_MAJORVERSION | VER_MINORVERSION, condition))
{
// Widows8.0以上なら
    if( pDxgiDebug == nullptr )
    {
        // 作成
        typedef HRESULT(__stdcall *fPtr)(const IID&, void**); 
        HMODULE hDll = GetModuleHandleW(L"dxgidebug.dll");
        fPtr DXGIGetDebugInterface = (fPtr)GetProcAddress(hDll, "DXGIGetDebugInterface"); 
 
        DXGIGetDebugInterface(__uuidof(IDXGIDebug), (void**)&pDxgiDebug);

        // 出力
        pDxgiDebug->ReportLiveObjects( DXGI_DEBUG_D3D11, DXGI_DEBUG_RLO_DETAIL);
    }
    else
        pDxgiDebug->ReportLiveObjects( DXGI_DEBUG_D3D11, DXGI_DEBUG_RLO_DETAIL);
}
else
{
    return pD3dDebug->ReportLiveDeviceObjects(D3D11_RLDO_DETAIL);
}

VerifyVersionInfo()はバージョンを調べています。if文の中が重要です。 が、はっきり言って、GetModuleHandle()あたりのところでを何しているかわかりません。コピペしたので。 関数ポインタってこんな使い方出来るんですね。

と、とにかく、これで出来ます。これだけです。
もしかしたら、リンクエラーが出ると思います。そういう時は、InitGUID.hIDXGIDebug.hよりも前にインクルードしてください。それでも駄目なら、d3d11.hよりも前にインクルードしてみてください。

これが出力結果です。ID3D11Debug::ReportLiveDeviceObjects()同様、メソッドを呼んだタイミングでのオブジェクトの状況が出力されます。 f:id:atori708:20150221191010p:plain

これでIDXGIDebug::ReportLiveObjects()によるRelease()忘れの調査が出来るようになりました。 渡している引数がDXGI_DEBUG_D3D11だと、前回の記事の出力と変化はあまりありません。 が、DXGI_DEBUG_DXGIやDXGI_DEBUG_APPなど、出力するものを変えることが出来ます。

以外と知られていないと思いますが、デバッグには必要な物だと思います。 実装してみてはいかがでしょうか。

IDXGIDebug::ReportLiveObjects()を使うまでの道のり その1

記念すべき初投稿は、DirectX11のデバッグレイヤーの話です。

DirectXでは、COMオブジェクトを使った設計がされています。 COMオブジェクトについては詳しく知らないので、ここでは割愛します。

ようは、Release()忘れを絶対にするなよということです。 とはいっても結構やってしまったりします。

そこで、Release忘れが発生しているかを調べる方法があります。それがデバッグレイヤーです。 デバッグレイヤーというのはD3Dデバイスの話です。 CreateDeviceをする時に、D3D11_CREATE_DEVICE_DEBUGを第4引数に渡すことで作成できます。

デバッグレイヤーはRelease忘れのチェックなどの処理をするため、普通よりも重たいです。 デバッグビルド時のみ有効にするとかしておくと便利ですね。

さて、Release忘れはこんなふうに、VisualStudioの出力ウィンドウに表示されます。
出力されるのは、アプリケーションが終了した時です。

f:id:atori708:20150220013441p:plain

ズラリと並んでいます。文字に色が付いているのは、拡張機能を入れているからです。 便利です。
VSColorOutput extension (VS2013です)

これすべてがRelease忘れなんですが、各文の後ろにUNKNOWNと出ています。 これでは何がRelease忘れかわかりません。レンダーターゲットビューやらテクスチャやら色々とあるのに、 これでは困ります。

しかし画像の中の一番上で、

Process is terminating. Using simple reporting. Please call ReportLiveObjects() at runtime for standard reporting.

と書いてあります。ReportLiveObjects()を呼べば詳細を見れそうな気がします。

ここから私の長い旅が始まります。
ググってみると、IDXGIDebug::ReportLiveObjects()ID3D11Debug::ReportLiveDeviceObjects()が引っかかりました。

前者を解説する前に後者の説明をしたいと思います。 前者はWindows8以降でしか使えないです。
2015年2月現在、実装に至っていません。

ID3DDebug::ReportLiveDeviceObjects()は、D3Dデバイスがデバッグレイヤーであることが前提です。
以下ソースコードです。

// dxgidebug.hをインクルードしてください。

HRESULT hr;

ID3D11Device* pD3dDevice;
ID3D11Debug* pD3dDebug;

// デバイス作成は割愛

// 作成
hr = pD3dDevice->QueryInterface(__uuidof(ID3D11Debug), reinterpret_cast<void**>(&m_pD3dDebug));
if( FAILED(hr))
{
    return E_FAIL;
}

// 詳細表示
hr = m_pD3dDebug->ReportLiveDeviceObjects(D3D11_RLDO_DETAIL);

これだけです。超簡単です。注意すべきなのは、詳細が出力ウィンドウに表示されるのは、 メソッドが呼ばれた時です。そのときのCOMオブジェクトの状況を見ることが出来ます。

f:id:atori708:20150220021041p:plain

先ほどと違い、オブジェクトのインターフェースが見えるようになり、InitRefというものも追加されています。 RefCount、InitRefの両方が0になって初めてCOMオブジェクトは解放されます。
(2つの違いは残念ながら私にはわかりません...)

しかし先程の一文、

Process is terminating. Using simple reporting. Please call ReportLiveObjects() at runtime for standard reporting.

と書いてあります。あくまでReportLiveObjects()の方です。ReportLiveDeviceObjects()ではありません。

次回は、それを実装しようと悪戦苦闘し、あと一歩まで行った記録を記事にします。出来ました。
それではまた次回。