Horizon+サバイバルモードの環境は、物資に乏しい序盤が最もつらく、余裕が出てくる中盤以降は緊張感にかけてきます。特に肉類は無限に手に入るため、戦前の食べ物が不要となり、発見しても有り難みがなくなってしまいます。
そこで、時間経過で肉類が腐るようにしてみました。
サンプルソースコード
FoodManagerScript.psc (papyrus)
Scriptname MyTweak:FoodManagerScript Extends Quest
Keyword Property ObjectTypeFoodRawMeat Auto Const
Form Property RottenFood_DiseasedMeat Auto Const
Container Property WorkshopWorkbench Auto Const
ActorValue Property LastCheckedGameTime Auto Const
Actor PlayerRef
Int CheckFoodInterval = 6 ; 6 hour
Float DestoryPercentage = 0.5
; OnTimerGameTime
Int TimerCheckFood = 1 Const
; OnTimer
Int TimerCheckWorkBench = 1 Const
Event OnQuestInit()
PlayerRef = Game.GetPlayer()
RegisterForRemoteEvent(PlayerRef, "OnItemAdded")
RegisterForRemoteEvent(PlayerRef, "OnLocationChange")
AddInventoryEventFilter(ObjectTypeFoodRawMeat)
StartTimerGameTime(CheckFoodInterval, TimerCheckFood)
EndEvent
Event OnTimer(Int aiTimerID)
if Utility.IsInMenuMode()
StartTimer(1.0, TimerCheckWorkBench)
return
endif
CheckWorkBench()
EndEvent
Event OnTimerGameTime(Int aiTimerID)
if Utility.IsInMenuMode()
StartTimerGameTime(0.1, TimerCheckFood)
return
endif
CheckFood(PlayerRef, DestoryPercentage)
StartTimerGameTime(CheckFoodInterval, TimerCheckFood)
EndEvent
Event Actor.OnLocationChange(Actor akActorRef, Location akOldLoc, Location akNewLoc)
CancelTimer(TimerCheckWorkBench)
StartTimer(10.0, TimerCheckWorkBench)
EndEvent
Event ObjectReference.OnItemAdded(ObjectReference akSender, Form akBaseItem, int aiItemCount, ObjectReference akItemReference, ObjectReference akSourceContainer)
;Debug.Trace(akBaseItem + " from " + akSourceContainer)
if !(akSourceContainer as Actor)
if Utility.RandomFloat() < DestoryPercentage
akSender.RemoveItem(akBaseItem, aiItemCount, abSilent = true)
akSender.AddItem(RottenFood_DiseasedMeat, aiItemCount, abSilent = true)
Debug.Notification(akBaseItem.GetName() + "は腐っている")
endif
endif
EndEvent
Function CheckFood(ObjectReference akTarget, float fPercentage)
Form[] kItemList = akTarget.GetInventoryItems()
Int i = kItemList.Length
while i > 0
i -= 1
if kItemList[i].HasKeyword(ObjectTypeFoodRawMeat)
if Utility.RandomFloat() < fPercentage
Int c = akTarget.GetItemCount(kItemList[i])
akTarget.RemoveItem(kItemList[i], c, abSilent = true)
akTarget.AddItem(RottenFood_DiseasedMeat, c, abSilent = true)
Debug.Notification(kItemList[i].GetName() + "は腐った")
endif
endif
endwhile
EndFunction
Function CheckWorkBench()
ObjectReference kWorkBench = Game.FindClosestReferenceOfTypeFromRef(WorkshopWorkbench, PlayerRef, 5000.0)
if !kWorkBench
return
endif
float fLastCheckedGameTime = kWorkBench.GetValue(LastCheckedGameTime)
float fCurrentGameTime = Utility.GetCurrentGameTime()
float fPassTime = fCurrentGameTime - fLastCheckedGameTime
Debug.Trace(kWorkBench + " last=" + fLastCheckedGameTime + " now=" + fCurrentGameTime + " pass=" + fPassTime)
float fInterval = (1.0 / 24.0) * 6.0
if fPassTime < fInterval
return
endif
float fPercentage = 1.0 - DestoryPercentage
fPercentage = 1.0 - Math.pow( fPercentage, Math.Floor(fPassTime / fInterval) )
Debug.Trace("interval=" + fInterval + " percentage=" + fPercentage)
CheckFood(kWorkBench, fPercentage)
kWorkBench.SetValue(LastCheckedGameTime, fCurrentGameTime)
EndFunction
腐る仕組みをどのように実装するか
SkyrimではEquipment ManagerというModを作成し、厳密な消費期限を設定して実現していました。より現実感が増すという利点がありますが、処理が複雑になり負荷が増すという問題もあります。
今回は、処理を省いて手を抜くことで、できるだけ動作がシンプルになる実装を考えてみました。
肉類を入手した時に腐らせる
プレイヤーのOnItemAddedイベントを捕捉します。AddInventoryEventFilterでフィルタを使って肉類だけに限定して負荷を下げます。
Horizonでは生肉にObjectTypeFoodRawMeatというキーワードが設定されていますので、これがそのまま使えます。
OnItemAddedのakSourceContainer引数を検査して、コンテナから入手した場合に限り50%で腐らせます。これは、Actorから入手したときに腐らせないためです。
つまり、厳密な消費期限を設定するのではなくランダムに腐るようにすることで、食べ物の一覧や消費期限の保持をしなくて済むようにしています。
プレイヤーの所持品にある肉類を一定間隔で腐らせる
StartTimerGameTimeを使ってゲーム内の6時間おきに所持品を検査して、生肉を50%で腐らせます。
タイミングによっては生肉を入手してすぐに腐ることもありますが、これを何とかしようとすると複雑になってしまうため、対策はせずに手を抜きます。
ワークベンチに入っている肉類を一定間隔で腐らせる
一番近くにあるワークベンチを検索して、生肉を50%で腐らせます。
一番のポイントは、検査するタイミングです。一定間隔だと無駄なスクリプト稼働が増えるので、できるだけ簡略化しつつも十分なタイミングで検査する必要があります。
プレイヤーのOnLocationChangeイベントをきっかけに検査するようにします。拠点に進入したタイミングで1回だけ検査すれば十分です。
いきなり検査を始めるのではなく、StartTimerを使って10秒後に検査するようにしています。これは、場合によってはOnLocationChangeイベントが短時間に連発することがあるからです。
Fallout 4ではActor Valueを新規に作成できるようになりました。また、Actor ValueといいつつもObjectReferenceにも持たせることができます。厳密にはActor ValueではなくてValueになったわけです。
LastCheckedGameTimeというActor Valueを作成します。これは最後に検査したゲーム内時刻です。初期値は0にしておきます。これをワークベンチに設定します。そして、最後に検査してから6時間以上が経過していたら再検査するようにします。はじめて検出したワークベンチは0なので、必ず検査をすることになります。検査が終わったらLastCheckedGameTimeに現在時刻を設定します。