2018年3月4日

【Papyrus.psc】自動脱衣・着衣機能をつけよう

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

寝るときには重い鎧を脱いで欲しい!

NPCおよびプレイヤーの自動脱衣・着衣機能を実装してみましょう。お風呂へ入る時に裸で入浴するようにしたり、ベッドで寝る時に重い鎧を脱ぐようにしたりすることができます。

※2020/07/14 新サイトでアップデート記事を書きました。→ 自動脱衣スクリプト2020 | 今もSkyrim


目次

  1. NPCの自動脱衣・着衣
  2. プレイヤーの自動脱衣・着衣(要SKSE)
  3. NPCおよびプレイヤーの自動脱衣・着衣スクリプト統合版(要SKSE)

1. NPCの自動脱衣・着衣

1-1. 脱衣させる場所にトリガーボックスを設定する

自動脱衣・着衣機能は、NPCとプレイヤーとで方法が違います。どちらもスクリプトを用いますが、プレイヤーの場合はSKSEが必要になります。簡単なNPCの場合から見ていきましょう。

まずは、この機能を付けたい部屋あるいは領域に、トリガーボックスを設定します。前回の NPCが室内ドアを閉めるようにする方法 と同様に、ブリーズホームのリディアさんの部屋を例にしてトリガーボックスを設置します。

Render Window にリディアさんの部屋を表示します。

ベッドを選択状態にしてツールバーの「T」ボタンを押し、Select Form ダイアログで「TriggerBox」を選んでトリガーボックスを設置します。

上図のように薄い色のボックスで囲まれたら、このボックスの Reference を開いて大きさと位置を調整します。
ベッドを大きく取り囲むようにして、寝る前に服を脱いでもらう作戦です。

これでトリガーボックスが出来ました。参考までに Reference の内容を出しておきます。

「Position」が位置、「Rotation」が回転、「Bounds」が大きさになります。
トリガーボックスで囲まれた範囲が脱衣ゾーンになりますので、場所と用途に合わせて適宜調節してください。

1-2. トリガーボックスにスクリプトを付ける

設置したトリガーボックスにスクリプトを付けましょう。単純な方法の1例を示します。
(今回はスクリプトの文字をやや小さくしています。ご了承ください。)

Scriptname EkaUndressScript01 extends ObjectReference ; NPC用簡易版

Armor Property armRing Auto ; JewelryRingGold を指定するプロパティ

Actor actPasser ; 通った人

;--------------------------------------------------入る時
Event OnTriggerEnter(ObjectReference akTriggerRef)
    actPasser = akTriggerRef as Actor
    If actPasser != Game.GetPlayer() && actPasser != None ; 通った人がプレイヤーでない時
        actPasser.UnEquipAll() ; 全ての装備を外す
        actPasser = None
    EndIf
EndEvent

;--------------------------------------------------出る時
Event OnTriggerLeave(ObjectReference akTriggerRef)
    actPasser = akTriggerRef as Actor
    If actPasser != Game.GetPlayer() && actPasser != None
        actPasser.AddItem(armRing, 1, true) ; Ringをインベントリに入れる
        actPasser.EquipItem(armRing, false, true) ; Ringを装備させる
        actPasser.RemoveItem(armRing, 1, true) ; Ringをインベントリから取り除く
        actPasser = None
    EndIf
EndEvent

トリガーボックスに入る時に脱衣し、出る時に着衣します。OnTriggerEnter/Leave でトリガーボックスから出入りするタイミングを取得して発動します。

脱衣の方は、プレイヤーの着衣を考慮しないなら「If actPasser != Game.GetPlayer()」の条件を消せばプレイヤーにも適用できます。そうすると「NPCは脱衣・着衣機能付き、プレイヤーは脱衣機能のみ」のスクリプトとなります。

NPCの着衣は、指輪を渡して一時的に装備させてから取り除き、自動で最強装備をさせることで実装しています。渡すものは何でも良いのですが、金の指輪が描画も軽そうで目立たないので最適でしょう。
この金の指輪法は複数のMODで採用されていて、どのMODが発端なのかは分かりません。バニラのシステムを生かしたシンプルで素晴らしい方法だと思います。

上記のスクリプトですが、実は脱衣する「actPasser.UnEquipAll()」のところでCTDすることがあります。特に、セーブデータをロードした直後でセルの移動を挟まずにトリガーボックスへ進入させた場合に起こります。セルのロードを挟めば問題ありません。
これについては海外のサイトで1件だけ質問が挙がっているのを見つけましたが、有効な回答は付いていませんでした。自分の環境ではほぼバニラの状態でも起こるため原因不明。

ということで、対策版スクリプトを示します。「入る時」の部分だけ変更しています。

Scriptname EkaUndressScript02 extends ObjectReference ; NPC用対策版

Armor Property armRing Auto ; JewelryRingGold を指定するプロパティ

Actor actPasser ; 通った人

;--------------------------------------------------入る時
Event OnTriggerEnter(ObjectReference akTriggerRef)
    Weapon wepTemp ; 武器情報格納用

    actPasser = akTriggerRef as Actor
    If actPasser == Game.GetPlayer() || actPasser == None ; 通った人がプレイヤーの時は終了
        actPasser = None
        Return
    EndIf

    wepTemp = actPasser.GetEquippedWeapon() ; 右手武器をチェック
    If wepTemp != None
        actPasser.UnequipItem(wepTemp) ; 武器を外す
    EndIf
    Utility.Wait(0.01)

    wepTemp = actPasser.GetEquippedWeapon(true) ; 左手武器をチェック
    If wepTemp != None
        actPasser.UnequipItem(wepTemp) ; 武器を外す
    EndIf

    Int i = 0
    While i < 31 ; 装備スロット30から60まで繰り返す
        actPasser.UnequipItemSlot(30 + i) ; 防具を外す
        i += 1
    EndWhile

    actPasser = None
    wepTemp = None
EndEvent

;--------------------------------------------------出る時
Event OnTriggerLeave(ObjectReference akTriggerRef)
    actPasser = akTriggerRef as Actor
    If actPasser != Game.GetPlayer() && actPasser != None
        actPasser.AddItem(armRing, 1, true) ; Ringをインベントリに入れる
        actPasser.EquipItem(armRing, false, true) ; Ringを装備させる
        actPasser.RemoveItem(armRing, 1, true) ; Ringをインベントリから取り除く
        actPasser = None
    EndIf
EndEvent

武器は右→左の順に GetEquippedWeapon で武器情報を取得して外します。

防具は装備スロットを指定するだけで外せるので、一般に使われるであろう30~60番を全て外しています。ここで While ~ EndWhile が出てきましたが、指定回数だけ行う単純なループ処理に良く使います。

スクリプトはどちらか問題無さそうな方を選んでください。処理内容としては最初の方がシンプルですが、対策版でも個人的には処理速度の差を感じません。CTDしないことの方が大事。

ちなみに、リディアさんは対策MODを入れないと自分のベッドで寝ません。拙作の Breeze Reform なら対策できますよ。

目次に戻る


2. プレイヤーの自動脱衣・着衣(要SKSE)

2-1. 防具用リストの用意

プレイヤーの場合は少し複雑になります。というのも「着衣」させるための装備情報をどこかに保存しておく必要があるからです。
そこで始めに、防具の情報を保存するためのリスト枠を作成しておきます。武器は最大2つなのでリスト形式にせず、スクリプト内から情報を保存するようにします。

Object Window の Miscellaneous > FormList を開き、右クリックして「New」を選ぶと新規にフォームリストを作成できます。

ここではIDに名前を入れるだけでOKです。これで防具リスト枠が用意できました。

2-2. プレイヤー用のスクリプト

次のようなスクリプトをトリガーボックスに付けます。武器の着脱や、防具情報を取得するためにSKSEの機能を使っています。

Scriptname EkaUndressScript03 extends ObjectReference ; プレイヤー用(要SKSE)

FormList Property folArmor Auto ; 防具リストを指定するプロパティ
Weapon Property wepL Auto ; 左手武器を格納するプロパティ、無指定
Weapon Property wepR Auto ; 右手武器を格納するプロパティ、無指定

Actor actPasser ; 通った人
Armor armTemp ; 防具情報格納用
Int i ; While用

;--------------------------------------------------入る時
Event OnTriggerEnter(ObjectReference akTriggerRef)
    actPasser = akTriggerRef as Actor
    If actPasser != Game.GetPlayer() ; 通った人がプレイヤーでない時は終了
        actPasser = None
        Return
    EndIf

    If folArmor.GetSize() > 0
        folArmor.Revert() ; 万が一防具リストが空でない時は初期化
    EndIf

    wepR = actPasser.GetEquippedWeapon() ; 右手武器を取得
    If wepR != None
        actPasser.UnequipItemEx(wepR, 1) ; 右手武器を外す
    EndIf

    wepL = actPasser.GetEquippedWeapon(true) ; 左手武器を取得
    If wepL != None
        actPasser.UnequipItemEx(wepL, 2) ; 左手武器を外す
    EndIf

    i = 0
    While i < 31 ; 装備スロット30から60まで繰り返す
        armTemp = actPasser.GetWornForm(Math.LeftShift(1, i)) as Armor ; 防具情報を取得
        If armTemp != None ; 対象スロットに装備していた時
            folArmor.AddForm(armTemp) ; 防具情報をリストに加える
            actPasser.UnequipItemSlot(30 + i) ; 防具を外す
            armTemp = None
        EndIf
        i += 1
    EndWhile

    actPasser = None
EndEvent

;--------------------------------------------------出る時
Event OnTriggerLeave(ObjectReference akTriggerRef)
    actPasser = akTriggerRef as Actor
    If actPasser != Game.GetPlayer()
        actPasser = None
        Return
    EndIf

    If wepR != None && actPasser.GetItemCount(wepR) > 0 ; 右手武器を所持している時
        actPasser.EquipItemEx(wepR, 1) ; 右手武器を装備
    EndIf
    wepR = None

    If wepL != None && actPasser.GetItemCount(wepL) > 0 ; 左手武器を所持している時
        actPasser.EquipItemEx(wepL, 2) ; 左手武器を装備
    EndIf
    wepL = None

    Int intNum ; 登録されている防具数
    intNum = folArmor.GetSize()

    i = 0
    While i < intNum && i < 31 ; 登録数だけ繰り返す
        armTemp = folArmor.GetAt(i) as Armor ; リスト番号から防具情報を取得
        If armTemp != None && actPasser.GetItemCount(armTemp) > 0 ; 防具を所持している時
            actPasser.EquipItemEx(armTemp, 0, false, true) ; 防具を装備
        EndIf
        armTemp = None
        i += 1
    EndWhile

    folArmor.Revert() ; 防具リスト初期化
    actPasser = None
EndEvent

プロパティが3つありますが、folArmor に先ほど作成した防具リスト枠を指定してください。他の2つは武器用で、スクリプト内から情報を保存できますので何も指定しなくて構いません。

スクリプトが長いので、コメントを入れて補足しておきました。少し分かりにくいかもしれない部分としては「actPasser.GetWornForm(Math.LeftShift(1, i))」の所でしょうか。

GetWornForm の括弧には装備スロットに対応した数字を入れるのですが、Slot Masks - Armor にそのリストがあります。簡単に言えば2の累乗なので、2の0乗から2の30乗まで順に入れて装備しているかどうか判定しています。
Math.LeftShift(1, i)」は「Math.Pow(2, i)」と同義で、2のi乗の値を返します。体感速度に差はありませんが、LeftShift の方が負荷が少なそうに思えるのと、見た目が格好良い(?)ので使っています。

また、プレイヤーの脱衣・着衣を自動で行わず、脱衣かごのようなアクティベーターを用意するのも1つの手です。部屋に入った時に勝手に脱いだりせず、ユーザーの意思で着替えるようにした方が雰囲気が出るような気がします。
アクティベーターにスクリプトを付けるときは、イベントの発動を Event OnActivate 1つだけに変更し、脱衣中かどうかを Bool タイプのプロパティに保存して分岐すれば実装できます。
※分岐条件が違いますが作りましたのでご参考にどうぞ。→ Simple Clothes Basket

---【注意点】(解消済み)2018/03/10追記---
防具を装備する actPasser.EquipItem(armTemp, false, true) の部分について、この方法で装備させた場合の重要な注意点を書いておきます。
それは「プレイヤーが付呪した装備品」をこのコードで装備させても、対象の付呪効果が得られないことです。装備はされるので気付きにくいのですが、「有効な効果」の欄に付呪効果が出ておらず、インベントリから再装備するときちんと効果を得られます。元から付呪されている装備品の場合はこの問題は起こりません。creationkit.com の該当ページ にも書いてあります。
この点については、種々の装備・アイテム系関数を検討してみたのですが解決できませんでした。SkyUIのお気に入りグループ機能で着衣させれば問題ないので、必要ならばそれを使ってもらうことになりそうです。お気に入り設定に介入したりする技術は無いし、今の所お手上げ状態…。すみません。

---2018/07/05追記---
actPasser.EquipItem(armTemp, false, true) によりプレイヤー付呪が適用されない件は、
actPasser.EquipItemEx(armTemp, 0, false, true) とすることにより解消しました。この変更により、プレイヤー付呪品を装備した際にも付呪効果が得られます。
記事内のスクリプトは修正済みです。

---追記以上---

目次に戻る


3. NPCおよびプレイヤーの自動脱衣・着衣スクリプト統合版(要SKSE)

自動脱衣・着衣についてNPCとプレイヤーとで分けて説明しましたが、それらを合わせたスクリプトをコピー&ペースト用に挙げておきます。トリガーボックスに付けることで、NPCもプレイヤーも自動脱衣・着衣するようになります。
プロパティを2つ指定するだけで使用できます。armRing には「JewelryRingGold」を、folArmor には作成した防具リスト枠を指定してください。

Scriptname EkaUndressScript04 extends ObjectReference ; NPC、プレイヤー統合版(要SKSE)

Armor Property armRing Auto ; JewelryRingGold を指定するプロパティ
FormList Property folArmor Auto ; 防具リストを指定するプロパティ
Weapon Property wepL Auto
Weapon Property wepR Auto

Actor actPasser
Armor armTemp
Int i

;--------------------------------------------------入る時
Event OnTriggerEnter(ObjectReference akTriggerRef)
    Weapon wepTemp

    actPasser = akTriggerRef as Actor

    If actPasser == None
        Return
    ElseIf actPasser != Game.GetPlayer()
        wepTemp = actPasser.GetEquippedWeapon()
        If wepTemp != None
            actPasser.UnequipItemEx(wepTemp, 1)
        EndIf
        Utility.Wait(0.01)

        wepTemp = actPasser.GetEquippedWeapon(true)
        If wepTemp != None
            actPasser.UnequipItemEx(wepTemp, 2)
            wepTemp = None
        EndIf
    Else
        wepR = actPasser.GetEquippedWeapon()
        If wepR != None
            actPasser.UnequipItemEx(wepR, 1)
        EndIf

        wepL = actPasser.GetEquippedWeapon(true)
        If wepL != None
            actPasser.UnequipItemEx(wepL, 2)
        EndIf
    EndIf

    i = 0
    If actPasser != Game.GetPlayer()
        While i < 31
            actPasser.UnequipItemSlot(30 + i)
            i += 1
        EndWhile
    Else
        If folArmor.GetSize() > 0
            folArmor.Revert()
        EndIf

        While i < 31
            armTemp = actPasser.GetWornForm(Math.LeftShift(1, i)) as Armor
            If armTemp != None
                folArmor.AddForm(armTemp)
                actPasser.UnequipItemSlot(30 + i)
                armTemp = None
            EndIf
            i += 1
        EndWhile
    EndIf

    actPasser = None
EndEvent

;--------------------------------------------------出る時
Event OnTriggerLeave(ObjectReference akTriggerRef)
    actPasser = akTriggerRef as Actor

    If actPasser == None
        Return
    ElseIf actPasser != Game.GetPlayer()
        actPasser.AddItem(armRing, 1, true)
        actPasser.EquipItem(armRing, false, true)
        actPasser.RemoveItem(armRing, 1, true)
    Else
        If wepR != None && actPasser.GetItemCount(wepR) > 0
            actPasser.EquipItemEx(wepR, 1)
        EndIf
        wepR = None

        If wepL != None && actPasser.GetItemCount(wepL) > 0
            actPasser.EquipItemEx(wepL, 2)
        EndIf
        wepL = None

        Int intNum = folArmor.GetSize()

        i = 0
        While i < intNum && i < 31
            armTemp = folArmor.GetAt(i) as Armor
            If armTemp != None && actPasser.GetItemCount(armTemp) > 0
                actPasser.EquipItemEx(armTemp, 0, false, true)
            EndIf
            armTemp = None
            i += 1
        EndWhile

        folArmor.Revert()
    EndIf

    actPasser = None
EndEvent

目次に戻る


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

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


0 件のコメント:

コメントを投稿

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