Main Menu Example
This example demonstrates creating a traditional main menu using C# code and the PopcorningGuineaPig Engine's UI system. Unlike the PGPSCN approach, this example shows manual UI creation with programmatic positioning, styling, and event handling.
Overview
The Main Menu example is located in examples/main-menu/ and showcases:
- Traditional C# UI Creation: Manual instantiation and configuration of UI elements
- Responsive Design: Percentage-based positioning that adapts to screen resolution
- Multi-Language Support: Unicode font support for Chinese, Japanese, and Korean
- Settings Integration: Persistent settings with resolution and language options
- Scene Management: Navigation between splash screens and main menu
- Input System: Command pattern for keyboard shortcuts (ESC to exit)
Key Features
1. Scene Progression
The game starts with splash screens before showing the main menu:
public MyGame() : base()
{
// Create scenes
_engineSplashScreenScene = new EngineSplashScreenScene();
_splashScreenScene = new SplashScreenScene();
_mainMenu = new MainMenuScene();
// Queue scenes with timing
this.SceneManager.QueueScene(_engineSplashScreenScene, 0f); // Start immediately
this.SceneManager.QueueScene(_splashScreenScene, 3.0f); // Show after 3 seconds
this.SceneManager.QueueScene(_mainMenu, 3.0f); // Show after another 3 seconds
}
2. Responsive UI Positioning
UI elements use percentage-based positioning for responsive design:
private const float COPYRIGHT_X_PERCENT = 0.02f; // 2% from left edge
private const float COPYRIGHT_Y_PERCENT = 0.02f; // 2% from top edge
private const float BUTTON_WIDTH_PERCENT = 0.125f; // 12.5% of screen width
private const float BUTTON_HEIGHT_PERCENT = 0.067f; // 6.7% of screen height
private void PositionUIElements()
{
int screenWidth = Raylib.GetScreenWidth();
int screenHeight = Raylib.GetScreenHeight();
Vector2 copyrightPos = new Vector2(
screenWidth * COPYRIGHT_X_PERCENT,
screenHeight * COPYRIGHT_Y_PERCENT
);
Vector2 buttonSize = new Vector2(
screenWidth * BUTTON_WIDTH_PERCENT,
screenHeight * BUTTON_HEIGHT_PERCENT
);
}
3. Multi-Language Font Management
Centralized font management supports multiple languages:
private void ConfigureCentralizedFonts()
{
var fontPaths = new Dictionary<string, string>
{
{"en", "assets/Roboto-Regular.ttf"},
{"ja", "assets/NotoSansJP-VariableFont_wght.ttf"},
{"ko", "assets/NotoSansKR-VariableFont_wght.ttf"},
{"zh", "assets/NotoSansSC-VariableFont_wght.ttf"},
{"emoji", "assets/NotoColorEmoji-Regular.ttf"}
};
Game.Instance.Localization.SetFontPaths(fontPaths);
}
4. Unicode Font Loading
Advanced font loading with Unicode support for CJK characters:
private Font LoadFontWithUnicodeSupport(string path)
{
// Define character ranges: ASCII + Chinese + Japanese + Korean
int[] codepoints = new int[/* calculated size */];
int index = 0;
// ASCII range (0x20-0x7E)
for (int i = 0x20; i <= 0x7E; i++)
codepoints[index++] = i;
// Chinese range (0x4E00-0x9FFF) - Common CJK
for (int i = 0x4E00; i <= 0x9FFF; i++)
codepoints[index++] = i;
// Japanese Hiragana (0x3040-0x309F)
for (int i = 0x3040; i <= 0x309F; i++)
codepoints[index++] = i;
// Korean Hangul (0xAC00-0xD7AF)
for (int i = 0xAC00; i <= 0xD7AF; i++)
codepoints[index++] = i;
// Load font with Unicode ranges
var font = Raylib.LoadFontEx(pathPtr, fontSize, codepointsPtr, codepoints.Length);
return font;
}
5. Dynamic Language Switching
Runtime language changes with immediate UI updates:
private void OnLanguageChanged(string selectedLanguage)
{
// Set the language in the localization system
Game.Instance.Localization.SetLanguage(selectedLanguage);
// Force refresh of all localized content with proper fonts
RefreshAllLocalizedContent();
}
private void RefreshAllLocalizedContent()
{
// Get translated strings for the new language
string titleText = Game.Instance.Localization.GetString("main_menu_title");
string settingsText = Game.Instance.Localization.GetString("settings_button");
string exitText = Game.Instance.Localization.GetString("exit_button");
// Update UI elements with new text and appropriate fonts
copyright.SetText(titleText);
settingsButton.SetText(settingsText);
exitButton.SetText(exitText);
// Update fonts based on language
string fontPath = GetFontPathForLanguage(currentLanguage);
UpdateElementFont("copyright", fontPath);
UpdateElementFont("settings_button", fontPath);
UpdateElementFont("exit_button", fontPath);
}
6. Command Pattern Input System
Keyboard shortcuts using the command pattern:
protected override void ConfigureInputBindings()
{
// Add ESC key binding to exit the game
SceneInputContext.AddBinding(new InputBinding(
"exit_game",
InputKey.Keyboard(KeyboardKey.Escape),
new LambdaCommand(() => ExitGame(), "exit_game"),
InputTriggerType.Pressed
));
}
7. Settings Management
Persistent settings with resolution and display mode configuration:
private void LoadSettings()
{
_settingsService = new SettingsService();
string settingsPath = Path.Combine(Directory.GetCurrentDirectory(), "settings.json");
if (File.Exists(settingsPath))
{
_settings = _settingsService.LoadSettings(settingsPath);
}
else
{
_settings = new Settings(); // Use defaults
}
}
UI Elements
Copyright Label
Displays the game title with localization support:
copyright = new Label(
Game.Instance.Localization.GetString("main_menu_title"),
copyrightPos,
20,
Color.White
);
Settings Button
Opens the settings scene:
settingsButton = new Button(
Game.Instance.Localization.GetString("settings_button"),
settingsButtonPos,
buttonSize,
Color.Blue
);
settingsButton.SetOnClick(OpenSettings);
Exit Button
Exits the application:
exitButton = new Button(
Game.Instance.Localization.GetString("exit_button"),
exitButtonPos,
buttonSize,
Color.Red
);
exitButton.SetOnClick(() => ExitGame());
Language Dropdown
Runtime language selection:
var languages = Game.Instance.Localization.AvailableLanguages.ToList();
languageDropdown = new Dropdown<string>(
languageDropdownPos,
languageDropdownSize,
languages,
0
);
languageDropdown.SetOnSelectionChanged((selectedLanguage) =>
OnLanguageChanged(selectedLanguage)
);
Running the Example
Prerequisites
- .NET 10.0 or later
- PopcorningGuineaPig.Engine reference
- Raylib-cs package
Build and Run
cd examples/main-menu/MainMenu
dotnet build
dotnet run
Project Structure
MainMenu/
├── MainMenu.csproj # Project file
├── MyGame.cs # Game initialization and scene management
├── Program.cs # Entry point
├── settings.json # Persistent game settings
├── assets/ # Assets directory
│ ├── Roboto-Regular.ttf # Western languages font
│ ├── NotoSansSC-VariableFont_wght.ttf # Chinese font
│ ├── NotoSansJP-VariableFont_wght.ttf # Japanese font
│ ├── NotoSansKR-VariableFont_wght.ttf # Korean font
│ ├── NotoColorEmoji-Regular.ttf # Emoji font
│ └── localization.csv # Translation data
└── scenes/
├── EngineSplashScreenScene.cs # Engine splash screen
├── SplashScreenScene.cs # Game splash screen
├── MainMenuScene.cs # Main menu implementation
└── SettingsScene.cs # Settings menu
What You'll Learn
- Manual UI Creation: Creating and positioning UI elements programmatically
- Responsive Design Patterns: Percentage-based positioning for multiple resolutions
- Unicode Font Handling: Loading and managing fonts for international text
- Localization Integration: Dynamic language switching with immediate UI updates
- Command Pattern Input: Keyboard shortcuts using the engine's input system
- Scene Management: Transitioning between splash screens and main menu
- Settings Persistence: Loading and saving game configuration
- Resolution Adaptation: Handling window resize events
Advanced Techniques
Resolution Change Handling
The scene automatically adjusts UI elements when resolution changes:
public override void Render(int width, int height)
{
// Check if resolution has changed
if (width != Raylib.GetScreenWidth() || height != Raylib.GetScreenHeight())
{
PositionUIElements();
}
copyright.Draw();
settingsButton.Draw();
exitButton.Draw();
languageDropdown.Draw();
}
Font Path Selection
Dynamic font selection based on language:
private string GetFontPathForLanguage(string languageCode)
{
return languageCode switch
{
"zh" => "assets/NotoSansSC-VariableFont_wght.ttf",
"ko" => "assets/NotoSansKR-VariableFont_wght.ttf",
"ja" => "assets/NotoSansJP-VariableFont_wght.ttf",
_ => "assets/Roboto-Regular.ttf" // Default to English font
};
}
Error Handling
Robust error handling for font loading and UI updates:
public override void Update(double deltaTime)
{
try
{
settingsButton.Update();
exitButton.Update();
languageDropdown.Update();
}
catch (Exception ex)
{
// Handle exceptions gracefully
Console.WriteLine($"Error in MainMenuScene.Update: {ex.Message}");
}
}
Comparison with PGPSCN Approach
Traditional C# Approach (This Example)
- More Control: Direct access to all UI element properties
- Type Safety: Compile-time checking of UI element interactions
- Debugging: Full debugging capabilities with breakpoints
- Complexity: More code required for basic UI layouts
- Compilation: Changes require recompilation
PGPSCN Approach
- Declarative: UI defined in JSON format
- Rapid Iteration: Changes without recompilation
- Designer Friendly: Non-programmers can modify UI
- Less Control: Limited to supported properties and actions
- Runtime Errors: JSON parsing errors only caught at runtime
Related Documentation
- PGPSCN Example - Declarative alternative to this approach
- UI Components - Available UI element types
- Input System - Command pattern input handling
- Localization - Multi-language support
- Settings Management - Configuration persistence
- Scene Management - Scene lifecycle
Next Steps
After exploring this example, consider:
- Add New UI Elements: Buttons, sliders, or custom components
- Implement Custom Fonts: Create your own font loading system
- Enhance Localization: Add more languages or dynamic text
- Create Custom Scenes: Build additional menu screens
- Compare Approaches: Try implementing the same UI using PGPSCN format
- Performance Optimization: Profile and optimize UI rendering
This example demonstrates the traditional, code-first approach to UI development with the PopcorningGuineaPig Engine, providing maximum flexibility and control over the user interface.