Enhance rendering system: improve screen resolution handling, add audio system initialization, and refine blood surface generation and blending

This commit is contained in:
2025-08-14 17:29:58 +02:00
parent c35fc9f1f7
commit f4ca5bba5b
2 changed files with 508 additions and 213 deletions
+214 -29
View File
@@ -1,8 +1,10 @@
# Mice! # Mice!
Mice! is a strategic game where players must kill rats with bombs before they reproduce and become too numerous. The game is a clone of the classic game Rats! for Windows 95. Mice! is a strategic game where players must kill rats with bombs before they reproduce and become too numerous. The game is a clone of the classic game Rats! for Windows 95.
## Compatibility ## Compatibility
*It's developed in Python 3.11, please use it* *It's developed in Python 3.11, please use it*
## Features ## Features
- **Maze Generation**: Randomly generated mazes using Depth First Search (DFS) algorithm. - **Maze Generation**: Randomly generated mazes using Depth First Search (DFS) algorithm.
@@ -11,27 +13,189 @@ Mice! is a strategic game where players must kill rats with bombs before they re
- **Sound Effects**: Audio feedback for various game events. - **Sound Effects**: Audio feedback for various game events.
- **Scoring**: Points system to track player progress. - **Scoring**: Points system to track player progress.
## Engine Architecture
The Mice! game engine is built on a modular architecture designed for flexibility and maintainability. The engine follows a component-based design pattern where different systems handle specific aspects of the game.
### Core Engine Components
#### 1. **Rendering System** (`engine/sdl2.py`)
- **GameWindow Class**: Central rendering manager using SDL2
- **Features**:
- Hardware-accelerated rendering via SDL2
- Texture management and caching
- Sprite rendering with transparency support
- Text rendering with custom fonts
- Resolution-independent scaling
- Fullscreen/windowed mode switching
- **Implementation**:
- Uses SDL2 renderer for efficient GPU-accelerated drawing
- Implements double buffering for smooth animation
- Manages texture atlas for optimized memory usage
- Handles viewport transformations for different screen resolutions
#### 2. **Input System** (`engine/controls.py`)
- **KeyBindings Class**: Handles all user input
- **Features**:
- Keyboard input mapping and handling
- Joystick/gamepad support
- Configurable key bindings
- Input state management
- **Implementation**:
- Event-driven input processing
- Key state buffering for smooth movement
- Support for multiple input devices simultaneously
- Customizable control schemes
#### 3. **Map System** (`engine/maze.py`)
- **Map Class**: Manages the game world structure
- **Features**:
- Maze data loading and parsing
- Collision detection system
- Tile-based world representation
- Pathfinding support for AI units
- **Implementation**:
- Grid-based coordinate system
- Efficient collision detection using spatial partitioning
- Support for different tile types (walls, floors, special tiles)
- Integration with maze generation algorithms
#### 4. **Audio System**
- **Sound Management**: Handles all audio playback
- **Features**:
- Sound effect playback
- Background music support
- Volume control
- Multiple audio channels
- **Implementation**:
- Uses subprocess module for audio playback
- Asynchronous sound loading and playing
- Audio file format support (WAV, MP3, OGG)
### Game Loop Architecture
The main game loop follows the standard pattern:
1. **Input Processing**: Capture and process user input
2. **Update Phase**: Update game state, unit logic, and physics
3. **Render Phase**: Draw all game objects to the screen
4. **Timing Control**: Maintain consistent frame rate
```
Input → Update → Render → Present → Repeat
```
## Units Implementation
The game uses an object-oriented approach for all game entities. Each unit type inherits from a base unit class and implements specific behaviors.
### Base Unit Architecture
All units share common properties and methods:
- **Position and Movement**: 2D coordinates with movement capabilities
- **Unique Identification**: UUID-based unique identifiers
- **Collision Detection**: Bounding box collision system
- **State Management**: Current state tracking (alive, dead, exploding, etc.)
- **Rendering**: Sprite-based visual representation
### Unit Types Implementation
#### 1. **Rat Units** (`units/rat.py`)
**Base Rat Class**:
- **AI Behavior**: Implements pathfinding using A* algorithm
- **Movement**: Grid-based movement with smooth interpolation
- **State Machine**: Multiple states (wandering, fleeing, reproducing)
**Male Rat Class**:
- **Reproduction Logic**: Seeks female rats for mating
- **Territorial Behavior**: Defends territory from other males
- **Lifespan Management**: Age-based death system
**Female Rat Class**:
- **Pregnancy System**: Gestation period simulation
- **Offspring Generation**: Creates new rat units
- **Maternal Behavior**: Protects offspring from threats
**Implementation Details**:
```python
# Simplified rat behavior structure
class Rat:
def update(self):
self.process_ai() # Decision making
self.handle_movement() # Position updates
self.check_collisions() # Collision detection
self.update_state() # State transitions
```
#### 2. **Bomb Units** (`units/bomb.py`)
**Bomb Class**:
- **Timer System**: Countdown mechanism before explosion
- **Placement Logic**: Player-controlled positioning
- **Damage Calculation**: Blast radius and damage computation
**Explosion Class**:
- **Visual Effects**: Animated explosion graphics
- **Damage Dealing**: Affects units within blast radius
- **Temporary Entity**: Self-destructs after animation
**Implementation Details**:
- **State Machine**: Armed → Countdown → Exploding → Cleanup
- **Collision System**: Different collision behaviors per state
- **Effect Propagation**: Chain reaction support for multiple bombs
#### 3. **Point Units** (`units/points.py`)
**Point Class**:
- **Collection Mechanics**: Player interaction system
- **Value System**: Different point values for different achievements
- **Visual Feedback**: Pickup animations and effects
### Unit Interaction System
Units interact through a centralized collision and event system:
1. **Collision Detection**:
- Grid-based broad phase for efficiency
- Precise bounding box narrow phase
- Custom collision responses per unit type pair
2. **Event System**:
- Unit death events
- Reproduction events
- Explosion events
- Point collection events
3. **AI Communication**:
- Shared pathfinding data
- Pheromone trail system for rat behavior
- Danger awareness (bombs, explosions)
## Technical Details ## Technical Details
- **Language**: Python 3 - **Language**: Python 3.11
- **Libraries**: - **Libraries**:
- `sdl2` for graphics and window management - `sdl2` for graphics and window management
- `Pillow` for image processing - `Pillow` for image processing
- `uuid` for unique unit identification - `uuid` for unique unit identification
- `subprocess` for playing sound effects - `subprocess` for playing sound effects
- `tkinter` for maze generation visualization - `tkinter` for maze generation visualization
- **Game Loop**: The game uses a main loop to handle events, update game state, and render graphics. - **Performance Optimizations**:
- **Collision Detection**: Each unit checks for collisions with other units and walls. - Spatial partitioning for collision detection
- **Sound Management**: Sound effects are managed using the `subprocess` module to play audio files. - Texture atlasing for reduced memory usage
- **Environment Variables**: - Object pooling for frequently created/destroyed units
- `SDL_VIDEODRIVER` to set the video driver - Delta time-based updates for frame rate independence
- `RESOLUTION` to set the screen resolution - **Memory Management**:
- **Engine**: The game engine is built using SDL2, providing efficient rendering and handling of game events. The engine supports: - Automatic cleanup of dead units
- **Image Loading**: Using `Pillow` to load and process images. - Texture caching and reuse
- **Text Rendering**: Custom fonts and text rendering using SDL2's text capabilities. - Efficient data structures for large numbers of units
- **Sound Playback**: Integration with SDL2's audio features for sound effects.
- **Joystick Support**: Handling joystick input for game controls. ## Environment Variables
- **Window Management**: Fullscreen and windowed modes, with adjustable resolution.
- `SDL_VIDEODRIVER`: Set the video driver (x11, wayland, etc.)
- `RESOLUTION`: Set the screen resolution (format: WIDTHxHEIGHT)
- `FULLSCREEN`: Enable/disable fullscreen mode (true/false)
- `SOUND_ENABLED`: Enable/disable sound effects (true/false)
## Installation ## Installation
@@ -54,21 +218,42 @@ Mice! is a strategic game where players must kill rats with bombs before they re
python rats.py python rats.py
``` ```
## Project Files ## Project Structure
- `maze.py`: Contains the `MazeGenerator` class for generating and visualizing the maze. ```
- `rats.py`: Main game file that initializes the game and handles game logic. mice/
- `engine/controls.py`: Contains the `KeyBindings` class for handling keyboard input. ├── engine/ # Core engine components
- `engine/maze.py`: Contains the `Map` class for loading and managing the maze structure. │ ├── controls.py # Input handling system
- `engine/sdl2.py`: Contains the `GameWindow` class for SDL2 window management and rendering. │ ├── maze.py # Map and collision system
- `units/bomb.py`: Contains the `Bomb` and `Explosion` classes for bomb units. │ └── sdl2.py # Rendering and window management
- `units/rat.py`: Contains the `Rat`, `Male`, and `Female` classes for rat units. ├── units/ # Game entity implementations
- `units/points.py`: Contains the `Point` class for point units. │ ├── bomb.py # Bomb and explosion logic
- `assets/`: Directory containing game assets such as images and fonts. │ ├── rat.py # Rat AI and behavior
- `sound/`: Directory containing sound effects. │ └── points.py # Collectible points
- `README.md`: This file, containing information about the project. ├── assets/ # Game resources
- `requirements.txt`: Lists the Python dependencies for the project. │ ├── images/ # Sprites and textures
- `.env`: Environment variables for the project. │ └── fonts/ # Text rendering fonts
- `.gitignore`: Specifies files and directories to be ignored by Git. ├── sound/ # Audio files
- `scores.txt`: File for storing high scores. ├── maze.py # Maze generation algorithms
├── rats.py # Main game entry point
├── requirements.txt # Python dependencies
├── .env # Environment configuration
└── README.md # This documentation
```
## Game Files Details
- `maze.py`: Contains the `MazeGenerator` class implementing DFS algorithm for procedural maze generation
- `rats.py`: Main game controller, initializes engine systems and manages game state
- `engine/controls.py`: Input abstraction layer with configurable key bindings
- `engine/maze.py`: World representation with collision detection and pathfinding support
- `engine/sdl2.py`: Low-level graphics interface wrapping SDL2 functionality
- `units/bomb.py`: Explosive units with timer mechanics and blast radius calculations
- `units/rat.py`: AI-driven entities with reproduction, pathfinding, and survival behaviors
- `units/points.py`: Collectible scoring items with visual feedback systems
- `assets/`: Game resources including sprites, textures, and fonts
- `sound/`: Audio assets for game events and feedback
- `scores.txt`: Persistent high score storage
- `.env`: Runtime configuration and environment settings
- `.gitignore`: Version control exclusion rules
+294 -184
View File
@@ -1,24 +1,28 @@
import os import os
import random import random
import ctypes
from ctypes import *
import sdl2 import sdl2
import sdl2.ext import sdl2.ext
from sdl2.ext.compat import byteify from sdl2.ext.compat import byteify
from ctypes import *
import ctypes
from PIL import Image
from sdl2 import SDL_AudioSpec from sdl2 import SDL_AudioSpec
from PIL import Image
class GameWindow: class GameWindow:
def __init__(self, width, height, cell_size, title="Default", key_callback=None): def __init__(self, width, height, cell_size, title="Default", key_callback=None):
# Display configuration
self.cell_size = cell_size self.cell_size = cell_size
self.width = width * cell_size self.width = width * cell_size
self.height = height * cell_size self.height = height * cell_size
# Screen resolution handling
actual_screen_size = os.environ.get("RESOLUTION", "640x480").split("x") actual_screen_size = os.environ.get("RESOLUTION", "640x480").split("x")
actual_screen_size = tuple(map(int, actual_screen_size)) actual_screen_size = tuple(map(int, actual_screen_size))
self.target_size = actual_screen_size if self.width > actual_screen_size[0] or self.height > actual_screen_size[1] else (self.width, self.height) self.target_size = actual_screen_size if self.width > actual_screen_size[0] or self.height > actual_screen_size[1] else (self.width, self.height)
# View offset calculations
self.w_start_offset = (self.target_size[0] - self.width) // 2 self.w_start_offset = (self.target_size[0] - self.width) // 2
self.h_start_offset = (self.target_size[1] - self.height) // 2 self.h_start_offset = (self.target_size[1] - self.height) // 2
self.w_offset = self.w_start_offset self.w_offset = self.w_start_offset
@@ -26,32 +30,56 @@ class GameWindow:
self.max_w_offset = self.target_size[0] - self.width self.max_w_offset = self.target_size[0] - self.width
self.max_h_offset = self.target_size[1] - self.height self.max_h_offset = self.target_size[1] - self.height
self.scale = self.target_size[1] // self.cell_size self.scale = self.target_size[1] // self.cell_size
print(f"Screen size: {self.width}x{self.height}") print(f"Screen size: {self.width}x{self.height}")
# SDL2 initialization
sdl2.ext.init(joystick=True) sdl2.ext.init(joystick=True)
sdl2.SDL_Init(sdl2.SDL_INIT_AUDIO) sdl2.SDL_Init(sdl2.SDL_INIT_AUDIO)
self.window = sdl2.ext.Window(title=title, size=self.target_size,)# flags=sdl2.SDL_WINDOW_FULLSCREEN) # Window and renderer setup
self.delay = 30 self.window = sdl2.ext.Window(title=title, size=self.target_size)
self.load_joystick()
self.window.show() self.window.show()
self.renderer = sdl2.ext.Renderer(self.window, flags=sdl2.SDL_RENDERER_ACCELERATED) self.renderer = sdl2.ext.Renderer(self.window, flags=sdl2.SDL_RENDERER_ACCELERATED)
self.factory = sdl2.ext.SpriteFactory(renderer=self.renderer) self.factory = sdl2.ext.SpriteFactory(renderer=self.renderer)
# Font system
self.fonts = self.generate_fonts("assets/decterm.ttf") self.fonts = self.generate_fonts("assets/decterm.ttf")
# Initial loading dialog
self.dialog("Loading assets...") self.dialog("Loading assets...")
self.renderer.present() self.renderer.present()
# Game state
self.running = True self.running = True
self.key_down, self.key_up, self.axis_scroll = key_callback self.delay = 30
self.performance = 0 self.performance = 0
self.audio_devs = {}
# Input handling
self.key_down, self.key_up, self.axis_scroll = key_callback
self.button_cursor = [0, 0] self.button_cursor = [0, 0]
self.buttons = {} self.buttons = {}
self.audio_devs["base"] = sdl2.SDL_OpenAudioDevice(None, 0, SDL_AudioSpec(freq=22050, aformat=sdl2.AUDIO_U8, channels=1, samples=2048), None, 0)
self.audio_devs["effects"] = sdl2.SDL_OpenAudioDevice(None, 0, SDL_AudioSpec(freq=22050, aformat=sdl2.AUDIO_U8, channels=1, samples=2048), None, 0) # Audio system initialization
self.audio_devs["music"] = sdl2.SDL_OpenAudioDevice(None, 0, SDL_AudioSpec(freq=22050, aformat=sdl2.AUDIO_U8, channels=1, samples=2048), None, 0) self._init_audio_system()
# Input devices
self.load_joystick()
def _init_audio_system(self):
"""Initialize audio devices for different audio channels"""
audio_spec = SDL_AudioSpec(freq=22050, aformat=sdl2.AUDIO_U8, channels=1, samples=2048)
self.audio_devs = {}
self.audio_devs["base"] = sdl2.SDL_OpenAudioDevice(None, 0, audio_spec, None, 0)
self.audio_devs["effects"] = sdl2.SDL_OpenAudioDevice(None, 0, audio_spec, None, 0)
self.audio_devs["music"] = sdl2.SDL_OpenAudioDevice(None, 0, audio_spec, None, 0)
# ======================
# TEXTURE & IMAGE METHODS
# ======================
def create_texture(self, tiles: list): def create_texture(self, tiles: list):
# Always create a fresh surface since we free it after use """Create a texture from a list of tiles"""
bg_surface = sdl2.SDL_CreateRGBSurface(0, self.width, self.height, 32, 0, 0, 0, 0) bg_surface = sdl2.SDL_CreateRGBSurface(0, self.width, self.height, 32, 0, 0, 0, 0)
for tile in tiles: for tile in tiles:
dstrect = sdl2.SDL_Rect(tile[1], tile[2], self.cell_size, self.cell_size) dstrect = sdl2.SDL_Rect(tile[1], tile[2], self.cell_size, self.cell_size)
@@ -60,20 +88,12 @@ class GameWindow:
sdl2.SDL_FreeSurface(bg_surface) sdl2.SDL_FreeSurface(bg_surface)
return bg_texture return bg_texture
def load_joystick(self):
sdl2.SDL_Init(sdl2.SDL_INIT_JOYSTICK)
sdl2.SDL_JoystickOpen(0)
def generate_fonts(self,font_file):
fonts = {}
for i in range(10, 70, 1):
fonts.update({i: sdl2.ext.FontManager(font_path=font_file, size=i)})
return fonts
def load_image(self, path, transparent_color=None, surface=False): def load_image(self, path, transparent_color=None, surface=False):
"""Load and process an image with optional transparency and scaling"""
image_path = os.path.join("assets", path) image_path = os.path.join("assets", path)
image = Image.open(image_path) image = Image.open(image_path)
# Handle transparency
if transparent_color: if transparent_color:
image = image.convert("RGBA") image = image.convert("RGBA")
datas = image.getdata() datas = image.getdata()
@@ -84,164 +104,181 @@ class GameWindow:
else: else:
new_data.append(item) new_data.append(item)
image.putdata(new_data) image.putdata(new_data)
# Scale image
scale = self.cell_size // 20 scale = self.cell_size // 20
if surface:
return sdl2.ext.pillow_to_surface(image.resize((image.width * scale, image.height * scale), Image.NEAREST))
image = image.resize((image.width * scale, image.height * scale), Image.NEAREST) image = image.resize((image.width * scale, image.height * scale), Image.NEAREST)
if surface:
return sdl2.ext.pillow_to_surface(image)
return self.factory.from_surface(sdl2.ext.pillow_to_surface(image)) return self.factory.from_surface(sdl2.ext.pillow_to_surface(image))
def get_image_size(self, image):
"""Get the size of an image sprite"""
return image.size
# ======================
# FONT MANAGEMENT
# ======================
def generate_fonts(self, font_file):
"""Generate font managers for different sizes"""
fonts = {}
for i in range(10, 70, 1):
fonts.update({i: sdl2.ext.FontManager(font_path=font_file, size=i)})
return fonts
# ======================
# DRAWING METHODS
# ======================
def draw_text(self, text, font, position, color): def draw_text(self, text, font, position, color):
"""Draw text at specified position with given font and color"""
sprite = self.factory.from_text(text, color=color, fontmanager=font) sprite = self.factory.from_text(text, color=color, fontmanager=font)
# Handle center positioning
if position == "center": if position == "center":
position = ("center", "center") position = ("center", "center")
if position[0] == "center": if position[0] == "center":
position = (self.target_size[0] // 2 - sprite.size[0] // 2, position[1]) position = (self.target_size[0] // 2 - sprite.size[0] // 2, position[1])
if position[1] == "center": if position[1] == "center":
position = (position[0], self.target_size[1] // 2 - sprite.size[1] // 2) position = (position[0], self.target_size[1] // 2 - sprite.size[1] // 2)
sprite.position = position
#print(sprite.position)
self.renderer.copy(sprite, dstrect=sprite.position)
sprite.position = position
self.renderer.copy(sprite, dstrect=sprite.position)
def draw_background(self, bg_texture): def draw_background(self, bg_texture):
"""Draw background texture with current view offset"""
self.renderer.copy(bg_texture, dstrect=sdl2.SDL_Rect(self.w_offset, self.h_offset, self.width, self.height)) self.renderer.copy(bg_texture, dstrect=sdl2.SDL_Rect(self.w_offset, self.h_offset, self.width, self.height))
def draw_image(self, x, y, sprite, tag, anchor="nw"): def draw_image(self, x, y, sprite, tag, anchor="nw"):
"""Draw an image sprite at specified coordinates"""
if not self.is_in_visible_area(x, y): if not self.is_in_visible_area(x, y):
return return
sprite.position = (x+self.w_offset, y+self.h_offset) sprite.position = (x + self.w_offset, y + self.h_offset)
self.renderer.copy(sprite, dstrect=sprite.position) self.renderer.copy(sprite, dstrect=sprite.position)
def draw_rectangle(self, x, y, width, height, tag, outline="red", filling=None): def draw_rectangle(self, x, y, width, height, tag, outline="red", filling=None):
"""Draw a rectangle with optional fill and outline"""
if filling: if filling:
self.renderer.fill((x, y, width, height), sdl2.ext.Color(*filling)) self.renderer.fill((x, y, width, height), sdl2.ext.Color(*filling))
else: else:
self.renderer.draw_rect((x, y, width, height), sdl2.ext.Color(*outline)) self.renderer.draw_rect((x, y, width, height), sdl2.ext.Color(*outline))
def draw_pointer(self, x, y): def draw_pointer(self, x, y):
x=x+self.w_offset """Draw a red pointer rectangle at specified coordinates"""
y=y+self.h_offset x = x + self.w_offset
y = y + self.h_offset
for i in range(3): for i in range(3):
self.renderer.draw_rect((x + i,y+i, self.cell_size-2*i, self.cell_size-2*i), color=sdl2.ext.Color(255, 0, 0)) self.renderer.draw_rect((x + i, y + i, self.cell_size - 2*i, self.cell_size - 2*i),
color=sdl2.ext.Color(255, 0, 0))
def delete_tag(self, tag): def delete_tag(self, tag):
"""Placeholder for tag deletion (not implemented)"""
pass pass
# ======================
# UI METHODS
# ======================
def dialog(self, text, **kwargs): def dialog(self, text, **kwargs):
self.draw_rectangle(50, 50, """Display a dialog box with text and optional extras"""
self.target_size[0] - 100, self.target_size[1] - 100, "win", filling=(255, 255, 255)) # Draw dialog background
self.draw_rectangle(50, 50,
self.target_size[0] - 100, self.target_size[1] - 100,
"win", filling=(255, 255, 255))
# Draw main text
self.draw_text(text, self.fonts[self.target_size[1]//20], "center", sdl2.ext.Color(0, 0, 0)) self.draw_text(text, self.fonts[self.target_size[1]//20], "center", sdl2.ext.Color(0, 0, 0))
# Draw subtitle if provided
if subtitle := kwargs.get("subtitle"): if subtitle := kwargs.get("subtitle"):
self.draw_text(subtitle, self.fonts[self.target_size[1]//30], ("center", self.target_size[1] // 2 + 50), sdl2.ext.Color(0, 0, 0)) self.draw_text(subtitle, self.fonts[self.target_size[1]//30],
("center", self.target_size[1] // 2 + 50), sdl2.ext.Color(0, 0, 0))
# Draw image if provided
if image := kwargs.get("image"): if image := kwargs.get("image"):
image_size = self.get_image_size(image) image_size = self.get_image_size(image)
self.draw_image(self.target_size[0] // 2 - image_size[0] // 2 - self.w_offset, self.draw_image(self.target_size[0] // 2 - image_size[0] // 2 - self.w_offset,
self.target_size[1] // 2 - image_size[1] * 2 - self.h_offset, self.target_size[1] // 2 - image_size[1] * 2 - self.h_offset,
image, "win") image, "win")
# Draw scores if provided
if scores := kwargs.get("scores"): if scores := kwargs.get("scores"):
#self.draw_text("Scores:", self.fonts[self.target_size[1]//20], (self.target_size[0] // 2 - 50, self.target_size[1] // 2 + 50), sdl2.ext.Color(0, 0, 0)) sprite = self.factory.from_text("Scores:", color=sdl2.ext.Color(0, 0, 0),
sprite = self.factory.from_text("Scores:", color=sdl2.ext.Color(0, 0, 0), fontmanager=self.fonts[self.target_size[1]//20]) fontmanager=self.fonts[self.target_size[1]//20])
sprite.position = (self.target_size[0] // 2 - 50, self.target_size[1] // 2 + 30) sprite.position = (self.target_size[0] // 2 - 50, self.target_size[1] // 2 + 30)
self.renderer.copy(sprite, dstrect=sprite.position) self.renderer.copy(sprite, dstrect=sprite.position)
for i, score in enumerate(scores[:5]):
score = " - ".join(score)
self.draw_text(score, self.fonts[self.target_size[1]//40], ("center", self.target_size[1] // 2 + 50 + 30 * (i + 1)), sdl2.ext.Color(0, 0, 0))
def get_image_size(self, image):
return image.size
def update_status(self, text):
fps = int(1000 / self.performance) if self.performance != 0 else 0
text = f"FPS: {fps} - {text}"
font = self.fonts[20]
sprite = self.factory.from_text(text, color=sdl2.ext.Color(0, 0, 0), fontmanager=font)
text_width, text_height = sprite.size
self.renderer.fill((3, 3, text_width + 10, text_height + 4), sdl2.ext.Color(255, 255, 255))
self.draw_text(text, font, (8, 5), sdl2.ext.Color(0, 0, 0))
def new_cycle(self, delay, callback):
pass
def full_screen(self,flag):
sdl2.SDL_SetWindowFullscreen(self.window.window, flag)
def is_in_visible_area(self, x, y):
return -self.w_offset -self.cell_size <= x <= self.width - self.w_offset and -self.h_offset -self.cell_size <= y <= self.height - self.h_offset
def get_perf_counter(self):
return sdl2.SDL_GetPerformanceCounter()
def mainloop(self, **kwargs):
while self.running:
performance_start = sdl2.SDL_GetPerformanceCounter()
self.renderer.clear()
if "bg_update" in kwargs:
kwargs["bg_update"]()
kwargs["update"]()
events = sdl2.ext.get_events()
for event in events:
if event.type == sdl2.SDL_QUIT:
self.running = False
elif event.type == sdl2.SDL_KEYDOWN and self.key_down:
key = sdl2.SDL_GetKeyName(event.key.keysym.sym).decode('utf-8')
self.key_down(key)
elif event.type == sdl2.SDL_KEYUP and self.key_down:
key = sdl2.SDL_GetKeyName(event.key.keysym.sym).decode('utf-8')
self.key_up(key)
print(key)
elif event.type == sdl2.SDL_MOUSEMOTION:
self.key_down("mouse", coords=(event.motion.x, event.motion.y))
elif event.type == sdl2.SDL_JOYBUTTONDOWN:
key = event.jbutton.button
self.key_down(key)
elif event.type == sdl2.SDL_JOYBUTTONUP:
key = event.jbutton.button
self.key_up(key)
# elif event.type == sdl2.SDL_JOYAXISMOTION:
# self.axis_scroll(event.jaxis.axis, event.jaxis.value)
# Disegna qui gli sprite
#rect = sdl2.SDL_Rect(self.w_offset, self.h_offset, self.target_size[0], self.target_size[1])
#sdl2.SDL_RenderSetClipRect(self.renderer.sdlrenderer, rect)
self.renderer.present()
self.performance = (sdl2.SDL_GetPerformanceCounter() - performance_start) / sdl2.SDL_GetPerformanceFrequency() * 1000
if self.performance < self.delay:
delay = self.delay - round(self.performance)
else:
delay = 0
sdl2.SDL_Delay(delay)
def close(self): for i, score in enumerate(scores[:5]):
self.running = False score_text = " - ".join(score)
sdl2.ext.quit() self.draw_text(score_text, self.fonts[self.target_size[1]//40],
("center", self.target_size[1] // 2 + 50 + 30 * (i + 1)),
sdl2.ext.Color(0, 0, 0))
def start_dialog(self, **kwargs):
"""Display the welcome dialog"""
self.dialog("Welcome to the Mice!", subtitle="A game by Matteo because was bored", **kwargs)
def draw_button(self, x, y, text, width, height, coords):
"""Draw a button with text"""
# TODO: Fix outline parameter usage
color = (0, 0, 255) if self.button_cursor == list(coords) else (0, 0, 0)
self.draw_rectangle(x, y, width, height, "button", outline=color)
self.draw_text(text, self.fonts[20], (x + 10, y + 10), (0, 0, 0))
def update_status(self, text):
"""Update and display the status bar with FPS information"""
fps = int(1000 / self.performance) if self.performance != 0 else 0
status_text = f"FPS: {fps} - {text}"
font = self.fonts[20]
sprite = self.factory.from_text(status_text, color=sdl2.ext.Color(0, 0, 0), fontmanager=font)
text_width, text_height = sprite.size
# Draw background for status text
self.renderer.fill((3, 3, text_width + 10, text_height + 4), sdl2.ext.Color(255, 255, 255))
self.draw_text(status_text, font, (8, 5), sdl2.ext.Color(0, 0, 0))
# ======================
# VIEW & NAVIGATION
# ======================
def scroll_view(self, pointer): def scroll_view(self, pointer):
""" """Adjust the view offset based on pointer coordinates"""
Adjusts the view offset based on the given pointer coordinates.
Scales them down by half, then adjusts offsets, ensuring they don't
exceed maximum allowed values.
"""
x, y = pointer x, y = pointer
# Scale down and invert # Scale down and invert coordinates
x = -(x // 2) * self.cell_size x = -(x // 2) * self.cell_size
y = -(y // 2) * self.cell_size y = -(y // 2) * self.cell_size
# Clamp horizontal offset # Clamp horizontal offset to valid range
if x <= self.max_w_offset + self.cell_size: if x <= self.max_w_offset + self.cell_size:
x = self.max_w_offset x = self.max_w_offset
# Clamp vertical offset to valid range
# Clamp vertical offset
if y < self.max_h_offset: if y < self.max_h_offset:
y = self.max_h_offset y = self.max_h_offset
self.w_offset = x self.w_offset = x
self.h_offset = y self.h_offset = y
def is_in_visible_area(self, x, y):
"""Check if coordinates are within the visible area"""
return (-self.w_offset - self.cell_size <= x <= self.width - self.w_offset and
-self.h_offset - self.cell_size <= y <= self.height - self.h_offset)
def get_view_center(self):
"""Get the center coordinates of the current view"""
return self.w_offset + self.width // 2, self.h_offset + self.height // 2
# ======================
# AUDIO METHODS
# ======================
def play_sound(self, sound_file, tag="base"): def play_sound(self, sound_file, tag="base"):
sound_file = os.path.join("sound", sound_file) """Play a sound file on the specified audio channel"""
rw = sdl2.SDL_RWFromFile(byteify(sound_file, "utf-8"), b"rb") sound_path = os.path.join("sound", sound_file)
rw = sdl2.SDL_RWFromFile(byteify(sound_path, "utf-8"), b"rb")
if not rw: if not rw:
raise RuntimeError("Failed to open sound file") raise RuntimeError("Failed to open sound file")
@@ -251,41 +288,116 @@ class GameWindow:
spec = SDL_AudioSpec(freq=22050, aformat=sdl2.AUDIO_U8, channels=1, samples=2048) spec = SDL_AudioSpec(freq=22050, aformat=sdl2.AUDIO_U8, channels=1, samples=2048)
if sdl2.SDL_LoadWAV_RW(rw, 1, byref(spec), byref(_buf), byref(_length)) == None: if sdl2.SDL_LoadWAV_RW(rw, 1, byref(spec), byref(_buf), byref(_length)) == None:
raise RuntimeError("Failed to load WAV") raise RuntimeError("Failed to load WAV")
devid = self.audio_devs[tag] devid = self.audio_devs[tag]
# Clear any queued audio # Clear any queued audio
sdl2.SDL_ClearQueuedAudio(devid) sdl2.SDL_ClearQueuedAudio(devid)
# Start playing audio # Start playing audio
sdl2.SDL_QueueAudio(devid, _buf, _length) sdl2.SDL_QueueAudio(devid, _buf, _length)
sdl2.SDL_PauseAudioDevice(devid, 0) sdl2.SDL_PauseAudioDevice(devid, 0)
def stop_sound(self):
for dev in self.audio_devs:
if not dev[0]:
sdl2.SDL_PauseAudioDevice(dev[1], 1)
sdl2.SDL_ClearQueuedAudio(dev[1])
def start_dialog(self, **kwargs):
self.dialog("Welcome to the Mice!", subtitle="A game by Matteo because was bored", **kwargs)
center = self.get_view_center()
#self.draw_button(center[0], center[1] + 10 * self.scale, "Start", 120, 50, (0, 0))
def draw_button(self, x, y, text, width, height, coords):
if self.button_cursor[0] == coords[0] and self.button_cursor[1] == coords[1]:
color = (0, 0, 255)
self.draw_rectangle(x, y, width, height, "button", outline8u=color)
self.draw_text(text, self.fonts[20], (x + 10, y + 10), (0,0,0))
def get_view_center(self): def stop_sound(self):
return self.w_offset + self.width // 2, self.h_offset + self.height // 2 """Stop all audio playback"""
for dev in self.audio_devs.values():
sdl2.SDL_PauseAudioDevice(dev, 1)
sdl2.SDL_ClearQueuedAudio(dev)
# ======================
# INPUT METHODS
# ======================
def load_joystick(self):
"""Initialize joystick support"""
sdl2.SDL_Init(sdl2.SDL_INIT_JOYSTICK)
sdl2.SDL_JoystickOpen(0)
# ======================
# MAIN GAME LOOP
# ======================
def mainloop(self, **kwargs):
"""Main game loop handling events and rendering"""
while self.running:
performance_start = sdl2.SDL_GetPerformanceCounter()
self.renderer.clear()
# Execute background update if provided
if "bg_update" in kwargs:
kwargs["bg_update"]()
# Execute main update
kwargs["update"]()
# Handle SDL events
events = sdl2.ext.get_events()
for event in events:
if event.type == sdl2.SDL_QUIT:
self.running = False
elif event.type == sdl2.SDL_KEYDOWN and self.key_down:
key = sdl2.SDL_GetKeyName(event.key.keysym.sym).decode('utf-8')
self.key_down(key)
elif event.type == sdl2.SDL_KEYUP and self.key_up:
key = sdl2.SDL_GetKeyName(event.key.keysym.sym).decode('utf-8')
self.key_up(key)
elif event.type == sdl2.SDL_MOUSEMOTION:
self.key_down("mouse", coords=(event.motion.x, event.motion.y))
elif event.type == sdl2.SDL_JOYBUTTONDOWN:
key = event.jbutton.button
self.key_down(key)
elif event.type == sdl2.SDL_JOYBUTTONUP:
key = event.jbutton.button
self.key_up(key)
# Present the rendered frame
self.renderer.present()
# Calculate performance and delay
self.performance = ((sdl2.SDL_GetPerformanceCounter() - performance_start) /
sdl2.SDL_GetPerformanceFrequency() * 1000)
delay = max(0, self.delay - round(self.performance))
sdl2.SDL_Delay(delay)
# ======================
# SPECIAL EFFECTS
# ======================
# ======================
# UTILITY METHODS
# ======================
def new_cycle(self, delay, callback):
"""Placeholder for cycle management (not implemented)"""
pass
def full_screen(self, flag):
"""Toggle fullscreen mode"""
sdl2.SDL_SetWindowFullscreen(self.window.window, flag)
def get_perf_counter(self):
"""Get performance counter for timing"""
return sdl2.SDL_GetPerformanceCounter()
def close(self):
"""Close the game window and cleanup"""
self.running = False
sdl2.ext.quit()
# ======================
# MAIN GAME LOOP
# ======================
# ======================
# SPECIAL EFFECTS
# ======================
def generate_blood_surface(self): def generate_blood_surface(self):
"""Genera dinamicamente una superficie di macchia di sangue usando SDL2""" """Generate a dynamic blood splatter surface using SDL2"""
size = self.cell_size size = self.cell_size
# Crea una superficie RGBA per la macchia di sangue # Create RGBA surface for blood splatter
blood_surface = sdl2.SDL_CreateRGBSurface( blood_surface = sdl2.SDL_CreateRGBSurface(
0, size, size, 32, 0, size, size, 32,
0x000000FF, # R mask 0x000000FF, # R mask
@@ -297,58 +409,56 @@ class GameWindow:
if not blood_surface: if not blood_surface:
return None return None
# Blocca la superficie per il disegno pixel per pixel # Lock surface for pixel manipulation
sdl2.SDL_LockSurface(blood_surface) sdl2.SDL_LockSurface(blood_surface)
# Ottieni i dati dei pixel # Get pixel data
pixels = cast(blood_surface.contents.pixels, POINTER(c_uint32)) pixels = cast(blood_surface.contents.pixels, POINTER(c_uint32))
pitch = blood_surface.contents.pitch // 4 # pitch in pixel (32-bit) pitch = blood_surface.contents.pitch // 4 # Convert pitch to pixels (32-bit)
# Colori del sangue (variazioni di rosso in formato ABGR) # Blood color variations (ABGR format)
blood_colors = [ blood_colors = [
0xFF00008B, # Rosso scuro (A=FF, B=00, G=00, R=8B) 0xFF00008B, # Dark red
0xFF002222, # Rosso mattone (A=FF, B=00, G=22, R=22) 0xFF002222, # Brick red
0xFF003C14, # Cremisi (A=FF, B=00, G=3C, R=14) 0xFF003C14, # Crimson
0xFF0000FF, # Rosso puro (A=FF, B=00, G=00, R=FF) 0xFF0000FF, # Pure red
0xFF000080, # Marrone rossastro (A=FF, B=00, G=00, R=80) 0xFF000080, # Reddish brown
] ]
# Genera la macchia con un algoritmo di diffusione # Generate splatter with diffusion algorithm
center_x, center_y = size // 2, size // 2 center_x, center_y = size // 2, size // 2
# Inizia dal centro e espandi verso l'esterno
max_radius = size // 3 + random.randint(-3, 5) max_radius = size // 3 + random.randint(-3, 5)
for y in range(size): for y in range(size):
for x in range(size): for x in range(size):
# Calcola la distanza dal centro # Calculate distance from center
distance = ((x - center_x) ** 2 + (y - center_y) ** 2) ** 0.5 distance = ((x - center_x) ** 2 + (y - center_y) ** 2) ** 0.5
# Probabilità di avere sangue basata sulla distanza # Calculate blood probability based on distance
if distance <= max_radius: if distance <= max_radius:
# Più vicino al centro, più probabile avere sangue # Closer to center = higher probability
probability = max(0, 1 - (distance / max_radius)) probability = max(0, 1 - (distance / max_radius))
# Aggiungi rumore per forma irregolare # Add noise for irregular shape
noise = random.random() * 0.7 noise = random.random() * 0.7
if random.random() < probability * noise: if random.random() < probability * noise:
# Scegli un colore di sangue casuale # Choose random blood color
color = random.choice(blood_colors) color = random.choice(blood_colors)
# Aggiungi variazione di alpha per trasparenza # Add alpha variation for transparency
alpha = int(255 * probability * random.uniform(0.6, 1.0)) alpha = int(255 * probability * random.uniform(0.6, 1.0))
color = (color & 0x00FFFFFF) | (alpha << 24) color = (color & 0x00FFFFFF) | (alpha << 24)
pixels[y * pitch + x] = color pixels[y * pitch + x] = color
else: else:
# Pixel trasparente # Transparent pixel
pixels[y * pitch + x] = 0x00000000 pixels[y * pitch + x] = 0x00000000
else: else:
# Fuori dal raggio, trasparente # Outside radius, transparent
pixels[y * pitch + x] = 0x00000000 pixels[y * pitch + x] = 0x00000000
# Aggiungi alcune gocce sparse intorno alla macchia principale # Add scattered droplets around main splatter
for _ in range(random.randint(3, 8)): for _ in range(random.randint(3, 8)):
drop_x = center_x + random.randint(-max_radius - 5, max_radius + 5) drop_x = center_x + random.randint(-max_radius - 5, max_radius + 5)
drop_y = center_y + random.randint(-max_radius - 5, max_radius + 5) drop_y = center_y + random.randint(-max_radius - 5, max_radius + 5)
@@ -360,36 +470,36 @@ class GameWindow:
nx, ny = drop_x + dx, drop_y + dy nx, ny = drop_x + dx, drop_y + dy
if 0 <= nx < size and 0 <= ny < size: if 0 <= nx < size and 0 <= ny < size:
if random.random() < 0.6: if random.random() < 0.6:
color = random.choice(blood_colors[:3]) # Colori più scuri per le gocce color = random.choice(blood_colors[:3]) # Darker colors for drops
alpha = random.randint(100, 200) alpha = random.randint(100, 200)
color = (color & 0x00FFFFFF) | (alpha << 24) color = (color & 0x00FFFFFF) | (alpha << 24)
pixels[ny * pitch + nx] = color pixels[ny * pitch + nx] = color
# Sblocca la superficie # Unlock surface
sdl2.SDL_UnlockSurface(blood_surface) sdl2.SDL_UnlockSurface(blood_surface)
# Converte la superficie in una texture usando il factory del gioco
return blood_surface return blood_surface
def draw_blood_surface(self, blood_surface, position): def draw_blood_surface(self, blood_surface, position):
# Create a new surface for the blood texture since bg_surface may have been freed """Convert blood surface to texture and return it"""
# Create temporary surface for blood texture
temp_surface = sdl2.SDL_CreateRGBSurface(0, self.cell_size, self.cell_size, 32, 0, 0, 0, 0) temp_surface = sdl2.SDL_CreateRGBSurface(0, self.cell_size, self.cell_size, 32, 0, 0, 0, 0)
if temp_surface is None: if temp_surface is None:
sdl2.SDL_FreeSurface(blood_surface) sdl2.SDL_FreeSurface(blood_surface)
return None return None
# Copy the blood surface to the temporary surface # Copy blood surface to temporary surface
sdl2.SDL_BlitSurface(blood_surface, None, temp_surface, None) sdl2.SDL_BlitSurface(blood_surface, None, temp_surface, None)
sdl2.SDL_FreeSurface(blood_surface) sdl2.SDL_FreeSurface(blood_surface)
# Create texture from the temporary surface # Create texture from temporary surface
texture = self.factory.from_surface(temp_surface) texture = self.factory.from_surface(temp_surface)
sdl2.SDL_FreeSurface(temp_surface) sdl2.SDL_FreeSurface(temp_surface)
return texture return texture
def combine_blood_surfaces(self, existing_surface, new_surface): def combine_blood_surfaces(self, existing_surface, new_surface):
"""Combine two blood surfaces by blending them together""" """Combine two blood surfaces by blending them together"""
# Create a new surface for the combined result # Create combined surface
combined_surface = sdl2.SDL_CreateRGBSurface( combined_surface = sdl2.SDL_CreateRGBSurface(
0, self.cell_size, self.cell_size, 32, 0, self.cell_size, self.cell_size, 32,
0x000000FF, # R mask 0x000000FF, # R mask
@@ -401,7 +511,7 @@ class GameWindow:
if combined_surface is None: if combined_surface is None:
return existing_surface return existing_surface
# Lock both surfaces for pixel manipulation # Lock surfaces for pixel manipulation
sdl2.SDL_LockSurface(existing_surface) sdl2.SDL_LockSurface(existing_surface)
sdl2.SDL_LockSurface(new_surface) sdl2.SDL_LockSurface(new_surface)
sdl2.SDL_LockSurface(combined_surface) sdl2.SDL_LockSurface(combined_surface)
@@ -411,9 +521,9 @@ class GameWindow:
new_pixels = cast(new_surface.contents.pixels, POINTER(c_uint32)) new_pixels = cast(new_surface.contents.pixels, POINTER(c_uint32))
combined_pixels = cast(combined_surface.contents.pixels, POINTER(c_uint32)) combined_pixels = cast(combined_surface.contents.pixels, POINTER(c_uint32))
pitch = combined_surface.contents.pitch // 4 # pitch in pixels (32-bit) pitch = combined_surface.contents.pitch // 4 # Convert pitch to pixels (32-bit)
# Combine pixels manually for better blending # Combine pixels with additive blending
for y in range(self.cell_size): for y in range(self.cell_size):
for x in range(self.cell_size): for x in range(self.cell_size):
idx = y * pitch + x idx = y * pitch + x
@@ -432,7 +542,7 @@ class GameWindow:
new_g = (new_pixel >> 8) & 0xFF new_g = (new_pixel >> 8) & 0xFF
new_b = new_pixel & 0xFF new_b = new_pixel & 0xFF
# Blend the colors (additive blending for blood accumulation) # Blend colors (additive blending for blood accumulation)
if new_a > 0: # If new pixel has color if new_a > 0: # If new pixel has color
if existing_a > 0: # If existing pixel has color if existing_a > 0: # If existing pixel has color
# Combine both colors, making it darker/more opaque # Combine both colors, making it darker/more opaque
@@ -462,7 +572,7 @@ class GameWindow:
sdl2.SDL_UnlockSurface(combined_surface) sdl2.SDL_UnlockSurface(combined_surface)
return combined_surface return combined_surface
def free_surface(self, surface): def free_surface(self, surface):
"""Safely free an SDL surface""" """Safely free an SDL surface"""
if surface is not None: if surface is not None: