The combined number of Nokia Asha platform and Series 40 phones out there is huge, and even if you narrow them down using the full touch feature as the criteria, you still get several devices which vary in terms of performance. When developing games, it is recommended to try to offer a similar user experience on all supported devices. This means that in devices with lesser capabilities you may need to compromise, for example, by lowering the quality of graphics. Roughly there are two approaches to achieve this: compile-time or runtime adaptation.
Compile-time adaptation may be more efficient but also increases the maintenance burden. It is also less meaningful with Java application compared to, for example, C++ based games. Therefore, let's focus on runtime adaptation techniques instead.
Runtime adaptation means figuring out the device and platform properties when the application is running and doing actions that affect the performance based on the properties. We might figure out details about the device, such as the model or specifications of some hardware component, but there are a few arguments against using that information. If we base our performance adaptation for specific models we can be sure that the adaptation is optimal, but it is not future proof - we cannot guess the future models that we want our game to run on. The hardware specs on the other-hand do not tell the whole story, i.e., how it all works together.
However, there is a trick that is both fairly accurate and future proof: measure of the performance that is evaluated runtime. For instance, frames per second (FPS) is a measure that is not too difficult to calculate:
Check the current time.
Render all the graphics of a single frame.
Check the current time and calculate the time step 2 took.
Repeat a few times until you get a decent average.
Here's how it is done in the main loop (MazeCanvas.java):
while (_running) { long time = System.currentTimeMillis(); // Update the model and render everything ... ticks = System.currentTimeMillis() - time; if (_fpsCount < MIN_FPS_STATS_COUNT) { // Calculate the FPS _fps = ticks > 0 ? 1000 / ticks : 60; if (_fpsCount < MIN_FPS_STATS_COUNT) { _averageFps += _fps; _fpsCount++; if (_fpsCount == MIN_FPS_STATS_COUNT) { // Enough figures calculated. Calculate now the average. _averageFps /= _fpsCount; if (_firstTime && _averageFps > 1) { if (_averageFps < 20) { // Remove the background to boost the performance setBackground(false); } _firstTime = false; } } } } // if (_debugMode || _fpsCount < MIN_FPS_STATS_COUNT) } // while (_running)
In addition to the adaptive design, there are several places where you can and should optimise, like for instance the usual performance bottlenecks: usage of floating points, reducing the number of method calls, heavy algorithms etc. Some of the places where optimisation is worthwhile are obvious, some may surprise you, so it is good to partially use trial-and-error method. When developing aMaze it turned out that the rendering is still causing the biggest performance hit, but it was surprising to find out that drawing just the background caused 30 - 50 % drop in FPS. Thus, it is removed automatically if the FPS measure is below 20.
Another thing to focus on is to use time for measure when updating the game model. In aMaze this means updating the whole model including the marble velocity and position based on the milliseconds elapsed since the previous update. The aim of this approach is, of course, that the performance of the device itself does not change the behavior of the marble:
private final void updateModel(final int ticks) { ... moveMarble(ticks); ... } ... private final void moveMarble(final int ticks) { // Calculate the velocity float[] velocity = _marbleModel.calculateVelocity(_ax, _ay, ticks); // Check for collisions int collision = -1; int safety = 0; // To prevent forever loop final float[] position = _marbleModel.position(); final float ticksCoefficient = (float)ticks / MazeCanvas.TICKS_COEFFICIENT; while (collision != 0) { collision = marbleCollidesAt( position[0] + velocity[0] * ticksCoefficient, position[2] + velocity[2] * ticksCoefficient); ... } // Set the velocity and move the marble _marbleModel.setVelocity(velocity); _marbleModel.move(ticks); }
For more information on this subject, see Performance optimisation.