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



予約語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

GotoState("Busy");BusyのStateに飛ばす
	int WeapType = (akSource as Weapon).GetWeaponType() ;武器の種類を取得
	if WeapType == 5 || WeapType == 6 ;両手剣と両手斧槌だったとき
		Game.GetPlayer().DamageAV("Stamina",5.0)
	 	Utility.Wait(0.5) ;0.5秒間の重複防止のために待機
	endif
	GotoState("") ;Stateを元の状態に戻す
EndEvent

State Busy
	Event OnHit(ObjectReference akAggressor, Form akSource, Projectile akProjectile, bool abPowerAttack, bool abSneakAttack, bool abBashAttack, bool abHitBlocked)}
	EndEvent ;StateがBusyの時はOnHitイベントが起こっても何も処理しない
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 = 0
While self.Is3Dloaded() == False || i < 10 ; 10秒したらタイムアウト
	Utility.Wait(1.0)
	i += 1
EndWhile

引数が多い関数やイベントほど重い

パピルスの仕様で、引数から変換するときの処理が重いのです。したがって引数が多いほど重いので
OnHitイベントやFind~()は重いです(OnHitは頻発するのとFindはそもそも探索が遅いのあります)。
別のイベントや関数で代替できるならそちらでしたほうが良い場合もあります。

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

スクリプト使わないパターンでよくあるのが 魔法のアビリティのコンディション で代替するパターンでこれは 極めて悪手 です。
スペルのアビリティのコンディションは 毎秒条件の判定がある のでスクリプトで毎秒ループしてるのと同じぐらい重いです。
スクリプトのループと違って、papyrusログにスタックエラーが出ないのでより悪質です。
素直にスクリプト使ってイベント駆動型にしたほうが断然軽く安定します。


スクリプト最適化実践編


装備時のイベント重複防止

OnObjectEquippedは何か装備したときに発動するイベントですが、
一つのアイテムにもかかわらず、6回以上(特にエンチャント武器)呼び出されたりするうえ、対処が厄介です。
例では武器装備時に処理したい場合です。

Form PreObj = None
Event OnObjectEquipped(Form akBaseObject, ObjectReference akReference)
if akBaseObject ==None || akBaseObject as Enchantment || PreObj == akBaseObject	; ベースオブジェクトが取得できない場合とエンチャントの除外、装備が前と同じ場合の除外
	return
endif

GotoState("Busy")
PreObj = akBaseObject
 ; 処理
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 || PreSource ==  akSource;攻撃者がいない場合のエラー防止とダメージソースが武器以外はリターンで即処理中断
  return
endif

GotoState("Busy");BusyのStateに飛ばす
	PreSource = akSource ;PreSourceに今のダメージ元を代入
	int WeapType = (akSource as Weapon).GetWeaponType() ;武器の種類を取得
	if WeapType == 5 || WeapType == 6 ;両手剣と両手斧槌だったとき
		Game.GetPlayer().DamageAV("Stamina",5.0)
	endif
	Utility.Wait(0.5) ;0.5秒間の重複防止のために待機
	PreSource = None ;PreSourceをなしの状態に戻す。
	GotoState("") ;Stateを元の状態に戻す
EndEvent

State Busy
	Event OnHit(ObjectReference akAggressor, Form akSource, Projectile akProjectile, bool abPowerAttack, bool abSneakAttack, bool abBashAttack, bool abHitBlocked)}
	EndEvent ;StateがBusyの時はOnHitイベントが起こっても何も処理しない
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(武器を振った時,左手武器は検出しない)
HitFrame(武器を振った時,左手武器も検出する)
BashExit(バッシュした時)
BashStop(バッシュした時)
BashRelease(バッシュボタンを押し続けてバッシュが発動した時,パワーバッシュは盾装備時のみ検出する)


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

例: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
セルロード時にイベントが発生しますが、メモリキャッシュ済みのセルの場合は発生しません。
続けてゲームプレイする場合に一度入ったセルにもう一度入った場合に発生しない可能性が高いです。
プレイヤーにも使えます。






添付ファイル