Merge pull request #5 from teppyboy/gdx-video-background

feat(scene/main_menu): add background video
This commit is contained in:
Nguyễn Thế Hưng
2025-10-01 02:19:07 +07:00
committed by GitHub
5 changed files with 111 additions and 25 deletions

View File

@ -6,6 +6,7 @@
*/ */
val libgdxVersion = "1.13.5" val libgdxVersion = "1.13.5"
val gdxVideoVersion = "1.3.3"
plugins { plugins {
// Apply the application plugin to add support for building a CLI application in Java. // Apply the application plugin to add support for building a CLI application in Java.
@ -46,6 +47,10 @@ dependencies {
// Logging // Logging
implementation("org.slf4j:slf4j-api:2.1.0-alpha1") implementation("org.slf4j:slf4j-api:2.1.0-alpha1")
implementation("ch.qos.logback:logback-classic:1.5.18") implementation("ch.qos.logback:logback-classic:1.5.18")
// gdx-video
implementation("com.badlogicgames.gdx-video:gdx-video:${gdxVideoVersion}")
implementation("com.badlogicgames.gdx-video:gdx-video-lwjgl3:${gdxVideoVersion}")
} }
// Apply a specific Java toolchain to ease working on different environments. // Apply a specific Java toolchain to ease working on different environments.

View File

@ -60,8 +60,13 @@ public class Intro extends Scene {
} }
currentOpacity = 1 - ((float) (System.currentTimeMillis() - endTime) / 1000); currentOpacity = 1 - ((float) (System.currentTimeMillis() - endTime) / 1000);
} }
batch.setColor(1, 1, 1, currentOpacity); // Multiply with any externally applied alpha (e.g., Transition)
float externalAlpha = batch.getColor().a; // alpha set by Transition before calling render
float finalAlpha = currentOpacity * externalAlpha;
batch.setColor(1, 1, 1, finalAlpha);
batch.draw(logo, WINDOW_WIDTH / 2 - logo.getWidth() / 4, WINDOW_HEIGHT / 2 - logo.getHeight() / 4, batch.draw(logo, WINDOW_WIDTH / 2 - logo.getWidth() / 4, WINDOW_HEIGHT / 2 - logo.getHeight() / 4,
logo.getWidth() / 2, logo.getHeight() / 2); logo.getWidth() / 2, logo.getHeight() / 2);
// Reset color for safety (Transition will set again anyway)
batch.setColor(1,1,1,externalAlpha);
} }
} }

View File

@ -2,39 +2,68 @@ package org.vibecoders.moongazer.scenes;
import static org.vibecoders.moongazer.Constants.*; import static org.vibecoders.moongazer.Constants.*;
import org.vibecoders.moongazer.Game;
import org.vibecoders.moongazer.State;
import org.vibecoders.moongazer.managers.Assets; import org.vibecoders.moongazer.managers.Assets;
import org.vibecoders.moongazer.ui.UIImageButton; import org.vibecoders.moongazer.ui.UIImageButton;
import org.vibecoders.moongazer.ui.UITextButton; import org.vibecoders.moongazer.ui.UITextButton;
import org.vibecoders.moongazer.Game;
import org.vibecoders.moongazer.State;
import com.badlogic.gdx.Gdx; import com.badlogic.gdx.Gdx;
import com.badlogic.gdx.files.FileHandle;
import com.badlogic.gdx.graphics.Texture; import com.badlogic.gdx.graphics.Texture;
import com.badlogic.gdx.graphics.g2d.SpriteBatch; import com.badlogic.gdx.graphics.g2d.SpriteBatch;
import com.badlogic.gdx.utils.TimeUtils;
import com.badlogic.gdx.video.VideoPlayer;
import com.badlogic.gdx.video.VideoPlayerCreator;
public class MainMenu extends Scene { public class MainMenu extends Scene {
private Texture backgroundTexture; private final Game game;
private VideoPlayer videoPlayer;
private FileHandle videoFileHandle;
private Texture titleTexture; private Texture titleTexture;
private float titleY; private float titleY, titleX, titleWidth, titleHeight;
private float titleX; private boolean videoPrepared = false;
private float titleWidth; private boolean firstFrameLogged = false;
private float titleHeight; private long videoStartTime;
private static final String WEBM_PATH = "videos/main_menu_background.webm";
private static final String OGV_PATH = "videos/main_menu_background.ogv";
public MainMenu(Game game) { public MainMenu(Game game) {
super(game); super(game);
backgroundTexture = Assets.getAsset("textures/main_menu/background.png", Texture.class); this.game = game;
initVideo();
initUI();
}
private void initVideo() {
try {
videoPlayer = VideoPlayerCreator.createVideoPlayer();
FileHandle webm = Gdx.files.internal(WEBM_PATH);
FileHandle ogv = Gdx.files.internal(OGV_PATH);
if (webm.exists()) {
videoFileHandle = webm;
log.info("Using menu background (WebM): {}", webm.path());
} else if (ogv.exists()) {
videoFileHandle = ogv;
log.info("Using menu background (OGV): {}", ogv.path());
} else {
log.warn("No background video found (expected {} or {}).", WEBM_PATH, OGV_PATH);
}
} catch (Exception e) {
log.error("Cannot create VideoPlayer", e);
}
}
private void initUI() {
titleTexture = Assets.getAsset("textures/main_menu/title.png", Texture.class); titleTexture = Assets.getAsset("textures/main_menu/title.png", Texture.class);
// Scale and position title
float targetTitleWidth = 500f; float targetTitleWidth = 500f;
float originalWidth = titleTexture.getWidth(); float scale = targetTitleWidth / titleTexture.getWidth();
float originalHeight = titleTexture.getHeight(); titleWidth = titleTexture.getWidth() * scale;
float scale = targetTitleWidth / originalWidth; titleHeight = titleTexture.getHeight() * scale;
titleWidth = originalWidth * scale;
titleHeight = originalHeight * scale;
titleX = (WINDOW_WIDTH - titleWidth) / 2f; titleX = (WINDOW_WIDTH - titleWidth) / 2f;
titleY = WINDOW_HEIGHT / 2f - titleHeight / 8f; titleY = WINDOW_HEIGHT / 2f - titleHeight / 8f;
// Buttons
var font = Assets.getFont("ui", 24); var font = Assets.getFont("ui", 24);
UITextButton playButton = new UITextButton("Play", font); UITextButton playButton = new UITextButton("Play", font);
UITextButton loadButton = new UITextButton("Load", font); UITextButton loadButton = new UITextButton("Load", font);
@ -48,16 +77,16 @@ public class MainMenu extends Scene {
loadButton.setSize(buttonWidth, buttonHeight); loadButton.setSize(buttonWidth, buttonHeight);
settingsButton.setSize(buttonWidth, buttonHeight); settingsButton.setSize(buttonWidth, buttonHeight);
exitButton.setSize(buttonWidth, buttonHeight); exitButton.setSize(buttonWidth, buttonHeight);
int centerX = WINDOW_WIDTH / 2 - buttonWidth / 2; int centerX = WINDOW_WIDTH / 2 - buttonWidth / 2;
int startY = WINDOW_HEIGHT / 2 - buttonHeight / 2; int startY = WINDOW_HEIGHT / 2 - buttonHeight / 2;
int buttonSpacing = 65; int spacing = 65;
playButton.setPosition(centerX, startY);
loadButton.setPosition(centerX, startY - buttonSpacing);
settingsButton.setPosition(centerX, startY - buttonSpacing * 2);
exitButton.setPosition(centerX, startY - buttonSpacing * 3);
playButton.setSize(buttonWidth, buttonHeight); playButton.setPosition(centerX, startY);
loadButton.setSize(buttonWidth, buttonHeight); loadButton.setPosition(centerX, startY - spacing);
settingsButton.setSize(buttonWidth, buttonHeight); settingsButton.setPosition(centerX, startY - spacing * 2);
exitButton.setSize(buttonWidth, buttonHeight); exitButton.setPosition(centerX, startY - spacing * 3);
playButton.onClick(() -> log.debug("Play clicked")); playButton.onClick(() -> log.debug("Play clicked"));
loadButton.onClick(() -> log.debug("Load clicked")); loadButton.onClick(() -> log.debug("Load clicked"));
settingsButton.onClick(() -> { settingsButton.onClick(() -> {
@ -79,9 +108,56 @@ public class MainMenu extends Scene {
game.stage.addActor(root); game.stage.addActor(root);
} }
private void ensureVideoStarted() {
if (videoPlayer == null || videoPrepared) return;
if (videoFileHandle == null || !videoFileHandle.exists()) return;
if (game.transition == null && game.state != State.MAIN_MENU) return;
startVideo();
}
private void startVideo() {
try {
videoPlayer.setLooping(true);
videoPlayer.play(videoFileHandle);
videoPrepared = true;
videoStartTime = TimeUtils.millis();
} catch (Exception e) {
log.error("Failed to play video: {}", videoFileHandle.path(), e);
}
}
public void forceStartVideo() { // used by Transition for early warm-up
if (!videoPrepared && videoFileHandle != null && videoFileHandle.exists()) startVideo();
}
@Override @Override
public void render(SpriteBatch batch) { public void render(SpriteBatch batch) {
batch.draw(backgroundTexture, 0, 0, WINDOW_WIDTH, WINDOW_HEIGHT); ensureVideoStarted();
if (videoPlayer != null && videoPrepared) {
videoPlayer.update();
Texture videoTexture = videoPlayer.getTexture();
if (videoTexture != null) {
if (!firstFrameLogged) {
firstFrameLogged = true;
log.info("Menu video first frame in {} ms", TimeUtils.timeSinceMillis(videoStartTime));
}
batch.draw(videoTexture, 0, 0, WINDOW_WIDTH, WINDOW_HEIGHT);
} else {
batch.draw(Assets.getBlackTexture(), 0, 0, WINDOW_WIDTH, WINDOW_HEIGHT);
}
} else {
batch.draw(Assets.getBlackTexture(), 0, 0, WINDOW_WIDTH, WINDOW_HEIGHT);
}
batch.draw(titleTexture, titleX, titleY, titleWidth, titleHeight); batch.draw(titleTexture, titleX, titleY, titleWidth, titleHeight);
} }
public void updateVideo() {
if (videoPlayer != null && videoPrepared) videoPlayer.update();
}
@Override
public void dispose() {
super.dispose();
if (videoPlayer != null) videoPlayer.dispose();
}
} }

View File

@ -58,4 +58,4 @@ public class Transition extends Scene {
to.root.setColor(1, 1, 1, toOpacity); to.root.setColor(1, 1, 1, toOpacity);
to.render(batch); to.render(batch);
} }
} }