• Welcome to Valhalla Legends Archive.
 

2D graphics in C#

Started by shout, May 16, 2006, 01:32 PM

Previous topic - Next topic

shout

The following 'thing' works, but it takes forever to redraw. There has to be a better way to do this. I don't care for any comments not related to the graphics part of it, that is the part I want help with.


public class Map
{
private Rectangle[] squares;
private TerrainTypes[] s_types;
private ImageList terrain_art;

private bool loaded;
/// <summary>
/// Gets if the map has finished loading.
/// </summary>
public bool Loaded
{
get
{
return loaded;
}
}

/// <summary>
/// Represents a map.
/// </summary>
/// <param name="mapfile">The file containing the map</param>
public Map(string mapfile)
{
this.loaded = false;
//Initalize terrain stuff
squares = new Rectangle[1024];
for (int i = 0, x = 0, y = 0; i < 1024;)
{
for (; x < 320; x += 10)
{
for (; y < 320; y += 10)
this.squares[i++] = new Rectangle(x, y, 10, 10);
y -= 320;
}
}
s_types = new TerrainTypes[1024];
for (int i = 0; i < 1024; i++)
{
s_types[i] = TerrainTypes.Ground;
}

//Now the artwork
string[] files;
files = Directory.GetFiles(Directory.GetCurrentDirectory() + @"\art\terrain\");
int[] numbers = new int[files.Length];

for (int i = 0; i < files.Length; i++)
{
char[] ss = new char[] {'_', '.'};
string[] temp = files[i].Split(ss, 3);
numbers[i] = int.Parse(temp[1]);
}

terrain_art = new ImageList();
terrain_art.ColorDepth = ColorDepth.Depth24Bit;
terrain_art.ImageSize = new Size(10, 10);
for (int i = 0; i < numbers.Length; i++)
terrain_art.Images.Add(new Bitmap(files[i]));

//Ok! Now we load each square of the map
System.IO.FileStream n = new FileStream(mapfile, FileMode.Open);
for (int i = 0; i < 1024; i++)
{
int tmp = n.ReadByte();
n.ReadByte();
n.ReadByte();
n.ReadByte();
if (tmp > 7)
this.s_types[i] = (TerrainTypes)0;
else
this.s_types[i] = (TerrainTypes)tmp;
}
this.loaded = true;
}

private void DrawGrid(PaintEventArgs e)
{
for (int i = 0; i < 340; i+=10)
{
e.Graphics.DrawLine(Pens.Green, 0, i, 320, i);
e.Graphics.DrawLine(Pens.Green, i, 0, i, 320);
}
}

/// <summary>
/// Draws the grid and terrain.
/// </summary>
/// <param name="e">Args passed from OnPaint event</param>
public void DrawTerrain(PaintEventArgs e)
{
for (int i = 0; i < 1024; i++)
{
e.Graphics.DrawImage(terrain_art.Images[(int)s_types[i]], squares[i]);
}
this.DrawGrid(e);
}
}


Specifically, is there a way to save the state of the e.Graphics instance? So that it can be drawn in one swipe instead of these loops?

MyndFyre

There are a couple of things you could possibly do.

1.) Consider grouping the rectangles into lists based on texture rather than a single list.  For example, in C# 2.0, you could use generic dictionaries to associate a texture with a list of rectangles:


private Dictionary<Image, List<Rectangle>> m_rectanglesByTexture;

This can be accomplished with slight casting penalties in 1.x by using a Hashtable and an ArrayList.

The problem this will solve is finding the image in the list at every draw.  This is your current code:
e.Graphics.DrawImage(terrain_art.Images[(int)s_types[i]], squares[i]);
Note that you're hunting down the image at every call in the loop.  Rather, I propose:

public void DrawTerrain(PaintEventArgs e)
{
Graphics g = e.Graphics;
Rectangle clip = e.ClipRectangle;
foreach (Image key in m_rectanglesByTextures.Keys)
{
List<Rectangle> rects = m_rectanglesByTextures[key];
int max = rects.Count;
for (int i = 0; i < max; i++)
{
g.DrawImage(key, rects[i]);
}
}
this.DrawGrid(e);
}


Now, this optimization technique is not necessarily that great because it includes what is generally considered to be expensive -- an inner loop.  You can improve it by unrolling the outer loop if you have a limited, known number of textures that you'll be reading.

2.) The other thing you should know is that you don't need to update the entire window on every OnPaint() call.  I'm going to replace the inner loop code above with the following:


for (int i = 0; i < max; i++)
{
Rectangle curRect = rects[i];
if (clip.IntersectsWith(curRect) || clip.Contains(curRect))
g.DrawImage(key, rects[i]);
}


This code ensures that the rectangle you are re-drawing exists within the clip rectangle.  If not, you get to skip the expensive draw operation.

3.) One last thing.  You'll noticed that I created local references to the Graphics and ClipRectangle properties of the PaintEventArgs class in the top of the method.  This avoids the expensive property call every time.  Unfortunately, C# does not allow us to specify a const modifier to properties and methods and parameters, meaning that the compiler isn't able to determine whether we're changing the state of an object, and it can't make those kinds of optimizations.  This allows us to make those optimizations on our own.

Hope that helped!
QuoteEvery generation of humans believed it had all the answers it needed, except for a few mysteries they assumed would be solved at any moment. And they all believed their ancestors were simplistic and deluded. What are the odds that you are the first generation of humans who will understand reality?

After 3 years, it's on the horizon.  The new JinxBot, and BN#, the managed Battle.net Client library.

Quote from: chyea on January 16, 2009, 05:05 PM
You've just located global warming.

shout

#2
Also, the image this produces will not be changed through the execution of the program.

Also, anyone know how to save the drawn image to a file? I have spent 2 hours fuitlessly searching MSDN.

MyndFyre

Quote from: Shout on May 18, 2006, 12:04 PM
Also, the image this produces will not be changed through the execution of the program.

Then why are you worried about it?  Just paint it once to a Bitmap:

Bitmap bmp = new Bitmap(width, height);
using (Graphics g = Graphics.FromImage(bmp))
{
DrawTerrain(g, new Rectangle(Point.Empty, bmp.Size));
}

Then use the g and rect parameters there instead of the PaintEventArgs.Graphics and PaintEventArgs.ClipRectangle properties.

Particularly if you draw every terrain tile by texture (rather than the original way), you shouldn't need to optimize that much more.  It should be a fairly fast method already.
QuoteEvery generation of humans believed it had all the answers it needed, except for a few mysteries they assumed would be solved at any moment. And they all believed their ancestors were simplistic and deluded. What are the odds that you are the first generation of humans who will understand reality?

After 3 years, it's on the horizon.  The new JinxBot, and BN#, the managed Battle.net Client library.

Quote from: chyea on January 16, 2009, 05:05 PM
You've just located global warming.

shout

Quote from: MyndFyre[vL] on May 18, 2006, 12:24 PM
Quote from: Shout on May 18, 2006, 12:04 PM
Also, the image this produces will not be changed through the execution of the program.

Then why are you worried about it? Just paint it once to a Bitmap:

Bitmap bmp = new Bitmap(width, height);
using (Graphics g = Graphics.FromImage(bmp))
{
DrawTerrain(g, new Rectangle(Point.Empty, bmp.Size));
}

Then use the g and rect parameters there instead of the PaintEventArgs.Graphics and PaintEventArgs.ClipRectangle properties.

Particularly if you draw every terrain tile by texture (rather than the original way), you shouldn't need to optimize that much more. It should be a fairly fast method already.

Yes, this works much faster. Thanks!