Introduction
Asteroid Sharpshooter is a game published on the Xbox LIVE Marketplace, under the independent games section.
It is written in F# and C#, using XNA Game Studio 3.1. The game falls in the category of 3d space shooters. Inspired by a classic,
Asteroids, the game puts the player in space, in a field filled with rocks. Some of these can be destroyed by shooting at them. The goal of the game is to shoot and destroy a number of rocks. To make things challenging, the controls of the ship are consistent with what you would expect from a ship drifting in space. There is no friction, and the ship does not necessarily point where it's headed. Controlling the ship to collect power-ups and approach asteroids to shoot requires skill. The higher difficulty levels feature enemies in the form of drones that track the player's ship and detonate themselves when they come close enough.
In this article, I present my thoughts about the development process of the game and the reception of the game by players.
Development
The game was written using Visual Studio in C# and in F#. C# is used for the higher levels of the software, which consist of menus, parts of the rendering, loading assets, loading and saving user progress.
Menus use the game state management sample from the App Hub.
Using F#
F# is a good programming language in general, and contributed positively to the development. Features such as discriminated unions and pattern matching are lacking in C#.
Although the game uses multiple threads, it does not use async workflows (these are primarily intended to ease the development of asynchronous code, but they also have interesting properties for parallel programming, as shown in my
earlier blog entries).
Some of F# features were at the time
not supported on Xbox and resulted in run-time errors: sprintf and async workflows. I haven't had the occasion to try async workflows with the current version of F#, but sprintf now works!
Building the game was tricky before I managed
to hack project files.
I initially used the free versions of Visual Studio: the Express edition for XNA Game Studio and C#, Visual Studio Shell for F#. This worked OK, even for debugging, but wasn't as practical of using Visual Studio Pro, which I ended up using at the end. You can use the free editions to try and see if F# works for you, but for regular development, you will probably want to use the Pro edition or higher.
I was a bit worried that using F# on the Xbox might not fully work, and might even be impossible as new versions of XNA or F# are released. Although there have been problems, all could be
resolved, and the mix got better or more stable with every new release. Although F# is still not officially supported on Xbox, the fact that Microsoft Research has developed an
XBLA game that uses F# sounds positive.
F# uses reference types for most of its data types: lists, tuples, records, classes, discriminated unions, function values, mutable cells (ref). This can cause problems because of the
limitations of the current garbage collector on Xbox. I decided to design my code so that garbage collections would have a low latency, and not care too much about how often they would occur. This worked well enough. The game triggers a garbage collection every 6 frames, which each last for 3.5ms. The remaining time was enough to update the physics and render all objects, but a more complex game with more models and more complex class hierarchies could have difficulties.
F#
does not allow circular dependencies between types unless they are declared in the same file and marked as mutually dependent. I was aware of this restriction, and it did not cause me any trouble. I started the project with all C# code in one project, and all F# code in another. Toward the end, I started moving reusable code into separate libraries. For my F# code, this was little more work than moving files to a new project and changing namespaces. The task was notably more difficult in C#, as the language supports circular dependencies within assembly bounds. Breaking apart an assembly will require the introduction of interfaces at the bounds. Although F# and C# do not differ on that point, the fact that F# forces you to work that way has benefits the day you want to split your code, in addition to all other benefits that non-circular designs have (better tool support, easier to understand...).
I don't know of any way to declare pass-by-reference parameters in F#, the way you can in C# using the "ref" keyword. In the XNA framework, some methods use passing by reference to save time. Although it is possible to call such functions from F# code, I don't know of any way to declare new functions.
There is however an F# way of avoiding copying large structs when calling functions: use inlining. Last time I checked, it was not fully reliable though, as the F# compiler tends to introduce lots of unnecessary variables in inlined code. Whether the .net CLR notices that and removes these variables isn't clear to me.
Project planning and tracking
I have used a free account on
fogbugz to organize my work and keep track of bugs. Although it may seem to be overkill, it allowed me to look at what went wrong when writing this article, as I had a record of most of the bugs. It also simplifies working on a project part-time. During my day job I can focus on my job and forget about the project. When the week-end comes, I can pick up the project where I left it, and start new tasks which were planned but not started.
Obstacles encountered
Although the game state management sample was very helpful to get the menu system up and running in no time, it's not obvious at first how to integrate user interaction forced by the StorageDevice API. One needs to keep track whether a device is selected, whether the user is currently selecting one... The first solution that comes to mind, using Boolean variables isn't maintainable when the number of variables grows. For instance, the device selection screen had to keep track of whether the user was signed in, currently signing in, if a title-specific storage device was chosen, being chosen, whether a user-specific storage device was chosen, being chosen. Mix that with event handlers that may need to display alert boxes, and it becomes tricky. I used handlers to deal with storage disconnection and I/O operation notification.
Even after writing my own version of EasyStorage in F#, cleaning up code, the Update() method of my game object still looked too complicated for its own good:
protected override void Update(GameTime gameTime)
{
base.Update(gameTime);
if (titleStorageLost == GuideStates.Requested)
{
if (!Guide.IsVisible) try
{
Guide.BeginShowMessageBox("Storage device disconnected",
"The storage device used for scores was disconnected. " +
"Scores will not be saved unless a new device is selected. " +
"Would you like to select a device now?",
new string[] { "Yes", "No" }, 0, MessageBoxIcon.Alert,
(result) =>
{
var choice = Guide.EndShowMessageBox(result);
if (choice.HasValue && choice.Value == 0)
requestTitleStorage = true;
titleStorageLost = GuideStates.None;
},
null);
titleStorageLost = GuideStates.Pending;
}
catch (GuideAlreadyVisibleException) { }
}
if (titleStorageLost == GuideStates.None && requestTitleStorage)
{
storage.RequestTitleStorage();
requestTitleStorage = false;
}
if (titleStorageLost == GuideStates.None && !requestTitleStorage && userStorageLost == GuideStates.Requested)
{
if (!Guide.IsVisible) try
{
Guide.BeginShowMessageBox("Storage device disconnected",
"The storage device used for player progress was disconnected. " +
"Progress will not be saved unless a new device is selected. " +
"Would you like to select a device now?",
new string[] { "Yes", "No" }, 0, MessageBoxIcon.Alert,
(result) =>
{
var choice = Guide.EndShowMessageBox(result);
if (choice.HasValue && choice.Value == 0)
requestUserStorage = true;
userStorageLost = GuideStates.None;
},
null);
userStorageLost = GuideStates.Pending;
}
catch (GuideAlreadyVisibleException) { }
}
if (titleStorageLost == GuideStates.None && userStorageLost == GuideStates.None && !requestTitleStorage && requestUserStorage)
{
var screens = screenManager.GetScreens();
if (screens.Length > 0)
{
var topScreen = screens[screens.Length - 1];
if (topScreen.ControllingPlayer.HasValue)
storage.RequestUserStorage(topScreen.ControllingPlayer.Value);
}
requestUserStorage = false;
}
}
Since then, I have developed a
better way of solving that kind of problem. All this code now becomes:
task {
do! storage.CheckPlayerStorage
}
Shorter, isn't it? The point is that F# has features that make it possible to compose code using a notation similar to traditional function calls, yet the execution can differ from a traditional call. The idea is so neat that the normally conservative C# design team decided to add a
variant of it to the next version of the language.
Back to the post-mortem, another related problem is to deal with screen transitions. There are several approaches: Hard-coding, events and delegates.
Using hard-coding, screen A creates and displays screen B when a certain action is performed.
This requires the class for screen A to know about the class for screen B, and must have the data needed during instantiation of B at hand. I found this approach was not very flexible, and caused me some trouble when I added support for local multiplayer (which wasn't planned from start).
The two other approaches, events and delegates make it easier to forward the data needed to instantiate new screens, as it's typically available in the class which registers the event handler, or creates the delegates which captures the data in question.
All these approaches share the same problem: the transition code is spread out all over the place, making it hard to debug, modify and extend. Of the 50 bugs I registered in fogbugz, 13 involved screen transitions at some level. For a game programmer who is interested in getting the gameplay right, getting 26% extra bugs because of menus is a very frustrating, even if most of those bugs are easy to fix.
Art assets
Asteroid models, ship models and textures were provided by
Benoit Roche. The title and menu music is from
Partners in Rhyme, sounds from
Soundsnap. The game backgrounds and the box art were done by myself using The Gimp and a tutorial on
starfields. When doing sky boxes, it's a good idea to test them on a PC screen. I failed to notice on my TV that the sides of the box had light levels that did not match. Happily, a tester on the App Hub noticed that and reported the problem.
The community on App Hub...
... was very helpful. Thanks to all of you fellow developers for your feedback and suggestions!
Due to my earlier involvement in free software and Linux, I thought that sending your game to playtest early and often was a good thing. While it was, don't expect feedback for every release. Other developers will not test your game time and again every month. Getting comments from other is a motivation boost, but you should not rely on that. I think it's a good idea to send to playtest as soon as your gameplay is done, to see how well it's received. After that, sending updates every time won't get you much feedback. It may actually be better to wait until a new milestone is reached, e.g. menus are done, art is done, game is ready for evil-checklist testing.
Reception of the game
The mix between a classic 2d game and a 3d space shooter was not well received by the market. After five weeks, the game sold 143 copies with a conversion rate of 8.06%
Few reviews were written, most of them judging the game as dull and hard to control. This is what
xboxindiegames had to say about the game:
You can't steer, you can't see, you can't aim and you can't shoot. You can avoid this game, though...
The Yellow Pages from Pencil Shavings sounded more positive:
Looks fantastic and plays well, just a little redundant [...]. Nice game, enjoyable, but lacking variety.
I also registered the game for Dream-Build-Play 2010, but the game did not make it to the best 20.
The game is rated 3 stars of 5 in the American Xbox LIVE Marketplace, which I think is characteristic of well-done but uninspiring games.
Conclusions
Technically, the game was a success and showed the feasibility of using F#. It took me way too much time (about two years), though. I hope I will be able to increase the production rate for my upcoming games.