从本篇开始,将展现一个迷你的游戏引擎制作过程和其设计思路,但由于文章重点不在针对性的讨论代码本身,而是讨论层次与设计结构,所以我会尽可能的缩小代码所占的篇幅,不过每次实例的源文件会设法通过各种渠道进行分享。
好的游戏引擎应该包含绘图和声效所需的全部处理方式,我们不妨把一个游戏引擎简单的拆分成图形部分和声音部分,而这一篇将带来一个绘制部分的基本框架。
在开始之前,先来回想一下上一篇中我们的结论,游戏是会随用户意向改变的影音数据集合;游戏跟电影的本质是如此地相似,但还是略有不同。
最重要的是帧数的问题,24帧/秒以上能在大多数人的大脑中产生足够的视觉暂留,以产生连续的效果;但对于游戏程序来讲,这个数字是如此地糟糕,每天都会有无数玩家因为帧数的问题疯狂叫喊亦或是摔键盘,原因在于游戏不仅是视觉上的交互,还有体感上的交互,从你按下一个控制键而期待屏幕上有所反应的时间,比视觉暂留要灵敏一些,这也是为什么游戏程序往往保持在一个较高的帧数上,或是干脆与显示器同步的原因。一般来说,要做到体感上能够接受,需要30帧/秒以上的绘制速度,而且这个随人的差距很大,一些敏感的人需要这个值在40~50亦或是更高都有可能。
为了适应大部分人的需求,一般游戏的绘制速度被设计为60帧/秒,是我见过的显示器最低的刷新率(有比这更低的么?不会坏眼睛么?);
那么任务明确了,编写程序在屏幕的指定区域以60帧/秒的速度绘制图像吧。
使用环境:Eclipse+JAVA(JDK1.7)+JOGL(1.1)
简要的总结一下搭建方式:对于下载下的JOGL,我们将其对于版本的JAR导入到Eclipse项目,并将dll文件放入调试所使用jre目录的bin下即可,无需修改环境变量。
先解决的问题是往哪里绘制图像,具体的窗体和部件。JOGL绘制图像的位置是名为GLCanvas类的实例,看名字就知道是转为GL准备的“画纸”,而且这个类继承了JAVA的Component类,意味着我们可以像添加控件一样添加入一个容器内。同时,我们需要对这个画纸添加一个名为GLEventListener的接口以便控制其在各个事件下的行为,如窗口大小变换后对图像的操作等等。
接下来讨论绘制任务的问题,想要保持60帧的速率,我们需要一个线程去不断地调用绘制方法,并使用Timer记录下所消耗的时间进行恰当的延时,使速度不至于过快或过慢,这听起来很麻烦,实际上亦如此,好在JOGL为我们提供了FPSAnimator这个类,我们只需要指定帧数,他即可完成上述的任务。
1 public class Displayer extends JFrame 2 { 3 private static final long serialVersionUID = 1L; 4 public Painter listener; 5 FPSAnimator animator=null; 6 public Displayer() throws HeadlessException 7 { 8 setSize(800,600); 9 setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); 10 listener=new Painter(); 11 12 GLCapabilities glcaps=new GLCapabilities();//这里注意一下画纸的实例化过程 13 GLCanvas canvas=new GLCanvas(glcaps); 14 15 getContentPane().add(canvas, BorderLayout.CENTER);//将画纸加入到容器 16 canvas.addGLEventListener(listener); //给画纸设置Listener 17 18 Image image = new ImageIcon(".\\Data\\DefaultTexture\\DefaultTexture.png").getImage();//我用一张空白图片替换掉了鼠标,我们以后可以用内部的画图机制画出各式各样的动态鼠标 19 setCursor(Toolkit.getDefaultToolkit().createCustomCursor(image, new Point(), null)); 20 21 canvas.setFocusable(true); 22 23 animator=new FPSAnimator(canvas,60,true); //这里实例化了一个FPSAnimator 24 } 25 public void startDraw()//启动绘制过程,同时不阻塞主线程 26 { 27 this.setVisible(true); 28 new Thread(){ 29 @Override 30 public void run() 31 { 32 animator.start(); 33 } 34 }.start(); 35 } 36 }
1 public class Painter implements GLEventListener 2 { 3 //GLEventListener声明了如下方法 4 @Override 5 public void display(GLAutoDrawable arg0) { 6 //这个方法最为重要,一般来讲,我们会以60次每秒的速度调用它 7 } 8 @Override 9 public void displayChanged(GLAutoDrawable arg0, boolean arg1, boolean arg2) { 10 //这个方法在JOGL1.1中仅作为了预留,可能以后会被实现(详见官方文档) 11 } 12 @Override 13 public void init(GLAutoDrawable arg0) { 14 //init不用多说,进行一些初始化设置:如投影模式、混合模式、视角设定等等 15 } 16 @Override 17 public void reshape(GLAutoDrawable arg0, int arg1, int arg2, int arg3, 18 int arg4) { 19 //这个方法在对窗体进行变换时被调用,如更改了窗体大小 20 } 21 }
我们从GLEventListener的drawable.getGL()获得GL上下文,以便调用OPENGL和其工具集的API对画纸进行各种绘制。
当然,我们有必要写一个主函数对其进行实例化与调用
1 public class mainTest 2 { 3 public static void main(String[] args) 4 { 5 Displayer frame = new Displayer(); 6 frame.startDraw(); 7 } 8 }
(import的问题用ctrl+shift+O吧~)
程序的运行结果,一片虚无:
我没有使用GL在其display进行绘制是因为我们不需要以堆积超长的图形绘制上下文来完成绘制任务,这一点将在今后体现,现在只是一个开始,在下一篇中,我们将切割视景体并作若干基本变换,使坐标系较为适宜2D图形,之后引入层次绘制的概念以及较为简单的帧同步运算。