I ended up using Nick Gravelyn's SpriteSheetPacker. I had to make a few configuration changes to the project to get it working, which I updated here: https://github.com/walteryoung/SpriteSheetPacker
I have had trouble when accessing assets with file extensions in the content pipeline, so I saved out the PNG and the XML from the SpriteSheetPacker file as extension-less files named 'sprite' and 'spriteIndex', respectively.
As an aside, I ending up writing a batch script to make copies of all of my assets with no file extension (I have a lot of XML files for level data).
Here is my SpriteSheet class, which loads the spriteSheet Texture2D and has a Dictionary for looking up sprites by name.
internal class SpriteSheet
internal Texture2D Texture;
Dictionary<String, Rectangle> index;
public void LoadContent(ContentManager content)
index = content.Load<Dictionary<String, Rectangle>>("spriteIndex");
Texture = content.Load<Texture2D>("sprites");
internal Rectangle? SourceRectangle(string spriteName)
bool hasValue = index.TryGetValue(spriteName, out r);
throw new Exception("value doesn't exist");
This solution ended up being pretty much exactly what I wanted, and super easy to implement.