Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion fxgl-samples/src/main/java/intermediate/MenuSample.java
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ protected void initSettings(GameSettings settings) {
settings.setMainMenuEnabled(true);
settings.setGameMenuEnabled(true);
settings.setFullScreenAllowed(true);
settings.setEnabledMenuItems(EnumSet.of(MenuItem.EXTRA));
settings.setEnabledMenuItems(EnumSet.of(MenuItem.DIFFICULTY, MenuItem.EXTRA));
settings.getCredits().addAll(Arrays.asList(
"Short Name - Lead Programmer",
"LongLongLongLongLongLongLong Name - Programmer",
Expand Down
7 changes: 6 additions & 1 deletion fxgl/src/main/kotlin/com/almasb/fxgl/app/Settings.kt
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,12 @@ enum class MenuItem {
/**
* Enables ONLINE (multiplayer).
*/
ONLINE
ONLINE,

/**
* Enables DIFFICULTY -> EASY, MEDIUM, HARD, NIGHTMARE
*/
DIFFICULTY,
}

/**
Expand Down
37 changes: 37 additions & 0 deletions fxgl/src/main/kotlin/com/almasb/fxgl/app/scene/FXGLDefaultMenu.kt
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import com.almasb.fxgl.dsl.*
import com.almasb.fxgl.dsl.FXGL.Companion.animationBuilder
import com.almasb.fxgl.dsl.FXGL.Companion.random
import com.almasb.fxgl.dsl.FXGL.Companion.texture
import com.almasb.fxgl.gameplay.GameDifficulty
import com.almasb.fxgl.input.Input
import com.almasb.fxgl.input.InputModifier
import com.almasb.fxgl.input.Trigger
Expand Down Expand Up @@ -258,6 +259,12 @@ open class FXGLDefaultMenu(type: MenuType) : FXGLMenu(type) {
itemOptions.setChild(createOptionsMenu())
box.add(itemOptions)

if (enabledItems.contains(MenuItem.DIFFICULTY)) {
val itemDifficulty = MenuButton("menu.difficulty")
itemDifficulty.setMenuContent({ createDifficultyMenu() })
box.add(itemDifficulty)
}

if (enabledItems.contains(MenuItem.EXTRA)) {
val itemExtra = MenuButton("menu.extra")
itemExtra.setChild(createExtraMenu())
Expand Down Expand Up @@ -297,6 +304,12 @@ open class FXGLDefaultMenu(type: MenuType) : FXGLMenu(type) {
itemOptions.setChild(createOptionsMenu())
box.add(itemOptions)

if (enabledItems.contains(MenuItem.DIFFICULTY)) {
val itemDifficulty = MenuButton("menu.difficulty")
itemDifficulty.setMenuContent({ createDifficultyMenu() })
box.add(itemDifficulty)
}

if (enabledItems.contains(MenuItem.EXTRA)) {
val itemExtra = MenuButton("menu.extra")
itemExtra.setChild(createExtraMenu())
Expand Down Expand Up @@ -343,6 +356,30 @@ open class FXGLDefaultMenu(type: MenuType) : FXGLMenu(type) {
return MenuBox(itemGameplay, itemControls, itemVideo, itemAudio, btnRestore)
}

private fun createDifficultyMenu(): MenuContent {
val difficultyBox = getUIFactoryService().newChoiceBox(
FXCollections.observableArrayList(GameDifficulty.entries)
)

difficultyBox.styleClass.add("fxgl-difficulty-choice-box")

difficultyBox.value = getSettings().gameDifficulty
getSettings().gameDifficultyProperty().bindBidirectional(difficultyBox.valueProperty())

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should this be the other way around? I'm thinking of a case where the user sets the difficulty in the main menu, then opens the game menu. At this point does the above mean the settings difficulty will rebound to the UI box's value property?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I believe it is in the correct order. For example, if the user sets the difficulty in the main menu, that will update getSettings().gameDifficulty. Then later, the game menu creates its own difficultyBox and executes difficultyBox.value = getSettings().gameDifficulty ->bindBidirectional(difficultyBox.valueProperty(), the box will show the same difficulty that was set in the main menu, any changes in either place after that will update the other.

I havent' tested the reverse order, but if the value was bound first and then assigned to one side, the assigned value should still propagate to the other menu because of the bidirectional binding.

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm, 2 questions:

  1. is createDifficultyMenu() rebuilt on each menu creation or is it built only once and cached? For example, main menu -> game menu -> main menu (at this last step do we still have the same difficultyBox that was created the first time main menu was constructed?)

  2. can one property be bidirectionally bound to multiple objects?

If the answer to question 1 is "no, the menu items are cached rather than rebuilt" and if the answer to question 2 is also "no", then I can see a potential issue:

  • User opens main menu settings difficulty is bound to main menu box
  • User opens game menu settings difficult is bound to game menu box, losing the binding to main menu box. If we change the difficulty here, then at the next step does the difficulty in main menu also change?
  • User opens main menu again...

Are you able to test this scenario and see if it works as expected?

@jamesmosey jamesmosey Apr 10, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To answer question 1: The createDifficultyMenu() is rebuilt on each menu creation. The bound difficulty value persists as that setting lives outside of the UI controls (settings.kt).

As for question 2: As far as I'm aware the answer is no, but I have just tested it and these were the results:

  • Difficulty is set to 'nightmare' in main menu
  • New game is started, opening game menu shows that the difficulty is still set to 'nightmare'. Difficulty was then changed to 'easy'.
  • Upon leaving the game and returning back to the main menu the difficulty is still set to 'easy'.

Perhaps this is due to the two seperate bidirectional bindings from both the main menu and game menu to the same difficulty property in the settings, allowing the difficulty to persist between each menu type.

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sounds good, thanks for testing.

I've looked up documentation of Property. Turns out we can have multiple bidirectional bindings. There is a note about weak listeners and garbage collection, but I don't expect any issues if the menu is rebuilt every time.


difficultyBox.valueProperty().addListener { _, _, _ ->
switchMenuContentTo(EMPTY)

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you remind me what this does / why this is needed?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, in hindsight I should've left this in a comment.
Line 367 binds the difficulty option in the UI to the built in difficulty settings. So that when the difficulty is changed through the menu, it changes it in the settings.

The rest of the code adds a listener that reacts whenever the difficulty selection changes, hiding the dropdown menu and returning the user to the previous menu. Which was the alternative to adding a seperate back button somewhere else in the menu, increasing visual clutter. I tested adding a back button but it made more sense to me to keep it so when the difficulty is changed (or in the event of not changing, clicking the already-selected difficulty) returns the user back to the previous menu.

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Or we could just leave the user in the same view, i.e. the difficulty menu remains where it is and the user can select the difficult value multiple times. Will this cause an issue do you think?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's fine, it won't cause an issue - it was that way initially during testing. How would you suggest leaving the difficulty menu after the changes have been made? Perhaps when another menu item (options, extras..) is selected, it hides the difficulty choicebox? As opposed to having a back button or something of that sort.

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's keep it as is for now. We can always action this separately if users raise an issue.

}

val row = HBox(
25.0,
getUIFactoryService().newText(localizedStringProperty("menu.difficulty").concat(":")),
difficultyBox
)
row.alignment = Pos.CENTER

return MenuContent(row)
}

private fun createExtraMenu(): MenuBox {
log.debug("createExtraMenu()")

Expand Down
34 changes: 34 additions & 0 deletions fxgl/src/main/resources/fxglassets/ui/css/fxgl_dark.css
Original file line number Diff line number Diff line change
Expand Up @@ -309,6 +309,40 @@
}


/*
* Styling for the difficulty choice box
*/
.fxgl-difficulty-choice-box {
-fx-background-color: rgba(0, 0, 0, 0.35);
-fx-background-radius: 4;
-fx-background-insets: 0;
-fx-border-color: rgba(255, 255, 255, 0.25);
-fx-border-radius: 4;
-fx-font-size: 18px;
}

.fxgl-difficulty-choice-box:hover,
.fxgl-difficulty-choice-box:focused,
.fxgl-difficulty-choice-box:focused:hover,
.fxgl-difficulty-choice-box:armed {
-fx-background-color: rgba(0, 0, 0, 0.45);
-fx-background-radius: 4;
-fx-background-insets: 0;
-fx-border-color: rgba(255, 255, 255, 0.35);
}

.fxgl-difficulty-choice-box .label {
-fx-text-fill: white;
-fx-background-color: transparent;
}

.fxgl-difficulty-choice-box .context-menu {
-fx-background-color: rgba(20, 20, 20, 0.9);
}

.fxgl-difficulty-choice-box .menu-item:focused {
-fx-background-color: rgba(255, 255, 255, 0.15);
}


/*
Expand Down
Loading