Papyrus入門

概要

スクリプト作成の流れや構造をざっくり掴みます。
流れをつかみやすくするために平たく説明するので厳密に言うと違うこともあります。
実践編はCK WikiのPapyrusチュートリアルをおすすめします。

こちらのPapyrus入門も合わせてお読みください。
DOVA SOUL - Papyrus

スクリプトに対する誤解

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

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

スクリプトを用いるメリット

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

Papyrusって何?

スカイリム専用の イベント駆動型オブジェクト指向スクリプト言語 です。
いきなり難しい用語が出てきましたが、大事なことなのでざっくりと概念を掴みましょう。

イベント駆動とは?

イベント駆動型というのはイベントが発生する時にしかスクリプトが動きません。イベントを起点として動くということです。
イベントとは例えば座る時(OnSit)、攻撃を受ける時(OnHit)、死ぬ時(OnDeath)、ロード(OnLoad)時などに発生します。
イベント一覧

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

オブジェクト指向

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

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



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

もう一つ事例を見てみましょう。


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

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

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

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

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

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

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

Papyrusを言語として覚える

言語なので文法(構文)があります。つまりルールですね。

プレイヤーを取得する。をPapyrusに翻訳すると。
Game.GetPlayer()

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

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

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

関数とライブラリは全部は把握できないのでCK wikiのパピルスリファレンスを見ながら、どんな関数や関数リストがあったけなーって何ができるかなーって探して使います。
たいてい使用例が書いてあるのでコピペで使えます。

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

次はアクター(キャラクター)の体力を取得したいと思います。
Actor.GetActorValue("Health")

ActorのライブラリからGetActorValue()という関数を呼んでます。
さて、関数のカッコ内に"Health"とありますがこれを 引数(ひきすう) と呼びます。
英語でParameter(argumentの方が一般的。argと略される)、今じゃ英語のほうがわかりやすい気がする、パラメータのことです。
GetActorValueはアクターに設定されている数値、たとえば体力、スタミナ、マジカ、錬金術のスキル値などを取得できます。※取得できるActorValue一覧
アクターの 何の数値を対象にするのか 指定しないと、ですね。ここでは"Health"です。対象がスタミナなら"Stamina"を指定します。

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

対象がプレイヤーの場合は
Game.GetPlayer().GetActorValue("Health")

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

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など。

変数、宣言、型

変数(variable) はデータを一時的に記録したり、そのデータを代入したりできます。

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

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

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


型 名前
float PlayerHealth

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

float PlayerHealth = 1.5

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

実例

float PlayerHealth
PlayerHealth = Game.GetPlayer().GetActorValue("Health")

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

こういう書き方もできます。
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

条件が 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計測しているのってスマートじゃない気がしませんか?

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

Event OnEnterBleedout()
	TransformSpell.Cast(self)
endEvent

よく使うもの
OnDying OnAnimationEvent - Form


配列

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

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

上の画像をpapyrusで書くと
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です。

インデックスは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