2

Description of Desired Behaviour

I have the following scenario:

  • Component A can run freely as long as it is not blocked.
  • Any other component can block the component A from running at any time.
  • After blocking, those components may unblock A.
  • Component A resumes running once ALL components that once blocked it "releases" the blocks. If there at least one component still blocking, the component A should not run.

Is there any canonical design/concept for this behaviour?

I have considered it to be a "infinite" Semaphore, but it kind of works like a reversed version of it. I have seen that C# has this CountdownEvent Class, but it looks like it is more than what I need.


Sample Implementation (In C#):

Public Interface:

public class ComponentA
{
    public ComponentA();
    public bool IsBlocked();
    public Block RequestBlock();
}

public class Block()
{
    public void Release();
}

Usage:

ComponentA myComponent = new ComponentA();
myComponent.IsBlocked(); // false

Block firstBlock = myComponent.RequestBlock();
myComponent.IsBlocked(); // true

Block secondBlock = myComponent.RequestBlock();
myComponent.IsBlocked(); // true

firstBlock.Release();
myComponent.IsBlocked(); // true

secondBlock.Release();
myComponent.IsBlocked(); // false

Implementation Details:


public class ComponentA
{
    private int _numBlocks;

    public ComponentA() {
        this._numBlocks = 0;
    }

    public bool IsBlocked() {
        return _numBlocks > 0;
    }

    public Block RequestBlock() {
        this._numBlocks++;
        return new Block(() => { this._numBlocks--; });
    }
}

public class Block
{
    private Action _unBlockAction;

    internal Block(Action unblockAction) {
        this._unBlockAction = unblockAction;
    }

    public void Release() {  
        this._unBlockAction();
    }
}
8
  • What problem is this solving? I'm at a loss as to where it would actually be used. Dec 20 '19 at 3:18
  • Also, if your Block implemented the IDisposable interface it can be used in a using block making the release more guaranteed. All in all, I think just using a Barrier is a safer way to ensure all initialization is complete before moving forward. Dec 20 '19 at 3:23
  • This exact problem has come up in many different scenarios during the development of my game. For example, the player should be blocked from opening the inventory menu by many different scenarios: when the player is in a dialogue, when it is jumping, when its transitioning to a different game zone, when its looting, etc etc etc. Many different subsystems should "block" another subsystem from running. I see many situations in a game in which a subsystem should be "on hold" while different "block conditions" exist. Dec 20 '19 at 3:35
  • 1
    Not sure what the concept is called in game dev, but, just wanted to say, you may want to consider a different design, because this can be tricky to debug if something goes wrong, and currently, you it looks like you have no info on what is holding the block; it looks like the only information you are keeping is the count (and your Block class is essentially a Memento, and is opaque to client code). Instead, you could model these "blockers" or "action filters" as components (or objects) that hold some info/metadata that you can display in debug output. 1/2 Dec 20 '19 at 8:54
  • 2
    "the player should be blocked from opening the inventory menu." I don't think you're using "blocked" in a conventional sense (as in a thread blocking). When something becomes unblocked it would typically proceed immediately, but I doubt you want the inventory menu to suddenly appear after it is unblocked. I would rephrase it "The inventory menu should be disabled while the player is ____." The disablement of UI elements is a type of UI mode.
    – John Wu
    Dec 20 '19 at 17:21
1

You describe a Readers-Writer Lock, but your readers can read after write has started and thread-safety is absent.

I think it is inconsistent to disallow a player to open inventory in dialog, but to allow a player to enter a dialog when inventory is open. If you make requirements symmetric, however, they will exactly match requirements for Readers-Writer lock.

1

I don't think there is a design pattern per se. You just have to pick the appropriate synchronization primitive.

If you do not need to drain the callers, if you just need to keep new callers from proceeding, you can use a manual reset event. This is like taxiing at an airport, the pilot needs permission from the tower, but ground control could stop issuing permits but not wait for you to arrive at the gate. There is no wait to issue the block.

If you need an exclusive semantic, use a acquire shared/exclusive lock like a reader/writer lock. If tower wants the runway de-iced, they certainly would want to wait until all airplanes which have been issued permission to land have actually landed. There is a wait to issue the block. (Technically, you could not wait for the readers to drain, but who would issue the un-block?)

1

What this boils down to is State Management, and there are several ways to address this. In the world where there is a user interface, it's common to have an MVVM architecture.

In MVVM, C# you would implement the ICommand interface to open your inventory window implemented to check the state of all the other models that represent the different game systems. For example:

public class InventoryCommand : ICommand
{
    public event EventHandler CanExecuteChanged;

    public bool CanExecute(object context)
    {
       var world = context as World;
       bool canExecute = world != null;

       // do all your checks here on your game state
       canExecute = canExecute && ....;

       return canExecute;
    }

    public void Execute(object context)
    {
        // actually do the thing here
    }

    public void RaiseCanExecuteChanged()
    {
        CanExecuteChanged?.Invoke(EventArgs.Empty);
    }
}

All that's left at this stage is to call the RaiseCanExecuteChanged() method when the world state changes in a way you care about. The InventoryCommand would be bound to the controls that open the inventory (buttons, key combination, etc.). At least this is the way that the MVVM approach works.


Your solution to the problem is essentially another way to handle the state management, but using a naive garbage collection mechanism called Reference Counting.

The implementation does leave something to be desired because there is no way to force that references to be cleaned up. You are relying on the good graces of the user to remember to release the block appropriately. You also need to prevent the user from accidentally calling the Release method in a loop. Those are a couple reasons that reference counting is not something that is in common use in garbage collected languages these days.

If we change your Block implementation to use the IDisposable pattern, we can take advantage of C#'s garbage collection and using construct to automate release of the blocks:

internal class Block : IDisposable
{
    private Action onComplete;

    public Block(Action completeAction)
    {
        onComplete = completeAction;
    }

    ~Block()
    {
        Dispose(false);
    }

    public void Dispose()
    {
        Dispose(true);
        GC.SuppressFinalize(this); // prevent second call in finalizer
    }

    private void Dispose(bool calledExplicitly)
    {
        onComplete?.Invoke();
        onComplete = null; // clear it to prevent double calls
    }
}

Your ComponentA.RequestBlock class would have to be changed like this:

public IDisposable RequestBlock() // or AcquireBlock()
{
    this._numBlocks++;
    return new Block(() => { this._numBlocks--; });
}

That leaves the correct way to use it like this:

using(inventory.RequestBlock())
{
    // do things, the block is automatically released
    // when the using block ends.
}

Also, if the programmer forgets the using statement, the Block will eventually get captured by the garbage collector and the counter will decrement. The implementation here ensures the action is called once, and only once.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.