Mod作成時の罠

Modを作ろう

Modを作る過程でハマった罠についてまとめます。

スクリプトの更新が反映されない

スクリプトは読み込まれた時点でセーブデータに残るので、後からスクリプトを更新しても完全には反映されません。

ロジックは反映されますが、変数の初期値は反映されません。

スクリプトの変数(プロパティ)はスクリプトが読み込まれた時点で初期化されて値が決定されます。あとからスクリプトを編集して初期値を変更してもゲームには反映されません。変数を新規に追加した場合は、次にゲームをロードした時点で変数が初期化されます。

例えば、Triggerにスクリプトを張り付けた場合で説明します。プレイヤーがTriggerのあるCellに進入するとTriggerが読み込まれてスクリプトも読み込まれます。これ以降にセーブするとスクリプトの更新が反映されなくなります。Cellに進入する直前のセーブデータを用意しておきましょう。

クエストが動かなくなる

Run Onceにチェックを入れてゲームを起動すると、そのクエストは1度だけ開始するものとして扱われるため、停止させたが最後、二度と開始できなくなります。Run Onceのチェックを外してゲームをロードしても、セーブデータ内に保存されているためか効果はありません。

この場合は以下のいずれかの対処が必要になります。

  • セーブデータのクリーニング
  • ニューゲーム
  • クエストを別のForm IDで作り直す

フォームが出てこない

ESLフラグの付いたプラグイン(ESLファイルやESPFEファイル)は、フォームIDの範囲が狭いので、範囲外に出ているとうまく動作しません。特にESPFEはCKでも編集できるので、CKでフォームを新規作成すると、大抵フォームIDが範囲外のものが割り当てられてしまうため、SSEEditで振り直しが必要です。SSEEditで読み込むとログにエラーが出るので確認できます。Modのリリース前には要チェックです。

計算式の勘違い

Papyrusには変数に型があり、Int型が整数、Float型が浮動小数点なので、式の中で整数の計算をさせてしまうと、小数点以下が失われます。

計算式の勘違い (papyrus)

Float fDamage
Int iDamageConfigValue = iGlobalDamageValue.GetValueInt() ; MCMで5に設定されている(5%という意味)
Float fMaxHealth = akTarget.GetActorValueMax("Health") ; ヘルスの最大値

; これは間違い
; なぜならiDamageConfigValue / 100は0になる
fDamage = fMaxHealth * (iDamageConfigValue / 100) as Float

; これが正しい
fDamage = fMaxHealth * (iDamageConfigValue as Float / 100.0)

ちなみに、上のコードは例として書いたのですが、最終的にFloat型が必要な場合はグローバル変数をFloat型で作った方が良さそうです。MCMで%表記にしたい場合はMCMの中で100倍すればいいです。なぜかというと、MCMを開いた時に100倍するのと、ゲーム中に常に100倍して使うのとでは、前者の方がはるかにマシだからです。

演算子の優先順位の勘違い

ifやwhileの条件式を間違えると正しく動作しません。

以下のwhileは無限ループです。

演算子の優先順位を間違えて無限ループする例 (papyrus)

Int i
Bool bLoop = true

; 最大で100回繰り返す
; ただし、途中で処理が失敗したら直ちに終了する
while i < 100 && bLoop
    if SomeFunction()
        i += 1
    else
        bLoop = false
    endif
endwhile

この条件式は「iが100未満、かつbLoopがtrue」という意図で書いたものですが、実際は&&の方が優先されてしまうので、「iが100 && bLoop未満」となっています。

IsWeaponOutではなくIsWeaponMagicOutを使う

CKの条件式にIsWeaponOutがあります。名前からしてBooleanかと思いきや、Integerです。

しかも、0で納刀、1で抜刀(素手)、2で抜刀(武器、魔法)ということらしいのですが、状況によっては抜刀しているのに0が返ってくることがあるようです。

抜刀か納刀かを見るにはIsWeaponMagicOutの方がいいです。

Find関数は-1を返す

配列の中にあるかどうかを調べるFind関数はインデックスを返します。

あるかどうかを調べるというよりは、配列の中の位置を返すということです。位置は0から始まり、見つからなかった場合は-1を返します。

Form[] MyItemList = new Form[3]

MyItemList[0] = hoge
MyItemList[1] = fuga
MyItemList[2] = moge

Int i = MyItemList.Find(hoge)    ; iは0になる
Int j = MyItemList.Find(fuga)    ; jは1になる
Int k = MyItemList.Find(moge)    ; kは2になる

あるかどうかは-1かどうかを調べます。

; 間違い、なぜならhogeは0になるのでif文は失敗となってしまう
if MyItemList.Find(hoge)
    ; 見つかったときの処理
endif

; 正解
if MyItemList.Find(hoge) != -1
    ; 見つかったときの処理
endif

Papyrusの配列はポインタである

配列を複製したいときに、配列をまるごと代入すると参照になってしまいます。

以下の例は間違いです。

String[] Original = new String[10]

Original[0] = "hoge"
Original[1] = "hogehoge"

String[] Copy = Original

Copy[0] = "fuga"
Copy[1] = "fugafuga"

Debug.Trace(Original[0])    ; fuga
Debug.Trace(Original[1])    ; fugafuga

複製したいときは要素ごとに値をコピーする必要があります。

String[] Copy = new String[10]
Int i = 0

while i < 10
    Copy[i] = Original[i]
    i += 1
endwhile

Copy[0] = "fuga"
Copy[1] = "fugafuga"

Debug.Trace(Original[0])    ; hoge
Debug.Trace(Original[1])    ; hogehoge
タイトルとURLをコピーしました