Python Forum
Thread Rating:
  • 0 Vote(s) - 0 Average
  • 1
  • 2
  • 3
  • 4
  • 5
Pygame Groups
#1
I'm working on a project that's nearing its completion. I'm in the process of addressing my problem with painting all the sprites in the correct order and here is what I have come up with so far. It would be good to get your feedback and suggestions on the topic.


My immediate thoughts were to create something that's:
- simple to use
- require low maintenance
- flexible enough to apply to future projects

I thought I would just build on the existing stuff from pygame (the class pygame.sprite.Group). I figure in most cases game screens are created with the following elements:

BackGround
----------
All the objects involved in making up the background.
This approach would allow the use of a big picture, and additional objects like trees could be added to it, or moved during game play to give a sense of depth.


ZPlanes
-------
Focuses more on the player and enemies that are moving about.
I'm thinking this should be some sort of array, but that's still in the theory stage.


Interface
---------
Objects used to make up the user interface like score, strength, etc.


Overlay
-------
Images that need to be painted on the topmost screen like "Game paused", "Loading ...", etc.


import pygame

pygame.init()

class ScreenObject(pygame.sprite.Sprite):
    pass
# ScreenObject() ------------------------------------------------------------



class SOGroups:
    'Class - manages groups from the pygame.sprite.Group collection.'
    _groups =[] # <-- don't know about this bit.
    
    class BackGround(pygame.sprite.Group):
        def __init__(self):
            self._group =pygame.sprite.Group()
        # __init__() --------------------------------------------------------

        
        def add(self, screen_object):
            'Function - adds an object to this group.'
            self._group.add(screen_object)
        # add() -------------------------------------------------------------


        def remove(self, screen_object):
            'Function - removes the object from this group.'
            pass
        # remove() ----------------------------------------------------------
        

    class ZPlanes(pygame.sprite.Group):
        'Container - for all the screen objects in the Z-Plane.'
        def __init__(self):
            self._group =pygame.sprite.Group()
        # __init__() --------------------------------------------------------

        
        def add(self, screen_object):
            'Function - adds a screen object to the background group.'
            self._group.add(screen_object)
        # add() -------------------------------------------------------------


        def remove(self, screen_object):
            'Function - removes the object from this group.'
            pass
        # remove() ----------------------------------------------------------

        
    class Interface(pygame.sprite.Group):
        'Container - for all the screen objects in the Interface.'
        def __init__(self):
            self._group =pygame.sprite.Group()
        # __init__() --------------------------------------------------------

        
        def add(self, screen_object):
            'Function - adds a screen object to the background group.'
            self._group.add(screen_object)
        # add() -------------------------------------------------------------


        def remove(self, screen_object):
            'Function - removes the object from this group.'
            pass
        # remove() ----------------------------------------------------------
        

    class Overlay(pygame.sprite.Group):
        'Container - for all the screen objects in the Overlay.'
        def __init__(self):
            self._group =pygame.sprite.Group()
        # __init__() --------------------------------------------------------

        
        def add(self, screen_object):
            'Function - adds a screen object to the background group.'
            self._group.add(screen_object)
        # add() -------------------------------------------------------------


        def remove(self, screen_object):
            'Function - removes the object from this group.'
            pass
        # remove() ----------------------------------------------------------
        
    
    @classmethod
    def paint(cls):
        'Function - paints any screen object found in the groups onto the screen.'
        pass
    # paint() ---------------------------------------------------------------
    

    @classmethod
    def remove(cls, screen_object):
        'Function - removes the screen object from all of the grooups.'
        pass
    # remove() --------------------------------------------------------------
    
# SOGroups ------------------------------------------------------------------
Reply
#2
Example. You could do something like this.
class SOGroup:
    group_background = pygame.sprite.Group()
    group_zplanes = pygame.sprite.Group()
    group_interface = pygame.sprite.Group()
    group_overlay = pygame.sprite.Group()

    @classmethod
    def add_background(cls, item):
        cls.group_background.add(item)

    @classmethod
    def add_zplanes(cls, item):
        cls.group_zplanes.add(item)

    #etc

    @classmethod
    def draw(cls, surface):
        cls.group_background.draw(surface)
        cls.group_zplanes.draw(surface)
        cls.group_interface.draw(surface)
        cls.group_overlay.draw(surface)

Example. Or you could do this.
from enum import Enum

Group = Enum('Group', 'background, zplanes, interface, overlay')
class SOGroup:
    groups = {
        Group.background: pygame.sprite.Group(),
        Group.zplanes: pygame.sprite.Group(),
        Group.interface: pygame.sprite.Group(),
        Group.overlay: pygame.sprite.Group(),
        }

    @classmethod
    def add(cls, group, item):
        cls.groups[group].add(item)

    @classmethod
    def draw(cls, surface):
        cls.groups[Group.background].draw(surface)
        cls.groups[Group.zplanes].draw(surface)
        cls.groups[Group.interface].draw(surface)
        cls.groups[Group.overlay].draw(surface)

Example. Or this
class SOGroup:
    groups = {
        'Background': pygame.sprite.Group(),
        'ZPlanes': pygame.sprite.Group(),
        'Interface': pygame.sprite.Group(),
        'Overlay': pygame.sprite.Group(),
        }

    @classmethod
    def add(cls, group, item):
        cls.groups[group].add(item)

    @classmethod
    def draw(cls, surface):
        cls.groups['Background'].draw(surface)
        cls.groups['Zplanes'].draw(surface)
        cls.groups['Interface'].draw(surface)
        cls.groups['Overlay'].draw(surface)
99 percent of computer problems exists between chair and keyboard.
Reply
#3
I like the last example the best, nice one. I'm going to play around with it for a little while to see if I can think of anything else to add to it. You've given me a lot to think about.

Thanks Windspar.
Reply
#4
Pygame is pretty cool. They've already made provisions for my z-plane theory:

pygame.sprite.LayeredUpdates.add

If it works the way I think it does I can use the z-axis from x,y,z co-ordinates, and it will handle the drawing part, so that would mean very little more to do on development side. I think it keeps it in a seperate group of its own. Think
Reply
#5
I've had another run at this group thing. I think the way I've got it now offers greater flexibility. With the next project I have in mind I'll be wanting to make use of the LayeredUpdates group, and this approach gives me the option of working with more or less groups. Its still under construction but most of the meat is on the bones.

In theory I should still be able to call upon all the groups to draw by calling a single method from the SpriteGroup class.

Here's one of my earlier attempts.
import pygame

pygame.init()

class ScreenObject(pygame.sprite.Sprite):
    pass
# ScreenObject() ------------------------------------------------------------



"""
One of my earlier attempts.
"""

import pygame

def enum(*args):
# ---------------------------------------------------------------------------
#   Used to create enumate values.
#
#       Example:
#           fruits =enum("APPLES", "ORANGES", "PEARS")
#           if fruits.APPLES ==fruits.ORANGES:
#               print("they're the same.")
#
# ---------------------------------------------------------------------------
    enums =dict(zip(args, range(len(args))))
    return type('Enum', (), enums)
# enum() --------------------------------------------------------------------


SPRITE_GROUPS =enum("BACKGROUND", "ZPLANES", "INTERFACE", "OVERLAY")


class SpriteGroup:
    """Screen Object Group  - manages sprites within a group.
    Returns: n/a
    Functions: add, remove, remove_all, draw
    Attributes: n/a"""
    
    CONST =SPRITE_GROUPS
    groups = {
        CONST.BACKGROUND:   pygame.sprite.Group(),
        CONST.ZPLANES:      pygame.sprite.Group(),
        CONST.INTERFACE:    pygame.sprite.Group(),
        CONST.OVERLAY:      pygame.sprite.Group(),
        }
    
    @classmethod
    def add(cls, group, item, z_plane =None):
        'Function - adds an item to a group.'
        result =False
        if group ==SPRITE_GROUPS.ZPLANES:
            if isinstance(z_plane, int):
                pygame.sprite.LayeredUpdates.add(item, z_plane)
                result =True
        else:
            cls.groups[group].add(item )
            result =True
        return result
    # add() -----------------------------------------------------------------
        

    @classmethod
    def remove(cls, group, item):
        'Function - removes the item from the group.'
        cls.groups[group].remove(item)
    # remove() --------------------------------------------------------------
    

    @classmethod
    def remove_all(cls, item):
        'Function - removes the item from all of the groups.'
        #   Build up a list of all the groups managed by this class.
        grp_lst =[]
        grp_lst.append(SPRITE_GROUPS.BACKGROUND)
        grp_lst.append(SPRITE_GROUPS.ZPLANES)
        grp_lst.append(SPRITE_GROUPS.INTERFACE)
        grp_lst.append(SPRITE_GROUPS.OVERLAY)
        #   _.

        #   Search all of our groups and remove the item when found.
        for grp in grp_lst:
            if grp.has(item):
                grp.remove()
        #   _.
    # remove_all() ----------------------------------------------------------
    
        
    @classmethod
    def draw(cls, surface):
        'Function - draws all the sprite items from all of managed groups.'
        CONST =SPRITE_GROUPS
        cls.groups[CONST.BACKGROUND].draw(surface)
        cls.groups[CONST.ZPLANES].draw(surface)
        cls.groups[CONST.INTERFACE].draw(surface)
        cls.groups[CONST.OVERLAY].draw(surface)
    # draw() ----------------------------------------------------------------
    
# SpriteGroup ---------------------------------------------------------------


item =pygame.sprite.Sprite()
CONST =SPRITE_GROUPS
SpriteGroup.add( CONST.BACKGROUND, item ) 

item =pygame.sprite.Sprite()
SpriteGroup.add( CONST.ZPLANES, item ) 


SpriteGroup.CONST.BACKGROUND
SPRITE_GROUPS.BACKGROUND 
But the one I want to use is listed below.
import pygame

if __name__ =="__main__":
    pygame.init()

from clsExceptions import * # My custom error.



class SpriteGroup:
    """Class - manages groups from the pygame.sprite.Group collection.
    Returns: n/a
    Functions: add_item, add_group, remove_item, remove_all_items, draw, init, groups
    Attributes: n/a"""
    _groups_list =[] 
    _grp_cntr_id =0
    _surface =None      # Pygame surface.


    @classmethod
    def _new_group_name(cls):
        for tries in range(1000):
            cls._grp_cntr_id +=1
            name ="Group" +str(cls._grp_cntr_id)
            grp =cls._search_groups(name)
            if grp ==None:
                #   The name is unique.
                fnd =False
                break
        else:
            fnd =True

        if fnd:
            #   A unique name could not be found, so lets return nothing.
            name =None
            
        return name
    # _new_group_name() -----------------------------------------------------


    @classmethod
    def _search_groups(cls, name):
        for grp in cls._groups_list:
            if grp.name.lower() ==name.lower():
                fnd =True
                break
        else:
            fnd =False
            
        if not fnd:
            grp =None

        return grp
    # _search_groups() ------------------------------------------------------
    
    
    @classmethod
    def add_group(cls, name =None, layered_updates =False):
        'Function - adds a new group to the collection.'
        #   NOTE(S):
        #   -------
        #   There can be only ONE.
        #   
        #   The Layered-Updates is a special group, and there can only be
        #   one.

        if cls._surface ==None:
            #   The class hasn't been initialised yet.
            cls.init()
            
        
        if isinstance(name, str):
            #   Check if they passed an empty string.
            if len(name.strip()) <1:
                raise CustomError("Group name cannont be an empty string.")
        else:
            #   Check if a name was passed.
            if not name ==None:
                #   Something other than a string was passed as a name.
                raise TypeError("Wrong variable type passed.  Expected 'name' to be string.")
                
        if not isinstance(layered_updates, bool):
            raise TypeError("Wrong variable type passed.  Expected 'layered_updates' to be boolean.")

        if name ==None:
            #   Generate a unique name.
            name =cls._new_group_name()
            
        if name ==None:
            raise CustomError("Failed to generate a unique group name.")

        #   Have a look at all the groups in our collection to see if the
        # name supplied is already been used by a group.
##        for grp in cls._groups_list:
##            if grp.name.lower() ==name.lower():
##                fnd =True
##                break
##        else:
##            fnd =False
##            
##        if fnd:
##            raise CustomError("A group with that name already exists.")

        grp =cls._search_groups(name)
        if not grp ==None:
            raise CustomError("A group with that name already exists.")
        #   _.

        #   Search through our collection of groups to see if any of them
        # have been designated as being the Layered-Updates group.
        for grp in cls._groups_list:
            if grp.layered_updates:
                fnd =True
                break
        else:
            fnd =False
        #   _.

        if fnd ==True and layered_updates ==True:
            raise CustomError("There can only be one.  {0} has already been designated as the layered-Updates groups.".format(grp.name))

        #   Create the group and add it to the list.
        grp =pygame.sprite.Group()
        grp.name =name
        grp.layered_updates =layered_updates
        cls._groups_list.append(grp)
        #   _.
        
        return grp
    # add_group() -----------------------------------------------------------


    @classmethod
    def add_item(cls, group, item, z_plane =None):
        'Function - adds a sprite item to a group.'
        if not isinstance(item, pygame.sprite.Sprite):
            raise TypeError

        if not hasattr(item, "layer"):
            #   NOTE(S):
            #   -------
            #   The default layer is 0.
            #
            #   If the sprite you add has an attribute 'layer' then that layer
            # will be used when applying it to the layered updates group.
            if not z_plane ==None:
                item.layer =z_plane # Create the attribute.
            else:
                item.layer =0
                
        if isinstance(group, pygame.sprite.Group):
            group.add(item)
        elif isinstance(group, str):
            for grp in cls._groups_list:
                if grp.name.lower() ==group.lower():
                    fnd =True
                    break
            else:
                fnd =False

            if fnd:
                if grp.layered_updates:
                    if z_plane ==None:
                        #   Allow pygame to add the item to layer based on
                        # the value of the items 'layer' attribute.
                        pygame.sprite.LayeredUpdates.add(item)
                    else:
                        #   Make a request to pygame for the item to be added
                        # to the specified layer.
                        pygame.sprite.LayeredUpdates.add(item, z_plane)
                else:
                    grp.add(item)
    # add_item() ------------------------------------------------------------


    @classmethod
    def init(cls):
        'Function - initialises the class.'
        return_status =False
        
        #   The pygame draw methods require a reference to a surface.  By
        # calling this routine we ensure that a reference can easily passed.
        cls._surface =pygame.display.get_surface()
        return_status =True

        return return_status
    # init() ----------------------------------------------------------------
    
    
    @classmethod
    def draw(cls):
        'Function - paints any screen object found in the groups onto the screen.'
        return_status =False
        
        for grp in cls._groups_list:
            if grp.layered_updates:
                #   Call the appropriate draw method.
                pygame.sprite.LayeredUpdates.draw(cls._surface)
            else:
                grp.draw()
                
        return_status =True

        return return_status
    # draw() ---------------------------------------------------------------


    @classmethod
    def groups(cls):
        'Function - returns a diction of all the group in the collection.'
        group_dict ={}
        
        for grp in cls._groups_list:
            group_dict[grp.name] =grp

        return group_dict
    # groups() --------------------------------------------------------------


    @classmethod
    def remove_all_items(cls, item):
        'Function - removes the item from all the groups.'
        for grp in cls._groups_list:
            cls.remove_item(grp.name, item)
    # remove_all_items() ----------------------------------------------------
    

    @classmethod
    def remove_item(cls, group, item):
        'Function - removes the item from a group.'
        return_status =False
            
        if isinstance(group, pygame.sprite.Group):
            group.remove(item)
        elif isinstance(group, str):
            for grp in cls._groups_list:
                if grp.name.lower() ==group.lower():
                    fnd =True
                    break
            else:
                fnd =False

            if fnd:
                if grp.layered_updates:
                    sprites_list =pygame.sprite.LayeredUpdates.remove_sprites_of_layer(item.layer)
                    for index in range(len(sprites_list)):
                        if sprite_list[index] ==item:
                            fnd =True
                    else:
                        fnd =False

                    if fnd:
                        removed_item =sprites_list.pop(index)
                        pygame.sprite.LayeredUpdates.add(sprite_list)
                        return_status =True
                else:
                    grp.remove(item)
                    return_status =True
                                       
        return return_status
                                       
    # remove_item() --------------------------------------------------------------
    
# SpriteGroup ------------------------------------------------------------------


if __name__ =="__main__":
    grp ={}
    
    #   Generate a new group by just providing a name.
    SpriteGroup.add_group("background")
    #   _.
    
    #   Generate new groups and store them in a dict object.
    grp["z_layers"] =SpriteGroup.add_group("z_layers", layered_updates=True)
    grp["interface"] =SpriteGroup.add_group("interface")
    #   _.

    #   Generate a new group and modiying the group name later.
    new_group =SpriteGroup.add_group()
    new_group.name ="overlay"
    #   _.

    #   Obtain a full list of groups stored in our collection.
    full_grp_listing =SpriteGroup.groups()
    #   _.
        
    #   Generate a new sprite and add it to the special layers group
    # (LayeredUpdates).
    player =pygame.sprite.Sprite()
    SpriteGroup.add_item("z_layers", player)
    #   _.
    
    print("Group '{0}' layered group status ={1}.".format(grp["z_layers"].name, grp["z_layers"].layered_updates))
    print("\nThe common group var ('grp') has the following groups:")
    for g in grp:
        print("   ",g)
    print()
        
    print("The full list of groups returned by the class is as follows:")

    for g in full_grp_listing:
        print("   ",g)

    print("\nPS: Not sure why the groups are listed in Shell output the way they are.\n    The groups aren't listed in the order they'll draw to the screen.")
and here's my [clsExceptions] exeption class file.
class CustomError(Exception):
    pass
Output:
Group 'z_layers' layered group status =True. The common group var ('grp') has the following groups: z_layers interface The full list of groups returned by the class is as follows: z_layers background interface overlay PS: Not sure why the groups are listed in Shell output the way they are. The groups aren't listed in the order they'll draw to the screen.
Reply


Forum Jump:

User Panel Messages

Announcements
Announcement #1 8/1/2020
Announcement #2 8/2/2020
Announcement #3 8/6/2020