我正在编写一款有趣的游戏,其中玩家可以跳过并射击激光的精灵。它最多可以有三名玩家。我的班级Sprite
对于所有三个玩家都是相同的,只是根据玩家#的结构给出了不同的控制布局。 Sprite使用KeyListener
来运行。
为了让我有多个玩家同时做事(比如拍摄激光或跳跃),我需要让每个Sprite
对象在一个单独的线程中创建。我知道我可以在implements Runnable
类上使用Sprite
,但这只会运行新线程上run()
方法中的代码。这不起作用,因为Sprite
有keyPressed()
和其他此类内容不会出现在新主题上。
我想到的是使用"帮助"类implements Runnable
然后在其run()
方法中创建新的Sprite
对象。
然而,这似乎是一种混乱的方法。有没有办法让我在一个全新的线程(Sprite
上创建所有新的KeyListener
个对象,并且所有这些都包含在这个帖子中?
代码:
public class Sprite() implements KeyListener { //I want this class on a brand new thread
int x;
int y;
int width;
int height;
Image spriteImage;
//code/methods for stuff
//key listeners:
@Override
public void keyPressed(KeyEvent arg0) {
// TODO Auto-generated method stub
}
@Override
public void keyReleased(KeyEvent arg0) {
// TODO Auto-generated method stub
}
@Override
public void keyTyped(KeyEvent arg0) {
// TODO Auto-generated method stub
}
}
当前解决方案:
public class SpriteStarter(/* Sprite class parameters go here */) implements Runnable{
void run() {
Sprite s = new Sprite(/*params*/);
}
}
...
class Linker() {
public static void main(String args[]) {
SpriteStarter s1 = new SpriteStarter();
SpriteStarter s2 = new SpriteStarter();
Thread t1 = new Thread(s1);
Thread t2 = new THread(s2);
t1.start();
t2.start();
}
}
修改
好的,经过很多很好的反馈后,对我来说很明显我的游戏应该是一个单线程的东西。我为没有意识到这一点而道歉,我还没有完成很多游戏编程,所以这对我来说是个新事物。我的新想法是有一个ArrayList,在keyPressed()
fires上将按下的键添加到列表中。在Sprite类中,我将使用update()方法查看按下的键并相应地更新坐标。然后将通过java.awt.Timer以固定间隔调用Update()。这似乎对我有用,但我不确定所以让我知道!再次感谢大家。此外,我仍然欣赏原始问题的答案(如何在新线程上启动类的每个实例),因为它可能对将来的程序有所帮助。
答案 0 :(得分:10)
首先让我们直截了当:对象不能在线程上运行。他们真的不会做任何事情。它们位于内存中并等待某个线程执行其方法。这就是为什么你可以有竞争条件。两个线程可能会一次尝试访问同一个内存(可能在同一个对象上)。在你的问题上。
尝试几下并考虑设计。你的输入不是多线程的(至少我是猜测)。事件从操作系统上的某个设备逐个进入您的应用程序(或者,基于您的注释,来自框架抽象,例如窗口面板)。通常,更新精灵只涉及琐碎的数学。这可以在为您提供事件的线程上在线完成。
此外,对于每个新事件(如果你想要做你正在描述的事情),你可能会产生更多的开销,而不是简单地在线执行计算。最重要的是,如果您正在处理一个事件并且有一个新事件进入,那么会发生什么?您需要阻止向每个线程提交事件(使线程无效)或在线程本地队列中排队事件。
但是..娱乐..让我们说更新每个精灵有可能需要很长时间(这很愚蠢,你的游戏无法播放)...
你每个精灵需要一个线程。在每个线程上,您需要一个消息队列。当您启动每个线程时,您将阻止消息队列,直到消息到达。当消息到达时,线程将其从队列中弹出并处理它。您需要在消息中对事件进行编码。消息需要通过值传递到队列中。为简单起见,消息和事件可以是同一个类。
最简单的方法是让一个侦听器用于事件,并让侦听器将相应的事件分派给相关的精灵。但是如果你想让每个sprite都监听它自己的事件,你只需将它们添加到队列中,以便线程处理sprite本身内的sprite事件。
package sexy.multithreaded.sprites;
public class GameDriver implements EventListener {
final EventDispatcher dispatcher;
final Framework framework;
final List<Sprite> sprites;
GameDriver(Framework framework) {
framework.addEventListener(self);
self.framework = framework;
sprites = new ArrayList<>();
dispatcher = new EventDispatcher(sprites);
}
public static void main(String[] args) {
// register for events form your framework
Framework f = new Framework(); // or window or whatever
new GameDriver(f).startGame(Integer.parseInt(args[0]));
}
void startGame(int players) {
// initialize game state
for (int player = 0; player <= players; player++) {
Sprite s = new Sprite(player);
sprites.add(s);
s.start();
}
// and your event processing thread
dispatcher.start();
// loop forever
framework.processEvents();
}
@Override
void onEvent(Event e) {
if (e == Events.Quit) {
dispatcher.interrupt();
} eles {
dispatcher.q.put(e);
}
}
}
class EventDispatcher extends Thread implements Runnable {
// setup a queue for events
final Queue<Event> q;
final List<Sprite> sprites;
EventDispatcher(List<Sprite> sprites) {
super(this, "Event Dispatcher");
this.sprites = sprites;
q = new BlockingQueue<>();
}
@Override
void run() {
while (!interrupted()) {
Event e = q.take();
getSpriteForEvent(e).q.put(e);
}
for (Sprite s : sprites) {
s.interrupt();
}
}
}
class Sprite extends Thread implements Runnable {
final int num;
final Queue<Event> q;
Sprite(int num) {
super(this, "Sprite " + num);
self.num = num;
q = new BlockingQueue<>();
}
@Override
void run() {
while (!interrupted()) {
Event e = q.take();
handle(e);
}
}
void handle(Event e) {
// remember we assumed this takes a really long time..
// but how do I know how to calculate anything?
switch (e) {
case Events.UP:
// try to do something really long...
waitForUpvoteOn("a/35911559/1254812");
break; // (;
...
}
}
}
现在你有新问题需要解决。你的游戏需要一个时钟。需要将事件分组到时间窗口中,这些时间窗口可能会或可能不会直接与帧相关联。当一个事件进来并且精灵仍在处理旧事件时会发生什么?你会取消旧活动的处理还是会丢帧?您还必须管理队列的大小 - 不能产生比您可以消耗的更多事件。
关键是必须有确定游戏状态的真相来源。裁判..如果你愿意的话。事实证明,在单个线程上处理所有事件通常最容易。想一想,如果每个精灵/线程都有一个参考,那么他们仍然必须同步他们各自的世界观。这有效地序列化了游戏逻辑的处理。
让我们添加计时器和绘图:
class GameDriver ... {
static final DELTA = 10; // ms
final Timer timer;
...
GameDriver(...) {
...
timer = new Timer(dispatcher, DELTA);
dispatcher = new EventDispatcher(sprites, f.canvas(), map);
}
void startGame(...) {
...
// and your event processing thread and timer
dispatcher.start();
timer.start();
...
}
@Override
void onEvent(Event e) {
if (e == Events.Quit) {
timer.stop();
dispatcher.interrupt();
} else {
if (!dispatcher.q.offer(e)) {
// Oh no! We're getting more events than we can handle.
// To avoid getting into this situation you can try to:
// 1. de-dupe/coalesce/buffer events
// 2. increase your tick interval (decrease frame rate)
// 3. drop events (I shot you I swear!)
}
}
}
}
class EventDispatcher ... {
// setup a queue for events
final Queue<Event> q;
final Canvas canvas;
EventDispatcher(List<Sprite> sprites, Canvas c) {
super(this, "Event Dispatcher");
q = new BlockingQueue<>();
canvas = c;
}
@Override
void tick() {
for (Sprite s : sprites) {
canvas.push();
s.draw(canvas);
canvas.pop();
}
}
}
class Sprite ... implements Drawable ... {
final Bitmap bitmap;
final Matrix matrix;
...
Sprite(int num) {
...
URL url = Sprite.class.getResource("sprites/player-"+num+".bmp");
bitmap = new Bitmap(url);
matrix = new Matrix();
}
@Override
void draw(Canvas c) {
c.apply(matrix);
c.paint(bitmap);
}
void handle(Event e) {
switch (e) {
case Events.Left:
matrix.translate(GameDriver.DELTA, 0);
break;
case Events.Down:
matrix.translate(0, GameDriver.DELTA);
...
}
}
}
在你为每个精灵切掉无用的线程之后:
class GameDriver ... {
void startGame(...) {
// don't need to start() the sprites anymore..
...
// give sprites a starting position
sprite.matrix.translate(0, player);
}
}
class EventDispatcher extends Thread implements Runnable {
final Map<Matrix, Sprite> map;
...
EventDispatcher(...) {
...
map = new HashMap<>();
}
...
@Override
void tick() {
for (Sprite s : sprites) {
// assuming we gave matrix a map-unique hash function
checkBounds(s);
map.put(s.matrix, s);
}
// process collisions or otherwise apply game logic
applyLogic(map);
map.clear();
// draw the sprites (or use yet another thread)
for (Sprite s : sprites) {
canvas.push();
s.draw(canvas);
canvas.pop();
}
}
@Override
void run() {
try {
while (!interrupted()) {
Event e = q.take();
getSpriteForEvent(e).handle(e)
}
} catch (InterruptedException e) {
} finally {
for (Sprite s : sprites) {
s.interrupt();
}
}
}
...
}
class Sprite implements Drawable {
...
// scratch the run method and thread constructor
...
}
我不会写游戏,所以我可能有些不对劲。
无论如何,有一些要点。回想一下,您的计算机具有固定数量的核心。任何数量的线程大于核心数将意味着您必须在线程之间切换上下文。暂停一个线程,保存其寄存器和堆栈,并加载新线程。这就是您的操作系统的调度程序所做的事情(如果它支持线程,就像大多数一样)。
因此,无数个精灵和/或其他游戏对象,每个都由它自己的线程支持,这只是你可以想象的最差设计。它可以真正降低你的滴答率。
其次,正如我已经提到的那样,为了避免竞争条件(游戏检查sprite的位置,而sprite的线程正在更新它的中间),你必须同步访问sprite数据,无论如何。如果你不能在一个线程上的一个刻度内计算你的逻辑,也许你可以探索有一个工作队列来执行精灵更新。但每个精灵不是一个线程。
这就是为什么,如评论中所建议的,3个主题是一个很好的球场。一个与操作系统接口。一个来处理游戏逻辑。一个渲染图形。 (如果您正在使用Java,请为您的GC线程留出空间。)
另一种思考方式是你的工作是找到一个最小的窗口,你可以在其中处理输入,解决游戏状态,并为整个游戏发出渲染事件。然后,重复一遍又一遍。窗口越小,游戏越流畅,帧速率越高。
最后,我应提及您所寻求的模型实际上是出现在现实世界的游戏设计中,但出于其他原因。想象一下拥有许多客户端和服务器的多人游戏(每个都有效地代表一个新的执行线程)。每个客户端将处理自己的输入事件,批量处理,将其转换为游戏事件,并将游戏事件提供给服务器。网络成为您的序列化层。服务器将淹没事件,解决游戏状态,并将回复发送回客户端。客户端将接受回复,更新其本地状态并进行呈现。但服务器肯定不会为慢客户端等待几帧以上。
Lag sux,我的朋友。