格闘ゲーム入力システム研究

タイトル通りのことを考えました。NScripter格闘ゲームやろうなんてアレですけどね。※実際のスクリプトは出ません。
まず、デフォルトのNSGetKey()は使いません。テンキーの取得やジョイスティックの取得ができないからです。
よって、入力システムの基礎にはhttp://kimikage.ddo.jp/gallery4/GetKey Ver0.5を使います。こちらは、ジョイスティックの入力に対応しているからです。
また、実機として【2006年モデル】エレコム ゲームパッド USB接続 連射機能付 10ボタン JC-U2410TBKがあったので使います。
予備調査として、ジョイスティックから得られる数値について確認しました。マニュアルによると「スティックの傾きを0-65535の数値として得られ、32768が中心」となっていたのですが、こちらで実行したところ、どういう訳か32511が中心になっていました。環境由来かシステム由来かはわかりませんが、スティックの傾きの検出を「X軸が30000以下なら左、35000以上なら右(数値は適当)」などと幅を持たせることで、ソフトウェア的に対処できそうです。同じようなズレの発生する環境があってはいけませんからね。
これで道具は準備できました。

そもそも、コマンド入力とはなんなのか

「Webや攻略本に書いてるだろう。矢印のつながったア・レ」と言われてもプログラマ的には全く役に立ちません。
ここでは、うろ覚えですが、大昔ストリートファイター2の攻略本で読んだシステム詳解を使うことにします。

例えば、波動拳コマンド。
下を入れてから、○フレーム以内に次は右下に倒す。それからまた○フレーム以内に右にレバーを倒す。その後、○フレーム以内にパンチボタンのいずれかを押す。

当時はまだフレームと言う単語は市民権を得ていませんでしたので、「強で約0.13秒以内、中で約0.17秒以内、小で約0.20秒以内」と表記していたように記憶しています。ちなみに1秒間60フレームとするならば、それぞれ8・10・12フレームの近似値になっています。今回のフォーマットはNScripterで、処理速度に少々不安が残りますので、1フレーム40ミリ秒、1秒間25フレームくらいでやってみたいですね。テレビアニメ並と考えたら充分じゃないですか? 6フレーム統一で。
※すぐ下のブログ記事と合わせると嘘八百な内容ですが、別にスト2クローンを作りたい訳でもないし、考え方を考察しているのでこれでいきます。

まだ、仕様としては足りない

キー入力についてはまだ考えなければならないところがあります。
カプコンとSNKの必殺技コマンド入力判定の厳しさの差を検証 - うさだBlog / ls@usada's Workshopを参考にしてください。
「ボタン入力はコマンドの一部かどうか」と言った問題です。
また、余分な入力は許せるかどうか、と言う問題がもあります。例えば、鳳凰脚コマンド(21416B+D)を入れるのに、途中で23が入ったりしても許せるかどうか、です。ここまでくると仕様の問題ですが。
私としては、私のような老頭児でもコマンドが出やすいように、途中寄り道はありとしたいところです。アイヴィーの「クリミナルシンフォニー」や「世界を呑む蛇」をレバガチャで出したいじゃないですか。ねえ? もっとも、寄り道することによるタイムロスでのコマンド失敗は受け入れるべきでしょう。

実際のプログラム

1フレーム毎に一回実行されるルーチンがあり、その中で「キーの入力」と「コマンドの判定」を行う。これが基本。
色々と考えた結果、コマンドの判定は、可能な行動それぞれにつき一つのクロージャ関数を作り、それに対してレバーの状態とボタンの状態を与えていくと言う形になるでしょう。
コマンド関数は与えられたレバーとボタンによって、内部の変数でどのステップまで入力されたかを管理し、コマンドが完成したら(今現在、キャラクターが別の行動を取っていなければ)キャラクターの行動を変更することになります。

-- statusの仕様
-- statusはテーブル
-- 第一引数は、ゲーム開始からのフレーム数
-- 第二引数は、スティックの倒されている方向(テンキー型)
-- 第三引数は、ボタンの状態。テーブル型。
do
	-- 波動拳判定ルーチン
	local step = 1
	local steps = {}
	local step_time = 0
	steps[1] = function(status)
		if status[2] ~= 2 then return end -- 下じゃなければ用はねえ。
		step_time = status[1] -- 入力された時間を記録。
		step = 2 -- 次のステップへ。
	end
	steps[2] = function(status)
		if 6 < status[1] - step_time then -- 時間切れ
			step = 1; return
		end
		if status[2] ~= 3 then return end -- 方向キーが違う。
		step_time = status[1] -- 入力された時間を記録
		step = 3
	end
	steps[3] = function(status)
		if 6 < status[1] - step_time then -- 時間切れ
			step = 1; return
		end
		if status[2] ~= 6 then return end -- 方向キーが違う。
		step_time = status[1] -- 入力された時間を記録
		step = 4
	end
	steps[4] = function(status)
		if 6 < status[1] - step_time then -- 時間切れ
			step = 1; return
		end
		if status[3].P then -- パンチボタンが押されてます。
			step = 5
			step_time = status[1]
		end
	end
	steps[5] = function(status) -- 時間待ちルーチン
		if 3 < status[1] - step_time then
			step = 1 -- 戻しておく
			return true
		end
	end
	command["波動拳"] = function(status) -- statusはテーブル。
		if steps[step](status) then change_action("波動拳") end -- change_action()はキャラクターの行動を変更するルーチン。変更可能(他の行動をしていない)なら変更する。
	end
end

波動拳一つ判定するのにこれだけ要るように思われます。
フレームが開始し、キー入力の状態を取得すれば、コマンド毎に用意されている関数一つ一つにキー入力状態を渡して実行していきます。(この場合、command["波動拳"](キー状態))
また、このルーチンはボタン成功後、発動までに3フレーム待っています。なぜかと言えば、最近のリュウは「滅・波動拳」を持っていますので、パンチ三つ同時押しを検出する必要があるからです。この技がなければ、steps[5]は必要なかったでしょう。
実際のプログラミングでは、このようなクロージャを動的に生成するための、簡易な記述法が必要になるでしょう。

溜めの必要な技の場合
do
	-- ソニックブーム判定ルーチン
	local step = 1
	local steps = {}
	local step_time = 0
	steps[1] = function(status) -- 溜め開始検出
		if status[2] == 7 then step = 2 end -- 左上
		if status[2] == 4 then step = 2 end -- 後ろ
		if status[2] == 1 then step = 2 end -- 左下
		if step == 2 then step_time = status[1] end -- 入力された時間を記録。
	end
	steps[2] = function(status) -- 溜め判定
		step = 1
		if status[2] == 7 then step = 2 end -- 左上
		if status[2] == 4 then step = 2 end -- 後ろ
		if status[2] == 1 then step = 2 end -- 左下
		if step = 2 then return end -- 溜めてます。
		if 19 > status[1] - step_time then return end -- 溜めが足りない。
		step_time = status[1]
		step = 3
	end
	steps[3] = function(status)
		if 6 < status[1] - step_time then -- 時間切れ
			step = 1; return
		end
		if status[3].P then -- パンチボタンが押されてます。
			step = 4
			step_time = status[1]
		end
	end
	steps[4] = function(status) -- 時間待ちルーチン
		if 3 < status[1] - step_time then
			step = 1 -- 戻しておく
			return true
		end
	end
	command["ソニックブーム"] = function(status) -- statusはテーブル。
		if steps[step](status) then change_action("ソニックブーム") end
	end
end
ダッシュ
do
	-- 前ダッシュ判定ルーチン
	local step = 1
	local steps = {}
	local step_time = 0
	steps[1] = function(status) -- 入力判定
		if status[2] ~= 6 then return end
		step_time = status[1]
		step = 2
	end
	steps[2] = function(status) -- 離した判定
		if 6 < status[1] - step_time then
			step = 1; return
		end
		if status[2] == 6 then return end -- 現状維持
		step = 3
		step_time = status[1]
	end
	steps[3] = function(status) -- 入力判定
		if 6 < status[1] - step_time then -- 時間切れ
			step = 1; return
		end
		if status[2] ~= 6 then return end
		step_time = status[1]
		step = 4
	end
	steps[4] = function(status) -- 離した判定
		if 6 < status[1] - step_time then
			step = 1; return
		end
		if status[2] == 6 then return end -- 現状維持
		step = 1
		return true
	end
	command["前ダッシュ"] = function(status) -- statusはテーブル。
		if steps[step](status) then change_action("前ダッシュ") end
	end
end