第一步:主类
首先要做的是大课。主类将处理向用户显示图像,调用其他类来重新计算应该向播放器显示的内容,并更新摄像机的位置。
对于这个类,导入将是:
导入Java . awt . color;
导入Java . awt . graphics;
导入Java . awt . image . buffer strategy;
导入Java . awt . image . buffered image;
导入Java . awt . image . data buffer int;
导入Java . util . ArrayList;
导入javax . swing . jframe;
该类及其变量如下所示:
公共类游戏扩展JFrame实现Runnable{
private static final long serialVersionUID=1L;
public int mapWidth=15
public int mapHeight=15
私有线程Thread;
私有布尔运行;
私有缓冲区图像;
public int[]像素;
public static int[][] map=
{
{1,1,1,1,1,1,1,1,2,2,2,2,2,2,2},
{1,0,0,0,0,0,0,0,2,0,0,0,0,0,2},
{1,0,3,3,3,3,3,0,0,0,0,0,0,0,2},
{1,0,3,0,0,0,3,0,2,0,0,0,0,0,2},
{1,0,3,0,0,0,3,0,2,2,2,0,2,2,2},
{1,0,3,0,0,0,3,0,2,0,0,0,0,0,2},
{1,0,3,3,0,3,3,0,2,0,0,0,0,0,2},
{1,0,0,0,0,0,0,0,2,0,0,0,0,0,2},
{1,1,1,1,1,1,1,1,4,4,4,0,4,4,4},
{1,0,0,0,0,0,1,4,0,0,0,0,0,0,4},
{1,0,0,0,0,0,1,4,0,0,0,0,0,0,4},
{1,0,0,2,0,0,1,4,0,3,3,3,3,0,4},
{1,0,0,0,0,0,1,4,0,3,3,3,3,0,4},
{1,0,0,0,0,0,0,0,0,0,0,0,0,0,4},
{1,1,1,1,1,1,1,4,4,4,4,4,4,4,4}
};
请注意,地图可以重新配置成需要的内容,我这里只是一个样本。地图上的数字表示该位置的墙壁类型。0表示空白空间,而任何其他数字表示实心墙和随后的纹理。BufferedImage显示给用户,像素是图像中所有像素的数组。其他变量实际上不会再出现,只是用来让图形和程序正常工作。
构造函数现在看起来像这样:
公共游戏(){
thread=新线程(this);
image=new BufferedImage(640,480,BufferedImage。TYPE _ INT _ RGB);
pixels=((DataBufferInt)image . get raster()。getDataBuffer())。get data();
setSize(640,480);
setResizable(false);
setTitle(“3D引擎”);
setDefaultCloseOperation(JFrame。EXIT _ ON _ CLOSE);
set background(color . black);
setLocationRelativeTo(null);
set visible(true);
start();
}
大部分只是类变量和框架的初始化。“pixels=”之后的代码将像素与图像连接起来,这样每当像素中的数据值发生变化时,相应的变化就会在图像显示给用户时显示在图像上。
start和stop方法很简单,用于确保程序安全地开始和结束。
私有同步void start() {
跑步=真;
thread . start();
}
公共同步void stop() {
跑步=假;
尝试{
thread . join();
} catch(InterruptedException e) {
e . printstacktrace();
}
}
游戏类中需要的最后两个方法是render和run方法。渲染方法如下:
public void render() {
buffer strategy bs=getbuffer strategy();
if(bs==null) {
create buffer strategy(3);
返回;
}
graphics g=bs . getdrawgraphics();
g.drawImage(image,0,0,image.getWidth(),image.getHeight(),null);
bs . show();
}
渲染时使用缓冲策略,让屏幕更新更流畅。一般来说,使用缓冲策略只会帮助游戏在运行时看起来更好。为了在屏幕上实际绘制图像,需要从缓冲策略中获取图形对象,并使用它们来绘制图像。
run方法非常重要,因为它可以处理程序不同部分的更新频率。为此,它使用一些代码来跟踪1/60秒过去的时间以及屏幕和摄像头更新的时间。这样可以提高节目的流畅性。运行方法如下:
公共无效运行(){
long last time=system . nano time();
最终双精度ns=100000000.0/60.0;//每秒60次
双=0;
request focus();
(跑步时){
long now=system . nano time();
delta=delta((now-last time)/ns);
lastTime=现在;
while(delta">=1)//确保每秒只更新60次
{
//处理所有逻辑限制时间
delta-;
}
render();//在屏幕上显示无限制的时间
}
}
一旦所有这些方法、构造函数和变量都在里面了,Game类中剩下唯一要做的事情就是添加一个main方法。主要方法很简单,你要做的就是:
公共静态void main(String [] args) {
Game Game=new Game();
}
现在,主课已经上完了!如果你现在运行这个程序,会弹出一个黑屏。
步骤2:纹理类
在进入寻找屏幕外观的计算之前,我会绕过并设置纹理类。纹理将应用于环境中的各种墙壁,并将来自保存在项目文件夹中的图像。在图像中,我包括了在互联网上找到的四个纹理,这将在这个项目中使用。你可以使用任何你想要的纹理。要使用这些纹理,我建议将它们放在项目文件的一个文件夹中。为此,转到项目文件夹(在Eclipse中,它位于工作区文件夹中)。转到项目文件夹后,创建一个名为“res”或其他名称的新文件夹。将纹理放在这个文件夹中。你可以把纹理放在其他地方,那是我存放纹理的地方。一旦完成,我们就可以开始编写代码来使纹理可用。
这个类的导入是:
导入Java . awt . image . buffered image;
导入Java . io . file;
导入Java . io . io exception;
导入javax . imageio . imageio;
类头及其变量如下所示:
公共类纹理{
public int[]像素;
私有字符串loc
公共最终int大小;
数组像素用于存储纹理图像中所有像素的数据。Loc用于向计算机指示可以找到纹理的图像文件的位置。SIZE是一面的纹理大小(一张64x64的图片大小是64),所有的纹理都会是完全正方形的。
构造函数初始化loc和SIZE变量,并调用方法将图像数据加载到像素中。看起来是这样的:
公共纹理(字符串位置,整数大小){
loc=位置;
尺寸=大小;
pixels=new int[SIZE * SIZE];
load();
}
现在,纹理类剩下的工作就是添加一个load方法来从图像中获取数据,并将它们存储在像素数据数组中。该方法将如下所示:
私有void load() {
尝试{
buffered image image=imageio . read(新文件(loc));
int w=image . getwidth();
int h=image . get height();
image.getRGB(0,0,w,h,像素,0,w);
} catch (IOException e) {
e . printstacktrace();
}
}
load方法的工作原理是从loc指向的文件中读取数据,并将数据写入缓冲的映像。然后,从缓冲的图像中获得每个像素的数据,并存储在该像素中。
至此,纹理类已经完成,所以我将继续定义一些要在最终程序中使用的纹理。要做到这一点,请把这个
公共静态纹理木材=新纹理("res/wood.png",64);
公共静态纹理砖=新纹理("res/redbrick.png",64);
公共静态纹理bluestone=new Texture("RES/bluestone . png",64);
公共静态纹理石头=新纹理("res/greystone.png",64);
放在“public class Texture”线和“public int [] pixel”之间。
使这些纹理可以被程序的其他部分访问。我们继续,把它们交给游戏类。为此,我们需要一个数组列表来容纳所有的纹理,并且我们需要向这个数组列表添加纹理。若要创建数组列表,请将以下代码行和变量放在类的顶部附近:
公共数组列表纹理;
这个ArrayList必须在构造函数中初始化,还应该在构造函数中添加一个纹理。将以下代码添加到构造函数中:
textures=new ArrayList();
textures . add(texture . wood);
textures . add(texture . brick);
textures . add(texture . blue stone);
textures . add(texture . stone);
现在你可以使用纹理了!
第三步:相机类
现在我们绕道设置一下相机类。Camera类跟踪玩家在2D地图中的位置,并负责更新玩家的位置。为此,该类将实现KeyListener,因此您需要导入KeyEvent和KeyListener。
导入Java . awt . event . key event;
导入Java . awt . event . key listener;
需要许多变量来跟踪摄像机的位置和它看到的内容。因此,该类的第一个块如下:
公共类摄像机实现KeyListener {
公共双xPos,yPos,xDir,yDir,xPlane,yPlane
public boolean左、右、前、后;
公终双动_速度=. 08;
公终双转_速=. 045;
XPos和yPos是玩家在游戏类中创建的2D地图上的位置。XDir和yDir是指向玩家面对的方向的向量的x和y分量。XPlane和yPlane也是向量的x和y分量。由xPlane和yPlane定义的向量始终垂直于方向向量,并指向一侧相机视野的最远边缘。另一边最远的边是负平面向量。方向向量和平面向量的组合定义了相机视野中的内容。布尔值用于跟踪用户按下了哪些键,以便用户可以移动相机。MOVE_SPEED和ROTATION_SPEED表示当用户按下相应的键时相机移动和旋转的速度。
接下来是构造函数。构造函数接受告诉类位置的值,并将摄像机分配给相应的变量(xPos,yPos。)。
公共摄像机(双x、双y、双xd、双yd、双xp、双yp)
{
xPos=x;
yPos=y;
xDir=xd
yDir=yd
xPlane=xp
yPlane=yp
}
相机对象会在最后的程序中需要它,所以我们继续添加一个。在带有所有其他变量声明的游戏类中,添加
公共相机摄像机;
并将其添加到构造函数中。
相机=新相机(4.5,4.5,1,0,0,-0.66);
addKeyListener(相机);
这个相机将和地图一起使用。我正在使用它。如果您使用另一个地图或想要从另一个位置开始,请调整xPos和yPos的值(在我的示例中为4和6)。使用. 66可以提供良好的视野,但是您可以调整该值以获得不同的FOV。
既然Camera类有了一个构造函数,我们可以开始添加方法来跟踪用户的输入并更新相机的位置/方向。因为Camera类实现了KeyboardListener,所以它必须拥有它实现的所有方法。Eclipse应该会自动提示您添加这些方法。您可以将keyTyped方法留空,但是将使用其他两个方法。当相应的键被按下时,KeyPressed将布尔值设置为true,当键被释放时,keyReleased将布尔值更改为false。该方法如下所示:
公共void按键(KeyEvent键){
if((key.getKeyCode()==KeyEvent。VK _左))
左=真;
if((key.getKeyCode()==KeyEvent。VK _右))
右=真;
if((key.getKeyCode()==KeyEvent。VK_UP))
forward=true
if((key.getKeyCode()==KeyEvent。VK _唐))
back=true
}
和
public void key released(key event key){
if((key.getKeyCode()==KeyEvent。VK _左))
左=假;
if((key.getKeyCode()==KeyEvent。VK _右))
右=假;
if((key.getKeyCode()==KeyEvent。VK_UP))
forward=false
if((key.getKeyCode()==KeyEvent。VK _唐))
back=false
}
现在Camera类正在跟踪哪些键被按下了,我们可以开始更新玩家的位置了。为此,我们将使用在Game类的run方法中调用的update方法。在这个过程中,我们将继续操作,并通过将地图传递给游戏类中的更新方法,为更新方法添加冲突检测。更新方法如下:
公共void更新(int[][] map) {
如果(转发){
if(map[(int)(xPos xDir * MOVE _ SPEED)][(int)yPos]==0){
xPos=xDir * MOVE _ SPEED
}
if(map[(int)xPos][(int)(yPos yDir * MOVE _ SPEED)]==0)
yPos=yDir * MOVE _ SPEED
}
如果(返回){
if(map[(int)(xPos-xDir * MOVE _ SPEED)][(int)yPos]==0)
xPos-=xDir * MOVE _ SPEED;
if(map[(int)xPos][(int)(yPos-yDir * MOVE _ SPEED)]==0)
yPos-=yDir * MOVE _ SPEED;
}
如果(右){
double oldxDir=xDir
xDir=xDir * math . cos(-旋转速度)-yDir * math . sin(-旋转速度);
yDir=old xdir * math . sin(-ROTATION _ SPEED)yDir * math . cos(-ROTATION _ SPEED);
double oldxPlane=xPlane
x plane=x plane * math . cos(-ROTATION _ SPEED)-y plane * math . sin(-ROTATION _ SPEED);
y plane=oldx plane * math . sin(-ROTATION _ SPEED)y plane * math . cos(-ROTATION _ SPEED);
}
如果(左){
double oldxDir=xDir
xDir=xDir*Math.cos(旋转速度)- yDir*Math.sin(旋转速度);
yDir=old xdir * math . sin(ROTATION _ SPEED)yDir * math . cos(ROTATION _ SPEED);
double oldxPlane=xPlane
x plane=x plane * math . cos(ROTATION _ SPEED)-y plane * math . sin(ROTATION _ SPEED);
y plane=oldx plane * math . sin(ROTATION _ SPEED)y plane * math . cos(ROTATION _ SPEED);
}
}
这种方法控制向前和向后运动的部分是通过将xDir和yDir分别添加到xPos和yPos来工作的。在这个动作发生之前,程序会检查这个动作是否会把摄像头放在墙上,如果是,就不检查。对于旋转,方向向量和平面向量都乘以旋转矩阵,即:
[ cos(旋转速度)-sin(旋转速度)]
[正弦(旋转速度)余弦(旋转速度)]
以获得其新值。完成更新方法后,我们现在可以从游戏类中调用它了。在Game类的run方法中,添加以下代码行,如下所示。
添加这个:
camera.update(地图);
在这里:
(跑步时){
long now=system . nano time();
delta=delta((now-last time)/ns);
lastTime=现在;
while(delta">=1)//确保每秒只更新60次
{
//处理所有逻辑限制时间
camera.update(地图);
delta-;
}
render();//在屏幕上显示无限制的时间
}
现在,完了,我们终于可以进入最后一节课,计算屏幕了!
步骤4:计算屏幕
在Screen类中,大部分的计算都是为了让程序正常工作。要工作,该类需要以下导入:
导入Java . util . ArrayList;
导入Java . awt . color;
实际的课程是这样开始的:
公共类屏幕{
public int[][]map;
public int mapWidth,mapHeight,Width,height
公共数组列表纹理;
地图和游戏类有关。屏幕用它来确定墙的位置和离玩家的距离。宽度和高度定义了屏幕的大小,应该始终与游戏类中创建的框架的宽度和高度相同。纹理是所有纹理的列表,以便屏幕可以访问纹理的像素。声明这些变量后,必须在构造函数中对它们进行初始化,如下所示:
public Screen(int[][] m,ArrayList tex,int w,int h) {
map=m;
纹理=tex
宽度=w;
高度=h;
}
现在是时候写一个类拥有的方法了:update方法。更新方法根据用户在地图中的位置重新计算屏幕的外观。这个方法被反复调用,更新后的像素数组被返回给Game类。该方法从“清除”屏幕开始。这是通过将上半部分的所有像素设置为一种颜色并将下半部分的所有像素设置为另一种颜色来实现的。
public int[] update(相机相机,int[]像素){
for(int n=0;n if(pixels[n]!=颜色. DARK_GRAY.getRGB())像素[n]=颜色暗灰色。获取RGB();
}
for(int I=像素。长度/2;我如果(像素[我]!=颜色。灰色。get RGB())pixels[I]=color。灰色。获取RGB();
}
让屏幕的顶部和底部为两个不同颜色也使它看起来好像有地板和天花板。清除像素阵列后,该是进行主要计算的时候了。该程序循环遍历屏幕上的每个垂直条,并投射光线以找出该垂直条上的屏幕上应该有什么墙。循环的开始看起来像这样:
for(int x=0;xdouble cameraX=2 * x/(double)(宽度)-1;
双光照相机。xdir相机。x平面* cameraX
双光照相机。ydir相机。yplane * cameraX
//地图位置
int mapX=(int)相机。xpos
int mapY=(int)相机。ypos
//从当前位置到下一个x或y边的射线长度
双面distx
双侧距;
//地图中从一边到另一边的光线长度
double deltaDistX=math。sqrt(1(雷diry *雷diry)/(雷dirx *雷dirx));
double deltaDistY=math。sqrt(1(雷dirx *雷dirx)/(雷diry *雷diry));
双垂直距离
//x和y的前进方向
int stepX,stepY
布尔命中=假;//碰壁了
int side=0;//墙是垂直的还是水平的
这里发生的所有事情都是计算出循环其余部分将要使用的一些变量照相机是摄影机平面上当前垂直条纹的x坐标,并且雷迪尔变量为射线创建矢量。计算所有以DistX或DistY结尾的变量,以便程序仅在可能发生碰撞的位置检查碰撞佩沃尔迪斯是从播放器到射线与之碰撞的第一堵墙的距离。这将在以后计算。完成此操作后,我们需要根据已经计算出的变量来找出其他一些变量。
//算出步进方向和到一边的初始距离
if (rayDirX 《0》
{
stepX=-1;
sideDistX=(相机。xpos-mapX)* deltaDistX;
}
其他
{
stepX=1;
sideDistX=(mapX 1.0-相机。xpos)* deltaDistX;
}
如果(瑞利《0》
{
stepY=-1;
侧距离=(相机。ypos-mapY)* delta disty;
}
其他
{
stepY=1;
side disty=(mapY 1.0-相机。ypos)* delta disty;
}
一旦完成,就该找出射线与何处碰撞了。一堵墙。为此,程序要经过一个循环,在该循环中检查射线是否与墙壁接触,如果没有,则移动到下一个可能的碰撞点,然后再次检查。
//循环查找光线击中墙壁的位置
而(!点击){
//跳到下一个方块
if (sideDistX 《sideDistY》
{
sideDistX=deltaDistX
mapX=stepX
side=0;
}
其他
{
sideDistY=deltaDistY
mapY=stepY
side=1;
}
//检查射线是否碰壁
if(map[mapX][mapY]"0)hit=true;
}
现在我们知道射线在何处撞击墙壁,我们可以开始计算墙壁在我们当前所在的垂直条纹中的外观。为此,我们首先计算到墙的距离,然后使用该距离来计算出墙在垂直条中应该有多高。然后,我们根据屏幕上的像素将该高度转换为起点和终点。代码如下所示:
//计算到撞击点的距离
if(side==0)
垂直距离=数学。ABS((mapX-camera。xpos(1-stepX)/2)/rayDirX);
其他
垂直距离=数学。ABS((mapY-camera。ypos(1-stepY)/2)/ray diry);
//现在根据离摄像机的距离计算墙的高度
int lineHeight
if(perp wall dist〉0)行高=数学。ABS((int)(高度/垂直墙距离));
else lineHeight=高度
//计算填充当前条纹的最低和最高像素
int draw start=-行高/2高度/2;
if(drawStart 《0》
绘制开始=0;
int drawind=行高/2高度/2;
if(drawind"=height)
drawind=height-1;
计算完之后,就该开始从墙的纹理中找出哪些像素会真正呈现给用户了。为此,我们首先必须确定与刚击中的墙关联的纹理,然后确定将向用户显示的像素的纹理的x坐标。
//添加纹理
int tex num=map[mapX][mapY]-1;
双wallX//墙壁被击中的确切位置
If(side==1){//如果是y轴墙
wallX=(camera . xpos((mapY-camera . ypos(1-stepY)/2)/rayDirY)* rayDirX);
} else {//X轴墙
wallX=(camera . ypos((mapX-camera . xpos(1-stepX)/2)/rayDirX)* rayDirY);
}
wallX-=math . floor(wallX);
//纹理上的x坐标
int texX=(int)(wallX *(textures . get(tex num))。大小));
if(side==0 ray dirx"0)texX=textures . get(tex num)。尺寸-texX-1;
if(side==1 ray diry《0》texX=textures . get(tex num)。尺寸-texX-1;
通过在2D地图上获得撞墙的确切位置并减去整数值(只保留小数部分)来计算x坐标。然后把这个分数(wallX)乘以墙的纹理大小,就得到我们要画的像素在墙上的确切X坐标。一旦我们知道了,剩下的就是计算纹理上像素的Y坐标,并把它们画在屏幕上。为此,我们在垂直条中遍历屏幕上的所有像素,然后计算它们,并计算纹理上像素的精确Y坐标。然后,使用这个程序,程序将纹理中像素的数据写入屏幕上的像素数组。该计划还使这里的水平墙比垂直墙更暗,以提供基本的照明效果。
//计算纹理上的y坐标
for(int y=draw start;y int texY=(((y * 2-height line height)《6)/line height)/2;
int颜色;
if(side==0)color=textures . get(tex num)。pixels[texX(texY * textures . get(tex num))。尺寸)];
else color=(textures.get(texNum)。pixels[texX(texY * textures . get(tex num))。尺寸)]》1)8355711;//使y边变暗
像素[x y*(宽度)]=颜色;
}
然后,Screen类中剩下的就是返回一个像素数组。
返回像素;
这门课已经结束了。现在,我们要做的就是在游戏类中添加几行代码,让屏幕正常运行。在变量的顶部添加以下内容:
公共屏幕屏幕;
,然后在构造函数中,初始化纹理后,在某处添加。
screen=新屏幕(贴图,贴图宽度,贴图高度,纹理,640,480);
最后,运行run方法add before camera.update(map)。
screen.update(摄像头,像素);程序完成了!
第五步:最终代码