Thursday, February 10, 2011

DDDHack - XNA mods

My XNA app mods are uploaded to: http://slodge.com/dddhack/ChangedFiles.zip

The changes are definitely "a ddd hack" :)

Some #DDDHack Action

Following on from the excellent DDD9, I thought I'd take up the challenge of the DDDHack - to take the sample apps from @paulfo and see what I could do with them.

I started at a gallop, downloading both sample apps straight away, building the XNA game and fixing the RSS reader to work in the post-CTP world.

However, then work and life got in the way... :)

Still it is supposed to be a hack... so now at the eleventh hour, let's have a play.

XNA XNA XNA

I've already built several Silverlight apps - Iron7, Overflow7, Translate-A-Bull, Xylophone, PocketPiano, ... so I decided to only do the XNA - everyone should learn a foreign language :)

Hack #1 change the UI into a car racing game
My first attempt at hacking was to try to change the UI - especially to add some different .x and .fbx models - picking up free ones from http://turbosquid.com.

However, this proved to be a little more problematic than I'd hoped - I think mainly because I was trying to download some of the flashier models with embedded images and all sorts of extras.

Actually, looking at the thumbstick.png in the project, maybe there's something wrong with the texture importing anyway... just a bit confused at present.

To cut a long story short... I failed... just couldn't get the content project to like my new models.

I did manage to change the colour of the floor....

Never mind... I like space ships... so on with the hacking!

Hack #2 Out of control
The first thing I noticed when playing with the game was that the controls were horrible - there are two virtual sticks on the screen, both of which are used for steering - plus both of which are used for thrust/acceleration too.

So... wouldn't it be nicer if this could be done using the built-in accelerometer? Let's do it.

We want to get steering from the left-right pitch of the phone
... and we want to get thrust from the forwards/backwards tilt of the phone.

To get the accelerometer working, the excellent Rob Miles helped with this post - http://www.robmiles.com/journal/2010/3/22/windows-phone-accelerometer-support-in-xna.html

Sadly this post is out of date (the API changed after this early CTP post), but getting the accelerometer included was pretty easy.

1. Add a reference to Microsoft.Devices.Sensor
2. Add this static class:

    public class WrappedAccelerometer

    {

        static Accelerometer accelerometer;

        static AccelerometerReadingEventArgs lastSeenAcceleration;

           

        static WrappedAccelerometer()

        {

            accelerometer = new Accelerometer();

            accelerometer.Start();

            accelerometer.ReadingChanged += new System.EventHandler<AccelerometerReadingEventArgs>(accelerometer_ReadingChanged);

            lastSeenAcceleration = null;

        }

 

 

        static void accelerometer_ReadingChanged(object sender, AccelerometerReadingEventArgs e)

        {

            lastSeenAcceleration = e;

        }

 

        public static AccelerometerReadingEventArgs GetState()

        {

            return lastSeenAcceleration;

        }

    }


3. Replace the old VirtualThumbsticks class with this new much simpler version:

       public static class VirtualThumbsticks

    {

 

        static VirtualThumbsticks()

        {

        }

 

        /// <summary>

              /// Gets the value for steering.

              /// </summary>

              public static double Steering

              {

                     get

                     {

                var accelerometerState = WrappedAccelerometer.GetState();

 

                           // if there is no accelerometer state, then return a value of 0.0

                if (null == accelerometerState)

                                  return 0.0;

 

                // because we are in landscape...

                // we have to use accerometer state

                return accelerometerState.Y;

                     }

              }

 

              /// <summary>

              /// Gets the value for acceleration.

              /// </summary>

              public static double Thrusters

              {

                     get

                     {

                var accelerometerState = WrappedAccelerometer.GetState();

 

                // if there is no accelerometer state, then return a value of 0.0

                if (null == accelerometerState)

                    return 0.0;

 

                           // calculate the scaled vector from the touch position to the center,

                           // scaled by the maximum thumbstick distance

 

                // this conversions sets normalised from the Z value:

                // accelerometerState.Z will be -1.0 when the phone is flat down

                // accelerometerState.Z will be 0.0 when the phone is upright

 

                // we convert that to thrust ratio using:

                var thrust = -2.0 * accelerometerState.Z - 1.0;

                if (thrust > 1.0)

                    thrust = 1.0;

                if (thrust < -1.0)

                    thrust = -1.0;

 

                // this actually works quite nicely thanks to trigonomics :)

 

                //System.Diagnostics.Debug.WriteLine(string.Format("Z is {0:0.00}, N is {1:0.00}", accelerometerState.Z, thrust));

                return thrust;

                     }

              }

 

        /// <summary>

        /// Updates the virtual thumbsticks based on current touch state. This must be called every frame.

        /// </summary>

        public static void Update()

        {

            // do nothing for now

       }

       }


4. Modify the code in Ship.cs, so that steering and thrust are picked up from the accelerometer instead of from the old VirtualThumbsticks code:

            // Determine rotation amount from input

            rotationAmount.X = (float)VirtualThumbsticks.Steering;


            // Scale rotation amount to radians per second

            rotationAmount = rotationAmount * RotationRate * elapsed;


            // ...


            // Determine thrust amount from input

            float thrustAmount = (float)VirtualThumbsticks.Thrusters;

 

            // Calculate force from thrust amount

            Vector3 force = Direction * thrustAmount * ThrustForce;

 


5. That's it!

Although you will need a phone to test it (or you'll need to use something like http://www.codeproject.com/KB/windows-phone-7/WP7AccelerometerEmulator.aspx)

And... I guess maybe the way I use the accelerometer is a bit resource-unfriendly - the accelerometer uses battery power, so should really be careful to switch it off sometime.

Also... there is one bug in my implementation - I've not taken account yet of the "Up" direction of the phone - so the acceleration works backwards if you hold the phone upside down!
 
Hack #3 But now I have these spare thumbs...

So the steering looked nice, and I liked the thrusters... but that left the screen with no purpose.

So I wondered about making the game a little more 3 dimensional - to see if I could add height to the game.

Hack #3.1 I feel sick!
My first attempt at this was to use a left thumb press for "pitch up" and a right thumb press for "pitch down".

To do this, I put some of the old VirtualThumbsticks code back in, and then added a new static property, PitchAdjustment to the class.

I then used this PitchAdjustment in the Update method of Ship.cs - something a little like:

            // Create rotation matrix from rotation amount

            Matrix rotationMatrix =

                Matrix.CreateFromAxisAngle(Right, rotationAmount.Y) *

                Matrix.CreateRotationY(rotationAmount.X) *

                Matrix.CreateRotationX(rotationAmount.Z);


This worked OK, the ship certainly started to head up and down according to touch.

However, whilst visually it looked nice, it was incredibly hard to control - a bit like so many flight simulators are!

I needed something simpler...

Hack #3.2 Flash Classic
Inspiration came from the old Flash game http://www.helicoptergame.net/

In this game, all you do is, press a button to go up... otherwise gravity will slowly bring you down.

So that's what my code did too:

1. In Update in VirtualThumbsticks.cs, just detect if there are any touches at all

            TouchCollection touches = TouchPanel.GetState();

            touchActive = touches.Count;


2. Map this to a VerticalAcceleration property:

        public static double PitchAcceleration

        {

            get

            {

                if (touchActive)

                    return 1.0;

 

                return -1.0;

            }

        }


3. Use this VerticalAcceleration in the Ship.cs class:

            // modify the Altitude

            Position.Y += AltitudeStep * (float)VirtualThumbsticks.PitchAcceleration;


4. To provide some variation in height, set some constants in Ship.cs:

        private const float MinimumAltitude = 100.0f;

        private const float MaximumAltitude = 6000.0f;

        private const float AltitudeStep = 50.0f; 


5. To make the environment a bit more interesting, change the altitude values of our prizes and enemy ships in the main game file. e.g.

        // Two arrays that define the location of obstacles and collection items.

        // these could be loaded per level giving different level designs

        Vector3[] coords = new Vector3[] { new Vector3(45000, 50, 57000),

                                               new Vector3(-45000, 2000, 57000),

                                               new Vector3(57000, 1500, -45000),

                                               new Vector3(-57000, 4000, -45000),

                                               new Vector3(8000f, 3000, 0f),

                                               new Vector3(-8000f, 2000, 0f),

                                               new Vector3(21500, 1000, 32500),

                                               new Vector3(-21500, 3000, 21500),

                                               new Vector3(21500, 2000, -21500),

                                               new Vector3(-21500, 400, -21500) };

 

        Vector3[] treecoords = new Vector3[] { new Vector3(-18000f, 1200f, 0f),

                                               new Vector3(0f, 2400f, 18000f),

                                               new Vector3(0f, 3600f, -18000f),

                                               new Vector3(57000f, 1200f, 0f),

                                               new Vector3(-57000f, 2400f, 0f),

                                               new Vector3(0f, 3600f, 57000f),

                                               new Vector3(0f, 1200f, -57000f),

                                               new Vector3(32500f, 2400f, 32500f),

                                               new Vector3(-32500f, 3600f, 32500f),

                                               new Vector3(32500f, 1200f, -32500f),

                                               new Vector3(-32500f, 1200f, -32500f) };:

  


Hack #4 Moving enemies

So the game's working... but it's a bit static... so next up was to hack into the main game loop and to give the enemies some motion...


To do this, I took each existing Enemy tree (currently just a sphere) and replaced them with an object:

    public class EnemyShip

    {

        private static float MaxSpeed = 80.0f;

        private static float AccelerationConstant = 10.0f;

 

        public BoundingSphere Sphere;

        public BoundingSphere OriginalSphere;

        public Vector3 CurrentSpeed;

 

        public EnemyShip(BoundingSphere sphere)

        {

            OriginalSphere = sphere;

            Reset();

        }

 

        public void Reset()

        {

            Sphere = new BoundingSphere(OriginalSphere.Center, OriginalSphere.Radius);

            CurrentSpeed = new Vector3();

        }

 

        public void AccelerateTowards(Vector3 Position)

        {

            var accerlationDirection = Position - Sphere.Center;

            accerlationDirection.Normalize();

 

            CurrentSpeed += AccelerationConstant * accerlationDirection;

            var currentLength = CurrentSpeed.Length();

            if (currentLength > MaxSpeed)

            {

                CurrentSpeed = CurrentSpeed * MaxSpeed / currentLength;

            }

        }

 

        public void UpdatePosition()

        {

            Sphere.Center += CurrentSpeed;

        }

    }


then each time around the loop, I call AccelerateTowards() and UpdatePosition on each of these enemies.


Result... 

- the enemies definitely come chase

- the enemies also quite often collide with each other - I should add some sort of protection to stop them overlapping...

- when you crash into an enemy it's also quite hard to escape again afterwards!


To cope with the overlapping... I added System.Linq, changed the update to 

        public void UpdatePosition(System.Collections.Generic.IEnumerable<BoundingSphere> DoNotMoveWithin)

        {

            var enlargedSphere = new BoundingSphere(Sphere.Center + CurrentSpeed, Sphere.Radius * 3);

            if (DoNotMoveWithin.Any(x => x.Intersects(enlargedSphere)))

                return;

 

            var candidateNewSphere = new BoundingSphere(Sphere.Center + CurrentSpeed, Sphere.Radius);

            Sphere = candidateNewSphere;

        } 


and called this using:

                enemyShip.UpdatePosition(enemyShips.Where(x => x != enemyShip).Select(x => x.Sphere));


This kind of works.... but it also leads to some problems - e.g. once the enemy ships have moved together they don't move apart again.


I guess I could add some code that means each enemy has to stay within X of its starting location.... another day... another hack...


        public void UpdatePosition(System.Collections.Generic.IEnumerable<BoundingSphere> DoNotMoveWithin)

        {

            var newCenter = Sphere.Center + CurrentSpeed;

            if ((newCenter - OriginalSphere.Center).Length() > MaxDistanceFromOriginal)

                return;

 

            var enlargedSphere = new BoundingSphere(newCenter, Sphere.Radius * 3);

            if (DoNotMoveWithin.Any(x => x.Intersects(enlargedSphere)))

                return;

 

            var candidateNewSphere = new BoundingSphere(newCenter, Sphere.Radius);

            Sphere = candidateNewSphere;

        }


Out of time

So that's it... out of time.


What have I learnt from my hack?

  • XNA's pretty easy to pick up - it's C# and .Net - you can use Linq :)
  • The contents projects look easy - but there's some gotchas in there when importing models - I'm sure if I play with these some more, then they will just work.
  • The 3D and Vector code in XNA is *lovely* - it's great to just be able to write code that adds and subtracts Positions and Vectors - and the Matrix code for Rotation is likewise very clean to read - love it.
  • Making a 3D model that works nicely on the screen takes work and thought!
  • Making a "fly-through" model playable is difficult - even with my "helicopter game" shortcut, then the spaceship was still sometimes hard to control. From a playability perspective, 2D still has lots of advantages which might be why games like Annoyed Avians are selling so well. 
  • Using the accelerometer in XNA is easy - and it does provide a very immersive experience for a gamer
  • Using "virtual buttons" on the screen is also pretty easy in XNA - but user feedback for these buttons is definitely needed...
  • The processors on WP7 phones (CPU and GPU) are stunningly quick - the amount of maths going on and the speed its done at is simply awesome.
  • I don't think I day gos by when I don't love using Linq - it's magic too.
  • I've still got plenty more to learn.... and plenty more hacking to do...

If anyone wants my code then I'll post it somewhere for you all to enjoy :)

Tuesday, February 01, 2011

Followup on .Net Collection numbers - release build with code "pre-JITed"

Just a quick release build update:

BaselineAdd     of size 1000    took 2893 ticks
BaselineAdd     of size 10000   took 60649 ticks
BaselineAdd     of size 100000  took 317393 ticks

ListAdd of size 1000    took 3192 ticks
ListAdd of size 10000   took 33153 ticks
ListAdd of size 100000  took 637581 ticks

PreSizedListAdd of size 1000    took 2917 ticks
PreSizedListAdd of size 10000   took 52575 ticks
PreSizedListAdd of size 100000  took 655949 ticks

WronglyPreSizedListAdd  of size 100000  took 631083 ticks
VeryWronglyPreSizedListAdd      of size 100000  took 627913 ticks

LinkedListAdd   of size 1000    took 3496 ticks
LinkedListAdd   of size 10000   took 53249 ticks
LinkedListAdd   of size 100000  took 660501 ticks

ListRemoveByIndex       of size 1000    took 189278 ticks
ListRemoveByObject      of size 1000    took 3552635 ticks

The BaselineAdd can show some interesting effects - because it doesn't actually store the TestObject references it can be quite slow - I think because Garbage Collection can kick in.

Some numbers on .Net Collections performance

After @GaryShort's talk on Saturday at DDD9 I've just spent an hour hacking some initial numbers together on performance:

BaselineAdd     of size 1000    took 10856 ticks
BaselineAdd     of size 10000   took 47490 ticks
BaselineAdd     of size 100000  took 343267 ticks

ListAdd of size 1000    took 5459 ticks
ListAdd of size 10000   took 43505 ticks
ListAdd of size 100000  took 738327 ticks

PreSizedListAdd of size 1000    took 4241 ticks
PreSizedListAdd of size 10000   took 152861 ticks
PreSizedListAdd of size 100000  took 664572 ticks

WronglyPreSizedListAdd  of size 100000  took 746042 ticks
VeryWronglyPreSizedListAdd      of size 100000  took 772499 ticks

LinkedListAdd   of size 1000    took 4210 ticks
LinkedListAdd   of size 10000   took 42905 ticks
LinkedListAdd   of size 100000  took 794595 ticks

ListRemoveByIndex       of size 1000    took 145411 ticks
ListRemoveByObject      of size 1000    took 3590562 ticks

These numbers already show some interesting facts....
  • If you can help it, don't use Remove() - use RemoveAt()!
  • For smallish lists, LinkedLists perform rather well
  • The penalty for wrongly sizing lists seems small (maybe the code is wrong or maybe my understanding is!).
  • It's remarkably hard to write generics that rely on generics (especially when LinkedList<> doesn't derive from the same interfaces as List<>)
But there's still plenty more to do...

The code so far is:

using System;

using System.Collections.Generic;

using System.Linq;

using System.Text;

using System.Diagnostics;

 

namespace CollectionsTiming

{

    class Program

    {

        private const int TestSize = 100000;

 

        static void Main(string[] args)

        {

            var p = new Program();

            p.DoIt();

        }

 

        public void DoIt()

        {

            InternalListAddTestRun("BaselineAdd", TestSize / 100, () => new NopList<TestObject>());

            InternalListAddTestRun("BaselineAdd", TestSize / 10, () => new NopList<TestObject>());

            InternalListAddTestRun("BaselineAdd", TestSize, () => new NopList<TestObject>());

            Console.WriteLine();

            InternalListAddTestRun("ListAdd", TestSize / 100, () => new List<TestObject>());

            InternalListAddTestRun("ListAdd", TestSize / 10, () => new List<TestObject>());

            InternalListAddTestRun("ListAdd", TestSize, () => new List<TestObject>());

            Console.WriteLine();

            InternalListAddTestRun("PreSizedListAdd", TestSize / 100, () => new List<TestObject>(TestSize / 100));

            InternalListAddTestRun("PreSizedListAdd", TestSize / 10, () => new List<TestObject>(TestSize / 10));

            InternalListAddTestRun("PreSizedListAdd", TestSize, () => new List<TestObject>(TestSize));

            Console.WriteLine();

            InternalListAddTestRun("WronglyPreSizedListAdd", TestSize, () => new List<TestObject>(TestSize / 100));

            InternalListAddTestRun("VeryWronglyPreSizedListAdd", TestSize, () => new List<TestObject>(TestSize / 1000));

            Console.WriteLine();

            InternalListAddTestRun("LinkedListAdd", TestSize / 100, () => new LinkedList<TestObject>());

            InternalListAddTestRun("LinkedListAdd", TestSize / 10, () => new LinkedList<TestObject>());

            InternalListAddTestRun("LinkedListAdd", TestSize, () => new LinkedList<TestObject>());

            Console.WriteLine();

            InternalListRemoveTestRun("ListRemoveByIndex", TestSize, TestSize / 100, (list, index, obj) => list.RemoveAt(index));

            InternalListRemoveTestRun("ListRemoveByObject", TestSize, TestSize / 100, (list, index, obj) => list.Remove(obj));

            Console.WriteLine();

            //InternalListRemoveTestRun("PreSizedAdd", TestSize, TestSize / 100, () => new List<TestObject>(TestSize / 100));

            Console.WriteLine();

 

            Console.ReadLine();

        }

 

        public void InternalListRemoveTestRun(string name, int totalSize, int removeSize, Action<List<TestObject>, int, TestObject> removeAction)

        {

            InternalTestRun(name, removeSize, () => InternalRemoveTestRun(totalSize, removeSize, removeAction));

        }

 

        public long InternalRemoveTestRun(int totalSize, int removeSize, Action<List<TestObject>, int, TestObject> removeMethod)

        {

            var testList = new List<TestObject>(totalSize);

            for (int i = 0; i < totalSize; i++)

            {

                testList.Add(new TestObject(i));

            }

            SortedList<int, Tuple<int, TestObject>> toRemove = new SortedList<int, Tuple<int, TestObject>>(removeSize);

            var rand = new Random();

            while (toRemove.Count < removeSize)

            {

                int candidate = rand.Next(totalSize);

                toRemove[candidate] = new Tuple<int, TestObject>(candidate, testList[candidate]);

            }

           

            Stopwatch s = new Stopwatch();

            s.Start();

            foreach (var tuple in toRemove.Values.Reverse())

            {

                removeMethod(testList, tuple.Item1, tuple.Item2);

            }

            s.Stop();

            var result = s.ElapsedTicks;

            return result;

        }

 

        public void InternalListAddTestRun(string name, int maxSize, Func<LinkedList<TestObject>> initialiser)

        {

            InternalTestRun(name, maxSize, () => LinkedListAddTestRun(maxSize, initialiser));

        }

 

        public void InternalListAddTestRun(string name, int maxSize, Func<IList<TestObject>> initialiser)

        {

            InternalTestRun(name, maxSize, () => ListAddTestRun(maxSize, initialiser));

        }

 

        public void InternalTestRun(string name, int maxSize, Func<long> testMethod)

        {

            var result = testMethod();

            Console.WriteLine("{0}\tof size {1}\ttook {2} ticks", name, maxSize, result);

        }

 

        public long ListAddTestRun(int maxSize, Func<IList<TestObject>> initialiser)

        {

            Stopwatch s = new Stopwatch();

            s.Start();

            var testObject = initialiser();

            for (int i = 0; i < maxSize; i++)

            {

                testObject.Add(new TestObject(i));

            }

            s.Stop();

            var result = s.ElapsedTicks;

            return result;

        }

 

        public long LinkedListAddTestRun(int maxSize, Func<LinkedList<TestObject>> initialiser)

        {

            Stopwatch s = new Stopwatch();

            s.Start();

            var testObject = initialiser();

            for (int i = 0; i < maxSize; i++)

            {

                testObject.AddLast(new TestObject(i));

            }

            s.Stop();

            var result = s.ElapsedTicks;

            return result;

        }

    }

 

    class NopList<T> : IList<T>

    {

        public void Add(T to)

        {

        }

 

        public NopList()

        {

 

        }

 

        public NopList(int capacity)

        {

 

        }

 

        public int IndexOf(T item)

        {

            throw new NotImplementedException();

        }

 

        public void Insert(int index, T item)

        {

            throw new NotImplementedException();

        }

 

        public void RemoveAt(int index)

        {

            throw new NotImplementedException();

        }

 

        public T this[int index]

        {

            get

            {

                throw new NotImplementedException();

            }

            set

            {

                throw new NotImplementedException();

            }

        }

 

 

        public void Clear()

        {

            throw new NotImplementedException();

        }

 

        public bool Contains(T item)

        {

            throw new NotImplementedException();

        }

 

        public void CopyTo(T[] array, int arrayIndex)

        {

            throw new NotImplementedException();

        }

 

        public int Count

        {

            get { throw new NotImplementedException(); }

        }

 

        public bool IsReadOnly

        {

            get { throw new NotImplementedException(); }

        }

 

        public bool Remove(T item)

        {

            throw new NotImplementedException();

        }

 

        public IEnumerator<T> GetEnumerator()

        {

            throw new NotImplementedException();

        }

 

        System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator()

        {

            throw new NotImplementedException();

        }

    }

 

    class TestObject

    {

        public string MyKey { get; set; }

        public string SomeRandomText { get; set; }

        public int Index { get; set; }

 

        public TestObject(int index)

        {

            MyKey = Guid.NewGuid().ToString();

            SomeRandomText = Guid.NewGuid().ToString();

            Index = index;

        }

    }

}


Will tidy this up and add more when I get some more "spare time"!