Restarting jME3 from AndroidHarness + heap size in Android 4.0

Hi folks!

My team is currently working on a network game for Android. In brief, it’s a quick action-like game with some space dogfight in a distant galaxy.
Whole menu is developed by using classic android sdk and the game itself is obviously made in jME3. :slight_smile: Separately, they both work well, but unfortunately we’ve got some troubles when connecting them together in one app.

To the point:
1) The first question is how to restart/reload/clear the jME part of our application?
We’re collecting data needed to start the game in menu, and then, we’re using AndroidHarness class to load the proper game. So far, so good. Space battle ends, we’re ‘shutting down’ the battle (i.e. clearing instances, removing listeners, = null, using stop(), calling System.gc() and that kind of stuff) in our main, controller class (the one with initApp() and simpleUpdate()) and players are moved back to the menu. The problem is, we’re running out of memory, when the player wants to start the second battle. It seems like we’re not closing the previous game correctly and some remains are still in the memory.

2) Another issue involves heap usage in the Android Ice Cream Sandwich API.
We’ve noticed a strange jME3 behavior on devices working under Android 4.0. E.g., starting our game on the device working under Android 2.3.x causes memory allocation at the level of about 12MB (and it’s fine). But at the same time, device working under Android 4.0.x allocates about 50-55MB of memory which is dangerously close to the maximum Java VM heap size. Our temporary workaround to avoid ‘out of memory’ exceptions at Android 4.0.x devices is using ‘very low details’ option, but using way worse textures, etc. is not a real solution here. Those devices handle game at ‘high details’ effortlessly when Android 2.3.x is installed… :confused:

I know, we should use Nifty to do menu and it’d be a remedy for (at least) our first problem. Trust me - we will in the next project. But now integration is way too far, because our game is in the final development stage. :frowning: Please help if you got any ideas.

Regards.

  1. Without knowing how you set up the game its hard to tell how you should tear it down :wink:
  2. Hm, I heard of some issues with 4.0 but thats indeed strange, any chance you can test if this is a general issue or has to do with certain assets? E.g. try starting an app that has no images and compare the heap size etc.

About the UI thing, we suggest that mainly because a jME-SDK built app will be portable to Android and iOS in the future, we just don’t want to disappoint too many people asking about how to port their android jME apps to iOS :slight_smile: Especially for games made for android that obviously makes a lot of sense. Generally its not that anybody is “against” doing whatever you want with the engine, its just a drag having people come with problems that have been solved already because they chose to ignore the existing solutions :wink:

Ok, so let’s take a look into the code.

LobbyActivity is the last screen of menu seen by players before battle. When the ‘start battle’ button is pressed, the new activity is started, and it’s AndroidHarness, which wraps and loads the jME game (GameController).

When the game ends, destroy() method in AndroidHarness is called, the game is closed and players are back in the menu checking out summary of the battle (ScoresActivity).

com.spaceadventure.View.LobbyActivity
[java]
startActivity(new Intent(getApplicationContext(), AndroidHarness.class));
finish();
[/java]

com.spaceadventure.View.AndroidHarness
[java]
public class AndroidHarness extends Activity implements …
{
protected String appClass = “com.spaceadventure.Controller.GameController”;

@Override
public void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);

    getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
    
    setVolumeControlStream(AudioManager.STREAM_MUSIC);
    
    overridePendingTransition(R.anim.slide_in_right, R.anim.slide_out_left);
    
    JmeAndroidSystem.setActivity(this);
    if (screenFullScreen) {
        requestWindowFeature(Window.FEATURE_NO_TITLE);
        getWindow().setFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN,
                WindowManager.LayoutParams.FLAG_FULLSCREEN);
    } else {
        if (!screenShowTitle) {
            requestWindowFeature(Window.FEATURE_NO_TITLE);
        }
    }

    final DataObject data = (DataObject) getLastNonConfigurationInstance();
    if (data != null) {
        logger.log(Level.INFO, "Using Retained App");
        this.app = data.app;

        ctx = (OGLESContext) app.getContext();
        view = ctx.createView(eglConfigType, eglConfigVerboseLogging);
        ctx.setSystemListener(this);
        layoutDisplay();

    } else {
        // Discover the screen reolution
        //TODO try to find a better way to get a hand on the resolution
        WindowManager wind = this.getWindowManager();
        Display disp = wind.getDefaultDisplay();
        Log.d("AndroidHarness", "Resolution from Window, width:" + disp.getWidth() + ", height: " + disp.getHeight());
            
        // Create Settings
        logger.log(Level.INFO, "Creating settings");
        AppSettings settings = new AppSettings(true);
        settings.setEmulateMouse(mouseEventsEnabled);
        settings.setEmulateMouseFlipAxis(mouseEventsInvertX, mouseEventsInvertY);
        settings.setUseJoysticks(joystickEventsEnabled);
        settings.setResolution(disp.getWidth(), disp.getHeight());

        // Create application instance
        try {
            if (app == null) {
                @SuppressWarnings("unchecked")
                Class clazz = (Class) Class.forName(appClass);
                app = clazz.newInstance();
            }

            app.setSettings(settings);
            app.start();
            ctx = (OGLESContext) app.getContext();
            view = ctx.createView(eglConfigType, eglConfigVerboseLogging);

            // AndroidHarness wraps the app as a SystemListener.
            ctx.setSystemListener(this);
            layoutDisplay();

        } catch (Exception ex) {
            handleError("Class " + appClass + " init failed", ex);
            setContentView(new TextView(this));
        }
    }
}

public void destroy()
{
if (app != null) {
app.destroy();
}

     try {
                    startActivity(new Intent(getApplicationContext(), ScoresActivity.class));
                finish();
            }
            catch(Exception e) {}

}
}
[/java]

com.spaceadventure.Controller.GameController
[java]
public class GameController extends SimpleApplication
{
public GameController()
{
super(new StatsAppState());
instance = this;
}

    public static GameController getInstance() 
{
   if (instance == null) {
       synchronized (GameController.class) {
           if (instance == null) {
               instance = new GameController();
           }
       }
   }
   return instance;

}

@Override
public void simpleInitApp() 
{  
    hpBars = new LinkedList();

    executor = new ScheduledThreadPoolExecutor(2);
    
    setBackground();
    setMapBounds();
    setShipControls();
    loadSounds();
    attachBasesToRootNode();
    attachScrapToRootNode(MenuResources.detailsLevel);
    enableSFX(false);
    setDisplayStatView(false);
    setDisplayFps(false);
    
    attachSpaceshipsToRootNode();
    setCamera();
    setHUD();

    performFirstShoot();

    height = getCamera().getHeight();
    width = getCamera().getWidth();     
    
    executor.scheduleAtFixedRate(new CollisionResultsThread(), 15, 30, TimeUnit.MILLISECONDS);
}

public void clearInstance() 
{
	instance = null;
}

public void endGame() 
{
    removeControls();
    this.inputEnabled=false;
    Audio.getInstance().stopGameMusic();
    stop();
}

}
[/java]

As for the Nifty - not using it had some major pros for us, but it’s not a matter now. :wink:

The simplest solution would be to just leave the simple application running. Remove everything from the root node and leave it idle until they start the next match…

Things like lighting, etc can probably be left in place and then you just need to remove and add the scene graph for the specific match you are doing.

It could be anything in there, sorry. All of your methods, I don’t know what they do. Can you make a test case? Ideally in one MainActivity.

You are somehow keeping stuff in memory when it shouldn’t,or by unwanted references or by threads running in background. I’d go for what @zarch suggested, just try to hide/remove everything instead of killing the app. Clear the stuff, pause the thread and give the control to another thread that can handle your UI.

The asset manager generally would have the largest memory usage since it caches all assets. Many things like models, sounds, materials, etc might keep references to the asset manager and thus prevent it from being garbage collected. Of course the easiest is to just call clearCache() on the asset manager …

@zarch, @shirkit
Thanks for reply. We’ll try this “hide/remove everything instead of killing the app” approach today.

@Momoko_Fan
I’ll check out how this clearCache() thing affects heap size.

Yeah, clearing and rebuilding the scene graph frequently is done in a lot of games (HeroDex included) so you can be fairly confident that it will work as expected unless you are keeping references to your old resources yourself somewhere. There was a thread recently that had something about the memory not all being freed from some of the buffers if the scene was completely empty though (so some memory will keep being used until you start building the scene up again at which point it will be freed). I don’t remember the details/thread name off hand but it was in the last few weeks.

Well, now that the search function is back (we have more then 3 results per page!) you can try to search for it.

http://hub.jmonkeyengine.org/forum/topic/scene-graph-tear-down