「Tips」の編集履歴(バックアップ)一覧に戻る

Tips - (2022/08/08 (月) 21:10:02) のソース

ここではスクリプトの小技やタメになる情報を扱います。 

#contents

*スクリプト作成時の注意点
**初心者は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>https://skyrimspecialedition.2game.info/detail.php?id=22854]]は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関数を用いる事で容易に実行可能でかつ自前でアルゴリズムを組んだ処理よりも高速に処理できます)

また、アクターやオブジェクトの状態の操作や確認、アクターやフォームの一覧を取得したい場合は"powerofthree's Papyrus Extender"、AIパッケージ上書きやファイル関連の操作は"PapyrusUtil"を使用する事で大抵の事はできます。
まずはこの2つだけでもダウンロードしてどのような処理が行えるか把握しておくと良いでしょう。

**コンソール経由でしか処理できないものもある
Papyrusで実行したい関数が無かった場合はコンソールコマンドも確認するようにしましょう。
例えばレベル設定は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]]
**デバッグのためエフェクトや状態をコンソールから確認できる環境を整える
LE版は[[Mfg Console>https://www.nexusmods.com/skyrim/mods/44596]]、SE版は[[More Informative Console>https://www.nexusmods.com/skyrimspecialedition/mods/19250]]を導入すれば、コンソール画面で対象を選ぶだけで対象の状態の確認が行えるようになります。
アビリティは付与されたけどマジックエフェクトがアクティブになってない等の確認も簡単に行えるようになるのでデバッグが非常に容易になります。

**Papyrusでキー入力系のスクリプトを作成する時は高速なレスポンスを求めた処理は望まない
キー入力後からの判定や処理は冗長化しないように可能な限り最適化するように心掛けましょう。これを怠るとキー入力直後に処理を実行したいのに入力後しばらくした後に処理が実行されるという事が起こります。
PapyrusはSkyrimのバックグラウンド処理の仕組み上の問題からどのように最適化しても基本的には僅かでも遅延は起こりえるという事を念頭にスクリプトを作成してください。

また、&color(#3B4EF0){『敵の攻撃がヒットする直前に特定のボタンを押せば相手の攻撃をキャンセルor自身を無敵化させてダメージ無効化』}のような非常に早いレスポンスが求められるものは&color(#F54738){&bold(){Papyrusだけで実現するのはまず不可能}}です。
実際にやろうとすると「判定処理→判定成功→相手の攻撃キャンセルor無敵処理」の判定成功からの判定成功後処理を行う直前で攻撃を受けてしまい、その後に判定成功後の処理が実行されるという事態がよく起こります。
(特にスクリプトのスタックが発生しやすい大規模戦闘では判定処理成功→攻撃を受ける→1秒以上経過した後に判定成功処理が行われるという事態が起こります)

高速なレスポンスを要求されるModを作成したい場合はSKSEプラグインを作成するか、Skyrim Platformでスクリプトを作成してください。

**戦闘が絡むスクリプトの動作確認について大規模戦闘下での確認は行う事
数人程度の敵と戦闘して問題ない事があっても内戦などの大規模戦闘ではスクリプト遅延の多発は容易に起こります。
このため、戦闘関連のスクリプトの動作確認は数人程度の敵と戦闘して問題ないか確認した後、大規模戦闘で動作に支障が無いかの確認をしましょう。

負荷テスト確認を行う場合は事前に「ホワイトランの戦い」等の大規模戦闘前のセーブデータを用意しておくと確認しやすくなります。
また、[[Assault on Valenwood>https://www.nexusmods.com/skyrimspecialedition/mods/48349]]というModは内戦クエスト以上の大規模戦闘かつ開始が非常に容易のためこちらを導入しての確認もオススメです。

**ニューゲーム、途中導入での動作確認は怠らない
ニューゲームで開始時、ゲーム途中からのMod導入どちらでも動作に問題ないか念の為確認しましょう。
スタート地点変更Modがあると確認がしやすくなります。

**(SE版のみ)Papyrusに慣れたらSkyrim Platformでのスクリプト開発を検討する
開発環境の構築に手間が掛かりますが、Skyrim Platform(SP)で開発が行えるならばスクリプト作成はこちらで作成したほうが良いです。理由として
・SKSEプラグインのDLLがスクリプトを読み込んで実行するという形なのでPapyrusの数十倍処理が高速
・特定の処理で実行しない限りはバックグラウンドでは実行せず、フォアグラウンドで実行されるため高速なレスポンスを求められる処理を作成可能
・Papyrusだと処理件数が非常に多くなると一部の処理を後回しにしてしまい、結果スタックが溜まって処理が遅れる(そしてそうゆう状況下だと高確率でまずスタックがどんどん貯まる可能性が高いため最悪フリーズという事態になる)がSkyrim Platformでは処理の後回しによる遅延はなく、仮に処理件数が膨大だった場合でも処理落ちという形で処理遅延の回避が可能となる
・ゲーム中にスクリプトを編集可能、編集内容の反映が可能でデバッグが非常に容易
とPapyrusでのスクリプト作成よりも遥かに利点が多いです。

Skyrim Platformのスクリプト関数はPapyrus関数を準拠にしておりPapyrusで実行できる関数のほとんどが実行可能(開発途中のため一部の関数で確定CTDするため注意)、
さらにSKSEプラグインのPapyrus関数拡張SKSEの関数も定義ファイルを作成すればSkyrim Platform上で実行可能とPapyrusでの経験も応用しやすいためPapyrusに慣れたらSkyrim Platformのスクリプト作成も検討しましょう。


*予約語self
予約語は役割が予め決まっており変数で使用できない語です。
&bold(){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つに分類します。

&bold(){1.同期が必要な[[Latent Function>http://www.creationkit.com/Category:Latent_Functions]]}
スクリプトは上から順に処理していきますが、関数の中でも処理が終わるまでスクリプトが止まるのがLatent Functionです。
代表的な例はWait()です。
Utilty.Wait(1.0)なら1秒経過するまでWaitの部分でスクリプトは待ってます。
次にCast関数です。
これも実際に画面上で魔法が放たれるまで待ってます。
しかし、ノーモーションで魔法が放たれるので、非常に処理が速いです。

CK wiki内のリストには入ってませんがFind系の関数は値が返ってくるまで待ち、処理が遅いです。

&bold(){2.非同期処理の関数}
これは関数の実行が終わったかどうかは関係なく、処理の手続きをしたらさっさと次に進む関数です。
モーションを再生するPlayIdle()がそうです。
モーションが終わったかどうかは関係なく次に進みます。
SoundのPlay()なども同じく、処理の手続きすればすぐ次に行きます。
これと同じ機能で同期処理版がPlayAndWaitです。
スクリプトではなく実際の処理自体はフレームレートやPCの性能に左右されます。
手続された順に再生されるので画面上では遅延が起きるかもしれません。
※仮説上の話ですが、パピルスが言語として遅いのは画面と同期するために意図的に遅くしている可能性も。

&bold(){3.画面と同期する必要のない[[Non-delayed Native Function>http://www.creationkit.com/Category:Non-delayed_Native_Function]]}
画面で起こってることとは全く関係ない、MathやRegister系の関数などです。
これらの関数はフレームレートに左右されずに、常に高速で動きます。

3.以外は1.だから速いとか2.だから遅いというわけではなく、個々での関数で速度を勘定したほうがいいでしょう。


*イベントの処理
同一のフォーム内のスクリプトではイベントのフラグは同時に受けとります。
たとえば、クエスト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使います。

* スクリプト最適化Tips
エラーの少なく、処理の早い書き方があります。

&bold(){原則}
&bold(){イベントも関数も呼び出しが少ないほうがよい}

なので重複処理を防止したり、繰り返しの処理をまとめたりが重要です。

**重複処理をさせない→Stateを使う(スタックエラーの防止策)
敵から攻撃受けた時に両手武器の場合はスタミナに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

&bold(){State}はその名のとおり&bold(){状態}を表してまして、指定したState中にイベントが起こった場合は、Stateに記述したイベントが優先されます。
例のスクリプトはGotoState()で"Busy"というStateに移動します。Stateが変わっても元のOnHitイベントは継続して処理が進みます。
この間にOnHitイベントが起こっても、すべてState Busyの方で処理されます。最後に空のStateにGotoStateで戻って、また通常のOnHitイベントが起きるようになります。

スタックエラーは特定の条件下で、何回も処理が動いてしまうのが原因の一つなので、Stateを使って複数回処理するのを制限することで回避できます。

**return文で処理を中断する
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("")で抜け出します。

 if 
 	return
 endif

 GotoState("Busy")
 if 
 	GotoState("")
 	return
 endif


**None(エラーを少なくする方法)
スクリプトが対象のオブジェクトが見つけれない時にエラーになりますが、これがエラーの中ではもっとも多いかと思います。
None Object、 has no 3d
基本的にスクリプトエンジンはこのエラーを無視するので問題無いですが、エラーログ出すぎると重くなったり不安定になったりする可能性があるのと、ログが読みにくくなるのでその対処法です。

オブジェクトがない状態をオブジェクトは&bold(){None}と返します。
また、&bold(){不必要になったオブジェクトにはNoneを代入する}と安全です。
 if PlayerRef != None ; プレイヤーのリファレンスがないときは処理を行わない
	..... 
 endif
もしくは
 if PlayerRef == None ; プレイヤーのリファレンスが取得できない時はリターンでイベントの強制終了。
 	return
 endif

***Noneを代入して完全に消す
ObjectReferenceやFormのデータはDeleteを使っただけではスクリプト上では完全に消えてないので、
これを解放するには&bold(){None}を代入する必要があります。

ObjectReference Box = PlayerRef.placeAtMe(FXEmptyActivator) ;透明オブジェクトを置く
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()の処理を行い、取得しているからです。
つまり例では&bold(){4回処理}してます。
はじめの一行でGetTargetActor()を取得して変数に代入し、あとはそれを当てはめたほうがコードの見通しもよく効率的です。

・よい例
 Event SomeEvent()
 	Actor selfActor = GetTargetActor()
 	selfActor.AddItem(coolItem, 1)
 	selfActor.AddSpell(coolSpell)
 	selfActor.Kill()
 	selfActor.Resurrect()
 EndEvent

例外としてはGetTargetActor()の&bold(){使用が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のエラーがでます。
 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はそもそも探索が遅いのあります)。
別のイベントや関数で代替できるならそちらでしたほうが良い場合もあります。

**安易なスクリプト使用の代替回避をしない
スクリプト使わないパターンでよくあるのが&bold(){魔法のアビリティのコンディション}で代替するパターンでこれは&bold(){極めて悪手}です。
スペルのアビリティのコンディションは&bold(){毎秒条件の判定がある}のでスクリプトで毎秒ループしてるのと同じぐらい重いです。
スクリプトのループと違って、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回以上(特にエンチャント武器)呼び出されたりするうえ、対処が厄介です。
例では武器装備時に処理したい場合です。

 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

&bold(){解説}
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


**スクリプトログをとる(デバッグの仕方)

***スクリプトログの設定
&bold(){&color(#134f5c){マイドキュメント\My Games\Skyrim\Skyrim.ini}}
を開いて、以下の通りにしたあと保存。(項目がなければ追加)
    [Papyrus]
    bEnableLogging=1
    bEnableTrace=1
    bLoadDebugInformation=1

すると次回から
&bold(){&color(#134f5c){マイドキュメント\My Games\Skyrim\Logs\Script}}
に&bold(){&color(#134f5c){Papyrus.0.log}}というのが出ますのでメモ帳以外のテキストエディタで開くとデバッグの内容が見れます。

***LogExpertを導入
リアルタイムでログが取れるツールです。

+[[LogExpert>>http://www.log-expert.de/]]をダウンロードします。
+LogExpert.exeを起動します。
+&bold(){File→Open}から、&bold(){&color(#134f5c){マイドキュメント\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名
&bold(){}

*OnAnimationEventで取得できるイベント
RegisterForAnimationEventで登録したアニメーションイベント(モーション)をOnAnimationEventで取得することができますが、
取得できるのとできないのがあります。
☓staggerStart
○staggerStop
Start系のモーションは軒並みダメです。
&color(#134f5c){&bold(){Skyrim - Animation.bsa}}の中の&color(#134f5c){&bold(){meshes\responses\actorresponse.txt}}
というテキストファイルに記載されてるのが使用可能なアニメーションイベントです。

ブロックの動作はじめにイベントを受け取りたい場合にBlockStartだとダメですが、裏ワザ的なやり方があります。
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が動く順番>https://www.creationkit.com/index.php?title=Animation_Events]]
[[Withe01さんのMOD作成日誌:20130930-対人用KillMove一覧>https://ameblo.jp/withe01/entry-11624951709.html]]
[[Withe01さんのMOD作成日誌:AnimationEvent>https://ameblo.jp/withe01/entry-11626350697.html]]
 Withe01さんブログは主に動作をさせる(イベントを起こす)ときのことが書いてある。

*AnimationVariableの使い方
スカイリムのモーションを司るHavok Behaviorが扱う&bold(){アニメーション変数(AnimationVariable)}を取得したり変更したりできます。
これを使うことによってActorに関して細かく状態を判定したり、制御したりできます。
この変数はActorの種類によって異なります。
例えばCharacter(人)だとIsStaggeringはありますが、Dragonにはありません。
どんなアニメーション変数が何があるのかは以下のスプレッドシートのデータを確認してください。
[[人のアニメーション変数とイベントデータ>https://docs.google.com/spreadsheets/d/1ZCH_K--r8urv0Cb8TUSeQPzNBEHxKc7jCJ4saytWU_s/edit?usp=sharing]]
[[人以外のアニメーション変数とイベントデータ>>https://docs.google.com/spreadsheets/d/1KSs_nOAeVGCLy393in53sd7CnuVvwhEtWR2e4hCeA-I/edit?usp=sharing]]

そのほかは、[[Josh Behavior file patcher>http://skyrim.nexusmods.com/mods/15906/]]で確認ができます。
人ならbehaviorフォルダの0_master.hkx、ドラゴンならdragonbehavior.hkx。

関数:
[[Get/SetAnimationVariableBool>http://www.creationkit.com/GetAnimationVariableBool_-_ObjectReference]]
[[Get/SetAnimationVariableFloat>http://www.creationkit.com/GetAnimationVariableFloat_-_ObjectReference]]
[[Get/SetAnimationVariableInt>http://www.creationkit.com/GetAnimationVariableInt_-_ObjectReference]]

これらのアニメーション関数は&bold(){Condition}でも使えます。
Condtionでの関数名は&bold(){GetGraphVariableFloat}と&bold(){GetGraphVariableInt}です。
例:GetGraphVariableInt "IsStaggering" == 1 ;BoolはIntで代用。1はtrue 0はfalse

**さまざまな状態の判定
&bold(){Actor.GetAnimationVariableBool("xxx")}

|&bold(){判定}|&bold(){xxxに記載する文字列}|
|攻撃中|IsAttacking|
|ブロック中|IsBlocking|
|バッシュ中|IsBashing|
|はじかれ中|IsRecoiling|
|よろめき中|IsStaggering|
|抜刀中|IsEquipping|
|納刀中|IsUnequipping|
|ジャンプ中|bInJumpState|
|ブロック成功|IsBlockHit|

例:Actor.GetAnimationVariableBool("IsAttacking") ;攻撃中、パワーアタックも含まれる

&bold(){一人称視点かどうかの判定}
 player.GetAnimationVariableInt("i1stPerson") == 1

&bold(){移動方向の判定}
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を使います。
立ち状態と前とで区別つけるときは下の移動中判定と組み合わせてください。

&bold(){移動中かどうかの判定}
一見するとbInMoveStateなんですが、壮大なトラップで片手どちらかに魔法か杖を持ってると移動中でもFalseを返します。
代わりにSpeedを使います。
 if Actor.GetAnimationVariableFloat("Speed") < 5.0 ;停止中

**iStateの変数
GetAnimationVariableInt("iState")で取得できる値はMovement Typeと連動していると思われます。

何ができるかというと、状態判定ができます。
例:パピルスには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で違う模様。アクターによっても違います。

* セーブデータに残るもの
[[Save File Note(CK wiki)>http://www.creationkit.com/Save_File_Notes_%28Papyrus%29]]

セーブするとセーブデータに保存されるものがあります。
・グローバル変数
・静的変数
・プロパティ
一旦セーブされたデータでMODを更新したときにプロパティや静的変数などを削除した場合やMODを抜いた場合はセーブ内と一致しないのでログにエラーを吐きます。
エラーを吐くのですが、データがないと無視するので基本的に害はありません。

**不必要になったプロパティの削除
&bold(){プロパティはesp側にも紐ついてるので}、そちらも消す必要があります。
+スクリプトのついてるPropertyボタンを押してプロパティウィンドウを出します。
+不要なプロパティの&bold(){Clear Value}を押して消してください。
+そのあと、スクリプト側のプロパティの記述を消します。
+espを保存します。

#image(ClearValue.PNG)


*[[SM Eventを使ったRepeatQuest>https://www50.atwiki.jp/skyrim_mod/pages/34.html]]
OnUpdateを使わずにループができるクエストの作り方です。


*セル移動時関係のイベント
セル移動時にスクリプトを動かしたいときにいくつかイベントがあるんですが、どれも癖があってそれを記したいと思います。

&bold(){Onload}
3Dオブジェクトがロードされるときに発生するイベントで、ロード画面が挟むセル移動でなら起きます。
ただしセルを移動してすぐ戻る場合はセル移動時にロード挟まないのでその場合は発生しません。
&bold(){プレイヤー}は稼働しません。

&bold(){OnAttachedToCell}
セルからセルに移動するときに発生するイベントです。
たとえばワールドTamrielのWildness1からWildness2に移動するときにも発生します。
ただし&bold(){プレイヤー}は稼働しません。
またスカイリムからホワイトランに入るときには動きません。(逆は動くので条件不明)

&bold(){OnLocationChange}
ロケーションの移動時に動くイベントですが、複数のセルをまとめて一つのロケーションとして扱う場合があって、例えばホワイトラン→スカイリム、スカイリム→ホワイトランは動きません。
ですので一般的にセル移動時判定には向いてません。

&bold(){OnCellLoad}
セルロード時にイベントが発生しますが、メモリキャッシュ済みのセルの場合は発生しません。
続けてゲームプレイする場合に一度入ったセルにもう一度入った場合に発生しない可能性が高いです。
プレイヤー(エイリアス)に使えます。

NPCであれば、Onloadをおすすめします。
プレイヤーは厳密さを要求しないのであれば、OnCellLoadで大抵何とかなります。

*NPCへのPerk付与について
オススメはSPIDを使ったゲーム起動時のPerk付与です。

&bold(){スクリプトのAddPerk関数}
プレイヤーに対しては正常に機能しますが、NPCに対しては全く機能しません。

&bold(){コンソールのAddPerkコマンド}
スクリプトのAddPerk関数と同様、プレイヤーに対してのみ正常に機能します。

&bold(){マジックエフェクトの"Perk to Apply"}
スクリプトのAddPerk関数と同様、プレイヤーに対してのみ正常に機能します。

&bold(){CKやxEditにて事前にPerkを持たせる}
プラグインを作成して事前にPerkを持たせておけば確実ですが、レコードの競合等により不具合が起こる危険性があります。

&bold(){Spell Perk Item Distributor (SPID) にてゲーム起動時にPerkを持たせる}
NPCに事前にPerkを持たせておくことができるというSKSEプラグイン([[LE>https://www.nexusmods.com/skyrim/mods/105121]]/[[SE>https://www.nexusmods.com/skyrimspecialedition/mods/36869]])です。ゲーム起動時にあたかもはじめから所持していたかのように処理されるため、レコードが競合する心配がありません。

&bold(){powerofthree's Papyrus ExtenderのAddBasePerk関数}
スクリプトでNPCへPerkの付与が可能になるSKSEプラグイン([[LE>https://www.nexusmods.com/skyrim/mods/95017]] / [[SE>https://www.nexusmods.com/skyrimspecialedition/mods/22854]])です。いまのところゲーム中で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がアンロードされてると保存されない)
・&s(){同一のアクターに二回以上AddBasePerkを行うと最初にAddBasePerkで付与したパークが再付与され二重に適用される(disable→enableで正常に戻る)} → 4.5.2で修正

*Perkの挙動について
追加Perk系のModを制作する時には特に下記の点に注意する事。

**特定行動時にスペル付与系
Apply Combat Hit SpellやApply Weapon Swing Spellなど、
特定行動時にスペル効果を付与するパークについて条件を満たしたものが複数ある場合、
Priorityが0に近いパークのスペル効果のみが付与されます。

例として両手武器のウォーマスター(後ろパワーアタックで麻痺効果付与)はPriorityが0、出血攻撃(両手斧攻撃で出血状態付与)はPriorityは3~10が設定されており、
この場合、両手斧で後ろパワーアタックを行った場合はウォーマスターの効果が優先され出血攻撃の効果は出ません。

攻撃時やヒット時に魔法効果を付与といったものを作成したい場合、下手にパークのApply Combat Hit SpellやApply Weapon Swing Spellを使うとバニラのパークを含めて上手く動作しなくなる可能性が高いため、スクリプトで処理を行うことをオススメします。

なおこの制限については[[Scrambled Bugs>https://www.nexusmods.com/skyrimspecialedition/mods/43532?tab=files]]というSKSEプラグインを導入して、ScrambledBugs.jsonのmultipleSpellsをtrueにする事で解除は可能です。
(制限解除時だと上の例にある両手斧で後ろパワーアタックで麻痺効果と出血攻撃の両方の効果が付与される)

**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の数値が高いパークから先に適応され計算されます。

例:
 ※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に修正する必要があります。

*IsInKillMove関数の注意点
IsInKillMoveはアクターがキルムーブを実行中、&color(#F54738){またはキルムーブを受けてる最中の場合にも}trueを返すため、キルムーブの実行者か受けてる側かの判定はOnHitイベント(キルムーブの一撃でもHitイベントは発生する)やOnDyingイベント(キルムーブを受けてる最中に発生)を併用する必要があります。

*スクリプトによる一部のフォームの値変更について
SpellやArmor、Weapon等のフォームについてSet~関数等で値を変更しても一時的な値変更と扱われるためセーブデータに保存されません。(ActorBaseの変更も大体が一時的な変更になる)
また、一時的な値変更となるものに関してはゲームを再起動するまではリセットされないため、値変更後に変更前のセーブデータをロードしても値は変更後のままになります。

*ConsoleUtilによるターゲットコマンド実行の注意点及び安全な実行について
ConsoleUtilでターゲットコマンド(SetAVやSetLevel等)を実行する場合、SetSelectedReference関数を使って対象の選択をすると思いますがSetSelectedReference関数にはコマンドの実行まで対象の切り替えを抑止する機能はありません。
ConsoleUtilを使ってるModが一つだけなら問題ないのですが複数のModがConsoleUtilでターゲットコマンドを使ってる場合は注意が必要です。
もしConsoleUtilを使ってるスクリプトの処理が重なるような事が起こった時にコマンド実行直前で別のスクリプトのSetSelectedReference関数で対象が変更される可能性があり指定していない対象に対してターゲットコマンドの誤実行が行われる危険性があります。

これに対する回避策としてターゲットコマンドを行う場合、下記のサンプルのようにコマンド文を&color(#F54738){ "[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
目安箱バナー