Source code for rubato.structure.gameobject.game_object

"""
A game object is a basic element describing a "thing" in rubato.
Its functionality is defined by the components it holds.
"""
from __future__ import annotations
from typing import Type, TypeVar

from . import Component
from ... import Game, Vector, DuplicateComponentError, Draw, ImplementationError, Camera, Color, Surface

T = TypeVar("T", bound=Component)


# DEV comment: changes to arguments of Game Object should be reflected in rubato.wrap().
[docs]class GameObject: """ An element describing a set of functionality grouped as a "thing", such as a player or a wall. Args: name: The name of the game object. Defaults to "". pos: The position of the game object. Defaults to (0, 0). rotation: The rotation of the game object. Defaults to 0. z_index: The z-index of the game object. Defaults to 0. debug: Whether to draw the center of the game object. Defaults to False. active: Whether the game object is active or not. Defaults to True. hidden: Whether the game object is hidden or not. Defaults to False. """ def __init__( self, name: str = "", pos: Vector | tuple[float, float] = (0, 0), rotation: float = 0, z_index: int = 0, debug: bool = False, active: bool = True, hidden: bool = False, ): self.name: str = name """ The name of the game object. Will default to: "Game Object {number in group}" """ self.pos: Vector = Vector.create(pos) """The current position of the game object.""" self.debug: bool = debug """Whether to draw a debug crosshair for the game object.""" self.z_index: int = z_index """The z_index of the game object.""" self.rotation: float = rotation """The rotation of the game object in degrees.""" self.hidden: bool = hidden """Whether the game object is hidden (not drawn).""" self.active: bool = active """Whether the game object should update and draw.""" self._components: dict[type, list[Component]] = {} self._debug_cross: Surface = Surface(10, 10) self._debug_cross.draw_line(Vector(0, 5), Vector(0, -5), Color.debug, thickness=2) # vertical line self._debug_cross.draw_line(Vector(-5, 0), Vector(5, 0), Color.debug, thickness=2) # horizontal line
[docs] def add(self, *components: Component) -> GameObject: """ Add a component to the game object. Args: components (Component): The component(s) to add. Raises: DuplicateComponentError: Raised when there is already a component of the same type in the game object. Note that this error is only raised if the component type's 'singular' attribute is True. Returns: GameObject: This GameObject. """ for component in components: comp_type = type(component) try: if component.singular and comp_type in self._components: raise DuplicateComponentError( f"There is already a component of type '{comp_type}' in the game object '{self.name}'" ) except AttributeError as err: raise ImplementationError( "The component does not have the attribute 'singular'. You most likely overrode the" "__init__ method of the component without calling super().__init__()." ) from err if comp_type not in self._components: self._components[comp_type] = [] self._components[comp_type].append(component) component.gameobj = self return self
[docs] def remove(self, comp_type: Type[Component]): """ Removes the first instance of a component from the game object. Args: comp_type: The type of the component to remove. Raises: IndexError: The component was not in the game object and nothing was removed. """ self.remove_ind(comp_type, 0)
[docs] def remove_by_ref(self, component: Component) -> bool: """ Removes a component from the game object. Args: component: The component to remove. Returns: Whether the component was removed. """ for key, val in self._components.items(): if issubclass(key, type(component)): val.remove(component) return True return False
[docs] def remove_ind(self, comp_type: Type[Component], ind: int): """ Removes a component from the game object. Args: comp_type: The Type of component to remove ind: The index of the component to remove. Raises: IndexError: The component was not in the game object and nothing was removed or the index was out of bounds. """ for key, val in self._components.items(): if issubclass(key, comp_type): if ind < len(val): del val[ind] return else: ind -= len(val) raise IndexError( f"There are no components of type '{comp_type}' in game object '{self.name}' or the index is out of bounds." )
[docs] def remove_all(self, comp_type: Type[Component]): """ Removes all components of a type from the game object. Args: comp_type: The type of the component to remove. Raises: IndexError: The components were not in the game object and nothing was removed. """ deleted = False for key, val in self._components.items(): if issubclass(key, comp_type): del val deleted = True if not deleted: raise IndexError(f"There are no components of type '{comp_type}' in game object '{self.name}'.")
[docs] def get(self, comp_type: Type[T]) -> T: """ Gets a component from the game object. Args: comp_type: The component type (such as `ParticleSystem`, or `Hitbox`). Raises: ValueError: There were no components of that type found. Returns: The first component of that type that the gameobject holds. """ for key, val in self._components.items(): if issubclass(key, comp_type): return val[0] # type: ignore raise ValueError(f"There are no components of type '{comp_type}' in game object '{self.name}'.")
[docs] def get_all(self, comp_type: Type[T]) -> list[T]: """ Gets all the components of a type from the game object. Args: comp_type: The type of component to search for. Returns: A list containing all the components of that type. If no components were found, the list is empty. """ fin = [] for key, val in self._components.items(): if issubclass(key, comp_type): fin.extend(val) return fin
def _draw(self, camera: Camera): if self.hidden or not self.active: return for comps in self._components.values(): for comp in comps: if not comp.hidden: comp.draw(camera) if self.debug or Game.debug: self._debug_cross.rotation = self.rotation # Done like this because we don't want the crosshair to be affected by the camera's zoom Draw.queue_surface(self._debug_cross, camera.transform(self.pos)) def _update(self): if not self.active: return all_comps = list(self._components.values()) for comps in all_comps: for comp in comps: comp._update() def _fixed_update(self): if not self.active: return for comps in self._components.values(): for comp in comps: comp.fixed_update()
[docs] def clone(self) -> GameObject: """ Clones the game object. """ new_obj = GameObject( name=f"{self.name} (clone)", pos=self.pos.clone(), rotation=self.rotation, z_index=self.z_index, debug=self.debug, active=self.active, hidden=self.hidden, ) for component in self._components.values(): for comp in component: new_obj.add(comp.clone()) return new_obj
def __contains__(self, comp_type): for key in self._components: if issubclass(key, comp_type): return True return False def __repr__(self): return ( f"GameObject(name='{self.name}', pos={self.pos}, rotation={self.rotation}, z_index={self.z_index}, " f"debug={self.debug}, active={self.active}, hidden={self.hidden})" ) def __str__(self): return f"<GameObject '{self.name}', with {len(self.get_all(Component))} components at {hex(id(self))}>"