独自にBGMを再生してみたので、やり方についてまとめました。
なぜ独自にBGMを再生するのか
SkyrimにはBGMを再生する仕組みが既にあります。楽曲を追加するModも色々と公開されています。
Skyrimは戦闘中であることをあらゆる方法でプレイヤーに伝えようとします。Skyrimにおける戦闘中とは、あるActorがプレイヤーと交戦状態であるということになります。FalloutでいうところのDANGER状態です。
私が気が付いた範囲で、以下の要素があります。
- 戦闘用BGMが再生される。
- フォロワーが敵に向かって突っ込んでいく。
- コンパスに赤い丸が表示される。
- ヘルスやマジカの自動回復速度が変化する。
- ファストトラベル、待機といったアクション、溶鉱炉、作業台といった各種設備が使えない。(戦闘中は使えない旨が通知される)
ダンジョンを攻略中に敵に気づかれても、プレイヤーはまだ気づいていない場合があります。また、すべての敵を倒したつもりでも、まだ残っている場合もあります。こういった状況でも上記の要素にて強制的に認識させられてしまうのです。あるいは、野外でスキーヴァーに見つかるだけで、無視して通り過ぎたくても壮大なBGMが流れっぱなしになり、フォロワーが勝手に突っ込んでいきます。これが没入感の妨げになって萎えるので、私は上記の要素を全て排除しています。
戦闘用BGMがないのには慣れましたが、やはり少し寂しい気もあります。特にボス戦ではそれっぽいBGMが流れて欲しいものです。そこで、独自の戦闘中という概念を用意して、戦闘用BGMを流してみました。
BGMを再生するには
CKでMusic Typeを用意します。
そして、スクリプトから以下のコードで再生と停止を行います。
BGMの再生と停止 (papyrus)
MusicType Property eqMUSCombat Auto
; 再生
eqMUSCombat.Add()
; 停止
eqMUSCombat.Remove()
たったこれだけです。
街のBGMを増やすだけならスクリプトから再生する必要はありません。ヴァニラにあるMusic Typeに楽曲を追加するだけです。今回はこのコードをどのようなタイミングで実行するのか、といったところがポイントになります。
BGMを用意する
必要なのはxwm形式の楽曲ファイルです。iTunesで購入した楽曲をxwm形式に変換する手順を説明します。
iTunesで購入した楽曲はm4a形式になっていると思います。これをAudacityで開き、wav形式で保存します。
Yakitori Audio Converterでxwm形式に変換します。
作った楽曲ファイルは data\music 以下に置きます。wav形式とxwm形式の両方を置いておきます。こうしないとCKで楽曲を選択できません。CKでの作業が終わったらwav形式は不要なので退避させます。SSEEditで編集するならwav形式は最初から不要です。
CKでMusic Trackを作ります。iTunesでいうところの曲になります。
Music Trackは新規作成するとインターフェイスがおかしくなるので、既存のMUSCombat01をコピーしました。
Choose Fileのところでwav形式のファイルを選びます。
Choose Finaleはよくわからないので無音のwavファイルを用意して指定しました。
CKでMusic Typeを作ります。iTunesでいうところのプレイリストになります。
以下のように設定しました。
あとはこのMusic Typeをスクリプトから再生すればOKです。
戦闘用BGMをループさせる
Music Trackはループの設定が可能です。
見ての通り、Contains Loopにチェックを入れたら、Loop Begins、Ends、Countに値を入力します。
Loop Beginsがループの開始地点、Loop Endsがループの終了地点、Loop Countがループ回数です。
ループ範囲の見つけ方ですが、Audacityで範囲選択をして、Shiftを押しながら再生でループ再生が可能です。これで実際に聴きながら調整します。
選択範囲は下のStart and End of Selectionになります。数値を直接変更して範囲をかえることもできます。範囲が決まったら分を秒に変換してCKに入力します。
戦闘中であるかどうかの判定
ここからはかなり大掛かりなので、こんな風にしましたという説明だけにします。
以下の状況ではプレイヤーが敵を認識したとみなして、戦闘中に移行します。
- プレイヤーまたはフォロワーが敵対Actorに攻撃をヒットさせる。
- 敵対Actorがプレイヤーまたはフォロワーに攻撃をヒットさせる。
以下の条件で、戦闘中から通常時に移行します。
- プレイヤーが納刀してから10秒が経過する。
- プレイヤーから見える範囲で敵対Actorが存在しない。
プレイヤーまたはフォロワーが敵対Actorに攻撃をヒットさせる
周囲のActorにスクリプトを配布しておき、OnHitイベントで捕捉します。
敵対Actorがプレイヤーまたはフォロワーに攻撃をヒットさせる
プレイヤーとフォロワーのOnHitイベントで捕捉します。
プレイヤーが納刀してから10秒が経過する
プレイヤーのOnActorActionで納刀を捕捉し、RegisterForSingleUpdateで10秒後のOnUpdateイベントを予約します。
プレイヤーから見える範囲で敵対Actorが存在しない
上記のOnUpdateイベントが発火したら、以下の順番で検査を行います。
- プレイヤーが死んでいたら戦闘中を解除します。(不死化Modによって死んでもゲームが続行するので)
- プレイヤーが抜刀してたら何もしません。
- 周囲のActorを確認して見える範囲に敵対Actorがいなければ戦闘中を解除します。
ボス戦の判定の仕方
以下の条件にしました。
- プレイヤーの近くにボスがいる。
- プレイヤーの近くにボス宝箱がある。
- プレイヤーの近くにドラゴンがいる。
Questを作ります。Start Game EnabledとRun Onceはオフです。
Referense Aliasを3つ用意します。
以下のスクリプトを用意してQuestに紐づけます。
ボス曲の判定 (papyrus)
Scriptname eqQuestLocationScript Extends Quest
ReferenceAlias Property Boss Auto
ReferenceAlias Property BossChest Auto
ReferenceAlias Property Dragon Auto
Actor Property PlayerRef Auto
Bool Function Start()
;debug.trace("eqQuestLocationScript Start")
return Parent.Start()
EndFunction
Bool Function IsNearBoss()
if !IsRunning()
if !Start()
return false
endif
endif
;/
debug.trace( \
"eqQuestLocationScript IsNearBoss: " \
+ " Boss=" + Boss.GetReference() \
+ " BossChest=" + BossChest.GetReference() \
+ " Dragon=" + Dragon.GetReference() \
)
/;
Bool result = Boss.GetReference() || Dragon.GetReference() || BossChest.GetReference()
Stop()
return result
EndFunction
IsNearBoss関数がtrueを返したらボス戦です。
仕上げ
BGM制御用QuestとプレイヤーのReference Aliasを別途用意しておき、以下のコードでBGMを制御します。
BGMの制御 (papyrus)
Scriptname eqQuestMainAliasPlayerScript Extends ReferenceAlias
MusicType Property eqMUSCombat Auto ; 雑魚戦BGM
MusicType Property eqMUSCombatBoss Auto ; ボス戦BGM
eqQuestLocationScript Property eqQuestLocation Auto ; ボス検知用Quest
eqQuestMainScript Property eqQuestMain Auto ; フォロワー管理用Quest
eqQuestTargetScript Property eqQuestTarget Auto ; 敵検知用Quest
Bool IsOnHitBusy
Bool _IsInCombat
Bool Property IsInCombat Hidden
Bool Function Get()
return _IsInCombat
EndFunction
Function Set(Bool value)
if value
if !eqQuestMain.PlayerAlive
return
endif
endif
if _IsInCombat
if !value
_IsInCombat = false
eqMUSCombat.Remove()
eqMUSCombatBoss.Remove()
endif
else
if value
_IsInCombat = true
if eqQuestLocation.IsNearBoss()
eqMUSCombatBoss.Add()
else
eqMUSCombat.Add()
endif
endif
endif
EndFunction
EndProperty
Event OnActorAction(int actionType, Actor akActor, Form source, int slot)
if akActor == PlayerRef
if actionType == 8 ; Draw End
if IsInCombat
UnregisterForUpdate()
endif
elseif actionType == 10 ; Sheathe End
RegisterForSingleUpdate(10.0)
endif
endif
EndEvent
Event OnHit(ObjectReference akAggressor, Form akSource, Projectile akP, bool abPA, bool abSA, Bool abBA, bool abHB)
if IsOnHitBusy || !akAggressor || akSource as Enchantment
return
endif
IsOnHitBusy = true
Actor kTarget = akAggressor as Actor
if kTarget && !eqQuestTarget.IsFriendlyFire(kTarget)
IsInCombat = true
endif
Utility.Wait(0.5)
IsOnHitBusy = false
EndEvent
Event OnUpdate()
if IsInCombat
if !eqQuestMain.PlayerAlive
IsInCombat = false
elseif PlayerRef.IsWeaponDrawn()
return
elseif eqQuestTarget.GetVisibleActorCount(PlayerRef) == 0
IsInCombat = false
endif
endif
EndEvent
フォロワーや敵からこのReference Aliasにたいして IsInCombat = true とすることで戦闘中に移行します。