SkyrimMOD作成wiki
http://w.atwiki.jp/skyrim_mod/
SkyrimMOD作成wiki
ja
2024-02-27T22:55:19+09:00
1709042119
-
CK Tips
https://w.atwiki.jp/skyrim_mod/pages/28.html
#contents
**CKの非公式修正パッチを適用してCKの不具合やパフォーマンスを改善する(SE版)
SE版のCKには主要なパッチは3つあり、どれもCKに関する様々な不具合を修正しパフォーマンスを改善するため3つとも適用する事をオススメします。
特にSSE Creation Kit Fixes Updateは導入して設定を行う事で&color(#F54738){CKで日本語が文字化けされずに表示できるよう}になります。(ただし非推奨との事。問題が発生したらオフにする事)
また、CKによるMod編集をする上で下記の問題はかなり煩わしい不具合なので早めにパッチを適応した方が良いです。
・espを読み込み中に無限ロードやエラーによる強制終了がランダムで発生(Mod環境が充実すると発生する可能性が大きくなる)
・特定の操作を行うとギズモ(オブジェクト選択時に出る移動や回転のアイコン)が突然表示されなくなりCKを開き直すまで表示されなくなる
***[[Unofficial Creation Kit Patch>>https://www.nexusmods.com/skyrimspecialedition/mods/41195]]
インターフェイス周りで様々な修正が施されるパッチです。
[導入方法]
&color(#F54738){AE版でなくても「Unofficial Creation Kit Patch AE」の方をダウンロードしてください。}AEと書かれてますがSE版にも対応されてます。
パッチファイルをCreationKit.exeのあるフォルダに全て入れて、UCKP-Backup.batを実行してexeファイルのバックアップを取ります。
その後、UCKP-Patcher.exeを実行しCreationKit.exeのあるフォルダを指定してパッチを適用してください。
***[[SSE CreationKit Fixes>>https://www.nexusmods.com/skyrimspecialedition/mods/20061]]
Unofficial Creation Kit Patchで修正されるものとは別の問題点を改善します。
[導入方法]
Main filesの「CK64Fixes Release 3.2」をダウンロードして、
パッチファイルをCreationKit.exeのあるフォルダに全て入れてください。
***[[SSE Creation Kit Fixes Update>>https://www.nexusmods.com/skyrimspecialedition/mods/71371]]
SSE CreationKit Fixesを別作者が改善したパッチです。
また、マルチバイト文字(日本語)を文字化けさせずに表示できるように改善されます。
[導入方法]
Main filesのファイルをダウンロードし、
SSE CreationKit Fixesを導入後に上記のパッチで上書きしてください。
日本語を表示する場合は導入後にskyrim64_test.iniの[CreationKit]項目にてUnicodeをtrueにする事で日本語が文字化けされずに表示されるようになります。
[CreationKit]
~(略)
Unicode=true
**CKで複数のマスターファイルを読む(multiple master file selected load. load operation abortedのエラーが出る場合)
スカイリムのルートフォルダ(TESV.exeがあるところ)にある&font(b,#134f5c){SkyrimEditor.ini}をテキストで開いて編集します。
[General]項目に以下のように&font(b,#134f5c){bAllowMultipleMasterLoads=1}を追加してください。
[General]
bAllowMultipleMasterLoads=1
補足:&bold(){旧CK}は&bold(){SkyrimEditor.ini}で&bold(){新しいCK}(SSE Creation Kit)は&bold(){CreationKit.ini}にファイル名が変わっています。
**CKでDLCのesmファイルを読む
スカイリムのルートフォルダ(TESV.exeがあるところ)にある&font(b,#134f5c){SkyrimEditor.ini}をテキストで開いて編集します。
[Archive]
SArchiveList=
SResourceArchiveList=
の最後尾に, Dawnguard.bsaというふうに追加する。
例:
SArchiveList=Skyrim - Textures.bsa, Skyrim - Meshes.bsa, Skyrim - Animations.bsa, Skyrim - Voices.bsa, Skyrim - Interface.bsa, Skyrim - Misc.bsa, Skyrim - Sounds.bsa, Skyrim - VoicesExtra.bsa, Skyrim - Shaders.bsa, Update.bsa, Dawnguard.bsa, HearthFires.bsa
**フィルターのワイルドカード
CKのフィルターはワイルドカード(ワイルドカード=*の部分がなんであれ一致します)が使えます。
たとえば、encbanditは通常のエンカウントする山賊ですが、山賊の両手持ちだけ抽出したい場合、
encbandit*2hと入力します。
#image(WildCard.png)
**プラグインを読み込むときの最初のエラーをスキップする設定にする
スカイリムのルートフォルダ(TESV.exeがあるところ)にある&font(b,#134f5c){SkyrimEditor.ini}をテキストで開いて編集します。
[MESSAGES]
bBlockMessageBoxes=1
※ただし全部のエラーをスキップしてしまうのでエラーがよくわからないデメリットもあります。スカイリムフォルダのEditorWarnings.txtにエラーが記載されてます。
**.espファイルで編集された項目一覧の表示(編集した項目の解除)
&font(b,#134f5c){File -> Data... Plugin/Master Files}の中から編集項目を一覧したいプラグインを選択
赤枠&bold(){Details...} ボタンを押すと一覧が表示されます。
#ref(details.png)
この項目の中で編集をとりやめたい(Ignored)場合はその項目を選んで&font(b,#134f5c){Deleteキー}。
なにやらメッセージが出ますが&bold(){はい}を選択。
編集をとりやめた項目は青枠のIマークが出ます。
変更を適用するにはこのあと、そのespファイルを&font(b,#cc0000){CKで読み込みセーブが必要}です。
**espファイルをマスター指定するには
オリジナルのCreation Kitではespのマスター指定をサポートしてません。
TES5Edit(SSEEdit)でマスター指定してください。
ネット検索で「espファイルをマスター指定」するとWrye Bashでespファイルをマスター指定する方法も検索結果にでてきますが、2023/8時点でダウンロードできるWrye Bashは仕様変更でespファイルをマスター指定できなくなっています。
**espファイルをマスター指定したパッチファイルがCreation Kitで保存できない場合の対処
非公式修正パッチを導入していない、オリジナルのCreation Kitではセーブ時にespのマスター指定が解除される仕様になっています。
CKの非公式修正パッチのSSE CreationKit Fixes(SSE Creation Kitのバージョンが1.5 あるいは 1.6の場合はSSE Creation Kit Fixes Updateも必要)を導入することでパッチの保存が可能となります。
**マスター指定の解除
&font(b,#134f5c){File -> Data...]で該当のespファイルを選択して右上ParentMastersの欄で解除したい.esp/.esmをCtrl+Deleteで削除します。
あとは該当のespをアクティブにして読み込みし保存します。
espファイルのマスター指定の解除については非公式パッチなしの環境でも実行可能です。
**espファイルのバックアップ
&font(b,#134f5c){スカイリムのフォルダ\Backup}にespのバックアップがあります。
保存時の時にここにも保存しているようです。
**レンダーウィンドウでのキー操作一覧
[[Creation Kit Keyboard Mapping>>http://www.creationkit.com/index.php?title=Creation_Kit_Keyboard_Mapping]]
**プレビューウィンドウの操作方法
視点の回転:Shift押しながらマウス移動
視点の上下左右移動:スペースキー押しながらマウス移動 or マウス中クリックしながらドラッグ
Mキーでマーカーのオン/オフ
クリックでオブジェクトの選択
ダブルクリックでEdit出す
**プレビュー画面でモデルデータを確認する。
装備やエフェクトを作った時にCKに登録したとき、espを一旦読み込み直したりファイル名を変えないとプレビューのモデルデータが更新されません。
必ずskyrim.esm等のプラグインを読み込み、ActorでもActivatorでもなんでも右クリックでPreviewを開いておきます。
ドラッグアンドドロップで放り込むだけです。
必ずData下のフォルダにいれることと、ドラッグアンドドロップしたデータは別のメッシュデータ開いても残るようなので、気になるときはプレビューを一回閉じてください。
**スクリプトのプロパティの解除
スクリプトに付いたプロパティが不必要になった場合は、
スクリプトの行を消してから、一旦プロパティ一覧を開いて、OK押さない限りは消えません。
**長さの単位
CK上での長さの単位は2つあります。
ひとつはフィート(ft,feet,単数形foot,フット)
1ft=30.48 cm
だいたい30cm、そのままの意味で(靴履いた状態で)一足分の長さと思うと覚えやすいでしょう。
魔法のRange,Areaなどがフィート単位です。
ふたつめはUnitです。
1Unit=目安1.4cmぐらいです。
参考:[[CK Wiki Unit>http://www.creationkit.com/Unit/ja]]
人間は種族や男女の身長差に関わらずすべて、128Unitです。
Unitはボーンのボックスサイズで規定されているからです。
GetDisatance(),GetHeight(), GetWidth(), GetLength()などがUnit単位です。
**Condition
Spell,Magic Effect, PackageやPerk等の様々なところにでてくるConditionとは&bold(){条件文}です。
この条件にあったら効果が発動します。
#image(Condition.png)
Conditionのチェックは一回たびに発動し、その負荷はスクリプトの関数と同等の重さです。
***ANDとORについて
ANDとORは常に&font(b,#b45f06){その行のあとにしか影響}しません。
&font(b,#073763){A AND&br()B OR}
ならA AND B、つまりAとBが両方真なら効果が発動します。
ORも同じくその次にしか影響しないので
&font(b,#073763){A OR&br()B AND}
ならA OR B、つまりAかBのどちらかが真なら効果が発動します。
2行ならわかりやすいですが複数行だとちょっとややこしくなりますが、
AND/ORは&font(b,#b45f06){直後にしか影響を与えない}ことを考えると分かりやすいです。
#image(Condition.png)
画像のような場合
Gauntlets=手 AND
Helmet=頭 OR
Boots=足 AND
Cuirass=胴 AND
&font(b,#134f5c){手AND(頭OR足)AND胴}になります。
手と胴の装備は必須、頭と足の装備はどちらか一つです。
&bold(){(手 AND 頭) OR (足 AND 胴)}
手と頭が揃ってるか、もしくは足と胴が揃ってる場合に発動します。
ORは直後にしか影響を与えないので、手と頭という()でひとまとめにできません。
その場合は分解して、
&font(b,#134f5c){(手 OR 足) & (頭 OR 足) &(手 OR 胴) & (頭 OR 胴)}
にすればいいのです。
手 OR
足 AND
頭 OR
足 AND
手 OR
胴 AND
頭 OR
胴 AND
処理する数が倍になってしまうので、あんまり複雑な条件はおすすめしません。
スクリプトとして違ってエラーを吐き出さなくてわかりにくいのも難点で、負荷はしっかりあるので、
基本的には簡潔な条件にしましょう。
**クラフトカテゴリの追加の仕方
メニューバーのGameplay->Setting...でFilterにkeywordと入力。
sGenericCraftKeywordName08か~09のString Valueにカテゴリの表示名を書きます。
ObjectWindowのKeywordに新しくキーワードを作り、適当な名前をつける。(例"ArmorMaterialMOD")
Gameplay->Default Objects...を開く。
Type FilterをKeywordsにして、Keyword - Generic Craftable Keyword 08 か 09に新しく追加したキーワードを選択。
あとは任意のWeaponやArmor等にKeywordsをAddしてください。
※DLCで01~07, 10は使ってます。被った場合はその他カテゴリに移動します。残り二枠しかないのでmodder間での協議の必要性あり?
**戦闘の割り込み
戦闘時にNPCに何かをさせようとすると中々うまく行きません。
戦闘中のAIは優先度の高い順から処理するので、事あるごとに上書きや中断されます。
たとえば、フォロワーが回復呪文使用中に殴られたりすると中断されてしまいます。
暗黙のAI上の割り込みは変更できないのと、優先度が分かりにくいのが厄介です。
これらの対処法は「素早く」「確実に」 処理を終わらせることです。
***素早く処理する
まずは動作そのものをなるべく速くします。
新規で魔法を作りCasting Timeを短縮したり、早く終わるモーションを使うようにしたりなどです。
スクリプトの方面では処理に時間のかかる関数を使わないことです(Find系の関数やIsInMenu()など)。
同Wiki内の[[スクリプト最適化Tips>http://www50.atwiki.jp/skyrim_mod/pages/18.html#id_98b12eda]]を参考に。
***確実に処理する
Procedureやシーンなどの処理は画面と同期する必要があるため、Waitを挟むことで安定する場合があります。
事例によって違ってくるので詳しくは書きませんが、ifやconditionを使っての条件制約や、中断された場合の処理を加えることで、確実に処理できるようにします。
そもそも、中断させられるのを防ぐのも有効です。
処理中にStaggerの耐性を持たせたり、SetGhostで当たり判定なくしたりなど。
同Wiki内の[[関数の処理について>http://www50.atwiki.jp/skyrim_mod/pages/18.html#id_bc5c46de]]も参考にどうぞ。
**魔法を使えないNPC(動物など)に魔法を使わせる
***1.まず使わせたい魔法のAbility版を作る
そのNPC専用に新規またはコピーしてSpellを作成し、SpellのTypeをAbilityにしてます。
***2.Packageを作ってUseMagicを使う
Packageの作り方はCharacter->Package->UseMagicをDuplicateします。
コピーしたものは名前を適当に変えて、いいえ(IDを新しくするか)→はい(レファレンスを引き継ぐか)。
コピーしたものを開いて右上Package TypeをPackage TemplateからPackageに変えます。
Public Package Data内のNameがSPELLTypeがTargetselectorの項目を選んで、
右側、SelectedPackageData内のSpell Range Targetボタンを押します。
出てきたPackageTarget欄のAnyObjectのObject IDから作ったAbilityを選びます。
そのほかの設定は作った魔法によるのでいろいろ試してください。
***3.Packageを適用する
2で作ったPackageを使わせたいNPCに適用させます。
Character->Questから新規でQuestを作ります。
Quest Aliasタブで新規Aliasを作ります。
新規で作ったAliasを開き、Alias Package Data欄内で右クリックしてAddします。
2で作ったPackageを選択します。
Script側で作ったAlias.ForceRefTo(NPC)でAliasを魔法を使わせたいNPCにかぶせてOnPacakageEnd等のEventまたはWaitでその後作ったAlias.Clear(NPC)でAliasから開放します。
これでバニラでAbilityを使えるアクターはすべてその魔法を使えるようになります。
犬や山羊や蟹などが魔法を使えるようになります。Abilityが使えない種族もいるので注意。
**WeatherのDirectional Ambientの方向
大雑把に方角で書きましたが、実際には面の向きと適用される色はノーマルマップで判別されます。
X-:西側の色
X+:南側の色
Y+:北側の色
Y-:東側の色
Z+:上(天井など)
Z-:下(地面)
例:日が昇るのは東で、太陽の当たる面の環境光なら、X-(太陽光の色も含まれる点に注意)
**ConditionのGetCurrentDeliveryType
魔法のDeliveryType判定です。
0:Self
1:Contact
2:Aimed
3:TargetActor
4:TagetLocation
**装備スロットの割り当て
Armorには装備箇所を決めるスロットがあり、それをどれに割り当てるかはArmorのBiped Objectの項目で変更可能です。
30 - 頭部
31 - 髪
32 - 胴体
33 - 両手
34 - 前腕(ひじから手首まで)
35 - 首飾り
36 - 指輪
37 - 足
38 - ふくらはぎ
39 - 盾
40 - 尻尾
41 - 長髪
42 - 頭飾り
43 - 耳
50 - 切断された頭部
51 - 切断部分
61 - FX01(ドラゴンアスペクト)
NEXUSフォーラムで提案されている未使用Slotの割り当て
(これはnif skopeのBSDismemberSkinInstanceのPartitionsリストにもとづいてます)
44 - 顔、口
45 - 首
46 - 胸、上着
47 - 背中
48 - その他1
49 - 腰1、上着
52 - 腰2、下着
53 - 脚1、上着、右脚
54 - 脚2、肌着、左脚
55 - 顔、装飾品
56 - 胸2、下着
57 - 肩
58 - 腕2、肌着、左腕
59 - 腕1、上着、右腕
60 - その他2
参考:
[[CK TIPS Biped Object Slot List>http://naaaaikikvicnvicn.blog.fc2.com/blog-entry-55.html]]
[[Biped Object>http://www.creationkit.com/Biped_Object]]
**マップに設置したアクターのPersistentフラグについて(アクター消滅バグについて)
フォロワーModとかを作成する際にアクターを設置した後にespを保存した後、
xEditでセルに設置したアクターを見ると大抵の場合はTemporaryというツリーの下に
設置したアクターのレコードが作成されています。
これは設置したオブジェクト(アクター)にRecord FlagsのPersistentフラグが設定されてないためであり
Persistentフラグが設定されている場合はPersistentという名前のツリーの下にレコードが表示されるようになっています。
(TemporaryのアクターのRecord FlagsにてPersistentを設定するとPersistentツリーに移動する)
基本的にPersistent(永続的)フラグがついているアクターの場合はゲーム開始時の地点でアクター情報が生成され
メモリに情報がキープされるようになり、同一セルに居ない場合でもメモリに保持されたままとなります。
逆にPersistentフラグがついていないアクターの場合はプレイヤーがそのアクターの下に行って
初めてアクター情報が生成されメモリに情報がキープされます。
滞在しているセルから離れたりそのセルに入る前のセーブをロードしたりすると監視から外れてメモリ上から情報をアンロードする処理がされます。
一見、Persistentフラグを付けない方がパフォーマンスに優れているように思えますが
このアンロード処理について時折、&color(#FF0000){アクターの存在そのものを一時的に消滅させてしまう}という、
かなり悪質な(しかも悪質な割に知名度が低いため対処法が全然載ってない)バグが存在します。
(プレイ途中でのセーブのロード時に発生しやすくアクターが設置された内部セルに入った後に内部セルに入る前のセーブ、特に設置したアクターと初めて遭遇する前のセーブをロードした場合は高確率で発生します)
このアンロードで消滅というのはdisable状態による非表示状態になるというものではなく、
&color(#FF0000){本当にアクターの存在そのものがゲーム上から消滅するため}
&color(#FF0000){&bold(){コンソールからのpridでの指定も不可能となってしまいます。(pridで指定しても存在しないと表示される)}}
幸いな事にこのバグは&bold(){一時的なもの}であるためゲームを再起動するか
該当するアクターが一緒にいる状態でセーブしたデータをロードすれば直りますが
それまでは&color(#FF0000){該当のアクターはゲーム内から消滅したままとなります。}
この現象は&bold(){Persistentフラグを設定しているアクターならば発生しません。}
もし、フォロワーMod等でユニークなアクターを設置した場合はPersistentフラグを付ける事をオススメします。
逆にフォロワーでもなくクエストに一切関係無いアクターの場合は
存在が一時的に消滅してもゲームに支障は無いはずですのでPersistentフラグを付けなくても問題ありません。
2024-02-27T22:55:19+09:00
1709042119
-
リンク
https://w.atwiki.jp/skyrim_mod/pages/45.html
&bold(){SkyrimやMOD制作に関するリンク集}
&s(){[[Creation Kit Wiki>http://www.creationkit.com/]]}
(2023年の11~12月ぐらいから突如wiki全体が大幅リニューアルされてしまい、Perk Entry Point等幾つかのページが消失。2024年2月頃にメンテナンスと表示されたままの状態となり閲覧不可になってしまったため↓のクローンサイトを代わりに参照してください)
[[Creation Kit Wiki by uesp>https://ck.uesp.net/wiki/Main_Page]]
(2021/12/1時点のCreation Kit Wikiのクローンサイト)
[[UESP:Skyrim>>http://www.uesp.net/wiki/Skyrim:Skyrim]]
[[CG Textures>>http://www.cgtextures.com/]]
[[Skyrim Wiki JP>>http://wiki.skyrim.z49.org/]]
[[キャラメイクwiki>>http://www4.atwiki.jp/skyrimmaking/]]
&bold(){MOD作成に関する解説しているサイトの暫定まとめ}
[[Skyrimshot>>https://tktk1.net/skyrim/]]
フォロワーやアニメーション、髪メッシュの作り方など
[[方向音痴のスカイリム>>http://skyrimeasy.blog-rpg.com/creationkit]]
AIパッケージの記事が充実
[[Skyrim箱庭DIY>>http://skmod.hatenablog.com/]]
検証や競合対策などが充実
[[おばちゃんのスカイリムガイド>>http://obachanskyrim.blogspot.jp/]]
検証記事など
[[Mod Life>>http://barom777.blog.fc2.com/]]
[[PSYCHO DELICIOUS>>http://psychodelicious7294.seesaa.net/]]
[[Dragonporn>>http://dragonporn.ldblog.jp/]]
閉鎖:
[[狭き桃>>http://semakimomo.blogspot.jp/]]、[[小麦粉MoD製作所>>http://komugiko20.blog.fc2.com/]]、[[DovaSoul>>http://dovasoul.com/skyrim-ck-papyrus-index/]]
2024-02-21T15:46:31+09:00
1708497991
-
メニュー
https://w.atwiki.jp/skyrim_mod/pages/2.html
**最初に
-[[mod制作をはじめる前に]]
-[[Skyrim Special Edition]]
**[[MOD制作Tips]]
-[[CK Tips]]
-[[Nif Tips]]
-[[ファイルの種類]]
-[[データ集]]
-[[テクスチャ作成]]
**[[スクリプト]]
-[[Papyrus入門]]
-[[逆引きリファレンス]]
-[[Tips]]
-[[SKSEプラグイン]]
**[[チュートリアル]]
-[[ポーズ・モーション作成]]
-[[鉛筆>鉛筆を作る]] / [[サイコロ>サイコロを作る]] / [[Tシャツ>Tシャツを作る]]
-[[武器作成]] [[2>Nifファイル作成]] [[3>CKでアイテムを登録]]
-[[GND.nif作成]]
-[[スクリプトmod製作]]
-[[SkyUI MCM]]
-[[ゲーム設定の調整]]
-[[装備のBBP対応]]
-[[チュートリアルリンク集]]
**[[ツール]]
**[[Blender>Blender関連まとめ]]
-[[Blenderで入出力]]
-[[Blender入門]]
-[[モデリング入門]]
-[[Blender Tips]]
-[[Blenderリンク集>Blender関連まとめ]]
-[[Blender アドオン]]
**[[その他]]
-[[ENB]]
-[[Modの英語]]
-[[Mod公開後のトラブル対策]]
**[[リンク>リンク]]
-&s(){[[Creation Kit Wiki>>http://www.creationkit.com/index.php?title=Main_Page]]}(2024年2月頃に事実上サイトが閲覧不可となっているため↓のクローンサイトを代わりに閲覧してください)
-[[Creation Kit Wiki by uesp(2021/12/1時点のクローンサイト)>>https://ck.uesp.net/wiki/Main_Page]]
-[[UESP:Skyrim>>http://www.uesp.net/wiki/Skyrim:Skyrim]]
-[[Textures.com>>http://www.cgtextures.com/]]
-[[Skyrim Wiki JP>>http://wiki.skyrim.z49.org/]]
-[[キャラメイクwiki>>http://www4.atwiki.jp/skyrimmaking/]]
----
**更新履歴
#recent(5)
----
合計:&counter()
今日:&counter(today)
昨日:&counter(yesterday)
-&link_editmenu(text=メニュー編集)
----
2024-02-21T15:42:58+09:00
1708497778
-
トップページ
https://w.atwiki.jp/skyrim_mod/pages/1.html
*メインページ
Skyrim MOD作成Wiki へようこそ。
このWikiは『The Elder Scrolls V: Skyrim』のMOD制作に関する共同運営サイトです。誰でも編集可能です。
&bold(){mod作成に困ったときはまずここを見ておこうというwikiを目指しています。}
*それでも分からないときは?
スカイリムコミュニティやmod作成・情報交換用に [[Discord>>https://t.co/0lco5wibVy]] を用意しました。
テキストチャットで雑談もできます。
Discordの説明は[[こちら>>https://www.gamespark.jp/article/2016/06/22/66824.html]](外部サイト)。
*メニュー
[[MOD制作Tips]]:役に立つ情報や基本的なデータはここで取り扱います。
[[チュートリアル]]:実践を通して学べます。
[[ツール]]:mod制作に役に立つツールの紹介です。
[[Blender]]:モデリングやアニメーションは主にblenderを使用します。
*Mod作成ニュース
SKSE、Nifplugin、Blender等の更新ありましたらこちらに記載していただけると助かります。
----
[[最近の更新はこちらから確認できます。>更新履歴]]
//2016/10/30 [[Skyrim Special Edition]]のページの作成
//2016/06/29 Animation RigCustom 1.5更新に合わせてポーズの記事を大きく改定。
//2016/06/11 スクリプトtipsを大幅更新。情報が古びている部分があるので全体的に整理中。
//2016/06/07 bsaファイルの展開ソフトはDLC Dragonbornが開けないFO3Archiveから、B.A.Eに全面的に変更してます。
//BSA Browser等より圧倒的に使いやすいので乗り換え推奨します。
//2016/05/28 [[Tシャツを作る]]のウェイトと出力まで完成してます。貴重な日本語で書かれたウェイト付きモデルの作成チュートリアルだと思います。
//他のページやメニューも全体的に細かく整理してます。
//2016/04/08 Nif plugin for Skyrimを更新しました。Niftools //Dismember Flags Panel の+ボタンが出ない問題を(勝手に)修正。
//----
&bold(){〇Nifplugin}
2023/11/4 [[Blender Niftools Addon - v0.1.1>>https://github.com/niftools/blender_niftools_addon/releases/tag/v0.1.1]]がリリースされました。
blender 2.8~3.6まで。4.0は非対応
揺れない(=non skined)SE用メッシュのエクスポートを実装
2022/5/27 [[Blender Niftools Addon - v0.0.14>>https://github.com/niftools/blender_niftools_addon/releases/tag/v0.0.14]]がリリースされました。
kfファイルによるアニメーションのインポートに対応済み。他
&bold(){〇NifSkope}
2023/9/24 [[NifSkope 2.0.dev9>>https://github.com/hexabits/nifskope/releases/tag/v2.0.dev9]]がリリースされました。
2020/7/9 [[NifSkope 2.0.dev8>>https://github.com/hexabits/nifskope/releases/tag/v2.0.dev8]]がリリースされました。
2017/12/19 [[NifSkope 2.0.dev7>>https://github.com/niftools/nifskope/releases/tag/v2.0.dev7]]がリリースされました。
Array/Compound(頂点カラー等)のコピー&ペーストが可能に。Blockのstringがコピペ時に維持。
Spell機能のBSTriShapeへの対応とアニメーション関係の機能追加。
BC圧縮のddsの表示。他多くの修正や既存の機能の改善。
2016/11/15 [[NifSkope 2.0.dev6 11-07>>https://github.com/jonwd7/nifskope/releases/tag/v2.0.dev6]] BSXやTrishapeのフラグが正しく変更。メモリリークの修正されてます。
2016/11/06 [[NifSkope 2.0.dev6>>https://github.com/jonwd7/nifskope/releases/tag/v2.0.dev6]]がリリースされました。
開発版ですが1.10から大幅に刷新され非常に使い勝手が良くなったため試す価値は十分にあります。
・dev6でSE版対応や、SE版に役に立つ機能が追加。さらに高速化されました。
&bold(){2.0の特徴}
・見た目がゲーム上と非常に近い。
・3倍以上の高速化
・エフェクトアニメーション対応
・本体のbsaファイルのテクスチャを読み込み、Open Archive機能でBSAファイルから解凍せずとも直接メッシュファイルが開けます。
&bold(){〇Animation Rig by tktk}
2021/4 Animation Tools N3+を別作者達がカスタマイズした[[SAE>>https://www.loverslab.com/applications/core/interface/file/attachment.php?id=1129255]]リリースされました。
Animation Tools N3+のように元の物から独立していないのでAnimation Tools N3+が必須です。
くわしくはLovorsLabの[[詳細ページ>>https://www.loverslab.com/topic/164692-skyrim-and-blender-28-working-with-meshes-animation-hdt-smp-and-behavior-in-2021/]]を参照。ダウンロードにはLoversLabへのログインが必要
2020/9 [[Animation Tools N3+ 1.61>>https://tktk1.net/skyrim/mymod/animation-tools-n3-plus/]]リリースされました。
2016/06/29 [[Animation RigCustom 1.5>>https://tktk1.net/skyrim/mymod/animation-tools-n3-rig-custom/]]リリースされました。作成したモーションがズレず、さらに使いやすく。
----
公式で用意されている&s(){[[Creation Kit wiki>>http://www.creationkit.com/Main_Page]]}は情報量が多く必読ですが、
2023年12月頃にwiki全体のリニューアルで一部ページ消失、2024年2月頃にメンテナンスと表示されたまま閲覧不可能となっています。
幸い有志によるwikiのクローンサイトが存在しており2021年12月頃の公式wikiとほぼ変わらない内容となっております。
[[Creation Kit Wiki by uesp>https://ck.uesp.net/wiki/Main_Page]]
ただしwebでの日本語訳された記事は少なく、有志による翻訳ダウンロード版もご活用ください。
&s(){【[[CK Wiki日本語ページ(web)>>https://www.creationkit.com/index.php?title=Main_Page/ja]]】} と 【[[CK Wiki日本語版(DL) >>https://www.nexusmods.com/skyrim/mods/15919?tab=description]]】
----
2024-02-21T15:40:14+09:00
1708497614
-
Tips
https://w.atwiki.jp/skyrim_mod/pages/18.html
ここではスクリプトの小技やタメになる情報を扱います。
#contents
*スクリプト作成時の注意点
**初心者はSE版のみ対応として作成を行う事(慣れるまでLE・SE両用を作成しない)
SE版が販売して数年はSKSEの更新等で不安定だった事もありLE版が主流でしたが、現在はSE版の販売から10年以上経過しており環境が比較的安定したため、SKSEプラグイン等のModリソース開発者の多くがSE版に移行しています。
それに伴いSPID等、Mod作成用のリソースがLE版での更新がされなくなっている、またはSE版のみリリースしてる所が多くなっているため、LE版と比較してSE版の方が開発リソースは豊富となっています。このため、LE版で高度な事をやりたい場合、特にLE・SE版両用を行いたい場合は作成のハードルが高くなります。
(特にこのwikiでも紹介しているPapyrus関数拡張SKSEである[[powerofthree's Papyrus Extender>https://skyrimspecialedition.2game.info/detail.php?id=22854]]はLE版の更新が&bold(){&color(#FF0000){2020/07/08}}が最後となっており、当然SE版と比べてやれる事が少なくなってます)
また、慣れない内にLE・SE両用のMod作成をした場合、配布後のトラブル対応に苦労する可能性があるため初心者はSE版のみの開発をオススメします。
**アルゴリズムを組む前にSKSEプラグインを確認する
アルゴリズムを組む前に、wikiメニュー一覧の「SKSEプラグイン」に記載されてる「主なPapyrus関数拡張SKSE」のSKSEをダウンロードし、それのソースを確認して実現したい処理がそのSKSEが提供しているPapyrus関数一つで行えるかを確認しましょう。
(例としてInt型をstring型に変換、string型の数値をInt型に変換する処理を作りたい場合、自前でアルゴリズムを組もうとするとかなり手間が掛かりますが"powerofthree's Papyrus Extender"のIntToString、StringToInt関数を用いる事で容易に実行可能でかつ自前でアルゴリズムを組んだ処理よりも高速に処理できます)
また、アクターやオブジェクトの状態の操作や確認、アクターやフォームの一覧を取得したい場合は"powerofthree's Papyrus Extender"、AIパッケージ上書きやファイル関連の操作は"PapyrusUtil"を使用する事で大抵の事はできます。
まずはこの2つだけでもダウンロードしてどのような処理が行えるか把握しておくと良いでしょう。
**コンソール経由でしか処理できないものもある
Papyrusで実行したい関数が無かった場合はコンソールコマンドも確認するようにしましょう。
例えばレベル設定はPapyrusではプレイヤーのみしか行えずNPCに対してはコンソールコマンドでしかレベル設定はできません。
SKSEプラグインのConsoleUtilを利用する事でPapyrus経由でのコンソールコマンドの実行が可能となります。
(ConsoleUtilは指定した文字列をコンソールに出力する関数があるため、デバッグ等でも便利です)
コンソールコマンドの表については以下のサイトが参考になります。
[[https://www.creationkit.com/index.php?title=Category:Console_Commands]]
[[https://elderscrolls.fandom.com/wiki/Console_Commands_(Skyrim)]]
[[https://en.uesp.net/wiki/Skyrim:Console]]
**デバッグのためエフェクトや状態をコンソールから確認できる環境を整える
LE版は[[Mfg Console>https://www.nexusmods.com/skyrim/mods/44596]]、SE版は[[More Informative Console>https://www.nexusmods.com/skyrimspecialedition/mods/19250]]を導入すれば、コンソール画面で対象を選ぶだけで対象の状態の確認が行えるようになります。
アビリティは付与されたけどマジックエフェクトがアクティブになってない等の確認も簡単に行えるようになるのでデバッグが非常に容易になります。
**Papyrusでキー入力系のスクリプトを作成する時は高速なレスポンスを求めた処理は望まない
キー入力後からの判定や処理は冗長化しないように可能な限り最適化するように心掛けましょう。これを怠るとキー入力直後に処理を実行したいのに入力後しばらくした後に処理が実行されるという事が起こります。
PapyrusはSkyrimのバックグラウンド処理の仕組み上の問題からどのように最適化しても基本的には僅かでも遅延は起こりえるという事を念頭にスクリプトを作成してください。
また、&color(#3B4EF0){『敵の攻撃がヒットする直前に特定のボタンを押せば相手の攻撃をキャンセルor自身を無敵化させてダメージ無効化』}のような非常に早いレスポンスが求められるものは&color(#F54738){&bold(){Papyrusだけで実現するのはまず不可能}}です。
実際にやろうとすると「判定処理→判定成功→相手の攻撃キャンセルor無敵処理」の判定成功からの判定成功後処理を行う直前で攻撃を受けてしまい、その後に判定成功後の処理が実行されるという事態がよく起こります。
(特にスクリプトのスタックが発生しやすい大規模戦闘では判定処理成功→攻撃を受ける→1秒以上経過した後に判定成功処理が行われるという事態が起こります)
高速なレスポンスを要求されるModを作成したい場合はSKSEプラグインを作成するか、Skyrim Platformでスクリプトを作成してください。
**戦闘が絡むスクリプトの動作確認について大規模戦闘下での確認は行う事
数人程度の敵と戦闘して問題ない事があっても内戦などの大規模戦闘ではスクリプト遅延の多発は容易に起こります。
このため、戦闘関連のスクリプトの動作確認は数人程度の敵と戦闘して問題ないか確認した後、大規模戦闘で動作に支障が無いかの確認をしましょう。
負荷テスト確認を行う場合は事前に「ホワイトランの戦い」等の大規模戦闘前のセーブデータを用意しておくと確認しやすくなります。
また、[[Assault on Valenwood>https://www.nexusmods.com/skyrimspecialedition/mods/48349]]というModは内戦クエスト以上の大規模戦闘かつ開始が非常に容易のためこちらを導入しての確認もオススメです。
**ニューゲーム、途中導入での動作確認は怠らない
ニューゲームで開始時、ゲーム途中からのMod導入どちらでも動作に問題ないか念の為確認しましょう。
スタート地点変更Modがあると確認がしやすくなります。
**(SE版のみ)Papyrusに慣れたらSkyrim Platformでのスクリプト開発を検討する
開発環境の構築に手間が掛かりますが、Skyrim Platform(SP)で開発が行えるならばスクリプト作成はこちらで作成したほうが良いです。理由として
・SKSEプラグインのDLLがスクリプトを読み込んで実行するという形なのでPapyrusの数十倍処理が高速
・特定の処理で実行しない限りはバックグラウンドでは実行せず、フォアグラウンドで実行されるため高速なレスポンスを求められる処理を作成可能
・Papyrusだと処理件数が非常に多くなると一部の処理を後回しにしてしまい、結果スタックが溜まって処理が遅れる(そしてそうゆう状況下だと高確率でまずスタックがどんどん貯まる可能性が高いため最悪フリーズという事態になる)がSkyrim Platformでは処理の後回しによる遅延はなく、仮に処理件数が膨大だった場合でも処理落ちという形で処理遅延の回避が可能となる
・ゲーム中にスクリプトを編集可能、編集内容の反映が可能でデバッグが非常に容易
とPapyrusでのスクリプト作成よりも遥かに利点が多いです。
Skyrim Platformのスクリプト関数はPapyrus関数を準拠にしておりPapyrusで実行できる関数のほとんどが実行可能(開発途中のため一部の関数で確定CTDするため注意)、
さらにSKSEプラグインのPapyrus関数拡張SKSEの関数も定義ファイルを作成すればSkyrim Platform上で実行可能とPapyrusでの経験も応用しやすいためPapyrusに慣れたらSkyrim Platformのスクリプト作成も検討しましょう。
*予約語self
予約語は役割が予め決まっており変数で使用できない語です。
&bold(){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つに分類します。
&bold(){1.同期が必要な[[Latent Function>http://www.creationkit.com/Category:Latent_Functions]]}
スクリプトは上から順に処理していきますが、関数の中でも処理が終わるまでスクリプトが止まるのがLatent Functionです。
代表的な例はWait()です。
Utilty.Wait(1.0)なら1秒経過するまでWaitの部分でスクリプトは待ってます。
次にCast関数です。
これも実際に画面上で魔法が放たれるまで待ってます。
しかし、ノーモーションで魔法が放たれるので、非常に処理が速いです。
CK wiki内のリストには入ってませんがFind系の関数は値が返ってくるまで待ち、処理が遅いです。
&bold(){2.非同期処理の関数}
これは関数の実行が終わったかどうかは関係なく、処理の手続きをしたらさっさと次に進む関数です。
モーションを再生するPlayIdle()がそうです。
モーションが終わったかどうかは関係なく次に進みます。
SoundのPlay()なども同じく、処理の手続きすればすぐ次に行きます。
これと同じ機能で同期処理版がPlayAndWaitです。
スクリプトではなく実際の処理自体はフレームレートやPCの性能に左右されます。
手続された順に再生されるので画面上では遅延が起きるかもしれません。
※仮説上の話ですが、パピルスが言語として遅いのは画面と同期するために意図的に遅くしている可能性も。
&bold(){3.画面と同期する必要のない[[Non-delayed Native Function>http://www.creationkit.com/Category: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
エラーの少なく、処理の早い書き方があります。
&bold(){原則}
&bold(){イベントも関数も呼び出しが少ないほうがよい}
なので重複処理を防止したり、繰り返しの処理をまとめたりが重要です。
**重複処理をさせない→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
;BusyのStateに飛ばす
GotoState("Busy")
;武器の種類を取得
int WeapType = (akSource as Weapon).GetWeaponType()
;両手剣または両手斧槌ならば
if WeapType == 5 || WeapType == 6
Game.GetPlayer().DamageAV("Stamina",5.0)
endif
;重複防止のために0.5秒待機
Utility.Wait(0.5)
;Stateを元の状態に戻す
GotoState("")
EndEvent
State Busy
;StateがBusyの時はOnHitイベントが起こっても何も処理しない
Event OnHit(ObjectReference akAggressor, Form akSource, Projectile akProjectile, bool abPowerAttack, bool abSneakAttack, bool abBashAttack, bool abHitBlocked)
EndEvent
EndState
&bold(){State}はその名のとおり&bold(){状態}を表してまして、指定した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
基本的にスクリプトエンジンはこのエラーを無視するので問題無いですが、エラーログ出すぎると重くなったり不安定になったりする可能性があるのと、ログが読みにくくなるのでその対処法です。
オブジェクトがない状態をオブジェクトは&bold(){None}と返します。
また、&bold(){不必要になったオブジェクトにはNoneを代入する}と安全です。
if PlayerRef != None ; プレイヤーのリファレンスがないときは処理を行わない
.....
endif
もしくは
if PlayerRef == None ; プレイヤーのリファレンスが取得できない時はリターンでイベントの強制終了。
return
endif
***Noneを代入して完全に消す
ObjectReferenceやFormのデータはDeleteを使っただけではスクリプト上では完全に消えてないので、
これを解放するには&bold(){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()の処理を行い、取得しているからです。
つまり例では&bold(){4回処理}してます。
はじめの一行でGetTargetActor()を取得して変数に代入し、あとはそれを当てはめたほうがコードの見通しもよく効率的です。
・よい例
Event SomeEvent()
Actor selfActor = GetTargetActor()
selfActor.AddItem(coolItem, 1)
selfActor.AddSpell(coolSpell)
selfActor.Kill()
selfActor.Resurrect()
EndEvent
例外としてはGetTargetActor()の&bold(){使用が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 = 10 ;時間切れを10秒に設定
While self.Is3Dloaded() == False && i > 0 ;3Dデータが読み込まれるか時間切れまで待つ
Utility.Wait(1.0)
i -= 1
EndWhile
3Dデータが必ず読み込まれるという保証はないため、タイムアウトを設定するのは極めて重要です。
**引数が多い関数やイベントほど重い
パピルスの仕様で、引数から変換するときの処理が重いのです。したがって引数が多いほど重いので
OnHitイベントやFind~()は重いです(OnHitは頻発するのとFindはそもそも探索が遅いのあります)。
別のイベントや関数で代替できるならそちらでしたほうが良い場合もあります。
**安易なスクリプト使用の代替回避をしない
スクリプト使わないパターンでよくあるのが&bold(){魔法のアビリティのコンディション}で代替するパターンでこれは&bold(){極めて悪手}です。
スペルのアビリティのコンディションは&bold(){毎秒条件の判定がある}のでスクリプトで毎秒ループしてるのと同じぐらい重いです。
スクリプトのループと違って、papyrusログにスタックエラーが出ないのでより悪質です。
素直にスクリプト使ってイベント駆動型にしたほうが断然軽く安定します。
**WhileとWaitによる処理及び処理待ちの多用は厳禁
以下のようにWhileとWaitを用いる処理について、Quest等の単一のスクリプトで行う場合ならまだしも、多数のアクターにアビリティを付与して以下のWhileとWait処理を行う場合は状況によってはスタックが溜まってしまい処理遅延発生の元になってしまいます。特に大規模戦闘等のアクターが大勢いる状況だと処理遅延の多発が起こり、最悪CTDが起きる可能性が高まるため注意が必要です。(例としてはEnhanced Blood Textures。アクターの死亡時で以下と同じような処理を行っている)
int i = 0
while i < 100
; ループ処理
~ 省略 ~
Utility.wait(1)
i += 1
endWhile
多数のアクターでどうしてもこのような処理を使用する必要がある場合は、GlobalVariable等にWhile&Wait処理を行っているアクターの総数を記録しておき、処理を行える人数を制限するか、一定数存在したら処理の中断を行うようにする事。(演出系の場合はHasLOS等でプレイヤーが視認できない場合は処理を省略する等を行う)
;一例、GlobalVariableで処理人数を記録して処理可能人数を制限する
if GlobalVariable.GetValueInt() < 5
GlobalVariable.Mod(1)
int i = 0
while i < 100
; ループ処理
~ 省略 ~
Utility.wait(1)
i += 1
endWhile
GlobalVariable.Mod(-1)
endif
*スクリプト最適化実践編
**装備時のイベント重複防止
OnObjectEquippedは何か装備したときに発動するイベントですが、
一つのアイテムにもかかわらず、6回以上(特にエンチャント武器)呼び出されたりするうえ、対処が厄介です。
例では武器装備時に処理したい場合です。
Form PreObj = None
Event OnObjectEquipped(Form akBaseObject, ObjectReference akReference)
; ベースオブジェクトが取得できない、エンチャント、装備が前と同じ場合は何もしない
if akBaseObject == None || akBaseObject as Enchantment || akBaseObject == PreObj
return
endif
; OnObjectEquippedイベントを受け取らないようにする
GotoState("Busy")
; 装備を記憶する
PreObj = akBaseObject
; やりたいことをここに書く
; 装備の記憶を解除する
PreObj = None
; OnObjectEquippedイベントを受け取るようにする
GotoState("")
EndEvent
State Busy
Event OnObjectEquipped(Form akBaseObject, ObjectReference akReference)
EndEvent
EndState
&bold(){解説}
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) || akSource == PreSource
return
endif
;BusyのStateに飛ばす
GotoState("Busy")
;PreSourceに今のダメージ元を代入
PreSource = akSource
;武器の種類を取得
int WeapType = (akSource as Weapon).GetWeaponType()
;両手剣または両手斧槌ならば
if WeapType == 5 || WeapType == 6
Game.GetPlayer().DamageAV("Stamina",5.0)
endif
;重複防止のため0.5秒待機
Utility.Wait(0.5)
;PreSourceをなしの状態に戻す
PreSource = None
;Stateを元の状態に戻す
GotoState("")
EndEvent
State Busy
;StateがBusyの時はOnHitイベントが起こっても何も処理しない
Event OnHit(ObjectReference akAggressor, Form akSource, Projectile akProjectile, bool abPowerAttack, bool abSneakAttack, bool abBashAttack, bool abHitBlocked)
EndEvent
EndState
**スクリプトログをとる(デバッグの仕方)
***スクリプトログの設定
&bold(){&color(#134f5c){マイドキュメント\My Games\Skyrim\Skyrim.ini}}
を開いて、以下の通りにしたあと保存。(項目がなければ追加)
[Papyrus]
bEnableLogging=1
bEnableTrace=1
bLoadDebugInformation=1
すると次回から
&bold(){&color(#134f5c){マイドキュメント\My Games\Skyrim\Logs\Script}}
に&bold(){&color(#134f5c){Papyrus.0.log}}というのが出ますのでメモ帳以外のテキストエディタで開くとデバッグの内容が見れます。
***LogExpertを導入
リアルタイムでログが取れるツールです。
+[[LogExpert>>http://www.log-expert.de/]]をダウンロードします。
+LogExpert.exeを起動します。
+&bold(){File→Open}から、&bold(){&color(#134f5c){マイドキュメント\My Games\Skyrim\Logs\Script\Papyrus.0.log}}を開きます。
+Options→Always on Topを押して、常に手前に表示にします。
+Optionの下あたりにあるメニューのFollow Tailsにチェックを入れます。
***スクリプトにデバッグ情報の記載
ログに書き出すにはDebug.Trace("文字")を使います。""のあとに+を使うと変数や関数の結果を書き出せます。
例:
Event OnObjectEquipped(Form akBaseObject, ObjectReference akReference)
Debug.Trace("Debug:FormName" + akBaseObject.GetName() )
EndEvent
これでゲームとLogExpertを起動して、装備を付けたりすることでスクリプトの動作をリアルタイムで確認できます。
意図する結果になるまで以下の手順で実験してみてください。
+スクリプトを書き直してコンパイル
+コンソールコマンドで reloadscript script名
&bold(){}
*OnAnimationEventで取得できるイベント
RegisterForAnimationEventで登録したアニメーションイベント(モーション)をOnAnimationEventで取得することができますが、
取得できるのとできないのがあります。
☓staggerStart
○staggerStop
Start系のモーションは軒並みダメです。
&color(#134f5c){&bold(){Skyrim - Animation.bsa}}の中の&color(#134f5c){&bold(){meshes\responses\actorresponse.txt}}
というテキストファイルに記載されてるのが使用可能なアニメーションイベントです。
ブロックの動作はじめにイベントを受け取りたい場合にBlockStartだとダメですが、裏ワザ的なやり方があります。
SoundPlay.NPCHumanCombatShieldBlockです。
SoundPlay系は受け取れるので開始時にイベント取得したいなという時にSoundPlayのAnimeEventをあたってみるといいと思います。
Gameplay -> Animations.. -> AnimEventの選択項目でSoundPlayを探して手当たり次第試してみましょう。
・使えそうなイベント一覧
|MRh_SpellFire_Event|右手で魔法を放ったとき|
|MLh_SpellFire_Event|左手で魔法を放ったとき|
|arrowRelease|矢を放ったとき|
|BowDrawn|最大限弓を引いたとき|
|weaponSwing|右手の武器を振ったとき|
|weaponLeftSwing|右手の武器を振ったとき|
|preHitFrame|近接攻撃がヒットする直前|
|HitFrame|近接攻撃がヒットしたとき。当たらない場合も検出する|
|BashExit|バッシュしたとき|
|BashStop|バッシュしたとき|
|BashRelease|バッシュボタンを押し続けてバッシュが発動したとき。パワーバッシュは盾装備時のみ検出する|
|RemoveCharacterControllerFromWorld|ラグドール状態になった時|
|KillMoveStart|キルムーブが発生した時。実行者及び被害者両方で検出されるため注意|
|Decapitate|キルムーブ等で首を刎ねられた時|
・参考になりそうなサイト
[[Creationkit.com:プレイヤー動作時のAnimEventが動く順番>https://www.creationkit.com/index.php?title=Animation_Events]]
[[Withe01さんのMOD作成日誌:20130930-対人用KillMove一覧>https://ameblo.jp/withe01/entry-11624951709.html]]
[[Withe01さんのMOD作成日誌:AnimationEvent>https://ameblo.jp/withe01/entry-11626350697.html]]
Withe01さんブログは主に動作をさせる(イベントを起こす)ときのことが書いてある。
*AnimationVariableの使い方
スカイリムのモーションを司るHavok Behaviorが扱う&bold(){アニメーション変数(AnimationVariable)}を取得したり変更したりできます。
これを使うことによってActorに関して細かく状態を判定したり、制御したりできます。
この変数はActorの種類によって異なります。
例えばCharacter(人)だとIsStaggeringはありますが、Dragonにはありません。
どんなアニメーション変数が何があるのかは以下のスプレッドシートのデータを確認してください。
[[人のアニメーション変数とイベントデータ>https://docs.google.com/spreadsheets/d/1ZCH_K--r8urv0Cb8TUSeQPzNBEHxKc7jCJ4saytWU_s/edit?usp=sharing]]
[[人以外のアニメーション変数とイベントデータ>>https://docs.google.com/spreadsheets/d/1KSs_nOAeVGCLy393in53sd7CnuVvwhEtWR2e4hCeA-I/edit?usp=sharing]]
そのほかは、[[Josh Behavior file patcher>http://skyrim.nexusmods.com/mods/15906/]]で確認ができます。
人ならbehaviorフォルダの0_master.hkx、ドラゴンならdragonbehavior.hkx。
関数:
[[Get/SetAnimationVariableBool>http://www.creationkit.com/GetAnimationVariableBool_-_ObjectReference]]
[[Get/SetAnimationVariableFloat>http://www.creationkit.com/GetAnimationVariableFloat_-_ObjectReference]]
[[Get/SetAnimationVariableInt>http://www.creationkit.com/GetAnimationVariableInt_-_ObjectReference]]
これらのアニメーション関数は&bold(){Condition}でも使えます。
Condtionでの関数名は&bold(){GetGraphVariableFloat}と&bold(){GetGraphVariableInt}です。
例:GetGraphVariableInt "IsStaggering" == 1 ;BoolはIntで代用。1はtrue 0はfalse
**さまざまな状態の判定
&bold(){Actor.GetAnimationVariableBool("xxx")}
|&bold(){判定}|&bold(){xxxに記載する文字列}|
|攻撃中|IsAttacking|
|ブロック中|IsBlocking|
|バッシュ中|IsBashing|
|はじかれ中|IsRecoiling|
|よろめき中|IsStaggering|
|抜刀中|IsEquipping|
|納刀中|IsUnequipping|
|ジャンプ中|bInJumpState|
|ブロック成功|IsBlockHit|
例:Actor.GetAnimationVariableBool("IsAttacking") ;攻撃中、パワーアタックも含まれる
&bold(){一人称視点かどうかの判定}
player.GetAnimationVariableInt("i1stPerson") == 1
&bold(){移動方向の判定}
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を使います。
立ち状態と前とで区別つけるときは下の移動中判定と組み合わせてください。
&bold(){移動中かどうかの判定}
一見すると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で違う模様。アクターによっても違います。
* セーブデータに残るもの
[[Save File Note(CK wiki)>http://www.creationkit.com/Save_File_Notes_%28Papyrus%29]]
セーブするとセーブデータに保存されるものがあります。
・グローバル変数
・静的変数
・プロパティ
一旦セーブされたデータでMODを更新したときにプロパティや静的変数などを削除した場合やMODを抜いた場合はセーブ内と一致しないのでログにエラーを吐きます。
エラーを吐くのですが、データがないと無視するので基本的に害はありません。
**不必要になったプロパティの削除
&bold(){プロパティはesp側にも紐ついてるので}、そちらも消す必要があります。
+スクリプトのついてるPropertyボタンを押してプロパティウィンドウを出します。
+不要なプロパティの&bold(){Clear Value}を押して消してください。
+そのあと、スクリプト側のプロパティの記述を消します。
+espを保存します。
#image(ClearValue.PNG)
*[[SM Eventを使ったRepeatQuest>https://www50.atwiki.jp/skyrim_mod/pages/34.html]]
OnUpdateを使わずにループができるクエストの作り方です。
*セル移動時関係のイベント
セル移動時にスクリプトを動かしたいときにいくつかイベントがあるんですが、どれも癖があってそれを記したいと思います。
&bold(){Onload}
3Dオブジェクトがロードされるときに発生するイベントで、ロード画面が挟むセル移動でなら起きます。
ただしセルを移動してすぐ戻る場合はセル移動時にロード挟まないのでその場合は発生しません。
&bold(){プレイヤー}は稼働しません。
&bold(){OnAttachedToCell}
セルからセルに移動するときに発生するイベントです。
たとえばワールドTamrielのWildness1からWildness2に移動するときにも発生します。
ただし&bold(){プレイヤー}は稼働しません。
またスカイリムからホワイトランに入るときには動きません。(逆は動くので条件不明)
&bold(){OnLocationChange}
ロケーションの移動時に動くイベントですが、複数のセルをまとめて一つのロケーションとして扱う場合があって、例えばホワイトラン→スカイリム、スカイリム→ホワイトランは動きません。
ですので一般的にセル移動時判定には向いてません。
&bold(){OnCellLoad}
セルロード時にイベントが発生しますが、メモリキャッシュ済みのセルの場合は発生しません。
続けてゲームプレイする場合に一度入ったセルにもう一度入った場合に発生しない可能性が高いです。
プレイヤー(エイリアス)に使えます。
NPCであれば、Onloadをおすすめします。
プレイヤーは厳密さを要求しないのであれば、OnCellLoadで大抵何とかなります。
*NPCへのPerk付与について
オススメはSPIDを使ったゲーム起動時のPerk付与です。
&bold(){スクリプトのAddPerk関数}
プレイヤーに対しては正常に機能しますが、NPCに対しては全く機能しません。
&bold(){コンソールのAddPerkコマンド}
スクリプトのAddPerk関数と同様、プレイヤーに対してのみ正常に機能します。
&bold(){マジックエフェクトの"Perk to Apply"}
スクリプトのAddPerk関数と同様、プレイヤーに対してのみ正常に機能します。
&bold(){CKやxEditにて事前にPerkを持たせる}
プラグインを作成して事前にPerkを持たせておけば確実ですが、レコードの競合等により不具合が起こる危険性があります。
&bold(){Spell Perk Item Distributor (SPID) にてゲーム起動時にPerkを持たせる}
NPCに事前にPerkを持たせておくことができるというSKSEプラグイン([[LE>https://www.nexusmods.com/skyrim/mods/105121]]/[[SE>https://www.nexusmods.com/skyrimspecialedition/mods/36869]])です。ゲーム起動時にあたかもはじめから所持していたかのように処理されるため、レコードが競合する心配がありません。
&bold(){powerofthree's Papyrus ExtenderのAddBasePerk関数}
スクリプトでNPCへPerkの付与が可能になるSKSEプラグイン([[LE>https://www.nexusmods.com/skyrim/mods/95017]] / [[SE>https://www.nexusmods.com/skyrimspecialedition/mods/22854]])です。いまのところゲーム中でPerkを付与する唯一の方法となります。
パークを付与する
; targetActorに対象のActor、addPerkに付与させたいPerkを渡す
po3_sksefunctions.AddBasePerk(targetActor, addPerk)
付与したパークを消す
po3_sksefunctions.RemoveBasePerk(targetActor, addPerk)
ただし、powerofthree's Papyrus Extenderのパーク付与は以下の問題点もあるため、使用する場合は付与状況の監視が必要となります。
※現状だと問題点あり
・NPCにパーク付与後に『対象NPCがパーク付与前かつセルにロードされている』セーブデータをロードするとセーブ時点でパーク未付与前のNPCにパークが付与される
・NPCにパーク付与後にNPCがセルからアンロードされた状態で保存した後にスカイリムを終了させ、再度起動してタイトルからロードした場合、ロード後のパーク付与がNPCに適用されない(正確にはセーブ時にAddBasePerkで何を追加されたかを保存するのだが、対象のNPCがアンロードされてると保存されない)
・&s(){同一のアクターに二回以上AddBasePerkを行うと最初にAddBasePerkで付与したパークが再付与され二重に適用される(disable→enableで正常に戻る)} → 4.5.2で修正
*Perkの挙動について
追加Perk系のModを制作する時には特に下記の点に注意する事。
**特定行動時にスペル付与系
Apply Combat Hit SpellやApply Weapon Swing Spellなど、
特定行動時にスペル効果を付与するパークについて条件を満たしたものが複数ある場合、
Priorityが0に近いパークのスペル効果のみが付与されます。
例として両手武器のウォーマスター(後ろパワーアタックで麻痺効果付与)はPriorityが0、出血攻撃(両手斧攻撃で出血状態付与)はPriorityは3~10が設定されており、
この場合、両手斧で後ろパワーアタックを行った場合はウォーマスターの効果が優先され出血攻撃の効果は出ません。
攻撃時やヒット時に魔法効果を付与といったものを作成したい場合、下手にパークのApply Combat Hit SpellやApply Weapon Swing Spellを使うとバニラのパークを含めて上手く動作しなくなる可能性が高いため、スクリプトで処理を行うことをオススメします。
なおこの制限については[[Scrambled Bugs>https://www.nexusmods.com/skyrimspecialedition/mods/43532?tab=files]]というSKSEプラグインを導入して、ScrambledBugs.jsonのmultipleSpellsをtrueにする事で解除は可能です。
(制限解除時だと上の例にある両手斧で後ろパワーアタックで麻痺効果と出血攻撃の両方の効果が付与される)
**Add ValueやSet Value等の数値変動系の計算の優先度
バニラのクリティカル率や同時付呪数を変更するパークを確認するとEntry PointのFunctionがSet Value(絶対値)となっています。もし、クリティカル率が増減するパークを作成したい時に下記のようにAとBのパーク両方を保持した際に絶対値の後に計算されるのか疑問に思った人はいるはずです。
A. クリティカル率(Calculate My Critical Hit Chance)の値をSet Valueで設定
B. クリティカル率の値をAdd ValueやMultiply Valueで変動
結論としてAとBのパークについてどちらも保持した場合、Priorityの数値が高いパークから先に適応され計算されます。
Priorityが同値の場合はFormIDが大きい方から先に適応されます。
例:
※AのSet Valueが25、BはAdd Valueで50を設定していた場合
AのPriorityが0で、BのPriorityが1の場合:Bの数値設定でどれほど値を変動させてもAの数値となる(25)
AのPriorityが1で、BのPriorityが0の場合:Aの数値に対してBの設定値で変動したものが最終的な値(75)
AとBのPriorityが同値:FormIDが大きいものから先に適応
そして、クリティカル率が変動するパーク(片手武器の剣士等)、追加付呪のパークはSet Valueで値を固定されていてなおかつPriorityが0となっています。このため、クリティカル率の増減、または同時付呪数の増減パークをModで作成してもバニラのパークを所有した瞬間、Mod追加パークの効果が正常に反映されなくなってしまいます。
解決策として、追加付呪等の一部を除いたパークは非公式パッチ(SEのUSSEPで確認)でAdd Valueに修正されているため、非公式パッチを前提Modとして作成する必要があります。
非公式パッチを前提Modにしない場合は修正用espを別途作成し、「剣士(片手武器パーク)、深手(両手武器パーク)、クリティカルショット(弓パーク)、追加付呪(付呪パーク)」のEntry PointのFunctionをAdd Valueに修正する必要があります。
*IsInKillMove関数の注意点
IsInKillMoveはアクターがキルムーブを実行中、&color(#F54738){またはキルムーブを受けてる最中の場合にも}trueを返すため、キルムーブの実行者か受けてる側かの判定はOnHitイベント(キルムーブの一撃でもHitイベントは発生する)やOnDyingイベント(キルムーブを受けてる最中に発生)を併用する必要があります。
*スクリプトによる一部のフォームの値変更について
SpellやArmor、Weapon等のベースフォームについてSet~関数等で値を変更しても一時的な値変更と扱われるためセーブデータに保存されません。(ActorBaseの関数もSetEssential等はセーブデータに保存されるが一部は一時的な変更になるものも存在する)
また、一時的な値変更となるものに関してはゲームを再起動するまではリセットされないため、値変更後に変更前のセーブデータをロードしても値は変更後のままになります。
*ConsoleUtilによるターゲットコマンド実行の注意点及び安全な実行について
**負荷が掛かると誤実行される危険性がある
ConsoleUtilでターゲットコマンド(SetAVやSetLevel等)を実行する場合、SetSelectedReference関数を使って対象の選択をすると思いますがSetSelectedReference関数にはコマンドの実行まで対象の切り替えを抑止する機能はありません。
ConsoleUtilを使ってるModが一つだけなら問題ないのですが複数のModがConsoleUtilでターゲットコマンドを使ってる場合は注意が必要です。
もしConsoleUtilを使ってるスクリプトの処理が重なるような事が起こった時にコマンド実行直前で別のスクリプトのSetSelectedReference関数で対象が変更される可能性があり指定していない対象に対してターゲットコマンドの誤実行が行われる危険性があります。(特にスクリプト遅延が起こってる状態では高確率で発生します)
**対策
これに対する回避策としてターゲットコマンドを行う場合、下記のサンプルのようにコマンド文を&color(#F54738){ "[ID]".[コマンド] }にする事でコンソールの選択対象に関係無く[ID]の対象に対してコマンドを実行されます。
こうする事により指定した対象以外への誤実行を防ぐ事ができます。
#highlight(linenumber,php){{
; ターゲットコマンドの安全実行
; SetSelectedReference→ExecuteCommandの順で実行する場合、複数のスクリプトがConsoleUtilを使ってると、
; コマンドが指定していない対象に対して誤実行される危険性があるため
; コマンド文を"[ID]".[コマンド]に変換し、コマンド実行を指定した対象に対して確実に行うようにする
; 数値の16進数文字変換はpowerofthree`s Papyrus ExtenderのSKSE関数を使用
function SafeTargetCommandExecute(string cmd, Form target)
if target
; IDをダブルクォートで囲まないとIDに英文字が混ざってない場合にエラーが起こる
cmd = "\"" + PO3_SKSEFunctions.IntToString(target.getFormID(), true) + "\"." + cmd
else
Debug.trace("Console Command Target is None:" + cmd, 2)
return
endif
ConsoleUtil.ExecuteCommand(cmd)
endfunction
}}
*アビリティ付与に関する注意
SPIDやスクリプト等でNPCに何かしらのアビリティを直接持たせた場合、NPCをテレポートしたり内部セルへ移動する等何かしらの要因で偶に&color(#F54738){アビリティに付属してる魔法効果が消滅する}という現象が発生します。
アビリティに付属した魔法効果が消えた場合はアビリティの再付与を行わない限りは魔法効果が復活しません。
この現象は&color(#F54738){パーク効果にアビリティが付属してるものをNPCに持たせた場合は発生しません。}アビリティをNPCに直接持たせた場合にのみに発生します。
(パークに付属したアビリティはゲームエンジンレベルで魔法効果が機能してるかチェックしてる?)
そのためスクリプト付きアビリティをプレイヤーやNPCに配布するModを作成する場合、確実にアビリティを持たせて機能させたければダミーパークにアビリティを付属させてそのパークを付与させるようにしましょう。
*Activate関係の処理について
**BlockActivation関数でアクティベートをブロックしてもOnActivateイベントは発生する
アクター等に対してBlockActivation関数を実行した場合、
対象をアクティベートしても見た目上は反応が返ってきませんが、この時OnActivateイベントはしっかり発生します。
これはスクリプトのActivate関数で第ニ引数にfalseを渡してアクティベートがブロックされた際も同様です。
**パークのアクティベート処理変更についての注意点
パークのEntry PointでActivateを設定してReplace Defaultにチェックを付けると
対象のアクティベート時に本来の動作の代わりにパークに付属したスクリプトの処理にすげ替える事ができます。
しかし、このアクティベートの動作変更を使用する場合は下記の点を考慮する必要があり、
特に下記の上2つの現象についてはちゃんと対処しておかないと
他のアクティベート関係の処理が行われるModと一緒に使用した場合に問題が発生する可能性があります。
(上2つの現象は[[EFF>https://skyrimspecialedition.2game.info/detail.php?id=7003]]で確認が可能です)
***OnActivateイベントが発生しない
本来ならばアクティベートした瞬間にOnActivateイベントが発生するのですが
パークによるアクティベート処理をスクリプト処理に挿げ替えてる場合はOnActivateイベントが発生しません。
アクティベート処理変更を行いつつOnActivateイベントを発生させたい場合は
スクリプト処理内で対象に対してActivate関数を使用する事でOnActivateイベントを発生させられます。
デフォルトのアクティベート処理を実行させずに
&color(#F54738){スクリプト処理だけ実行してOnActivateイベントを発生させたい場合}は
blockActivation関数でアクティベートをブロック後にActivate関数(第二引数はfalse)を実行し
再度blockActivation関数でブロック解除を行う必要があります。
スクリプト処理後にデフォルトのアクティベート処理を実行をする場合は
blockActivation関数は使用せずに
スクリプトの最後にActivate関数を実行すればよいだけです。
なお、Activate関数の第ニ引数をtrueを渡した場合はOnActivateイベントは発生しないので注意しましょう。
***BlockActivation関数でアクティベートのブロックをしても処理が実行される
BlockActivation関数によるアクティベートのブロックについては
デフォルトのアクティベート動作のみが対象となり、
アクティベート処理をスクリプト処理に挿げ替えた場合は下記のようにスクリプト側で対象が
アクティベートブロック状態かをチェックして抑制しないとそのままスクリプト処理が実行されてしまいます。
#highlight(linenumber,php){{
if akTargetRef.isActivationBlocked()
return
endif}}
***スクリプト側でアクティベートした場合は動作変更が行われない
パークによるアクティベート変更はゲーム内で直接アクティベートした場合のみ有効で
スクリプトでActivate関数を呼び出した場合はデフォルトのアクティベート処理が行われます。
***対処法のサンプル
上記のOnActivateイベントとblockActivation関数による抑制による
スクリプトのサンプルは下記の通りとなります。
#highlight(linenumber,php){{
; パークによるアクティベート動作変更時に
; blockActivation関数によるアクティベートのブロックをしてる時に
; 処理を実行しないようにしたり、
; OnActivateイベントを発生させるためのサンプル
; Fragment_2の関数名は環境によって異なるはずなので注意
Function Fragment_2(ObjectReference akTargetRef, Actor akActor)
; パークによるアクティベート動作変更時は
; スクリプト側でブロックしないとそのまま処理されてしまう
if akTargetRef.isActivationBlocked()
;OnActivateイベントを発生させる
akTargetRef.activate(akActor, false);
Debug.trace("Block Activate")
return
endif
; 以下~~~にアクティベート時に行う処理を記述する
~~~~
; パークによるアクティベート動作変更時はOnActivateイベントが発生しない
; アクティベート対象に対してActivate関数を使用すればOnActivateイベントを発生させられるが
; デフォルトのアクティベート処理まで実行してしまうため
; blockActivation関数でデフォルトのアクティベート処理をブロックし、
; Activate関数を使用後、再びブロックを解除するようにする
;
; 処理後にデフォルトのアクティベート処理をしたい場合は
; blockActivationはコメントアウトする
akTargetRef.blockActivation(true);
akTargetRef.activate(akActor, false);
akTargetRef.blockActivation(false);
EndFunction
}}
2024-02-10T21:40:57+09:00
1707568857
-
逆引きリファレンス
https://w.atwiki.jp/skyrim_mod/pages/10.html
この項目ではやりたいことから調べられます。
やり方は一例であって必ずしもということはないので、他のパターンもあればぜひ記載してください。
#contents
*ゲーム開始時に魔法やアイテムを自動で追加したい(初期化クエスト)
MOD導入後に自動的にカスタマイズメニューの魔法やアイテムを追加するMODがあります。それのやり方です。
MOD導入後にセーブを読み込んだあとに一回しか動かないクエストです。
例としてカスタマイズメニューの魔法をプレイヤーに追加するクエストを作ります。
まず、Object WindowのCharacterツリーのQuestを選択します。
AchievementsQuestというのがあるので右クリック->Duplicateでコピー。
新規作成だとうまくいかない場合もあるので使えるクエストをコピーします。
コピーしたクエストを開いてIDと名前をつけます。(例:InitQuest)
Priorityは99など高い数値にしておきます。
Start Game EnabledとRun at Onceにチェック。
そのままScriptsのボタンに移動して、要らないスクリプトをすべてRemove。
Addボタン->[New Script]でスクリプト新規作成。
extendsはQuest。
Spell property CustomMenuSpell(魔法名) auto
Event oninit()
Actor player = game.getplayer()
if !(player.hasspell(CustomMenuSpell))
player.addspell(CustomMenuSpell)
self.stop()
endif
endEvent
Oninitは初期化した時に駆動するイベントです。
Oninitは同タイミングで二回読むっぽいので、重複防止のためにCustomMenuSpellを持っていない場合でしかプレイヤーに魔法を追加しません。
Run at Onceのため一度しか起動しないですが、念のためself.Stop()でクエストを止めます。
oninitはActorにつけても動きませんクエストで駆動するようにしましょう。
**ゲームロード度に動くスクリプト
Oninitだと一回だけで、OnLoadは正しく動作しないのでOnPlayerLoadGameを使います(skyrim1.6以上必要)。
ただこれはActorのPlayerにしか返さないので、直接QuestにスクリプトをつけるのではなくAlias(エイリアス)を経由させて使います。
まず、Object WindowのCharacterツリーのQuestを選択します。
AchievementsQuestというのがあるので右クリック->Duplicateでコピー。
コピーしたクエストを開いてIDと名前をつけます。(例:LoadQuest)
Priorityは99など高い数値にしておきます。
Start Game Enabledにチェックが入ってるか確認。
そのままScriptsのボタンに移動して、スクリプトをすべてRemove。
Addボタン->[New Script]でスクリプト新規作成。
extendsはQuest。例ではTestLoadScriptと名前をつけました。
Scriptname TestLoadScript extends Quest
Event OnInit()
; 自前の関数
LoadFunc()
EndEvent
;実際の処理はここに書く
Function LoadFunc()
debug.notification("Hello Work!")
EndFunction
コンパイルしてスクリプトのウィンドウを閉じる。
Quest Aliasesボタン->右クリック->New Referense Alias
Aliasはplayerとでもしておく。
Unique ActorからPlayerを選ぶ。
ScriptのところをAddボタン->[New Script]でスクリプト新規作成。
TestLoadScript Property QuestScript Auto
; ↑QuestScript名
Event OnPlayerLoadGame()
QuestScript.LoadFunc()
EndEvent
コンパイルしてスクリプトのウィンドウを閉じる。
Propertyで当クエストの指定も忘れずに。
*常時稼動させるスクリプト
&bold(){※できる限り使わないことを考えぬいてください。}
常時稼働する事自体スクリプト(Papyrus)かそうでないかにかかわらず負荷がかかるために避けたほうが良いです。
とくに&bold(){間隔の短い}OnUpdateによるスクリプト回しは、環境にもよりますがスタックエラーにつながりやすいです。
OnUpdateとRegisterForSingleUpdate()を使います。
RegisterForUpdate()はスタックエラーの原因になりやすいので使いません。
つけたり止めたりしやすいのでクエストかマジックエフェクトで回します。
クエストの作り方は上記2つに書いてあるので省略。
例はプレイヤーがスニーク中かつ灯火使用中の場合は灯火を消します。
;(マジックエフェクトの場合はEvent OnEffectStart)
Event Oninit()
RegisterForSingleUpdate(10)
EndEvent
Event OnUpdate()
;実際の処理はここ
player = game.getplayer()
if (player.isSneaking()) && (player.HasSpell(Candlelight))
DispelSpell(Candlelight)
endif
;10秒後OnUpdate開始。つまり10秒単位で回る。
RegisterForSingleUpdate(10)
EndEvent
に1秒など短い間隔でループさせると大量にスタックしてCTDの要因や他のスクリプトの遅延になります。
(OnUpdate内の記述によりますが)できれば2秒以上ゆとりを持って設定しましょう。
高速でループさせる必要があるかどうかをよく吟味して、何か単発のイベントで代替できるか探りましょう。
*DLCや特定のespが読み込まれてる場合に処理をする
GetFormFromFile()だと存在しない場合にログにエラーが出てしますのでGetModByName()を使います。
例ではドーンガード。
if Game.GetModByName("Dawnguard.esm") < 255
; ドーンガードがある場合の処理。別のespのフォームIDからFormを取得するには Game.GetFormFromFile(0x00000000,"Dawnguard.esm")
else ;GetModByNameが255を返した場合読み込まれていません
;ドーンガードがない場合の処理
endIf
*一度のみ実行
Oninit()などで絶対に一度のみしか走らないで欲しい処理などに
Bool doOnce = False
Event Oninit()
if (doOnce)
return
else
doOnce = True
endif
;実際の処理はここ
EndEvent
*名前
**アクターの名前の取得
対象アクターの名前を取得して左上に表示させたい時に、
debug.notification("ActorName:" + Actor)
とやってもスクリプト名やActorと表示されるだけなので、以下のようにアクターベースから名前を取得します。
debug.notification("ActorName:"+ Actor.GetActorBase().GetName())
#highlight(linenumber,php){{
Actor actTarget;
String szName;
; 現在のゲーム上で表示されている名前
szName = actTarget.GetDisplayName();
; 表示名の変更に影響されない元の設定名
; レベルドリストから生成されたアクターの場合、空白になる場合がある。
szName = actTarget.GetActorBase().GetName();
; 表示名の変更に影響されない元の設定名
; レベルドリストから生成されたアクターの場合でも、元になったActorBaseの名前が入る。
; 通常のアクターの場合でも使えるので、設定名を取得する場合はこの方が確実。
szName = actTarget.GetLeveledActorBase().GetName();
}}
**場所の名前の取得
場所の名前をセル→ロケーション→ワールドの順に取得する。
屋外などでは、セルやロケーションに名前が設定されていない場合がある為。
celPlaceで指定したセルか、objrefMarkerで指定したリファレンスの場所名を返す。
#highlight(linenumber,php){{
; ----------------------------------------------------------
; @name getPlaceName
; @function
; @global
; @param [celPlace=None] {Cell}
; @param [objrefMarker=None] {ObjectReference}
; @returns {String}
; ----------------------------------------------------------
String Function getPlaceName( \
Cell celPlace = None, \
ObjectReference objrefMarker = None \
) global
;-------------------------------------------------------
; Init Variables
;-------------------------------------------------------
Location locMarker;
WorldSpace wsMarker;
String szName = "";
;-------------------------------------------------------
; Check Arguments
;-------------------------------------------------------
If !celPlace && objrefMarker
celPlace = objrefMarker.GetParentCell();
Elseif celPlace && !objrefMarker
objrefMarker = celPlace.GetNthRef(0);
Endif; !celPlace && objrefMarker
;-------------------------------------------------------
; Get Cell Name
;-------------------------------------------------------
If celPlace
szName = celPlace.GetName();
Endif; celPlace
;-------------------------------------------------------
; Get Location Name
;-------------------------------------------------------
If (szName == "")
If objrefMarker
locMarker = objrefMarker.GetCurrentLocation();
EndIf; objrefMarker
If locMarker
szName = locMarker.GetName();
Endif; locMarker
Endif; (szName == "")
;-------------------------------------------------------
; Get World Name
;-------------------------------------------------------
If (szName == "")
If objrefMarker
wsMarker = objrefMarker.GetWorldSpace();
EndIf; objrefMarker
If wsMarker
szName = wsMarker.GetName();
Endif; wsMarker
Endif; (szName == "")
;-------------------------------------------------------
; Return
;-------------------------------------------------------
return szName;
EndFunction; getPlaceName()
}}
*espファイルのゴミを消す
一度設定してしまったプロパティはespに保存されます。
その後、不要になった場合にそのまま消してしまうとPapyrus.logに以下のようなwarningメッセージを吐くespが出来上がります。
[12/24/2020 - 00:00:01PM] warning: Property <プロパティ名> on script <スクリプト名> attached to <オブジェクトのEDID> (オブジェクトのformID) cannot be initialized because the script no longer contains that property
espが自身に保存されたプロパティ情報を元に初期化しようとしたがスクリプトには存在しないために出る注意文で、新規ゲームでも関係なくメッセージを出します。
実害は無いですが邪魔な存在である事は確かなのでなるべくなら消しましょう。
-消し方
#ref(clear_value.jpg)
消したいプロパティを選択後clear valueを押し<<defalult>>にする事でespから情報を削除出来ます。
設定後、CKを保存してespを更新するのを忘れないでください。
更新を忘れて、スクリプトからプロパティを削除してしまうと注意文は消えません。
消してしまった場合、一度スクリプトにプロパティを加えてコンパイルし直してください。
なお、これはセーブデータに残ってしまった情報とは違います。
こういうの↓
12/24/2020 - 01:00:00PM] warning: Could not find type <オブジェクトのEDIDやAlias> in the type table in save
これはセーブデータに残ってしまった情報で確実な方法は発見されていません(2013/07/04現在)
セーブデータに残ったスクリプト情報の残骸は[[Save game script cleaner>http://www.nexusmods.com/skyrim/mods/52363/]]というツールで消せます。
*扉の開錠/施錠
-サンプル: &ref(ExampleUnlockDoor.zip)(動かし方は中にある readme.txt を参照のこと)
扉の施錠判断には [[ObjectReference.IsLocked>http://www.creationkit.com/index.php?title=IsLocked_-_ObjectReference]] を使います。扉の開錠/施錠には [[ObjectReference.Lock>http://www.creationkit.com/index.php?title=Lock_-_ObjectReference]] を使います。
以下は、アクティベートイベントを元に、対象の扉が開いている場合は施錠/閉じている場合は解錠するスクリプトの例です。
Scriptname ExampleUnlockDoor extends ObjectReference
ObjectReference Property targetDoor Auto
Event OnActivate(ObjectReference akActionRef)
if (targetDoor.IsLocked())
Debug.Notification("Unlock")
targetDoor.Lock(false)
else
Debug.Notification("Lock")
targetDoor.Lock(true)
endIf
endEvent
targetDoor プロパティに事前に対象となる扉を指定する必要があります。
また、Creation Kit にて事前に扉の状態を決めるには Lock タブにある情報を設定します。
以下、スクリーンショットでは鍵を必要とする状態で扉を閉めています。
#ref(set_lock.png)
何らかのイベント等をトリガーに先に進めるようにする場合などに使えます。
*オブジェクトの移動
-サンプル: &ref(ExampleMoveObject.zip)(動かし方は中にある readme.txt を参照のこと)
設置済みのオブジェクトを移動させるには [[ObjectReference.SetPosition>http://www.creationkit.com/index.php?title=SetPosition_-_ObjectReference]] を使います。
以下はアクティベートした対象そのものを後ろへ移動させるスクリプトの例です。
Scriptname ExampleMoveObject extends ObjectReference
Event OnActivate(ObjectReference akActionRef)
SetPosition(x, y - 128, z)
endEvent
[[ObjectReference.SetPosition>http://www.creationkit.com/index.php?title=SetPosition_-_ObjectReference]] で指定する座標は、セル内の絶対座標系の値です。
[[ObjectReference>http://www.creationkit.com/index.php?title=ObjectReference_Script]] には現在位置を表す x, y, z プロパティがあるので、それを使って相対位置を指定するのが常套手段です。
向きも変えたい場合は [[ObjectReference.SetAngle>http://www.creationkit.com/index.php?title=SetAngle_-_ObjectReference]] を併用します。
''※ポイント'':この移動方法は、今の座標から徐々に指定の場所へ移動させるものではありません。[[ObjectReference.SetPosition>http://www.creationkit.com/index.php?title=SetPosition_-_ObjectReference]] は、瞬時の移動しかできません。徐々に移動させるには前提として対象が [[Actor>http://www.creationkit.com/index.php?title=Actor_Script]] である必要があります。
*新しいオブジェクトの設置
**簡単な例
-サンプル: &ref(ExampleSetObject.zip)(動かし方は中にある readme.txt を参照のこと)
オブジェクトを新しく設置するには [[ObjectReference.PlaceAtMe>http://www.creationkit.com/index.php?title=PlaceAtMe_-_ObjectReference]] を使います。
第一引数に [[Form>http://www.creationkit.com/index.php?title=Form_Script]] オブジェクトを指定します。
オブジェクト指向熟練者への補足ですが、以下の対比がちょうど当てはまると覚えておくと良いでしょう。
|[[Form>http://www.creationkit.com/index.php?title=Form_Script]] オブジェクト|クラス|
|[[ObjectReference.PlaceAtMe>http://www.creationkit.com/index.php?title=PlaceAtMe_-_ObjectReference]]|new 演算子|
|[[ObjectReference>http://www.creationkit.com/index.php?title=ObjectReference_Script]] オブジェクト|インスタンス|
以下(ExampleSetObject サンプル)はアクティベートする度に WETempActivator が後ろに増えていくスクリプトの例です。
Scriptname ExampleSetObject extends ObjectReference
Activator Property setObject Auto
int count = 0
Event OnActivate(ObjectReference akActionRef)
count = count + 1
ObjectReference newObject = PlaceAtMe(setObject, 1)
newObject.SetPosition(x, y - count * 128, z)
endEvent
サンプルは WETempActivator を新しく設置するものですが、他のものでも Form オブジェクトがあれば新しいオブジェクトを生成できます。
**スキーヴァーを大量発生させる例
-サンプル: &ref(ExampleSetSkeever.zip)(動かし方は中にある readme.txt を参照のこと)
アクティベートするたびにスキーヴァーが部屋のどこかに設置されるサンプルです。
Scriptname ExampleSetSkeever extends ObjectReference
ActorBase Property skeever Auto
Event OnActivate(ObjectReference akActionRef)
float newx = -1000 + 2000 * Utility.RandomFloat()
float newy = -1000 + 2000 * Utility.RandomFloat()
Actor newActor = PlaceAtMe(skeever, 1) as Actor
newActor.SetPosition(newx, newy, z)
newActor.StopCombat()
endEvent
skeever プロパティには「EncSkeever」が設定してあります。何度もアクティベートすれば当然…。
#ref(skeevers.jpg)
囲まれます。
**SpawnerTaskを使用して大量発生させる
SpawnerTaskを使えば、敵やオブジェクトを効率よくスポーンさせることが出来ます。
下記の例では、PCの周囲に一気に出現させています。
#highlight(linenumber,php){{
Bool Function spawnAroundPlayer(Form frmTarget, Int nAddNum = 1)
;-------------------------------------------------------
; Init Variables
;-------------------------------------------------------
Actor actPlayer = Game.GetPlayer();
Float[] aPos = new Float[3];
Float[] aRot = new Float[3];
Int nTaskID;
;-------------------------------------------------------
; Spawn
;-------------------------------------------------------
If frmTarget && (nAddNum > 0)
nTaskID = SpawnerTask.Create();
SpawnerTask.AddSpawn( \
nTaskID, frmTarget, actPlayer, aPos, aRot, nAddNum);
SpawnerTask.Run(nTaskID);
Endif; frmTarget && (nAddNum > 0)
;-------------------------------------------------------
; Return
;-------------------------------------------------------
return true;
EndFunction; spawnAroundPlayer()
}}
**FormList を使ったランダム設置の例
-サンプル: &ref(ExampleSetRandomObject.zip)(動かし方は中にある readme.txt を参照のこと)
アクティベートするたびに [[FormList>http://www.creationkit.com/index.php?title=FormList_Script]] の中にある何かが部屋のどこかに設置されるサンプルです。
FormList には ObjectWindow の「WorldObjects/Tree/Plants」カテゴリにある植物群が入っており、設置後、素材を採集できます。
Scriptname ExampleSetRandomObject extends ObjectReference
FormList Property flowers Auto
Event OnActivate(ObjectReference akActionRef)
float newx = -1000 + 2000 * Utility.RandomFloat()
float newy = -1000 + 2000 * Utility.RandomFloat()
Debug.Notification("Activate ... new object (" + newx + ", " + newy + ")")
ObjectReference newObject = PlaceAtMe(flowers.GetAt(Utility.RandomInt(0, flowers.GetSize() - 1)), 1)
newObject.SetPosition(newx, newy, z)
endEvent
*アクターの移動
**アクターの誘導
-サンプル: &ref(ExampleSkeeverAroundStone.zip)(動かし方は中にある readme.txt を参照のこと)
アクターとは動きまわるオブジェクトのことです。プレイヤーや敵やNPCなどが該当します。
「オブジェクトの移動」で紹介した [[ObjectReference.SetPosition>http://www.creationkit.com/index.php?title=SetPosition_-_ObjectReference]] は瞬時の移動です。誘導には [[Actor.PathToReference>http://www.creationkit.com/index.php?title=PathToReference_-_Actor]] を使います。
誘導する際、移動先のオブジェクトを指定する必要があります。[[ObjectReference>http://www.creationkit.com/index.php?title=ObjectReference_Script]] であれば何でも良いですが、もしも自由に誘導させたいならば XMarker を使うのがお勧めです。
Scriptname ExampleSkeeverAroundStone extends ObjectReference
Actor Property skeever Auto
ObjectReference Property destination Auto
int angle = 0
Event OnCellLoad()
RegisterForSingleUpdate(1)
endEvent
Event OnUpdate()
float distance = skeever.GetDistance(destination)
if (distance < 64)
angle = angle + 45
destination.SetPosition(x + 256 * Math.cos(angle), y + 256 * Math.sin(angle), z)
endIf
skeever.PathToReference(destination, 0.5)
RegisterForSingleUpdate(1)
endEvent
以上のサンプルはスキーヴァーが石碑の周りをぐるぐると回らせるものです。
destination に XMarker をセットし、スキーヴァーの到着を判断して位置を変えていきます。
また、本サンプルは「常時稼動させるスクリプト」のサンプルにもなっています。
**隊列の形成
KeepOffsetFromActor()を使えば、対象のアクターを別のアクターの隣や背後といった定位置に移動させることが出来る。
隊列の形成などは、この手法が使われていることが多い。
#highlight(linenumber,php){{
Actor actTarget;
Actor actFollow;
actTarget.KeepOffsetFromActor( \
actFollow, \
170.0, -170.0, 0.0, \
afCatchUpRadius = 260.0, \
afFollowRadius = 8.0 );
}}
*倒した敵からコンテナードロップ
-サンプル: &ref(ExampleDropChest.zip)(動かし方は中にある readme.txt を参照のこと)
敵を倒した際、コンテナーをドロップさせてアイテムをゲットできるようにしてみましょう。
方法は簡単です。[[Actor.OnDeath>http://www.creationkit.com/index.php?title=OnDeath_-_Actor]] イベントで敵が倒されたタイミングを見計らい
-コンテナーを設置
-敵を排除
するだけです。以下、事例です。
Scriptname ExampleDropChest extends ObjectReference
Container Property BaseChest Auto
Event OnDeath(Actor akKiller)
ObjectReference chest = PlaceAtMe(BaseChest, 1)
Disable(false)
Delete()
chest.SetAngle(0, 0, chest.z)
EndEvent
BaseChest に事前にドロップさせるコンテナーをセットしておく必要があります。
コンテナーを設置したら、敵本体を即時に無効にして削除します。
サンプルではスキーヴァー(EncSkeever)がフィールドにいます。倒すと空の宝箱(TreasDraugrChestEMPTYSmall)がドロップします。
最後にアングルを変えているのは、敵が死亡時、横たわるのでそれに合わせて宝箱も傾いてしまうのを補正するためです。
*キルムーブ時のイベントの取得
要:SKSE
Event Init()
RegisterForCameraState();まず別のイベントで登録する
EndEvent
Event OnPlayerCameraState()
if newState == 2 ;キルムーブのカメラ切り替え
EndIf
EndEvent
*NPCのインベントリから見えない装備を外す方法
単に今着ている装備を外すだけだと、ロードを挟んだ際に元々着ていた服に戻ります。
これを防ぐためには透明な装備を登録したOutfitをSetすればよいです。
価値を0にしておけば別の装備を渡したときにそちらを優先してきてくれます。
なおOutfitをSetすると元の服装に戻らなくなるため、予め回収しておくのもよいでしょう。
Outfit Property NakedOutfit Auto ; AAの無い装備を一つ登録したOutfit
Function ExampleFunction(Actor akNPC)
; 単に今着ている装備を外したい場合
akNPC.UnequipAll()
akNPC.RemoveAllItems()
; アイテムを回収したい場合は自分を指定
; akNPC.RemoveAllItems(Game.GetPlayer())
; 恒久的に装備を外してしまいたい場合
akNPC.SetOutfit(NakedOutfit)
EndFunction
*ラグドール状態の取得
ラグドールはフスロダなどで吹っ飛んでいるとき、または死亡時の手足がブラブラした状態のことです。
判定方法は二通りありますが片方はアクターが『フロスダやマヒ等で倒れているか』を判定する時に使い、
もう片方はオブジェクトが重力の影響下や固定されてるオブジェであるかの判定に使う事を推奨します。(理由は後述)
**powerofthree's Papyrus ExtenderのGetActorKnockState関数を使う
こちらは対象がアクターでより正確にラグドール状態を取得したい場合に有効となります。
GetActorKnockState関数はアクターがフロスダやマヒ(あるいはPushActorAway)等で倒れてラグドール状態になり、起き上がりが終わるまでは0以外を返します。
ただし、あくまで死亡以外の要因でラグドール状態になった時に判定するためのもので、死亡が原因でラグドールになった場合はGetActorKnockStateは0を返すためIsDeadによるチェックも必要です。
(マヒ状態や吹っ飛んでる途中で死亡した場合はGetActorKnockStateは0以外を返します)
#highlight(linenumber,php){{
; ----------------------------------------------------------
; @name isInRagdollActor
; @desc 対象の人物がフロスダ等によるラグドール影響下にあるかを返す
; @function
; @global
; @param actTarget {Actor} 対象の人
; @param isDeadRagdollCheck {Bool} 死亡からラグドール状態に移行した場合を考慮するか
; @returns {Bool} 影響下にあればtrue
; ----------------------------------------------------------
Bool Function isInRagdollActor(Actor actTarget, Bool isDeadRagdollCheck = true) global
;-------------------------------------------------------
; Init Variables
;-------------------------------------------------------
Bool isInRagdoll = false;
;-------------------------------------------------------
; Checking Ragdoll Status
;-------------------------------------------------------
If actTarget
; 死亡時にラグドール状態に移行する場合はGetActorKnockStateは0を返すため
; 従来のGetMass関数による判定と同じ挙動を行いたい時はIsDeadのチェックを加える
If (PO3_SKSEFunctions.GetActorKnockState(actTarget) != 0)
isInRagdoll = true
ElseIf isDeadRagdollCheck
isInRagdoll = actTarget.IsDead()
EndIf
Endif; actTarget
;-------------------------------------------------------
; Return
;-------------------------------------------------------
return isInRagdoll;
EndFunction; isInRagdollActor()
}}
**GetMass関数を使う(従来のやり方、SE版ではアクターに対しては非推奨)
アクターが吹っ飛んでいるかの判定では最もメジャーな方法でしたが、[[Precision>https://www.nexusmods.com/skyrimspecialedition/mods/72347]]の登場により事情が変わりました。
このMod環境下では武器攻撃の判定をより正確な判定にするのにHavok衝突判定を使うため、アクターに対してアクティブラグドールというものを付与されるようになります。
その影響でこのModの環境下の場合はアクターがラグドール状態でなくてもGetMassが0以外を返すようになります。
そのため、Precision環境下でアクターがふっ飛ばされてラグドールになってるかをGetMass関数で判定すると問題が発生する可能性があるため注意が必要です。
Precision環境でない場合はアクターに対するGetMass関数は通常時は常に0を返しますが、ラグドール中だけは設定されているラグドールの計算に使う重量(Mass)を返します。
※フロスダなどで吹っ飛んでから静止すると起き上がりモーションが発生しますが、このモーション中もラグドール扱いとして0以外を返します。
Precision環境下でなくても今後出てくるModによってはGetMass関数がアクターに対して通常時でも0以外を返すようになる可能性があるため前述のGetActorKnockState関数でラグドール判定を行う事を推奨します。
例:
if Game.GetPlayer().GetMass() == 0 ;ラグドール中じゃないとき
debug.SendAnimationEvent(Game.GetPlayer(),"staggerStart")
endif
#highlight(linenumber,php){{
; ----------------------------------------------------------
; @name isInRagdoll
; @desc 対象の人や物がラグドール影響下にあるかを返す
; @function
; @global
; @param obrfThis {ObjectReference} 対象の人や物
; @returns {Bool} 影響下にあればtrue
; ----------------------------------------------------------
Bool Function isInRagdoll(ObjectReference obrfThis) global
;-------------------------------------------------------
; Init Variables
;-------------------------------------------------------
Bool isInRagdoll = false;
;-------------------------------------------------------
; Checking Ragdoll Status
;-------------------------------------------------------
If obrfThis && (obrfThis.GetMass() != 0)
isInRagdoll = true;
Endif; obrfThis && (obrfThis.GetMass() != 0)
;-------------------------------------------------------
; Return
;-------------------------------------------------------
return isInRagdoll;
EndFunction; isInRagdoll()
}}
**ラグドール中のモーション再生によるバグ
ラグドール中あるいは起き上がってる途中に別のモーションを再生すると、その場で固定されて行動不能になるバグがあります。ラグドール中は基本的に別のモーションでは割り込めないですが、ドラウグルはよろめき(StaggerStart)が割り込めてしまうので、上記の条件で除外しましょう。
*十進数と十六進数の変換
Papyrusには、こういった標準的な関数がないので不便する場合がある。
**十進数→十六進数
#highlight(linenumber,php){{
; ----------------------------------------------------------
; @name convDecToHex
; @desc 整数値を十六進数表記の文字列に変換
; @function
; @global
; @param nNum {Int} 変換したい整数値
; @returns {String} 十六進数表記の文字列
; ----------------------------------------------------------
String Function convDecToHex(Int nNum) global
;-------------------------------------------------------
; Init Variables
;-------------------------------------------------------
String szHex;
Bool isNegative;
Int[] aNums = new Int[8];
String[] aHex = new String[8];
Int nIdx = 0;
Int nLen = aNums.Length;
;-------------------------------------------------------
; Checking over 0x80000000
;-------------------------------------------------------
If (nNum < 0)
isNegative = True;
nNum += 0x80000000;
Endif
;-------------------------------------------------------
; Parse integer
;-------------------------------------------------------
aNums[0] = (nNum / 0x10000000);
nNum -= (aNums[0] * 0x10000000);
aNums[1] = (nNum / 0x01000000);
nNum -= (aNums[1] * 0x01000000);
aNums[2] = (nNum / 0x00100000);
nNum -= (aNums[2] * 0x00100000);
aNums[3] = (nNum / 0x00010000);
nNum -= (aNums[3] * 0x00010000);
aNums[4] = (nNum / 0x00001000);
nNum -= (aNums[4] * 0x00001000);
aNums[5] = (nNum / 0x00000100);
nNum -= (aNums[5] * 0x00000100);
aNums[6] = (nNum / 0x00000010);
nNum -= (aNums[6] * 0x00000010);
aNums[7] = nNum;
;-------------------------------------------------------
; Convert to HEX
;-------------------------------------------------------
If isNegative
aNums[0] = aNums[0] + 8;
Endif
While (nIdx < nLen)
If (aNums[nIdx] == 10)
aHex[nIdx] = "A";
Elseif (aNums[nIdx] == 11)
aHex[nIdx] = "B";
Elseif (aNums[nIdx] == 12)
aHex[nIdx] = "C";
Elseif (aNums[nIdx] == 13)
aHex[nIdx] = "D";
Elseif (aNums[nIdx] == 14)
aHex[nIdx] = "E";
Elseif (aNums[nIdx] == 15)
aHex[nIdx] = "F";
Else
aHex[nIdx] = (aNums[nIdx] as String);
Endif
nIdx += 1;
EndWhile
;-------------------------------------------------------
; Join characters
;-------------------------------------------------------
szHex = aHex[0] + aHex[1] + aHex[2] + aHex[3] \
+ aHex[4] + aHex[5] + aHex[6] + aHex[7];
;-------------------------------------------------------
; Return
;-------------------------------------------------------
return szHex;
EndFunction; convDecToHex()
}}
**十六進数→十進数
#highlight(linenumber,php){{
; ----------------------------------------------------------
; @name convHexToDec
; @desc 十六進数表記の文字列を整数値で応答
; @function
; @global
; @param szHex {String} 十六進数表記の文字列
; @returns {Int} 変換した整数値
; ----------------------------------------------------------
Int Function convHexToDec(String szHex) global
;-------------------------------------------------------
; Init Variables
;-------------------------------------------------------
Int nIdx = 0;
Int nLen = StringUtil.GetLength(szHex);
String cChar;
Int nNum;
Int nRet;
;-------------------------------------------------------
; Parse Hex-String
;-------------------------------------------------------
While (nIdx < nLen)
cChar = StringUtil.GetNthChar(szHex, nIdx);
If StringUtil.IsDigit(cChar)
nNum = cChar as Int;
Elseif (cChar == "A") || (cChar == "a")
nNum = 10;
Elseif (cChar == "B") || (cChar == "b")
nNum = 11;
Elseif (cChar == "C") || (cChar == "c")
nNum = 12;
Elseif (cChar == "D") || (cChar == "d")
nNum = 13;
Elseif (cChar == "E") || (cChar == "e")
nNum = 14;
Elseif (cChar == "F") || (cChar == "f")
nNum = 15;
Else
nNum= 0;
Endif
nNum = (nNum * Math.pow(16, (nLen - (nIdx + 1)))) as Int;
nRet += nNum;
nIdx += 1
EndWhile
;-------------------------------------------------------
; Return
;-------------------------------------------------------
return nRet;
EndFunction; convHexToDec()
}}
*攻撃側と防御側の位置と角度判定
バックスタブなどのMODで使われる手法。二者の相対的な距離と角度から判定している。
#highlight(linenumber,php){{
; ----------------------------------------------------------
; @name getAttackAngle
; @desc 攻撃側と防御側の向きと距離のタイプを返す
; @function
; @global
; @param actAtk {Actor} 攻撃側の人
; @param actDef {Actor} 防御側の人
; @param [nCloseRange=170.0] {Float} 近接と判断する距離限界
; @param [nFrontAngle=44.0] {Float} 正面と判断する角度限界
; @param [nRearAngle=125.0] {Float} 背面と判断する角度限界
; @returns {Int}
; 距離のタイプとして、以下を返す
; -1 : 以下のどれでもない、または処理エラー
; 11 : 正面近距離
; 12 : 背面近距離
; 21 : 正面遠距離
; 22 : 背面遠距離
; ----------------------------------------------------------
Int Function getAttackAngle( \
Actor actAtk, \
Actor actDef, \
Float nCloseRange = 170.0, \
Float nFrontAngle = 44.0, \
Float nRearAngle = 125.0 \
) global
;-------------------------------------------------------
; Init Variables
;-------------------------------------------------------
Int nRet = -1;
Float nFaceMin;
Float nFaceMax;
Float nRearMin;
Float nRearMax;
Float nDist;
Float nAngleAtk;
Float nAngleDef;
;-------------------------------------------------------
; Check Arguments
;-------------------------------------------------------
If !actAtk || !actDef || (actAtk == actDef)
return nRet;
Endif; !actAtk || !actDef || (actAtk == actDef)
If (nCloseRange < 0.0)
nCloseRange *= -1.0;
Endif; (nCloseRange < 0.0)
If (nFrontAngle < 0.0)
nFrontAngle *= -1.0;
Endif; (nFrontAngle < 0.0)
If (nFrontAngle > 90.0)
nFrontAngle = 90.0;
Endif; (nFrontAngle > 90.0)
If (nRearAngle < 0.0)
nRearAngle *= -1.0;
Endif; (nRearAngle < 0.0)
If (nRearAngle < 90.0)
nRearAngle = 90.0;
Elseif (nRearAngle > 180.0)
nRearAngle = 180.0;
Endif; (nRearAngle < 90.0)
;-------------------------------------------------------
; Init Range Values
;-------------------------------------------------------
nFaceMin = nFrontAngle * -1.0;
nFaceMax = nFrontAngle;
nRearMin = nRearAngle * -1.0;
nRearMax = nRearAngle;
;-------------------------------------------------------
; Get Distance and Angles
;-------------------------------------------------------
nDist = actAtk.GetDistance(actDef);
nAngleAtk = actAtk.GetHeadingAngle(actDef);
nAngleDef = actDef.GetHeadingAngle(actAtk);
;-------------------------------------------------------
; Check Distance
;-------------------------------------------------------
If (nDist <= nCloseRange)
nRet = 10;
Else
nRet = 20;
Endif; (nDist <= nCloseRange)
;-------------------------------------------------------
; Check Defender Angle
;-------------------------------------------------------
If (nAngleAtk >= nFaceMin) && (nAngleAtk <= nFaceMax)
If (nAngleDef >= nFaceMin) \
&& (nAngleDef <= nFaceMax)
nRet += 1;
Elseif (nAngleDef <= nRearMin) \
|| (nAngleDef >= nRearMax)
nRet += 2;
Else
nRet = -1;
Endif; (nDist <= nCloseRange) && ...
Else
nRet = -1;
Endif; (nAngleAtk >= nFaceMin) && ...
;-------------------------------------------------------
; Return
;-------------------------------------------------------
return nRet;
EndFunction; getAttackAngle()
}}
*コンソールやクロスヘアで選択した対象を取得
コンソールやクロスヘアで選択中のオブジェクトのリファレンスを取得したい場合の処理。
Game.GetCurrentConsoleRef()は昔のSKSEではサポートしていなかった関数なので、念の為にバージョンをチェックしている。
#highlight(linenumber,php){{
; ----------------------------------------------------------
; @name getObjectSelected
; @desc 選択中のオブジェクトを取得する
; @function
; @global
; @param [enableCrosshair=True] {Bool}
; クロスヘア選択を有効にする
; @param [enableConsole=True] {Bool}
; コンソール選択を有効にする
; @returns {ObjectReference}
; 取得した収納オブジェクト
; ----------------------------------------------------------
ObjectReference Function getObjectSelected( \
Bool enableCrosshair = True, \
Bool enableConsole = True \
) global
;-------------------------------------------------------
; Init Variables
;-------------------------------------------------------
ObjectReference obrfFind;
Int nSkseVer = SKSE.GetScriptVersionRelease();
;-------------------------------------------------------
; Console selected
;-------------------------------------------------------
If !obrfFind && enableConsole && (nSkseVer > 47)
obrfFind = \
Game.GetCurrentConsoleRef() as ObjectReference;
Endif
;-------------------------------------------------------
; Crosshair
;-------------------------------------------------------
If !obrfFind && enableCrosshair
obrfFind = \
Game.GetCurrentCrosshairRef() as ObjectReference;
Endif
;-------------------------------------------------------
; Return
;-------------------------------------------------------
Return obrfFind;
EndFunction; getObjectSelected()
}}
*ゲーム内の時間取得
Utility.GetCurrentGameTime()でゲーム内の経過日数を取得してから計算する。
**ゲーム内時間の週を数値で返す
#highlight(linenumber,php){{
; ----------------------------------------------------------
; @name getCurrentWeekIdx
; @desc ゲーム内時間の週を数値で返す
; @function
; @global
; @returns {Int} 週を示す整数値(0 - 6)
; ----------------------------------------------------------
Int Function getCurrentWeekIdx() global
;-------------------------------------------------------
; Init Variables
;-------------------------------------------------------
Float nGameTime = Utility.GetCurrentGameTime();
Int nDays = Math.Floor(nGameTime);
Int nWeeks = Math.Floor(nDays / 7);
Int nWeekIdx = nDays - (nWeeks * 7);
;-------------------------------------------------------
; Return
;-------------------------------------------------------
return nWeekIdx;
EndFunction; getCurrentWeekIdx()
}}
**ゲーム内時間の時間を数値で返す
#highlight(linenumber,php){{
; ----------------------------------------------------------
; @name getCurrentHour
; @desc ゲーム内時間の時間を数値で返す
; @function
; @global
; @returns {Int} 時間を示す整数値(0 - 23)
; ----------------------------------------------------------
Int Function getCurrentHour() global
;-------------------------------------------------------
; Init Variables
;-------------------------------------------------------
Float nGameTime = Utility.GetCurrentGameTime();
Int nDays = Math.Floor(nGameTime);
Int nHours = Math.Floor((nGameTime - nDays) * 24);
;-------------------------------------------------------
; Return
;-------------------------------------------------------
return nHours;
EndFunction; getCurrentHour()
}}
*判定処理
様々な判定を行う処理の例。
**対象の人が敵性であるかを返す
この例では、敵と判定する為の3つの条件を組み合わせている。
#highlight(linenumber,php){{
; ----------------------------------------------------------
; @name isEnemy
; @desc 対象の人が敵性であるかを返す
; @function
; @global
; @param actEnemy {Actor} 対象の人
; @param [actTarget=None] {Actor}
; 誰に対する敵かを指定(None時はプレイヤーに対する敵)
; @returns {Bool} 敵性であればtrue
; ----------------------------------------------------------
Bool function isEnemy(Actor actEnemy, Actor actTarget = None) global
;-------------------------------------------------------
; Init Variables
;-------------------------------------------------------
Bool isEnemy = false;
Int nReaction;
Int nRank;
;-------------------------------------------------------
; Check Argument
;-------------------------------------------------------
If !actEnemy
return isEnemy;
Elseif !actTarget
actTarget = Game.GetPlayer();
Endif; !actEnemy
;-------------------------------------------------------
; Check Actor
;-------------------------------------------------------
If (actEnemy == actTarget)
return isEnemy;
Endif; (actEnemy == actTarget)
;-------------------------------------------------------
; Check Hostile
;-------------------------------------------------------
If !isEnemy
isEnemy = actEnemy.IsHostileToActor(actTarget);
Endif; !isEnemy
;-------------------------------------------------------
; Check Reaction
;-------------------------------------------------------
If !isEnemy
; Obtains this actors faction-based reaction
; to the other actor
; 0 - Neutral
; 1 - Enemy
; 2 - Ally
; 3 - Friend
nReaction = actEnemy.getFactionReaction(actTarget);
isEnemy = (nReaction == 1);
Endif; !isEnemy
;-------------------------------------------------------
; Check Relationship
;-------------------------------------------------------
If !isEnemy
; Relationship functions use the following values:
; 4 - Lover
; 3 - Ally
; 2 - Confidant
; 1 - Friend
; 0 - Acquaintance
; -1 - Rival
; -2 - Foe
; -3 - Enemy
; -4 - Archnemesis
nRank = actEnemy.GetRelationshipRank(actTarget);
isEnemy = (nRank <= -2);
Endif; !isEnemy
;-------------------------------------------------------
; Return
;-------------------------------------------------------
return isEnemy;
EndFunction; isEnemy()
}}
**対象の人がフォロワーであるかを返す
フォロワーMODを使用している場合では、判定条件が異なるかも知れない。
#highlight(linenumber,php){{
; ----------------------------------------------------------
; @name isFollower
; @desc 対象の人がフォロワーであるかを返す
; @function
; @global
; @param actTarget {Actor} 対象の人
; @returns {Bool} フォロワーであればtrue
; ----------------------------------------------------------
Bool Function isFollower(Actor actTarget) global
;-------------------------------------------------------
; Init Variables
;-------------------------------------------------------
Faction[] aFollower;
Bool isFollower = false;
;-------------------------------------------------------
; Get Factions
;-------------------------------------------------------
; CurrentFollowerFaction 0x5C84E
; CurrentHireling 0xBD738
aFollower = new Faction[2];
aFollower[0] = Game.GetForm(0x5C84E) As Faction;
aFollower[1] = Game.GetForm(0xBD738) As Faction;
;-------------------------------------------------------
; Check Factions
;-------------------------------------------------------
If actTarget.IsInFaction(aFollower[0]) \
|| actTarget.IsInFaction(aFollower[1])
isFollower = true;
Endif; actTarget.IsInFaction(aFollower[0]) || ...
;-------------------------------------------------------
; Return
;-------------------------------------------------------
return isFollower;
EndFunction; isFollower()
}}
**対象の人が魔法使いであるかを返す
この例では、戦闘スタイルで魔法使いかを判定している。
この判定条件が有効かどうかは、使う場面による。
#highlight(linenumber,php){{
; ----------------------------------------------------------
; @name isMagicUser
; @desc 対象の人が魔法使いであるかを返す
; @function
; @global
; @param actTarget {Actor} 対象の人
; @returns {Bool} 魔法使いであればtrue
; ----------------------------------------------------------
Bool Function isMagicUser(Actor actTarget) global
;-------------------------------------------------------
; Init Variables
;-------------------------------------------------------
ActorBase abTarget = actTarget.GetLeveledActorBase();
CombatStyle csTarget = abTarget.GetCombatStyle();
Float nMeleeMult = csTarget.GetMeleeMult();
Float nMagicMult = csTarget.GetMagicMult();
Float nRangedMult = csTarget.GetRangedMult();
Bool isMagicUser = false;
;-------------------------------------------------------
; Check
;-------------------------------------------------------
If (nMagicMult > nMeleeMult) || (nMagicMult > nRangedMult)
isMagicUser = true;
Endif; (nMagicMult > nMeleeMult) || (nMagicMult > nRangedMult)
;-------------------------------------------------------
; Return
;-------------------------------------------------------
return isMagicUser;
EndFunction; isMagicUser()
}}
**対象の人がクリーチャーであるかを返す
この例では、人間タイプでないことをクリーチャーの条件としている。
MODで、ActorTypeCreatureのキーワードがないクリーチャーが多い為。
良し悪しあるので、ケェス・バイ・ケェスではある。
#highlight(linenumber,php){{
; ----------------------------------------------------------
; @name isCreature
; @desc 対象の人がクリーチャーであるかを返す
; @function
; @global
; @param actTarget {Actor} 対象の人
; @returns {Bool} クリーチャーであればtrue
; ----------------------------------------------------------
Bool Function isCreature(Actor actTarget) global
;-------------------------------------------------------
; Init Variables
;-------------------------------------------------------
ActorBase abTarget = actTarget.GetLeveledActorBase();
Int nSex = abTarget.GetSex();
Bool isCreature = false;
;-------------------------------------------------------
; Check Keyword
;-------------------------------------------------------
If !actTarget.HasKeywordString("ActorTypeNPC")
isCreature = true;
Elseif (nSex == -1)
isCreature = true;
Endif; !actTarget.HasKeywordString("ActorTypeNPC")
;-------------------------------------------------------
; Return
;-------------------------------------------------------
return isCreature;
EndFunction; isCreature()
}}
**対象の人が人間男性であるかを返す
#highlight(linenumber,php){{
; ----------------------------------------------------------
; @name isMale
; @desc 対象の人が人間男性であるかを返す
; @function
; @global
; @param actTarget {Actor} 対象の人
; @returns {Bool} 人間男性であればtrue
; ----------------------------------------------------------
Bool Function isMale(Actor actTarget) global
;-------------------------------------------------------
; Init Variables
;-------------------------------------------------------
ActorBase abTarget = actTarget.GetLeveledActorBase();
Int nSex = abTarget.GetSex();
Bool isMale = false;
;-------------------------------------------------------
; Check Keyword
;-------------------------------------------------------
If !actTarget.HasKeywordString("ActorTypeNPC")
isMale = false;
Elseif (nSex == 0)
isMale = true;
Endif; !actTarget.HasKeywordString("ActorTypeNPC")
;-------------------------------------------------------
; Return
;-------------------------------------------------------
return isMale;
EndFunction; isMale()
}}
**対象の人が人間女性であるかを返す
#highlight(linenumber,php){{
; ----------------------------------------------------------
; @name isFemale
; @desc 対象の人が人間女性であるかを返す
; @function
; @global
; @param actTarget {Actor} 対象の人
; @returns {Bool} 人間女性であればtrue
; ----------------------------------------------------------
Bool Function isFemale(Actor actTarget) global
;-------------------------------------------------------
; Init Variables
;-------------------------------------------------------
ActorBase abTarget = actTarget.GetLeveledActorBase();
Int nSex = abTarget.GetSex();
Bool isFemale = false;
;-------------------------------------------------------
; Check Keyword
;-------------------------------------------------------
If !actTarget.HasKeywordString("ActorTypeNPC")
isFemale = false;
Elseif (nSex == 1)
isFemale = true;
Endif; !actTarget.HasKeywordString("ActorTypeNPC")
;-------------------------------------------------------
; Return
;-------------------------------------------------------
return isFemale;
EndFunction; isFemale()
}}
**対象の人が戦闘継続可能であるかを返す
その瞬間に継続可能かどうかの判定。GetMass()はラグドール状態かの判定。
#highlight(linenumber,php){{
; ----------------------------------------------------------
; @name canCombat
; @desc 対象の人が戦闘継続可能であるかを返す
; @function
; @global
; @param [actTarget=None] {Actor}
; 対象の人 (None時はプレイヤーが対象)
; @returns {Bool}
; 戦闘継続可能であればtrue
; ----------------------------------------------------------
Bool Function canCombat(Actor actTarget = None) global
;-------------------------------------------------------
; Init Variables
;-------------------------------------------------------
Bool canCombat = true;
;-------------------------------------------------------
; Checking Actor
;-------------------------------------------------------
If !actTarget
actTarget = Game.GetPlayer();
Endif; !actTarget
;-------------------------------------------------------
; Checking Keywords
;-------------------------------------------------------
If !actTarget.Is3DLoaded() \
|| actTarget.IsDisabled() \
|| actTarget.IsDead() \
|| actTarget.IsUnconscious() \
|| actTarget.IsBleedingOut() \
|| (actTarget.GetMass() != 0)
canCombat = false;
Endif; !actTarget.Is3DLoaded() || ...
;-------------------------------------------------------
; Return
;-------------------------------------------------------
return canCombat;
EndFunction; canCombat()
}}
**パワーアタック判定
スプリント攻撃以外はアニメーション変数「bAllowRotation」で判定できる。
#highlight(linenumber,php){{
; Animation event, sent when an object we are listening
; to hits one of the events we are listening for
Event OnAnimationEvent(ObjectReference akSource, string asEventName)
;-------------------------------------------------------
; Init Variables
;-------------------------------------------------------
Actor actTarget = akSource as Actor;
Bool isPowerAtk = true;
;-------------------------------------------------------
; Check Power Attack
;-------------------------------------------------------
If (asEventName == "weaponSwing") \
|| (asEventName == "weaponLeftSwing")
isPowerAtk = \
actTarget.GetAnimationVariableBool( \
"bAllowRotation" );
Endif; (szEvent == "weaponSwing") || ...
EndEvent; OnAnimationEvent()
}}
***The Ultimate Dodge Mod か Nemesis使用環境の場合
The Ultimate Dodge Modを使用している場合か、
アニメーション管理ツールのNemesis実行時に「The Ultimate Dodge Mod」にチェックを入れて処理した場合、人間NPCのアニメーションから受信できるAnimationEventが増える。
通常攻撃時は"NextAttackInitiate"、パワーアタック時は"NextPowerAttackInitiate"が発生する。
(スプリントのパワーアタック時も"NextPowerAttackInitiate"が発生する)
#highlight(linenumber,php){{
Event OnAnimationEvent(ObjectReference akSource, string asEventName)
;-------------------------------------------------------
; Init Variables
;-------------------------------------------------------
Bool isPowerAtk = false;
;-------------------------------------------------------
; Check Power Attack
;-------------------------------------------------------
If (asEventName == "NextAttackInitiate")
isPowerAtk = false
ElseIf (asEventName == "NextPowerAttackInitiate")
isPowerAtk = true
Endif; (asEventName == "NextAttackInitiate") ElseIf ...
EndEvent; OnAnimationEvent()
}}
**設備や器具の判定
設備や器具であるかを判定する為の処理例。
基本的には、キーワードで判定することが可能。
椅子などは、キーワードで判定できない場合もあるので注意。
#highlight(linenumber,php){{
; ----------------------------------------------------------
; @name isSittingObject
; @desc 対象のオブジェクトが座るタイプの家具かどうか
; @function
; @global
; @param obrfTarget {ObjectReference} 対象のオブジェクト
; @returns {Bool}
; 座るタイプの家具の場合はtrue
; ----------------------------------------------------------
Bool Function isSittingObject(ObjectReference obrfTarget) global
;-------------------------------------------------------
; Init Variables
;-------------------------------------------------------
Bool isObject = false;
Form frmTarget = obrfTarget.GetBaseObject();
;-------------------------------------------------------
; Check the name in the name list
;-------------------------------------------------------
; IsTable [KYWD:0009B9A9]
; isBarStool [KYWD:00074EC7]
If frmTarget.HasKeywordString("IsTable") \
|| frmTarget.HasKeywordString("isBarStool")
isObject = true;
Endif; frmTarget.HasKeywordString("IsTable") || ...
;-------------------------------------------------------
; Return
;-------------------------------------------------------
return isObject;
EndFunction; isSittingObject()
; ----------------------------------------------------------
; @name isSleepingObject
; @desc 対象のオブジェクトが寝るタイプの家具かどうか
; @function
; @global
; @param obrfTarget {ObjectReference} 対象のオブジェクト
; @returns {Bool}
; 寝るタイプの家具の場合はtrue
; ----------------------------------------------------------
Bool Function isSleepingObject(ObjectReference obrfTarget) global
;-------------------------------------------------------
; Init Variables
;-------------------------------------------------------
Bool isObject = false;
Form frmTarget = obrfTarget.GetBaseObject();
;-------------------------------------------------------
; Check the name in the name list
;-------------------------------------------------------
; FurnitureBedRoll [KYWD:000E4AD6]
If frmTarget.HasKeywordString("FurnitureBedRoll")
isObject = true;
Endif; frmTarget.HasKeywordString("FurnitureBedRoll")
;-------------------------------------------------------
; Return
;-------------------------------------------------------
return isObject;
EndFunction; isSleepingObject()
; ----------------------------------------------------------
; @name isAlchemyLab
; @desc 対象のオブジェクトが錬金器具かどうか
; @function
; @global
; @param obrfTarget {ObjectReference} 対象のオブジェクト
; @returns {Bool}
; 錬金器具の場合はtrue
; ----------------------------------------------------------
Bool Function isAlchemyLab(ObjectReference obrfTarget) global
;-------------------------------------------------------
; Init Variables
;-------------------------------------------------------
Bool isObject = false;
Form frmTarget = obrfTarget.GetBaseObject();
;-------------------------------------------------------
; Check the name in the name list
;-------------------------------------------------------
; isAlchemy [KYWD:0002A40B]
; WICraftingAlchemy [KYWD:0004F6E6]
If frmTarget.HasKeywordString("isAlchemy") \
|| frmTarget.HasKeywordString("WICraftingAlchemy")
isObject = true;
Endif; frmTarget.HasKeywordString("isAlchemy") || ...
;-------------------------------------------------------
; Return
;-------------------------------------------------------
return isObject;
EndFunction; isAlchemyLab()
; ----------------------------------------------------------
; @name isCookingObject
; @desc 対象のオブジェクトが調理器具かどうか
; @function
; @global
; @param obrfTarget {ObjectReference} 対象のオブジェクト
; @returns {Bool}
; 調理器具の場合はtrue
; ----------------------------------------------------------
Bool Function isCookingObject(ObjectReference obrfTarget) global
;-------------------------------------------------------
; Init Variables
;-------------------------------------------------------
Bool isObject = false;
Form frmTarget = obrfTarget.GetBaseObject();
;-------------------------------------------------------
; Check the name in the name list
;-------------------------------------------------------
; isCookingSpit [KYWD:00068ADA]
; CraftingCookpot [KYWD:000A5CB3]
; isSmallCookingPot [KYWD:001010B2]
; isSmallCookingPotDBPoison [KYWD:001010B5]
; isCraftingOven [KYWD:01002840]
If frmTarget.HasKeywordString("isCookingSpit") \
|| frmTarget.HasKeywordString("isCraftingOven") \
|| frmTarget.HasKeywordString("CraftingCookpot") \
|| frmTarget.HasKeywordString("isSmallCookingPot") \
|| frmTarget.HasKeywordString("isSmallCookingPotDBPoison")
isObject = true;
Endif; frmTarget.HasKeywordString("isCookingSpit") || ...
;-------------------------------------------------------
; Return
;-------------------------------------------------------
return isObject;
EndFunction; isCookingObject()
; ----------------------------------------------------------
; @name isEnchanter
; @desc 対象のオブジェクトが通常の付呪器具かどうか
; @function
; @global
; @param obrfTarget {ObjectReference} 対象のオブジェクト
; @returns {Bool}
; 通常の付呪器具の場合はtrue
; ----------------------------------------------------------
Bool Function isEnchanter(ObjectReference obrfTarget) global
;-------------------------------------------------------
; Init Variables
;-------------------------------------------------------
Bool isObject = false;
Form frmTarget = obrfTarget.GetBaseObject();
;-------------------------------------------------------
; Check the name in the name list
;-------------------------------------------------------
; isEnchanting [KYWD:0006E2A3]
; WICraftingEnchanting [KYWD:0004F6DD]
; DLC2StaffEnchanter [KYWD:02017738]
If frmTarget.HasKeywordString("DLC2StaffEnchanter")
isObject = false;
Elseif frmTarget.HasKeywordString("isEnchanting") \
|| frmTarget.HasKeywordString("WICraftingEnchanting")
isObject = true;
Endif; frmTarget.HasKeywordString("DLC2StaffEnchanter")
;-------------------------------------------------------
; Return
;-------------------------------------------------------
return isObject;
EndFunction; isEnchanter()
; ----------------------------------------------------------
; @name isStaffEnchanter
; @desc 対象のオブジェクトが杖の付呪器具かどうか
; @function
; @global
; @param obrfTarget {ObjectReference} 対象のオブジェクト
; @returns {Bool}
; 杖の付呪器具の場合はtrue
; ----------------------------------------------------------
Bool Function isStaffEnchanter(ObjectReference obrfTarget) global
;-------------------------------------------------------
; Init Variables
;-------------------------------------------------------
Bool isObject = false;
Form frmTarget = obrfTarget.GetBaseObject();
;-------------------------------------------------------
; Check the name in the name list
;-------------------------------------------------------
; DLC2StaffEnchanter [KYWD:02017738]
If frmTarget.HasKeywordString("DLC2StaffEnchanter")
isObject = true;
Endif; frmTarget.HasKeywordString("DLC2StaffEnchanter")
;-------------------------------------------------------
; Return
;-------------------------------------------------------
return isObject;
EndFunction; isStaffEnchanter()
; ----------------------------------------------------------
; @name isSharpeningWheel
; @desc 対象のオブジェクトが研ぎ石かどうか
; @function
; @global
; @param obrfTarget {ObjectReference} 対象のオブジェクト
; @returns {Bool}
; 研ぎ石の場合はtrue
; ----------------------------------------------------------
Bool Function isSharpeningWheel(ObjectReference obrfTarget) global
;-------------------------------------------------------
; Init Variables
;-------------------------------------------------------
Bool isObject = false;
Form frmTarget = obrfTarget.GetBaseObject();
;-------------------------------------------------------
; Check the name in the name list
;-------------------------------------------------------
; WICraftingSmithingTempering [KYWD:0004F6FD]
; CraftingSmithingSharpeningWheel [KYWD:00088108]
If frmTarget.HasKeywordString("WICraftingSmithingTempering") \
|| frmTarget.HasKeywordString("CraftingSmithingSharpeningWheel")
isObject = true;
Endif; frmTarget.HasKeywordString("WICraftingSmithingTempering") || ...
;-------------------------------------------------------
; Return
;-------------------------------------------------------
return isObject;
EndFunction; isSharpeningWheel()
; ----------------------------------------------------------
; @name isArmorTable
; @desc 対象のオブジェクトが作業机かどうか
; @function
; @global
; @param obrfTarget {ObjectReference} 対象のオブジェクト
; @returns {Bool}
; 作業机の場合はtrue
; ----------------------------------------------------------
Bool Function isArmorTable(ObjectReference obrfTarget) global
;-------------------------------------------------------
; Init Variables
;-------------------------------------------------------
Bool isObject = false;
Form frmTarget = obrfTarget.GetBaseObject();
;-------------------------------------------------------
; Check the name in the name list
;-------------------------------------------------------
; isBlacksmithWorkbench [KYWD:000D932E]
; CraftingSmithingArmorTable [KYWD:000ADB78]
If frmTarget.HasKeywordString("isBlacksmithWorkbench") \
|| frmTarget.HasKeywordString("CraftingSmithingArmorTable")
isObject = true;
Endif; frmTarget.HasKeywordString("isBlacksmithWorkbench") || ...
;-------------------------------------------------------
; Return
;-------------------------------------------------------
return isObject;
EndFunction; isArmorTable()
; ----------------------------------------------------------
; @name isForge
; @desc 対象のオブジェクトが鍛造器具かどうか
; @function
; @global
; @param obrfTarget {ObjectReference} 対象のオブジェクト
; @returns {Bool}
; 鍛造器具の場合はtrue
; ----------------------------------------------------------
Bool Function isForge(ObjectReference obrfTarget) global
;-------------------------------------------------------
; Init Variables
;-------------------------------------------------------
Bool isObject = false;
Form frmTarget = obrfTarget.GetBaseObject();
;-------------------------------------------------------
; Check the name in the name list
;-------------------------------------------------------
; isBlacksmithForge [KYWD:000CAE0A]
; CraftingSmithingForge [KYWD:00088105]
; isBlacksmithAnvil [KYWD:000EB60B]
If frmTarget.HasKeywordString("isBlacksmithForge") \
|| frmTarget.HasKeywordString("isBlacksmithAnvil") \
|| frmTarget.HasKeywordString("CraftingSmithingForge")
isObject = true;
Endif; frmTarget.HasKeywordString("isBlacksmithForge") || ...
;-------------------------------------------------------
; Return
;-------------------------------------------------------
return isObject;
EndFunction; isForge()
; ----------------------------------------------------------
; @name isTanningRack
; @desc 対象のオブジェクトが皮なめしかどうか
; @function
; @global
; @param obrfTarget {ObjectReference} 対象のオブジェクト
; @returns {Bool}
; 皮なめしの場合はtrue
; ----------------------------------------------------------
Bool Function isTanningRack(ObjectReference obrfTarget) global
;-------------------------------------------------------
; Init Variables
;-------------------------------------------------------
Bool isObject = false;
Form frmTarget = obrfTarget.GetBaseObject();
;-------------------------------------------------------
; Check the name in the name list
;-------------------------------------------------------
; isTanning [KYWD:000727A0]
; CraftingTanningRack [KYWD:0007866A]
If frmTarget.HasKeywordString("isTanning") \
|| frmTarget.HasKeywordString("CraftingTanningRack")
isObject = true;
Endif; frmTarget.HasKeywordString("isTanning") || ...
;-------------------------------------------------------
; Return
;-------------------------------------------------------
return isObject;
EndFunction; isTanningRack()
; ----------------------------------------------------------
; @name isSmelter
; @desc 対象のオブジェクトが溶鉱炉かどうか
; @function
; @global
; @param obrfTarget {ObjectReference} 対象のオブジェクト
; @returns {Bool}
; 溶鉱炉の場合はtrue
; ----------------------------------------------------------
Bool Function isSmelter(ObjectReference obrfTarget) global
;-------------------------------------------------------
; Init Variables
;-------------------------------------------------------
Bool isObject = false;
Form frmTarget = obrfTarget.GetBaseObject();
;-------------------------------------------------------
; Check the name in the name list
;-------------------------------------------------------
; isSmelter [KYWD:0009C6C3]
; CraftingSmelter [KYWD:000A5CCE]
If frmTarget.HasKeywordString("isSmelter") \
|| frmTarget.HasKeywordString("CraftingSmelter")
isObject = true;
Endif; frmTarget.HasKeywordString("isSmelter") || ...
;-------------------------------------------------------
; Return
;-------------------------------------------------------
return isObject;
EndFunction; isSmelter()
; ----------------------------------------------------------
; @name isWoodChoppingBlock
; @desc 対象のオブジェクトが薪割りかどうか
; @function
; @global
; @param obrfTarget {ObjectReference} 対象のオブジェクト
; @returns {Bool}
; 薪割りの場合はtrue
; ----------------------------------------------------------
Bool Function isWoodChoppingBlock(ObjectReference obrfTarget) global
;-------------------------------------------------------
; Init Variables
;-------------------------------------------------------
Bool isObject = false;
Form frmTarget = obrfTarget.GetBaseObject();
;-------------------------------------------------------
; Check the name in the name list
;-------------------------------------------------------
; FurnitureWoodChoppingBlock / 0x00072dfb (KYWD)
If frmTarget.HasKeywordString("FurnitureWoodChoppingBlock")
isObject = true;
Endif; frmTarget.HasKeywordString("FurnitureWoodChoppingBlock")
;-------------------------------------------------------
; Return
;-------------------------------------------------------
return isObject;
EndFunction; isWoodChoppingBlock()
; ----------------------------------------------------------
; @name isOreVein
; @desc 対象のオブジェクトが鉱脈かどうか
; @function
; @global
; @param obrfTarget {ObjectReference} 対象のオブジェクト
; @returns {Bool}
; 鉱脈の場合はtrue
; ----------------------------------------------------------
Bool Function isOreVein(ObjectReference obrfTarget) global
;-------------------------------------------------------
; Init Variables
;-------------------------------------------------------
Bool isObject = false;
Form frmTarget = obrfTarget.GetBaseObject();
;-------------------------------------------------------
; Check the name in the name list
;-------------------------------------------------------
; isPickaxeFloor [KYWD:000613A8]
; isPickaxeTable [KYWD:000613A9]
; isPickaxeWall [KYWD:000A82C3]
If frmTarget.HasKeywordString("isPickaxeFloor") \
|| frmTarget.HasKeywordString("isPickaxeTable") \
|| frmTarget.HasKeywordString("isPickaxeWall")
isObject = true;
Endif; frmTarget.HasKeywordString("isPickaxeFloor") || ...
;-------------------------------------------------------
; Return
;-------------------------------------------------------
return isObject;
EndFunction; isOreVein()
}}
**盗み判定
盗みかどうかを判定する関数はない為、不法侵入と所有権で判定している。
#highlight(linenumber,php){{
Bool Function isStealing(ObjectReference obrfTarget, Actor actTarget)
;-------------------------------------------------------
; Declare Variables
;-------------------------------------------------------
Bool isStealing;
ActorBase abTarget;
Cell celThis;
ActorBase abOwnThis;
Faction fctOwnThis;
ActorBase abOwnHere;
Faction fctOwnHere;
;-------------------------------------------------------
; Init Variables
;-------------------------------------------------------
isStealing = actTarget.IsTrespassing();
abTarget = actTarget.GetActorBase();
celThis = obrfTarget.GetParentCell();
abOwnThis = obrfTarget.GetActorOwner();
fctOwnThis = obrfTarget.GetFactionOwner();
abOwnHere = celThis.GetActorOwner();
fctOwnHere = celThis.GetFactionOwner();
;-------------------------------------------------------
; Check Stealing
;-------------------------------------------------------
If abOwnThis && (abOwnThis != abTarget)
isStealing = true;
Elseif abOwnHere && (abOwnHere != abTarget)
isStealing = true;
Elseif fctOwnThis && !actTarget.IsInFaction(fctOwnThis)
isStealing = true;
Elseif fctOwnHere && !actTarget.IsInFaction(fctOwnHere)
isStealing = true;
Endif; abOwnThis && (abOwnThis != abTarget)
;-------------------------------------------------------
; Return
;-------------------------------------------------------
return true;
EndFunction; isStealing()
}}
*配置オブジェクトの操作
セル内に配置されたオブジェクトを操作する処理。
**配置オブジェクトを一括で初期位置に戻す
MoveToMyEditorLocation()が初期位置に戻す処理。
この例では、nObjectTypeでオブジェクトの種類を指定して、セル内のオブジェクトを一括で元の位置に戻している。
#highlight(linenumber,php){{
; ----------------------------------------------------------
; @name resetPositionsByObjectTypeIn
; @function
; @global
; @param [celTarget=None] {Cell}
; @param [nObjectType=0] {Int}
; @returns {Int}
; ----------------------------------------------------------
Int Function resetPositionsByObjectTypeIn( \
Cell celTarget = None, \
Int nObjectType = 0 \
) global
;-------------------------------------------------------
; Init Variables
;-------------------------------------------------------
ObjectReference obrfEach;
Int nIdx;
Int nCnt;
Int nReset = 0;
;-------------------------------------------------------
; Check the target cell
;-------------------------------------------------------
If !celTarget
celTarget = Game.GetPlayer().GetParentCell();
Endif; !celTarget
;-------------------------------------------------------
; Loop of objects
;-------------------------------------------------------
nIdx = 0;
nCnt = celTarget.GetNumRefs(nObjectType);
While (nIdx < nCnt)
obrfEach = celTarget.GetNthRef(nIdx, nObjectType);
If obrfEach
obrfEach.MoveToMyEditorLocation();
nReset += 1;
Endif; obrfEach
nIdx += 1;
EndWhile; (nIdx < nCnt)
;-------------------------------------------------------
; Return
;-------------------------------------------------------
return nReset;
EndFunction; resetPositionsByObjectTypeIn()
}}
**配置オブジェクトを一括で固定化する
SetMotionType()でMotion_Keyframedを指定すると、オブジェクトが固定化されて動かなくなる。
つまり、体当たりしても動かないオブジェクトになる。
この例では、nObjectTypeでオブジェクトの種類を指定して、セル内のオブジェクトを一括で固定化したり、固定化を解除したりしている。
#highlight(linenumber,php){{
; ----------------------------------------------------------
; @name setMotionTypeByObjectTypeIn
; @function
; @global
; @param [celTarget=None] {Cell}
; @param [enableLock=true] {Bool}
; @param [nObjectType=0] {Int}
; @returns {Int}
; ----------------------------------------------------------
Int Function setMotionTypeByObjectTypeIn( \
Cell celTarget = None, \
Bool enableLock = true, \
Int nObjectType = 0 \
) global
;-------------------------------------------------------
; Init Variables
;-------------------------------------------------------
ObjectReference obrfEach;
Int nIdx;
Int nCnt;
Int nLockCnt = 0;
;-------------------------------------------------------
; Check the target cell
;-------------------------------------------------------
If !celTarget
celTarget = Game.GetPlayer().GetParentCell();
Endif
;-------------------------------------------------------
; Loop of objects
;-------------------------------------------------------
nIdx = 0;
nCnt = celTarget.GetNumRefs(nObjectType);
While (nIdx < nCnt)
obrfEach = celTarget.GetNthRef(nIdx, nObjectType);
; Motion_Dynamic = 1
; Motion_SphereIntertia = 2
; Motion_BoxIntertia = 3
; Motion_Keyframed = 4
; Motion_Fixed = 5
; Motion_ThinBoxIntertia = 6
; Motion_Character = 7
If obrfEach.GetMass() && enableLock
obrfEach.SetMotionType(obrfEach.Motion_Keyframed);
nLockCnt += 1;
Elseif !enableLock
obrfEach.SetMotionType(obrfEach.Motion_Dynamic);
nLockCnt += 1;
Endif
nIdx += 1;
EndWhile
;-------------------------------------------------------
; Return
;-------------------------------------------------------
return nLockCnt;
EndFunction; setMotionTypeByObjectTypeIn()
}}
**配置オブジェクトを一括で収集する
死体は単純にMoveTo()では移動できないので、Disable()してMoveTo()してEnable()している。
#highlight(linenumber,php){{
; ----------------------------------------------------------
; @name gatherObjectsIn
; @function
; @global
; @param [celTarget=None] {Cell}
; @param [obrfMoveTo=None] {ObjectReference}
; @param [nType=0] {Int}
; Form-Type
; @param [nIsDeadAlive=0] {Int}
; 0 = both, 1 = lives only, -1 = deads only
; @returns {Bool}
; ----------------------------------------------------------
Bool Function gatherObjectsIn( \
Cell celTarget = None, \
ObjectReference obrfMoveTo = None, \
Int nType = 0, \
Int nIsDeadAlive = 0 \
) global
;-------------------------------------------------------
; Init Variables
;-------------------------------------------------------
Int nIdx;
Int nCnt;
ObjectReference obrfEach;
Actor actEach;
;-------------------------------------------------------
; Check arguments
;-------------------------------------------------------
If !obrfMoveTo
obrfMoveTo = Game.GetPlayer() as ObjectReference;
Endif
If !celTarget
celTarget = obrfMoveTo.GetParentCell();
Endif
;-------------------------------------------------------
; Scan chests
;-------------------------------------------------------
nIdx = 0;
nCnt = celTarget.GetNumRefs(nType);
While (nIdx < nCnt)
obrfEach = celTarget.GetNthRef(nIdx, nType);
If !obrfEach.IsEnabled() || obrfEach.IsDeleted()
obrfEach = None;
Endif
If obrfEach && (nType == 43)
actEach = obrfEach as Actor;
Else
actEach = None;
Endif
If actEach
If (nIsDeadAlive > 0) && actEach.IsDead()
obrfEach = None;
actEach = None;
Elseif (nIsDeadAlive < 0) && !actEach.IsDead()
obrfEach = None;
actEach = None;
Endif
Endif
If actEach && (nType == 43)
If actEach.IsDead()
actEach.Disable();
Endif; actEach.IsDead()
actEach.MoveTo(obrfMoveTo);
If actEach.IsDead()
actEach.Enable();
Endif; actEach.IsDead()
Elseif !actEach && obrfEach
obrfEach.MoveTo(obrfMoveTo);
Endif
nIdx += 1;
EndWhile
;-------------------------------------------------------
; Return
;-------------------------------------------------------
return true;
EndFunction; gatherObjectsIn()
}}
**配置オブジェクトを一括で検知する
変性呪文の検知魔法のようなもの。スクリプトで実装すれば、好きなエフェクトを設定できる。
#highlight(linenumber,php){{
; ----------------------------------------------------------
; @name detectObjectsIn
; @function
; @global
; @param [celTarget=None] {Cell}
; @param [nType=0] {Int}
; Form-Type
; @param [isLootableOnly=false] {Bool}
; @param [nIsDeadAlive=0] {Int}
; 0 = both, 1 = lives only, -1 = deads only
; @returns {Bool}
; ----------------------------------------------------------
Bool Function detectObjectsIn( \
Cell celTarget = None, \
Int nType = 0, \
Bool isLootableOnly = false, \
Int nIsDeadAlive = 0 \
) global
;-------------------------------------------------------
; Init Variables
;-------------------------------------------------------
Int nIdx;
Int nCnt;
ObjectReference obrfEach;
Actor actEach;
Actor actPlayer = Game.GetPlayer();
EffectShader efsEnemy;
EffectShader efsAlly;
Hazard hzdDetect;
Bool isLootable;
;-------------------------------------------------------
; Get Effect
;-------------------------------------------------------
; LifeDetected [EFSH:0x000146]
; LifeDetectedEnemy [EFSH:0x0DC209]
; LifeDetectedUndead [EFSH:0x0AAEB3]
; LifeDetectedUndeadEnemy [EFSH:0x016439]
; KynesPeaceFXS [EFSH:0x084B39]
; HealCircleFXS [EFSH:0x10CDC9]
; WerewolfTransFXS [EFSH:0x0EBEC5]
; WerewolfTrans02FXS [EFSH:0x0EBECD]
; DragonPowerAbsorbFXS [EFSH:0x0280C0]
; AbsorbBlueFXS [EFSH:0x0ABF08]
; AbsorbGreenFXS [EFSH:0x0ABF07]
; AbsorbHealthFXS [EFSH:0x0ABEFF]
efsAlly = Game.GetForm(0x000146) as EffectShader;
efsEnemy = Game.GetForm(0x0DC209) as EffectShader;
; CircleOfProtectionHazard [HAZD:0x04E80C]
; CircleVitalityHazard [HAZD:0x0B62EA]
; GuardianCircleHazard [HAZD:0x0E0CD3]
; GuardianCircleTurnHazard [HAZD:0x0FEAD3]
hzdDetect = Game.GetForm(0x04E80C) as Hazard;
;-------------------------------------------------------
; Check the target cell
;-------------------------------------------------------
If !celTarget
celTarget = Game.GetPlayer().GetParentCell();
Endif
;-------------------------------------------------------
; Scan chests
;-------------------------------------------------------
nIdx = 0;
nCnt = celTarget.GetNumRefs(nType);
While (nIdx < nCnt)
obrfEach = celTarget.GetNthRef(nIdx, nType);
If !obrfEach.IsEnabled() || obrfEach.IsDeleted()
obrfEach = None;
Endif
If obrfEach && (nType == 43)
actEach = obrfEach as Actor;
Else
actEach = None;
Endif
If actEach
If (nIsDeadAlive > 0) && actEach.IsDead()
obrfEach = None;
actEach = None;
Elseif (nIsDeadAlive < 0) && !actEach.IsDead()
obrfEach = None;
actEach = None;
Endif
Endif
If !isLootableOnly
isLootable = false;
Elseif actEach
isLootable = (actEach.GetNumItems() > 1);
Elseif obrfEach && (nType == 28)
isLootable = (obrfEach.GetNumItems() > 0);
Else
isLootable = false;
Endif
If isLootableOnly && !isLootable
obrfEach = None;
actEach = None;
Endif
If actEach && (nType == 43)
If isEnemy(actEach)
efsEnemy.Play(actEach, 26.0);
Else
efsAlly.Play(actEach, 26.0);
Endif; isEnemy(actEach)
Elseif !actEach && obrfEach
obrfEach.placeAtMe(hzdDetect);
Endif
nIdx += 1;
EndWhile
;-------------------------------------------------------
; Return
;-------------------------------------------------------
return true;
EndFunction; detectObjectsIn()
}}
*NPCを操作する
NPCをPCのように操作する為に必要な一連の処理。
PCの操作停止、NPCの操作、カメラの切替、など。
#highlight(linenumber,php){{
Bool Function startNpcControl(Actor actTarget)
;-------------------------------------------------------
; Init Variables
;-------------------------------------------------------
;-------------------------------------------------------
; Start controlling
;-------------------------------------------------------
Game.DisablePlayerControls( \
abMovement = false, \
abFighting = true, \
abCamSwitch = false, \
abLooking = false, \
abSneaking = false, \
abMenu = true, \
abActivate = false, \
abJournalTabs = false, \
aiDisablePOVType = 0 );
actTarget.SetPlayerControls(true);
Game.SetPlayerAIDriven(true);
actTarget.EnableAI();
Game.SetCameraTarget(actTarget);
Game.ForceThirdPerson();
;-------------------------------------------------------
; Return
;-------------------------------------------------------
Return true;
EndFunction; startNpcControl()
Bool Function stopNpcControl(Actor actTarget)
;-------------------------------------------------------
; Init Variables
;-------------------------------------------------------
Actor actPlayer = Game.GetPlayer();
;-------------------------------------------------------
; Switch the controlling
;-------------------------------------------------------
actTarget.SetPlayerControls(false);
Game.SetPlayerAIDriven(false);
Game.EnablePlayerControls();
actTarget.EnableAI();
Game.SetCameraTarget(actPlayer);
;-------------------------------------------------------
; Return
;-------------------------------------------------------
Return true;
EndFunction; stopNpcControl()
}}
*マップ・マーカーを表示する
指定したクエストのエイリアスに任意のリファレンスをマーカーとして表示する。
クエストは名前でも指定可能。エイリアスは名前やIDや番号でも指定可能。
例では、isActiveで表示と非表示を切り替える。
#highlight(linenumber,php){{
; ----------------------------------------------------------
; @name showMapMarker
; @function
; @global
; @param obrfTarget {ObjectReference}
; @param [isActive=true] {Bool}
; @param [szQuestName=] {String}
; @param [nObjective=1] {Int}
; @param [szAliasName=RefAlias001] {String}
; @param [qstMarker=None] {Quest}
; @param [ralsMarker=None] {ReferenceAlias}
; @param [nAliasIdx=-1] {Int}
; @param [nAliasID=-1] {Int}
; @returns {Bool}
; ----------------------------------------------------------
Bool Function showMapMarker( \
ObjectReference obrfTarget, \
Bool isActive = true, \
String szQuestName = "", \
Int nObjective = 1, \
String szAliasName = "RefAlias001", \
Quest qstMarker = None, \
ReferenceAlias ralsMarker = None, \
Int nAliasIdx = -1, \
Int nAliasID = -1 \
) global
;-------------------------------------------------------
; Init Variables
;-------------------------------------------------------
Bool isOK = false;
;-------------------------------------------------------
; Check Arguments
;-------------------------------------------------------
If !obrfTarget
return isOK;
Endif
;-------------------------------------------------------
; Get Marker Quest
;-------------------------------------------------------
If !qstMarker
qstMarker = Quest.GetQuest(szQuestName);
Endif
If !qstMarker
return isOK;
Endif
;-------------------------------------------------------
; Get Reference Alias
;-------------------------------------------------------
If !ralsMarker && (nAliasIdx >= 0)
ralsMarker = \
qstMarker.GetNthAlias( \
nAliasIdx ) as ReferenceAlias;
Elseif !ralsMarker && (nAliasID >= 0)
ralsMarker = \
qstMarker.GetAliasById( \
nAliasID ) as ReferenceAlias;
Elseif !ralsMarker && (szAliasName != "")
ralsMarker = \
qstMarker.GetAliasByName( \
szAliasName ) as ReferenceAlias;
Endif
If !ralsMarker
return isOK;
Endif
;-------------------------------------------------------
; Show Marker
;-------------------------------------------------------
If isActive
ralsMarker.ForceRefTo(obrfTarget);
qstMarker.SetObjectiveDisplayed( \
nObjective, true, false );
isOK = true;
Else
ralsMarker.Clear();
qstMarker.SetObjectiveDisplayed( \
nObjective, false, false );
isOK = true;
Endif
;-------------------------------------------------------
; Return
;-------------------------------------------------------
return isOK;
EndFunction; showMapMarker()
}}
*対象の人の戦闘を強制停止させる
戦闘を停止させる処理の詰め合わせ。
StopCombat()は対象のアクターの戦闘状態を停止させることしか出来ない。
本格的に戦闘停止させるには、警戒状態も解除し、相手方の戦闘状態も解除する必要がある。
disableRecoverのオプションは、戦闘停止時に全回復するようなMOD対策。
戦闘停止後に、停止前の体力に強制的に戻す為のオプション。
#highlight(linenumber,php){{
; ----------------------------------------------------------
; @name stopCombatEx
; @function
; @global
; @param actTarget {Actor}
; @param [disableRecover=true] {Bool}
; @param [enableStopAlarm=false] {Bool}
; @param [enableStopTarget=false] {Bool}
; @returns {Bool}
; ----------------------------------------------------------
Bool Function stopCombatEx( \
Actor actTarget, \
Bool disableRecover = true, \
Bool enableStopAlarm = false, \
Bool enableStopTarget = false \
) global
;-------------------------------------------------------
; Init Variables
;-------------------------------------------------------
Float nHealth;
Float nDamage;
Actor actEnemy;
;-------------------------------------------------------
; Check Arguments
;-------------------------------------------------------
If !actTarget || !actTarget.IsInCombat()
return false;
Endif; !actTarget || !actTarget.IsInCombat()
;-------------------------------------------------------
; Get Current Health
;-------------------------------------------------------
If disableRecover
nHealth = actTarget.GetActorValue("Health");
Endif; disableRecover
;-------------------------------------------------------
; Stop Combat (Self)
;-------------------------------------------------------
actTarget.StopCombat();
;-------------------------------------------------------
; Stop Alarm
;-------------------------------------------------------
If enableStopAlarm
actTarget.StopCombatAlarm();
Endif; enableStopAlarm
;-------------------------------------------------------
; Stop Combat (Enemy)
;-------------------------------------------------------
If enableStopTarget
actEnemy = actTarget.GetCombatTarget();
If actEnemy
stopCombatEx(actEnemy, disableRecover, false, false);
Endif
Endif; enableStopTarget
;-------------------------------------------------------
; Damage Health
;-------------------------------------------------------
If disableRecover
nDamage = actTarget.GetActorValue("Health") - nHealth;
If (nDamage > 0.0)
actTarget.DamageActorValue("Health", nDamage);
Endif; (nDamage > 0.0)
Endif; disableRecover
;-------------------------------------------------------
; Return
;-------------------------------------------------------
return true;
EndFunction; stopCombatEx()
}}
*外見変更
**顔テキスチャの変更
NetImmerse Overrideが必要。
アクターの顔テキスチャを変更する処理。顔用のTextureSetを指定する。
#highlight(linenumber,php){{
; ----------------------------------------------------------
; @name changeFace
; @function
; @global
; @param actTarget {Actor}
; @param txtSkin {TextureSet}
; @returns {Bool}
; ----------------------------------------------------------
Bool Function changeFace(Actor actTarget, TextureSet txtSkin) global
;-------------------------------------------------------
; Init Variables
;-------------------------------------------------------
ActorBase abTarget;
Int nHeadIdx = -1;
String szNode = "";
;-------------------------------------------------------
; Check Arguments
;-------------------------------------------------------
If !actTarget || !txtSkin
return false;
Else
abTarget = actTarget.GetLeveledActorBase();
Endif; !actTarget || !txtSkin
;-------------------------------------------------------
; Get Head Index
;-------------------------------------------------------
If abTarget
nHeadIdx = abTarget.GetIndexOfHeadPartByType(1);
Endif; abTarget
;-------------------------------------------------------
; Get Node Name
;-------------------------------------------------------
If (nHeadIdx >= 0)
szNode = abTarget.GetNthHeadPart(nHeadIdx).GetName();
Endif; (nHeadIdx >= 0)
;-------------------------------------------------------
; Apply Face Texture
;-------------------------------------------------------
If (nHeadIdx >= 0)
abTarget.SetFaceTextureSet(txtSkin);
NetImmerse.SetNodeTextureSet( \
actTarget, szNode, txtSkin, false);
Endif; (nHeadIdx >= 0)
;-------------------------------------------------------
; Return
;-------------------------------------------------------
return true;
EndFunction; changeFace()
}}
**髪パーツの変更
#highlight(linenumber,php){{
; ----------------------------------------------------------
; @name changeHair
; @function
; @global
; @param actTarget {Actor}
; @param hpNewHair {HeadPart}
; @returns {Bool}
; ----------------------------------------------------------
Bool Function changeHair(Actor actTarget, HeadPart hpNewHair)
;-------------------------------------------------------
; Init Variables
;-------------------------------------------------------
ActorBase abTarget = actTarget.GetLeveledActorBase();
Bool isPlayer = (Game.GetPlayer() == actTarget);
Int nHeadIdx;
HeadPart hpCurHair;
;-------------------------------------------------------
; Get Hair Headparts
;-------------------------------------------------------
nHeadIdx = abTarget.GetIndexOfHeadPartByType(3);
hpCurHair = abTarget.GetNthHeadPart(nHeadIdx);
;-------------------------------------------------------
; Reset Hair Part
;-------------------------------------------------------
If !isPlayer && hpNewHair
actTarget.ReplaceHeadPart(hpCurHair, hpNewHair);
Elseif isPlayer && hpNewHair
actTarget.ChangeHeadPart(hpNewHair);
Endif; !isPlayer && hpNewHair
;-------------------------------------------------------
; Return
;-------------------------------------------------------
return true;
EndFunction; changeHair()
}}
*プレイヤーのヘッド・トラッキング
指定したオブジェクトに対して、プレイヤーにヘッド・トラッキングさせる処理。
自分自身を対象に指定すると、ゆっくり正面を向く。(ヘッド・トラッキングは継続)
ヘッド・トラッキングそのものを停止すると、急に正面を向く。
#highlight(linenumber,php){{
Bool Function startPlayerHeadTracking(ObjectReference obrfTarget)
;-------------------------------------------------------
; Declare Variables
;-------------------------------------------------------
Actor actPlayer = Game.GetPlayer();
;-------------------------------------------------------
; Player Head Tracking
;-------------------------------------------------------
If obrfTarget
actPlayer.SetHeadTracking(true);
actPlayer.SetLookAt(obrfTarget, false);
actPlayer.SetAnimationVariableBool("bHeadTrackSpine", false);
actPlayer.SetAnimationVariableInt("IsNPC", 1);
Endif; obrfTarget
;-------------------------------------------------------
; Return
;-------------------------------------------------------
return true;
EndFunction; startPlayerHeadTracking()
Bool Function stopPlayerLooking()
;-------------------------------------------------------
; Declare Variables
;-------------------------------------------------------
Actor actPlayer = Game.GetPlayer();
;-------------------------------------------------------
; Player Head Tracking
;-------------------------------------------------------
actPlayer.SetLookAt(actPlayer, false);
;-------------------------------------------------------
; Return
;-------------------------------------------------------
return true;
EndFunction; stopPlayerLooking()
Bool Function stopPlayerHeadTracking()
;-------------------------------------------------------
; Declare Variables
;-------------------------------------------------------
Actor actPlayer = Game.GetPlayer();
;-------------------------------------------------------
; Player Head Tracking
;-------------------------------------------------------
actPlayer.ClearLookAt();
actPlayer.SetAnimationVariableBool( \
"bHeadTrackSpine", false );
actPlayer.SetAnimationVariableInt("IsNPC", 0);
actPlayer.SetHeadTracking(false);
;-------------------------------------------------------
; Return
;-------------------------------------------------------
return true;
EndFunction; stopPlayerHeadTracking()
}}
*配列の要素数を128を超えたものを扱いたい、または動的に配列を宣言したい
Papyrusの配列は通常は1~128までの範囲しか扱う事ができず、
それ以上の配列数を宣言しようと128を超える数値を指定するとエラーが発生します。
また、配列宣言時に配列数の部分を変数で指定した場合もエラーが発生します。
**NGパターン
#highlight(linenumber,php){{
Actor[] NPCArray
int[] ValArray
Function Test()
int num = 0
NPCArray = new Actor[200] ;1~128までの範囲で宣言しろとエラーが出る
; 変数の値分の配列を宣言したいが
; 配列数の指定に変数を指定するなとエラーが出る
num = 200
ValArray = new int[num]
EndFunction; Test()
}}
配列数が128を超える配列を作成したい場合、
Float、Int、String、Form、Alias型の場合は
SKSEのPapyrus拡張関数である
Utility.Create~Arrayを
用いる事で配列数が128を超える配列を作成することができます。(~の部分は型によって異なりますのでスクリプトリファレンスを確認してください。)
https://www.creationkit.com/index.php?title=Utility_Script
Actor型の場合は標準のSKSEの拡張関数には存在しないため
Papyrus拡張SKSEプラグインであるPapyrusUtilが必要となります。
この方法で配列の宣言をする場合、処理に必要な配列数を動的に宣言できるようになる他に第2引数に値を指定する事で宣言と同時に各要素の値を指定値で初期化する事ができます。
**実装例
#highlight(linenumber,php){{
Actor[] NPCArray
int[] ValArray
Function Test()
int num = 200 ;
NPCArray = PapyrusUtil.ActorArray(num, None)
ValArray = Utility.CreateIntArray(num, 10) ; 配列の各要素の値を10で初期化
EndFunction; Test()
}}
*装備してる武器や防具に付いているエンチャントを取得する(自分で付呪したものも取得する方法)
装備してる武器、防具のエンチャントを取得する処理。
WeaponとArmorのGetEnchantment関数は自分から付呪を施したエンチャントを取得できないため、
WornObject.GetEnchantment関数でのチェックも行う。
#highlight(linenumber,php){{
; 装備してる武器のエンチャントを取得
; iRightHandに0を渡せば左手武器のエンチャントを取得
Enchantment Function GetWeaponEnchantment(Actor akTarget, int iRightHand = 1)
Weapon w = akTarget.GetEquippedObject(iRightHand) as Weapon
if !w
return None
endif
Enchantment ench = w.GetEnchantment()
if ench
return ench
endif
return WornObject.GetEnchantment(akTarget, iRightHand, 0)
EndFunction
; 装備してる防具のエンチャントを取得
Enchantment Function GetArmorEnchantment(Actor akTarget, int iSlotMask)
Armor arm = akTarget.GetWornForm(iSlotMask) as Armor
if !arm
return None
endif
Enchantment ench = arm.GetEnchantment()
if ench
return ench
endif
return WornObject.GetEnchantment(akTarget, 0, iSlotMask)
EndFunction
}}
上記サンプルのGetArmorEnchantment関数で第ニ引数に渡す値はスロットマスクとなります。
スロットマスクの値は[[こちら>https://www.creationkit.com/index.php?title=Slot_Masks_-_Armor]]のページを参照してください。
最初からエンチャントが付いてる装備については上記のようにGetEquippedObjectやGetWornForm等でアイテムを取得し
それに対してGetEnchantment関数を使用すれば取得可能です。
ただし、GetEquippedObject等で取得できる武器と防具のデータはベースフォームであり、
自分から付呪を行った武器や防具に対してGetEnchantment関数を使用した場合はNoneを返します。
自分から付呪を行った武器や防具のエンチャントを取得したい場合は
WornObjectオブジェクトのGetEnchantment関数を使用すれば取得できます。
ただし、WornObjectオブジェクトのGetEnchantment関数は
&color(#F54738){対象の装備に自分で付呪したエンチャントがある場合のみ、エンチャントを返す}という仕様のため
逆に最初からエンチャントが付いてる装備の場合はNoneを返します。
そのため、現在装備しているアイテムのエンチャントを取得する場合は
サンプルのように最初から付呪がされてるアイテムであるか確認し、
そうでない場合、WornObjectオブジェクトのGetEnchantment関数で自前で作成した付呪を取得するようにしましょう。
*MCMの設定を保存、ゲーム開始時や再開時に設定を自動読込する
外部ファイルにMCMの設定を保存する事でニューゲーム時や違うセーブデータでゲームを再開した時に
一々MCMの再設定をする手間を省かせるようにする。
現状でメジャーなのは[[PapyrusUtil>https://skyrimspecialedition.2game.info/detail.php?id=13048]]のJsonUtilオブジェクトを使用する方法か
[[MCMHelper(SEのみ)>https://skyrimspecialedition.2game.info/detail.php?id=53000]]を使用する方法だが、
PapyrusUtilによる方法を記載する。
&color(#F54738){注:FISSでの自動読込は禁止。仕組み上の問題で複数のスクリプトが同時にFISSを使用するとCTDとなるため。}
#highlight(linenumber,php){{
Scriptname [スクリプト名] extends ski_configbase
GlobalVariable Property SampleGV auto
~省略~
String settings_path = "..\\[Mod名]\\UserSettings"
; MCMはセーブからのロード時にOnGameReloadイベントが発生するため
; この時に設定の読み込みを行う
Event OnGameReload()
parent.OnGameReload()
LoadUserSettingsPapyrus()
endEvent
; MCMを閉じた時に実行
; 設定を自動保存
Event OnConfigClose()
parent.OnConfigClose()
SaveUserSettingsPapyrus()
EndEvent
; MCMの初期化
; ニューゲーム時に設定の自動読込が実行される
event OnConfigInit()
;MCMの初期設定を記述
~省略~
LoadUserSettingsPapyrus()
endEvent
Bool function SaveUserSettingsPapyrus()
JsonUtil.SetPathIntValue(settings_path, "Sample", SampleGV.GetValueInt())
if !JsonUtil.Save(settings_path, false)
debug.Trace(ModName + ": Error saving user settings.", 0)
return false
endIf
return true
endFunction
Bool function LoadUserSettingsPapyrus()
if !JsonUtil.IsGood(settings_path)
ShowMessage(ModName + ": Can't load User Settings. Errors: {" + JsonUtil.getErrors(settings_path) + "}")
return false
endIf
SampleGV.SetValueInt(JsonUtil.GetPathIntValue(settings_path, "Sample", SampleGV.GetValueInt()))
return true
endFunction
~OnConfigOpen等のMCMの各イベントの処理の記述は省略~
}}
MCMを開いて後に閉じた後に
(ルートディレクトリ)\SKSE\Plugins\[Mod名]ディレクトリにUserSettings.jsonが生成されている事、
セーブからのロードやニューゲーム時にMCMの設定が引き継がれている事を確認できたら、
MCMの自動保存と自動読込の作成は完了となります。
(MO2の場合は\overwrite\SKSE\Plugins\[Mod名]ディレクトリにUserSettings.jsonが生成されます。)
・上記サンプルでUserSettings.jsonに記載される内容
#highlight(linenumber,php){{
{
Sample:[設定値(整数)]
} }}
ゲーム開始時にModの設定を一つ一つ手動でロードするのはユーザー側にとってかなり手間となるため
基本的には自動読込は実装するようにしましょう。
保存に関しては設定の切り替えが簡単だったり項目数が少なければ自動保存で問題ありませんが、
設定項目がかなりの数がある場合は自動保存はMCM側の設定で実行するか制御できるようにし、
手動保存と手動読込の手段も一緒に追加すると良いです。
余裕があれば自動保存、自動読込は設定で制御できるようにすると良いでしょう。
2024-02-10T18:07:34+09:00
1707556054
-
SKSEプラグイン
https://w.atwiki.jp/skyrim_mod/pages/17.html
#contents
* SKSE
外部拡張スクリプトであるSkyrim Script Extender(SKSE)の情報です。
**SKSEの特徴
SKSEは2つの機能があって、ひとつはPapyrus言語そのものを拡張すること、ふたつめはプラグインで拡張することです。
***Papyrus関数拡張
ひとつめはPapyrus言語で使える関数を増やします。
例えば、Get(Set)WeaponSpeed()は武器の振る速度を取得したりセットしたりできます。
スキルレベルに応じて武器速度を変えたりといったような応用できます。
Papyrusでは武器そのものの速度を扱うことはできません。
SKSEの関数一覧
[[https://www.creationkit.com/index.php?title=Category:SKSE]]
単に関数を書くだけで使えるので使用は容易ですがPapyrusの実行速度がボトルネックになる為、後述のプラグイン拡張よりは遅いです。
***プラグイン拡張
C/C++のコードをコンパイルしてdllファイルにし、Data->SKSE->Pluginsに入れて動くものです。
C/C++の速度で動くのでPapyrusに比べて駆動自体は圧倒的に速いですが、Papyrusの関数を呼び出すときはその関数の速度でしか動きません。
また資料が少なく難解で膨大なSKSEのAPIを理解していないとプラグインの作成が難しいのがデメリットです。
***SKSE開発情報&サンプル
-[[SKSE64プラグイン開発環境構築手順]]
-[[Papyrus関数追加サンプル]]
**Papyrus関数拡張SKSE
以下のSKSEを導入しておけば標準ではできない事のほとんどがスクリプトで行えるようになるため可能ならば全て導入しておく事を推奨。(変数の保存、ファイル読み書きについてはPapyrusUtilかJContainersのどちらか好みで)
***PapyrusUtil
・TFC、TMコマンドの実行、アクターのAI Packageの上書き、指定したアクター・オブジェクトから一定範囲内にいるオブジェクト・アクターの検索等、多機能の関数を追加する
・オブジェクト、アクター毎に変数の保存、読込が行える。(これによりスクリプトでアクターの状態、フラグの保存にダミーのFactionを作って変数代わりに設定する必要が無くなる。)
・Jsonファイルにデータを保存、読込が可能になる(ファイル読み書きを行うものとしてFISSがあるが検証によるとPapyrusUtilの方が高機能、またFISSは自動ロード等で一斉に読み込もうとするとCTDの要因になるとの事でファイル保存・読込関連はPapyrusUtil、または後述のJContainersを使うのを推奨)
・AE対応済み
ダウンロード([[LE>https://www.nexusmods.com/skyrim/mods/58705]] / [[SE>https://www.nexusmods.com/skyrimspecialedition/mods/13048]])
***ConsoleUtil
・コンソールコマンドをPapyrusから実行する事が可能になる、コンソールのメッセージの取得も可能
・一部のコンソールコマンドはPapyrus側には存在しないため、それを実行するために必要(NPCのレベルを設定するSetLevel等)
・AE対応済み
ダウンロード([[LE>https://www.nexusmods.com/skyrim/mods/66257]] / [[SE>https://www.nexusmods.com/skyrimspecialedition/mods/24858]]) ※注:AEの最新版に対応していません
[[NG版(SEとAE両用、AEの最新版にも対応してます)>https://www.nexusmods.com/skyrimspecialedition/mods/76649]]
***powerofthree's Papyrus Extender(ドキュメントあり)
・PapyrusUtil同様、多機能の関数を実装。特にアクター関係の関数に関しては随一
・Oblivionでは存在したがSkyrimでは存在しない関数と同機能の関数が使えるようになる(IsActorInWater、GetCombatTargets、GetActorsByProcessingLevel等)
・アクター、エイリアス、マジックエフェクトのスクリプトで受信できるEventを拡張する事が可能
・このSKSEのAddBasePerk関数はNPCに対してゲーム中にPerkを付与させられる唯一の手段となる
・AE対応済み
・機能ピックアップ
・・戦闘にて指定したアクターの味方のアクター、敵のアクターの一覧を習得
・・水に浸かっているか、または水中にいるか判定
・・パークを動的に習得
・・死亡時アイテムを動的に設定
・・関数一つで肌テクスチャを変える(このSKSEを使用しない場合、かなり面倒)
・・キルムーブの首刎ねモーションなしで首と胴体を分離
・・所有しているパークの数、一覧を取得
・・フォームのエディターIDの取得
・・エディターIDを指定してフォームを取得
[[ドキュメント>https://github.com/powerof3/PapyrusExtenderSSE/wiki]]
ダウンロード([[LE>https://www.nexusmods.com/skyrim/mods/95017]] / [[SE>https://www.nexusmods.com/skyrimspecialedition/mods/22854]])
***JContainers
・Papyrusでクラスやスクリプトのインスタンスを実現するため、JSONベースのデータ構造を用いてPapyrusを拡張する
・PapyrusUtil同様に変数の保存、読込が可能、外部にデータをファイルとして保存する機能も搭載されている
・AE対応済み
※これを追記した筆者はあまり使ってないため、詳しい人が居ましたら追記お願いします
ダウンロード([[LE>https://www.nexusmods.com/skyrim/mods/49743]] / [[SE>https://www.nexusmods.com/skyrimspecialedition/mods/16495]])
***SUP SKSE
・文字列に関する関数が豊富で置換や部分削除等を行える他、検索と置換を正規表現で行える関数も用意されている
・一時的な値の設定・読込を行う関数が用意されている。一時的に保持した値はゲーム終了時に破棄される(OblivionのModLocalDataと同機能のものが使える)
・スカイリムのインストールフォルダのパスを取得、ダイアログを開いてファイルを選択、スクリーンショットを取るなど、他のPapyrus拡張SKSEにない独特の関数が用意されてる
[[ダウンロード(SE)>https://www.nexusmods.com/skyrimspecialedition/mods/58133]]
[[ドキュメント>https://www.nexusmods.com/skyrimspecialedition/articles/3528]]
***Papyrus MessageBox - SKSE NG
・このSKSEで追加した関数一つで選択肢付きメッセージボックスを表示させられるようになるSKSE
・従来では選択肢付きメッセージボックスを出す場合はCKやxEditでMessageレコードを作成して、スクリプトから参照する必要があったがそれが不要になる
・また、特定の条件下でメッセージの内容を変えるという事をやる場合、条件の数だけMessageレコードを作成する必要があったり、 選択肢をCondition Functionsではなくスクリプト内で条件を絞り込みたい場合、このSKSEを用いる事で表示させるメッセージや選択肢をスクリプト内で動的に制御できるようになる
[[ダウンロード(SE・AE)>https://www.nexusmods.com/skyrimspecialedition/mods/83578]]
**開発や動作の補助をするSKSE
主に有名所なModでの使用や現在のMod環境では導入が前提である事が多かったり、
Mod開発者にとっても利用する事による利点が非常に大きいSKSEプラグインの一群を記載。
なお、ほとんどのSKSEはSE版が安定した後に制作されたものであるため
&bold(){LE版には存在しないものがほとんど}なので注意。
([[こちら>https://w.atwiki.jp/skyrim_mod/pages/18.html#id_148ac90b]]で記述してますが特に初心者はLE・SE両用ではなくSE版のみのModを作る事を&bold(){強く推奨します})
利用しなくてもMod開発自体はできるがユーザーにとっても開発側にとっても&color(#F54738){利用しない事によるデメリット(特にフォームの競合による動作不良)が非常に大きいため、}
Modのリリースを行いたいという人は一通り目を通しておく事。
***Spell Perk Item Distributor (SPID)
・設定ファイルを用いる事でNPCにアビリティやPerk、アイテムを事前に付与させる事が可能になるSKSE
・付与対象の条件絞り込みが可能
・最新バーションでは一定レベルによる付与の設定はNPCの現在のレベルを参照に動的に付与されるようになった(以前は一定レベルによる付与は初期レベル依存だった)
・NPCのレコードを汚さずにアビリティやPerkを持たせられるため、競合による不具合やマージを行わずにすむようになる。
・クロークを使用して周りのNPCにスクリプト付きスペルを付与するという事をやらずに済むため負荷も少なくなる
・上記の理由によりNPCにスクリプト付アビリティを持たせるModではSPIDを用いるのが主流…どころか&color(#F54738){&bold(){現Mod環境において数多くのModはこれを導入した環境が大前提となるためこれが未導入の環境はまず考慮しなくても良い。SPIDで処理できるものならばSPIDで処理する事で競合による問題が無くなるので扱いこなす事を強く推奨}}
・LE版は一部機能が使えないため注意(対象をエディターIDで指定する等が使えない)
・&bold(){プレイヤーに対しての呪文追加やパーク追加は&color(#F54738){SPIDでは行えない。}}プレイヤーに対して呪文追加やパーク追加をしたいならば後述する[[SkyPatcher>#id_286a3ed2]]を使う事
ダウンロード([[LE>https://www.nexusmods.com/skyrim/mods/105121]] / [[SE・AE>https://www.nexusmods.com/skyrimspecialedition/mods/36869]])
***Keyword Item Distributor(KID)
・アイテムや魔法エフェクト等、NPC以外のデータにキーワードを事前に付与させる事が可能になるSKSE(NPCはSPIDで付与可能)
・SPIDが導入されていた場合、KIDの付与処理実行後にSPIDの付与処理が実行される
・エンチャントであるか、攻撃的な効果であるか(Hostileフラグがあるか)等の条件で絞ってキーワード付与が可能
・魔法やパークを作成時にCondition Functionsで詳細な条件で絞り込みたい時に非常に有用。例えばこのSKSEでHostileフラグのある全Magic Effectにキーワード付与を行う事で従来ではできなかった「攻撃的な魔法のみ対象」といった判定条件が行えるようになる
・SPID程ではないが&color(#F54738){&bold(){現Mod環境ではこれを導入した環境が当たり前となってるためバニラの既存レコードの編集をする時にキーワードを追加したいだけの場合はKIDで処理する事を強く推奨}}
ダウンロード([[SE・AE>https://www.nexusmods.com/skyrimspecialedition/mods/55728]])
***FormList Manipulator - FLM
・設定ファイルを使用してフォームリストをゲーム起動時に動的に追加するSKSE
・Espの編集で既存のフォームリストに追加したいものがある場合、別のModでそのフォームリストの編集があると競合して編集内容が正常に反映されない危険性があるがFLMを使用する事により競合を回避する事ができる
・バニラのフォームリストの変更を行うModを作る場合はこれを使用する事を&bold(){強く推奨}
・Papyrusの&bold(){ModEvent受信時に動的にフォームリストに追加する事が可能。}特定イベント発生後に店に並ぶ商品が増えるという事を行いたい時に有効
ダウンロード([[SE・AE>https://www.nexusmods.com/skyrimspecialedition/mods/74037]])
***Item Property Manipulator - IPM
・設定ファイルを使用してアイテムのフォームデータをゲーム起動時やロード時、あるいはスクリプトから動的に変更するSKSE
・アイテム名、価格、ダメージ値や重量などを変更する事ができる
・バニラのアイテムを変更するModを作る場合はこれを使用する事を&bold(){強く推奨}
ダウンロード([[SE・AE>https://www.nexusmods.com/skyrimspecialedition/mods/95795]])
***SkyPatcher
・設定ファイルを使用してゲーム起動時に様々なフォームデータの中身を動的に追加するSKSE
・2023年12月頃に出たSKSEプラグインで上記のSPID等の総合版とも呼べる内容(元はFallout4用に作られたものをSkyrim用に移植)
・・従来のプラグインでは対応していなかったレベルドリストやレースの編集を行う事が可能
・・これにより競合が起こりそうなレコードのパッチ対応はクエスト関連を除けばほとんどSKSEプラグインで行うことができるようになった
・SPIDやKIDと同じようにNPCへのアイテムやパーク、キーワード追加が行える他にSPIDではできなかったクラスの変更や不死設定が行える
・・&bold(){所有アイテムや呪文、パークの削除}が行える。従来のプラグインには追加や変更はあったが特定のアイテムや呪文をNPCから消すという事ができなかった
・・SPIDやKIDではできなかった&bold(){プレイヤーに対するアイテムや呪文、キーワード追加が可能}
・・SPIDとは違い&bold(){一定確率で付与、または現在レベルが条件というような動的なアイテムや呪文、パークの付与はできない。}これ一つで全てを補えるわけではないので注意
・上記の通り、従来のSPID等のSKSEプラグイン群にはゲーム内での条件に合わせて動的に付与する仕組みがあるのとフィルタリングに関してもあちら側で条件として絞れてもSkyPatcherでは絞れないというのもあるので従来のは全く不要となるわけではない
ダウンロード([[SE・AE>https://www.nexusmods.com/skyrimspecialedition/mods/106659]])
[[ドキュメント>https://www.nexusmods.com/skyrimspecialedition/mods/106659?tab=articles]]
***Papyrus Tweaks NG
・スクリプト処理のアルゴリズムを修正し、パフォーマンスを劇的に改善するSKSEDLL
・一度に処理できるタスク(命令数)が100までだったのを500まで増加(設定で変更可能)。これにより一度に処理できる命令数が低いせいで大規模戦闘等でFPSが下がる(スクリプト処理速度も低下する)状況下だと処理待ち命令が増えて行き、最終的にフリーズするという現象を回避しやすくなる
・実験的な処理高速化の機能として一部の命令をメインスレッド上で処理する機能を追加。これにより命令処理する度に一々同期処理してた事による処理遅延(1命令0.01~0.1秒、FPSが低いほど長くなる)が消える、すなわちスクリプトの処理速度が実質FPS依存となってた問題が消えるため処理速度が大幅に向上(例として10秒ほど掛かる処理が1~2秒、0.5秒ほど処理が掛かるものが一瞬で終わるほどに短縮)
・仕組み上の問題で実質一度に処理できるスクリプト処理の上限が設けられた上にその上限自体が低く上限設定も不可能であったようなものであったため、パソコンのCPUを高性能化してもスクリプト処理に対する恩恵がほとんど得れなかったが、このSKSEプラグインによる大幅改善によりCPU高性能化の恩恵がかなり得られるようになった(特に処理高速化はCPUが強力である程効果大)
・処理高速化によるスクリプトの高速処理はCTDやフリーズのリスクを大幅に軽減してくれるがスクリプトの記述の仕方によっては正常に動作しない事もある。そのため、これの処理高速化の機能をONにした環境で正常に動作するスクリプトを作成する事を&color(#F54738){&bold(){強く推奨}}
ダウンロード([[SE・AE>https://www.nexusmods.com/skyrimspecialedition/mods/77779]])
***Payload Interpreter
・モーションに無敵時間の発生や魔法効果の発動等の命令を仕込む事ができるようになるSKSE
・モーションの特定のタイミングでスクリプト処理を実行させる事が可能になる
・モーションに仕込まれた命令は実行する前にモーションが中断された場合は実行されない。このため、従来の必殺技系のModでは処理が全部スクリプトで制御されてたためにモーションが何かしらの要因で中断されてしまった後も処理が続くという問題がPayload Interpreterを使う事で無くせる
・これを使ったModを作成する場合は基本的に[[DAR>https://skyrimspecialedition.2game.info/detail.php?id=33746]]や[[OAR>https://skyrimspecialedition.2game.info/detail.php?id=92109]]等の条件に応じた動的なアニメーション変更Modとの併用が前提となる
ダウンロード([[SE・AE>https://www.nexusmods.com/skyrimspecialedition/mods/65089]])
**Papyrus以外でスクリプト処理を行えるようになるSKSE
***Skyrim Platform
&color(#F54738){注:バージョンによっては動作が不安定だったりするため上級者向け}
・JavaScriptのコードからSkyrimの動作を制御するという革命的なSKSE
・DLLファイルがJavaScriptのコードを解析、処理を行うため圧倒的な速度で処理を行える(PapyrusとSkyrim Platformの両方でほぼ同じアルゴリズムを使用して、552個のアイテムをプレーヤーからチェストに転送が完了するまで計測した所、Papyursで130秒ほど掛かるものが、Skyrim Platformだと1秒以内に終わる[ModページのPostより])
・Skyrim起動中にJavaScriptのコードが更新されると再読み込みが行われ、コードの内容が即座に反映されるためデバッグが非常に容易
・このSKSEに使用するJavaScriptのコードは、配布元のツールからTypeScriptをコンパイルして変換する必要がある。(コンパイル方法はダウンロードした圧縮ファイルのPlatform/plugin-example/のReadme.mdを確認する事。またコンパイルにはnpmのインストールが必須)
・HTMLを読み込みゲーム画面上に表示させる事が可能。(HTMLを使ったModを作成したい場合、HTMLの読み込みや表示関連のソースコードのサンプルは非常に少ないですが幸いElden Equipの作者がModのソースコードをgithubで公開しているのでそちらが参考になります。[[Elden EquipのModページ>>https://www.nexusmods.com/skyrimspecialedition/mods/74220]]の下部にgithubのリンクがあります。)
・2022/4/11のVer2.6にてAE対応(ただし2.5以降は不安定で安定版はAE未対応の2.4のため注意)
・&s(){2022年4月から更新が無いためAE版の場合は注意}2023年6月に更新されたため現AEのバージョンに対応
[[ダウンロード>https://www.nexusmods.com/skyrimspecialedition/mods/54909]]
Papyrus関数拡張SKSEの関数をSkyrim Platformで使用するための定義ファイルのダウンロードは[[こちら>https://www.nexusmods.com/skyrimspecialedition/mods/56916]]
2023-12-24T12:06:15+09:00
1703387175
-
Papyrus入門
https://w.atwiki.jp/skyrim_mod/pages/54.html
#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を改善する事による命令処理の向上がFPS依存問題と一度に処理できる命令数が少ないせいで恩恵が全く得られなくなってる)
SE版は2022年にて、[[Papyrus Tweaks NG>https://skyrimspecialedition.2game.info/detail.php?id=77779]]等のスクリプトエンジン改修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以上超えるような環境でもない限りはスクリプト処理速度は実質頭打ちとなってしまいます。
この問題は[[Papyrus Tweaks NG>https://skyrimspecialedition.2game.info/detail.php?id=77779]]のSpeed up native callsをONにする事で大幅に改善されます。
***※FPS制限解除時のMCMで目に見えて処理が高速化されるのが確認可能
[[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]]で行われるスクリプト処理が非常に高速なのは
スクリプトの処理がメインスレッド上で行われているため同期処理が必要ないためです。
(メインスレッド上で行われてるためオブリビオンのようにスクリプトが無限ループになるとフリーズする)
**一度に処理する命令の数に制限が掛けられてる
ロースペPCで動作するために負荷を掛けないようにするためかどうにも一度に実行される命令数を制限してたようで
処理件数が制限数を超えてる場合は処理を後回し(次のフレームで処理?)にする仕組みになっているようです。
実質1フレーム内で実行できる命令数に制限をかけられてた状態となっています。
これと上記の同期処理の仕様と合わさると低FPS下になると処理待ち命令が増え続けてスクリプトが遅延するという問題が発生します。
NPCが大勢いる状態、特に大規模戦闘になるとさらに状況も相まってFPSが低下しやすくなるため、
スクリプトが多く稼働している場合は容易にこの現象が起こります。
この問題はPapyrus Tweaks NGのMax Operations Per Taskで改善可能となりました。
**無限ループ化した場合、処理を中断する仕組みがない
通常スクリプトはメインスレッド上で無限ループ化した場合、待機処理が入っていない場合はフリーズします。
この場合は明らかにフリーズという形で問題が起こってると認識が可能ですが、
前述に記載しましたがPapyrusは別スレッド上でスクリプト処理が行われており、
別スレッドで無限ループ化した場合は特にフリーズが起こる等の異常が起こらず問題が起こっているかどうか認識しづらいです。
(スクリプトエラーログを確認すると長時間スクリプトを稼働してる事によるスタックダンプが発生しているので問題発生の確認は可能です)
その状態でゲームのセーブを行うと問題のある処理が延々と繰り返し続ける事になり、
セーブデータのクリーンを行わない限り、問題の処理が除去されないので注意してください。
この問題はMod製作者の不注意が原因で発生する問題なので、注意してスクリプトを記述すれば回避はできます。
また、この問題は[[Recursion Monitor>https://skyrimspecialedition.2game.info/detail.php?id=76867]]で改善可能です。
**スクリプト遅延と処理待ちによる不安定化について
***フリーズが発生する危険性が高まる
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を起こす危険性があります。
(例にあるPushActorAway関数はアクターの3Dモデルがロードされてない状態で呼び出すと確定CTD)
スクリプトエラーログで条件判定処理でチェックしてるはずなのにNoneエラーが起こるという事が稀に現れますが
条件判定直後に何かしらの要因で対象が消滅してたり、魔法効果が消滅した等でこういったものが発生する事があります。
このようにPapyrusにはCTD回避のために条件判定しないといけない命令が存在しますが
CTD回避のために条件判定で安全に実行しようとしても条件判定後にスクリプト遅延が起こってしまうと、
その間に条件判定外になってしまった場合、そのまま処理してしまったためCTDが起こってしまうという事が起こりえます。
特に内戦クエストの大規模戦闘では帝国兵とストームクローク兵は倒されるとしばらくした後に消滅するため
Modによるスクリプト次第ではCTDのリスクが非常に高くなります。
このリスクは[[Papyrus Tweaks NG>https://skyrimspecialedition.2game.info/detail.php?id=77779]]のSpeed up native callsで大幅に軽減する事ができます。
2023-11-02T20:15:03+09:00
1698923703
-
武器作成
https://w.atwiki.jp/skyrim_mod/pages/22.html
**中見出し
&bold(){ここに文字を入力}&bold(){ここに文字を入力}*オリジナル武器作成 Blender2.7系対応版
Blenderを使ってオリジナル形状の武器を作ってみよう!
このチュートリアルでは「ピコピコハンマー」をモデルに、オリジナルモデルの武器を作ります。
#contents
**武器の構想
最初に、どういう武器にするのか検討します。大好きな漫画やゲームから選んでもいいでしょう。作りながら決めていってもいいのですが、最初で決めておくと、作っていく時に迷わずに済みます。また、Skyrim Nifの仕様に合わせて作ることになるので、最初で制限を確認する意味もあります。最初で、片手武器なのか両手武器なのか、鞘はあるのか無いのか、それだけでも決めておきましょう。今回製作のピコピコハンマーは、両手武器で制作していきます。
**ツールの準備
本チュートリアルでは以下のツールを使用します。
[[Blender 2.7x系>>http://www.blender.org/download/]]
[[NifSkope>>http://niftools.sourceforge.net/wiki/NifSkope]]
[[B.A.E>>http://www.nexusmods.com/fallout4/mods/78/]]
[[Nif Plugin for Skyrim>>http://www.mediafire.com/download/gb8308ssx4vwnfo/Nif_plugin_for_Skyrim.7z]]
- [[Gimp>>http://www.gimp.org/]]やPhotoshop等のペイントツール
Nif Plugin for Skyrimは[[Blenderで入出力]]の項目を、その他のツールは[[ツール]]の項目を参照して導入してください。
**モデリング
では、実際に作っていく作業に入りましょう。
***Blenderへのモデルインポート
ピコピコハンマー両手版を作るにあたり、Skyrimから目安となるハンマーのモデルをBlnderにインポートします。
今回はDwarven Warhammer(ドワーフの戦鎚)を目安として使います。
バニラのメッシュファイルはSkyrim - Meshes.bsaに圧縮されているので、解凍しましょう。
B.A.Eを起動して、上部メニューのFile>Open ArchiveでSkyrim - Meshes.bsaを選択して読み込みます。
フォルダがツリー表示されるので、meshes>weapons>dwarvenと辿り、1stpersondwarvenwarhammer.nifを選択して、
上部メニューのAction>Extract Selected Filesを選択し、任意のフォルダにnifファイルを解凍します。
&ref(20150104220633.png,,width=600)
1stpersondwarvenwarhammer.nifを適当な場所に解凍したら、BlenderにインポートするためにNifskopeで整形します。
赤枠の部分は不要なので、1つずつ選択してCtrl+Delで削除しましょう。
&ref(20150104221600.png,,width=600)
修正後はこんなかんじです。
&ref(20150104221730.png,,width=600)
1stPersonDwarvenWarhammer:0を選択して右クリック、Transform>Editを選択します。
&ref(20150104223306.png,,width=600)
表示されたウィンドウを見てみるとTranslationとScaleの数値がバラバラになっていることが分かります。
&ref(20150104223507.png)
Blenderにインポートした、1stPersonDwarvenWarhammer:0を選択して右クリック、Transform>Applyを選択します。
&ref(20150104222840.png,,width=600)
再び1stPersonDwarvenWarhammer:0を選択して右クリック、Transform>Editを選択。
表示された画面でTranslationとScaleの数値が以下のようになっていればOKです。
&ref(20150104222935.png)
これでnifファイルの整形は完了です。
Ctrl+Sでファイルを保存しましょう。解凍したバニラのファイルはまた後で使うので、別名で保存しましょう。
保存したファイルをBlenderにインポートします。
上部メニューのFile>Import>NetImmerse/Gamebryo(.nif)を選択。
&ref(20141230210151.png,,width=600)
設定はファイルブラウザの左下に表示されます。
そのままでもインポート可能ですが、今回はインポートサイズを1/10にします。
Scale Correction Importを1.00→0.10に変更してimport NIFをクリックします。
&ref(20150104232905.png)
インポートに成功すると黒いドワーフの戦鎚のメッシュが表示されます。
&ref(20150104234625.png,,width=600)
インポートした際、以下のような警告メッセージが表示されることがありますが、無視して大丈夫です。
ファイルで指定された場所にテクスチャファイルが存在しないことを知らせるメッセージです。
> Texture 'XXXXXX\textures\weapons\dwarven\DwarvenWarhammer.dds' not found or not supported and no alternate available
> Texture 'XXXXXX\textures\weapons\dwarven\DwarvenWarhammer_n.dds' not found or not supported and no alternate available
インポートが完了したら、Mキーを押してレイヤー切り替えパレットを表示し、すぐ下の四角をクリック。
&ref(20150104235045.png,,width=600)
これで、すぐ下のレイヤーにハンマーが移動しました。
移動が完了したら、画面下側のレイヤーマーカーをクリックし、元の作業レイヤーに戻ります。
&ref(20150104235731.png)
こうしてレイヤーを分けて作業を進めていきます。
***3Dモデル製作
さて、いよいよモデリングです。
Blenderはショートカットキーを多用する操作体系になっています。こちら[[Blender基本操作(ショートカットキー)>http://cg.xyamu.net/Blender/entry26.html]]でショートカットキーを確認しながら作業していきましょう。
最初にヘッド部分から作って行きましょう。モデリングは、まず断面を作り、そこから面を伸ばしていく方法で作成していきます。
具体的には、ObjectモードでShift+Cを押してカーソルをグリッドの中心に移動してから、Shift+Aでメニューを表示し、Mesh>Circleと選択して、円を追加します。
&ref(20150105001136.png)
Vertices(角)は10でいいでしょう。
&ref(20150105001226.png)
この円が、ピコハンのヘッドの断面になります。
円が作成できたらTABキーを押してEditモード(黄色い線と点が表示される)へ。
今後ObjectモードとEditモードを切り替えながらモデリングしていくことになりますが、Objectモードでオブジェクトを操作してしまうとオブジェクトの座標、サイズ、角度を変えてしまうことになります。
そのまま出力するとゲーム中での表示位置がずれたりといった問題が起こるので、移動、回転、拡大縮小等の操作は基本的にEditモードで行う癖をつけておくと良いと思います。
1キーを押してカメラを切り替え、Rキーで回線させて直立させます。この時、Ctrlキーを押して回転させると正確に90度回転させることができます。
余談ですが、Rキー(回転)→Yキー(Y軸回りに)→90(90度)と順番に押しても同じことができます。
&ref(20150105204008.png,,width=600)
まずはヘッドの蛇腹部分を作っていきます。
作り方は色々ありますが、今回はMirrorモディファイアとArrayモディファイアを使って作っていきます。
モディファイアはBlenderの自動処理機能です。
円のオブジェクトが選択されている状態で、画面右端に表示されているプロパティエディタからレンチのマークのアイコンをクリックします。
&ref(20150105204039.png,,width=600)
切り替わった画面でAdd Modifierを押すと、オブジェクトに追加できるモディファイアが一覧表示されるので、Mirrorモディファイアを選択します。
&ref(20150105204040.png)
これで円のオブジェクトにMirrorモディファイアが付加されました。
Editモードで円の頂点を全選択した状態で、Eキーを押して頂点を押し出し、そのままX軸の正方向(画像では右方向)に引っ張って伸ばしてみましょう。
伸ばす時にホイールキーをクリックすると、伸ばす方向を固定できます。
&ref(20150105204255.png)
X軸の正方向に引っ張ると同時に負方向(画像では左方向)に伸びていくのがわかると思います。
Mirrorモディファイアはこのようにオブジェクトの軸に沿ってメッシュを鏡面コピーしてくれます。
対称な形状のメッシュを作成する場合、編集作業が少なくて済みます。
伸ばした断面の頂点を選択したままSキーを押してやや縮小します。
&ref(20150105204329.png)
ドワーフの戦鎚をインポートした際に、Viewport ShadingがMaterialに変更されて、メッシュの表示が真っ黒になってしまっているので、Solidに変更しておきましょう。
&ref(20150105204525.png,,width=600)
蛇腹の山1つ分の形状ができました。
ここまでできたら一旦Objectモードに切り替えてMirrorモディファイアをApplyします。
Applyすることでモディファイアの編集処理が永続的なものになります。
&ref(20150105204610.png)
今度はArrayモディファイアを付加します。
&ref(20150105204810.png,,width=600)
デフォルト設定で蛇腹の山がX軸正方向に1つ複製されると思います。
Arrayモディファイアは付加したオブジェクトを複製してくれます。
今回は以下のように設定しました。
&ref(20150105205007.png)
設定できたらObjectモードでApplyします。
EditモードのままAキーを押すと全体を選択できます。
全体を選択した後、赤い矢印を掴んで少し左に移動。
この矢印はその方向にしか動かないので便利です。
&ref(20150105205129.png)
蛇腹を作った時と同じ要領で蛇腹の端を少し伸ばしておきましょう。
断面の頂点を選択。
&ref(20150105205150.png)
Eキーで押し出して、少し引っ張る。
&ref(20150105205417.png)
Sキーを押して少し拡大。
&ref(20150105205448.png)
なんとなく形が見えてきました。
&ref(20150105205627.png)
****面をスムーズに表示する
ある程度出来てきましたが、このままでは10角形のカクカクとしたピコハンになってしまいます。
それはそれで味があっていいものですが、ここは面はそのままでなめらかに表示するように設定しましょう。
Objectモードに切り替えて画面左側に表示されているメニューのSmoothをクリックします。
&ref(20150105205729.png)
これでスムーズに表示されるようになりました。
&ref(20150105205730.png)
****頂点を統一して面を閉じる
今は蛇腹を持った筒の状態になっているはずです。
今度は端っこを塞いでしまいましょう。
打撃面になる部分を選択してEキーで押し出し。ピコハンの柔らかな感じを出すために、Sキーでやや縮小させます。
&ref(20150105205820.png)
さらにそこから押し出し。
Sキー(拡大縮小)→Shift + Xキー(X軸方向以外)→0(0にする)を順に入力し、1点に縮小させて面を塞ぎます。
この状態ですと、頂点がかぶっているので、Wキーを押してスペシャルメニューを表示、そこからRemove Doublesを選びます。これで重なった頂点を削除して1点に統一してくれます。
&ref(20150105210042.png,,width=600)
これで、ピコハンヘッドの半分はできあがりました。
このメッシュを反対側にコピーしてしまいましょう。
****Mirrorモディファイアによる複製
ハンマーヘッドの中央部分を作りましょう。
いまは中心部分は、Z軸のラインから離れた位置にあります。
これをZ軸ピッタリにあわせたいのですが、手動ではどうしてもずれてしまいます。これをピッタリに合わせていきましょう。
Shift + Cキーを押してカーソルを中央部分に表示させます。
続いて、下のPivotメニューから3D Cursorを選択。
&ref(20150105211829.png)
これはどれを中心点にするか。というメニューです。3D Cursorを選択すると、全てカーソルを中心として作業することになります。
適時切り替えて作業していきましょう。
この状態で、断面を選択。
Eキーで押し出し。
&ref(20150105211830.png,,width=600)
Sキーで少し拡大。
&ref(20150105211858.png,,width=600)
再度Eキーで押し出してから、Sキー、Xキー、0キーの順番で押していきます。
S(拡大縮小)、X(X軸方向へ)、0(0の位置へ)ということで、カーソルの部分(Z軸上)へ頂点が移動したはずです。
&ref(20150105211925.png,,width=600)
これでヘッドの半分が完成したので、再度Mirrorモディファイアを付加します。
このMirrorモディファイアはApplyしないでおきましょう。
&ref(20150105212115.png,,width=600)
これで、ピコハンのヘッド部分が完成しました。
&ref(20150105212132.png,,width=600)
以上が基本的なモデリングの流れです。
Blenderは解説がサイトが豊富ですので、検索して色々調べてみてください。もっと効率良くモデリングする方法があるはずです。
モデリングの基礎ができたら、この方法を応用して「柄」の部分を作って行きましょう。
柄を作る前に、最初に読み込んだドワーフの戦鎚を表示させます。
ObjectモードでShiftを押しながらドワーフの戦鎚のレイヤーをクリック。
&ref(20150105212427.png,,width=600)
これで両方のモデルが表示されます。
&ref(20150105212502.png,,width=600)
ややサイズが大きすぎるので縮小させて、移動。Editモードにしてから移動するのをお忘れなく。
&ref(20150105212549.png,, width=400)
Zキーでワイヤフレーム表示にできるので、透かして見ながらヘッド部分の位置をドワーフの戦鎚と合わせておきます。
&ref(20150105212605.png,, width=400)
柄はシンプルな棒で構わないのですが、太さは元のドワーフの戦鎚と同じ太さにしましょう。あまり太すぎると指が埋まってしまいます。
&ref(20150105213414.png,,width=400)
これでモデリングは完了です。
さあ、次はテクスチャを作る前段階、UV展開をしていきます。
**UV展開
モデルは完成しましたが、この段階ではまだ色の付いていない素材の状態です。このモデルにテクスチャを貼り付けるため、まずはこの立体物を平面に展開する必要があります。それがUV展開です。
***ヘッド部分のMirrorモディファイアはまだApplyしないの?
まだしません。
左半分をUV展開してテクスチャを作成した後で、MirrorモディファイアをApplyして右半分をつなげると、左と同じテクスチャを使う事になるので、テクスチャサイズが節約できるのです。
テクスチャはサイズが大きければ大きいほど高精細に表示されますが、同時にゲーム内での表示が重くなってしまいます。自分の製作しているモデルに合わせて調整しましょう。複雑な模様のある武器であれば左右は別のテクスチャを使った方がいいのですが、ピコハンは単純なデザインなので、左右同じテクスチャを使用します。
***Seam(切り取り線)の設定
まずはSeamの設定をしましょう。これは、モデルをどこで切り離してUV展開するのか。というものです。サイコロの展開図を想像してもらえれば理解しやすいでしょう。
&ref(2012-09-19_1403.png)
まずは、打撃面の部分を切り離します。打撃面の縦ラインを選択したらCtrl + Eキーを押して辺スペシャルメニューを表示、そこからMark Seamを選びます。
&ref(20150105213720.png,,width=600)
選択されたラインが赤色になったと思います。ここから分割されて展開図になるわけです。
&ref(20150105213800.png)
どの部分で切り離した方がいいか、はモデルによって違うので、何度かSeamラインを設定して試していくと良いでしょう。[[UV展開]]の項目も参考になります。
打撃面の設定が完了したら、今度は上下二ヶ所のラインを選択してSeam設定。
&ref(20150105214048.png)
これでヘッド部分のSeam設定は完了しました。
どんな展開図になるかを確認するにはWindow typeをUV/Image Editorに切替える必要があります。
ディスプレイサイズにもよりますが、画面を2分割して3D ViewとUV/Image Editorを両方表示させた方が作業しやすいので、まずは画面右上をドラッグしたまま左に引っ張り、画面を2分割します。
&ref(skyrim_20150105214314_Resize.png)
&ref(skyrim_20150105214323_Resize.png)
&ref(skyrim_20150105214339_Resize.png)
左側のWindow TypeをUV/Image Editorに切り替えます。
&ref(20150105214511.png)
暗い方眼紙のような画面が出てきました。UV展開やテクスチャの設定はこちらの画面でやっていきます。
***Unwrapによる自動展開
UV/Image Editorを表示させたらヘッドをAキーで全選択し、UキーでUV Mappingメニューを表示、Unwrapを選択します。
&ref(20150105214649.png,,width=600)
これで、Seam設定に沿って展開図がUV/Image Editorに表示されます。
&ref(20150105214802.png)
自動で展開すると、多くの場合UVがややずれて表示されています。ピコハンのような単純な形状の場合、このまま利用してもさほど問題は無いのですが、できるだけ綺麗に整形した方が後々の修正が楽になります。
***UVの整形
というわけで、UV/Image EditorでUVの整形をしていきます。Bキーを押して範囲選択モードにし、一番上のラインを選択。Wキーを押すと整形メニューが出るので、Align Yを選択。
&ref(20150105215101.png)
これでY軸に沿って一直線に並びます。
&ref(20150105215140.png)
全ての横線を全て直線に並べたら、次は縦を同じようにAlign Xで整形。地味で地道な作業になりますが、やっておくと後々楽できますよ。
全て整形し終わると、このように綺麗な展開図になっているはずです。
&ref(20150105220946.png)
これでヘッド部分のUV展開は完了しました。Objectモードに切り替えて、ヘッド部分に付加しているMirrorモディファイアをApplyしましょう。
ちなみに、Seam設定しないでそのままUnwrapで展開するとどういう状態になるかというと…
&ref(20150107010358.png)
こうなります。これでは、どこが打撃面か蛇腹の部分かわからないうえに、テクスチャの密度がおかしな事になってしまいます。きちんとSeam設定とUV展開していきましょう。
次は柄の展開ですが、その前に同じ展開図に広げるためにヘッドに柄をくっつけておきます。
柄、ヘッドの順番で選択(Shift+右クリック)し、Ctrl + J キーでJoin。
これで同じEditモード内で編集できるようになりました。ヘッドと同じようにSeam設定をし、Unwrapで展開していきましょう。
最終的な展開図はこのようになりました。なるべく正方形の中にうまく納めるように並べます。
&ref(20150105222407.png)
これで展開図は完成しました。
余談ですが、Blenderには[[UvSquares>https://github.com/Radivarig/UvSquares]]というAddonがあり、導入することによって追加される機能を使うと…
&ref(20150105215402.png,,width=600)
このように一発で綺麗に縦横整形してくれたりします。
必ずしも縦横揃えるのが良いわけではありませんが、便利なAddonです。
&ref(20150105215417.png)
**テクスチャ作成
展開図が完成したら、いよいよテクスチャの製作です。
***展開図を書き出す
ここから先はGIMPやPhotoshop等の画像加工ツールでの作業になりますが、その前に出来上がった展開図を画像として書き出しましょう。この展開図を元にテクスチャを描いていきます。
UVs > Export UV Layoutを選択。
&ref(20150105222529.png)
出力サイズは1024x1024でいいでしょう。
&ref(20150105222613.png)
***画像編集ソフトでの作業
指定したパスに画像ファイルが出力されているはずです。
これを画像編集ソフトで読み込みます。画像を反転し、背景を黒、ラインを白にします。この状態でレイヤーの合成モードを「スクリーン」にすると、黒い部分が透けるので作業しやすくなります。
&ref(20150105222922.png)
さて、ヘッドの部分を赤、柄の部分を黄色で塗っていきましょう。
&ref(20150105223733.png)
これでテクスチャはひとまず完成です。展開図のレイヤーを非表示にして保存(後で加工しやすい形式を選択)。
***モデルに貼り付けてテクスチャを確認
Blenderに戻り、UV/Image EditorのImageからOpen...を選択。
&ref(20150105223928.png)
先ほど保存したテクスチャファイルを選択します。
UV/Image Editorに画像が読み込まれました。
&ref(20150105223957.png)
モデルの表示に反映させるため、3D ViewのViewport ShadingをTextureにしましょう。
&ref(20150105224213.png)
さらに、3D View上でNキーを押して右側にメニューを表示させ、Shading>Material ModeをMultitextureに変更します。
&ref(20150105224321.png)
これでテクスチャが貼り付けられた状態で表示されるはずです。
&ref(20150105224346.png)
ピコハンが完成しました。
***陰影の追加
さて、この状態ですと見ての通りのっぺりとしていて立体感がありません。このままSkyrimに持っていってもいまいち表情に乏しい武器になってしまいます。そこで、陰影を描き込んで立体物を増す作業が必要になります。陰影は手描きで描き込んでいっても良いのですが、実は自動で陰影を焼き付けてくれる機能がBlenderに備わっています。
画像のように設定したら、Bakeボタンを押しましょう。
&ref(20150105231418.png)
&ref(20150105231441.png)
テクスチャが表示されていたUV/Image Editorに陰影が生成された画像が出てきたはずです。
&ref(20150105231503.png)
同時に陰影画像が貼り付けられたモデルが表示されているはずです。
&ref(20150105231538.png)
これだけでも結構リアルになったと思いませんか?さあ、早速この陰影をテクスチャに追加しましょう。
UV/Image EditorのImageからSave As... を選択。新規にKAGE等の名前を付けて保存しましょう。生成された陰影画像を、先ほどのテクスチャファイルに読み込み、レイヤーモードを「乗算」にします。
&ref(20150105234424.png)
陰影が追加されたテクスチャが出来上がりました。保存し、再度Blenderでモデルに貼り付けて確認しましょう。
&ref(20150105234517.png)
先ほどよりもずっと立体感のあるピコハンになりました。陰影が薄いと感じた場合は、もう一枚上に陰影を重ねて乗算にすると、より影が濃くなります。ゲームで見る場合はコントラストがハッキリしている方がより立体的に感じられるので、陰影は濃いめの方が効果的です。
これで、モデルとテクスチャの両方が完成しました。
お疲れさまでした。
次は、このデータをゲームに読み込むための設定を行っていきましょう。
NEXT-> [[Nifファイル作成]]
2023-10-19T18:31:52+09:00
1697707912
-
Skyrim Special Edition
https://w.atwiki.jp/skyrim_mod/pages/73.html
#contents
2016年10月28日に&bold(){Skyrim Special Edition(以下SE版)}が発売されました。
SE版特有のMOD制作についての情報をまとめるページです。
旧来のスカイリムとの変更点は少なく、&bold(){SKSE以外}ならmodの互換性が高いです。
*簡易まとめ
互換性なしだが変換が必要:bsaファイル、アニメーション、メッシュファイルの一部
そのまま使える:スクリプト、テクスチャ、音声ファイル、triファイル
そのまま使えるが変換推奨:esp、esm
変換不可能(コードの書き換えが必要):SKSEプラグイン
コンバートの配布に関しては必ず原作者に許諾を確認してください。
*ファイル
**esp
ファイル構造はほぼ変わりません。大半はそのまま使えますが、移植する場合はCreation kitでespの読み込み、保存をすることが推奨されています。水のシェーダーやボリュメトリックライトの設定に変更があります。
**fuz
音声ファイルなどはそのまま使える…ハズ…
ボイスを変えたい場合はLE版同様に「Ly Player Only Voice Replacer」が使える…ハズ…
**スクリプト
これもそのまま使えます。コンパイルし直す必要もなし。しかし、スクリプトのソースフォルダがデフォルトでData\Scripts\Sourceから、Data\Sourceに変更になりました。
**テクスチャ
これもそのまま使えます。ただし、RGB5.6.5は非対応になりました。
BC7などDirectX10のフォーマットが使えます。
**メッシュ
後方互換性があるので旧来のほとんどのnifファイルはそのまま使うことができます。
バニラのファイルは&bold(){NiTrishape}から&bold(){BSTriShape}に変更されています。
以前はNiTrishapeとNiTrishapeDataに分かれてデータを保持してましたが、BSTriShapeは両方まとめたようなデータです。
[[AFKMod nifskope開発者のひとりであるJon氏の発言>>http://afkmods.iguanadons.net/index.php?/topic/4633-skyrim-se-things-to-know-when-converting-standard-mods-to-sse/page-2#entry163238]]によればSE版用に最適化されているみたいです。
SE版では&font(b,#134f5c){NiTriStrips}は対応していません。[[パフォーマンスの低下>>https://forums.nexusmods.com/index.php?/topic/1994809-decreased-frame-rate-by-blender-export-settings/]]を招くので、2.7系のエクスポーターでは廃止されてますが、一部古いmodでTriShapeの代わりに使われてます。
***TriStripsからTrishapeへの変換方法
+[NifSkope 2.0 dev6>>https://github.com/jonwd7/nifskope/releases/tag/v2.0.dev6]をダウンロードします。
+NifSkopeを開いて、&font(b,#134f5c){Spells→Batch→Traianglate All Strips}で変換します。
+&font(b,#134f5c){Spells→Batch→Add Tangent Space and Updates}でタンジェントを更新します。
バージョンが&font(b,#134f5c){User Version1}は&bold(){12}でそのまま、&font(b,#134f5c){User Version2}は&bold(){100}に変更されました。(83でも問題なし)
**bsaファイル
旧式と&bold(){互換性がありません}。
[[BSA Brower>>http://www.nexusmods.com/skyrimspecialedition/mods/1756/]]SE対応版等のbsa展開ツールでないと開けません。
SE版で配布する場合は、Creation Kit 64bitにて再度bsaを作り直す必要があります。
**アニメーション
64bit化したことにより&bold(){互換性がありません}。
Creation Kitをダウンロードし、&font(b,#134f5c){Skyrim Special Edition\Tools\HavokBehaviorPostProcess\HavokBehaviorPostProcess.exe}で変換する必要があります。
逆に言うと変換するだけで使えます。
コマンドプロンプトを使う必要があるんですが、一括変換できるバッチファイルを作ったのでお使いください。
[[フォルダ内のアニメーションファイルを一括で64bit版に変換するバッチ>>http://www.mediafire.com/file/7lo53820da0ldu3/ConvertAllHKXTo64bit.7z]]
&font(b,#134f5c){HavokBehaviorPostProcess.exe}と同じフォルダにバッチとアニメーションファイルを置いて、バッチ起動するだけで&bold(){サブフォルダも含めて}一括で変換できます。
&font(b,#990000){※64bit化したhkxファイルは32bitには戻せませんの必ずバックアップしてください。}
*外部ツール
**Creation Kit
旧来のsteamからではなく、[[Bethesda.net>https://mods.bethesda.net/#en/workshop/skyrim]]から
Bethesda.net Launcherをダウンロードし、さらにランチャーからCreation Kitからダウンロードします。
esp/esm読み込み中に [Warning] がよく出ますが、基本全て「キャンセル」を選んで問題ないようです。
CKの動作を改善する非公式MODもあります。「[[SSE CreationKit Fixes>https://skyrimspecialedition.2game.info/detail.php?id=20061]]」
**[[NifSkope 2.0 dev6>>https://github.com/jonwd7/nifskope/releases/tag/v2.0.dev6]]
SE版対応のdev6が出ました。
SE版に便利なTriStrips→TriShapeの変換で使う&font(b,#134f5c){Triangulate All Strips}、&font(b,#134f5c){Add Tangent Spaces and Update}
BSXなど名前とDataが一致しないとCTD要因になる&font(b,#134f5c){Fix Invalid Block Names}、使ってないStringsデータを削除する&font(b,#134f5c){Remove Unused Strings}など便利な機能多数追加されてます。
**[[BSA Brower>>http://www.nexusmods.com/skyrimspecialedition/mods/1756/]]
使いやすさに定評のあるBSA Brower(FOMM Fork)のSE版。bsaファイルを展開できます。
高速表示、ツリー表示、ファイル検索、複数表示、クイック展開(フォルダを展開しない)、最近開いたファイルの記録など使いやすく高機能。
**[[SSEEdit>>http://www.nexusmods.com/skyrimspecialedition/mods/164/?]]
TES5EditのSE版対応バージョン。
**[[Wryebash SSE>>http://www.nexusmods.com/skyrimspecialedition/mods/443/?]]
WryebashのSE版対応バージョン
**[[SSE NIF Optimizer>>http://www.nexusmods.com/skyrimspecialedition/mods/4089/?]]
旧スカイリムからSE版対応したメッシュに一括変換するツールです。GUIになりました。
&font(b,#990000){変換後は旧スカイリムでは使えないので、必ずバックアップしてください。}
***使い方
+まず変換したいnifファイルをコピーしてどこかのフォルダに置きます。
+SSE NIF Optimizerを起動します。
+Browseからさきほどコピーを移したフォルダを指定します。
+(オプション)顔・口・髪などを変換する場合は「Head Parts Only」にチェックを入れます。
+「Optimize」で最適化完了です。
個別に&font(b,#134f5c){SSE NIF Optimizer.exe}にドラッグアンドドロップしても使えます。
その場合は途中に上書きの確認があるのでYを押してEnter。
BSTrishapeへの変換、不要な頂点カラーやStringsの整理、Stringsの一致等を一括で行っています。
**[[Intel Texture Works Plugin for Photoshop>>https://software.intel.com/en-us/articles/intel-texture-works-plugin]]
SE版のテクスチャ対応しているIntel製のフォトショップ専用のDDS入出力プラグイン。
(C:\Program Files\Adobe\ご使用のフォトショップバージョン\Required\Plug-Ins\File Formats\に展開したファイルを入れてください。)
**[[texconv.exe>>https://github.com/Microsoft/DirectXTex/releases]]
SE版のテクスチャ対応している変換ツール。コマンドを入力する必要あり。
**[[NifScan>>http://www.nexusmods.com/skyrim/mods/75916/?]]
nifファイルのSE版の対応やエラーのチェックをしてくれます。
***使い方
+「nifscan.exe」を調べたいnifファイルのあるフォルダに入れる。
+テキストを新規作成して以下を入力する。&br()
&bold(){nifscan.exe >errors.txt}
+保存して、名前をnifscan.bat等にする。
+nifscan.batを起動するとerrors.txtが生成されるので参考にします。
*テクスチャtips
**モデルスペースノーマルマップの軽量化(BC7圧縮のススメ)
DDSの圧縮でDirectX11仕様の形式が使えるようになりました。
本来、ノーマルマップはBC5という専用の形式があるんですが、Skyrim SEは対応していません。
顔とボディに使う[[モデルスペースノーマルマップ(*_msn.dds)>>https://www50.atwiki.jp/skyrim_mod/pages/27.html#id_c4359e7f]]は圧縮するとブロックノイズが出るので旧版では無圧縮を推奨してました。しかし、無圧縮なので容量をたくさん使ってしまうデメリットがありました。
&bold(){SE版では}ほどほどの圧縮と綺麗さを両立させた「&bold(){BC7}」という形式を推奨します。
ほぼ劣化が分からない見た目と無圧縮の半分ぐらいのファイルサイズになります。モデルスペースノーマルマップ以外は従来通りDXT1,DXT5の使用をおすすめします。
#image(bc1.JPG)
BC1、普段使う圧縮。ブロックノイズが出る。
#image(bc7.JPG)
BC7。無圧縮(左)と比べてもほぼ分からない。
***Paint.NETを使う
バージョン4.24から標準でBC7に対応しています。
保存するときにファイルの種類でDDS形式を選び、設定でBC7(線形 DX11+)にしてください。
***ConvertBC7を使う
BC7に変換するためのバッチファイル集「ConvertBC7」を用意しました。
&bold(){下準備}
「[[ConvertBC7.7z>https://1drv.ms/u/s!AvZZrjSfqkOcjDfjhw8bxdVc2CCK?e=OCy64f]]」をダウンロードします。展開します。
&bold(){各ファイルの役割}
&bold(){ConvertBC7.bat}
ドラッグアンドドロップした画像ファイルをBC7で圧縮します。
&bold(){ConvertBC7All.bat}
このバッチファイルがあるフォルダとサブフォルダ含めてすべてのddsファイルをBC7で圧縮します。
&bold(){ConvertMSNtoBC7.bat}
このバッチファイルがあるフォルダとサブフォルダ含めてすべての*_msn.ddsファイルをBC7で圧縮をします。
&bold(){texconv.exe}
圧縮するために必要なツール。必ずバッチファイルと同じ場所に置いてください。
&bold(){msnファイルのみ圧縮する場合}
-「ConvertMSNtoBC7.bat」と「texconv.exe」をdataフォルダにコピーします。&br()Mod Organizer(2も)の場合はmodsフォルダにコピーします。
-「ConvertMSNtoBC7.bat」を起動します。自動で変換されるので、そのまま待ちます。
-「変換完了」が出れば終わりです。なにかのキーを押して終了します。
***Intel plugin
要フォトショップ
-[[Intel Texture Works Plugin for Photoshop>>https://software.intel.com/en-us/articles/intel-texture-works-plugin]]を展開します。
-「C:\Program Files\Adobe\ご使用のフォトショップバージョン\Required\Plug-Ins\File Formats」に展開したファイルを入れてください。
-出力時の設定は「&bold(){BC7 8bpp Fine (Linear DX11+)}」を使用してください。
*逆変換(SE→LE)
SE版からLE版に戻します。
**メッシュ
SE版専用のBSTriShapeからLE版のTriShapeに戻します。完全ではなく頂点カラーが失われます。頂点カラーはマスクや草等のアニメーションで使われています。
[[NiTriShape Converter (Optimizer)>https://skyrimspecialedition.2game.info/detail.php?id=19911]]を使います。
***使い方
Windowsの検索欄もしくはWindowsキー+Rキーを押し、「cmd」と入力します。
コマンドプロンプトが立ち上がるので、「nifopt.exe」をドラッグアンドドロップします。
半角スペースを入力します。そのあと、変換したいSE版のメッシュをドラッグアンドドロップします。
コマンドプロンプトは以下のようになります。
C:\Users\xxx>xxx\xxx\nifopt.exe xxx\xxx\xxx.nif
Enterキーを押して変換します。
変換したいnifと同じフォルダにxxx.rev.nifというファイルが出力されます。
これがLE版のファイルです。リネームして使いましょう。以上で終了です。
2021-12-07T15:59:08+09:00
1638860348