SmartMonsters

Tuesday, February 4, 2014

Using the "Strategy" OO Pattern for Player Subjectivity

Uniquely to any "game" we're familiar with, TriadCity imposes elaborate subjectivities on its players.

This means that if you and I walk our characters into the same Room at the same time, we might see it described differently depending on our characters' attributes, skills, moral alignment, history, and other variables. The type and degree of subjectivity is largely controlled by the world author who wrote the Room. Most authors tread lightly, but there's no reason they can't make things wildly different if that makes sense for their context.

It also means that one of our characters could be Schizophrenic. We can impose any manner of "unreal" experiences on characters, from highly distorted Room descriptions to conversations with other characters who don't actually exist.

The code which enables these two forms of subjectivity is conceptually simple. Each form relies on the Strategy OO pattern, applied in different places.  I'll briefly sketch them both.

Rooms, Items, Exits, Characters all share common attributes inherited from their parent class, WorldObject. These include Descriptions, Smells, Sounds, Tastes, and Touches. I'm capitalizing the nouns because these are Java Interfaces which WorldObject possesses by composition. A code snip for illustration:

public abstract class WorldObject extends Object() {
          protected Descriptions descriptions;
          protected Smells smells;
          protected Sounds sounds;
          protected Tastes tastes;
          protected Touches touches;
          public final String getDescription(WorldObject observer) {
                    return this.descriptions.getDescription(observer);
          }
          public final String getSmell(WorldObject observer) {
                    return this.smells.getSmell(observer);
          }
           public final String getSound(WorldObject observer) {
                    return this.sounds.getSound(observer);
          }
           public final String getTaste(WorldObject observer) {
                    return this.tastes.getTaste(observer);
          }
           public final String getTouch(WorldObject observer) {
                    return this.touches.getTouch(observer);
          }


}

Descriptions, Smells, Sounds, Tastes and Touches are Strategy Pattern Interfaces defining the simple contract which will be implemented by a rich hierarchy of concrete classes. At runtime, the correct implementing class is assigned to each WorldObject according to the directions given to that WorldObject by the author who wrote it.

For example, a world author may write a Room and choose to have its descriptions vary by hour of day. Using our authoring tools, she assigns it the TimeBasedDescriptions type, and writes a description for dawn, dusk, day, and night. At server boot, that Room will be assigned the type of Descriptions she specified, and populated with her writing. The TimeBasedDescriptions class has the responsibility of figuring out what time it is whenever its getDescription() method is called, and returning the right one.

That's not subjective, though. Although the descriptions vary by time of day, the same description will be shown to every character who enters the Room. If she wants to, our world author can be more adventurous, assigning concrete Description types from our library. She could choose AlignmentAndTimeBasedDescriptions which will show descriptions which vary by both time of day and character moral alignment; or Descriptions which vary by Gender, or health condition, or attribute status. Each concrete implementing class knows how to calculate the description which is appropriate for the observer. As you can see, the code for WorldObject remains conceptually simple.

A second and often more radical flavor of subjectivity is implemented by a Strategy Interface called Consciousness. Each character has a composed Consciousness object which acts as a filter, transforming messages received via game Events, or not, according to its logic. The TriadCharacter class includes these snips:

public abstract class TriadCharacter extends WorldObject() {
          protected Consciousness consciousness;
          public boolean proccessEvent(MovementEvent evt) {
                    return this.consciousness.processEvent(evt);
          }
          public boolean proccessEvent(SenseEvent evt) {
                    return this.consciousness.processEvent(evt);
          } 
          public boolean proccessEvent(SocialEvent evt) {
                    return this.consciousness.processEvent(evt);
          }
}

Concrete Consciousness implementations include SleepingConsciousness, FightingConsciousness, SchizophrenicConsciousness, IncapacitatedConsciousness, and many others. Their job is to analyze incoming Events and determine whether to pass along the messages associated with those Events unchanged, or dynamically altered. A simple change might be to squelch an Event altogether if the character is asleep. A radical one might be to modify a ChatEvent or TellEvent, or even generate entirely new ones, if the character is experiencing SchizophrenicConsciousness. In these cases, the Consciousness type is assigned by the server at runtime, not by world authors, and it'll be swapped out for a different one as circumstances change. So these transformations are extremely dynamic.

Using the Strategy pattern via composition allows the class hierarchy to remain reasonably flat and really very simple. It would be possible for example to create a massive class tree resulting in things like TriadCharacterWithDescriptionsThatVaryByAlignmentAndTimeWithSchizophrenicConsciousness. But for obvious reasons that would be a nightmare to maintain. Composition keeps the WorldObject tree simple; Strategy objects chosen at runtime provide the rich library of behaviors.

a coloured rendering of a complex graph network

No comments:

Post a Comment