630 lines
17 KiB
Python
630 lines
17 KiB
Python
from __future__ import annotations
|
|
|
|
from typing import Any, Dict, List, Optional, Tuple, Type, TypeVar
|
|
import pyray as rl
|
|
|
|
T = TypeVar("T")
|
|
|
|
|
|
class Component:
|
|
"""Base class for all game object components.
|
|
|
|
Attributes:
|
|
owner: The GameObject that owns this component, or None if unassigned.
|
|
"""
|
|
def __init__(self) -> None:
|
|
self.owner: Optional[GameObject] = None
|
|
|
|
def init(self) -> None:
|
|
"""Lifecycle hook called when the component is initialized.
|
|
|
|
Returns:
|
|
None
|
|
"""
|
|
pass
|
|
|
|
def update(self, delta_time: float) -> None:
|
|
"""Lifecycle hook called every frame to update the component.
|
|
|
|
Args:
|
|
delta_time: Seconds since the last frame.
|
|
|
|
Returns:
|
|
None
|
|
"""
|
|
pass
|
|
|
|
def draw(self) -> None:
|
|
"""Lifecycle hook called every frame to draw the component.
|
|
|
|
Returns:
|
|
None
|
|
"""
|
|
pass
|
|
|
|
|
|
class GameObject:
|
|
"""Base class for all game objects (entities) in a scene.
|
|
|
|
Attributes:
|
|
scene: The Scene this object belongs to.
|
|
components: Mapping of component type to component instance.
|
|
tags: Set of string tags for lookup/filtering.
|
|
is_active: If False, update/draw are skipped.
|
|
"""
|
|
def __init__(self) -> None:
|
|
self.scene: Optional[Scene] = None
|
|
self.components: Dict[Type[Any], Component] = {}
|
|
self.tags: set[str] = set()
|
|
self.is_active: bool = True
|
|
|
|
def init(self) -> None:
|
|
"""Lifecycle hook called when the object is initialized.
|
|
|
|
Returns:
|
|
None
|
|
"""
|
|
pass
|
|
|
|
def update(self, delta_time: float) -> None:
|
|
"""Lifecycle hook called every frame to update the object.
|
|
|
|
Args:
|
|
delta_time: Seconds since the last frame.
|
|
|
|
Returns:
|
|
None
|
|
"""
|
|
pass
|
|
|
|
def draw(self) -> None:
|
|
"""Lifecycle hook called every frame to draw the object.
|
|
|
|
Returns:
|
|
None
|
|
"""
|
|
pass
|
|
|
|
def init_object(self) -> None:
|
|
"""Initialize the object and its components.
|
|
|
|
Returns:
|
|
None
|
|
"""
|
|
self.init()
|
|
for component in list(self.components.values()):
|
|
component.init()
|
|
|
|
def update_object(self, delta_time: float) -> None:
|
|
"""Update the object and its components if active.
|
|
|
|
Args:
|
|
delta_time: Seconds since the last frame.
|
|
|
|
Returns:
|
|
None
|
|
"""
|
|
if not self.is_active:
|
|
return
|
|
self.update(delta_time)
|
|
for component in list(self.components.values()):
|
|
component.update(delta_time)
|
|
|
|
def draw_object(self) -> None:
|
|
"""Draw the object and its components if active.
|
|
|
|
Returns:
|
|
None
|
|
"""
|
|
if not self.is_active:
|
|
return
|
|
self.draw()
|
|
for component in list(self.components.values()):
|
|
component.draw()
|
|
|
|
def add_component(self, component_or_cls: Any, *args: Any, **kwargs: Any) -> Component:
|
|
"""Add a component instance or construct one from a class.
|
|
|
|
Args:
|
|
component_or_cls: A Component instance or a Component class.
|
|
*args: Positional args forwarded to the component constructor.
|
|
**kwargs: Keyword args forwarded to the component constructor.
|
|
|
|
Returns:
|
|
The component instance added.
|
|
"""
|
|
if isinstance(component_or_cls, Component):
|
|
component = component_or_cls
|
|
else:
|
|
component = component_or_cls(*args, **kwargs)
|
|
component.owner = self
|
|
key = component.__class__
|
|
if key in self.components:
|
|
print(f"Duplicate component added: {key.__name__}")
|
|
self.components[key] = component
|
|
return component
|
|
|
|
def get_component(self, cls: Type[T]) -> Optional[T]:
|
|
"""Get a component by type, if present.
|
|
|
|
Args:
|
|
cls: Component class to look up.
|
|
|
|
Returns:
|
|
The component instance if found, otherwise None.
|
|
"""
|
|
component = self.components.get(cls)
|
|
return component if component is None else component # type: ignore[return-value]
|
|
|
|
def add_tag(self, tag: str) -> None:
|
|
"""Add a tag to this object.
|
|
|
|
Args:
|
|
tag: Tag string to add.
|
|
|
|
Returns:
|
|
None
|
|
"""
|
|
self.tags.add(tag)
|
|
|
|
def remove_tag(self, tag: str) -> None:
|
|
"""Remove a tag from this object.
|
|
|
|
Args:
|
|
tag: Tag string to remove.
|
|
|
|
Returns:
|
|
None
|
|
"""
|
|
self.tags.discard(tag)
|
|
|
|
def has_tag(self, tag: str) -> bool:
|
|
"""Check if a tag is present.
|
|
|
|
Args:
|
|
tag: Tag string to check.
|
|
|
|
Returns:
|
|
True if the tag is present, otherwise False.
|
|
"""
|
|
return tag in self.tags
|
|
|
|
|
|
class Service:
|
|
"""Base class for scene-level services.
|
|
|
|
Attributes:
|
|
scene: The Scene this service is attached to.
|
|
is_init: True once init_service has been run.
|
|
is_visible: If False, draw_service is skipped.
|
|
"""
|
|
def __init__(self) -> None:
|
|
self.scene: Optional[Scene] = None
|
|
self.is_init: bool = False
|
|
self.is_visible: bool = True
|
|
|
|
def init(self) -> None:
|
|
"""Lifecycle hook called when the service is initialized.
|
|
|
|
Returns:
|
|
None
|
|
"""
|
|
pass
|
|
|
|
def update(self, delta_time: float) -> None:
|
|
"""Lifecycle hook called every frame to update the service.
|
|
|
|
Args:
|
|
delta_time: Seconds since the last frame.
|
|
|
|
Returns:
|
|
None
|
|
"""
|
|
pass
|
|
|
|
def draw(self) -> None:
|
|
"""Lifecycle hook called every frame to draw the service.
|
|
|
|
Returns:
|
|
None
|
|
"""
|
|
pass
|
|
|
|
def init_service(self) -> None:
|
|
"""Initialize the service once.
|
|
|
|
Returns:
|
|
None
|
|
"""
|
|
if self.is_init:
|
|
return
|
|
self.init()
|
|
self.is_init = True
|
|
|
|
def draw_service(self) -> None:
|
|
"""Draw the service if visible.
|
|
|
|
Returns:
|
|
None
|
|
"""
|
|
if self.is_visible:
|
|
self.draw()
|
|
|
|
|
|
class Manager:
|
|
"""Base class for global managers.
|
|
|
|
Attributes:
|
|
is_init: True once init_manager has been run.
|
|
"""
|
|
def __init__(self) -> None:
|
|
self.is_init: bool = False
|
|
|
|
def init(self) -> None:
|
|
"""Lifecycle hook called when the manager is initialized.
|
|
|
|
Returns:
|
|
None
|
|
"""
|
|
pass
|
|
|
|
def init_manager(self) -> None:
|
|
"""Initialize the manager once.
|
|
|
|
Returns:
|
|
None
|
|
"""
|
|
if self.is_init:
|
|
return
|
|
self.init()
|
|
self.is_init = True
|
|
|
|
|
|
class Scene:
|
|
"""Base class for scenes that contain objects and services.
|
|
|
|
Attributes:
|
|
game_objects: List of GameObjects in the scene.
|
|
services: List of (type, Service) pairs.
|
|
game: Owning Game instance.
|
|
is_init: True once init_scene has been run.
|
|
"""
|
|
def __init__(self) -> None:
|
|
self.game_objects: List[GameObject] = []
|
|
self.services: List[Tuple[Type[Any], Service]] = []
|
|
self.game: Optional[Game] = None
|
|
self.is_init: bool = False
|
|
|
|
def init_services(self) -> None:
|
|
"""Hook to add services before scene init.
|
|
|
|
Returns:
|
|
None
|
|
"""
|
|
pass
|
|
|
|
def init(self) -> None:
|
|
"""Lifecycle hook called when the scene initializes.
|
|
|
|
Returns:
|
|
None
|
|
"""
|
|
pass
|
|
|
|
def update(self, delta_time: float) -> None:
|
|
"""Lifecycle hook called every frame to update the scene.
|
|
|
|
Args:
|
|
delta_time: Seconds since the last frame.
|
|
|
|
Returns:
|
|
None
|
|
"""
|
|
pass
|
|
|
|
def draw(self) -> None:
|
|
"""Lifecycle hook called every frame to draw the scene.
|
|
|
|
Returns:
|
|
None
|
|
"""
|
|
pass
|
|
|
|
def init_scene(self) -> None:
|
|
"""Initialize services, scene, and objects.
|
|
|
|
Returns:
|
|
None
|
|
"""
|
|
if self.is_init:
|
|
return
|
|
self.init_services()
|
|
for _, service in self.services:
|
|
service.init_service()
|
|
self.init()
|
|
for game_object in list(self.game_objects):
|
|
game_object.init_object()
|
|
self.is_init = True
|
|
|
|
def update_scene(self, delta_time: float) -> None:
|
|
"""Update the scene, services, and objects.
|
|
|
|
Args:
|
|
delta_time: Seconds since the last frame.
|
|
|
|
Returns:
|
|
None
|
|
"""
|
|
self.update(delta_time)
|
|
for _, service in self.services:
|
|
service.update(delta_time)
|
|
for game_object in list(self.game_objects):
|
|
game_object.update_object(delta_time)
|
|
|
|
def draw_scene(self) -> None:
|
|
"""Draw the scene, services, and objects.
|
|
|
|
Returns:
|
|
None
|
|
"""
|
|
self.draw()
|
|
for _, service in self.services:
|
|
service.draw_service()
|
|
for game_object in list(self.game_objects):
|
|
game_object.draw_object()
|
|
|
|
def on_enter(self) -> None:
|
|
"""Hook called when the scene becomes active.
|
|
|
|
Returns:
|
|
None
|
|
"""
|
|
pass
|
|
|
|
def on_exit(self) -> None:
|
|
"""Hook called when the scene is exited.
|
|
|
|
Returns:
|
|
None
|
|
"""
|
|
pass
|
|
|
|
def add_game_object(self, game_object: GameObject) -> GameObject:
|
|
"""Add an existing object to this scene.
|
|
|
|
Args:
|
|
game_object: The object to add.
|
|
|
|
Returns:
|
|
The same object, after being attached to the scene.
|
|
"""
|
|
game_object.scene = self
|
|
self.game_objects.append(game_object)
|
|
return game_object
|
|
|
|
def add_game_object_type(self, cls: Type[T], *args: Any, **kwargs: Any) -> T:
|
|
"""Create and add a new object of a given type.
|
|
|
|
Args:
|
|
cls: GameObject class to instantiate.
|
|
*args: Positional args forwarded to the constructor.
|
|
**kwargs: Keyword args forwarded to the constructor.
|
|
|
|
Returns:
|
|
The newly created object.
|
|
"""
|
|
game_object = cls(*args, **kwargs)
|
|
self.add_game_object(game_object)
|
|
return game_object
|
|
|
|
def add_service(self, service_or_cls: Any, *args: Any, **kwargs: Any) -> Service:
|
|
"""Add a service instance or construct one from a class.
|
|
|
|
Args:
|
|
service_or_cls: A Service instance or a Service class.
|
|
*args: Positional args forwarded to the constructor.
|
|
**kwargs: Keyword args forwarded to the constructor.
|
|
|
|
Returns:
|
|
The service instance added.
|
|
"""
|
|
if isinstance(service_or_cls, Service):
|
|
service = service_or_cls
|
|
else:
|
|
service = service_or_cls(*args, **kwargs)
|
|
service.scene = self
|
|
key = service.__class__
|
|
for svc_key, _ in self.services:
|
|
if svc_key == key:
|
|
print(f"Duplicate service added: {key.__name__}")
|
|
return service
|
|
self.services.append((key, service))
|
|
return service
|
|
|
|
def get_service(self, cls: Type[T]) -> T:
|
|
"""Get a service by type.
|
|
|
|
Args:
|
|
cls: Service class to look up.
|
|
|
|
Returns:
|
|
The service instance.
|
|
|
|
Raises:
|
|
RuntimeError: If no matching service exists.
|
|
"""
|
|
for svc_key, svc in self.services:
|
|
if svc_key == cls:
|
|
if not svc.is_init:
|
|
print(f"Service not initialized: {cls.__name__}")
|
|
return svc # type: ignore[return-value]
|
|
print(f"Service of requested type not found in scene: {cls.__name__}")
|
|
raise RuntimeError(f"Service not found: {cls.__name__}")
|
|
|
|
def get_game_objects_with_tag(self, tag: str) -> List[GameObject]:
|
|
"""Get all objects that contain a tag.
|
|
|
|
Args:
|
|
tag: Tag string to match.
|
|
|
|
Returns:
|
|
List of matching game objects.
|
|
"""
|
|
return [obj for obj in self.game_objects if obj.has_tag(tag)]
|
|
|
|
|
|
class Game:
|
|
"""Main game class that owns managers and scenes.
|
|
|
|
Attributes:
|
|
managers: Mapping of manager type to instance.
|
|
scenes: Mapping of scene name to instance.
|
|
scene_order: Ordered list of scene names.
|
|
current_scene: Active scene.
|
|
next_scene: Scene queued for transition.
|
|
"""
|
|
def __init__(self) -> None:
|
|
self.managers: Dict[Type[Any], Manager] = {}
|
|
self.scenes: Dict[str, Scene] = {}
|
|
self.scene_order: List[str] = []
|
|
self.current_scene: Optional[Scene] = None
|
|
self.next_scene: Optional[Scene] = None
|
|
|
|
def init(self) -> None:
|
|
"""Initialize all managers.
|
|
|
|
Returns:
|
|
None
|
|
"""
|
|
for manager in self.managers.values():
|
|
manager.init_manager()
|
|
|
|
def update(self, delta_time: float) -> None:
|
|
"""Update the active scene and render it.
|
|
|
|
Args:
|
|
delta_time: Seconds since the last frame.
|
|
|
|
Returns:
|
|
None
|
|
"""
|
|
if self.current_scene:
|
|
self.current_scene.init_scene()
|
|
self.current_scene.update_scene(delta_time)
|
|
|
|
rl.begin_drawing()
|
|
rl.clear_background(rl.RAYWHITE)
|
|
self.current_scene.draw_scene()
|
|
rl.end_drawing()
|
|
|
|
if self.next_scene:
|
|
if self.current_scene:
|
|
self.current_scene.on_exit()
|
|
self.current_scene = self.next_scene
|
|
self.current_scene.on_enter()
|
|
self.next_scene = None
|
|
|
|
def add_manager(self, manager_or_cls: Any, *args: Any, **kwargs: Any) -> Manager:
|
|
"""Add a manager instance or construct one from a class.
|
|
|
|
Args:
|
|
manager_or_cls: A Manager instance or a Manager class.
|
|
*args: Positional args forwarded to the constructor.
|
|
**kwargs: Keyword args forwarded to the constructor.
|
|
|
|
Returns:
|
|
The manager instance added.
|
|
"""
|
|
if isinstance(manager_or_cls, Manager):
|
|
manager = manager_or_cls
|
|
else:
|
|
manager = manager_or_cls(*args, **kwargs)
|
|
key = manager.__class__
|
|
if key in self.managers:
|
|
print(f"Duplicate manager added: {key.__name__}")
|
|
self.managers[key] = manager
|
|
return manager
|
|
|
|
def get_manager(self, cls: Type[T]) -> T:
|
|
"""Get a manager by type.
|
|
|
|
Args:
|
|
cls: Manager class to look up.
|
|
|
|
Returns:
|
|
The manager instance.
|
|
|
|
Raises:
|
|
RuntimeError: If no matching manager exists.
|
|
"""
|
|
manager = self.managers.get(cls)
|
|
if manager is None:
|
|
print(f"Manager of requested type not found: {cls.__name__}")
|
|
raise RuntimeError(f"Manager not found: {cls.__name__}")
|
|
if not manager.is_init:
|
|
print(f"Manager not initialized: {cls.__name__}")
|
|
return manager # type: ignore[return-value]
|
|
|
|
def add_scene(self, name: str, scene_or_cls: Any, *args: Any, **kwargs: Any) -> Scene:
|
|
"""Add a scene instance or construct one from a class.
|
|
|
|
Args:
|
|
name: Name to register the scene under.
|
|
scene_or_cls: A Scene instance or a Scene class.
|
|
*args: Positional args forwarded to the constructor.
|
|
**kwargs: Keyword args forwarded to the constructor.
|
|
|
|
Returns:
|
|
The scene instance added.
|
|
"""
|
|
if isinstance(scene_or_cls, Scene):
|
|
scene = scene_or_cls
|
|
else:
|
|
scene = scene_or_cls(*args, **kwargs)
|
|
self.scenes[name] = scene
|
|
scene.game = self
|
|
self.scene_order.append(name)
|
|
if not self.current_scene:
|
|
self.current_scene = scene
|
|
return scene
|
|
|
|
def go_to_scene(self, name: str) -> Optional[Scene]:
|
|
"""Queue a transition to a named scene.
|
|
|
|
Args:
|
|
name: Registered name of the scene.
|
|
|
|
Returns:
|
|
The target scene if found, otherwise None.
|
|
"""
|
|
scene = self.scenes.get(name)
|
|
if not scene:
|
|
print(f"Scene not found: {name}")
|
|
return None
|
|
self.next_scene = scene
|
|
return scene
|
|
|
|
def go_to_scene_next(self) -> Optional[Scene]:
|
|
"""Queue a transition to the next scene in order.
|
|
|
|
Returns:
|
|
The next scene if one is available, otherwise None.
|
|
"""
|
|
if not self.current_scene:
|
|
return None
|
|
current_name = None
|
|
for name, scene in self.scenes.items():
|
|
if scene == self.current_scene:
|
|
current_name = name
|
|
break
|
|
if current_name is None:
|
|
return None
|
|
if current_name in self.scene_order:
|
|
idx = self.scene_order.index(current_name)
|
|
if idx + 1 < len(self.scene_order):
|
|
next_name = self.scene_order[idx + 1]
|
|
else:
|
|
next_name = self.scene_order[0]
|
|
self.next_scene = self.scenes[next_name]
|
|
return self.next_scene
|