libstormgames

A Python library to make creating audio games easier.

Overview

libstormgames provides a comprehensive set of tools for developing accessible games with audio-first design. It handles common game development tasks including:

  • Sound and music playback with positional audio and 3D spatialization
  • Text-to-speech integration
  • Configuration management
  • Score tracking and high score tables
  • Input handling and keyboard controls
  • Menu systems and text display
  • GUI initialization

Installation

Requirements

  • pygame>=2.0.0
  • pyperclip>=1.8.0
  • requests>=2.25.0
  • pyxdg>=0.27
  • setproctitle>=1.2.0
  • numpy>=1.19.0
  • wxpython

Speech Providers (one required)

  • Linux/Unix: python-speechd or accessible-output2>=0.14
  • Windows/macOS: accessible-output2>=0.14

Install from source

If you are on Linux, check your package manager first to see if the packages in requirements.txt are available.

git clone https://git.stormux.org/storm/libstormgames
cd libstormgames
pip install -e .

Getting Started

You can use libstormgames in two ways: the traditional function-based approach or the new class-based approach.

Traditional Function-Based Approach

#!/usr/bin/env python3
import libstormgames as sg

def main():
    # Initialize the game
    sounds = sg.initialize_gui("My First Audio Game")
    
    # Welcome message
    sg.speak("Welcome to My First Audio Game!")
    
    # Create a scoreboard
    scoreboard = sg.Scoreboard()
    
    # Main game loop
    def play_game():
        sg.speak("Game started!")
        scoreboard.increase_score(10)
        sg.speak(f"Your score is {scoreboard.get_score()}")
        scoreboard.add_high_score()
        return "menu"  # Return to menu after game ends
    
    # Define menu options
    while True:
        choice = sg.game_menu(sounds, play_game)
        
        # The game_menu function already includes standard options like
        # high scores, instructions, credits, and exit

if __name__ == "__main__":
    main()

Modern Class-Based Approach

#!/usr/bin/env python3
import libstormgames as sg

def main():
    # Create and initialize a game
    game = sg.Game("My First Audio Game").initialize()
    
    # Welcome message
    game.speak("Welcome to My First Audio Game!")
    
    # Main game loop
    def play_game():
        game.speak("Game started!")
        game.scoreboard.increase_score(10)
        game.speak(f"Your score is {game.scoreboard.get_score()}")
        game.scoreboard.add_high_score()
        return "menu"
    
    # Define menu options
    while True:
        choice = sg.game_menu(game.sound.sounds, play_game)
        
        # The game_menu function already includes standard options like
        # high scores, instructions, credits, and exit

if __name__ == "__main__":
    main()

Library Structure

The library is organized into modules, each with a specific focus:

  • config: Configuration management
  • services: Core services that replace global variables
  • sound: Sound and music playback with 3D positional audio
  • speech: Text-to-speech functionality
  • scoreboard: High score tracking
  • input: Input handling and dialogs
  • display: Text display and GUI functionality
  • menu: Menu systems
  • utils: Utility functions and Game class

Core Classes

Game

The Game class provides a central way to manage all game systems:

# Create and initialize a game
game = sg.Game("My Game").initialize()

# Use fluent API for chaining commands
game.speak("Hello").play_bgm("music/theme")

# Access components directly
game.scoreboard.increase_score(10)
game.sound.play_random("explosion")

# Display text
game.display_text(["Line 1", "Line 2"])

# Clean exit
game.exit()

The initialization process sets up proper configuration paths:

  • Creates game-specific directories if they don't exist
  • Ensures all services have access to correct paths
  • Connects all components for seamless operation

Services

The library includes several service classes that replace global variables:

# Volume service manages all volume settings
volume = sg.VolumeService.get_instance()
volume.adjust_master_volume(0.1)

# Path service manages file paths
paths = sg.PathService.get_instance()
print(paths.game_path)

# Config service manages configuration
config = sg.ConfigService.get_instance()

Config

Handles configuration file management with local and global settings.

# Create a configuration manager
config = sg.Config("My Game")

# Access the local configuration
config.local_config.add_section("settings")
config.local_config.set("settings", "difficulty", "easy")
config.write_local_config()

# Read settings
config.read_local_config()
difficulty = config.local_config.get("settings", "difficulty")

Configuration files are automatically stored in standard system locations:

  • Linux/Unix: ~/.config/storm-games/
  • Windows: %APPDATA%\storm-games\
  • macOS: ~/Library/Application Support/storm-games/

Each game has its own subdirectory based on the game name (lowercase with hyphens), for example, Wicked Quest becomes wicked-quest.

Create a sound manager

sound_system = sg.Sound("sounds/")

Get the dictionary of loaded sounds

sounds = sound_system.sounds

Play a sound

channel = sound_system.play_sound("explosion")

Play a looping sound

channel = sound_system.play_sound("ambient", loop=True)

Play a sound with 3D positional audio (horizontal and vertical positioning)

channel = sound_system.obj_play("footsteps", playerPos=5, objPos=10, playerY=0, objY=5, loop=True)

Update sound position as player or object moves

channel = sound_system.obj_update(channel, 6, 10, 0, 5) # Player moved to x=6

Stop the sound

channel = sound_system.obj_stop(channel)

Update all active looping sounds with a single call

updated_count = sound_system.update_all_active_loops(player_x=6, player_y=0)

Global function equivalents

channel = sg.obj_play(sounds, "footsteps", 5, 10, 0, 5, loop=True) channel = sg.obj_update(channel, 6, 10, 0, 5) channel = sg.obj_stop(channel)

Play background music

sg.play_bgm("sounds/background.ogg", loop=True)

Adjust volume

sg.adjust_master_volume(0.1) # Increase master volume sg.adjust_bgm_volume(-0.1) # Decrease background music volume sg.adjust_sfx_volume(0.1) # Increase sound effects volume

Create a sound manager

sound_system = sg.Sound("sounds/")

Get the dictionary of loaded sounds

sounds = sound_system.sounds

Play a sound

channel = sound_system.play_sound("explosion")

Play a looping sound

channel = sound_system.play_sound("ambient", loop=True)

Play a sound with 3D positional audio (horizontal and vertical positioning)

channel = sound_system.obj_play("footsteps", playerPos=5, objPos=10, playerY=0, objY=5, loop=True)

Update sound position as player or object moves

channel = sound_system.obj_update(channel, 6, 10, 0, 5) # Player moved to x=6

Stop the sound

channel = sound_system.obj_stop(channel)

Update all active looping sounds with a single call

updated_count = sound_system.update_all_active_loops(player_x=6, player_y=0)

Global function equivalents

channel = sg.obj_play(sounds, "footsteps", 5, 10, 0, 5, loop=True) channel = sg.obj_update(channel, 6, 10, 0, 5) channel = sg.obj_stop(channel)

Play background music (now with loop parameter)

sg.play_bgm("sounds/background.ogg", loop=True)

Adjust volume

sg.adjust_master_volume(0.1) # Increase master volume sg.adjust_bgm_volume(-0.1) # Decrease background music volume sg.adjust_sfx_volume(0.1) # Increase sound effects volume

Complete Game Structure with Class-Based Architecture

import libstormgames as sg
import pygame
import random

class MyGame:
    def __init__(self):
        # Create a Game instance that manages all subsystems
        self.game = sg.Game("My Advanced Game").initialize()
        
        # Game state
        self.player_x = 5
        self.player_y = 0
        self.difficulty = "normal"
        
        # Load settings
        try:
            self.difficulty = self.game.config_service.local_config.get("settings", "difficulty")
        except:
            self.game.config_service.local_config.add_section("settings")
            self.game.config_service.local_config.set("settings", "difficulty", "normal")
            self.game.config_service.write_local_config()
    
    def play_game(self):
        self.game.speak(f"Starting game on {self.difficulty} difficulty")
        self.game.play_bgm("sounds/game_music.ogg")
        
        # Game loop
        running = True
        
        # Create enemies with positional audio
        enemies = []
        for i in range(3):
            enemy = {
                'x': random.uniform(10, 30),
                'y': random.uniform(-5, 5),
                'channel': None
            }
            # Start looping sound for each enemy
            enemy['channel'] = self.game.sound.play_random_positional(
                "enemy", self.player_x, enemy['x'], self.player_y, enemy['y'], loop=True
            )
            enemies.append(enemy)
        
        while running:
            # Update game state
            for enemy in enemies:
                # Move enemies toward player
                if enemy['x'] > self.player_x:
                    enemy['x'] -= 0.1
                else:
                    enemy['x'] += 0.1
            
            # Handle input
            for event in pygame.event.get():
                if event.type == pygame.KEYDOWN:
                    if event.key == pygame.K_ESCAPE:
                        running = False
                    elif event.key == pygame.K_LEFT:
                        self.player_x -= 1
                    elif event.key == pygame.K_RIGHT:
                        self.player_x += 1
                    elif event.key == pygame.K_SPACE:
                        self.game.scoreboard.increase_score()
                        self.game.speak(f"Score: {self.game.scoreboard.get_score()}")
            
            # Update all sound positions with one call
            self.game.sound.update_all_active_loops(self.player_x, self.player_y)
            
            pygame.time.delay(50)
        
        # Stop all enemy sounds
        for enemy in enemies:
            if enemy['channel']:
                self.game.sound.obj_stop(enemy['channel'])
        
        # Game over
        position = self.game.scoreboard.check_high_score()
        if position:
            self.game.speak(f"New high score! Position {position}")
            self.game.scoreboard.add_high_score()
        
        return "menu"
    
    def settings(self):
        options = ["easy", "normal", "hard", "back"]
        current = options.index(self.difficulty) if self.difficulty in options else 1
        
        while True:
            self.game.speak(f"Current difficulty: {options[current]}")
            
            # Wait for input
            event = pygame.event.wait()
            if event.type == pygame.KEYDOWN:
                if event.key == pygame.K_UP and current > 0:
                    current -= 1
                elif event.key == pygame.K_DOWN and current < len(options) - 1:
                    current += 1
                elif event.key == pygame.K_RETURN:
                    if options[current] == "back":
                        return
                    self.difficulty = options[current]
                    self.game.config_service.local_config.set("settings", "difficulty", self.difficulty)
                    self.game.config_service.write_local_config()
                    self.game.speak(f"Difficulty set to {self.difficulty}")
                    return
                elif event.key == pygame.K_ESCAPE:
                    return
    
    def run(self):
        # Main menu loop
        while True:
            # Use playCallback for play option and add custom "settings" option
            # Note: standard options like instructions, high_scores, etc. are handled automatically
            choice = sg.game_menu(self.game.sound.sounds, self.play_game, "settings")
            
            if choice == "settings":
                self.settings()

# Run the game
if __name__ == "__main__":
    game = MyGame()
    game.run()

Best Practices

  1. Sound Looping and Positioning:

    • Use the loop=True parameter for sounds that need to loop
    • For moving sound sources, use update_all_active_loops() to efficiently update all sounds at once
    • Include vertical positioning (Y-axis) if you need 3D audio
  2. Modern vs Traditional Approach:

    • New projects: Use the Game class for better organization
    • Existing projects: Continue with global functions for compatibility
    • Both approaches are fully supported
  3. Always clean up resources:

    • Use exit_game() or game.exit() when exiting to ensure proper cleanup
    • Stop sounds that are no longer needed with obj_stop()
  4. Volume control:

    • Implement the Alt+key volume controls in your game
    • Use volume services for better control
  5. Configuration:

    • Save user preferences using the Config class
    • Load settings at startup
  6. Path initialization:

    • Always initialize the framework with a proper game title
    • Game title is used to determine configuration directory paths
    • Services are interconnected, so proper initialization ensures correct operation

Troubleshooting

No Sound

  • Ensure pygame mixer is properly initialized
  • Check if sound files exist in the correct directory
  • Verify file formats (OGG and WAV are supported)

No Speech

  • Make sure at least one speech provider is installed
    • Linux/Unix: python-speechd or accessible-output2
    • Windows/macOS: accessible-output2
  • Check if pygame display is initialized properly

Input Issues

  • Ensure pygame is properly handling events
  • Check event loop for proper event handling
  • Remember to use pygame.event.pump() especially in the game loop

Scoreboard/Configuration Issues

  • Check if path services are properly initialized with a game title

  • Verify write permissions in the configuration directory

  • For debugging, use the following code to view path configuration:

    from libstormgames.services import PathService, ConfigService
    
    # Print path information
    path_service = PathService.get_instance()
    print(f"Game Name: {path_service.gameName}")
    print(f"Game Path: {path_service.gamePath}")
    
    # Check config service connection
    config_service = ConfigService.get_instance()
    print(f"Config connected to path: {hasattr(config_service, 'pathService')}")
    

Contributing

Contributions are welcome! Please feel free to submit a Pull Request.

License

This project is licensed under the GPL v3 License - see the LICENSE file for details.

Description
Library to make writing audiogames easier.
Readme GPL-3.0 372 KiB
Languages
Python 100%