JavaScriptでキー入力を判別してドラクエ風の戦闘画面を作ってみる
前回、「プログラマーになったきっかけはINKEY$」ではキー入力を検知するINKEY$関数に出会うことで一気にプログラミングを学ぶきっかけを掴めた、というような話を書きました。
昔のパソコンとは違って、今はマウス操作、あるいはスマホやタブレットだったらタッチ操作が主流ですが、あえてここでキー入力を検知するプログラムサンプルを作ってみたいと思います。
JavaScriptでキー入力を検出する
動作サンプル
下のテキストボックスを一度クリックしてアクティブにしてから、キーボードでカーソルキーを入力すると矢印が表示されます。
また、エンターキーのときは「Enter!」それ以外のときはそのキーのキーコードが表示されます。
サンプルコード
<html>
<body>
<input type="text" id="debug">
<script>
document.getElementById("debug").onkeydown = debug_onkeydown;
function debug_onkeydown()
{
switch (event.keyCode) {
case 37:
document.getElementById("debug").value = "←";
break;
case 38:
document.getElementById("debug").value = "↑";
break;
case 39:
document.getElementById("debug").value = "→";
break;
case 40:
document.getElementById("debug").value = "↓";
break;
case 13: //Enter
document.getElementById("debug").value = "Enter!";
break;
default:
document.getElementById("debug").value = "キーコード:" + event.keyCode;
break;
}
}
</script>
</body>
</html>
解説
inputタグのonkeydownイベント発生時にdebug_onkeydown関数が呼ばれるようにして、event.keyCodeでキーコードを取得しているだけです。
上から下へ流れるだけのシングルタスクとは異なり、今どきのPCはマルチタスクが当たり前なので、こうしたイベント・ドリブン型のプログラミングになります。
(INKEY$でキーを待ち受けるのではなく、OS自体がキー発生を検知して、プログラムをキックしてくれる)
尚、inputタグのonkeydownではなく、document.onkeydown = debug_onkeydown; とすればドキュメント全体のキー入力を取得できます。
ただ、当たり前ですが、カーソルキーの下を押すとブラウザがスクロールするため、別ウィンドウで表示したり、上述のようにテキストボックスをアクティブにすることでスクロールしないようにしたり、一工夫が必要となります。
せっかくなのでドラクエ風の戦闘画面にしてみる
前回と同様、画面下のテキストボックスをクリックすることでキーボードのカーソルキーが使えるようになります。カーソルキーの上下でメニュー移動、Enterキーでメニュー実行です。
今回はマウスも使えるようにしてあるため、メニューをマウスでクリック、あるいはスマホやタブレットの場合はタッチすることでもメニューを選択可能です。
また、既に選択されているメニューをクリック(またはタップ)すると、そのメニュー内容を実行する、という動きにしてあります。
動作サンプル
サンプルコード
ちと長いです。
<html>
<body>
<script>
var menu_id = 0;
//キー入力による分岐処理
function game_keydown()
{
switch (event.keyCode) {
case 37: //カーソルキーの左
document.getElementById("game_control").value = "←";
break;
case 38: //カーソルキーの上
document.getElementById("game_control").value = "↑";
if (menu_id <= 1) {
activeMenu(4);
} else {
activeMenu(menu_id - 1);
}
break;
case 39: //カーソルキーの右
document.getElementById("game_control").value = "→";
break;
case 40: //カーソルキーの下
document.getElementById("game_control").value = "↓";
if (menu_id >= 4) {
activeMenu(1);
} else {
activeMenu(menu_id + 1);
}
break;
case 13: //エンターキー
doCommand(menu_id);
break;
default: //その他の場合はキーコードを表示
document.getElementById("game_control").value = "キーコード:" + event.keyCode;
break;
}
}
function activeMenu(id)
{
if (menu_id == id) {
//前回と同じメニューが選ばれた場合はコマンドを実行
doCommand(id);
} else {
if (menu_id != 0) {
//現在のメニューのカーソルを消す
document.getElementById('menu' + menu_id).className = 'menu';
}
//今回選ばれたメニューにカーソルを表示
document.getElementById('menu' + id).className = 'menu menu-active';
menu_id = id;
}
}
//コマンドの実行
function doCommand(command_id)
{
document.getElementById("game_control").value = "コマンド番号:" + command_id;
switch (command_id) {
case 1: //たたかう
document.getElementById('message').innerHTML = '<span class="message">ああああの こうげき! ミス!</span>';
break;
case 2: //ぼうぎょ
document.getElementById('message').innerHTML = '<span class="message">ああああは みをまもっている!</span>';
break;
case 3: //どうぐ
document.getElementById('message').innerHTML = '<span class="message">しかし なにももっていなかった。</span>';
break;
case 4: //にげる
document.getElementById('message').innerHTML = '<span class="message">しかし まわりこまれてしまった!</span>';
break;
default:
break;
}
}
</script>
<div class="game_window">
<div class="game_menu">
<div id="menu1" onclick="activeMenu(1);" class="menu">たたかう</div>
<div id="menu2" onclick="activeMenu(2);" class="menu">ぼうぎょ</div>
<div id="menu3" onclick="activeMenu(3);" class="menu">どうぐ</div>
<div id="menu4" onclick="activeMenu(4);" class="menu">にげる</div>
</div>
<div class="game_enemy">
ヽ(=´▽`=)ノ
</div>
<div id="message" style="">
<span class="message">顔文字マン が あらわれた</span>
</div>
</div>
<input type="text" id="game_control" onkeydown="game_keydown();"></div>
<style>
/* ゲーム画面全体のデザイン */
.game_window {
width:100%;
height:300px;
background-color:black;
color:white;
}
/* メニュー */
.game_menu {
width:100px;
border:3px solid white;
border-radius:6px;
margin:1em;
padding:1em;
float:left;
}
.game_enemy {
width:300px;
text-align:center;
margin:1em;
padding:1em;
float:left;
}
#message {
width:300px;
border:3px solid white;
border-radius:6px;
margin:1em;
padding:1em;
float:left;
}
/* メッセージのアニメーション */
@-webkit-keyframes typing {
from { width: 0 }
to { width:100% }
}
span.message {
display:inline-block;
width:100%;
white-space:nowrap;
overflow:hidden;
-webkit-animation: typing 1s;
}
/* メニュー用 */
.menu {
cursor:pointer;
}
.menu:before {
cursor:pointer;
content: ' ';
}
/* メニュー選択状態 */
.menu-active:before {
content: '>';
}
</style>
<script>
activeMenu(1);
</script>
</body>
</html>
解説
画面デザインは置いておくとしてw ポイントは var menu_id というグローバル変数を用意して、そこにメニュー番号を持っているところでしょうか。
カーソルの上や下が押されるたびに menu_id を(1~4の範囲内で)プラスしたりマイナスしたり。
menu_idには常に現在のカーソル位置が入っており、activeMenu関数にはカーソルを移動させたい先のメニュー番号が渡されます。そのため、現在位置のカーソルを消して、移動先のカーソルを表示する、という処理が出来るわけです。
また、そのカーソルの表示、非表示にはCSSを使っています。.menu:before という部分。contentの中には全角の空白が入っています。「たたかう」や「ぼうぎょ」の前に全角の空白が常に表示されている状態です。
そしてactiveMenu関数が呼ばれた際、そのclassNameには.menu-activeも追加されます。.menu-active:beforeにはcontent:'▶'が入っているため「たたかう」や「ぼうぎょ」の前にカーソルが表示される、という寸法。
あと、メッセージ表示が左から右になめらかに表示されるのもCSSのおかげですね。
-webkit-keyframesのfrom~toで要素の幅が0%~100%まで変化するようにしてあります。
このキーフレームを-webkit-animation: typing 1s;で1秒と指定しているため、1秒かけてメッセージの幅が0%~100%までアニメーションします。
ただ、これはかなりやっつけ仕事ではあります。
0~100%までの秒数しか指定できないため、そのメッセージが1文字だろうと100文字だろうと1秒で表示しきってしまうわけで、長文であればあるほど表示速度が早い、ということになります。本来であれば1文字を表示する秒数を指定したいところ。
また、このCSSでは2行以上表示することも出来ません。やろうと思えば表示自体はできますが、1行目と2行目が同時に左から右にかけて文字表示されるため、かなり違和感を覚えるでしょう。
CSSで出来ないもんかなぁーとお試しでやってみただけなので、実際はJavaScriptのsetTimeout関数あたりを使って1文字ずつ表示する関数を作ったほうが良さそうです。
まとめ
単にJavaScriptでのonkeydownイベントを解説するのもつまらない気がしたので悪ノリしてドラクエ風の戦闘画面を作ってみた。