意图
本来想在 Java
课堂上展示一下如何绘图,用来形象说明面向对象编程的一些概念。 之前一直用的 Processing,但是很多学生在刚开始学 Java
时会把两个程序混淆。这和同时学习汉语拼音和英文差不多。如果直接使用 processing.core
项目,则需要额外引入类似 gradle
的项目管理内容,需要讲太多无关的东西。因此就想纯粹基于 Java
写一个绘图框架。尽量模仿 Processing
的 API 风格。
本项目不依赖任何其他的库,只需要完整的 jdk
安装即可。
引擎内容
引擎分为入口:App.java
,绘图:GamePanel.java
,鼠标事件:MouseHandler.java
和键盘事件 KeyHandler.java
四个部分。
入口
我们以 App
类为入口。
其中包含了绘图相关接口、鼠标和键盘的相关事件。
package engine;
import javax.swing.JFrame;
import java.awt.*;
import java.util.Random;
public class App {
public App(String title, int width, int height) {
this.title = title;
this.width = width;
this.height = height;
gamePanel = new GamePanel(this);
gamePanel.setPreferredSize(new Dimension(width, height));
rand = new Random();
}
public void draw() {}
public void update(double delta) {}
public int getRandomInt(int left, int right) {
return rand.nextInt(left, right + 1);
}
public double getRandom() {
return rand.nextDouble();
}
public void run() {
window = new JFrame();
window.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
window.setResizable(false);
window.setTitle(title);
window.add(gamePanel);
window.pack();
window.setLocationRelativeTo(null);
window.setVisible(true);
gamePanel.startGameThread();
}
public Dimension getSize() {
return window.getSize();
}
public int getWidth() {
return width;
}
public int getHeight() {
return height;
}
public void setColor(Color c) {
if (g == null) return;
g.setColor(c);
}
public void setStrokeColor(Color c) {
if (g == null) return;
strokeColor = c;
}
public void setStrokeWidth(int w) {
strokeWidth = w;
}
public void rect(int x, int y, int width, int height) {
if (g == null) return;
g.fillRect(x, y, width, height);
Color c = g.getColor();
g.setColor(strokeColor);
g.setStroke(new BasicStroke(strokeWidth));
g.drawRect(x, y, width, height);
g.setColor(c);
}
public void ellipse(int x, int y, int width, int height) {
if (g == null) return;
g.fillOval(x, y, width, height);
Color c = g.getColor();
g.setColor(strokeColor);
g.setStroke(new BasicStroke(strokeWidth));
g.drawOval(x, y, width, height);
g.setColor(c);
}
public void circle(int x, int y, int radius) {
ellipse(x - radius, y - radius, radius * 2, radius * 2);
}
public void background(Color c) {
if (g == null) return;
g.clearRect(0, 0, width, height);
g.setColor(c);
g.fillRect(0, 0, width, height);
}
public void setFPS(int fps) {
this.fps = fps;
}
public boolean isKeyDown() {
return gamePanel.keyHandler.isKeyDown();
}
public boolean isKeyPressed(int keycode) {
return gamePanel.keyHandler.isKeyPressed(keycode);
}
public void mouseClicked() {}
public void mousePressed() {}
public void mouseReleased() {}
public double mouseX() {
return gamePanel.mouseX;
}
public double mouseY() {
return gamePanel.mouseY;
}
public double prevMouseX() {
return gamePanel.prevMouseX;
}
public double prevMouseY() {
return gamePanel.prevMouseY;
}
private String title;
private int width, height;
private JFrame window;
GamePanel gamePanel;
Graphics2D g;
private int strokeWidth = 0;
private Color strokeColor = Color.BLACK;
int fps = 60;
private Random rand;
}
绘图接口
GamePanel
是控制绘图的地方,其内容如下:
package engine;
import java.awt.*;
import javax.swing.JPanel;
public class GamePanel extends JPanel implements Runnable {
private Thread gameThread;
private App app;
KeyHandler keyHandler;
MouseHandler mouseHandler;
double mouseX, mouseY, prevMouseX, prevMouseY;
public GamePanel(App app) {
this.app = app;
keyHandler = new KeyHandler();
mouseHandler = new MouseHandler(app);
addMouseListener(mouseHandler);
addMouseMotionListener(mouseHandler);
setDoubleBuffered(true);
addKeyListener(keyHandler);
setFocusable(true);
}
public void paintComponent(Graphics g) {
super.paintComponent(g);
app.g = (Graphics2D)g;
app.draw();
}
@Override
public void run() {
setPreferredSize(app.getSize());
double drawInterval;
double nextDrawTime;
while (gameThread != null) {
drawInterval = 1e9 / app.fps;
nextDrawTime = System.nanoTime() + drawInterval;
prevMouseX = mouseX;
prevMouseY = mouseY;
app.update(drawInterval / 1e9);
repaint();
try {
double remainingTime = nextDrawTime - System.nanoTime();
if (remainingTime < 0) {
remainingTime = 0;
} else {
remainingTime /= 1e6;
}
Thread.sleep((long) remainingTime);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public void startGameThread() {
gameThread = new Thread(this);
gameThread.start();
}
public void setMousePosition(double x, double y) {
mouseX = x;
mouseY = y;
}
}
鼠标事件
package engine;
import java.awt.event.MouseEvent;
import java.awt.event.MouseListener;
import java.awt.event.MouseMotionListener;
public class MouseHandler implements MouseListener, MouseMotionListener{
App app;
public MouseHandler(App app) {
this.app = app;
}
@Override
public void mouseClicked(MouseEvent e) {
app.mouseClicked();
}
@Override
public void mousePressed(MouseEvent e) {
app.mousePressed();
}
@Override
public void mouseReleased(MouseEvent e) {
app.mouseReleased();
}
@Override
public void mouseEntered(MouseEvent e) {
}
@Override
public void mouseExited(MouseEvent e) {
}
@Override
public void mouseDragged(MouseEvent e) {
updateMousePosition(e);
}
@Override
public void mouseMoved(MouseEvent e) {
updateMousePosition(e);
}
private void updateMousePosition(MouseEvent e) {
app.gamePanel.setMousePosition(e.getX(), e.getY());
}
}
键盘事件
package engine;
import java.awt.event.MouseEvent;
import java.awt.event.MouseListener;
import java.awt.event.MouseMotionListener;
public class MouseHandler implements MouseListener, MouseMotionListener{
App app;
public MouseHandler(App app) {
this.app = app;
}
@Override
public void mouseClicked(MouseEvent e) {
app.mouseClicked();
}
@Override
public void mousePressed(MouseEvent e) {
app.mousePressed();
}
@Override
public void mouseReleased(MouseEvent e) {
app.mouseReleased();
}
@Override
public void mouseEntered(MouseEvent e) {
}
@Override
public void mouseExited(MouseEvent e) {
}
@Override
public void mouseDragged(MouseEvent e) {
updateMousePosition(e);
}
@Override
public void mouseMoved(MouseEvent e) {
updateMousePosition(e);
}
private void updateMousePosition(MouseEvent e) {
app.gamePanel.setMousePosition(e.getX(), e.getY());
}
}
应用案例
一般来说,使用这个引擎,需要继承 App
,并按照需求重写。
Sketch
import java.awt.Color;
// import java.awt.event.KeyEvent;
import engine.App;
public class Sketch extends App {
public Sketch() {
super("My Sketch", 500, 500);
ball = new Ball(this, 100, 100, 50, Color.RED);
}
@Override
public void draw() {
background(Color.WHITE);
ball.draw();
}
@Override
public void update(double delta) {
ball.update(delta);
}
@Override
public void mousePressed() {
double dx = mouseX() - ball.x;
double dy = mouseY() - ball.y;
if (dx * dx + dy * dy < ball.radius * ball.radius)
ball.isActive = false;
}
@Override
public void mouseReleased() {
if (ball.isActive) return;
ball.isActive = true;
ball.velx = 10 * (mouseX() - prevMouseX());
ball.vely = 10 * (mouseY() - prevMouseY());
}
Ball ball;
}
自定义 Sprite
在游戏引擎中,可以自己运动的物体称为 Sprite
,我们在这里自定义一个小球:
import java.awt.Color;
import engine.App;
public class Ball {
Ball(App app, int x, int y, int radius, Color c) {
this.app = app;
this.x = x;
this.y = y;
this.radius = radius;
this.c = c;
velx = app.getRandomInt(-200, 200);
velx = app.getRandomInt(-200, 200);
isActive = true;
}
public void draw() {
app.setColor(c);
app.setStrokeWidth(1);
app.circle((int)x, (int)y, (int)radius);
}
public void update(double delta) {
if (!isActive) {
x = (float)app.mouseX();
y = (float)app.mouseY();
return;
}
if (x + radius > app.getWidth()) {
x = app.getWidth() - radius;
velx *= -0.9;
} else if (x - radius < 0) {
x = radius;
velx *= -0.9;
} else if (y + radius > app.getHeight()) {
y = app.getHeight() - radius;
vely *= -0.85;
} else if (y - radius < 0) {
y = radius;
vely *= -1;
}
vely += 270 * delta;
x += velx * delta;
y += vely * delta;
}
double x, y;
double radius;
private Color c;
private App app;
double velx, vely;
boolean isActive = true;
}
主程序
主程序内容如下:
public class Main {
public static void main(String[] args) {
Sketch app = new Sketch();
app.run();
}
}
程序目录结构为:
|- engine
| |- App.java
| |- GamePanel.java
| |- KeyHandler.java
| |- MouseHandler.java
|- Ball.java
|- Main.java
|- Sketch.java