Loading screen on android

Hello,

Game loading on android can be sometimes quite long and the user is waiting with a black screen wondering if his phone is running out of battery… I’ve implemented a method I found on the forum: I raise a flag in the init and I just display a background picture and a progress bar. I’m loading the remaining assets and building the tree in the update procedure step by step… Once init phase is over, I change the flag status so that the update procedure is used for the game behaviour.
I’m wondering if this approach is safe since I’m experiencing some stranges issues when I run the game on my android device. Sometimes, the game is not properly initialized and I have some nodes missing leading to a crash.

Do you think the behaviour I’ve noticed on my android phone is linked to the method I’m using for loading assets and initializing the game structure?

can you paste your code? just to check what you do…
sometimes it can be a simple mistake. :slight_smile:

Here it is… I hope it will be readable enough:
[java]

package fr.didier.casolaro.Asteroids.game;

import fr.didier.casolaro.Asteroids.utils.SpringMotion;
import com.jme3.app.Application;
import com.jme3.app.state.AppState;
import com.jme3.app.state.AppStateManager;
import com.jme3.asset.AssetManager;
import com.jme3.audio.AudioNode;
import com.jme3.bullet.BulletAppState;
import com.jme3.bullet.collision.PhysicsCollisionEvent;
import com.jme3.bullet.collision.PhysicsCollisionListener;
import com.jme3.input.InputManager;
import com.jme3.light.AmbientLight;
import com.jme3.light.DirectionalLight;
import com.jme3.math.ColorRGBA;
import com.jme3.math.FastMath;
import com.jme3.math.Vector3f;
import com.jme3.renderer.Camera;
import com.jme3.renderer.RenderManager;
import com.jme3.renderer.ViewPort;
import com.jme3.scene.Node;
import com.jme3.ui.Picture;
import fr.didier.casolaro.Asteroids.AsteroidsApplication;
import fr.didier.casolaro.Asteroids.GameParameters;
import fr.didier.casolaro.controls.progressbar.ProgressBar;
import java.util.logging.Level;
import java.util.logging.Logger;

public class GamePlayAppState implements AppState, PhysicsCollisionListener{
private static final Logger logger = Logger.getLogger(GamePlayAppState.class.getName());

private Node _appRootNode;
private AssetManager assetManager;
private AsteroidsApplication app;
private InputManager inputManager;
private Node _appGuiNode;
private Camera cam;
private BulletAppState bulletAppState;
private AppStateManager stateManager;

private GameHUD scoreManager;
private ShipNode _ship;
private int m_intScreenWidth;
private int m_intScreenHeight;
private int _intAsteroidsAmount = 3;
private float untouchableCounter = 0;
private boolean _blnEndOfGame;
private SpringMotion _eogMotion;
private Vector3f _eog_target;
private Picture _youwon;
private Picture _youlost;
private ShipControl _shipControl;
private float _timeWait;
private boolean _blnInitialized;
private boolean _blnEnabled;
private Node _rootNode;
private Node _guiNode;
private boolean _blnLoading;
private int _intLoadingStep;
private boolean _blnGaming;
private Picture _splash = new Picture("splash");
private ProgressBar _pgr;
private Explosion _explosion;
private Dust _dust;
private ShipHitAnim _hitAnim;
private AudioNode _laser;
private AudioNode _explosion_asteroid;
private AudioNode _explosion_2;
private int _intLevel = 1;

public GamePlayAppState(int w, int h, AsteroidsApplication app){
    Logger.getLogger("").setLevel(Level.INFO);

    this.app = app;
    m_intScreenWidth = w;
    m_intScreenHeight = h;
    _blnEndOfGame = false;
    _timeWait = 0.0f;

    this.cam = this.app.getCamera();
    this._appRootNode = this.app.getRootNode();
    this.assetManager = this.app.getAssetManager();
    this.inputManager = this.app.getInputManager();
    this._appGuiNode = this.app.getGuiNode();
    this.stateManager = this.app.getStateManager();
    
    bulletAppState = new BulletAppState();
    bulletAppState.setThreadingType(BulletAppState.ThreadingType.PARALLEL);
    scoreManager = new GameHUD(assetManager, m_intScreenWidth, m_intScreenHeight);
    
    BulletNode.initialize(this.assetManager);
    
    _youwon = new Picture("Win Message");
    _youwon.setImage(assetManager, "Textures/messages/eog_win.png", true);
    _youlost = new Picture("Loose Message");
    _youlost.setImage(assetManager, "Textures/messages/eog_gameover.png", true);
    _youwon.setWidth(320);
    _youwon.setHeight(200);
    _youlost.setWidth(320);
    _youlost.setHeight(200);

    //AsteroidNode.initialize(this.assetManager);
    
    ShipNode.initialize(assetManager);
    _ship = new ShipNode();       

    app.getFlyByCamera().setEnabled(false);

    _rootNode = new Node();
    _guiNode = new Node();

    _blnLoading = false;
    _blnInitialized = false;
    _blnGaming = false;
    _intLoadingStep = 0;
}

public void setGameLevel(int lvl){
    _intLevel = lvl;
}

public int getGameLevel(){
    return _intLevel;
}

public void initialize(AppStateManager stateManager, Application app) {
    _blnLoading = false;
    _blnGaming = false;
    _intLoadingStep = 0;
    
    _splash.setImage(assetManager, "Interface/splashScreen.png", false);
    _splash.setHeight(cam.getHeight());
    _splash.setWidth(cam.getWidth());
    _appGuiNode.attachChild(_splash);
    
    _pgr = new ProgressBar("pgr");
    _pgr.setPicture(assetManager, "Interface/progressbar_test.png", 320, 16);
    _pgr.setLocalTranslation(0.5f*cam.getWidth()-160, 0.1f*cam.getHeight()-8, 0);
    _appGuiNode.attachChild(_pgr);
    
    _blnLoading = true;
    _blnInitialized = true;
    
    Picture p = new Picture("background");
    p.setImage(assetManager, "Textures/background/stars.png", false);

    float xRatio = (float)m_intScreenWidth/(float)1280;
    float yRatio = (float)m_intScreenHeight/(float)720;
    
    p.setWidth(1280*xRatio);
    p.setHeight(720*yRatio);
    p.setPosition(0, 0);

    ViewPort pv = app.getRenderManager().createPreView("background", cam);
    pv.setClearFlags(true, true, true);

    pv.attachScene(p);

    app.getViewPort().setClearFlags(false, true, true);
    p.updateGeometricState();
    this.setEnabled(true);
}

private void _setupCamera(){
    logger.log(Level.INFO, "set up camera");
    app.getCamera().setLocation(new Vector3f(0,50,0));
    app.getCamera().lookAt(new Vector3f(0,0,0), new Vector3f(0,0,0));
}

private void _setupShip(){
    logger.log(Level.INFO, "set up ship");
    bulletAppState.getPhysicsSpace().add(_ship.getRigidBodyControl());
    _rootNode.attachChild(_ship);
}

private void _setupLighting() {
    logger.log(Level.INFO, "set up lights");
    DirectionalLight sun1 = new DirectionalLight();
    sun1.setColor(ColorRGBA.White);
    sun1.setDirection(new Vector3f(-1.13f, -1.13f, 1.13f).normalizeLocal());
    _rootNode.addLight(sun1);

    AmbientLight al = new AmbientLight();
    al.setColor(ColorRGBA.White);
    _rootNode.addLight(al);
}

private void _setupScoreManager(){
    logger.log(Level.INFO, "set up score manager");
    stateManager.attach(bulletAppState);
    //bulletAppState.setDebugEnabled(true);       
    scoreManager.setLifeCount(GameParameters.getLifeCount());
    _guiNode.attachChild(scoreManager.getSpatial());
}
private void _initGame(){
    switch(_intLoadingStep){
        case 0:
            _setupScoreManager();
            _pgr.setProgress(0.10f);
            break;
        case 1:
            _setupShip();
            _pgr.setProgress(0.20f);
            break;
        case 2:
            _setupLighting();
            _pgr.setProgress(0.30f);
            break;
        case 3:
            _createLevel();
            _pgr.setProgress(0.40f);
            break;
        case 4:
            _createMessages();
            _pgr.setProgress(0.50f);
            break;
        case 5:

// _addShipControl();
_pgr.setProgress(0.60f);
break;
case 6:
_setSounds();
_pgr.setProgress(0.70f);
break;
case 7:
_buildExplosion();
_pgr.setProgress(0.80f);
break;
case 8:
_buildDust();
_pgr.setProgress(0.90f);
break;
default:
_pgr.setProgress(1.00f);
_setupCamera();

            _appRootNode.attachChild(_rootNode);
            _appGuiNode.attachChild(_guiNode);
            _appGuiNode.detachChild(_pgr);
            _appGuiNode.detachChild(_splash);
            _blnLoading = false;
            _blnGaming = true;
            
            _addShipControl();
            
            AsteroidsApplication.nodeMusic.play();
            break;
    }
}

private void _setSounds(){
    logger.log(Level.INFO, "set up sounds");
    AsteroidsApplication.nodeMusic = new AudioNode(assetManager, "Sounds/asteroids_game.ogg", false);
    AsteroidsApplication.nodeMusic.setPositional(false);
    AsteroidsApplication.nodeMusic.setLooping(true);
    AsteroidsApplication.nodeMusic.setVolume(AsteroidsApplication.gameData.getMusicVolume());
    _rootNode.attachChild(AsteroidsApplication.nodeMusic);
    
    AsteroidsApplication.nodeFX = new AudioNode();
    AsteroidsApplication.nodeFX.setPositional(false);
    AsteroidsApplication.nodeFX.setLooping(false);
    AsteroidsApplication.nodeFX.setVolume(AsteroidsApplication.gameData.getFXvolume());
    
    _laser = new AudioNode(assetManager, "Sounds/laser.ogg", false);
    _laser.setPositional(false);
    _laser.setLooping(false);
    _laser.setVolume(AsteroidsApplication.gameData.getFXvolume());
    AsteroidsApplication.nodeFX.attachChild(_laser);
    
    _explosion_asteroid = new AudioNode(assetManager, "Sounds/explosion.ogg", false);
    _explosion_asteroid.setPositional(false);
    _explosion_asteroid.setLooping(false);
    _explosion_asteroid.setVolume(AsteroidsApplication.gameData.getFXvolume());
    AsteroidsApplication.nodeFX.attachChild(_explosion_asteroid);
    
    _explosion_2 = new AudioNode(assetManager, "Sounds/explosion_2.ogg", false);
    _explosion_2.setPositional(false);
    _explosion_2.setLooping(false);
    _explosion_2.setVolume(AsteroidsApplication.gameData.getFXvolume());
    AsteroidsApplication.nodeFX.attachChild(_explosion_2);
    
    _ship.setLaserSound(_laser);
}

private void _buildDust(){
    logger.log(Level.INFO, "set up dust");
    _dust = new Dust("asteroid dust",assetManager);
    _rootNode.attachChild(_dust);        
}
private void _buildExplosion(){
    logger.log(Level.INFO, "set up explosion");
    _explosion = new Explosion("asteroid explosion",assetManager);
    _rootNode.attachChild(_explosion);
    
    _hitAnim = new ShipHitAnim("hit Anim",assetManager);
    _rootNode.attachChild(_hitAnim);
}

private void _addShipControl(){
    logger.log(Level.INFO, "set up ship control");
    _shipControl = new ShipControl(this.inputManager,this.assetManager, _guiNode, _ship, this.cam);
    bulletAppState.getPhysicsSpace().setGravity(new Vector3f(0,0,0));
    bulletAppState.getPhysicsSpace().addCollisionListener(this);
}
private void _updateGame(float tpf){
    _ship.update(this.cam,tpf);
    _explosion.explode(tpf);
    _dust.explode(tpf);
    _hitAnim.animate(tpf);
    m_updateAsteroids();
    m_moveBullet(tpf);
    m_testHit();
    m_testVictory();
    scoreManager.addTime(tpf);
            
    untouchableCounter += tpf;
    untouchableCounter = Math.min(untouchableCounter, GameParameters.getInvulnerabilityTimeOut() + 1.0f);
    
    if (_blnEndOfGame){
        _eogMotion.move(tpf);
        float v = 0.99f * AsteroidsApplication.nodeMusic.getVolume();
        AsteroidsApplication.nodeMusic.setVolume(v);

        if (!_eogMotion.isMotionFinished()){
            if (scoreManager.getLifeCount() <= 0){
                _youlost.setLocalTranslation(_eogMotion.getCurrentPosition());
            }
            else{
                _youwon.setLocalTranslation(_eogMotion.getCurrentPosition());
            }
        }
        else{
            _timeWait+=tpf;
            if (_timeWait>=GameParameters.getMaxTimeWait()){
                stateManager.detach(this);
                this.app.startMainMenu();
            }
        }
    }
}

@Override
public void update(float tpf) {
    if (_blnLoading){
        _initGame();
        _intLoadingStep++;
    }
    else if (_blnGaming){
        _updateGame(tpf);
    }
}

@Override
public void cleanup() {
    inputManager.clearMappings();
    AsteroidsApplication.nodeMusic.stop();
    _appRootNode.detachChild(_rootNode);
    _appGuiNode.detachChild(_guiNode);
}

private void m_testHit() {
    if (_ship.getBullet() == null) return;
    
    for (AsteroidNode asteroid : AsteroidNode.asteroids) {
        if (asteroid.getWorldBound().intersects(_ship.getBullet().getWorldBound())) {
            
            int asteroidLevel = ((Integer) asteroid.getUserData("roid_level")).intValue();
            scoreManager.addScore(GameParameters.getAsteroidScore(asteroidLevel,scoreManager.getTime()));

            _rootNode.detachChild(_ship.getBullet());
            _ship.destroyBullet();
            _rootNode.detachChild(asteroid);
            
            bulletAppState.getPhysicsSpace().remove(asteroid);
            Vector3f asteroidPosition = asteroid.getLocalTranslation();
            AsteroidNode.asteroids.remove(asteroid);
            
            if (asteroidLevel < GameParameters.getMaxSplitCount()) {
                _explosion_asteroid.playInstance();
                _dust.setLocalTranslation(asteroidPosition);
                _dust.start();
                AsteroidNode.activateChildren(
                    asteroid,
                    _rootNode,
                    bulletAppState);
            }
            else{
                _explosion_2.playInstance();
                _explosion.setLocalTranslation(asteroidPosition);
                _explosion.start();
            }
            break;
        }
    }
}

private void m_updateAsteroids() {
     for (AsteroidNode a : AsteroidNode.asteroids) {
        a.update(this.cam);
    }
}

private void _createLevel() {
    logger.log(Level.INFO, "set up level");
    float r = 25.0f;
    
    AsteroidNode.initialize(this.assetManager);
    AsteroidNode.buildLevel(_intAsteroidsAmount, GameParameters.getMaxSplitCount());
    
    logger.log(Level.INFO, "... asteroids created = {0}",new Integer[]{AsteroidNode.asteroids.size()});
    
    for (AsteroidNode asteroid : AsteroidNode.asteroids) {
        _rootNode.attachChild(asteroid);
        bulletAppState.getPhysicsSpace().add(asteroid.getRigidBodyControl());
        float ang = FastMath.nextRandomFloat() * FastMath.TWO_PI;
        Vector3f p = new Vector3f(r*FastMath.sin(ang),0,r*FastMath.cos(ang));
        asteroid.getRigidBodyControl().setPhysicsLocation(p);
        asteroid.getRigidBodyControl().setLinearVelocity(
                (new Vector3f(FastMath.sin(ang),0,FastMath.cos(ang)))
                .mult(GameParameters.getMaxAsteroidSpeed()));
    }
}

private void m_moveBullet(float tpf) {
    if (_ship.getBullet() == null) return;
    _ship.getBullet().computeMotion(tpf, this.cam, _rootNode);
    if (_ship.getBullet().isDestroyed()) _ship.destroyBullet();
}
   
private void m_testVictory() {
    if (AsteroidNode.asteroids.isEmpty() && (!_blnEndOfGame)) {
        _displayGameOver_Win();
        //this.app.stop();
    }
}

public synchronized void collision(PhysicsCollisionEvent event) {
    String strNodeA = event.getNodeA().getName();
    String strNodeB = event.getNodeB().getName();

    if (untouchableCounter > GameParameters.getInvulnerabilityTimeOut()) {
        if ((strNodeA.compareTo(ShipNode.NAME)==0)||(strNodeB.compareTo(ShipNode.NAME)==0)){
            untouchableCounter = 0.0f;
            scoreManager.looseLife();
            _hitAnim.setLocalTranslation(_ship.getLocalTranslation());
            _hitAnim.start();
        }
        if ((scoreManager.getLifeCount() == 0) && (!_blnEndOfGame)){
            _explosion_2.playInstance();
            _explosion.setLocalTranslation(_ship.getLocalTranslation());
            _rootNode.detachChild(_ship);
            _explosion.start();
            _displayGameOver_Loose();
            //this.app.stop();
        }
    }
}

private void _displayGameOver_Win() {
    _eogMotion = new SpringMotion(_eog_target);
    _eogMotion.start(_youwon.getLocalTranslation());
    _shipControl.disableListener();
    
    boolean blnSave = false;
    if (scoreManager.getScore()>AsteroidsApplication.gameData.getMaxScore()){
        AsteroidsApplication.gameData.setMaxScore(scoreManager.getScore());
        blnSave = true;
    }
    if (scoreManager.getTime()<AsteroidsApplication.gameData.getBestTime()){
        AsteroidsApplication.gameData.setBestTime(scoreManager.getTime());
        blnSave = true;
    }
    if (_intLevel>AsteroidsApplication.gameData.getCompletedLevel()){
        AsteroidsApplication.gameData.setCompletedLevel(_intLevel);
        blnSave = true;
    }

    if (blnSave) AsteroidsApplication.saveGame();

    _blnEndOfGame = true;
}

private void _displayGameOver_Loose() {
    _eogMotion = new SpringMotion(_eog_target);
    _eogMotion.start(_youlost.getLocalTranslation());
    _shipControl.disableListener();
    
    _blnEndOfGame = true;
}

private void _createMessages() {
    logger.log(Level.INFO, "set up messages");
    _eog_target = new Vector3f(0.5f*m_intScreenWidth-160,0.5f*m_intScreenHeight-100,0);

    _youwon.setPosition(_eog_target.getX(), -200);                
    _youlost.setPosition(_eog_target.getX(), -200);
    
    _guiNode.attachChild(_youwon);
    _guiNode.attachChild(_youlost);
}

public boolean isInitialized() {
    return _blnInitialized;
}

public void setEnabled(boolean active) {
    _blnEnabled = active;
}

public boolean isEnabled() {
    return _blnEnabled;
}

public void stateAttached(AppStateManager stateManager) {
    // Nothing to do
}

public void stateDetached(AppStateManager stateManager) {
    // Nothing to do
}

public void render(RenderManager rm) {
    // Nothing to do
}

public void postRender() {
    // Nothing to do
}

}

[/java]

Hello,

I’ve been looking around my code and I think I found the solution. The problem is not coming from where I thought. For lauching the game, I’m using a first AppState as a main menu. When the user touches the start button, I detach the current appstate and I attach the game Appstate.

While logging the code I noticed that the game appstate was behaving sometimes as if it had been attached twice… This is maybe due to the way I’m managing the touch input… I think the solution is there, but I’m wondering how it is possible to attach 2 appstate to the state manager. Is this useful? If not, having an error message when it happens would help a lot!

@dcaso said: Hello,

I’ve been looking around my code and I think I found the solution. The problem is not coming from where I thought. For lauching the game, I’m using a first AppState as a main menu. When the user touches the start button, I detach the current appstate and I attach the game Appstate.

While logging the code I noticed that the game appstate was behaving sometimes as if it had been attached twice… This is maybe due to the way I’m managing the touch input… I think the solution is there, but I’m wondering how it is possible to attach 2 appstate to the state manager. Is this useful? If not, having an error message when it happens would help a lot!

You can attach as many app states to the state manager as you want. I typically have about 10-12 different ones attached. Even the same class there may be reasons to attach more than one (for example a ViewPortAppState that manages a single viewport but you might have multiple viewports.)