Looking at implementing OpenGL in to app
I've written a game which uses a lot of lines, circles (some outlined, some filled, some with both) and text elements throughout to create a guitar fretboard which I get the user to interact with. Some of these elements are animated (coordinate, alpha, colour or a combination) and the app starts to skip lots of frames during most of the animations which I'd like to fix. I think OpenGL is the way to go, but I'm interested in some pointers before I jump in.
Currently my animation is achieved with lookup tables, async tasks and dynamic bitmap creation (for the text - I render it to bitmaps as I use custom fonts - so I never draw text directly to the canvas). I've got the async task running for n * 1000 ms and in the thread it waits for x ms (typically 50ms) and then pushes a progress message out - the helper classes then work out where in the time indexed lookup table the animation is and calculates the relative values based on that. I draw the static bits of the fretboard directly to the canvas with the included draw circle and draw line methods.
I'm not sure what is slowing my app down currently (mostly because I've not yet profiled it) but I'm pretty sure that even though I cache the bitmaps and have been fairly sensible about the way that I'm changing size & transparency, the use of bitmaps drawn directly to the Canvas is what is causing the slow down.
I've followed some tutorials and have written some OpenGL, but not enough to know much at all - I know it's fast though which is important. I don't know if I can use the same methods of drawing lines and circles directly to the canvas with OpenGL, and I think I'll still have to create some bitmaps for the text and I think I have to apply these as textures in order to show them.
So can anyone give me some pointers? Some sample code of drawing lines, circles and text would be amazing. Any pointers on animating within OpenGL - I think my current setup is pretty solid and can prob port it over but any advice would be great as this is my first look in to animating.
* EDIT *
Here's a basic overview of my code - there are many pieces, but I'm including them in the hope that someone else may be able to use some of it.:
I have a look up table
public enum LookUpTable { COUNT_DOWN { @Override public float[][] getLookUpTable() { /* * 1 = total time left * 2 = font alpha * 3 = font size */ float[][] lookUpTable = { { 0f, 0f, 400f }, { 400f, 255f, 200f }, { 700f, 255f, 150f }, { 1000f, 0f, 5f } }; return lookUpTable; } @Override public LookUpType getLookUpType() { return LookUpType.REPEAT; } }; // does the timer loop around, or is it a one off run private enum LookUpType { REPEAT, SINGLE } abstract public LookUpType getLookUpType(); abstract public float[][] getLookUpTable(); }
I have extended the AsyncTask task into a builder function:
public class CountDownTimerBuilder { // callbacks - instantiated in the view protected CountDownEndEvent countDownEndEvent; protected CountDownProgressEvent countDownProgressEvent; protected CountDownInitEvent countDownInitEvent; protected int updatePeriod; protected float runTime; public CountDownTimerBuilder withCountDownEndEvent(CountDownEndEvent countDownEndEvent) { this.countDownEndEvent = countDownEndEvent; return this; } public CountDownTimerBuilder withCountDownProgressEvent(CountDownProgressEvent countDownProgressEvent) { this.countDownProgressEvent = countDownProgressEvent; return this; } public CountDownTimerBuilder withCountDownInitEvent(CountDownInitEvent countDownInitEvent) { this.countDownInitEvent = countDownInitEvent; return this; } public CountDownTimerBuilder withUpdatePeriod(int updatePeriod) { this.updatePeriod = updatePeriod; return this; } public CountDownTimerBuilder withRunTime(float runTime) { this.runTime = runTime; return this; } public CountDownTimer build() { return new CountDownTimer(); } public static interface CountDownEndEvent { public abstract void dispatch(Long... endResult); } public static interface CountDownInitEvent { public abstract void dispatch(); } public static interface CountDownProgressEvent { public abstract void dispatch(Long... progress); } public class CountDownTimer { AsyncTask<Void, Long, Long> genericTimerTask; /** * Starts the internal timer */ public void start() { genericTimerTask = new GenericCountDownTimer().execute(new Void[] {}); } public void cancel() { if (genericTimerTask != null) { genericTimerTask.cancel(true); genericTimerTask = null; } } private class GenericCountDownTimer extends AsyncTask<Void, Long, Long> { @Override protected Long doInBackground(Void... params) { long startTime = System.currentTimeMillis(); long currentTime; long countDown; Log.i(ApplicationState.getLogTag(getClass()), "Timer running for " + runTime + " ms, updating every " + updatePeriod + " ms"); do { try { Thread.sleep(updatePeriod); } catch (InterruptedException e) { e.printStackTrace(); } if (this.isCancelled()) { Log.i(ApplicationState.getLogTag(getClass()), "Timer Cancelled"); break; } currentTime = System.currentTimeMillis(); countDown = currentTime - startTime; publishProgress((long)runTime - countDown); } while (countDown <= runTime); return 0l; } @Override protected void onPreExecute() { if (countDownInitEvent != null) { countDownInitEvent.dispatch(); } } @Override protected void onProgressUpdate(Long... progress) { Log.v(ApplicationState.getLogTag(getClass()), "Timer progress " + progress[0] + " ms"); if (countDownProgressEvent != null) { countDownProgressEvent.dispatch(progress); } } @Override protected void onPostExecute(Long endresult) { if (countDownEndEvent != null) { countDownEndEvent.dispatch(endresult); } } } } }
I have a class where my animation values are calculated:
public class AnimationHelper { private LookUpTable lookUpTable; private float[][] lookUpTableData; private float currentTime = -1; private float multiplier; private int sourceIndex; public void setLookupTableData(LookUpTable lookUpTable) { if (this.lookUpTable != lookUpTable) { this.lookUpTableData = lookUpTable.getLookUpTable(); this.currentTime = -1; this.multiplier = -1; this.sourceIndex = -1; } } private void setCurrentTime(float currentTime) { this.currentTime = currentTime; } public float calculate(float currentTime, int index) { if (this.currentTime == -1 || this.currentTime != currentTime) { setCurrentTime(currentTime); getCurrentLookupTableIndex(); getMultiplier(); } return getCurrentValue(index); } private void getCurrentLookupTableIndex() { sourceIndex = -1; for (int scanTimeRange = 0; scanTime开发者_StackOverflowRange < (lookUpTableData.length - 1); scanTimeRange++) { if (currentTime < lookUpTableData[scanTimeRange + 1][0]) { sourceIndex = scanTimeRange; break; } } } private void getMultiplier() { if ((lookUpTableData[sourceIndex][0] - lookUpTableData[sourceIndex + 1][0]) == 0.0f) { multiplier = 0.0f; } else { multiplier = (currentTime - lookUpTableData[sourceIndex][0]) / (lookUpTableData[sourceIndex + 1][0] - lookUpTableData[sourceIndex][0]); } } public float getCurrentValue(int index) { float currentValue = lookUpTableData[sourceIndex][index] + ((lookUpTableData[sourceIndex + 1][index] - lookUpTableData[sourceIndex][index]) * multiplier); return currentValue > 0 ? currentValue : 0; } }
In my game code I tie it all together by specifying the lookup table to use and creating callbacks for each of the different states, creating the timer with the builder class and starting it:
AnimationHelper animHelper = new AnimationHelper(); animHelper.setLookupTableData(LookUpTable.COUNT_DOWN); CountDownInitEvent animationInitEvent = new CountDownInitEvent() { public void dispatch() { genericTimerState = TimerState.NOT_STARTED; } }; CountDownProgressEvent animationProgressEvent = new CountDownProgressEvent() { public void dispatch(Long... progress) { genericTimerState = TimerState.IN_PROGRESS; // update the generic timer - we'll use this in all animations genericTimerCountDown = progress[0]; invalidate(); } }; CountDownEndEvent animationEndEvent = new CountDownEndEvent() { public void dispatch(Long... endValue) { genericTimerState = TimerState.FINISHED; startGame(); } }; CountDownTimer timer = new CountDownTimerBuilder() .withRunTime(getCountDownPeriod(countDownTimePeriod)) // getCountDownPeriod() is used for handling screen rotation - esentially returns the run time for the timer in ms .withUpdatePeriod(TIMER_UPDATE_PERIOD) // currently set at 50 .withCountDownInitEvent(animationInitEvent) .withCountDownProgressEvent(animationProgressEvent) .withCountDownEndEvent(animationEndEvent) .build(); timer.start();
in my onDraw I get the specific values from the lookup table and act on them:
private int IFTL = 0; // total time left private int IFY1 = 1; // initial instructions y offset private int IFY2 = 2; // start message y offset private int IFA1 = 3; // note to guess alpha float yPosition1 = animHelper.calculate(genericTimerCountDown, IFY1); float yPosition2 = animHelper.calculate(genericTimerCountDown, IFY2); float alpha1 = animHelper.calculate(genericTimerCountDown, IFA1); // getScreenDrawData() returns the coordinates and other positioning info for the bitmap final ScreenDrawData guessNoteTitleDrawValues = FretBoardDimensionHelper.getScreenDrawData(AssetId.GUESS_NOTE_TITLE); //change the y position of the bitmap being drawn to screen guessNoteTitleDrawValues.withAlteredCoordinate(Constants.Y_COORDINATE, 0-yPosition1); // DrawBitmapBuilder.createInstance() .withCanvas(getCanvas()) .withBitmap(bitmapCacheGet(AssetId.GUESS_NOTE_TITLE)) .withBitmapDrawValues(guessNoteTitleDrawValues) .draw(); Paint paint = new Paint(); paint.setAlpha((int)alpha1); final ScreenDrawData initialNoteDrawValues = FretBoardDimensionHelper.getScreenDrawData(AssetId.GUESS_NOTE_INITIAL_NOTE); // draw to screen with specified alpha DrawBitmapBuilder .createInstance() .withCanvas(getCanvas()) .withBitmap(bitmapCacheGet(AssetId.GUESS_NOTE_INITIAL_NOTE)) .withBitmapDrawValues(initialNoteDrawValues) .withPaint(paint) .draw();
You'll have to read between the lines a bit in that last bit of code as there are a load of helper functions in there.
Does this look like a reasonable approach? I'd love a code review if anyone can be bothered - more than happy to post up more code or explanations if requiredI've been working a bit with OpenGL and well... It's not trivial.
For the text, you'll have to create a mutable Bitmap, write some text in it with canvas.drawText(...), and then convert this Bitmap (which should be in powerOfTwo dimension) to a gl texture, and apply it to a rectangle.
For circles... it's gonna be complicated. OpenGL draws lines, triangles, dots... not circles i'm afraid. One way to achieve a circle in OpenGL is to have a texture of a circle, and apply it to a rectangle.
Each time your application is put on pause, and resumed, you'll have to recreate each of your textures, and set up your gl surface once again...
If you need blending, you'll probably find that, with a lot of textures, your phone doesn't have enough memory to handle it all...
Now that I have scared you off : open gl is really one of the way to go ! It'll allow your app to delegate the drawing to the phone GPU, which will let you keep CPU for calculating animations and such.
You'll find some information about using OpenGL ES for android on this website : http://blog.jayway.com/2009/12/03/opengl-es-tutorial-for-android-part-i/
There are 6 tutorials, and the 6th one is about textures. The 2nd tutorial tells you about drawing polygon, and lines.
Last of all, you should read this book : http://my.safaribooksonline.com/book/office-and-productivity-applications/9781430226475/copyright/ii It's about android, openGL, ...
Good luck.
Edit :
Look like I don't have enough privilege to write a comment, therefor I'll edit this :
The way I'd do it would be maybe a lot more simpler :
I'd have a surface view, with a renderer dedicated to draw as often as possible. He would draw synchronized list of items (Synchronized, because of multi thread), with position, alpha, ... no async task here, just a regular rendering thread.
The surface view would also start a thread (regular thread once again, no async task), with a List (or something like that). This thread would, every 16 ms or something, run through the animation list, apply them. (synchronously of course if it needs to change some items used by the rendering thread).
When an animation is over (like, if it's been in the list for more than 2000 ms, easy to check with function such as System.currentTimeMillis()), I'd remove it from the list.
Voila !
Then you'll tell me : hey, but if i do a lot of animation, calculation might takes longer than 16 ms, and it appears to get slower ! My answer is : there is an easy enough solution for that : in the thread that deals with animation, before applying them you do something like that :
long lastTime = currentTime;
// Save current time for next frame;
currentTime = System.currentTimeMillis();
// Get elapsed time since last animation calculus
long ellapsedTime = currentTime - lastTime;
animate(ellapsedTime);
And in your animate function, you animate more if more time elapsed ! Hope this helps.
精彩评论