When developing mobile 2D games using Python, Pygame, and Pydroid 3 on Android, rotating the device frequently causes application crashes or frozen rendering loops. This core instability stems from how the Android operating system handles application lifecycles and SDL video contexts. When an orientation shift occurs, the system destroys the underlying hardware surface. Standard Pygame implementations try to catch this through resize events or re-initialize the surface using display mode settings. On Android architectures, this dynamic re-allocation often triggers memory faults or direct binary crashes. Furthermore, fixed pixel logic locks game entities into rigid coordinate boundaries, causing objects to render off-screen when the display aspect ratio changes
To solve these performance bottlenecks, this script implements a robust mobile rendering pattern that combines three key engineering strategies.
Non-Destructive Context Polling: Instead of intercepting risky window resize events that break SDL bindings, this script queries hardware display properties dynamically per frame using system info calls. This approach preserves the active video memory context, preventing system-level crashes.
Delta Time Frame-Independent Physics: Traditional loop iterations rely on rigid frame counts, which fluctuate wildly during background tasks or screen rotations. This script measures precise time deltas, ensuring that entity velocity calculations remain strictly constant in pixels per second regardless of processor load or refresh rates.
Proportional Relative Mapping: To preserve entity placement during aspect ratio flips, this script tracks horizontal coordinates as normalized fractions. When viewport dimensions swap, entities instantly project to their accurate proportional coordinates without scaling distortion.
Let's see the source code:
import pygame
import random
pygame.init()
info = pygame.display.Info()
WIDTH, HEIGHT = info.current_w, info.current_h
screen = pygame.display.set_mode((WIDTH, HEIGHT), pygame.FULLSCREEN)
clock = pygame.time.Clock()
class FallingBox:
def __init__(self, rel_x):
self.rel_x = rel_x # Horizontal percentage (kept during rotation)
self.y_pixels = random.uniform(0, 100) # Current vertical pixel position
self.size = 50 # Box size in pixels
# Maximum falling distance (recalculated dynamically)
self.max_dist_pixels = random.randint(150, 400)
# Constant speed in pixels per second
self.speed_pps = random.uniform(150.0, 300.0)
self.color = (
random.randint(100, 255),
random.randint(100, 255),
random.randint(100, 255)
)
def update(self, dt, current_h):
# Time‑based movement (dt in seconds)
self.y_pixels += self.speed_pps * dt
# Reset if reaching limit or bottom of screen
if self.y_pixels >= self.max_dist_pixels or self.y_pixels >= current_h - self.size:
self.y_pixels = 0.0
self.max_dist_pixels = random.randint(150, int(current_h * 0.6))
self.speed_pps = random.uniform(150.0, 300.0)
def draw(self, surface, current_w):
# X position scales with screen width
pos_x = int(self.rel_x * current_w) - (self.size // 2)
pos_y = int(self.y_pixels)
# Guide line
limit_y = int(self.max_dist_pixels)
pygame.draw.line(surface, (70, 70, 90), (pos_x, limit_y), (pos_x + self.size, limit_y), 2)
# Draw the box
pygame.draw.rect(surface, self.color, (pos_x, pos_y, self.size, self.size))
# Create 4 falling boxes
boxes = [FallingBox(i / 5.0) for i in range(1, 5)]
running = True
while running:
dt = clock.tick(60) / 1000.0 # Frame time in seconds
for event in pygame.event.get():
if event.type == pygame.QUIT:
running = False
# Update screen size dynamically (rotation, resizing)
info = pygame.display.Info()
current_w, current_h = info.current_w, info.current_h
# Update boxes
for box in boxes:
box.update(dt, current_h)
# Render
screen.fill((20, 20, 30))
for box in boxes:
box.draw(screen, current_w)
pygame.display.flip()
pygame.quit()