【前言】
最近,“爱消除”游戏异常的火爆,它正是山寨之王的作品。从今天开始,我们将连续7天,每天一个小时,用OpenGL ES技术,按照解决一般问题的思路,进入有趣的山寨之旅。
学习本教程最好的方法,就是运行附件的代码,对于不理解的地方再看看课程是如何解释的。
第一天
【课程内容】
今天我们将基于OpenGL ES搭建一个简单的游戏框架,并绘制出第一个图案。
【源代码下载地址】http://download.csdn.net/detail/elong_2009/6444773
1、设计程序框架
实现这个游戏的框架非常简单,仅包含一个activity,一个渲染视图及若干个渲染类对象。主框架代码加起来只有120行。
1.1 CrazyLinkActivity 类
该类通过创建OpenGL ES渲染视图,实现OpenGL图形的显示。
package elong.CrazyLink;
import android.app.Activity;
import android.os.Bundle;
public class CrazyLinkActivity extends Activity {
CrazyLinkGLSurfaceView mGLSurfaceView;
/** Called when the activity is first created. */
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
mGLSurfaceView = new CrazyLinkGLSurfaceView(this);
setContentView(mGLSurfaceView);
mGLSurfaceView.requestFocus();
mGLSurfaceView.setFocusableInTouchMode(true);
}
@Override
protected void onResume() {
// TODO Auto-generated method stub
super.onResume();
mGLSurfaceView.onResume();
}
@Override
protected void onPause() {
// TODO Auto-generated method stub
super.onPause();
mGLSurfaceView.onPause();
}
}
1.2 CrazyLinkGLSurfaceView 类
该类提供了一个OpenGL ES场景渲染器,通过onDrawFrame 方法,将要绘制的图案渲染后输出。
package elong.CrazyLink;
import java.io.IOException;
import elong.CrazyLink.Draw.DrawAnimal;
import java.io.InputStream;
import javax.microedition.khronos.egl.EGLConfig;
import javax.microedition.khronos.opengles.GL10;
import elong.CrazyLink.R;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.opengl.GLSurfaceView;
import android.opengl.GLUtils;
public class CrazyLinkGLSurfaceView extends GLSurfaceView{
private SceneRenderer mRenderer;//场景渲染器
static int animalTextureId;//动物素材纹理id
public CrazyLinkGLSurfaceView(CrazyLinkActivity activity) {
super(activity);
mRenderer = new SceneRenderer();//创建场景渲染器
setRenderer(mRenderer); //设置渲染器
setRenderMode(GLSurfaceView.RENDERMODE_CONTINUOUSLY);//设置渲染模式为主动渲染
}
private class SceneRenderer implements GLSurfaceView.Renderer
{
DrawAnimal drawAnimal;
public void onDrawFrame(GL10 gl) {
gl.glShadeModel(GL10.GL_SMOOTH);//着色模式为平滑着色
gl.glClear(GL10.GL_COLOR_BUFFER_BIT|GL10.GL_DEPTH_BUFFER_BIT);//清除颜色缓冲区及深度缓冲区
gl.glMatrixMode(GL10.GL_MODELVIEW);//设置矩阵为模式矩阵
gl.glLoadIdentity(); //设置当前矩阵为单位矩阵
gl.glTranslatef(0f, 0f, -2.0f); //调整Z轴,可以调整图像显示的大小
drawAnimal.draw(gl,1,0,0); //在这里绘制需要显示的素材对象
}
public void onSurfaceChanged(GL10 gl, int width, int height) {
gl.glViewport(0, 0, width, height); //设置当前矩阵为投影矩阵
gl.glMatrixMode(GL10.GL_PROJECTION); //设置当前矩阵为单位矩阵
gl.glLoadIdentity(); //计算透视投影的比例
float ratio = (float) width / height; //调用此方法计算产生透视投影矩阵
gl.glFrustumf(-ratio, ratio, -1, 1, 1, 100);
}
public void onSurfaceCreated(GL10 gl, EGLConfig config) {
gl.glDisable(GL10.GL_DITHER);//关闭抗抖动
gl.glHint(GL10.GL_PERSPECTIVE_CORRECTION_HINT,GL10.GL_FASTEST);//设置特定Hint项目的模式,这里为设置为使用快速模式
gl.glClearColor(0,0,0,0); //设置屏幕背景色黑色RGBA
gl.glShadeModel(GL10.GL_SMOOTH); //设置着色模型为平滑着色
gl.glEnable(GL10.GL_DEPTH_TEST);//启用深度测试
animalTextureId = initTexture(gl, R.drawable.animal);//初始化纹理对象
drawAnimal = new DrawAnimal(animalTextureId);//参加动物素材对象
}
}
public int initTexture(GL10 gl, int drawableId)
{
int[] textures = new int[1];
gl.glGenTextures(1, textures, 0);
int currTextureId = textures[0];
gl.glBindTexture(GL10.GL_TEXTURE_2D, currTextureId);
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_CLAMP_TO_EDGE);//指定S坐标轴贴图模式
gl.glTexParameterf(GL10.GL_TEXTURE_2D, GL10.GL_TEXTURE_WRAP_T, GL10.GL_CLAMP_TO_EDGE);//指定T坐标轴贴图模式
InputStream is = this.getResources().openRawResource(drawableId);
Bitmap bitmapTmp;
try{
bitmapTmp = BitmapFactory.decodeStream(is);
}
finally{
try{
is.close();
}
catch (IOException e) {
e.printStackTrace();
}
}
GLUtils.texImage2D(GL10.GL_TEXTURE_2D, 0, bitmapTmp, 0);
bitmapTmp.recycle();
return currTextureId;
}
}
以上实现了游戏框架的主要内容。今后的课程,都会基于这个基本框架来设计代码。
1.3 DrawAnimal 类
本课程专门设计一系列的渲染类,用于对特定对象或场景的绘制,凡是Draw打头的类,都是渲染类。后续课程还会陆续提到更多的渲染类。所有渲染类,都集中放在package elong.CrazyLink.Draw包中。
渲染类的核心用途就是将一个对象或动作抽象出来,通过渲染类对象,实现特定对象或动作的绘制。在后续的应用中,我们会将多个渲染类合并一起使用,以达到显示特定场景或特效的目的。
每一个渲染类都会有一个draw方法,该方法实现对特定对象或场景的绘制。要实现一个新的场景或效果,您只需用设计好渲染类的draw方法即可。
DrawAnimal 类的代码请参考附件。部分代码在下节还会有讲解。
2、绘制第一个图案
我们要绘制的第一个图案效果如下图,这是素材图片animal.png中7个素材中的其中一个。
2.1基本原理
很显然,我们要绘制的是一张正方形的图片,通过纹理贴图,可以很容易地实现。需要注意的是,在OpenGL ES中,并没用提供绘制正方形的操作,绘制一个正方形,需要转换成绘制两个三角形。
这里需要关注的一个技术细节是背面裁剪,这是OpenGL ES的一项功能,含义是,打开此功能后,视角在三角形的背面时不渲染此三角形(即无法看到此三角形),该功能可以提高渲染的效率。
因此,我们需要保证在观察方向上渲染三角形,否则就有可能会看不到所绘制的图像。很多初学者经常会遇到这个问题,如果您绘制的三角形没有按预期出现,您可能需要检查是不是这个原因引起的。
以下是确定一个三角形正反面的方法:通常情况下,当面对一个三角形时,如果顶点的顺序是逆时针的,则位于三角形的正面;反之就是反面。
如下图所示:
2.2顶点坐标数据
知道了这个原理之后,我们就可以设计正方形的顶点数据了,如下就是顶点坐标数据的定义:
int vertices[]=new int[]//顶点坐标数据数组
{
-32*UNIT_SIZE,32*UNIT_SIZE,0,
-32*UNIT_SIZE,-32*UNIT_SIZE,0,
32*UNIT_SIZE,-32*UNIT_SIZE,0,
32*UNIT_SIZE,-32*UNIT_SIZE,0,
32*UNIT_SIZE,32*UNIT_SIZE,0,
-32*UNIT_SIZE,32*UNIT_SIZE,0
};
附件的代码中还根据显示位置(col,row)计算了偏移量
int deltaX = ((col-3)*64*UNIT_SIZE);
int deltaY = ((row-3)*64*UNIT_SIZE);
为了清晰起见,这里的定义没有加上偏移量deltaX及deltaY。
注意:顶点坐标数据采用的笛卡尔坐标系,其坐标值得范围是任意的;而纹理顶点数据所采用的S-T坐标系,其坐标取值范围是0.0~1.0
2.3 纹理顶点坐标数据
对应的,我们需要为正方形的每个顶点设置对应的纹理顶点坐标,如下定义:
float textureCoors[]=new float[]//顶点纹理S、T坐标值数组
{
(witch - 1) * textureRatio,0,
(witch - 1) * textureRatio,1,
witch * textureRatio,1,
witch * textureRatio,1,
witch * textureRatio,0,
(witch - 1) * textureRatio,0
};
值得一提的是,由于我们将7个动物素材集中在一张图片(animal.png)中加载进来(为了满足OpenGL ES对像素的要求,实际空出了一个素材的位置,在OpenGL ES中进行纹理映射时对纹理图片的尺寸是有要求的,纹理图片的宽度和高度必须为2n (2的n次方),即32x32,256x512等。)
而实际显示的时候,我们仅想显示其中的一个素材,因此引入了一个变量textureRatio,该变量的值是textureRatio = (float)(1/8.0f),用来精确控制每个动物素材(witch:有效地范围为1~7)对应的纹理坐标。通过传入不同的witch可以渲染不同的对象。
如下图示:
2.4 渲染类的核心方法draw
最后,介绍一下DrawAnimal类中得draw方法。在后续的课程中,用DrawXxxx命名的类都是用来渲染某个特定场景的,这种类中都会有一个公有的方法draw。
DrawAnimal中的draw方法是这样定义的:
public void draw(GL10 gl, int witch, int col, int row)
该方法可以将witch素材绘制在x=col, y=row的位置。
在这里,我们把draw方法的实现贴出来,具体含义直接参考代码的注释。在后续的课程当中,只会将新提到的知识点将代码贴出。完整的代码请参考对应的附件,不再啰嗦。
public void draw(GL10 gl, int witch, int col, int row)
{
initVertexBuffer(col, row); //根据col,row初始化顶点坐标
initTextureBuffer(witch); //根据witch来初始化纹理顶点数据
//gl.glTranslatef(col * textureRatio, row * textureRatio, 0);//在x=col,y=row的位置绘制选定的素材对象
//顶点坐标,允许使用顶点数组
gl.glEnableClientState(GL10.GL_VERTEX_ARRAY);
//为画笔指定顶点坐标数据
gl.glVertexPointer
(
3, //每个顶点的坐标数量为3 xyz
GL10.GL_FIXED, //顶点坐标值的类型为 GL_FIXED
0, //连续顶点坐标数据之间的间隔
mVertexBuffer //顶点坐标数据
);
//纹理坐标,开启纹理
gl.glEnable(GL10.GL_TEXTURE_2D);
//允许使用纹理数组
gl.glEnableClientState(GL10.GL_TEXTURE_COORD_ARRAY);
//为画笔指定纹理uv坐标数据
gl.glTexCoordPointer
(
2, //每个顶点两个纹理坐标数据 S、T
GL10.GL_FLOAT, //数据类型
0, //连续纹理坐标数据之间的间隔
mTextureBuffer //纹理坐标数据
);
gl.glBindTexture(GL10.GL_TEXTURE_2D,textureId);//为画笔绑定指定名称ID纹理
//绘制图形
gl.glDrawArrays
(
GL10.GL_TRIANGLES,
0,
vCount
);
gl.glDisable(GL10.GL_TEXTURE_2D);//关闭纹理
}
2.5考虑字节序的问题
这里额外强调的一点是:不同平台其字节序有可能不同,如果数据单元不是字节的,就一定要经过ByteBuffer进行转换,转换的关键就是要通过ByteOrder设置为nativeOrder(),以适应对应平台的字节序,否则就有可能会出现问题。
ByteBuffer cbb = ByteBuffer.allocateDirect(textureCoors.length*4);
cbb.order(ByteOrder.nativeOrder());//设置本地字节顺序
1.下载PagerSlidingTabStrip这个库文件,用来滑动导航栏下载地址 https://github.com/astuetz/PagerSlidingTabStrip
2.导入自己创建的项目中,在导入的过程中可能会出错,一是因为support.v4.jar包冲突,将其中的一个删掉,并引入另一个的support.v4.jar;二是因为项目的版本问题,修改一下创建的项目版本就可以了,项目版本越高越好。
3.简单使用:(1)在main_activity.xml文件中:
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res/com.kgcyy.app"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent">
<com.astuetz.viewpager.extensions.PagerSlidingTabStrip
android:id="@+id/pagertab"
android:layout_width="match_parent"
android:layout_height="50dp"
android:layout_alignParentTop="true"
android:layout_centerInParent="true"
android:saveEnabled="true"
app:tabPaddingLeftRight="20dp"
android:background="#6699FF"
app:indicatorColor="@color/indicator_color" />
<android.support.v4.view.ViewPager
android:id="@+id/viewpager"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_below="@id/pagertab"
android:layout_gravity="center" >
</android.support.v4.view.ViewPager>
</RelativeLayout>
(2)在activity中
public class MainActivity extends Activity{
private ViewPager mViewPager;
private PagerSlidingTabStrip mPagerSlidingTabStrip;
private View view1,view2,view3;
private List<View> viewList = new ArrayList<View>;//用来添加需要滑动的view
private List<String> titleList = new ArrayList<String>;//用来添加导航栏的标题
private MyAdapter mAdapter;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout..main_activity);
LayoutInflater inflater = getLayoutInflater();
//layout 的定义不在此显示
view1 = inflater.inflate(R.layout.layout1,null);
view2 = inflater.inflate(R.layout.layout2,null);
view2 = inflater.inflate(R.layout.layout3,null);
viewList.add(view1);
viewList.add(view2);
viewList.add(view3);
titleList.add("A");
titleList.add("B");
titleList.add("C");
mViewPager = (ViewPager) findViewById(R.id.viewpager);
mPagerSlidingTabStrip = (PagerSlidingTabStrip) findViewById(R.id.pagertab);
mAdapter = new MyAdapter();
mViewPager.setAdapter(mAdapter);
mPagerSlidingTabStrip.setViewPager(mViewPager);
mPagerTabStrip.setOnPageChangeListener(new PageChangeListener());
}
class PageChangeListener implements OnPageChangeListener {
@Override
public void onPageScrollStateChanged(int arg0) {
}
@Override
public void onPageScrolled(int arg0, float arg1, int arg2) {
}
@Override
public void onPageSelected(int arg0) {
//page滑动时的事件
}
}
class MyPagerAdapter extends PagerAdapter {
@Override
public int getCount() {
return viewList.size();
}
@Override
public boolean isViewFromObject(View arg0, Object arg1) {
return arg0 == arg1;
}
@Override
public void destroyItem(ViewGroup container, int position, Object object) {
((ViewPager) container).removeView(viewList.get(position));
}
@Override
public int getItemPosition(Object object) {
return super.getItemPosition(object);
}
@Override
public CharSequence getPageTitle(int position) {
return titleList.get(position);
}
@Override
public Object instantiateItem(ViewGroup container, int position) {
((ViewPager) container).addView(viewList.get(position), 0);
return viewList.get(position);
}
}
}
4.此项目能够实现基本的滑动,但是界面不太美观,尤其是导航栏,需要自己动手改变库文件的内容。
华为手机默认是关闭logcat信息的,这在开发调试时当然很不方便,打开log信息的方法如下
1. 进入拨号界面输入:*#*#2846579#*#*
2、2. 依次选择ProjectMenu---后台设置----LOG设置---LOG开关 点击打开, 然后在LOG级别选VERBOSE
3、
重新启动手机