PGPSCN Format
The PGPSCN (PopcorningGuineaPig Scene) format is a JSON-based declarative scene description system that allows developers to define UI layouts, 3D worlds, and interactive behaviors without writing C# code. This format works alongside existing C# scene classes, providing a clean separation between presentation and logic while maintaining full backward compatibility.
Overview
The PGPSCN format offers:
- Unified 2D/3D Support: Single format handles both traditional 2D UI and 3D world scenes
- Asset Management: Centralized asset referencing with ID-based system
- Command Integration: Seamless integration with the existing command pattern system
- Property Binding: Dynamic property binding to game state and settings
- Backward Compatible: Existing scenes continue to work without modification
- Extensible: Clean architecture for future enhancements including Rive animation support
Basic Structure
All PGPSCN files follow this basic JSON structure:
{
"version": "1.0",
"sceneType": "2d",
"metadata": {
"name": "MainMenuScene",
"description": "Simple main menu with buttons and logo"
},
"assets": { /* Asset definitions */ },
"world": { /* 3D world definition (optional) */ },
"cameras": [ /* Camera configurations */ ],
"ui_elements": [ /* UI element definitions */ ]
}
Scene Types
"2d": Traditional 2D UI scenes"3d": 3D world scenes with minimal UI"hybrid": Mixed 2D UI and 3D world elements
Getting Started
Your First PGPSCN Scene
Create a simple main menu by creating a file named MainMenuScene.pgpscn:
{
"version": "1.0",
"sceneType": "2d",
"metadata": {
"name": "MainMenuScene",
"description": "Simple main menu"
},
"assets": {
"textures": {
"button_bg": "assets/kenney/panel-002.png"
},
"sounds": {
"click": "assets/sounds/click.wav",
"hover": "assets/sounds/hover.wav"
}
},
"ui_elements": [
{
"id": "game_title",
"type": "Label",
"properties": {
"text": "@game_title",
"position": {
"type": "anchor",
"anchor": "TopCenter",
"offset": {"x": 0, "y": 50}
},
"fontSize": 32,
"color": "#FFFFFF"
}
},
{
"id": "start_button",
"type": "Button",
"properties": {
"text": "@start_game",
"position": {
"type": "anchor",
"anchor": "MiddleCenter",
"offset": {"x": 0, "y": -50}
},
"size": {"type": "absolute", "width": 200, "height": 50},
"texture": "button_bg"
},
"actions": [
{
"trigger": "onClick",
"command": {
"type": "scene_transition",
"target": "GameplayScene"
}
},
{
"trigger": "onHover",
"command": {
"type": "play_sound",
"asset": "hover"
}
}
]
}
]
}
Integrating with C# Scenes
Your C# scene class works alongside the PGPSCN file:
public class MainMenuScene : Scene
{
private Button startButton;
protected override void LoadScene()
{
// PGPSCN file handles UI creation automatically
// Access elements created by the scene file
if (SceneFileElements?.ContainsKey("start_button") == true)
{
startButton = (Button)SceneFileElements["start_button"];
}
}
// Your existing Update and Render methods work unchanged
public override void Update(double deltaTime) { /* existing code */ }
public override void Render(int width, int height) { /* existing code */ }
public override void Unload() { /* existing code */ }
}
UI Elements
Supported Element Types
All existing engine UI components are supported:
- Button: Interactive buttons with click actions
- Label: Text display with formatting
- Panel: Container panels with nine-slice backgrounds
- Dropdown: Selection dropdowns with callbacks
- Slider: Value input sliders with ranges
- Toggle: Boolean toggle controls
Button Example
{
"id": "settings_button",
"type": "Button",
"properties": {
"text": "@settings",
"position": {
"type": "percentage",
"x": 50.0,
"y": 70.0
},
"size": {
"type": "percentage",
"width": 25.0,
"height": 8.0
},
"normalColor": "#0066CC",
"hoverColor": "#0080FF",
"pressedColor": "#004499",
"fontSize": 18,
"textColor": "#FFFFFF",
"texture": "button_normal",
"nineSlice": {
"enabled": true,
"cornerSize": 10
}
},
"actions": [
{
"trigger": "onClick",
"command": {
"type": "scene_method",
"method": "OpenSettings"
}
}
]
}
Label Example
{
"id": "score_display",
"type": "Label",
"properties": {
"position": {
"type": "anchor",
"anchor": "TopRight",
"offset": {"x": -20, "y": 20}
},
"fontSize": 24,
"color": "#FFFFFF"
},
"bindings": [
{
"property": "text",
"source": "game.score",
"format": "Score: {0:N0}"
}
]
}
Slider Example
{
"id": "volume_slider",
"type": "Slider",
"properties": {
"position": {
"type": "anchor",
"anchor": "MiddleCenter",
"offset": {"x": 0, "y": 0}
},
"size": {"type": "absolute", "width": 300, "height": 30},
"minValue": 0.0,
"maxValue": 1.0
},
"bindings": [
{
"property": "value",
"source": "settings.masterVolume",
"type": "number"
}
],
"actions": [
{
"trigger": "onValueChanged",
"command": {
"type": "set_property",
"target": "settings.masterVolume",
"value": "{value}"
}
}
]
}
Positioning System
The PGPSCN format supports multiple positioning modes for responsive design:
Absolute Positioning
Fixed pixel coordinates:
"position": {
"type": "absolute",
"x": 100,
"y": 50
}
Percentage Positioning
Position relative to screen size:
"position": {
"type": "percentage",
"x": 50.0,
"y": 25.0
}
Anchor-Based Positioning
Position relative to screen anchors:
"position": {
"type": "anchor",
"anchor": "TopLeft",
"offset": {"x": 10, "y": 10}
}
Available anchors:
TopLeft,TopCenter,TopRightMiddleLeft,MiddleCenter,MiddleRightBottomLeft,BottomCenter,BottomRight
Relative Positioning
Position relative to other elements:
"position": {
"type": "relative",
"relativeTo": "main_title",
"relation": "below",
"offset": {"x": 0, "y": 20}
}
Size Definitions
Absolute Size
Fixed pixel dimensions:
"size": {
"type": "absolute",
"width": 200,
"height": 50
}
Percentage Size
Size relative to screen dimensions:
"size": {
"type": "percentage",
"width": 25.0,
"height": 8.0
}
Content-Fit Size
Size based on content with padding:
"size": {
"type": "content_fit",
"padding": {"x": 20, "y": 10}
}
Asset Management
Asset Registry
Define all assets in a centralized registry:
{
"assets": {
"textures": {
"panel_bg": "assets/kenney/panel-000.png",
"button_normal": "assets/kenney/panel-002.png",
"button_hover": "assets/kenney/panel-003.png"
},
"sounds": {
"click": "assets/sounds/click.wav",
"hover": "assets/sounds/hover.wav",
"success": "assets/sounds/success.ogg"
},
"fonts": {
"ui_font": "assets/fonts/ui_font.ttf",
"title_font": "assets/fonts/title_font.ttf"
}
}
}
Asset References
Reference assets by their ID throughout the scene:
{
"id": "styled_button",
"type": "Button",
"properties": {
"texture": "button_normal",
"font": "ui_font"
},
"actions": [
{
"trigger": "onClick",
"command": {
"type": "play_sound",
"asset": "click"
}
}
]
}
Asset Preloading
Control asset loading behavior:
{
"preload": {
"essential": [
"panel_bg",
"button_normal",
"click",
"ui_font"
],
"deferred": [
"character_portrait",
"logo_animation"
]
}
}
Actions and Commands
Command Types
The action system integrates with the existing command pattern:
Scene Method Commands
Call methods on your scene class:
{
"trigger": "onClick",
"command": {
"type": "scene_method",
"method": "OpenSettings",
"parameters": ["quick"]
}
}
Scene Transition Commands
Navigate between scenes:
{
"trigger": "onClick",
"command": {
"type": "scene_transition",
"target": "SettingsScene",
"transition": "fade",
"duration": 0.5
}
}
Static Method Commands
Call static methods on any class:
{
"trigger": "onClick",
"command": {
"type": "static_method",
"class": "GameManager",
"method": "SaveGame"
}
}
Built-in Commands
Use built-in commands for common operations:
{
"trigger": "onHover",
"command": {
"type": "play_sound",
"asset": "hover_sound",
"volume": 0.8
}
}
{
"trigger": "onClick",
"command": {
"type": "set_property",
"target": "settings.volume",
"value": 0.5
}
}
Action Triggers
Available triggers for UI events:
onClick- Mouse click or taponHover- Mouse hover enteronHoverExit- Mouse hover exitonValueChanged- Value change (sliders)onSelectionChanged- Selection change (dropdowns)onFocusGained- Input focus gainedonFocusLost- Input focus lost
Conditional Actions
Execute actions based on conditions:
{
"trigger": "onClick",
"condition": {
"type": "property_check",
"property": "game.canSave",
"operator": "equals",
"value": true
},
"command": {
"type": "static_method",
"class": "GameManager",
"method": "SaveGame"
},
"else_command": {
"type": "play_sound",
"asset": "error_sound"
}
}
Chained Actions
Execute multiple actions in sequence:
{
"trigger": "onClick",
"commands": [
{
"type": "play_sound",
"asset": "click"
},
{
"type": "scene_method",
"method": "ShowConfirmDialog"
},
{
"type": "scene_transition",
"target": "GameplayScene",
"delay": 1.0
}
]
}
Property Binding
Basic Binding
Bind UI properties to game state:
{
"bindings": [
{
"property": "text",
"source": "player.name",
"format": "Welcome, {0}!"
},
{
"property": "visible",
"source": "game.debugMode",
"type": "boolean"
}
]
}
Advanced Binding
Transform values with mappings:
{
"bindings": [
{
"property": "color",
"source": "player.healthPercentage",
"mapping": {
"type": "color_gradient",
"stops": [
{"value": 0.0, "color": "#FF0000"},
{"value": 0.5, "color": "#FFFF00"},
{"value": 1.0, "color": "#00FF00"}
]
}
}
]
}
Binding Sources
Available binding sources:
game.*- Game state propertiesplayer.*- Player object propertiessettings.*- Settings service valuesscene.*- Scene object propertiesui.{elementId}.*- Other UI element propertieslocalization.*- Localization service values
Value Transformations
Transform bound values:
{
"property": "text",
"source": "game.score",
"transform": {
"type": "format",
"format": "Score: {0:N0}",
"fallback": "Score: 0"
}
}
{
"property": "size.width",
"source": "player.health",
"transform": {
"type": "scale",
"inputRange": {"min": 0, "max": 100},
"outputRange": {"min": 0, "max": 200}
}
}
3D Scene Support
Basic 3D Scene
Define 3D scenes with world geometry:
{
"version": "1.0",
"sceneType": "3d",
"world": {
"type": "quake_map",
"mapFile": "assets/maps/dm_warehouse.bsp",
"spawn_points": [
{
"id": "player_spawn",
"position": {"x": 256, "y": 256, "z": 32},
"rotation": {"x": 0, "y": 0, "z": 0},
"type": "player"
}
]
},
"cameras": [
{
"id": "main_camera",
"type": "perspective",
"isPrimary": true,
"properties": {
"fov": 75,
"nearPlane": 0.1,
"farPlane": 1000.0
}
}
]
}
Hybrid Scene with 3D and UI
Combine 3D worlds with 2D UI overlays:
{
"version": "1.0",
"sceneType": "hybrid",
"world": {
"type": "quake_map",
"mapFile": "dm_warehouse.bsp"
},
"cameras": [
{
"id": "fps_camera",
"type": "perspective",
"isPrimary": true,
"properties": {
"fov": 75,
"nearPlane": 0.1,
"farPlane": 1000.0
},
"bindings": [
{
"property": "position",
"source": "player.position"
},
{
"property": "target",
"source": "player.lookTarget"
}
]
}
],
"ui_elements": [
{
"id": "health_display",
"type": "Label",
"renderSpace": "screen_2d",
"properties": {
"position": {
"type": "anchor",
"anchor": "TopLeft",
"offset": {"x": 20, "y": 20}
},
"fontSize": 20,
"color": "#00FF00"
},
"bindings": [
{
"property": "text",
"source": "player.health",
"format": "Health: {0}"
}
]
}
]
}
World-Space UI Elements
Position UI elements in 3D world space:
{
"id": "floating_damage",
"type": "Label",
"renderSpace": "world_3d",
"properties": {
"text": "-25",
"position": {
"type": "world_position",
"x": 256, "y": 256, "z": 80
},
"fontSize": 16,
"color": "#FF0000",
"billboardMode": "camera_facing"
},
"animations": [
{
"trigger": "onCreate",
"type": "float_up_and_fade",
"duration": 2.0
}
]
}
Localization Support
Use localization keys with the @ prefix:
{
"id": "welcome_message",
"type": "Label",
"properties": {
"text": "@welcome_message",
"position": {"type": "anchor", "anchor": "TopCenter"},
"fontSize": 24
}
}
The engine's localization service will automatically resolve @welcome_message to the appropriate localized text.
Migration Guide
From Manual C# Scenes
Before (C# only):
public class MainMenuScene : Scene
{
private Button startButton;
private Button settingsButton;
public override void Load()
{
startButton = new Button("Start Game", new Vector2(400, 300), new Vector2(200, 50), Color.Blue);
settingsButton = new Button("Settings", new Vector2(400, 370), new Vector2(200, 50), Color.Gray);
startButton.SetOnClick(() => SceneManager.LoadScene("GameplayScene"));
settingsButton.SetOnClick(() => OpenSettings());
}
}
After (PGPSCN + C#):
MainMenuScene.pgpscn:
{
"version": "1.0",
"sceneType": "2d",
"ui_elements": [
{
"id": "start_button",
"type": "Button",
"properties": {
"text": "@start_game",
"position": {"type": "percentage", "x": 50, "y": 50},
"size": {"type": "absolute", "width": 200, "height": 50}
},
"actions": [
{
"trigger": "onClick",
"command": {"type": "scene_transition", "target": "GameplayScene"}
}
]
},
{
"id": "settings_button",
"type": "Button",
"properties": {
"text": "@settings",
"position": {"type": "percentage", "x": 50, "y": 60},
"size": {"type": "absolute", "width": 200, "height": 50}
},
"actions": [
{
"trigger": "onClick",
"command": {"type": "scene_method", "method": "OpenSettings"}
}
]
}
]
}
MainMenuScene.cs:
public class MainMenuScene : Scene
{
protected override void LoadScene()
{
// PGPSCN handles UI creation automatically
// Only custom logic needed now
}
public void OpenSettings()
{
// Your custom settings logic
}
}
Best Practices
Organization
- Use descriptive IDs:
health_bar,inventory_button,damage_text - Group related elements: Use consistent naming patterns
- Separate concerns: Keep presentation in PGPSCN, logic in C#
- Document complex bindings: Use clear property paths
Performance
- Minimize binding updates: Use appropriate binding types
- Preload essential assets: Use the preload system
- Use appropriate positioning: Percentage for responsive, absolute for fixed
- Batch related operations: Group actions when possible
Maintainability
- Use version control: PGPSCN files are text-based and diff-friendly
- Consistent formatting: Use proper JSON formatting
- Validate schemas: Ensure proper structure
- Test iteratively: Quick iteration without recompilation
Error Handling
Common issues and solutions:
Invalid JSON
Error: Invalid JSON syntax at line 15
Solution: Validate JSON structure, check for missing commas or brackets
Missing Assets
Error: Asset 'missing_texture' not found
Solution: Ensure asset path exists and is registered in assets section
Invalid Property Binding
Error: Cannot bind to 'invalid.property.path'
Solution: Verify the binding source exists and is accessible
Unknown Element Type
Error: Element type 'CustomElement' not supported
Solution: Use supported element types or extend the factory system
Advanced Features
Future Rive Support
The PGPSCN format is designed to support Rive animations when available:
{
"id": "animated_logo",
"type": "RiveComponent",
"properties": {
"riveFilePath": "logo_animation.riv",
"artboardName": "MainArtboard",
"stateMachineName": "LogoStateMachine",
"autoPlay": true
},
"actions": [
{
"trigger": "onRiveEvent",
"eventName": "LogoClicked",
"command": {
"type": "scene_transition",
"target": "MainMenuScene"
}
}
]
}
Custom Element Factories
Extend the system with custom UI elements:
public class CustomUIElementFactory : UIElementFactory
{
public override bool CanHandle(string elementType)
{
return elementType == "CustomWidget";
}
public override object CreateElement(UIElementDefinition definition)
{
return new CustomWidget(definition.Properties);
}
}
The PGPSCN format provides a powerful, flexible foundation for scene definition that grows with your project needs while maintaining simplicity and backward compatibility.
Further Reading
- 3D Scenes - Detailed 3D scene capabilities
- UI Components - Available UI element types
- Game Architecture - Core engine integration
- Asset Registry - Asset management system