"""
Sprite Sheets Example - Full Feature Showcase for LunaEngine
LOCATION: examples/spritesheets.py
DESCRIPTION:
Demonstrates all capabilities of the SpriteSheet and Animation system:
- Time-based animations with fade in/out
- Colour replacement, tinting, painting, colour-to-alpha
- Mask creation and visualisation
- Resizing / scaling of sprites
- Live effect preview on the running animation
- Preview uses a raw sprite sheet frame (not a copy from the animation)
"""
import sys
import os
sys.path.append(os.path.join(os.path.dirname(__file__), '..'))
import pygame
from lunaengine.core import Scene, LunaEngine
from lunaengine.ui import *
from lunaengine.graphics.spritesheet import SpriteSheet, Animation
# ------------------------------------------------------------
# Main Demo Scene
# ------------------------------------------------------------
class SpriteSheetTestScene(Scene):
"""
Full demo: animation controls + real-time sprite manipulation effects.
"""
def __init__(self, engine: LunaEngine):
super().__init__(engine)
self.CurrentTheme = ThemeType.DEFAULT
# Animation instances
self.walk_animation = None
self.idle_animation = None
self.current_animation = None
self.current_animation_name = "Walk"
# SpriteSheet reference for raw frames
self.sprite_sheet = None
self.raw_frame = None # unmodified frame from sprite sheet
self.raw_frame_rect = None # (x, y, width, height) of the frame
# Settings
self.animation_scale = 2.0
self.animation_speed = 1.0
self.fade_in_duration = 1.0
self.fade_out_duration = 1.0
self.auto_fade_transitions = True
# Effect management
self.current_effect = "Original"
self.preview_surface = None # effect applied to raw_frame
self.setup_ui()
# Keyboard shortcuts
@engine.on_event(pygame.KEYDOWN)
def on_key_press(event):
self.handle_key_press(event.key)
def on_enter(self, previous_scene=None):
super().on_enter(previous_scene)
self.load_animations()
if self.current_animation:
self.current_animation.reset()
self.current_animation.play()
# ------------------------------------------------------------
# Animation Loading (with fallback placeholders)
# ------------------------------------------------------------
def load_animations(self):
"""Load the tiki texture animations and store a raw frame for preview."""
try:
texture_path = './examples/tiki_texture.png'
self.sprite_sheet = SpriteSheet(texture_path) # using pathlib internally
# Define the rectangle for the first walk frame (size 70x70 at (0,0))
self.raw_frame_rect = pygame.Rect(0, 0, 70, 70)
self.raw_frame = self.sprite_sheet.get_sprite_at_rect(self.raw_frame_rect)
# Walk animation: 6 frames starting at (0,0)
self.walk_animation = Animation(
spritesheet_file=self.sprite_sheet,
size=(70, 70),
start_pos=(0, 0),
frame_count=6,
scale=(self.animation_scale, self.animation_scale),
duration=1.0 / self.animation_speed,
loop=True,
fade_in_duration=self.fade_in_duration,
fade_out_duration=self.fade_out_duration
)
# Idle animation: 6 frames starting at (0,70)
self.idle_animation = Animation(
spritesheet_file=self.sprite_sheet,
size=(70, 70),
start_pos=(0, 70),
frame_count=6,
scale=(self.animation_scale, self.animation_scale),
duration=1.5 / self.animation_speed,
loop=True,
fade_in_duration=self.fade_in_duration,
fade_out_duration=self.fade_out_duration
)
self.current_animation = self.walk_animation
self.current_animation_name = "Walk"
# Update preview using the raw frame
self.update_preview()
print("Animations loaded - full sprite features ready.")
except Exception as e:
print(f"Warning: Could not load tiki_texture.png: {e}")
self.create_placeholder_animations()
def create_placeholder_animations(self):
"""Fallback: coloured squares if texture is missing."""
# Create a placeholder raw frame
self.raw_frame = pygame.Surface((70, 70), pygame.SRCALPHA)
self.raw_frame.fill((255, 0, 255)) # magenta
pygame.draw.rect(self.raw_frame, (255, 255, 255), self.raw_frame.get_rect(), 2)
colors = [(255,0,0), (0,255,0), (0,0,255), (255,255,0)]
frames = []
for color in colors:
surf = pygame.Surface((70,70), pygame.SRCALPHA)
surf.fill(color)
pygame.draw.rect(surf, (255,255,255), surf.get_rect(), 2)
if self.animation_scale != 1.0:
surf = pygame.transform.scale(surf, (int(70*self.animation_scale), int(70*self.animation_scale)))
frames.append(surf)
class DummyAnimation:
def __init__(self, frames, duration, fade_in, fade_out):
self.frames = frames
self.duration = duration
self.frame_duration = duration / len(frames)
self.current_frame_index = 0
self.last_update = pygame.time.get_ticks()/1000
self.acc_time = 0.0
self.playing = True
self.loop = True
self.fade_in_duration = fade_in
self.fade_out_duration = fade_out
self.fade_alpha = 0 if fade_in>0 else 255
self.fade_mode = 'in' if fade_in>0 else None
self.fade_start = pygame.time.get_ticks()/1000 if fade_in>0 else None
def update(self):
if not self.playing or len(self.frames)<=1:
return
now = pygame.time.get_ticks()/1000
dt = now - self.last_update
self.last_update = now
self.acc_time += dt
adv = int(self.acc_time / self.frame_duration)
if adv:
self.acc_time -= adv * self.frame_duration
if self.loop:
self.current_frame_index = (self.current_frame_index + adv) % len(self.frames)
else:
self.current_frame_index = min(self.current_frame_index+adv, len(self.frames)-1)
def get_current_frame(self):
return self.frames[self.current_frame_index]
def reset(self):
self.current_frame_index = 0
self.acc_time = 0.0
self.playing = True
def play(self):
self.playing = True
def pause(self):
self.playing = False
def get_frame_count(self):
return len(self.frames)
def set_duration(self, d):
self.duration = d
def start_fade_in(self, d=None):
pass
def start_fade_out(self, d=None):
pass
self.walk_animation = DummyAnimation(frames, 1.0/self.animation_speed, self.fade_in_duration, self.fade_out_duration)
self.idle_animation = DummyAnimation(frames[::-1], 1.5/self.animation_speed, self.fade_in_duration, self.fade_out_duration)
self.current_animation = self.walk_animation
self.current_animation_name = "Walk (Placeholder)"
self.update_preview()
# ------------------------------------------------------------
# Effect Application (using raw frame for preview)
# ------------------------------------------------------------
def update_preview(self):
"""Apply selected effect to the raw sprite sheet frame."""
if self.raw_frame is None:
return
frame = self.raw_frame.copy() # copy for preview (safe, only once per effect change)
effect = self.current_effect
if effect == "Original":
pass
elif effect == "Replace Color (red->green)":
frame = SpriteSheet.replace_color(frame, (255,0,0), (0,255,0), tolerance=30)
elif effect == "Tint (blue 50%)":
frame = SpriteSheet.tint(frame, (50,100,255), intensity=0.5, blend_mode="multiply")
elif effect == "Paint (solid yellow)":
frame = SpriteSheet.paint(frame, (255,255,0), preserve_alpha=True)
elif effect == "Color to Alpha (remove green)":
frame = SpriteSheet.color_to_alpha(frame, (0,255,0), threshold=40)
elif effect == "Resize (2x)":
frame = SpriteSheet.resize_surface(frame, frame.get_width()*2, frame.get_height()*2, smooth=True)
elif effect == "Scale (0.5x)":
frame = SpriteSheet.scale_surface(frame, 0.5, 0.5, smooth=True)
elif effect == "Mask Outline":
mask = SpriteSheet.create_mask(frame, threshold=10)
w, h = frame.get_size()
# Use a separate surface to avoid modifying during iteration
outline_surface = frame.copy()
for x in range(w):
for y in range(h):
if not mask.get_at((x, y)):
continue
# Check neighbours (with bounds checking)
left = mask.get_at((x-1, y)) if x > 0 else False
right = mask.get_at((x+1, y)) if x < w-1 else False
top = mask.get_at((x, y-1)) if y > 0 else False
bottom = mask.get_at((x, y+1)) if y < h-1 else False
if not (left and right and top and bottom):
outline_surface.set_at((x, y), (255, 0, 0, 255))
frame = outline_surface
self.preview_surface = frame
def apply_effect_to_surface(self, surface):
"""Apply current effect to a live animation frame."""
if self.current_effect == "Original":
return surface
result = surface.copy()
effect = self.current_effect
if effect == "Replace Color (red->green)":
result = SpriteSheet.replace_color(result, (255,0,0), (0,255,0), tolerance=30)
elif effect == "Tint (blue 50%)":
result = SpriteSheet.tint(result, (50,100,255), intensity=0.5)
elif effect == "Paint (solid yellow)":
result = SpriteSheet.paint(result, (255,255,0), preserve_alpha=True)
elif effect == "Color to Alpha (remove green)":
result = SpriteSheet.color_to_alpha(result, (0,255,0), threshold=40)
elif effect == "Resize (2x)":
result = SpriteSheet.resize_surface(result, result.get_width()*2, result.get_height()*2, smooth=True)
elif effect == "Scale (0.5x)":
result = SpriteSheet.scale_surface(result, 0.5, 0.5, smooth=True)
elif effect == "Mask Outline":
mask = SpriteSheet.create_mask(result, threshold=10)
w, h = result.get_size()
outline_surface = result.copy()
for x in range(w):
for y in range(h):
if not mask.get_at((x, y)):
continue
left = mask.get_at((x-1, y)) if x > 0 else False
right = mask.get_at((x+1, y)) if x < w-1 else False
top = mask.get_at((x, y-1)) if y > 0 else False
bottom = mask.get_at((x, y+1)) if y < h-1 else False
if not (left and right and top and bottom):
outline_surface.set_at((x, y), (255, 0, 0, 255))
result = outline_surface
return result
# ------------------------------------------------------------
# UI Setup (Tabs)
# ------------------------------------------------------------
def setup_ui(self):
title = TextLabel(512, 30, "Sprite Sheet System - Full Feature Demo", 36,
root_point=(0.5,0), theme=self.CurrentTheme)
self.add_ui_element(title)
# Adjusted width to 920 (was 980) to prevent overlap, and height 580 (was 650)
self.main_tabs = Tabination(25, 90, 700, 580, 20)
self.main_tabs.add_tab("Animation")
self.main_tabs.add_tab("Sprite Effects")
self.setup_animation_tab()
self.setup_effects_tab()
self.add_ui_element(self.main_tabs)
# Back button
back_btn = Button(50, 50, 120, 30, "<- Main Menu", 20,
root_point=(0,0), theme=self.CurrentTheme)
back_btn.set_on_click(lambda: self.engine.set_scene("MainMenu"))
self.add_ui_element(back_btn)
def setup_animation_tab(self):
"""Traditional animation controls (speed, zoom, fade, switch)."""
tab = "Animation"
self.main_tabs.add_to_tab(tab, TextLabel(10, 10, "Animation Controls", 24, (255,255,0)))
# Animation info
self.anim_name_label = TextLabel(10, 50, f"Current: {self.current_animation_name}", 20, (200,200,255))
self.main_tabs.add_to_tab(tab, self.anim_name_label)
self.frame_label = TextLabel(10, 80, "Frame: 0/0", 16)
self.main_tabs.add_to_tab(tab, self.frame_label)
# Speed slider
speed_label = TextLabel(10, 120, "Animation Speed:", 16, (200,200,255))
self.main_tabs.add_to_tab(tab, speed_label)
speed_slider = Slider(180, 115, 200, 25, 0.5, 3.0, self.animation_speed)
speed_slider.on_value_changed = self.on_speed_changed
self.main_tabs.add_to_tab(tab, speed_slider)
self.speed_val = TextLabel(390, 120, f"{self.animation_speed:.1f}x", 16)
self.main_tabs.add_to_tab(tab, self.speed_val)
# Zoom slider
zoom_label = TextLabel(10, 165, "Zoom Level:", 16, (200,200,255))
self.main_tabs.add_to_tab(tab, zoom_label)
zoom_slider = Slider(180, 160, 200, 25, 0.5, 4.0, self.animation_scale)
zoom_slider.on_value_changed = self.on_zoom_changed
self.main_tabs.add_to_tab(tab, zoom_slider)
self.zoom_val = TextLabel(390, 165, f"{self.animation_scale:.1f}x", 16)
self.main_tabs.add_to_tab(tab, self.zoom_val)
# Fade sliders
fade_in_label = TextLabel(10, 210, "Fade-in Duration:", 16, (200,200,255))
self.main_tabs.add_to_tab(tab, fade_in_label)
fade_in_slider = Slider(180, 205, 200, 25, 0.0, 3.0, self.fade_in_duration)
fade_in_slider.on_value_changed = self.on_fade_in_changed
self.main_tabs.add_to_tab(tab, fade_in_slider)
self.fade_in_val = TextLabel(390, 210, f"{self.fade_in_duration:.1f}s", 16)
self.main_tabs.add_to_tab(tab, self.fade_in_val)
fade_out_label = TextLabel(10, 255, "Fade-out Duration:", 16, (200,200,255))
self.main_tabs.add_to_tab(tab, fade_out_label)
fade_out_slider = Slider(180, 250, 200, 25, 0.0, 3.0, self.fade_out_duration)
fade_out_slider.on_value_changed = self.on_fade_out_changed
self.main_tabs.add_to_tab(tab, fade_out_slider)
self.fade_out_val = TextLabel(390, 255, f"{self.fade_out_duration:.1f}s", 16)
self.main_tabs.add_to_tab(tab, self.fade_out_val)
# Auto-fade toggle
auto_fade_switch = Switch(10, 295, 60, 30, self.auto_fade_transitions)
auto_fade_switch.set_on_toggle(lambda v: setattr(self, 'auto_fade_transitions', v))
self.main_tabs.add_to_tab(tab, auto_fade_switch)
auto_label = TextLabel(80, 300, "Auto fade on switch", 16)
self.main_tabs.add_to_tab(tab, auto_label)
# Buttons
btn_y = 340
play_btn = Button(10, btn_y, 80, 30, "Play")
play_btn.set_on_click(self.play_animation)
self.main_tabs.add_to_tab(tab, play_btn)
pause_btn = Button(100, btn_y, 80, 30, "Pause")
pause_btn.set_on_click(self.pause_animation)
self.main_tabs.add_to_tab(tab, pause_btn)
reset_btn = Button(190, btn_y, 80, 30, "Reset")
reset_btn.set_on_click(self.reset_animation)
self.main_tabs.add_to_tab(tab, reset_btn)
switch_anim_btn = Button(280, btn_y, 120, 30, "Switch Anim")
switch_anim_btn.set_on_click(self.switch_animation)
self.main_tabs.add_to_tab(tab, switch_anim_btn)
apply_btn = Button(410, btn_y, 120, 30, "Apply Settings")
apply_btn.set_on_click(self.reload_animations)
self.main_tabs.add_to_tab(tab, apply_btn)
# Info (split into multiple lines)
info_y = 400
info1 = TextLabel(10, info_y, "Keyboard shortcuts:", 14, (150,150,200))
info2 = TextLabel(10, info_y+20, "SPACE - switch animation", 14, (150,150,200))
info3 = TextLabel(10, info_y+40, "R - reset", 14, (150,150,200))
info4 = TextLabel(10, info_y+60, "P - play/pause", 14, (150,150,200))
info5 = TextLabel(10, info_y+80, "I/O - fade in/out", 14, (150,150,200))
info6 = TextLabel(10, info_y+100, "F - toggle auto fade", 14, (150,150,200))
self.main_tabs.add_to_tab(tab, info1)
self.main_tabs.add_to_tab(tab, info2)
self.main_tabs.add_to_tab(tab, info3)
self.main_tabs.add_to_tab(tab, info4)
self.main_tabs.add_to_tab(tab, info5)
self.main_tabs.add_to_tab(tab, info6)
def setup_effects_tab(self):
"""Tab for sprite manipulation effects, with preview on the right side."""
tab = "Sprite Effects"
self.main_tabs.add_to_tab(tab, TextLabel(10, 10, "Sprite Manipulation", 24, (255,255,0)))
# Effect selector
effect_label = TextLabel(20, 50, "Select Effect:", 18, (200,200,255))
self.main_tabs.add_to_tab(tab, effect_label)
effect_options = [
"Original",
"Replace Color (red->green)",
"Tint (blue 50%)",
"Paint (solid yellow)",
"Color to Alpha (remove green)",
"Resize (2x)",
"Scale (0.5x)",
"Mask Outline"
]
effect_dropdown = Dropdown(180, 45, 250, 35, effect_options)
effect_dropdown.set_on_selection_changed(lambda i, val: self.set_effect(val))
self.main_tabs.add_to_tab(tab, effect_dropdown)
# --- Preview placed on the right side (using absolute positioning inside tab) ---
preview_container = UiFrame(425, 90, 200, 220)
preview_container.set_background_color((30,30,40))
preview_container.set_border((100,100,150), 2)
preview_container.set_corner_radius(8)
self.main_tabs.add_to_tab(tab, preview_container)
# Label inside container
preview_label = TextLabel(100, 10, "Effect Preview", 16, (220,220,255), root_point=(0.5,0))
preview_container.add_child(preview_label)
# This ImageLabel will hold the preview image; updated in update()
self.preview_image = ImageLabel(100, 110, None, 100, 100, root_point=(0.5,0.5))
preview_container.add_child(self.preview_image)
# Explanation text on the left side
explanation = UiFrame(20, 100, 380, 450)
explanation.set_background_color((20,20,30, 200))
explanation.set_corner_radius(5)
self.main_tabs.add_to_tab(tab, explanation)
exp_y = 10
exp1 = TextLabel(10, exp_y, "SpriteSheet static methods:", 14, (180,180,200))
explanation.add_child(exp1)
exp2 = TextLabel(10, exp_y+20, "replace_color() - change one colour to another", 12, (180,180,200))
explanation.add_child(exp2)
exp3 = TextLabel(10, exp_y+40, "tint() - apply colour tint (multiply/add/overlay)", 12, (180,180,200))
explanation.add_child(exp3)
exp4 = TextLabel(10, exp_y+60, "paint() - fill sprite with solid colour (preserves alpha)", 12, (180,180,200))
explanation.add_child(exp4)
exp5 = TextLabel(10, exp_y+80, "color_to_alpha() - turn a colour transparent", 12, (180,180,200))
explanation.add_child(exp5)
exp6 = TextLabel(10, exp_y+100, "resize_surface() / scale_surface()", 12, (180,180,200))
explanation.add_child(exp6)
exp7 = TextLabel(10, exp_y+120, "create_mask() - generate collision mask", 12, (180,180,200))
explanation.add_child(exp7)
exp8 = TextLabel(10, exp_y+160, "The selected effect is applied LIVE to the running animation.", 12, (200,200,255))
explanation.add_child(exp8)
exp9 = TextLabel(10, exp_y+180, "Preview window shows the effect on a static raw frame.", 12, (200,200,255))
explanation.add_child(exp9)
# ------------------------------------------------------------
# Event Handlers
# ------------------------------------------------------------
def set_effect(self, effect_name):
self.current_effect = effect_name
self.update_preview()
print(f"Effect changed to: {effect_name}")
def on_speed_changed(self, val):
self.animation_speed = val
self.speed_val.set_text(f"{val:.1f}x")
if self.current_animation:
new_dur = (1.0 if self.current_animation == self.walk_animation else 1.5) / val
self.current_animation.set_duration(new_dur)
def on_zoom_changed(self, val):
self.animation_scale = val
self.zoom_val.set_text(f"{val:.1f}x")
self.reload_animations()
def on_fade_in_changed(self, val):
self.fade_in_duration = val
self.fade_in_val.set_text(f"{val:.1f}s")
def on_fade_out_changed(self, val):
self.fade_out_duration = val
self.fade_out_val.set_text(f"{val:.1f}s")
def reload_animations(self):
"""Reload animations with current scale, speed and fade settings."""
was_walk = (self.current_animation == self.walk_animation) if self.current_animation else True
self.load_animations()
if was_walk:
self.current_animation = self.walk_animation
self.current_animation_name = "Walk"
else:
self.current_animation = self.idle_animation
self.current_animation_name = "Idle"
self.anim_name_label.set_text(f"Current: {self.current_animation_name}")
if self.current_animation:
self.current_animation.reset()
self.current_animation.play()
def switch_animation(self):
if self.auto_fade_transitions and self.current_animation:
self.current_animation.start_fade_out(self.fade_out_duration)
# Switch
if self.current_animation == self.walk_animation:
self.current_animation = self.idle_animation
self.current_animation_name = "Idle"
else:
self.current_animation = self.walk_animation
self.current_animation_name = "Walk"
if self.auto_fade_transitions:
self.current_animation.start_fade_in(self.fade_in_duration)
self.current_animation.reset()
self.current_animation.play()
self.anim_name_label.set_text(f"Current: {self.current_animation_name}")
def play_animation(self):
if self.current_animation:
self.current_animation.play()
def pause_animation(self):
if self.current_animation:
self.current_animation.pause()
def reset_animation(self):
if self.current_animation:
self.current_animation.reset()
def handle_key_press(self, key):
if key == pygame.K_ESCAPE:
self.engine.set_scene("MainMenu")
elif key == pygame.K_SPACE:
self.switch_animation()
elif key == pygame.K_r:
self.reset_animation()
elif key == pygame.K_p:
if self.current_animation and self.current_animation.playing:
self.pause_animation()
else:
self.play_animation()
elif key == pygame.K_i:
if self.current_animation:
self.current_animation.start_fade_in(self.fade_in_duration)
elif key == pygame.K_o:
if self.current_animation:
self.current_animation.start_fade_out(self.fade_out_duration)
elif key == pygame.K_f:
self.auto_fade_transitions = not self.auto_fade_transitions
# ------------------------------------------------------------
# Update and Render
# ------------------------------------------------------------
def update(self, dt):
if self.current_animation:
self.current_animation.update()
# Frame counter
fc = self.current_animation.get_frame_count()
if fc > 0:
cur = self.current_animation.current_frame_index + 1
self.frame_label.set_text(f"Frame: {cur}/{fc}")
# Get frame and apply effect
frame = self.current_animation.get_current_frame()
self.effected_frame = self.apply_effect_to_surface(frame)
# Update preview image with the static preview surface
if hasattr(self, 'preview_surface') and self.preview_surface:
preview = self.preview_surface
# Fit into 200x200 (the container is 250x250, image area ~200x200)
max_w, max_h = 200, 200
scale = min(max_w/preview.get_width(), max_h/preview.get_height())
if scale != 1.0:
preview = SpriteSheet.scale_surface(preview, scale, scale, smooth=True)
self.preview_image.set_image(preview)
self.preview_image.width = preview.get_width()
self.preview_image.height = preview.get_height()
def render(self, renderer):
renderer.fill_screen(ThemeManager.get_color('background'))
# Draw the animated sprite with effect applied (positioned to the right of tabs)
if hasattr(self, 'effected_frame') and self.effected_frame:
frame = self.effected_frame
# Place it near the right edge (centered between tab end and window edge)
x = self.engine.width - frame.get_width() - 50
y = (self.engine.height - frame.get_height()) // 2
renderer.draw_rect(x, y, frame.get_width(), frame.get_height(),
(255,255,255), fill=False)
renderer.draw_surface(frame, x, y)
# Effect name text
font = pygame.font.Font(None, 24)
info_surf = font.render(f"Effect: {self.current_effect}", True, (255,255,255))
renderer.draw_surface(info_surf, x, y + frame.get_height() + 10)
# Render UI
for element in self.ui_elements:
element.render(renderer)
# ------------------------------------------------------------
# Main Menu Scene
# ------------------------------------------------------------
class MainMenuScene(Scene):
def __init__(self, engine: LunaEngine):
super().__init__(engine)
self.CurrentTheme = ThemeType.DEFAULT
title = TextLabel(512, 150, "Sprite Sheet Full Demo", 72, root_point=(0.5,0), theme=self.CurrentTheme)
self.add_ui_element(title)
subtitle = TextLabel(512, 220, "LunaEngine - Animation + Sprite Effects", 32, root_point=(0.5,0), theme=self.CurrentTheme)
self.add_ui_element(subtitle)
start_btn = Button(512, 300, 250, 40, "Start Demo", 28, root_point=(0.5,0), theme=self.CurrentTheme)
start_btn.set_on_click(lambda: engine.set_scene("SpriteSheetTest"))
self.add_ui_element(start_btn)
exit_btn = Button(512, 360, 250, 40, "Exit", 28, root_point=(0.5,0), theme=self.CurrentTheme)
exit_btn.set_on_click(lambda: setattr(engine, 'running', False))
self.add_ui_element(exit_btn)
def update(self, dt):
pass
def render(self, renderer):
renderer.fill_screen(ThemeManager.get_color('background'))
for el in self.ui_elements:
el.render(renderer)
def main():
engine = LunaEngine("LunaEngine - Sprite Sheet Full Demo", 1024, 720)
engine.fps = 60
engine.add_scene("MainMenu", MainMenuScene)
engine.add_scene("SpriteSheetTest", SpriteSheetTestScene)
engine.set_scene("MainMenu")
engine.run()
if __name__ == "__main__":
main()
Sprite Sheets Example - Full Feature Showcase for LunaEngine