SkyrimMOD作成wiki

Tips

最終更新:

匿名ユーザー

- view
だれでも歓迎! 編集
ここではスクリプトの小技やタメになる情報を扱います。 


スクリプト作成時の注意点

初心者は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版と比べてやれる事が少なくなってます)

また、慣れない内に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は指定した文字列をコンソールに出力する関数があるため、デバッグ等でも便利です)

デバッグのためエフェクトや状態をコンソールから確認できる環境を整える

LE版はMfg Console、SE版はMore Informative Consoleを導入すれば、コンソール画面で対象を選ぶだけで対象の状態の確認が行えるようになります。
アビリティは付与されたけどマジックエフェクトがアクティブになってない等の確認も簡単に行えるようになるのでデバッグが非常に容易になります。

Papyrusでキー入力系のスクリプトを作成する時は高速なレスポンスを求めた処理は望まない

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

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

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

戦闘が絡むスクリプトの動作確認について大規模戦闘下での確認は行う事

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

負荷テスト確認を行う場合は事前に「ホワイトランの戦い」等の大規模戦闘前のセーブデータを用意しておくと確認しやすくなります。
また、Assault on Valenwoodという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

予約語は役割が予め決まっており変数で使用できない語です。
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関数です。
これも実際に画面上で魔法が放たれるまで待ってます。
しかし、ノーモーションで魔法が放たれるので、非常に処理が速いです。

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

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

3.画面と同期する必要のない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

エラーの少なく、処理の早い書き方があります。

原則
イベントも関数も呼び出しが少ないほうがよい

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

重複処理をさせない→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

Stateはその名のとおり状態を表してまして、指定した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
基本的にスクリプトエンジンはこのエラーを無視するので問題無いですが、エラーログ出すぎると重くなったり不安定になったりする可能性があるのと、ログが読みにくくなるのでその対処法です。

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

Noneを代入して完全に消す

ObjectReferenceやFormのデータはDeleteを使っただけではスクリプト上では完全に消えてないので、
これを解放するには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()の処理を行い、取得しているからです。
つまり例では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のエラーがでます。
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はそもそも探索が遅いのあります)。
別のイベントや関数で代替できるならそちらでしたほうが良い場合もあります。

安易なスクリプト使用の代替回避をしない

スクリプト使わないパターンでよくあるのが魔法のアビリティのコンディションで代替するパターンでこれは極めて悪手です。
スペルのアビリティのコンディションは毎秒条件の判定があるのでスクリプトで毎秒ループしてるのと同じぐらい重いです。
スクリプトのループと違って、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

解説
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というのが出ますのでメモ帳以外のテキストエディタで開くとデバッグの内容が見れます。

LogExpertを導入

リアルタイムでログが取れるツールです。

  1. LogExpertをダウンロードします。
  2. LogExpert.exeを起動します。
  3. File→Openから、マイドキュメント\My Games\Skyrim\Logs\Script\Papyrus.0.logを開きます。
  4. Options→Always on Topを押して、常に手前に表示にします。
  5. Optionの下あたりにあるメニューのFollow Tailsにチェックを入れます。

スクリプトにデバッグ情報の記載

ログに書き出すにはDebug.Trace("文字")を使います。""のあとに+を使うと変数や関数の結果を書き出せます。

例:
Event OnObjectEquipped(Form akBaseObject, ObjectReference akReference)
	Debug.Trace("Debug:FormName" + akBaseObject.GetName() )
EndEvent

これでゲームとLogExpertを起動して、装備を付けたりすることでスクリプトの動作をリアルタイムで確認できます。

意図する結果になるまで以下の手順で実験してみてください。
  1. スクリプトを書き直してコンパイル
  2. コンソールコマンドで reloadscript script名

OnAnimationEventで取得できるイベント

RegisterForAnimationEventで登録したアニメーションイベント(モーション)をOnAnimationEventで取得することができますが、
取得できるのとできないのがあります。
☓staggerStart
○staggerStop
Start系のモーションは軒並みダメです。
Skyrim - Animation.bsaの中の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 キルムーブ等で首を刎ねられた時

  • 参考になりそうなサイト

Withe01さんブログは主に動作をさせる(イベントを起こす)ときのことが書いてある。

AnimationVariableの使い方

スカイリムのモーションを司るHavok Behaviorが扱うアニメーション変数(AnimationVariable)を取得したり変更したりできます。
これを使うことによってActorに関して細かく状態を判定したり、制御したりできます。
この変数はActorの種類によって異なります。
例えばCharacter(人)だとIsStaggeringはありますが、Dragonにはありません。
どんなアニメーション変数が何があるのかは以下のスプレッドシートのデータを確認してください。
人のアニメーション変数とイベントデータ
人以外のアニメーション変数とイベントデータ

そのほかは、Josh Behavior file patcherで確認ができます。
人ならbehaviorフォルダの0_master.hkx、ドラゴンならdragonbehavior.hkx。


これらのアニメーション関数はConditionでも使えます。
Condtionでの関数名はGetGraphVariableFloatGetGraphVariableIntです。
例: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を使います。
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を使います。
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で違う模様。アクターによっても違います。

セーブデータに残るもの


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

不必要になったプロパティの削除

プロパティはesp側にも紐ついてるので、そちらも消す必要があります。
  1. スクリプトのついてるPropertyボタンを押してプロパティウィンドウを出します。
  2. 不要なプロパティのClear Valueを押して消してください。
  3. そのあと、スクリプト側のプロパティの記述を消します。
  4. espを保存します。



SM Eventを使ったRepeatQuest

OnUpdateを使わずにループができるクエストの作り方です。


セル移動時関係のイベント

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

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

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

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

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

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

NPCへのPerk付与について

オススメはSPIDを使ったゲーム起動時のPerk付与です。

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

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

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

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

Spell Perk Item Distributor (SPID) にてゲーム起動時にPerkを持たせる
NPCに事前にPerkを持たせておくことができるというSKSEプラグイン(LE/SE)です。ゲーム起動時にあたかもはじめから所持していたかのように処理されるため、レコードが競合する心配がありません。

powerofthree's Papyrus ExtenderのAddBasePerk関数
スクリプトで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など、
特定行動時にスペル効果を付与するパークについて条件を満たしたものが複数ある場合、
Priorityが0に近いパークのスペル効果のみが付与されます。

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

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

なおこの制限についてはScrambled Bugsという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の数値が高いパークから先に適応され計算されます。
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に修正する必要があります。

IsInKillMove関数の注意点

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

スクリプトによる一部のフォームの値変更について

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

ConsoleUtilによるターゲットコマンド実行の注意点及び安全な実行について

負荷が掛かると誤実行される危険性がある

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

対策

これに対する回避策としてターゲットコマンドを行う場合、下記のサンプルのようにコマンド文を "[ID]".[コマンド]にする事でコンソールの選択対象に関係無く[ID]の対象に対してコマンドを実行されます。
こうする事により指定した対象以外への誤実行を防ぐ事ができます。

  1. ; ターゲットコマンドの安全実行
  2. ; SetSelectedReference→ExecuteCommandの順で実行する場合、複数のスクリプトがConsoleUtilを使ってると、
  3. ; コマンドが指定していない対象に対して誤実行される危険性があるため
  4. ; コマンド文を"[ID]".[コマンド]に変換し、コマンド実行を指定した対象に対して確実に行うようにする
  5. ; 数値の16進数文字変換はpowerofthree`s Papyrus ExtenderのSKSE関数を使用
  6. function SafeTargetCommandExecute(string cmd, Form target)
  7. if target
  8. ; IDをダブルクォートで囲まないとIDに英文字が混ざってない場合にエラーが起こる
  9. cmd = "\"" + PO3_SKSEFunctions.IntToString(target.getFormID(), true) + "\"." + cmd
  10. else
  11. Debug.trace("Console Command Target is None:" + cmd, 2)
  12. return
  13. endif
  14. ConsoleUtil.ExecuteCommand(cmd)
  15. endfunction
  16.  

アビリティ付与に関する注意

SPIDやスクリプト等でNPCに何かしらのアビリティを直接持たせた場合、NPCをテレポートしたり内部セルへ移動する等何かしらの要因で偶にアビリティに付属してる魔法効果が消滅するという現象が発生します。
アビリティに付属した魔法効果が消えた場合はアビリティの再付与を行わない限りは魔法効果が復活しません。

この現象はパーク効果にアビリティが付属してるものをNPCに持たせた場合は発生しません。アビリティをNPCに直接持たせた場合にのみに発生します。
(パークに付属したアビリティはゲームエンジンレベルで魔法効果が機能してるかチェックしてる?)
そのためスクリプト付きアビリティをプレイヤーやNPCに配布するModを作成する場合、確実にアビリティを持たせて機能させたければダミーパークにアビリティを付属させてそのパークを付与させるようにしましょう。

Activate関係の処理について

BlockActivation関数でアクティベートをブロックしてもOnActivateイベントは発生する

アクター等に対してBlockActivation関数を実行した場合、
対象をアクティベートしても見た目上は反応が返ってきませんが、この時OnActivateイベントはしっかり発生します。
これはスクリプトのActivate関数で第ニ引数にfalseを渡してアクティベートがブロックされた際も同様です。

パークのアクティベート処理変更についての注意点

パークのEntry PointでActivateを設定してReplace Defaultにチェックを付けると
対象のアクティベート時に本来の動作の代わりにパークに付属したスクリプトの処理にすげ替える事ができます。
しかし、このアクティベートの動作変更を使用する場合は下記の点を考慮する必要があり、
特に下記の上2つの現象についてはちゃんと対処しておかないと
他のアクティベート関係の処理が行われるModと一緒に使用した場合に問題が発生する可能性があります。
(上2つの現象はEFFで確認が可能です)

OnActivateイベントが発生しない

本来ならばアクティベートした瞬間にOnActivateイベントが発生するのですが
パークによるアクティベート処理をスクリプト処理に挿げ替えてる場合はOnActivateイベントが発生しません。

アクティベート処理変更を行いつつOnActivateイベントを発生させたい場合は
スクリプト処理内で対象に対してActivate関数を使用する事でOnActivateイベントを発生させられます。

デフォルトのアクティベート処理を実行させずに
スクリプト処理だけ実行してOnActivateイベントを発生させたい場合
blockActivation関数でアクティベートをブロック後にActivate関数(第二引数はfalse)を実行し
再度blockActivation関数でブロック解除を行う必要があります。

スクリプト処理後にデフォルトのアクティベート処理を実行をする場合は
blockActivation関数は使用せずに
スクリプトの最後にActivate関数を実行すればよいだけです。
なお、Activate関数の第ニ引数をtrueを渡した場合はOnActivateイベントは発生しないので注意しましょう。

BlockActivation関数でアクティベートのブロックをしても処理が実行される

BlockActivation関数によるアクティベートのブロックについては
デフォルトのアクティベート動作のみが対象となり、
アクティベート処理をスクリプト処理に挿げ替えた場合は下記のようにスクリプト側で対象が
アクティベートブロック状態かをチェックして抑制しないとそのままスクリプト処理が実行されてしまいます。
  1. if akTargetRef.isActivationBlocked()
  2. return
  3. endif

スクリプト側でアクティベートした場合は動作変更が行われない

パークによるアクティベート変更はゲーム内で直接アクティベートした場合のみ有効で
スクリプトでActivate関数を呼び出した場合はデフォルトのアクティベート処理が行われます。

対処法のサンプル

上記のOnActivateイベントとblockActivation関数による抑制による
スクリプトのサンプルは下記の通りとなります。

  1. ; パークによるアクティベート動作変更時に
  2. ; blockActivation関数によるアクティベートのブロックをしてる時に
  3. ; 処理を実行しないようにしたり、
  4. ; OnActivateイベントを発生させるためのサンプル
  5. ; Fragment_2の関数名は環境によって異なるはずなので注意
  6. Function Fragment_2(ObjectReference akTargetRef, Actor akActor)
  7. ; パークによるアクティベート動作変更時は
  8. ; スクリプト側でブロックしないとそのまま処理されてしまう
  9. if akTargetRef.isActivationBlocked()
  10. ;OnActivateイベントを発生させる
  11. akTargetRef.activate(akActor, false);
  12. Debug.trace("Block Activate")
  13. return
  14. endif
  15.  
  16. ; 以下~~~にアクティベート時に行う処理を記述する
  17. ~~~~
  18.  
  19. ; パークによるアクティベート動作変更時はOnActivateイベントが発生しない
  20. ; アクティベート対象に対してActivate関数を使用すればOnActivateイベントを発生させられるが
  21. ; デフォルトのアクティベート処理まで実行してしまうため
  22. ; blockActivation関数でデフォルトのアクティベート処理をブロックし、
  23. ; Activate関数を使用後、再びブロックを解除するようにする
  24. ;
  25. ; 処理後にデフォルトのアクティベート処理をしたい場合は
  26. ; blockActivationはコメントアウトする
  27. akTargetRef.blockActivation(true);
  28. akTargetRef.activate(akActor, false);
  29. akTargetRef.blockActivation(false);
  30. EndFunction
  31.  

タグ:

+ タグ編集
  • タグ:

このサイトはreCAPTCHAによって保護されており、Googleの プライバシーポリシー利用規約 が適用されます。

添付ファイル
目安箱バナー