NScripterでのDaemon的なアレを実現する。

こんなのがあった。

で、実際に作ってみて、かつその過程を見せることで、誰か一人のスキルアップに役立てば御の字と考えた。

要件定義

「好きなときに特定キー1回押したらとある効果音鳴る仕組み」とあるが、これは下記のように言い換えられ、かつ三つに分割できる。

要件1
「ミリ秒単位で定期的に」
要件2
「ある特定キーが押されているかチェックし」
要件3
「効果音を鳴らす」

困難は分割せよと言ったのは確かアインシュタインで、どんなに難しそうなことでも、ばらしてしまえば一つ一つは簡単な問題であり、そういう簡単な部品の組み合わせが複雑なシステムを作っていると言うことを実感するのは問題解決の初歩であり奥義である。ただし、どう分割するのかは、結構慣れとか勉強が必要だが。

要件1:「ミリ秒単位で定期的に」

NScripterの仕様上、「ミリ秒単位で定期的に」実行されるシステムは複数あるが、開発者が自由に扱えるものは二種類しかない。
一つは、レイヤー型プラグインであり、もう一つが、NSLuaのNSCALL_animationである。
レイヤー型プラグインは、define節でsetlayerでどれくらいの間隔で実行するかを指定できる。ただし、レイヤー型プラグインの作成はガチなプログラミングになるので、万人向けとは言えないし、何より他の要件を満たすのに手間がかかりすぎる。よって、NSLuaを使うとする。

; define節に記述。
luacall animation ; これで、nsluaのNSCALL_animationが使えるようになる。

次に、system.luaの中に記述。

function NSCALL_animation() -- NSCALL_animation関数を定義しなければならない。
end

NSLuaAnimationInterval(25) -- アニメーションの割り込み処理のミリ秒数。この数値ミリ秒毎にNSCALL_animation()が実行される。
NSLuaAnimationMode(true) -- 引数にtrueを与えると割り込み処理をする。falseを与えると割り込み処理をしない。
-- この場合、常に見張る関係で、最初にmodeをtrueにしたら後はいじる必要はほとんどないだろう。

ここで簡単に説明しておく。
NSCALL_animationは、NSLuaAnimationModeがtrueな限り、定期的に実行されるが、その名が示すとおり本来はスプライト等のアニメーション処理をする想定で作成された。
よって、この関数の最後はreturnで、boolean値(trueかfalse)を返すことが期待されている。
ちなみに、この値がtrueなら、自動的にNSUpdate(画面更新)が呼び出され、そうでなければ飛ばされる。
今回いじりたいのは音だけなので、常にfalseを返すようにするのがいいだろう。

function NSCALL_animation()
	check_key("K", play_se_any) -- 要件2で作る予定の関数を実行する。
	-- 第一引数は、特定のキーがどれかで、第二引数は、押された場合に実行する関数とする。
	-- 第二引数は、要件3で作る予定とする。
	return false -- 常にfalseを返し、画面の更新をしない。
end

また、NSLuaAnimationIntervalに与える数値は、17で一秒間に約60フレーム(現行のほとんどの格闘ゲーム)になり、33で一秒間に約30フレーム(バーチャファイター1と同じ)になり、42で一秒間に約24フレーム(「ぬるぬる動く」アニメ)、62フレームで一秒間に約16フレーム(テレビアニメ)と同じくらいになる。

要件2:「ある特定キーが押されているかどうかチェック」

この要件は、さらに言い換えれば、「前回のチェック時にキーが押されていない状態であり、かつ、今回のチェック時に押されている状態になっている時」を検出するべきである。
もし、このチェックを怠った場合は、ほとんどの人間がミリ秒単位でキーを上げ下げなどできないので、非常に長い押しっぱなし状態が発生してしまう。

	-- 前回の状態をキー毎に保存する変数
	local pre = {}
	-- 関数の定義
	function check_key(key_name, func)
		-- あるキーが押されているかどうかを取得する。
		local push = NSGetKey(key_name) -- NSGetKey命令を使う。
		if not(pre[key_name]) and push then -- 前回がfalseでかつ今回がtrueなら
			func() -- 与えられた関数を実行する。
		end
		pre[key_name] = push -- 今回の取得結果を過去の取得結果にする。
	end

なお、将来の拡張に備えて、実行時にどのキーかを指定するようにした。こうしておけば、複数のキーに単一あるいは別々の機能を割り振ることも簡単になる。
check_keyをどんどん増やせばよい。

要件3:「効果音を鳴らす」

NSLua側での効果音の鳴らし方は、以下が基本である。

	NSOggClose(channel) -- 念の為にチャンネルをフリーにする。
	NSOggLoad(channel, se_filename) -- あるチャンネルにファイルを読み込んでおく。
	NSOggPlay(channel) -- あるチャンネルに読み込まれたファイルを演奏する。

これを他の場所からでも使いやすいように、一つの関数にまとめる。

	function play_se_any() -- チャンネル番号とファイルは書き換えが必要。
		NSOggClose(channel)
		NSOggLoad(channel, se_filename)
		NSOggPlay(channel)
	end

で、以上の要件毎のスクリプトをまとめて表記すると完成だが、ちょっと考える。
もし、使った音楽ファイルの演奏時間が多少長くて、その上でプレイヤーが連打をしたならば。同じチャンネルにどんどん上書きするので、DJのスクラッチのような状況になってしまう。それでもよいのであれば、このでおしまい。そうでなければ、SEが被るように考えてみる。
複数のチャンネルを用意して、それを使いまわすことでこの問題を解決する。

SEかぶり対応版
do
	-- 使うチャンネル番号を入れる容器
	local channels = {}
	for i=20, 30 do channels[#channels+1] = i end -- チャンネル20から30を容器に入れる。

	local get_channel = function() -- 呼び出す度に、違う番号を返す関数。
		local channel = table.remove(channels, 1)
		channels[#channels+1] = channel -- 元に戻しておく。
		return channel
	end

	function play_se_any()
		local channel = get_channel() -- 空いてるチャンネル番号を取得
		NSOggClose(channel)
		NSOggLoad(channel, se_filename) -- チャンネル番号とファイルは書き換えが必要。
		NSOggPlay(channel)
	end
end

まとめ

これでわかるかなあ。