通常的概念中,2D也就是所谓的二维,也就是平面图形——即由X与Y坐标构成的图形,其内容由水平的X轴向与垂直的Y轴描绘确定,也就是由长和高的形成所谓的二维平面。
而所谓3D,也称之为三维。其图形内容除了有水平的X轴向与垂直的Y轴向外还有进深的Z轴,故称三维(XYZ),也就是由长、宽、高三项要素形成所谓的三维立体。
2D与3D的主要区别在于,3D可以包含360度的信息,能从各个角度去表现,构成近似于现实空间的有质感视角;而2D通常只能表现如表格、棋盘版的平面数据。 3D的立体感、光景效果要比二维平面图形要好的多,因为它的立体、光线、阴影都是相对真实存在的,而2D显然不具备这些优势。高拟真度、高自由度使得3D图形大受欢迎。
所以渐渐的3D图形开始成为主流,充斥于电影、电视乃至游戏的各个角落。但是,由于3D技术实现的复杂性及对用户环境的高要求,在所有领域都完全使用3D构图还并不现实,由此引发了另一种图形表现形式的出现——2.5D图形。
所谓的2.5D,介乎于模真的3D与完全平面的2D之间。即模拟了3D的空间感,也兼具2D的灵动简单,是一种“优势”的综合体。就我个人认为,诚然2.5D最早的出现动机只是为了2D到3D间过渡。但就其应用而讲,好的2.5D图形即有3D的自由度与质感,又能利用2D图形将漫画式人物塑造得惟妙惟肖,使其拥有纯3D还无法做到的优势。因此2.5D在现在乃至未来的一段较长时间里还会和3D并存,直到3D图形的开发效率及表现形式能彻底取代2.5D为止。
目前2.5D有两种大方向的实现思路:
一、纯2D模拟实现,是由2D人物及2D模拟3D场景:比较常见的如《神奇传说》、《皇家骑士团》、《超时空英雄传说》(PS:我从1开始玩的,也由此和宇峻游戏结缘,虽然我不是游戏开发人员,但遥想当年宇峻科技刚开张时,李玉山一个人写出超时空英雄传说1所有程序代码[虽然有部分是国外引擎改的]的功力我至今望尘莫及,其不愧为我等程序员之楷模)《魔力宝贝online》等,利用近大远小的视觉差模拟3D物体,令用户产生在使用3D图形的错觉,通常为一个左或右偏的45度角,当然也有偏55度或其他不同的角度可供选择。
二、2D+3D实现,是由3D场景加2D人物贴图:比如《MU奇迹online》、《RO仙境传说online》(PS:在RO中我练的高INT创造者,爱好是城战丢硫酸瓶毁对方装备……),由于场景采用3D绘制,可以360度自由旋转视角,真实感较强。纯2D的角色与遮挡关系处理简单。相对于纯3D来说代码量少且简单。美术方面工作量较少。比之纯3D占用系统资源也更少。
平心而论,个人比较推崇2D+3D哪种类RO的表现方式,但由于吾辈是Java程序员且非游戏开发人员并不熟悉Java3D的关系,只能采用Java2D来进行演示。
在当采用2D模拟3D实现2.5D(斜视角)时,我认为难点只有两个:
1.你要有美工帮你做好斜视的tile及role
2.你要转换2D坐标为2.5D坐标
对于第一点,神仙都爱莫能助……没有美工就自己画吧……
而对于第二点,也就是斜视图的坐标变换,大多数人最直接的想法可能就是利用三角函数。是的,从某种意义上说,俯视角度与正斜视角度的变化就是三角函数的应用,我也见过这类的45度斜视用例。但是,利用cos在Java中进行三角函数的运算,首先速度不能达到要求,其次图像衔接度结果并不理想,结果可能令图像有锯齿状偏移,所以我并不推荐使用。
对于如
类的菱形斜视图像,我们可以利用一个简单的行列运算获得俯视角度地砖的绘制坐标与斜视角度地砖的绘制坐标的变换,公式如下:
X:2.5D图形中X轴位置=(目标图像Width/2)*(2D图形中X-2D图形中Y)
Y:2.5D图形中Y轴位置=(目标图像Height/2)*(2D图形中X+2D图形中Y)/2
当然,这个公式并不是绝对的,尤其是在图像数据有所偏移时。比如斜视角的地砖的宽与高的比例理论上应该是2:1的关系,即如果高度是12的话,那么物体宽度就应该是24,这个在数学上讲和绘制地图上的时候是没有错的。但是问题出现在美工可能做不出来这么一个图(比如我,做了三个演示用图基准线全不一样|||)。
因为美工在画图的时候,肯定是要找一个基准线(一般是水平方向上的中分线,因为这个方向是高度,而高度的比例是单位1),然后对称的画图的,在开始的时候问题不会出现,但是在画到后面的时候就会发现,无论如何都没有办法把另一个方向上的顶点画出来,总是没有办法在指定的范围内相交。这是因为计算机的绘图是按照像素点一个个绘制出来的,总是没0.5个像素的时候,所以就出现了这个情况。于是,美工一般为了在指定的范围划出来一个菱形往往会画花2个像素,也就是24*12的图像很有可能会被画成22*12……
此时这是我们就需要增或者减公式中的数值,令基准线和图形的实际保持一致,大家可以自己推导。
演算后图形表现应如上图所示。
下面,我给出一个在Java中此公式的实际实现代码。
Test25D.java
package
org.loon.framework.game.test;
import
java.awt.Color;
import
java.awt.Frame;
import
java.awt.Graphics;
import
java.awt.Image;
import
java.awt.Panel;
import
java.awt.Point;
import
java.awt.event.WindowAdapter;
import
java.awt.event.WindowEvent;
import
org.loon.framework.game.image.Bitmap;
/***/
/**
*<p>
*Title:LoonFramework
*</p>
*<p>
*Description:Java2.5D地图构建
*</p>
*<p>
*Copyright:Copyright(c)2007
*</p>
*<p>
*Company:LoonFramework
*</p>
*
*@authorchenpeng
*@email:[email protected]
*@version0.1
*/
public
class
Test25D
extends
Panel
...
{
/***//**
*
*/
privatestaticfinallongserialVersionUID=1L;
finalstaticintframeWidth=490;
finalstaticintframeHeight=330;
Bitmapfloor,tile,role;
Imagescreen;
Graphicsgraphics;
int[][]map=...{
...{1,1,1,1,1,1,1,1},
...{1,0,0,0,0,0,0,1},
...{1,0,1,0,0,0,0,1},
...{1,0,0,0,0,1,0,1},
...{1,0,0,0,0,0,0,1},
...{1,1,1,1,1,1,1,1}};
publicTest25D()...{
setBackground(Color.WHITE);
floor=newBitmap("./imagerpg/floor.gif");
tile=newBitmap("./imagerpg/tile.gif");
role=newBitmap("./imagerpg/obj.gif");
//定位为X=1,Y=2(由0开始)
role.setDrawXY(1,2);
screen=newBitmap(frameWidth,frameHeight).getImage();
graphics=screen.getGraphics();
}
publicvoidupdate(Graphicsg)...{
paint(g);
}
publicvoidpaint(Graphicsg)...{
intX,Y;
//将图像位置向x点坐标校正180,y点校正50
intoffsetX=180;
intoffsetY=50;
intfloorWidth=floor.getWidth();
intfloorHeight=floor.getHeight();
for(inti=0;i<map.length;i++)
for(intj=0;j<map[i].length;j++)...{
//转换为斜角坐标
Pointp=reviseXY(floorWidth,floorWidth,j,i,offsetX,offsetY);
X=p.x;
Y=p.y;
if(map[i][j]==0)...{
graphics.drawImage(floor.getImage(),X,Y,X+floorWidth,
Y+floorHeight,0,0,floorWidth,floorHeight,
this);
}
}
intblackWidth=tile.getWidth();
intblackHeight=tile.getHeight();
for(inti=0;i<map.length;i++)
for(intj=0;j<map[i].length;j++)...{
if(map[i][j]!=0)...{
Pointp=reviseXY(blackWidth,blackWidth,j,i,offsetX,offsetY);
X=p.x;
Y=p.y-(blackHeight-floorHeight);
graphics.drawImage(tile.getImage(),X,Y,X+blackWidth,
Y+blackHeight,0,0,blackWidth,blackHeight,
this);
}
}
//绘制移动角色
introleWidth=role.getWidth();
introleHeight=role.getHeight();
Pointp=reviseXY(roleWidth,roleWidth,role.getDrawX(),role.getDrawY(),offsetX,offsetY);
X=p.x;
Y=p.y-15;
graphics.drawImage(role.getImage(),X,Y,X+roleWidth,
Y+roleHeight,0,0,roleWidth,roleHeight,
this);
//在伪45度图像构建时,需要处理角色和建筑间的遮挡关系,我在这里只使用了简单的重绘
//即如果构成和角色相遮挡的物体,首先绘制角色,而后用建筑遮挡该角色。
//比如x+1=1即前方有物体存在时,重绘遮挡物以遮挡角色
if(map[role.getDrawY()][role.getDrawX()+1]==1)...{
p=reviseXY(blackWidth,blackWidth,role.getDrawX()+1,role.getDrawY(),offsetX,offsetY);
X=p.x;
Y=p.y-(blackHeight-floorHeight);
graphics.drawImage(tile.getImage(),X,Y,X+blackWidth,
Y+blackHeight,0,0,blackWidth,blackHeight,
this);
}
g.drawImage(screen,0,0,this);
}
/***//**
*修正为斜角坐标
*@paramwidth
*@paramx
*@paramy
*@paramoffsetX
*@paramoffsetY
*@return
*/
privatePointreviseXY(intwidth,intheight,intx,inty,intoffsetX,intoffsetY)...{
intx1=x*(width/2);
inty1=y*(height/2);
intnx=x1-y1;
intny=(x1+y1)/2;
returnnewPoint(nx+offsetX,ny+offsetY);
}
publicstaticvoidmain(String[]args)...{
java.awt.EventQueue.invokeLater(newRunnable()...{
publicvoidrun()...{
Framefrm=newFrame("Java2.5D地图构建");
frm.setResizable(false);
frm.setSize(frameWidth,frameHeight);
frm.add(newTest25D());
frm.addWindowListener(newWindowAdapter()...{
publicvoidwindowClosing(WindowEvente)...{
System.exit(0);
}
});
frm.setLocationRelativeTo(null);
frm.setVisible(true);
}
});
}
}
效果图如下:
明天晚上有时间再继续……赶紧睡觉……