XNA 4.0 Behavior-driven Development, Chapter 7

2 views
Skip to first unread message

David Wallace

unread,
Dec 24, 2011, 3:14:21 AM12/24/11
to bost...@googlegroups.com
Chapter 7: You will be quizzed on this

    public class CollideTest : TestBase
    {
        Scene scene;
        bool init;

        public override bool Test(BehaviorArgs args)
        {
            if (!init)
            {
                scene = args.State as Scene;
                if (scene == null) return false;
                init = true;
            }
            var targets = scene.Collidables(args.Subject);
            if (targets.Count() == 0) return false;
            args.Other = targets.FirstOrDefault(t => t.HasCollided(args.Subject));
            return args.Other != null;
        }

        public CollideTest()
        {
        }

        public static TestBase Build(ContentService service, string source)
        {
            return new CollideTest();
        }
    }

Collision detection is in integral part of game mechanics.  First I determine which objects are in the area where they might collide with the subject.  Then I test only those objects.  That's the power of spatial grids--you could have hundreds or thousands of objects on the map and only a small fraction are near enough to test for.  By isolating the collision test method I can expand on it later without disrupting the test.

    public class DestroyTest : TestBase
    {
        public static TestBase Build(ContentService service, string source)
        {
            return new DestroyTest();
        }
    }

The DestroyTest isn't supposed to do anything by itself.  The Actor class uses it a unique way.

        public override void Destroy()
        {
            base.Destroy();
            var rites = behaviors.Values.Where(b => b.Tests.Exists(t => t is DestroyTest));
            foreach (var item in rites) item.Act(new BehaviorArgs(), true);
        }

This method in the Actor is the equivalent of Dispose.  At this point any Behavior with a DestroyTest in it fires.  You could have that behavior work under other circumstances by flagging the behavior as Any or placing it in a LogicTest with an Or.  This is necessary as the base class always returns false.

    public class TimeTest : TestBase
    {
        float tMin, tMax, period, elapsed;
        Random rnd;

        public override bool Test(BehaviorArgs args)
        {
            var result = false;
            elapsed += (float)args.Time.ElapsedGameTime.TotalSeconds;
            args.Position.X = (elapsed / period).Clamp(0, 1);
            result = elapsed >= period;
            if (result)
            {
                elapsed -= period;
                if (rnd != null) period = rnd.Lerp(tMin, tMax);
            }
            return result;
        }

        public TimeTest(ContentService service, string[] data)
        {
            if (data.Count() < 1) return;
            if (data.Count() == 1)
                data[0].Parse(out period);
            else
            {
                rnd = service.Serve<RandomService>().New;
                data[0].Parse(out tMin);
                data[1].Parse(out tMax);
                period = rnd.Lerp(tMin, tMax);
            }
        }

        public static TestBase Build(ContentService service, string source)
        {
            var sources = source.Split(',');
            return new TimeTest(service, sources);
        }
    }

This test is actually two overloaded tests.  One form takes a single float and fires every n seconds while the other takes two and forms a range from which a random number is taken each time.  By passing the progress into the argument, an action can use that progress to do something.  Not just any action mind you, as those fire only when the test is true and the argument is 1.  When I get to presenting actions I will show you that one.

    public class LocationTest : TestBase
    {
        string dimension;
        float amount, maxX, maxY;
        bool greater;

        public override bool Test(BehaviorArgs args)
        {
            Vector3 pos = args.Subject.Transform.Position;
            switch (dimension)
            {
                case "X0":
                    return (greater ? pos.X > amount : pos.X < amount);
                case "Y0":
                    return (greater ? pos.Y > amount : pos.Y < amount);
                case "Z":
                    return (greater ? pos.Z > amount : pos.Z < amount);
                case "Xn":
                    return (greater ? pos.X > (maxX - amount) : pos.X < (maxX - amount));
                case "Yn":
                    return (greater ? pos.Y > (maxY - amount) : pos.Y < (maxY - amount));
            }
            return false;
        }

        public LocationTest(GridData gd, string[] data)
        {
            maxX = gd.MaxX;
            maxY = gd.MaxY;
            dimension = data[0];
            data[1].Parse(out amount);
            data[2].Parse(out greater);
        }

        public static TestBase Build(ContentService service, string source)
        {
            var sm = service.Serve<StateManager>();
            var gd = sm.Grid;
            var data = source.Split(',');
            if (data.Count() < 3) return null;
            return new LocationTest(gd, data);
        }
    }

The LocationTest determines if an object is within a certain distance of the edge of the map.  The actual edge is determined by the dimension parameter, the distance is in amount, and greater determines which side of the line causes the test to return true.

    public class MeterTest : TestBase
    {
        Meter watch;
        string key;
        bool max, init;

        public override bool Test(BehaviorArgs args)
        {
            if (!init)
            {
                var person = args.Subject as Character;
                if (person == null) return false;
                if (!person.Meters.ContainsKey(key)) return false;
                watch = person.Meters[key];
                init = true;
            }
            return watch.Fraction == (max ? 1 : 0);
        }

        public MeterTest(ContentService service, string[] data)
        {
            if (data.Count() < 1) return;
            data[0].Parse(out key);
            if (data.Count() >= 2)
                data[1].Parse(out max);
        }

        public static TestBase Build(ContentService service, string source)
        {
            var sources = source.Split(',');
            return new MeterTest(service, sources);
        }
    }

Character is a type of actor that has stats, skills, and upgrade types like schools.  A meter, usually just under the actor in question, displays a certain stat like health.  This test returns true if the meter is full (max is true) or empty (max is false).  The init method in test is necessary because the Build method has no access to the arguments.

    public class NearTest : TestBase
    {
        float distance;
        Scene scene;
        bool init;

        public override bool Test(BehaviorArgs args)
        {
            if (!init)
            {
                scene = args.State as Scene;
                if (scene == null) return false;
                init = true;
            }
            args.Other = scene.Find(s => SubTest(args.Subject, s), s => args.Subject.DistanceTo(s));
            return args.Other != null;
        }

        private bool SubTest(Actor a, Sprite s)
        {
            return (s is Actor) && (a.DistanceTo(s) <= distance);
        }

        public NearTest(ContentService service, string data)
        {
            data.Parse(out distance);
        }

        public static TestBase Build(ContentService service, string source)
        {
            return new NearTest(service, source);
        }
    }

The NearTest finds the nearest object to the subject.  If that object is within a certain distance then the test returns true and the object will be passed to the action through args.Other.

    public class TypeTest : TestBase
    {
        Type form;

        public override bool Test(BehaviorArgs args)
        {
            if (args.Other == null) return false;
            return args.Other.GetType() == form;
        }

        public TypeTest(Type type)
        {
            form = type;
        }

        public static TestBase Build(ContentService service, string source)
        {
            return new TypeTest(Type.GetType(source));
        }
    }

This test returns true if the object in other is of a specific type.  It is normally used after tests that place an object in other like NearTest.

That's all of the tests I have so far.  More may come later.

Reply all
Reply to author
Forward
0 new messages