はじめに
Little Thief
というタイトルで、ルーム遮蔽を作ってみた話です。
デモ
ルーム遮蔽とは?
隣の部屋の音は少し籠って聞こえる。
という感覚を再現したいところから始まりました。
名称が正しいかは怪しいです。
部屋の定義
部屋はboxコライダーで作れそうと思いました。
部屋の距離
部屋が隣り合っているかどうか?の設定をどうしようかと思ったところ
ChatGptに聞いてみたところ
部屋と部屋をポータルでつないで、部屋のネットワークをつくり、
部屋の距離(Hop)を計算することで、どれくらい部屋が近いかどうかを判定する方法をとってみました。
Aという部屋、Bという部屋、Cという部屋があって、
AとBをつなぐABポータル、BとCをつなぐBCポータル
という感じで
A-AB-B-BC-C
という感じになり、
Aから数えて、Bは隣 (Hop1) Cは隣の隣 (Hop2)
という感じになります。
リスナーの部屋の位置は籠らない
リスナーとおなじ部屋で鳴っている音は籠らない。
外で走っている車とかは、籠っている。
A:リスナーのいる部屋
C:車の走っている音源位置
という感じになります。
リスナーが部屋を移動したら、Hopを計算しなおす。
もし、リスナーが
AからBに移動したら、
Bからの距離を計算し、各ルームのHop値として与えていきます。
Bに繋がっているポータルを順にたどっていって、計算していきます。
距離の計算方法は
/// <summary>
/// BFS(幅優先探索)でHop値を計算
/// </summary>
private void CalculateHopValues(int maxHop)
{
while (_roomQueue.Count > 0)
{
var (currentRoom, currentHop) = _roomQueue.Dequeue();
// 最大Hop値制限
if (currentHop >= maxHop) continue;
ProcessConnectedRooms(currentRoom, currentHop);
}
}
/// <summary>
/// 現在の部屋に接続された部屋を処理
/// </summary>
private void ProcessConnectedRooms(RoomZone currentRoom, int currentHop)
{
if (currentRoom?.connectedPortals == null) return;
foreach (var portal in currentRoom.connectedPortals)
{
if (portal == null) continue;
var nextRoom = NormalizeRoom(portal.GetOtherRoom(currentRoom));
var nextHop = CalculateNextHop(portal, currentHop);
if (ShouldUpdateRoom(nextRoom, nextHop))
{
_hopMapInstance[nextRoom] = nextHop;
_roomQueue.Enqueue((nextRoom, nextHop));
}
}
}
/// <summary>
/// ポータルの状態に基づいて次のHop値を計算
/// </summary>
private int CalculateNextHop(RoomPortal portal, int currentHop)
{
// 開いているドア:Hop値変化なし、閉じているドア:+1
return portal.isOpen ? currentHop : currentHop + 1;
}
/// <summary>
/// 部屋のHop値を更新すべきかどうかを判定
/// </summary>
private bool ShouldUpdateRoom(RoomZone room, int newHop)
{
// 既により小さいhopで到達済みなら更新しない
return !_hopMapInstance.TryGetValue(room, out var existingHop) || newHop < existingHop;
}
接続された部屋をリスナーの位置から順にたどって、Hop値を入れていき、
順に図っていく感じ
実際ゲームで使われたかどうか
実際には、この処理は少し重いようで、quest2では機能しないようにしてあります。
重そうな点として
- リスナーが動くと全部屋チェックが入る
- 部屋の探索が泥臭い(毎度となりの部屋を探す)
- 部屋に音源があるかどうか、リスナーがどの部屋かなどの判定がUnityコライダー依存(pysix)
- 鳴っている音すべてのフィルターが一気に変化する
とくにフィルターを少しずつ変化させているところとかで、音が多く鳴っていると問題がありそうです。
(音数xコライダー判定、エフェクト変化)
今回のゲームでは同時の発音数がかなり少ない方なので、quest3では許されましたが、
あんまり効率はよくない実装かもしれません。
拡張性
今回はポータルはhop計算可能な最小限のただのつなぎでした。
ドアを開けるでつながっている場合同じ部屋扱いする形になっていて、複雑な形状の時に利用しています。
ドア(ポータル)から音を鳴らしたい
部屋の外の音はポータルよりで鳴ってほしいとかはあるかもしれません。
今回は、その必要性は無さそうなのと、処理がこれ以上複雑にしても効果が薄いと判断して実装はしませんでした。
おわりに
計算リソースがもっと多いハードがでてくれると、オーディオのリアルタイム処理も気軽にできるようになるだろうなと思いつつ。
効果のある処理に限定して、一部、実際に効果があった時は良かったなぁと心にとどめています。
あと、この部屋の設定は自動化されていないくて、地道に手作業で設定する必要があるのですが、
壁際でのイベントなどで、若干融通が利かせられたので、この規模ならありかもですが、
これ以上大きい規模の場合は、さらなる自動設定化を考える必要がありそうです。
反省点としては、今回のゲームではここまで部屋増やさなくてもよかったかも
以上
追記:レイは飛ばしていないのか?
レイ飛ばして、背景にも反応するもの(音の遮蔽として利用可能なコライダー)があれば、それもあり。
仕組みを後から、シーンのデザインとは別に 外側から当てはめたような形だったので、ゲームに合わせた最適化していない感じになってしまったのが心残り。
初期の段階で「遮蔽も含めてやりたい」となっていればまた別な方法が考えられたかもしれない。
ただ、今回のプロジェクトは、複数プロジェクトが稼働していたところでのチャレンジだったので時間的にも厳しいところ。
今回の需要としては、距離減衰で表現しきれなかったこと
当初は距離減衰でなんとかしようとしたところ、
1階と2階の発生音が近接すぎて、どうにも距離設定が難しいというところから、
なんとか自動でそれっぽくできないだろうかというところから模索した。
今回のやり方だと、家や部屋が増えた時の対応は比較的簡単との見込みで始めた。
1階、2階の対応だけで良かったのでは?
効果のわかりやすい場面だけにしぼって、特別対応するみたいな形で実装する余裕もない状況での、簡易的な遮蔽効果を試した感じになります。
個人的に、効果が分かる場面も、結果として少なかったので、もし絞るとしたら
- 1階と2階の音の差 (階違いで距離が近い時の音の遮蔽効果)
- 2階ベランダのドアを超えた時の、燃えている音と外を走る車の音の遮蔽の変化
- 建物の中・外の音の遮蔽の変化(銃声など)
- 環境音(鳥のさえずりなど)←途中で鳴らし方を変えたので、ルーム遮蔽なくてもよくなってしまった。(初期は単純に外の音一つ、後期は木々の近くで聞こえるように複数音に)
外を走る車の音なども、距離減衰でこもらせるとかできれば、ルーム遮蔽はしなくても良いかもしれない。
となると、結局距離減衰でなんとかなってしまうことが多い。
さらなる壁際の音のリアルさ追求として
ドア付近・窓の壊れ変化などでの音の変化があれば、良かったけど
ポータル設定が増えすぎるのと、やるとすると、音源位置をポータルに寄せるとか、さらに複雑な処理が必要になる。そこの最適化はすすめていなかったので、あきらめた。
このゲームでは、ドアの開放は通過できるけど、窓の開放は見た目は変化するけど、それほどユーザーの危機感に繋がらない。
あと敵もゆっくり動いているから、音での危険度変化もそれほど重要度がない。
もし、こんな感じの複雑なことをやるなら、アプローチを少し変える必要がでてくる。(近接の壁・音のみ処理する。 遠方は別の処理を用意するなど手間が増える)
おまけ:壁際の対応の別解
アルトデウスで、あるキャラが入れない部屋があって、部屋の外から声をかけてくるシーンがありました。
この時は、ボイス自体にフィルターをかけた音にしていました。
ドアが開くと、フィルターがとけます。
おまけ2:かくれんぼ
ディスクロニアで、あるキャラが隠れているシーンがあって、外の出来事の音が聞こえてくるシーンがありました。
こちらも事前に音にフィルターをかけて対応していました。
リアルタイムではないけれど、動的な変化が無い場合は、負荷の低い手法をまず想定します。
事前処理の場合、単純なローパスだけでなく、フィルターをより自然な処理にできるメリットがあったりします。