分享
 
 
 

3D编程指南第四部分:M3G内建碰撞、光照物理学和照相机视点

王朝other·作者佚名  2006-02-01
窄屏简体版  字體: |||超大  

作者:mydeman 文章来源:J2ME开发网

现在我们来到来自Mikael Baros的“使用M3G(JSR184)进行移动3D编程”的系列指南的第四部分,Mikael Baros是Redikod的高级程序员。在前三部分的基础上,他将带你进入碰撞和照相机视点的世界。在讲解了一些理论之后,他将指导你从头到尾创建一个3D乒乓球游戏。

下面是指南的前三部分的链接:

l 第一部分:快速进入移动JAVA 3D编程世界

l 第二部分:光的3D理论与定位

l 第三部分:粒子系统和立即模式渲染

好,让我们一同来看激动人心的第四部分。

介绍

欢迎来到M3G指南系列的第四部分。这次我将教你一些3D游戏中有关动力学的重要知识,也就是碰撞和物理学。我还将向你展示照相机的视点矩阵是如何工作的,以及通过操纵它你可以完成什么工作。

和以前一样,无论什么时候你感到了困惑就参考这里。

首先,或许也是最重要的,就是在索尼爱立信开发者世界上专业的移动Java 3D网络区。其次,如果你碰到困难,就去索尼爱立信移动Java 3D论坛。对于其他的任何情况,使用索尼爱立信开发者世界网络门户,在那里你可以找到你的问题的答案,并且可以了解到更多。

在这个指南系列的第四部分有很多目标,因此我将把它们分解开来为你讲解。下面是通过这篇指南你应该可以掌握的:

l 学习视点矩阵和照相机操作

l 学习M3G内建的快速碰撞检测机制

l 对如何更改游戏使其更具有现实的感觉有一个基本的理解

l 使用3D实现一个十分简单的类似乒乓球的游戏

也许有点多了?不,实际不是这样的。你将会看到今天我讲解的大部分内容一旦你掌握了,实际上十分简单。这篇指南将会由大量的理论开始,但是当我们接触到实际的游戏时,理论就会变的少起来。

因为文中的代码是作为演示使用的,所以它不是最优化的,并且它也没有包括所有可能发生的错误。稍后将会有更高级的话题。

你应该了解的

在阅读这篇指南以前,我希望你具有3D数学的基本知识,特别是关于向量和矩阵操作。当然你也应该已经阅读了前三篇指南,正好开始了解M3G API和它的渲染方法。

照相机和视点

现在,你可能想知道我们讨论的视点是什么?视点矩阵在技术上是一个数学实体,在3D引擎中它定义了一些参数,这些参数可以改变我们观察场景的方式。但是,此后我会解释它的原理,而不是纯粹的数学知识。

当我们需要在M3G中观察一个3D对象时,我们就在空间中一个定义好的位置上放置一个照相机,并且从那里根据一个特定的视野观察我们周围的世界。例如,人类和鸟具有不同的视野。一些鸟可以具有高达360度的视野,而人类通常在150-180度之间。现在,在3D游戏中视野或者FOV(从现在开始我们将这样称呼,所以请记住它!)是对照相机(认为是玩家)如何观察我们世界中的对象的一个基本的描述。今天我将讨论的参数有四个。这些参数都是在M3G中调用Camera.setPerspective方法时所需要的同样类型的参数。这个方法如下:

setPerspective (float fovy, float aspectRatio, float near, float far)

Y轴视野,或者简称为fovy

大多数的API为了能够构造一个视点矩阵,都需要一个以度数表示的视野。M3G也不例外。setPerspective方法的第一个参数是一个float型fovy。fovy代表在y轴上的视野。正常情况下,人们关心的是在x轴上的视野(或者是多宽)。然而,M3G感兴趣的是你可以看多高。它的正常值时在45~90度之间的范围内,具体的值依赖于你的计划。如果你很难做出选择,就坚持使用60度。

那么fovy做什么呢?它只是告诉照相机在什么角度上的对象,在渲染的时候应该被抛弃。例如,如果一个对象在从照相机向上y轴110度,而我们的fovy只有60度,那么只有我们的照相机向上倾斜足够的角度时才能看到它(大概是50度)。为了使你的3D游戏视觉效果看起来更符合实际,选择合适的fovy值是一个至关重要的部分。一个太小的fovy使得除了正前方的对象以外什么都看不到,然而一个过大的fovy又使得可以看到比一个正常人的习惯视野多得多的对象,因此又太不切实际。在这篇指南中,你将会看到如何操作fovy并根据你的需要来改变游戏的视野。

屏幕高宽比

这是一个相当简单的参数,它是一个分数,告诉引擎当前屏幕的宽和高的关系。大多数计算机屏幕的比例是4:3(也就是高是宽的0.75倍),然而正常的移动电话屏幕有很多种不同的比例。要得到这个变量的值,你需要做的就是用高除当前屏幕的宽。

关于这个参数,我不再赘述了,因为它是自明的。为了能够在显示图形时不会发生不自然的歪斜,你就必须要知道你的电话屏幕的比例。

近截面和远截面

另外一个非常简单的参数。近截面和远截面定义多近/多远的一个对象依然可以被渲染。那么例如,设置近截面为0.1和远截面为50,意味着所有距离照相机小于0.1单位的对象将不会被渲染。同样所有距离照相机大于50单位的对象也不会被渲染。实际上,0.1和50是相当正常的值,当然,在游戏中你可以改为任何需要的值。它涉及到你怎样测量你的游戏以及所使用的单位。在这篇指南中,我们将使用0.1作为近截面,50作为远截面。

为什么,为什么,我需要知道这个呢?

嗯,对于以上的问题有很多种回答,但是我将先回答最显而易见的。因为M3G中默认的照相机(也就是你没有从M3G文件中抽取)不含有一个定义好的实际视点矩阵。如果你试图仅仅使用默认的照相机(Camera cam = new Camera())在屏幕渲染几何图形,你会很惊讶。 如果你想使用自己的照相机(90%的情况的你会愿意使用,特别是在使用立即模式渲染时),你应该定义自己的视点矩阵。另外,了解视点矩阵还有一个好处,你可以仅仅通过操作视点参数实现一些相当酷的效果。

那么,我们如何创建一个纯净的、全新的照相机并且为它指定一个视点矩阵呢?如下:

float ar = (float)getWidth() / getHeight();

Camera cam = new Camera(); // 这是我们的全新的照相机

cam.setPerspective( 60.0f, // fovy

ar, // 屏幕高宽比

0.1f, // 近截面

50.0f ); // 远截面

倒不是很难,不是吗?实际上,在这篇指南的源码一个叫做loadCamera的方法中你将会看到一个与之十分相近的代码片断。稍后你可以看到它。

三维空间中的移动对象

要移动对象,事实上你不需要做太多工作,并且你可能有一个如何实现的好主意。我将通过一些十分轻量级的和简单的物理方法在3D空间中移动对象。

正常地,当一个对象在3D空间移动时,你需要计算得到三个不同的速率:x、y和z的速率。然而,大多数的游戏可以将x和z速率合成一个,就是前进速率。(例如,如果你的前进速率是1.0,那么你要做的就是使用正弦和余弦分别得到x和z轴上速率)在这篇指南中,我们将不作这样的区分,代替的是,我们将使用三个相区别的速率来描述一个在3D空间中移动的对象。

完成这个最简单的方法,就是通过使用三个向量。

1、 坐标向量

第一个向量是一个对象的坐标向量。这是一个至关重要的向量,它通过保持对象的x、y和z坐标的轨迹保存了对象在3D空间的当前位置。否则,你如何知道在什么地方渲染这个对象呢?

2、 速率向量

第二个向量是速率向量。它定义了对象的坐标体系在下一次移动时需要移动多少个单位。因此它也需要x、y和z三部分。基本上你需要做的就是,每次把x速率加到x坐标上,y速率加到y坐标上,z速率加到z坐标上。那么,如果你有一个太空船,每帧在y轴上径直向上一个单位,那么速率向量就是(0,1,0)。

3、 加速度向量

最后一个向量是加速度向量。虽然在这里我们用不到,但是它仍然是很重要的。加速度向量工作原理和速率向量一样,但是它作用的是速率向量而不在坐标系上。这意味着加速度向量不直接操作坐标系,而是在每帧增加速率,给玩家一个加速的感觉。正常情况下,一个物体不可能从停顿状态立即达到最高速度。这需要时间,并且为了使一个3D游戏更接近于现实,你需要加速度。假定,上面例子中的太空船想从每帧1个单位开始移动并且加速到每帧5个单位。它可能具有一个速率向量(0,1,0)和一个加速度向量(0,0.1,0),这也就意味着在40帧后,太空船将会达到每帧5个单位的速率。

移动实例

现在,让我们把太空船实例放在代码中。它看起来可能像这样:

float[] spaceShipCoords = {0, 0, 0};

float[] spaceShipVel = {0, 1, 0};

float[] spaceShipAcc = {0, 0.1, 0};

while(gameLoopIsRunning)

{

for(int i = 0; i < 3; i++)

{

spaceShipCoords[i] = (spaceShipVel[i] += spaceShipAcc[i]);

}

}

看起来多么简单?在每一个游戏循环中我们都将加速和移动我们的太空船。

碰撞

三维空间中的对象通常是不透明的,并且会和另外一个对象发生碰撞。一个彗星可能撞击一个行星,一个(相当可怜)司机可能会驾车闯进一幢大楼。然而,在我们的3D游戏中,它并不像真实世界中那么明显。这是因为,迄今为止,我们只渲染了对象,并且渲染的对象也仅仅是被转化为屏幕上像素的3D模型的数学表示。我们还没有考虑过两个对象之间有多少不同。那么如果移动太空船到一些可能存在的行星中,它将会继续穿过这个行星,然后出现在另外一面。所有这些都是因为我们没有碰撞。现在我们将改变这个状况。

光线交叉的碰撞

光线交叉是一种十分直观和简单的碰撞检测形式。至少对常规和简单的碰撞情况是这样。它的工作原理是:你希望检测碰撞的对象从它的中心(或者身体的其它位置)在一个给定的方向上(通常是对象的速率向量,但是就像你将要看到,当然不一定一直是这种情况)放射出光线。光线在你的3D世界中传播,直到它实际撞到某些物体上(像激光一样)。当它撞上某些物体时,就报告碰撞,并且告诉你在它撞上一个物体前还有多少远可以移动。根据这个距离,你将决定这个碰撞是否是会实际发生。大多数是因为如果你知道在前面几千英尺有一个建筑物,就意味着你不一定要撞上它。(嗯,你可能会撞上,但是你仍然可以确定现在还没有把事情搞糟。)

M3G的方法

M3G为我们提供了一个异常简单的方式来计算碰撞。在我们的世界中的每一个Group(记住,Group只是Node的集合,它可能是在我们的世界中存在的任何事物)有一个叫做pick的方法。它就是为我们完成所有碰撞的方法。它是这样工作的,假如说你将为下一代高速的帧速率(FPS)移动电话上开发,并且你想通过检查在一个房间中你的主人公是否和一面墙发生碰撞开始,或者是否和这个房间中一个怪物发生碰撞。那么你需要做的就是创建两个Group。一个为所有的墙,一个为所有的怪物。然后,你只需要简单的向每一组投射光线,从人物出发并且朝着它的方向,然后看你是否会与某些对象发生碰撞。

M3G根据两个组成部分处理游戏的碰撞部分。pick方法和RayIntersection类。

RayIntersection类

这是一个相当简单的类,只保存了对3D世界中光线投射至关重要的信息。今天我们将要使用的方法(对于轻量级碰撞可能是唯一需要的方法)是getDistance方法。这个方法如下:

public float getDistance()

它和看起来一样简单。它返回特定的光线与这个对象发生碰撞的距离,由你提供给pick方法(在后面将有更多讲解)的方向向量测量。所以仅仅通过完成一个简单的检测,你就可以明白你是否和一个Group中的一个对象距离足够近,以确定你是否产生碰撞。来看下面的代码片断:

// 在我们的游戏中默认的碰撞距离

float collisionDistance = 0.1f;

// 这个方法返回一个光线,它会或者不会与墙发生碰撞

RayIntersection ray = checkCollisionWithWalls();

// 现场距墙的距离 (如果没有碰撞, 这个距离将会很大)

if(ray.getDistance() < collisionDistance)

{

// 得到发生碰撞的墙

Node wall = ray.getIntersected();

//完成其他事情...

}

看,一点也不难!上面就是RayIntersection中的所有奥秘。现在我们看看如何实际得到填充这个类的碰撞信息。

Group.pick方法

为了在一个RayIntersection类中填充必要的信息,你需要使用pick方法(除非你想要伪造碰撞并且填充你自己的信息。或许你想实现自己的光线交叉算法?)。pick方法有两种不同的形式,在进行下一步讲解之前,先看pick方法的两种形式:

public boolean pick(int scope, float ox, float oy, float oz, float dx, float dy, float dz, RayIntersection ri)

public boolean pick(int scope, float x, float y, Camera camera, RayIntersection ri)

首先我们讨论它们的类似之处。它们都需要一个范围。这个范围定义了哪些对象被认为是可以和光线碰撞的。在M3G API中的每一个Node都有一个范围,所以如果你为pick方法传递值1作为范围参数,那么所有节点的范围都等于1。通过提供值-1,你可以对那个组中的所有节点做相反的测试。有时,给一个-1值是有害的,因为你不想你的算法计算和某些对象的碰撞,由于你已经知道你的对象和这些对象并不接近。算法需要做的就是一直设法减少工作量。

第二个也是最后一个共有的就是一个RayIntersection对象。这是因为万一有碰撞,它们将填充提供的RayIntersection类。

现在,我们看第一个方法。这个方法是最常使用的,因为它允许你定义光线的一个起点。ox、oy和oz是原点向量的三个部分。也就是说,这三个值定义了光线在空间中的出发点。通常,你将这个值设置为你的对象碰撞的中心。接下来的三个部分,dx、dy和dz,组成了方向向量。这个向量决定光线的传播方向。你会一直想把它作为单位向量(长度为1的向量),因为pick方法根据你的方向向量的长度测量getDistance报告的距离。有时,这是必须的,但是多数情况下你不需要它,因此只需要确定你的方向向量的长度为1。

那么,第一个方法在代码中是什么样子呢?

// 得到墙面所在的Group

Group walls = getWallGroup();

// Create a RayIntersection object for the method to fill out

RayIntersection ri = new RayIntersection();

// Get coordinates of our character

float[] coords = getPlayerCoords();

// Get direction of our character (where is his nose pointing?)

float[] dir = getPlayerDirection();

// Try colliding (method returns true if collision has taken place)

if(walls.pick(-1, coords[0], coords[1], coords[2], dir[0], dir[1], dir[2], ri))

{

// We've intersected something, check for distance

if(ri.getDistance() < acceptableDistance)

{

// The player has collided with a wall. Make him explode...

// Or something.

}

}

看,它并不像你想象的那样令人恐慌?它实际上相当地简单,只定义了你从什么地方发射光线以及光线的方向。M3G为你完成了剩下的工作。难道我们不受宠若惊吗?

现在你也许感到疑惑,另一个方法做什么呢?它有自己的用处,并且它的工作方式也有些不同。我不会对它进行讲解,因为在这篇指南中我们没有用到它。然而,我特别提醒,你可以要阅读M3G API文档中关于它的部分,来了解它的用法。

Getting down and dirty

现在,现在我将教你一些如何利用它们的小窍门。在我的脑海里有一个游戏,在复杂程度上刚好适合我们。我们开始3D 乒乓球。我们不需要两个玩家,然而,你将在屏幕和在屏幕内的球拍中间使球来回跳跃。游戏的场地将会由四面墙组成,你的球将会和它们发生撞击而反弹(啊哈,碰撞!)。现在我们将要做的就是将这个简单的想法转化为一些关键的部分。

游戏场地和球拍

我们游戏场地,像我已经告诉你的那样,由一个球拍和四面墙组成。我们将会使用和指南第三部分中完全一样的代码建立平面,所以,现在将会唤醒你的记忆的好时机。

球拍和墙全部是经过贴图的平面。为了可以区别,球拍和墙将会使用不相同的纹理。所有这些将会有MeshFactory.createPlane方法创建。

这是创建球拍的代码片断,使用了我们在指南第三部分中已经建立的MeshFactory类。

// Create a plane using our nifty MeshFactory class

paddle = MeshFactory.createPlane("/res/paddle.png", PolygonMode.CULL_BACK);

这里没有什么难的,到现在为止它应该是非常常见的。球拍当然需要一个转换,我们可以利用它移动球拍。我们称之为trPaddle,初始化方法如下:

// 设定球拍在它的初始位置

trPaddle = new Transform();

trPaddle.postTranslate(paddleCoords[0], paddleCoords[1], paddleCoords[2]);

paddle.setTransform(trPaddle);

paddleCoords变量是一个浮点向量,定义了当前球拍的位置。将它赋给trPaddle一次,球拍就移动一次。

现在,为了确定的是我们的球拍将会发生碰撞,我们需要设置一些变量。首先,我们需要一个通用的方式可以在我们使用RayIntersection.getIntersected方法时识别我们的球拍。它返回一个Node类,还记得吗?那么,现在是使用每一个Object3D的userID变量的大好时机。我创建了一些变量来定义墙和球拍,如下:

// Wall 常量

public static final int TOP_WALL = 0;

public static final int LEFT_WALL = 1;

public static final int RIGHT_WALL = 2;

public static final int BOTTOM_WALL = 3;

public static final int PADDLE_WALL = 4;

public static final int PLAYING_FIELD = 5;

有了那些变量,我们可以简单地对球拍调用setUserID,并且给它PADDLE_WALL常量。最后我们需要做的事是确保我们的球拍可以被拾取。还记得碰撞的方法叫做pick吗?所有的对象在那个方法中都会被检测两种东西。第一个是范围(scope)(我们将会把它设置为-1,所以不用担心它),第二个是可以拾取标志。如果一个对象是不能被拾取的,它将完全不被包含在碰撞计算之内。这个标志可以通过对球拍对象调用setPickingEnabled方法简单地开关。设置userID和拾取标志的示例如下:

// Make sure it's collidable

paddle.setPickingEnable(true);

paddle.setUserID(PADDLE_WALL);

在createPaddle方法的结尾还有一些代码,在稍后一点讨论球的反弹时再详细说明那段代码。

我们使用一种非常类似的方式创建四面墙,所以我不再重复代码。如果你想知道是如何完成的,就查看源码中的createWalls方法。

球(ball)

有些人会说球时乒乓球中最重要的部分,也有人说是球拍。我认为它们是同等重要的。现在球需要做什么呢?首先,它应该是一个Mesh,可以在屏幕上渲染(实际上一个球的模型)。其次,它应该具有足够的物理属性,这样就可以在3D空间中移动和加速。最后,穿越3D世界时,它应该可以计算自己的进程。

反弹

在我们十分简单的例子中,当球离开墙面时我们不需要任何过难的理论。然而,无论如何我还是给你一些理论,以防万一,你决定使游戏中的墙面旋转,或者尝试将游戏变得更真实并且在非正则曲面上反弹。现在,如何构造一个反弹(反射)向量?我们需要做的是,在球撞击墙面时,仅仅地反射它的方向向量。我们还必须在正确的平面上反射(如果和右面的墙发生碰撞,那么在xz平面上反射对我们没有什么好处)。这些都可以由简单向量数学进行管理。我不会讲解过多的数学理论,因为我希望你有3D数学的背景,由于你正在尝试建立一个3D游戏。

首先,我创建了一个VectorOps类,在你创建以及测试向量和跳跃时应该是有帮助的。它持有一些非常基础的操作,在对向量操作可能用到,例如在平面上的投影、点积和叉积、规一化、计算长度,等等。我还添加了反射方法。这个方法会对给定的平面(在这种情况下,平面只需要通过它的法线向量定义)反射一个向量。

那么反射的工作原理是什么呢?事实上这相当简单。首先,映射源向量(v)到平面(n)的法线上。然后,通过一些简单的向量数学运算,你会认识到通过使用映射向量(p)可以构造一个两倍长度的新向量(u)。现在新向量u和源向量v可以组合形成反射向量。怎样组合?看这张图片。

你可以很容易的看到我们要求的反射向量v可以从这张图上计算。如果你想知道如何完成的,就参考VectorOps的mirror方法。

这就意味着在这游戏中,我们不得不保存每一面墙(和球拍)的法线向量。这一点也不困难,因为我们只有四面墙和一个球拍,但是对更为复杂的工程这可能是一个问题。不过,我们将会使用组成平面的两个向量的叉积构造平面的法线向量。(还记得3D数学?每一个平面可以通过两个向量描述。通过计算这两个向量的叉积可以得到平面的法线向量)

在我们的应用中,我们进行反射计算是只需要使用法线向量,并且不需要担心定位和其它在计算法线时你认为应该考虑到的事情。这也意味着左边和右边的墙将会拥有完全相同的法线,顶部和底部的也是一样。法线在计算后放进一个浮点型的数组里,于是我们的球就可以利用它们计算反弹。这个数组叫做wallVec,这里是展示如何计算左边墙的法线向量和放入wallVec中的代码片断。

float[] v = VectorOps.vector(0.0f, 1.0f, 0.0f);

float[] u = VectorOps.vector(0.0f, 0.0f, 1.0f);

float[] normVec = VectorOps.calcNormal(v, u);

wallVec[LEFT_WALL][0] = v;

wallVec[LEFT_WALL][1] = u;

wallVec[LEFT_WALL][2] = normVec;

没有任何不可思议的!像所有你知道的那样,左边的墙实际上是yz平面将一些单位转化在x轴上,这就是为什么它只有y和z向量组成。在向量创建后,通过调用calcNormal方法计算法线向量。它只是计算提供的两个的叉积,然后通过规一化将向量变为单位向量。

上面的操作会对游戏场地中的所有墙进行,但是照相机所在的墙除外(如果你愿意的话,就是玩家)。那个碰撞的完成方式将会不同(并且是一种更加简单的方式)。

敏锐地读者和程序员会立即发现上面的方法对这个简单的应用太麻烦。因为我们的墙面时xz、xy和zy平面,我们可以只使用一个分支if语句检查和哪一个墙面发生了碰撞,然后反射这面墙代表的坐标。然而,我想给你一个更加优雅和通用的解决方法。上面的解决方法适用于任何墙面,无论它们怎样变换。像我已经说过的,这不是一个最优化的指南,所以如果你想程序跑得更快,去掉反射计算和使用嵌套的if子句。

实现

因为我已经给你了指导方针,所以我将向你展示球的渲染方法的实现,这个方法不仅是渲染,还有移动和碰撞检测。我们先看一些开始的代码:

// 清除变换

trBall.setIdentity();

// 检查是否在移动

if(moveVec != null)

{

// 首先将球旋转一点

//trBall.postRotate(rotated += 1.0f, 1.0f, 1.0f, 1.0f);

// 规一化移动向量

ri = new RayIntersection();

float[] nMove = VectorOps.normalize(moveVec);

如你所见,开始的一些代码行相当地简单明了。这里我们完成的,首先清除球的Transform类(trBall)中的所有转换信息。接着,我们简单地作一个移动检测(游戏由球静止开始)。球还有一个rotated变量,它保存了球已经旋转了多少,以给出旋转和飞行的球的印象。这个变量在postRatate方法中使用,这个方法现在应该是你熟记的。在这之后,有趣的部分就开始了!我们分配为碰撞将要用到的RayIntersection对象,规一化球的速率向量(moveVec)。是否记得为什么要规一化?因为pick方法一直通过方向向量测量交叉点的距离。如果方向向量的长度是1,交叉点的距离就不需要测量。这就是我们需要的。我们继续这个方法,现在是碰撞:

// 看是否有碰撞

if(walls.pick(-1, coords[0], coords[1], coords[2], nMove[0], nMove[1], nMove[2], ri) && ri.getDistance() <= 0.5f)

{

//发生碰撞, 得到交叉的平面

Node n = ri.getIntersected();

// 通过反射纠正移动向量

moveVec = VectorOps.mirror(moveVec, wallVectors[n.getUserID()][2]);

那就是对Group.pick的调用。如你所见,它在一个叫做walls的组(group)上执行,这个组实际上传递给球的渲染方法的playingField组。所以这个walls组既包含了所有的墙面也包含了球拍。我们检查pick方法是否返回true和与对象的距离是否小于0.5——在这个应用中它是一个临界的距离。

如果发生了碰撞,我们首先取得交叉的节点。这是walls组中的墙面之一。然后得到墙的userID。还记得它们吗?我们使用它们作为在wallVec数组中存储墙面的向量的索引。这里通过对Node调用getUserID方法得到它们。当我们有了法线向量时,调用mirror方法在交叉的墙面周围反射球的当前方向向量。这就是所有!

可是上面的并不上那么有趣。因为我们对所有的墙都是一样对待,球也将会平平地在球拍反弹,将会给我们一些乏味和静态游戏播放的感觉。这就是为什么我们使求和球拍的碰撞有一点点不同。参考下面的代码片断:

// Let user have contol over the movement by moving the paddle

if(n.getUserID() == M3GCanvas.PADDLE_WALL)

{

// Add extra speed to a maximum amount depending on ball/paddle position

float distX = (paddleCoords[0] - coords[0]) / 10.0f;

float distY = (paddleCoords[1] - coords[1]) / 10.0f;

moveVec[0] = Math.max(-0.3f, Math.min(moveVec[0] - distX, 0.3f));

moveVec[1] = Math.max(-0.3f, Math.min(moveVec[1] - distY, 0.3f));

// After 30 bounces it should be impossibly fast (HAH! A challenge!)

moveVec[2] = moveVec[2] + 0.01f;

// Increase number of bounces

bounces++;

}

那么这里我们将要完成什么呢?首先,我们通过比较Node的userID和PADDLE_WALL常量检查是否将在球拍上反弹。然后,使用简单的数学方法计算从球的中心到球拍的中心的距离。我们是用这个结果偏斜作为结果的方向向量以创建更加动态的游戏效果。

另外一件有趣的事情就是在每一次反弹时使球移动的更快。如你所知,这个游戏中的速度可以和沿着z轴的速率一样简单,所以每次对于球拍,我们加速z轴方向的速度。

最后一件事,增加反弹的变量,仅仅用作计分。最好的玩家有最多的反弹数。一个相当简单的概念,但是它可以工作。

现在,我们几乎已经看到Ball.render方法中的所有,除了对于照相机所在墙面(或者玩家的前端)的反弹。这个由接下来的代码片断完成。我不再解释它,因为通过观察你应该可以指出它完成了什么。

// Check for bouncing against screen

if(coords[2] >= -0.7f)

{

// Flip over the screen (same as for paddle)

moveVec = VectorOps.mirror(moveVec, wallVectors[M3GCanvas.PADDLE_WALL][2]);

}

这就是全部。实际并没有像我们讨论的那么多,不是吗?

视点和tube-view

我保证在这篇指南里使用有些不同的视点计算,并且使游戏看起来有些特殊。这就是我们调整FOV变量完成的。我们改变fovy到一个很大的数字,因此产生一个奇怪的切变效果,在这个上下文中看起了十分不错!我曾经使用130度(M3G允许的最大值是180)因为它产生俯视一个非常长的和扩展的管道。你可以在源代码中更改这个值看看它会给你带来什么样的切变效果。

游戏循环

现在,只有一件最后的事情,那就是实现游戏循环。如今闭着眼、把手绑在背上,你应该也可以完成,但是我将再一次讲解一下语义。首先看整个游戏循环:

private void draw(Graphics g)

{

// 将所有包含在 try/catch 块中,以防万一

try

{

// 得到Graphics3D 上下文

g3d = Graphics3D.getInstance();

// 首先绑定 graphics 对象. 使用预先定义的渲染提示.

g3d.bindTarget(g, true, RENDERING_HINTS);

// 清除背景

g3d.clear(back);

// Bind camera at fixed position in origo

g3d.setCamera(cam, trCam);

// Render the playing field and ball

g3d.render(playingField, identity);

ball.render(g3d, playingField, wallVec, paddleCoords);

// Check controls for paddle movement

if(key[UP])

{

paddleCoords[1] += 0.2f;

if(paddleCoords[1] > 3.0f)

paddleCoords[1] = 3.0f;

}

if(key[DOWN])

{

paddleCoords[1] -= 0.2f;

if(paddleCoords[1] < -3.0f)

paddleCoords[1] = -3.0f;

}

if(key[LEFT])

{

paddleCoords[0] -= 0.2f;

if(paddleCoords[0] < -3.0f)

paddleCoords[0] = -3.0f;

}

if(key[RIGHT])

{

paddleCoords[0] += 0.2f;

if(paddleCoords[0] > 3.0f)

paddleCoords[0] = 3.0f;

}

// Set paddle's coords

trPaddle.setIdentity();

trPaddle.postTranslate(paddleCoords[0], paddleCoords[1], paddleCoords[2]);

paddle.setTransform(trPaddle);

// Quit if user presses fire

if(key[FIRE])

ball.start();

}

catch(Exception e)

{

reportException(e);

}

finally

{

// Always remember to release!

g3d.releaseTarget();

}

// Do some old-fashioned 2D drawing

if(!ball.isMoving())

{

g.setColor(0);

g.drawString("Press fire to start!", 2, 2, Graphics.TOP | Graphics.LEFT);

}

else

{

int red = Math.min(255, ball.getBounces() * 12);

g.setColor(red, 0, 0);

g.drawString("Score: " + ball.getBounces(), 2, 2, Graphics.TOP | Graphics.LEFT);

}

}

那么,第一步就是立即模式渲染填充。(如果你记不起来了,就参考指南第三部分)。

l 得到Graphics3D实例

l 绑定到Graphics对象

l 清除背景

l 设置和更新照相机

接下来要做的也相当简单。我只需要渲染整个游戏场地(playingField)组,包括所有墙面和它们的变换。使用这种方式,我们将渲染的五个实体压缩到一个组里。使用场景图有利于编写简洁的代码。

在渲染了游戏场地之后,调用Ball.render方法,给它传递必要的值,它就完成剩下的工作。我们已经从头至尾看过这个方法,这里就不再赘述。

接下来我们检查控制。这里也没有大不了的。我们使用游戏杆移动球拍,通过FIRE键重置球的位置。在完成了所有3D图形,我们做一些老样式的2D绘制,因为我们希望用户可以立即知道他们的得分和球是否出界了(这时需要重置球的位置)。

这就是整个墨西哥菜,因为快餐时代的人们很可能这样说。现在,你已经完全见识了游戏中最复杂的工作,这是一些运行中屏幕截图。

你可以看到130度fovy值的给我们一个相当有趣的隧道效果。另外,别忘了这个游戏实际上是相当好玩。再进行一点点改进,你就会使它成为一个真正使人上瘾的游戏。作为一个联系,你可以尝试使游戏场地的墙面旋转!切记,为了每一次旋转墙面时作出适当的反射,你不得不旋转存储在wallVec数组中的所有法线向量。

TutorialMidlet

import javax.microedition.lcdui.Command;

import javax.microedition.lcdui.CommandListener;

import javax.microedition.lcdui.Display;

import javax.microedition.lcdui.Displayable;

import javax.microedition.midlet.MIDlet;

import javax.microedition.midlet.MIDletStateChangeException;

public class TutorialMidlet extends MIDlet implements CommandListener

{

// A variable that holds the unique display

private Display display = null;

// The canvas

private M3GCanvas canvas = null;

// The MIDlet itself

private static MIDlet self = null;

/** Called when the application starts, and when it is resumed.

* We ignore the resume here and allocate data for our canvas

* in the startApp method. This is generally very bad practice.

*/

protected void startApp() throws MIDletStateChangeException

{

// Allocate

display = Display.getDisplay(this);

canvas = new M3GCanvas(30);

// Add a quit command to the canvas

// This command won't be seen, as we

// are running in fullScreen mode

// but it's always nice to have a quit command

canvas.addCommand(new Command("Quit", Command.EXIT, 1));

// Set the listener to be the MIDlet

canvas.setCommandListener(this);

// Start canvas

canvas.start();

display.setCurrent(canvas);

// Set the self

self = this;

}

/** Called when the game should pause, such as during a call */

protected void pauseApp()

{

}

/** Called when the application should shut down */

protected void destroyApp(boolean unconditional) throws MIDletStateChangeException

{

// Method that shuts down the entire MIDlet

notifyDestroyed();

}

/** Listens to commands and processes */

public void commandAction(Command c, Displayable d) {

// If we get an EXIT command we destroy the application

if(c.getCommandType() == Command.EXIT)

notifyDestroyed();

}

/** Static method that quits our application

* by using the static field 'self' */

public static void die()

{

self.notifyDestroyed();

}

}

M3GCanvas

import java.io.IOException;

import javax.microedition.lcdui.Graphics;

import javax.microedition.lcdui.game.GameCanvas;

import javax.microedition.m3g.Background;

import javax.microedition.m3g.Camera;

import javax.microedition.m3g.Graphics3D;

import javax.microedition.m3g.Group;

import javax.microedition.m3g.Light;

import javax.microedition.m3g.Loader;

import javax.microedition.m3g.Mesh;

import javax.microedition.m3g.Object3D;

import javax.microedition.m3g.PolygonMode;

import javax.microedition.m3g.Transform;

import javax.microedition.m3g.World;

public class M3GCanvas

extends GameCanvas

implements Runnable {

// Thread-control

boolean running = false;

boolean done = true;

// If the game should end

public static boolean gameOver = false;

// Rendering hints

public static final int STRONG_RENDERING_HINTS = Graphics3D.ANTIALIAS | Graphics3D.TRUE_COLOR | Graphics3D.DITHER;

public static final int WEAK_RENDERING_HINTS = 0;

public static int RENDERING_HINTS = STRONG_RENDERING_HINTS;

// Key array

boolean[] key = new boolean[5];

// Key constants

public static final int FIRE = 0;

public static final int UP = FIRE + 1;

public static final int DOWN = UP + 1;

public static final int LEFT = DOWN + 1;

public static final int RIGHT = LEFT + 1;

// Global identity matrix

Transform identity = new Transform();

// Global Graphics3D object

Graphics3D g3d = null;

// The background

Background back = null;

// The global camera object

Camera cam = null;

// The playing field

Mesh paddle;

Group playingField;

Ball ball;

// Transforms

Transform trLeftWall, trRightWall, trTopWall, trBottomWall;

Transform trPaddle;

Transform trCam = new Transform();

// Paddle's coords

float[] paddleCoords = {0.0f, 0.0f, -5.0f};

// Wall constants

public static final int TOP_WALL = 0;

public static final int LEFT_WALL = 1;

public static final int RIGHT_WALL = 2;

public static final int BOTTOM_WALL = 3;

public static final int PADDLE_WALL = 4;

public static final int PLAYING_FIELD = 5;

// Vectors for our walls

// Explanation: Each wall holds two vectors that

// define the plane (See linear algebra) and

// the wall's normal vector.

float[][][] wallVec = new float[PLAYING_FIELD][3][3];

/** Constructs the canvas

*/

public M3GCanvas(int fps)

{

// We don't want to capture keys normally

super(true);

// We want a fullscreen canvas

setFullScreenMode(true);

// Create our playing field

createField();

// Load our camera

loadCamera();

// Load our background

loadBackground();

// Set up graphics 3d

setUp();

}

/** Prepares the Graphics3D engine for immediate mode rendering by adding a light */

private void setUp()

{

// Get the instance

g3d = Graphics3D.getInstance();

// Add a light to our scene, so we can see something

g3d.addLight(createAmbientLight(), identity);

}

/** Creates a simple ambient light */

private Light createAmbientLight()

{

Light l = new Light();

l.setMode(Light.AMBIENT);

l.setIntensity(1.0f);

return l;

}

/** When fullscreen mode is set, some devices will call

* this method to notify us of the new width/height.

* However, we don't really care about the width/height

* in this tutorial so we just let it be

*/

public void sizeChanged(int newWidth, int newHeight)

{

}

/** Loads our camera */

private void loadCamera()

{

// Create a new camera

cam = new Camera();

// Set the perspective of our camera (choose a pretty wide FoV for a nifty tube effect)

cam.setPerspective(130.0f, (float)getWidth() / (float)getHeight(), 0.1f, 50.0f);

}

/** Loads the background */

private void loadBackground()

{

// Create a new background, set bg color to black

back = new Background();

back.setColor(0);

}

/** Creates our playing field. It will instantiate the ball and the three

* walls (fourth wall is the "screen"

*/

private void createField()

{

try

{

loadBall();

createPaddle();

createWalls();

}

catch(IOException e)

{

System.out.println("Loading error: " + e);

}

}

/**

*

*/

private void createWalls() {

// Create all planes with our nifty MeshFactory class (we need several for collision)

Mesh wall1 = MeshFactory.createPlane("/res/wall.png", PolygonMode.CULL_BACK);

Mesh wall2 = MeshFactory.createPlane("/res/wall.png", PolygonMode.CULL_BACK);

Mesh wall3 = MeshFactory.createPlane("/res/wall.png", PolygonMode.CULL_BACK);

Mesh wall4 = MeshFactory.createPlane("/res/wall.png", PolygonMode.CULL_BACK);

// We want nice perspective correction here

MeshOperator.setPerspectiveCorrection(wall1, true);

MeshOperator.setPerspectiveCorrection(wall2, true);

MeshOperator.setPerspectiveCorrection(wall3, true);

MeshOperator.setPerspectiveCorrection(wall4, true);

// Set the left wall at its true position

trLeftWall = new Transform();

trLeftWall.postTranslate(-4.0f, 0.0f, -5.0f);

trLeftWall.postRotate(90, 0.0f, 1.0f, 0.0f);

trLeftWall.postScale(5.0f, 5.0f, 5.0f);

wall1.setTransform(trLeftWall);

// Make its vectors

float[] v = VectorOps.vector(0.0f, 1.0f, 0.0f);

float[] u = VectorOps.vector(0.0f, 0.0f, 1.0f);

float[] normVec = VectorOps.calcNormal(v, u);

wallVec[LEFT_WALL][0] = v;

wallVec[LEFT_WALL][1] = u;

wallVec[LEFT_WALL][2] = normVec;

// Set the right wall at its true position

trRightWall = new Transform();

trRightWall.postTranslate(4.0f, 0.0f, -5.0f);

trRightWall.postRotate(-90, 0.0f, 1.0f, 0.0f);

trRightWall.postRotate(180, 0.0f, 0.0f, 1.0f);

trRightWall.postScale(5.0f, 5.0f, 5.0f);

wall2.setTransform(trRightWall);

// Same vectors as the left wall

wallVec[RIGHT_WALL][0] = v;

wallVec[RIGHT_WALL][1] = u;

wallVec[RIGHT_WALL][2] = normVec;

// Set the top wall at its true position

trTopWall = new Transform();

trTopWall.postTranslate(0.0f, 4.0f, -5.0f);

trTopWall.postRotate(90, 1.0f, 0.0f, 0.0f);

trTopWall.postRotate(-90, 0.0f, 0.0f, 1.0f);

trTopWall.postScale(5.0f, 5.0f, 5.0f);

wall3.setTransform(trTopWall);

// Make its vectors

v = VectorOps.vector(1.0f, 0.0f, 0.0f);

u = VectorOps.vector(0.0f, 0.0f, 1.0f);

normVec = VectorOps.calcNormal(v, u);

wallVec[TOP_WALL][0] = v;

wallVec[TOP_WALL][1] = u;

wallVec[TOP_WALL][2] = normVec;

// Set the bottom wall at its true position

trBottomWall = new Transform();

trBottomWall.postTranslate(0.0f, -4.0f, -5.0f);

trBottomWall.postRotate(-90, 1.0f, 0.0f, 0.0f);

trBottomWall.postRotate(90, 0.0f, 0.0f, 1.0f);

trBottomWall.postScale(5.0f, 5.0f, 5.0f);

wall4.setTransform(trBottomWall);

// Same vectors as top wall

wallVec[BOTTOM_WALL][0] = v;

wallVec[BOTTOM_WALL][1] = u;

wallVec[BOTTOM_WALL][2] = normVec;

// So we can recognize them later

wall1.setUserID(LEFT_WALL);

wall2.setUserID(RIGHT_WALL);

wall3.setUserID(TOP_WALL);

wall4.setUserID(BOTTOM_WALL);

// Make sure we can collide with them

wall1.setPickingEnable(true);

wall2.setPickingEnable(true);

wall3.setPickingEnable(true);

wall4.setPickingEnable(true);

// Add walls to field group

playingField.addChild(wall1);

playingField.addChild(wall2);

playingField.addChild(wall3);

playingField.addChild(wall4);

}

/**

*

*/

private void createPaddle()

{

// Create a plane using our nifty MeshFactory class

paddle = MeshFactory.createPlane("/res/paddle.png", PolygonMode.CULL_BACK);

// Set the paddle at its initial position

trPaddle = new Transform();

trPaddle.postTranslate(paddleCoords[0], paddleCoords[1], paddleCoords[2]);

paddle.setTransform(trPaddle);

// Make sure it's collidable

paddle.setPickingEnable(true);

paddle.setUserID(PADDLE_WALL);

// Add to the playing field

playingField = new Group();

playingField.setUserID(PLAYING_FIELD);

playingField.addChild(paddle);

// Create its vector

float[] v = {0.0f, 1.0f, 0.0f};

float[] u = {1.0f, 0.0f, 0.0f};

float[] normVec = VectorOps.calcNormal(v, u);

wallVec[PADDLE_WALL][0] = v;

wallVec[PADDLE_WALL][1] = u;

wallVec[PADDLE_WALL][2] = normVec;

}

/**

* Loads our ball

*/

private void loadBall() throws IOException

{

// Simply allocate an instance of the Ball class

ball = new Ball();

}

/** Draws to screen

*/

private void draw(Graphics g)

{

// Envelop all in a try/catch block just in case

try

{

// Get the Graphics3D context

g3d = Graphics3D.getInstance();

// First bind the graphics object. We use our pre-defined rendering hints.

g3d.bindTarget(g, true, RENDERING_HINTS);

// Clear background

g3d.clear(back);

// Bind camera at fixed position in origo

g3d.setCamera(cam, trCam);

// Render the playing field and ball

g3d.render(playingField, identity);

ball.render(g3d, playingField, wallVec, paddleCoords);

// Check controls for paddle movement

if(key[UP])

{

paddleCoords[1] += 0.2f;

if(paddleCoords[1] > 3.0f)

paddleCoords[1] = 3.0f;

}

if(key[DOWN])

{

paddleCoords[1] -= 0.2f;

if(paddleCoords[1] < -3.0f)

paddleCoords[1] = -3.0f;

}

if(key[LEFT])

{

paddleCoords[0] -= 0.2f;

if(paddleCoords[0] < -3.0f)

paddleCoords[0] = -3.0f;

}

if(key[RIGHT])

{

paddleCoords[0] += 0.2f;

if(paddleCoords[0] > 3.0f)

paddleCoords[0] = 3.0f;

}

// Set paddle's coords

trPaddle.setIdentity();

trPaddle.postTranslate(paddleCoords[0], paddleCoords[1], paddleCoords[2]);

paddle.setTransform(trPaddle);

// Quit if user presses fire

if(key[FIRE])

ball.start();

}

catch(Exception e)

{

reportException(e);

}

finally

{

// Always remember to release!

g3d.releaseTarget();

}

// Do some old-fashioned 2D drawing

if(!ball.isMoving())

{

g.setColor(0);

g.drawString("Press fire to start!", 2, 2, Graphics.TOP | Graphics.LEFT);

}

else

{

int red = Math.min(255, ball.getBounces() * 12);

g.setColor(red, 0, 0);

g.drawString("Score: " + ball.getBounces(), 2, 2, Graphics.TOP | Graphics.LEFT);

}

}

/** Starts the canvas by firing up a thread

*/

public void start() {

Thread myThread = new Thread(this);

// Make sure we know we are running

running = true;

done = false;

// Start

myThread.start();

}

/** Run, runs the whole thread. Also keeps track of FPS

*/

public void run() {

while(running) {

try {

// Call the process method (computes keys)

process();

// Draw everything

draw(getGraphics());

flushGraphics();

// Sleep to prevent starvation

try{ Thread.sleep(30); } catch(Exception e) {}

}

catch(Exception e) {

reportException(e);

}

}

// Notify completion

done = true;

}

/**

* @param e

*/

private void reportException(Exception e) {

System.out.println(e.getMessage());

System.out.println(e);

e.printStackTrace();

}

/** Pauses the game

*/

public void pause() {}

/** Stops the game

*/

public void stop() { running = false; }

/** Processes keys

*/

protected void process()

{

int keys = getKeyStates();

if((keys & GameCanvas.FIRE_PRESSED) != 0)

key[FIRE] = true;

else

key[FIRE] = false;

if((keys & GameCanvas.UP_PRESSED) != 0)

key[UP] = true;

else

key[UP] = false;

if((keys & GameCanvas.DOWN_PRESSED) != 0)

key[DOWN] = true;

else

key[DOWN] = false;

if((keys & GameCanvas.LEFT_PRESSED) != 0)

key[LEFT] = true;

else

key[LEFT] = false;

if((keys & GameCanvas.RIGHT_PRESSED) != 0)

key[RIGHT] = true;

else

key[RIGHT] = false;

}

/** Checks if thread is running

*/

public boolean isRunning() { return running; }

/** checks if thread has finished its execution completely

*/

public boolean isDone() { return done; }

}

VectorOps

/**

* A simple class that simplifies some vector math.

*/

public class VectorOps

{

/** Calculates the dot product of two vectors.

* Takes for granted that the vectors v and u exist

* in the 3-dimensional room.

*/

public static float dotProduct(float[] v, float[] u)

{

return v[0] * u[0] + v[1] * u[1] + v[2] * u[2];

}

/** Calculates the cross product of two vectors.

* Takes for granted that the vectors v and u exist

* in the 3-dimensional room.

*/

public static float[] crossProduct(float[] v, float[] u)

{

float[] newVec = {v[1] * u[2] - v[2] * u[1], v[2] * u[0] - v[0] * u[2], v[0] * u[1] - v[1] * u[0]};

return newVec;

}

/** Calculates length of vector v */

public static float length(float[] v)

{

return (float)Math.sqrt(v[0] * v[0] + v[1] * v[1] + v[2] * v[2]);

}

/** Projects vector v onto vector u */

public static float[] project(float[] v, float[] u)

{

float lenU = length(u);

float scalar = dotProduct(v, u) / (lenU * lenU);

return scalarMul(scalar, u);

}

/** Calculates the normal of the plane defined by vector v and vector u */

public static float[] calcNormal(float[] v, float[] u)

{

float[] cross = crossProduct(v, u);

return normalize(cross);

}

/** Normalizes a vector (e.g. makes its length = 1) */

public static float[] normalize(float[] v)

{

float len = length(v);

float[] newVec = {v[0] / len, v[1] / len, v[2] / len};

return newVec;

}

/** Multiplies a vector with a scalar */

public static float[] scalarMul(float scalar, float[] v)

{

float[] newVec = {v[0] * scalar, v[1] * scalar, v[2] * scalar};

return newVec;

}

/** Subtracts two vectors. v - u */

public static float[] sub(float[] v, float[] u)

{

float[] newVec = {v[0] - u[0], v[1] - u[1], v[2] - u[2]};

return newVec;

}

/** Adds two vectors. v - u */

public static float[] add(float[] v, float[] u)

{

float[] newVec = {v[0] + u[0], v[1] + u[1], v[2] + u[2]};

return newVec;

}

/** Mirrors the vector v in the plane that has the normal vector n */

public static float[] mirror(float[] v, float[] n)

{

float[] u = VectorOps.project(v, n);

return VectorOps.sub(v, VectorOps.scalarMul(2.0f, u));

}

/** Made for simple construction of a vector */

public static float[] vector(float x, float y, float z)

{

float[] v = {x, y, z};

return v;

}

/** Made for debugging. Transforms a vector to a String. */

public static String toString(float[] v)

{

return "(" + v[0] + ", " + v[1] + ", " + v[2] + ")";

}

}

MeshFactory

import javax.microedition.lcdui.Image;

import javax.microedition.m3g.Appearance;

import javax.microedition.m3g.Image2D;

import javax.microedition.m3g.IndexBuffer;

import javax.microedition.m3g.Mesh;

import javax.microedition.m3g.PolygonMode;

import javax.microedition.m3g.Texture2D;

import javax.microedition.m3g.TriangleStripArray;

import javax.microedition.m3g.VertexArray;

import javax.microedition.m3g.VertexBuffer;

/**

* Static class that handles creation of code-generated Meshes

*/

public class MeshFactory

{

/** Creates a texture plane that is alpha-blended

*

* @param texFilename The name of the texture image file

* @param cullFlags The flags for culling. See PolygonMode.

* @param alpha The alpha value of blending. Is a full color in 0xAARRGGBB format

* @return The finished textured mesh

*/

public static Mesh createAlphaPlane(String texFilename, int cullFlags, int alpha)

{

// Create a normal mesh

Mesh mesh = createPlane(texFilename, cullFlags);

// Make it blended

MeshOperator.convertToBlended(mesh, alpha, Texture2D.FUNC_BLEND);

return mesh;

}

/**

* Creates a textured plane.

* @param texFilename The name of the texture image file

* @param cullFlags The flags for culling. See PolygonMode.

* @return The finished textured mesh

*/

public static Mesh createPlane(String texFilename, int cullFlags)

{

// The vertrices of the plane

short vertrices[] = new short[] {-1, -1, 0,

1, -1, 0,

1, 1, 0,

-1, 1, 0};

// Texture coords of the plane

short texCoords[] = new short[] {0, 255,

255, 255,

255, 0,

0, 0};

// The classes

VertexArray vertexArray, texArray;

IndexBuffer triangles;

// Create the model's vertrices

vertexArray = new VertexArray(vertrices.length/3, 3, 2);

vertexArray.set(0, vertrices.length/3, vertrices);

// Create the model's texture coords

texArray = new VertexArray(texCoords.length / 2, 2, 2);

texArray.set(0, texCoords.length / 2, texCoords);

// Compose a VertexBuffer out of the previous vertrices and texture coordinates

VertexBuffer vertexBuffer = new VertexBuffer();

vertexBuffer.setPositions(vertexArray, 1.0f, null);

vertexBuffer.setTexCoords(0, texArray, 1.0f/255.0f, null);

// Create indices and face lengths

int indices[] = new int[] {0, 1, 3, 2};

int[] stripLengths = new int[] {4};

// Create the model's triangles

triangles = new TriangleStripArray(indices, stripLengths);

// Create the appearance

Appearance appearance = new Appearance();

PolygonMode pm = new PolygonMode();

pm.setCulling(cullFlags);

appearance.setPolygonMode(pm);

// Create and set the texture

try

{

// Open image

Image texImage = Image.createImage(texFilename);

Texture2D theTexture = new Texture2D(new Image2D(Image2D.RGBA, texImage));

// Replace the mesh's original colors (no blending)

theTexture.setBlending(Texture2D.FUNC_REPLACE);

// Set wrapping and filtering

theTexture.setWrapping(Texture2D.WRAP_CLAMP, Texture2D.WRAP_CLAMP);

theTexture.setFiltering(Texture2D.FILTER_BASE_LEVEL, Texture2D.FILTER_NEAREST);

// Add texture to the appearance

appearance.setTexture(0, theTexture);

}

catch(Exception e)

{

// Something went wrong

System.out.println("Failed to create texture");

System.out.println(e);

}

// Finally create the Mesh

Mesh mesh = new Mesh(vertexBuffer, triangles, appearance);

// All done

return mesh;

}

}

Ball

import java.io.IOException;

import java.util.Random;

import javax.microedition.m3g.Graphics3D;

import javax.microedition.m3g.Group;

import javax.microedition.m3g.Loader;

import javax.microedition.m3g.Node;

import javax.microedition.m3g.Object3D;

import javax.microedition.m3g.RayIntersection;

import javax.microedition.m3g.Transform;

import javax.microedition.m3g.World;

/**

* Encapsulates our ball. Holds only the movement vector and current coordinates.

* Also, rotates the ball.

*/

public class Ball

{

// The ball's mesh

Group ball = null;

// The ball's movement vector

float[] moveVec = null;

// The ball's transform

Transform trBall = new Transform();

// Vector used in computations

float[] coords;

// How much the ball has been rotated

float rotated = 0.0f;

// Number of bounces

int bounces = 0;

// RayIntersection for collision

RayIntersection ri = new RayIntersection();

/** Constructs our ball and its transform */

public Ball() throws IOException

{

// Use the loader to load it as a Object3D

Object3D[] ballObj = Loader.load("/res/rediBall.m3g");

// Find the World node (Group)

int i = 0;

while(!(ballObj[i] instanceof World))

{

i++;

}

ball = (World)ballObj[i];

// Let the ball start on the paddle

trBall = new Transform();

coords = VectorOps.vector(0.0f, 0.0f, 10.0f);

}

/**

* Renders the ball. Also performs all neccecary computations

* such as mirroring, rotating, etc...

* Returns TRUE if the ball has passed the paddle and game is over

*/

public boolean render(Graphics3D g3d, Group walls, float[][][] wallVectors, float[] paddleCoords)

{

// Clear transform

trBall.setIdentity();

// Check if we are moving

if(moveVec != null)

{

// First rotate the ball a bit

//trBall.postRotate(rotated += 1.0f, 1.0f, 1.0f, 1.0f);

// Normalize the movement vector

ri = new RayIntersection();

float[] nMove = VectorOps.normalize(moveVec);

// See if there is any collision

if(walls.pick(-1, coords[0], coords[1], coords[2], nMove[0], nMove[1], nMove[2], ri) && ri.getDistance() <= 0.5f)

{

// We have collided, get the surface we intersected

Node n = ri.getIntersected();

// Correct our movement vector by mirroring

moveVec = VectorOps.mirror(moveVec, wallVectors[n.getUserID()][2]);

// Let user have contol over the movement by moving the paddle

if(n.getUserID() == M3GCanvas.PADDLE_WALL)

{

// Add extra speed to a maximum amount depending on ball/paddle position

float distX = (paddleCoords[0] - coords[0]) / 10.0f;

float distY = (paddleCoords[1] - coords[1]) / 10.0f;

moveVec[0] = Math.max(-0.3f, Math.min(moveVec[0] - distX, 0.3f));

moveVec[1] = Math.max(-0.3f, Math.min(moveVec[1] - distY, 0.3f));

// After 30 bounces it should be impossibly fast (HAH! A challenge!)

moveVec[2] = moveVec[2] + 0.01f;

// Increase number of bounces

bounces++;

}

}

// Check for bouncing against screen

if(coords[2] >= -0.7f)

{

// Flip over the screen (same as for paddle)

moveVec = VectorOps.mirror(moveVec, wallVectors[M3GCanvas.PADDLE_WALL][2]);

}

// Move the ball

coords[0] += moveVec[0];

coords[1] += moveVec[1];

coords[2] += moveVec[2];

}

// If we should quit (paddle let ball through)

boolean quit = coords[2] <= -7.5f;

rotated += 15.0f;

// Fix transform

trBall.postTranslate(coords[0], coords[1], coords[2]);

trBall.postRotate(rotated, 1.0f, 1.0f, 1.0f);

// Finally: Render the ball

g3d.render(ball, trBall);

if(quit)

{

moveVec = null;

coords = VectorOps.vector(0.0f, 0.0f, 10.0f);

}

return quit;

}

/** Starts the ball off in a reasonable direction at a random position */

public void start()

{

Random r = new Random();

bounces = 0;

moveVec = VectorOps.vector(-0.1f + r.nextFloat() * 0.2f, -0.1f + r.nextFloat() * 0.2f, 0.1f);

coords = VectorOps.vector(-3.0f + r.nextFloat() * 6.0f, -3.0f + r.nextFloat() * 6.0f, -5.0f);

}

/** Returns true if ball is moving */

public boolean isMoving() { return moveVec != null; }

/** Gets number of bounces (for nifty score-print) */

public int getBounces() { return bounces; }

}

 
 
 
免责声明:本文为网络用户发布,其观点仅代表作者个人观点,与本站无关,本站仅提供信息存储服务。文中陈述内容未经本站证实,其真实性、完整性、及时性本站不作任何保证或承诺,请读者仅作参考,并请自行核实相关内容。
2023年上半年GDP全球前十五强
 百态   2023-10-24
美众议院议长启动对拜登的弹劾调查
 百态   2023-09-13
上海、济南、武汉等多地出现不明坠落物
 探索   2023-09-06
印度或要将国名改为“巴拉特”
 百态   2023-09-06
男子为女友送行,买票不登机被捕
 百态   2023-08-20
手机地震预警功能怎么开?
 干货   2023-08-06
女子4年卖2套房花700多万做美容:不但没变美脸,面部还出现变形
 百态   2023-08-04
住户一楼被水淹 还冲来8头猪
 百态   2023-07-31
女子体内爬出大量瓜子状活虫
 百态   2023-07-25
地球连续35年收到神秘规律性信号,网友:不要回答!
 探索   2023-07-21
全球镓价格本周大涨27%
 探索   2023-07-09
钱都流向了那些不缺钱的人,苦都留给了能吃苦的人
 探索   2023-07-02
倩女手游刀客魅者强控制(强混乱强眩晕强睡眠)和对应控制抗性的关系
 百态   2020-08-20
美国5月9日最新疫情:美国确诊人数突破131万
 百态   2020-05-09
荷兰政府宣布将集体辞职
 干货   2020-04-30
倩女幽魂手游师徒任务情义春秋猜成语答案逍遥观:鹏程万里
 干货   2019-11-12
倩女幽魂手游师徒任务情义春秋猜成语答案神机营:射石饮羽
 干货   2019-11-12
倩女幽魂手游师徒任务情义春秋猜成语答案昆仑山:拔刀相助
 干货   2019-11-12
倩女幽魂手游师徒任务情义春秋猜成语答案天工阁:鬼斧神工
 干货   2019-11-12
倩女幽魂手游师徒任务情义春秋猜成语答案丝路古道:单枪匹马
 干货   2019-11-12
倩女幽魂手游师徒任务情义春秋猜成语答案镇郊荒野:与虎谋皮
 干货   2019-11-12
倩女幽魂手游师徒任务情义春秋猜成语答案镇郊荒野:李代桃僵
 干货   2019-11-12
倩女幽魂手游师徒任务情义春秋猜成语答案镇郊荒野:指鹿为马
 干货   2019-11-12
倩女幽魂手游师徒任务情义春秋猜成语答案金陵:小鸟依人
 干货   2019-11-12
倩女幽魂手游师徒任务情义春秋猜成语答案金陵:千金买邻
 干货   2019-11-12
 
推荐阅读
 
 
 
>>返回首頁<<
 
靜靜地坐在廢墟上,四周的荒凉一望無際,忽然覺得,淒涼也很美
© 2005- 王朝網路 版權所有