0

Hi, I'm making a simple oop game in python using pygame.

Currently, my game object classes have methods like move, draw and check collision. I would like to create a new class with the intention of handling these interactions in a way that my game object classes don't have to know about other game objects anymore

Here are the abstract classes for the game objects

import pygame
from abc import ABC, abstractmethod
import copy


class ABCObject(ABC):
def __init__(self, initial_x: int, initial_y: int, size: int, color: tuple):
    self._size = size
    self._rect = pygame.Rect(
        initial_x, initial_y, self._size, self._size)
    self._color = color

@property
def size(self):
    return self._size

@property
def rect(self):
    return self._rect

@property
def color(self):
    return self._color

@size.setter
def size(self, new_size: int):
    if isinstance(new_size, int):
        self._size = new_size

@color.setter
def color(self, new_color: tuple):
    if isinstance(new_color, tuple):
        if len(new_color) == 3 and isinstance(new_color[1], int) and isinstance(new_color[0], int) and isinstance(new_color[2], int):
            self._color = new_color

def draw(self, win, offset):
    fake_rect = copy.deepcopy(self._rect)
    fake_rect.topleft -= offset
    pygame.draw.rect(win, self._color, fake_rect)
from abc_object import ABCObject


class Kinetic_object(ABCObject, ABC):
    def __init__(self, initial_x: int, initial_y: int, size: int, color: tuple, speed: int):
        super().__init__(initial_x, initial_y, size, color)
        self._speed = speed
        self._velX = 0
        self._velY = 0

    @property
    def speed(self):
        return self._speed

    @property
    def velX(self):
        return self._velX

    @property
    def velY(self):
        return self._velY

    @velX.setter
    def velX(self, new_velX):
        if isinstance(new_velX, (int, float)):
            self._velX = new_velX

    @velY.setter
    def velY(self, new_velY):
        if isinstance(new_velY, (int, float)):
            self._velY = new_velY

    @speed.setter
    def speed(self, new_speed):
        if isinstance(new_speed, int):
            self._speed = new_speed

    @abstractmethod
    def move(self):
        pass

And here is my player class:

import math
import copy
from abc_command import Command
from abc_kinetic_object import Kinetic_object


class Player(Kinetic_object, Command):
    def __init__(self, initial_x: int, initial_y: int, size: int, speed: int, obstacles: list):
        Kinetic_object.__init__(
            self, initial_x, initial_y, size, (250, 160, 60),  speed)
        Command.__init__(
            self, {'up': False, 'down': False, 'right': False, 'left': False})
        # usar depois para mudar sprite
        self._facing_direction = 'down'
        self._obstacles = obstacles

    # usar para normalizar os vetores de velocidade, caso contrario, anda mais rápido nas diagonais
    def normalize(self):
        if self._velX != 0 and self._velY != 0:
            self._velX *= 1/math.sqrt(2)
            self._velY *= 1/math.sqrt(2)


    # Colisoes, funciona se nao colidir com objetos em movimento
    def move(self):
        self.normalize()
        self._rect.x += self._velX
        self.check_collisions('horizontal')
        self._rect.y += self._velY
        self.check_collisions('vertical')

    def check_collisions(self, direction):
        if direction == 'horizontal':
            for obstacle in self._obstacles:
                if obstacle.rect.colliderect(self._rect):
                    if self._velX > 0:  # direita
                        self._rect.right = obstacle.rect.left
                    elif self._velX < 0:  # esquerda
                        self._rect.left = obstacle.rect.right

        if direction == 'vertical':
            for obstacle in self._obstacles:
                if obstacle.rect.colliderect(self._rect):
                    if self._velY > 0:  # baixo
                        self._rect.bottom = obstacle.rect.top
                    elif self._velY < 0:  # cima
                        self._rect.top = obstacle.rect.bottom

    def change_facing_direction(self):
        # so apertando para direita
        if self._commands['right'] and not (self._commands['down'] or self._commands['up'] or self._commands['left']):
            self._facing_direction = 'right'
        # so apertando para esquerda
        elif self._commands['left'] and not (self._commands['down'] or self._commands['up'] or self._commands['right']):
            self._facing_direction = 'left'
        # so apertando para cima
        elif self._commands['up'] and not (self._commands['down'] or self._commands['right'] or self._commands['left']):
            self._facing_direction = 'up'
        # so apertando para baixo
        elif self._commands['down'] and not (self._commands['up'] or self._commands['left'] or self._commands['right']):
            self._facing_direction = 'down'

    def execute_commands(self):
        self.change_facing_direction()
        self._velX = 0
        self._velY = 0
        if self._commands['left'] and not self._commands['right']:
            self._velX = -self._speed
        if self._commands['right'] and not self._commands['left']:
            self._velX = self._speed
        if self._commands['up'] and not self._commands['down']:
            self._velY = -self._speed
        if self._commands['down'] and not self._commands['up']:
            self._velY = self._speed

I would like to know which design pattern would be appropriate in order to remove methods like "move" from game objects and implement them in other controller classes, I have different objects that all move in a different way.

Rabbid76
  • 202,892
  • 27
  • 131
  • 174
Zyphoon
  • 3
  • 1
  • Your code would be much easier to read if you got rid of all the superfluous properties. For example, drop `velX` and work directly with your attributes. If someone tries to add something that *isn't* a number to such an attribute, that's a bug to fix, not something to ignore. – chepner Mar 05 '22 at 20:18
  • https://en.wikipedia.org/wiki/Entity_component_system – Matt Timmermans Mar 08 '22 at 13:13

1 Answers1

0

I do not know Python, however I would like to share with my thoughts via C# and in addition I will attach link about how to convert interface to abstract class of Python.

I do not think that there is a pattern which says where we should put move() method. However, we can create an interface IMoveable and implement it in desired classes.

public interface IMoveable
{
    void Move();
}

And classes where we can implement this interface:

public class Player : IMoveable
{
    public void Move() { }
}   


class Kinetic_object : IMoveable
{
    public void Move() { }
}   

It is possible to use inheritance here. However, by using inheritance we can easily violate of Liskov substitution.

For example we can create base class and call it Unit and declare method Move:

public abstract class Unit
{
    public abstract void Move();
}

And then just use it in out classes:

public class Player : Unit
{
    public void Move()
    {
        
    }
}   

However, if game will have turret, then by doing this we violates Liskov substitution principle:

public class Turret : Unit
{
    public void Move()
    {
        throw new NotImplementedException(); // violation of Liskov substitution principle
    }
}

So, we prefered composition over inheritance here.

StepUp
  • 36,391
  • 15
  • 88
  • 148