Skip to content

Instantly share code, notes, and snippets.

@argius
Last active April 10, 2017 12:52
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save argius/6e0f0f85fb2f14934bb02ea45ecf7c02 to your computer and use it in GitHub Desktop.
Save argius/6e0f0f85fb2f14934bb02ea45ecf7c02 to your computer and use it in GitHub Desktop.
Javaでゲーム: 画面のスクロール Java8+JInput使用 (JavaFX未使用) http://argius.hatenablog.jp/entry/2017/04/10/100000
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);
}
}
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