MaxScript で初回実行時のみ結果が異なる現象の原因

MaxScript を書いていて、3dsmax の起動後最初に実行したときだけ結果が異なる、という現象に出会ったことはありませんか?

なぜかわからないけれど一回目だけ違う結果になる、というのには実は原因があります。

MaxScript の変数に潜む罠

まずは実際にそういう例をやってみます。
3dsmax を起動し、MaxScript エディタを開いて以下のコードを入力してください。

※できれば3dsmax を新たに起動して試してみてください。

v1 = (v2 = 1)

format "v1: %\n" v1
format "v2: %\n" v2

さぁ、これを実行してみるわけですが、実行する前にちょっと考えてみてください。どういう結果になるでしょうか。

コードはごく単純で、v2という変数に1を代入し、その結果をv1という変数に代入して、それぞれの変数の中身をformatで出力しています。

※補足ですが、format というビルトイン関数は2つの引数を取り、1つ目の文字列の中の「%」を2つ目の変数の内容で置き換えてprint する、という機能を持っています。

上のコードを実行したとき、変数v1とv2の値はどうなるか、というのが今回の題材です。

結果を想像しましたか?

では実行してみましょう。スクリプトエディタの窓の中でCtrl + e を押して上記のコードを評価します。
するとスクリプトエディタに結果が出力されます。

1
v1: 1
OK
v2: undefined
OK

ポイントはv2 = 1 が( ) で括られているところです。( ) があるのでここはブロックという扱いになり、その内側で宣言されたv2は( ) の外からは見えません。だからこのような出力になったわけです。

ここまでは良いですね。ではこれをもう一度実行します。

1
v1: 1
OK
v2: 1
OK

さあ、どうでしょうか。予想通りですか?
ちなみに以降は何度実行してもこの2回目の結果と同じになります。3dsmax を起動して初めての実行時だけ、v2 がundefined になります。

これが予想通りだった人は既にMaxScript をちゃんと理解されているのでこの先は読む必要がないでしょう。
意外な結果だと感じた人はぜひこの先をご覧ください。

何が起きたのか

改めてコードを確認します。

v1 = (v2 = 1)

たったこれだけです。

式の実行時に起きたこと

v2 = 1 を実行し、その結果をv1 に代入しています。ポイントはv2 = 1 に()がついているところです。()がついているので、この部分はブロックという扱いになります。

最初に実行したとき、3dsmax の中にはv1という変数もv2という変数も存在しません。

()の中でv2 = 1が実行されたとき、()内のブロックスコープにv2という変数が生まれ、1 で初期化されます。この時点では、()の外側からv2という変数は見えず、存在しません。MaxScript は代入式が代入した値を返すので、(v2 = 1) は 1 を返します。それをv1 に代入しているので、v1 は1で初期化されます。

この時点で、v1は1、v2は存在しない、という状態になりました。

変数の呼び出し時に起きたこと

次に、format 文を使用して変数の中身を確認しています。

format "v1: %\n" v1
format "v2: %\n" v2

v1とv2の内容を確認しようとしたわけです。重要なのはここで変数への参照が行われた、ということです。

結果はv1が1、v2がundefined となります。これは最初の式の動作を考えれば順当な結果と言えます。v2は()の中でだけ有効なので外からは見えないからです。

が、存在しないv2という変数をformat 文で参照しようとした時、グローバルスコープにv2という変数が宣言され、undefined で初期化される、という事態が発生しました。

これがMaxScript の仕様で、大変わかりにくい部分かつ今回の最重要ポイントです。

MaxScript では、存在しない変数を呼び出すと、暗黙的にその変数名が宣言され、undefined で初期化される

重要なのは、ある変数が存在しないのと、存在していてundefined である、というのはまったく異なる状況である、ということです。

二回目の実行時に起きたこと

上記の事態が起きたあと、二回目の実行で何が起きたのでしょうか。

v1 = (v2 = 1)

実行したのは前と同じこのコードです。ここで重要なのは、v1もv2もすでにグローバルスコープに存在している、ということです。v1は1、v2はundefined です。

ここで上記の式を実行すると、()内を実行するとき、v2という変数はグローバルスコープにあるので、そのグローバルスコープにあるv2が使われます。ここでv2に1が代入されました。このv2はグローバルスコープであるという点が重要です。

そして出力を行います。

format "v1: %\n" v1
format "v2: %\n" v2

もうお分かりですね。グローバルスコープに存在するv1とv2は、上記の経緯を経てどちらも1になっています。以降は何度実行してもv1、v2とも1という結果になるわけです。

この事態を回避する方法

このややこしい事態は、存在しない変数を参照して暗黙に作られた変数に、ブロックスコープであるべき変数が代入されてスコープ外へ漏れ出た、という事実によって引き起こされました。
そのため、ブロックスコープを確実にブロックスコープとして動作するように宣言すれば良いということになります。

v1 = (local v2 = 1)

format "v1: %\n" v1
format "v2: %\n" v2

v2 の宣言を明示的にlocal にしました。

では3dsmax を起動しなおして、実行してみましょう。

1
v1: 1
OK
v2: undefined
OK

今度は何度実行しても同じ結果になります。

前回と同じように、format 文による参照時に、v2はグローバルスコープにundefined として作られました。ですが( )内で宣言されているv2 は明示的にlocal なので、グローバルスコープにあるv2とは別のものという扱いになります。そのため、ここでv2に何を代入しても、グローバルスコープのv2はundefined のままなのです。

結論

MaxScript で変数を宣言するときは、かならずglobal かlocal かを明示的に指示しましょう。特別な理由がない限りはすべてlocal 変数にするのが良いです。

余談

このようにMaxScript では存在しない変数を使用するとグローバルスコープにその変数が作られ、そのまま3dsmax セッション内に存在し続けます。

例えば一時的にスクリプトを使用してなにか操作を行うなどすると、そのとき使用した変数は3dsmax セッション内に残ってメモリを消費し続けます。グローバルスコープに残った変数は思わぬ不具合につながったり、無駄なメモリを消費したりするので、ときどき3dsmax を起動しなおしてメモリをクリアすることをお勧めします。

グローバル変数を削除する方法

なお、グローバルスコープにある変数はglobalVars という構造体を使用して削除することができます。

globalVars.remove "{変数名}"

ただし、グローバルスコープの変数には重要なものもたくさんあるので、むやみに削除することも不具合につながります。消すべきものがはっきりしている場合を除いて、この機能で変数を削除するのは避けた方が良いでしょう。