How Not To Write A Game – The Reports of the Xbox 360 GC Are Greatly Exaggerated
Ok, so that title seemed shorter in my head.
A common theme around the XNA forums is that garbage collections are the most devastating thing on the Xbox 360. If you listened to some people you would think a single garbage collection would cause your Xbox 360 to implode, create a black hole, and be the catalyst for the end of the world. I’m afraid my experience shows somewhat of the opposite.
Let’s get something straight first. I do try to avoid garbage collections. Object management is a big part of my framework whereby I can re-use object references to avoid lots of garbage. However there are lots of places where I throw those “best practices” for avoiding garbage right out the window because it just takes too much work to remember to worry about it. I think the prime way to show this is simply to put some code on the table.
First let’s take a look at a custom text drawing routine I wrote for my tutorial screens as well as the little instruction text in the bottom left corner of the screen:
public static void DrawSpecialText(
SpriteBatch batch,
SpriteFont font,
SpriteFont carotFont,
string text,
Vector2 position,
Color color)
{
string[] splitText = text.Split('@');
float xPosition = position.X;
float characterHeight = font.MeasureString("O").Y;
foreach (string s in splitText)
{
if (s.Length == 1)
{
Rectangle buttonRect = new Rectangle(
(int)xPosition,
(int)position.Y,
(int)characterHeight,
(int)characterHeight);
if (s == AButtonText)
batch.Draw(AButton, buttonRect, Color.White);
else if (s == BButtonText)
batch.Draw(BButton, buttonRect, Color.White);
else if (s == XButtonText)
batch.Draw(XButton, buttonRect, Color.White);
else if (s == YButtonText)
batch.Draw(YButton, buttonRect, Color.White);
else if (s == StartButtonText)
batch.Draw(StartButton, buttonRect, Color.White);
else if (s == BackButtonText)
batch.Draw(BackButton, buttonRect, Color.White);
xPosition += characterHeight;
}
else
{
string[] boldSplit = s.Split('^');
bool bold = s.StartsWith("^");
foreach (string subStr in boldSplit)
{
SpriteFont fontForString = (bold) ? carotFont : font;
batch.DrawString(
fontForString,
subStr,
new Vector2(xPosition, position.Y),
color);
xPosition += fontForString.MeasureString(subStr).X;
bold = !bold;
}
}
}
}
What’s that, you say? “Nick, you’re using string literals! And allocating an array! 60 times a second?!” Yep. This code, when something is using the method obviously, runs every frame and just throws out garbage. Could I “fix” this? Sure. You can fix anything. But why spend the time when this had no effect on my frame rates.
Let’s look at another example. This is the code I use to render the in-game HUD:
private void DrawInGameHUD(Rectangle screen)
{
BlocGame.SpriteBatch.Begin();
Color hudColor = Color.White;
float alpha = 255f;
if (coop)
{
if (!circle.IsDead && !circle2.IsDead)
alpha = Math.Min(
Math.Abs(circle.Position.Y - screen.Top),
Math.Abs(circle2.Position.Y - screen.Top));
else if (circle.IsDead)
alpha = Math.Abs(circle2.Position.Y - screen.Top);
else if (circle2.IsDead)
alpha = Math.Abs(circle.Position.Y - screen.Top);
}
else if (!circle.IsDead)
{
alpha = Math.Abs(circle.Position.Y - screen.Top);
}
// clamp the alpha between 50 and 255
alpha = MathHelper.Clamp(alpha, 50, 255);
hudColor = new Color(hudColor.R, hudColor.G, hudColor.B, (byte)alpha);
int totalScore = player1Score.Score + player2Score.Score;
if (!coop)
{
string caption = ProfileManager.GetGamer(ProfileManager.PlayerOne).Gamertag+":";
Vector2 captionSize = hudFont.MeasureString(caption);
string scoreString;
if (totalScore > 999999)
scoreString = totalScore.ToString();
else
scoreString = string.Format(scoreFormat, totalScore);
BlocGame.SpriteBatch.DrawString(
hudFont,
caption,
new Vector2(screen.Left + 10f, screen.Top - 3f),
hudColor);
BlocGame.SpriteBatch.DrawString(
hudFont,
scoreString,
new Vector2(screen.Left + 10f, screen.Top + captionSize.Y - 10f),
hudColor);
int placement;
HighScore nextScore = highScoreList.GetNextHighScore(
currentPlayTime,
ProfileManager.GetGamer(ProfileManager.PlayerOne).Gamertag,
player1Score.Score,
player1Score.PlayTime,
out placement);
// figure out the caption
caption = (placement > 0) ? nextScoreCaption1 : nextScoreCaption2;
captionSize = hudFont.MeasureString(caption);
totalScore = (placement > 0)
? nextScore.TotalScore
: player1Score.Score + player2Score.Score;
if (totalScore > 999999)
scoreString = totalScore.ToString();
else
scoreString = string.Format(scoreFormat, totalScore);
// measure some stuff
Vector2 scoreSize = hudFont.MeasureString(scoreString);
// draw the score itself
BlocGame.SpriteBatch.DrawString(
hudFont,
caption,
new Vector2(screen.Right - 10f - captionSize.X, screen.Top - 3f),
hudColor);
BlocGame.SpriteBatch.DrawString(
hudFont,
scoreString,
new Vector2(
screen.Right - 10f - scoreSize.X,
screen.Top + captionSize.Y - 10f),
hudColor);
}
else
{
PlayerIndex p;
GameScore score;
if (ProfileManager.PlayerOne < ProfileManager.PlayerTwo)
{
p = ProfileManager.PlayerOne;
score = player1Score;
}
else
{
p = ProfileManager.PlayerTwo;
score = player2Score;
}
string caption = ProfileManager.GetGamer(p).Gamertag + ":";
Vector2 captionSize = hudFont.MeasureString(caption);
string scoreString;
if (player1Score.Score > 999999)
scoreString = score.Score.ToString();
else
scoreString = string.Format(scoreFormat, score.Score);
BlocGame.SpriteBatch.DrawString(
hudFont,
caption,
new Vector2(screen.Left + 10f, screen.Top - 3f),
hudColor);
BlocGame.SpriteBatch.DrawString(
hudFont,
scoreString,
new Vector2(screen.Left + 10f, screen.Top + captionSize.Y - 10f),
hudColor);
// figure out the caption
if (ProfileManager.PlayerOne > ProfileManager.PlayerTwo)
{
p = ProfileManager.PlayerOne;
score = player1Score;
}
else
{
p = ProfileManager.PlayerTwo;
score = player2Score;
}
caption = ProfileManager.GetGamer(p).Gamertag + ":";
captionSize = hudFont.MeasureString(caption);
if (player2Score.Score > 999999)
scoreString = score.Score.ToString();
else
scoreString = string.Format(scoreFormat, score.Score);
// measure some stuff
Vector2 scoreSize = hudFont.MeasureString(scoreString);
// draw the score itself
BlocGame.SpriteBatch.DrawString(
hudFont,
caption,
new Vector2(screen.Right - 10f - captionSize.X, screen.Top - 3f),
hudColor);
BlocGame.SpriteBatch.DrawString(
hudFont,
scoreString,
new Vector2(
screen.Right - 10f - scoreSize.X,
screen.Top + captionSize.Y - 10f),
hudColor);
}
float y = screen.Top + 15f;
if (coop)
{
int cumulateScore = player1Score.Score + player2Score.Score;
string scoreString;
if (cumulateScore > 999999)
scoreString = cumulateScore.ToString();
else
scoreString = string.Format(scoreFormat, cumulateScore);
BetterSpriteBatch.DrawCenteredText(
BlocGame.SpriteBatch,
hudFont,
scoreString,
new Vector2(screen.Left + screen.Width / 2, y),
hudColor);
y += hudFont.MeasureString(scoreString).Y - 10f;
}
string timeString = string.Format(
(currentPlayTime.Hours == 0) ? BlocGame.TimeFormat : BlocGame.TimeFormat2,
currentPlayTime.Hours,
currentPlayTime.Minutes,
currentPlayTime.Seconds);
BetterSpriteBatch.DrawCenteredText(
BlocGame.SpriteBatch,
hudFont,
timeString,
new Vector2(
screen.Left + screen.Width / 2,
y),
hudColor);
BlocGame.SpriteBatch.End();
}
In addition to be way too long for a single method, and being quite a mess of code, you’ll notice quite a few string.Formats in there, a few ToStrings, and even a few string literals. From my quick counting this method allocates, in co-op mode, eight strings per frame. That’s equivalent to 240 string allocations per second. Should I be worried? I mean, the Xbox 360 garbage collector is definitely not going to like that! So what. The game still runs fine so why should I put any effort into fixing a problem I don’t have?
So while I once was in the camp of “avoid garbage collection at all costs”, I’ve fallen further to the side of “know where you allocate in case you need to optimize out the allocations”. Don’t start a game trying to avoid allocating arrays here and there or string literals once in a while.
Possibly Related Posts
(Automatically Generated)Catching Exceptions on Xbox 360
How Not To Write A Game – Everything Changes
Write a Tutorial, Win an Xbox 360!
Pong in F# with XNA Game Studio
Paint.NET plugin for premultiplied alpha

I’ve always been in the “don’t optimize unless you need to” camp. It takes enough time and effort just to get a game done that I don’t want to waste it on not-very-useful optimizations.
I have never had a big problem with strings creating too much garbage. What I found on the Zune was that List`T.Contains and a Dictionary look up with an enumeration key both generated amazing amounts of garbage. It was causing constant collections.
The first was solved by simply looping through the collection and comparing myself. The second was solved by casting the enumeration values to shorts and using a short data type as the key in the Dictionary.
I think, in general, strings and foreach statements are fairly cheap in terms of garbage generation. You can clean them up for sure, but the gains made would be a lot less if you had spent time elsewhere.
Another thing to look out is setting render states and values in shaders. Much like strings, they are cheap operations but when you do a lot (and I mean a lot), you can start to have some minor problems.
Yes, enumerations in generic collections when using any sort of comparison (be it the List.Contains method or as the key in a Dictionary) will incur boxing/unboxing as it puts it into an object and runs the Equals method on it. There is actually a way around that which involves making your own IEqualityComparer object and passing that to the collection. I summarized a couple of blog posts about that here: http://www.xnawiki.com/index.php?title=Enumerations_as_Dictionary_Keys.