NScriptしか知らない人向けのLua講座に挑戦してみる。(仮原稿)

NScriptしか学んでこなかった人のためのLua入門を作ってみようと思います。

NScriptには命令しかない。Luaはそうではない。

NScripterの一番最初の設計を想像してみましょう。主要な命令は ld, cl, bg, print, bgm くらいでした。実は最初期のNScripterには lsp すらありません。ただし、この時点でNScriptの思想はすでに固まっていました。
スクリプトとは命令の塊である」
何を今更、と言う向きもあるかと思いますが、たとえば何故現在の背景画像のファイル名を取得する命令がないんだろう、とか、背景画像のサイズを取得する命令がないんだろう、とか、端的に言うと「○○を調査する命令」がないんだろうか、と言う疑問が今のNScriptには常につきまといます。
これは目的からすれば当然です。NScripterでノベルを開発する人は、自分で素材を集めたり作っていたりするはずです。そんな人が作るのですから、素材の大きさだのなんだのは自明、調べる必要すらないものなのです。

開発者「背景画像のサイズを知りたいんだが……」
NScripter「そんなのエクスプローラーで自分で調べろよ」

この思想があるため、NScripterの命令はLuaでも実装されている「返り値」を持ちません。何かの命令を実行した時に、それが成功したかどうかはわからないのです。成功したものとして次の命令をどんどん投げていくしかないのです。(もっとも、失敗していればNScripterが止まっているでしょうから、成否はユーザーにとって明白です。そしてそれは、開発者にとって敗北です。バグ入りをリリースしてしまった!)

命令するだけじゃなく、状態を知りたい

こういった情報を知りたければ、あらかじめ変数に保存して、それを本来の命令に与えるしかありませんでした。知りたい時には、命令を実行するのではなく、変数の方を調べるのです。

mov $100,"tatie.bmp"
bg $100,1
; 背景画像のファイル名を知りたいのあれば、$100を調べる。

当然、この方式は間違いが多かったでしょう。 bg の前に記録し忘れたら? そのためにdefsubで命令を上書きしたりもしました。 bg した時に必ず記録するようにしたのですね。

numalias bg_file,100
numalias bg_effect,101
defsub bg
(中略)
*bg
getparam $bg_file,%bg_effect ; $100に保存する。
_bg $bg_file,%bg_effect ; 本来の命令を実行する。
; $bg_fileと%bg_effectを調べれば、最後に実行した bg の引数がわかる。
return

ただし、現在のNScriptは様々な事情から情報系・調査系の命令が追加されています(スプライトのサイズを知りたいんだが……そんなあなたに getspsize )が、その仕様は元々の言語仕様に返り値がないため、いささか変則的です。

getspsize 0,%1,%2,%3 ; スプライト番号0のスプライトの幅と高さを%1と%2に代入する。

NScriptの仕様が(他の言語経験者から見て)変なのは、本来命令がどう動くのかを規定する引数の部分に、結果を受け取る変数を混ぜなければならないことです。入力と出力に同じ口を使うのは、気持ち悪くないですか? ご飯食べた口からうんこ出したいですか?
これをLuaで書くとこうなります。

width, height, alpha = NSGetSpSize(0) -- 0番スプライト情報を取得する。

結果=命令(引数)の形になり、整理ができています。右手で投げて左手で受け取る。おや、振替伝票と同じですね(←関係ない話)
NScriptしかやってこなかった人は、Luaを理解するためにまず「結果を返り値として受け取る」と言う思想転換を受け入れる必要があります。

NScriptをLuaに翻訳する。

練習として、NScripterの処理をLuaに置き換えてみます。お題は以下の通り。

同人サークル「セカンドグレードシンドローム」では平行世界ループ物「酒呑童子門」を製作中です。
そのシナリオ中、主人公が世界を移動する度に最初の世界との差異を観察して把握すると言うイベントが入ります。
プロデューサーがそのイベントを、イラストを2枚並べた間違い探しミニゲームにしようと言い出しました。
2枚のイラストを左右に並べ、片方に変更点を作ります。その変更点を指摘していくミニゲームです。
仕様として、元画像と変更後画像のどちらをクリックしても反応があるようにしたいと考えています。
さらに、カーソルが元画像にある間は、変更後画像上にも連動した擬似カーソルが表示されるようにしたいと要求されました。また、逆も同様で、変更後画像上にカーソルがある間は、元画像上にも擬似カーソルが表示されるようにしたいとのことです。
まずは、この連動カーソルを表示するルーチンを作ってみましょう。

NScriptのみで作成してみる。

numalias dc_sp,100 ; 擬似カーソルのスプライト番号
numalias dc_x,200 ; 擬似カーソルのx座標を入れる変数の番号
numalias dc_y,201 ; 擬似カーソルのy座標を入れる変数の番号
stralias dc_file,":a;cursor.bmp" ; 擬似カーソルのファイル名

defsub double_cursor_init ; 擬似カーソル表示前に一度実行しておくこと。
defsub double_cursor ; 擬似カーソルを表示、動かすルーチン。これを何度も実行する。
defsub double_cursor_end ; ミニゲームモードが終わった時に擬似カーソルを消すルーチン。

(中略)

*double_cursor_init
lsph dc_sp,dc_file,0,0
return

*double_cursor
getcursorpos %dc_x,%dc_y ; 現在のカーソル位置を取得する。
vsp dc_sp,1 ; 一応表示しておく。
if %dc_x<0 vsp dc_sp,0 ; カーソルがゲーム外なら表示しない。
if %dc_y<0 vsp dc_sp,0 ; カーソルがゲーム外なら表示しない。
if %dc_x>639 vsp dc_sp,0 ; カーソルがゲーム外なら表示しない。
if %dc_y>479 vsp dc_sp,0 ; カーソルがゲーム外なら表示しない。
if %dc_x<320 add %dc_x,320:skip 2 ; 画面左側なら、擬似カーソルは右側
dec %dc_x,320 ; 画面右側なら、擬似カーソルは左側
amsp dc_sp,%dc_x,%dc_y ; 位置変更
~
print 1 ; 瞬間表示
return

*double_cursor_end
csp dc_sp
return

Luaで作成してみる。

dc_sp = 100 -- 擬似カーソルのスプライト番号
dc_x = 0 -- 擬似カーソルのx座標
dc_y = 0 -- 擬似カーソルのy座標
dc_file = ":a;cursor.bmp" -- 擬似カーソルのファイル名

NSExec("luasub double_cursor_init")
NSExec("luasub double_cursor")
NSExec("luasub double_cursor_end")

function NSCOM_double_cursor_init()
	NSSpLoad(dc_sp, dc_file, 255)
end

function NSCOM_double_cursor()
	dc_x, dc_y = NSGetMouse() -- 現在のカーソル位置を取得する。
	NSSpVisible(dc_sp, true) -- 一応、表示しておく。
	if dc_x<0 then NSSpVisible(dc_sp, false) end
	if dc_y<0 then NSSpVisible(dc_sp, false) end
	if dc_x>639 then NSSpVisible(dc_sp, false) end
	if dc_y>319 then NSSpVisible(dc_sp, false) end
	if dc_x<320 then
		dc_x = dc_x + 320
	else
		dc_x = dc_x - 320
	end
	NSSpMove(dc_sp, dc_x, dc_y, 255) -- 位置変更
	NSUpdate() -- 瞬間表示
end

function NSCOM_double_cursor_end()
	NSSpClear(sc_sp)
end

コードを比較してみる。

NScript Lua コメント
numalias dc_sp,100 dc_sp = 100 擬似カーソルのスプライト番号
numalias dc_x,200 dc_x = 0 擬似カーソルのx座標
numalias dc_y,201 dc_y = 0 擬似カーソルのy座標
stralias dc_file,":a;cursor.bmp" dc_file = ":a;cursor.bmp" 擬似カーソルのファイル名
defsub double_cursor_init NSExec("luasub double_cursor_init") 擬似カーソル表示前に一度実行しておくこと。
defsub double_cursor NSExec("luasub double_cursor") 擬似カーソルを表示、動かすルーチン。これを何度も実行する。
defsub double_cursor_end NSExec("luasub double_cursor_end") ミニゲームモードが終わった時に擬似カーソルを消すルーチン。
*double_cursor_init function NSCOM_double_cursor_init()
lsph dc_sp,dc_file,0,0 NSSpLoad(dc_sp, dc_file, 255)
return end
*double_cursor function NSCOM_double_cursor()
getcursorpos %dc_x,%dc_y dc_x, dc_y = NSGetMouse() 現在のカーソル位置を取得する。
vsp dc_sp,1 NSSpVisible(dc_sp, true) 一応表示しておく。
if %dc_x<0 vsp dc_sp,0 if dc_x<0 then NSSpVisible(dc_sp, false) end カーソルがゲーム外なら表示しない。
if %dc_y<0 vsp dc_sp,0 if dc_y<0 then NSSpVisible(dc_sp, false) end カーソルがゲーム外なら表示しない。
if %dc_x>639 vsp dc_sp,0 if dc_x>639 then NSSpVisible(dc_sp, false) end カーソルがゲーム外なら表示しない。
if %dc_y>479 vsp dc_sp,0 if dc_y>479 then NSSpVisible(dc_sp, false) end カーソルがゲーム外なら表示しない。
if %dc_x<320 add %dc_x,320:skip 2 ; if dc_x<320 then dc_x = dc_x + 320 画面左側なら、擬似カーソルは右側
dec %dc_x,320 else dc_x = dc_x - 320 end 画面右側なら、擬似カーソルは左側
amsp dc_sp,%dc_x,%dc_y ; 位置変更 NSSpMove(dc_sp, dc_x, dc_y, 255) 位置変更
~
print 1 NSUpdate() 瞬間表示
return end
*double_cursor_end function NSCOM_double_cursor_end()
csp dc_sp NSSpClear(sc_sp)
return end

NSriptとLuaの違いがなんとなくでもわかったでしょうか。