安卓游戏秘籍-二-

124 阅读35分钟

安卓游戏秘籍(二)

原文:Android Game Recipes

协议:CC BY-NC-SA 4.0

五、读取玩家输入

如果这是你第一次为移动设备或平板电脑编写游戏代码,你可能会很快注意到明显缺少输入选项来将玩家的意图反馈到游戏代码中。没有游戏控制器、键盘或鼠标的好处,很难为你的玩家提供复杂的输入系统。

连接游戏来检测和响应设备上的触摸事件并不像表面上看起来那么困难。

让我们来看看使用触摸屏作为游戏输入的一些更常见的问题。

5.1 检测屏幕触摸

问题

您的游戏无法检测到玩家何时触摸了屏幕。

解决办法

使用onTouchEvent()检测玩家触摸屏幕的位置和时间。

它是如何工作的

你的 Android 游戏是从一个扩展了Activity的类启动的。这个类将用于检测游戏中发生的触摸事件并做出反应。请记住,你的游戏代码和游戏循环将通过RendererGLSurfaceView中运行。但是,您仍然可以使用启动游戏的Activity来跟踪玩家在屏幕上的输入。

在您的Activity中,按如下方式覆盖onTouchEvent():

@Override
public boolean onTouchEvent(MotionEvent event) {
}

onTouchEvent()接收一个MotionEvent。当事件调用生成时,这个MotionEvent由系统自动传入。

MotionEvent包含了所有你需要的信息来帮助你判断和解读玩家的行动。从MotionEvent中,你可以获得玩家触摸的 x 和 y 坐标、触摸的压力和持续时间等信息,甚至可以确定滑动的方向。

例如,这里您只是简单地获取玩家的触摸坐标:

@Override
public boolean onTouchEvent(MotionEvent event) {
float x = event.getX();
float y = event.getY();
}

现在,您可以根据需要对 x 和 y 坐标做出反应。

5.2 检测屏幕多点触摸

问题

您的游戏无法使用onTouchEvent() 同时检测多个屏幕触摸。

解决办法

使用getPointerCount()PointerCoords帮助检索指针对象,以检测多点触摸输入。

它是如何工作的

传递给onTouchEvent()MotionEvent可以跟踪多达五个不同的同时发生的屏幕触摸。这里的概念是遍历所有使用getPointerCount()检测到的指针。在圈内,你是

将使用getPointerID()来检索每个指针所需的信息。

首先设置你的onTouchEvent()并遍历检测到的指针,如清单 5-1 所示。

清单 5-1onTouchEvent()

@Override
public boolean onTouchEvent(MotionEvent event) {

MotionEvent.PointerCoords[] coords = new MotionEvent.PointerCoords[event.getPointerCount()];

   For(int i = 0; i< event.getPointerCount(); i++)
   {
event.getPointerCoords(i, coords[i]);
   }
}

现在,您可以从检测到的每个指针中获得所需的所有信息。将coord[]传递到你的游戏循环中,你将获得每个触摸点的 x 和 y 坐标。您还将获得触摸点的方向、压力、大小(面积)以及长轴和短轴的长度。

5.3 将屏幕划分为触摸区域

问题

您需要确定玩家是触摸了屏幕的右侧还是左侧。

解决办法

使用屏幕的高度和宽度来确定玩家触摸了屏幕的哪一侧。

它是如何工作的

你知道如何使用onTouchEvent()来确定玩家是否以及何时触摸了屏幕,以及玩家触摸的坐标。当你试图为你的游戏创建一个输入系统时,这是非常有用的信息。你现在面临的问题是试图确定给你的 x 和 y 坐标是否落在屏幕的特定区域内。

假设你正在创建一个平台游戏,玩家可以向左向右跑。你已经设置好了你的onTouchEvent(),每次玩家触摸屏幕时,你都要捕捉 x 和 y 坐标。你怎么能轻易地确定这些坐标应该把玩家推向左边还是右边呢?

答案是把屏幕分成触摸区。在这种情况下,我们希望一个区域位于屏幕的左侧,另一个区域位于屏幕的右侧。一些简单的if语句可以用来检查玩家在屏幕上触摸的位置。

以平台游戏为例,玩家只能向左和向右移动,你可以把屏幕分成两半,一个代表左边,一个代表右边。你也可以考虑把触摸区放在屏幕底部,玩家的拇指可能会在那里。

这意味着您必须忽略落在左右触摸区上方的任何触摸坐标。看一看图 5-1 和图 5-2 中的图,了解这一概念的直观表示。

9781430257646_Fig05-01.jpg

图 5-1 。具有左右触摸区的肖像模式

9781430257646_Fig05-02.jpg

图 5-2 。带左右触摸区的横向模式

创建触摸区域的第一步是获取屏幕的高度。为此,在公共类上创建一个新的Display属性,如下所示:

public static Display display;

在应用的主Activity上,使用WINDOW_SERVICE将默认显示复制到这个属性,如清单 5-2 所示。

清单 5-2 。使用WINDOW_SERVICE

MyClass.display = ((WindowManager) getSystemService(Context.WINDOW_SERVICE)).getDefaultDisplay();

你现在可以在你的游戏代码中决定屏幕的高度和宽度,如清单 5-3 所示。

清单 5-3 。确定高度和宽度

@Override
public boolean onTouchEvent(MotionEvent event) {
//Get the non-touchable area of the screen -
//the upper two-thirds of the screen
int height = MyClass.display.getHeight() / 3;

//The playable area is now the lower third of the screen
int playableArea = MyClass.display.getHeight() - height;
}

警告这种方法有效,但只有当你的游戏像这个一样使用全屏时才完全有效。如果你的游戏不打算使用全屏,等到游戏的视图加载后再调用<view>.getHeight()

使用值playableArea 作为 y 轴值,您可以很容易地判断出您的玩家是否触摸到了屏幕的正确部分。创建一个简单的if语句来测试玩家触摸坐标的位置(参见清单 5-4 )。

清单 5-4 。使用playableArea

@Override
public boolean onTouchEvent(MotionEvent event) {
//Get the non-touchable area of the screen -
//the upper two-thirds of the screen
int height = MyClass.display.getHeight() / 3;

//The playable area is now the lower third of the screen
 int playableArea = MyClass.display.getHeight() - height;

if (y > playableArea){

//This y coordinate is within the touch zone

}
}

现在你知道玩家已经触摸到了屏幕的正确区域,那么就可以通过测试 x 坐标是大于还是小于屏幕中心点来确定触摸区的左右和两侧(见清单 5-5 ) 。

清单 5-5 。测试触摸区域

@Override
public boolean onTouchEvent(MotionEvent event) {
//Get the non-touchable area of the screen -
//the upper two-thirds of the screen
int height = MyClass.display.getHeight() / 3;

//Get the center point of the screen
int center = MyClass.display.getWidth() / 2;

//The playable area is now the lower third of the screen
int playableArea = MyClass.display.getHeight() - height;

if (y > playableArea){

//This y coordinate is within the touch zone

if(x < center){
//The player touched the left
}else{
//The player touched the right
}

}
}

您已成功确定玩家触摸了屏幕的左侧还是右侧。将注释替换为您的特定代码,以根据玩家触摸的位置启动操作。

5.4 检测屏幕滑动

问题

你需要确定玩家是否滑动或投掷了屏幕,以及向哪个方向。

解决办法

使用SimpleOnGestureListener和然后计算投掷的方向。

它是如何工作的

对于一些游戏——比如《神庙逃亡》——你想让用户滑动或投掷屏幕来指示他们想要移动的方向。例如,向上一扔可能代表一次跳跃。这可能是一种更加通用的玩家输入方法,但是它也需要稍微多一点的设置代码。

实现这一点所需的代码将在与OnTouchEvent()相同的Activity上运行。事实上,你可以把这两个词——OnTouchEvent()SimpleOnGestureListener——结合起来使用。

打开您的Activity并实例化一个SimpleInGestureListener,如下所示:

GestureDetector.SimpleOnGestureListener gestureListener = new GestureDetector.SimpleOnGestureListener(){
};

您需要在手势监听器中实现几个方法。然而,在这个解决方案中,你唯一要使用的是OnFling(),它在 清单 5-6 中提供。

清单 5-6OnFling()

GestureDetector.SimpleOnGestureListener gestureListener = new GestureDetector.SimpleOnGestureListener(){
@Override
public boolean onDown(MotionEvent arg0) {
//TODO Auto-generated method stub
return false;
}

@Override
public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX,
float velocityY) {
//React to the fling action
return false;
}
@Override
public void onLongPress(MotionEvent e) {
//TODO Auto-generated method stub

}
@Override
public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX,
float distanceY) {
//TODO Auto-generated method stub
return false;
}
@Override
public void onShowPress(MotionEvent e) {
//TODO Auto-generated method stub

}
@Override
public boolean onSingleTapUp(MotionEvent e) {
//TODO Auto-generated method stub
return false;
}

};

现在,在您的Activity中创建一个新变量,如下所示:

private GestureDetector gd;

GestureDetector将用于抛出手势事件。将Activity、的onCreate()中的探测器初始化如下:

@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
gd = new GestureDetector(this,gestureListener);
}

最后,在OnTouchEvent()中,扔给gestureListener,像这样:

@Override
public boolean onTouchEvent(MotionEvent event) {
returngd.onTouchEvent(event);
}

当玩家抛出屏幕时,会执行OnFling()方法中的代码。这处理了什么和什么时候;接下来你需要确定什么方向。

注意OnFling()有两个MotionEvent属性。因为您之前使用过它,所以您知道MotionEvent包含一个getX()和一个getY(),用于获取事件各自的坐标。

这两个事件(e1e2)代表投掷的起点和终点。因此,使用每个事件的 x 和 y 坐标,可以计算出玩家向哪个方向移动(参见清单 5-7 ) 。

清单 5-7 。检测投掷运动

float leftMotion = e1.getX() - e2.getX();
float upMotion = e1.getY() - e2.getY();

float rightMotion = e2.getX() - e1.getX();
float downMotion = e2.getY() - e1.getY();

if((leftMotion == Math.max(leftMotion, rightMotion)) && (leftMotion > Math.max(downMotion, upMotion)) )
{
//The player moved left
}

if((rightMotion == Math.max(leftMotion, rightMotion)) && rightMotion > Math.max(downMotion, upMotion) )
{
//The player moved right
}
if((upMotion == Math.max(upMotion, downMotion)) && (upMotion > Math.max(leftMotion, rightMotion)) )
{
//The player moved up
}

if((downMotion == Math.max(upMotion, downMotion)) && (downMotion > Math.max(leftMotion, rightMotion)) )
{
//The player moved down
}

现在你可以为你在游戏中需要采取的行动填入适当的代码。

因为这个解决方案绕过了Activity一点点,清单 5-8 显示了完成的Activity看起来应该是的样子。

清单 5-8SBGGameMain的完整代码

public class SBGGameMain extends Activity {
private GestureDetector gd;

@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(myContentView);
gd = new GestureDetector(this,gestureListener);
}
@Override
protected void onResume() {
super.onResume();
}

@Override
protected void onPause() {
super.onPause();
}

@Override
public boolean onTouchEvent(MotionEvent event) {
return gd.onTouchEvent(event);
}

GestureDetector.SimpleOnGestureListener gestureListener = new GestureDetector.SimpleOnGestureListener(){
@Override
public boolean onDown(MotionEvent arg0) {
//TODO Auto-generated method stub
return false;
}

@Override
public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX,
float velocityY) {

float leftMotion = e1.getX() - e2.getX();
float upMotion = e1.getY() - e2.getY();

float rightMotion = e2.getX() - e1.getX();
float downMotion = e2.getY() - e1.getY();

if((leftMotion == Math.max(leftMotion, rightMotion)) && (leftMotion > Math.max(downMotion, upMotion)) )
{

}

if((rightMotion == Math.max(leftMotion, rightMotion)) && rightMotion > Math.max(downMotion, upMotion) )
{

}
if((upMotion == Math.max(upMotion, downMotion)) && (upMotion > Math.max(leftMotion, rightMotion)) )
{

}

if((downMotion == Math.max(upMotion, downMotion)) && (downMotion > Math.max(leftMotion, rightMotion)) )
{

}
return false;
}
@Override
public void onLongPress(MotionEvent e) {
//TODO Auto-generated method stub

}
@Override
public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX,
float distanceY) {
//TODO Auto-generated method stub
return false;
}
@Override
public void onShowPress(MotionEvent e) {
//TODO Auto-generated method stub

}
@Override
public boolean onSingleTapUp(MotionEvent e) {
//TODO Auto-generated method stub
return false;
}

};
}

5.5 使用设备加速度计

问题

当玩家倾斜设备时,游戏角色不会移动。

解决办法

使用设备的内置加速度计来检测设备何时向特定方向倾斜,然后相应地移动角色。

它是如何工作的

大多数(如果不是全部的话)Android 设备都包含一个加速度计。这种传感器的一个普遍用途是作为游戏的另一个输入设备。使用来自加速度计的反馈,您可以检测玩家是否倾斜了设备,然后在代码中做出相应的反应。

在清单 5-9 中,你检测玩家是向左还是向右倾斜手机,然后设置适当的变量使角色向倾斜的方向移动。首先,在你的Activity类的中实现SensorEventListener。然后允许 Eclipse(或您选择的 IDE)添加所需的方法覆盖。

清单 5-9 。SensorEvenListener

public class SBGGameMain extends Activityimplements SensorEventListener{
@Override
public void onCreate(Bundle savedInstanceState) {
//TODO Auto-generated method stub
}
@Override
protected void onResume() {
//TODO Auto-generated method stub
}
@Override
protected void onPause() {
//TODO Auto-generated method stub
}
@Override
public void onAccuracyChanged(Sensor sensor, int accuracy) {
//TODO Auto-generated method stub

}
@Override
public void onSensorChanged(SensorEvent event) {
//TODO Auto-generated method stub
}

}

需要几个变量。prevXprevY跟踪先前的 x 和 y 轴倾斜位置,以确定倾斜是否有变化。布尔函数isInitialized确定先前是否检测到倾斜;如果不是,新值被存储在prevXprevY中。静态浮动NOISE保存一个值,让您根据环境设备的移动来确定实际的倾斜变化。最后,设置SensorManager和加速度计的变量。参见清单 5-10 。

清单 5-10 。传感器管理器

public class SBGGameMain extends Activity implements SensorEventListener{
private float prevX;
private float prevY;
private boolean isInitialized;
private final float NOISE = (float) 2.0;
private SensorManager sensorManager;
private Sensor accelerometer;
@Override
public void onCreate(Bundle savedInstanceState) {
//TODO Auto-generated method stub
}
@Override
protected void onResume() {
//TODO Auto-generated method stub
}
@Override
protected void onPause() {
//TODO Auto-generated method stub
}
@Override
public void onAccuracyChanged(Sensor sensor, int accuracy) {
//TODO Auto-generated method stub

}
@Override
public void onSensorChanged(SensorEvent event) {
//TODO Auto-generated method stub
}

}

接下来,在执行onSensorChanged()方法中的核心代码之前,在onCreate()onPause()onResume()方法中执行一些内务处理(参见清单 5-11 ) 。

清单 5-11 。onSensorChanged

@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
gameView = new SBGGameView(this);
setContentView(gameView);

isInitialized= false;
sensorManager= (SensorManager) getSystemService(this.SENSOR_SERVICE);
accelerometer= sensorManager.getDefaultSensor(Sensor.TYPE_ACCELEROMETER);
sensorManager.registerListener(this, accelerometer, SensorManager.SENSOR_DELAY_NORMAL);

}
Override
protected void onResume() {
super.onResume();

sensorManager.registerListener(this, accelerometer, SensorManager.SENSOR_DELAY_NORMAL);

gameView.onResume();

}

@Override
protected void onPause() {
super.onPause();

sensorManager.unregisterListener(this);

gameView.onPause();
}

现在是解决方案的核心。当检测到传感器的变化时,触发onSensorChanged()方法;在这种情况下,这就是加速度计。捕捉变化,并使用 x 和 y 向量来设置你的PLAYER_MOVE_LEFTPLAYER_MOVE_JUMP,如清单 5-12 中的所示。

清单 5-12 。设置玩家动作

public class SBGGameMain extends Activity implements SensorEventListener{
private float prevX;
private float prevY;
private boolean isInitialized;
private final float NOISE = (float) 2.0;
private SensorManager sensorManager;
private Sensor accelerometer;

@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
gameView = new SBGGameView(this);
setContentView(gameView);

isInitialized= false;
sensorManager= (SensorManager) getSystemService(this.SENSOR_SERVICE);
accelerometer= sensorManager.getDefaultSensor(Sensor.TYPE_ACCELEROMETER);
sensorManager.registerListener(this, accelerometer, SensorManager.SENSOR_DELAY_NORMAL);

}
@Override
protected void onResume() {
super.onResume();

sensorManager.registerListener(this, accelerometer, SensorManager.SENSOR_DELAY_NORMAL);

gameView.onResume();

}

@Override
protected void onPause() {
super.onPause();

sensorManager.unregisterListener(this);

gameView.onPause();
}

@Override
public void onAccuracyChanged(Sensor sensor, int accuracy) {
//TODO Auto-generated method stub

}
@Override
public void onSensorChanged(SensorEvent event) {
float x = event.values[0];
float y = event.values[1];
if (!isInitialized) {
prevX = x;
prevY = y;
isInitialized = true;
} else {
float deltaX = Math.abs(prevX - x);
float deltaY = Math.abs(prevY - y);
if (deltaX < NOISE) deltaX = (float)0.0;
if (deltaY < NOISE) deltaY = (float)0.0;
prevX = x;
prevY = y;
if (deltaX > deltaY) {
playeraction = PLAYER_MOVE_LEFT;
} else if (deltaY > deltaX) {
playeraction = PLAYER_MOVE_JUMP;
} else {

}
}
}
}

六、加载精灵表

很有可能到这个时候,你已经有了 Android 平台上游戏的外壳或雏形。也有可能你已经尝试了动画你的一个或所有的角色,武器,或其他屏幕上的对象没有运气。

如果您尝试过加载单独的图像,您无疑会发现翻转这些图像来创建动画的过程极其缓慢。这个问题的解决方案几乎和视频游戏本身一样古老:精灵表。大多数 2D 的视频游戏仍然采用一种久经考验的动画技术,并且非常适合这项任务;也就是说,创建游戏中需要的动画帧的 sprite 表。

在这一章中,你将使用 sprite sheets 完成一些常见问题的解决方案。

6.1 使用一个精灵表

问题

为动画加载多个独立的图像会占用太多空间,而且速度很慢。

解决办法

使用一个 sprite 表,在一个图像文件中包含所有动画帧。

它是如何工作的

让我们从基础开始。sprite sheet 是一个单独的图像文件,它保存了可用于创建动画 sprite 的所有不同图像。

我们示例游戏的主角——超级强盗——应该能够在屏幕上跑来跑去。这就要求超级强盗的精灵在奔跑时是动画的。动画中的每个图像都被加载到一个名为 sprite sheet 的单个文件中,而不是为动画的每一帧创建一个单独的图像(这样会非常耗费资源,以至于最终的游戏可能都无法加载)。图 6-1 显示了超级强盗家伙奔跑动画的精灵表的细节。

9781430257646_Fig06-01.jpg

图 6-1 。超级强盗家伙运行(详细)

请注意,动画的不同帧都放在一个文件中,因此减少了存储、调用、交换和显示单独图像所需的资源。

将图像放入您的res/drawable文件夹。这与用于任何其他图像文件的过程相同。所有图像文件都可以存储在res/drawable文件夹中,然后使用R.drawable.<imagename>通过 id 轻松调出。但是,请记住,所有图像名称必须小写,否则您将无法调用它们。

现在的问题是:如何一次只显示一帧,而不是一次显示整个 sprite 工作表?这实际上比看起来容易。使用 OpenGL ES,您将调整该图像或纹理的大小,以便您想要显示的一帧动画一次适合顶点(在下一个解决方案中解释)。记住,在 OpenGL ES 中,你的纹理和顶点可以有不同的大小。

注意仅仅因为 OpenGL ES 使用的所有图像都必须是正方形,并不意味着 sprite 表中的每个空间都必须包含一帧动画。虽然我们为超级强盗家伙使用的 4x4sheet 可以容纳 16 帧动画,但我们只使用了 10 帧。

图 6-2 显示了超级强盗正在使用的精灵表。

9781430257646_Fig06-02.jpg

图 6-2 。超级强盗(全精灵表)

注意为了在本书中展示,图像的背景被染成灰色。理想情况下,你的图片应该有透明的背景。

6.2 访问精灵表中的图像

问题

显示一个 sprite 表显示整个图像,而不是需要的单个图像。

解决办法

调整纹理映射以显示所需的 sprite sheet 部分。

它是如何工作的

为了理解这个解决方案是如何工作的,你需要首先理解你的纹理映射到的顶点和纹理对象本身是两个独立的实体,可以彼此独立地操作。这意味着您可以调整大小,移动或改变纹理,而不会影响顶点。

你已经知道图 6-2 中的精灵表包含了让超级强盗看起来像在跑的所有动画帧。然而,如果你尝试使用精灵作为纹理,两件事会立即变得明显。一是纹理出现倒挂;其次,整个 sprite 表被映射到顶点上,而不仅仅是一帧动画。

当 OpenGL 创建一个纹理时,图像被加载到一个字节数组中。当图像被加载到数组中时,图像的第一个字节被加载到数组的后面,接着是第二个字节,依此类推。当 OpenGL 开始从数组中读取纹理信息时,它读取的第一个字节(数组中的第一个字节)实际上是从文件中出来的最后一个字节。所以 OpenGL 的纹理是你原图的反转版。

你需要翻转 OpenGL 中的纹理,使其正面朝上。然后,您需要调整映射到顶点的纹理的大小,以便只有一帧 sprite 工作表是可见的。图 6-3 说明了这个概念。

9781430257646_Fig06-03.jpg

图 6-3 。将精灵纹理翻转并映射到顶点

首先,让我们注意翻转图像,使其正面朝上。

提示 OpenGL ES 以同样的方式处理所有图像到纹理的加载,不管它们是否是精灵片。因此,当你的所有图像变成纹理时,它们总是会反过来。解决方案的这一步应该在加载所有纹理时执行。

在您编写的将图像加载到纹理的代码中,实例化一个新的Matrix并使用postScale()方法创建新的矩阵,该矩阵沿 y 轴翻转纹理。新的矩阵被传递给通常用于加载纹理的createBitmap()方法。

在清单 6-1 中,texture代表你想要加载的图像的参考 id,在 drawable文件夹中可以找到。

清单 6-1 。使用postScale()

InputStreamimagestream = context.getResources().openRawResource(texture);
Bitmap bitmap = null;
Bitmap temp = null;

Matrix mtrx = new Matrix();
mtrx.postScale(1f, -1f);

temp = BitmapFactory.decodeStream(imagestream);
bitmap = Bitmap.createBitmap(temp, 0, 0, temp.getWidth(), temp.getHeight(), mtrx, true);

imagestream.close();
imagestream = null;

现在你的纹理已经以正确的方式翻转了,是时候调整纹理了,这样只有一帧可以明显地映射到你的顶点。同样,这可以在构建纹理和顶点时加载完成。

你目前用来加载纹理的代码,部分看起来应该像清单 6-2 。

清单 6-2 。纹理数组

privateFloatBuffervertexBuffer;
privateFloatBuffertextureBuffer;
privateByteBufferindexBuffer;

private float[] vertices = {
0f, 0f, 0f,
1f, 0f, 0f,
1f, 1f, 0f,
0f, 1f, 0f,
};

private float[] texture = {
0f, 0f,
1f, 0f,
1f, 1f,
0f, 1f,
};

因为 OpenGL ES 中的默认坐标系从 0 到 1,所以清单 6-2 中的数组使用整个纹理。使用这个数组,整个纹理将被映射到顶点上。然而,考虑到图 6-2 中的 sprite 工作表,你一次只想看到 sprite 工作表的四分之一。

图 6-2 中的 sprite 页被分成四行四幅图像(并未全部使用)。因此,每行是整个纹理高度的 25 %,每列是整个纹理宽度的 25%。

修正纹理数组,如清单 6-3 所示,只显示精灵表中的一帧动画。

清单 6-3 。新纹理数组

privateFloatBuffervertexBuffer;
privateFloatBuffertextureBuffer;
privateByteBufferindexBuffer;

private float[] vertices = {
0f, 0f, 0f,
1f, 0f, 0f,
1f, 1f, 0f,
0f, 1f, 0f,
};

private float[] texture = {
0f, 0f,
.25f, 0f,
.25f, .25f,
0f, .25f,
};

6.3 改变精灵图框

问题

图像需要从 sprite 表中的一帧变化到另一帧,而不是静态的。

解决办法

通过沿 x 和/或 y 轴平移纹理,从一个 sprite sheet 帧移动到另一个 sprite sheet 帧。

它是如何工作的

在 OpenGL ES 1 中使用glTranslatef()方法在坐标系中平移或移动矩阵。要从 sprite 工作表的第一帧切换到第二帧,需要将纹理矩阵沿 x 轴平移 25%。(这是假设你正在使用一个像图 6-2 中那样设置的 sprite 工作表)。

第一步是将 OpenGL ES 置于纹理矩阵模式,从而确保您修改的是纹理的坐标,而不是顶点。以下代码将 OpenGL ES 置于纹理矩阵模式,并将 sprite 工作表的第一帧(左上角)映射到顶点。

gl.glMatrixMode(GL10.GL_TEXTURE);
gl.glLoadIdentity();
gl.glTranslatef(0f,.75f, 0f);

注意,传递给glTranslatef()的 y 坐标是. 75。在 0–1 的坐标范围内,. 75 对应于 sprite 工作表中第一行帧的左下角。在这个代码示例中,传递给glTranslatef()的 x 和 y 坐标分别是 0 和. 75。把这个带到图 6-4 中的图像,(0,. 75)是精灵表第一行第一帧的左下角。图 6-4 显示了 y 轴上的坐标是如何与精灵表对齐的。

9781430257646_Fig06-04.jpg

图 6-4 。具有 y 轴坐标的 Sprite 工作表

如果你想改变贴图到顶点的纹理到 sprite 表第一行的第二帧,使用glTranslatef()方法移动纹理到(. 25,. 75)。0.25 的 x 坐标表示第一行第二帧的 x 轴左下角。

gl.glMatrixMode(GL10.GL_TEXTURE);
gl.glLoadIdentity();
gl.glTranslatef(.25f,.75f, 0f);

如果您使用的是 OpenGL ES 2 或 3,更改 sprite 工作表框架的过程是不同的。您将需要添加一对浮点到您的片段着色器。这些浮动将接受框架位置的 x 和 y 坐标值,很像glTranslatef()

首先,将浮点添加到片段着色器代码,如清单 6-4 中的所示。

清单 6-4 。向片段着色器代码添加浮点

private final String fragmentShaderCode =
"precisionmediump float;" +
"uniformvec4vColor;" +
"uniformsampler2DTexCoordIn;" +
"uniform float posX;" +
"uniform float posY;" +
"varyingvec2TexCoordOut;" +
"void main() {" +
"}";

接下来,修改片段着色器的main()方法来调用texture2d()并向其传递清单 6-5 中posXposY, as shown in 和的值。

清单 6-5 。修改main()方法

private final String fragmentShaderCode =
"precisionmediump float;" +
"uniformvec4vColor;" +
"uniformsampler2DTexCoordIn;" +
"uniform float posX;" +
"uniform float posY;" +
"varyingvec2TexCoordOut;" +
"void main() {" +
" gl_FragColor = texture2D(TexCoordIn, vec2(TexCoordOut.x + posX,TexCoordOut.y + posY));"+
"}";

着色器代码现已修改。您需要一种方法将posXposY的值传递到着色器代码中。这最终是使用glUniform1f()完成的。改变纹理的 x 和 y 位置的代码应该放在对象类的draw()方法中。修改方法签名,允许在调用draw()时传递坐标。

public void draw(float[] mvpMatrix,float posX, float posY) {
...
}

使用glGetUniformLocation()获得posXposY浮动在着色器中的位置,然后使用glUniform1f()分配新值,如清单 6-6 所示。

清单 6-6draw()

public void draw(float[] mvpMatrix, float posX, float posY) {
GLES20.glUseProgram(mProgram);

mPositionHandle = GLES20.glGetAttribLocation(mProgram, "vPosition");

GLES20.glEnableVertexAttribArray(mPositionHandle);

intvsTextureCoord = GLES20.glGetAttribLocation(mProgram, "TexCoordIn");
GLES20.glVertexAttribPointer(mPositionHandle, COORDS_PER_VERTEX,
GLES20.GL_FLOAT, false,
vertexStride, vertexBuffer);
GLES20.glVertexAttribPointer(vsTextureCoord, COORDS_PER_TEXTURE,
GLES20.GL_FLOAT, false,
textureStride, textureBuffer);
GLES20.glEnableVertexAttribArray(vsTextureCoord);
GLES20.glActiveTexture(GLES20.GL_TEXTURE0);
GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, textures[0]);
intfsTexture = GLES20.glGetUniformLocation(mProgram, "TexCoordOut");
intfsPosX = GLES20.glGetUniformLocation(mProgram, "posX");
intfsPosY = GLES20.glGetUniformLocation(mProgram, "posY");
GLES20.glUniform1i(fsTexture, 0);
GLES20.glUniform1f(fsPosX, posX);
GLES20.glUniform1f(fsPosY, posY);
mMVPMatrixHandle = GLES20.glGetUniformLocation(mProgram, "uMVPMatrix");

GLES20.glUniformMatrix4fv(mMVPMatrixHandle, 1, false, mvpMatrix, 0);

GLES20.glDrawElements(GLES20.GL_TRIANGLES, drawOrder.length,
GLES20.GL_UNSIGNED_SHORT, drawListBuffer);

GLES20.glDisableVertexAttribArray(mPositionHandle);
}

6.4 将 Sprite 表中的图像 制作成动画

问题

一个图像需要是一个随时间变化的动画 (就好像角色在跑)。

解决办法

以特定顺序浏览多个 sprite 工作表图像。

它是如何工作的

在这个解决方案中,您将构建在前一个解决方案中使用的glTranslatef()glUnifor1f()方法。OpenGL ES 1 的glTranslatef()方法已经显示了移动顶点上的贴图纹理,这样只有 sprite sheet 的特定部分是可见的。如果你足够快地执行这个动作,并且有足够的帧数,你将会有动画。

对于这个解决方案,你再次使用图 6-2 中所示的精灵表。这个解决方案也建立在第五章“读取玩家输入”的基础上

创建一个枚举,当玩家触摸屏幕的右侧或左侧时,可以设置该枚举,指示角色应该分别向右或向左跑(见清单 6-7 )。

这些变量应该放在你可以从渲染器和主Activity 访问它们的地方。

清单 6-7 。更新玩家移动

public static intplayerAction = 0;
public static final int PLAYER_MOVE_LEFT = 1;
public static final int PLAYER_STAND = 0;
public static final int PLAYER_MOVE_RIGHT = 2;

你还需要设置另外六个变量(清单 6-8 )。

清单 6-8 。设置另外六个变量

public static float playerCurrentLocation  = .75f;
public static float currentRunAniFrame  = 0f;
public static float currentStandingFrame   = 0f;

public static final float PLAYER_RUN_SPEED = .25f;
public static final float STANDING_LEFT = 0f;
public static final float STANDING_RIGHT = .75f;

playerCurrentLocation用于跟踪屏幕上精灵的当前位置。currentRunAniFrame用于跟踪精灵表中动画的当前帧,这使得角色看起来在运行。像currentRunAniFrame一样,currentStandingFrame被用来跟踪精灵表的哪一帧被用来使角色看起来是站着的。

PLAYER_RUN_SPEED将用于以特定的间隔在屏幕上移动精灵。结合动画,PLAYER_RUN_SPEED用来给人一种角色实际在跑的错觉。最后,STANDING_LEFTSTANDING_RIGHT变量保存代表角色站立的 sprite 表中两个帧的 x 轴左下角的值。一个框架面向左,另一个面向右。

回头参考第五章,清单 6-9 根据玩家是触摸了屏幕的右侧还是左侧来设置playerAction。游戏主ActivityonTouchEvent被修改为playerAction设置为PLAYER_MOVE_RIGHTPLAYER_MOVE_LEFTPLAYER_STAND

清单 6-9onTouchEvent()

@Override
publicbooleanonTouchEvent(MotionEvent event) {
float x = event.getX();
float y = event.getY();
DisplayMetricsoutMetrics = new DisplayMetrics();

display.getMetrics(outMetrics);

int height = outMetrics.heightPixels / 4;

int playableArea = outMetrics.heightPixels - height;
if (y >playableArea){
switch (event.getAction()){
case MotionEvent.ACTION_DOWN:
if(x <outMetrics.widthPixels / 2){
playerAction = PLAYER_MOVE_RIGHT;
}else{
playerAction = PLAYER_MOVE_LEFT;
}
break;
case MotionEvent.ACTION_UP:
playerAction = PLAYER_STAND;
break;
}
}

return false;
}

接下来,设置一个case语句来读取playerAction的值。游戏循环包含在RendereronDraw()方法中。该方法在一个常量循环中执行。因此,您可以在Renderer中创建一个名为movePlayer()的新方法,并从RendereronDraw()方法中调用它。

每次onDraw()方法执行,都会调用movePlayer()。在movePlayer()方法中,你所需要做的就是告诉 OpenGL ES 你想如何翻转精灵页面并“移动”角色。

首先,创建movePlayer()方法并设置一个case语句来遍历playerAction。在清单 6-10 所示的代码中,goodguy指的是SuperBanditGuy类的实例化。这可以代表你在 游戏中使用的任何职业。

清单 6-10movePlayer()

private void movePlayer(GL10gl){
if(!goodguy.isDead)
{
switch(playeraction){
case PLAYER_MOVE_RIGHT:

break;

case PLAYER_MOVE_LEFT:

break;

case PLAYER_STAND:

break;
}
}
}

在 Recipe 6.3 中,你学习了如何使用glTanslatef()glUniform1f()方法从 sprite 工作表的一帧移动到另一帧。这个解决方案中唯一的不同是您将自动化这个过程。这意味着因为onDraw(),也就是movePlayer(),是在一个循环中被调用的,你必须以这样一种方式编写对glTranslatef()的调用,它将在每次被调用时自动从一帧循环到下一帧。清单 6-11 和 6-12 展示了当您想要使用 OpenGL ES 1 和 OpenGL ES 2/3 将字符向右移动时,这段代码的样子。

清单 6-11 。用播放器移动画面(OpenGLES 1)

currentStandingFrame   = STANDING_RIGHT;
playerCurrentLocation  += PLAYER_RUN_SPEED;

currentRunAniFrame  += .25f;
if (currentRunAniFrame> .75f)
{
currentRunAniFrame  = .0f;
}

gl.glMatrixMode(GL10.GL_MODELVIEW);
gl.glLoadIdentity();
gl.glPushMatrix();
gl.glScalef(.15f, .15f, 1f);
gl.glTranslatef(playercurrentlocation, .75f, 0f);
gl.glMatrixMode(GL10.GL_TEXTURE);
gl.glLoadIdentity();
gl.glTranslatef(currentRunAniFrame,.75f, 0.0f);
goodguy.draw(gl,spritesheets,SBG_RUNNING_PTR);
gl.glPopMatrix();
gl.glLoadIdentity();

清单 6-12 。用播放器移动帧(OpenGL ES 2/3)

currentStandingFrame   = STANDING_RIGHT;
playerCurrentLocation  += PLAYER_RUN_SPEED;

currentRunAniFrame  += .25f;
if (currentRunAniFrame> .75f)
{
currentRunAniFrame  = .0f;
}

goodguy.draw(mMVPMatrix, currentRunAniFrame, .75f );

首先,因为角色跑向右边,当他停止奔跑时,他应该面向右边。因此,currentStandingFrame被设置为STANDING_RIGHT。然后,PLAYER_RUN_SPEED被加到playercurrentlocation上,得到一个距离原始位置 0.25 的值。渲染时,精灵会移动到新位置。

下一个块保持动画循环移动。sprite 工作表有四个图像,左下角在 x 轴上分别为 0、. 25、. 50 和. 75。为了实现平滑的动画,您将从第一帧(0)开始,然后添加. 25 以到达第二帧,依此类推。当到达动画的最后一帧(. 75)时,需要从 0 重新开始。一个if()语句检查你是否在动画的最后一帧,并重置你回到第一帧。

最后,使用 OpenGL 绘制新的动画帧。注意glTranslatef()被调用了两次——一次在模型矩阵模式下,一次在纹理矩阵模式下。当在模型矩阵模式下调用它时,它会移动纹理映射到的顶点的物理位置,从而将角色向右移动。在纹理矩阵模式下调用glTranslatef()时,动画的帧前进。

清单 6-13 和 6-14 展示了完成的movePlayer()方法,同样使用了 OpenGL ES 1 和 OpenGL ES 2/3T6。

清单 6-13 。已完成movePlayer() ( OpenGL ES 1)

private void movePlayer(GL10gl){
if(!goodguy.isDead)
{
switch(playeraction){
case PLAYER_MOVE_RIGHT:
currentStandingFrame   = STANDING_RIGHT;
playerCurrentLocation  += PLAYER_RUN_SPEED;
currentRunAniFrame  += .25f;
if (currentRunAniFrame> .75f)
{
currentRunAniFrame  = .0f;
}

gl.glMatrixMode(GL10.GL_MODELVIEW);
gl.glLoadIdentity();
gl.glPushMatrix();
gl.glScalef(.15f, .15f, 1f);
gl.glTranslatef(playerCurrentLocation, .75f, 0f);
gl.glMatrixMode(GL10.GL_TEXTURE);
gl.glLoadIdentity();
gl.glTranslatef(currentRunAniFrame,.75f, 0.0f);
goodguy.draw(gl,spritesheets,SBG_RUNNING_PTR);
gl.glPopMatrix();
gl.glLoadIdentity();

break;

case PLAYER_MOVE_LEFT:
currentStandingFrame   = STANDING_LEFT;
playerCurrentLocation  -= PLAYER_RUN_SPEED;
currentRunAniFrame  += .25f;
if (currentRunAniFrame> .75f)
{
currentRunAniFrame  = .0f;
}

gl.glMatrixMode(GL10.GL_MODELVIEW);
gl.glLoadIdentity();
gl.glPushMatrix();
gl.glScalef(.15f, .15f, 1f);
gl.glTranslatef(playerCurrentLocation, .75f, 0f);
gl.glMatrixMode(GL10.GL_TEXTURE);
gl.glLoadIdentity();
gl.glTranslatef(currentRunAniFrame,.50f, 0.0f);
goodguy.draw(gl,spritesheets,SBG_RUNNING_PTR);
gl.glPopMatrix();
gl.glLoadIdentity();

break;

case PLAYER_STAND:
gl.glMatrixMode(GL10.GL_MODELVIEW);
gl.glLoadIdentity();
gl.glPushMatrix();
gl.glScalef(.15f, .15f, 1f);
gl.glTranslatef(playerCurrentLocation, .75f, 0f);
gl.glMatrixMode(GL10.GL_TEXTURE);
gl.glLoadIdentity();
gl.glTranslatef(currentStandingFrame,.25f, 0.0f);
goodguy.draw(gl,spritesheets,SBG_RUNNING_PTR);
gl.glPopMatrix();
gl.glLoadIdentity();
break;
}
}
}

清单 6-14 。已完成movePlayer() (OpenGL ES 2/3)

private void movePlayer(GL10gl){
if(!goodguy.isDead)
{
switch(playeraction){
case PLAYER_MOVE_RIGHT:
currentStandingFrame   = STANDING_RIGHT;
playerCurrentLocation  += PLAYER_RUN_SPEED;
currentRunAniFrame  += .25f;
if (currentRunAniFrame> .75f)
{
currentRunAniFrame  = .0f;
}

goodguy.draw(mMVPMatrix, currentRunAniFrame, .75f );
break;

case PLAYER_MOVE_LEFT:
currentStandingFrame   = STANDING_LEFT;
playerCurrentLocation  -= PLAYER_RUN_SPEED;
currentRunAniFrame  += .25f;
if (currentRunAniFrame> .75f)
{
currentRunAniFrame  = .0f;
}

goodguy.draw(mMVPMatrix, currentRunAniFrame, .50f );
break;

case PLAYER_STAND:
goodguy.draw(mMVPMatrix, currentStandingFrame, .25f );
break;
}
}
}

七、滚动背景

本章的解决方案将帮助你创建一个游戏的滚动背景。许多游戏类型都有背景图像,当玩家玩游戏时,背景图像会滚动。你可能对如何让游戏的背景图像移动有一些疑问。

在某些情况下,图像会自动滚动。例如,滚动射击和其他“轨道”风格的游戏将有自动滚动的背景。这与其他游戏类型形成对比,例如侧滚平台游戏,其中背景图像将随着玩家的移动而滚动(这在第八章“滚动多个背景”中有所介绍)。

本章将介绍三种加载背景图像、垂直滚动图像和水平滚动图像的解决方案。

7.1 加载背景图像

问题

您的游戏无法使用 OpenGL ES 加载背景图像。

解决办法

创建一个类,可以将图像作为纹理加载,并将其映射到一组顶点 。

它是如何工作的

加载图像以供 OpenGL ES 使用的最简单方法是创建一个自定义类,该类创建所需的所有顶点,并将图像作为纹理映射到这些顶点。因为这个背景将会滚动,所以这个类还需要以一种能够重复的方式映射纹理。如果 OpenGL ES 可以在滚动时重复纹理,背景图像将会看起来好像无限延续。

用于无限滚动的最常见的背景类型之一,也是最容易操作的背景类型之一,是一个星空。星域是点的随机模式,很容易无缝重复。像侧滚射击游戏这样的游戏经常使用星域作为无限滚动的背景。

图 7-1 展示了将在本解决方案中使用的 星域图像。

9781430257646_Fig07-01.jpg

图 7-1 。星域图像

第一步是将图像添加到项目的正确的res/drawable文件夹中。我们已经讨论过将图像添加到项目中,以及可用于此目的的各种文件夹(参见第二章、第三章、第六章、【加载精灵表】了解更多具体信息)。将图像文件添加到项目中后,创建一个新的类。对于这个解决方案,新类的名称将是SBGBackground()

public class SBGBackground {

}

在第六章中创建了一个类似的类来加载 spritesheet 角色的图像和顶点。清单 7-1 (用于 OpenGL ES 1)和清单 7-2 (用于 OpenGL ES 2/3)的大部分代码直接来自第六章中的解决方案。

清单 7-1SBGBackground() (OpenGL 是 1)

public class SBGBackground {

private FloatBuffer vertexBuffer;
private FloatBuffer textureBuffer;
private ByteBuffer indexBuffer;

private int[] textures = new int[1];

private float vertices[] = {
0.0f, 0.0f, 0.0f,
1.0f, 0.0f, 0.0f,
1.0f, 1.0f, 0.0f,
0.0f, 1.0f, 0.0f,
};

private float texture[] = {
0.0f, 0.0f,
1.0f, 0f,
1f, 1.0f,
0f, 1f,
};

private byte indices[] = {
0,1,2,
0,2,3,
};

public SBGBackground() {
      ByteBuffer byteBuf = ByteBuffer.allocateDirect(vertices.length * 4);
byteBuf.order(ByteOrder.nativeOrder());
vertexBuffer = byteBuf.asFloatBuffer();
vertexBuffer.put(vertices);
vertexBuffer.position(0);

byteBuf = ByteBuffer.allocateDirect(texture.length * 4);
byteBuf.order(ByteOrder.nativeOrder());
textureBuffer = byteBuf.asFloatBuffer();
textureBuffer.put(texture);
textureBuffer.position(0);

indexBuffer = ByteBuffer.allocateDirect(indices.length);
indexBuffer.put(indices);
indexBuffer.position(0);
   }

public void draw(GL10 gl) {
gl.glBindTexture(GL10.GL_TEXTURE_2D, textures[0]);

gl.glFrontFace(GL10.GL_CCW);
gl.glEnable(GL10.GL_CULL_FACE);
gl.glCullFace(GL10.GL_BACK);

gl.glEnableClientState(GL10.GL_VERTEX_ARRAY);
gl.glEnableClientState(GL10.GL_TEXTURE_COORD_ARRAY);

gl.glVertexPointer(3, GL10.GL_FLOAT, 0, vertexBuffer);
gl.glTexCoordPointer(2, GL10.GL_FLOAT, 0, textureBuffer);

gl.glDrawElements(GL10.GL_TRIANGLES, indices.length, GL10.GL_UNSIGNED_BYTE, indexBuffer);

gl.glDisableClientState(GL10.GL_VERTEX_ARRAY);
gl.glDisableClientState(GL10.GL_TEXTURE_COORD_ARRAY);
gl.glDisable(GL10.GL_CULL_FACE);
   }
}

清单 7-2SBGBackground() (OpenGL 是 2/3)

class SBGBackground{
private final String vertexShaderCode =
"uniform mat4 uMVPMatrix;" +
"attribute vec4 vPosition;" +
"attribute vec2 TexCoordIn;" +
"varying vec2 TexCoordOut;" +
"void main() {" +
"  gl_Position = uMVPMatrix * vPosition;" +
"  TexCoordOut = TexCoordIn;" +
"}";

private final String fragmentShaderCode =
"precision mediump float;" +
"uniform vec4 vColor;" +
"uniform sampler2D TexCoordIn;" +
"uniform float scroll;" +
"varying vec2 TexCoordOut;" +
"void main() {" +
"}";
private float texture[] = {
 0f, 0f,
.25f, 0f,
.25f, .25f,
0f, .25f,
};

private int[] textures = new int[1];
private final FloatBuffer vertexBuffer;
private final ShortBuffer drawListBuffer;
private final FloatBuffer textureBuffer;
private final int mProgram;
private int mPositionHandle;
private int mMVPMatrixHandle;

static final int COORDS_PER_VERTEX = 3;
static final int COORDS_PER_TEXTURE = 2;
static float squareCoords[] = { -1f,  1f, 0.0f,
-1f, -1f, 0.0f,
1f, -1f, 0.0f,
1f,  1f, 0.0f };

private final short drawOrder[] = { 0, 1, 2, 0, 2, 3 };

private final int vertexStride = COORDS_PER_VERTEX * 4;
public static int textureStride = COORDS_PER_TEXTURE * 4;

public SBGBackground() {
ByteBuffer bb = ByteBuffer.allocateDirect(
bb.order(ByteOrder.nativeOrder());
vertexBuffer = bb.asFloatBuffer();
vertexBuffer.put(squareCoords);
vertexBuffer.position(0);

bb = ByteBuffer.allocateDirect(texture.length * 4);
bb.order(ByteOrder.nativeOrder());
textureBuffer = bb.asFloatBuffer();
textureBuffer.put(texture);
textureBuffer.position(0);

ByteBuffer dlb = ByteBuffer.allocateDirect(
dlb.order(ByteOrder.nativeOrder());
drawListBuffer = dlb.asShortBuffer();
drawListBuffer.put(drawOrder);
drawListBuffer.position(0);

int vertexShader = SBGGameRenderer.loadShader(
GLES20.GL_VERTEX_SHADER,vertexShaderCode);
int fragmentShader = SBGGameRenderer.loadShader(
GLES20.GL_FRAGMENT_SHADER,fragmentShaderCode);

mProgram = GLES20.glCreateProgram();
GLES20.glAttachShader(mProgram, vertexShader);
GLES20.glAttachShader(mProgram, fragmentShader);
GLES20.glLinkProgram(mProgram);
}

public void draw(float[] mvpMatrix) {
GLES20.glUseProgram(mProgram);

mPositionHandle = GLES20.glGetAttribLocation(mProgram, "vPosition");

GLES20.glEnableVertexAttribArray(mPositionHandle);

int vsTextureCoord = GLES20.glGetAttribLocation(mProgram, "TexCoordIn");
GLES20.glVertexAttribPointer(
mPositionHandle, COORDS_PER_VERTEX,
GLES20.GL_FLOAT, false,
vertexStride, vertexBuffer);
GLES20.glVertexAttribPointer(vsTextureCoord, COORDS_PER_TEXTURE,
GLES20.GL_FLOAT, false,
textureStride, textureBuffer);
GLES20.glEnableVertexAttribArray(vsTextureCoord);
GLES20.glActiveTexture(GLES20.GL_TEXTURE0);
GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, textures[0]);
int fsTexture = GLES20.glGetUniformLocation(mProgram, "TexCoordOut");
GLES20.glUniform1i(fsTexture, 0);
mMVPMatrixHandle = GLES20.glGetUniformLocation(mProgram, "uMVPMatrix");

GLES20.glUniformMatrix4fv(mMVPMatrixHandle, 1, false, mvpMatrix, 0);

GLES20.glDrawElements(GLES20.GL_TRIANGLES, drawOrder.length,
GLES20.GL_UNSIGNED_SHORT, drawListBuffer);

GLES20.glDisableVertexAttribArray(mPositionHandle);
}
}

这个类以其当前的形式创建顶点、索引和纹理数组。它还包含一个初始化缓冲区的构造函数和一个在需要绘制背景图像时调用的draw()方法。基于你在本书之前的解决方案中看到的其他图像类,这个类应该看起来很熟悉。

请特别注意清单 7-1 中加粗的代码行。这一行创建了一个名为texturesint数组,但是只将其实例化为一个元素。其原因是现有的用于生成纹理名称的 OpenGL ES 方法(glGenTextures)只接受一组纹理,因为它是为处理多个纹理而构建的。

现在我们将使用 OpenGL ES 1 和 OpenGL ES 2/3 创建一个名为loadTexture()的新方法,这是加载图像文件并将其作为纹理映射到顶点所需的。对于 OpenGL ES 1,请使用以下内容:

```java` public void loadTexture(GL10 gl,int texture, Context context) {

}


对于 OpenGL ES 2/3,请使用以下内容:

```java
public void loadTexture(int texture, Context context) {

}

请注意,该方法的 OpenGL ES 1 版本接受 OpenGL ES 对象、要加载的图像的 ID 和当前 Android 上下文。在这个方法中,您需要从图像创建一个位图(使用传入的 ID),然后设置一些纹理参数,这些参数将指示 OpenGL ES 如何处理纹理(参见清单 7-3 和 7-4 ) )。

清单 7-3loadTexture() (OpenGL 是 1)

public void loadTexture(GL10 gl,int texture, Context context) {
InputStream imagestream = context.getResources().openRawResource(texture);
Bitmap bitmap = null;
try {

bitmap = BitmapFactory.decodeStream(imagestream);

}catch(Exception e){

}finally {
try {
imagestream.close();
imagestream = null;
} catch (IOException e) {
}
}

gl.glGenTextures(1, textures, 0);
gl.glBindTexture(GL10.GL_TEXTURE_2D, textures[0]);

gl.glTexParameterf(GL10.GL_TEXTURE_2D, GL10.GL_TEXTURE_MIN_FILTER, GL10.GL_NEAREST);
gl.glTexParameterf(GL10.GL_TEXTURE_2D, GL10.GL_TEXTURE_MAG_FILTER, GL10.GL_LINEAR);

gl.glTexParameterf(GL10.GL_TEXTURE_2D, GL10.GL_TEXTURE_WRAP_S, GL10.GL_REPEAT);
gl.glTexParameterf(GL10.GL_TEXTURE_2D, GL10.GL_TEXTURE_WRAP_T, GL10.GL_REPEAT);

GLUtils.texImage2D(GL10.GL_TEXTURE_2D, 0, bitmap, 0);

bitmap.recycle();
}

清单 7-4loadTexture() (OpenGL 是 2/3)

public void loadTexture(int texture, Context context) {
InputStream imagestream = context.getResources().openRawResource(texture);
Bitmap bitmap = null;

android.graphics.Matrix flip = new android.graphics.Matrix();
flip.postScale(-1f, -1f);

try {

bitmap = BitmapFactory.decodeStream(imagestream);

}catch(Exception e){
//Handle your exceptions here
}finally {
try {
imagestream.close();
imagestream = null;
} catch (IOException e) {
 //Handle your exceptions here
}
}

GLES20.glGenTextures(1, textures, 0);
GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, textures[0]);

GLES20.glTexParameterf(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_MIN_FILTER, GLES20.GL_NEAREST);
GLES20.glTexParameterf(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_MAG_FILTER, GLES20.GL_LINEAR);

GLES20.glTexParameterf(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_WRAP_S, GLES20.GL_REPEAT);
GLES20.glTexParameterf(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_WRAP_T, GLES20.GL_REPEAT);

GLUtils.texImage2D(GLES20.GL_TEXTURE_2D, 0, bitmap, 0);

bitmap.recycle();
}

请特别注意这个方法中加粗的代码。这段代码显式地将纹理设置为沿 x 和 y 轴重复。在 OpenGL ES 中,S 纹理坐标轴指的是 x 笛卡尔轴;t 是指 y 轴。在这个例子中,重复纹理是至关重要的,因为我们使用了一个将被无限重复的星域图像。

既然SBGBackground()类已经完成,那么需要将代码添加到利用新类的游戏循环中。完成这个解决方案还需要两个步骤。首先是实例化一个新的SBGBackground。然后图像 ID 必须传递给loadTexture()方法。

在您的游戏循环中,实例化一个新的SBGBackground,如下所示:

private SBGBackground background1 = new SBGBackground();

游戏循环包含在 OpenGL ES Renderer的实现中。因此,它有一些必需的方法,这些方法在前面的章节中已经详细介绍过了。其中一个方法是onSurfaceCreated(),这是加载纹理的代码应该被调用的地方。

public void onSurfaceCreated(GL10 gl, EGLConfig config) {
//TODO Auto-generated method stub

...

background1.loadTexture(gl, R.drawable.starfield, context);
}

接下来的两个解决方案将包括滚动已经加载的背景纹理。

7.2 水平滚动背景

问题

背景目前是静态的,应该会水平滚动。

解决办法

在游戏循环中创建一个新类,将背景纹理在 y 轴上平移一个设定的量。

它是如何工作的

这个解决方案的 OpenGL ES 1 版本的第一步是创建两个变量,分别用于跟踪背景纹理的当前位置和平移纹理的值。

int bgScroll1 = 0;
float SCROLL_BACKGROUND_1 = .002f;

这些变量可以是您的Renderer类的本地变量,或者您可以将它们存储在一个单独的类中。

OpenGL ES Renderer实现中的onDrawFrame()方法会在游戏循环的每次迭代中被调用。你需要创建一个新的方法,叫做scrollBackground(),它又从onDrawFrame()方法中被调用(见清单 7-5 )。

清单 7-5scrollBackground() (OpenGL 是 1)

private void scrollBackground1(GL10 gl){
if (bgScroll1 == Float.MAX_VALUE){
bgScroll1 = 0f;
}

gl.glMatrixMode(GL10.GL_MODELVIEW);
gl.glLoadIdentity();
gl.glPushMatrix();
gl.glScalef(1f, 1f, 1f);
gl.glTranslatef(0f, 0f, 0f);

gl.glMatrixMode(GL10.GL_TEXTURE);
gl.glLoadIdentity();
gl.glTranslatef(0.0f, bgScroll1, 0.0f);

background1.draw(gl);
gl.glPopMatrix();
bgScroll1 +=  SCROLL_BACKGROUND_1;
gl.glLoadIdentity();

}

该方法的第一部分测试bgScroll1变量的当前值。考虑到浮动有一个上限,这个if语句是必要的,以确保你不超载你的浮动。

接下来,在开始使用纹理矩阵之前,将缩放和转换模型矩阵视图。注意,纹理模型的 y 坐标被bgScroll1中的值平移。这就是在屏幕上移动背景的原因。

最后,调用SBGBackground()类的draw()方法,用SCROLL_BACKGROUND_1变量中的值增加bgScroll1变量,为循环的下一次迭代做准备。

onDrawFrame()方法调用新的scrollBackground()方法,背景星域将在屏幕上水平平滑移动。

在 OpenGL ES 2/3 中完成同样的过程略有不同(见清单 7-6 )。控制滚动的变量在 object 类的draw()方法中设置。这个变量也可以传递到draw()方法中,就像在第六章中用于 spritesheet 解决方案的一样。然而,由于这个背景是自动滚动的,而且是无限滚动的,所以在方法中处理一切更有意义。

清单 7-6scrollBackground() (OpenGL 是 2/3)

class SBGBackground{
public float scroll = 0;
private final String vertexShaderCode =
"uniform mat4 uMVPMatrix;" +
"attribute vec4 vPosition;" +
"attribute vec2 TexCoordIn;" +
"varying vec2 TexCoordOut;" +
"void main() {" +
"  gl_Position = uMVPMatrix * vPosition;" +
"  TexCoordOut = TexCoordIn;" +
"}";

private final String fragmentShaderCode =
"precision mediump float;" +
"uniform vec4 vColor;" +
"uniform sampler2D TexCoordIn;" +
"uniform float scroll;" +
"varying vec2 TexCoordOut;" +
"void main() {" +
" gl_FragColor = texture2D(TexCoordIn, vec2(TexCoordOut.x + scroll,TexCoordOut.y));"+
"}";
private float texture[] = {
0f, 0f,
.25f, 0f,
.25f, .25f,
0f, .25f,
};

private int[] textures = new int[1];
private final FloatBuffer vertexBuffer;
private final ShortBuffer drawListBuffer;
private final FloatBuffer textureBuffer;
private final int mProgram;
private int mPositionHandle;
private int mMVPMatrixHandle;

static final int COORDS_PER_VERTEX = 3;
static final int COORDS_PER_TEXTURE = 2;
static float squareCoords[] = { -1f,  1f, 0.0f,
-1f, -1f, 0.0f,
1f, -1f, 0.0f,
1f,  1f, 0.0f };

private final short drawOrder[] = { 0, 1, 2, 0, 2, 3 };

private final int vertexStride = COORDS_PER_VERTEX * 4;
public static int textureStride = COORDS_PER_TEXTURE * 4;

public void loadTexture(int texture, Context context) {
      ...
   }
public SBGBackground() {
...
}

public void draw(float[] mvpMatrix) {
scroll += .01f;
GLES20.glUseProgram(mProgram);

mPositionHandle = GLES20.glGetAttribLocation(mProgram, "vPosition");

GLES20.glEnableVertexAttribArray(mPositionHandle);

int vsTextureCoord = GLES20.glGetAttribLocation(mProgram, "TexCoordIn");
GLES20.glVertexAttribPointer(mPositionHandle, COORDS_PER_VERTEX,
GLES20.GL_FLOAT, false,
vertexStride, vertexBuffer);
GLES20.glVertexAttribPointer(vsTextureCoord, COORDS_PER_TEXTURE,
GLES20.GL_FLOAT, false,
textureStride, textureBuffer);
GLES20.glEnableVertexAttribArray(vsTextureCoord);
GLES20.glActiveTexture(GLES20.GL_TEXTURE0);
GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, textures[0]);
int fsTexture = GLES20.glGetUniformLocation(mProgram, "TexCoordOut");
int fsScroll = GLES20.glGetUniformLocation(mProgram, "scroll");
GLES20.glUniform1i(fsTexture, 0);
GLES20.glUniform1f(fsScroll, scroll);
mMVPMatrixHandle = GLES20.glGetUniformLocation(mProgram, "uMVPMatrix");

GLES20.glUniformMatrix4fv(mMVPMatrixHandle, 1, false, mvpMatrix, 0);

GLES20.glDrawElements(GLES20.GL_TRIANGLES, drawOrder.length,
GLES20.GL_UNSIGNED_SHORT, drawListBuffer);

GLES20.glDisableVertexAttribArray(mPositionHandle);
}
}

7.3 垂直滚动背景

问题

背景目前是静态的,它应该水平滚动。

解决办法

在游戏循环中创建一个新类,在 x 轴上将背景纹理平移一个设定的量。

它是如何工作的

在前一个解决方案的基础上,只需要做一个改变就可以垂直而不是水平滚动背景,如清单 7-7 和清单 7-8 所示。

清单 7-7 。垂直滚动(OpenGL ES 1)

private void scrollBackground1(GL10 gl){
if (bgScroll1 == Float.MAX_VALUE){
bgScroll1 = 0f;
}

gl.glMatrixMode(GL10.GL_MODELVIEW);
gl.glLoadIdentity();
gl.glPushMatrix();
gl.glScalef(1f, 1f, 1f);
gl.glTranslatef(0f, 0f, 0f);

gl.glMatrixMode(GL10.GL_TEXTURE);
gl.glLoadIdentity();
gl.glTranslatef(bgScroll1, 0.0f, 0.0f);

background1.draw(gl);
gl.glPopMatrix();
bgScroll1 +=  SCROLL_BACKGROUND_1;
gl.glLoadIdentity();

}

清单 7-8 。垂直滚动(OpenGL ES 2/3)

private final String fragmentShaderCode =
"precision mediump float;" +
"uniform vec4 vColor;" +
"uniform sampler2D TexCoordIn;" +
"uniform float scroll;" +
"varying vec2 TexCoordOut;" +
"void main() {" +
" gl_FragColor = texture2D(TexCoordIn, vec2(TexCoordOut.x,TexCoordOut.y+ scroll));"+
"}";

注意 OpenGL ES 1 方法的scrollBackground()中加粗的代码。在glTranslatef()方法调用中,bgScroll1值已经从 y 轴位置移动到 x 轴位置。这就是使背景垂直滚动而不是水平滚动所需的全部内容。

OpenGL ES 2/3 唯一需要更改的代码是片段着色器。滚动浮动现在被添加到纹理的 y 坐标而不是 x 坐标。`

八、滚动多个背景

在第七章中,提出了创建可滚动背景的解决方案。虽然该解决方案将帮助您创建一个引人注目的游戏,它可以有更多的深度。

在这一章中,你将会看到在游戏背景中加载和使用两张图片的解决方案。这不仅会让你的游戏更有活力,还能让你以不同的速度滚动两幅图像。

在这一章的最后,给出了以不同速度滚动两幅不同背景图像的解决方案。这给了游戏一个更真实的外观,并增加了平面环境的深度。

8.1 加载两幅背景图像

问题

游戏的背景需要包含两个图像。

解决办法

使用 OpenGL 加载两幅图像来创建一个分层的背景,可以独立滚动以获得更动态的外观。

它是如何工作的

正如在第七章的中所讨论的,加载图像以供 OpenGL ES 使用的最简单的方法是创建一个自定义类来加载所有需要的顶点,并将图像作为纹理映射到这些顶点。

在这个解决方案中,您将复制两个图像到项目的res文件夹中。然后,您将实例化为第七章中的解决方案创建的类的两个副本。使用这两个独立的实例,您将在后台加载并绘制两个不同的图像。图 8-1 和 8-2 显示了将在本解决方案中使用的星域图像和碎片域图像。

9781430257646_Fig08-01.jpg

图 8-1 。星空图像

9781430257646_Fig08-02.jpg

图 8-2 。碎片区域图像

第一步是将图像添加到项目的正确的res/drawable文件夹中。我们之前讨论过将图像添加到项目,以及可用于此目的的各种文件夹。将图像文件添加到项目中后,您可以实例化在第七章中创建的类的两个副本。

这些类需要在包含游戏循环的类中实例化。包含游戏循环的类是 OpenGL ES Renderer的一个实现。背景类应该在一个所有的Renderer方法都可以访问的位置被实例化。

作为参考,清单 8-1 和 8-2 显示了来自第七章 的SBGBackground()类的完整代码。

清单 8-1SBGBackground (OpenGL 是 1)

public class SBGBackground {

private FloatBuffer vertexBuffer;
private FloatBuffer textureBuffer;
private ByteBuffer indexBuffer;

private int[] textures = new int[1];

private float vertices[] = {
0.0f, 0.0f, 0.0f,
1.0f, 0.0f, 0.0f,
1.0f, 1.0f, 0.0f,
0.0f, 1.0f, 0.0f,
};

private float texture[] = {
0.0f, 0.0f,
1.0f, 0f,
1f, 1.0f,
0f, 1f,
};

private byte indices[] = {
0,1,2,
0,2,3,
};

Public SBGBackground() {
ByteBuffer byteBuf = ByteBuffer.allocateDirect(vertices.length * 4);
byteBuf.order(ByteOrder.nativeOrder());
vertexBuffer = byteBuf.asFloatBuffer();
vertexBuffer.put(vertices);
vertexBuffer.position(0);

byteBuf = ByteBuffer.allocateDirect(texture.length * 4);
byteBuf.order(ByteOrder.nativeOrder());
textureBuffer = byteBuf.asFloatBuffer();
textureBuffer.put(texture);
textureBuffer.position(0);

indexBuffer = ByteBuffer.allocateDirect(indices.length);
indexBuffer.put(indices);
indexBuffer.position(0);
   }

public void draw(GL10gl) {
gl.glBindTexture(GL10.GL_TEXTURE_2D, textures[0]);

gl.glFrontFace(GL10.GL_CCW);
gl.glEnable(GL10.GL_CULL_FACE);
gl.glCullFace(GL10.GL_BACK);

gl.glEnableClientState(GL10.GL_VERTEX_ARRAY);
gl.glEnableClientState(GL10.GL_TEXTURE_COORD_ARRAY);

gl.glVertexPointer(3, GL10.GL_FLOAT, 0, vertexBuffer);
gl.glTexCoordPointer(2, GL10.GL_FLOAT, 0, textureBuffer);

gl.glDrawElements(GL10.GL_TRIANGLES, indices.length, GL10.GL_UNSIGNED_BYTE, indexBuffer);

gl.glDisableClientState(GL10.GL_VERTEX_ARRAY);
gl.glDisableClientState(GL10.GL_TEXTURE_COORD_ARRAY);
gl.glDisable(GL10.GL_CULL_FACE);
   }

public void loadTexture(GL10gl,int texture, Context context) {
InputStream imagestream = context.getResources().openRawResource(texture);
Bitmap bitmap = null;
try {

bitmap = BitmapFactory.decodeStream(imagestream);

}catch(Exception e){

}finally {
try {
imagestream.close();
imagestream = null;
} catch (IOException e) {
}
}

gl.glGenTextures(1, textures, 0);
gl.glBindTexture(GL10.GL_TEXTURE_2D, textures[0]);

gl.glTexParameterf(GL10.GL_TEXTURE_2D, GL10.GL_TEXTURE_MIN_FILTER, GL10.GL_NEAREST);
gl.glTexParameterf(GL10.GL_TEXTURE_2D, GL10.GL_TEXTURE_MAG_FILTER, GL10.GL_LINEAR);

gl.glTexParameterf(GL10.GL_TEXTURE_2D, GL10.GL_TEXTURE_WRAP_S, GL10.GL_REPEAT);
gl.glTexParameterf(GL10.GL_TEXTURE_2D, GL10.GL_TEXTURE_WRAP_T, GL10.GL_REPEAT);

GLUtils.texImage2D(GL10.GL_TEXTURE_2D, 0, bitmap, 0);

bitmap.recycle();
}

}

清单 8-2SBGBackground (OpenGL 是 2/3)

class SBGBackground{
private final String vertexShaderCode =
"uniform mat4 uMVPMatrix;" +
"attribute vec4 vPosition;" +
"attribute vec2 TexCoordIn;" +
"varying vec2 TexCoordOut;" +
"void main() {" +
"  gl_Position = uMVPMatrix * vPosition;" +
"  TexCoordOut = TexCoordIn;" +
"}";

private final String fragmentShaderCode =
"precision mediump float;" +
"uniform vec4 vColor;" +
"uniform sampler2D TexCoordIn;" +
"uniform float scroll;" +
"varying vec2 TexCoordOut;" +
"void main() {" +
" gl_FragColor = texture2D(TexCoordIn, vec2(TexCoordOut.x + scroll,TexCoordOut.y));"+
"}";
private float texture[] = {
0f, 0f,
.25f, 0f,
.25f, .25f,
0f, .25f,
};

Private int[] textures = new int[1];
private final FloatBuffer vertexBuffer;
private final ShortBuffer drawListBuffer;
private final FloatBuffer textureBuffer;
private final int mProgram;
private int mPositionHandle;
private int mMVPMatrixHandle;

static final int COORDS_PER_VERTEX = 3;
static final int COORDS_PER_TEXTURE = 2;
static float squareCoords[] = { -1f,  1f, 0.0f,
 -1f, -1f, 0.0f,
1f, -1f, 0.0f,
1f,  1f, 0.0f };

private final short drawOrder[] = { 0, 1, 2, 0, 2, 3 };

private final int vertexStride = COORDS_PER_VERTEX * 4;
public static int textureStride = COORDS_PER_TEXTURE * 4;

public void loadTexture(int texture, Context context) {
InputStreami magestream = context.getResources().openRawResource(texture);
      Bitmap bitmap = null;

android.graphics.Matrix flip = new android.graphics.Matrix();
flip.postScale(-1f, -1f);
try {

bitmap = BitmapFactory.decodeStream(imagestream);

}catch(Exception e){

}finally {
try {
imagestream.close();
imagestream = null;
} catch (IOException e) {
}
      }

GLES20.glGenTextures(1, textures, 0);
GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, textures[0]);

GLES20.glTexParameterf(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_MIN_FILTER,
GLES20.GL_NEAREST);
GLES20.glTexParameterf(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_MAG_FILTER,
GLES20.GL_LINEAR);

GLES20.glTexParameterf(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_WRAP_S, GLES20.GL_REPEAT);
GLES20.glTexParameterf(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_WRAP_T, GLES20.GL_REPEAT);

GLUtils.texImage2D(GLES20.GL_TEXTURE_2D, 0, bitmap, 0);

bitmap.recycle();
   }
public SBGBackground() {
ByteBuffer bb = ByteBuffer.allocateDirect(
bb.order(ByteOrder.nativeOrder());
vertexBuffer = bb.asFloatBuffer();
vertexBuffer.put(squareCoords);
vertexBuffer.position(0);

bb = ByteBuffer.allocateDirect(texture.length * 4);
bb.order(ByteOrder.nativeOrder());
textureBuffer = bb.asFloatBuffer();
textureBuffer.put(texture);
textureBuffer.position(0);

ByteBuffer dlb = ByteBuffer.allocateDirect(
dlb.order(ByteOrder.nativeOrder());
drawListBuffer = dlb.asShortBuffer();
drawListBuffer.put(drawOrder);
drawListBuffer.position(0);

int vertexShader = SBGGameRenderer.loadShader(GLES20.GL_VERTEX_SHADER,
vertexShaderCode);
int fragmentShader = SBGGameRenderer.loadShader(GLES20.GL_FRAGMENT_SHADER,
fragmentShaderCode);

mProgram = GLES20.glCreateProgram();
GLES20.glAttachShader(mProgram, vertexShader);
GLES20.glAttachShader(mProgram, fragmentShader);
GLES20.glLinkProgram(mProgram);
}

public void draw(float[] mvpMatrix, float scroll) {
GLES20.glUseProgram(mProgram);

mPositionHandle = GLES20.glGetAttribLocation(mProgram, "vPosition");

GLES20.glEnableVertexAttribArray(mPositionHandle);

int vsTextureCoord = GLES20.glGetAttribLocation(mProgram, "TexCoordIn");
GLES20.glVertexAttribPointer(mPositionHandle, COORDS_PER_VERTEX,
GLES20.GL_FLOAT, false,
vertexStride, vertexBuffer);
GLES20.glVertexAttribPointer(vsTextureCoord, COORDS_PER_TEXTURE,
GLES20.GL_FLOAT, false,
textureStride, textureBuffer);
GLES20.glEnableVertexAttribArray(vsTextureCoord);
GLES20.glActiveTexture(GLES20.GL_TEXTURE0);
GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, textures[0]);
int fsTexture = GLES20.glGetUniformLocation(mProgram, "TexCoordOut");
int fsScroll = GLES20.glGetUniformLocation(mProgram, "scroll");
GLES20.glUniform1i(fsTexture, 0);
GLES20.glUniform1f(fsScroll, scroll);
mMVPMatrixHandle = GLES20.glGetUniformLocation(mProgram, "uMVPMatrix");

GLES20.glUniformMatrix4fv(mMVPMatrixHandle, 1, false, mvpMatrix, 0);

GLES20.glDrawElements(GLES20.GL_TRIANGLES, drawOrder.length,
GLES20.GL_UNSIGNED_SHORT, drawListBuffer);

GLES20.glDisableVertexAttribArray(mPositionHandle);
}
}

如果您将这段代码与第七章中的代码进行比较,您应该会注意到 OpenGL ES 2/3 版本的一个小变化。scroll 变量已被移动到构造函数中。这允许您传递滚动量,以便您可以以不同的速率滚动背景的多个实例化。

在你的游戏中实例化两个新的SBGBackground(),如下所示:

private SBGBackground background1 = new SBGBackground();
private SBGBackground background2 = new SBGBackground();

现在您需要加载图像,并使用SBGBackground()loadTexture()方法将它们映射为纹理。加载纹理的代码应该在RendereronSurfaceCreated()方法中调用。

public void onSurfaceCreated(GL10gl, EGLConfigconfig) {
//TODO Auto-generated method stub

...

background1.loadTexture(gl, R.drawable.starfield, context);
background1.loadTexture(gl, R.drawable.debrisfield, context);
}

接下来的两个解决方案将包括滚动背景纹理,现在它们已经被加载了。

8.2 滚动两个背景图像

问题

只有一个背景图像滚动。

解决办法

通过修改每个图像的滚动变量,修改游戏循环,使两个图像都滚动。

它是如何工作的

解决方案的第一步是创建四个变量,分别用于跟踪背景纹理的当前位置和平移纹理的值。

int bgScroll1 = 0;
int bgScroll2 = 0;

float SCROLL_BACKGROUND_1 = .002f;
float SCROLL_BACKGROUND_2 = .002f;

这些变量可以是您的Renderer类的本地变量,或者您可以将它们存储在一个单独的类中。这个解决方案借鉴了第七章中的一个解决方案。然而,如果你一开始不慢的话,在游戏中跟踪多个移动的元素可能会很棘手。尽量避免跳过这一步,因为很容易错过一个重要的细节。

OpenGL ES Renderer实现中的onDrawFrame()方法会在游戏循环的每次迭代中被调用。创建一个名为scrollBackgrounds()的新方法,它将从onDrawFrame()方法中被调用。参见清单 8-3 和清单 8-4 。

清单 8-3scrollBackgrounds() (OpenGL 是 1)

private void scrollBackgrounds(GL10gl){
if (bgScroll1 == Float.MAX_VALUE){
bgScroll1 = 0f;
}

if (bgScroll2 == Float.MAX_VALUE){
bgScroll2 = 0f;
}

gl.glMatrixMode(GL10.GL_MODELVIEW);
gl.glLoadIdentity();
gl.glPushMatrix();
gl.glScalef(1f, 1f, 1f);
gl.glTranslatef(0f, 0f, 0f);

gl.glMatrixMode(GL10.GL_TEXTURE);
gl.glLoadIdentity();
gl.glTranslatef(0.0f, bgScroll1, 0.0f);

background1.draw(gl);
gl.glPopMatrix();
bgScroll1 +=  SCROLL_BACKGROUND_1;
gl.glLoadIdentity();

gl.glMatrixMode(GL10.GL_TEXTURE);
gl.glLoadIdentity();
gl.glTranslatef(0.0f, bgScroll2, 0.0f);

background2.draw(gl);
gl.glPopMatrix();
bgScroll2 +=  SCROLL_BACKGROUND_2;
gl.glLoadIdentity();

}

清单 8-4scrollBackgrounds() (OpenGL 是 2/3)

private void scrollBackgrounds(GL10gl){
if (bgScroll1 == Float.MAX_VALUE){
bgScroll1 = 0f;
}

if (bgScroll2 == Float.MAX_VALUE){
bgScroll2 = 0f;
}

background1.draw(mMVPMatrix, bgScroll1);
background2.draw(mMVPMatrix, bgScroll2);
bgScroll1 +=  SCROLL_BACKGROUND_1;
bgScroll2 +=  SCROLL_BACKGROUND_2;
}

该方法的第一部分测试bgScroll1bgScroll2变量的当前值。就像第七章中的一样,if声明对于确保你不会让你的浮存金超载是必要的。

视图矩阵和纹理矩阵模型被缩放和转换,以提供背景图像所需的“运动”。

最后,从onDrawFrame()方法调用新的scrollBackgrounds()方法,两个背景图像应该一起在屏幕上滚动。背景应该出现在图 8-3 中。

9781430257646_Fig08-03.jpg

图 8-3 。两个背景图像在一起

8.3 以不同的速度滚动两幅背景图像

问题

背景图像不会以不同的速度滚动。

解决办法

通过修改游戏循环以不同速度滚动多个背景图像来增加深度感。

它是如何工作的

在前一个解决方案的基础上,只需要做一个改变就可以以不同的速度滚动背景图像。

理想情况下,为了创造一种人工的深度感,您会希望前景图像(两幅图像中)的滚动速度比背景中最远的图像快。

为了实现这一效果,将先前解决方案中的SCROLL_BACKGROUND_2值更改为一个更大的数字。设置的数值越大,图像滚动的速度越快。

int bgScroll1 = 0;
int bgScroll2 = 0;

float SCROLL_BACKGROUND_1 = .002f;
float SCROLL_BACKGROUND_2 = .005f;