Game Architecture
The Popcorning Guinea Pig engine follows a clean, modular architecture designed to provide flexibility while maintaining simplicity. Understanding the core architecture will help you build more effective games and extend the engine when needed.
Overview
The engine is built around several key architectural principles:
- Scene-Based Organization: Games are composed of scenes that manage their own state and lifecycle
- Service-Oriented Design: Core functionality is provided through injectable services
- Component Composition: UI elements and game objects are built through composition
- Event-Driven Communication: Loose coupling through event systems and callbacks
Core Architecture Diagram
┌─────────────────────────────────────────────────────────────┐
│ Game Class │
│ ┌─────────────────┐ ┌──────────────────────────────────┐│
│ │ Initialization │ │ Services ││
│ │ - Window │ │ ┌─────────────────────────────┐ ││
│ │ - Raylib │ │ │ ISoundPlayer │ ││
│ │ - Services │ │ │ ISettingsService │ ││
│ └─────────────────┘ │ │ ILocalizationService │ ││
│ │ │ IDiscordService │ ││
│ ┌─────────────────┐ │ │ IAssetRegistry │ ││
│ │ Game Loop │ │ │ IInputService │ ││
│ │ - Update │ │ └─────────────────────────────┘ ││
│ │ - Render │ └──────────────────────────────────┘│
│ │ - Input │ │
│ └─────────────────┘ ┌──────────────────────────────────┐│
│ │ Scene Manager ││
│ │ ┌─────────────────────────────┐ ││
│ │ │ Current Scene │ ││
│ │ │ Scene Queue │ ││
│ │ │ Transition System │ ││
│ │ └─────────────────────────────┘ ││
│ └──────────────────────────────────┘│
└─────────────────────────────────────────────────────────────┘
Game Class
The Game class is the entry point and orchestrator of your application. It manages the overall lifecycle and coordinates between systems.
Core Responsibilities
public abstract class Game
{
// Initialization and setup
public void Init(int width, int height, int fps = 60, string title = "Game Title")
// Resource loading (override in your game)
public virtual void Load()
// Game loop methods (override in your game)
public virtual void Update(double deltaTime)
public abstract void Render()
// Main game loop execution
public void RunGame()
// Cleanup and shutdown
protected virtual void CleanupResources()
}
Implementation Pattern
public class MyGame : Game
{
private SceneManager sceneManager;
private ISoundPlayer soundPlayer;
private ISettingsService settingsService;
public override void Load()
{
// Initialize services
soundPlayer = new SoundPlayer();
settingsService = new SettingsService();
// Initialize scene management
sceneManager = new SceneManager();
// Load initial scene
var mainMenu = new MainMenuScene(sceneManager, soundPlayer);
sceneManager.ChangeScene(mainMenu);
}
public override void Update(double deltaTime)
{
// Update scene system
sceneManager.Update(deltaTime);
}
public override void Render()
{
// Delegate rendering to scene manager
sceneManager.Render();
}
protected override void CleanupResources()
{
soundPlayer?.Dispose();
sceneManager?.UnloadAllScenes();
}
}
Scene System Architecture
Scenes are self-contained units of gameplay or UI that manage their own state and resources.
Scene Lifecycle
public abstract class Scene
{
// Called when scene becomes active
public virtual void Load() { }
// Called every frame while active
public virtual void Update(double deltaTime) { }
// Called every frame for rendering
public virtual void Render(int width, int height) { }
// Called when scene is deactivated
public virtual void Unload() { }
}
Scene Types
Base Scene (2D)
- Handles 2D UI elements and rendering
- Manages user input and interaction
- Suitable for menus, 2D games, UI screens
public class MainMenuScene : Scene
{
private List<Button> buttons = new();
private Panel backgroundPanel;
public override void Load()
{
// Create UI elements
var playButton = new Button(/*parameters*/);
buttons.Add(playButton);
}
public override void Update(double deltaTime)
{
foreach (var button in buttons)
button.Update();
}
public override void Render(int width, int height)
{
Raylib.ClearBackground(Color.DARKBLUE);
backgroundPanel?.Draw();
foreach (var button in buttons)
button.Draw();
}
}
3D Scene
- Extends base Scene with 3D capabilities
- Manages 3D cameras and models
- Handles 3D UI elements and interactions
public class GameplayScene : Scene3D
{
public override void Load()
{
// Set up 3D camera
var camera = new Camera3D
{
position = new Vector3(0, 10, 10),
target = new Vector3(0, 0, 0),
up = new Vector3(0, 1, 0),
fovy = 45.0f,
projection = CameraProjection.CAMERA_PERSPECTIVE
};
SetMainCamera(camera);
// Load 3D models
var worldModel = Raylib.LoadModel("assets/world.obj");
SetWorldModel(worldModel);
}
public override void Render(int width, int height)
{
// 3D rendering is handled automatically
// You can add custom 3D rendering here
base.Render(width, height);
}
}
Service Architecture
Services provide reusable functionality across the application and implement dependency injection patterns.
Service Interface Pattern
public interface ISoundPlayer
{
Task PlaySound(string soundPath, float volume = 1.0f);
Task PlayMenuClick();
Task StartGameBackgroundMusic();
Task StopGameBackgroundMusic();
void Dispose();
}
public class SoundPlayer : ISoundPlayer
{
// Implementation details...
public async Task PlaySound(string soundPath, float volume = 1.0f)
{
// Play sound using Raylib
}
// Other interface implementations...
}
Service Injection
public class GameScene : Scene
{
private readonly ISoundPlayer soundPlayer;
private readonly ISettingsService settingsService;
public GameScene(ISoundPlayer soundPlayer, ISettingsService settingsService)
{
this.soundPlayer = soundPlayer;
this.settingsService = settingsService;
}
private async void OnButtonClick()
{
await soundPlayer.PlayMenuClick();
// Handle button logic
}
}
Component Composition
UI elements and game objects use composition for flexibility and reusability.
UI Component Hierarchy
// Base UI element interface
public interface IUIElement
{
Vector2 GetWorldPosition();
Vector2 GetLocalPosition();
void Update();
void Draw();
// Hierarchical relationships
void AddChild(IUIElement child);
void RemoveChild(IUIElement child);
void RemoveFromParent();
}
// Base implementation with hierarchy support
public abstract class UIElementBase : IUIElement
{
protected IUIElement parent;
protected List<IUIElement> children = new();
protected Vector2 localPosition;
public virtual Vector2 GetWorldPosition()
{
if (parent != null)
return parent.GetWorldPosition() + localPosition;
return localPosition;
}
public virtual void Update()
{
UpdateSelf();
foreach (var child in children)
child.Update();
}
public virtual void Draw()
{
DrawSelf();
foreach (var child in children)
child.Draw();
}
protected virtual void UpdateSelf() { }
protected abstract void DrawSelf();
}
Composition Example
public class GameUI : UIElementBase
{
public GameUI()
{
// Create health bar
var healthBar = new Panel(/*parameters*/);
var healthFill = new Panel(/*parameters*/);
healthBar.AddChild(healthFill);
AddChild(healthBar);
// Create inventory panel
var inventory = new GridContainer(/*parameters*/);
for (int i = 0; i < 10; i++)
{
var slot = new Button(/*parameters*/);
inventory.AddChild(slot);
}
AddChild(inventory);
}
protected override void DrawSelf()
{
// Draw background or container-specific elements
}
}
Event System
The engine uses callbacks and events for loose coupling between components.
Event Patterns
Button Events
var button = new Button(position, size, "Click Me");
button.SetOnClick(() => {
// Handle click
Console.WriteLine("Button clicked!");
});
button.SetOnHover(() => {
// Handle hover
button.SetColors(Color.LIGHTBLUE, Color.BLUE, Color.DARKBLUE);
});
Custom Events
public class GameEvents
{
public static event Action<int> OnScoreChanged;
public static event Action<Player> OnPlayerDeath;
public static void TriggerScoreChange(int newScore)
{
OnScoreChanged?.Invoke(newScore);
}
}
// Usage
GameEvents.OnScoreChanged += (score) => {
scoreLabel.SetText($"Score: {score}");
};
Resource Management
The engine provides structured resource management to prevent memory leaks and optimize performance.
Scene Resource Management
public class ResourceManagedScene : Scene
{
private List<Texture2D> textures = new();
private List<Sound> sounds = new();
private List<IDisposable> disposables = new();
public override void Load()
{
// Load resources and track them
var backgroundTexture = Raylib.LoadTexture("background.png");
textures.Add(backgroundTexture);
var clickSound = Raylib.LoadSound("click.wav");
sounds.Add(clickSound);
var soundPlayer = new SoundPlayer();
disposables.Add(soundPlayer);
}
public override void Unload()
{
// Clean up all resources
foreach (var texture in textures)
Raylib.UnloadTexture(texture);
textures.Clear();
foreach (var sound in sounds)
Raylib.UnloadSound(sound);
sounds.Clear();
foreach (var disposable in disposables)
disposable.Dispose();
disposables.Clear();
}
}
Asset Registry Pattern
public class AssetRegistry : IAssetRegistry
{
private Dictionary<string, Texture2D> textures = new();
private Dictionary<string, Font> fonts = new();
public void RegisterAsset(string id, string path, AssetType type)
{
switch (type)
{
case AssetType.Texture:
if (!textures.ContainsKey(id))
textures[id] = Raylib.LoadTexture(path);
break;
case AssetType.Font:
if (!fonts.ContainsKey(id))
fonts[id] = Raylib.LoadFont(path);
break;
}
}
public T GetAsset<T>(string id) where T : struct
{
if (typeof(T) == typeof(Texture2D) && textures.ContainsKey(id))
return (T)(object)textures[id];
// Handle other types...
return default(T);
}
}
Threading Considerations
The engine is designed to run on the main thread, but provides patterns for background processing.
Async Service Operations
public class NetworkService
{
public async Task<PlayerData> LoadPlayerDataAsync(string playerId)
{
// Perform network operation on background thread
var data = await httpClient.GetStringAsync($"/players/{playerId}");
// Return to main thread for UI updates
return JsonSerializer.Deserialize<PlayerData>(data);
}
}
// Usage in scene
public class LoadingScene : Scene
{
private async void LoadPlayerData()
{
try
{
var playerData = await networkService.LoadPlayerDataAsync(playerId);
// Update UI on main thread
playerNameLabel.SetText(playerData.Name);
playerLevelLabel.SetText($"Level {playerData.Level}");
}
catch (Exception ex)
{
errorLabel.SetText($"Failed to load: {ex.Message}");
}
}
}
Performance Considerations
Scene Update Optimization
public class OptimizedScene : Scene
{
private List<IUpdatable> activeElements = new();
private List<IUpdatable> staticElements = new();
private bool needsStaticUpdate = false;
public override void Update(double deltaTime)
{
// Always update active elements
foreach (var element in activeElements)
element.Update(deltaTime);
// Update static elements only when needed
if (needsStaticUpdate)
{
foreach (var element in staticElements)
element.Update(deltaTime);
needsStaticUpdate = false;
}
}
public void MarkStaticNeedsUpdate()
{
needsStaticUpdate = true;
}
}
Memory Management
public class MemoryEfficientScene : Scene
{
private ObjectPool<Particle> particlePool = new();
public void CreateExplosion(Vector2 position)
{
// Reuse particles instead of creating new ones
for (int i = 0; i < 10; i++)
{
var particle = particlePool.Get();
particle.Initialize(position);
// Use particle...
}
}
public void ReturnParticle(Particle particle)
{
particlePool.Return(particle);
}
}
Best Practices
1. Scene Organization
- Keep scenes focused on specific functionality
- Use composition over inheritance for complex UI
- Implement proper resource cleanup in Unload()
2. Service Design
- Use interfaces for all services
- Implement dependency injection patterns
- Keep services stateless when possible
3. Performance
- Cache frequently accessed objects
- Use object pooling for temporary objects
- Minimize allocations in Update loops
4. Architecture
- Follow single responsibility principle
- Use events for loose coupling
- Implement proper error handling
Architecture Extensions
Custom Scene Types
public abstract class NetworkScene : Scene
{
protected NetworkService networkService;
protected NetworkScene(NetworkService networkService)
{
this.networkService = networkService;
}
protected virtual async Task OnNetworkEvent(NetworkMessage message)
{
// Handle network events
}
}
Custom Services
public interface IAnalyticsService
{
void TrackEvent(string eventName, Dictionary<string, object> parameters);
Task FlushEventsAsync();
}
public class AnalyticsService : IAnalyticsService
{
// Implementation...
}
Common Patterns
Singleton Pattern (Use Sparingly)
public class GameState
{
private static GameState instance;
public static GameState Instance => instance ??= new GameState();
public int Score { get; set; }
public Player CurrentPlayer { get; set; }
}
Observer Pattern
public class HealthSystem
{
public event Action<int> OnHealthChanged;
private int health;
public int Health
{
get => health;
set
{
if (health != value)
{
health = value;
OnHealthChanged?.Invoke(health);
}
}
}
}
Further Reading
- Scene Management - Detailed scene system documentation
- Services - Complete service implementation guide
- UI System - UI architecture and components
- Steam Integration - Platform service integration
This architecture provides a solid foundation for building games while maintaining flexibility for different project needs. The modular design allows you to use only the parts you need while providing room for extension and customization.