Hello World for Android

From JPCT
Jump to: navigation, search

A very simple HelloWorld for jPCT-AE

This code should help to get you started. It displays a simple, lit cube that you can rotate by using the touch screen. It includes basic pause/resume handling. I'll post the complete source code and comment on some important parts later on the page.

Please note that this example is meant to be really basic. Usually, you don't want to have all these fields in your Activity but in some other class of yours instead. For this example, i tried to keep things really really simple, which is why everything in part of the Activity. Don't take this as an example of great design.

Also keep in mind that this code has been written to be executed on an old Android 1.5 "powered" device. On a current device, some tweaks might not be needed anymore. I'll comment on that in more detail in the source code comment part of this page.

The source cde

package com.threed.jpct.example;

import java.lang.reflect.Field;

import javax.microedition.khronos.egl.EGL10;
import javax.microedition.khronos.egl.EGLConfig;
import javax.microedition.khronos.egl.EGLDisplay;
import javax.microedition.khronos.opengles.GL10;

import android.app.Activity;
import android.opengl.GLSurfaceView;
import android.os.Bundle;
import android.view.MotionEvent;

import com.threed.jpct.Camera;
import com.threed.jpct.FrameBuffer;
import com.threed.jpct.Light;
import com.threed.jpct.Logger;
import com.threed.jpct.Object3D;
import com.threed.jpct.Primitives;
import com.threed.jpct.RGBColor;
import com.threed.jpct.SimpleVector;
import com.threed.jpct.Texture;
import com.threed.jpct.TextureManager;
import com.threed.jpct.World;
import com.threed.jpct.util.BitmapHelper;
import com.threed.jpct.util.MemoryHelper;

/**
 * A simple demo. This shows more how to use jPCT-AE than it shows how to write
 * a proper application for Android. It includes basic activity management to
 * handle pause and resume...
 * 
 * @author EgonOlsen
 * 
 */
public class HelloWorld extends Activity {

	// Used to handle pause and resume...
	private static HelloWorld master = null;

	private GLSurfaceView mGLView;
	private MyRenderer renderer = null;
	private FrameBuffer fb = null;
	private World world = null;
	private RGBColor back = new RGBColor(50, 50, 100);

	private float touchTurn = 0;
	private float touchTurnUp = 0;

	private float xpos = -1;
	private float ypos = -1;

	private Object3D cube = null;
	private int fps = 0;

	private Light sun = null;

	protected void onCreate(Bundle savedInstanceState) {

		Logger.log("onCreate");

		if (master != null) {
			copy(master);
		}

		super.onCreate(savedInstanceState);
		mGLView = new GLSurfaceView(getApplication());

		mGLView.setEGLConfigChooser(new GLSurfaceView.EGLConfigChooser() {
			public EGLConfig chooseConfig(EGL10 egl, EGLDisplay display) {
				// Ensure that we get a 16bit framebuffer. Otherwise, we'll fall
				// back to Pixelflinger on some device (read: Samsung I7500)
				int[] attributes = new int[] { EGL10.EGL_DEPTH_SIZE, 16, EGL10.EGL_NONE };
				EGLConfig[] configs = new EGLConfig[1];
				int[] result = new int[1];
				egl.eglChooseConfig(display, attributes, configs, 1, result);
				return configs[0];
			}
		});

		renderer = new MyRenderer();
		mGLView.setRenderer(renderer);
		setContentView(mGLView);
	}

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

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

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

	private void copy(Object src) {
		try {
			Logger.log("Copying data from master Activity!");
			Field[] fs = src.getClass().getDeclaredFields();
			for (Field f : fs) {
				f.setAccessible(true);
				f.set(this, f.get(src));
			}
		} catch (Exception e) {
			throw new RuntimeException(e);
		}
	}

	public boolean onTouchEvent(MotionEvent me) {

		if (me.getAction() == MotionEvent.ACTION_DOWN) {
			xpos = me.getX();
			ypos = me.getY();
			return true;
		}

		if (me.getAction() == MotionEvent.ACTION_UP) {
			xpos = -1;
			ypos = -1;
			touchTurn = 0;
			touchTurnUp = 0;
			return true;
		}

		if (me.getAction() == MotionEvent.ACTION_MOVE) {
			float xd = me.getX() - xpos;
			float yd = me.getY() - ypos;

			xpos = me.getX();
			ypos = me.getY();

			touchTurn = xd / -100f;
			touchTurnUp = yd / -100f;
			return true;
		}

		try {
			Thread.sleep(15);
		} catch (Exception e) {
			// No need for this...
		}

		return super.onTouchEvent(me);
	}

	protected boolean isFullscreenOpaque() {
		return true;
	}

	class MyRenderer implements GLSurfaceView.Renderer {

		private long time = System.currentTimeMillis();

		public MyRenderer() {
		}

		public void onSurfaceChanged(GL10 gl, int w, int h) {
			if (fb != null) {
				fb.dispose();
			}
			fb = new FrameBuffer(gl, w, h);

			if (master == null) {

				world = new World();
				world.setAmbientLight(20, 20, 20);

				sun = new Light(world);
				sun.setIntensity(250, 250, 250);

				// Create a texture out of the icon...:-)
				Texture texture = new Texture(BitmapHelper.rescale(BitmapHelper.convert(getResources().getDrawable(R.drawable.icon)), 64, 64));
				TextureManager.getInstance().addTexture("texture", texture);

				cube = Primitives.getCube(10);
				cube.calcTextureWrapSpherical();
				cube.setTexture("texture");
				cube.strip();
				cube.build();

				world.addObject(cube);

				Camera cam = world.getCamera();
				cam.moveCamera(Camera.CAMERA_MOVEOUT, 50);
				cam.lookAt(cube.getTransformedCenter());

				SimpleVector sv = new SimpleVector();
				sv.set(cube.getTransformedCenter());
				sv.y -= 100;
				sv.z -= 100;
				sun.setPosition(sv);
				MemoryHelper.compact();

				if (master == null) {
					Logger.log("Saving master Activity!");
					master = HelloWorld.this;
				}
			}
		}

		public void onSurfaceCreated(GL10 gl, EGLConfig config) {
		}

		public void onDrawFrame(GL10 gl) {
			if (touchTurn != 0) {
				cube.rotateY(touchTurn);
				touchTurn = 0;
			}

			if (touchTurnUp != 0) {
				cube.rotateX(touchTurnUp);
				touchTurnUp = 0;
			}

			fb.clear(back);
			world.renderScene(fb);
			world.draw(fb);
			fb.display();

			if (System.currentTimeMillis() - time >= 1000) {
				Logger.log(fps + "fps");
				fps = 0;
				time = System.currentTimeMillis();
			}
			fps++;
		}
	}
}

Comments on the source code

onCreate(Bundle savedInstanceState)

This is the basic setup for an Activity that uses OpenGL ES 1.x. It can be found in many Android examples except for two small parts:

  • The master/copy-lines: With these, i'm doing very basic pause/resume handling. I'm sure that there are better ways but this is the first thing that i came up with, i never had a problem with it and it works fine for me even in complex applications. Feel free to use anything you like instead.
  • The creation of a dedicated EGLConfigChooser: This serves one single purpose, which is to make 3D acceleration on my old Samsung Galaxy. I never came across any other device that required this and i most likely never will. But it doesn't hurt either, so i'm using it in all my OpenGL ES 1.x Activities. If you aren't targetting a real old device running on Android 1.5, you should be save to omit this part. If you are using OpenGL ES 2.0, it's not an issue anyway.
protected void onCreate(Bundle savedInstanceState) {

	Logger.log("onCreate");

	if (master != null) {
		copy(master);
	}

	super.onCreate(savedInstanceState);
	mGLView = new GLSurfaceView(getApplication());

	mGLView.setEGLConfigChooser(new GLSurfaceView.EGLConfigChooser() {
		public EGLConfig chooseConfig(EGL10 egl, EGLDisplay display) {
			// Ensure that we get a 16bit framebuffer. Otherwise, we'll fall
			// back to Pixelflinger on some device (read: Samsung I7500)
			int[] attributes = new int[] { EGL10.EGL_DEPTH_SIZE, 16, EGL10.EGL_NONE };
			EGLConfig[] configs = new EGLConfig[1];
			int[] result = new int[1];
			egl.eglChooseConfig(display, attributes, configs, 1, result);
			return configs[0];
		}
	});

	renderer = new MyRenderer();
	mGLView.setRenderer(renderer);
	setContentView(mGLView);
}
copy(Object src)

Again, this is my way to implement pause/resume/destroy/create of the Activity. It's pretty hacky. The basic idea is to keep a copy of the first instance of the Activity in a static field and copy all other fields to the new one. Again, i don't claim that this is a good solution. But it works for me and i never bothered to find another one.

private void copy(Object src) {
	try {
		Logger.log("Copying data from master Activity!");
		Field[] fs = src.getClass().getDeclaredFields();
		for (Field f : fs) {
			f.setAccessible(true);
			f.set(this, f.get(src));
		}
	} catch (Exception e) {
		throw new RuntimeException(e);
	}
}
onTouchEvent(MotionEvent me)

This is the basic touch event handling. There are two things to note here:

  • The events all set flags/variables to be evaluated in the render thread. They don't fiddle around with jPCT-AE objects directly. That's because that yould interfere with the rendering (which happens in another thread in parallel) and jPCT-AE isn't thread safe, so this wouldn't be a good idea.
  • There's a sleep in there. On older Android versions, the DalvikVM's garbage collection was really slow and the idea was to slow down the processing of touch events, so that less garbage would be created (but you might get choppy controls in return). However, looking at this code, it doesn't seem to make much sense, because in almost every case, the method will returned earlier anyway and it won't reach that sleep in most cases. I'm not sure, what i was thinking here...it might be best to ignore this sleep...
public boolean onTouchEvent(MotionEvent me) {

	if (me.getAction() == MotionEvent.ACTION_DOWN) {
		xpos = me.getX();
		ypos = me.getY();
		return true;
	}

	....

	try {
		Thread.sleep(15);
	} catch (Exception e) {
		// No need for this...
	}

	return super.onTouchEvent(me);
}
onSurfaceChanged(GL10 gl, int w, int h)

This sets up the FrameBuffer and the World and everything...if needed. In case that this method has been called before, it simply disposes the old FrameBuffer and creates a new one. One thing to note is the call to MemoryHelper.compact();. Again, this is a tribute to older Android versions, which had massive problems with garbage collection. What it does is to force the gc to run at this stage to avoid that it runs at runtime, which will cause stuttering when you don't want to see some.

public void onSurfaceChanged(GL10 gl, int w, int h) {
	if (fb != null) {
		fb.dispose();
	}
	fb = new FrameBuffer(gl, w, h);

	if (master == null) {

		world = new World();
		world.setAmbientLight(20, 20, 20);

		....

		MemoryHelper.compact();

		if (master == null) {
		     Logger.log("Saving master Activity!");
		     master = HelloWorld.this;
		}
	}
}
onDrawFrame(GL10 gl)

This is the render method, called by Android in the render thread. If you are experienced in game development, you might have used/heard of the concept of a game loop, which is a (timed) loop that constantly executes your game logic and rendering. In an Android Activity, there is no such loop, but the code inside onDrawFrame() is what comes closest to what you would usually have in your game loop...just without the loop. Don't loop within here, because that would stall the Activity.

As you can see, the variables set in the touch event handler above are evaluated here so that object rotations don't interfere with the rendering of the same objects. The rest is basic jPCT-AE render stuff. Whatever you do before and after, do render a scene, you'll basically do something like

fb.clear();
world.renderScene(fb);
world.draw(fb);
fb.display();

almost every time. In addition, there's some code that displays the fps to the console. If you want to limit your frame rate to maybe 30fps to save battery, this method is a good place to do it. You can search for the source code of my Alien Runner game for an example of how to do this.

public void onDrawFrame(GL10 gl) {
	if (touchTurn != 0) {
		cube.rotateY(touchTurn);
		touchTurn = 0;
	}

	if (touchTurnUp != 0) {
		cube.rotateX(touchTurnUp);
		touchTurnUp = 0;
	}

	fb.clear(back);
	world.renderScene(fb);
	world.draw(fb);
	fb.display();

	if (System.currentTimeMillis() - time >= 1000) {
		Logger.log(fps + "fps");
		fps = 0;
		time = System.currentTimeMillis();
	}
	fps++;
}

One last thing...

In this code, i'm using a cube created by the Primitives class. These objects have no proper texture coordinates, which is why i fake some by calling

cube.calcTextureWrapSpherical();

If you are replacing the cube with some model loaded from a 3ds or an obj file, REMOVE THIS LINE or it will screw up your texture coordinates.

That's it!?

Yes, that's basically it. For more details on the world setup itself, you can refer to the HelloWorld for desktop jPCT which explains these parts in more details: Hello World