注:本系列教程全部翻译完之后可能会以PDF的形式发布。
如果有什么错误可以到http://blog.csdn.net/kakashi8841留言或EMAIL:[email protected]给我。
jME版本 :jME_2.0.1_Stable
开发工具:MyEclipse8.5
操作系统:Window7/Vista
迄今为止,我们拥有一个带驾驶参数的box。允许我们创建不同性能类型的vehicle。box在地形上表现得很好。我们开始看一些新的。我们开始获得一些玩游戏所必需的基础。所以,让我们花这一节课来让游戏中的事物变得好看点。是时候增加一些炫的啦。我们将让terrain看起来更真实,用一辆酷的未来主义vehicle代替box,而且让这个酷的未来主义vehicle跟随terrain得更好。让我们开始!
就像之前的向导提到的一样,我们的action正在做一些相同的事,并重复很多代码。此刻,我们为了优化代码将做一些巩固。首先,AccelerateAction和BrakeAction被融合进一个单一的类VehicleRotateRightAction。然而VehicleRotateLeftAction和VehicleRotateRightAction被组合进VehicleRotateAction。
ForwardAndBackAction的update方法决定了我们想要让vehicle前进的方向,并调用vehicle相应的方法。它不再update translation。我过一会将说下这个。
/**
* 这个action调用vehicle的
* accelerate 或 brake 命令去调整它的速度
*
*/
@Override
public void performAction(InputActionEvent evt) {
if( direction == FORWARD){
node.accerate(evt.getTime());
}else if( direction == BACKWARD ){
node.brake(evt.getTime());
}
}
这里的FORWARD和BACKWARD是类的常量,而一个int参数被加到构造方法去定义我们想要移动的方向。
相似的,VehicleRotationAction被改为:
/**
* 由vehicle的转弯速度转弯。
* 如果vehicle正在后退,方向相反。
*/
@Override
public void performAction(InputActionEvent evt) {
//影响方向
if(direction == LEFT) {
modifier = 1;
} else if(direction == RIGHT) {
modifier = -1;
}
//我们想根据我们运动的方向转向不同的方向
if(vehicle.getVelocity() < 0) {
incr.fromAngleNormalAxis(
-modifier * vehicle.getTurnSpeed()
* evt.getTime(),
upAxis
);
} else {
incr.fromAngleNormalAxis(
modifier * vehicle.getTurnSpeed()
* evt.getTime(),
upAxis
);
}
vehicle.getLocalRotation().fromRotationMatrix(
incr.mult(
vehicle.getLocalRotation().toRotationMatrix(tempMa),
tempMb
)
);
vehicle.getLocalRotation().normalize();
}
RIGHT和LEFT做为常量被添加,而direction做为构造参数。modifier值定义了我们将转的方向,它是根据direction决定的。
最后,DriftAction被轻微更新了一下。
@Override
public void performAction(InputActionEvent evt) {
vehicle.drift(evt.getTime());
}
正如你看到的,translation代码被移除了。它被移往哪里?注意,我们将讲一下它。
既然action被修改了,我们需要修改一下handler使用它们。
ForwardAndBackwardAction forward =
new ForwardAndBackwardAction(
node,
ForwardAndBackwardAction.FORWARD
);
addAction(forward,"forward",true);
ForwardAndBackwardAction backward =
new ForwardAndBackwardAction(
node,
ForwardAndBackwardAction.BACKWARD
);
addAction(backward,"backward",true);
VehicleRotateAction rotateLeft =
new VehicleRotateAction(
node,
VehicleRotateAction.LEFT
);
addAction(rotateLeft,"turnLeft",true);
VehicleRotateAction rotateRight =
new VehicleRotateAction(
node,
VehicleRotateAction.RIGHT
);
addAction(rotateRight,"turnRight",true);
所以,最后的改变是Vehicle怎样移动它的位置。这个问题之前都是AccelerateAction / BrakeAction和DriftAction在应用移位的。这实际上导致vehicle每次移动2次。因此,增加一个update方法给Vehicle并在handler的update方法中调用vehicle的update。
public void update(float time){
this.localTranslation.addLocal(
this.localRotation.getRotationColumn(2, tempVa)
.mult(velocity*time)
);
}
和
vehicle.update(time);
增加到handler的update方法。
现在,vehicle移动了它原来速度的一半,所以我将调整vehicle的性能。附加地,我也觉得它转得太快。所以,我也降低了转速。
player.setTurnSpeed(2.5f);
player.setMaxSpeed(25);
player.setMinSpeed(15);
就是那样。所以,让我们用好的素材继续做下去!
我们将增加的第一个悦目的东西是detail Texture。一个detail Texture通常是由黑色和白色组合而成的图片,从而表现地面的特性。通常,黑色和白色的texture模拟了不光滑地面凹凸物形成的阴影。所以,这个texture被应用于terrain并和原始terrain的texture组合。terrain已有的颜色texture和这个detail texture的组合允许颜色texture呈现出比它本身更多的细节。
为了完成这个,我们将使用jME的multitexturing特性。这包含将第二张texture放入绘图卡/显卡(Graphics Card)的第二Texture单元,并为texture定义参数,指明它们2个是怎样被组合的。
首先,我们加载detail texture(在buildTerrain方法中进行):
//加载细节纹理并为2个terrain的texture设置组合模型
Texture t2 = TextureManager.loadTexture(
Lesson7.class.getClassLoader()
.getResource("res/Detail.jpg"),
Texture.MinificationFilter.Trilinear,
Texture.MagnificationFilter.Bilinear
);
ts.setTexture(t2, 1);
我们就像我们之前一样使用TextureManager加载一个Detail.jpg图像。我们接着设置它到我们的TextureState做为第二个texture(unit 1, 记住我们从0开始数)。这告诉TextureState ts去保存2张Texture(t1和t2).
下一步,由于这是一种细节纹理,我们想让它……很好,细节。创建一个高分辨率的texture图像不会有什么影响,由于这只是提供为地面基础texture。因此,我们将用一张相当低的分辨率并让它在地面上重复(repeat)很多次。实际上,detail纹理将在terrain上的一边到另一边repeat16次。为了允许这个repeat,我们需要设置texture的wrap模式。
t2.setWrap(Texture.WrapMode.Repeat);
现在,我们需要定义这2张texture是怎样被组合的。Texture定义了很多组合模式(combine mode)。首先,我们将设置每个texture为ApplyMode.Combine,这告诉TextureState,那2张Texture的颜色将被Combine进一个输出颜色。第一个texture将是我们的基础texture,所以我们让它用它的颜色(RGB)乘以第二张texture以得到最后颜色。我们这么做,因为第二张texture是黑色和白色。因此,细节全白,我们将得到完整颜色,而当细节完全黑色,输出颜色将是黑色。我们接着告诉texture 1,所有的输入源将从0到1,而且只适用颜色(RGB),我们不必关心Alpha值。
所以,我们主要的texture(t1)看起来将是这样:
t1.setApply(Texture.ApplyMode.Combine);
t1.setCombineFuncRGB(Texture.CombinerFunctionRGB.Modulate);
t1.setCombineSrc0RGB(Texture.CombinerSource.CurrentTexture);
t1.setCombineOp0RGB(Texture.CombinerOperandRGB.SourceColor);
t1.setCombineSrc1RGB(Texture.CombinerSource.PrimaryColor);
t1.setCombineOp1RGB(Texture.CombinerOperandRGB.SourceColor);
接下来,我们需要修改t2去应用它的颜色值给之前的texture(t1).一切都和t1一样(定义颜色来自哪里),但我们设置CombineFuncRGB为AddSigned,这意味着它将把自己的加到其它的texture单元上,该texture单元在和原始texture相乘之前就存在。
t2.setApply(Texture.ApplyMode.Combine);
t2.setCombineFuncRGB(Texture.CombinerFunctionRGB.AddSigned);
t2.setCombineSrc0RGB(Texture.CombinerSource.CurrentTexture);
t2.setCombineOp0RGB(Texture.CombinerOperandRGB.SourceColor);
t2.setCombineSrc1RGB(Texture.CombinerSource.Previous);
t2.setCombineOp1RGB(Texture.CombinerOperandRGB.SourceColor);
所以,现在已经设置了这2个texture,我们的render到屏幕的结果texture单元将是这2个美丽的结合。
然而,我们仍然没定义怎样将texture应用到terrain本身。那就是,纹理坐标(texture coordinate)。我们本应该为TerrainBlock定义texture coordinate,但幸运的是,我们不必这么做。使用setDetailTexture方法,我们能让它为我们完成。所以,调用:
tb.setDetailTexture(1, 16);
这告诉terrain去设置texture单元1 repeat 16次.那就是沿着texture的高度和宽度repeat 16次。
现在,当你运行例子,我们的box将飞奔在一个看起来有更多细节的terrain上。
既然我们的terrain看起来更有细节,让我们让box也在地面上跟得更好。box保持它的高度在terrain上以便它能跟着山的高度变化自己的高度,这很好,但是,它看起来却不是很正确,因为它一直保持相同的朝向(Orientation)。
我们所要做的是使用2个工具方法:getSurfaceNormal和rotateUpTo。getSurfaceNormal方法给我们沿着TerrainBlock上任何点的normal。就像getHeight它将玩家的位置插入height map最近的点并计算那点的normal。这个normal是保存在一个提供的Vector对象中,所以你可以增加:
//保存terrain的任何一个给出点的法向
private Vector3f normal = new Vector3f();
到应用程序的顶部。我们将于每次update循环期间把这个和玩家的位置做为输入传入getSurfaceNormal方法。这将在每帧给我们一个玩家当前位置的normal。
现在,我们知道我们正处的terrain的角度,我们想要调整玩家对齐山。调用rotateUpTo允许我们这样做。简单在player上带着我们从terrain获取的normal调用方法,那么我们就做完了。只是简单的处理就为游戏增加了更多的真实性。
//获取terrain在我们当前位置的normal。
//我们然后将它应用到player上面。
tb.getSurfaceNormal(
player.getLocalTranslation(),
normal
);
if(normal != null) {
player.rotateUpTo(normal);
}
现在,驾驶着box并看它是怎样跟着terrain的。太激动了!现在,让我们处理掉那个丑陋的box。
那么,现在我们想真正驾驶一辆cool的未来主义vehicle。很好,就像很多没艺术的人一样,我开始寻找免费的模型。3D Cafe 有一些未来主义的摩托车。它甚至在两边有漂亮的枪。
所以,我们需要做一些事去让这个进入我们的scene。
1、 加载.3ds文件(它是3DS MAX的格式,相当标准)
2、 转换为.jme(jME二进制格式)
3、 加载二进制.jme格式到scene graph
很明显,每次都将.3ds转为.jme不是必须的。可以优化,我们将在发布游戏之前先这么做,那么以后只需加载.jme。这在下节课的优化将进行更深入的讨论。
加载模型的第一个任务是使用jME的Model Conversion工具。有一些被支持的格式(md2、md3、milkshape、3ds、obj、ase),选择和你的格式对应的Converter。这个converter创建一个.jme二进制文件。转换需要2件事:文件的InputStream,用于保存.jme的OutputStream。所以,我们将创建一个MaxToJme converter和一个ByteArrayOutputStream对象。我们接着创建一个指向我们模型的URL(我们将使用这个URL去获取InputStream)。
调用converter的convert方法将使用jme文件的数据填充我们的ByteArrayOutputStream。
我们现在拥有能加载进scene graph的.jme数据。使用BinaryImporter我们能调用它的load方法去创建一个Node。这个Node将被用于替换vehicle中的box。
如果你继续工作你可能会注意到,model有点大…,事实上,它太巨大了。手动使用你在网上找到的模型并没有精确为我们的游戏制作。所以,我们需要缩放它到适合我们的terrain。事实上,我们将缩放很多(0.0025%)。
我们现在已经加载了bike(将buildPlayer中的box创建部分换成下面)。
Node model = null;
try {
MaxToJme C1 = new MaxToJme();
ByteArrayOutputStream BO = new ByteArrayOutputStream();
URL maxFile = Lesson7.class.getClassLoader()
.getResource("res/bike.3ds");
C1.convert(
new BufferedInputStream(maxFile.openStream()),
BO
);
model = (Node)BinaryImporter.getInstance().load(
new ByteArrayInputStream(BO.toByteArray())
);
//缩放它,让它比原来更小
model.setLocalScale(.0025f);
model.setModelBound(new BoundingBox());
model.updateModelBound();
} catch (IOException e) {
e.printStackTrace();
}
最后,用它代替在vehicle的box位置:
player = new Vehicle("Player Node",model);
我们现在有一个cool的bike用于驾驶。
另一个轻微的改变是我们将在增加model之后update实体scene。这是因为Model是由许多Node(或scene graph中的分支)组成的。由于这样,这个能影响到实体树BoundingVolumes的合并。所以,我们将调用updateGeometricState去重建scene:
scene.updateGeometricState(0, true);
我们已经确实地为我们的游戏添加了一些视觉上具有吸引力的东西。
terrain看起来更好,我们驾驶一辆真正的交通工具而且它精确紧跟着terrain。下一步,我们将再优化一点:保存一个.jme文件并加载,从而不必每次都转化,让bike在转弯时倾斜和车轮旋转。我们将介绍终点Flag,所以我们将有一个目标!继续观看!