Introduction
This competition submission is a game called Celerity: Sensory Overload. It combines the classic Tunnel Game genre with sensor control and most notably a novel take on 3D. This article will showcase the journey we took to produce this game, the many difficulties we faced and how we were able to overcome them.
Logo
In-Game
The Menu UI
Video
Check out this quick demo of our app on YouTube:
Background
The main inspirations for this game are:
How could I resist?
Time was tight as I only started after the details of the competition went up. I invited some talented friends to help out; @Dave_Panic (3D trickery guru), @BreadPuncher (game theorist & graphic designer) and @PatrickYtting (maestro).
I was thinking about doing something along the lines of gyro-based steering and/or perspective and Dave suggested doing the head tracking effect with the webcam. Marrying this concept with a classic tunnel game we had the foundation of an innovative, sensor-tastic game. You know how gamers subconsciously tend to lean and move their head around when playing some games even though it makes no difference? Now it will!
VR Head Tracking With Just A Webcam
In Johnny Lee's video, above, he is able to detect the position of the user's head in 3D space through infra red (IR) LEDs and an IR camera.
The effect is fantastic but requires a special IR sensor and also that the user wears an IR-emitting device. I needed to create this effect using only sensors on an Intel® Ultrabook™, and no other equipment. Thankfully the Ultrabook™ has an integrated webcam.
The application polls the webcam for frames and passes them to a Computer Vision (CV) image-processing library, EMGUCV. This returns a rectangle representing a detected face within the bounding box of the camera's view. As the user moves their face, the rectangle will move around relative to the bounds, giving me a relative X/Y offset of the user's face. The X & Y of the 3D world's view can be skewed in relation to the user's own physical position. Note that we need to flip the image horizontally as the webcam is looking in the opposite direction to us.
This effect can be taken even further, as the rectangle representing the user's face inherently has a size. This gets larger as they move towards the webcam, so we can also determine the relative Z position, too.
It's helpful if the user starts with their head roughly central and not too close, which we encourage with the intro menu design. There the user can see if they're vaguely "calibrated" before starting.
Designing The UI
I wanted to make use of Metro design principles in the user interface, as although this is a desktop game and not a Metro app, I value consistency and feel the UI will be more intuitive if it shares conventions with the operating system, Windows 8.
As we're building the app with XNA my usual vector approach to UI is not much help. Even doing very simple UI elements can be a great deal of effort, so up front I have cut out as many UI elements as possible whilst maintaining an intuitive means of navigation.
Here are the original layout mocks for the UI. It's interesting looking back to see what was kept the same and what evolved under the pressures of developing to a deadline and the limitations discovered along the way.
Building The App
I wanted to do the app as an XNA app, as it's perfectly fast enough and nice and easy to code for. I don't know C++ yet and there's not enough time to both learn it and complete the game. Unfortunately XNA is an awkward combination with Windows 8, as it's not supported in Visual Studio 2012, at least at the time of writing. This turned out to be such a pain that a number of issues threatened the viability of the project.
Throughout this article I'll be showing how we overcame some of these key difficulties when developing XNA for an AppUp® Intel® Ultrabook™ with Win8 included:
- No support for XNA projects in VS2012
- No means of producing an acceptable installer in VS2012 (InstallShield LE didn't work on my VS)
- The installer would have to include an XNA Redistributable dependency
- XNA Touch is deliberately disabled for Windows (eek!)
- Windows 8 Sensors can't be accessed in my Windows 7 development environment
Step 1: Project Pains
A great deal of effort and time was wasted trying to establish a stable XNA project in VS2012. I restarted from scratch at least three times, possibly more. Thankfully you can benefit from my many tears, as I can show you below a method which works, and has been tested all the way from project creation through to end-user installation.
First of all, here are some approaches I tried which didn't work out for me:
XNAML Solution
Thanks to a tip-off from Sacha Barber, I looked into XNAML. XNAML is a very clever solution to the problem of "How do I combine WPF-like UI elements with XNA?" It uses a cunning trick of layering two windows over each other, one XAML one XNA. This would have saved hours of UI build time even on a small project such as this, as even doing the simplest of interactive UI elements in XNA is a big pain. Whilst I got this working, I ran into an issue where the two overlaid windows were running at different DPIs depending on the OS settings. I love the idea of XNAML, but unfortunately I couldn't get past this issue so had to abandon it for this project.
Ibrahim's Solution
My thanks goes to Ibrahim's solution which helped me get XNA working in VS2012 for the first time. This was working really well for me until I tried running the projects in Windows 8 and ran into all sorts of issues with blank screens and so on. It's possible this was simply a bug with my code making this unworkable, but I had much more success with the following...
Step 2: A Working "XNA on VS2012 & Win8" Configuration
The main two problems faced are:
- XNA Game Studio 4 won't (normally) install on Windows 8
- VS2012 won't acknowledge XNA projects
First of all do the following in preparation:
- Install VS2010 (you'll see why shortly)
- Install VS2012
To get XNA Game Studio going I used Aaron Stebner's solution, which is essentially to:
- Install Games for Windows LIVE (this was non-obvious to me, but an essential step)
- Install Windows Phone SDK 7.1
- Install XNA Game Studio 4
It's the installation of Games for Windows LIVE which seems to be the trick for allowing the XNA-oriented SDKs to install without errors.
With XNA Game Studio installed on Win 8, Steve Beaugé's solution to enable XNA projects within VS2012, namely to:
- Copy VS2010's XNA extension folder to VS2012's equivalent, but otherwise missing, directory
- Manually edit the
extension.vsixmanifest
file to include the supported versions
Not only do XNA projects open and play nicely, but the standard Content Pipeline works perfectly.
Step 3: SpriteFont Workaround
Thanks to the above means of getting XNA in VS2012 on Win8 this section and the next are somewhat redundant, but I'll leave them in the article in case they're of use. Essentially if you have no XNA Content Project to import content from, then you can use this method to load SpriteFonts.
The issue is that whilst you have access to the SpriteFont
class, you might not have SpriteFont
s as project items in VS2012, so here is a workaround. My solution is to simply create a Windows app in VS2010, insert a SpriteFont
there, build the project, and then copy the .XNB file from the bin directory over as a resource.
It can then be loaded like this:
SpriteFont f = contentManager.Load<SpriteFont>(@"Fonts\SegoeUILight56");
Step 4: The Sound of Music
"Song" audio content files can be loaded similar to SpriteFonts. I used VS2010 to import an .MP3 content file. When it builds it both converts it to .WMA and produces an .XNB file. Copy both these files from the bin directory as before, and load like this:
Song s = contentManager.Load<Song>(@"Audio\mySongName");
Play your song as normal:
MediaPlayer.Play(s);
As it turned out, we didn't need to use this in the final code. We had grander ideas than simply playing an MP3, and they would require a more powerful audio engine.
The basic idea for the music score was that we would have several layers or channels of music, all playing simultaneously. Depending on the current level of in-game tension, more and more layers would be faded in to add a dynamic texturing to the music score.
Enter XACT.
XACT is Microsoft's "Cross Platform Audio Tool". Is is a high level library and toolset for managing and executing audio. Many XNA developers will already be familiar as XNA and XACT play very nicely together.
I used the "Audio Creation Tool" to import a number of music layers and sound effects. In this tool you can easily group sounds into "Categories", which can be treated like audio channels in your game code. Categories can have various parameters sent to them dynamically, such as source location (wow!) and volume. The wave files themselves may be declared as looping so that in-game this is automatic.
Seeing it for the first time halfway through the project I was nervous to take on this unfamiliar technology but highly recommend it. I was up and running within an hour or so thanks to this very simple XACT tutorial.
I barely scratched the surface of it in this project, but even with the basics we have a dynamic music score, which in my opinion is pretty cool!
Part of making a dynamic score a reality is working closely with the composer of course. I must extend my gratitude to Patrick for his professionalism, general calm and impossibly fast turnarounds.
Here's a copy of my AudioModule
, the class I used to handle audio logic in the game. Note the difference between Play
(for one-shot SFX)
and PlayCue
(for looping song WAVs).
namespace Celerity.Audio
{
using Celerity.Data;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Audio;
public class AudioModule
{
bool hasMusicStarted = false;
AudioEngine audio;
WaveBank waveBank;
SoundBank soundBank;
AudioCategory music01Category;
AudioCategory music02Category;
AudioCategory music03Category;
AudioCategory music04Category;
AudioCategory ambienceCategory;
AudioCategory sfxCategory;
float music01CategoryVolume = 1.0f;
float music02CategoryVolume = 1.0f;
float music03CategoryVolume = 1.0f;
float music04CategoryVolume = 1.0f;
float ambienceCategoryVolume = 1.0f;
float sfxCategoryVolume = 1.0f;
public void Initialize()
{
audio = new AudioEngine("Content\\Audio\\Waves\\CelerityAudio.xgs");
waveBank = new WaveBank(audio, "Content\\Audio\\Waves\\Wave Bank.xwb");
soundBank = new SoundBank(audio, "Content\\Audio\\Waves\\Sound Bank.xsb");
music01Category = audio.GetCategory("MusicLayer01");
music02Category = audio.GetCategory("MusicLayer02");
music03Category = audio.GetCategory("MusicLayer03");
music04Category = audio.GetCategory("MusicLayer04");
ambienceCategory = audio.GetCategory("Ambience");
sfxCategory = audio.GetCategory("SFX");
}
public void Update(GameTime gameTime, float chaosFactor)
{
float muteMultiplier = GlobalGameStates.MuteState == MuteState.Muted ? 0f : 1f;
if (!hasMusicStarted)
{
hasMusicStarted = true;
if (CeleritySettings.PlayMusic)
{
soundBank.PlayCue("Celerity_Music_Layer1");
soundBank.PlayCue("Celerity_Music_Layer2");
soundBank.PlayCue("Celerity_Music_Layer3");
soundBank.PlayCue("Celerity_Music_ShortIntro");
soundBank.PlayCue("Celerity_FX_ambience");
}
}
ambienceCategoryVolume = 1f * muteMultiplier;
music01CategoryVolume = 1f * muteMultiplier;
music02CategoryVolume = chaosFactor > 0.3 ? chaosFactor * muteMultiplier : 0f;
music03CategoryVolume = chaosFactor > 0.5 ? chaosFactor * muteMultiplier : 0f;
music04CategoryVolume = 1f * muteMultiplier;
sfxCategoryVolume = 1f * muteMultiplier;
ambienceCategory.SetVolume(ambienceCategoryVolume);
music01Category.SetVolume(music01CategoryVolume);
music02Category.SetVolume(music02CategoryVolume);
music03Category.SetVolume(music03CategoryVolume);
music04Category.SetVolume(music04CategoryVolume);
sfxCategory.SetVolume(sfxCategoryVolume);
audio.Update();
}
public void PlayClick()
{
soundBank.PlayCue("Celerity_Blip");
}
public void PlayCrash()
{
soundBank.PlayCue("Celerity_Ship_CrashNburn");
}
public void PlaySmartBomb()
{
soundBank.PlayCue("Celerity_Music_SmartBomb");
soundBank.PlayCue("Celerity_Ship_CrashNburn");
}
}
}
Step 5: Webcam Woes (and EMGUCV)
In this project I'm using EMGUCV version 2.4.0. I was using 2.4.2. but was able to get a much smaller DLL profile with 2.4.0. whilst still keeping everything I needed. It was a cinch to perform a basic capture from a webcam thanks to their simple tutorials, however when I attempted something more ambitious I ran into two related problems.
- EMGUCV gives me a
System.Drawing.Bitmap
rather than a Texture2D
- The performance became disastrous
I found a number of examples of converters online and tried several but still had the performance issues. I suspected it was an issue with running it on a single thread, but when searching the net I found many people warning against using threading in XNA. Eventually I ignored their advice and tried a combination of this method of image conversion and my first ever use of Parallel.Invoke()
. All of a sudden worked a treat without me ever having to quite get to the bottom of it! Don't you just love it when that happens?
Here's my naïve but incredibly effective solution:
Parallel.Invoke(() => QueryCamera(elapsedMilliseconds));
Simply by calling my existing method like this instead of directly from my Update method it was insta-fixed. Sweet!
Yes, okay I ought to read what that's actually doing but first I have a game to make.
One last note on the head tracking; over time the appearance of the resultant rectangle is a bit jittery z-axis wise. This is because it often catches two rectangles per face, but sometimes just either one. This makes for frequent changes in size. I fixed this by taking a recent sample history and taking a rolling average to smooth it out.
EDIT: Webcam Woes continued...
Everything was working well on my desktop development machine, and I was polling my simple 3rd party webcam at around 30fps which gave a silky smooth feed of the user's face. I hit issues when trying the application on the Ultrabook™ itself.
Unfortunately, without any dedicated camera driver for my unreleased version of the Ultrabook™ and using EMGUCV, I could only safely poll the Ultrabook™'s integrated webcam at around 8fps. Any more and the application would grind to a near halt.
I worked around this by adding three "modes" to the application's head tracking; "off", "slow" and "fast". I added a warning that "fast" mode is only supported by some cameras.
The 8fps "slow" mode still gives you a hint of the reactive 3D head tracking effect, especially if you hold the device at arms' length, but I do recommend trying the game with a 3rd party camera (you can temporarily disable the integrated device) to see what it can be like. I'm hoping that if I can lay my hands on specific drivers for the camera that the performance will spring to life, but was unable to do so within the time frame of the competition.
Another possibility I'm holding out for is that the fantastic looking CV library FaceAPI will hopefully soon have a C# API. It's my hope that this will not only be faster but would also handle face-tracking of off-axis faces better than EMGUCV.
Step 6: Alpha Mares
We hit an interesting issue in the rendering of our 2D UI assets. Thomas was doing a grand job of designing icons and similar material in Inkscape, but when I rendered the files in XNA there were lots of alpha artefacts.
I fiddled for a long while with various combinations of XNA rendering modes, and whilst some of them produced the desired effect with his images, they would interfere with rendering other assets such as fonts.
Thankfully I stumbled on a tip-off to use the program PixelFormer. Whilst it's intended for editing, we simply used it to open images from Inkcape, and then re-export them, but with the pre-multiplied Alpha on. Various sources on the internet suggested that Inkscape already exported with pre-multiplied Alpha, but didn't seem to be the case for us. Running the images through PixelFormer did the trick for us.
Step 7: Dark At The End Of The Tunnel
Dave has been beavering away on a custom 3D engine for the tunnel. Here are a few words from him explaining how it was done...
The game is going to feature a continuous tunnel, filled with obstacles, that the player must negotiate. In order to achieve this, first we’re going to need a tunnel. I decided to break down the problem into 2 classes: one to deal with the section of the tunnel we can actually see, TunnelSection
, and one to deal with the tunnel as a whole, Tunnel
.
TunnelSection
will have the following responsibilities:
- Construct the vertices of this piece of the
Tunnel
. - Construct the texture coordinates for each vertex.
- Draw the tunnel.
Tunnel
will:
- Maintain the position of the
TunnelSection
s (we’re going to be moving the tunnel rather than the player in order to prevent coordinates growing too large). - Define the actual shape or curvature of the tunnel as a whole.
Today we’re going to look at creating the basic tunnel shape. Here’s the basic outline for the TunnelSection
class.
A tunnel made from triangles has the following properties:
public float Radius { get; set; } public int NumSegments { get; set; } public int TunnelLengthInCells { get; set; } private float cellSize;
Here's how we create the vertices:
private void ConstructVertices()
{
int numVertex = NumSegments * TunnelLengthInCells;
vertices = new VertexPositionColorTexture[numVertex];
float sectionAngle = 2 * (float)Math.PI / NumSegments;
int vertexCounter = 0;
for (int i = 0; i < TunnelLengthInCells; i++)
{
for (int j = 0; j < NumSegments; j++)
{
Matrix rotationMatrix = Matrix.CreateRotationZ(j * sectionAngle);
vertices[vertexCounter].Position = Vector3.Transform(
new Vector3(0.0f, this.Radius, 0.0f), rotationMatrix);
vertices[vertexCounter].Position.Z = -cellSize * i;
vertexCounter++;
}
}
}
What we’re doing here is first we’re creating a new vertex with an x and z coordinate of zero and a y coordinate equal to our desired radius. Then we’re rotating that point around the origin by the appropriate angle and moving on to the next point. When we’ve done a full circle, we repeat the process but move out point further away by the distance defined by cellSize
.
Since we’d like to keep the square sections that make up the tunnel looking like squares we need to work out what the distance between 2 points in the ring of vertices would be. We can do this by creating a point at (0, radius, 0)
rotating around the z axis appropriate angle (2 * (float)Math.PI / NumSegments)
then measuring the distance between the 2 points. As follows:
private float CalculateSectionSize()
{
Vector3 point1 = new Vector3(0.0f, this.Radius, 0.0f);
Vector3 point2 = Vector3.Transform(point1, Matrix.CreateRotationZ(2 * (float)Math.PI / NumSegments));
return Vector3.Distance(point1, point2);
}
Now we have our vertices we’re going to need to fill our index buffer. The index buffer tells the GPU which vertices to use in which triangle. It’s a list that points to the index of each vertex in the vertex array.
private void ConstructIndices()
{
int indexCount = TunnelLengthInCells * NumSegments * 6;
indices = new short[indexCount];
int indexCounter = 0;
for (int i = 0; i < vertices.GetUpperBound(0) - NumSegments; i += NumSegments)
{
for (int j = 0; j < NumSegments; j++)
{
indices[indexCounter] = (short)(i + j);
indices[indexCounter + 1] = (short)(i + j + NumSegments);
indices[indexCounter + 2] = (short)(i + j + 1);
if (j == NumSegments - 1) indices[indexCounter + 2] = (short)i;
if (j < NumSegments - 1)
{
indices[indexCounter + 3] = (short)(i + j + 1);
indices[indexCounter + 4] = (short)(i + j + NumSegments);
indices[indexCounter + 5] = (short)(i + j + NumSegments + 1);
}
else
{
indices[indexCounter + 3] = (short)(i + j + NumSegments);
indices[indexCounter + 4] = (short)(i);
indices[indexCounter + 5] = (short)(i + j + 1);
}
indexCounter += 6;
}
}
}
Here we’re looping though the vertices in the same order we created them wiring up the triangles as we go. The trick is to get them all winding clockwise so backface culling will not make the triangles invisible which requires a bit of mental visualisation to work out which vertices you want to wire into your triangle based on where we are in the triangle. Also, you’ll notice there’s a special case where we get to the end of ring of vertices. If this special case were not present, we would end up creating triangles that corkscrew along the tunnel and end up leaving a one triangle gap at the start and end of the tunnel section.
Here is a TunnelSection
:
And here are several TunnelSection
s sewn together to form a Tunnel
, with a nice curve for good measure:
Step 8: Gyromancy
It was not as straight forward to access the sensors as I'd hoped. Another lurking surprise in developing for Windows 8 was that the Sensor namespace was exclusive to WinRT, and therefore unavailable to Desktop applications. Thankfully none other than Intel provided the answer. The trick was to open up the .csproj
file and simply add a target platform version number. This allows you to add a reference to "Windows" which would otherwise be unavailable. Inside this library is the Windows.Devices.Sensors
namespace.
I had problems combining this directly with my XNA project, however simply putting the sensors in a separate project allowed everything to build and inter operate smoothly. I put any direct reference to any WinRT objects encapsulated within a proxy class, so my XNA project was only interacting with simple data types.
The sensors I currently use from this namespace are:
- Inclinometer for telling me how much the device is tilted left/right for steering
- Accelerometer for giving me a "Shaken" event, which I use to trigger a smart bomb
The code for reading these values is thankfully very straight forward:
Inclinometer
The readings on offer are the Pitch
, Roll
and Yaw
; these might be thought of as the X, Y and Z respectively. For tilting the screen to the left and right I simply subscribe my proxy to the Roll
's ReadingChanged
event, expose a public property of the last read value and then read the property from XNA once per Update
call.
Something like this:
public class SensorProxy()
{
const uint inclineDesiredInterval = 16;
Inclinometer incline;
public double InclineY { get; set; }
public SensorProxy()
{
incline = Inclinometer.GetDefault();
if(incline != null)
{
uint minInclineInterval = incline.MinimumReportInterval;
incline.ReportInterval = minInclineInterval;
incline.ReadingChanged += (s, e) => { InclineY = e.Reading.RollDegrees; };
}
}
}
Accelerometer
Wiring up the accelerometer was even easier than the inclinometer, as there is a pre-built Shaken event. This means I, as the developer, don't have to worry about calibrating the sensitivity. I just listen for an event and respond to it.
I wanted to entirely encapsulate all the sensor namespace objects, due to previous issues, and so I kept the direct event internal to the proxy class, and in the handler raised a new plain EventHandler
event. This was to prevent the caller from having to reference the AccelerometerShakenEventArgs
object.
public event EventHandler OnShaken;
Accelerometer accel;
accel = Accelerometer.GetDefault();
if(accel != null)
{
accel.Shaken += (s, e) => { If (OnShaken != null) OnShaken(this, new EventArgs()); };
}
Screen Orientation Issue
When I first tested the inclinometer steering I ran straight into a somewhat ironic issue. I was tilting the screen left and the reading was coming through for a second or two, but then my screen starting flipping around. The simple orientation sensor was detecting the change in angle and assuming I wanted to switch to portrait mode. With XNA running in full screen mode this went what I can only describe as "a bit mental". Auto-scaling horror!
This was clearly going to interfere with one of the core concepts of the application so I had to nip it in the bud. Thankfully a very simple method in my SensorProxy
, called from XNA during the Initialize phase, does the trick.
public void LockOrientation()
{
DisplayProperties.AutoRotationProperties = DisplayOrientations.Landscape;
}
Step 9: Out Of Your Depth (The impact of Depth Cueing)
To be continued.
Without Depth Cueing
With Depth Cueing
Step 10: Out Of Touch (How I got touch working in XNA on Win8)
As Shawn Hargreaves writes, Touch in XNA was unfortunately deliberately disabled for Windows, limited to use on the Windows Phone 7. That means whilst the namespace Microsoft.Xna.Framework.Input.Touch
does include the easy-to-use TouchPanel
class, it simply doesn't work. It does nothing.
Mercifully, Shawn does indicate two approaches for making it work. To make touch work in Windows 8 we in fact turn to Windows 7's touch implementation.
I took the first of Shawn's suggestions, namely using the .NET Interop Library. Tucked away in the .zip
file is the crucial assembly Windows7.Multitouch.dll
. Once we've added a reference to this library in our project, using it in XNA requires a slight side-step, in that we have to provide our touch-handling class an IntPtr
to the application's window. That sounds tricky, but in reality we just pass it the following from our main Game
instance:
input = new InputModule(this.Window.Handle);
On the receiving end, here is the constructor of my InputModule
, the class I use to process the various forms of input:
using Windows7.Multitouch;
using Windows7.Multitouch.Win32Helper;
public InputModule(IntPtr windowHandle)
{
touchHandler = Factory.CreateHandler<TouchHandler>(windowHandle);
touchHandler.TouchUp += (s, e) =>
{
lastTouchPoint = e.Location;
hasUnprocessedTouch = true;
};
}
Whilst the sight of the IntPtr
type may be scary for some, we let the built in classes deal with the details. All we need to process is the simple and friendly TouchUp
event (or others as your needs may be). You may be able to spot from the above that this isn't technically a multi-touch implementation. Our simplistic UI didn't really warrant full multi-touch as currently there is only ever single button taps to handle, but I'll likely rework this when I come to do thumb-controlled weapons in a future update.
Step 11: Collision Course (Creating obstacles and smashing into them)
To be continued.
Step 12: Stress Release
Prior to this competition my only dealings with installers and code signing certificates were very straight forward. I had used ClickOnce to create the installers which is almost as easy as just hitting "Publish", and the code signing certificates were arranged by other people.
The requirements of the competition were that the installer was a single file, and signed with a code signing certificate of my own. This ruled out ClickOnce as it creates a whole folder full of installation-related files, and also meant I had to go about acquiring a certificate.
Obtaining the Code Signing Certificate
Along with many other competitors I encountered a number of issues with the default supplier, Commodo. I won't waste too much space in this article, but suffice to say I was very disappointed. The standard of communication was terrible, and I was sent in circles.
Thankfully Intel® stepped in and referred me instead to Symantec. This was a world of difference in service. I received my certificate promptly after completing their clearer and more sensible procedure. The only step of note was that I needed to find a "notary". This would seem to mean different things in different cultures, but in the case of England a solicitor did just fine for a small fee.
Creating the Installer
I knew early on that the installer was going to be a pain. Our project, being XNA, would not install and run on a system unless the installer could prompt the user to download the XNA Game Studio 4 redistributable. Being unfamiliar with creating installers I suspected I was probably looking at some form of custom scripting.
I considered WiX but it looked a bit fiddly to pick up quickly. I tried using InstallShield LE but it was incompatible with my version of VS2012 Professional, only working on my copy of VS2010. Sadly VS2010 wasn't up to the job of building a VS2012 project so InstallShield was ruled out.
I ended up coming back to a suggestion I'd read earlier, which was the blandly titled Advanced Installer. I tried it, beginning to lose hope of finding a script-less means of installing an XNA game via a single file, but this turned out to be perfect. The key benefits for me were:
- Understands and implements a dependency on XNA Game Studio 4 Redistributable
- Has an easy to use GUI, intuitive for a first-timer
- Points at the build directory so I don't need to configure each time
- Automatically signs the installer for me
I did have to purchase the pro version for the XNA bit, but at least I can now release updates to Celerity nice and easily.
AppUp® Submission
Our submission was mostly quite smooth but we did have one one rejection due to forgetting to put the trade mark symbol in our meta data. Ouch! Thankfully the binary validation passed first time, which was the bit I was most concerned about.
Project Review
What We Did Well
- The core idea was very strong
- Scope control (lots of things we'd love to add but would have blown the deadline)
- Realistic technical difficulty to our abilities & deadline
- Perseverance (e.g. shoe-horning XNA into XS2012 on Win8)
What We Did Not Do Well
- Project workflow (mish-mash of Git, SkyDrive and copy-paste backups)
- Code/life balance
- Blew money on a premium installer in a last-minute panic (should have looked into WiX earlier)
Project Fun Facts
Technical hitches in the final days of the project
- My DVD drive broke when trying to install Windows 8
- I had 2x USB sticks die on me
- The Ultrabook touchscreen broke temporarily, leaving me thinking my code was duff (just needed a reboot)
- My internet cut off for an hour
- My internet went down to 1/10th its normal speed for 24 hours
Latest night worked
- 4am (NEVER again! Don't do it, people!)
State of my desktop by the end of it
I'm normally ultra-tidy so this is like looking at a nightmare for me. Almost every file or folder has the same prefix. Not great for finding stuff in a hurry.
How much it cost to make
- £3.26 - For the player ship 3D model
- £10 - For faxes sent to Commodo
- £10 - Solicitor's charge for verifiying my ID in the code signing certificate application process
- £227.12 - Advanced installer
...plus a little bit in phone calls.
~= £250 / $400
Ultrabook™-Specific Features
I deliberately kept the app as straight-forward as possible for the V1 release to ensure it was done in time. We'll be adding some more features and enriching existing ones in future updates. The emphasis was on doing something focused well, rather than having lots of features rushed in and poorly implemented. Here is the basic "V1" sensor feature set:
- Webcam-Based VR Head Tracking - hold the Ultrabook at arms' length by the hinges for best results
- Inclinometer-Based Steering - easy & intuitive/natural
- Accelerometer-Based (Shake-Activated) Smart Bombs & Menu Navigation - you can start the game just by shaking so you don't have to adjust your grip, and shaking in-game fires one of 3 smart bombs
- Basic Touch-Enabled UI - which is actually an achievement in a Win8 XNA project!
For best results on the Ultrabook™, try the following:
- Don't bring your face too near / arms length seems about right for me
- Only have one person's face in clear view of the camera
- Use "slow" or "off" for camera tracking unless you have dedicated drivers
Potential Future Features
Here are some more features we're considering for future updates:
- Multitouch Thumb Weapon Controls - why dodge an obstacle when you can blast it, right? This will be two areas of the screen which independently respond to thumb taps. Maybe even different gestures for different weapons, depending how viable a thumb gesture is when twirling an Ultrabook.
- Ambient Light Sensor-Informed Contrast Adjustment - I want to keep the look of the game similar, but the greys on white can become full black on white, for example.
- Stylised Explosion Effects - We didn't quite have time to finish this, but it's very high on the priority list.
- Additional Gameplay Mechanics - Power-ups, alt game-modes etc.
Source Code
I will be publishing all the source code here once the competition is over.
Note: In order to open and build the project you will need to go through some of the steps mentioned in this article, such as installing components and adding VS extensions. That means you will need Windows 8 and VS2012.
Project News
Celerity has been published here, in the Intel AppUp® Store in time for the contest judging.
We are about two-thirds of the way through our first update, which will feature basic game-balancing tweaks and visual explosion effects. If you have any particular feature requests please post them in the comments and we'll consider them for inclusion.
Feel free to bookmark this article or follow my Twitter feed as I'll keep updating them with future progress.
Competition Category
This app is intended as a submission in the Desktop Games category.
History
- V1 First Draft
- V2 Minor corrections & progress updates
- V3 Fixed typo and minor updates
- V4 Added tunnel progress & how-to
- V5 Major post-submission update, most sections updated
- V6 Fixed corrupted code samples & added Stress Release section
- V7 Added Out Of Touch, embellished audio section + minor fixes
- V8 Added link to a video demo of the app, an AppUp store link and updated news section