标题: 使用Mobile 3D API进行高级游戏开发 [打印本页] 作者: 51testing 时间: 2007-10-31 16:36 标题: 使用Mobile 3D API进行高级游戏开发 在移动设备上使用M3G编程手册
第一部分快速进入移动Java3D编程
译者:张惠明(碧云天) 摘要
在开始前我建议你去了解一些关于M3G领域的一些网络链接,这样对我们的编程是有帮助的。
首先也是最重要的就是Sony Ericsson Developer World。第二个我们要经常去看的就是Sony Ericsson Mobile Java 3D forum。除此之外我们还可以使用Sony Ericsson Developer World portal,在这里你能够发现你问题的答案或者更多。
既然你已经知道了如果出了问题我们将去那里找到答案,那么让我们进行我们的教程。这个教程的目标就是教会每个人如何设置你自己的3D动画和在屏幕上材质着色。对于着色模式,我们将展示如何载入他们并且告诉你那些创建M3G模型时使用的工具。然后我们将通过操作摄像机的一些参数使得我们可以在我们的场景中走动。我们只是想使你在开发第一个使用M3G的3D游戏中感到更直观,所以这个教程将相当快和直接,而有很少的解释,在这系列教程的其他部分将会对M3G的其他属性进行详细的讨论。
既然这个程序代码是针对教学目的的,它不是最佳的也不能覆盖所有可能出现的错误。我们将在以后的部分中讨论更多的深入的问题。 你需要知道什么
我们在开始阅读这篇文章之前,你应当了解一个MIDlet类和一个Canvas类的基本部分。如果你感到迷糊,参考游戏代码(这个教程中讨论的)和查看M3GMIDlet和M3GConvas类,这并不是一个困难的问题。当然如果你有3D编程和数学的背景那将是一件很好的事情,但并不是必须的。 Convas画板
当我们使用JSR184库编程时,我们使用的时MIDP2.0的配置文件,这意味着我们可以自用的使用更多的功能。让我们从设置我们的屏幕画板开始。和我们在通常的2D游戏编程有着同样的过程。你需要设置你的MIDlet类,并且启动你的屏幕画板和在Paint方法中进行绘画。既然你已经知道如何作并且这是一个相当容易的过程,我们将跳过这个过程。首先让我们看一下Canvas类的开头,这里是一些导入文件和变量声明。
import javax.microedition.lcdui.Graphics;
import javax.microedition.lcdui.game.GameCanvas;
import javax.microedition.M3G.Camera;
import javax.microedition.M3G.Graphics3D;
import javax.microedition.M3G.Light;
import javax.microedition.M3G.Loader;
import javax.microedition.M3G.Object3D;
import javax.microedition.M3G.Transform;
import javax.microedition.M3G.World;
/**
*
* @author Biovenger
* @version
*/
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;
这是相当基础的部分,让我们快速的查看这段是用来干什么的。事项我们看到的是一些导入文件,我们只导入了我们在教程中使用的所有的类和你在JSR184 API文档中发现的其他的类文件。我们也看到了一些其他的例如running和done这样的一些线程变量,不过这些都是一些自解释型的。
现在,让我们看一下“rendering hints”,这些“hits”是用来告诉我们当我们要着色的时候使用的是什么样的质量。既然这些着色的质量是依赖于手机的,所以我们第一了两个不同的“hits”:弱和强。正如你看到的一样,弱的着色模式是使用双消除混叠现象、真实的颜色、和抖动。弱的模式根本就没有着色的效果,这是我们能够得到的最快和最差的着色模式。当你查看代码的时候,这些不同的效果可以通过使用简单的逻辑OR来合并。在我们系列文章的后面章节中将会展示更多的特效效果。
接下来我们看到的实按键数组,我们使用了简单的数组形式,在键盘操作进程中他们将被改变。如果你对按键是如何处理的感到好奇,你可以在例子代码中找到相关的代码。举例说明一下,如果我们的UP键被按下那么if(key[UP])将是正确的。 M3G文件格式
JSR184有它自己的文件格式,叫做M3G。这是一个通用的3D文件格式,这里可以存储例如模型、灯光、摄像机、蒙皮甚至动画。注意,不但这个格式是好的,将模型载入你的应用程序也是相当容易的。我敢说M3G将是无止境的。我们怎样来建立一个M3G文件呢?我在下面的部分将来解释集中建立M3G的方法:
1.首先,最新的Discreet's 3D Studio Max版本已经内建了一个M3G的导出器。我们只需要点击导出按钮,你就能导出你建立的场景、动画、骨骼、材质等成为M3G文件。然而,很多人发现Discreet的导出器比较麻烦并且有一些错误,所以为了得到更好的效果,我们使用第二种方法。
2.HI公司是索尼爱立信公司的JSR184接口的提供者,他们制作了针对3个十分流行的3D建模软件例如3D Studio Max, LightWave和 Maya的插件,你可以在here>>找到他们。
3.Blender,这是一个具有可用的M3G导出器的免费的3D建模工具,然而它在开发的比较早,并且有一些错误。
那么我们怎样在我们的程序中调入这些非常有用的文件呢?非常简单。在JSR184中包含一个类叫做Loader,它可以正确的做到这点。使用一个简单的方法就能从一个M3G文件中载入所有的对象。一个可以是字符串类型的URL另一个方法是一个原始字节流中的偏移,下面是如何使用的例子:
Object3D[] objects = Loader.load("file.M3G");
Object3D[] objects2 = Loader.load(byteArray, offset);
这个Load方法通常返回的是一个Object3D类型的数组,对于这点有一个非常重要的原因。最好的就是使得Loader类可以载入碧M3G文件更多的数据。它可以转换成任何一个从Object3D类中继承过来的类。虽然,你们几乎都是使用这种方式来调入M3G文件。
现在,我创建了一个简单的M3G文件,命名为map.M3G并且显示它。我们将使用loader.load方法来载入这个文件,正如我们看见的那样,这个函数返回的是一个Object3D的数组。正如程序显示的那样我们不能使用Object3D直接进行显示,我们必须将它转化成能够在屏幕上显示的对象。在这个教程中,我们将载入这个World节点。World节点是JSR184屏幕绘图的顶节点。它将处理各种类型的信息其中包括摄像机、灯光、背景、和一些贴图。在系列文章的以后部分将深入的讲解场景绘图和JSR184的其他接口,现在我们只需要知道的就是World类可以擦作一个整个的场景,和我们想做的额外的事情。请看下面的代码,这段代码的作用是从一个M3G文件中导入一个World节点。
** Loads our world */
private void loadWorld()
{
try
{
// Loading the world is very simple. Note that I like to use a
// res-folder that I keep all files in. If you normally just put your
// resources in the project root, then load it from the root.
Object3D[] buffer = Loader.load("/res/map.M3G");
// Find the world node, best to do it the "safe" way
for(int i = 0; i < buffer.length; i++)
{
if(buffer instanceof World)
{
world = (World)buffer;
break;
}
}
// Clean objects
buffer = null;
}
catch(Exception e)
{
// ERROR!
System.out.println("Loading error!");
reportException(e);
}
}
正如你所看见的,我们使用Loader类载入Object3D数组以后,我们只需要简单地遍历整个数组并且找到World节点。这是查找一个World节点的最安全的方法。当我们发现我们的World节点以后我们应当中断我们的循环并且清除我们的缓冲区(既然不需要了,就应当删除了,虽然离开方法以后会自动删除,但养成好的习惯)。
好了,新在我们已经载入了我们的World节点,也就是我们已经知道了一个场景的最高节点,和操作所有节点的信息。在向你展示简单的显示他们之前,让我们首先来展示一下我们如何移动一个摄像机在我们刚刚载入的文件中。
摄像机操作:
我们已经有一个准备绘制的World节点那么我们需要一个摄像机,这个摄像机的作用是使我们能够在整个世界中移动。如果你回忆一下你会发现我们已经准备好了一个我们能够使用的摄像机信息。所以我们应当从World中拾取出摄像机,并且操作它。
在JSR184中一个摄像机通过Camera类来描述。这个类使得我们在我们的3D程序中更容易的操作摄像机完成简单地定位和旋转方法。我们在例子中只使用了两个方法分别是translate(float, float, float)和setOrientation(float, float, float, float)。第一种方法是在3D空间中移动偏移为X,Y和Z。举个例子,如果你想移动摄像机到X轴3个单位和Z轴3个单位,你应当做如下处理:
Camera cam = new Camera(); // This is our camera
//移动摄像机偏移 X Y Z
cam.translate(3.0f, 0.0f, 3.0f);
小菜一碟,每个方法调用移动摄像机是在以前的基础上操作,也就是入阁我们调用了两次以上的方法那么实际上我们是在X轴上移动了6个单位、在Z轴上也移动了6个单位。旋转就象移动一样简单,但是我们首先还要解释一下这个方法。它看起来十分象3DAPI旋转的方法。我们需要四个参数,第一个参数是旋转的实际角度,其他三个参数是一个要旋转的方向向量(X轴,Y轴,Z轴)。转向和方向向量将在以后讲到,现在,我们只需要知道以下的就可以了:
//沿着X轴旋转30度
cam.setOrientation(30.0f, 1.0f, 0.0f, 0.0f);
//沿着Y轴旋转30度
cam.setOrientation(30.0f, 0.0f, 1.0f, 0.0f);
//沿着Z轴旋转30度
cam.setOrientation(30.0f, 0.0f, 0.0f, 1.0f);
注意这个方法的名称是setOrientation,这意味着将清除摄像机以前的的所有旋转操作。我假设你已经知道了沿着一个轴旋转的意义是什么,我们将不再这里讨论这个问题。
既然你已经知道了操作一个摄像机移动和旋转的方法,我们将展示如何从一个World中提取一个摄像机。
/** 载入我们的摄像机 */
private void loadCamera()
{
// BAD!
if(world == null)
return;
// Set rotation
cam.setOrientation(camRot, 0.0f, 1.0f, 0.0f);
// Calculate trigonometry for camera movement
double rads = Math.toRadians(camRot);
camSine = Math.sin(rads);
camCosine = Math.cos(rads);
正如你所看到的一样这个函数的一半是如此的简单,首先我们检查用户是否按下了左键或者右键,如果按下我们就是增加和减少摄像机的角度,个相当的简单。接下来的几行是相当有趣的,我们使得当用户按下左或右键时用户的头会转,我们沿着Y轴进行旋转,这就是说旋转的方向向量是0.0f,1.0f,0.0f。我们旋转摄像机以后我们计算新的正弦和余弦值,我们将根据这个结构来移动摄像机。接下来我们看一下接下来的代码:
if(key[UP])
{
// Move forward
cam.translate(-0.1f * (float)camSine, 0.0f, -0.1f * (float)camCosine);
// Bob head
headDeg += 0.5f;
// A simple way to "bob" the camera as the user moves
cam.translate(0.0f, (float)Math.sin(headDeg) / 40.0f, 0.0f);
}
else if(key[DOWN])
{
// Move backward
cam.translate(0.1f * (float)camSine, 0.0f, 0.1f * (float)camCosine);
// Bob head
headDeg -= 0.5f;
// A simple way to "bob" the camera as the user moves
cam.translate(0.0f, (float)Math.sin(headDeg) / 40.0f, 0.0f);
}
// If the user presses the FIRE key, let's quit
if(key[FIRE])
M3GMidlet.die();
}
这里我们检查UP或DOWN。UP将移动摄像机向前而DOWN将移动摄像机向后。这里有一个简单的转化,我们马上就会解释的。摄像机总是从负Z轴的方向看过去的,所以我们向前移动只需要沿着Z轴的方向移动就可以了。然而,如果我们旋转了摄像机我们不能在只沿着Z轴方向移动,这样看上去是错误的。我们必须也沿着我们的方向在X轴移动。这样我们就用到了三角函数的方法。因为这个教程不是关于3D数学的,我们不能讨论更多的细节,如果你认为这是比较难的,那么在互联网上查找一个关于3D数学的教程将让你解决这些问题。
在完成所有的移动后,我们通过简单的头部抖动来移动我们的头部,我们只是通过一个简单的正弦函数实现沿着Y轴上下移动,以至于我们的看起来上下移动,他是通过增加或者减少headDeg这个变量来实现的。在最后我们检查了FIRE这个键,以至于用户可以在任何他想要退出的时候退出游戏。(我们也可以使用我们在Canvas建立的时候添加的退出命令菜单退出)。
综上所述,这就是我们摄像机的所有操作,现在所有剩下的操作就是使用World节点绘制了。 绘制多边型
我们在分析这段代码之前,我必须谈到的就是立即模式和保留模式的绘图。保留模式就是我们在现在的这个教程中使用的方式。当我们要绘制一个整个的包含摄像机、灯光、贴图灯的World节点时我们基本使用这种方式。这是一个绘制的简单的模式,但是这也使得我们对整个世界的控制变得最少。在立即模式下,我们绘制的时一个多边形的群,直接时简单的贴图和顶点数据。这将给我们更多的控制我们可以在绘制以前使用一个旋转矩阵对他进行旋转。我们在立即模式中,我们绘制一个World节点,通过使用一个旋转矩阵的方式来操作,但是我们将忽略所有的特效的效果,比如摄像机、背景、和其他。我们在以后的系列教程中将详细的讨论两个模式的细节问题。那么现在让我们看一下如何来绘制一个世界吧。 Graphics3D
在JSR184中所有的绘制操作都是使用Graphics3D对象。我们甚至能够操作摄像机和灯光信息,如果使用的时立即模式绘图。既然我们已经决定在以后讨论,那么我们现在就不需要考虑这些问题。
为了使用Graphics3D对象绘图,我们必须绑定一个绘图设备上下文。一个绘图设备上下文的基本意思是一个Graphics对象要绘画的地方。如果我们想在一个图像上绘制我们的对象,那们它就是这个图像的对象,或者我们通过主Graphics对象中的getGraphics()获得的。通过主Graphics对象的方法获得的就是直接绘制在屏幕上,我们可以在上面做任何我们可以做的操作。对于得到一个Graphics3D对象是简单的,你只需要调用Graphics3D.getInstance()方法。你将得到每一个MIDlet唯一的Graphics3D对象,这就是我们只能使用getInstance()来得到它的原因了。通过bindTarget方法来实现绑定,这是一个很少用到的方法,这里是一个简单的例子。
//这是我们的 Graphics3D 对象
Graphics3D g3d = Graphics3D.getInstance();
// 绑定到一个图像文件
Image img = Image.createImage("myImage.png");
Graphics g = img.getGraphics();
g3d.bindTarget(g);
// 绑定主 Graphics 对象
g3d.bindTarget(getGraphics());
// We can also supply rendering hints. Remember those? I talked about them at the beginning.
// This is done by using the other form of the bindTarget method.
// It takes a Graphics object to begin with, as always, and then it needs a boolean
// and an integer mask of hints.
// The boolean simply tells the Graphics3D object if it should use a depth buffer
// and you'll probably always set it to 'true'. Here is how we'll use it to bind
// with our hints:
g3d.bindTarget(getGraphics(), true, RENDERING_HINTS);
既然你知道了如何绑定你的目标了,你也必须知道这个目标必须在每个程序循环中释放。这意味着每当你要画其他的对象的时候,你必须释放这个目标。释放和绑定这个问题有时会出现。所以更多的人保持整个的游戏循环都在try、catch的异常处理的类中并且在异常的最后调用释放目标。在这个例子中我们就是这样做的。现在让我们来看一下绘图函数。为了绘制物体,我们需要使用一些绘制方法变量,在这一章里我们感兴趣的只有一个就是World的绘图方法。简单吗?当然,我们只需要提供你的世界节点,它将自动的绘制它。让我们看一看游戏的主循环是如何实现的。
/** 绘制到屏幕上
*/
private void draw(Graphics g)
{
// Envelop all in a try/catch block just in case
try
{
// 移动摄像机
moveCamera();