I'm working on a tile-based game, and am looking to improve the efficiency of my code to slightly improve framerate. I call this method on every tile, every frame, so it is very performance critical. What optimizations can be made?
public bool OnScreen(Vector2 point)
{
Rectangle rect = this.Rectangle;
return ((((rect.X <= point.X) && (point.X < (rect.X + rect.Width))) && (rect.Y <= point.Y)) && (point.Y < (rect.Y + rect.Height)));
}
public bool OnScreen(Vector2 topLeft, Vector2 size)
{
// Check if each corner is inside of the camera rectangle
return (
OnScreen(topLeft) // Top-left
|| OnScreen(topLeft + new Vector2(0, size.Y)) // Bottom-left
|| OnScreen(topLeft + new Vector2(size.X, 0)) // Top-right
|| OnScreen(topLeft + size) // Bottom-right
);
}
I am doing this so that I can pool the objects that actually render my tiles, since only a maximum of about 800 can be on screen at any one time. These methods are called as so:
// Assign renderers to any tiles in range of the camera.
foreach (var tile in _tiles)
{
if (tile.IsInRange(camera))
{
if (tile.Renderer == null)
{
tile.Renderer = _pool.Get();
}
}
else
{
tile.Renderer = null;
}
}
4 Answers 4
You could try to use the following built-in methods instead:
- Rectangle.Contains(Point) instead of the
OnScreen(Vector2 point)
. - Rectangle.IntersectsWith instead of the
OnScreen(Vector2 topLeft, Vector2 size)
.
Thus your latter OnScreen
method will become:
public bool OnScreen(Vector2 topLeft, Vector2 size)
{
return this.Rectangle.IntersectsWith(new Rectangle(topLeft.X, topLeft.Y, size.X, size.Y));
}
Source code at referencesource.microsoft.com:
-
\$\begingroup\$ Ah, good catch! And that just mootinated my answer! :-) \$\endgroup\$Mathieu Guindon– Mathieu Guindon2015年05月28日 21:36:23 +00:00Commented May 28, 2015 at 21:36
-
1\$\begingroup\$ I'm marking this as accepted because that is a huge improvement. Since I'm not using the System.Drawing rectangle but rather the Monogame rectangle, I'm going to see if there is a similar method. Could you add the actual logic behind that method call to your answer? \$\endgroup\$Pip– Pip2015年05月28日 21:38:51 +00:00Commented May 28, 2015 at 21:38
-
1\$\begingroup\$ @Pip Ah, my bad. I've added links to the source codes. \$\endgroup\$Dmitry– Dmitry2015年05月28日 21:50:45 +00:00Commented May 28, 2015 at 21:50
At a glance, OnScreen
looks like a method that would raise some Screen
event, as On[EventName]
is, by convention, the name we use for methods that raise an event. Now, those would obviously return void
, and yours is returning a bool
- I think I would have it like this:
public bool IsOnScreen(Vector2 point)
The return
statement is pretty intense in terms of parentheses. Are they meant to improve readability?
return ((((rect.X <= point.X) && (point.X < (rect.X + rect.Width))) && (rect.Y <= point.Y)) && (point.Y < (rect.Y + rect.Height)));
Let's try something:
return rect.X <= point.X
&& point.X < (rect.X + rect.Width)
&& rect.Y <= point.Y
&& point.Y < (rect.Y + rect.Height);
Much better isn't it?
for vs foreach
If _tiles
is not an array you can gain some performance by using a simple for(int index = 0; index < _tiles.Count; index++)
.
This is because foreach uses the IEnumerable<out T>.GetEnumerator
which can result in a lot of extra method calls in a tight loop. When the compiler can verify that the foreach is used on an array it will optimize it into the equivalent for-loop.
This short but non scientific example, compiled in release x64 in vs2012 and run outside of Visual Studio, shows that foreach is just short of three times as slow as for on a list of 1 million elements:
class Program
{
static void Main()
{
var totalTime = Stopwatch.StartNew();
var list = Enumerable.Range(0, 1000000).ToList();
long sum = 0;
//warmup
for (int i = 0; i < 10000; i++)
{
sum = UsingFor(sum, list);
sum = UsingForeach(sum, list);
}
sum = 0;
var stopwatch = Stopwatch.StartNew();
sum = UsingForeach(sum, list);
stopwatch.Stop();
Console.WriteLine("Foreach-result=" + sum + " found in " + stopwatch.Elapsed);
sum = 0;
stopwatch.Restart();
sum = UsingFor(sum, list);
stopwatch.Stop();
Console.WriteLine("For-result=" + sum + " found in " + stopwatch.Elapsed);
Console.WriteLine("TotalTime: " + totalTime.Elapsed);
}
static long UsingFor(long sum, List<int> list)
{
for (int i = 0; i < list.Count; i++)
{
sum += list[i];
}
return sum;
}
static long UsingForeach(long sum, List<int> list)
{
foreach (var value in list)
{
sum += value;
}
return sum;
}
}
On my computer it's 39 seconds of warmup and
- Foreach-result found in 00:00:00.0029817
- For-result found in 00:00:00.0009497
This is only a measure of the iteration cost which very well might be dwarfed by the cost of what is being calculated in the loop.
-
\$\begingroup\$ Huh, I didn't know that. How much of a performance difference is this roughly? \$\endgroup\$Pip– Pip2015年05月29日 13:53:46 +00:00Commented May 29, 2015 at 13:53
-
2\$\begingroup\$ It's very hard to tell but around 3 additional method invocations per iteration. A few nanoseconds at worst but for a loop over 1 million elements it does add up. It might also allow the compiler to unroll the loop and do several comparisons per iteration which can result in better utilization of the CPU pipelines. TL:DR; Use a profiler. \$\endgroup\$Johnbot– Johnbot2015年05月29日 13:59:40 +00:00Commented May 29, 2015 at 13:59
Change the design?
I don't know the details of your game, so this might be silly...
A tile game is generally easily organized as a grid:
- Store all tiles in a matrix
- Depending on how your camera and your rendering works, you can find the top-left and bottom-right coordinates, in that matrix, of visible tiles.
- Render only tiles in the area that is visible, with two nested
for
loops. This is faster than checking, for each tile, whether it should be rendered.
Explore related questions
See similar questions with these tags.