Interactions between objects - Attributes and Interfaces

One of the greatest features of NetHack was the ability to achieve a rich interaction between objects.

· What happens if you use a candle on a scroll? It should start on fire.

· What if you throw a potion on the ground? The potion shatters.

· What if you dip a flaming long sword in water? It becomes a normal long sword.

Attention to small details like this are what typically make a good game great. So we don’t want .NetHack to be any exception, but the question is how can we leverage .Net to give us this rich interaction between objects without a lot of work? The answer: Attributes and Interfaces.

Using Interfaces
Built into C# and VB.Net is a great language feature that checks if an object is ‘type compatible’ with a given type. So if I give you a generic System.Object, you can easily check if that object is compatible with class Foobar or better yet implements the interface IDoStuff.

Using Attributes
Unfortunately the way to access attributes isn’t nearly as clean as checking if an object implements an interface, but it is still doable. Simply get the objects’s underlying type and then check. In our example we provide this functionality via a based object 'DnhObject'.

Example
In this example we will define a rich interaction easily by using interfaces and attributes. It may look like a lot of code, but the key is that we are very declaritive when defining the behaviour of our objects. We don't need a lot of special cases for this or that. Simply declaring an object is Fragile means that when you drop it it breaks. If an object implements IDrinkable then you can quaff it.

using System;

namespace DnhDemo

{

    class Program

    {

        static void Main(string[] args)

        {

            DnhItem[] itemsToPlayWith = { new Sword(), new HealthPotion(), new OilDrum() };

            foreach (DnhItem item in itemsToPlayWith)

            {

                Console.WriteLine("Interacting with a " + item.ToString());

               

                Console.Write("Dropping... ");

                item.Drop();

                Console.Write("Kicking... ");

                item.Kick();

                if (!(item is IDrinkable))

                    Console.WriteLine("Item is not drinkable.");

                else

                {

                    Console.Write("Drinking... ");

                    (item as IDrinkable).OnDrink();

                    Console.WriteLine();

                }

                if (!(item is IReadable))

                    Console.WriteLine("Item is not readable.");

                else

                {

                    Console.Write("Reading... ");

                    (item as IReadable).OnRead();

                    Console.WriteLine();

                }

                Console.WriteLine();

            }

        }

    }

#region "Base Objects"

    /// <summary>

    /// Base type for all .NetHack objects.

    /// </summary>

    public class DnhObject

    {

        /// <summary>

        /// Returns whether or not this type implements a given attribute. (Checks

        /// up the inheritance chain as well.)

        /// </summary>

        /// <param name="attributeType"></param>

  /// <returns></returns>

        public bool HasAttribute(Type attributeType)

        {

            return (this.GetType().GetCustomAttributes(attributeType, true).Length > 0);

        }

    }

    /// <summary>

    /// Any 'item'

    /// </summary>

    public class DnhItem : DnhObject

    {

        public void Drop()

        {

            if (this.HasAttribute(typeof(Fragile)) && this is IBreakable)

                (this as IBreakable).OnBreak();

            else

                Console.Write("thud.");

            Console.WriteLine();

        }

        public void Kick()

        {

            if (this.HasAttribute(typeof(MadeOfMetal)))

                Console.Write("Ouch! It is made of metal! ");

            if (this is IBreakable)

                (this as IBreakable).OnBreak();

            Console.WriteLine();

        }

    }

#endregion

#region "Attributes"

    /// <summary>

    /// Give an object this attribute if it is fragile. (Assumes object

    /// is breakable.)

    /// </summary>

    public class Fragile : Attribute

    {

    }

    /// <summary>

    /// Give an object this attribute if it is made of steel.

    /// </summary>

    public class MadeOfMetal : Attribute

    {

    }

#endregion

#region "Interfaces"

    public interface IBreakable

    {

        void OnBreak();

    }

    public interface IDrinkable

    {

        void OnDrink();

    }

    public interface IReadable

    {

        void OnRead();

    }

#endregion

#region "Objects"

    [MadeOfMetal]

    public class Sword : DnhItem, IBreakable, IReadable

    {

        public void OnBreak()

        {

            Console.Write("The sword shatters.");

        }

        public void OnRead()

        {

            Console.Write("The hilt says \'Sting\'");

        }

    }

    [Fragile]

    public class HealthPotion : DnhItem, IDrinkable, IBreakable

    {

        public void OnDrink()

        {

            Console.Write("Mmm tastey.");

        }

        public void OnBreak()

        {

            Console.Write("The potion shatters.");

        }

    }

    [MadeOfMetal]

    public class OilDrum : DnhItem, IDrinkable, IReadable

    {

        public void OnDrink()

        {

            Console.Write("Ack! This tastes terrible.");

        }

        public void OnRead()

        {

            Console.Write("The lable has a Mr. Yuck sticker on it.");

        }

    }

    #endregion

}

Hopefully that provides a good example of defining objects with rich interaction capabilities. In the next few weeks I'll show how we can drastically reduce the amount of code we have to write and maintain. Three words for you: System, Reflection, Emit.