2011年12月15日木曜日

キー同時押し対応: Processingでゲームを作るときに使うキーの状態を管理するクラス

課題

Processingでは、キーを押したり、離したりといったイベントに対応したプログラムを簡単に書くことができる(※1)。 しかし、どのキーが押されていて、離されているのかを調べることは、素直にはできない。 これはキーの同時押しなどに、ナイーブには対応できないということだ。

ゲームでは、キーの同時押しなどにも対応する必要がある。 そこで、キーの状態を管理するクラスを作ってみた。

※1: それぞれkeyPressed()とkeyReleased()メソッドをオーバーライドする。


原因

Processingには、イベントが発生したキーの値を調べるために、char型のkeyとint型のkeyCodeという変数が用意されている(※2)。 この変数には、最後にイベントが起こったキーが入っている。 それは、一般に現在押されているキーとは異なる。

※2: キーがASCIIコード表にあるものなら、keyにその文字が入る。ない場合は、keyの値がCODEDになり、keyCodeにUPなどの対応した値が入る。詳しくはリファレンスを参照。


解決方法

キーのクラスを作り、押されているか否かという状態をフィールドに用意する。

キーが押されたり、離されたりしたイベントが発生したら、どのキーでそのイベントが起こったか調べ、押されているか否かという状態を更新する。


実装例

クラス図は次のようになる。

キー
キーの名前: String
keyの値: char
keyCodeの値: int
キーが押されているか否か: boolean
引数で指定されたキーと一致するか(keyの値: char, keyCodeの値: int)
引数で指定されたキーが押された(keyの値: char, keyCodeの値: int)
引数で指定されたキーが離された(keyの値: char, keyCodeの値: int)
キーの絵を描画(表示位置のx座標: int, 表示位置のy座標: int, 横幅: int, 高さ: int, フォント: PFont, フォントサイズ: int)

キーの絵を描画するメソッドは、下の方に書いた使用例で使うために用意した。 これは必要なければ、用意しなくてもいい。

具体的なコードは以下である。

// Processingでキーの状態を管理するクラス
class Key {
  String name; // キーの名前
  char key; // keyの値
  int keyCode; // keyCodeの値
  boolean isPressed; // 押されているか否か

  // コンストラクタ
  Key(String tempName, char tempKey, int tempKeyCode) {
    name = tempName;
    key = tempKey;
    keyCode = tempKeyCode;
    isPressed = false;
  }

  // 引数で指定されたキーと一致するかどうかを判定
  boolean equals(char tempKey, int tempKeyCode) {
    if (tempKey != CODED) {
      if (key == tempKey) {
        return true;
      } else {
        return false;
      }
    } else {
      if (keyCode == tempKeyCode) {
        return true;
      } else {
        return false;
      }
    }
  }

  // 引数で指定されたキーが押された
  void pressed(char tempKey, int tempKeyCode) {
    if (equals(tempKey, tempKeyCode) == true) {
      isPressed = true;
    }
  }

  // 引数で指定されたキーが離された
  void released(char tempKey, int tempKeyCode) {
    if (equals(tempKey, tempKeyCode) == true) {
      isPressed = false;
    }
  }

  // キーの絵を描画する
  void draw(int xOffset, int yOffset, int width, int height, PFont font, int fontSize) {
    int textColor, backgroundColor;

    if (isPressed == true) {
      textColor = 255;
      backgroundColor = 0;
    } else {
      textColor = 0;
      backgroundColor = 255;
    }

    stroke(0);

    fill(backgroundColor);
    rect(xOffset, yOffset, width, height);

    textAlign(CENTER);
    textFont(font);
    fill(textColor);
    text(name, xOffset + width / 2, yOffset + height / 2 + fontSize / 2);
  }
}

使用例

ゲームでよく使うキー(上下左右スペース)の状態を画面に表示するプログラムを作ってみた。

ゲームのキーのコントローラー

まず、ゲームでよく使うキーをコントロールするコードを、メインプログラムに直接書くとぐちゃぐちゃする。 よって、ゲームのキーのコントローラーのクラスを用意した。 このクラスは、ゲームに必要なキー(この例では上下左右スペース)によって、変更を受ける。 クラス図は以下のようになる。

ゲームに使うキーのコントローラー
上キー: キー
下キー: キー
左キー: キー
右キー: キー
スペースキー: キー
使用するすべてのキー: キー[]
キーを描画する横幅: int
キーを描画する高さ: int
キーを描画するフォント: PFont
キーを描画するフォントのサイズ: int
引数で指定されたキーが押された(keyの値: char, keyCodeの値: int)
引数で指定されたキーが離された(keyの値: char, keyCodeの値: int)
キーのコントローラーを描画(表示位置のx座標: int, 表示位置のy座標: int)

具体的なコードは以下である。

// ゲームに使うキーのコントローラーのクラス
class KeyController {
  Key upKey, downKey, leftKey, rightKey, spcKey; // 使用するキー
  Key[] keys; // キーを入れる配列
  int keyWidth, keyHeight; // キーの描画サイズ
  PFont keyFont; // キーの描画フォント
  int fontSize; // キーの描画フォントのサイズ

  KeyController() {
    // キーの描画の設定
    keyWidth = 50;
    keyHeight = 50;
    fontSize = 10;
    keyFont = createFont("MS Gothic", fontSize);

    // 上下左右スペースキーを作る
    upKey    = new Key("UP",    (char)CODED, UP);
    downKey  = new Key("DOWN",  (char)CODED, DOWN);
    leftKey  = new Key("LEFT",  (char)CODED, LEFT);
    rightKey = new Key("RIGHT", (char)CODED, RIGHT);
    spcKey   = new Key("SPC",   ' ',         0);

    // 作ったキーを登録する
    keys = new Key[5];
    keys[0] = upKey;
    keys[1] = downKey;
    keys[2] = leftKey;
    keys[3] = rightKey;
    keys[4] = spcKey;
  }

  // 引数で指定されたキーが押された
  void pressed(char tempKey, int tempKeyCode) {
    // 全キーに引数で指定されたキーが押されたことを通知
    for (int i = 0; i < keys.length; i++) {
      keys[i].pressed(tempKey, tempKeyCode);
    }
  }

  // 引数で指定されたキーが押された
  void released(char tempKey, int tempKeyCode) {
    // 全キーに引数で指定されたキーが離されたことを通知
    for (int i = 0; i < keys.length; i++) {
      keys[i].released(tempKey, tempKeyCode);
    }
  }

  // キーの状態を描画する
  void draw(int left, int top) {
    /*
                  -------
                  | UP  |
      -------------------------
      | SPC |LEFT |DOWN |RIGHT|
      -------------------------
    */
    upKey.draw(   2 * keyWidth + left,             top, keyWidth, keyHeight, keyFont, fontSize);
    downKey.draw( 2 * keyWidth + left, keyHeight + top, keyWidth, keyHeight, keyFont, fontSize);
    leftKey.draw(     keyWidth + left, keyHeight + top, keyWidth, keyHeight, keyFont, fontSize);
    rightKey.draw(3 * keyWidth + left, keyHeight + top, keyWidth, keyHeight, keyFont, fontSize);
    spcKey.draw(                 left, keyHeight + top, keyWidth, keyHeight, keyFont, fontSize);
  }
}

メインプログラム

ゲーム用のキーのコントローラーのクラスを作って、そちらにコードをまとめたので、メインプログラムは以下のようにすっきり書ける。

// ゲームに使うキーのコントローラーのオブジェクト
KeyController keyController;

void setup() {
  size(400, 400);
  keyController = new KeyController();
}

void draw() {
  keyController.draw(100, 100);
}

void keyPressed() {
  keyController.pressed(key, keyCode);
}

void keyReleased() {
  keyController.released(key, keyCode);
}

メモ

この実装は、Casey Reas、Ben Fry著、『Processingをはじめよう』、オライリー、2011年程度の知識がある人が見ることを想定している。 このため、カプセル化などは行なっていないし、拡張性のある柔軟な設計にもしていない。

たとえば、KeyControllerクラスとKeyクラスには、デザインパターンのObserverパターンの一部を使っている。 しかし、KeyControllerに、柔軟にKeyを追加したり、削除したりするようなメソッドを用意していない。

Javaに関してより高度な知識があれば、KeyやKeyControllerクラスのフィールドを外部から隠蔽し、必要なsetterやgetterを付けてもいいかもしれない。 また、KeyやKeyControllerから、画面表示に関連するコードを分離してもいいかもしれない。