- Separating Abstraction from Implementation
- Dice, and Visual Representations of Dice
- Playing Sound Files with API Calls
- Managing Object State Change with the Observer Pattern and Events
Managing Object State Change with the Observer Pattern and Events
The last technique to talk about is how we might coordinate changes in observable state and the objects underneath them. Consider our loan Die. When it begins rolling, we need to update its appearance and play a sound file. When the Die ceases rolling, in the context of a game, we may need to tally up the value of the Die or several Dice. In Dihtzee, like Yahtzee, there are five dice that make up possible scores (for example, three of a kind or a small straight).
Coordinating all of the views in an application can be a daunting task. However, we can coordinate the third pinnacle of event-driven programming, events, with an easy to implement pattern. That lets us easily coordinate relationships between objects and interested parties.
The Observer pattern is comprised of observers and observable things; I'll refer to them as observers and subjects respectively. Because no requirement exists that an observer or subject stem from a common family, observers and subjects are more aptly represented as interfaces.
Consistent with common practice we will call these interfaces IObserver and ISubject. ISubject is the thing being observed and IObserver will have one property, its subject. Listing 5 contains the definition of IObserver and ISubject.
Listing 5: IObserver and ISubject.
using System; namespace RollOfTheDice { public interface IObserver { ISubject Subject{set;} } public interface ISubject{} }
IObserver uses a write-only property, because it only needs to know its subject but doesn't need to reveal that information to any other entity. (This is one of a few valid uses of write-only properties.)
To employ the Observer pattern, suppose we want a separate window to keep track of the value of the roll. The observer could be a class, UserControl, Windows Form, or something else. For our example, we will make the observer a separate Windows Form and the subject will be the VisualDie.
Making the VisualDie Observable
We need to do some very basic things to add the ISubject facet to the VisualDie. First, we must indicate that the VisualDie implements ISubject. This is accomplished by changing the class header, as follows:
public class VisualDie : Control, ISubject
The next thing to do is provide something to observe. Typically, these are formulated as public events to which the observer can subscribe. We know that we want to know when a Die has finished rolling. To observe this behavior we first need to add it to the VisualDie class: add a public EventHandler named OnEndRoll, and modify the Roll method to raise the event when the internal call to Roll returns. Listing 6 shows the minor revision to the VisualDie class.
Listing 6: Adding a behavior to the VisualDie class that an Observer can subscribe to.
public EventHandler OnEndRoll; public void Roll() { internalDie.Roll(); if( OnEndRoll != null ) OnEndRoll(this, EventArgs.Empty); }
Implementing an Observer Form
The next step is to implement our observer. While I said this could be any class, I'll use a Form to demonstrate how loosely coupled the subject and observer can remain.
The form itself simply has a label on it, and it implements IObservable. When we create the form we tell it which Die it should observer. As soon as the form obtains a subject it binds an event handler to the events it wants to observer. Listing 7 contains a partial listing showing just the salient details we added to the form.
Listing 7: A separate form observing a specific die.
using System; using System.Drawing; using System.Collections; using System.ComponentModel; using System.Windows.Forms; using RollOfTheDice; namespace VisualGuess { public class RollValue : System.Windows.Forms.Form, IObserver { // elided form code (an empty form with a single Label #region IObserver Members private VisualDie die = null; public ISubject Subject { set { die = value as VisualDie; die.OnEndRoll += new EventHandler(OnEndRoll); } } #endregion private void OnEndRoll(object sender, EventArgs e) { label1.Text = "Value: " + die.CurrentFace; } } }
The class header indicates that the Form RollValue is implementing IObservable. Visual Studio .NET 2003 will automatically stub out members of realized—that is when you inherit from—interfaces on your behalf. All we need to do is assign the subject to our internal reference to the subject instance passed in, and subscribe to the event. The statement die += value as VisualDie caches the subject object. (We might want to add an is test to ensure that the subject is of the correct type.) And, the statement die.OnEndRoll += new EventHandler(OnEndRoll) subscribes our observer to the OnEndRoll event of the subject.
When the VisualDie finishes rolling, it raises an event, and we get an opportunity to update the value of the roll here.
Observing a Roll of the Die
The final step to create both the observer and subject, and tell the observer what to observe. In our example, the observer is an instance of the RollValue form, and the subject is the instance of the VisualDie on our original form. Listing 8 illustrates how we can orchestrate this marriage.
Listing 8: Marrying subject and observer.
private VisualDie die; private void Form1_Load(object sender, System.EventArgs e) { die = new VisualDie(); die.SetBounds(10, 10, 40, 40); die.Parent = this; RollValue form = new RollValue(); form.Owner = this; form.Subject = die; form.Show(); }
Figure 2: The Observer—the top-most form—seeing, or observing, what its subject, the dice, is doing.
In Listing 8, we create the subject first“the VisualDie“followed by the observer“the RollValue form. The extra step is to assign the die to the Form's Subject property. After this code runs, the RollValue form responds in its own unique way to a roll of the dice. Neither the form that owns the Die or the Die itself cares about what the observer is doing. This separation of responsibilities is as it should be.
Summary
If you are new to .NET, then this article provided you with a chance to jump right in and work on a variety of skills from the pedagogic to the advanced. Knowing how to invoke API methods is still occasionally useful, as demonstrated by calling into the sound API. Knowing how to use the much more powerful GDI+ can help you add visual power and dynamic interest to your applications, as demonstrated with the animated dice, and, mastering common patterns will make it much easier to implement solutions using existing, general solutions.
I hope you enjoyed the very basic introduction to game programming, and found some new useful skills to help in other programming endeavors.
Paul Kimmel is the founder and chief architect for Software Conceptions, Inc. and a co-founder and President of the Greater Lansing Area .NET Users Group. He has written several books on .NET and object-oriented programming, including Visual Basic .NET Power Coding, and is available to build software for your company. You may contact him at pkimmel@softconcepts.com.