implement basic pause menu

This commit is contained in:
2025-11-14 19:17:04 +01:00
parent caa59cd72f
commit 6868f53e38
3 changed files with 238 additions and 30 deletions

View File

@@ -16,6 +16,7 @@ import com.badlogic.gdx.graphics.g3d.utils.DepthShaderProvider;
import com.badlogic.gdx.graphics.glutils.FrameBuffer;
import wtf.beatrice.retrorender.engine.DebugHud;
import wtf.beatrice.retrorender.engine.FpsCameraController;
import wtf.beatrice.retrorender.engine.PauseMenu;
import wtf.beatrice.retrorender.engine.World3D;
public class GameScreen implements Screen {
@@ -30,6 +31,11 @@ public class GameScreen implements Screen {
private World3D world;
private DebugHud hud;
private boolean showHud = false;
private boolean paused = false;
private PauseMenu pauseMenu;
// Shadow Mapping
private ModelBatch shadowBatch;
private DirectionalShadowLight shadowLight;
@@ -42,8 +48,6 @@ public class GameScreen implements Screen {
private static final int RETRO_WIDTH = 320;
private static final int RETRO_HEIGHT = 240;
private boolean showHud = false;
public GameScreen(Main game) {
this.game = game;
}
@@ -89,8 +93,9 @@ public class GameScreen implements Screen {
world = new World3D();
}
private void initHud() {
private void initMenus() {
hud = new DebugHud();
pauseMenu = new PauseMenu();
}
// --- screen methods
@@ -118,25 +123,35 @@ public class GameScreen implements Screen {
initCamera();
initEnvironment();
initWorld();
initHud();
initMenus();
initRetroBuffer();
cameraController.onShow();
cameraController.onShow(); // captures mouse with warmup
}
@Override
public void render(float delta) {
// ESC: toggle pause and mouse capture
if (Gdx.input.isKeyJustPressed(Input.Keys.ESCAPE)) {
paused = !paused;
if (paused) {
cameraController.releaseMouse();
} else {
cameraController.captureMouse();
}
}
if (!paused) {
cameraController.update(delta, world);
world.update(delta);
}
if (Gdx.input.isKeyJustPressed(Input.Keys.TAB)) {
showHud = !showHud;
}
// --- shadow pass: render depth from light's point of view
// point to center the shadow camera on;
// can be adjusted this later (e.g. follow player).
shadowLight.begin(new com.badlogic.gdx.math.Vector3(0f, 0f, 0f), camera.direction);
shadowBatch.begin(shadowLight.getCamera());
@@ -165,6 +180,11 @@ public class GameScreen implements Screen {
cameraController.getYaw(),
cameraController.getPitch());
// pause menu
if (paused && pauseMenu != null) {
pauseMenu.render(RETRO_WIDTH, RETRO_HEIGHT);
}
frameBuffer.end();
// -- scale framebuffer to screen, preserve aspect ratio --
@@ -180,9 +200,6 @@ public class GameScreen implements Screen {
windowH / (float) RETRO_HEIGHT
);
// strict integer scaling
// scale = (float)Math.max(1, Math.floor(scale));
int drawW = Math.round(RETRO_WIDTH * scale);
int drawH = Math.round(RETRO_HEIGHT * scale);
@@ -190,6 +207,36 @@ public class GameScreen implements Screen {
int offsetX = (windowW - drawW) / 2;
int offsetY = (windowH - drawH) / 2;
// --- handle mouse click on pause button (window -> retro coords) ---
if (paused && pauseMenu != null && Gdx.input.justTouched()) {
int mouseX = Gdx.input.getX();
int mouseY = Gdx.graphics.getHeight() - Gdx.input.getY(); // to bottom-left origin
// is the click inside the scaled game area?
if (mouseX >= offsetX && mouseX <= offsetX + drawW &&
mouseY >= offsetY && mouseY <= offsetY + drawH) {
float relX = (mouseX - offsetX) / (float) drawW;
float relY = (mouseY - offsetY) / (float) drawH;
float retroX = relX * RETRO_WIDTH;
float retroY = relY * RETRO_HEIGHT;
PauseMenu.MenuAction action = pauseMenu.getActionAt(retroX, retroY);
switch (action) {
case RESUME:
paused = false;
cameraController.onShow();
break;
case QUIT:
Gdx.app.exit();
break;
default:
break;
}
}
}
// viewport in backbuffer coordinates
Gdx.gl.glViewport(0, 0, backW, backH);
Gdx.gl.glClearColor(0f, 0f, 0f, 1f);
@@ -229,5 +276,8 @@ public class GameScreen implements Screen {
shadowBatch.dispose();
shadowLight.dispose();
if (pauseMenu != null) pauseMenu.dispose();
if (frameBuffer != null) frameBuffer.dispose();
if (screenBatch != null) screenBatch.dispose();
}
}

View File

@@ -65,6 +65,16 @@ public class FpsCameraController {
/** Call from Screen.show() */
public void onShow() {
captureMouse();
}
/** Call from Screen.hide() */
public void onHide() {
releaseMouse();
}
/** Explicitly capture mouse (used when resuming from pause). */
public void captureMouse() {
mouseCaptured = true;
centerX = Gdx.graphics.getWidth() / 2;
centerY = Gdx.graphics.getHeight() / 2;
@@ -76,8 +86,8 @@ public class FpsCameraController {
captureWarmupFrames = WARMUP_FRAMES;
}
/** Call from Screen.hide() */
public void onHide() {
/** Explicitly release mouse (used when pausing). */
public void releaseMouse() {
mouseCaptured = false;
Gdx.graphics.setSystemCursor(Cursor.SystemCursor.Arrow);
}
@@ -95,23 +105,8 @@ public class FpsCameraController {
/** Update each frame with delta time */
public void update(float delta, World3D world) {
// --- ESC: release mouse, stop movement / camera
if (Gdx.input.isKeyJustPressed(Input.Keys.ESCAPE)) {
mouseCaptured = false;
Gdx.graphics.setSystemCursor(Cursor.SystemCursor.Arrow);
}
// only capture inputs if focused/captured
// if we don't own the mouse, just don't move camera / player
if (!mouseCaptured) {
if (Gdx.input.justTouched()) { // if clicked
mouseCaptured = true;
centerX = Gdx.graphics.getWidth() / 2;
centerY = Gdx.graphics.getHeight() / 2;
Gdx.input.setCursorPosition(centerX, centerY);
ensureInvisibleCursor();
Gdx.graphics.setCursor(invisibleCursor);
captureWarmupFrames = WARMUP_FRAMES;
}
return;
}

View File

@@ -0,0 +1,163 @@
package wtf.beatrice.retrorender.engine;
import com.badlogic.gdx.Gdx;
import com.badlogic.gdx.graphics.Color;
import com.badlogic.gdx.graphics.Pixmap;
import com.badlogic.gdx.graphics.Texture;
import com.badlogic.gdx.graphics.g2d.BitmapFont;
import com.badlogic.gdx.graphics.g2d.GlyphLayout;
import com.badlogic.gdx.graphics.g2d.SpriteBatch;
import com.badlogic.gdx.graphics.g2d.freetype.FreeTypeFontGenerator;
import java.util.ArrayList;
import java.util.List;
public class PauseMenu {
private final SpriteBatch batch;
private final BitmapFont font;
private final Texture pixel;
// simple action enum for the menu
public enum MenuAction {
NONE,
RESUME,
QUIT
}
// simple button model
private static class Button {
String label;
MenuAction action;
float x, y, w, h;
Button(String label, MenuAction action) {
this.label = label;
this.action = action;
}
}
private final List<Button> buttons = new ArrayList<>();
public PauseMenu() {
batch = new SpriteBatch();
// Same style as DebugHud
FreeTypeFontGenerator generator =
new FreeTypeFontGenerator(Gdx.files.internal("fonts/red-hat-mono.ttf"));
FreeTypeFontGenerator.FreeTypeFontParameter param =
new FreeTypeFontGenerator.FreeTypeFontParameter();
param.size = 12;
param.color = Color.WHITE;
param.mono = true;
param.hinting = FreeTypeFontGenerator.Hinting.None;
param.borderWidth = 0.2f;
param.borderColor = Color.WHITE;
param.shadowOffsetX = 0;
param.shadowOffsetY = 0;
param.minFilter = Texture.TextureFilter.Nearest;
param.magFilter = Texture.TextureFilter.Nearest;
font = generator.generateFont(param);
generator.dispose();
// create buttons
buttons.add(new Button(" Resume ", MenuAction.RESUME));
buttons.add(new Button(" Quit ", MenuAction.QUIT));
// 1x1 pixel texture (white, we tint it)
Pixmap pm = new Pixmap(1, 1, Pixmap.Format.RGBA8888);
pm.setColor(1f, 1f, 1f, 1f);
pm.fill();
pixel = new Texture(pm);
pm.dispose();
}
public void render(int width, int height) {
batch.getProjectionMatrix().setToOrtho2D(0, 0, width, height);
batch.begin();
// --- black bar at bottom ---
float barH = 40f; // in retro pixels
float barY = 0f;
batch.setColor(0f, 0f, 0f, 0.9f);
batch.draw(pixel, 0f, barY, width, barH);
// --- buttons ---
float spacing = 8f; // horizontal space between buttons
// measure each button label
GlyphLayout layout = new GlyphLayout();
float totalWidth = 0f;
float maxButtonH = 0f;
for (Button b : buttons) {
layout.setText(font, b.label);
float bw = layout.width + 16f; // padding
float bh = layout.height + 8f;
b.w = bw;
b.h = bh;
totalWidth += bw;
if (bh > maxButtonH) maxButtonH = bh;
}
// add spacing between buttons
if (!buttons.isEmpty()) {
totalWidth += spacing * (buttons.size() - 1);
}
float startX = (width - totalWidth) / 2f;
float centerY = barY + (barH - maxButtonH) / 2f;
// draw each button
float currentX = startX;
for (Button b : buttons) {
b.x = currentX;
b.y = centerY;
// button background
batch.setColor(0.1f, 0.1f, 0.1f, 1f);
batch.draw(pixel, b.x, b.y, b.w, b.h);
// simple 1px white border
batch.setColor(1f, 1f, 1f, 1f);
float border = 1f;
// top
batch.draw(pixel, b.x, b.y + b.h - border, b.w, border);
// bottom
batch.draw(pixel, b.x, b.y, b.w, border);
// left
batch.draw(pixel, b.x, b.y, border, b.h);
// right
batch.draw(pixel, b.x + b.w - border, b.y, border, b.h);
// text
layout.setText(font, b.label);
float textX = b.x + (b.w - layout.width) / 2f;
float textY = b.y + b.h - (b.h - layout.height) / 2f - 2f;
font.draw(batch, layout, textX, textY);
currentX += b.w + spacing;
}
batch.end();
}
public MenuAction getActionAt(float x, float y) {
for (Button b : buttons) {
if (x >= b.x && x <= b.x + b.w &&
y >= b.y && y <= b.y + b.h) {
return b.action;
}
}
return MenuAction.NONE;
}
public void dispose() {
batch.dispose();
font.dispose();
pixel.dispose();
}
}