スポンサーサイト

上記の広告は1ヶ月以上更新のないブログに表示されています。
新しい記事を書く事で広告が消せます。

OBSEの配列に関する要点メモ Part.1

以下の内容には私が独自に調べて「なるほど、こういうことか!」と勝手に判断した内容も含まれています。
大体は合ってるんじゃないかなぁとは思いますが(適当w)、OBSEのソースを解析したりはしていませんので、その辺はご注意下さいませ。
逆に「ここは違うよ!」という修正・指摘なども大歓迎です。

※2011/6/15 文面を一部変更
 


●OBSEにおける配列管理の概念

OBSEにおける配列は「C言語」のそれと比べるとだいぶ違いがあります。

・配列の実データを管理する為の「ID」があります
 (array_var配列変数に直接格納されているのはこのIDだと思います)
・その配列を参照している配列変数がいくつあるかを管理する「参照カウンタ」が存在します

例えば…

;配列宣言
array_var arSample

;配列構築
let arSample := ar_Construct Array

こうやって「ar_Construct」による配列確保が行われると…

・OBSE内部で配列の実データが生まれます(まだ内容は空ですが「空の配列」という実データができます)
・この配列に対して空きIDが割り当てられます(仮に「ID=47」だったと仮定して話を進めます)
・戻り値としてIDが返されます(arSample変数の内容が47になります)

そして配列の内容を変更する場合…

let arSample[ 0 ] := 12345

これはOBSE内部処理としては「arSampleのID、すなわちID=47の配列の、要素0番を、12345にする」という処理が行われているはずです。

配列を解放(データ上から削除)するには…

let arSample := ar_Null

とするわけですが、当初私が勘違いしていたのは「この命令を行うと配列が削除されるんだろう」と思っていたのです。
しかし実際には…

・arSampleすなわちID=47の配列に対する「参照カウンタ」を1つ減らす
・その結果「参照カウンタ」が0になるのであれば、配列の実データを削除する

こういう処理が行われています。
なので、もし別の配列変数がこの同じ配列実データ(ID=47)を扱っていた(参照していた)場合、上記の処理を行っても配列の実データは消えずに残ったままになります。
この配列を「参照」している全ての配列変数が「ar_Null」された時、初めて実データが削除されるようです。
ちなみに配列の実データが削除されたのか、されていないのかに関わらず、「:=ar_Null」された配列変数の内容は「0」になります。

そしてさらに注目すべき点があるのですが、参照カウンタが0になって「配列の実データが削除」される際に…

・その配列内の子配列や文字列などは全て自動的に解放処理される

例えば…

let arSample := ar_Construct Array
let arSample[ 0 ] := ar_Construct Array    ;子配列
let arSample[ 1 ] := "あいうえお"    ;文字列
let arSample := ar_Null

↑このソースは「子配列をar_Nullで解放し忘れている!」し「"あいうえお"文字列をsv_Destructで解放し忘れている!」というダメなパターンに見えますが、実際には自動的に解放されているのでOKなのです。

ここまでが「OBSEにおける配列管理の概念」です。



●実際の中身を覗く

「Wrye Bash」の「Saves」タブでセーブデータを右クリックして「.obase Statistics」を選ぶと「.obseファイルに格納されているデータリスト」が表示されます。
例えば…

 RVRA 1 00000022
    Mod : AE (aaaTest.esp)
    ID : 47
    Type: Array
    Refs:
      AE (aaaTest.esp)
      AE (aaaTest.esp)
    Size: 1
    [0]:NUM = 12345

こんな感じのデータが見つかりました。

MODは「この配列を構築したMOD」のことです。
IDは「この配列に割り当てられているID」ですね。
Typeは配列のタイプです。
「Array」「Map」「StringMap」とありますが、次節で解説します。
Refsは「参照カウンタ」です。
上記の場合「AE (aaaTest.esp)」が2つあるので、aaaTest.espにおいて2つの配列変数から参照されている状態(参照カウンタは2)ということになります。
Sizeは「その配列の全体サイズ」、その下は実データの内容です。



●配列「Array」タイプの説明

「ar_Construct Array」で確保した配列の特徴は以下の通りです。

・配列は[0]からの整数値でアクセスします(マイナスは不可)
・新しい要素を追加するには「今ある最後の配列+1」の要素に追加します
 (飛び飛びではダメです)

構築したばかりの配列は何も要素がありませんので[0]にだけ要素を追加可能です。
その次に新しい要素を追加する場合は[1]にだけ可能です。
その次なら[2]にだけ、その次は[3]にだけ…と「ひとつずつ後ろにつなげていく」ことになります。

let arSampleArray[ 0 ] := "あ"
let arSampleArray[ 1 ] := "い"
let arSampleArray[ 2 ] := "う"
;× ↓要素の番号が飛んでいるので追加できません
let arSampleArray[ 4 ] := "え"



●配列「Map」タイプの説明

「ar_Construct Map」で確保した配列はfloat値を使ってアクセスを行います。
Arrayと違っていて連続している必要はありませんし、マイナスも使えます。

let arSampleMap[ 0.15 ] := "あ"
let arSampleMap[ 100.50 ] := "い"
let arSampleMap[ 0.005 ] := "う"
let arSampleMap[ -500.0 ] := "え"

この配列タイプをどうやって実用すべきか…は、下の方のコメントをご覧下さい!



●配列「StringMap」の説明

「ar_Construct StringMap」で確保した配列は文字列を使って要素にアクセスします。

let arSampleStringMap[ "Name" ] := "バジリコ風味"
let arSampleStringMap[ "Strength" ] := 5
let arSampleStringMap[ "Eros" ] := 100
let arSampleStringMap[ "Personality" ] := 0

こんな感じになります。
これは非常に便利な配列タイプで、例えば上記のサンプルを「Arrayタイプの配列」で扱っていたとしたら…

let arSampleStringMap[ 0 ] := "バジリコ風味"
let arSampleStringMap[ 1 ] := 5
let arSampleStringMap[ 2 ] := 100
let arSampleStringMap[ 3 ] := 0

こんな感じになってしまい、「1の要素って何だっけ?」「2の要素が異常に高いけど何だ…?」というようなことになります。
「StringMap」を使って文字列で配列要素にアクセスすれば、スクリプトソース内での可読性が非常に高まります。



●直接は無理

配列内のデータは直接扱えない場合があります。

・リファレンスとして直接Functionを実行させることはできない
・printCやMessageBoxExなどの引数として直接渡すことはできない

例えば…

let arSample[0] := PlayerRef    ;プレイヤーのリファレンスを代入
arSample[0].SetActorAlpha 0.5    ;代入されているリファレンスの透明度を0.5にする

↑やりたいことは正しい(?)のですが、これはコンパイルエラーとなり不可能です。
こんな感じで対処する必要があります。

ref tmpRef
;
let tmpRef := arSample[0]
tmpRef.SetActorAlpha 0.5

そして…

MessageBoxEx "このリファレンスの名前は %n" arSample[0]

こういう使い方↑もできないので、上記と同じように別のref変数に代入して間接的に処理しましょう!



●ユーザー関数における配列変数の挙動

これは理解するまで時間がかかった点です (´;ω;`)

結露から言ってしまうと「ユーザー関数で宣言された配列変数は、ユーザー関数から抜ける際に自動的にar_Nullされている」ということです。

例えば…

scn aaaUserFunction00
array_var arData
Begin Function { }
    let arData := ar_Construct Array
    let arData[ 0 ] := 12345
End

↑このソースと…

scn aaaUserFunction00
array_var arData
Begin Function { }
    let arData := ar_Construct Array
    let arData[ 0 ] := 12345
    let arData := ar_Null
End

↑このソースは全く同じ結果になります。
上のソースの場合、明らかに「配列を構築したのに解放していない」というダメなパターンに見えますが、実はOKという罠…

;=====ユーザー関数=====
scn aaaUserFunction01
array_var arData
Begin Function { arData }
    let arData[ 0 ] := 12345
End

;=====メインスクリプト=====
array_var arSample
;
let arSample := ar_Construct Array
Call aaaUserFunction01 arSample
;この時点でarSample[ 0 ]の内容は12345になっています
let arSample := ar_Null

この2つのソースはパッと見で理解できるような簡単な内容ですが、実は結構複雑な内部処理が行われており…

(メインスクリプト側)
let arSample := ar_Construct Array
・Arrayタイプ配列の実データが作られ新しいIDが与えられる
・そのIDの値がarSampleに対して代入される
・↑この時点で配列データに対する参照カウンタは「1」になる

Call aaaUserFunction01 arSample
・ユーザー関数aaaUserFunction01の引数としてarSampleのIDが渡され「arData」に代入される
・↑この時点で配列データに対する参照カウンタは「2」に増える

(ユーザー関数側)
let arData[ 0 ] := 12345
・ユーザー関数内でarDataのID(arSampleのIDと等しい)の要素[0]に対して12345が代入される

・ソースにはないが、ユーザー関数が終了する際に自動的に「let arData := ar_Null」と等しい処理が行われる
・↑この時点でarDataの参照は終了し、配列データに対する参照カウンタは「1」に減る

(メインスクリプト側)
let arSample := ar_Null
・↑この時点でarSampleの参照は終了し、配列データに対する参照カウンタは「0」に減る
・↑参照カウンタが「0」になったので配列の実データは削除される

…と、こういう流れになります。



●ar_Nullが不要なケース

前節の「ユーザー関数で宣言している配列変数」の他にも「ar_Nullしなくても自動的に行ってくれる状況」があります。
それは「明確的に他の配列を扱うことになった場合」です。

例)
array_var arSample
;
let arSample := ar_Construct Array
arSample[0] := 12345
arSample[1] := "あいうえお"
let arSample := ar_Construct StringMap    ;←え、いきなり他の配列を構築!?
;(略)

どう見ても、使用中の配列を解放しないで新しい配列を構築しちゃっているダメなパターンですが、実はOKです。
OBSEの内部で「え、今参照してる配列があるのに他の配列に鞍替えすんの?しょうがないなぁ…じゃあ今まで参照してた配列はar_Nullしておいてやるよ」という感じですね、きっと。

ただ個人的な意見としては「使わなくなった配列はちゃんとar_Nullしておく」方がいいと思います。
「どの時点で不要になったのか」がフローの中で明確化されますし、何より「構築」したけど「解放」しない、というのは気持ち悪いですからね(´Д`;



●IDが代入されていない配列変数

array_var arSample

↑このような「宣言しただけでまだ配列の実データがない(=IDが代入されていない)配列変数」や「ar_Nullによって参照が終了している配列変数」は「0」であることが保証されます。
なので…

if arSample == 0
    MessageBox "配列が構築されていないので、構築します。"
    let arSample := ar_Construct Array
endif

↑こういう処理ができます。



●配列って便利(その1)

ケース1)同系列で多数のデータを管理する場合

例えば「10人分のアクターリファレンスを管理する」場合、配列を使わないと…

ref refActor0
ref refActor1
ref refActor2
ref refActor3
ref refActor4
ref refActor5
ref refActor6
ref refActor7
ref refActor8
ref refActor9

↑まずこれだけのref変数を宣言しておきリファレンスを代入することになります。
(実際にリファレンスの代入を行う部分は省略)

そして彼ら10人の生存を確認する場合…

if IsFormValid refActor0
    if refActor0.GetDead
        printC "%nは死亡しています..." refActor0
    endif
endif
if IsFormValid refActor1
    if refActor1.GetDead
        printC "%nは死亡しています..." refActor1
    endif
endif
if IsFormValid refActor2
    if refActor2.GetDead
        printC "%nは死亡しています..." refActor2
    endif
endif
if IsFormValid refActor3
    if refActor3.GetDead
        printC "%nは死亡しています..." refActor3
    endif
endif
if IsFormValid refActor4
    if refActor4.GetDead
        printC "%nは死亡しています..." refActor4
    endif
endif
if IsFormValid refActor5
    if refActor5.GetDead
        printC "%nは死亡しています..." refActor5
    endif
endif
if IsFormValid refActor6
    if refActor6.GetDead
        printC "%nは死亡しています..." refActor6
    endif
endif
if IsFormValid refActor7
    if refActor7.GetDead
        printC "%nは死亡しています..." refActor7
    endif
endif
if IsFormValid refActor8
    if refActor8.GetDead
        printC "%nは死亡しています..." refActor8
    endif
endif
if IsFormValid refActor9
    if refActor9.GetDead
        printC "%nは死亡しています..." refActor9
    endif
endif

…と、こうなります。

これを配列を使った場合だと…

array_var arRefActor

(配列構築や実際にリファレンスの代入を行う部分は省略)

ref tmpRef
int max
int index
;
let max := ar_Size arRefActor
let index := 0
while( index < max )
    let tmpRef := arRefActor[ index ]
    if IsFormValid tmpRef
        if tmpRef.GetDead
            printC "%nは死亡しています..." tmpRef
        endif
    endif
    let index += 1
loop

こんなにスッキリします。
しかもOBSEの配列は「上限がない」ので、上記のサンプルでもそうなっていますが「10人と言わず、必要な人数分を好きなだけ配列として管理することが可能」なのです。
素晴らしい (゚∀゚)



●配列って便利(その2)

ケース2)ユーザー関数の戻り値として扱う場合

ユーザー関数には「戻り値」というものがあり「SetFunctionValue」で設定すると「戻り値」として扱うことができます。

例)
if 0 == Call aaaUserFunction10
    MessageBox "aaaUserFunction10の戻り値が0です!エラーだと思います!"
endif

しかしこの場合「単一の戻り値」しか扱えず「複数の戻り値」を扱うことが出来ません。
そこで配列の登場となります。

;=====ユーザー関数
scn aaaUserFunction10
array_var arData
Function { }
    let arData := ar_Construct Array
    let arData[ 0 ] := 12345
    let arData[ 1 ] := "Oblivion"
    SetFunctionValue arData
End

;=====メインスクリプト
array_var arSample
;
let arSample := Call aaaUserFunction10
;この時点で
;arSample[ 0 ]の内容は12345
;arSample[ 1 ]の内容は"Oblivion"

こんな感じです。
ここまで読んでくださった方は「え!?」と思われるかもしれません。
「ユーザー関数で宣言された配列変数は、ユーザー関数が終わる時に自動的にar_Nullされる」からです。
ということは、せっかく構築して代入した配列が、メインスクリプトに戻った時にはすでに削除されているのでは!?
それが…大丈夫です!
詳しい内部処理はよくわかりませんが、多分「SetFunctionValueで戻り値として設定されている配列変数は、ユーザー関数が終わった際の配列自動開放処理の対象にはならない」のかもしれません。
逆に言うと…

;=====ユーザー関数
scn aaaUserFunction10
array_var arData
Function { }
    let arData := ar_Construct Array
    let arData[ 0 ] := 12345
    let arData[ 1 ] := "Oblivion"
    SetFunctionValue arData
    let arData := ar_Null ;←これ余計!
End

こんな風にやってしまうと、ユーザー関数が終了する前に参照カウンタが0になり、この配列自体が消えてなくなってしまいます。
そして呼び出し側に戻されるのは「すでに存在しない配列の不正なID」なのでエラーが発生します。
スポンサーサイト

コメント

非公開コメント

大感謝です

まさか呟いてからわずか1日足らずでここまで書き上げるとは
バジリコさん恐るべしですよ
Oblivionの配列の日本語解説はホント貴重なので大感謝感激です。

最後の配列による参照渡しなんか、特に便利そうに見えます。
スマートなScriptを目指して頑張りマス

ほんとありがとうございました。

No title

素晴らしい解説お疲れさまです。

> 参照カウンタが0になって「配列の実データが削除」される際に…
> ・その配列内の子配列や文字列などは全て自動的に削除される

は内部的には子オブジェクトの参照カウンタがデクリメントされる→カウンタが0になったら
破棄される が再帰的に行われているのではないかと想像してます。

> 配列「Map」タイプ
> この配列タイプはどうやって実用すべきかがよくわからず

一般的にMapはkeyの重複を許さない(同じkeyだと前のが破棄される)ので、keyが
不規則な数値でかつ対応する一つのオブジェクトを保持したい場合に使えるかと。

> ユーザー関数における配列変数の挙動

一応OBSEのマニュアルに「Local array variables are automatically cleaned up
so there is no need to use ar_Null to reset them.」と書いてありますね。

> 使わなくなった配列はちゃんとar_Nullしておく

オブジェクトに参照カウンタを持つ高級言語ではこれを省略できるのが利点だったり
するのですが、OBSEだとなんとなく心配という・・・

コメントありがとうございます

kikiさんコメントありがとうございます。
最初に書いていた時は頭の中で整理ができずメチャクチャになってしまったので
最終的には「自分用メモ」みたいな感じになってしまいました…w
私自身、日本語の資料やサンプルを探していたのですが見つからず
人様のMODのソースを参考にしつつトライ&エラーでやっていたので
そのように言ってもらえるのは本当にありがたいことです T_T
これからもステキなMODを提供してくださいね、1.1bダウンロードさせて頂きました!

名無しさん、コメントありがとうございます。
あー、確かに子配列から参照している配列に関しては
記事には「自動的に削除されます」と書いちゃいましたが
実際は「参照カウンタが1減らされます」ですね…
配列「Map」に関してはなるほど。
確かにそうやって考えてみると、例えば…

arArray[0][0] := 0.86 ;このスケールのアクターに対して
arArray[0][1] := 5.00 ;←適用するのはこのデータ
arArray[1][0] := 1.00 ;このスケールのアクターに対して
arArray[1][1] := 7.98 ;←適用するのはこのデータ

Arrayの場合こういう管理になるけど、Mapであれば…

arArrayMap[ 0.86 ] := 5.00
arArrayMap[ 1.00 ] := 7.98

こんな感じでスマートに管理できそうですね、これなら使い道がありそう…
私は根っからのC言語育ちなもので
「自分で確保したものは自分で解放する」というクセが染みこんでしまって
妙なこだわりがあって変な解説になってしまったかも(´Д`;
すいません…英語が全然わからなくてすいません!w

No title

ar_Null を代入する意味について補足です。
array_var に ar_Null を代入すると元々指し示していたArrayオブジェクトが解放されますが、
でもよく見るとこれは別のオブジェクトを指すようになったため、参照カウントが減って
結果として解放されている訳です。
ということは、ar_Null を代入するのも別のArrayオブジェクトを代入するのも参照数を減らす
という意味ではやっていることは同じで、たまたま ar_Null が固定的に空なArrayオブジェクト
であるというだけです。
array_var を更新した際には必ず元のオブジェクトの参照カウントが減り、新たなオブジェクト
の参照カウントが増えると考えておけば良いと思います。
仮に let a := a としたらプラマイ0で現状維持になりますし、
また ar_Null オブジェクトは固定的にあるものなのでそれの参照数に実質意味は無いですが。

この辺りはオブジェクト主体で考えると分かり易いかなと思います。
プロフィール

r_basilico

バジリコ風味 (r_basilico)
Twitter: r_basilico
Steam: r_basilico
艦これ: 嫁艦は祥鳳

リンク
最新記事
最新コメント
月別アーカイブ
カテゴリ
検索フォーム
上記広告は1ヶ月以上更新のないブログに表示されています。新しい記事を書くことで広告を消せます。