2014/03/03

憧れのHUDウィジェット

ちょうど一年くらい前、「MCM」というスカイリムのUIを拡張した素晴らしいシステムを目にした時から……いや、このゲームのユーザーインターフェースが、あのFlashでデザインされていると知った時から、おばちゃんはずっと自作のFlashファイルをゲーム画面に表示させてみたいなあ、と思っていました。
別にこういうものが作りたい、という具体的な野望があったわけではないんですが、あのwebでよく使われている(今はそうでもないけど)Flashのコンテンツがスカイリムの中で再生できちゃうなんて、すごいじゃないですか。何でもいいから表示させてみたいじゃないですか。
でも、そんな思いつきを簡単に試せるほどスカイリムのUIは甘いものではなくて、「Unofficial Skyrim UI SDK(スカイリムのUIのFlashソースキット)」を勢いでダウンロードしてみたものの、おばちゃんには何をどう弄ればよいのかさっぱりわからず、あっさり挫折してしまいました。
だいたいそんなに簡単に弄れるような代物だったら、もっと巷にはUIを大改造するModが溢れている筈ですもんね……おばちゃんごときが手を出すなんて、百万光年早かった。

でも先日、SkyUIのWikiを眺めていたら、ちょっと気になるものを見つけまして。
この「Widget Framework」って何かしら……「Meter Widget(メーター ウィジェット)」ってなんぞ?
Meter Widget (※いつもの如くおばちゃんが勝手に意訳したものなのであまり信用なさらぬように…)
・「MyMod_MyMeter.psc」という新規スクリプトを作成する。
このスクリプトは「scriptname MyMod_MyMeter extends SKI_MeterWidget
(SKI_MeterWidgetスクリプトの拡張スクリプト)にする。
・「MyMod_MyMeter」のスクリプトをクエスト(MyMeterInstanceとする)につけて、プロパティをセットする。
・メーターをコントロールするスクリプトに「MyMod_MyMeter property MyMeter auto」を追加する。
・「MyMeterInstance」クエストの「MyMeter」(のプロパティ)を満たす。
これで「SKI_MeterWidget」のプロパティのすべてにアクセスできるようになり、メーターを実行したり更新(アップデート)したりできる。
インスタンス(MyMeterInstanceクエストのこと?)からメーターのパーセンテージを更新したい時は、「myMeter.Percent = percent as float」を呼ぶ。
これは……この「Meter Widget」って、もしかしてHUDで表示される体力とかマジカとかのバーみたいな奴のことでしょうか?
まさかあのメーターがCK上で自由に追加できたり、Papyrusスクリプトで動かしたりできるの?
ええー!マジで?!すごい!これはぜひ挑戦してみなくては……!
…というわけで、あれこれ試行錯誤して何とかHUDメーターが出せるようになりました。

ちなみにこのHUDメーターのFlashファイルは、GitHubのSkyUIのページのリポジトリのソースファイル一式を使わせていただいています。
それからmeterウィジェットのCKでの作成方法やスクリプトは、「Lovers Lab」というサイトのフォーラムで見つけたh38fh2mfさんという方のカキコミを参考にさせていただきました。
(ちなみにご存知かと思いますが、上記はアダルトなサイトですので閲覧の際はご注意くださいませ)
そんなわけで、そちらを参照していただければわざわざ作成方法をまとめなくても良いような気がするんですが、たぶん一ヶ月もしたら、おばちゃん自身が作り方をすっかり忘れてしまうような気がするので、念のために手順を書き残しておきたいと思います。

■SkyUI meterウィジェットの作成方法

(1)まず、HUDメーターのFlashファイル、「meter.swf」をSkyUIのソースキットを使って作成します。
(個人で試す分には、他人様が作ったmeter.swfを使わせていただいてもよいかもしれませんが)
SkyUIのソースは、上記にも書きましたがGitHubのSkyUIのページからダウンロードすることができます。

SkyUIのソースファイルをダウンロードしたら、次にこちらのページからTweenLiteというトゥイーンアニメーションを簡単に実行するためのFlashのアクションスクリプト用のライブラリをダウンロードします。
スカイリムのUIのデータはみんなそうですが、SkyUIのFlashファイルも使っているのは「ActionScript2.0」ですので、AS2用のライブラリを間違えずに入手しましょう。
ちなみにおばちゃんは同ページの下の方にあるFAQのところの「Download AS2」と書いてあるところからダウンロードしました。

TweenLiteのライブラリをダウンロードしたら、解凍してできた中の「gs」フォルダをSkyUIのソースファイルの「src/HUDWidgets」フォルダに入れます。
そして「src/HUDWidgets/skyui/widgets」フォルダ内にある「WidgetBase.as」をテキストエディタで開いて、冒頭の二行を以下のように修正します。
import com.greensock.TweenLite; → import gs.TweenLite;
import com.greensock.easing.Linear; → import gs.easing.Linear;
これは何をしてるのかというと、インポートするTweenLiteのライブラリのパスを書き換えてるわけです。
TweenLiteの方を「com.greensock」の指定に合わせて書き換えてもいいんですが、ひとつのファイルでは済まなくてややこしいので、上記のように「WidgetBase.as」の方を変えちゃった方が早いです。

TweenLiteライブラリの準備が済んだら、「src/HUDWidgets」フォルダの中にある「meter.fla」をFlashで開きます。ちなみにFlashの最新版であるCCでは、なんと、ActionScript2.0が完全廃棄されましたので、FlashはCS4~CS6のどれかを使う必要があります。(ちなみにおばちゃんはCS6を使いました)
これがSkyUI謹製?のmeterウィジェットのflashの元ファイル…「meter.fla」です。

それから「meter.fla」だけでなく他のFlashファイルも全部そうなんですが、Flashファイルは、SkyUIのソースの階層そのままの状態で、ファイルを勝手にどこかに移動させたりせずに作業する必要があります。
Flashファイルは単体で構成されているわけではなく、他の場所にあるスクリプトと複雑に連携していますので、ファイルの階層が変わってしまうとうまくパブリッシュできません。
くれぐれもダウンロードして解凍した時の状態を変えてしまわないようにご注意ください。

またFlashファイルを開く時にフォントがないよ、というお知らせが出ますが、それは無視して大丈夫。
「meter.fla」を開いたら、そのまま特に設定を弄る必要はなく、「書き出し」>「ムービーの書き出し」を行ってパブリッシュ(※swfファイルを書き出すこと)します。
ムービー(swf)を書き出す時には、「コンパイルエラー」のパネルを開いて、念のため何かエラーが出ていないかどうか確認してください。
書き出した「meter.swf」はスカイリムのDataフォルダの「Interface/Exported/Widgets」以下に適当なフォルダ名を作って、その中に入れます。

(2)「meter.swf」の用意ができたら、次はPapyrusスクリプトを用意します。
 meterウィジェットで使用するスクリプトは主に以下の3つです。
(A) meterウィジェットそのものを定義するための、基幹のスクリプト
(B) 使いたいmeterを設置するために、meterごとに個別につけるスクリプト
(C) 実際にmeterを運用・更新するためのスクリプト
(A)は上でリンクを張ったh38fh2mfさんという方のカキコミの、「First create new script, call it "MyWidgetMeter", here is the contents:」という行以下にある、「MyWidgetMeter」というスクリプトがそうです。
このスクリプトはSkyUIの「SKI_WidgetBase」スクリプトをmeterウィジェット用に拡張したもので、meterの表示幅や高さ、バーのグラデーションの色、値と連動するパーセンテージの定義などを行っています。

(B)は同カキコミの「Then create next script, for example if you want to make hunger bar you could do this:」という行の以下にある「 MyHungerBar」というスクリプトです。
これは(A)のスクリプトを拡張したもので、これをCKでクエスト上に設置することで実際にmeterをゲーム中に表示・制御することになります。
また、このスクリプト内で自分で用意した「meter.swf」の場所を記述しておきます。
ちなみにFlashファイルの場所の指定は「Data/Interface/Exported/Widgets」は省略です。
つまり「Data/Interface/Exported/Widgets/obachan/meter.swf」なら、「obachan/meter.swf」になります。

最後に(C)は、meterウィジェットを設置するクエストにつけて初期設定を行ったり、またOnUpdate()でmeterのバーのパーセンテージを随時更新するためにつけるスクリプトです。
これは同カキコミの「So create another script:」という行以下にある「HungerBarUpdate」というスクリプトになります。
ちなみにこのカキコミのスクリプト例では「myGlobalValue」というGlovalValueの値を2秒ごとに確認して、meterのバーの長さに反映するスクリプトになっています。

…以上の3つのスクリプトを用意したら、pexファイルにコンパイルしておきます。
(B)のFlashファイルの場所さえ正しく書き換えれば、あとは上記のカキコミ元からソースを単純にコピペしただけでも動くはずです。
ちなみに当然ですが、このスクリプトはSkyUIとSKSEのスクリプトソースを導入した環境じゃないとコンパイルはできませんよ。

(3)CKでmeterウィジェットを実行するModを作成する。

さて「meter.swf」と3種のスクリプトの用意ができたら、あとは完成まで60秒です。
あ、CKを起動するだけで30秒くらいたっちゃうか……ま、作業時間約1分といったところですね。
さてCKを起動したら、まず適当な「GlovalValue」を新規作成します。
これは前述した「myGlobalValue」というGlovalValueのプロパティに使われるもので、meterの数値(バーの長さ)になるものです。
これはゲーム中で動作を確認するため、コンソールで指定しやすいように、入力しやすい簡単な名前にしとくのがよいでしょう。
また数値はバーのパーセンテージになりますので、0~100の間で適当にいれときます。

次にmeterをとりつけるクエストを作成します。設定はこんな感じで。

クエストを作成したら、一旦クエストのウィンドウを閉じて、Modをちゃんと保存しましょう。
(保存しないと、クエストはうまく設定できない箇所が多いんですヨ)
しっかり保存したら、もう一度このクエストを開いて、「Scripts」のタブを開きます。
そしてあらかじめ用意してある「MyHungetBar」と「HungerBarUpdate」の2つのスクリプト(BとC)をこのクエストにAddしてください。

スクリプトをAddしたら、またこのクエストのウィンドウを閉じて、一旦セーブしてください。
(一旦ウィンドウを閉じたり保存したりしないと、クエストはうまく設定できない箇所が多いんですヨ)
んで、またクエストを開きなおしたら、「HungerBarUpdate」のスクリプトのプロパティを設定してやります。
こんな感じね。

「HungerBar」というのは、このスクリプトをAddしたクエスト自身を指定します。
それから「myGlobalValue」の方は、30秒前に作ったGlovalVariableをセット。
これでCKの作業は完了です。ゲームを起動してHUDメーターが表示されてるか確認してみましょう。
(当然ですがSKSEとSkyUIを一緒にロードしないと動きませんよ)
コピペしたままの状態でしたら、こんな風に画面左上に黄緑のバーが表示されるかと思います。

コンソールでmeterバーの数値として作成したGlovalValueの値を変えてやると、バーが連動して動くのが確認できるかと思います。
ちなみにこのmeterの表示位置、大きさ、バーの色なんかは、スクリプトの設定で変えられます。
ざっとでしたが、meterウィジェットの基本的な設置方法は以上です。


ところでmeterウィジェットと同じSkyUIの「HUDWidgets」に「arrowcount」というサンプル用?のウィジェットが入っているんですが、これが非常にシンプルな作りをしていまして、これを見ていたらどうすればCKのスクリプトからFlashファイルが制御できるのか、ほんのちょっとだけですが、SkyUIのHUDウィジェットのカスタマイズ方法がわかったような気がしました。
たとえばこんな感じのタイムラインの内容を持つ「atmos」というムービークリップが
rootのArrowCountWidgetムービークリップの直下に配置されてるとします。

Papyrusスクリプトから、このFlashファイルの「atmos」ムービークリップを制御するには、このFlashファイル(arrocount.fla)をコンパイルする時に読み込むことになる、「src/HUDWidgets/skyui/widgets/arrowcount」内に入ってる「ArrowCountWidget.as」というアクションスクリプトファイルを編集します。
まずは「STAGE ELEMENTS」が定義されている場所(デフォルトのArrowCountWidget.asでは「countText」というテキストフィールドが設定されてます)に、「public var atmos: MovieClip;」と追加して、「atmos」のムービークリップを登録します。
んで、名前はなんでもいいんですが、「atmos」ムービークリップを操作するための関数(仮にcallAtmosとしました)を「ArrowCountWidget.as」内に設定してやります。
// @Papyrus
public function callAtmos(a_num: Number): Void
{
if (a_num == 1){
atmos.gotoAndPlay("atmos1");
}else if(a_num == 2){
atmos.gotoAndPlay("atmos2");
}else if(a_num == 3){
atmos.gotoAndPlay("atmos3");
}else if(a_num == 4){
atmos.gotoAndPlay("atmos4");
}else{
atmos.gotoAndStop("non");
}
}
ちなみにこれはアクションスクリプトですが、別にそんなの深く知らんでも大丈夫です。
現におばちゃんが知ってるのは「gotoAndPlay」と「stop」くらいですが、GIFアニメに毛が生えた程度のアニメーションだったら、その二つを知ってりゃ大抵なんとかなります(ホントか?)
ちなみに「atmos.gotoAndPlay("atmos1")」というのは、「atmos」ムービークリップの「atmos1」というラベル位置からフレームを再生しろという命令です。
つまり「callAtmos」で「1」が入力されたら「atmos1」を、「3」が入力されたら「atmos3」のラベル位置からFlashアニメが再生されるように定義しておいて、これをPapyrusスクリプトから呼んでやるわけですね。
一方、このFlashファイルを動かすための、Papyrusスクリプトの記述はこんな感じになります。
int Property atmosChange
int function get()
return _atmos
endFunction

function set(int a_val)
_atmos = a_val
if (Ready)
UI.InvokeInt(HUD_MENU, WidgetRoot + ".callAtmos", _atmos)
endIf
endFunction
endProperty
これはSkyUIの「misc」フォルダの中に「PN_ArrowCountWidget.psc」というArrowCountウィジェットを定義するためのスクリプトが入ってますので、それをベースにして作成します。
meterウィジェットのスクリプトでいったら、(A)にあたるのがこの「PN_ArrowCountWidget」スクリプトです。
つまり、このウィジェットを定義するスクリプト内に、Flashファイルの「callAtmos」にアクセスするための新たなプロパティを設定してやれば、SkyUIのウィジェットのしくみを通して、Flash ファイルが操作できるようになるわけなんですね。
ちなみにこのスクリプトを見ると一目瞭然なんですが、SkyUIのHUDウィジェットって、ゲームデフォルトのHUD画面に追加というか便乗するような感じで表示させてるようです。
(つまりNPCと対話するダイアログ画面になると、HUDがきかなくなるので表示されない)
SKSEのUIスクリプトって、どういう使い方するんだろう…と今まで謎に思ってましたが、この手の拡張したUIを制御するために使われてたんですね……目からウロコです。

ArrowCountウィジェットを改造して作った雰囲気HUD

よく考えたら、空気の読めないノルドの国、スカイリムでは意味のないHUDでした……

2014/02/17

PapyrusUtilで遊んでみる

ここ最近はずっと、以前NEXUSで見かけてから気になっていた「PapyrusUtil」という、スクリプトを拡張するSKSEのプラグインモジュールで遊んでました。
この拡張セットはなんと、ModのスクリプトからコンソールのTFCコマンドやバッチファイルを実行できてしまったり、外部のテキストファイルを読み書きできたり…と、夢みたいな関数がいっぱい搭載されてるんですが、中でも一番おばちゃんが一番おおーっと思ったのは、NPCやアイテムといったオブジェクトに直接いろんな値を紐付けて保存や読込ができるという「StorageUtil(ストレージ機能)」です。
これは……全NPCにシムズみたいな特質や隠しステータスを付けられるってことじゃないですか!
NPC対NPCの対人関係も、あらかじめ決まったRerationshipではなく、ゲーム中にリアルタイムに変動する友好度や愛情値のようなパラメータを設定できるわけですよ。
NPCが他のNPCと勝手に仲良くなったり、熱いビームのような視線を飛ばしまくったりするガンパレード・マーチのような世界にスカイリムを改造するのも夢ではないかもしれません……!
……ああ、やばい、妄想しただけで鼻血が出てきました。

ちなみに以下は、StrageUtilのpscファイルの冒頭にある説明をおばちゃんが勝手に訳したものです。
おばちゃんは中学レベルの英語もあやしいので誤訳してる部分が沢山あるかと思いますが、どういうことができるスクリプトなのか、ご参考までに……
Mod制作者の方はお読みください!(MOD AUTHORS, READ!)
このスクリプトには、int(整数)・float(小数点を含む数)・form(スカイリムの基本オブジェクト)・string(文字)といったあらゆる「値」を、「名称」によって、form上、もしくはグローバル上に保存したり読み込んだりする機能があります。
これらの「値」にはどんなModからでもアクセスしたり、値を変えたりすることがことが可能ですし、また別のModを必要としたり、このプラグインのバージョンの一致を気にしたりしなくても、互換性を保つことができます。
この「値」はアンセット、もしくは、値のリストがクリアされるまで、form上もしくはグローバル上に保持されます。
仮に「値」をあるform上にセットして、そのオブジェクトを削除したら、その値はゲームのセーブ時に取り除かれます。
もし確実な「値」を使いたいのであれば(他人が使っているかもしれないから?)、その値をアンセットもしくはクリアしてから使用すべきですが、しかしそれはおすすめできません。
MCMのコンフィグの値を保存すれば、他のModにあなたのModの使用設定を変えさせることができます。
同様にあなたもまた、何も新しい値が追加されていないスクリプトならば、MCMのコンフィグスクリプトをバージョンに関係なく変更することができるでしょう。
またファイルから取り出した値を別のファイルに保存できるという機能は、すべてのセーブデータから同じ値にアクセスできることを意味します。これはコンフィグの設定に非常に使える機能となるでしょう。
保存された「値」が占有するメモリはごくわずかです……おそらく、数千の「値」を設定しても、物理的メモリを500kb以上超えることはないでしょう。
▼続きを読む...
●「値」の名称は大文字と小文字の差を違うものとしては認識しません。つまり、
GetIntValue(none, "abc")はGetIntValue(none, "aBC")と記述したのと同じ値になります。
●「値」はすべて、それぞれの「型」によって別々になっています。
つまり、SetIntValue(none, "abc", 1)と「Int」型の値として設定した「abc」と、SetFloatValue(none, "abc", 2.0)という「Float」型の値として設定した「abc」は、それぞれ別個の値として登録され、お互いの領域を侵すことはありません。
(例)StorageUtil.SetIntValue(none, "myValue", 1)
StorageUtil.SetFloatValue(none, "myValue", 5.0)
int value = StorageUtil.GetIntValue(none, "myValue")
↑この場合、valueの値は「1」になります。(valueは「5」ではありません)
●「値」の名称をつける際は、すべてのModが同じストレージにアクセスするということを忘れないでください。
たとえばもしあなたが、値に「name」というありふれた名称を使ったら、他のModもそれと同じ値を使ってしまうかもしれないし、それによって望ましくない動作が起きるでしょう。
おすすめは、あなたのMod名の接頭辞を値の名称の頭につけることです。
そうすれば誰もあなたの使った値と同じ名称の値を使ったりはしないでしょう。
たとえば「realistic needs and diseases」というModならば、hunger(空腹)の値を「rnd_hungervalue」などとするとよいでしょう。

また、あなたのModの機能を他のModに利用させるためにこのストレージを使うこともできます。
たとえば、もしあなたのModにActorを見えなくするという関数(MyScriptFunction)があるとして、その機能を使う際にはこんな感じでチェックを入れておきます。
int i = StorageUtil.FormListCount(none, "MakeInvisible")
while(i > 0)
i--
Actor make = StorageUtil.FormListGet(none, "MakeInvisible", i) as Actor
if(make)
MyScriptFunction(make)
endif
StorageUtil.FormListRemoveAt(none, "MakeInvisible", i)
endwhile
他のModからこの関数の機能を使う場合は、このように書きます。
StorageUtil.FormListAdd(none, "MakeInvisible", myActor)
これであなたのModを使って誰かを見えなくすることができます。しかしあなたのModが無い場合は何も起こりません。もっともこれは単なるサンプルにすぎませんので、適合させるためにはもっと良い方法があるかと思います。
また他のModと連携したい場合は、そのModの説明書きに当たってみてください。
…というわけで、さっそくこの「PapyrusUtil」のストレージ機能を使って、何かNPC同士の人間関係を構築するようなModをためしに作ってみることにしました。
おばちゃんは前々からずっと、スカイリムにはNPC同士のたわいもない「雑談」が少なくて寂しいよなあと思っていたので、そういったNPC同士の汎用会話にストレージの値をからませるようなModにひとつチャレンジしてみたいと思います。
ちなみに雑談というと、街の住人なんかはわりとおしゃべりしている印象があると思うんですが、あれは特定の決まった相手との会話なので、シチュエーションが限られてしまっているんですよね。
おばちゃんが欲しいなあと思うのは、人物限定の会話ではない、ボイスタイプさえ合えばどのキャラでも発生するというたぐいの汎用会話イベントです。
まあ、誰とでも発生する「雑談」となると、前作オブリビオンの時のマッドクラブの話題みたいに間が抜けたものになっちゃうのでスカイリムではやめたんでしょうが……しかしNPCを大勢家に居候させて大家族ごっこをやっていると、当たり障りの無い会話が無いのがこたえるんですよねえ。
せっかく全員で食卓を囲んでも、辺りに響くのはパンをむしる音やあくびの音ばかりで。
ひとつ屋根の下に住んでる者同士なんだから、挨拶くらいすればいいのに……って思いません?

ちなみに、どのようにしてNPCに対人関係の値を設定するか……いろいろ考えたんですが、NPCにはまず登録順にIDをつけて、そのIDを元に作成した値を、会話の際にその都度保存していくことにしました。
NPCには対人関係だけでなく、各スキルから算出した性格のステータスなども設定してます。

「魅力」が高いNPCは人から好かれやすく、「優しさ」が高いNPCは嫌われにくいです。
たとえばレイロフ君が、ID6番のヘイムスカー氏のことをどう思っているかという値は、「obaNPCR6」というキーワードでレイロフ君のRef本体に保存します。
つまり、「obaNPCR」というのが、おばちゃんのModの接頭辞でして、こいつを頭につけた「obaNPCR」+「対象NPCのID」を「キーワード」としてPapyrusUtilのストレージの値として保存しておくわけです。
レイロフ君のIDが5なら、ヘイムスカー氏のRefに保存されるレイロフ君への関係値は「obaNPCR5」というキーワードで保存されることになります。…つまり、お互いがお互いをどう思っているかという数値を、お互いのID番号を使って自分自身に保存するわけですね。
そうなるとNPCが他のNPCとお近づきになればなるほど、保存されるストレージの値が増えてゆくわけですが、これこそPapyrusUtilのストレージ機能の本領発揮!といったところです。
なにしろゲームのデフォルトの仕様では、何かしらの値を保存するためには、あらかじめ値を保存するための「箱」を用意しておかないといけませんでしたからねえ。こういったNPCの対人関係のように、無限の組み合わせが発生するような値を保存しておくのは、今まではかなり難しかったんです。
(少なくともおばちゃんにとっては……ですが)
ただし、このストレージの値は、ゲームデフォルトのActorValueなどの値とは違って、イベントやクエスト、会話などの発生条件(Conditions)には直接使えない、というのが少々ネックかなあと思います。
どうやってこのストレージの値を使ってイベントを起こすかが、思案のしどころですね。

…というわけで、ここをああしたとかこうしたとか文章をいくら書き連ねても伝わらないと思うので、実際にどんなシロモノが出来たかは、動画を見ていただきたいなと思います。
ちなみに会話イベントは「MaleNord(ノルド男の声・レイロフ君とか)」と「FemaleEventoned(訛りのない女性の声・リディアさんとか)」のみしか用意していないので、両ボイスのNPCにしか発生しないです。
テストはホワイトランのマーケット広場で、NPC達がヒマをつぶせるように、井戸の周りにベンチを置いてやったのですが、面倒がってNavMeshまでは修正しなかったので、NPCの動線がちょっとおかしなことになっております。
あとの問題は……まあご覧になればわかります(ため息)



シムズは偉大なゲームなんだな~としみじみ思いました。