我正在尝试用Java制作一个简单的2D游戏。
据我所知,我的游戏应该由两个线程组成:“事件分配线程”(用于GUI操作)和“游戏线程”(用于游戏循环)。
我创建了轮廓,但是找不到放置游戏循环的位置。
简而言之,我试图在不冻结UI线程的情况下创建游戏循环。
如果您能提供有关我做错事情的任何信息,我将不胜感激。
那是我的游戏循环(您也可以提供一些技巧来创建更好的游戏循环):
while(true) {
repaint();
try {
Thread.sleep(17);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
import java.awt.Color;
import java.awt.Graphics;
import java.awt.event.KeyEvent;
import java.awt.event.KeyListener;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.awt.event.MouseListener;
import javax.swing.JFrame;
import javax.swing.JPanel;
import javax.swing.SwingUtilities;
public class Main {
public static void main(String[] args) {
SwingUtilities.invokeLater(new Runnable() {
@Override
public void run() {
createAndShowGUI();
}
});
}
private static void createAndShowGUI() {
JFrame frame = new JFrame("Forge and Attack");
frame.setExtendedState(JFrame.MAXIMIZED_BOTH);
frame.setUndecorated(true);
frame.setVisible(true);
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
frame.setFocusable(true);
frame.add(new MyPanel());
}
}
class MyPanel extends JPanel implements KeyListener, MouseListener {
public MyPanel() {
setBackground(Color.BLACK);
setOpaque(true);
addKeyListener(this);
addMouseListener(new MouseAdapter(){
public void mousePressed(MouseEvent e){
}
});
}
@Override
public void paint(Graphics g) {
}
}
我认为这是一个值得扩展的有趣话题...我已经介绍了您提出的问题,并展示了一种更好或更正确的方法来完成某些工作,例如绘画,聆听按键以及分离等的担忧,并使整个游戏更具可重用性/可扩展性。
1.在哪里放置游戏循环?
因此,这不是直截了当的,并且可能取决于每个人的编码风格,但实际上,我们在此要实现的所有目的是创建游戏循环并在适当的时间启动它。我相信代码说出1000个字(有时可能只是1000个字:)),但是下面是一些代码,这些代码以最小的方式(仍产生有效的工作示例)显示了可以在何处创建/放置游戏循环以及在代码中使用时,为了清楚和理解起见,对代码进行了大量注释:
import java.awt.Dimension;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.event.ActionEvent;
import java.awt.event.KeyEvent;
import java.awt.image.BufferedImage;
import java.io.IOException;
import java.util.ArrayList;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.imageio.ImageIO;
import javax.swing.AbstractAction;
import javax.swing.JComponent;
import javax.swing.JFrame;
import javax.swing.JPanel;
import javax.swing.KeyStroke;
import javax.swing.SwingUtilities;
public class MyGame {
private Scene scene;
private Sprite player;
private Thread gameLoop;
private boolean isRunning;
public MyGame() {
createAndShowUI();
}
public static void main(String[] args) {
SwingUtilities.invokeLater(MyGame::new);
}
/**
* Here we will create our swing UI as well as initialise and setup our
* sprites, scene, and game loop and other buttons etc
*/
private void createAndShowUI() {
JFrame frame = new JFrame("MyGame");
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
player = new Sprite(/*ImageIO.read(getClass().getResourceAsStream("...."))*/);
this.scene = new Scene();
this.scene.add(player);
this.addKeyBindings();
this.setupGameLoop();
frame.add(scene);
frame.pack();
frame.setVisible(true);
// after setting the frame visible we start the game loop, this could be done in a button or wherever you want
this.isRunning = true;
this.gameLoop.start();
}
/**
* This method would actually create and setup the game loop The game loop
* will always be encapsulated in a Thread, Timer or some sort of construct
* that generates a separate thread in order to not block the UI
*/
private void setupGameLoop() {
// initialise the thread
gameLoop = new Thread(() -> {
// while the game "is running" and the isRunning boolean is set to true, loop forever
while (isRunning) {
// here we do 2 very important things which might later be expanded to 3:
// 1. We call Scene#update: this essentially will iterate all of our Sprites in our game and update their movments/position in the game via Sprite#update()
this.scene.update();
// TODO later on one might add a method like this.scene.checkCollisions in which you check if 2 sprites are interesecting and do something about it
// 2. We then call JPanel#repaint() which will cause JPanel#paintComponent to be called and thus we will iterate all of our sprites
// and invoke the Sprite#render method which will draw them to the screen
this.scene.repaint();
// here we throttle our game loop, because we are using a while loop this will execute as fast as it possible can, which might not be needed
// so here we call Thread#slepp so we can give the CPU some time to breathe :)
try {
Thread.sleep(15);
} catch (InterruptedException ex) {
}
}
});
}
private void addKeyBindings() {
// here we would use KeyBindings (https://docs.oracle.com/javase/tutorial/uiswing/misc/keybinding.html) and add them to our Scene/JPanel
// these would allow us to manipulate our Sprite objects using the keyboard below is 2 examples for using the A key to make our player/Sprite go left
// or the D key to make the player/Sprite go to the right
this.scene.getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW).put(KeyStroke.getKeyStroke(KeyEvent.VK_A, 0, false), "A pressed");
this.scene.getActionMap().put("A pressed", new AbstractAction() {
@Override
public void actionPerformed(ActionEvent e) {
player.LEFT = true;
}
});
this.scene.getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW).put(KeyStroke.getKeyStroke(KeyEvent.VK_A, 0, true), "A released");
this.scene.getActionMap().put("A released", new AbstractAction() {
@Override
public void actionPerformed(ActionEvent e) {
player.LEFT = false;
}
});
this.scene.getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW).put(KeyStroke.getKeyStroke(KeyEvent.VK_D, 0, false), "D pressed");
this.scene.getActionMap().put("D pressed", new AbstractAction() {
@Override
public void actionPerformed(ActionEvent e) {
player.RIGHT = true;
}
});
this.scene.getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW).put(KeyStroke.getKeyStroke(KeyEvent.VK_D, 0, true), "D released");
this.scene.getActionMap().put("D released", new AbstractAction() {
@Override
public void actionPerformed(ActionEvent e) {
player.RIGHT = false;
}
});
}
public class Scene extends JPanel {
private final ArrayList<Sprite> sprites;
public Scene() {
// we are using a game loop to repaint, so probably dont want swing randomly doing it for us
this.setIgnoreRepaint(true);
this.sprites = new ArrayList<>();
}
@Override
protected void paintComponent(Graphics g) {
super.paintComponent(g);
Graphics2D g2d = (Graphics2D) g;
// this method gets called on Scene#repaint in our game loop and we then render each in our game
sprites.forEach((sprite) -> {
sprite.render(g2d);
});
}
@Override
public Dimension getPreferredSize() {
// because no components are added to the JPanel, we will have a default sizxe of 0,0 so we instead force the JPanel to a size we want
return new Dimension(500, 500);
}
public void add(Sprite go) {
this.sprites.add(go);
}
private void update() {
// this method gets called on Scene#update in our game loop and we then update the sprites movement and position our game
sprites.forEach((go) -> {
go.update();
});
}
}
public class Sprite {
private int x = 50, y = 50, speed = 5;
//private final BufferedImage image;
public boolean LEFT, RIGHT, UP, DOWN;
public Sprite(/*BufferedImage image*/) {
//this.image = image;
}
public void render(Graphics2D g2d) {
//g2d.drawImage(this.image, this.x, this.y, null);
g2d.fillRect(this.x, this.y, 100, 100);
}
public void update() {
if (LEFT) {
this.x -= this.speed;
}
if (RIGHT) {
this.x += this.speed;
}
if (UP) {
this.y -= this.speed;
}
if (DOWN) {
this.y += this.speed;
}
}
}
}
2.创建更好的游戏循环的技巧
这很像我回答中的第一点,非常主观地取决于您要实现的目标以及对您的问题的满意程度。因此,而不是规定一种游戏循环。让我们看一下我们可以拥有的各种类型:
首先,什么是游戏循环?*
游戏循环是整个游戏程序的整体流程控制。这是一个循环,因为游戏会不断重复执行一系列操作,直到用户退出为止。游戏循环的每次迭代都称为帧。大多数实时游戏每秒更新几次:30和60是两个最常见的间隔。如果游戏以60 FPS(每秒帧数)运行,则意味着游戏循环每秒完成60次迭代。
一种。While循环
我们在上面的示例中已经看到了这一点,它只是一个封装在a中的while循环Thread
,可能包含Thread#sleep
调用以帮助限制CPU使用率。这个和Swing计时器可能是您可以使用的最基本的计时器。
gameLoop = new Thread(() -> {
while (isRunning) {
this.scene.update();
this.scene.repaint();
try {
Thread.sleep(15);
} catch (InterruptedException ex) {
}
}
});
优点:
缺点:
b。摇摆计时器
与while循环类似,可以使用Swing计时器,其中定期触发一个动作事件,因为它是定期触发的,我们可以简单地使用if语句检查游戏是否正在运行,然后调用我们的必要方法
gameLoop = new Timer(15, (ActionEvent e) -> {
if (isRunning) {
MyGame.this.scene.update();
MyGame.this.scene.repaint();
}
});
优点:
缺点:
C。固定时间步长*
这是一个更复杂的游戏循环(但比可变时间步长的游戏循环更简单)。这是在我们要达到特定的FPS(即每秒30或60帧)的前提下进行的,因此,只需确保我们将更新和渲染方法称为每秒精确的次数即可。更新方法不接受“经过的时间”,因为它们假定每次更新都在固定的时间段内进行。计算可以按进行position += distancePerUpdate
。该示例包括渲染期间的插值。
gameLoop = new Thread(() -> {
//This value would probably be stored elsewhere.
final double GAME_HERTZ = 60.0;
//Calculate how many ns each frame should take for our target game hertz.
final double TIME_BETWEEN_UPDATES = 1000000000 / GAME_HERTZ;
//If we are able to get as high as this FPS, don't render again.
final double TARGET_FPS = 60;
final double TARGET_TIME_BETWEEN_RENDERS = 1000000000 / TARGET_FPS;
//At the very most we will update the game this many times before a new render.
//If you're worried about visual hitches more than perfect timing, set this to 1.
final int MAX_UPDATES_BEFORE_RENDER = 5;
//We will need the last update time.
double lastUpdateTime = System.nanoTime();
//Store the last time we rendered.
double lastRenderTime = System.nanoTime();
while (isRunning) {
double now = System.nanoTime();
int updateCount = 0;
//Do as many game updates as we need to, potentially playing catchup.
while (now - lastUpdateTime > TIME_BETWEEN_UPDATES && updateCount < MAX_UPDATES_BEFORE_RENDER) {
MyGame.this.scene.update();
lastUpdateTime += TIME_BETWEEN_UPDATES;
updateCount++;
}
//If for some reason an update takes forever, we don't want to do an insane number of catchups.
//If you were doing some sort of game that needed to keep EXACT time, you would get rid of this.
if (now - lastUpdateTime > TIME_BETWEEN_UPDATES) {
lastUpdateTime = now - TIME_BETWEEN_UPDATES;
}
//Render. To do so, we need to calculate interpolation for a smooth render.
float interpolation = Math.min(1.0f, (float) ((now - lastUpdateTime) / TIME_BETWEEN_UPDATES));
MyGame.this.scene.render(interpolation);
lastRenderTime = now;
//Yield until it has been at least the target time between renders. This saves the CPU from hogging.
while (now - lastRenderTime < TARGET_TIME_BETWEEN_RENDERS && now - lastUpdateTime < TIME_BETWEEN_UPDATES) {
//allow the threading system to play threads that are waiting to run.
Thread.yield();
//This stops the app from consuming all your CPU. It makes this slightly less accurate, but is worth it.
//You can remove this line and it will still work (better), your CPU just climbs on certain OSes.
//FYI on some OS's this can cause pretty bad stuttering. Scroll down and have a look at different peoples' solutions to this.
//On my OS it does not unpuase the game if i take this away
try {
Thread.sleep(1);
} catch (Exception e) {
}
now = System.nanoTime();
}
}
});
此循环将需要进行其他更改以允许插值:
现场:
public class Scene extends JPanel {
private float interpolation;
@Override
protected void paintComponent(Graphics g) {
...
sprites.forEach((sprite) -> {
sprite.render(g2d, this.interpolation);
});
}
public void render(float interpolation) {
this.interpolation = interpolation;
this.repaint();
}
}
雪碧:
public class Sprite {
public void render(Graphics2D g2d, float interpolation) {
g2d.fillRect((int) (this.x + interpolation), (int) (this.y + interpolation), 100, 100);
}
}
优点:
缺点:
d。可变时间步长*
通常在实施物理系统或需要记录经过的时间(即动画)时使用。物理/动画更新传递了“自上次更新以来已经过的时间”参数,因此与帧速率有关。这可能意味着按进行计算position += distancePerSecond * timeElapsed
。
gameLoop = new Thread(() -> {
// how many frames should be drawn in a second
final int FRAMES_PER_SECOND = 60;
// calculate how many nano seconds each frame should take for our target frames per second.
final long TIME_BETWEEN_UPDATES = 1000000000 / FRAMES_PER_SECOND;
// track number of frames
int frameCount;
// if you're worried about visual hitches more than perfect timing, set this to 1. else 5 should be okay
final int MAX_UPDATES_BETWEEN_RENDER = 1;
// we will need the last update time.
long lastUpdateTime = System.nanoTime();
// store the time we started this will be used for updating map and charcter animations
long currTime = System.currentTimeMillis();
while (isRunning) {
long now = System.nanoTime();
long elapsedTime = System.currentTimeMillis() - currTime;
currTime += elapsedTime;
int updateCount = 0;
// do as many game updates as we need to, potentially playing catchup.
while (now - lastUpdateTime >= TIME_BETWEEN_UPDATES && updateCount < MAX_UPDATES_BETWEEN_RENDER) {
MyGame.this.scene.update(elapsedTime);//Update the entity movements and collision checks etc (all has to do with updating the games status i.e call move() on Enitites)
lastUpdateTime += TIME_BETWEEN_UPDATES;
updateCount++;
}
// if for some reason an update takes forever, we don't want to do an insane number of catchups.
// if you were doing some sort of game that needed to keep EXACT time, you would get rid of this.
if (now - lastUpdateTime >= TIME_BETWEEN_UPDATES) {
lastUpdateTime = now - TIME_BETWEEN_UPDATES;
}
MyGame.this.scene.repaint(); // draw call for rendering sprites etc
long lastRenderTime = now;
//Yield until it has been at least the target time between renders. This saves the CPU from hogging.
while (now - lastRenderTime < TIME_BETWEEN_UPDATES && now - lastUpdateTime < TIME_BETWEEN_UPDATES) {
Thread.yield();
now = System.nanoTime();
}
}
});
现场:
public class Scene extends JPanel {
private void update(long elapsedTime) {
// this method gets called on Scene#update in our game loop and we then update the sprites movement and position our game
sprites.forEach((go) -> {
go.update(elapsedTime);
});
}
}
雪碧:
public class Sprite {
private float speed = 0.5f;
public void update(long elapsedTime) {
if (LEFT) {
this.x -= this.speed * elapsedTime;
}
if (RIGHT) {
this.x += this.speed * elapsedTime;
}
if (UP) {
this.y -= this.speed * elapsedTime;
}
if (DOWN) {
this.y += this.speed * elapsedTime;
}
}
}
优点:
缺点:
本文收集自互联网,转载请注明来源。
如有侵权,请联系[email protected] 删除。
我来说两句