Hidden Object: Episode 10 – Counting Items Found with Counter Triggers, Actions & a Behavior

October 4, 2009 in Hidden Object Game, Silverlight

In episode 9 of Creating a Hidden Object Game is Silverlight 3 we added additional screens to the game. In this episode, we will add the Win screen and a collection of triggers, actions, & behaviors that work with a global counter.

The Win screen is shown after all 13 items have been clicked. What we need is an integer counter that subtracts one for each item clicked and when the count gets to zero change the state of the MainPage UserControl to show the screen.

Similar to what we did in the last episode, create a new Canvas called winCanvas and position it to the left of the UserControl on the artboard.

In the States panel, add the WinScreen state to the ScreenStates group:

With recording mode on for the WinScreen state, set the Top and Left properties of winCanvas to 0.

All we need to do is figure out a way to trigger a state change to show this screen.

The solution is a variation of the Global State Behavior sample by Christian Schormann which uses a Singleton object containing a Dictionary to hold values based on a key (or tag).

Let’s go step-by-step through the use of the GlobalCounter classes before we look at any code.

The counter store (GlobalCounterStore) has an empty dictionary keyed using a CounterKey. Some trigger invokes SetGlobalCounterAction which sets the initial values for Value, MinValue, and MaxValue in a dictionary item with a CounterKey of “HiddenItems”. In this case the trigger that sets the values is an EventTrigger on the MainPage UserControl for the Loaded event:

100409_0710_HiddenObjec3.png 

 

Any time that an entry is changed in the counter store, the Changed event is raised which passes Key, Value, MinValue, and MaxValue. Both the ShowGlobalCounterBehavior and the GlobalCounterChangedTrigger handle this event. Since every instance of the GlobalCounterChangedTrigger & ShowGlobalCounterBehavior handle the Changed event they need to have a CounterKey property to determine if they should take any action for that event. In the case of ShowGlobalCounterBehavior, the associated TextBlock’s Text property is set to Value. For the GlobalCounterChangedTrigger any actions are invoked. In this episode, the GlobalCounterChangedTrigger is not used but is included for completeness.

Now that the counter store has been initialized with values, we can use the IncrementGlobalCounterAction to increment the value. In this case we are incrementing the value by -1 which updates the value in the counter store causing the Changed event to fire which is handled by the ShowGlobalCounterBehavior which sets the text on the TextBlock.

100409_0710_HiddenObjec4.png

 

The IncrementGlobalCounterAction is invoked for each Path object clicked until the counter reaches the MinValue:

100409_0710_HiddenObjec5.png

 

The Changed event is fired to update the TextBlock, but the MinValueReached event is also fired. This is handled by the GlobalCounterMinReachedTrigger which compares its CounterKey with the Key passed in the event and if they match then the trigger invokes its actions. For this episode, there is a GoToStateAction associated with the UserControl that responds to the GlobalCounterMinReachedTrigger and sets the current state to WinScreen which shows winCanvas.

In cases where you want an incrementing counter instead of a decrementing one, pass a positive value for the IncrementCounterValueBy property of IncrementGlobalCounterAction and use the GlobalCounterMaxReachedTrigger.

The global counter classes can be used for many things including a lives counter, health, shield strength, or to keep track of a player’s score.

Here are a class diagram of the GlobalCounterStore and related classes:

Besides the implementation of the Singleton pattern, the most interesting code can be found in the SaveToStore and IncrementCounter methods of GlobalCounterStore:

internal sealed class GlobalCounterStore
{
    private Dictionary<string, GlobalCounterItem> store;

    internal void SaveToStore(string key, int value, int minValue, int maxValue)
    {
        if (key == null)
            return;
        var item = new GlobalCounterItem() {Value = value, MinValue = minValue, MaxValue = maxValue };

        if (store.ContainsKey(key))
        {
            store[key] = item;
        }
        else
        {
            store.Add(key, item);
        }

        OnChanged(key, item);
    }

    internal void IncrementCounter(string key, int incrementBy)
    {
        if ((key == null) || !store.ContainsKey(key))
            return;

        int newValue = store[key].Value + incrementBy;
        store[key].Value = newValue;

        OnChanged(key, store[key]);

        if (newValue >= store[key].MaxValue)
            OnMaxValueReached(key, store[key]);

        if (newValue <= store[key].MinValue)
            OnMinValueReached(key, store[key]);
    }
}

 

The SetGlobalCounterAction is a TriggerAction that defines four dependency properties for CounterKey, CounterValue, CounterMinValue, and CounterMaxValue. The Invoke method calls the SaveToStore method on GlobalCounterStore.

The IncrementGlobalCounterAction is a TargetedTriggerAction that defines dependency properties for CounterKey and IncrementCounterValueBy. It’s Invoke method calls the IncrementCounter method on GlobalCounterStore.

 

The three triggers are very similar in that each defines a CounterKey dependency property and each adds a handler to an event raised by GlobalCounterStore:

GlobalCounterTriggers

 

In the case of GlobalCounterMinReachedTrigger, the OnAttached method adds a handler for the GlobalCounterStore’s MinValueReached event whereas the handler is removed in the OnDetaching method:

public class GlobalCounterMinReachedTrigger : TriggerBase<DependencyObject>
{
    protected override void OnAttached()
    {
        base.OnAttached();
        GlobalCounterStore.Instance.MinValueReached += Instance_MinValueReached;
    }

    protected override void OnDetaching()
    {
        base.OnDetaching();
        GlobalCounterStore.Instance.MinValueReached -= Instance_MinValueReached;
    }

    void Instance_MinValueReached(object sender, GlobalCounterEventArgs e)
    {
        if (CounterKey == null)
            return;

        if (CounterKey == e.Key)
            this.InvokeActions(e);
    }
}

 

When the MinValueReached event is raised due to a change in the GlobalCounterStore to any dictionary entry regardless of its key, the passed key (e.Key) is compared to the CounterKey property. If the values match, then the actions for this trigger are invoked.

 

ShowGlobalCounterBehavior is a behavior that can be attached to a TextBlock control and functions is a similar way as the triggers:

 

 

It defines the CounterKey property and handles the Changed event raised by GlobalCounterKey:

public class ShowGlobalCounterBehavior : Behavior<TextBlock>
{
    protected override void OnAttached()
    {
        base.OnAttached();
        GlobalCounterStore.Instance.Changed += Instance_Changed;
    }

    protected override void OnDetaching()
    {
        base.OnDetaching();
        GlobalCounterStore.Instance.Changed -= Instance_Changed;
    }
 
    void Instance_Changed(object sender, GlobalCounterEventArgs e)
    {
        if (CounterKey == e.Key)
        {
            if (e.Value > e.MaxValue)
                AssociatedObject.Text = e.MaxValue.ToString();
            else if (e.Value < e.MinValue)
                AssociatedObject.Text = e.MinValue.ToString();
            else
                AssociatedObject.Text = e.Value.ToString();
        }
    }
}

 

It is possible to increment the Value in the counter store by more than 1 so I added checks so that the TextBlock will never show values greater than MaxValue or less than MinValue.

 

Now it is time to use these classes in Blend. Drop an instance of SetGlobalCounterAction on the UserControl, a GoToStateAction on LayoutRoot, and a IncrementGlobalCounterAction on a Path:

 

 

The values for SetGlobalCounterAction are set as follows:

 

 

The new GoToStateAction uses GlobalCounterMinReachedTrigger and sets the StateName to WinScreen:

 

 

Each path will have an IncrementGlobalCounterAction set with these values:

 

 

 

Finally, we add a TextBlock to our Canvas to hold the count of remaining items and add the ShowGlobalCounterBehavior to it:

 

 

 100409_0710_HiddenObjec15.png

 

 

The only property to set on the behavior is the CounterKey: 

 

 

Once we add IncrementGlobalCounterAction to each Path, we can run the game and as we find items the counter will decrement by 1. When the value reaches 0, then the winning screen is shown.

 

Source code

Demo

 

In the next episode we will update the Particles Behavior to allow particle custom shapes.

Hidden Object: Episode 10 – Counting Items Found with Counter Triggers, Actions & a Behavior

0 Comments

    1. [...] the next episode, we will try to figure out a way to determine when all the objects have been found so we can show a [...]

    2. [...] Episode 10 – Counting Items Found with Counter Triggers, Actions & a Behavior (source, demo) [...]

Leave a Reply

Hidden Object: Episode 10 – Counting Items Found with Counter Triggers, Actions & a Behavior

2 Trackbacks