如何通过按键在 Java 中旋转二维矩形?
How do I rotate a 2D rectangle in Java by pressing keys?
我目前正在尝试制作我的第一款简单 java 游戏。到目前为止,我一直在关注某个 Youtube 教程,但我想添加自己的功能,其中之一是能够通过按某个键来旋转播放器。一段时间以来,我一直在研究如何执行此操作,但经过多次失败的尝试后,如果有人可以建议我应该如何执行此操作,我将不胜感激。
这是我的播放器 class,我尝试通过实现 KeyListener 来旋转播放器:
package topDownGame;
import java.awt.Color;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.Rectangle;
import java.awt.event.KeyEvent;
import java.awt.event.KeyListener;
import java.awt.geom.AffineTransform;
import java.awt.geom.Path2D;
import javax.swing.Timer;
public class Player extends GameObject implements KeyListener{
private Handler handler;
private HUD hud;
public float rotation = 0;
public Player(int x, int y, ID id, Handler handler, HUD hud) {
super(x, y, id);
this.handler = handler;
this.hud = hud;
}
public void tick() {
x += velX;
y += velY;
x = Game.clamp(x, 0, Game.WIDTH - 38);
y = Game.clamp(y, 0, Game.HEIGHT - 67);
collision();
}
public void render(Graphics g) {
//g.setColor(Color.WHITE);
//g.fillRect(x, y, 32, 32);
Graphics2D g2d = (Graphics2D)g;
Rectangle r = new Rectangle(x, y, 32, 32);
Path2D.Double path = new Path2D.Double();
path.append(r, false);
AffineTransform t = new AffineTransform();
t.rotate(rotation);
path.transform(t);
g2d.setColor(Color.WHITE);
g2d.draw(path);
}
public void collision() {
for (int i = 0; i < handler.object.size(); i++) {
GameObject tempObject = handler.object.get(i);
if (tempObject.getId() == ID.BasicEnemy) {
if (getBounds().intersects(tempObject.getBounds())) {
hud.HEALTH -= 2;
}
}
}
}
public Rectangle getBounds() {
return new Rectangle(x, y, 32, 32);
}
@Override
public void keyPressed(KeyEvent e) {
int key = e.getKeyCode();
for (int i = 0; i < handler.object.size(); i++) {
GameObject tempObject = handler.object.get(i);
if (tempObject.getId() == ID.Player) {
if (key == KeyEvent.VK_E) {
rotation = (float) (rotation + 0.1);
}
}
}
}
@Override
public void keyReleased(KeyEvent arg0) {
// TODO Auto-generated method stub
}
@Override
public void keyTyped(KeyEvent arg0) {
// TODO Auto-generated method stub
}
}
下面是我剩下的一些可能很重要的代码
游戏class:
package topDownGame;
import java.awt.Canvas;
import java.awt.Color;
import java.awt.Graphics;
import java.awt.event.KeyEvent;
import java.awt.event.KeyListener;
import java.awt.image.BufferStrategy;
public class Game extends Canvas implements Runnable{
/**
*
*/
private static final long serialVersionUID = 1744439430685015162L;
public static final int WIDTH = 640, HEIGHT = WIDTH / 12*9;
private boolean running = false;
private Thread thread;
private Handler handler;
public Game() {
handler = new Handler();
this.addKeyListener(new KeyInput(handler));
new Window(WIDTH, HEIGHT, "Game", this);
handler.addObject(new Player(200, 200, ID.Player, handler, hud));
}
public synchronized void start() {
running = true;
thread = new Thread(this);
thread.start();
}
public synchronized void stop() {
try{
running = false;
thread.join();
}catch(Exception e) {
e.printStackTrace();
}
}
public void run() {
this.requestFocus();
long lastTime = System.nanoTime();
double delta = 0.0;
double amountOfTicks = 60.0;
double ns = 1000000000/amountOfTicks;
long timer = System.currentTimeMillis();
int frames = 0;
while(running) {
long now = System.nanoTime();
delta += (now-lastTime)/ns;
lastTime = now;
while(delta >= 1) {
delta--;
tick();
}
if (running) {
frames++;
render();
}
if (System.currentTimeMillis() - timer > 1000) {
timer += 1000;
System.out.println("FPS: " + frames);
frames = 0;
}
}
stop();
}
public void tick() {
handler.tick();
}
public void render() {
BufferStrategy bs = this.getBufferStrategy();
if (bs == null) {
this.createBufferStrategy(3);
return;
}
Graphics g = bs.getDrawGraphics();
g.setColor(Color.BLACK);
g.fillRect(0, 0, WIDTH, HEIGHT);
handler.render(g);
g.dispose();
bs.show();
}
public static int clamp(int var, int min, int max) {
if (var <= min) {
var = min;
}
if (var >= max) {
var = max;
}
return var;
}
public static void main(String args[]) {
new Game();
}
}
Window class:
package topDownGame;
import java.awt.Canvas;
import java.awt.Dimension;
import javax.swing.JFrame;
public class Window extends Canvas{
/**
*
*/
private static final long serialVersionUID = -8646632868321067448L;
public Window(int width, int height, String title, Game game) {
JFrame jframe = new JFrame(title);
jframe.setMaximumSize(new Dimension(width, height));
jframe.setMinimumSize(new Dimension(width, height));
jframe.setPreferredSize(new Dimension(width, height));
jframe.setVisible(true);
jframe.setDefaultCloseOperation(jframe.EXIT_ON_CLOSE);
jframe.setResizable(false);
jframe.setLocationRelativeTo(null);
jframe.add(game);
game.start();
}
}
游戏对象class:
package topDownGame;
import java.awt.Graphics;
import java.awt.Rectangle;
public abstract class GameObject {
protected int x, y;
protected ID id;
protected int velX, velY;
public GameObject(int x, int y, ID id) {
this.x = x;
this.y = y;
this.id = id;
}
public abstract void tick();
public abstract void render(Graphics g);
public abstract Rectangle getBounds();
public int getX() {
return x;
}
public void setX(int x) {
this.x = x;
}
public int getY() {
return y;
}
public void setY(int y) {
this.y = y;
}
public ID getId() {
return id;
}
public void setId(ID id) {
this.id = id;
}
public int getVelX() {
return velX;
}
public void setVelX(int velX) {
this.velX = velX;
}
public int getVelY() {
return velY;
}
public void setVelY(int velY) {
this.velY = velY;
}
}
处理程序class:
package topDownGame;
import java.awt.Graphics;
import java.util.LinkedList;
public class Handler {
LinkedList <GameObject> object = new LinkedList <GameObject>();
public void tick() {
for (int i = 0; i < object.size(); i++) {
GameObject tempObject = object.get(i);
tempObject.tick();
}
}
public void render(Graphics g) {
for (int i = 0; i < object.size(); i++) {
GameObject tempObject = object.get(i);
tempObject.render(g);
}
}
public void addObject(GameObject object) {
this.object.add(object);
}
public void removeObject(GameObject object) {
this.object.remove(object);
}
public void addObject(int x, int y, ID basicenemy) {
// TODO Auto-generated method stub
}
}
按键输入class:
package topDownGame;
import java.awt.event.KeyAdapter;
import java.awt.event.KeyEvent;
public class KeyInput extends KeyAdapter{
private Handler handler;
public KeyInput(Handler handler) {
this.handler = handler;
}
public void keyPressed(KeyEvent e) {
int key = e.getKeyCode();
for (int i = 0; i < handler.object.size(); i++) {
GameObject tempObject = handler.object.get(i);
if (tempObject.getId() == ID.Player) {
if (key == KeyEvent.VK_W) {
tempObject.setVelY(-5);
}
if (key == KeyEvent.VK_S) {
tempObject.setVelY(5);
}
if (key == KeyEvent.VK_A) {
tempObject.setVelX(-5);
}
if (key == KeyEvent.VK_D) {
tempObject.setVelX(5);
}
}
}
if (key == KeyEvent.VK_SPACE) {
System.exit(1);
}
}
public void keyReleased(KeyEvent e) {
int key = e.getKeyCode();
for (int i = 0; i < handler.object.size(); i++) {
GameObject tempObject = handler.object.get(i);
if (tempObject.getId() == ID.Player) {
if (key == KeyEvent.VK_W) {
tempObject.setVelY(0);
}
if (key == KeyEvent.VK_S) {
tempObject.setVelY(0);
}
if (key == KeyEvent.VK_A) {
tempObject.setVelX(0);
}
if (key == KeyEvent.VK_D) {
tempObject.setVelX(0);
}
}
}
}
}
ID 枚举:
package topDownGame;
public enum ID {
Player();
}
好的,所以我已经仔细研究了这个例子,你遇到的“基本”问题是什么都没有调用 Player
的 keyPressed/Released
方法——事实是,什么都不应该.
目的是,“输入”应该与实体分离,实体应该根据游戏引擎的当前状态更新它们的状态。
因此,我要做的第一件事是“概括”可能发生(并且游戏模型可以响应)的可用“输入操作”
public enum InputAction {
UP, DOWN, LEFT, RIGHT, ROTATE;
}
就是这样。这些是游戏支持且实体可以使用的输入。它们与它们可能发生的“方式”无关,只是提供一种方法。
现在,为了支持这个想法,我们实际上需要以某种方式告诉实体它们应该“更新”,这应该在它们被渲染之前完成,但由于我们试图分离这些操作(所以例如,对象可以比渲染更频繁地更新),我们需要提供一个执行此操作的新方法...
public abstract class GameObject {
//...
public void update() {
}
//...
}
(注意:从技术上讲,此方法可能是 abstract
,因为几乎所有实体都需要以某种方式进行更改,但为了简单起见,我只是将其设为空执行)
接下来,我们需要一些方法让实体响应这些输入操作,并需要一些方法来管理它们,在您的情况下,Handler
可能是最佳选择,因为它提供了一个 link 在实体和系统的其他方面(如渲染和输入控制)之间
public class Handler {
//...
private Set<InputAction> inputActions = new HashSet<InputAction>();
public void render(Graphics g) {
for (int i = 0; i < object.size(); i++) {
GameObject tempObject = object.get(i);
tempObject.update();
tempObject.render(g);
}
}
public boolean is(InputAction action) {
return inputActions.contains(action);
}
public void set(InputAction action) {
inputActions.add(action);
}
public void remove(InputAction action) {
inputActions.remove(action);
}
//...
}
好的,现在“输入机制”可以根据其实现告诉 Handler
状态何时发生变化...
public class KeyInput extends KeyAdapter {
private Handler handler;
public KeyInput(Handler handler) {
this.handler = handler;
}
public void keyPressed(KeyEvent e) {
int key = e.getKeyCode();
if (key == KeyEvent.VK_W) {
handler.set(InputAction.UP);
}
if (key == KeyEvent.VK_S) {
handler.set(InputAction.DOWN);
}
if (key == KeyEvent.VK_A) {
handler.set(InputAction.LEFT);
}
if (key == KeyEvent.VK_D) {
handler.set(InputAction.RIGHT);
}
if (key == KeyEvent.VK_E) {
handler.set(InputAction.ROTATE);
}
}
public void keyReleased(KeyEvent e) {
int key = e.getKeyCode();
if (key == KeyEvent.VK_W) {
handler.remove(InputAction.UP);
}
if (key == KeyEvent.VK_S) {
handler.remove(InputAction.DOWN);
}
if (key == KeyEvent.VK_A) {
handler.remove(InputAction.LEFT);
}
if (key == KeyEvent.VK_D) {
handler.remove(InputAction.RIGHT);
}
if (key == KeyEvent.VK_E) {
handler.remove(InputAction.ROTATE);
}
}
}
(是的,它们可能是 if-else if
语句,但为了简洁起见,我只是修改现有代码)
最后,我们需要更新 Player
对象,以便它可以根据游戏引擎的当前“状态”“更新”它的状态...
public class Player extends GameObject {
private Handler handler;
public float rotation = 0;
public Player(int x, int y, ID id, Handler handler) {//, HUD hud) {
super(x, y, id);
this.handler = handler;
}
@Override
public void update() {
if (handler.is(InputAction.UP)) {
setVelY(-5);
} else if (handler.is(InputAction.DOWN)) {
setVelY(5);
} else {
setVelY(0);
}
if (handler.is(InputAction.LEFT)) {
setVelX(-5);
} else if (handler.is(InputAction.RIGHT)) {
setVelX(5);
} else {
setVelX(0);
}
if (handler.is(InputAction.ROTATE)) {
rotation += 0.1;
}
}
public void tick() {
x += velX;
y += velY;
x = Game.clamp(x, 0, Game.WIDTH - 38);
y = Game.clamp(y, 0, Game.HEIGHT - 67);
collision();
}
public void render(Graphics g) {
//g.setColor(Color.WHITE);
//g.fillRect(x, y, 32, 32);
Graphics2D g2d = (Graphics2D) g.create();
Rectangle r = new Rectangle(0, 0, 32, 32);
Path2D.Double path = new Path2D.Double();
path.append(r, false);
AffineTransform t = new AffineTransform();
t.translate(x, y);
t.rotate(rotation, 16, 16);
path.transform(t);
g2d.setColor(Color.WHITE);
g2d.draw(path);
g2d.dispose();
}
public void collision() {
for (int i = 0; i < handler.object.size(); i++) {
GameObject tempObject = handler.object.get(i);
// if (tempObject.getId() == ID.BasicEnemy) {
// if (getBounds().intersects(tempObject.getBounds())) {
// hud.HEALTH -= 2;
// }
// }
}
}
public Rectangle getBounds() {
return new Rectangle(x, y, 32, 32);
}
}
我想仔细看看 render
方法,因为它有点复杂...
public void render(Graphics g) {
// 1...
Graphics2D g2d = (Graphics2D) g.create();
// 2...
Rectangle r = new Rectangle(0, 0, 32, 32);
Path2D.Double path = new Path2D.Double();
path.append(r, false);
AffineTransform t = new AffineTransform();
// 3...
t.translate(x, y);
// 4...
t.rotate(rotation, 16, 16);
path.transform(t);
g2d.setColor(Color.WHITE);
g2d.draw(path);
// 5...
g2d.dispose();
}
好的:
Graphics
是一个分片概念,这意味着每个需要绘制的实体都将获得相同的 Graphics
上下文,包括之前实体对其所做的任何和所有更改.这种“可能”是可取的,但总的来说,您希望减少可能发生的“副作用”的数量。所以,我们首先创建一个新的副本。
- 我们创建了
Rectangle
。奇怪的是,(现在)这是一个不好的地方,因为它的状态实际上永远不会改变。 Rectangle
总是在 0x0
位置创建,大小为 32x32
...但是等等,我想让它移动并做一些事情!我知道,您会在 ... 中看到“如何”
- 我们将
Graphics
上下文的原点转换为玩家的位置...这使得 0x0
位置与玩家位置相同。这是一个巧妙的作弊手段,正如我上面所说,您不再需要在每次调用 render
时都创建一个 Rectangle
,这将进一步提高性能
- 我们围绕对象的中心点旋转
Graphics
上下文(对象 32x32
使中心点 16x16
- 记住,原点是 0x0
...你明白为什么这个小改变如此重要和有用吗)
- 我们
dispose
的copy。这只会释放此副本持有的所有资源,我们采取的操作仍会应用回原始副本,但不会影响之后可能发生的任何操作(因此原点和旋转与 render
第一次调用)。
观察...
我在研究代码时,很明显代码组织得不好。真正让我恼火的一件事是,Game
会创建一个 Window
的实例来显示自己——这实际上是一种副作用,是 Game
不应该做的事情(它不应该在乎)。
所以,我采用了您的 main
方法并将其争论为...
public static void main(String args) {
EventQueue.invokeLater(new Runnable() {
@Override
public void run() {
JFrame jframe = new JFrame("Game");
Game game = new Game();
jframe.setDefaultCloseOperation(jframe.EXIT_ON_CLOSE);
jframe.add(game);
jframe.pack();
jframe.setLocationRelativeTo(null);
jframe.setVisible(true);
game.start();
}
});
}
所以,一些小的变化...
- 几乎立即将
game
的实例添加到框架中(这对我所做的另一项更改很重要)
- 框架被“打包”在组件周围
- 框架的位置已设置(因此它出现在屏幕中间),这是在打包 window 之后完成的,因为在打包之前不会设置框架的大小。
- 框架可见 - 这会阻止 window“跳”到屏幕中央
我也加了...
@Override
public Dimension getPreferredSize() {
return new Dimension(WIDTH, HEIGHT);
}
到 Game
,这为添加了 Game
的容器提供大小调整提示。这也意味着当 JFrame
被 pack
编辑时,window 将略大于内容区域,因为框架的边框环绕着它。
我还建议您看一下 JavaDocs for BufferStrategy
,因为它有一个“如何”使用它的示例。
为此,我相应地修改了您的 render
方法...
public void render() {
BufferStrategy bs = this.getBufferStrategy();
if (bs == null) {
this.createBufferStrategy(3);
return;
}
do {
do {
Graphics g = bs.getDrawGraphics();
g.setColor(Color.BLACK);
g.fillRect(0, 0, getWidth(), getHeight());
handler.render(g);
g.dispose();
} while (bs.contentsRestored());
bs.show();
} while (bs.contentsLost());
}
我做的一个重大改变是 g.fillRect(0, 0, getWidth(), getHeight());
- 现在它将填充组件的“实际”大小,而不仅仅是“所需”大小......我是那些讨厌的人之一(热情地)不可调整大小 windows ;)
如果您有兴趣看到一个稍微复杂的解决方案,您可以选择 look at this example,它提出了一个更“通用”和“解耦”的概念
我目前正在尝试制作我的第一款简单 java 游戏。到目前为止,我一直在关注某个 Youtube 教程,但我想添加自己的功能,其中之一是能够通过按某个键来旋转播放器。一段时间以来,我一直在研究如何执行此操作,但经过多次失败的尝试后,如果有人可以建议我应该如何执行此操作,我将不胜感激。
这是我的播放器 class,我尝试通过实现 KeyListener 来旋转播放器:
package topDownGame;
import java.awt.Color;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.Rectangle;
import java.awt.event.KeyEvent;
import java.awt.event.KeyListener;
import java.awt.geom.AffineTransform;
import java.awt.geom.Path2D;
import javax.swing.Timer;
public class Player extends GameObject implements KeyListener{
private Handler handler;
private HUD hud;
public float rotation = 0;
public Player(int x, int y, ID id, Handler handler, HUD hud) {
super(x, y, id);
this.handler = handler;
this.hud = hud;
}
public void tick() {
x += velX;
y += velY;
x = Game.clamp(x, 0, Game.WIDTH - 38);
y = Game.clamp(y, 0, Game.HEIGHT - 67);
collision();
}
public void render(Graphics g) {
//g.setColor(Color.WHITE);
//g.fillRect(x, y, 32, 32);
Graphics2D g2d = (Graphics2D)g;
Rectangle r = new Rectangle(x, y, 32, 32);
Path2D.Double path = new Path2D.Double();
path.append(r, false);
AffineTransform t = new AffineTransform();
t.rotate(rotation);
path.transform(t);
g2d.setColor(Color.WHITE);
g2d.draw(path);
}
public void collision() {
for (int i = 0; i < handler.object.size(); i++) {
GameObject tempObject = handler.object.get(i);
if (tempObject.getId() == ID.BasicEnemy) {
if (getBounds().intersects(tempObject.getBounds())) {
hud.HEALTH -= 2;
}
}
}
}
public Rectangle getBounds() {
return new Rectangle(x, y, 32, 32);
}
@Override
public void keyPressed(KeyEvent e) {
int key = e.getKeyCode();
for (int i = 0; i < handler.object.size(); i++) {
GameObject tempObject = handler.object.get(i);
if (tempObject.getId() == ID.Player) {
if (key == KeyEvent.VK_E) {
rotation = (float) (rotation + 0.1);
}
}
}
}
@Override
public void keyReleased(KeyEvent arg0) {
// TODO Auto-generated method stub
}
@Override
public void keyTyped(KeyEvent arg0) {
// TODO Auto-generated method stub
}
}
下面是我剩下的一些可能很重要的代码
游戏class:
package topDownGame;
import java.awt.Canvas;
import java.awt.Color;
import java.awt.Graphics;
import java.awt.event.KeyEvent;
import java.awt.event.KeyListener;
import java.awt.image.BufferStrategy;
public class Game extends Canvas implements Runnable{
/**
*
*/
private static final long serialVersionUID = 1744439430685015162L;
public static final int WIDTH = 640, HEIGHT = WIDTH / 12*9;
private boolean running = false;
private Thread thread;
private Handler handler;
public Game() {
handler = new Handler();
this.addKeyListener(new KeyInput(handler));
new Window(WIDTH, HEIGHT, "Game", this);
handler.addObject(new Player(200, 200, ID.Player, handler, hud));
}
public synchronized void start() {
running = true;
thread = new Thread(this);
thread.start();
}
public synchronized void stop() {
try{
running = false;
thread.join();
}catch(Exception e) {
e.printStackTrace();
}
}
public void run() {
this.requestFocus();
long lastTime = System.nanoTime();
double delta = 0.0;
double amountOfTicks = 60.0;
double ns = 1000000000/amountOfTicks;
long timer = System.currentTimeMillis();
int frames = 0;
while(running) {
long now = System.nanoTime();
delta += (now-lastTime)/ns;
lastTime = now;
while(delta >= 1) {
delta--;
tick();
}
if (running) {
frames++;
render();
}
if (System.currentTimeMillis() - timer > 1000) {
timer += 1000;
System.out.println("FPS: " + frames);
frames = 0;
}
}
stop();
}
public void tick() {
handler.tick();
}
public void render() {
BufferStrategy bs = this.getBufferStrategy();
if (bs == null) {
this.createBufferStrategy(3);
return;
}
Graphics g = bs.getDrawGraphics();
g.setColor(Color.BLACK);
g.fillRect(0, 0, WIDTH, HEIGHT);
handler.render(g);
g.dispose();
bs.show();
}
public static int clamp(int var, int min, int max) {
if (var <= min) {
var = min;
}
if (var >= max) {
var = max;
}
return var;
}
public static void main(String args[]) {
new Game();
}
}
Window class:
package topDownGame;
import java.awt.Canvas;
import java.awt.Dimension;
import javax.swing.JFrame;
public class Window extends Canvas{
/**
*
*/
private static final long serialVersionUID = -8646632868321067448L;
public Window(int width, int height, String title, Game game) {
JFrame jframe = new JFrame(title);
jframe.setMaximumSize(new Dimension(width, height));
jframe.setMinimumSize(new Dimension(width, height));
jframe.setPreferredSize(new Dimension(width, height));
jframe.setVisible(true);
jframe.setDefaultCloseOperation(jframe.EXIT_ON_CLOSE);
jframe.setResizable(false);
jframe.setLocationRelativeTo(null);
jframe.add(game);
game.start();
}
}
游戏对象class:
package topDownGame;
import java.awt.Graphics;
import java.awt.Rectangle;
public abstract class GameObject {
protected int x, y;
protected ID id;
protected int velX, velY;
public GameObject(int x, int y, ID id) {
this.x = x;
this.y = y;
this.id = id;
}
public abstract void tick();
public abstract void render(Graphics g);
public abstract Rectangle getBounds();
public int getX() {
return x;
}
public void setX(int x) {
this.x = x;
}
public int getY() {
return y;
}
public void setY(int y) {
this.y = y;
}
public ID getId() {
return id;
}
public void setId(ID id) {
this.id = id;
}
public int getVelX() {
return velX;
}
public void setVelX(int velX) {
this.velX = velX;
}
public int getVelY() {
return velY;
}
public void setVelY(int velY) {
this.velY = velY;
}
}
处理程序class:
package topDownGame;
import java.awt.Graphics;
import java.util.LinkedList;
public class Handler {
LinkedList <GameObject> object = new LinkedList <GameObject>();
public void tick() {
for (int i = 0; i < object.size(); i++) {
GameObject tempObject = object.get(i);
tempObject.tick();
}
}
public void render(Graphics g) {
for (int i = 0; i < object.size(); i++) {
GameObject tempObject = object.get(i);
tempObject.render(g);
}
}
public void addObject(GameObject object) {
this.object.add(object);
}
public void removeObject(GameObject object) {
this.object.remove(object);
}
public void addObject(int x, int y, ID basicenemy) {
// TODO Auto-generated method stub
}
}
按键输入class:
package topDownGame;
import java.awt.event.KeyAdapter;
import java.awt.event.KeyEvent;
public class KeyInput extends KeyAdapter{
private Handler handler;
public KeyInput(Handler handler) {
this.handler = handler;
}
public void keyPressed(KeyEvent e) {
int key = e.getKeyCode();
for (int i = 0; i < handler.object.size(); i++) {
GameObject tempObject = handler.object.get(i);
if (tempObject.getId() == ID.Player) {
if (key == KeyEvent.VK_W) {
tempObject.setVelY(-5);
}
if (key == KeyEvent.VK_S) {
tempObject.setVelY(5);
}
if (key == KeyEvent.VK_A) {
tempObject.setVelX(-5);
}
if (key == KeyEvent.VK_D) {
tempObject.setVelX(5);
}
}
}
if (key == KeyEvent.VK_SPACE) {
System.exit(1);
}
}
public void keyReleased(KeyEvent e) {
int key = e.getKeyCode();
for (int i = 0; i < handler.object.size(); i++) {
GameObject tempObject = handler.object.get(i);
if (tempObject.getId() == ID.Player) {
if (key == KeyEvent.VK_W) {
tempObject.setVelY(0);
}
if (key == KeyEvent.VK_S) {
tempObject.setVelY(0);
}
if (key == KeyEvent.VK_A) {
tempObject.setVelX(0);
}
if (key == KeyEvent.VK_D) {
tempObject.setVelX(0);
}
}
}
}
}
ID 枚举:
package topDownGame;
public enum ID {
Player();
}
好的,所以我已经仔细研究了这个例子,你遇到的“基本”问题是什么都没有调用 Player
的 keyPressed/Released
方法——事实是,什么都不应该.
目的是,“输入”应该与实体分离,实体应该根据游戏引擎的当前状态更新它们的状态。
因此,我要做的第一件事是“概括”可能发生(并且游戏模型可以响应)的可用“输入操作”
public enum InputAction {
UP, DOWN, LEFT, RIGHT, ROTATE;
}
就是这样。这些是游戏支持且实体可以使用的输入。它们与它们可能发生的“方式”无关,只是提供一种方法。
现在,为了支持这个想法,我们实际上需要以某种方式告诉实体它们应该“更新”,这应该在它们被渲染之前完成,但由于我们试图分离这些操作(所以例如,对象可以比渲染更频繁地更新),我们需要提供一个执行此操作的新方法...
public abstract class GameObject {
//...
public void update() {
}
//...
}
(注意:从技术上讲,此方法可能是 abstract
,因为几乎所有实体都需要以某种方式进行更改,但为了简单起见,我只是将其设为空执行)
接下来,我们需要一些方法让实体响应这些输入操作,并需要一些方法来管理它们,在您的情况下,Handler
可能是最佳选择,因为它提供了一个 link 在实体和系统的其他方面(如渲染和输入控制)之间
public class Handler {
//...
private Set<InputAction> inputActions = new HashSet<InputAction>();
public void render(Graphics g) {
for (int i = 0; i < object.size(); i++) {
GameObject tempObject = object.get(i);
tempObject.update();
tempObject.render(g);
}
}
public boolean is(InputAction action) {
return inputActions.contains(action);
}
public void set(InputAction action) {
inputActions.add(action);
}
public void remove(InputAction action) {
inputActions.remove(action);
}
//...
}
好的,现在“输入机制”可以根据其实现告诉 Handler
状态何时发生变化...
public class KeyInput extends KeyAdapter {
private Handler handler;
public KeyInput(Handler handler) {
this.handler = handler;
}
public void keyPressed(KeyEvent e) {
int key = e.getKeyCode();
if (key == KeyEvent.VK_W) {
handler.set(InputAction.UP);
}
if (key == KeyEvent.VK_S) {
handler.set(InputAction.DOWN);
}
if (key == KeyEvent.VK_A) {
handler.set(InputAction.LEFT);
}
if (key == KeyEvent.VK_D) {
handler.set(InputAction.RIGHT);
}
if (key == KeyEvent.VK_E) {
handler.set(InputAction.ROTATE);
}
}
public void keyReleased(KeyEvent e) {
int key = e.getKeyCode();
if (key == KeyEvent.VK_W) {
handler.remove(InputAction.UP);
}
if (key == KeyEvent.VK_S) {
handler.remove(InputAction.DOWN);
}
if (key == KeyEvent.VK_A) {
handler.remove(InputAction.LEFT);
}
if (key == KeyEvent.VK_D) {
handler.remove(InputAction.RIGHT);
}
if (key == KeyEvent.VK_E) {
handler.remove(InputAction.ROTATE);
}
}
}
(是的,它们可能是 if-else if
语句,但为了简洁起见,我只是修改现有代码)
最后,我们需要更新 Player
对象,以便它可以根据游戏引擎的当前“状态”“更新”它的状态...
public class Player extends GameObject {
private Handler handler;
public float rotation = 0;
public Player(int x, int y, ID id, Handler handler) {//, HUD hud) {
super(x, y, id);
this.handler = handler;
}
@Override
public void update() {
if (handler.is(InputAction.UP)) {
setVelY(-5);
} else if (handler.is(InputAction.DOWN)) {
setVelY(5);
} else {
setVelY(0);
}
if (handler.is(InputAction.LEFT)) {
setVelX(-5);
} else if (handler.is(InputAction.RIGHT)) {
setVelX(5);
} else {
setVelX(0);
}
if (handler.is(InputAction.ROTATE)) {
rotation += 0.1;
}
}
public void tick() {
x += velX;
y += velY;
x = Game.clamp(x, 0, Game.WIDTH - 38);
y = Game.clamp(y, 0, Game.HEIGHT - 67);
collision();
}
public void render(Graphics g) {
//g.setColor(Color.WHITE);
//g.fillRect(x, y, 32, 32);
Graphics2D g2d = (Graphics2D) g.create();
Rectangle r = new Rectangle(0, 0, 32, 32);
Path2D.Double path = new Path2D.Double();
path.append(r, false);
AffineTransform t = new AffineTransform();
t.translate(x, y);
t.rotate(rotation, 16, 16);
path.transform(t);
g2d.setColor(Color.WHITE);
g2d.draw(path);
g2d.dispose();
}
public void collision() {
for (int i = 0; i < handler.object.size(); i++) {
GameObject tempObject = handler.object.get(i);
// if (tempObject.getId() == ID.BasicEnemy) {
// if (getBounds().intersects(tempObject.getBounds())) {
// hud.HEALTH -= 2;
// }
// }
}
}
public Rectangle getBounds() {
return new Rectangle(x, y, 32, 32);
}
}
我想仔细看看 render
方法,因为它有点复杂...
public void render(Graphics g) {
// 1...
Graphics2D g2d = (Graphics2D) g.create();
// 2...
Rectangle r = new Rectangle(0, 0, 32, 32);
Path2D.Double path = new Path2D.Double();
path.append(r, false);
AffineTransform t = new AffineTransform();
// 3...
t.translate(x, y);
// 4...
t.rotate(rotation, 16, 16);
path.transform(t);
g2d.setColor(Color.WHITE);
g2d.draw(path);
// 5...
g2d.dispose();
}
好的:
Graphics
是一个分片概念,这意味着每个需要绘制的实体都将获得相同的Graphics
上下文,包括之前实体对其所做的任何和所有更改.这种“可能”是可取的,但总的来说,您希望减少可能发生的“副作用”的数量。所以,我们首先创建一个新的副本。- 我们创建了
Rectangle
。奇怪的是,(现在)这是一个不好的地方,因为它的状态实际上永远不会改变。Rectangle
总是在0x0
位置创建,大小为32x32
...但是等等,我想让它移动并做一些事情!我知道,您会在 ... 中看到“如何”
- 我们将
Graphics
上下文的原点转换为玩家的位置...这使得0x0
位置与玩家位置相同。这是一个巧妙的作弊手段,正如我上面所说,您不再需要在每次调用render
时都创建一个Rectangle
,这将进一步提高性能 - 我们围绕对象的中心点旋转
Graphics
上下文(对象32x32
使中心点16x16
- 记住,原点是0x0
...你明白为什么这个小改变如此重要和有用吗) - 我们
dispose
的copy。这只会释放此副本持有的所有资源,我们采取的操作仍会应用回原始副本,但不会影响之后可能发生的任何操作(因此原点和旋转与render
第一次调用)。
观察...
我在研究代码时,很明显代码组织得不好。真正让我恼火的一件事是,Game
会创建一个 Window
的实例来显示自己——这实际上是一种副作用,是 Game
不应该做的事情(它不应该在乎)。
所以,我采用了您的 main
方法并将其争论为...
public static void main(String args) {
EventQueue.invokeLater(new Runnable() {
@Override
public void run() {
JFrame jframe = new JFrame("Game");
Game game = new Game();
jframe.setDefaultCloseOperation(jframe.EXIT_ON_CLOSE);
jframe.add(game);
jframe.pack();
jframe.setLocationRelativeTo(null);
jframe.setVisible(true);
game.start();
}
});
}
所以,一些小的变化...
- 几乎立即将
game
的实例添加到框架中(这对我所做的另一项更改很重要) - 框架被“打包”在组件周围
- 框架的位置已设置(因此它出现在屏幕中间),这是在打包 window 之后完成的,因为在打包之前不会设置框架的大小。
- 框架可见 - 这会阻止 window“跳”到屏幕中央
我也加了...
@Override
public Dimension getPreferredSize() {
return new Dimension(WIDTH, HEIGHT);
}
到 Game
,这为添加了 Game
的容器提供大小调整提示。这也意味着当 JFrame
被 pack
编辑时,window 将略大于内容区域,因为框架的边框环绕着它。
我还建议您看一下 JavaDocs for BufferStrategy
,因为它有一个“如何”使用它的示例。
为此,我相应地修改了您的 render
方法...
public void render() {
BufferStrategy bs = this.getBufferStrategy();
if (bs == null) {
this.createBufferStrategy(3);
return;
}
do {
do {
Graphics g = bs.getDrawGraphics();
g.setColor(Color.BLACK);
g.fillRect(0, 0, getWidth(), getHeight());
handler.render(g);
g.dispose();
} while (bs.contentsRestored());
bs.show();
} while (bs.contentsLost());
}
我做的一个重大改变是 g.fillRect(0, 0, getWidth(), getHeight());
- 现在它将填充组件的“实际”大小,而不仅仅是“所需”大小......我是那些讨厌的人之一(热情地)不可调整大小 windows ;)
如果您有兴趣看到一个稍微复杂的解决方案,您可以选择 look at this example,它提出了一个更“通用”和“解耦”的概念