所以,我正在开发一款游戏引擎,并取得了不错的进展。但是,我的引擎是单线程的,将更新和渲染拆分为单独的线程的优点听起来是一个非常好的主意。
我该怎么做?单线程游戏引擎(概念上)非常容易制作,你有一个更新的循环 - >渲染 - >睡觉 - >重复。但是,我想不出一个打破更新和渲染分离的好方法,特别是如果我改变他们的更新速率(比如我经历更新循环25x一秒,并且有60fps用于渲染) - 如果我开始中途更新怎么办?通过渲染循环,反之亦然?
答案 0 :(得分:6)
将更新逻辑放在某种Updater
工人类中(实现Runnable
),并将渲染器放入单独的工作类中。当您需要更新数据时,让Updater将该更新放入Updater和Producer共享的队列中。最方便的是使用已经具有内置多线程支持的队列,如BlockingQueue
的子类。例如代码,请参阅BlockingQueue
的javadoc。
如果您需要渲染所有更改,甚至是过时的更改,使用队列是很自然的。如果您只想渲染最新的更改,请使用ConcurrentHashMap
而不是队列。
不要忘记让更新成为不可变对象,因此在渲染时更新不会发生变化。
正如Nirmal指出的那样,您可以使用某种线程池来限制线程数并简化线程的启动/停止。请参阅JDK中的Executor
接口和Executors
实用程序类,以查看此处的可用选项。
答案 1 :(得分:4)
一个不错的实现是在更新线程和呈现线程之间几乎不需要同步的实现。困难在于一个线程以不同的速度运行(很可能)。看看
http://blog.slapware.eu/game-engine/programming/multithreaded-renderloop-part1/ http://blog.slapware.eu/game-engine/programming/multithreaded-renderloop-part2/ http://blog.slapware.eu/game-engine/programming/multithreaded-renderloop-part3/ http://blog.slapware.eu/game-engine/programming/multithreaded-renderloop-part4/
如何实现这一目标。该网站提供了解释和实施(来源+二进制文件)。
答案 2 :(得分:1)
我建议使用这种架构进行管道传输,这意味着渲染阶段将渲染前一帧上更新的所有元素,它将如下所示:
更新0
更新1渲染0
更新2渲染1
更新3渲染2
...
这意味着您的游戏将使用更多内存,并且所有对象都必须具有每帧状态/数据
如果你在这个管道中引入更多图层,你的游戏会受到输入延迟的影响(意味着用户会在屏幕后看到他的动作然后正常),所以我建议只使用这个2阶段管道
答案 3 :(得分:0)
为每个类别创建pojo
,一个runnable对象包含数据,如fps率,UI屏幕类n所有需要的信息,你可以使公共信息单例,所以每次渲染开始线程进行更新时,我推荐线程池保持内存消耗限制
答案 4 :(得分:0)
另请注意,您的绘制线程永远不会比您的更新线程运行得更快。因为如果您的更新线程尚未完成当前步骤,您将绘制与之前相同的内容。在执行此操作时,您可能会错过更新步骤的完成,这最终会导致低于最佳帧速率。
(请记住绘制与以前完全相同的图片不会让任何人受益)。
答案 5 :(得分:0)
我使用三个线程制作了我的程序(虽然可以使用更多)。
更新逻辑(数据收集和预处理)
辅助线程(在无限睡眠1ms循环时计算耗时的缓存预计算等...所以这个线程不关心更新逻辑的去向,或者有多快。它只是检查它的去向并计算iF需要缓存新项目)
渲染线程(仅进行渲染,渲染所需的一切都经过预处理,因此它只绘制函数并计算屏幕位置)
如果您正在绘制“线程安全”项目,那么这样做非常简单。但是在游戏中,我个人认为,如果你将玩家1提前一个比玩家2更好,那也不是坏事...因为你仍然希望在游戏中尽可能快地画出来。游戏逻辑线程确保没有逻辑异常...所以通常我认为你画的是什么并不重要,你只需要尽可能快地做到而不考虑任何“同步”。
我原先更喜欢public static volatile item
在线程之间共享数据。 AtomicIntegerArray
也是一个有用的类。
答案 6 :(得分:0)
我想说添加一个字段来指定运行和呈现所需的线程,并对线程进行编号,如果线程编号==所需的线程,则允许它运行和渲染,并增加所需的线程字段,直到它达到最大值,然后循环回到0.或者,你可以使用一个线程用于刻度而另一个用于渲染,这可能更容易。这是一个示例代码:
public Game() {
this.tickThread=new Thread(this::tickLoop());
this.renderThread=new Thread(this::renderLoop());
}
public void tickLoop() {
//code for timing...
while(running) {
//more code for timing...
tick();
}
}
public void renderLoop() {
//code for timing or syncing frames...
while(running) {
//more code for timing...
render();
}
}
或者,您可以说:
| MyRunnable.java |
public interface MyRunnable
{
public abstract void run(boolean toRender);
}
| MyThread.java |
public class MyThread extends Thread
{
private boolean isRender;
private MyRunnable runnable
public MyThread(boolean isRender,MyRunnable runnable)
{
this.isRender=isRender;
this.runnable=runnable;
}
public void run()
{
this.runnable.run(this.isRender);
}
}
| Game.java |
public class Game extends /*JPanel/Canvas/JFrame/Some other component*/
{
private MyThread tickThread;
private MyThread renderThread;
private boolean running;
public Game()
{
super();
tickThread=new MyThread(this::run);
renderThread=new MyThread(this::run);
//other constructor code
}
public void tick()
{
//tick code here
}
public void render()
{
//render code here
}
public void run(boolean isRender)
{
//timing variables
while(running)
{
//timing code
if(isRender)
{
this.render();
}
else
{
this.tick();
}
}
}
}