Thursday, February 10, 2011

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 :)

No comments:

Post a Comment