Android Question libGDX, ScreenManager & Auto transitions

Foz

Member
Licensed User
Longtime User
I've come across something that appears to be a bug when using the screen manager.

Using the MultiScreen example, I've modified it so there is a countdown of 100 (per frame render). On hitting zero, at the end of the render, I'm telling the screen manager to switch to the other screen.

This works, but it works differently to the touch event - the touch event switches instantly, whereas the countdown version as approximately a 1 second delay.

What is the correct way to detect when to automatically change the screen? A timer that just runs asynchronously to the game loop?

I've added the code from my modified MultiScreen so you can see what I'm trying to do.

B4X:
#Region Module Attributes
    #FullScreen: False
    #IncludeTitle: True
    #ApplicationLabel: MultiScreen
    #VersionCode: 1
    #VersionName:
    #SupportedOrientations: unspecified
    #CanInstallToExternalStorage: False
#End Region

'Activity module
Sub Process_Globals
    Dim LastScreen As Int = 0
End Sub

Sub Globals
    Dim lGdx As LibGDX
    Dim GL As lgGL
    Dim LG_Input As lgInputProcessor

    Dim LGSM As lgScreenManager
    Dim LG_Screen(2) As lgScreen

    Dim Camera As lgOrthographicCamera
    Dim Batch As lgSpriteBatch
    Dim Texture As lgTexture
    Dim Region As lgTextureRegion
    Dim Sprite As lgSprite
    Dim Font As lgBitmapFont
    Dim TextHeight, TextWidth As Int
   
    Dim countdown As Int
End Sub

Sub Activity_Create(FirstTime As Boolean)
    Activity.Color = Colors.Blue

    'Adds the libGDX surface to the activity
    Dim V As View = lGdx.InitializeView("LG")
    Activity.AddView(V, 10%x, 10%y, 80%x, 80%y)
    Log("Initialized = " & V.IsInitialized)
    lGdx.LogLevel = lGdx.LOGLEVEL_Debug

    'Adds a label
    Dim lbl As Label
    lbl.Initialize("")
    lbl.Gravity = Gravity.CENTER_HORIZONTAL
    lbl.Text = "Touch the libGDX view to alternate its screens"
    lbl.TextColor = Colors.White
    lbl.TextSize = 18
    Activity.AddView(lbl, 5%x, 92%y, 90%x, 20dip)
End Sub

Sub Activity_Resume
    'Informs libGDX of Resume events
    If lGdx.IsInitialized Then lGdx.Resume
End Sub

Sub Activity_Pause (UserClosed As Boolean)
    'Informs libGDX of Pause events
    If lGdx.IsInitialized Then lGdx.Pause
End Sub

Sub LG_Create
    'Initializes the InputProcessor for touch events
    LG_Input.Initialize("LG")

    'Creates two screens
    LGSM.Initialize(lGdx)
    LG_Screen(0) = LGSM.AddScreen("LGS1")
    LG_Screen(1) = LGSM.AddScreen("LGS2")
    LGSM.CurrentScreen = LG_Screen(LastScreen)
End Sub

Sub LG_Resize(Width As Int, Height As Int)
End Sub

Sub LG_Render
End Sub

Sub LG_Resume
End Sub

Sub LG_Pause
End Sub

Sub LG_Dispose
End Sub

Sub LG_KeyDown(KeyCode As Int) As Boolean
    Return False
End Sub

Sub LG_KeyUp(KeyCode As Int) As Boolean
    Return False
End Sub

Sub LG_KeyTyped(Character As Char) As Boolean
    Return False
End Sub

Sub LG_TouchDown(ScreenX As Int, ScreenY As Int, Pointer As Int) As Boolean
    Return False
End Sub

Sub LG_TouchUp(ScreenX As Int, ScreenY As Int, Pointer As Int) As Boolean
    'Changes the current screen
    If LGSM.CurrentScreen = LG_Screen(1) Then
        LGSM.CurrentScreen = LG_Screen(0)
    Else
        LGSM.CurrentScreen = LG_Screen(1)
    End If
    Return True
End Sub

Sub LG_TouchDragged(ScreenX As Int, ScreenY As Int, Pointer As Int) As Boolean
    Return False
End Sub

#Region Screen 1
'-------------------------------------------------------------------------------------
' SCREEN 1
'-------------------------------------------------------------------------------------
Sub LGS1_Show
    Log("1 show")
    LastScreen = 0
countdown = 100
    Batch.Initialize

    Texture.Initialize("libgdx.png")
    Texture.SetFilter(Texture.FILTER_Linear, Texture.FILTER_Linear)
    Region.InitializeWithTexture2(Texture, 512, 275)
    Sprite.InitializeWithRegion(Region)
    Sprite.SetSize(0.9, 0.9 * Sprite.Height / Sprite.Width)
    Sprite.SetOrigin(Sprite.Width / 2, Sprite.Height / 2)
    Sprite.SetPosition(-Sprite.Width / 2, -Sprite.Height / 2)
End Sub

Sub LGS1_Resize(Width As Int, Height As Int)
    Log("1 resize")

    'Sets the camera viewport
    Camera.Initialize2(1, Height / Width)
End Sub

Sub LGS1_Render(Delta As Float)
'    Log("1 render " & Delta)
    GL.glClearColor(1, 1, 1, 1)
    GL.glClear(GL.GL10_COLOR_BUFFER_BIT)

    Batch.ProjectionMatrix = Camera.Combined
    Batch.Begin
    Sprite.Draw(Batch)
    Batch.End
   
    countdown = countdown - 1
    If countdown < 0 Then
Log(DateTime.Now & "    Switch to screen 2 start")
LGSM.CurrentScreen = LG_Screen(1)
Log(DateTime.Now & "    Switch to screen 2 end")
    End If
End Sub

Sub LGS1_Pause
    Log("1 pause")
End Sub

Sub LGS1_Resume
    Log("1 resume")
End Sub

Sub LGS1_Hide
    Log("1 hide")
    Texture.dispose
    Batch.dispose
    Log("1 disposed")
End Sub
#End Region

#Region Screen 2
'-------------------------------------------------------------------------------------
' SCREEN 2
'-------------------------------------------------------------------------------------
Sub LGS2_Show
    Log("2 show")
    LastScreen = 1
countdown = 100

    Batch.Initialize

    Dim FG As lgFontGenerator
    Font = FG.CreateFont("font/liquidcrystal.otf", 48 * Density, "2nd scre013456789")
    Font.SetColorRGBA(0, 0, 1, 1) 'Blue
    TextHeight = Font.GetBounds("2nd screen").Height
    TextWidth = Font.GetBounds("2nd screen").Width
End Sub

Sub LGS2_Resize(Width As Int, Height As Int)
    Log("2 resize")

    'Sets the camera viewport
    Camera.Initialize
    Camera.SetToOrtho(False)
End Sub

Sub LGS2_Render(Delta As Float)
    'Log("2 render " & Delta)
    GL.glClearColor(0.5, 0.8, 0.6, 1)
    GL.glClear(GL.GL10_COLOR_BUFFER_BIT)

    Batch.ProjectionMatrix = Camera.Combined
    Batch.Begin
    Font.Draw(Batch, "2nd screen", (Camera.ViewportWidth - TextWidth) * 0.5, (Camera.ViewportHeight + TextHeight) * 0.7)

    Font.Draw(Batch, "" & countdown, (Camera.ViewportWidth - TextWidth) * 0.5, (Camera.ViewportHeight + TextHeight) * 0.3)

    Batch.End

    countdown = countdown - 1
    If countdown < 0 Then
Log(DateTime.Now & "    Switch to screen 1 start")
LGSM.CurrentScreen = LG_Screen(0)
Log(DateTime.Now & "    Switch to screen 1 end")
    End If
End Sub

Sub LGS2_Pause
    Log("2 pause")
End Sub

Sub LGS2_Resume
    Log("2 resume")
End Sub

Sub LGS2_Hide
    Log("2 hide")
    Font.dispose
    Batch.dispose
    Log("2 disposed")
End Sub
#End Region
 

Foz

Member
Licensed User
Longtime User
How can you catch events for a screen that is not displayed yet? Hide, Show and the other events are not called until the timeout has occured.
When people are tapping on the screen that isn't responding, the moment the screen is active, it catches that touch before the user realises there is a change.

For instance - the last life has gone, and the call then is to the game over screen. For 1 second, the app freezes, so the user is tapping on the screen wondering if they have done something wrong. As they are tapping, the game over screen flickers on, and their finger then hits the "start again" area, which then calls the screen switch which is instant, so what appears to the user is that the game restarts for no reason with a very brief (one or two frames) flicker of the game over screen.

Okay, so to fix this, a non-touch time out is needed, so implement a non-touch delay is easy enough, but there is still the application freeze. It should not be freezing!

Disk or network operations should be done inside the libGDX thread. Never outside because that could block the main thread and triggers an ANR if the operation takes too long and the user tries to interact with the app.
I'm not talking about synchronous calls - they are an abomination that should never be used in a high performance environment like a game!
No, I'm talking about asynchronous calls that respond usually on their own thread - in the same way the timer runs on it's own thread.

You miss nothing. The screen system is just a convenient way to do things, but you can do the same with a select in each event to call separate subs.
Excellent, I was just worried that I was missing something obvious.

Thank you for your assistance!
 
Upvote 0

wonder

Expert
Licensed User
Longtime User
They both run on the main thread.
 
Upvote 0

Informatix

Expert
Licensed User
Longtime User
When people are tapping on the screen that isn't responding, the moment the screen is active, it catches that touch before the user realises there is a change.

For instance - the last life has gone, and the call then is to the game over screen. For 1 second, the app freezes, so the user is tapping on the screen wondering if they have done something wrong. As they are tapping, the game over screen flickers on, and their finger then hits the "start again" area, which then calls the screen switch which is instant, so what appears to the user is that the game restarts for no reason with a very brief (one or two frames) flicker of the game over screen.

Okay, so to fix this, a non-touch time out is needed, so implement a non-touch delay is easy enough, but there is still the application freeze. It should not be freezing!


I'm not talking about synchronous calls - they are an abomination that should never be used in a high performance environment like a game!
No, I'm talking about asynchronous calls that respond usually on their own thread - in the same way the timer runs on it's own thread.


Excellent, I was just worried that I was missing something obvious.

Thank you for your assistance!
I will try to find a solution for the next version of libGDX.
 
Upvote 0

wonder

Expert
Licensed User
Longtime User
I don't remember how Erel implemented it but a timer, technically, cannot run on the main thread. Only its events are raised in the main thread. CallSubUI places a message in the message queue of B4A so the called sub will be run by the B4A handler in the main thread whenever possible.
I'm sorry, I didn't use the correct words. By Timer I meant the Tick event. Its code runs on the main thread, right?
Consider the following code:
B4X:
'Indeed this is a very dumb example, I would never code anything like this! ;)
Sub LG_Render
    Log(DateTime.Now) 'Let assume the current time is: 1444908840000
    ...
    lgdx.CallSubUI("Update_Text", Null)
    ...
    Log(DateTime.Now) 'Will the value be 1444908840001????
    Update_Map_Coordinates
End Sub

Sub Update_Text(param As Object)
    For i = 0 to 999
        Label(i).Text = "Text updated at " & DateTime.Now
    Next
End Sub

Will the Update_Text function run in the main thread at the same time as the Update_Map_Coordinates runs on the LibGDX thread,
- or -
will the Update_Map_Coordinates function run only after the Update_Text is finished?
 
Upvote 0

Informatix

Expert
Licensed User
Longtime User
I'm sorry, I didn't use the correct words. By Timer I meant the Tick event. Its code runs on the main thread, right?
Consider the following code:
B4X:
'Indeed this is a very dumb example, I would never code anything like this! ;)
Sub LG_Render
    Log(DateTime.Now) 'Let assume the current time is: 1444908840000
    ...
    lgdx.CallSubUI("Update_Text", Null)
    ...
    Log(DateTime.Now) 'Will the value be 1444908840001????
    Update_Map_Coordinates
End Sub

Sub Update_Text(param As Object)
    For i = 0 to 999
        Label(i).Text = "Text updated at " & DateTime.Now
    Next
End Sub

When the Update_Text function will run is unpredictable. One thing is sure: the code in Render event won't wait for it.
And yes you're right, Timer and CallSubUI has something in common. They run in the background but their goal is to trigger code execution in the main thread.
 
Upvote 0
Top