概要
スクリプトに対する誤解
エラーや競合を気にして、スクリプトを避けてコンディション(条件設定)やパッケージ(AIの振る舞いを決めるもの)で似たようなことが出来ますが、負荷はほぼ変わりません。
エラーに関しては、書き方が悪い場合や、やり方に問題がある、またはスクリプトで処理をするのが最適ではない場合です。アンインストールも書き方次第です。問題ありません。
エラーに関しては、書き方が悪い場合や、やり方に問題がある、またはスクリプトで処理をするのが最適ではない場合です。アンインストールも書き方次第です。問題ありません。
競合に関してはむしろ積極的にスクリプトを使ったほうが柔軟な設計ができるでしょう。
スクリプトを多用するmodは概して改変範囲が広いので競合しやすいように思えますが、だからといって「スクリプト」と「競合しやすい」は結びつきません。
スクリプトを多用するmodは概して改変範囲が広いので競合しやすいように思えますが、だからといって「スクリプト」と「競合しやすい」は結びつきません。
スクリプトを用いるメリット
できることが増えます。
既存のスクリプトを流用したり、改変するだけでオリジナルのトラップを作ったり、かっこいいエフェクトを動的につけたりもできます。
デバック用のスクリプトを組んで戦闘データを取って自分のフォロワーの強さを調整したり、別のmodと機能が被ってる場合、競合回避のためにどちらかをオフにしたり。
既存のスクリプトを流用したり、改変するだけでオリジナルのトラップを作ったり、かっこいいエフェクトを動的につけたりもできます。
デバック用のスクリプトを組んで戦闘データを取って自分のフォロワーの強さを調整したり、別のmodと機能が被ってる場合、競合回避のためにどちらかをオフにしたり。
Papyrusって何?
スカイリム専用のイベント駆動型オブジェクト指向スクリプト言語です。
いきなり難しい用語が出てきましたが、大事なことなのでざっくりと概念を掴みましょう。
いきなり難しい用語が出てきましたが、大事なことなのでざっくりと概念を掴みましょう。
イベント駆動とは?
イベント駆動型というのはイベントが発生する時にしかスクリプトが動きません。イベントを起点として動くということです。
イベントとは例えば座る時(OnSit)、攻撃を受ける時(OnHit)、死ぬ時(OnDeath)、ロード(OnLoad)時などに発生します。
イベント一覧
イベントとは例えば座る時(OnSit)、攻撃を受ける時(OnHit)、死ぬ時(OnDeath)、ロード(OnLoad)時などに発生します。
イベント一覧
ゲームは基本的にイベント駆動型です。なぜイベント駆動型なんでしょうか?
ゲーム画面には大量のオブジェクトが設置してあり、その一つ一つに対しての状態を0.1秒毎に監視して動くようにしたらどんなにハイスペックでもまず重くなってしまって動きません。
それに何が・どこで・どう動いているのかがわかりにくくなり複雑すぎます。
これをイベント駆動にすると見た・触った・動いた等のタイミングでスクリプトが動くだけなので、構造がずっとシンプルで動作は軽いです。
ゲームは大体、スイッチを踏んだらトラップが稼働するなど、何かのアクション(イベント)に対して反応という仕組みでできています。
なのでゲームにはイベント駆動型が向いています。
ゲーム画面には大量のオブジェクトが設置してあり、その一つ一つに対しての状態を0.1秒毎に監視して動くようにしたらどんなにハイスペックでもまず重くなってしまって動きません。
それに何が・どこで・どう動いているのかがわかりにくくなり複雑すぎます。
これをイベント駆動にすると見た・触った・動いた等のタイミングでスクリプトが動くだけなので、構造がずっとシンプルで動作は軽いです。
ゲームは大体、スイッチを踏んだらトラップが稼働するなど、何かのアクション(イベント)に対して反応という仕組みでできています。
なのでゲームにはイベント駆動型が向いています。
オブジェクト指向
オブジェクト指向というのはオブジェクトを主体として考える手法です。
あなたが操作するプレイヤーはオブジェクトです。あなたが持っている武器もオブジェクトですし、NPCもオブジェクト、天気が晴れているならその晴れの状態もオブジェクトです。
見たまんまの実体としてオブジェクトで画面が構成されているからわかりやすいですね。
だからゲームには非常に相性が良いです。
でも、オブジェクト自体はなんかしらの役割を持つものとして覚えておいてください。必ずしもすべてがゲーム画面で実体を持っているわけではないです。
あなたが操作するプレイヤーはオブジェクトです。あなたが持っている武器もオブジェクトですし、NPCもオブジェクト、天気が晴れているならその晴れの状態もオブジェクトです。
見たまんまの実体としてオブジェクトで画面が構成されているからわかりやすいですね。
だからゲームには非常に相性が良いです。
でも、オブジェクト自体はなんかしらの役割を持つものとして覚えておいてください。必ずしもすべてがゲーム画面で実体を持っているわけではないです。
CK上のオブジェクト=フォーム(Form)です。そしてそのFormの種類がFormType。Formが被らないようにするための管理番号がFormID。
実際に見たほうが早そうですね。
それではCKでSkyrim.esmを開いて、Object Windowを見てみます。
実際に見たほうが早そうですね。
それではCKでSkyrim.esmを開いて、Object Windowを見てみます。
左側のツリーに入ってるものはすべてオブジェクト(フォーム)です。
右側見ますとPlayerが一つのフォームでFormIDは00000007、FormTypeがNPC_。
UserっていうのはこのPlayerを参照にしているオブジェクトです。
Countは実際のゲーム世界(Cell)に設置してある数です。このUserとCountが実際に何に参照されていてどこに設置してあるかは対象を右クリックしてUseInfoで見れます。
オブジェクト同士が相互に作用しあってゲームが成り立っているのでこのUseInfoは何と何がつながっているのか手がかりになるので極めて重要です。
右側見ますとPlayerが一つのフォームでFormIDは00000007、FormTypeがNPC_。
UserっていうのはこのPlayerを参照にしているオブジェクトです。
Countは実際のゲーム世界(Cell)に設置してある数です。このUserとCountが実際に何に参照されていてどこに設置してあるかは対象を右クリックしてUseInfoで見れます。
オブジェクト同士が相互に作用しあってゲームが成り立っているのでこのUseInfoは何と何がつながっているのか手がかりになるので極めて重要です。
もう一つ事例を見てみましょう。
BasicTankard01を開いた状態にしてます。ただのジョッキです。
名称(Name)、Weight(インベントリでの重さ)、Value(価格)が設定されてますね。
画像にはないですがモデルデータの指定もここです。
名称(Name)、Weight(インベントリでの重さ)、Value(価格)が設定されてますね。
画像にはないですがモデルデータの指定もここです。
これ、ゲーム画面での実体のあるモノではないんです。
ただの設定だけのオブジェクトですね。これをベースオブジェクト=ゲームに実体のない元となるデータと言います。
ただの設定だけのオブジェクトですね。これをベースオブジェクト=ゲームに実体のない元となるデータと言います。
実際にゲーム画面に出てくるジョッキはセル内に設置してあります。
これがオブジェクトリファレンス=実ゲーム内においてあるものです。
画像準備中。
これがオブジェクトリファレンス=実ゲーム内においてあるものです。
画像準備中。
なぜ、ベースとリファレンスで分けるのでしょうか?ややこしいですよね。
ではジョッキ一個の値段を10にしてみましょか。
ベースとリファレンスを分けずに、リファレンス(実体)単体が価格の設定を持っていると仮定したら、セル上にある4179個設置されているのを一つ一つ価格を直さないといけません。途方も無いですよね。
これを回避するために設定=ベースと設置=リファレンスの役割を分けるのです。
役割を分けた上でリファレンスはベースの設定を持っています。(包含関係。リファレンスを変更してもベースは変わらない。)
ではジョッキ一個の値段を10にしてみましょか。
ベースとリファレンスを分けずに、リファレンス(実体)単体が価格の設定を持っていると仮定したら、セル上にある4179個設置されているのを一つ一つ価格を直さないといけません。途方も無いですよね。
これを回避するために設定=ベースと設置=リファレンスの役割を分けるのです。
役割を分けた上でリファレンスはベースの設定を持っています。(包含関係。リファレンスを変更してもベースは変わらない。)
この役割でオブジェクトを分けるというのがオブジェクト指向の肝だと思います。
魔法なら:威力の強さ、持続時間、魔法名を持つSpellと、効果の種類、耐性、エフェクトやサウンドなどを設定するMagicEffectで役割を分けてます。
鎧なら:防具の性能や種類を決めるArmorと、モデルデータと装備箇所と適用する種族のArmorAddonに分かれています。こうやって分けてあるから種族別で革の兜のモデルデータを変えたりできます。
鎧なら:防具の性能や種類を決めるArmorと、モデルデータと装備箇所と適用する種族のArmorAddonに分かれています。こうやって分けてあるから種族別で革の兜のモデルデータを変えたりできます。
要はテンプレ化(ひな形を作る)です。
基本となるテンプレ作っちゃえば後は組み合わせであとは無数のバリエーション作れます。
キャラクター例:
基本となるテンプレ作っちゃえば後は組み合わせであとは無数のバリエーション作れます。
キャラクター例:
名前 | 種族 | 戦闘AI | 装備 | 一日のスケジュール |
山賊長 | ノルド | ボスクラス | 重装ボスセット | 一日鍛冶してるスケジュール |
山賊 | ブレトン | 魔法使い | 魔法使いセット | ダンジョン内巡回 |
市民A | インペリアル | 非戦闘 | 服セット | 畑仕事 |
Papyrusを言語として覚える
言語なので文法(構文)があります。つまりルールですね。
プレイヤーを取得する。をPapyrusに翻訳すると。
Game.GetPlayer()
Game.GetPlayer()
後ろから分解してみて、GetPlayer()は関数です。
英語でFunction、直訳すれば機能です。
関数の語尾には必ず()がつきます。これが付いてるものは人も機械も関数だってわかります。
GetPlayer()はそのまんま、プレイヤーを取得する機能(関数)です。
英語でFunction、直訳すれば機能です。
関数の語尾には必ず()がつきます。これが付いてるものは人も機械も関数だってわかります。
GetPlayer()はそのまんま、プレイヤーを取得する機能(関数)です。
関数は自分で作ることもできますが、ゲーム側でまとまった関数リスト(以下ライブラリ)を作ってます。
このリストの中の一つがGameで、このGameはゲーム全般に関わるライブラリです。
実際のこのリストの場所はData\Scripts\Source\Game.pscです。
このリストの中の一つがGameで、このGameはゲーム全般に関わるライブラリです。
実際のこのリストの場所はData\Scripts\Source\Game.pscです。
ここからGetPlayer()を引っ張ってくるために先頭にGameを付けます。
つまりGameというライブラリの中からGetPlayer()を呼び出しただけなんです。
.は単に区切りです。、みたいなものです。
つまりGameというライブラリの中からGetPlayer()を呼び出しただけなんです。
.は単に区切りです。、みたいなものです。
関数とライブラリは全部は把握できないのでCK wikiのパピルスリファレンスを見ながら、どんな関数や関数リストがあったけなーって何ができるかなーって探して使います。
たいてい使用例が書いてあるのでコピペで使えます。
たいてい使用例が書いてあるのでコピペで使えます。
※Tips SKSEの基本的な機能はこの関数とイベントのリスト(ライブラリ)を大幅に拡張するものです。
次はアクター(キャラクター)の体力を取得したいと思います。
Actor.GetActorValue("Health")
Actor.GetActorValue("Health")
ActorのライブラリからGetActorValue()という関数を呼んでます。
さて、関数のカッコ内に"Health"とありますがこれを引数(ひきすう)と呼びます。
英語でParameter(argumentの方が一般的。argと略される)、今じゃ英語のほうがわかりやすい気がする、パラメータのことです。
GetActorValueはアクターに設定されている数値、たとえば体力、スタミナ、マジカ、錬金術のスキル値などを取得できます。※取得できるActorValue一覧
アクターの何の数値を対象にするのか指定しないと、ですね。ここでは"Health"です。対象がスタミナなら"Stamina"を指定します。
さて、関数のカッコ内に"Health"とありますがこれを引数(ひきすう)と呼びます。
英語でParameter(argumentの方が一般的。argと略される)、今じゃ英語のほうがわかりやすい気がする、パラメータのことです。
GetActorValueはアクターに設定されている数値、たとえば体力、スタミナ、マジカ、錬金術のスキル値などを取得できます。※取得できるActorValue一覧
アクターの何の数値を対象にするのか指定しないと、ですね。ここでは"Health"です。対象がスタミナなら"Stamina"を指定します。
実は上のコードでは動きません。対象が必要なんです。
一体誰のアクターの値を取得するんだ、とコンピュータにはわかりません。
一体誰のアクターの値を取得するんだ、とコンピュータにはわかりません。
対象がプレイヤーの場合は
Game.GetPlayer().GetActorValue("Health")
Game.GetPlayer().GetActorValue("Health")
実はActorの部分、ライブラリだけではなくて型の役割を持ってます。
このGetActorValueの関数はActorの型にしか使えません。壷などがスタミナの値を持ってませんしね。
型はデータの種類だと思っていいです。関数の使用できる範囲を区切る役割が型にはあります。
このGetActorValueの関数はActorの型にしか使えません。壷などがスタミナの値を持ってませんしね。
型はデータの種類だと思っていいです。関数の使用できる範囲を区切る役割が型にはあります。
Game.GetPlayer()で取得したプレイヤーのデータはActorという型に入ります。
これで対象のアクターをプレイヤーにすることができます。
これで対象のアクターをプレイヤーにすることができます。
今は型の概念を理解するのは難しいかもしれないですが、
一つの構文パターン(SVとかSVOとか)だと思ったら全然構造は難しくないです。
一つの構文パターン(SVとかSVOとか)だと思ったら全然構造は難しくないです。
対象があるパターンの構文
~の.~を~する()
対象.実行()
型.関数(引数)
対象.実行()
型.関数(引数)
上全部意味は同じ。
例:
Actor、ObjectReference、Form、Formlistなど
Actor、ObjectReference、Form、Formlistなど
Actor.GetActorValue() 指定したアクターの指定したActorValueの取得
ObjectReferance.Disable() 指定したオブジェクトリファレンスを非表示にする
Form.GetGoldValue() 指定したフォームの金額のベース値の取得
ObjectReferance.Disable() 指定したオブジェクトリファレンスを非表示にする
Form.GetGoldValue() 指定したフォームの金額のベース値の取得
対象の指定がないパターンの構文
ライブラリ.~を~する()
例:
Game.GetPlayer() プレイヤーをアクター型として取得する。
Utility.Wait(0.5) これの書かれた部分でこのスクリプトの処理を0.5秒待つ。
Debug.notification("Hello world.") 左上にHello worldと通知を出す。
Math.abs(-1.0) 引数の数値を絶対値として返す。つまり結果は1.0。
Game.GetPlayer() プレイヤーをアクター型として取得する。
Utility.Wait(0.5) これの書かれた部分でこのスクリプトの処理を0.5秒待つ。
Debug.notification("Hello world.") 左上にHello worldと通知を出す。
Math.abs(-1.0) 引数の数値を絶対値として返す。つまり結果は1.0。
Utility、Debug、Math、Gameなど。
変数、宣言、型
変数(variable)はデータを一時的に記録したり、そのデータを代入したりできます。
変数には型と名前をつけます。この型と名前を明確に定義することを宣言といいます。
型というのは格納するデータの種類です。
基本形が4つあるのでそれをまず覚えましょう。
型というのは格納するデータの種類です。
基本形が4つあるのでそれをまず覚えましょう。
基本の4型
int | 整数の型 |
float | 小数点も扱える数字型(浮動小数点数型) |
bool | true(真)かfalse(偽)かで返す型 |
string | 文字列の型。""で囲う必要がある |
名前は自由につけられますが、接頭辞に数字と記号(例外はアンダーバー→_)はダメです。
※ダメな理由を見たことないですが、文字のはじめが数字なら数字という時代の名残りのようです。
☓0IsWalking
☓-IsWalking
○IsWalking
○_IsWalking
※ダメな理由を見たことないですが、文字のはじめが数字なら数字という時代の名残りのようです。
☓0IsWalking
☓-IsWalking
○IsWalking
○_IsWalking
型 名前
float PlayerHealth
float PlayerHealth
このように記述することで、float型のPlayerHealthという名前の変数が作れます。
この変数に入ってる数値はデフォルトだと0.0です。
この数値ははじめから代入しておくことができます。
この変数に入ってる数値はデフォルトだと0.0です。
この数値ははじめから代入しておくことができます。
float PlayerHealth = 1.5
変数の基本は数字や文字列なんですが、以下も同じく変数です。
Actor player
playerの名前のActorの型です。
Actor player
playerの名前のActorの型です。
実例
float PlayerHealth
PlayerHealth = Game.GetPlayer().GetActorValue("Health")
PlayerHealth = Game.GetPlayer().GetActorValue("Health")
PlayerHealthをfloat型として宣言したあと、それにプレイヤーのヘルス値代入しています。=は代入です。
こういう書き方もできます。
float PlayerHealth = Game.GetPlayer().GetActorValue("Health")
PlayerHealthに直接代入しています。
float PlayerHealth = Game.GetPlayer().GetActorValue("Health")
PlayerHealthに直接代入しています。
プロパティ
if
ifは英語と同じ「もし~ならば~」、条件文です。
もしAならばBをpapyrus風に書くなら以下のとおりです。
もしAならばBをpapyrus風に書くなら以下のとおりです。
if A B endif
実際に書く形式はAがCならBを処理するみたいな感じです。
if A == C B endif
==は数学の=と同じで同格のisとだと思ってください。A is C
while
条件がFalse(偽=不一致)になるまでループします。
そのとおりに繰り返しの処理をする場合に使ったり、特定の条件を待機だとかにも使います。
条件が満たされない場合のタイムアウトのために(でないと永遠と回り続けてしまう)、処理の手前でカウントの変数を用意して、カウントまで達したら抜けるようにしておいたほうがいいです。
例:
そのとおりに繰り返しの処理をする場合に使ったり、特定の条件を待機だとかにも使います。
条件が満たされない場合のタイムアウトのために(でないと永遠と回り続けてしまう)、処理の手前でカウントの変数を用意して、カウントまで達したら抜けるようにしておいたほうがいいです。
例:
int count = 0 While self.Is3DLoaded == False && count < 10 Utility.wait(1.0) count += 1 EndWhile
イベント
前述のとおり、イベント駆動型なので、とあるイベントが発動することによって初めて動きます。
イベントを制するものがPapyrusを制すと言っても過言ではありません。
実際の制作上でどんなイベントで駆動するかをまずはじめに考えるからです。
イベントを制するものがPapyrusを制すと言っても過言ではありません。
実際の制作上でどんなイベントで駆動するかをまずはじめに考えるからです。
取得できるイベントによってはmodの内容すら変わっていきます。
たとえば、フォロワーを作っていてHPが半分切ったら変身するというふうに作りたい場合、HPが半分切ったらという条件で直接取得できるイベントはありません。
たとえば、フォロワーを作っていてHPが半分切ったら変身するというふうに作りたい場合、HPが半分切ったらという条件で直接取得できるイベントはありません。
代用で考えられるのはOnHitイベントで、これはスクリプトが付いている対象がヒットを受けた時に発動するイベントです。
ヒット度にGetActorValuePercentage("Health")を取得すればいいわけです。
ヒット度にGetActorValuePercentage("Health")を取得すればいいわけです。
Event OnHit(ObjectReference akAggressor, Form akSource, Projectile akProjectile, bool abPowerAttack, bool abSneakAttack, \ bool abBashAttack, bool abHitBlocked) if self.GetActorValuePercentage("Health") < 0.5 TransformSpell.Cast(self) endif EndEvent
ただしOnHitイベント自体、そこそこ重い上に、いちいちヒット度にHP計測しているのってスマートじゃない気がしませんか?
欲しい機能は、端的に言ってしまえば低HP時での変身なわけですから、膝付く動作のイベントで発動するOnEnterBleedoutでいいのです。
膝ついたときしか発動しないのでずっとシンプルで軽量です。不安定化や重くなる原因になるパッケージやAblilityで毎秒チェックしたりしないで済みます。
膝ついたときしか発動しないのでずっとシンプルで軽量です。不安定化や重くなる原因になるパッケージやAblilityで毎秒チェックしたりしないで済みます。
Event OnEnterBleedout() TransformSpell.Cast(self) endEvent
よく使うもの
OnDying OnAnimationEvent - Form
OnDying OnAnimationEvent - Form
配列
Papyrusで難解なものの一つが配列なんですが、使いこなせれば強力です。
Papyrus上で基本となるのは一次元の配列で、これは平たく言って変数の集合リストだと思ってください(厳密にはリストではない)。
Papyrus上で基本となるのは一次元の配列で、これは平たく言って変数の集合リストだと思ってください(厳密にはリストではない)。
変数は一時的にデータを記録したり、代入したりするものですから、例えたら箱と言えます。
この箱が連なってるのが配列です。箱には番号が振り当てられます。番地みたいなもんです。
その番号がインデックス(添え字)です。
この箱が連なってるのが配列です。箱には番号が振り当てられます。番地みたいなもんです。
その番号がインデックス(添え字)です。
上の画像をpapyrusで書くと
int[] a = new int[4]
a[0] = 12
です。
int[] a = new int[4]
a[0] = 12
です。
分解していきます。
intは整数型の指定ですが、配列の時は通常時と区別するため[]を付けます。
aは変数名です。ここまでは普通の変数の宣言とあまり変わりません。
newは新しく配列の長さをセットします。
[x]は配列の長さです。例のように[4]なら0,1,2,3の4つの箱が作られます。
これが[2]なら0,1ですし、[5]なら0,1,2,3,4です。
intは整数型の指定ですが、配列の時は通常時と区別するため[]を付けます。
aは変数名です。ここまでは普通の変数の宣言とあまり変わりません。
newは新しく配列の長さをセットします。
[x]は配列の長さです。例のように[4]なら0,1,2,3の4つの箱が作られます。
これが[2]なら0,1ですし、[5]なら0,1,2,3,4です。
インデックスは0から始まるのが、ややこしく間違いやすい点です。([5]の長さで設定したなら[4]で終わる。一個ずれる)
最初の行は配列の変数を宣言、そして配列の長さを新しく定義しました。
あとの行では箱の中身に数値を入れます。
例ではa[0]の箱に12を代入してます。
a[1] = 13
a[2] = 15
a[3] = 7
みたいに箱別に代入できます。
debug.notification(""+ a[0]) で表示されるのは12です。
例ではa[0]の箱に12を代入してます。
a[1] = 13
a[2] = 15
a[3] = 7
みたいに箱別に代入できます。
debug.notification(""+ a[0]) で表示されるのは12です。
例2:
string[] myArray = new string[5] myArray[0] = "Hello" myArray[1] = "World" myArray[2] = "Hello" myArray[3] = "World" myArray[4] = "Again" Event OnInit() int i = 0 While i <= 5 debug.notification("" + myArray[i]) i += 1 EndWhile EndEvent
で順番にHello,World,Hello,World,Againと左上に表示されていきます。
宣言の部分は前と同じです。
iというカウント用の変数作って0に設定します。
Whileでループして5以上になったら止めます。
myArrayの変数にiを代入します。
宣言の部分は前と同じです。
iというカウント用の変数作って0に設定します。
Whileでループして5以上になったら止めます。
myArrayの変数にiを代入します。
例3:
Actor[] Property DeadActorList Auto
プロパティにも配列使えます。プロパティのウィンドウで複数のプロパティを指定できます。
Actor[] Property DeadActorList Auto
プロパティにも配列使えます。プロパティのウィンドウで複数のプロパティを指定できます。
活用すると、まとめて変数を扱えるため冗長なコードになりにくくなり、またインデックスは自然数ですから足したり引いたりできて扱いやすいです。
(かなり応用例があるのですがそれはまた今度)
(かなり応用例があるのですがそれはまた今度)
実例としてはCK wikiのComplete Example Scripts(テキスト検索で[]で調べる)
http://www.creationkit.com/index.php?title=Complete_Example_Scripts
http://www.creationkit.com/index.php?title=Complete_Example_Scripts
またこちらの配列の解説も読んでおきましょう。
http://www.creationkit.com/index.php?title=Arrays_%28Papyrus%29/ja
http://www.creationkit.com/index.php?title=Arrays_%28Papyrus%29/ja
Papyrusの問題点
※検証から得た知見とPapyrus Tweaks NGの修正内容から得た知見を元に記述しています。
そのため細かい点で間違ってる記述がある可能性があります。
そのため細かい点で間違ってる記述がある可能性があります。
SkyrimのPapyrusエンジンはロースペPCでも動作するようにした仕組みに問題があり、処理速度や安定性が犠牲になっています。
そのため前作のOblivionと比較してPCスペックが向上しても恩恵があまり得づらくなっています。(特にCPUを改善する事による命令処理の向上がFPS依存問題と一度に処理できる命令数が少ないせいで恩恵が全く得られなくなってる)
SE版は2022年にて、Papyrus Tweaks NG等のスクリプトエンジン改修Modの登場により問題のあった仕組みが改善されるようになりました。
そのため前作のOblivionと比較してPCスペックが向上しても恩恵があまり得づらくなっています。(特にCPUを改善する事による命令処理の向上がFPS依存問題と一度に処理できる命令数が少ないせいで恩恵が全く得られなくなってる)
SE版は2022年にて、Papyrus Tweaks NG等のスクリプトエンジン改修Modの登場により問題のあった仕組みが改善されるようになりました。
処理速度がFPSに依存する
Papyrusはメインスレッド上ではなく、別スレッドでスクリプトが処理されるため命令処理後は同期処理というものが必要になります。
2022年現在のPCのスペックならば実際のスクリプトの命令1つを処理するのは0.0001秒未満で行えますが、
命令処理後の同期処理は1フレームの描画速度に依存するためそれを換算した場合、実際の処理時間は0.01秒以上掛かる事になります。
2022年現在のPCのスペックならば実際のスクリプトの命令1つを処理するのは0.0001秒未満で行えますが、
命令処理後の同期処理は1フレームの描画速度に依存するためそれを換算した場合、実際の処理時間は0.01秒以上掛かる事になります。
この同期処理は構文内の命令を一通り処理した後ではなく一命令を処理する毎に発生します。
そのためFPSが下がれば下がる程、1命令を処理する時間が増加する事に繋がり、
スクリプト外での負荷が大きい状況(高画質設定をしたり大規模戦闘する等)では必然と処理速度が低下するという事になります。
(1つの命令処理時点のFPSが60fpsで約0.016秒、30fpsで約0.033秒、15fpsで0.066秒程、処理に時間が掛かるとみなして良いです)
これにより、FPSが常時120以上超えるような環境でもない限りはスクリプト処理速度は実質頭打ちとなってしまいます。
この問題はPapyrus Tweaks NGのSpeed up native callsをONにする事で大幅に改善されます。
そのためFPSが下がれば下がる程、1命令を処理する時間が増加する事に繋がり、
スクリプト外での負荷が大きい状況(高画質設定をしたり大規模戦闘する等)では必然と処理速度が低下するという事になります。
(1つの命令処理時点のFPSが60fpsで約0.016秒、30fpsで約0.033秒、15fpsで0.066秒程、処理に時間が掛かるとみなして良いです)
これにより、FPSが常時120以上超えるような環境でもない限りはスクリプト処理速度は実質頭打ちとなってしまいます。
この問題はPapyrus Tweaks NGのSpeed up native callsをONにする事で大幅に改善されます。
※FPS制限解除時のMCMで目に見えて処理が高速化されるのが確認可能
SSE Display TweaksでMCMメニュー時のFPS制限を解除すると
MCMの処理が高速化するのはこれが理由となっています。(FPS上限を開放したMCMメニューは200FPSを軽く超えるため同期処理が即座に行われる)
また、Skyrim Platformで行われるスクリプト処理が非常に高速なのは
スクリプトの処理がメインスレッド上で行われているため同期処理が必要ないためです。
(メインスレッド上で行われてるためオブリビオンのようにスクリプトが無限ループになるとフリーズする)
MCMの処理が高速化するのはこれが理由となっています。(FPS上限を開放したMCMメニューは200FPSを軽く超えるため同期処理が即座に行われる)
また、Skyrim Platformで行われるスクリプト処理が非常に高速なのは
スクリプトの処理がメインスレッド上で行われているため同期処理が必要ないためです。
(メインスレッド上で行われてるためオブリビオンのようにスクリプトが無限ループになるとフリーズする)
一度に処理する命令の数に制限が掛けられてる
ロースペPCで動作するために負荷を掛けないようにするためかどうにも一度に実行される命令数を制限してたようで
処理件数が制限数を超えてる場合は処理を後回し(次のフレームで処理?)にする仕組みになっているようです。
実質1フレーム内で実行できる命令数に制限をかけられてた状態となっています。
処理件数が制限数を超えてる場合は処理を後回し(次のフレームで処理?)にする仕組みになっているようです。
実質1フレーム内で実行できる命令数に制限をかけられてた状態となっています。
これと上記の同期処理の仕様と合わさると低FPS下になると処理待ち命令が増え続けてスクリプトが遅延するという問題が発生します。
NPCが大勢いる状態、特に大規模戦闘になるとさらに状況も相まってFPSが低下しやすくなるため、
スクリプトが多く稼働している場合は容易にこの現象が起こります。
NPCが大勢いる状態、特に大規模戦闘になるとさらに状況も相まってFPSが低下しやすくなるため、
スクリプトが多く稼働している場合は容易にこの現象が起こります。
この問題はPapyrus Tweaks NGのMax Operations Per Taskで改善可能となりました。
無限ループ化した場合、処理を中断する仕組みがない
通常スクリプトはメインスレッド上で無限ループ化した場合、待機処理が入っていない場合はフリーズします。
この場合は明らかにフリーズという形で問題が起こってると認識が可能ですが、
前述に記載しましたがPapyrusは別スレッド上でスクリプト処理が行われており、
別スレッドで無限ループ化した場合は特にフリーズが起こる等の異常が起こらず問題が起こっているかどうか認識しづらいです。
(スクリプトエラーログを確認すると長時間スクリプトを稼働してる事によるスタックダンプが発生しているので問題発生の確認は可能です)
この場合は明らかにフリーズという形で問題が起こってると認識が可能ですが、
前述に記載しましたがPapyrusは別スレッド上でスクリプト処理が行われており、
別スレッドで無限ループ化した場合は特にフリーズが起こる等の異常が起こらず問題が起こっているかどうか認識しづらいです。
(スクリプトエラーログを確認すると長時間スクリプトを稼働してる事によるスタックダンプが発生しているので問題発生の確認は可能です)
その状態でゲームのセーブを行うと問題のある処理が延々と繰り返し続ける事になり、
セーブデータのクリーンを行わない限り、問題の処理が除去されないので注意してください。
セーブデータのクリーンを行わない限り、問題の処理が除去されないので注意してください。
この問題はMod製作者の不注意が原因で発生する問題なので、注意してスクリプトを記述すれば回避はできます。
また、この問題はRecursion Monitorで改善可能です。
また、この問題はRecursion Monitorで改善可能です。
スクリプト遅延と処理待ちによる不安定化について
フリーズが発生する危険性が高まる
Papyrusは保持できる処理待ち命令に限界があるらしく、処理待ち命令が増え続けると最終的にフリーズしてしまいます。
これはEnhanced Blood Textures等のNPCにスクリプトを付与するModを複数入れて大規模戦闘を行うとよく発生します。
これはEnhanced Blood Textures等のNPCにスクリプトを付与するModを複数入れて大規模戦闘を行うとよく発生します。
条件判定直後の命令処理前に条件判定外になる危険性がある
例:
- if (Target.Is3DLoaded()) && (Caster.Is3DLoaded())
- ; 条件処理直後のここの段階でCasterやTargetがdisableされる余地が実はある
- ; スクリプト遅延が起こるとその余地が増える
- Caster.PushActorAway(Target, 1)
- endif
上記の一連の命令は一見、条件判定後にすぐに処理を行うように見えますが実はif文内の関数実行直後に同期処理による待機時間が存在するため、
実際はif文の実行後、ほんの僅かに遅れてPushActorAwayが実行される形となります。
この僅かの遅れの時間はFPSが低下するほど長くなり、上にあった命令数制限に引っ掛かる場合はさらに遅れます。
この間に条件判定外になってしまう可能性があり、実行する関数によっては良くてエラー、最悪CTDを起こす危険性があります。
(例にあるPushActorAway関数はアクターの3Dモデルがロードされてない状態で呼び出すと確定CTD)
実際はif文の実行後、ほんの僅かに遅れてPushActorAwayが実行される形となります。
この僅かの遅れの時間はFPSが低下するほど長くなり、上にあった命令数制限に引っ掛かる場合はさらに遅れます。
この間に条件判定外になってしまう可能性があり、実行する関数によっては良くてエラー、最悪CTDを起こす危険性があります。
(例にあるPushActorAway関数はアクターの3Dモデルがロードされてない状態で呼び出すと確定CTD)
スクリプトエラーログで条件判定処理でチェックしてるはずなのにNoneエラーが起こるという事が稀に現れますが
条件判定直後に何かしらの要因で対象が消滅してたり、魔法効果が消滅した等でこういったものが発生する事があります。
条件判定直後に何かしらの要因で対象が消滅してたり、魔法効果が消滅した等でこういったものが発生する事があります。
このようにPapyrusにはCTD回避のために条件判定しないといけない命令が存在しますが
CTD回避のために条件判定で安全に実行しようとしても条件判定後にスクリプト遅延が起こってしまうと、
その間に条件判定外になってしまった場合、そのまま処理してしまったためCTDが起こってしまうという事が起こりえます。
特に内戦クエストの大規模戦闘では帝国兵とストームクローク兵は倒されるとしばらくした後に消滅するため
Modによるスクリプト次第ではCTDのリスクが非常に高くなります。
このリスクはPapyrus Tweaks NGのSpeed up native callsで大幅に軽減する事ができます。
CTD回避のために条件判定で安全に実行しようとしても条件判定後にスクリプト遅延が起こってしまうと、
その間に条件判定外になってしまった場合、そのまま処理してしまったためCTDが起こってしまうという事が起こりえます。
特に内戦クエストの大規模戦闘では帝国兵とストームクローク兵は倒されるとしばらくした後に消滅するため
Modによるスクリプト次第ではCTDのリスクが非常に高くなります。
このリスクはPapyrus Tweaks NGのSpeed up native callsで大幅に軽減する事ができます。