better splash

This commit is contained in:
minjaesong
2026-04-06 11:11:36 +09:00
parent f3fb8a96f0
commit 9edf3a6573
3 changed files with 111 additions and 130 deletions

View File

@@ -265,10 +265,10 @@ public class App implements ApplicationListener {
public static Hq2x hq2x; public static Hq2x hq2x;
public static Mesh fullscreenQuad; public static Mesh fullscreenQuad;
private static OrthographicCamera camera; public static OrthographicCamera camera;
private static FlippingSpriteBatch logoBatch; public static FlippingSpriteBatch logoBatch;
public static TextureRegion splashScreenLogo; public static TextureRegion splashScreenLogo;
private static TextureRegion splashBackdrop; public static TextureRegion splashBackdrop;
public static AudioDevice audioDevice; public static AudioDevice audioDevice;
public static FlippingSpriteBatch batch; public static FlippingSpriteBatch batch;
@@ -550,7 +550,7 @@ public class App implements ApplicationListener {
} }
} }
private static Color splashTextCol = new Color(0x282828FF); public static Color splashTextCol = new Color(0x282828FF);
@Override @Override
public void create() { public void create() {
@@ -752,6 +752,7 @@ public class App implements ApplicationListener {
// hand over the scene control to this single class; Terrarum must call // hand over the scene control to this single class; Terrarum must call
// 'AppLoader.getINSTANCE().screen.render(delta)', this is not redundant at all! // 'AppLoader.getINSTANCE().screen.render(delta)', this is not redundant at all!
SplashScreen.loadingComplete = true;
IngameInstance title = ModMgr.INSTANCE.getTitleScreen(batch); IngameInstance title = ModMgr.INSTANCE.getTitleScreen(batch);
if (title != null) { if (title != null) {
@@ -882,107 +883,7 @@ public class App implements ApplicationListener {
} }
public static void drawSplash() { public static void drawSplash() {
setCameraPosition(0f, 0f);
logoBatch.setColor(Color.WHITE);
logoBatch.setBlendFunctionSeparate(GL20.GL_SRC_ALPHA, GL20.GL_ONE_MINUS_SRC_ALPHA, GL20.GL_ONE, GL20.GL_ONE_MINUS_SRC_ALPHA);
int drawWidth = Toolkit.INSTANCE.getDrawWidth();
int safetyTextLen = fontGame.getWidth(Lang.INSTANCE.get("APP_WARNING_HEALTH_AND_SAFETY", true));
int logoPosX0 = (drawWidth - splashScreenLogo.getRegionWidth() - safetyTextLen) >>> 1;
int logoPosY = Math.round(scr.getHeight() / 15f);
int textY = logoPosY + splashScreenLogo.getRegionHeight() - 16;
// draw custom backdrop (if exists)
if (splashBackdrop != null) {
logoBatch.setShader(null);
logoBatch.begin();
var size = ((float) Math.max(scr.getWidth(), scr.getHeight()));
var x = 0f;
var y = 0f;
if (scr.getWidth() > scr.getHeight()) {
y = (scr.getHeight() - size) / 2f;
}
else {
x = (scr.getWidth() - size) / 2f;
}
logoBatch.draw(splashBackdrop, x, y, size, size);
logoBatch.end();
}
int logoPosX = logoPosX0;//(int)(logoPosX0 + Math.round(100 * Math.sin(GLOBAL_RENDER_TIMER / 50.0)));
// draw logo reflection
logoBatch.setShader(shaderReflect);
logoBatch.setColor(Color.WHITE);
logoBatch.begin();
if (getConfigBoolean("showhealthmessageonstartup")) {
logoBatch.draw(splashScreenLogo, logoPosX, logoPosY + splashScreenLogo.getRegionHeight());
}
else {
logoBatch.draw(splashScreenLogo, (drawWidth - splashScreenLogo.getRegionWidth()) / 2f,
(scr.getHeight() - splashScreenLogo.getRegionHeight() * 2) / 2f + splashScreenLogo.getRegionHeight()
);
}
logoBatch.end();
logoBatch.setShader(null);
logoBatch.begin();
if (getConfigBoolean("showhealthmessageonstartup")) {
logoBatch.draw(splashScreenLogo, logoPosX, logoPosY);
}
else {
logoBatch.draw(splashScreenLogo, (drawWidth - splashScreenLogo.getRegionWidth()) / 2f,
(scr.getHeight() - splashScreenLogo.getRegionHeight() * 2) / 2f
);
}
logoBatch.end();
// draw loading bar and subtitle
SplashScreen.render(logoBatch, camera); SplashScreen.render(logoBatch, camera);
// draw health messages
if (getConfigBoolean("showhealthmessageonstartup")) {
logoBatch.setShader(null);
logoBatch.begin();
logoBatch.setColor(splashTextCol);
fontGame.draw(logoBatch, Lang.INSTANCE.get("APP_WARNING_HEALTH_AND_SAFETY", true),
logoPosX + splashScreenLogo.getRegionWidth(),
textY
);
// some chinese stuff
if (GAME_LOCALE.contentEquals("zhCN")) {
for (int i = 1; i <= 4; i++) {
String s = Lang.INSTANCE.get("APP_CHINESE_HEALTHY_GAME_MSG_" + i, true);
fontGame.draw(logoBatch, s,
(drawWidth - fontGame.getWidth(s)) >>> 1,
Math.round(scr.getHeight() * 12f / 15f + fontGame.getLineHeight() * (i - 1))
);
}
}
Texture tex1 = CommonResourcePool.INSTANCE.getAsTexture("title_health1");
Texture tex2 = CommonResourcePool.INSTANCE.getAsTexture("title_health2");
int virtualHeight = scr.getHeight() - logoPosY - splashScreenLogo.getRegionHeight() / 4;
int virtualHeightOffset = scr.getHeight() - virtualHeight;
logoBatch.drawFlipped(tex1, (drawWidth - tex1.getWidth()) >>> 1, virtualHeightOffset + (virtualHeight >>> 1) - 16, tex1.getWidth(), -tex1.getHeight());
logoBatch.drawFlipped(tex2, (drawWidth - tex2.getWidth()) >>> 1, virtualHeightOffset + (virtualHeight >>> 1) + 16 + tex2.getHeight(), tex2.getWidth(), -tex2.getHeight());
}
logoBatch.end();
batch.setBlendFunctionSeparate(GL20.GL_SRC_ALPHA, GL20.GL_ONE_MINUS_SRC_ALPHA, GL20.GL_ONE, GL20.GL_ONE_MINUS_SRC_ALPHA);
} }
/** /**
@@ -1471,7 +1372,7 @@ public class App implements ApplicationListener {
} }
private static void setCameraPosition(float newX, float newY) { public static void setCameraPosition(float newX, float newY) {
camera.position.set((-newX + scr.getWidth() / 2), (-newY + scr.getHeight() / 2), 0f); // deliberate integer division camera.position.set((-newX + scr.getWidth() / 2), (-newY + scr.getHeight() / 2), 0f); // deliberate integer division
camera.update(); camera.update();
logoBatch.setProjectionMatrix(camera.combined); logoBatch.setProjectionMatrix(camera.combined);

View File

@@ -31,16 +31,6 @@ object CommonResourcePool {
private val slowLoadingQueue = ConcurrentLinkedQueue<ResourceLoadingDescriptor>() private val slowLoadingQueue = ConcurrentLinkedQueue<ResourceLoadingDescriptor>()
private val slowLoadingRemaining = AtomicInteger(0) private val slowLoadingRemaining = AtomicInteger(0)
private val slowLoadingTotal = AtomicInteger(0)
/** 0.0 = not started yet, 1.0 = all done. Only meaningful during / after [loadAllSlowly]. */
val loadingProgress: Float
get() {
val total = slowLoadingTotal.get()
if (total == 0) return 0f
val remaining = slowLoadingRemaining.get()
return (total - remaining).toFloat() / total.toFloat()
}
fun setGLThread(thread: Thread) { fun setGLThread(thread: Thread) {
glThread = thread glThread = thread
@@ -162,7 +152,6 @@ object CommonResourcePool {
val desc = loadingList.removeFirst() val desc = loadingList.removeFirst()
slowLoadingQueue.add(desc) slowLoadingQueue.add(desc)
slowLoadingRemaining.incrementAndGet() slowLoadingRemaining.incrementAndGet()
slowLoadingTotal.incrementAndGet()
} }
// Block until the GL thread has processed all slow items // Block until the GL thread has processed all slow items
while (slowLoadingRemaining.get() > 0) { while (slowLoadingRemaining.get() > 0) {

View File

@@ -1,20 +1,32 @@
package net.torvald.terrarum package net.torvald.terrarum
import com.badlogic.gdx.graphics.Color import com.badlogic.gdx.graphics.Color
import com.badlogic.gdx.graphics.GL20
import com.badlogic.gdx.graphics.OrthographicCamera import com.badlogic.gdx.graphics.OrthographicCamera
import com.badlogic.gdx.graphics.Texture
import com.badlogic.gdx.graphics.g2d.SpriteBatch import com.badlogic.gdx.graphics.g2d.SpriteBatch
import net.torvald.terrarum.langpack.Lang
import net.torvald.terrarum.ui.Toolkit import net.torvald.terrarum.ui.Toolkit
import net.torvald.terrarum.ui.UICanvas import net.torvald.terrarum.ui.UICanvas
import net.torvald.terrarum.ui.UIItemHorzSlider import net.torvald.terrarum.ui.UIItemHorzSlider
import kotlin.math.max
/** /**
* Draws the loading bar and subtitle on the cold-boot splash screen. * Draws the cold-boot splash screen: backdrop, logo, loading bar, subtitle, and health messages.
* Called from App.drawSplash() (Java static context) on the GL thread. * Consolidated from App.drawSplash() so all splash rendering lives here.
*
* Progress is time-based: p = t / (t + HALF_TIME_MS), snapping to 1.0 when
* [loadingComplete] is set to true by App right before transitioning to the title screen.
* *
* Created by minjaesong on 2026-04-06. * Created by minjaesong on 2026-04-06.
*/ */
object SplashScreen { object SplashScreen {
/** Set to true by App when loading completes, to snap the bar to 100%. */
@JvmField @Volatile var loadingComplete = false
private const val HALF_TIME_MS = 1500L // progress = t / (t + HALF_TIME_MS)
private val stubParent = object : UICanvas() { private val stubParent = object : UICanvas() {
override var width = 0 override var width = 0
override var height = 0 override var height = 0
@@ -25,15 +37,74 @@ object SplashScreen {
private var loadingBar: UIItemHorzSlider? = null private var loadingBar: UIItemHorzSlider? = null
private var lastBarWidth = 0 private var lastBarWidth = 0
private var startTime = 0L
private val subtitleCol = Color(0.75f, 0.75f, 0.75f, 1f) private val subtitleCol = Color(0.75f, 0.75f, 0.75f, 1f)
@JvmStatic @JvmStatic fun render(batch: SpriteBatch, camera: OrthographicCamera) {
fun render(batch: SpriteBatch, camera: OrthographicCamera) { App.setCameraPosition(0f, 0f)
val progress = CommonResourcePool.loadingProgress
val batch = App.logoBatch
val scr = App.scr
val drawWidth = Toolkit.drawWidth
val showHealth = App.getConfigBoolean("showhealthmessageonstartup")
batch.color = Color.WHITE
batch.setBlendFunctionSeparate(GL20.GL_SRC_ALPHA, GL20.GL_ONE_MINUS_SRC_ALPHA, GL20.GL_ONE, GL20.GL_ONE_MINUS_SRC_ALPHA)
val safetyTextLen = if (showHealth) App.fontGame.getWidth(Lang["APP_WARNING_HEALTH_AND_SAFETY", true]) else 0
val logoPosX0 = (drawWidth - App.splashScreenLogo.regionWidth - safetyTextLen) ushr 1
val logoPosY = Math.round(scr.height / 15f)
val textY = logoPosY + App.splashScreenLogo.regionHeight - 16
// backdrop
App.splashBackdrop?.let { backdrop ->
batch.setShader(null)
batch.inUse {
val size = max(scr.width, scr.height).toFloat()
val x = if (scr.width > scr.height) 0f else (scr.width - size) / 2f
val y = if (scr.width > scr.height) (scr.height - size) / 2f else 0f
batch.draw(backdrop, x, y, size, size)
}
}
val logoPosX = logoPosX0
// logo reflection
batch.setShader(App.shaderReflect)
batch.color = Color.WHITE
batch.inUse {
if (showHealth) {
batch.draw(App.splashScreenLogo, logoPosX.toFloat(), (logoPosY + App.splashScreenLogo.regionHeight).toFloat())
} else {
batch.draw(App.splashScreenLogo,
(drawWidth - App.splashScreenLogo.regionWidth) / 2f,
(scr.height - App.splashScreenLogo.regionHeight * 2) / 2f + App.splashScreenLogo.regionHeight)
}
}
// logo
batch.setShader(null)
batch.inUse {
if (showHealth) {
batch.draw(App.splashScreenLogo, logoPosX.toFloat(), logoPosY.toFloat())
} else {
batch.draw(App.splashScreenLogo,
(drawWidth - App.splashScreenLogo.regionWidth) / 2f,
(scr.height - App.splashScreenLogo.regionHeight * 2) / 2f)
}
}
// loading bar+subtitle
if (startTime == 0L) startTime = System.currentTimeMillis()
val elapsed = System.currentTimeMillis() - startTime
val progress = if (loadingComplete) 1f
else elapsed.toFloat() / (elapsed + HALF_TIME_MS).toFloat()
val barWidth = (Toolkit.drawWidth * 0.4f).toInt().coerceAtLeast(200) val barWidth = (Toolkit.drawWidth * 0.4f).toInt().coerceAtLeast(200)
val barX = (Toolkit.drawWidth - barWidth) / 2 val barX = (Toolkit.drawWidth - barWidth) / 2
val barY = App.scr.height - 48 val barY = App.scr.height - App.scr.tvSafeGraphicsHeight - 24
if (loadingBar == null || lastBarWidth != barWidth) { if (loadingBar == null || lastBarWidth != barWidth) {
lastBarWidth = barWidth lastBarWidth = barWidth
@@ -48,16 +119,36 @@ object SplashScreen {
bar.posY = barY bar.posY = barY
bar.handleWidth = (progress * barWidth).toInt().coerceIn(12, barWidth) bar.handleWidth = (progress * barWidth).toInt().coerceIn(12, barWidth)
val subtitle = "${App.GAME_NAME} ${App.getVERSION_STRING()}" val subtitle = "Reticulating Splines..."
val subtitleW = App.fontGame.getWidth(subtitle) val subtitleW = App.fontGame.getWidth(subtitle)
val subtitleX = (Toolkit.drawWidth - subtitleW) / 2f val subtitleX = (Toolkit.drawWidth - subtitleW) / 2f
val subtitleY = (barY - 20).toFloat() // leave gap above the bar val subtitleY = (barY - 32).toFloat()
batch.inUse {
batch.color = subtitleCol
App.fontGame.draw(batch, subtitle, subtitleX, subtitleY)
batch.color = Color.WHITE
bar.render(0f, batch, camera)
}
// health messages
if (showHealth) {
batch.setShader(null)
batch.inUse {
batch.color = App.splashTextCol
App.fontGame.draw(batch, Lang["APP_WARNING_HEALTH_AND_SAFETY", true],
(logoPosX + App.splashScreenLogo.regionWidth).toFloat(), textY.toFloat())
val tex1 = CommonResourcePool.getAsTexture("title_health1")
val tex2 = CommonResourcePool.getAsTexture("title_health2")
val virtualHeight = scr.height - logoPosY - App.splashScreenLogo.regionHeight / 4
val virtualHeightOffset = scr.height - virtualHeight
batch.drawFlipped(tex1, ((drawWidth - tex1.width) ushr 1).toFloat(), (virtualHeightOffset + (virtualHeight ushr 1) - 16).toFloat(), tex1.width.toFloat(), (-tex1.height).toFloat())
batch.drawFlipped(tex2, ((drawWidth - tex2.width) ushr 1).toFloat(), (virtualHeightOffset + (virtualHeight ushr 1) + 16 + tex2.height).toFloat(), tex2.width.toFloat(), (-tex2.height).toFloat())
}
}
App.batch.setBlendFunctionSeparate(GL20.GL_SRC_ALPHA, GL20.GL_ONE_MINUS_SRC_ALPHA, GL20.GL_ONE, GL20.GL_ONE_MINUS_SRC_ALPHA)
batch.begin()
batch.color = subtitleCol
App.fontGame.draw(batch, subtitle, subtitleX, subtitleY)
batch.color = Color.WHITE
bar.render(0f, batch, camera)
batch.end()
} }
} }