496 lines
15 KiB
Markdown
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.
|