libstormgames/README.md

496 lines
15 KiB
Markdown

# 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.
```bash
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
```python
#!/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
```python
#!/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:
```python
# 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:
```python
# 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.
```python
# 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
```python
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:
```python
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.