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

Papyrus入門 - (2022/11/21 (月) 20:33:02) のソース

#contents

*概要
スクリプト作成の流れや構造をざっくり掴みます。
流れをつかみやすくするために平たく説明するので厳密に言うと違うこともあります。
実践編はCK Wikiの[[Papyrusチュートリアル>http://www.creationkit.com/index.php?title=Bethesda_Tutorial_Papyrus_Hello_World/ja]]をおすすめします。

// 404 page not found
// こちらのPapyrus入門も合わせてお読みください。
// [[DOVA SOUL - Papyrus>>http://dovasoul.com/skyrim-ck-papyrus-index/]]

*スクリプトに対する誤解
エラーや競合を気にして、スクリプトを避けてコンディション(条件設定)やパッケージ(AIの振る舞いを決めるもの)で似たようなことが出来ますが、負荷はほぼ変わりません。
エラーに関しては、書き方が悪い場合や、やり方に問題がある、またはスクリプトで処理をするのが最適ではない場合です。アンインストールも書き方次第です。問題ありません。

競合に関してはむしろ積極的にスクリプトを使ったほうが柔軟な設計ができるでしょう。
スクリプトを多用するmodは概して改変範囲が広いので競合しやすいように思えますが、だからといって「スクリプト」と「競合しやすい」は結びつきません。

*スクリプトを用いるメリット
できることが増えます。
既存のスクリプトを流用したり、改変するだけでオリジナルのトラップを作ったり、かっこいいエフェクトを動的につけたりもできます。
デバック用のスクリプトを組んで戦闘データを取って自分のフォロワーの強さを調整したり、別のmodと機能が被ってる場合、競合回避のためにどちらかをオフにしたり。

*Papyrusって何?
スカイリム専用の&font(b,#ff6600){イベント駆動型オブジェクト指向スクリプト言語}です。
いきなり難しい用語が出てきましたが、大事なことなのでざっくりと概念を掴みましょう。

**イベント駆動とは?
イベント駆動型というのはイベントが発生する時にしかスクリプトが動きません。イベントを起点として動くということです。
イベントとは例えば座る時(OnSit)、攻撃を受ける時(OnHit)、死ぬ時(OnDeath)、ロード(OnLoad)時などに発生します。
[[イベント一覧>http://www.creationkit.com/index.php?title=Category:Events]]

ゲームは基本的にイベント駆動型です。なぜイベント駆動型なんでしょうか?
ゲーム画面には大量のオブジェクトが設置してあり、その一つ一つに対しての状態を0.1秒毎に監視して動くようにしたらどんなにハイスペックでもまず重くなってしまって動きません。
それに何が・どこで・どう動いているのかがわかりにくくなり複雑すぎます。
これをイベント駆動にすると見た・触った・動いた等のタイミングでスクリプトが動くだけなので、構造がずっとシンプルで動作は軽いです。
ゲームは大体、スイッチを踏んだらトラップが稼働するなど、何かのアクション(イベント)に対して反応という仕組みでできています。
なので&bold(){ゲームにはイベント駆動型が向いています。}

**オブジェクト指向
オブジェクト指向というのは&bold(){オブジェクトを主体として考える}手法です。
あなたが操作するプレイヤーはオブジェクトです。あなたが持っている武器もオブジェクトですし、NPCもオブジェクト、天気が晴れているならその晴れの状態もオブジェクトです。
見たまんまの実体としてオブジェクトで画面が構成されているからわかりやすいですね。
だからゲームには非常に相性が良いです。
でも、オブジェクト自体は&bold(){なんかしらの役割を持つもの}として覚えておいてください。必ずしもすべてがゲーム画面で実体を持っているわけではないです。

CK上の&font(b,#ff6600){オブジェクト=フォーム(Form)}です。そしてその&bold(){Formの種類}が&font(b,#ff6600){FormType}。Formが被らないようにするための&bold(){管理番号}が&font(b,#ff6600){FormID}。
実際に見たほうが早そうですね。
それではCKでSkyrim.esmを開いて、Object Windowを見てみます。

#ref(player.png)


左側のツリーに入ってるものはすべてオブジェクト(フォーム)です。
右側見ますとPlayerが一つのフォームでFormIDは00000007、FormTypeがNPC_。
UserっていうのはこのPlayerを参照にしているオブジェクトです。
Countは実際のゲーム世界(Cell)に設置してある数です。このUserとCountが実際に何に参照されていてどこに設置してあるかは対象を右クリックして&bold(){UseInfo}で見れます。
オブジェクト同士が相互に作用しあってゲームが成り立っているのでこの&bold(){UseInfoは何と何がつながっているのか手がかりになるので極めて重要です。}

もう一つ事例を見てみましょう。
#ref(basictankard.png)


BasicTankard01を開いた状態にしてます。ただのジョッキです。
名称(Name)、Weight(インベントリでの重さ)、Value(価格)が設定されてますね。
画像にはないですがモデルデータの指定もここです。

これ、ゲーム画面での実体のあるモノではないんです。
ただの設定だけのオブジェクトですね。これを&font(b,#ff6600){ベースオブジェクト}&bold(){=ゲームに実体のない元となるデータ}と言います。

実際にゲーム画面に出てくるジョッキはセル内に設置してあります。
これが&font(b,#ff6600){オブジェクトリファレンス}&bold(){=実ゲーム内においてあるもの}です。
画像準備中。

なぜ、ベースとリファレンスで分けるのでしょうか?ややこしいですよね。
ではジョッキ一個の値段を10にしてみましょか。
ベースとリファレンスを分けずに、リファレンス(実体)単体が価格の設定を持っていると仮定したら、セル上にある4179個設置されているのを一つ一つ価格を直さないといけません。途方も無いですよね。
これを回避するために&bold(){設定=ベース}と&bold(){設置=リファレンス}の役割を分けるのです。
役割を分けた上でリファレンスはベースの設定を持っています。(包含関係。リファレンスを変更してもベースは変わらない。)

この&bold(){役割でオブジェクトを分ける}というのがオブジェクト指向の肝だと思います。

魔法なら:威力の強さ、持続時間、魔法名を持つSpellと、効果の種類、耐性、エフェクトやサウンドなどを設定するMagicEffectで役割を分けてます。
鎧なら:防具の性能や種類を決めるArmorと、モデルデータと装備箇所と適用する種族のArmorAddonに分かれています。こうやって分けてあるから種族別で革の兜のモデルデータを変えたりできます。

要は&bold(){テンプレ化(ひな形を作る)}です。
基本となるテンプレ作っちゃえば後は組み合わせであとは無数のバリエーション作れます。
キャラクター例:
|名前|種族|戦闘AI|装備|一日のスケジュール|
|山賊長|ノルド|ボスクラス|重装ボスセット|一日鍛冶してるスケジュール|
|山賊|ブレトン|魔法使い|魔法使いセット|ダンジョン内巡回|
|市民A|インペリアル|非戦闘|服セット|畑仕事|

*Papyrusを言語として覚える
言語なので文法(構文)があります。つまりルールですね。

プレイヤーを取得する。をPapyrusに翻訳すると。
&font(b,#FF1493){Game.GetPlayer()}

後ろから分解してみて、&font(b,#ff6600){GetPlayer()}は&font(b,#ff6600){関数}です。
英語でFunction、直訳すれば&bold(){機能}です。
関数の語尾には必ず&bold(){()}がつきます。これが付いてるものは人も機械も関数だってわかります。
&font(b,#ff6600){GetPlayer()}はそのまんま、&bold(){プレイヤーを取得する機能(関数)}です。

関数は自分で作ることもできますが、ゲーム側でまとまった&bold(){関数リスト(以下ライブラリ)}を作ってます。
このリストの中の一つがGameで、このGameはゲーム全般に関わるライブラリです。
実際のこのリストの場所は&font(b,##134f5c){Data\Scripts\Source\Game.psc}です。

ここからGetPlayer()を引っ張ってくるために先頭に&bold(){Game}を付けます。
つまり&bold(){Gameというライブラリ}の中からGetPlayer()を呼び出しただけなんです。
&bold(){.}は単に区切りです。、みたいなものです。

関数とライブラリは全部は把握できないので[[CK wikiのパピルスリファレンス>http://www.creationkit.com/index.php?title=Category:Papyrus]]を見ながら、どんな関数や関数リストがあったけなーって何ができるかなーって探して使います。
たいてい使用例が書いてあるのでコピペで使えます。

※Tips &bold(){SKSE}の基本的な機能はこの関数とイベントのリスト(ライブラリ)を大幅に拡張するものです。

次はアクター(キャラクター)の体力を取得したいと思います。
&font(b,#ff6600){Actor.GetActorValue("Health")}

ActorのライブラリからGetActorValue()という関数を呼んでます。
さて、関数のカッコ内に"Health"とありますがこれを&font(b,#ff6600){引数(ひきすう)}と呼びます。
英語でParameter(argumentの方が一般的。argと略される)、今じゃ英語のほうがわかりやすい気がする、パラメータのことです。
GetActorValueはアクターに設定されている数値、たとえば体力、スタミナ、マジカ、錬金術のスキル値などを取得できます。※[[取得できるActorValue一覧>http://www.creationkit.com/index.php?title=Actor_Valuet]]
アクターの&bold(){何の数値を対象にするのか}指定しないと、ですね。ここでは"Health"です。対象がスタミナなら"Stamina"を指定します。

実は上のコードでは動きません。対象が必要なんです。
一体誰のアクターの値を取得するんだ、とコンピュータにはわかりません。

対象がプレイヤーの場合は
&font(b,#ff6600){Game.GetPlayer().GetActorValue("Health")}

実はActorの部分、ライブラリだけではなくて&font(b,#ff6600){型}の役割を持ってます。
このGetActorValueの関数はActorの型にしか使えません。壷などがスタミナの値を持ってませんしね。
&bold(){型はデータの種類}だと思っていいです。関数の使用できる範囲を区切る役割が型にはあります。

Game.GetPlayer()で取得したプレイヤーのデータはActorという型に入ります。
これで対象のアクターをプレイヤーにすることができます。

今は型の概念を理解するのは難しいかもしれないですが、
一つの構文パターン(SVとかSVOとか)だと思ったら全然構造は難しくないです。

***対象があるパターンの構文
~の.~を~する()
対象.実行()
型.関数(引数)

上全部意味は同じ。

例:
Actor、ObjectReference、Form、Formlistなど

Actor.GetActorValue() 指定したアクターの指定したActorValueの取得
ObjectReferance.Disable() 指定したオブジェクトリファレンスを非表示にする
Form.GetGoldValue() 指定したフォームの金額のベース値の取得

***対象の指定がないパターンの構文
ライブラリ.~を~する()

例:
Game.GetPlayer() プレイヤーをアクター型として取得する。
Utility.Wait(0.5) これの書かれた部分でこのスクリプトの処理を0.5秒待つ。
Debug.notification("Hello world.") 左上にHello worldと通知を出す。
Math.abs(-1.0) 引数の数値を絶対値として返す。つまり結果は1.0。

Utility、Debug、Math、Gameなど。

**変数、宣言、型
&font(b,#ff6600){変数(variable)}はデータを一時的に記録したり、そのデータを代入したりできます。

変数には型と名前をつけます。この型と名前を明確に定義することを&bold(){宣言}といいます。
型というのは格納するデータの種類です。
基本形が4つあるのでそれをまず覚えましょう。

基本の4型
|int|整数の型|
|float|小数点も扱える数字型(浮動小数点数型)|
|bool|true(真)かfalse(偽)かで返す型|
|string|文字列の型。""で囲う必要がある|

名前は自由につけられますが、接頭辞に数字と記号(例外はアンダーバー→_)はダメです。
※ダメな理由を見たことないですが、文字のはじめが数字なら数字という時代の名残りのようです。
☓0IsWalking
☓-IsWalking
○IsWalking
○_IsWalking


型 名前
&bold(){float PlayerHealth}

このように記述することで、&bold(){float型}の&bold(){PlayerHealth}という名前の変数が作れます。
この変数に入ってる数値はデフォルトだと0.0です。
この数値ははじめから代入しておくことができます。

&bold(){float PlayerHealth = 1.5}

変数の基本は数字や文字列なんですが、以下も同じく変数です。
&bold(){Actor player}
playerの名前のActorの型です。

***実例
float PlayerHealth
PlayerHealth = Game.GetPlayer().GetActorValue("Health")

PlayerHealthをfloat型として宣言したあと、それにプレイヤーのヘルス値代入しています。&bold(){=は代入}です。

こういう書き方もできます。
float PlayerHealth = Game.GetPlayer().GetActorValue("Health")
PlayerHealthに直接代入しています。


*プロパティ
プロパティは変数に似てますが、少し違います。
こちらも宣言が必要なんですが、データの中身をesp側で保持しています。
Actor property Player auto

*if
ifは英語と同じ「もし~ならば~」、条件文です。
もしAならばBをpapyrus風に書くなら以下のとおりです。
 if A
 	B
 endif

実際に書く形式はAがCならBを処理するみたいな感じです。
 if A == C
 	B
 endif
==は数学の=と同じで同格のisとだと思ってください。A is C


*while
条件が&bold(){False(偽=不一致)}になるまでループします。
そのとおりに繰り返しの処理をする場合に使ったり、特定の条件を待機だとかにも使います。
条件が満たされない場合のタイムアウトのために(でないと永遠と回り続けてしまう)、処理の手前でカウントの変数を用意して、カウントまで達したら抜けるようにしておいたほうがいいです。
例:
 int count = 0
 While self.Is3DLoaded == False && count < 10
 	Utility.wait(1.0)
	count += 1
 EndWhile

*イベント
前述のとおり、イベント駆動型なので、とあるイベントが発動することによって初めて動きます。
イベントを制するものがPapyrusを制すと言っても過言ではありません。
実際の制作上でどんなイベントで駆動するかをまずはじめに考えるからです。

取得できるイベントによってはmodの内容すら変わっていきます。
たとえば、フォロワーを作っていてHPが半分切ったら変身するというふうに作りたい場合、HPが半分切ったらという条件で直接取得できるイベントはありません。

代用で考えられるのはOnHitイベントで、これはスクリプトが付いている対象がヒットを受けた時に発動するイベントです。
ヒット度に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計測しているのってスマートじゃない気がしませんか?

欲しい機能は、端的に言ってしまえば&bold(){低HP時での変身}なわけですから、膝付く動作のイベントで発動するOnEnterBleedoutでいいのです。
膝ついたときしか発動しないのでずっとシンプルで軽量です。不安定化や重くなる原因になるパッケージやAblilityで毎秒チェックしたりしないで済みます。

 Event OnEnterBleedout()
 	TransformSpell.Cast(self)
 endEvent

よく使うもの
OnDying OnAnimationEvent - Form


*配列
Papyrusで難解なものの一つが配列なんですが、使いこなせれば強力です。
Papyrus上で基本となるのは一次元の配列で、これは平たく言って&bold(){変数の集合リスト}だと思ってください(厳密にはリストではない)。

変数は一時的にデータを記録したり、代入したりするものですから、例えたら&bold(){箱}と言えます。
この箱が連なってるのが配列です。箱には番号が振り当てられます。番地みたいなもんです。
その番号が&bold(){インデックス(添え字)}です。
#ref(arraybox.png)

上の画像をpapyrusで書くと
int[] a = new int[4]
a[0] = 12
です。

分解していきます。
intは整数型の指定ですが、配列の時は通常時と区別するため&bold(){[]}を付けます。
aは&bold(){変数名}です。ここまでは普通の変数の宣言とあまり変わりません。
newは新しく&bold(){配列の長さをセット}します。
[x]は&bold(){配列の長さ}です。例のように[4]なら0,1,2,3の4つの箱が作られます。
これが[2]なら0,1ですし、[5]なら0,1,2,3,4です。

&font(#600000){インデックスは0から始まるのが、ややこしく間違いやすい点です。([5]の長さで設定したなら[4]で終わる。一個ずれる)}


最初の行は配列の変数を宣言、そして配列の長さを新しく定義しました。

あとの行では箱の中身に数値を入れます。
例では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を代入します。

例3:
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=Arrays_%28Papyrus%29/ja

*Papyrusの問題点
※検証から得た知見と[[Papyrus Tweaks NG>https://skyrimspecialedition.2game.info/detail.php?id=77779]]の修正内容から得た知見を元に記述しています。
 そのため細かい点で間違ってる記述がある可能性があります。

SkyrimのPapyrusエンジンはロースペPCでも動作するようにした仕組みに問題があり、処理速度や安定性が犠牲になっています。
そのため前作のOblivionと比較してPCスペックが向上しても恩恵があまり得づらくなっています。(特にCPUを改善する事による命令処理の向上が下記2つの問題点のせいで恩恵が全く得られなくなってる)
2022年になって、上記のスクリプトエンジン修正Modの登場によりようやく問題のあった仕組みが改善される兆しがでました。

**処理速度がFPSに依存
Papyrusはメインスレッド上ではなく、別スレッドでスクリプトが処理されるため命令処理後は同期処理というものが必要になります。
2022年現在のPCのスペックならば実際のスクリプトの命令1つを処理するのは0.0001秒未満で行えますが、
命令処理後の同期処理は1フレームの描画速度に依存するためそれを換算した場合、実際の処理時間は0.01秒以上掛かる事になります。

この同期処理は構文内の命令を一通り処理した後ではなく一命令を処理する毎に発生します。
そのためFPSが下がれば下がる程、1命令を処理する時間が増加する事に繋がり、
スクリプト外での負荷が大きい状況(高画質設定をしたり大規模戦闘する等)では必然と処理速度が低下するという事になります。
(1つの命令処理時点のFPSが60fpsで約0.016秒、30fpsで約0.033秒、15fpsで0.066秒程、処理に時間が掛かるとみなして良いです)
これにより、FPSが常時120以上超えるような環境でもない限りはスクリプト処理速度は実質頭打ちとなってしまいます。

※
[[SSE Display Tweaks>https://skyrimspecialedition.2game.info/detail.php?id=34705]]でMCMメニュー時のFPS制限を解除すると
MCMの処理が高速化するのはこれが理由となっています。(FPS上限を開放したMCMメニューは200FPSを軽く超えるため同期処理が即座に行われる)
また、[[Skyrim Platform>https://skyrimspecialedition.2game.info/detail.php?id=54909]]で行われるスクリプト処理が非常に高速なのは
スクリプトの処理がメインスレッド上で行われているため同期処理が必要ないためです。
(メインスレッド上で行われてるためオブリビオンのようにスクリプトが無限ループになるとフリーズする)

この問題はPapyrus Tweaks NGのRun Scripts On Main Threadで改善可能となりました。

**一度に処理する命令の数に制限が掛けられてる
ロースペPCで動作するために負荷を掛けないようにするためかどうにも一度に実行される命令数を制限してたようで
処理件数が制限数を超えてる場合は処理を後回し(次のフレームで処理?)にする仕組みになっているようです。
実質1フレーム内で実行できる命令数に制限をかけられてた状態となっています。

これと上記の同期処理の仕様と合わさると低FPS下になると処理待ち命令が増え続けてスクリプトが遅延するという問題が発生します。
NPCが大勢いる状態、特に大規模戦闘になるとさらに状況も相まってFPSが低下しやすくなるため、
スクリプトが多く稼働している場合は容易にこの現象が起こります。

この問題はPapyrus Tweaks NGのMax Operations Per Taskで改善可能となりました。

**上記2つの問題による不安定化について
***フリーズが発生する危険性が高まる
Papyrusは保持できる処理待ち命令に限界があるらしく、処理待ち命令が増え続けると最終的にフリーズしてしまいます。
これは[[Enhanced Blood Textures>https://skyrimspecialedition.2game.info/detail.php?id=2357]]等のNPCにスクリプトを付与するModを複数入れて大規模戦闘を行うとよく発生します。

***条件判定直後の命令処理前に条件判定外になる危険性がある
例:
#highlight(linenumber,php){{
if (Target.Is3DLoaded()) && (Caster.Is3DLoaded())
	; 条件処理直後のここの段階でCasterやTargetがdisableされる余地が実はある
	; スクリプト遅延が起こるとその余地が増える
	Caster.PushActorAway(Target, 1) 
endif}}

上記の一連の命令は一見、条件判定後にすぐに処理を行うように見えますが実はif文内の関数実行直後に同期処理による待機時間が存在するため、
実際はif文の実行後、ほんの僅かに遅れてPushActorAwayが実行される形となります。
この僅かの遅れの時間はFPSが低下するほど長くなり、上にあった命令数制限に引っ掛かる場合はさらに遅れます。
この間に条件判定外になってしまう可能性があり、実行する関数によっては良くてエラー、最悪CTDを起こす危険性があります。

スクリプトエラーログで条件判定処理でチェックしてるはずなのにNoneエラーが起こるという事が稀に現れますがこれが原因となっています。

例にあるPushActorAwayはアクターの3Dモデルがロードされてないと確定CTDするため条件判定を行っています。
このようにPapyrusにはCTD回避のために条件判定しないといけない命令が存在しますが
CTD回避のために条件判定で安全に実行しようとしても条件判定後にスクリプト遅延が起こってしまい、
その間に条件判定外になってしまった後に処理してしまったためCTDが起こってしまう事が起こりえます。

 
目安箱バナー