2019年4月4日

【Papyrus.psc】移動するマネキンの謎

- Papyrusスクリプトの記憶を記録に残すシリーズその15 -

スタァァップ、マネキン!


目次

  1. マネキンはなぜ移動するのか?
  2. 「MannequinActivatorSCRIPT」を検証しよう
  3. 装備増殖はなぜ起こる?
  4. 対策スクリプトを考えてみる

1. マネキンはなぜ移動するのか?

1-1. マネキンとは何か

「マネキンが移動していた」なんて現象、スカイリムでは珍しくありません。現実世界で起こったら恐怖以外の何物でもないですが、スカイリムなら皆さん慣れていると思います。

そこで、このマネキンにどういう設定がされているのかを調べてみました。バグの修正とかではなく、「なぜそうなるのか」を知りたいというのが主な目的です。

先に書いておきますが、Bethesda様は、マネキンが移動しないようにこれでもかというくらいの対策をしています
それでも移動しちゃうのだからまぁ、そういうこともあるんだろうで済ますのが大人の対応ってもんでしょうけども、今回はできる限り追究してみました。

検証の対象に選んだのは、個人的に移動しているのを見たことがある、レイクビュー邸の武器庫のマネキンです。

ここ↓

ほんとハースファイアは鬼仕様ですね…。余計なものを非表示にしてマネキンを出してみます。

この部屋のマネキンの1つは、以下のオブジェクト群で構成されていました。括弧書きは RefID です。

  • BYOHHouseMannequin [03015D5D] … ゲーム内で見えるマネキン本体
  • MannequinActivateTrig [0300C577] … アクティベートするためのボックス
  • XmarkerHeading [03014C5F] … スクリプトで位置を戻すためのマーカー

全部の構成を見た上での結論として、移動してしまう原因は「BYOHHouseMannequin」そのものにあるようです。これに付けられている以下の2つの設定が関係ありそうでした。

  1. MannequinStay [000D7510] … 行動を決めるAI
  2. MannequinActivatorSCRIPT.pex … スクリプト

マネキンは Actor に分類されます。NPCと同じなんですね。装備を着せるためにはそうする必要があったのでしょう。
Actor なのでAIが設定されていますし、プレイヤーがアクティベートしたときに装備を渡すためのスクリプトも付いています。

AIとスクリプト、いかにもありそうな原因です。ということでそれぞれ見ていきましょう。

1-2. マネキンのAI

まずAIからいきます。

マネキンのAIは、「Travel」という移動テンプレートを使っています。
「初期位置から半径5の範囲(ほぼ足元)に移動する」というAIで、設定されているのはこれ1つです。何かあっても初期位置に戻るように、ということだったのかもしれません。

ところが、このAIがまず原因の1つとなっています。

AIが有効な状態のとき、もしマネキンの足元にナビメッシュが敷かれていなかったらどうなるでしょう?

これはすぐにピンとくると思いますが、「ちょっとワープ」します。Actor というのは、ナビメッシュのないところに置かれたり、移動経路で引っかかったりすると、最寄りのナビメッシュの位置にワープすることがあります。

ハースファイアのマネキンも、作成されると同時に「コリジョン」という見えないボックスを足元に作成して、ナビメッシュをカットしています。おそらく、出現したマネキンに他のNPCが引っかからないようにするためでしょう。

静止状態のマネキンはAIが「false (無効)」の状態なのでワープしないのですが、実はごくわずかにAIが有効になる瞬間があって、その瞬間にピンポイントでナビメッシュワープをしたマネキンが、移動した先で固定されてしまうと考えられます。

この「ごくわずかにAIが有効になる瞬間」は、次のタイミングで発生しています。

  1. 初めて作成された瞬間。すぐにスクリプトでAIを無効にしているものの、作成されてからスクリプト処理が行われるまでのごくわずかの間、AIは有効な状態にある。
  2. スクリプトの処理中。後述する理由により、セルをロードするたび、瞬間的にAIのオンオフをしている。

Disable(非表示)状態の Actor を Enable(表示)状態にする際、あらかじめAIを無効にしておくという設定はないようです。
そのため、初めて出現する際にはAIが必ず有効になっていると考えられます。実際、スクリプトの中でAIをすぐ無効化する処理が書かれています。この無効化されるまでの瞬間がまず1つ目。

また、後述する通りマネキンをロードする際に毎回AIをオンオフしていて、AIが有効になる瞬間を作っています。これが2つ目です。

調べてみると、この「AIによるナビメッシュワープ」がマネキン移動の原因の1つだと突き止めているサイト様がすでにありました。

このサイト様では、マネキンのある場所にナビメッシュを敷いておくことで、移動しないように修正できたことが書かれています。
ナビメッシュの上に立っていればワープしないはずなので、自作の家でしたらこれが最も簡便な対処法でしょう。

すべてのマネキンにこの対策を施すのは困難ですから、それではとAI自体を修正しているのが以下のMODです。

Description を読むと、この作者様もAIに着目し、「Travel」しないAIに置き換えることで移動させないようにしているようです。
おそらく、マネキンの移動対策としてはこのMODが最も有効なのではないかと思います。

また、AIの設定では少し面白い部分もありましたので、ここで書いておきます。

このAIには「パピルスフラグメント」というミニスクリプトのようなものがついていて、以下の関数を実行しています。

akActor.MoveToMyEditorLocation()

「MoveToMyEditorLocation」は初期位置に移動するための関数です。AIはこれ1つなのに、開始、停止、変更される場面すべてに設定されています。後述のスクリプトでAIのオンオフを頻繁に行っているのですが、そのたびにこの関数が実行される仕様です。(あくまでも仕様というだけで、これがすべて忠実に実行されているとは限りません。)

まぁなんというか、徹底的に元の位置に戻そうとしていた形跡が伺えますね。…それでも移動してしまうのがなんか悲しい。

1-3. マネキンのスクリプト

スクリプトの話に移りますが、内容の詳しい検証は後回しにして、先に少し触れておきたいことがあります。
マネキンに付いている「MannequinActivatorSCRIPT」というスクリプトが、「やろうとしたけどボツにした部分」のお話です。

実はこのスクリプトには、コメントアウトして無効にしている行がかなりあります。分かりやすく、サクラエディタで色分けした下の画像をご覧ください。

緑色の部分はすべてコメント扱いで、実際には実行されません。しかし、何か色々と書かれた行が並んでいるのがお分かりになるかと思います。

ここで何をやろうとしていたのかというと、「マネキンのポーズを変更できるようにする」機能を付けようとしたようです。見慣れたマネキンは空手の不動立ちのようなポーズをしていますが、これを3パターンに変更できるようにしようとした形跡があります。
実装されていればかなりアツいんですが、逆にこの機能を付けようとしたせいでマネキンが移動してしまうようになったはずです。

というわけで、次項でその内容を検証していきたいと思います。

目次に戻る


2. 「MannequinActivatorSCRIPT」を検証しよう

2-1. ロードされたときの処理

スクリプトソースは357行もありますので、大事な部分だけ追っていきます。

まずはマネキン、およびそのセルがロードされたときの処理を見てみます。コメント行はすべて消しています。

EVENT OnCellLoad()
    if IsEnabled() && !bResetOnLoad 
        ResetPosition()
    endif
EndEVENT

EVENT OnLoad()
    if bResetOnLoad
        bResetOnLoad = false
        ResetPosition()
    endif
endEVENT

まず後半の「EVENT OnLoad()」を見てください。この部分の意義はただ一つです。
マネキンを作成して初めて表示されたときにだけ、初期位置へ戻す」ということをしています。

後で触れますが、「ResetPosition()」が初期位置へ戻す Function (機能、命令) です。これが実行されると、ほぼ確実に初期位置へ戻ると思っていてください。

「OnLoad」というイベントが発動する条件は次の3つです。

  1. マネキンが「Disable (非表示)」から「Enable (表示)」状態にされたとき。
  2. Enable のマネキンと同じセルにいる状態のセーブデータをロードしたとき。(不確実)
  3. Enable のマネキンが存在しているセルに進入したとき。(メモリにロード済みの場合は発動しない)

マネキンは初期状態が「Disable」ですので、作成して「Enable」になったときに初めて1番の条件で OnLoad が起こります。

このとき、「bResetOnLoad」というBool型のプロパティを使って分岐することで、マネキンを作成したときにだけIf分岐内に入るようにしています。「bResetOnLoad」の初期値は「true」に設定されています。

マネキンが作成されたときは、AIが有効な状態になっています。その瞬間にナビメッシュワープが発生したとしても、この「OnLoad」の部分ですぐ初期位置へ戻るようになっています。

次に「EVENT OnCellLoad()」の部分を見ます。これはその名の通りセルがロードされたときに発動します。OnLoad 条件の3番と関連し、セル内のオブジェクトがすべてロードされた瞬間に発動します。
ただし、すでにそのセル内にいるセーブデータをロードしても発動しません。セルへの進入でも、プレイ中にすでに訪れていてメモリにロード済みの場合は発動しません。

この OnCellLoad にはIf分岐が組まれていますが、マネキンが作成されている場合、100%分岐内に入って ResetPosition() を実行し、元の位置に戻ります。

発動条件はやや複雑なものの、マネキンがロードされるたびに元の位置へ戻す指示が出されていることは確かです。

2-2. 元の位置に戻す処理

上で出てきた「ResetPosition()」が実行されたときの処理を見ていきましょう。
この部分はいろいろと書いてあるのですが、コメントアウトされた行を除くと非常に短い処理です。

Function ResetPosition()
    self.BlockActivation()
    self.EnableAI(TRUE)

    disable()
    if !IsDisabled()
        MoveTo(GetLinkedRef())
    endif
    enable()

    EquipCurrentArmor()
;    PlayCurrentPose()
    self.EnableAI(FALSE)
endFunction

最初の「self.BlockActivation()」は、マネキン本体をアクティベートできなくさせる処理です。
しかし、設定で「MannequinActivateTrig」がアクティベートの親になっているので、これはあまり関係ありません。

次の「self.EnableAI(TRUE)」で、AIを有効にしています。これは、理由を2つ見つけることができます。

  1. 後の行「MoveTo」でマネキンを「GetLinkedRef()」の位置(初期位置)に移動するため。
  2. 後の行「PlayCurrentPose()」でポーズを変更するため。(コメントアウトで無効化済み)

「MoveTo」で Actor を移動させるには、AIが有効である必要があります
「GetLinkedRef()」には「XmarkerHeading」が設定されていて、これがほぼ初期位置に置かれていますから、この関数が当たることで元の位置に戻る設定です。

その前後で「disable()」や「enable()」等の処理をしている理由の1つは、このマネキンに「Enable Parent」が設定されているときだけ「MoveTo」を行うためなようです。
「Enable Parent」は、マネキンの「Enable」状態を決める親の設定で、この親が「Enable」ならマネキンも「Enable」になります。

ハースファイアのマネキンには「Enable Parent」が設定されておらず、If分岐に入らないため、「MoveTo」されることはありません。もしかしたらハースファイア以外で置かれたマネキンのために、この判定が必要なのかもしれません。
親の存在は、SKSEがあれば「GetEnableParent」関数で取得できますが、バニラでやろうと思うと上記の処理になるでしょう。

「MoveTo」が「Enable Parent」を持つものだけに作動するとなると、ハースファイアのマネキンはどうやって元の位置に戻っているのでしょうか?

これが実は、AIのところで触れたパピルスフラグメントの設定です。
AIのオンオフのたびに「akActor.MoveToMyEditorLocation()」という関数が実行され、元の位置に戻っています。
この Function では2回、「EnableAI(TRUE)」と「EnableAI(FALSE)」のときにこの処理が走る設定です。(あくまでも設定上は。)

それから、「disable()」と「enable()」の意味はもう1つあります。

特に最初に出現させたときがそうなんですが、ポーズが「不動立ち」ではなく「普通に立っている」状態になってしまうんです。

これはおそらくAIが関係しているのかなと思います。「歩きだそうとした瞬間」のようなポーズで固定されているので、出現させてからスクリプトでAIが無効化されるまでのわずかな間に動いてしまっているようです。
どちらにせよ、出現時のポーズを「不動立ち」にさせるには、「disable()」と「enable()」の処理が必要でした

また、AIをオンオフしていたもう1つの理由として、元々は「PlayCurrentPose()」の部分でポーズを変更しようとしていたことが挙げられます。
ポーズ変更には「PlayIdle()」という関数が使われていて、これはAIが有効でないと効きません。最終的にはコメントアウトされているものの、このためにもAIを有効にする必要があったと思われます。

2-3. 移動する原因

長々と書いていますが、元の位置に戻す処理がしっかり組み込まれていることだけは、なんとなくお伝えできていますでしょうか。

でも、「それじゃあ何で移動しちゃうの?」と思いますよね。

Papyrusスクリプトは、原則として忠実に、書かれている内容を実行していきます。
もし、書き方があまりよくなくてエラーが出た際には、その行をちゃんとスキップして次の処理を続行してくれます。エラーがあったからといってゲームが落ちてしまわないように、スキップしてくれるのです。

一方で、エラーのない処理がすべて確実に実行されるのかといえば、それは違うと思っています。

私見ですが、ごく短時間に同じスクリプトファイル(pex)で複数の処理を実行させようとした場合、一部はスルーされる可能性があると感じています。

Papyrusはもともと、同じスクリプトファイルを同時に複数走らせることができます。マネキンで言えば、各個体同時に「MannequinActivatorSCRIPT.pex」の処理が走っていて、1つのスクリプトを順番待ちして1体ずつ実行しているのではないはずです。
ところが、短時間で同時に複数走らせると、内容によってはその一部がスルーされます。おそらく過剰な負荷でゲームが落ちないための対策が施されているのではないかと思っています。

マネキンのAIについたパピルスフラグメントも「PF_MannequinStay」という1つのファイルですから、これに大量の指示が入ると、一部はスルーされている可能性があります。
もしスルーされないなら、スクリプト的には必ず元の位置に戻るはずだからです。
多数のマネキンがあるセルに進入すると、AIについたパピルスフラグメント処理の一部がスルーされ、元の位置に戻らないものが出るのではないかと思います。

以上をもとに、私が推定したマネキンの移動原因は、次のようになります。

  • AIが有効になった瞬間にナビメッシュワープをしてしまったマネキンが、初期位置から移動してしまう。
  • この際、スクリプト処理が重なって一部がスルーされた場合、元の位置に戻る処理が行われないことがある。

出現時やセル進入時にたまたまナビメッシュワープをしたマネキンで、元の位置に戻るスクリプト処理をたまたまスルーされたものだけが「移動現象」を起こしている、と考えると、忘れたころに遭遇するのも納得できそうです。

目次に戻る


3. 装備増殖はなぜ起こる?

3-1. マネキンの装備し直し

上記の「ResetPosition()」で、1つ触れなかった行があります。「EquipCurrentArmor()」です。

これは、その名の通りマネキンに装備し直させる処理です。
実はその行の前で「disable()」と「enable()」を行ったとき、マネキンの装備はすべて外れてしまいます。そのため、保存してある装備品情報をもとに、それらをすべて装備し直しています。

装備のし直しは「ResetPosition()」の中で行われていますから、マネキンがロードされるたびに、位置を戻す指示と装備し直す指示が出されていることになります。
1体ならまだしも、10体とかのマネキンが同時にそれらの処理を行うとしたら、何となくスクリプトが詰まってしまいそうな感じがします。

3-2. 装備情報の登録処理

マネキンに装備を渡すと自動的に着てくれますが、そのときにもスクリプトが走っています。
装備の登録枠があらかじめ10個用意されており、空いた枠を探して情報を登録する手順になっています。

Function AddToArmorSlot(Form akBaseItem)
    bool FoundEmptySlot = FALSE
    
    if (ArmorSlot01 == EmptySlot) && (FoundEmptySlot == FALSE)
        ArmorSlot01 = akBaseItem
        FoundEmptySlot = TRUE
    endif
    
    if (ArmorSlot02 == EmptySlot) && (FoundEmptySlot == FALSE)
        ArmorSlot02 = akBaseItem
        FoundEmptySlot = TRUE
    endif
    
    if (ArmorSlot03 == EmptySlot) && (FoundEmptySlot == FALSE)
        ArmorSlot03 = akBaseItem
        FoundEmptySlot = TRUE
    endif
    
    if (ArmorSlot04 == EmptySlot) && (FoundEmptySlot == FALSE)
        ArmorSlot04 = akBaseItem
        FoundEmptySlot = TRUE
    endif
    
    if (ArmorSlot05 == EmptySlot) && (FoundEmptySlot == FALSE)
        ArmorSlot05 = akBaseItem
        FoundEmptySlot = TRUE
    endif

    if (ArmorSlot06 == EmptySlot) && (FoundEmptySlot == FALSE)
        ArmorSlot06 = akBaseItem
        FoundEmptySlot = TRUE
    endif
    
    if (ArmorSlot07 == EmptySlot) && (FoundEmptySlot == FALSE)
        ArmorSlot07 = akBaseItem
        FoundEmptySlot = TRUE
    endif
    
    if (ArmorSlot08 == EmptySlot) && (FoundEmptySlot == FALSE)
        ArmorSlot08 = akBaseItem
        FoundEmptySlot = TRUE
    endif
    
    if (ArmorSlot09 == EmptySlot) && (FoundEmptySlot == FALSE)
        ArmorSlot09 = akBaseItem
        FoundEmptySlot = TRUE
    endif
    
    if (ArmorSlot10 == EmptySlot) && (FoundEmptySlot == FALSE)
        ArmorSlot10 = akBaseItem
        FoundEmptySlot = TRUE
    endif
    FoundEmptySlot = FALSE
endFunction

検証のために一応載せましたが、あまりスマートな方法とは言えません。枠を1番から10番まで順番に判定し、空いた枠が見つかったらBool型の変数を使って残りの枠の判定を飛ばす、というやり方です。

スクリプトだけで見ると問題なく動作するはずなんですが、ちょっと繊細な処理になっています。
Bool変数の「FoundEmptySlot = TRUE」という処理をミスっただけで、複数の枠に同じ装備情報を入れてしまう可能性があるのと、もし重複していた場合にそれをクリアするエラー処理がないのです。

装備情報枠は、「プレイヤーが渡したときに登録、取り上げたときに解除」という処理がされていて、10個の枠を順次Bool型で判定していくという同じような方法が取られています。

もちろんすべてが正常に処理されれば何ら問題はありません。しかし、何らかのエラーで重複して登録されたり、登録解除されなかったりしたときの処理が組み込まれていないのも事実です。

3-3. 装備増殖問題

登録情報にエラーが起こると、すでにマネキンが所持していない装備情報を持った登録枠ができてしまう可能性があります。マネキンの装備増殖というのは、これが原因になると思われます。

マネキンから装備を取り上げたとき、当然マネキンのインベントリからはなくなるわけですが、もしエラーで装備情報が残った場合、いつの間にか持っていないはずの装備を着てしまっているはずです。
これは、マネキンに装備を着させている「EquipCurrentArmor()」という Function に原因があります。

Function EquipCurrentArmor()
    if (ArmorSlot01 != EmptySlot)
        self.EquipItem(ArmorSlot01)
    endif
; ~~~~~
    if (ArmorSlot10 != EmptySlot)
        self.EquipItem(ArmorSlot10)
    endif
endFunction

処理が同様なので途中を省きました。枠1番から10番まで同じことをしています。

ここで「EquipItem()」という装備させるための関数を使っています。実はこの関数、指示されたものをインベントリに所持していればそれを着ますが、もし所持していなかった場合、新しく作り出して着せてしまうという仕様を元から持っています。

つまり、プレイヤーが装備を取り上げたときに情報枠の解除が正常に行われなかった場合、ロードされた際にマネキンのインベントリにその装備がなくても、残っている情報に基づいて新しく作り出し、装備してしまうわけです。

もしご自身でスクリプトを書くときに「EquipItem()」関数を使う場合は、次のように所持数で分岐をかけるようにしましょう。

Actor actNPC ; 装備させる人
Form akItem ; 装備アイテム

If actNPC.GetItemCount(akItem) > 0 ; アイテムを持っている場合
    actNPC.EquipItem(akItem) ; 装備させる
EndIf

…と書いておいてなんですが、もしかしたらマネキンではわざと所持数の分岐をかけなかった可能性もあります。
次項の最後で少し触れているのですが、今回の検証過程で「マネキンに渡した装備が消失する」という起きてはならない現象を経験しました。
マネキン担当者の方がこれに気付き、「装備が消えてしまうくらいなら増殖した方がまし」と考えて分岐をかけなかったのかもしれません。

目次に戻る


4. 対策スクリプトを考えてみる

4-1. 対策スクリプトの条件

ここまでの検証をもとにして、対策スクリプトを作れるかどうか考えてみます。次のような条件を想定します。

  • 負荷をなるべく減らすため、位置移動などの指示を最低限にする。
  • 装備情報が残ってしまった場合の対策をしておく。
  • 念のためバニラのスクリプトに戻せるよう、プロパティの種類を一切変更しない。

一番ネックになるのは、プロパティを変更しないという条件です。変更するとバニラに戻すことが難しくなるため、いわゆる「アンインストール非推奨」になってしまいます。

4-2. 対策スクリプトの案

私が考えた対策スクリプトです。あくまでも、途中導入しても正常動作するように、そしてアンインストールできるように、と考えた結果です。

私はこのスクリプトを「良い」とは言えません。自作MODでオリジナルのマネキンを設置するときに、わざわざこのスクリプトを使うようなことはお勧めできません。
バニラのマネキンのために途中導入して、「移動問題」と「装備増殖問題」を改善させるためのものになります。

Scriptname MannequinActivatorSCRIPT extends Actor

Bool Property bResetOnLoad = false Auto
Form Property ArmorSlot01 Auto Hidden
Form Property ArmorSlot02 Auto Hidden
Form Property ArmorSlot03 Auto Hidden
Form Property ArmorSlot04 Auto Hidden
Form Property ArmorSlot05 Auto Hidden
Form Property ArmorSlot06 Auto Hidden
Form Property ArmorSlot07 Auto Hidden
Form Property ArmorSlot08 Auto Hidden
Form Property ArmorSlot09 Auto Hidden
Form Property ArmorSlot10 Auto Hidden
Message Property MannequinArmorWeaponsMESSAGE Auto

;--------------------------------------------------オンロード
Event OnLoad() ; 同時2回発動
    Self.EnableAI(false) ; これさえ効かせれば移動しないはず。以降trueにはしない。
    If bResetOnLoad == true ; 初期状態でtrue指定、初回出現時のみ実行
        bResetOnLoad = false
        Self.Disable()
        Utility.Wait(0.1)
        Self.Enable() ; 立ちポーズを初期状態に戻す
        Utility.Wait(0.1)
        Self.MoveToMyEditorLocation() ; 初期位置へ移動。
    EndIf
EndEvent

;--------------------------------------------------セル読み込み
Event OnCellLoad()
    Self.Disable() ; ポーズがおかしいときのための対策。ここで装備が外れる。
    Utility.Wait(0.1)
    Self.Enable()
    Self.MoveToMyEditorLocation() ; 初期位置へ移動。

    If ArmorSlot01 != None && Self.IsEquipped(ArmorSlot01) == false ; 念のため持っていなくても装備
        Self.EquipItem(ArmorSlot01, false, true)
    EndIf
    If ArmorSlot02 != None && Self.IsEquipped(ArmorSlot02) == false
        Self.EquipItem(ArmorSlot02, false, true)
    EndIf
    If ArmorSlot03 != None && Self.IsEquipped(ArmorSlot03) == false
        Self.EquipItem(ArmorSlot03, false, true)
    EndIf
    If ArmorSlot04 != None && Self.IsEquipped(ArmorSlot04) == false
        Self.EquipItem(ArmorSlot04, false, true)
    EndIf
    If ArmorSlot05 != None && Self.IsEquipped(ArmorSlot05) == false
        Self.EquipItem(ArmorSlot05, false, true)
    EndIf
    If ArmorSlot06 != None && Self.IsEquipped(ArmorSlot06) == false
        Self.EquipItem(ArmorSlot06, false, true)
    EndIf
    If ArmorSlot07 != None && Self.IsEquipped(ArmorSlot07) == false
        Self.EquipItem(ArmorSlot07, false, true)
    EndIf
    If ArmorSlot08 != None && Self.IsEquipped(ArmorSlot08) == false
        Self.EquipItem(ArmorSlot08, false, true)
    EndIf
    If ArmorSlot09 != None && Self.IsEquipped(ArmorSlot09) == false
        Self.EquipItem(ArmorSlot09, false, true)
    EndIf
    If ArmorSlot10 != None && Self.IsEquipped(ArmorSlot10) == false
        Self.EquipItem(ArmorSlot10, false, true)
    EndIf
EndEvent

;--------------------------------------------------アクティベート(MannequinActivateTrig)
Event OnActivate(ObjectReference TriggerRef)
    Self.OpenInventory(true)
EndEvent

;--------------------------------------------------アイテムを渡したとき
Event OnItemAdded(Form akBaseItem, int aiItemCount, ObjectReference akItemReference, ObjectReference akSourceContainer)
    If akBaseItem as Armor != None ; 防具のとき
        AddToArmorSlot(akBaseItem) ; 登録処理
        Self.EquipItem(akBaseItem)
    Else ; 防具でないとき
        MannequinArmorWeaponsMESSAGE.Show()
        Self.RemoveItem(akBaseItem, aiItemCount, true, Game.GetPlayer())
    EndIf
EndEvent

;--------------------------------------------------装備品が外れたとき(取る、装備スロットが被る防具を渡した)
Event OnObjectUnequipped(Form akBaseObject, ObjectReference akReference)
    If akBaseObject as Armor != None
        RemoveFromArmorSlot(akBaseObject) ; 登録解除処理
    EndIf
EndEvent

;--------------------------------------------------ArmorSlot登録
Function AddToArmorSlot(Form akBaseItem) ; OnItemAddedで防具を渡したときに呼び出し
    Bool FoundEmptySlot = false

    If ArmorSlot01 == None && FoundEmptySlot == false ; 空いているスロットを見つけて登録
        ArmorSlot01 = akBaseItem
        FoundEmptySlot = true ; 以降のIfをスキップするためにこの仕様にしたらしい。
    EndIf
    If ArmorSlot02 == None && FoundEmptySlot == false
        ArmorSlot02 = akBaseItem
        FoundEmptySlot = true
    EndIf
    If ArmorSlot03 == None && FoundEmptySlot == false
        ArmorSlot03 = akBaseItem
        FoundEmptySlot = true
    EndIf
    If ArmorSlot04 == None && FoundEmptySlot == false
        ArmorSlot04 = akBaseItem
        FoundEmptySlot = true
    EndIf
    If ArmorSlot05 == None && FoundEmptySlot == false
        ArmorSlot05 = akBaseItem
        FoundEmptySlot = true
    EndIf
    If ArmorSlot06 == None && FoundEmptySlot == false
        ArmorSlot06 = akBaseItem
        FoundEmptySlot = true
    EndIf
    If ArmorSlot07 == None && FoundEmptySlot == false
        ArmorSlot07 = akBaseItem
        FoundEmptySlot = true
    EndIf
    If ArmorSlot08 == None && FoundEmptySlot == false
        ArmorSlot08 = akBaseItem
        FoundEmptySlot = true
    EndIf
    If ArmorSlot09 == None && FoundEmptySlot == false
        ArmorSlot09 = akBaseItem
        FoundEmptySlot = true
    EndIf
    If ArmorSlot10 == None && FoundEmptySlot == false
        ArmorSlot10 = akBaseItem
    EndIf
EndFunction

;--------------------------------------------------ArmorSlot解除
Function RemoveFromArmorSlot(Form akBaseItem) ; OnObjectUnequippedで防具を外したときに呼び出し
    If ArmorSlot01 == akBaseItem ; Bool判定をなくして、不正な情報が残りにくくなるようにする
        ArmorSlot01 = None
    EndIf
    If ArmorSlot02 == akBaseItem
        ArmorSlot02 = None
    EndIf
    If ArmorSlot03 == akBaseItem
        ArmorSlot03 = None
    EndIf
    If ArmorSlot04 == akBaseItem
        ArmorSlot04 = None
    EndIf
    If ArmorSlot05 == akBaseItem
        ArmorSlot05 = None
    EndIf
    If ArmorSlot06 == akBaseItem
        ArmorSlot06 = None
    EndIf
    If ArmorSlot07 == akBaseItem
        ArmorSlot07 = None
    EndIf
    If ArmorSlot08 == akBaseItem
        ArmorSlot08 = None
    EndIf
    If ArmorSlot09 == akBaseItem
        ArmorSlot09 = None
    EndIf
    If ArmorSlot10 == akBaseItem
        ArmorSlot10 = None
    EndIf
EndFunction

;--------------------------------------------------未使用
Form Property EmptySlot Auto Hidden
Idle Property Pose01 Auto
Idle Property Pose02 Auto
Idle Property Pose03 Auto
Int Property CurrentPose = 1 Auto
Message Property MannequinActivateMESSAGE Auto
Function ResetPosition()
EndFunction
Function PlayCurrentPose()
EndFunction
Function EquipCurrentArmor()
EndFunction

このスクリプトでテストプレイを20回繰り返してみたところ、一度だけ位置が戻らないことがありました。
場面としては、ロード済みのセルに入り直すときでした。AIはすでにオフのはずだし、OnCellLoad イベントが起こらずとも移動しないはずなんですが…。

たまたまスクリプト処理が重なったのか、あるいはまた別の原因もあるのか…。もう少し奥が深いのかもしれないですね。

----------------------------------------
※2020/04/15 追記
このスクリプトをしばらく使ってみたところ使い心地は悪くないので、MODとしてこっそり公開します。

Stop Mannequin Script 0.10

----------------------------------------

4-3. リスポーンさせたときの挙動

あと、テストプレイで気づいた重要なことが1つあります。リスポーンに関してです。
新しくマネキンを設置するMODを、ニューゲームでなく途中導入した場合のお話です。

マネキンに装備させてから別セルに行き、30日待機をして戻ると、セルがリスポーンしますよね?
これを自作MODで試してみたら、MODマネキンの装備がすべて外れていたんです。バニラからもともとある(ニューゲーム時から存在する)マネキンは問題ありませんでした。

MODマネキンだけ装備が外れるのに加えて、なんと渡してあった「吸血鬼の王族の鎧」が消失するという現象に遭遇しました。インベントリから消えたんです。
再現性もあるのでいろいろ確認したんですが、リスポーンさせるたびに消失し、その原因は特定できませんでした。他のアミュレットやブーツなどは装備こそ外れていましたが、インベントリには所持したままで無事でした。

しかもこのリスポーンしたMODマネキンは、プロパティもすべて初期状態に戻っていました。当然装備情報もすべて空にされていました。

これ、マネキンのあるMOD家を途中導入した際にはどうなるのか、気になるところです。うちの環境だけなのか、途中導入したマネキンではそうなるのか。

考えた末、もしかしたらマネキンも「ニューゲームでセーブデータに情報枠が確保されるオブジェクト」なんだろうかと思いました。

家具だと良くあるんですが、「初期位置からどうしても移動できないオブジェクト」というのがあって、どうやらそれがニューゲームの際に決定されているようなんです。
それらのオブジェクトは、あとからespやスクリプトで位置を移動しようとしても、すべて無視されてしまって移動できません。

このようになるのは、位置情報やスクリプトのプロパティ情報等を保持する「保存枠」が、セーブデータ内に固定で確保されるためではないかと考えています。
固定の「保存枠」の情報は、後から書き換えられないようになっていたり、リスポーンで初期化されなかったりするものがあるのではないかという予想です。

この現象がもしマネキンにも適用されるとすれば、バニラのマネキンはリスポーンに影響されないのに、MODマネキンは初期化されてしまうのも納得できます。MODマネキンの「保存枠」は固定で確保されていない、ということです。
これがもし正しいなら、マネキンを追加するMODもニューゲームから入れておけば大丈夫、という推測が成り立つんですけども、さすがにそれは面倒で検証する気になれませんでした。

以上で、マネキン検証を終わります。
推測が多くて特に得るものもない結果になってしまいましたが、「マネキンが移動するのにも原因があるんだな」と感じていただければ幸いです。(適当)

位置を元に戻す処理もしっかりと組み込まれてはいるので、「Bethesdaさん、バグを放置してる…」なんて思わないようにしていただけたら、この記事を書いた私も嬉しい限りです。

目次に戻る


【Papyrus.psc】シリーズリスト

補足:私はスクリプトを書くのは好きですが、専門家ではありません。内容は creationkit.com の情報と個人的な経験を基にして書いています。どうかご参考程度にご覧ください。

トップに戻る

4 件のコメント:

  1. あなたのおかげで、装備複製されるバグが修正できました!
    コンソールで装備をequipitem/unequipitemで地道に登録削除して重複登録を削除できました!(直にスクリプト内の変数イジるコマンドが分からなかった。。。というかあるんですかね?

    返信削除
    返信
    1. ひたすら文字だらけの記事をお読みいただいてありがとうございます!
      コンソールで登録を削除できるなんて思いつきませんでした。unequipitemコマンドでもスクリプトが動いてくれるんですね。奥が深いなぁ。

      スクリプトの変数をコンソールで直接変更するようなコマンドは見当たらなかったです。やはりスクリプトを介さないと変更できないんでしょうかね。

      削除
    2. むしろequipitemでは登録呼ばれないのもどうなのって感じですけどね(。。;
      やっぱスクリプト変数の直操作コマンドは無さそうなんですかねぇ、バグ時の対処とか出来ると楽そうなんですが、、、
      最近PC新調してskyrimSEどっぷりプレイ中なのでまた見に来るかもです。ありがとでしたー!

      削除
    3. 他の方のご参考にもなりそうなのでちょっとテストプレイしてきました。

      コンソールの「unequipitem」コマンドでは、スクリプトの「OnObjectUnequipped」イベントが発動するので、装備登録を解除することができます。
      コンソールの「equipitem」コマンドに相当するスクリプトの記述はなく、もし「OnObjectEquipped」イベントが設定されていれば発動します。

      コンソールから「additem」コマンドで渡せば、スクリプトの「OnItemAdded」が発動して装備登録されます。
      (まぁ、これは普通にマネキンをアクティベートして渡せば良いのですけども。)

      勉強になりました。コンソールでのスクリプト発動を教えてもらってありがとうございます!
      PC新調いいですねー。お互いスカイリムを楽しみましょう☆

      削除

注: コメントを投稿できるのは、このブログのメンバーだけです。