RSS2.0

LWJGL でキーボートやジョイスティックを使ってみる

前回の更新から少し間が空いてしまいましたが、LWJGL によるゲームプログラミング講座の続きを書いてみました。今回のテーマは入力処理です。
PC ゲームでは基本的には入力装置としてキーボードを使うことになると思いますが、LWJGL ではジョイスティック(ジョイパッド?ゲームパッド?)の API が用意されているので、合わせてご紹介していこうと思いますー。

今回の成果物はこちら。台風を縦横無尽に操って、尾道のあたりを荒らしまわりましょう。
Screenshot.png
ちなみに、マウス入力については今回は見送っています。私がコンシューマー系のゲームが好きなこともあって、ジョイスティックのみのインターフェースが好きなのが理由だったりします。マウスの入力処理については要望があればそのうち触れてみようかなーと思います。

ソースコード

サンプルプログラムのソースコードです。
実行には昔使った以下のファイルが必要になりますので、いつも通りプロジェクトに配置してください。
test.monolith.v3.TextureLoader
test.monolith.v3.Texture
test.alphablend.AlphaBlend

サンプルプログラムのソースコードは、以下の4つになります。

InputTest.java
package test.input;

import static org.lwjgl.opengl.GL11.GL_BACK;
import static org.lwjgl.opengl.GL11.GL_BLEND;
import static org.lwjgl.opengl.GL11.GL_COLOR_BUFFER_BIT;
import static org.lwjgl.opengl.GL11.GL_CULL_FACE;
import static org.lwjgl.opengl.GL11.GL_MODELVIEW;
import static org.lwjgl.opengl.GL11.GL_PROJECTION;
import static org.lwjgl.opengl.GL11.GL_QUADS;
import static org.lwjgl.opengl.GL11.GL_TEXTURE_2D;
import static org.lwjgl.opengl.GL11.glBegin;
import static org.lwjgl.opengl.GL11.glClear;
import static org.lwjgl.opengl.GL11.glClearColor;
import static org.lwjgl.opengl.GL11.glColor4f;
import static org.lwjgl.opengl.GL11.glCullFace;
import static org.lwjgl.opengl.GL11.glEnable;
import static org.lwjgl.opengl.GL11.glEnd;
import static org.lwjgl.opengl.GL11.glLoadIdentity;
import static org.lwjgl.opengl.GL11.glMatrixMode;
import static org.lwjgl.opengl.GL11.glOrtho;
import static org.lwjgl.opengl.GL11.glPopMatrix;
import static org.lwjgl.opengl.GL11.glPushMatrix;
import static org.lwjgl.opengl.GL11.glRotatef;
import static org.lwjgl.opengl.GL11.glScalef;
import static org.lwjgl.opengl.GL11.glTranslatef;
import static org.lwjgl.opengl.GL11.glVertex3f;

import java.util.ArrayList;
import java.util.List;

import org.lwjgl.LWJGLException;
import org.lwjgl.Sys;
import org.lwjgl.input.Controller;
import org.lwjgl.input.Controllers;
import org.lwjgl.opengl.Display;
import org.lwjgl.opengl.DisplayMode;

import test.alphablend.AlphaBlend;
import test.input.GameController.Input;
import test.monolith.v3.Texture;
import test.monolith.v3.TextureLoader;

public class InputTest {
	private static final float	TYPHOON_OUTER_ROTATE_MILLSECOND= 38000;
	private static final float	TYPHOON_ROTATE_MILLSECOND= 22000;
	private static final float	TYPHOON_INNER_ROTATE_MILLSECOND = 12000;
	private static final int	MOVE_DISTANCE_PER_SECOND = 100;

	private int			width = 1280;
	private int			height = 720;
	private int			depth = 100;
	private String		title = "Game Controllers";

	private long		time;
	private long		timeDelta;
	private long		fps;

	private Texture		textureMap;
	private Texture		textureTyphoon;
	private Texture		textureYurie;
	
	private long		actionStartTime;
	private long		actionTime;
	private float		x;
	private float		y;

	private List<GameController>	controllers;
	private int				controllerIndex;
	private GameController	controller;
	private GameController	keyboard;

	public void start() {
		try {
			//	ウインドウを生成する
			Display.setDisplayMode(new DisplayMode(width, height));
			Display.setTitle(title);
			Display.create();
		} catch(LWJGLException e) {
			e.printStackTrace();
			return;
		}

		try {
			//	OpenGL の初期設定

			//	オフスクリーンを初期化する際の背景色を指定する
			glClearColor(1f, 1f, 1f, 1f);

			//	テクスチャーを有効化する
			glEnable(GL_TEXTURE_2D);

			//	アルファブレンドを有効化する
			glEnable(GL_BLEND);

			//	ポリゴンの表示面を表のみに設定する
			glEnable(GL_CULL_FACE);
			glCullFace(GL_BACK);

			//	カメラ用の設定変更を宣言する
			glMatrixMode(GL_PROJECTION);
			//	設定を初期化する
			glLoadIdentity();
			//	視体積(目に見える範囲)を定義する
			glOrtho(0, width, 0, height, 0, depth);

			//	物体モデル用の設定変更を宣言する
			glMatrixMode(GL_MODELVIEW);

			init();

			while (!Display.isCloseRequested()) {
				//	オフスクリーンを初期化する
				glClear(GL_COLOR_BUFFER_BIT);

				//	キー入力を処理する
				updateControllers();

				//	FPS を計算する
				calcFps();

				update();

				//	オフスクリーンに描画する
				render();

				//	オフスクリーンをスクリーンに反映する
				Display.update();
			}
		} catch(Exception e) {
			e.printStackTrace();
		} finally {
			terminate();
			Display.destroy();
		}
	}

	private void updateControllers() {
		//	すべてのジョイパッドの入力を取得する
		Controllers.poll();

		while (Controllers.next()) {
			//	現在利用しているジョイパッドのイベントであれば、入力状況を更新する
			if (controller.isOwner(Controllers.getEventSource())) {
				controller.update();
			}
		}

		//	キーボードは常に入力状況を更新する
		keyboard.update();
	}

	private void init() throws Exception {
		time = getMillisecond();
		fps = 0;

		//	ファイルのパス指定で画像を読み込む
		textureMap = new TextureLoader().loadTexture("images/map.png");
		textureTyphoon = new TextureLoader().loadTexture("images/typhoon.png");
		textureYurie = new TextureLoader().loadTexture("images/yurie.png");

		//	ゆりえ様を初期化する
		actionStartTime = actionTime = getMillisecond();
		x = 600;
		y = 300;

		//	コントローラーを用意する
		controllers = new ArrayList<GameController>();
		try {
			Controllers.create();
			for (int i = 0; i < Controllers.getControllerCount(); i++) {
				Controller	controller = Controllers.getController(i);

				System.out.println("Detected controller(" + i + "): " + controller.getName());
				controllers.add(new JoyStick(controller));
			}
		} catch (LWJGLException e) {
			e.printStackTrace();
		}
		keyboard = new PcKeyboard();
		controllers.add(keyboard);

		controllerIndex = controllers.size() - 1;
		controller = controllers.get(controllerIndex);
	}
	
	private void terminate() {
		if (textureMap != null) textureMap.dispose();
		if (textureTyphoon != null) textureTyphoon.dispose();
		if (textureYurie != null) textureYurie.dispose();
	}

	private void update() {
		//	GameController.isPressing() ではなく、最初に押して離されたキーをイベントとして取得する
		if (keyboard.getKeyEvent() == Input.SELECT) {
			controllerIndex = (controllerIndex + 1) % controllers.size();
			controller = controllers.get(controllerIndex);
		}
		
		//	移動の方向と大きさを判定する
		int			moveSize = 1;
		int			moveX = 0;
		int			moveY = 0;

		if (controller.isPressing(Input.UP)) {
			moveY = 1;
		} else if (controller.isPressing(Input.DOWN)) {
			moveY = -1;
		}

		if (controller.isPressing(Input.RIGHT)) {
			moveX = 1;
		} else if (controller.isPressing(Input.LEFT)) {
			moveX = -1;
		}

		if (controller.isPressing(Input.CROSS)) {
			moveSize = 3;
		}

		//	移動する
		long	now = getMillisecond();
		long	delta = now - actionTime;
		int		move = (int)(MOVE_DISTANCE_PER_SECOND * (delta / 1000f));

		x += (moveX * move * moveSize);
		y += (moveY * move * moveSize);
		actionTime = now;

		//	FPS を調整する
		Display.sync(60);
	}

	private void render() {
		//	設定を初期化する
		glLoadIdentity();

		drawPicture(textureMap, 0, height);
	
		//	行列スタックに現在の行列を退避させ、新しい行列に対してモデルビュー変換を開始する
		glPushMatrix();

		//	ゆりえ様の中心が原点となるよう座標軸を移動する
		glTranslatef(x, y, 0);

		AlphaBlend.Screen.config(textureTyphoon);
		drawTyphoon(TYPHOON_OUTER_ROTATE_MILLSECOND, 0.5f, 1.3f);
		drawTyphoon(TYPHOON_ROTATE_MILLSECOND, 1f, 1f);
		drawTyphoon(TYPHOON_INNER_ROTATE_MILLSECOND, 0.7f, 0.7f);

		AlphaBlend.AlphaBlend.config(textureYurie);
		glColor4f(1f, 1f, 1f, 1f);
		drawPicture(textureYurie, textureYurie.getWidth() / -2, textureYurie.getHeight() / 2);
		
		//	行列スタックからもとの行列を取り出す
		glPopMatrix();

	}
	
	private void drawTyphoon(float rotateTime, float alpha, float scale) {
		glPushMatrix();

		//	Z 軸を中心にを回転させる
		float	angle = ((getMillisecond() - actionStartTime) % rotateTime) / rotateTime * 360;
		glRotatef(angle, 0, 0, 1);

		//	スケールを設定する
		glScalef(scale, scale, scale);

		glColor4f(1f, 1f, 1f, alpha);
		
		drawPicture(textureTyphoon, textureTyphoon.getWidth() / -2, textureTyphoon.getHeight() / 2);

		glPopMatrix();
	}

	private void drawPicture(Texture texture, float x, float y) {
		//	テクスチャをバインドする
		texture.bind();

		glBegin(GL_QUADS);

		float	x2 = x + texture.getWidth();
		float	y2 = y - texture.getHeight();

		texture.point(texture.getWidth(), 0);
		glVertex3f(x2, y, 0);

		texture.point(0, 0);
		glVertex3f(x, y, 0);

		texture.point(0, texture.getHeight());
		glVertex3f(x, y2, 0);
	
		texture.point(texture.getWidth(), texture.getHeight());
		glVertex3f(x2, y2, 0);
		
		glEnd();
	}

	private void calcFps() {
		long	now = getMillisecond();

		//	前のフレームからの経過時間を求め、timeDelta に足す
		timeDelta += now - time;
		time = now;

		if (1000 <= timeDelta) {
			//	timeDelta が 1 秒を上回ったら、FPS をリセットする
			timeDelta -= 1000;
			Display.setTitle(title + "(" + fps + "fps), controller(" + controllerIndex + "): " + controller.getName());
			fps = 1;
		} else {
			fps++;
		}
	}
	
	public static long getMillisecond() {
		//	現在の時間をミリ秒で返す
		return (Sys.getTime() * 1000) / Sys.getTimerResolution();
	}

	public static void main(String[] args) {
		new InputTest().start();
	}
}

PcKeyboard.java
package test.input;

import java.util.Collections;
import java.util.HashMap;
import java.util.Map;

import org.lwjgl.input.Controller;
import org.lwjgl.input.Keyboard;

public class PcKeyboard extends GameController {
	//	デフォルトのキーコンフィグ
	private static final Map<Integer, Input>	DEFAULT_KEY_CONFIGS;
	static {
		Map<Integer, Input>	keyConfigs = new HashMap<Integer, Input>();

		keyConfigs.put(Keyboard.KEY_UP, Input.UP);
		keyConfigs.put(Keyboard.KEY_DOWN, Input.DOWN);
		keyConfigs.put(Keyboard.KEY_RIGHT, Input.RIGHT);
		keyConfigs.put(Keyboard.KEY_LEFT, Input.LEFT);
		keyConfigs.put(Keyboard.KEY_Z, Input.CIRCLE);
		keyConfigs.put(Keyboard.KEY_X, Input.CROSS);
		keyConfigs.put(Keyboard.KEY_F1, Input.SELECT);

		DEFAULT_KEY_CONFIGS = Collections.unmodifiableMap(keyConfigs);
	}

	public PcKeyboard() {
		super(DEFAULT_KEY_CONFIGS);
	}

	public void update() {
		//	キーボードの入力をすべて取得する
		while (Keyboard.next()) {
			boolean		isPressed = Keyboard.getEventKeyState();
			int			keyCode = Keyboard.getEventKey();
			Input		input = getInput(keyCode);
			
			//	キーボードからの入力で、論理キーの状態を更新する
			if (input != null) {
				setKeyStatus(input, isPressed);
			}
		}
	}

	@Override
	public String getName() {
		return "Keyboard";
	}

	@Override
	public boolean isOwner(Controller controller) {
		return false;
	}
	
}

JoyStick.java
package test.input;

import java.util.Collections;
import java.util.HashMap;
import java.util.Map;

import org.lwjgl.input.Controller;

public class JoyStick extends GameController {
	//	デフォルトのキーコンフィグ
	private static final Map<Integer, Input>	DEFAULT_KEY_CONFIGS;
	static {
		Map<Integer, Input>	keyConfigs = new HashMap<Integer, Input>();

		keyConfigs.put(3, Input.CIRCLE);
		keyConfigs.put(2, Input.CROSS);
		DEFAULT_KEY_CONFIGS = Collections.unmodifiableMap(keyConfigs);
	}

	private final Controller	controller;

	public JoyStick(Controller controller) {
		super(DEFAULT_KEY_CONFIGS);
		this.controller = controller;
	}

	@Override
	public void update() {
		//	ボタンの入力状態を反映する
		for (int keyCode: keyCodes()) {
			Input	input = getInput(keyCode);
			boolean	isPressed = false;

			if (keyCode < controller.getButtonCount()) {
				isPressed = controller.isButtonPressed(keyCode);
			}
			setKeyStatus(input, isPressed);
		}
		
		float	xAxis = controller.getXAxisValue();
		float	yAxis = controller.getYAxisValue();

		//	アナログスティックの X 軸方向の入力状態を反映する
		boolean	isLeftPressing = false;
		boolean	isRightPressing = false;
		if (xAxis < 0) {
			isLeftPressing = true;
		} else if (0 < xAxis) {
			isRightPressing = true;
		}
		setKeyStatus(Input.LEFT, isLeftPressing);
		setKeyStatus(Input.RIGHT, isRightPressing);

		//	アナログスティックの Y 軸方向の入力状態を反映する
		boolean	isUpPressing = false;
		boolean	isDownPressing = false;
		if (yAxis < 0) {
			isUpPressing = true;
		} else if (0 < yAxis) {
			isDownPressing = true;
		}
		setKeyStatus(Input.UP, isUpPressing);
		setKeyStatus(Input.DOWN, isDownPressing);
	}

	public boolean isOwner(Controller controller) {
		return (this.controller == controller);
	}

	@Override
	public String getName() {
		return controller.getName();
	}

}

GameController.java
package test.input;

import java.util.HashMap;
import java.util.Map;
import java.util.Set;

import org.lwjgl.input.Controller;

public abstract class GameController {
	/**
	 *	論理キーを表す enum
	 *	ゲームコントローラーが持つ○ボタン(決定ボタン)や×ボタン(キャンセルボタン)などに該当する
	 */
	public enum Input {
		UP,
		DOWN,
		RIGHT,
		LEFT,
		
		CIRCLE,
		CROSS,
		
		SELECT;
	}

	private final Map<Integer, Input>	keyConfigs;
	private final Map<Input, Boolean>	keyStatus = new HashMap<Input, Boolean>();

	private Input				keyEvent;
	
	public GameController(Map<Integer, Input> keyConfigs) {
		this.keyConfigs = new HashMap<Integer, Input>();

		//	デフォルトのキーコンフィグとして、引数 keyConfigs の内容を設定する
		for (int keyCode: keyConfigs.keySet()) {
			setKeyConfig(keyCode, keyConfigs.get(keyCode));
		}
	}

	/**
	 *	キーの入力状態をチェックし、状態を更新する
	 */
	public abstract void update();

	/**
	 *	引数に渡されたジョイスティックがこのゲームコントローラーのものなら true を、そうでなければ false を返す
	 */
	public abstract boolean isOwner(Controller controller);
	
	/**
	 *	このゲームコントローラーの名前を返す
	 */
	public abstract String getName();

	/**
	 *	引数に渡されたキーコードに割り当てられた論理キーを返す
	 */
	protected Input getInput(int keyCode) {
		return keyConfigs.get(keyCode);
	}

	/**
	 *	論理キーに割り当てられているすべてのキーコードの Set を返す
	 */
	protected Set<Integer> keyCodes() {
		return keyConfigs.keySet();
	}
	
	/**
	 *	引数に渡されたキーコードに、論理キーを割り当てる
	 */
	public void setKeyConfig(int keyCode, Input input) {
		keyConfigs.put(keyCode, input);
	}
	
	/**
	 *	引数に渡された論理キーの入力状態を設定する
	 */
	protected void setKeyStatus(Input input, boolean isPressing) {
		//	論理キーが今まで押されていて、かつ離された場合、論理キーの入力イベントが完了したものとして保持しておく
		//	ただし、保持できる入力イベントは、最初の 1 つのみとする
		if ((isPressing(input)) && (!isPressing) && (this.keyEvent == null)) {
			this.keyEvent = input;
		}
		
		//	論理キーの入力状態を設定する
		keyStatus.put(input, isPressing);
	}

	/**
	 *	引数に渡された論理キーの入力状態を返す
	 */
	public boolean isPressing(Input input) {
		//	現在押されていれば true を、そうでなければ false を返す
		Boolean	isPressing = keyStatus.get(input);
		
		if (isPressing == null) {
			return false;
		} else {
			return isPressing.booleanValue();
		}
	}

	/**
	 *	論理キーの入力イベントを返す
	 *	入力イベントとは、キーが押された後に離された時点で発生する。
	 *	また、入力イベントは最初に発生したものを1つだけ保するものとする。
	 */
	public Input getKeyEvent() {
		Input	keyEvent = this.keyEvent;

		//	論理キーの入力イベントは、取得したら空にする
		this.keyEvent = null;
		
		return keyEvent;
	}

}

こちらが使用する画像です。プロジェクトのディレクトリ直下に images というディレクトリを用意して、その中に指定の名前で置いてください。

map.png
map.png

typhoon.png
typhoon.png

yurie.png
yurie.png
なんかゆりえ様っていうよりゆっくりになってる気がしないでもないですが(笑

プログラムの使い方

キーボード
・F1 キー … 利用するコントローラーを切り替える
・上下左右 キー … 上下左右に移動する(キーボード利用時のみ)
・X キー … 移動速度が 3 倍になる(キーボード利用時のみ)

ジョイスティック
・アナログスティック … 上下左右に移動する(ジョイスティック利用時のみ)
・ボタン 2 … 移動速度が 3 倍になる(ジョイスティック利用時のみ)

キーボードからの入力

まずはキーボードからの入力を取得してみます。今までにもポリゴンを回転させたりアルファブレンドをいろいろ試したりした時に実装してきた方法ですが、ここで改めてまとめてみたいと思います。

キー入力の取得

LWJGL では、キーボードからの入力は org.lwjgl.input.Keyboard クラスで扱うことができます。
一番シンプルな実装として、Keyboard.isKeyDown() メソッドが true を返すかどうかで特定のキーが現在押されているかを調べることができます。
//	矢印上キーが押されているか調べる
if (Keyboard.isKeyDown(Keyboard.KEY_UP)) {
	...
}
isKeyDown() の引数には調べたいキーボードのキーに対応する定数を渡します。定数は Keyboard の static 変数として用意されていて、アルファベットや数値はもちろん、ファンクションキーや矢印キーもあります。
Ctrl や Shift キーも、左右それぞれについて別々に Keyboard.KEY_RCONTROL、Keyboard.KEY_LCONTROL、Keyboard.KEY_RSHIFT というような定数で用意されています。Alt キーは定数の名前が Keyboard.KEY_RMENU、Keyboard.KEY_LMENU となっているので注意してください。
例えば左の Ctrl キーと Z キーが同時に押されているか判定するプログラムは、以下のようになります。
//	Ctrl + Z キーが押されているか調べる
if ((Keyboard.isKeyDown(Keyboard.KEY_LCONTROL)) && (Keyboard.isKeyDown(Keyboard.KEY_Z))) {
	...
}
日本語キーボード用に、Keyboard.KEY_KANJI なんてキーまであったりします。かゆいところに手が届いてる感じですねー。

すべてのキー入力を取得する

Keyboard.isKeyDown() メソッドにはひとつデメリットがあって、isKeyDown() メソッドを呼び出した瞬間のキー状態しか調べられません。ゲームではメインループの中で一定間隔でキーボードからの入力チェックを行うと思いますが、もしキーを押したタイミングがたまたま入力チェック中でなかった場合、プレイヤーがキーボードを押したにも関わらずそれが検知できないことになります。FPS が高いゲームでは、チェックする間隔が短くなるので事実上問題ないかもしれませんが、FPS が落ちてくる可能性があるゲームだと少し不安が残ります。

LWJGL では、キーボードからの入力をすべて取得する API もちゃんと提供されています。内部的には、キーボードからの入力イベントをためておくイベントバッファが用意されていて、そのイベントバッファの中身を先頭から拾っていくことができます。キー入力の順序もきちんと保証されているので、これによって正確な入力状態を調べることができます。
//	キーボードからの入力を、先頭から1つずつ取り出していく
while (Keyboard.next()) {
	//	キーが押されているか、離されているかを調べる
	boolean		isPressed = Keyboard.getEventKeyState();
	//	押されたキーのキーコードを取得する
	int			keyCode = Keyboard.getEventKey();
	...
}
Keyboard.next() メソッドはキーボードの入力バッファにキー入力が貯めてあれば true を返すので、while ループの条件とすることで、古いキー入力から順にひとつずつ取得していくことができます。キー入力は、それぞれのキーが押されたか離された時点で発生します。キーを数秒間押し続けた場合にも、押し始めた時点と離した時点でしかキー入力は発生しません。

ジョイスティックからの入力

続いてジョイスティック(ゲームパッド)を使った入力処理を見てみたいと思います。ジョイスティックが使えるようになると、だいぶゲームっぽくなってきますねー。

jinput

LWJGL ではジョイスティックの実装で、内部的に jinput というライブラリを使っています。なので eclipse のプロジェクト(Java の CLASSPATH)には jinput.jar も加える必要があるのですが、maven を使っている場合には、LWJGL の依存する外部ライブラリとして jinput.jar を自動でダウンロード・追加してくれているので、特に準備は必要ありません。maven 便利ですよねー。
maven を使わない場合は、公式ページの LWJGL アーカイブに含まれている jinput.jar を自分で CLASSPATH に加えてください。

ジョイスティックの検出

まずは PC に接続されているジョイスティックの検出を行います。コードはこんな感じです。
try {
	//	ジョイスティックを検出する
	Controllers.create();
	//	検出したジョイスティックを一覧表示する
	for (int i = 0; i < Controllers.getControllerCount(); i++) {
		Controller	controller = Controllers.getController(i);

		System.out.println("Detected controller(" + i + "): " + controller.getName());
		...
	}
} catch (LWJGLException e) {
	e.printStackTrace();
}
org.lwjgl.input.Controllers.create() を呼び出すことで、ジョイスティックの検出が行われます。Controllers.getControllerCount() で検出したジョイスティックの個数を、Controllers.getController() でインデックス指定でのジョイスティックの取得を行えます。
うちの環境ではこんな感じで検出されました。ペンタブとかワイヤレスマウスとかいろいろリストアップされててかなりカオスな感じです。
Detected controller(0): USB Receiver
Detected controller(1): USB Receiver
Detected controller(2): USB Receiver
Detected controller(3): Cordless Receiver
Detected controller(4): Cordless Receiver
Detected controller(5): PTK-840
Detected controller(6): RF Keyboard Mouse
Detected controller(7): RF Keyboard Mouse
Detected controller(8): Wacom Virtual Hid Driver
Detected controller(9): Wacom Virtual Hid Driver
これだけ検出されてしまうこともあるので、ちゃんとゲームを作りこんでいくなら、プレイヤーが利用するジョイスティックを設定できるような UI も必要かなーと思います。

ボタンからの入力

ジョイスティックの各ボタンの入力状態を調べるには、org.lwjgl.input.Controller.isButtonPressed() メソッドを使います。引数にはジョイスティックのボタン番号を渡します。
isPressed = controller.isButtonPressed(keyCode);
isButtonPressed() は指定したボタンが押されていれば true を、離されていれば false を返します。
引数として渡すジョイスティックのボタン番号はハードウェアに依存するため、PC に接続しているジョイパッド毎に異なる可能性があります。サンプルプログラムでは私の環境に合わせてボタン番号を割り振っているので、実行時にはこれを書き換える必要があるかもしれません。
ジョイパッドのボタン番号の割り当て(キーコンフィグ)はこんな感じで実装しています。
public class JoyStick extends GameController {
	//	デフォルトのキーコンフィグ
	private static final Map<Integer, Input>	DEFAULT_KEY_CONFIGS;
	static {
		Map<Integer, Input>	keyConfigs = new HashMap<Integer, Input>();

		keyConfigs.put(3, Input.CIRCLE);
		keyConfigs.put(2, Input.CROSS);
		DEFAULT_KEY_CONFIGS = Collections.unmodifiableMap(keyConfigs);
	}
...

また、返り値となるボタンの状態は、キーボード入力で使った Keyboard.isKeyDown() と同じく呼び出した段階での入力状態となります。
キーボード入力では Keyboard.next() メソッドによって、イベントバッファに詰まれたすべての入力イベントを発生順で取得することができました。しかし現段階での LWJGL の実装では、ジョイパッドのイベントバッファの内容(値)を参照することはできません。

ジョイパッドのイベントバッファに詰まれた入力イベントには、一応以下のようなコードでアクセスすることができます。
//	すべてのジョイパッドの入力イベントを取得する
Controllers.poll();

while (Controllers.next()) {
	//	入力イベントの発生した(入力された)コントローラーを取得する
	Controller	inputController = Controllers.getEventSource()
			
	//	入力イベントがボタンの入力によるものかを返す
	boolean		isButtonEvent = Controllers.isEventButton();

	//	入力イベントがアナログスティックの入力によるものかを返す
	boolean		isAxisEvent = Controllers.isEventAxis();
}
プレイヤーによって入力されたコントローラーを返す Controllers.getEventSource() メソッドまで用意されているにも関わらず、なぜか肝心の入力内容を返すメソッドは用意されていません。Controllers.isEventButton() や Controllers.isEventAxis() は、入力されたのがボタンか、アナログスティックかを返すメソッドですが、実際に押されたボタンの番号や、ボタンの状態(押したことによる入力イベントなのか、離したことによる入力イベントなのか)、押されたアナログスティックの値などを取得するメソッドはありません。
Controllers.getEventSource() によって入力されたコントローラーを取り出し、そのコントローラーの状態を調べればいいようにも思えますが、実際には入力イベントが複数回発生することによってコントローラーの状態は刻々と更新されていくため、入力イベント発生当時の状態はわからなくなってしまうのです。

残念ですが、これについては将来機能拡張されることを期待するしかありませんね…。

アナログスティックからの入力

前のセクションでも少し触れていますが、ジョイスティックでおなじみのアナログスティックも、ボタンと同じように入力状態を調べることができます。
float	xAxis = controller.getXAxisValue();
float	yAxis = controller.getYAxisValue();
Controller.getXAxisValue()、Controller.getYAxisValue() は、それぞれアナログスティックの X 軸方向、Y 軸方向の入力状態を返します。Controller.getXAxisValue() は、アナログスティックが左に倒されていれば 0 より小さい値を、右に倒されていれば 0 より大きい値を、アナログスティックが倒されていなければ 0 を返す、という感じです。

Controllers.isEventButton() と同じく、Controllers.getXAxisValue() / getYAxisValue() もこれらのメソッドを呼び出した時点での入力状態が返されます。

GameController クラスの構築

キーボードとジョイスティックによる入力処理については、ここまで説明してきた内容でだいたい終わりです。
ただ、これだけではゲームのロジック側から扱うには少し不便な所があるので、サンプルプログラムでは jinput の上位に、ゲームコントローラーを表す薄いラッパー層を構築してみました。ここからは jinput をどう活用していくかという話になるので、製作するゲームの種類に応じて実装しなければならない機能要件は変わってくると思います。今回はどんなゲームにも活かせるような基本機能を実装したつもりですが、細かい所はそれぞれ書き換えるなり拡張するなりして詰めていってみてください。

キーボード、ジョイスティックの抽象化

ゲームで利用できるキーボードやジョイスティックをゲームコントローラー(test.input.GameController クラス)として抽象化しています。キーボードに該当する test.input.PcKeyboard クラスや、ジョイスティックに該当する test.input.JoyStick クラスは、それぞれ GameController クラスを継承しています。これらのクラスが、今まで説明してきたハードウェア毎に異なる入力処理の差異を吸収しています。

例えば、キーの入力状態を更新する test.input.GameController.update() メソッドですが、PcKeyboard クラスでは org.lwjgl.input.Keyboard による入力チェックを、JoyStick クラスでは org.lwjgl.input.Controller による入力チェックを行うよう、それぞれオーバーライドしています。

test.input.PcKeyboard.update()
public void update() {
	//	キーボードの入力をすべて取得する
	while (Keyboard.next()) {
		boolean		isPressed = Keyboard.getEventKeyState();
		int			keyCode = Keyboard.getEventKey();
		Input		input = getInput(keyCode);

		//	キーボードからの入力で、論理キーの状態を更新する
		if (input != null) {
			setKeyStatus(input, isPressed);
		}
	}
}

test.input.JoyStick.update()
@Override
public void update() {
	//	ボタンの入力状態を反映する
	for (int keyCode: keyCodes()) {
		...
		boolean	isPressed = false;

		if (keyCode < controller.getButtonCount()) {
			isPressed = controller.isButtonPressed(keyCode);
		}
		...
	}
	
	float	xAxis = controller.getXAxisValue();
	float	yAxis = controller.getYAxisValue();
...
一方でゲームのメインロジック側からは、コントローラーはハードウェアに関わらずすべて GameController クラスのインスタンスとして見えているので、使っているコントローラーの test.input.GameController.update() を呼び出すことで、一律に入力状態を更新することができます。Java らしくオブジェクト指向で構造化させた感じですね。
もっと実用的な利点について言えば、ゲーム中で使用しているコントローラーの切り替えや、対人・協力プレイでプレイヤー毎に特定のコントローラーを割り当てる、といった場面でも役立ってきます。

論理キーの構築

コントローラーの抽象化と被る部分ではあるのですが、コントローラーから入力されるキーの種類についても仮想的な論理キーを用意することで、抽象化を行っています。PlayStation のゲームコントローラーにあるような矢印キーや、○×△□といったボタンを思い浮かべてください。これらのボタンを論理キー(test.input.GameController.Input クラス)とし、例えばキーボードなら Z キーが、ジョイスティックならボタン 3 が押された場合に○ボタンが押されたものとして扱うようにしています。これにより、プレイヤーが使っているゲームコントローラーがキーボードか、ジョイスティックなのかに関係なく、単純にどの論理キーが押されたのかだけを考えてプログラムを書いていくことができます。
//	×ボタンが押されている間はダッシュしているものとみなし、移動量を 3 倍にする
if (controller.isPressing(Input.CROSS)) {
	moveSize = 3;
}
test.input.GameController.isPressing() メソッドが、引数に渡した論理キーが現在押されているかどうかを返すメソッドです。JoyStick クラスではアナログスティックによる入力を論理キーの上下左右に反映するようにしてあるので、アナログスティックの扱いもプログラム側からは意識する必要はなくなっています(もちろん、あくまでアナログスティックによる入力かを区別しないという要件に基づくものです)。

また、論理キーに割り当てる実際の物理キー(キーボードのキーの種類や、ジョイスティックのボタン番号)を、ゲームコントローラー毎に設定することができるので、(設定用の UI さえ作れば)プレイヤー自身によるキーコンフィグ機能も実装しやすくなります。ゲームの入力判定ロジックとゲームコントローラーのキーの種別を疎結合とすることで、論理キーへの物理キーの割り当てが動的に変更しやすくなります。
public GameController(Map<Integer, Input> keyConfigs) {
	this.keyConfigs = new HashMap<Integer, Input>();

	//	デフォルトのキーコンフィグとして、引数 keyConfigs の内容を設定する
	for (int keyCode: keyConfigs.keySet()) {
		setKeyConfig(keyCode, keyConfigs.get(keyCode));
	}
}
GameController クラスのコンストラクタ内で呼び出している setKeyConfig() は、物理キーに割り当てる論理キーを設定するメソッドです。public メソッドにしておいたので、ゲームのオプション変更時など、任意のタイミングで割り当てを変更することができます。

論理キーの入力イベント

キャラクターの移動ではキーが押しっぱなしになっている間ずっと移動し続けますが、○ボタンでの決定操作などは、キーが押された後に離されて初めて、入力イベントとして扱う必要があります。キャラクターの移動ではどれくらいの期間キーが押されていたかが重要となりますが、RPG でのコマンドの決定やキャンセルといった操作では、何回キーが押されたかが重要になります。キーの押された回数を検出するには、キーが押されているかだけではなく、キーが押された後に離されたかまでをきちんと検出し、ふたつの状態の変化を合わせて一回の入力イベントとみなさなければなりません。
	/**
	 *	引数に渡された論理キーの入力状態を設定する
	 */
	protected void setKeyStatus(Input input, boolean isPressing) {
		//	論理キーが今まで押されていて、かつ離された場合、論理キーの入力イベントが完了したものとして保持しておく
		//	ただし、保持できる入力イベントは、最初の 1 つのみとする
		if ((isPressing(input)) && (!isPressing) && (this.keyEvent == null)) {
			this.keyEvent = input;
		}
		...
	}
この入力イベントの検出を、test.input.GameController.setKeyStatus() メソッド内で実装しています。
また、発生した入力イベントは、test.input.GameController.getKeyEvent() メソッドで取得することができます。

サンプルプログラムでは、論理キーの入力イベントは最初の1つのみを保持するようにしています。getKeyEvent() の呼び出しによって入力イベントが取得された段階で保持していた入力イベントをクリアし、次の入力イベントを受け付けるようにしています。いくつ入力イベントを保持するかはゲームの種類によって変わってくる部分なので、必要に応じて書き換えてみてください。

渦巻く雲を表現する

今回のテーマである入力処理からは話が変わるのですが、せっかくなのでサンプルプログラムの台風の雲の実装方法についても少し触れておきたいと思います。

基本的な表示方法はアニメーションの回での花の表示と同じなのですが、渦巻く雲をよりリアルに見せるため、雲のテクスチャ(typhoon.png)を3度重ねて表示しています。3回とも、それぞれスケール、透過率、回転速度を変えていて、それらが重なり合って単純な回転よりも複雑な効果を作っています。ゲームのエフェクトでは、このようにテクスチャをいくつか組み合わせてランダムでより自然に見えるようにする、という手法がよく使われています。

また雲のテクスチャは、素材となる画像の作成上の都合で透過イメージではなく、黒成分をアルファ値として合成しています。アルファブレンドの手法は一般的なアルファブレンドではなく、スクリーン合成になります。透過色背景に白色だけで雲を描き、一般的なアルファブレンドで合成しても同じ効果は得られますが、素材の特徴上スクリーン合成としました。

おしまい

ということで、LWJGL の入力編は以上となります。

今回の記事はすごい難産となりました。気づけば今日は 4 月 16 日ですが、これが今月最初の記事になるんですよねー。そういえばせっかくのエイプリルフールも結局遊べずに終わってしまいました。Google マップがドラクエ風になってたりしてて、今年も面白かったですね。
サンプルプログラムのネタ探しや LWJGL のジョイパッドの実装なんかを調べていたら意外と手間取ってしまった感じなのですが、できればもう少しコンスタントに更新していければなと、心構えだけは持っていますので、気長に待っていていただけると嬉しいです。
そしてそろそろ何かゲームも完成させたいところですね。どうなるかはわかりませんが(笑
  LWJGL  コメント (0)  2012/04/16 23:44:44


公開範囲:
プロフィール HN: ももかん
ゲーム作ったり雑談書いたり・・・していた時期が私にもありました。
カレンダー
<<2017, 5>>
30123456
78910111213
14151617181920
21222324252627
28293031123