Flyweight

This pattern is used to minimize memory usage by sharing as much data as possible with other similar objects, rather than keeping common state information in the object itself. This can be seen in characters of a text editor, for example.

TL;DR

The Flyweight pattern is used to minimize RAM expenditure by sharing as much data as possible between objects.

https://refactoring.guru/design-patterns/flyweight

Problem

Suppose you are creating a bullet-storm game. Given that at any moment you might have millions of bullets flying around, your performance starts to degrade very quickly if each bullet itself has to maintain all its properties intrinsically, including those that are always the same for every bullet (such as the sprite and the color).

Solution

The solution is to implement a separate class that stores the extrinsic properties of the objects so that they get reused, rather than recomputed.

Text example

class TextFormat:
    """Flyweight class - stores intrinsic state (font, size, color)"""
    def __init__(self, font, size, color):
        self.font = font
        self.size = size
        self.color = color
    
    def display(self, text, position):
        """Operation that uses both intrinsic and extrinsic state"""
        print(f"Text: '{text}' at {position} - Font: {self.font}, Size: {self.size}, Color: {self.color}")
 
 
class TextFormatFactory:
    """Factory to manage flyweight instances"""
    _formats = {}
    
    @classmethod
    def get_format(cls, font, size, color):
        key = (font, size, color)
        if key not in cls._formats:
            cls._formats[key] = TextFormat(font, size, color)
            print(f"Created new TextFormat: {key}")
        return cls._formats[key]
    
    @classmethod
    def get_created_formats_count(cls):
        return len(cls._formats)
 
 
class TextElement:
    """Context class - stores extrinsic state (text content, position)"""
    def __init__(self, text, position, font, size, color):
        self.text = text  # extrinsic state
        self.position = position  # extrinsic state
        self.format = TextFormatFactory.get_format(font, size, color)  # flyweight reference
    
    def display(self):
        self.format.display(self.text, self.position)
 
 
# Example usage
if __name__ == "__main__":
    # Create multiple text elements
    elements = []
    
    # Many elements with the same formatting
    elements.append(TextElement("Hello", (10, 20), "Arial", 12, "black"))
    elements.append(TextElement("World", (50, 20), "Arial", 12, "black"))
    elements.append(TextElement("Python", (100, 20), "Arial", 12, "black"))
    
    # Some elements with different formatting
    elements.append(TextElement("Title", (10, 50), "Arial", 16, "blue"))
    elements.append(TextElement("Subtitle", (10, 80), "Arial", 14, "gray"))
    elements.append(TextElement("Footer", (10, 200), "Arial", 12, "black"))
    
    print("Displaying all text elements:")
    for element in elements:
        element.display()
    
    print(f"\nTotal TextFormat objects created: {TextFormatFactory.get_created_formats_count()}")
    print(f"Total TextElement objects created: {len(elements)}")
    print("Notice how flyweights are reused - only 4 TextFormat objects were created for 6 TextElements!")

Bullet example

To do this, we might implement a BulletType class to serve as a flyweight; this class can store the sprite, damage, speed, and sound effect of each bullet.

Next we implement a BulletTypeFactory, a Factory that can manage the bullet types themselves (i.e. for different weapons such as pistols, rifles, etc.). Finally, we implement a Bullet class that stores extrinsic data for each bullet that can vary, such as position, direction of travel, etc.

This means that despite the potential existence of thousands of Bullets, the entire set of Bullets can exist with only a few BulletType instances.

class BulletType:
    """Flyweight class - stores intrinsic state (sprite, damage, speed, sound)"""
    def __init__(self, name, sprite, damage, speed, sound_effect):
        self.name = name
        self.sprite = sprite  # image/texture data
        self.damage = damage
        self.speed = speed
        self.sound_effect = sound_effect
    
    def fire(self, position, direction, velocity):
        """Operation that uses both intrinsic and extrinsic state"""
        print(f"Firing {self.name} bullet:")
        print(f"  Position: {position}, Direction: {direction}")
        print(f"  Speed: {self.speed}, Damage: {self.damage}")
        print(f"  Playing sound: {self.sound_effect}")
        print(f"  Rendering sprite: {self.sprite}")
    
    def render(self, position):
        """Render the bullet at a specific position"""
        print(f"Rendering {self.sprite} at {position}")
 
 
class BulletTypeFactory:
    """Factory to manage bullet type flyweights"""
    _bullet_types = {}
    
    @classmethod
    def get_bullet_type(cls, name, sprite, damage, speed, sound_effect):
        if name not in cls._bullet_types:
            cls._bullet_types[name] = BulletType(name, sprite, damage, speed, sound_effect)
            print(f"Created new BulletType: {name}")
        return cls._bullet_types[name]
    
    @classmethod
    def get_types_count(cls):
        return len(cls._bullet_types)
 
 
class Bullet:
    """Context class - stores extrinsic state (position, direction, velocity)"""
    def __init__(self, bullet_type, position, direction, velocity=(0, 0)):
        self.bullet_type = bullet_type  # flyweight reference
        self.position = list(position)  # extrinsic state - changes frequently
        self.direction = direction      # extrinsic state
        self.velocity = list(velocity)  # extrinsic state - changes during flight
        self.active = True
    
    def update(self, delta_time):
        """Update bullet position based on its type's speed"""
        if self.active:
            # Calculate movement based on flyweight's speed and current direction
            speed = self.bullet_type.speed * delta_time
            self.position[0] += self.direction[0] * speed
            self.position[1] += self.direction[1] * speed
    
    def render(self):
        """Render the bullet using its flyweight"""
        if self.active:
            self.bullet_type.render(tuple(self.position))
    
    def fire(self):
        """Fire the bullet"""
        self.bullet_type.fire(tuple(self.position), self.direction, self.velocity)
 
 
class Game:
    """Game context that manages many bullets"""
    def __init__(self):
        self.bullets = []
        
        # Pre-create bullet types (flyweights)
        self.pistol_type = BulletTypeFactory.get_bullet_type(
            "Pistol", "pistol_bullet.png", 25, 300, "pistol_shot.wav"
        )
        self.rifle_type = BulletTypeFactory.get_bullet_type(
            "Rifle", "rifle_bullet.png", 50, 500, "rifle_shot.wav"
        )
        self.shotgun_type = BulletTypeFactory.get_bullet_type(
            "Shotgun", "shotgun_pellet.png", 15, 250, "shotgun_blast.wav"
        )
    
    def fire_pistol(self, position, direction):
        bullet = Bullet(self.pistol_type, position, direction)
        bullet.fire()
        self.bullets.append(bullet)
    
    def fire_rifle(self, position, direction):
        bullet = Bullet(self.rifle_type, position, direction)
        bullet.fire()
        self.bullets.append(bullet)
    
    def fire_shotgun(self, position, direction):
        # Shotgun fires multiple pellets
        import random
        for i in range(5):  # 5 pellets per shot
            # Slight random spread for each pellet
            spread_x = direction[0] + random.uniform(-0.2, 0.2)
            spread_y = direction[1] + random.uniform(-0.2, 0.2)
            
            bullet = Bullet(self.shotgun_type, position, (spread_x, spread_y))
            self.bullets.append(bullet)
        
        print(f"Shotgun blast fired 5 pellets!")
    
    def update(self, delta_time):
        """Update all active bullets"""
        for bullet in self.bullets:
            bullet.update(delta_time)
    
    def render(self):
        """Render all active bullets"""
        print("\n--- Rendering all bullets ---")
        for bullet in self.bullets:
            bullet.render()
    
    def get_stats(self):
        return {
            'total_bullets': len(self.bullets),
            'bullet_types_created': BulletTypeFactory.get_types_count()
        }
 
 
# Example usage - Simulating a game scenario
if __name__ == "__main__":
    game = Game()
    
    print("=== Game Combat Simulation ===\n")
    
    # Player fires various weapons
    game.fire_pistol((100, 200), (1, 0))      # Fire right
    game.fire_pistol((100, 200), (0, 1))      # Fire up
    game.fire_rifle((150, 250), (-1, 0))      # Fire left
    game.fire_shotgun((200, 300), (0, -1))    # Fire down (creates 5 bullets)
    
    # Enemy fires back
    game.fire_pistol((300, 400), (-1, -1))    # Fire diagonally
    game.fire_rifle((350, 450), (-1, 0))      # Fire left
    
    print(f"\n=== Game Stats ===")
    stats = game.get_stats()
    print(f"Total bullets in game: {stats['total_bullets']}")
    print(f"Bullet types created: {stats['bullet_types_created']}")
    print(f"\nMemory efficiency: {stats['total_bullets']} bullets share only {stats['bullet_types_created']} flyweight objects!")
    
    print(f"\n=== Update and Render Cycle ===")
    game.update(0.016)  # ~60 FPS
    game.render()
    
    print(f"\n=== Key Benefits ===")
    print("✓ Sprite data, damage, speed, and sound effects are shared")
    print("✓ Only position, direction, and velocity are stored per bullet")
    print("✓ Hundreds of bullets can exist with minimal memory overhead")
    print("✓ Easy to add new bullet types without changing existing code")