ここではスクリプトの小技やタメになる情報を扱います。
- スクリプト作成時の注意点
- 予約語self
- 否定の "!"
- 関数の処理について
- イベントの処理
- スクリプト最適化Tips
- スクリプト最適化実践編
- OnAnimationEventで取得できるイベント
- AnimationVariableの使い方
- セーブデータに残るもの
- SM Eventを使ったRepeatQuest
- セル移動時関係のイベント
- NPCへのPerk付与について
- Perkの挙動について
- IsInKillMove関数の注意点
- スクリプトによる一部のフォームの値変更について
- ConsoleUtilによるターゲットコマンド実行の注意点及び安全な実行について
- アビリティ付与に関する注意
- Activate関係の処理について
スクリプト作成時の注意点
初心者はSE版のみ対応として作成を行う事(慣れるまでLE・SE両用を作成しない)
SE版が販売して数年はSKSEの更新等で不安定だった事もありLE版が主流でしたが、現在はSE版の販売から10年以上経過しており環境が比較的安定したため、SKSEプラグイン等のModリソース開発者の多くがSE版に移行しています。
それに伴いSPID等、Mod作成用のリソースがLE版での更新がされなくなっている、またはSE版のみリリースしてる所が多くなっているため、LE版と比較してSE版の方が開発リソースは豊富となっています。このため、LE版で高度な事をやりたい場合、特にLE・SE版両用を行いたい場合は作成のハードルが高くなります。
(特にこのwikiでも紹介しているPapyrus関数拡張SKSEであるpowerofthree's Papyrus ExtenderはLE版の更新が2020/07/08が最後となっており、当然SE版と比べてやれる事が少なくなってます)
それに伴いSPID等、Mod作成用のリソースがLE版での更新がされなくなっている、またはSE版のみリリースしてる所が多くなっているため、LE版と比較してSE版の方が開発リソースは豊富となっています。このため、LE版で高度な事をやりたい場合、特にLE・SE版両用を行いたい場合は作成のハードルが高くなります。
(特にこのwikiでも紹介しているPapyrus関数拡張SKSEであるpowerofthree's Papyrus ExtenderはLE版の更新が2020/07/08が最後となっており、当然SE版と比べてやれる事が少なくなってます)
また、慣れない内にLE・SE両用のMod作成をした場合、配布後のトラブル対応に苦労する可能性があるため初心者はSE版のみの開発をオススメします。
アルゴリズムを組む前にSKSEプラグインを確認する
アルゴリズムを組む前に、wikiメニュー一覧の「SKSEプラグイン」に記載されてる「主なPapyrus関数拡張SKSE」のSKSEをダウンロードし、それのソースを確認して実現したい処理がそのSKSEが提供しているPapyrus関数一つで行えるかを確認しましょう。
(例としてInt型をstring型に変換、string型の数値をInt型に変換する処理を作りたい場合、自前でアルゴリズムを組もうとするとかなり手間が掛かりますが"powerofthree's Papyrus Extender"のIntToString、StringToInt関数を用いる事で容易に実行可能でかつ自前でアルゴリズムを組んだ処理よりも高速に処理できます)
(例としてInt型をstring型に変換、string型の数値をInt型に変換する処理を作りたい場合、自前でアルゴリズムを組もうとするとかなり手間が掛かりますが"powerofthree's Papyrus Extender"のIntToString、StringToInt関数を用いる事で容易に実行可能でかつ自前でアルゴリズムを組んだ処理よりも高速に処理できます)
また、アクターやオブジェクトの状態の操作や確認、アクターやフォームの一覧を取得したい場合は"powerofthree's Papyrus Extender"、AIパッケージ上書きやファイル関連の操作は"PapyrusUtil"を使用する事で大抵の事はできます。
まずはこの2つだけでもダウンロードしてどのような処理が行えるか把握しておくと良いでしょう。
まずはこの2つだけでもダウンロードしてどのような処理が行えるか把握しておくと良いでしょう。
コンソール経由でしか処理できないものもある
Papyrusで実行したい関数が無かった場合はコンソールコマンドも確認するようにしましょう。
例えばレベル設定はPapyrusではプレイヤーのみしか行えずNPCに対してはコンソールコマンドでしかレベル設定はできません。
SKSEプラグインのConsoleUtilを利用する事でPapyrus経由でのコンソールコマンドの実行が可能となります。
(ConsoleUtilは指定した文字列をコンソールに出力する関数があるため、デバッグ等でも便利です)
例えばレベル設定はPapyrusではプレイヤーのみしか行えずNPCに対してはコンソールコマンドでしかレベル設定はできません。
SKSEプラグインのConsoleUtilを利用する事でPapyrus経由でのコンソールコマンドの実行が可能となります。
(ConsoleUtilは指定した文字列をコンソールに出力する関数があるため、デバッグ等でも便利です)
コンソールコマンドの表については以下のサイトが参考になります。
https://www.creationkit.com/index.php?title=Category:Console_Commands
https://elderscrolls.fandom.com/wiki/Console_Commands_(Skyrim)
https://en.uesp.net/wiki/Skyrim:Console
https://www.creationkit.com/index.php?title=Category:Console_Commands
https://elderscrolls.fandom.com/wiki/Console_Commands_(Skyrim)
https://en.uesp.net/wiki/Skyrim:Console
デバッグのためエフェクトや状態をコンソールから確認できる環境を整える
LE版はMfg Console、SE版はMore Informative Consoleを導入すれば、コンソール画面で対象を選ぶだけで対象の状態の確認が行えるようになります。
アビリティは付与されたけどマジックエフェクトがアクティブになってない等の確認も簡単に行えるようになるのでデバッグが非常に容易になります。
アビリティは付与されたけどマジックエフェクトがアクティブになってない等の確認も簡単に行えるようになるのでデバッグが非常に容易になります。
Papyrusでキー入力系のスクリプトを作成する時は高速なレスポンスを求めた処理は望まない
キー入力後からの判定や処理は冗長化しないように可能な限り最適化するように心掛けましょう。これを怠るとキー入力直後に処理を実行したいのに入力後しばらくした後に処理が実行されるという事が起こります。
PapyrusはSkyrimのバックグラウンド処理の仕組み上の問題からどのように最適化しても基本的には僅かでも遅延は起こりえるという事を念頭にスクリプトを作成してください。
PapyrusはSkyrimのバックグラウンド処理の仕組み上の問題からどのように最適化しても基本的には僅かでも遅延は起こりえるという事を念頭にスクリプトを作成してください。
また、『敵の攻撃がヒットする直前に特定のボタンを押せば相手の攻撃をキャンセルor自身を無敵化させてダメージ無効化』のような非常に早いレスポンスが求められるものはPapyrusだけで実現するのはまず不可能です。
実際にやろうとすると「判定処理→判定成功→相手の攻撃キャンセルor無敵処理」の判定成功からの判定成功後処理を行う直前で攻撃を受けてしまい、その後に判定成功後の処理が実行されるという事態がよく起こります。
(特にスクリプトのスタックが発生しやすい大規模戦闘では判定処理成功→攻撃を受ける→1秒以上経過した後に判定成功処理が行われるという事態が起こります)
実際にやろうとすると「判定処理→判定成功→相手の攻撃キャンセルor無敵処理」の判定成功からの判定成功後処理を行う直前で攻撃を受けてしまい、その後に判定成功後の処理が実行されるという事態がよく起こります。
(特にスクリプトのスタックが発生しやすい大規模戦闘では判定処理成功→攻撃を受ける→1秒以上経過した後に判定成功処理が行われるという事態が起こります)
高速なレスポンスを要求されるModを作成したい場合はSKSEプラグインを作成するか、Skyrim Platformでスクリプトを作成してください。
戦闘が絡むスクリプトの動作確認について大規模戦闘下での確認は行う事
数人程度の敵と戦闘して問題ない事があっても内戦などの大規模戦闘ではスクリプト遅延の多発は容易に起こります。
このため、戦闘関連のスクリプトの動作確認は数人程度の敵と戦闘して問題ないか確認した後、大規模戦闘で動作に支障が無いかの確認をしましょう。
このため、戦闘関連のスクリプトの動作確認は数人程度の敵と戦闘して問題ないか確認した後、大規模戦闘で動作に支障が無いかの確認をしましょう。
負荷テスト確認を行う場合は事前に「ホワイトランの戦い」等の大規模戦闘前のセーブデータを用意しておくと確認しやすくなります。
また、Assault on ValenwoodというModは内戦クエスト以上の大規模戦闘かつ開始が非常に容易のためこちらを導入しての確認もオススメです。
また、Assault on ValenwoodというModは内戦クエスト以上の大規模戦闘かつ開始が非常に容易のためこちらを導入しての確認もオススメです。
ニューゲーム、途中導入での動作確認は怠らない
ニューゲームで開始時、ゲーム途中からのMod導入どちらでも動作に問題ないか念の為確認しましょう。
スタート地点変更Modがあると確認がしやすくなります。
スタート地点変更Modがあると確認がしやすくなります。
(SE版のみ)Papyrusに慣れたらSkyrim Platformでのスクリプト開発を検討する[非推奨]
※扱いこなせれば非常に強力だがドキュメントの可読性や安定性に難あり。
開発環境の構築に手間が掛かりますが、Skyrim Platform(SP)で開発を行うのも手です。
- SKSEプラグインのDLLがスクリプトを読み込んで実行するという形なのでPapyrusの数十倍処理が高速
- 特定の処理で実行しない限りはバックグラウンドでは実行せず、フォアグラウンドで実行されるため高速なレスポンスを求められる処理を作成可能
- Papyrusだと処理件数が非常に多くなると一部の処理を後回しにしてしまい、結果スタックが溜まって処理が遅れる(そしてそうゆう状況下だと高確率でまずスタックがどんどん貯まる可能性が高いため最悪フリーズという事態になる)がSkyrim Platformでは処理の後回しによる遅延はなく、仮に処理件数が膨大だった場合でも処理落ちという形で処理遅延の回避が可能となる(*1)
- ゲーム中にスクリプトを編集可能、編集内容の反映が可能でデバッグが非常に容易
当時はPapyrus Tweaks NGがまだリリースされておらず
どう足掻いても改善ができなかったPapyrusの処理速度がネックだったため
Papyrusでのスクリプト作成よりも利点がありました。
どう足掻いても改善ができなかったPapyrusの処理速度がネックだったため
Papyrusでのスクリプト作成よりも利点がありました。
Skyrim Platformのスクリプト関数はPapyrus関数を準拠にしておりPapyrusで実行できる関数のほとんどが実行可能(開発途中のため一部の関数で確定CTDするため注意)、
さらにSKSEプラグインのPapyrus関数拡張SKSEの関数も定義ファイルを作成すればSkyrim Platform上で実行可能とPapyrusでの経験も応用しやすいですが、
残念な事に現状だと環境によっては安定性に問題があり、前提Modとしての導入に関しても逃避する人もいるため自環境でのみの使用を推奨します。
さらにSKSEプラグインのPapyrus関数拡張SKSEの関数も定義ファイルを作成すればSkyrim Platform上で実行可能とPapyrusでの経験も応用しやすいですが、
残念な事に現状だと環境によっては安定性に問題があり、前提Modとしての導入に関しても逃避する人もいるため自環境でのみの使用を推奨します。
予約語self
予約語は役割が予め決まっており変数で使用できない語です。
selfはそのスクリプトをつけているオブジェクトそのものを指します。
例えばリディア(Actor)についているスクリプトの場合はselfが指すのはリディア(housecarlwhiterun)です。
わざわざプロパティ作ったり変数作ったりしなくていいのできれいにコードが書けます。
selfはそのスクリプトをつけているオブジェクトそのものを指します。
例えばリディア(Actor)についているスクリプトの場合はselfが指すのはリディア(housecarlwhiterun)です。
わざわざプロパティ作ったり変数作ったりしなくていいのできれいにコードが書けます。
self.GetDistance(player) ;リディアとプレイヤーとの距離を測ったり Debug.SendAnimationEvent(self,"attackStop") ;リディアに攻撃停止のモーションを送ったりできます
他にもActiveMagicEffectにつけたものでその魔法効果を消す場合は
self.dispel()
クエストにつけたものでそのクエストを停止させるには
self.stop()
このように幅広く使えます。
否定の "!"
スクリプト上で!をつけると~でないという否定の意味になります。
!Actor.IsSprinting() ;スプリント中でない、Actor.IsSprinting() == falseと同じ。
!(Actor.GetEquippedItemType(1) == 0) ; 右手の武器が素手ではない Actor.GetEquippedItemType(1) != 0と同じ
関数の処理について
関数の処理の仕方についておおまかに3つに分類します。
1.同期が必要なLatent Function
スクリプトは上から順に処理していきますが、関数の中でも処理が終わるまでスクリプトが止まるのがLatent Functionです。
代表的な例はWait()です。
Utilty.Wait(1.0)なら1秒経過するまでWaitの部分でスクリプトは待ってます。
次にCast関数です。
これも実際に画面上で魔法が放たれるまで待ってます。
しかし、ノーモーションで魔法が放たれるので、非常に処理が速いです。
スクリプトは上から順に処理していきますが、関数の中でも処理が終わるまでスクリプトが止まるのがLatent Functionです。
代表的な例はWait()です。
Utilty.Wait(1.0)なら1秒経過するまでWaitの部分でスクリプトは待ってます。
次にCast関数です。
これも実際に画面上で魔法が放たれるまで待ってます。
しかし、ノーモーションで魔法が放たれるので、非常に処理が速いです。
CK wiki内のリストには入ってませんがFind系の関数は値が返ってくるまで待ち、処理が遅いです。
2.非同期処理の関数
これは関数の実行が終わったかどうかは関係なく、処理の手続きをしたらさっさと次に進む関数です。
モーションを再生するPlayIdle()がそうです。
モーションが終わったかどうかは関係なく次に進みます。
SoundのPlay()なども同じく、処理の手続きすればすぐ次に行きます。
これと同じ機能で同期処理版がPlayAndWaitです。
スクリプトではなく実際の処理自体はフレームレートやPCの性能に左右されます。
手続された順に再生されるので画面上では遅延が起きるかもしれません。
※仮説上の話ですが、パピルスが言語として遅いのは画面と同期するために意図的に遅くしている可能性も。
これは関数の実行が終わったかどうかは関係なく、処理の手続きをしたらさっさと次に進む関数です。
モーションを再生するPlayIdle()がそうです。
モーションが終わったかどうかは関係なく次に進みます。
SoundのPlay()なども同じく、処理の手続きすればすぐ次に行きます。
これと同じ機能で同期処理版がPlayAndWaitです。
スクリプトではなく実際の処理自体はフレームレートやPCの性能に左右されます。
手続された順に再生されるので画面上では遅延が起きるかもしれません。
※仮説上の話ですが、パピルスが言語として遅いのは画面と同期するために意図的に遅くしている可能性も。
3.画面と同期する必要のないNon-delayed Native Function
画面で起こってることとは全く関係ない、MathやRegister系の関数などです。
これらの関数はフレームレートに左右されずに、常に高速で動きます。
画面で起こってることとは全く関係ない、MathやRegister系の関数などです。
これらの関数はフレームレートに左右されずに、常に高速で動きます。
3.以外は1.だから速いとか2.だから遅いというわけではなく、個々での関数で速度を勘定したほうがいいでしょう。
イベントの処理
同一のフォーム内のスクリプトではイベントのフラグは同時に受けとります。
たとえば、クエスト1に対してスクリプトA・スクリプトBをつけ、
たとえば、クエスト1に対してスクリプトA・スクリプトBをつけ、
;スクリプトA Event OnInit() RegisterForSingleUpdate(1) EndEvent Event OnUpdate() Debug.trace("Script A") EndEvent
;スクリプトB Event OnUpdate() Debug.trace("Script B") EndEvent
どっちのOnUpdateも動きます。
OnUpdateを別に動かしたい場合は、クエストを別にするか、Stateを使ってうまく振り分けましょう。
また、別のスクリプトが誤作動してしまうために、スクリプトをアクターにつけず、基本的に独立しているMagic Effect使います。
OnUpdateを別に動かしたい場合は、クエストを別にするか、Stateを使ってうまく振り分けましょう。
また、別のスクリプトが誤作動してしまうために、スクリプトをアクターにつけず、基本的に独立しているMagic Effect使います。
スクリプト最適化Tips
エラーの少なく、処理の早い書き方があります。
原則
イベントも関数も呼び出しが少ないほうがよい
イベントも関数も呼び出しが少ないほうがよい
なので重複処理を防止したり、繰り返しの処理をまとめたりが重要です。
重複処理をさせない→Stateを使う(スタックエラーの防止策)
敵から攻撃受けた時に両手武器の場合はスタミナに5ダメージという仕組みに加えて、
一度イベントが起きたら0.5秒間同じ処理をさせたくない場合です。
一度イベントが起きたら0.5秒間同じ処理をさせたくない場合です。
Event OnHit(ObjectReference akAggressor, Form akSource, Projectile akProjectile, bool abPowerAttack, bool abSneakAttack, bool abBashAttack, bool abHitBlocked) ;攻撃者がいない場合、ダメージソースが武器以外の場合は即処理中断 if akAggressor == None || !(akSource as Weapon) return endif ;BusyのStateに飛ばす GotoState("Busy") ;武器の種類を取得 int WeapType = (akSource as Weapon).GetWeaponType() ;両手剣または両手斧槌ならば if WeapType == 5 || WeapType == 6 Game.GetPlayer().DamageAV("Stamina",5.0) endif ;重複防止のために0.5秒待機 Utility.Wait(0.5) ;Stateを元の状態に戻す GotoState("") EndEvent State Busy ;StateがBusyの時はOnHitイベントが起こっても何も処理しない Event OnHit(ObjectReference akAggressor, Form akSource, Projectile akProjectile, bool abPowerAttack, bool abSneakAttack, bool abBashAttack, bool abHitBlocked) EndEvent EndState
Stateはその名のとおり状態を表してまして、指定したState中にイベントが起こった場合は、Stateに記述したイベントが優先されます。
例のスクリプトはGotoState()で"Busy"というStateに移動します。Stateが変わっても元のOnHitイベントは継続して処理が進みます。
この間にOnHitイベントが起こっても、すべてState Busyの方で処理されます。最後に空のStateにGotoStateで戻って、また通常のOnHitイベントが起きるようになります。
例のスクリプトはGotoState()で"Busy"というStateに移動します。Stateが変わっても元のOnHitイベントは継続して処理が進みます。
この間にOnHitイベントが起こっても、すべてState Busyの方で処理されます。最後に空のStateにGotoStateで戻って、また通常のOnHitイベントが起きるようになります。
スタックエラーは特定の条件下で、何回も処理が動いてしまうのが原因の一つなので、Stateを使って複数回処理するのを制限することで回避できます。
return文で処理を中断する
returnは本来、戻り値を返すためのものですが、実行されると、実行中のイベントや関数から強制的に中断します。
OnHitなどの頻繁に動くイベントの場合は、条件に合わない処理を事前にreturnで中断させるのは非常に有効です。
OnHitなどの頻繁に動くイベントの場合は、条件に合わない処理を事前にreturnで中断させるのは非常に有効です。
- ダメな例
Event SomeEvent() bool keepRunning = true If someCondition1() keepRunning = false ElseIf someCondition2() keepRunning = false EndIf If(!keepRunning) DoStuff() EndIf EndEvent
- 良い例
Event SomeEvent() If someCondition1() || someCondition2() return EndIf DoStuff() EndEvent
悪い例ではkeepRunningを代入したり、チェックしたりの分の処理してますが、
良い例では条件が合わない場合は即処理を中断します。
良い例では条件が合わない場合は即処理を中断します。
Stateとreturnを組み合わせる場合は、returnで中断されるとStateが戻ってこれなくなるので、
GotoState前に書くか、return前にGotoState("")で抜け出します。
GotoState前に書くか、return前にGotoState("")で抜け出します。
if return endif
GotoState("Busy") if GotoState("") return endif
None(エラーを少なくする方法)
スクリプトが対象のオブジェクトが見つけれない時にエラーになりますが、これがエラーの中ではもっとも多いかと思います。
None Object、 has no 3d
基本的にスクリプトエンジンはこのエラーを無視するので問題無いですが、エラーログ出すぎると重くなったり不安定になったりする可能性があるのと、ログが読みにくくなるのでその対処法です。
None Object、 has no 3d
基本的にスクリプトエンジンはこのエラーを無視するので問題無いですが、エラーログ出すぎると重くなったり不安定になったりする可能性があるのと、ログが読みにくくなるのでその対処法です。
オブジェクトがない状態をオブジェクトはNoneと返します。
また、不必要になったオブジェクトにはNoneを代入すると安全です。
また、不必要になったオブジェクトにはNoneを代入すると安全です。
if PlayerRef != None ; プレイヤーのリファレンスがないときは処理を行わない ..... endif
もしくは
if PlayerRef == None ; プレイヤーのリファレンスが取得できない時はリターンでイベントの強制終了。 return endif
Noneを代入して完全に消す
ObjectReferenceやFormのデータはDeleteを使っただけではスクリプト上では完全に消えてないので、
これを解放するにはNoneを代入する必要があります。
これを解放するにはNoneを代入する必要があります。
ObjectReference Box = PlayerRef.placeAtMe(FXEmptyActivator) ;透明オブジェクトを置く
Box.MoveTo(PlayerRef, 0, 50, 85) ;透明オブジェクト移動
Box.Delete() ;透明オブジェクトを削除
Box = None ;Deleteでゲーム上からは消えますがスクリプトでは残っているのでNone入れて、ないことにする。
Box.MoveTo(PlayerRef, 0, 50, 85) ;透明オブジェクト移動
Box.Delete() ;透明オブジェクトを削除
Box = None ;Deleteでゲーム上からは消えますがスクリプトでは残っているのでNone入れて、ないことにする。
同じアクセサ関数(Get~系)を複数回使用しないこと。
アクセサ関数はなにか取得する(Get~)関数です。Game.GetPlayer()だとか、GetTargetActor()ですね。
- ダメな例
Event SomeEvent() GetTargetActor().AddItem(coolItem, 1) GetTargetActor().AddSpell(coolSpell) GetTargetActor().Kill() GetTargetActor().Resurrect() EndEvent
なぜダメかといえば、毎行たびにGetTargetActor()の処理を行い、取得しているからです。
つまり例では4回処理してます。
はじめの一行でGetTargetActor()を取得して変数に代入し、あとはそれを当てはめたほうがコードの見通しもよく効率的です。
つまり例では4回処理してます。
はじめの一行でGetTargetActor()を取得して変数に代入し、あとはそれを当てはめたほうがコードの見通しもよく効率的です。
- よい例
Event SomeEvent() Actor selfActor = GetTargetActor() selfActor.AddItem(coolItem, 1) selfActor.AddSpell(coolSpell) selfActor.Kill() selfActor.Resurrect() EndEvent
例外としてはGetTargetActor()の使用が1回だけの場合にはselfActor等の不必要な変数を追加する必要はなく、以下のが効率的です。
GetTargetActor().AddItem(coolItem, 1)
変数はローカルで保持する
不必要な静的変数を設定しないことです。
- ダメな例
int onHitVariable ;OnHitイベントで使う変数 int onDeathVariable ;OnDeath event int bothEventsVariable ;両方のイベントで使う変数 Event OnHit(<parameters>) DoStuffWith(onHitVariable) DoStuffWith(bothEventsVariable) EndEvent Event OnDeath(<parameters>) DoStuffWith(onDeathVariable) DoStuffWith(bothEventsVariable) EndEvent
- 良い例
int bothEventsVariable ;両方のイベント間で使う変数は静的変数としてイベント外で定義しておく Event OnHit(<parameters>) int onHitVariable ;OnHitでしか使わない変数はOnHit内で定義 DoStuffWith(onHitVariable) DoStuffWith(bothEventsVariable) EndEvent Event OnDeath(<parameters>) int onDeathVariable ;OnDeathでしか使わない変数はOnDeath内で定義 DoStuffWith(onDeathVariable) DoStuffWith(bothEventsVariable) EndEvent
Is3Dloaded()
3Dデータとして読み込まれているかどうかの判定をする関数で、has no 3d~のエラー対策に使えます。
インベントリに回収しちゃって処理ができない場合や、ラグがあって3Dオブジェクトが設置される前にスクリプトが稼働した場合にhas no 3dのエラーがでます。
インベントリに回収しちゃって処理ができない場合や、ラグがあって3Dオブジェクトが設置される前にスクリプトが稼働した場合にhas no 3dのエラーがでます。
If self.Is3Dloaded() == True ;3Dデータが読まれているなら処理 Endif
ラグ防止の場合:3Dデータが読まれるまで待機
int i = 10 ;時間切れを10秒に設定 While self.Is3Dloaded() == False && i > 0 ;3Dデータが読み込まれるか時間切れまで待つ Utility.Wait(1.0) i -= 1 EndWhile
3Dデータが必ず読み込まれるという保証はないため、タイムアウトを設定するのは極めて重要です。
引数が多い関数やイベントほど重い
パピルスの仕様で、引数から変換するときの処理が重いのです。したがって引数が多いほど重いので
OnHitイベントやFind~()は重いです(OnHitは頻発するのとFindはそもそも探索が遅いのあります)。
別のイベントや関数で代替できるならそちらでしたほうが良い場合もあります。
OnHitイベントやFind~()は重いです(OnHitは頻発するのとFindはそもそも探索が遅いのあります)。
別のイベントや関数で代替できるならそちらでしたほうが良い場合もあります。
安易なスクリプト使用の代替回避をしない
スクリプト使わないパターンでよくあるのが魔法のアビリティのコンディションで代替するパターンでこれは極めて悪手です。
スペルのアビリティのコンディションは毎秒条件の判定があるのでスクリプトで毎秒ループしてるのと同じぐらい重いです。
スクリプトのループと違って、papyrusログにスタックエラーが出ないのでより悪質です。
素直にスクリプト使ってイベント駆動型にしたほうが断然軽く安定します。
スペルのアビリティのコンディションは毎秒条件の判定があるのでスクリプトで毎秒ループしてるのと同じぐらい重いです。
スクリプトのループと違って、papyrusログにスタックエラーが出ないのでより悪質です。
素直にスクリプト使ってイベント駆動型にしたほうが断然軽く安定します。
WhileとWaitによる処理及び処理待ちの多用は厳禁
以下のようにWhileとWaitを用いる処理について、Quest等の単一のスクリプトで行う場合ならまだしも、多数のアクターにアビリティを付与して以下のWhileとWait処理を行う場合は状況によってはスタックが溜まってしまい処理遅延発生の元になってしまいます。特に大規模戦闘等のアクターが大勢いる状況だと処理遅延の多発が起こり、最悪CTDが起きる可能性が高まるため注意が必要です。(例としてはEnhanced Blood Textures。アクターの死亡時で以下と同じような処理を行っている)
int i = 0 while i < 100 ; ループ処理 ~ 省略 ~ Utility.wait(1) i += 1 endWhile
多数のアクターでどうしてもこのような処理を使用する必要がある場合は、GlobalVariable等にWhile&Wait処理を行っているアクターの総数を記録しておき、処理を行える人数を制限するか、一定数存在したら処理の中断を行うようにする事。(演出系の場合はHasLOS等でプレイヤーが視認できない場合は処理を省略する等を行う)
;一例、GlobalVariableで処理人数を記録して処理可能人数を制限する if GlobalVariable.GetValueInt() < 5 GlobalVariable.Mod(1) int i = 0 while i < 100 ; ループ処理 ~ 省略 ~ Utility.wait(1) i += 1 endWhile GlobalVariable.Mod(-1) endif
スクリプト最適化実践編
装備時のイベント重複防止
OnObjectEquippedは何か装備したときに発動するイベントですが、
一つのアイテムにもかかわらず、6回以上(特にエンチャント武器)呼び出されたりするうえ、対処が厄介です。
例では武器装備時に処理したい場合です。
一つのアイテムにもかかわらず、6回以上(特にエンチャント武器)呼び出されたりするうえ、対処が厄介です。
例では武器装備時に処理したい場合です。
Form PreObj = None Event OnObjectEquipped(Form akBaseObject, ObjectReference akReference) ; ベースオブジェクトが取得できない、エンチャント、装備が前と同じ場合は何もしない if akBaseObject == None || akBaseObject as Enchantment || akBaseObject == PreObj return endif ; OnObjectEquippedイベントを受け取らないようにする GotoState("Busy") ; 装備を記憶する PreObj = akBaseObject ; やりたいことをここに書く ; 装備の記憶を解除する PreObj = None ; OnObjectEquippedイベントを受け取るようにする GotoState("") EndEvent State Busy Event OnObjectEquipped(Form akBaseObject, ObjectReference akReference) EndEvent EndState
解説
aksourceをas enchantmentでキャスト(変換)すると、エンチャントかどうかの判定になる。
エンチャントが武器より先に処理されてしまって肝心の武器のほうがBusyで除外されちゃうので、エンチャントは最初にリターンで中断。
PreObjに前回のオブジェクトを代入しておいて、前回と同じだったらリターンで中断。ほぼ同タイミングぐらいに処理されるのでBusyだけだと間に合わずにこれが必要。
aksourceをas enchantmentでキャスト(変換)すると、エンチャントかどうかの判定になる。
エンチャントが武器より先に処理されてしまって肝心の武器のほうがBusyで除外されちゃうので、エンチャントは最初にリターンで中断。
PreObjに前回のオブジェクトを代入しておいて、前回と同じだったらリターンで中断。ほぼ同タイミングぐらいに処理されるのでBusyだけだと間に合わずにこれが必要。
OnHitの重複防止
OnHitはエンチャントなどで延焼させている場合に、延焼ダメージが攻撃した武器ダメージと同じ換算してしまう仕様なので、けっこう重複しやすいのです。
事前にソース元を代入して被ったら飛ばす仕組み。
事前にソース元を代入して被ったら飛ばす仕組み。
Form PreSource = None Event OnHit(ObjectReference akAggressor, Form akSource, Projectile akProjectile, bool abPowerAttack, bool abSneakAttack, bool abBashAttack, bool abHitBlocked) ;攻撃者がいない場合のエラー防止とダメージソースが武器以外はリターンで即処理中断 if akAggressor == None || !(akSource as Weapon) || akSource == PreSource return endif ;BusyのStateに飛ばす GotoState("Busy") ;PreSourceに今のダメージ元を代入 PreSource = akSource ;武器の種類を取得 int WeapType = (akSource as Weapon).GetWeaponType() ;両手剣または両手斧槌ならば if WeapType == 5 || WeapType == 6 Game.GetPlayer().DamageAV("Stamina",5.0) endif ;重複防止のため0.5秒待機 Utility.Wait(0.5) ;PreSourceをなしの状態に戻す PreSource = None ;Stateを元の状態に戻す GotoState("") EndEvent State Busy ;StateがBusyの時はOnHitイベントが起こっても何も処理しない Event OnHit(ObjectReference akAggressor, Form akSource, Projectile akProjectile, bool abPowerAttack, bool abSneakAttack, bool abBashAttack, bool abHitBlocked) EndEvent EndState
スクリプトログをとる(デバッグの仕方)
スクリプトログの設定
マイドキュメント\My Games\Skyrim\Skyrim.ini
を開いて、以下の通りにしたあと保存。(項目がなければ追加)
を開いて、以下の通りにしたあと保存。(項目がなければ追加)
[Papyrus] bEnableLogging=1 bEnableTrace=1 bLoadDebugInformation=1
すると次回から
マイドキュメント\My Games\Skyrim\Logs\Script
にPapyrus.0.logというのが出ますのでメモ帳以外のテキストエディタで開くとデバッグの内容が見れます。
マイドキュメント\My Games\Skyrim\Logs\Script
にPapyrus.0.logというのが出ますのでメモ帳以外のテキストエディタで開くとデバッグの内容が見れます。
LogExpertを導入
リアルタイムでログが取れるツールです。
- LogExpertをダウンロードします。
- LogExpert.exeを起動します。
- File→Openから、マイドキュメント\My Games\Skyrim\Logs\Script\Papyrus.0.logを開きます。
- Options→Always on Topを押して、常に手前に表示にします。
- Optionの下あたりにあるメニューのFollow Tailsにチェックを入れます。
スクリプトにデバッグ情報の記載
ログに書き出すにはDebug.Trace("文字")を使います。""のあとに+を使うと変数や関数の結果を書き出せます。
例:
Event OnObjectEquipped(Form akBaseObject, ObjectReference akReference) Debug.Trace("Debug:FormName" + akBaseObject.GetName() ) EndEvent
これでゲームとLogExpertを起動して、装備を付けたりすることでスクリプトの動作をリアルタイムで確認できます。
意図する結果になるまで以下の手順で実験してみてください。
- スクリプトを書き直してコンパイル
- コンソールコマンドで reloadscript script名
OnAnimationEventで取得できるイベント
RegisterForAnimationEventで登録したアニメーションイベント(モーション)をOnAnimationEventで取得することができますが、
取得できるのとできないのがあります。
☓staggerStart
○staggerStop
Start系のモーションは軒並みダメです。
Skyrim - Animation.bsaの中のmeshes\responses\actorresponse.txt
というテキストファイルに記載されてるのが使用可能なアニメーションイベントです。
取得できるのとできないのがあります。
☓staggerStart
○staggerStop
Start系のモーションは軒並みダメです。
Skyrim - Animation.bsaの中のmeshes\responses\actorresponse.txt
というテキストファイルに記載されてるのが使用可能なアニメーションイベントです。
ブロックの動作はじめにイベントを受け取りたい場合にBlockStartだとダメですが、裏ワザ的なやり方があります。
SoundPlay.NPCHumanCombatShieldBlockです。
SoundPlay系は受け取れるので開始時にイベント取得したいなという時にSoundPlayのAnimeEventをあたってみるといいと思います。
Gameplay -> Animations.. -> AnimEventの選択項目でSoundPlayを探して手当たり次第試してみましょう。
SoundPlay.NPCHumanCombatShieldBlockです。
SoundPlay系は受け取れるので開始時にイベント取得したいなという時にSoundPlayのAnimeEventをあたってみるといいと思います。
Gameplay -> Animations.. -> AnimEventの選択項目でSoundPlayを探して手当たり次第試してみましょう。
- 使えそうなイベント一覧
MRh_SpellFire_Event | 右手で魔法を放ったとき |
MLh_SpellFire_Event | 左手で魔法を放ったとき |
arrowRelease | 矢を放ったとき |
BowDrawn | 最大限弓を引いたとき |
weaponSwing | 右手の武器を振ったとき |
weaponLeftSwing | 右手の武器を振ったとき |
preHitFrame | 近接攻撃がヒットする直前 |
HitFrame | 近接攻撃がヒットしたとき。当たらない場合も検出する |
BashExit | バッシュしたとき |
BashStop | バッシュしたとき |
BashRelease | バッシュボタンを押し続けてバッシュが発動したとき。パワーバッシュは盾装備時のみ検出する |
RemoveCharacterControllerFromWorld | ラグドール状態になった時 |
KillMoveStart | キルムーブが発生した時。実行者及び被害者両方で検出されるため注意 |
Decapitate | キルムーブ等で首を刎ねられた時 |
- 参考になりそうなサイト
Creationkit.com:プレイヤー動作時のAnimEventが動く順番
Withe01さんのMOD作成日誌:20130930-対人用KillMove一覧
Withe01さんのMOD作成日誌:AnimationEvent
Withe01さんのMOD作成日誌:20130930-対人用KillMove一覧
Withe01さんのMOD作成日誌:AnimationEvent
Withe01さんブログは主に動作をさせる(イベントを起こす)ときのことが書いてある。
AnimationVariableの使い方
スカイリムのモーションを司るHavok Behaviorが扱うアニメーション変数(AnimationVariable)を取得したり変更したりできます。
これを使うことによってActorに関して細かく状態を判定したり、制御したりできます。
この変数はActorの種類によって異なります。
例えばCharacter(人)だとIsStaggeringはありますが、Dragonにはありません。
どんなアニメーション変数が何があるのかは以下のスプレッドシートのデータを確認してください。
人のアニメーション変数とイベントデータ
人以外のアニメーション変数とイベントデータ
これを使うことによってActorに関して細かく状態を判定したり、制御したりできます。
この変数はActorの種類によって異なります。
例えばCharacter(人)だとIsStaggeringはありますが、Dragonにはありません。
どんなアニメーション変数が何があるのかは以下のスプレッドシートのデータを確認してください。
人のアニメーション変数とイベントデータ
人以外のアニメーション変数とイベントデータ
そのほかは、Josh Behavior file patcherで確認ができます。
人ならbehaviorフォルダの0_master.hkx、ドラゴンならdragonbehavior.hkx。
人ならbehaviorフォルダの0_master.hkx、ドラゴンならdragonbehavior.hkx。
これらのアニメーション関数はConditionでも使えます。
Condtionでの関数名はGetGraphVariableFloatとGetGraphVariableIntです。
例:GetGraphVariableInt "IsStaggering" == 1 ;BoolはIntで代用。1はtrue 0はfalse
Condtionでの関数名はGetGraphVariableFloatとGetGraphVariableIntです。
例:GetGraphVariableInt "IsStaggering" == 1 ;BoolはIntで代用。1はtrue 0はfalse
さまざまな状態の判定
Actor.GetAnimationVariableBool("xxx")
判定 | xxxに記載する文字列 |
攻撃中 | IsAttacking |
ブロック中 | IsBlocking |
バッシュ中 | IsBashing |
はじかれ中 | IsRecoiling |
よろめき中 | IsStaggering |
抜刀中 | IsEquipping |
納刀中 | IsUnequipping |
ジャンプ中 | bInJumpState |
ブロック成功 | IsBlockHit |
例:Actor.GetAnimationVariableBool("IsAttacking") ;攻撃中、パワーアタックも含まれる
一人称視点かどうかの判定
player.GetAnimationVariableInt("i1stPerson") == 1
移動方向の判定
floatのDirectionを使います。
floatのDirectionを使います。
Actor.GetAnimationVariableFloat("Direction") == 0 ; forward
時計回りに0から1まで。
0 | 前と立ち状態 |
0.125 | 右斜め前 |
0.25 | 右 |
0.375 | 右斜め後 |
0.5 | 後ろ |
0.625 | 左斜め後 |
0.75 | 左 |
0.875 | 左斜め前 |
コントローラーのアナログスティックはSKSEやScriptDragon(※ScriptDragonはAnimationVaribleが使えません)でも検知できないので、このDirectionを使います。
立ち状態と前とで区別つけるときは下の移動中判定と組み合わせてください。
立ち状態と前とで区別つけるときは下の移動中判定と組み合わせてください。
移動中かどうかの判定
一見するとbInMoveStateなんですが、壮大なトラップで片手どちらかに魔法か杖を持ってると移動中でもFalseを返します。
代わりにSpeedを使います。
一見するとbInMoveStateなんですが、壮大なトラップで片手どちらかに魔法か杖を持ってると移動中でもFalseを返します。
代わりにSpeedを使います。
if Actor.GetAnimationVariableFloat("Speed") < 5.0 ;停止中
iStateの変数
GetAnimationVariableInt("iState")で取得できる値はMovement Typeと連動していると思われます。
何ができるかというと、状態判定ができます。
例:パピルスにはIsBlockingがないので、以下のようにします。(上のIsBlockingを使ったほうが確実)
例:パピルスにはIsBlockingがないので、以下のようにします。(上のIsBlockingを使ったほうが確実)
if (Actor.GetAnimationVariableInt("iState") == 4) || (Actor.GetAnimationVariableInt("iState") == 17)
変数の数値が何を意味するかは以下の通り。
スプリント中 | 1 | iState_NPCSprinting |
スニーク移動中 | 2 | iState_NPCSneaking |
弓・クロスボウ構え中、リロード中 | 3 | iState_NPCBowDrawn |
ブロック中 | 4 | iState_NPCBlocking |
ダウン中 | 5 | iState_NPCBleedout |
片手・素手移動中 | 6 | iState_NPC1HM |
両手移動中 | 7 | iState_NPC2HM |
弓・クロスボウ移動中 | 8 | iState_NPCBow |
魔法移動中・停止中 | 9 | iState_NPCMagic |
魔法・杖キャスト中 | 10 | iState_NPCMagicCasting |
騎乗時? | 11 | iState_NPCHorse |
片手・素手攻撃中 | 12 | iState_NPCAttacking |
両手攻撃中 | 13 | iState_NPCAttacking2H |
パワーアタック中 | 14 | iState_NPCPowerAttacking |
酩酊中? | 15 | iState_NPCDrunk |
弓・クロスボウ構え中(QuickShot習得後) | 16 | iState_NPCBowDrawnQuickShot |
ブロックランナー取得後ブロック中 | 17 | iState_NPCBlockingShieldCharge |
騎乗時移動中 | 60 | iState_HorseDefault |
騎乗時スプリント中 | 61 | iState_HorseSprint |
騎乗時ジャンプ中 | 62 | iState_HorseFall |
騎乗時水泳中 | 63 | iState_HorseSwim |
これはBehaviorファイルのBSiStateTaggingGeneratorという項目で指定してます。
iStateToSetAsで数値指定で、iPriorityが優先度です。
一部プレイヤーとNPCで違う模様。アクターによっても違います。
iStateToSetAsで数値指定で、iPriorityが優先度です。
一部プレイヤーとNPCで違う模様。アクターによっても違います。
セーブデータに残るもの
セーブするとセーブデータに保存されるものがあります。
- グローバル変数
- 静的変数
- プロパティ
一旦セーブされたデータでMODを更新したときにプロパティや静的変数などを削除した場合やMODを抜いた場合はセーブ内と一致しないのでログにエラーを吐きます。
エラーを吐くのですが、データがないと無視するので基本的に害はありません。
エラーを吐くのですが、データがないと無視するので基本的に害はありません。
不必要になったプロパティの削除
プロパティはesp側にも紐ついてるので、そちらも消す必要があります。
- スクリプトのついてるPropertyボタンを押してプロパティウィンドウを出します。
- 不要なプロパティのClear Valueを押して消してください。
- そのあと、スクリプト側のプロパティの記述を消します。
- espを保存します。
SM Eventを使ったRepeatQuest
OnUpdateを使わずにループができるクエストの作り方です。
セル移動時関係のイベント
セル移動時にスクリプトを動かしたいときにいくつかイベントがあるんですが、どれも癖があってそれを記したいと思います。
Onload
3Dオブジェクトがロードされるときに発生するイベントで、ロード画面が挟むセル移動でなら起きます。
ただしセルを移動してすぐ戻る場合はセル移動時にロード挟まないのでその場合は発生しません。
プレイヤーは稼働しません。
3Dオブジェクトがロードされるときに発生するイベントで、ロード画面が挟むセル移動でなら起きます。
ただしセルを移動してすぐ戻る場合はセル移動時にロード挟まないのでその場合は発生しません。
プレイヤーは稼働しません。
OnAttachedToCell
セルからセルに移動するときに発生するイベントです。
たとえばワールドTamrielのWildness1からWildness2に移動するときにも発生します。
ただしプレイヤーは稼働しません。
またスカイリムからホワイトランに入るときには動きません。(逆は動くので条件不明)
セルからセルに移動するときに発生するイベントです。
たとえばワールドTamrielのWildness1からWildness2に移動するときにも発生します。
ただしプレイヤーは稼働しません。
またスカイリムからホワイトランに入るときには動きません。(逆は動くので条件不明)
OnLocationChange
ロケーションの移動時に動くイベントですが、複数のセルをまとめて一つのロケーションとして扱う場合があって、例えばホワイトラン→スカイリム、スカイリム→ホワイトランは動きません。
ですので一般的にセル移動時判定には向いてません。
ロケーションの移動時に動くイベントですが、複数のセルをまとめて一つのロケーションとして扱う場合があって、例えばホワイトラン→スカイリム、スカイリム→ホワイトランは動きません。
ですので一般的にセル移動時判定には向いてません。
OnCellLoad
セルロード時にイベントが発生しますが、メモリキャッシュ済みのセルの場合は発生しません。
続けてゲームプレイする場合に一度入ったセルにもう一度入った場合に発生しない可能性が高いです。
プレイヤー(エイリアス)に使えます。
セルロード時にイベントが発生しますが、メモリキャッシュ済みのセルの場合は発生しません。
続けてゲームプレイする場合に一度入ったセルにもう一度入った場合に発生しない可能性が高いです。
プレイヤー(エイリアス)に使えます。
NPCであれば、Onloadをおすすめします。
プレイヤーは厳密さを要求しないのであれば、OnCellLoadで大抵何とかなります。
プレイヤーは厳密さを要求しないのであれば、OnCellLoadで大抵何とかなります。
NPCへのPerk付与について
オススメはSPIDを使ったゲーム起動時のPerk付与です。
スクリプトのAddPerk関数
プレイヤーに対しては正常に機能しますが、NPCに対しては全く機能しません。
プレイヤーに対しては正常に機能しますが、NPCに対しては全く機能しません。
コンソールのAddPerkコマンド
スクリプトのAddPerk関数と同様、プレイヤーに対してのみ正常に機能します。
スクリプトのAddPerk関数と同様、プレイヤーに対してのみ正常に機能します。
マジックエフェクトの"Perk to Apply"
スクリプトのAddPerk関数と同様、プレイヤーに対してのみ正常に機能します。
スクリプトのAddPerk関数と同様、プレイヤーに対してのみ正常に機能します。
CKやxEditにて事前にPerkを持たせる
プラグインを作成して事前にPerkを持たせておけば確実ですが、レコードの競合等により不具合が起こる危険性があります。
プラグインを作成して事前にPerkを持たせておけば確実ですが、レコードの競合等により不具合が起こる危険性があります。
Spell Perk Item Distributor (SPID) にてゲーム起動時にPerkを持たせる
NPCに事前にPerkを持たせておくことができるというSKSEプラグイン(LE/SE)です。ゲーム起動時にあたかもはじめから所持していたかのように処理されるため、レコードが競合する心配がありません。
NPCに事前にPerkを持たせておくことができるというSKSEプラグイン(LE/SE)です。ゲーム起動時にあたかもはじめから所持していたかのように処理されるため、レコードが競合する心配がありません。
powerofthree's Papyrus ExtenderのAddBasePerk関数
スクリプトでNPCへPerkの付与が可能になるSKSEプラグイン(LE / SE)です。いまのところゲーム中でPerkを付与する唯一の方法となります。
スクリプトでNPCへPerkの付与が可能になるSKSEプラグイン(LE / SE)です。いまのところゲーム中でPerkを付与する唯一の方法となります。
パークを付与する
; targetActorに対象のActor、addPerkに付与させたいPerkを渡す po3_sksefunctions.AddBasePerk(targetActor, addPerk)
付与したパークを消す
po3_sksefunctions.RemoveBasePerk(targetActor, addPerk)
ただし、powerofthree's Papyrus Extenderのパーク付与は以下の問題点もあるため、使用する場合は付与状況の監視が必要となります。
※現状だと問題点あり
- NPCにパーク付与後に『対象NPCがパーク付与前かつセルにロードされている』セーブデータをロードするとセーブ時点でパーク未付与前のNPCにパークが付与される
- NPCにパーク付与後にNPCがセルからアンロードされた状態で保存した後にスカイリムを終了させ、再度起動してタイトルからロードした場合、ロード後のパーク付与がNPCに適用されない(正確にはセーブ時にAddBasePerkで何を追加されたかを保存するのだが、対象のNPCがアンロードされてると保存されない)
同一のアクターに二回以上AddBasePerkを行うと最初にAddBasePerkで付与したパークが再付与され二重に適用される(disable→enableで正常に戻る)→ 4.5.2で修正
Perkの挙動について
追加Perk系のModを制作する時には特に下記の点に注意する事。
特定行動時にスペル付与系
Apply Combat Hit Spell(命中時にスペル効果付与)やApply Weapon Swing Spell(攻撃を行って振りかぶった時にスペル効果付与)、Apply Reanimate Spell(蘇生時にスペル効果付与)など、
特定行動時にスペル効果を付与するパークについて条件を満たしたものが複数ある場合、
Priorityが0に近いパークのスペル効果のみが付与されます。(条件を満たしたパークのPriorityが同じ場合はFormIDが低いものが優先)
特定行動時にスペル効果を付与するパークについて条件を満たしたものが複数ある場合、
Priorityが0に近いパークのスペル効果のみが付与されます。(条件を満たしたパークのPriorityが同じ場合はFormIDが低いものが優先)
例として両手武器のウォーマスター(後ろパワーアタックで麻痺効果付与)はPriorityが0、出血攻撃(両手斧攻撃で出血状態付与)はPriorityは3~10が設定されており、
この場合、両手斧で後ろパワーアタックを行った場合はウォーマスターの効果が優先され出血攻撃の効果は出ません。
この場合、両手斧で後ろパワーアタックを行った場合はウォーマスターの効果が優先され出血攻撃の効果は出ません。
この制限のせいでMod制作面で最も弊害を被っているのがApply Reanimate Spell(蘇生時にスペル効果付与)で
召喚パークのダークソウル(蘇生したアンデットにHP+100ボーナス)がよりにもよってPriorityが0かつ無条件となっているため
例えば蘇生したアンデットに特殊効果を与えるというパークを作ってそれをプレイヤーに持たせたとしても、
召喚パークのダークソウルを所持した時点で新たに作成した蘇生したアンデットに特殊効果を与えるというパークは一切機能しなくなります。
召喚パークのダークソウル(蘇生したアンデットにHP+100ボーナス)がよりにもよってPriorityが0かつ無条件となっているため
例えば蘇生したアンデットに特殊効果を与えるというパークを作ってそれをプレイヤーに持たせたとしても、
召喚パークのダークソウルを所持した時点で新たに作成した蘇生したアンデットに特殊効果を与えるというパークは一切機能しなくなります。
この制限についてはScrambled BugsというSKSEプラグインを導入して、ScrambledBugs.jsonのmultipleSpellsをtrueにする事で解除は可能です。
(制限解除時だと上の例にある両手斧で後ろパワーアタックで麻痺効果と出血攻撃の両方の効果が付与される、蘇生したアンデットに特殊効果を与えるというパークとダークソウルのパークの併用が可能となる)
これにより従来では攻撃時やヒット時、あるいは死霊術での蘇生で魔法効果を付与といったものを作成したい場合は、
バニラのパークを含めて上手く動作しなくなる可能性が高かったため、スクリプト処理でスペル効果を与える方法を取らざるを得ませんでしたが
現在ではScrambled Bugsを前提Modとする事でパーク効果経由でのスペル効果の付与が安全に行えます。
(例としてKykvendi - Dangerous Creatures - Engaging Combat等でScrambled Bugsを前提Modにしています)
(制限解除時だと上の例にある両手斧で後ろパワーアタックで麻痺効果と出血攻撃の両方の効果が付与される、蘇生したアンデットに特殊効果を与えるというパークとダークソウルのパークの併用が可能となる)
これにより従来では攻撃時やヒット時、あるいは死霊術での蘇生で魔法効果を付与といったものを作成したい場合は、
バニラのパークを含めて上手く動作しなくなる可能性が高かったため、スクリプト処理でスペル効果を与える方法を取らざるを得ませんでしたが
現在ではScrambled Bugsを前提Modとする事でパーク効果経由でのスペル効果の付与が安全に行えます。
(例としてKykvendi - Dangerous Creatures - Engaging Combat等でScrambled Bugsを前提Modにしています)
Add ValueやSet Value等の数値変動系の計算の優先度
バニラのクリティカル率や同時付呪数を変更するパークを確認するとEntry PointのFunctionがSet Value(絶対値)となっています。もし、クリティカル率が増減するパークを作成したい時に下記のようにAとBのパーク両方を保持した際に絶対値の後に計算されるのか疑問に思った人はいるはずです。
A. クリティカル率(Calculate My Critical Hit Chance)の値をSet Valueで設定 B. クリティカル率の値をAdd ValueやMultiply Valueで変動
結論としてAとBのパークについてどちらも保持した場合、Priorityの数値が高いパークから先に適応され計算されます。
Priorityが同値の場合はFormIDが大きい方から先に適応されます。
Priorityが同値の場合はFormIDが大きい方から先に適応されます。
例:
※AのSet Valueが25、BはAdd Valueで50を設定していた場合 AのPriorityが0で、BのPriorityが1の場合:Bの数値設定でどれほど値を変動させてもAの数値となる(25) AのPriorityが1で、BのPriorityが0の場合:Aの数値に対してBの設定値で変動したものが最終的な値(75) AとBのPriorityが同値:FormIDが大きいものから先に適応
そして、クリティカル率が変動するパーク(片手武器の剣士等)、追加付呪のパークはSet Valueで値を固定されていてなおかつPriorityが0となっています。このため、クリティカル率の増減、または同時付呪数の増減パークをModで作成してもバニラのパークを所有した瞬間、Mod追加パークの効果が正常に反映されなくなってしまいます。
解決策として、追加付呪等の一部を除いたパークは非公式パッチ(SEのUSSEPで確認)でAdd Valueに修正されているため、非公式パッチを前提Modとして作成する必要があります。
非公式パッチを前提Modにしない場合は修正用espを別途作成し、「剣士(片手武器パーク)、深手(両手武器パーク)、クリティカルショット(弓パーク)、追加付呪(付呪パーク)」のEntry PointのFunctionをAdd Valueに修正する必要があります。
非公式パッチを前提Modにしない場合は修正用espを別途作成し、「剣士(片手武器パーク)、深手(両手武器パーク)、クリティカルショット(弓パーク)、追加付呪(付呪パーク)」のEntry PointのFunctionをAdd Valueに修正する必要があります。
IsInKillMove関数の注意点
IsInKillMoveはアクターがキルムーブを実行中、またはキルムーブを受けてる最中の場合にもtrueを返すため、キルムーブの実行者か受けてる側かの判定はOnHitイベント(キルムーブの一撃でもHitイベントは発生する)やOnDyingイベント(キルムーブを受けてる最中に発生)を併用する必要があります。
スクリプトによる一部のフォームの値変更について
SpellやArmor、Weapon等のベースフォームについてSet~関数等で値を変更しても一時的な値変更と扱われるためセーブデータに保存されません。(ActorBaseの関数もSetEssential等はセーブデータに保存されるが一部は一時的な変更になるものも存在する)
また、一時的な値変更となるものに関してはゲームを再起動するまではリセットされないため、値変更後に変更前のセーブデータをロードしても値は変更後のままになります。
また、一時的な値変更となるものに関してはゲームを再起動するまではリセットされないため、値変更後に変更前のセーブデータをロードしても値は変更後のままになります。
ConsoleUtilによるターゲットコマンド実行の注意点及び安全な実行について
負荷が掛かると誤実行される危険性がある
ConsoleUtilでターゲットコマンド(SetAVやSetLevel等)を実行する場合、SetSelectedReference関数を使って対象の選択をすると思いますがSetSelectedReference関数にはコマンドの実行まで対象の切り替えを抑止する機能はありません。
ConsoleUtilを使ってるModが一つだけなら問題ないのですが複数のModがConsoleUtilでターゲットコマンドを使ってる場合は注意が必要です。
もしConsoleUtilを使ってるスクリプトの処理が重なるような事が起こった時にコマンド実行直前で別のスクリプトのSetSelectedReference関数で対象が変更される可能性があり指定していない対象に対してターゲットコマンドの誤実行が行われる危険性があります。(特にスクリプト遅延が起こってる状態では高確率で発生します)
ConsoleUtilを使ってるModが一つだけなら問題ないのですが複数のModがConsoleUtilでターゲットコマンドを使ってる場合は注意が必要です。
もしConsoleUtilを使ってるスクリプトの処理が重なるような事が起こった時にコマンド実行直前で別のスクリプトのSetSelectedReference関数で対象が変更される可能性があり指定していない対象に対してターゲットコマンドの誤実行が行われる危険性があります。(特にスクリプト遅延が起こってる状態では高確率で発生します)
対策
これに対する回避策としてターゲットコマンドを行う場合、下記のサンプルのようにコマンド文を "[ID]".[コマンド]にする事でコンソールの選択対象に関係無く[ID]の対象に対してコマンドを実行されます。
こうする事により指定した対象以外への誤実行を防ぐ事ができます。
こうする事により指定した対象以外への誤実行を防ぐ事ができます。
- ; ターゲットコマンドの安全実行
- ; SetSelectedReference→ExecuteCommandの順で実行する場合、複数のスクリプトがConsoleUtilを使ってると、
- ; コマンドが指定していない対象に対して誤実行される危険性があるため
- ; コマンド文を"[ID]".[コマンド]に変換し、コマンド実行を指定した対象に対して確実に行うようにする
- ; 数値の16進数文字変換はpowerofthree`s Papyrus ExtenderのSKSE関数を使用
- function SafeTargetCommandExecute(string cmd, Form target)
- if target
- ; IDをダブルクォートで囲まないとIDに英文字が混ざってない場合にエラーが起こる
- cmd = "\"" + PO3_SKSEFunctions.IntToString(target.getFormID(), true) + "\"." + cmd
- else
- Debug.trace("Console Command Target is None:" + cmd, 2)
- return
- endif
- ConsoleUtil.ExecuteCommand(cmd)
- endfunction
-
アビリティ付与に関する注意
SPIDやスクリプト等でNPCに何かしらのアビリティを直接持たせた場合、NPCをテレポートしたり内部セルへ移動する等何かしらの要因で偶にアビリティに付属してる魔法効果が消滅するという現象が発生します。
アビリティに付属した魔法効果が消えた場合はアビリティの再付与を行わない限りは魔法効果が復活しません。
アビリティに付属した魔法効果が消えた場合はアビリティの再付与を行わない限りは魔法効果が復活しません。
この現象はパーク効果にアビリティが付属してるものをNPCに持たせた場合は発生しません。アビリティをNPCに直接持たせた場合にのみに発生します。
(パークに付属したアビリティはゲームエンジンレベルで魔法効果が機能してるかチェックしてる?)
そのためスクリプト付きアビリティをプレイヤーやNPCに配布するModを作成する場合、確実にアビリティを持たせて機能させたければダミーパークにアビリティを付属させてそのパークを付与させるようにしましょう。
(パークに付属したアビリティはゲームエンジンレベルで魔法効果が機能してるかチェックしてる?)
そのためスクリプト付きアビリティをプレイヤーやNPCに配布するModを作成する場合、確実にアビリティを持たせて機能させたければダミーパークにアビリティを付属させてそのパークを付与させるようにしましょう。
Activate関係の処理について
BlockActivation関数でアクティベートをブロックしてもOnActivateイベントは発生する
アクター等に対してBlockActivation関数を実行した場合、
対象をアクティベートしても見た目上は反応が返ってきませんが、この時OnActivateイベントはしっかり発生します。
これはスクリプトのActivate関数で第ニ引数にfalseを渡してアクティベートがブロックされた際も同様です。
対象をアクティベートしても見た目上は反応が返ってきませんが、この時OnActivateイベントはしっかり発生します。
これはスクリプトのActivate関数で第ニ引数にfalseを渡してアクティベートがブロックされた際も同様です。
パークのアクティベート処理変更についての注意点
パークのEntry PointでActivateを設定してReplace Defaultにチェックを付けると
対象のアクティベート時に本来の動作の代わりにパークに付属したスクリプトの処理にすげ替える事ができます。
しかし、このアクティベートの動作変更を使用する場合は下記の点を考慮する必要があり、
特にOnActivateイベントが発生しない件とBlockActivation関数でアクティベートのブロックをしても処理が実行される件については
ちゃんと対処しておかないとOnActivateイベントで処理が行われるModと一緒に使用した場合に問題が発生する可能性があります。
(この2つの現象はEFFで確認が可能です)
対象のアクティベート時に本来の動作の代わりにパークに付属したスクリプトの処理にすげ替える事ができます。
しかし、このアクティベートの動作変更を使用する場合は下記の点を考慮する必要があり、
特にOnActivateイベントが発生しない件とBlockActivation関数でアクティベートのブロックをしても処理が実行される件については
ちゃんと対処しておかないとOnActivateイベントで処理が行われるModと一緒に使用した場合に問題が発生する可能性があります。
(この2つの現象はEFFで確認が可能です)
OnActivateイベントが発生しない
本来ならばアクティベートした瞬間にOnActivateイベントが発生するのですが
パークによるアクティベート処理をスクリプト処理に挿げ替えてる場合はOnActivateイベントが発生しません。
パークによるアクティベート処理をスクリプト処理に挿げ替えてる場合はOnActivateイベントが発生しません。
アクティベート処理変更を行いつつOnActivateイベントを発生させたい場合は
スクリプト処理内で対象に対してActivate関数を使用する事でOnActivateイベントを発生させられます。
この場合、OnActivateイベント発生と同時に通常のアクティベート動作が行われます。(相手がNPCの場合、会話orスリを行う)
スクリプト処理内で対象に対してActivate関数を使用する事でOnActivateイベントを発生させられます。
この場合、OnActivateイベント発生と同時に通常のアクティベート動作が行われます。(相手がNPCの場合、会話orスリを行う)
スクリプト処理だけ実行した後に通常のアクティベート処理を行わずにOnActivateイベントを発生させたい場合は
blockActivation関数でアクティベートをブロック後にActivate関数(第二引数はfalse)を実行し
再度blockActivation関数でブロック解除を行う必要があります。
blockActivation関数でアクティベートをブロック後にActivate関数(第二引数はfalse)を実行し
再度blockActivation関数でブロック解除を行う必要があります。
スクリプト処理後に通常のアクティベート処理を実行をする場合はblockActivation関数は使用せずに
スクリプトの最後にActivate関数を実行すればよいだけです。
なお、Activate関数の第ニ引数をtrueを渡した場合はOnActivateイベントは発生しないので注意しましょう。
スクリプトの最後にActivate関数を実行すればよいだけです。
なお、Activate関数の第ニ引数をtrueを渡した場合はOnActivateイベントは発生しないので注意しましょう。
BlockActivation関数でアクティベートのブロックをしても処理が実行される
BlockActivation関数によるアクティベートのブロックについては
デフォルトのアクティベート動作のみが対象となり、
アクティベート処理をスクリプト処理に挿げ替えた場合は下記のようにスクリプト側で対象が
アクティベートブロック状態かをチェックして抑制しないとそのままスクリプト処理が実行されてしまいます。
デフォルトのアクティベート動作のみが対象となり、
アクティベート処理をスクリプト処理に挿げ替えた場合は下記のようにスクリプト側で対象が
アクティベートブロック状態かをチェックして抑制しないとそのままスクリプト処理が実行されてしまいます。
- if akTargetRef.isActivationBlocked()
- return
- endif
スクリプト側でアクティベートした場合は動作変更が行われない
パークによるアクティベート変更はゲーム内で直接アクティベートした場合のみ有効で
スクリプトでActivate関数を呼び出した場合はデフォルトのアクティベート処理が行われます。
スクリプトでActivate関数を呼び出した場合はデフォルトのアクティベート処理が行われます。
対処法のサンプル
上記のOnActivateイベントとblockActivation関数による抑制による
スクリプトのサンプルは下記の通りとなります。
スクリプトのサンプルは下記の通りとなります。
- ; パークによるアクティベート動作変更時に
- ; blockActivation関数によるアクティベートのブロックをしてる時に
- ; 処理を実行しないようにしたり、
- ; OnActivateイベントを発生させるためのサンプル
- ; Fragment_2の関数名は環境によって異なるはずなので注意
- Function Fragment_2(ObjectReference akTargetRef, Actor akActor)
- ; パークによるアクティベート動作変更時は
- ; スクリプト側でブロックしないとそのまま処理されてしまう
- if akTargetRef.isActivationBlocked()
- ;OnActivateイベントを発生させる
- akTargetRef.activate(akActor, false);
- Debug.trace("Block Activate")
- return
- endif
-
- ; 以下~~~にアクティベート時に行う処理を記述する
- ~~~~
-
- ; パークによるアクティベート動作変更時はOnActivateイベントが発生しない
- ; アクティベート対象に対してActivate関数を使用すればOnActivateイベントを発生させられるが
- ; デフォルトのアクティベート処理まで実行してしまうため
- ; blockActivation関数でデフォルトのアクティベート処理をブロックし、
- ; Activate関数を使用後、再びブロックを解除するようにする
- ;
- ; 処理後にデフォルトのアクティベート処理をしたい場合は
- ; blockActivationはコメントアウトする
- akTargetRef.blockActivation(true);
- akTargetRef.activate(akActor, false);
- akTargetRef.blockActivation(false);
- EndFunction
-
添付ファイル