ビームやミサイルの追尾(ホーミング)計算

f:id:hirasho0:20190627121834p:plain

こんにちは。技術部平山です。

2019/06/25に4社合同のLT大会があったのですが、 お客さんがIT業界の方々ということで何を話していいかわからず、 たまたま趣味でやったことをそのまましゃべってしまいました。 今回の記事は、このデモの追尾計算に絞って説明するものです。

なお、このデモのパーティクル計算部分は参考にしないでください。標準のParticleSystemを使いましょう。完全に趣味です。

ちなみに、このデモに含まれる追尾以外の要素についても順次記事にしていきます。

  • 球面分布とベクトルの回転
  • カメラ制御

デモ概要

デモはWebGLで動くものを用意しました。 ソースコードはgitHubにあります

右上のゲージは処理負荷です。左の数字が最近60フレームの平均時間、右の数字が最近60フレームの最大時間です。 平均が16に収まっていれば、60フレーム毎秒で気持ち良く動いていることを示します。

Fireボタンを押すと、ビームが敵に向かって飛んでいきます。 Fire16ボタンを押すと、ビームを同時に16本発射します。

Threadボタンを押すと、スレッドによる並列化が有効になりますが、WebGLではスレッドが使えないので効きません。 また、パーティクル数はStandaloneでは10万、それ以外は2万に設定してあります(ビームをたくさん出すとパーティクルが消えて寂しくなります)。 MacBook Pro 2018では、PCビルドで10万パーティクル出しても、ほぼ60fpsを保てました。 が、標準のパーティクルを使えばもっと大量に出せますので、どうでも良いことです。

このデモを作った動機

ずいぶんと昔の話ですが、2003年にANUBIS というゲームが発売されまして、そのビームや雪、煙といった表現に私は衝撃を受けました(動画は検索するといっぱい出ます)。 パーティクル表現がかっこ良かったのです。

だいたい同じ頃に私もビームその他を出す仕事をしていて、 ビームが命中した際の火花や雪の粒などで パーティクル表現を試みたりもしていたのですが、 「パーティクル前提の表現を模索する」というところまでは踏み込めませんでした(新卒一年目でしたし)。

その後、GDCで「これからはシェーダで100万個パーティクル出せるぜ」的な講演 を聞いて、「ビームの本体含めて丸ごとパーティクルで行けるんじゃない?」と思うようになりましたが、 いろいろそれどころじゃなかったので、何もしないまま時が過ぎました。

そして今回、 「全くゲームを知らない人に5分で話してゲーム作りを感じてもらうネタ」 を考えていた時に、ふとビームを思い出したのです。

追尾の実装クラス

今回説明するビームの追尾を行うのは、 Beamクラス です。Beam自体は何も描画しないので、MonoBehaviourではありません。

今回はビームを線ではなく、内部的には動く点として実装しました。 移動しながら経路上にパーティクルを置いていった結果、線に見える、 というイメージです。当たり判定は先頭にしかありません。 ゲームによっては途中や末尾にも当たり判定が欲しいこともあるでしょうが、 曲がる場合は履歴が必要になって面倒なので、今回はやめました。

持っているデータのうち毎フレーム変化するのは、

  • 位置(position)
  • 速度(velocity)
  • 動作中か否か(time >= 0で判定)

です。Update()で積分して更新します。不変のパラメータとしては、

  • 方向転換用加速度の最大値(maxCentripetalAccel)
  • 推進加速度(propulsion)
  • 空気抵抗(damping)

があります。追尾を行い、速度を一定に保つためのパラメータです。

さまざまな追尾の手法

敵を追尾する方法はいくつかあります。主なものは以下の3つでしょうか。

  • 縦横の角度を独立に動かす
  • Quaternion(クォータニオン)の補間を使う
  • 加速度を加えて曲げる

縦横の角度を独立に動かす

一番素直なのは、 縦角度(仰角/俯角。オイラー角のx)と、横角度(方位角。オイラー角のy)を バラバラに操作する方法です。数学の知識が最も少なくて済みます。

今自分から見て標的が上20度、左40度に見えるとしましょう。 毎フレーム角度を変えられる量を決めておきます。 例えば「縦は2度、横は3度」としましょうか。 次のフレームでは縦角度を18度に、横角度を左37度に修正します。 また次のフレームでは角度を測り直し、また角度を修正します。 これを繰り返します。

この方式は「地面と空の方向がはっきりしているゲーム」であれば扱いやすく、 ゲームバランスの調整もしやすいという利点があります。 例えば「縦方向はあまり追尾しないが横はよく追う」という調整をすることで、 「ジャンプで避けやすく走っても避けられない」といった性質を出すことができます。

しかしながら、宇宙ゲームや戦闘機ゲームのように「上も下もない感じ」のゲームでは問題が出ます。 だいたい真横を向いていれば(縦角度が0付近なら)問題ないのですが、 真下や真上に近づくとおかしくなるのです。

今、縦角度が89度で、横角度が南(-90度)を向いているとしましょう。これを北(+90度)に向けるには、 縦に2度回すのが一番近道です。しかし、縦横独立で計算すると、 縦角度は同じ89度なので変えられず、横角度は180度回すことになります。毎フレーム3度なら60 フレームかかってしまうのです。

f:id:hirasho0:20190627120712p:plain

なので、ゲームデザイン的に縦角度と横角度を分けたい、という話でなければ、 この方法はおすすめできません。 角度を経由するとどうしても三角関数を経由して計算するので、負荷も大きくなります。

Quaternion(クォータニオン)を使う

「今向いている方向」は縦角度と横角度の2つの角度で表せますが、 別の表現としてQuaternion(クォータニオン) というものがあります(rは長音にするという規約に従えばクォターニオンの方がいいと思うなあ。余談)。

これを使えば先程の例で「真上を通って逆側へ最短で回す」ことができます。 この場合は、「毎フレーム2度回せる」のように縦と横を分けない角度で済むので、パラメータが減って楽です。

ただし、UnityのQuaternion の機能はかなり少ないですし、使い方を紹介する記事もあまりありません。 クォータニオンは大変便利ですので、いずれこのブログでも取り上げますが、 今回は別の方法を用います。

速度ベクトルに加速度を加える

これが今回採用した方法です。 詳細を説明します。

追尾計算の詳細

Beamクラスは元々速度ベクトルvelocityを持っているので、 これに加速度ベクトルを加算して方向を曲げます。

f:id:hirasho0:20190627120714p:plain

今Aにいて真北Bを向いているとして、敵が北東Cにいる場合、 移動方向(ベクトルAB)に東(ベクトルBC)を足せば、北東(AC)に向きますよね? そういうイメージです。

コードは以下です。

var toTarget = target - position;
var vn = velocity.normalized;
var dot = Vector3.Dot(toTarget, vn);
var centripetalAccel = toTarget - (vn * dot);
var centripetalAccelMagnitude = centripetalAccel.magnitude;
if (centripetalAccelMagnitude > 1f)
{
    centripetalAccel /= centripetalAccelMagnitude;
}
var force = centripetalAccel * maxCentripetalAccel;
force += vn * propulsion;
force -= velocity * damping;
velocity += force * deltaTime;

まず、敵へ向かうベクトルを得ます(toTarget)。 「AからBへ向かうベクトルはB-A」です。 ベクトル演算に不慣れな方は「B-AはAからB!AからBはB-A!」と声に出して 唱えると良いでしょう。クセになると間違えません。

次に、「速度を敵方向に曲げるベクトル」を計算します。 図で書けばこうです。

f:id:hirasho0:20190627120709p:plain

図中のPが今の位置、Tが敵の位置で、 vが速度ベクトル、という時に、図中のcが求めるベクトルです。 まず速度ベクトルを正規化したものを用意します(コード中のvn)。 そして、敵へ向かうベクトルをvnに射影します。

射影、というのは文字の通り「影を写す」ことで、 図のT'を求めることです。下の図をご覧ください。

f:id:hirasho0:20190627120706p:plain

図中のベクトルABを、ベクトルACに射影してできる 点Dの位置が欲しい、という場合、

cosθ = Dot(AB, AC) / |AB||AC|

で、

cosθ = |AD|/|AB|

なので、

|AD| = cosθ*|AB|
     = (Dot(AB, AC) / |AB||AC|) * |AB|
     = Dot(AB, AC) / |AC|

となります。先程の話で言うvにあたるACを前もって正規化しておけば、 |AC|での割り算はいらなくなります。コードで書けば、

AC.Normalize();
D = A - (AC * (Vector3.Dot(AB, AC));

となります。 Dは元のコードではcentripetalAccelに当たりますから、 これで欲しいものが得られたわけです。 あとは長さをテキトーに変えて速度に足せば、速度を曲げることができます。

この「長さをテキトーに変える部分」は以下になります。

var centripetalAccelMagnitude = centripetalAccel.magnitude;
if (centripetalAccelMagnitude > 1f)
{
    centripetalAccel /= centripetalAccelMagnitude;
}
var force = centripetalAccel * maxCentripetalAccel;

やけに長いcentripetalAccelという変数は、 曲げるために加える加速度を表しています。 centripetalというのは「中心を向いた」という意味です。 ひもがついた重りをグルグル回す時、重りが飛んでいかないのは ひもが重りを中心方向に引っぱっているからで、 これを向心力(centripetal force) といいます。ここでは直接加速度で扱っているので「力」でなく「加速度」、 ということでAccelをつけていますが、同じものです。

さて、centripetalAccelの長さ(magnitude)が1以上なら、長さを1に正規化します。 1未満ならそのままにします。 そしてmaxCentripetalAccelを掛けます。これで 「長さがmaxCentripetalAccel以下ならそのまま、以上なら長さをmaxCentripetalAccelにする」 という処理になります。 つまりmaxCentripetalAccelは「曲げる力の大きさの最大値」ということになります。

パラメータを扱いやすく

さて、前の二つの手法と違って、「毎フレームこれくらい曲げる」を 角度でなく加速度で定義していますので、このままでは直感的ではありません。 なので、外からは別のものを受け取っています。

public void Emit(
    Vector3 position,
    Vector3 velocity,
    float speed,
    float curvatureRadius,
    float damping)
{
    this.position = position;
    this.velocity = velocity;
    // 速さv、半径rで円を描く時、その向心力はv^2/r。これを計算しておく。
    this.maxCentripetalAccel = speed * speed / curvatureRadius;

maxCentripetalAccelは内部で計算されており、外からは見えません。 外からもらうのはcurvatureRadius、日本語で言えば「曲率半径」で、 要するに、「半径何メートルの円の感じに曲がれるか」を表します。 小さいほどキビキビ曲がれてよく追いかけられるわけです。 例えば、標的との距離が100mの時に、あさっての方向を向いていても当たって欲しい、 というのであれば、曲率半径は50m以下である必要があります。 そうでないと、地球の周りを回る月のように永遠に当たりません。 「1秒あたりの角度」よりも「追尾の鋭さ」がわかりやすいと思うのですが、いかがでしょうか。

そして、向心力は速度の二乗/曲率半径で求まります。

速さを一定に保つ

今回のデモでは、ビームの速さは一定です。これを実現しましょう。

ところで、今「速さ」と言いました。「速度」とは区別しています。 「速さ」は「速度ベクトルの長さ」のことです。 コード中ではspeedで表し、速度ベクトルはvelocityです。 この「速さ」と「速度」の使い分けは比較的メジャーなものかと思います。

さて、ビームが曲がっても速さは一定にしたい、というのが今回の要望です。 しかし、速度ベクトルに足し算をすれば、当然速さは変わってしまいます。 そこで、速さを一定にするために、dampingによって毎フレーム速度を落とし、 さらにパラメータとして与えたpropulsion(推進力)によって毎フレーム加速を加えています。

「速度ベクトルを正規化しちゃえばいい」 と思われるかもしれません。それでもいいと言えばいいのですが、 今回はせっかくなので「力を与えて制御し、位置と速度は直接いじらない」 という物理シミュレーションの基本に乗せてみました。

例えばdampingに0.1が入っていれば、速度は1秒のうちに90%に減速します。 これによって、曲げたことによる加速の影響はどんどん消えていきます。 そして、速度を一定に保つために、推進力propulsionによって、速度方向に加速するのです。 いずれ、抵抗と推進が釣り合って速度が一定になります。 ミサイルが燃料を燃やして進むのがpuropulsionで、空気抵抗がdampingと考えると、 現実の物理的な解釈がしやすいかと思います。

force += vn * propulsion; // 推進力
force -= velocity * damping; // 空気抵抗
velocity += force * deltaTime; // 速度積分

物理シミュレーションの枠組みから逸脱しないので、 何らかの物理シミュレーションのフレームワークに乗っかっていて(Rigidbodyとか) 積分を自分で書けない時でも使えます。

パラメータを使いやすく

先程の曲率半径と同じく、外から直接推進力を与えるのは不便なので、 違うものを与えています。

public void Emit(
    Vector3 position,
    Vector3 velocity,
    float speed,
    float curvatureRadius,
    float damping)
{
    //...省略...
    this.propulsion = speed * damping;

単純に「速さ(speed)」を与え、 これとdampingからpropulsionを計算しています。

dampingによる抵抗力は-velocity * dampingで、 velocityの大きさがspeedになった時にこれを打ち消せば良いので、 propulsionにはspeed * dampingを設定すれば良いわけです。

今回の場合dampingはいくらにしても大差ないので、外から与えず中でテキトーに 0.1などと入れておけば良いのですが、まあそのへんは細かい話ですね。

まとめ

今回紹介した追尾の計算の利点は、

  • 普通のベクトル演算だけで書ける
  • クォータニオンも行列もいらない
  • 三角関数が出てこない
  • 物理シミュレーションの枠組みから外れない

といったものです。とりわけ「普通のベクトル演算だけで書ける」 ことは、「2Dでも3Dでもコードがほとんど同じ」という利点にもなります。 実際、Vector3をVector2に置き換えるだけで、そのまま2Dゲームでの追尾処理になるのです。

欠点は、

  • 厳密には等速を保てない
  • 空気抵抗という本来いらないパラメータが介在している
  • ベクトル操作に慣れていないとわかりにくい
  • 曲率半径が小さすぎると曲げすぎて落ちつかず軌道が揺れる

といった感じになるでしょうか。

なお、「決まった時刻に必ず当たるように追尾したい」 という場合にはまた違った手法があると思います(24ページあたりから)。 今回は「当たるとは限らず、追尾性能に限りがある」という条件での計算です。

デシベルのすすめ

画面写真をクリックするとWebGLビルドに飛びます。

こんにちは。技術部平山です。

今日は、デシベル(db)のお話です。どうでしょう?使い慣れてますか?

使い慣れている方は読む必要がないお話です。

Unity屋にとっての音量

Unity上で音量を調整しようとすれば、 AudioSource.volumeに0から1の値を入れます。

0だと無音で、1だと元のデータそのまま、あとは小さいほど音が小さい、というだけの話です。 何もわかりにくさはありません。

また、もう少し詳しいプログラマであれば、 サウンドデータが波を表す値の配列であることを知っているかもしれません。 例えば16bit値で波を表していれば、 -32768から32767までの値がズラッと並んだものがサウンドデータです。 それにAudioSource.volumeの0から1の値が掛け算されて、 スピーカーから出てきます。0.5なら半分にされますし、0.1なら1/10です。

わかりやすいですね。何か問題なんですか?

サウンド屋さんにとっての音量

実は、サウンドのプロの方々は、音量を表すのに 「デシベル(db)」という単位を使っています。 10倍大きいと20増える、というもので、 例えばデシベルが-6だと、音量が半分になります。

元の値 デシベル
1 0
0.5 -6
0.1 -20
0.01 -40
0.001 -60

「何それわかりにくい」

と思いますよね?0.5で何が悪いの?

でも、音のプロにとっては、デシベルの方がずっと使いやすいのです。何故か?

理由は、人間の聴覚特性にあります。

人間の聴覚特性

人間の感覚は多くの場合対数的です。

1000円のランチが500円引きなのと、 100万円の車が500円引きなのと、 違いは同じ500円です。

でも、100万円が99万9500円になっても、 全然うれしくないですよね? 1000円なら半額で50%ですが、 100万円なら99.95%ですから、 比率で見れば全然ありがたみがありません。

このように、人は多くの場合差ではなく比率で見ます。 これは聴覚に関しても同じです。

音のデータの大きさが1から0.1になるのと、 0.1から0.01になるのは、同じような変化と感じます。 両方とも1/10ですからね。 前者は0.9も減っていて、後者は0.09しか減ってないのにです。

スライダー問題

音量の調節をスライダーである場合には、特にこれが問題になります。 0から1のスライダーがあると考えてください。

f:id:hirasho0:20190626104516p:plain

左端が0で右端が1だとします。 1から0.1に落とした所で、「今下げた分くらいもう一回下げたい」 と思ったとします。もう一回1/10にすればいいので、 0.01にすれば良さそうです。 しかし、調整に使える幅はさっきの1/10しかありません。 さらにもう一回1/10にしようと思うと、 もう1/100しか幅がないので、微調整はほとんど無理です。

このように、 「右の方はかなりいじってもあまり変わらないのに、 左の方はわずかに動かしただけで派手に変わってしまう」 ということが起きます。

しかし、スライダーで調整する時には、 「どこであれ1cm動かした時の変化は同じであってほしい」 のです。デシベルにすれば、かなりそれに近づきます。

f:id:hirasho0:20190626104508p:plainf:id:hirasho0:20190626104511p:plain

第二のスライダーはデシベルで設定するためのものです。 まず0から-12に下げ、次に-24まで下げたとします。 どちらも差は12ですが、 それに対応する元の音量は、「1から0.25」と「0.25から0.625」 です。差は0.75と0.1875で、全然違いますが、 デシベルにすれば同じ幅になるのです。 是非サンプル でいじってみてください。

これが、デシベルが広く使われている理由です。 ほぼほぼ「整数2桁」で調整ができます。

一般的に、調整幅が広い場合には、 デシベルのように対数化した値で調整できるようにします。 そうでないと、0.0001から1、というように調整幅の桁数が 多くなって調整が難しくなります。

例えばグラフィックスでも同じことが起こり、 光の明るさなんかは対数で設定できた方が良いでしょう。 星の明るさと昼の太陽の明るさでは100億倍違ったりします (wikipediaの「等級」のページ参考) 。

「整数2桁」の使いやすさ

音量に限らず、大抵の調整は「整数2桁」でできるようにしておくと便利です。 人間が違いを認識できる段階はそれくらいでしょう。

例えば、敵の攻撃力なんかは、 序盤は5とか10で、終盤は1万、みたいなことになりますよね? 序盤は1の価値が凄まじく大きいですが、終盤は1どころか100 変えても大差ありません。

もしこれをスライダーにして調整させれば、 音量と同じく使いにくい状態になるでしょう。 こういう場合も対数にすればスライダーで調整しやすくなるのかもしれませんね。

実装

相互変換

まず0から1の値とデシベルの相互変換が必要です。 0から1の値にも名前が欲しいので、仮に「リニア値」 と呼んでおきましょうか。

public static float ToDecibel(float linear, float dbMin)
{
    var decibel = dbMin;
    if (linear > 0f)
    {
        decibel = 20f * Mathf.Log10(linear);
        decibel = Mathf.Max(decibel, dbMin);
    }
    return decibel;
}

public static float FromDecibel(float decibel)
{
    return Mathf.Pow(10f, decibel / 20f);
}

リニア値は、10を底とする対数(Log10)を取り、 20を掛ければデシベルになります。 例えば0.1であれば、対数を取ると-1です。 これに20を掛けて-20となります。

この際、Log10(0)はマイナス無限になるため、 一定以上小さい値は最低値に固定してしまいます。 「どれくらいで無音とみなすか」がコード中のdbMinで、 -80あたりが良いのではないでしょうか。

サウンドデータが16bitであれば、-32768から32767で、 -80dbは1万分の1ですから、-3から3までの幅に落ちます。 ほぼ無音と言えるでしょう。 もし完全に0に落としたいのであれば、-90とか-100とかにしても 良いのかもしれません。

幅が狭い方がスライダーの調整はしやすいのですが、 -60くらいだと、スピーカーの音量が大きい時に微妙に聴こえてしまいます。 私が試した限りではありますが、-80で良い気がします。

見える所を全部デシベルにする

あとは、データの運用の話です。 データのパイプラインのどこをデシベル、どこをリニア値にするか、 という話なのですが、私の現状の結論は可能な限りデシベルです。

すなわち、AudioSource.volumeに値を渡す寸前に FromDecibel()を使ってリニア値に変換し、 逆にAudioSource.volumeを外に出す時には、 すぐにToDecibel()を使ってデシベルにしてしまいます。

例えばBGMの音量を設定する関数、 みたいなのを作った場合はデシベルを受け取るようにし、 お客さんが音量調節できるスライダーを作った場合は、それもデシベルにします。 それを端末に保存する時もデシベルで、 効果音の音量をエクセルやスプレッドシートで設定する時もデシベルです。 AudioSourceを直接いじるプログラマ以外にはデシベルしか見えない という状態にします。

複数の設定値の合成

設定値が複数重なった場合は、デシベルを加算します。

例えばある音素材のデフォルトの音量が-6dbで、 あるシーンでそれを-12dbして鳴らす設定で、 さらにお客さんが全体の音量を-6dbと設定しているのであれば、 全部足して-24dbとなります。

これは、リニア値であれば掛け算です。元データが0.5(=-6db)、 シーンの設定が0.25(=-12db)、お客さんが0.5(=-6db)を設定していれば、 全部掛けて0.0625とします。これはデシベルで言えば-24dbでして、 計算結果は同じになります。デシベルの方が足し算で済んで簡単だと思いませんか?

スライダーを作る時に一工夫

デシベルのスライダーを作るのは簡単で、 左端で-80、右端で0、となるようなスライダーを用意すれば済みます。

ただ、-40、つまり1/100以下の領域を微妙に調整したいことは あんまりありません。すごく小さい音から無音の間って、 そんなに調整細かくやらなくてもいいですよね?

「細かく調整したい場所にスライダーの長さを割く」 ことが使いやすさにつながるわけで、 -80から0まで均等に使ってしまうと、-80から-40までという あんまり必要ない場所にかなりの長さが割かれてしまいます。

そこで、もう一工夫してはいかがでしょうか。

とりあえず二乗

-80から-40までの幅よりも、 -40から0までの幅の方が大きくなるように、 さらに変換関数を挟みます。 いろんな方法がありますが、「二乗する」は一番簡単でしょう。

今、-80dbで0、0dbで1である、としましょう。 例えば、-40dbであれば真ん中ですから0.5ですね。 これをこのまま使わず、二乗して0.25にしてからスライダーの位置とします。

どうでしょう?

-40dbで0.25ということは、-80dbから-40dbまでがスライダーの1/4の長さしか 使っていないということです。残りの3/4は -40dbから0dbに使われます。 細かく調整したい上の方により長い距離を使えるので、調整がしやすくなるのです。

f:id:hirasho0:20190626104519p:plain

コードはこんな感じでしょう。

static float DbToSliderPos(float db)
{
    float pos = (db + 80f) / 80f; //[-80,0]→[0,1]
    return pos * pos; // 二乗
}

逆に、スライダーからデシベルにする時には、 2乗の逆関数、つまり平方根を取ります。 コードにすれば、こんな感じです。

static float SliderPosToDb(float pos)
{
    pos = Mathf.Sqrt(pos);
    float db = (pos * 80f) - 80f; //[0,1]→[-80,0]
    return db;
}

お客さんに調整してもらうUIであれば、 これくらいが丁度いいんじゃないかなと思います。

実際、サウンドのプロの方が使うCUBASE 等のサウンド編集ソフトでも、 音量調節のスライダーは下の方が狭くなっています。 こういう工夫はあってもいいのではないでしょうか。

終わりに

このへんの整備は、 データの量産が始まる前にやっておいた方がいいと思います。

開発序盤は音量のことなんか気にせずにダミーデータをテキトーに 鳴らしていて、開発中盤になってからサウンドが合流、 ということは結構ありますよね。

そうなると、サウンド素人なプログラマが サウンド絡みの処理を作り終わってデータ入力パイプラインの 設計まで終わった後に、サウンドの専門家が参加することになります。 効果音のパラメータのスプレッドシートやjson等々も 全部リニア値で指定、みたいなことになっていると、 サウンドの人がまともな調整をするのはかなり大変になります。 「セリフしゃべってる間はBGMを-8dbで」 とかサウンドさんが言っても、それをいちいちリニア値に変換しないと いけなくなってしまうのです。

実は東京プリズンの開発がそうでした。 かなり後になって慌ててサウンドさん向けに デシベルでデバグ用の音量グラフをつけたり、 お客さん用のオプション画面のスライダーを デシベルベースに直したりしたのです(しかも2乗の工夫はしてなかった)。 そして、入力データはもはやデシベルにはできませんでした。

もし、同じように入力データ側をいじれない状況になってしまっていても、 できることはあります。 お客さん用の音量調節UIや、サウンドさん向けの音量デシベル表示くらいは、 後からでもできるはずです。 やっておいて損はないと思いますよ。