Defining Base Classes/Models in Pygame

Context

I am creating a game in Python using pygame to apply SOLID principles and MVC concepts that I self-studied.

I once had a class that, I think, has more than one responsibility (Move by changing position, change movement angle/direction, reacts to user input, fire, damage entity, etc.). So I decomposed that class into the following:

  • Static
  • Movable
  • Controllable
  • Shooter
  • Destructible
  • Projectile

I separated them like interfaces so that different kinds of classes can use any one of them (eg. An Obstacle class is Static, Bullet class is Movable and Projectile, Player class is Controllable and Shooter, etc.).

The first three interfaces feels odd for me. Basically:

  • Static is responsible for positioning an entity in the window area;
  • Movable is responsible for defining the direction of movement (has an angle and movement speed);
  • Controllable is responsible for reacting to user input (when to move, fire, etc.)

Problem

The 3 classes above have this kind of dependency:

Static <---extends--- Movable <---extends--- Controllable

Here is the code of the 3 base classes and on how they interact with one another:

class Static(object):
    def __init__(self, 
                 pos: Vector2 = None, 
                 off: Vector2 = None):
        self._position = pos
        self._offset = off

    # has some setters and getters
    ...


class Movable(Static):
    def __init__(self, 
                 angle: Vector2 = None, 
                 mov_spd: int = 0, 
                 **static_properties):
        super(Movable, self).__init__(**static_properties)

        self._angle_vector = angle
        self._move_speed = mov_spd

    def update_position(self, *args, **kwargs):
        raise NotImplementedError

    def update_angle(self, *args, **kwargs):
        raise NotImplementedError


class Controllable(Movable):
    def __init__(self, 
                 move_state: Dict[Enum, bool] = None,
                 **movable_properties):
        super(Controllable, self).__init__(**movable_properties)
        
        # self._move_state = { LEFT: False, RIGHT: False, UP: False, DOWN: False }
        self._move_state = move_state

    def set_move_state(self, direction):
        self._move_state[direction] = not self._move_state[direction]

    # overrides and defines Movable method
    def update_position(self):
    ...

    # overrides and defines Movable method
    def update_angle(self, direction, magnitude):
    ...

The reason of the separation is that there are/will be entities in the game that are like Widgets, or game obstacles (thus, implements Static), entities that just moves like Bullets (thus, implements Movable), or entities that can be controlled by the Player (thus, implements Controllable).

But the reason for the dependency is that Controllable will define the direction of the movement, in which Movable will use but will need to know the position to update it, in which Static will provide.


Questions (2)

  • Are the decomposition/separation of the classes in this case sensible? If not, how small/large enough should they be?
  • Are the dependencies in this case sensible? If not, how can they be improved/coupled/decoupled?

Initial Solutions:

  • I did familiarize myself with Inheritance vs Composition to know when it is good (and bad) to inherit/compose from the base class, but I have no other way of knowing if I properly used it in my case. Also, I still don’t understand the concept of ‘is a’ and ‘has a’ because it doesn’t seem to apply in this case where I a trying to build the base classes/interfaces for entities. But it feels like it should apply.

  • I understood the Single Responsibility Principle well enough to know that positioning, moving and controlling should be processed on a separate class instead of inside a big class (unless I am mistaken on defining these responsibilities). But now I am wondering how small/large a responsibility should positioning, moving and controlling be.

  • I just found out about this StackOverflow question from 9 years ago, and somewhat answered my second question and personal concerns. But I would like to know if there are other useful designs aside from mixins to solve my case.

Source: Python Questions

LEAVE A COMMENT