Scalaでゲームプログラミング - vol2 キー入力を受け付ける -



プログラムを組む上で重要になってくる入力情報。
今回はキーボードからの入力をチェックする機構を作ってみようと思います。


フレーム間の入力について


KeyListenerを用いてキー入力を監視します。
ただし、この機構はイベントドリブンなので1フレームの間に何度も入力があるかもしれません。
また、フレーム間で入力が変更されてしまうのは嫌ですよね。


そこで画面表示のダブルバッファリング時のように変数を二つ持ちましょう。
1つはフレーム間固定の変数。もうひとつはキー入力を受け付ける変数。
フレームの終了時に値をコピーすれば固定の方はフレーム間で値が変わる心配がありません。

keypad.scala


さらっと眺めて長いと思っても大丈夫です。後でゆっくり解説します。

import javax.swing._
import java.awt.event._
//---------------------------------------------------------
// keypad
//---------------------------------------------------------
object Keypad
{
    //-----------------------------------------------------
    // public
    //-----------------------------------------------------
    def update() =
    {
        mKeyStatusPrev = mKeyStatusNext
        mKeyStatusNext = KeyBoard.status
        KeyBoard.update()
    }

    def initalize(frame:JFrame) : Unit =
    {
        frame.setFocusable(true)
        frame.addKeyListener(KeyBoard)
    }

    //-----------------------------------------------------
    // key status
    //-----------------------------------------------------
    def isKeyPressing(keycode:Int) : Boolean = (keycode&mKeyStatusNext) != 0
    def isKeyPressed(keycode:Int) : Boolean =
    {
        val next = (keycode&mKeyStatusNext) != 0
        val prev = (keycode&mKeyStatusPrev) == 0
        if( next && prev ) true else false
    }

    def isKeyReleased(keycode:Int) : Boolean =
    {
        val next = (keycode&mKeyStatusNext) == 0
        val prev = (keycode&mKeyStatusPrev) != 0
        if( next && prev ) true else false
    }

    private var mKeyStatusNext : Int = 0
    private var mKeyStatusPrev : Int = 0

    val KEY_UP    = 1
    val KEY_DOWN  = 2
    val KEY_LEFT  = 4
    val KEY_RIGHT = 8
    val KEY_Z = 16
    val KEY_X = 32
    val KEY_SPACE  = 64
    val KEY_ENTER  = 128
    val KEY_ESCAPE = 256

    //-----------------------------------------------------
    // KEYBOARD
    //-----------------------------------------------------
    private object KeyBoard extends KeyListener
    {
        // ...
    } 

}


なんといっても最初は初期化のところを見てみましょう。
(JFrameは最初に作ったMainFrameのオブジェクトですね、引数にとってます)

初期化
def initalize(frame:JFrame) : Unit =
{
    frame.setFocusable(true)
    frame.addKeyListener(KeyBoard)
}


ウィンドウに対して、キー入力を受け付けるように指示を出します。(setFocusable)
そして、キー入力を監視するオブジェクトを提示します。
KeyBoardがそのオブジェクトですがこれは後述します。


次に変数を見てみましょう。

private var mKeyStatusNext : Int = 0
private var mKeyStatusPrev : Int = 0


mKeyStatusNextは今回のフレームにおけるキーの値(フレーム間固定)
mKeyStatusPrevは前回のフレームにおけるキーの値(フレーム間固定)
今回と前回を比較することでキーを押し始めた、離したタイミングが分かります。

キー値における視点
def isKeyPressing(keycode:Int) : Boolean = (keycode&mKeyStatusNext) != 0
def isKeyPressed(keycode:Int) : Boolean =
{
    val next = (keycode&mKeyStatusNext) != 0
    val prev = (keycode&mKeyStatusPrev) == 0
    if( next && prev ) true else false
}
def isKeyReleased(keycode:Int) : Boolean =
{
    val next = (keycode&mKeyStatusNext) == 0
    val prev = (keycode&mKeyStatusPrev) != 0
    if( next && prev ) true else false
}


ここでは、Intを引数として受け取っています。
整数を利用することで、複数のキーの状態をひとつの変数にまとめることができます。
詳しくはビット演算と管理方法の項目を参照してください。

ビット対応表
val KEY_UP    = 1    // 000000001
val KEY_DOWN  = 2    // 000000010
val KEY_LEFT  = 4    // 000000100
val KEY_RIGHT = 8    // 000001000
val KEY_Z = 16       // 000010000
val KEY_X = 32       // 000100000
val KEY_SPACE  = 64  // 001000000
val KEY_ENTER  = 128 // 010000000
val KEY_ESCAPE = 256 // 100000000


次は更新の部分です。
ここを1フレームに1回呼ぶことで値の変わるタイミングを合わせることができます。

タイミング合わせ(sync)
def update() =
{
    mKeyStatusPrev = mKeyStatusNext
    mKeyStatusNext = KeyBoard.status
}


前回のキー値 <- 今回のキー値
今回のキー値 <- KeyBoardの値


上記の手順を踏んでいます。
回のキー値、前回のキー値がチェーンのように連鎖したモデルになっています。


KeyBoardの値はフレーム間に変化する可能性がありますが、update関数を読んだときにだけコピーされるので影響はありません。
実際にプログラムが解釈するのは今回のキー値や前回のキー値なのです。

KeyBoard


privateなobjectなので外部から参照されることはありません。

private object KeyBoard extends KeyListener
{

    def status() : Int = mKeyStatus

    //-------------------------------------------------
    // keys
    //-------------------------------------------------
    override def keyTyped(e:KeyEvent) : Unit = {}
    override def keyReleased(e:KeyEvent) : Unit = 
    {
        keycase( e, mKeyStatus &= ~(_) )
    }


    override def keyPressed(e:KeyEvent) : Unit = 
    {
        keycase( e, mKeyStatus |= _ )
    }

    //-------------------------------------------------
    // key function
    //-------------------------------------------------
    private def keycase(e:KeyEvent,func:Int=>Unit)
    {
        e.getKeyCode() match
        {
            case KeyEvent.VK_UP     => func(KEY_UP)
            case KeyEvent.VK_DOWN   => func(KEY_DOWN)
            case KeyEvent.VK_LEFT   => func(KEY_LEFT)
            case KeyEvent.VK_RIGHT  => func(KEY_RIGHT)
            case KeyEvent.VK_ESCAPE => func(KEY_ESCAPE)
            case KeyEvent.VK_SPACE  => func(KEY_SPACE)
            case KeyEvent.VK_ENTER  => func(KEY_ENTER)
            case KeyEvent.VK_X => func(KEY_X)
            case KeyEvent.VK_Z => func(KEY_Z)
        }        
    }

    private var mKeyStatus : Int = 0
}

keycaseという関数があります。
これはKeyEventを受け取ってパターンマッチを行い、もうひとつ受け取った関数で加工するという形になっています。


keyPressedの場合は、無名関数「 mKeyStatus |= _ 」を利用しています。

=はorの役割を持ち、func(...)で指定されたビットを立てます。

keyReleasedの場合は、無名関数「 mKeyStatus &= ~(_) 」を利用しています。
&=はandの役割を持ち、~はビットの反転なので、func(...)で指定されたビットをクリアします。


キーが押されたときにビットを立て、キーが離されたときにビットをクリアします。
ちなみに、KeyPressedやKeyReleasedの発生間隔がキーが押されてから1フレームくらい差がでるかもしれません。



使い方


Keypad.update()を毎フレーム呼び出す。
Keypad.isKeyPressing()はキーを押しているか判定する。
Keypad.isKeyPressed()はキーを押しはじめたか判定する。
Keypad.isKeyReleased()はキーを離しはじめたか判定する。



今回のポイントはフレームというものを意識するということでした。
次回はフルスクリーンをやろうと考えていましたが、その前にゲームプログラムの骨格を考えたいと思います。