Javaでゲーム: 画面のスクロール Java8+JInput使用 (JavaFX未使用) http://argius.hatenablog.jp/entry/2017/04/10/100000
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
package game; | |
import static game.App.*; | |
import java.awt.Color; | |
import java.awt.Font; | |
import java.awt.Graphics; | |
import java.awt.image.BufferedImage; | |
import java.io.IOException; | |
import java.io.UncheckedIOException; | |
import java.util.ArrayList; | |
import java.util.List; | |
import java.util.Optional; | |
import java.util.Scanner; | |
import java.util.concurrent.Executors; | |
import java.util.concurrent.ForkJoinPool; | |
import java.util.concurrent.ScheduledExecutorService; | |
import java.util.concurrent.TimeUnit; | |
import java.util.function.Supplier; | |
import javax.imageio.ImageIO; | |
import javax.swing.JFrame; | |
import javax.swing.JOptionPane; | |
import javax.swing.JPanel; | |
import javax.swing.SwingUtilities; | |
import javax.swing.WindowConstants; | |
import game.ControllerInput.State; | |
/** | |
* 迷路ゲーム。 | |
*/ | |
public final class App { | |
/* | |
* 解説を優先しているため、以下の点にご注意ください。 | |
* | |
* ・必須でないアクセス修飾子は一部省略しています | |
* ・定数の命名規則はJavaの一般的ルールに反しています | |
* ・マジックナンバー(以下略 | |
* ・他にもコーディングスタイルについては適切なルールに従ってください | |
*/ | |
// ゲームのタイトル | |
static final String TITLE = "迷路"; | |
// タイルサイズ | |
static final int TSIZE = 32; | |
// 開始位置の座標 | |
static final int startX = 16; | |
static final int startY = 16; | |
// 画面再描画の間隔 | |
static final long refreshIntervalMicrosecs = 16666L; | |
// スクロールのウェイト時間 | |
static final long scrollWaitTime = 16L; | |
// ゲーム画面 | |
GameScreen screen; | |
// 入力デバイス | |
ControllerInput keyboardInput; | |
ControllerInput gamepadInput; | |
// 迷路 | |
Maze maze; | |
// 現在位置の座標 | |
int posX; | |
int posY; | |
App() { | |
this.keyboardInput = new KeyboardControllerInput(); | |
this.gamepadInput = new GamepadControllerInput(); | |
this.maze = new Maze(); | |
this.screen = new GameScreen(maze); | |
} | |
/** | |
* ゲーム状態の初期化。 | |
*/ | |
void initialize() { | |
this.posX = startX; | |
this.posY = startY; | |
screen.initialize(); | |
} | |
/** | |
* ゲームの開始。 | |
*/ | |
void startGame() { | |
// 画面更新スレッド | |
ScheduledExecutorService es = Executors.newSingleThreadScheduledExecutor(); | |
es.scheduleAtFixedRate(() -> screen.repaint(), 0L, refreshIntervalMicrosecs, TimeUnit.MICROSECONDS); | |
// メインループ実行スレッド | |
ForkJoinPool.commonPool().execute(() -> { | |
try { | |
mainloop(); | |
} catch (Exception e) { | |
e.printStackTrace(); | |
JOptionPane.showMessageDialog(null, "メインループで予期しない例外が発生"); | |
} | |
}); | |
} | |
/** | |
* メインループ。 | |
*/ | |
void mainloop() { | |
Supplier<Optional<ControllerInput.State>> input = integrateInputDevice(); | |
// ゲーム初期化 | |
initialize(); | |
// メインループ | |
while (true) { | |
Optional<State> op = input.get(); | |
if (!op.isPresent()) { | |
sleep(16L); | |
continue; | |
} | |
ControllerInput.State controllerState = op.get(); | |
int x = controllerState.x; | |
int y = controllerState.y; | |
// 斜め移動禁止 | |
if (x != 0) { | |
// x は -1, +1のどちらか | |
assert x == -1 || x == 1 : "controllerState.x is not -1 or 1"; | |
if (!maze.canPass(posX + x, posY)) { | |
sleep(100L); | |
continue; | |
} | |
for (int i = 0; i < 16; i++) { | |
screen.x += x * 2; | |
sleep(scrollWaitTime); | |
} | |
this.posX += x; | |
} | |
else { | |
// y は -1, +1のどちらか | |
assert y == -1 || y == 1 : "controllerState.y is not -1 or 1"; | |
if (!maze.canPass(posX, posY + y)) { | |
sleep(100L); | |
continue; | |
} | |
for (int i = 0; i < 16; i++) { | |
screen.y += y * 2; | |
sleep(scrollWaitTime); | |
} | |
this.posY += y; | |
} | |
// ゴールに到達? | |
if (maze.isGoal(posX, posY)) { | |
// ゴールした! | |
screen.goalReached = true; | |
sleep(1000L); | |
while (true) { | |
if (input.get().isPresent()) { | |
sleep(1000L); | |
initialize(); | |
break; | |
} | |
sleep(100L); | |
} | |
continue; | |
} | |
} | |
} | |
/** | |
* 入力デバイスを統合する。 | |
* @return 統合した入力デバイス | |
*/ | |
Supplier<Optional<ControllerInput.State>> integrateInputDevice() { | |
boolean keyboardAvailable = keyboardInput.available(); | |
boolean gamepadAvailable = gamepadInput.available(); | |
if (gamepadAvailable && keyboardAvailable) { | |
// ゲームパッドとキーボードが両方有効 | |
return () -> { | |
Optional<State> kb = keyboardInput.getState(); | |
return kb.isPresent() ? kb : gamepadInput.getState(); | |
}; | |
} | |
else if (gamepadAvailable) { | |
// ゲームパッドのみ有効 | |
return gamepadInput::getState; | |
} | |
else if (keyboardAvailable) { | |
// キーボードのみ有効 | |
return keyboardInput::getState; | |
} | |
throw new IllegalStateException("入力デバイスが見つかりません"); | |
} | |
static void sleep(long millis) { | |
try { | |
Thread.sleep(millis); | |
} catch (InterruptedException e) { | |
throw new RuntimeException(e); | |
} | |
} | |
public static void main(String[] args) { | |
SwingUtilities.invokeLater(new Runnable() { | |
public void run() { | |
class GameWindow extends JFrame { | |
/* | |
* ウィンドウサイズの補正 | |
* (環境とLook&Feelによってウィンドウサイズは一定でないため) | |
*/ | |
static final int WINDOW_WIDTH = TSIZE * 9 + 30; | |
static final int WINDOW_HEIGHT = TSIZE * 9 + 48; | |
GameWindow() { | |
setTitle(TITLE); | |
setResizable(false); | |
setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE); | |
setSize(WINDOW_WIDTH, WINDOW_HEIGHT); | |
setLocationRelativeTo(null); | |
} | |
} | |
App app = new App(); | |
JFrame f = new GameWindow(); | |
JPanel p = new JPanel(null); | |
p.setBackground(Color.LIGHT_GRAY); | |
f.add(p); | |
p.add(app.screen); | |
app.screen.setBounds(8, 8, TSIZE * 9, TSIZE * 9); | |
f.setVisible(true); | |
app.startGame(); | |
} | |
}); | |
} | |
} | |
/** | |
* ゲーム画面。 | |
*/ | |
final class GameScreen extends JPanel { | |
volatile int x; | |
volatile int y; | |
volatile boolean goalReached; | |
BufferedImage playerImage; | |
BufferedImage mazeImage; | |
Font goalFont; | |
boolean doneFirstDrawing; | |
GameScreen(Maze maze) { | |
setBackground(Color.BLACK); | |
Tileset tileset = new Tileset(); | |
this.playerImage = tileset.getTile('E'); | |
this.mazeImage = maze.createImage(); | |
this.goalFont = getFont().deriveFont(36.0f).deriveFont(Font.BOLD); | |
initialize(); | |
} | |
/** | |
* 初期化する。 | |
* 初期状態にリセットする。 | |
*/ | |
void initialize() { | |
this.x = TSIZE * (startX + 4); | |
this.y = TSIZE * (startY + 4); | |
this.goalReached = false; | |
} | |
@Override | |
protected void paintComponent(Graphics g) { | |
super.paintComponent(g); | |
// XXX ワークアラウンド: Macで初回のdrawStringが遅延する | |
if (!doneFirstDrawing) { | |
g.drawString("", 0, 0); | |
doneFirstDrawing = true; | |
} | |
// マップ表示 | |
g.drawImage(mazeImage.getSubimage(x, y, getWidth(), getHeight()), 0, 0, this); | |
// プレイヤーキャラクター表示 | |
g.drawImage(playerImage, TSIZE * 4, TSIZE * 4, this); | |
if (goalReached) { | |
// ゴール表示 | |
g.setColor(Color.GREEN.darker()); | |
g.fillRect(76, 78, 140, 40); | |
g.setColor(Color.WHITE); | |
g.setFont(goalFont); | |
g.drawString("GOAL!", 88, 110); | |
} | |
} | |
} | |
/** | |
* 迷路。 | |
* 地図情報の保持と地形判定を担当する。 | |
*/ | |
final class Maze { | |
private char[][] data; | |
Maze() { | |
this.data = readMapData(); | |
} | |
/** | |
* 地図データを読み込む。 | |
* @return 地図データ | |
*/ | |
private char[][] readMapData() { | |
List<String> a = new ArrayList<>(); | |
try (Scanner scanner = new Scanner(getClass().getResourceAsStream("map.txt"))) { | |
while (scanner.hasNextLine()) { | |
String line = scanner.nextLine(); | |
a.add(line.replace("\t", "")); | |
} | |
} | |
int lineCount = a.size(); | |
char[][] chars = new char[lineCount][]; | |
for (int i = 0; i < lineCount; i++) { | |
chars[i] = a.get(i).toCharArray(); | |
} | |
// すべての行が同じ桁数かどうかのチェックは省略 | |
return chars; | |
} | |
/** | |
* イメージを生成する。 | |
* @return 迷路イメージ | |
*/ | |
BufferedImage createImage() { | |
BufferedImage bi = new BufferedImage(TSIZE * 124, TSIZE * 120, BufferedImage.TYPE_INT_RGB); | |
Graphics g = bi.getGraphics(); | |
Tileset tileset = new Tileset(); | |
int verticalLength = data.length; | |
int horizontalLength = data[0].length; | |
int gY = TSIZE * 8; | |
for (int y = 0; y < verticalLength; y++) { | |
int gX = TSIZE * 8; | |
for (int x = 0; x < horizontalLength; x++) { | |
g.drawImage(tileset.getTile(getCode(x, y)), gX, gY, null); | |
gX += TSIZE; | |
} | |
gY += TSIZE; | |
} | |
return bi; | |
} | |
/** | |
* 地形コードを取得する。 | |
* @param x X座標 | |
* @param y Y座標 | |
* @return 地形コード | |
*/ | |
char getCode(int x, int y) { | |
// 引数チェックは省略 | |
return data[y][x]; | |
} | |
/** | |
* 指定した座標が通過できるかどうかを調べる。 | |
* @param x X座標 | |
* @param y Y座標 | |
* @return 通過できるなら <code>true</code>、そうでなければ <code>false</code> | |
*/ | |
boolean canPass(int x, int y) { | |
// 第6ビットが0なら通過できる | |
return (getCode(x, y) & 0b01000000) == 0; | |
} | |
/** | |
* 指定した座標がゴールかどうかを調べる。 | |
* @param x X座標 | |
* @param y Y座標 | |
* @return ゴールなら <code>true</code>、そうでなければ <code>false</code> | |
*/ | |
boolean isGoal(int x, int y) { | |
return getCode(x, y) == '9'; | |
} | |
} | |
/** | |
* タイルセット。 | |
*/ | |
final class Tileset { | |
static String codemap = ".0123456789ABCDE"; | |
private BufferedImage image; | |
Tileset() { | |
try { | |
this.image = ImageIO.read(getClass().getResourceAsStream("tileset.png")); | |
} catch (IOException e) { | |
throw new UncheckedIOException(e); | |
} | |
} | |
/** | |
* タイルを取得する。 | |
* @param code 地形コード | |
* @return タイル | |
*/ | |
BufferedImage getTile(char code) { | |
int index = codemap.indexOf(code); | |
if (index < 0) { | |
throw new IllegalArgumentException("unexpected code: " + code); | |
} | |
int x = (index % 4) * TSIZE; | |
int y = (index / 4) * TSIZE; | |
return image.getSubimage(x, y, TSIZE, TSIZE); | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
package game; | |
import java.util.Optional; | |
import net.java.games.input.*; | |
import net.java.games.input.Component.Identifier; | |
/** | |
* コントローラー入力。 | |
*/ | |
interface ControllerInput { | |
Optional<ControllerInput.State> getState(); | |
boolean available(); | |
static Controller detectController(Controller.Type type) { | |
Controller[] controllers = ControllerEnvironment.getDefaultEnvironment().getControllers(); | |
for (Controller controller : controllers) { | |
if (controller != null && controller.getType() == type) { | |
return controller; | |
} | |
} | |
return NullController.INSTANCE; | |
} | |
/** | |
* コントローラー入力の状態。 | |
*/ | |
static final class State { | |
int x; | |
int y; | |
boolean attackButtonPushed; | |
State(int x, int y, boolean attackButtonPushed) { | |
this.x = x; | |
this.y = y; | |
this.attackButtonPushed = attackButtonPushed; | |
} | |
@Override | |
public String toString() { | |
return String.format("State(x=%s, y=%s, attackButtonPushed=%s)", x, y, attackButtonPushed); | |
} | |
} | |
} | |
/** | |
* コントローラーが無効な場合のスタブ。 | |
*/ | |
final class NullController implements Controller { | |
static final Controller INSTANCE = new NullController(); | |
@Override | |
public Controller[] getControllers() { | |
return new Controller[0]; | |
} | |
@Override | |
public Type getType() { | |
return Controller.Type.UNKNOWN; | |
} | |
@Override | |
public Component[] getComponents() { | |
return new Component[0]; | |
} | |
@Override | |
public Component getComponent(Identifier id) { | |
return null; | |
} | |
@Override | |
public Rumbler[] getRumblers() { | |
return new Rumbler[0]; | |
} | |
@Override | |
public boolean poll() { | |
return false; | |
} | |
@Override | |
public void setEventQueueSize(int size) { | |
} | |
@Override | |
public EventQueue getEventQueue() { | |
return null; | |
} | |
@Override | |
public PortType getPortType() { | |
return Controller.PortType.UNKNOWN; | |
} | |
@Override | |
public int getPortNumber() { | |
return -1; | |
} | |
@Override | |
public String getName() { | |
return "NullController"; | |
} | |
} | |
/** | |
* ゲームパッドコントローラー入力。 | |
*/ | |
final class GamepadControllerInput implements ControllerInput { | |
final Controller controller; | |
volatile boolean button3Released; | |
GamepadControllerInput() { | |
this.controller = ControllerInput.detectController(Controller.Type.GAMEPAD); | |
this.button3Released = true; | |
} | |
@Override | |
public boolean available() { | |
return controller != NullController.INSTANCE; | |
} | |
@Override | |
public Optional<ControllerInput.State> getState() { | |
if (controller.poll()) { | |
float x0 = controller.getComponent(Identifier.Axis.X).getPollData(); | |
float y0 = controller.getComponent(Identifier.Axis.Y).getPollData(); | |
int x = x0 > 0.0f | |
? 1 | |
: x0 < -0.1f | |
? -1 | |
: 0; | |
int y = y0 > 0.0f | |
? 1 | |
: y0 < -0.1f | |
? -1 | |
: 0; | |
// Button._2はボタン3(0オリジン) | |
boolean button3Pushed = controller.getComponent(Identifier.Button._2).getPollData() > 0.0f; | |
boolean attackButtonPushed = button3Released && button3Pushed; | |
button3Released = !button3Pushed; | |
if (x != 0 || y != 0 || attackButtonPushed) { | |
return Optional.of(new ControllerInput.State(x, y, attackButtonPushed)); | |
} | |
} | |
return Optional.empty(); | |
} | |
} | |
/** | |
* キーボードコントローラー入力。 | |
*/ | |
final class KeyboardControllerInput implements ControllerInput { | |
final Controller controller; | |
volatile boolean spaceKeyReleased; | |
KeyboardControllerInput() { | |
this.controller = ControllerInput.detectController(Controller.Type.KEYBOARD); | |
this.spaceKeyReleased = true; | |
} | |
@Override | |
public boolean available() { | |
return controller != NullController.INSTANCE; | |
} | |
@Override | |
public Optional<ControllerInput.State> getState() { | |
if (controller.poll()) { | |
int x = controller.getComponent(Identifier.Key.LEFT).getPollData() > 0.0f | |
? -1 | |
: controller.getComponent(Identifier.Key.RIGHT).getPollData() > 0.0f | |
? 1 | |
: 0; | |
int y = controller.getComponent(Identifier.Key.UP).getPollData() > 0.0f | |
? -1 | |
: controller.getComponent(Identifier.Key.DOWN).getPollData() > 0.0f | |
? 1 | |
: 0; | |
// Spaceキー | |
boolean spaceKeyPushed = controller.getComponent(Identifier.Key.SPACE).getPollData() > 0.0f; | |
boolean attackButtonPushed = spaceKeyReleased && spaceKeyPushed; | |
spaceKeyReleased = !spaceKeyPushed; | |
if (x != 0 || y != 0 || attackButtonPushed) { | |
return Optional.of(new ControllerInput.State(x, y, attackButtonPushed)); | |
} | |
} | |
return Optional.empty(); | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment