15 KiB
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
oraccessible-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
-
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
- Use the
-
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
-
Always clean up resources:
- Use
exit_game()
orgame.exit()
when exiting to ensure proper cleanup - Stop sounds that are no longer needed with
obj_stop()
- Use
-
Volume control:
- Implement the Alt+key volume controls in your game
- Use volume services for better control
-
Configuration:
- Save user preferences using the Config class
- Load settings at startup
-
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
oraccessible-output2
- Windows/macOS:
accessible-output2
- Linux/Unix:
- 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.