Click here to Skip to main content
15,867,141 members
Articles / Game Development

Decorator Pattern Usage on a Simplified Pacman Game

Rate me:
Please Sign up or sign in to vote.
5.00/5 (24 votes)
9 Dec 2013CPOL11 min read 47.6K   881   45   6
An example of how Decorator, Dependency Injection, and Composition Root patterns can be used to extend the base game functionality.

Introduction

Decorator pattern is one of the essential software development patterns. It works nicely with Dependency Injection and Composition Root patterns. This small project is a showcase of its usage.

Background

I'm no game developer, I write enterprise applications. It's expected that I deliver code that is extensible and reusable. I struggled for a while but the situation has vastly improved since I write code with Dependency Injection and Decorator patterns in mind.

There are many introductory articles on Dependency Injection, Composition Root, and Decorator patterns so if you are unfamiliar with the concepts, I would recommend reading those first. I'm really a "show me the code" person so I will dive straight to the code and explain the concepts as I go.

Using the Code

Why a Pacman game clone? I wrote a CodeProject article about unit testing for a Tic Tac Toe game, and I read somewhere that Pacman should be the second novice game development project.

The project goals I had set were:

  • keep the code as simple as possible
  • implement an extendable game base using Dependency Injection and code to abstractions
  • compose everything in one place (composition root)
  • decorate game in composition root

To keep the game as simple as possible, no third party tools were used. All game base logic is placed in a class library project MazeLib, and the GUI implemented in a Windows Forms project Maze.

When Dependency Injection concepts are in use, dependencies can be decorated before they are sent to the class that needs them. With that concept, the base game GUI form can be implemented and functionality can be added without modifying the form later on.

The base game looks like this:

Image 1

By extending the player object via Decorator, it can change the appearance based on the direction it is moving.

Image 2

Same goes for ghosts.

Image 3

All options to add game functionality are accessible through options at the top of the main game form:

Image 4

Remaining options enable pickups to make ghosts vulnerable, temporarily freeze them, or eating them can add to player score. So can eating a ghost.

So how can this be achieved? In a more traditional approach, the main form could contain all the different code variations based on user options preference. And there is class decorating.

The high level concept of Decorator pattern works like this:

Image 5

The Decorator pattern has four parts. The first part, the component defines the abstract class with all the basic methods and properties, without any implementation. There are four classes in the game that can be decorated - AbstractBorder, AbstractGhost, AbstractPickup, AbstractPlayer. AbstractShape is the base class for other classes. It is a generic class, and it's most important property is Display. It returns a generic object that is used to display the shape on the form. For a Windows Forms GUI project, I used PictureBox as a generic parameter to display the shapes on the screen. The class hierarchy looks like this:

Image 6

The second pattern part is the concrete component. That is a non abstract class with basic implementation only. It must be inherited from the component class. So the concrete component classes are:

Image 7

The Decorator pattern part is where things start to become interesting. As the pattern says, decorator classes are classes that are inherited from the component class. They also get an instance of the component class through the constructor that they save as a protected variable. Last, but not the least, they are abstract. First, the class hierarchy:

Image 8

And part of the code for the GhostDecorator class:

C#
public abstract class GhostDecorator<T> : AbstractGhost<T>
{
    protected readonly AbstractGhost<T> _ghostToDecorate;

    public GhostDecorator(AbstractGhost<T> ghostToDecorate)
    {
        if (ghostToDecorate == null)
        {
            throw new ArgumentNullException("ghostToDecorate");
        }

        _ghostToDecorate = ghostToDecorate;
    }

    public override void ResetToStartLocation()
    {
        _ghostToDecorate.ResetToStartLocation();
    }

    // some code omitted
    public override Point Location
    {
        get
        {         
            return _ghostToDecorate.Location;
        }
    }

Important parts of Decorator class to notice are:

  • It is an abstract class
  • It is inherited from the Component class
  • It receives a Component class instance through the constructor and saves it in a protected member variable
  • All override methods and properties call the methods of protected member class instance that is received as constructor parameter

The Decorator class' sole purpose is to call all methods of the class it receives through the constructor. When we come to concrete decorators, it will make sense.

One more important class in the reusable MazeLib library is AbstractLevel. It receives a player object, a collection of borders, pickups, and ghost objects, and it handles collision detection between objects. It also fires an LevelCompleted event when the game is finished.

C#
public abstract class AbstractLevel<T>
{
    protected int _stepSize;
    protected AbstractPlayer<T> _player;
    protected List<AbstractBorder<T>> _borders;
    protected List<AbstractPickup<T>> _pickups;
    protected List<AbstractGhost<T>> _ghosts;

    public event EventHandler<AbstractLevel<T>> LevelCompleted;

    public AbstractLevel(int stepSize, AbstractPlayer<T> player, 
             List<AbstractBorder<T>> borders,
                 List<AbstractPickup<T>> pickups, 
             List<AbstractGhost<T>> ghosts)
    {
    // rest of the code omitted
} 

That's all the logic needed for the game, so it is separated from the GUI in the reusable MazeLib library.

Another project in the solution is the GUI project, Maze. It calls out the library methods for the base game functions and decorates the classes as needed. The high level game logic is, the first part occurs in the library, the rest is GUI project responsibility.

Image 9

In code, this is achieved by composing the class structure at one place - composition root. In the Windows Forms project, it is the Main() method of the Program class. Here, we create a class dependency graph for the game. The main form needs a level in order to operate - a level is the dependency of the main form:

C#
static void Main()
{
    FormMain mainForm = new FormMain(level);
    mainForm.GameRestarted += (sender, e) =>
    {
            level = ComposeLevel(e.Value);
            mainForm.Level = level;
    };
 
    Application.Run(mainForm);
}

public static AbstractLevel<PictureBox> ComposeLevel(ConfigurationOptions configurationOptions)
{
    AbstractPlayer<PictureBox> player = null;
        List<AbstractBorder<PictureBox>> borders = null;
        List<AbstractPickup<PictureBox>> pickups = null;
        List<AbstractGhost<PictureBox>> ghosts = null;

        var levelDataCreator = new LevelDataCreator<PictureBox>(_stepSize);
        levelDataCreator.LevelOneData(out player, out borders, out pickups, out ghosts);

        player = new PlayerPictureBoxDecorator(player);

        if (configurationOptions.PlayerFaces))
        {
            player = new PlayerPictureBoxFacesDecorator(player);
        }

        for (int i = 0; i < ghosts.Count; i++)
        {
            AbstractGhost<PictureBox> ghost = new GhostPictureBoxDecorator(ghosts[i]);

            if (configurationOptions.GhostFaces))
            {
                ghost = new GhostPictureBoxFacesDecorator(ghost);
            }
            if (configurationOptions.GhostTrackScore))
            {
                ghost = new GhostTrackScoreDecorator(ghost);
            }

            ghosts[i] = ghost;
        }

        for (int i = 0; i < pickups.Count; i++)
        {
            AbstractPickup<PictureBox> pickup = new PickupPictureBoxDecorator(pickups[i]);
                
            if (configurationOptions.PickupGhostVurneability))
            {
                pickup = new PickupGhostVulnerabilityDecorator(pickup, ghosts);
            }
            if (configurationOptions.PickupGhostTempFreeze))
            {
                pickup = new PickupGhostsTempFreezeDecorator(pickup, ghosts);
            }
            if (configurationOptions.PickupTrackScore))
            {
                pickup = new PickupTrackScoreDecorator(pickup);
            }

            pickups[i] = pickup;
        }

        return new LevelPictureBox(_stepSize, player, borders, pickups, ghosts);
    }
}

I will break down the code into high level concept parts:

Image 10

C#
AbstractPlayer<PictureBox> player = null;
List<AbstractBorder<PictureBox>> borders = null;
List<AbstractPickup<PictureBox>> pickups = null;
List<AbstractGhost<PictureBox>> ghosts = null;
 
var levelDataCreator = new LevelDataCreator<PictureBox>(_stepSize);
levelDataCreator.LevelOneData(out player, out borders, out pickups, out ghosts); 

Level borders, ghosts, and player objects are created first. Notice that all objects are abstract classes. Since objects are abstract, they can be decorated before sending them as dependencies to the level object.

The important things to notice here are the class types that are in the collections. Collections are originally defined as collections of abstract classes. Every collection can contain objects that are inherited. So after the call:

C#
levelDataCreator.LevelOneData(out player, out borders, out pickups, out ghosts);

We get the following results:

  • AbstractPlayer<PictureBox> player is an instance of the Ghost<PictureBox> class
  • List<AbstractBorder<PictureBox> borders is the list of Border<Ghost> objects
  • List<AbstractPickup<PictureBox>> pickups is the list of Pickup<PictureBox> objects
  • List<AbstractGhost<PictureBox>> ghosts is the list of Ghost<PictureBox> objects

Although variables are defined as abstract classes, they must contain instances of concrete objects. Declaring variables are abstraction enabled class decoration. In Decorator pattern words, we declare a variable to be a component type (abstraction) and we assign the concrete component instance to it.

If we just delete all the code in the ComposeLevel method after creating dependencies for the level object and return it:

C#
public static AbstractLevel<PictureBox> ComposeLevel(ConfigurationOptions configurationOptions)
{
    AbstractPlayer<PictureBox> player = null;
    List<AbstractBorder<PictureBox>> borders = null;
    List<AbstractPickup<PictureBox>> pickups = null;
    List<AbstractGhost<PictureBox>> ghosts = null;

    var levelDataCreator = new LevelDataCreator<PictureBox>(_stepSize);
    levelDataCreator.LevelOneData(out player, out borders, out pickups, out ghosts);

    // code deleted

    return new LevelPictureBox(_stepSize, player, borders, pickups, ghosts);
}

The code would compile, but we would get an empty form. What happened? All game logic is there and code compiles, but the form needs the information for how to paint the PictureBox object that is used as the Display property in the shape class. There are several ways to remedy the situation, one is directly setting the picture box properties to the object before sending them to the level class. If we just add PictureBox properties before sending classes to level object:

C#
public static AbstractLevel<PictureBox> ComposeLevel(ConfigurationOptions configurationOptions)
{     
     AbstractPlayer<PictureBox> player = null;        
     List<AbstractBorder<PictureBox>> borders = null; 
     List<AbstractPickup<PictureBox>> pickups = null;
     List<AbstractGhost<PictureBox>> ghosts = null;
     var levelDataCreator = new LevelDataCreator<PictureBox>(_stepSize);
     levelDataCreator.LevelOneData(out player, out borders, out pickups, out ghosts);

     // temp code for example

     var playerPictureBox = new PictureBox();
     playerPictureBox.BackColor = Color.Blue;
     playerPictureBox.Location = player.Location;
     playerPictureBox.Size = player.OccupiedSpace.Size;
     player.Display = playerPictureBox;

     foreach (var border in borders)
     {
         var borderPictureBox = new PictureBox();
         borderPictureBox.BackColor = Color.Green;
         borderPictureBox.Location = border.Location;
         borderPictureBox.Size = border.OccupiedSpace.Size;
         border.Display = borderPictureBox;
     }

     foreach (var pickup in pickups)
     {
         var pickupPictureBox = new PictureBox();
         pickupPictureBox.BackColor =  Color.CornflowerBlue;
         pickupPictureBox.Location = pickup.Location;
         pickupPictureBox.Size = pickup.OccupiedSpace.Size;
         pickup.Display = pickupPictureBox;
     }

     foreach (var ghost in ghosts)
     {
     	 var ghostPictureBox = new PictureBox();
         ghostPictureBox.BackColor = Color.Red;
         ghostPictureBox.Location = ghost.Location;
         ghostPictureBox.Size = ghost.OccupiedSpace.Size;
         ghost.Display = ghostPictureBox;
     }

     // temp code for example
     // code deleted

     return new LevelPictureBox(_stepSize, player, borders, pickups, ghosts);
}   

This would display the form and objects, as intended:

Image 11

But when we start the game, neither ghosts nor the player appear to be moving. Yet they do in code, but we need to override the base class methods to include the PictureBox moving on the screen when ghosts and the player move. So we can go two ways: inherit from the ghost class and add all the functionality there, or add Decorator classes.

Image 12

C#
player = new PlayerPictureBoxDecorator(player);

if (configurationOptions.PlayerFaces))
{
    player = new PlayerPictureBoxFacesDecorator(player);
}

for (int i = 0; i < ghosts.Count; i++)
{
    AbstractGhost<PictureBox> ghost = new GhostPictureBoxDecorator(ghosts[i]);

        if (configurationOptions.GhostFaces))
        {
            ghost = new GhostPictureBoxFacesDecorator(ghost);
        }
        if (configurationOptions.GhostTrackScore))
        {
               ghost = new GhostTrackScoreDecorator(ghost);
        }

    ghosts[i] = ghost;
}

for (int i = 0; i < pickups.Count; i++)
{
    AbstractPickup<PictureBox> pickup = new PickupPictureBoxDecorator(pickups[i]);
               
    if (configurationOptions.PickupGhostVurneability))
    {
            pickup = new PickupGhostVulnerabilityDecorator(pickup, ghosts);
    }
    if (configurationOptions.PickupGhostTempFreeze))
    {
            pickup = new PickupGhostsTempFreezeDecorator(pickup, ghosts);
    }
    if (configurationOptions.PickupTrackScore))
    {
            pickup = new PickupTrackScoreDecorator(pickup);
    }

    pickups[i] = pickup;
}  

This is where all the decorating occurs. Before the player object or the collection of ghosts, borders, and pickups are sent to the level instance, they are decorated. As we saw previously, for PictureBox implementation, we at least need to extend the base game to include the logic for the PictureBox moving. So we create the first concrete decorator component, GhostPictureBoxDecorator.

C#
internal class GhostPictureBoxDecorator : GhostDecorator<PictureBox>
{
     private PictureBox _display;

     public GhostPictureBoxDecorator(AbstractGhost<PictureBox> ghostToDecorate)
            : base(ghostToDecorate)
     {
          ghostToDecorate.GhostMoved += (sender, e) =>
          {
               Display.Location = e.Location;
          };
     }

     public override PictureBox Display
     {
          get
          {
               if (_display == null)
               {
                    _display = new PictureBox()
                    {
                       Location = Location,
                       Size = OccupiedSpace.Size,
                       BackColor = Color.Red
                    };
               }
               
               return _display;
          }
          set
          {
               base.Display = value;
          }
     }

     public override void MoveLeft()
     {
          _ghostToDecorate.MoveLeft();
          OnGhostMoved();
     } 

     // some code omitted
}  

To make PictureBox move on the screen is a two part process - setting up PictureBox instance in Display property:

C#
internal class GhostPictureBoxDecorator : GhostDecorator<PictureBox>
{

    // some code omitted    

    public override PictureBox Display
    {
         get
         {
              if (_display == null)
              {
                   _display = new PictureBox()
                   {
                       Location = Location,
                       Size = OccupiedSpace.Size,
                       BackColor = Color.Red
                   };
              }     
         }
} 

and updating PictureBox.Location property when ghost is moving by subscribing to GhostMoved event.

C#
public GhostPictureBoxDecorator(AbstractGhost<PictureBox> ghostToDecorate)
            : base(ghostToDecorate)
{
     ghostToDecorate.GhostMoved += (sender, e) =>
     {
         Display.Location = e.Location;
     };
} 

First concrete decorators classes for borders, player and pickups have the same responsibility -
set up properties for picture boxes so they can be displayed on the form.

The most basic composition for fully functional game with PictureBox could be set in the following way:

C#
public static AbstractLevel<PictureBox> ComposeLevel()
{
     AbstractPlayer<PictureBox> player = null;
     List<AbstractBorder<PictureBox>> borders = null;
     List<AbstractPickup<PictureBox>> pickups = null;
     List<AbstractGhost<PictureBox>> ghosts = null;

     var levelDataCreator = new LevelDataCreator<PictureBox>(_stepSize);
     levelDataCreator.LevelOneData(out player, out borders, out pickups, out ghosts);

     player = new PlayerPictureBoxDecorator(player);

     for (int i = 0; i < borders.Count; i++)
     {
          borders[i] = new BorderPictureBoxDecorator(borders[i]);
     }
     for (int i = 0; i < ghosts.Count; i++)
     {
          ghosts[i] = new GhostPictureBoxDecorator(ghosts[i]);
     }
     for (int i = 0; i < pickups.Count; i++)
     {
          pickups[i] = new PickupPictureBoxDecorator(pickups[i]);
     }

     return new LevelPictureBox(_stepSize, player, borders, pickups, ghosts); 
}

The game would function - all logic implemented in library would work, players could eat pickups but not ghosts, score is not tracked and pickups have no extra properties. But this is where the decorator concept begins to shine - adding functionality to existing, fully functional system. Let's add a simple one, GhostPictureBoxFaces decorator, which will change PictureBox.Image of ghosts objects depending on their movement direction.

C#
public class GhostPictureBoxFacesDecorator : GhostDecorator<PictureBox>
{
     public GhostPictureBoxFacesDecorator(AbstractGhost<PictureBox> ghostToDecorate)
            : base(ghostToDecorate)
     {
     }

     public override DIRECTION MoveAutomatically(IEnumerable<DIRECTION> availableDirections)
     {
          DIRECTION result = _ghostToDecorate.MoveAutomatically(availableDirections);

          if (result == DIRECTION.LEFT)
          {
               if (_ghostToDecorate.Display.BackColor == Color.Red)
               {
                    _ghostToDecorate.Display.Image = Properties.Resources.IMG_GHOST_MOVE_LEFT;
               }

               OnGhostMoved();
          }
          else if (result == DIRECTION.UP)
          {
               if (_ghostToDecorate.Display.BackColor == Color.Red)
               {
                   _ghostToDecorate.Display.Image = Properties.Resources.IMG_GHOST_MOVE_UP;
               }

               OnGhostMoved();
          }
          else if (result == DIRECTION.RIGHT)
          {
               if (_ghostToDecorate.Display.BackColor == Color.Red)
               {
                   _ghostToDecorate.Display.Image = Properties.Resources.IMG_GHOST_MOVE_RIGHT;
               }

              OnGhostMoved();
          }
          else if (result == DIRECTION.DOWN)
          {
               if (_ghostToDecorate.Display.BackColor == Color.Red)
               {
                   _ghostToDecorate.Display.Image = Properties.Resources.IMG_GHOST_MOVE_DOWN;
               }

               OnGhostMoved();
          }

          return result;
        } 
} 

MoveAutomatically is a method that generates a random ghost movement. After the base implementation call, we get the direction where the ghost had moved and we can update the Image accordingly.
To add this functionality to game, we need to wrap GhostPictureBoxDecorator instance with GhostPictureBoxFaces decorator in the composition root:

C#
public static AbstractLevel<PictureBox> ComposeLevel()
{
     AbstractPlayer<PictureBox> player = null;
     List<AbstractBorder<PictureBox>> borders = null;
     List<AbstractPickup<PictureBox>> pickups = null;
     List<AbstractGhost<PictureBox>> ghosts = null;

     var levelDataCreator = new LevelDataCreator<PictureBox>(_stepSize);
     levelDataCreator.LevelOneData(out player, out borders, out pickups, out ghosts);

     player = new PlayerPictureBoxDecorator(player);

     for (int i = 0; i < borders.Count; i++)
     {
          borders[i] = new BorderPictureBoxDecorator(borders[i]);
     }
     
     for (int i = 0; i < ghosts.Count; i++)
     {
          ghosts[i] = new GhostPictureBoxFacesDecorator(new GhostPictureBoxDecorator(ghosts[i]));
     }

     for (int i = 0; i < pickups.Count; i++)
     {
          pickups[i] = new PickupPictureBoxDecorator(pickups[i]);
     }

     return new LevelPictureBox(_stepSize, player, borders, pickups, ghosts); 
} 

This is the most important part of the article to understand - the class hierarchy of decorated ghosts objects so far.

Image 13

We will go step by step:

C#
levelDataCreator.LevelOneData(out player, out borders, out pickups, out ghosts);

After this line, ghosts objects are instances of Ghost object from the class library and they contain all base functionality. After the call:

C#
ghosts[i] = new GhostPictureBoxFacesDecorator(new GhostPictureBoxDecorator(ghosts[i]));

each ghost object first becomes GhostPictureBoxDecorator instance, decorator class that implemented Display property and handled movement animations. In other words, Ghost object instance is decorated with GhostPictureBoxDecorator . Then, that object is further decorated with GhostPictureBoxFacesDecorator.

C#
ghosts[i] = new GhostPictureBoxFacesDecorator(new GhostPictureBoxDecorator(ghosts[i])); 

The decorator added more functionality to an existing object, in this case, it changed display image based on ghost movement direction. All concrete decorators besides the necessary basic PictureBox decorators are available through game menu. When an option "Different movement faces" is checked:

Image 14

the player object will be decorated in this line:

C#
if (configurationOptions.PlayerFaces))
{
    player = new PlayerPictureBoxFacesDecorator(player);
}

After the AbstractPlayer object instance player is decorated, the decorated instance is sent as the dependency to the level object. The level object just uses the player object, whether it is decorated or not.

The question I get a lot is - why don't you use simple object inheritance, why complicate things for future maintenance developers with your fancy decorators? And it's a fair question, all OO developers understand object inheritance so it is sometimes smarter to avoid decorators altogether. After all, decorator is just a variation of object inheritance. But take a look at the options menu. Why don't I just extend the ghost class and implement Display property and movement in it? No PictureBoxDecorator class is necessary, right? Yes, that's correct. And after that, I can just implement score tracking in the same class? Of course. In that scenario, object graph would be much simpler:

Image 15

But how about more complex options system? With no decorator pattern, I need a separate class for every option combination. This game still implements small number of options, but the problem becomes evident as we add options to the system. Since player object has only one optional functionality added, using decorators seems like an overkill.

Image 16

But things tie in decorator vs simple inheritance discussion when we have two options added on base functionality.

Image 17

Notice here that we need a separate class to include ghost movement faces and score tracking functionality. I guess another approach could be class that inherits from GhostFaces and implements score tracking and similar class that is inherited from GhostScore and it implements faces functionality. That way, instead of one, we would get two additional classes. In any case, code would be duplicated.

The problem really becomes evident with 3 options, like for the pickup class:

Image 18

We have 3 options to cover, therefore 7 combinations in total:

  1. Ghost vulnerability only
  2. Ghost temp freezing only
  3. Score tracking only
  4. Ghost vulnerability and temp freezing
  5. Ghost vulnerability and score tracking
  6. Ghost temp freezing and score tracking
  7. Ghost vulnerability, ghost temp freezing and score tracking (all)

Bottom line is - the more options we add to the existing system, the more powerful decorator concept becomes. By using decorator, I have made a composition root with simple class instancing logic. Instead of comparing single options to decorate class with in composition root, I would have logic that compares option combinations and creates instance of concrete classes instead:

C#
// pseudo code

if (PickupGhostVulneability && !PickupGhostTempFreeze && !PickupTrackScore)
{
     // only PickupGhostVurneability is true, therefore
     // instantiate PickupGhostVurneable class
     // this is option combination 1 from previous list
}
else if (!PickupGhostVulneability && PickupGhostTempFreeze && !PickupTrackScore)
{
   // only PickupGhostTempFreeze is true, therefore
   // instantiate PickupFreezeGhost class
   // this is option combination 2 from previous list
}
else if (!PickupGhostVulneability && !PickupGhostTempFreeze && PickupTrackScore)
{
   // only PickupTrackScore is true, therefore
   // instantiate PickupTrackScore class
   // this is option combination 3 from previous list
}
else if (PickupGhostVulneability && PickupGhostTempFreeze && !PickupTrackScore)
{
   // PickupGhostVurneability and PickupGhostTempFreeze are both true, therefore
   // instantiate PickupGhostVulnerableFreeze class
   // this is option combination 4 from previous list
}
else if (PickupGhostVulneability && !PickupGhostTempFreeze && PickupTrackScore)
{
   // PickupGhostVulneability and PickupTrackScore are both true, therefore
   // instantiate PickupGhostVurneableTrackScore class
   // this is option combination 5 from previous list
}
else if (!PickupGhostVulneability && PickupGhostTempFreeze && PickupTrackScore)
{
   // PickupGhostTempFreeze and PickupTrackScore are both true, therefore
   // instantiate PickupFreezeGhostsTrackScore class
   // this is option combination 6 from previous list
} 
else if (PickupGhostVurneability && PickupGhostTempFreeze && PickupTrackScore)
{
   // all parameters are true, therefore
   // create class that includes all additional functionality, PickupVurneableFreezeTrackScore 
   // this is option combination 7. from previous list
}  

Instead, with decorators, the same can be achieved in a simpler way.

C#
// actual code

if (PickupGhostVulneability)
{
     pickup = new PickupGhostVulnerabilityDecorator(pickup, ghosts);
}
if (PickupGhostTempFreeze)
{
     pickup = new PickupGhostsTempFreezeDecorator(pickup, ghosts);
}
if (PickupTrackScore)
{
     pickup = new PickupTrackScoreDecorator(pickup);
}  

Last but not least, system stays extendable for future expansion.

Back to the project code, after classes level dependencies are created, they are passed to level object via level constructor.

Image 19

C#
return new LevelPictureBox(_stepSize, player, borders, pickups, ghosts); 

Then main form is displayed:

Image 20

C#
static void Main()
{
    Application.EnableVisualStyles();
    Application.SetCompatibleTextRenderingDefault(false);

    var level = ComposeLevel();

    FormMain mainForm = new FormMain(level);
    mainForm.GameRestarted += (sender, e) =>
    {
        level = ComposeLevel(e.Value);
            mainForm.Level = level;
    };
 
    Application.Run(mainForm);
}

And finally, when the game is restarted, game options are sent as an eventargs so classes can be composed and decorated again:

C#
static void Main()
{
    Application.EnableVisualStyles();
    Application.SetCompatibleTextRenderingDefault(false);

    var level = ComposeLevel();

    FormMain mainForm = new FormMain(level);      
   
    mainForm.GameRestarted += (sender, e) =>
    {
        level = ComposeLevel(e.Value);
        mainForm.Level = level;
    };

    Application.Run(mainForm);
} 

Points of Interest

Class decorating is a powerful concept, basically it is a variation of class inheritance, with one important difference - it keeps single responsibility per class, can reduce code size, and drastically improve the overall code quality.

Please rate and comment if I can make any article improvements. Thank you!

History

  • 11-30-2013: Initial version
  • 12-08-2013: More code examples, discussion about simple class inheritance vs class decorating

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
Software Developer
Serbia Serbia
Code, chicks and rock'n'roll \m/

Comments and Discussions

 
QuestionDecorator or Composite Pattern Pin
zafirov31-Jan-16 10:23
zafirov31-Jan-16 10:23 
AnswerRe: Decorator or Composite Pattern Pin
Petar Brkusanin1-Feb-16 12:20
Petar Brkusanin1-Feb-16 12:20 
Questionsuperb article!!! thanks. Pin
AmitMukherjee2-Dec-13 22:48
AmitMukherjee2-Dec-13 22:48 
AnswerRe: superb article!!! thanks. Pin
Petar Brkusanin2-Dec-13 22:50
Petar Brkusanin2-Dec-13 22:50 
GeneralMy vote of 5 Pin
M Rayhan2-Dec-13 19:04
M Rayhan2-Dec-13 19:04 
GeneralRe: My vote of 5 Pin
Petar Brkusanin2-Dec-13 20:05
Petar Brkusanin2-Dec-13 20:05 
Thank you!

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.