Engine 1.1 · Java 16+ · Swing, AWT, java.io, JavaFX
module engine {
requires java.desktop;
requires javafx.graphics;
exports engine;
exports engine.io;
exports engine.fx;
}
package engine;
import engine.io.Storage;
/** Единая точка доступа к библиотекам: другие блоки и модули работают только через этот класс. */
public final class Engine {
public static final String VERSION = "1.1";
private Engine() { }
/** Холст Swing/AWT для отрисовки игры. */
public static GameCanvas canvas(int width, int height) {
return new GameCanvas(width, height);
}
/** Окно приложения (javax.swing.JFrame) с уже подключённым холстом. */
public static GameWindow window(String title, GameCanvas canvas) {
return new GameWindow(title, canvas);
}
/** Игровой цикл с фиксированным шагом обновления. */
public static GameLoop loop(GameCanvas canvas, int fps, Runnable onFrame) {
return new GameLoop(canvas, fps, onFrame);
}
/** Подключает клавиатуру и мышь к холсту (java.awt.event). */
public static void attachInput(GameCanvas canvas, Keyboard keyboard, Mouse mouse) {
keyboard.attach(canvas);
mouse.attach(canvas);
}
/** Файловое хранилище (java.io): сохранение рекордов и настроек. */
public static Storage storage(String fileName) {
return new Storage(fileName);
}
}
package engine;
import javax.swing.JFrame;
import javax.swing.WindowConstants;
import java.awt.Dimension;
/** Окно приложения (javax.swing.JFrame): создание, размер, показ и закрытие. */
public final class GameWindow {
private final JFrame frame;
public GameWindow(String title, GameCanvas canvas) {
frame = new JFrame(title);
frame.setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE);
frame.setResizable(false);
frame.add(canvas);
frame.pack();
frame.setLocationRelativeTo(null);
}
public void show() { frame.setVisible(true); }
public void close() { frame.dispose(); }
public void setSize(int width, int height) {
frame.getContentPane().setPreferredSize(new Dimension(width, height));
frame.pack();
}
/** Доступ к исходному JFrame для расширенной настройки. */
public JFrame raw() { return frame; }
}
package engine;
import javax.swing.JPanel;
import java.awt.Dimension;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.RenderingHints;
/** Холст (java.awt / javax.swing.JPanel) с двойной буферизацией; рисование делегируется Scene. */
public class GameCanvas extends JPanel {
private final int width, height;
private volatile Scene scene;
public GameCanvas(int width, int height) {
this.width = width;
this.height = height;
setPreferredSize(new Dimension(width, height));
setDoubleBuffered(true);
setFocusable(true);
}
public void setScene(Scene scene) { this.scene = scene; }
public int getGameWidth() { return width; }
public int getGameHeight() { return height; }
@Override
protected void paintComponent(Graphics g) {
super.paintComponent(g);
Graphics2D g2 = (Graphics2D) g.create();
g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
g2.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_ON);
Scene s = scene;
if (s != null) s.render(g2, width, height);
g2.dispose();
}
/** Интерфейс сцены: логика и отрисовка текущего экрана игры. */
public interface Scene {
void update(double dt);
void render(Graphics2D g, int width, int height);
}
}
package engine;
/** Игровой цикл с фиксированным шагом обновления и отдельным потоком. */
public final class GameLoop implements Runnable {
private final GameCanvas canvas;
private final double step;
private final Runnable onFrame;
private volatile boolean running;
private Thread thread;
public GameLoop(GameCanvas canvas, int fps, Runnable onFrame) {
this.canvas = canvas;
this.step = 1.0 / fps;
this.onFrame = onFrame;
}
public synchronized void start() {
if (running) return;
running = true;
thread = new Thread(this, "game-loop");
thread.setDaemon(true);
thread.start();
}
public void stop() { running = false; }
@Override
public void run() {
long last = System.nanoTime();
double acc = 0;
while (running) {
long now = System.nanoTime();
acc += (now - last) / 1_000_000_000.0;
last = now;
while (acc >= step) {
onFrame.run();
acc -= step;
}
canvas.repaint();
try {
Thread.sleep(1);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
break;
}
}
}
}
package engine;
import java.awt.Component;
import java.awt.event.KeyAdapter;
import java.awt.event.KeyEvent;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
/** Состояние клавиатуры (java.awt.event): нажата и только что нажата клавиша. */
public final class Keyboard extends KeyAdapter {
private final Set<Integer> down = ConcurrentHashMap.newKeySet();
private final Set<Integer> pressed = ConcurrentHashMap.newKeySet();
public void attach(Component c) { c.addKeyListener(this); }
@Override
public void keyPressed(KeyEvent e) {
if (down.add(e.getKeyCode())) pressed.add(e.getKeyCode());
}
@Override
public void keyReleased(KeyEvent e) { down.remove(e.getKeyCode()); }
public boolean isDown(int keyCode) { return down.contains(keyCode); }
/** Возвращает true один раз на нажатие (для пошаговых действий). */
public boolean consumePressed(int keyCode) { return pressed.remove(keyCode); }
}
package engine;
import java.awt.Component;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
/** Положение курсора и кнопки мыши (java.awt.event). */
public final class Mouse extends MouseAdapter {
private volatile int x, y;
private volatile boolean leftDown;
private volatile boolean clicked;
public void attach(Component c) {
c.addMouseListener(this);
c.addMouseMotionListener(this);
}
@Override
public void mouseMoved(MouseEvent e) { x = e.getX(); y = e.getY(); }
@Override
public void mouseDragged(MouseEvent e) { mouseMoved(e); }
@Override
public void mousePressed(MouseEvent e) {
mouseMoved(e);
if (e.getButton() == MouseEvent.BUTTON1) {
leftDown = true;
clicked = true;
}
}
@Override
public void mouseReleased(MouseEvent e) {
if (e.getButton() == MouseEvent.BUTTON1) leftDown = false;
}
public int getX() { return x; }
public int getY() { return y; }
public boolean isLeftDown() { return leftDown; }
/** Возвращает true один раз на клик левой кнопкой. */
public boolean consumeClick() {
boolean c = clicked;
clicked = false;
return c;
}
}
package engine;
import javax.imageio.ImageIO;
import java.awt.Graphics2D;
import java.awt.image.BufferedImage;
import java.io.IOException;
import java.io.InputStream;
import java.util.HashMap;
import java.util.Map;
/** Загрузка (javax.imageio) и отрисовка изображений с кешем. */
public final class Sprite {
private static final Map<String, BufferedImage> CACHE = new HashMap<>();
private Sprite() { }
public static synchronized BufferedImage load(String resourcePath) throws IOException {
BufferedImage img = CACHE.get(resourcePath);
if (img != null) return img;
try (InputStream in = Sprite.class.getResourceAsStream(resourcePath)) {
if (in == null) throw new IOException("Ресурс не найден: " + resourcePath);
img = ImageIO.read(in);
if (img == null) throw new IOException("Неподдерживаемый формат: " + resourcePath);
CACHE.put(resourcePath, img);
return img;
}
}
public static void draw(Graphics2D g, BufferedImage img, double x, double y) {
g.drawImage(img, (int) Math.round(x), (int) Math.round(y), null);
}
/** Вырезает кадр из листа спрайтов (сетка cols x rows). */
public static BufferedImage frame(BufferedImage sheet, int cols, int rows, int index) {
int fw = sheet.getWidth() / cols, fh = sheet.getHeight() / rows;
int cx = index % cols, cy = (index / cols) % rows;
return sheet.getSubimage(cx * fw, cy * fh, fw, fh);
}
}
package engine;
import javax.sound.sampled.AudioInputStream;
import javax.sound.sampled.AudioSystem;
import javax.sound.sampled.Clip;
import java.io.InputStream;
import java.util.HashMap;
import java.util.Map;
/** Воспроизведение коротких звуков (WAV) через javax.sound.sampled. */
public final class Sound {
private static final Map<String, Clip> CLIPS = new HashMap<>();
private Sound() { }
public static synchronized void play(String resourcePath) {
try {
Clip clip = CLIPS.get(resourcePath);
if (clip == null) {
try (InputStream in = Sound.class.getResourceAsStream(resourcePath)) {
if (in == null) return;
try (AudioInputStream ais = AudioSystem.getAudioInputStream(in)) {
clip = AudioSystem.getClip();
clip.open(ais);
}
}
CLIPS.put(resourcePath, clip);
}
clip.stop();
clip.setFramePosition(0);
clip.start();
} catch (Exception e) {
// звук необязателен: при ошибке игра продолжает работу
}
}
}
package engine.io;
import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.File;
import java.io.FileReader;
import java.io.FileWriter;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
/** Файловое хранилище текстовых данных (java.io): рекорды, настройки. */
public final class Storage {
private final File file;
public Storage(String fileName) {
this.file = new File(System.getProperty("user.home"), fileName);
}
public List<String> readLines() throws IOException {
List<String> lines = new ArrayList<>();
if (!file.exists()) return lines;
try (BufferedReader r = new BufferedReader(new FileReader(file))) {
String line;
while ((line = r.readLine()) != null) lines.add(line);
}
return lines;
}
public void writeLines(List<String> lines) throws IOException {
try (BufferedWriter w = new BufferedWriter(new FileWriter(file))) {
for (String line : lines) {
w.write(line);
w.newLine();
}
}
}
public File getFile() { return file; }
}
package engine.fx;
import javafx.animation.AnimationTimer;
import javafx.application.Application;
import javafx.scene.Group;
import javafx.scene.Scene;
import javafx.scene.canvas.Canvas;
import javafx.scene.canvas.GraphicsContext;
import javafx.stage.Stage;
/** Базовый класс игры на JavaFX: окно, холст и игровой цикл AnimationTimer. */
public abstract class FxGame extends Application {
protected int width = 800;
protected int height = 600;
protected String title = "Game";
protected abstract void update(double dt);
protected abstract void render(GraphicsContext g, int width, int height);
@Override
public void start(Stage stage) {
Canvas canvas = new Canvas(width, height);
GraphicsContext g = canvas.getGraphicsContext2D();
Scene scene = new Scene(new Group(canvas));
stage.setTitle(title);
stage.setScene(scene);
stage.setResizable(false);
new AnimationTimer() {
private long last = -1;
@Override
public void handle(long now) {
double dt = last < 0 ? 0 : (now - last) / 1_000_000_000.0;
last = now;
update(dt);
render(g, width, height);
}
}.start();
stage.show();
}
}