OpenLightGroup Blog

rss

Blogs from OpenLightGroup.net


The Seeker – Version 3: The Rise of ooBer

See it live: http://www.adefwebserver.com/Richard/TheSeekerSitev3/

After ooNaLife, I intended to just post my latest version of The Seeker so I could move on to focusing on Silverlight issues. But I had to indulge myself by using interfaces to supply the Seekers with “brains”. But that’s cool, I think it got me more in a good place to look at the MVVM model (judging from the little I know now).

image

Let me explain the on screen changes to what I now call ooNa and then I’ll get on to the code. Seekers are now red ellipses. When a Seeker ‘catches’ a Prey, the Prey disappears from the screen. You can create seeker or prey by clicking on the playing field. The Left Mouse Button radio buttons on the lower left control which is created. If you turn on ‘Seeker Reproduction’, then a new Seeker will be created whenever any existing Seeker has consumed the number of prey specified by the Prey Quota slider. You can set the maximum number of Seekers on the field. When that number is about to be exceeded all the current seekers are killed and the new offspring is the only one left. You can select Auto-Create Prey to have a new prey generated at the interval you specify with the slider below – the number of prey onscreen at one time is limited to 100. The ‘Clear Seekers’ and ‘Clear Prey’ button do just what they say.

In general, a Seeker will start to chase a Prey when the Seeker receives a sighting Event. It will not start to pursue a Prey that is already being pursued. If the Seeker is already in pursuit of a particular Prey it will turn and start to chase the new Prey if the new Prey is closer. Prey are initially light green, they turn purple when being pursued, and dark green after they have escaped a pursuit.

The Code

I think the most interesting thing about this version is the change in the way Seekers think. Previously all the intelligence on how to recognize and pursue a prey were written in the Seeker code-behind. From working on ooNaLife I decided I had a way to make this more flexible – but first, let’s talk about why. Right now, every Seeker will use exactly the same logic to deal with every situation. The only variables will be the Seeker’s current situation, not any individuality of the Seeker itself. Of course I could add individuality by adding properties and code to take those properties into account. But then each Seeker has to drag around all the code to handle every situation in combination with every aspect that makes the Seeker an individual.

What if you could instead give the Seeker individuality by changing him internally. In the real world, we are individuals right from the start. In ooNaWorld, ooBer can make Seekers individual by giving them different internal logic (I’m intentionally avoiding the word behavior for now). And ooBer can change that logic at any time, just like swapping out boards in a computer.

To put this into practice, In this version I introduced two interfaces, IooNaSighting and IooNaMove. Each Seeker has a reference to each one, which I call SightingLogic and MoveLogic – I’m not crazy about the naming convention, but SightingBrains just seemed silly.

        // These references are set by ooBer. Logically the seeker acts on
        // his own, but his decisions and actions are controlled by ooBer through
        // these references
        public IooNaSighting SightingLogic {get; set;}
        public IooNaMove MoveLogic { get; set; }

ooBer creates the objects behind the interfaces (in MainPage.xaml.cs)

_ooNaSighting = new ooNaSighting();
_ooNaMove = new ooNaMove();

Then, as ooBer creates each new seeker, he sets the references, SightingLogic and MoveLogic to point to the implementations in _ooNaSighting and _ooNaMove, which are member variables in MainPage.xaml.cs

        public void CreateSeeker(Point position)
        {
            Seeker newSeeker = new Seeker(this);


            newSeeker.SightingLogic = _ooNaSighting;
            newSeeker.MoveLogic = _ooNaMove;
            AddToOonaverse(newSeeker, position);
        }

Here’s the resulting code in Seeker

        public void HandleSightingEvent(object sender, SightingEventArgs e)
        {
            if (Alive)
                SightingLogic.React(this, e.SightedPrey);
        }

        public void Pursue(iPrey prey)
        {
            if (MoveLogic != null)
                MoveLogic.Pursue(this, prey);
        }

Notice that the calls to React and Pursue pass this as the first parameter. As much as I’d like to keep the coupling loose, the code behind React and Pursue represent the Seeker’s ‘brains’ so for now I’m giving in to making the two tightly intertwined. Here’s the implementation of IooNaSighting.React()

        public void React(IooNaThing thing, IooNaThing sightedThing)
        {
            Seeker seeker = thing as Seeker;
            if (seeker == null)
                return;
            React(seeker, sightedThing);
        }           
        private void React (Seeker seeker, IooNaThing sightedThing)
        {
            Prey prey = sightedThing as Prey;
            if (prey == null || prey.Pursued)
                return;

            if (seeker.AcquiredTarget == null)
            {
                seeker.Pursue(prey);
                return;
            }

            Point currentTarget = seeker.AcquiredTarget.Position;
            Point potentialTarget = prey.Position;
            Double currentDistance = Common.ComputeDistance(seeker.Position, currentTarget);
            Double potentialDistance = Common.ComputeDistance(seeker.Position, potentialTarget);
            if (potentialDistance < currentDistance)
                seeker.Pursue(prey);
        }

It accepts IooNaThing as the first parameter …

When I, and C#, for that matter use a word that describes an interface this way, it really means a reference to an object that implements that interface, and anything that implements that interface can be behind it. That’s why I can add the inheritance of IooBer to MainPage …

public partial class MainPage : UserControl, IooBer

… add the code to support the interface to MainPage.asmx.cs and then pass a reference to IooBer to the Seeker constructor like this:

Seeker newSeeker = new Seeker(this);

 

So React accepts anything that supports the IooNaThing interface, and then has to determine what type of IooNaThing it has, or more specifically, is it the type of IooNaThing for which there is an overload of the React method, and if so, call it. Now, down to specifics, Seeker’s version of React first checks to see if the Seeker is even interested by checking the sighted IooNaThing to see if it’s Prey. If not it’s ignored.

Now here’s the good part. If React decides that Seeker wants to smoke this guy, he calls seeker.Pursue(prey). I bet you can guess what happens next. seeker.Pursue(prey) calls MoveLogic.Pursue(this, prey);

The methods below, Pursue, MoveToWayPoint, and PlotWayPoint are all methods in the ooNaMove class (ooNaMove.cs), which is the implementation behind the MoveLogic reference placed in Seeker by ooBer:

        private void Pursue(Seeker seeker, Prey prey)
        {
            if (seeker.AcquiredTarget != null)
            {
                seeker.AcquiredTarget.Pursued = false;
            }
            seeker.AcquiredTarget = (Prey)prey;
            seeker.AcquiredTarget.Pursued = true;
            MoveToWayPoint(seeker);
        }

        public void MoveToWayPoint(Seeker seeker)
        {
            if (seeker.AcquiredTarget == null)
                return;
            // Calculate a point along the vector the specified distance
            Point WayPoint = PlotWayPoint(seeker, seeker.AcquiredTarget.Position, seeker.WayPointDistance);
            // Move there in the specified number of seconds
            Common.MoveTo(seeker, WayPoint, new Duration(TimeSpan.FromSeconds(seeker.SecondsToWayPoint)), new System.EventHandler(seeker.sbMove_Completed));
        }

        private static Point PlotWayPoint(Seeker seeker, Point targetPoint, Double distanceOnVector) (Not shown)
        {  }

 

You might notice that one of the arguments passed to Common.MoveTo in MoveToWayPoint() is an EventHandler in the seeker we’re currently thinking for. When the animation that does the MoveTo is complete, this event handler will be called. I rationalize by saying this is so Seeker can decide what to do at the end of each move to a new WayPoint, but really I don’t want to have to figure out how to deal with it purely in the ooNaMove code. In fact what sbMove_Completed does is to call another method on the MoveLogic interface, MoveToWayPoint();

        public void sbMove_Completed(object sender, System.EventArgs e)
        {
            if (MoveLogic != null)
                MoveLogic.MoveToWayPoint(this);
        }

Don’t get the idea that interfaces are a panacea. I’ll have to keep my interfaces small, because otherwise every change in one aspect of the logic means another version of the interface that has to maintain all the unchanged logic. For instance let’s say I want to publish an alternate version of PlotWayPoint, which should really be in CommonFunctions anyway, I have to create an alternate version of ooNaMove to back it up. Also, the code behind the interfaces has to be current with the whoever it’s thinking for, so all my IooNaFaces and ooNaThings need to be built at the same time. Versioning would be pretty much impossible until some kind of stability comes out of this whole ooNaMess.

I’m anxious to get on to learning more about behaviors and whatnot because I’m hoping it will alleviate these problems.

The ooNaThing heritage

The methods behind the ‘ooNaLogic’ references implanted in  each ooNaThing have to deal with every variety in this hierarchy:

    // Everything in the ooNaverse inherits from IooNaThing at some level
    // Everything has a reference to ooBer and a settable position
    public interface IooNaThing
    {
        IooBer OOBER { get; set; }
        Point Position {get; set;}
    }

    // IooNaBeings are mortal, at least for now
    public interface IooNaBeing : IooNaThing
    {
        IooNaSighting SightingLogic { get; set; }
        void Die();
    }

    // Prey can be pursued and periodically raise a sighting event
    public interface iPrey : IooNaBeing
    {
        bool Pursued {get; set;}
        // For reporting movement to ooBer
        int ReportingInterval {get; set;}
        event SightingEventHandler SightingEvent;
    }

    // Seekers pursue prey
    public interface iSeeker : IooNaBeing
    {
        void Pursue(iPrey prey);
    }
}

I just noticed that I stuck a reference to IooNaMove directly into the Seeker class instead of the iSeeker interface. Not important, but it shows you can do it either way. Maybe for some reason I don’t want every implementation of iSeeker to support a reference to IooNaMove, so I would stick it in the object directly, or probably more likely into another interface that inherited from iSeeker.

Letting Go:

Seeker Implementation of IooNaBeing.Die()

This is from Seeker:

        public void Die()
        {
            Alive = false;
            if (_acquiredTarget != null)
            {
                _acquiredTarget.Pursued = false;
                _acquiredTarget = null;
            }
            _ooBer.killMe(this);
        }

I just recently put in that Alive flag. Why? Because I kept catching SightingEvents, apparently even after ooBer killed me. Before I put in the Alive flag for the event handler to check I was acquiring new targets even after I was gone. I fixed the whole thing correctly when I put modified killMe() so it removes the dying Seeker from the SightingEvent broadcast that ooBer sends out. Notice if I’m removing a Prey that I remove ooBer from the Sighting event that the prey sends out. These are two sides to the same event. ooBer catches sighting event from every prey and re-broadcasts them to every seeker. Potentially it can filter what it broadcasts. Right now it suppresses and SightingEvents raised by Prey that is already being pursued.

This is from ooBer:

        public void killMe(IooNaBeing denizen)
        {
            if (denizen is Seeker)
                SightingEvent -= (denizen as Seeker).HandleSightingEvent;
            else if (denizen is Prey)
                (denizen as Prey).SightingEvent -= HandleSightingEvent;

            ooNaWorld.Children.Remove((UIElement)denizen);
            if (denizen is Prey)
                UpdatePreyCount();
        }

My point is, the child lives on after being removed from Children until garbage collection picks it up. Putting in the Alive flag relieved the symptom of catching events after I was dead, but I still held a reference on the dead seeker because the Seeker was subscribed to ooBer’s broadcast of SightingEvent, which probably interfered with garbage collection. Anyway, when you kill off a virtual being, make sure they’re dead.

Along the same line, prey was still sending out movement reports to ooBer on every heartbeat, which kept going because I wasn’t stopping the timer, which is fixed in the code below.

This is from Prey:

        public void Die()
        {
            Alive = false;
            if (heartBeat != null)
                heartBeat.Stop();
            _ooBer.killMe(this);
        }

This is the last version where I just wing it on putting the logic together. I’ve got the core logic I want; now I’m going to focus on implementing it through Silverlight.

Merry Christmas!!!

Download the code:

http://www.adefwebserver.com/Richard/Seekerv3.0.zip





Comments are closed.