Hopefully improved the sound handling for looping and for Y positioning. Should make it easier to have things falling or approaching from the Y axis.

This commit is contained in:
Storm Dragon 2025-03-17 19:41:15 -04:00
parent 3b01662d98
commit d513927d52
2 changed files with 614 additions and 555 deletions

552
README.md
View File

@ -7,7 +7,7 @@ A Python library to make creating audio games easier.
`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
- Sound and music playback with positional audio and 3D spatialization
- Text-to-speech integration
- Configuration management
- Score tracking and high score tables
@ -28,6 +28,7 @@ A Python library to make creating audio games easier.
- numpy>=1.19.0
- wxpython
#### Speech Providers (one required)
- Linux/Unix: `python-speechd` or `accessible-output2>=0.14`
@ -76,14 +77,16 @@ def main():
# Define menu options
while True:
choice = sg.game_menu(sounds)
if choice == "play":
play_game()
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
@ -107,9 +110,10 @@ def main():
# Define menu options
while True:
choice = sg.game_menu(game.sound.get_sounds())
if choice == "play":
play_game()
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()
@ -122,7 +126,7 @@ 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
- **sound**: Sound and music playback with 3D positional audio
- **speech**: Text-to-speech functionality
- **scoreboard**: High score tracking
- **input**: Input handling and dialogs
@ -149,11 +153,9 @@ game.scoreboard.increase_score(10)
game.sound.play_random("explosion")
# Display text
game.display_text(["Line 1", "Line 2"])
# Clean exit
game.exit()
```
@ -205,444 +207,85 @@ Configuration files are automatically stored in standard system locations:
Each game has its own subdirectory based on the game name (lowercase with hyphens), for example, Wicked Quest becomes wicked-quest.
### Sound
Manages sound loading and playback with positional audio support.
```python
# Create a sound manager
sound_system = sg.Sound("sounds/")
# Get the dictionary of loaded sounds
sounds = sound_system.get_sounds()
sounds = sound_system.sounds
# Play a sound
sg.play_sound(sounds["explosion"])
channel = sound_system.play_sound("explosion")
# Play a sound with positional audio (player at x=5, object at x=10)
channel = sg.obj_play(sounds, "footsteps", 5, 10)
# 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 = sg.obj_update(channel, 6, 10) # Player moved to x=6
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")
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/")
### Speech
# Get the dictionary of loaded sounds
sounds = sound_system.sounds
Provides text-to-speech functionality using available speech providers.
# Play a sound
channel = sound_system.play_sound("explosion")
```python
# Create a speech manager (usually you'll use the global instance)
speech = sg.Speech()
# Play a looping sound
channel = sound_system.play_sound("ambient", loop=True)
# Speak text
speech.speak("Hello, world!")
# 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)
# Or use the global function for convenience
sg.speak("Hello, world!")
# Update sound position as player or object moves
channel = sound_system.obj_update(channel, 6, 10, 0, 5) # Player moved to x=6
# Speak without interrupting previous speech
sg.speak("This won't interrupt", interrupt=False)
# Stop the sound
channel = sound_system.obj_stop(channel)
# Clean up when done
exit_game() or as the class, game.exit_game()
```
# 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)
### Scoreboard
Tracks scores and manages high score tables.
```python
# Create a scoreboard
scoreboard = sg.Scoreboard()
# Manipulate score
scoreboard.increase_score(10)
scoreboard.decrease_score(5)
current_score = scoreboard.get_score()
# Check for high score
position = scoreboard.check_high_score()
if position:
print(f"You're in position {position}!")
# Add high score (prompts for player name)
scoreboard.add_high_score()
# Get all high scores
high_scores = scoreboard.get_high_scores()
for entry in high_scores:
print(f"{entry['name']}: {entry['score']}")
```
The Scoreboard system automatically manages high score data in the game's configuration directory:
- Linux/Unix: `~/.config/storm-games/game-name/config.ini`
- Windows: `%APPDATA%\storm-games\game-name\config.ini`
- macOS: `~/Library/Application Support/storm-games/game-name/config.ini`
Where `game-name` is the lowercase, hyphenated version of your game title. For example,
"My Awesome Game" would use the directory `my-awesome-game`.
## Key Functions
### Game Initialization and Control
```python
# Initialize game systems (traditional approach)
sounds = sg.initialize_gui("Game Title")
# Or use the Game class (modern approach)
game = sg.Game("Game Title").initialize()
# Pause the game (freezes until user presses backspace)
sg.pause_game()
# Check if user wants to exit
if sg.check_for_exit():
sg.exit_game()
# Exit the game properly
sg.exit_game()
```
### Menu System
```python
# Basic menu example (returns option string)
choice = sg.game_menu(sounds)
if choice == "play":
# Start the game
play_game()
elif choice == "instructions":
sg.instructions()
# etc...
# Menu with play callback (callback is executed directly)
def play_callback():
sg.speak("Game started with callback!")
# Game code goes here
sg.game_menu(sounds, play_callback)
# Menu with custom options
choice = sg.game_menu(sounds, None, "practice_mode")
if choice == "practice_mode":
# Handle practice mode
practice_game()
elif choice == "difficulty":
# Handle difficulty settings
set_difficulty()
elif choice == "options":
# Handle options screen
show_options()
# etc...
```
### Complete Menu Example
```python
import libstormgames as sg
import pygame
def main():
# Initialize the game
sounds = sg.initialize_gui("Menu Example Game")
# Play menu music
sg.play_bgm("sounds/music_menu.ogg")
# Define play function with callback
def play_game():
sg.speak("Starting game!")
# Game code here
sg.speak("Game over!")
return "menu" # Return to menu after game ends
# Define custom menu option handlers
def practice_mode():
sg.speak("Starting practice mode!")
# Practice mode code here
return "menu"
def difficulty_settings():
# Show difficulty options
options = ["easy", "normal", "hard", "back"]
current = 0
while True:
sg.speak(options[current])
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 "menu"
sg.speak(f"Difficulty set to {options[current]}")
return "menu"
elif event.key == pygame.K_ESCAPE:
return "menu"
# Main menu loop
while True:
# Display menu with play callback and custom options
choice = sg.game_menu(sounds, play_game, "practice_mode", "difficulty")
# Handle menu choices
if choice == "practice_mode":
practice_mode()
elif choice == "difficulty":
difficulty_settings()
# Other options are handled automatically by game_menu
if __name__ == "__main__":
main()
```
### Class-Based Menu Example
```python
import libstormgames as sg
import pygame
class MyGame:
def __init__(self):
# Create and initialize game
self.game = sg.Game("Class-Based Menu Example").initialize()
self.sounds = self.game.sound.get_sounds()
def play_game(self):
self.game.speak("Starting game!")
# Game code here
self.game.speak("Game over!")
return "menu"
def practice_mode(self):
self.game.speak("Starting practice mode!")
# Practice mode code here
return "menu"
def settings(self):
# A nested menu example
submenu_options = ["graphics", "audio", "controls", "back"]
current = 0
while True:
self.game.speak(submenu_options[current])
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(submenu_options) - 1:
current += 1
elif event.key == pygame.K_RETURN:
if submenu_options[current] == "back":
return "menu"
self.game.speak(f"Selected {submenu_options[current]} settings")
return "menu"
elif event.key == pygame.K_ESCAPE:
return "menu"
def run(self):
# Main menu loop
while True:
# Use playCallback parameter to directly call play_game when "play" is selected
choice = sg.game_menu(self.sounds, None, "practice_mode",
"settings")
# Handle other menu options
if choice == "practice_mode":
self.practice_mode()
elif choice == "settings":
self.settings()
# Run the game
if __name__ == "__main__":
game = MyGame()
game.run()
```
### Text Display
```python
# Display text with navigation
sg.display_text([
"Line 1 of instructions",
"Line 2 of instructions",
"Line 3 of instructions"
])
# Display a simple message box
sg.messagebox("Game Over! You scored 100 points.")
# Get text input from user
name = sg.get_input("Enter your name:", "Player")
```
### Sound Effects
```python
# Play a random variation of a sound
sg.play_random(sounds, "explosion")
# Play positional sound with distance-based volume
sg.play_random_positional(sounds, "footsteps", player_x=5, object_x=10)
# Play directional sound (simplified left/right positioning)
sg.play_directional_sound(sounds, "voice", player_x=5, object_x=10)
# Play a sound as a cutscene (interrupts other sounds, waits until complete)
sg.cut_scene(sounds, "intro_speech")
# Play or update a falling sound
channel = sg.play_random_falling(sounds, "rock", player_x=5, object_x=8, start_y=10, currentY=5)
```
### Utility Functions
```python
# Check for game updates
update_info = sg.check_for_updates("1.0.0", "My Game", "https://example.com/version.json")
# Check compatibility with library version
is_compatible = sg.check_compatibility("1.0.0", "1.2.3")
# Sanitize a filename for any OS
safe_name = sg.sanitize_filename("User's File.txt")
# Calculate distance between points
distance = sg.distance_2d(x1=5, y1=10, x2=8, y2=15)
# Interpolation functions
mid_value = sg.lerp(start=0, end=10, factor=0.5) # Returns 5.0
smooth_value = sg.smooth_step(edge0=0, edge1=10, x=5) # Smooth transition
```
## Advanced Examples
### Using the Game Class (Modern Approach)
```python
import libstormgames as sg
import pygame
import random
def main():
# Create and initialize the game
game = sg.Game("My Advanced Game").initialize()
# Set up game environment
game.play_bgm("sounds/background.ogg")
# Main game loop
running = True
player_x = 5
while running:
# Process events
for event in pygame.event.get():
if event.type == pygame.KEYDOWN:
if event.key == pygame.K_ESCAPE:
running = False
elif event.key == pygame.K_SPACE:
# Score points
game.scoreboard.increase_score(5)
game.speak(f"Score: {game.scoreboard.get_score()}")
# Update game state
player_x += random.uniform(-0.2, 0.2)
# Add random ambient sounds
if random.random() < 0.05:
sounds = game.sound.get_sounds()
if "ambient" in sounds:
sg.play_random_positional(sounds, "ambient", player_x,
player_x + random.uniform(-5, 5))
pygame.time.delay(50)
# Game over and cleanup
game.speak("Game over!")
game.exit()
if __name__ == "__main__":
main()
```
### Complex Sound Environment
```python
import libstormgames as sg
import time
import random
def create_sound_environment(player_x, player_y):
sounds = sg.initialize_gui("Sound Environment Demo")
# Place sound sources
water_x, water_y = 10, 5
fire_x, fire_y = 15, 8
wind_x, wind_y = 3, 12
# Play ambient sounds
water_channel = sg.obj_play(sounds, "water", player_x, water_x)
fire_channel = sg.obj_play(sounds, "fire", player_x, fire_x)
wind_channel = sg.obj_play(sounds, "wind", player_x, wind_x)
# Main loop
running = True
while running:
# Simulate player movement
player_x += random.uniform(-0.5, 0.5)
player_y += random.uniform(-0.5, 0.5)
# Update sound positions
water_channel = sg.obj_update(water_channel, player_x, water_x)
fire_channel = sg.obj_update(fire_channel, player_x, fire_x)
wind_channel = sg.obj_update(wind_channel, player_x, wind_x)
# Occasionally play random sound
if random.random() < 0.1:
sg.play_random_positional(sounds, "creature", player_x,
player_x + random.uniform(-5, 5))
# Check for exit
if sg.check_for_exit():
running = False
time.sleep(0.1)
# Clean up
sg.obj_stop(water_channel)
sg.obj_stop(fire_channel)
sg.obj_stop(wind_channel)
sg.exit_game()
```
# 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
@ -658,7 +301,7 @@ class MyGame:
# Game state
self.player_x = 5
self.player_y = 5
self.player_y = 0
self.difficulty = "normal"
# Load settings
@ -675,28 +318,53 @@ class MyGame:
# 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
self.player_x += random.uniform(-0.2, 0.2)
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()}")
# Add some random sounds
if random.random() < 0.05:
sounds = self.game.sound.get_sounds()
if "ambient" in sounds:
sg.play_random_positional(sounds, "ambient",
self.player_x, self.player_x + random.uniform(-10, 10))
# 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:
@ -733,20 +401,12 @@ class MyGame:
def run(self):
# Main menu loop
while True:
sounds = self.game.sound.get_sounds()
choice = sg.game_menu(sounds, self.play_game, "settings",
"instructions", "credits", "donate", "exit_game")
# 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()
elif choice == "instructions":
sg.instructions()
elif choice == "credits":
sg.credits()
elif choice == "donate":
sg.donate()
elif choice == "exit_game":
self.game.exit()
# Run the game
if __name__ == "__main__":
@ -757,24 +417,29 @@ if __name__ == "__main__":
## Best Practices
1. **Modern vs Traditional Approach**:
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
2. **Always clean up resources**:
3. **Always clean up resources**:
- Use `exit_game()` or `game.exit()` when exiting to ensure proper cleanup
- Stop sounds that are no longer needed
- Stop sounds that are no longer needed with `obj_stop()`
3. **Volume control**:
4. **Volume control**:
- Implement the Alt+key volume controls in your game
- Use volume services for better control
4. **Configuration**:
5. **Configuration**:
- Save user preferences using the Config class
- Load settings at startup
5. **Path initialization**:
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
@ -799,6 +464,7 @@ if __name__ == "__main__":
- 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

617
sound.py

File diff suppressed because it is too large Load Diff