Introducing new feature to jme v3.4 android

Hi @sgold , on your request for new features for jme 3.4 & bug fixes ,i have read through the guidelines for new contributors & thought to create this thread to fix 3 similar bugs.

when i was navigating android related issues at the git repo found those 3 similar bugs:

i already fixed those , although i categorize them to be not really bugs , but i have introduced better approaches , better coding styles & more features .

The Old approach :

→ uses com.android.Activity or com.android.Fragment classes which blocks the user from engaging those instances inside other android services , plus , it’s deprecated.

→ uses GLSurfaceView on a FrameLayout then add that frameLayout as the com.android.Activity layout(instead of using xml setContentView(R.layout.activity);) which blocks the user from using other android.view.View components which really cancels out a powerful default native android feature.

→ creates confusion in using it , really no one knows the purpose of a whole class for rendering , that really shouldn’t be the case.

The new approach :

→ introduces a new feature to jme android that basically utilizes jme as a Component UI (GLSurfaceView) instead of utilizing the whole activity of rendering .

–>Fixes the incompatibility between AndroidHarness & androidx packages when trying to refer to the AndroidHarness instance from another source.

–>Fixes the confusion that happens from multiple class usages throughout the game cycle, now you can code your pc game normally as SimpleApplication class with or w/o BaseAppStates , AbstractControls,etc & one additional class which is your regular com.androidx.AppCompatActivity that’s basically created by the default android SDK & loaded inside Manifest.xml , that com.androidx.AppCompatActivity would basically hold JmeSurfaceView that’s a UI component like a button but it holds the powerful renderer GLSurfaceView & with that SimpleApplication instance can be passed & the game would be engaged by the help of SystemListener interface to that gets its methods called within OGLESContext to initialize GLRenderer for your game.

→ By this new approach you can render jme on an com.androidx.AppCompatActivity activity or com.android.Activity or on com.androidx.Fragment or com.android.Fragment , it depends upon in what the user willl use his/her xml layout file.

→ Notice , never uses the whole android activity or fragment for one UI component , without using android Permissions services & Intent services.

The idea in more depth : (March 2021) Monthly WIP Screenshot Thread



I donot say that we are going to replace Androidharness , no , because there’s already jme devs that are still using it , so that will break their code , i am just introducing new feature with git issues fixes.

let me know your feedback , your thoughts , your fears & etc ,

Through reading some of the issues that were fixed along the issues tab , found that TestCases are strongly required , so currently there are 2 proves or TestCases , the game demo which i pasted from jme.examples & the Advanced Vehicles project has been converted to android.

I would also convert my J-Pluto Arcade game into android using the same approach.

Thanks for paying attention :slight_smile: .

4 Likes

I don’t know enough about JME on Android to have an opinion this feature. Would any Android developers care to comment on it?

1 Like

i just want to add something here , that , this NPE basically takes place , because of referring to the className by a String :

in which if you have entered the package name wrong you will get NPE at this level of code :

which can be simply replaced by a setter easily w/o class generics because its already a generics :

  /**
     * sets the jme game instance that will be engaged into the {@link SystemListener}.
     * @param simpleApplication your jme game instance.
     */
    public void setSimpleApplication(SimpleApplication simpleApplication) {
        this.simpleApplication = simpleApplication;
    }
    public void setLegacyApplication(LegacyApplication  legacyApplication) {
        this.legacyApplication = legacyApplication;
    }

which are still generics too , any class extends LegacyApplication would be used.

Because the “catch everything” catch above it didn’t do its job.

I think it does not matter if you even use try…catch{} block here , because if there was an error , it’s better be manifested in stackTrace , but we are doing this only to avoid app crashes when exception is thrown & to ensure the users who will use the game , can have an exception dialog since there’s no stackTrace in a release apk though :slightly_smiling_face: , otherwise it’s not mandatory , but the error above is because wrong reference of String to a package name in users’ code as if you are starting a SystemListener & consuming GL.Renderer ES resources w/o having an actual JmeContext to update or to initialize.

Yes, but it’s bad practice to handle an exception and then not actually handle it and let the code continue on to make more errors.

If the catch() happens, then ‘app’ is bad and there is no reason to continue.

2 Likes

This is an actual ClassNotFoundException in mind , but because the class instance is referred to as a String & not used in anything else rather than JmeContext & so it bounces off into it as a NPE because JmeContext instance is still not initialized , stills a null pointer.

So , which one is better & why , this dude :

   /**
     * starts the jmeRenderer on a GlSurfaceView attached to a RelativeLayout
     * @param delayMillis delay of the appearance of jme game on the screen , this doesn't delay the renderer though.
     */
    public synchronized void startRenderer(int delayMillis) {
        this.delayMillis=delayMillis;
        if ( legacyApplication != null ){
            /*initialize App Settings & start the Game*/
            appSettings = new AppSettings(true);
            appSettings.setAudioRenderer(audioRendererType);
            appSettings.setResolution(JmeSurfaceView.this.getLayoutParams().width, JmeSurfaceView.this.getLayoutParams().height);
            appSettings.setAlphaBits(eglAlphaBits);
            appSettings.setDepthBits(eglDepthBits);
            appSettings.setSamples(eglSamples);
            appSettings.setStencilBits(eglStencilBits);
            appSettings.setBitsPerPixel(eglBitsPerPixel);
            appSettings.setEmulateKeyboard(emulateKeyBoard);
            appSettings.setEmulateMouse(emulateMouse);
            appSettings.setUseJoysticks(useJoyStickEvents);
            legacyApplication.setSettings(appSettings);
            try {
                /*start jme game context*/
                legacyApplication.start();
            } catch (Exception e) {
                new OptionPane((AppCompatActivity) getContext()).showErrorDialog(e, e.getMessage());
                if ( onExceptionThrown != null ){
                    onExceptionThrown.Throw(e);
                    jmeSurfaceViewLogger.log(Level.WARNING, e.getMessage());
                }
            } finally {
                /*attach the game to JmE OpenGL.Renderer context */
                OGLESContext oglesContext = (OGLESContext) legacyApplication.getContext();
                /*create a glSurfaceView that will hold the renderer thread*/
                glSurfaceView = oglesContext.createView(JmeSurfaceView.this.getContext());
                /*set the current view as the system engine thread view for future uses*/
                JmeAndroidSystem.setView(JmeSurfaceView.this);
                /*set JME system Listener to initialize game , update , requestClose & destroy on closure*/
                oglesContext.setSystemListener(JmeSurfaceView.this);
                /* set the glSurfaceView to fit the widget */
                glSurfaceView.setLayoutParams(new LayoutParams(JmeSurfaceView.this.getLayoutParams().width, JmeSurfaceView.this.getLayoutParams().height));
                /*post delay the renderer join into the UI thread*/
                handler.postDelayed(new RendererThread(), delayMillis);
            }
        }
    }

or that one :

 public synchronized void startRenderer(int delayMillis) {
        this.delayMillis=delayMillis;
        if ( simpleApplication != null ){
            try {
                /*initialize App Settings & start the Game*/
                appSettings = new AppSettings(true);
                appSettings.setAudioRenderer(audioRendererType);
                appSettings.setResolution(JmeSurfaceView.this.getLayoutParams().width, JmeSurfaceView.this.getLayoutParams().height);
                appSettings.setAlphaBits(eglAlphaBits);
                appSettings.setDepthBits(eglDepthBits);
                appSettings.setSamples(eglSamples);
                appSettings.setStencilBits(eglStencilBits);
                appSettings.setBitsPerPixel(eglBitsPerPixel);
                appSettings.setEmulateKeyboard(emulateKeyBoard);
                appSettings.setEmulateMouse(emulateMouse);
                appSettings.setUseJoysticks(useJoyStickEvents);
                simpleApplication.setSettings(appSettings);
                /*start jme game context*/
                simpleApplication.start();
                /*attach the game to JmE OpenGL.Renderer context */
                OGLESContext oglesContext = (OGLESContext) simpleApplication.getContext();
                /*create a glSurfaceView that will hold the renderer thread*/
                glSurfaceView = oglesContext.createView(JmeSurfaceView.this.getContext());
                /*set the current view as the system engine thread view for future uses*/
                JmeAndroidSystem.setView(JmeSurfaceView.this);
                /*set JME system Listener to initialize game , update , requestClose & destroy on closure*/
                oglesContext.setSystemListener(JmeSurfaceView.this);
                /* set the glSurfaceView to fit the widget */
                glSurfaceView.setLayoutParams(new LayoutParams(JmeSurfaceView.this.getLayoutParams().width, JmeSurfaceView.this.getLayoutParams().height));
                /*post delay the renderer join into the UI thread*/
                handler.postDelayed(new RendererThread(),delayMillis);
            } catch (Exception e) {
                if( onExceptionThrown !=null){
                    onExceptionThrown.Throw(e);
                    jmeSurfaceViewLogger.log(Level.WARNING,e.getMessage());
                }
            }
        }
    }

The JME code is poorly written is all I’m saying.

If the catch block is hit then ‘app’ is most likely null and there was no reason to continue on in the method and hide the real exception with an NPE. There certainly wasn’t going to be a valid context either.

Should have been:

        } catch (Exception ex) {
            handleError("Class " + appClass + " init failed", ex);
            setContentView(new TextView(this));
            return; //no point in continuing
        }
1 Like

I got your point now , thanks @pspeed , i will consider this in my code :slight_smile: .

1 Like

i think this part would be the same , if it catches then no finally block

This statement makes no sense to me.

The finally block is executed no matter what… even on exception. That’s the point of a finally block. “Do this no matter what happens.”

1 Like

This is true i get confused a little bit by this :

but now i understood that i cannot terminate the method at that level of code , because yes finally{} would be executed , no matter what :+1:

And in this case it makes no sense… because you probably only want to run it when there is no catch(), basically.

So it looks like the finally block is unneeded to me.

Yeh , to me too after your explanation , I undid it.

@Darkchaos as far as i know , you have created those issues recently , & you’ve asked for a discussion for it , so what do you think ? , in brief , IS it better to use JmonkeyEngine on Android as a UI-Component or a Custom-View ?

1 Like

This is also interesting when app.start() fails, e.g. on a phone that doesn’t even have openGL ES Support or it just fails for some other reason (we had issues with texture formats).

On Topic: I think we’re talking about two different issues here:
Rendering jme inside a component instead of an Activity/Fragment is definitely a nice feature because it gives more freedom to the user.

I don’t know the difference between UI-Component and Custom View though, searching the web just tells me that UI Components are custom views.

My issues/the other side is mainly about the code smells of having a hardcoded string refering to a class, where one could pass in a pre-instantiated class or something. I agree now that generics aren’t useful here, because like it could even be multiple different simple application subclasses and the user can always care about it properly.

There is one thing that needs to be ensured though and that is that there is no downside in using only a view (i.e. an openGL context may only be created on a fragment or is only running at 20 FPS because not “fullscreen” or whatever other limitation one could imagine). And if there is one, document it properly, maybe we can do something about it.

While we are here: There are a lot of other android related issues, starting with the sound, over to Texture loading. We may at least build an app that has all tests (there used to be one on the store, stone old though) and try to assess the damage. The JME Tests need an overhaul as well, but for now they will do.
Maybe we can also unit test them using the AVD.

2 Likes

First of all , thanks for responding :slight_smile:

It’s good that my message was clear ! :slight_smile:

well, they are the same thing ,but i have mentioned UI Component word for illustrating only,
in android development , they are named CustomViews in which they are nothing other than Classes which are childs of the View class hierarchy , i.e classses extending View .

well , nice question though , if you have navigated the AndroidHarnessActivity or AndroidHarnessFragement before , you will find out that they use the same approach that i have just introduced here , which is rendering jme on a GlSurfaceView then attaching it to a FrameLayout then attaching that Framelayout to the android.Activity instance : using setContentView(frameLayout); , but this indeed prevents the user from using jme freely & at the same time , it wastes android resources like PermissionManager , IntentServices , & on top of that it uses android.Activity not androidx.AppCompatActivity which is pretty much old.

OGLESContext class

as you can see , the final result is GlSurfaceView , but the code is hard to read , because , its much more approaching legacy code.

so, the conclusion logically , no drop down issues for using jme as a CustomView(from now on , i will name it as JmeSurfaceView , but as you said i will provide intensive testCases for that like :

  • using JmeSurfaceView in a AppCompatActivity with other Android Views & Android CustomViews.
  • using JmeSurfaceView on a androidx.Fragment.
  • using JmeSurfaceView on a androidx.AlertDialog.
  • using JmeSurfaceView on an AppWidget ( this is crazy stuff :sweat_smile: ).

Currently , i have 2 examples , the one i have showed you on Discord & Jme Advanced Vehicles is now android :smile: .

i am welcoming any new idea or code you want to introduce.

this is mine :

First example :

the jme class :

the activity xml :

the AppCompatActivity class :

BTW , one of the folks here , have tried JmeSurfaceView , it works even on a non-standard emulator.

Sorry , for this too long article.

1 Like

So , in conclusion , it doesn’t really matter where you are going to render jme , on a Fragment , in anActivity or in an AppCompatActivity or even an AlertDialog , these doesn’t control anything , they are just android application containers that are subClasses of the parent abstract Context class hierarchy , so what controls rendering is the GLSurfaceView.Renderer interface.

For more about android views architecture , visit :

https://developer.android.com/training/custom-views/create-view

How jme is rendered using GLSurfaceView.Renderer interface which is implemented in OGLESContext jmonkeyEngine class :

https://developer.android.com/reference/android/opengl/GLSurfaceView.Renderer?hl=en

So in short you say that you propose to remove/deprecate the Harness to move the XML/Activity Wrapping Code down to the user?

1 Like