Saturday, August 22, 2009

Silverlight – SpriteSheet management with WriteableBitmap

As part of my Silverlight education process I have been writing some games. First a few basic things like Sildoku and Peg Solitaire. Now I am advancing to something a bit more challenging, I am writting a small 2d racing game. For this I started with some of the fun parts, writing a Sprite manager, while this is still in the early stages I thought I would share one of my basic classes the SpriteSheet class.
For those of you who do not know what a sprite sheet is, it is an set of images placed in a grid layout and composed into one larger image. Often sequential images in the grid will represent frames in an animation of some action such as walking etc. Take a look at this Wikipedia link.
My sprite manager at this stage supports both static and animated sprites, using one or more sprite sheets as a source of the images. Here is the code for the SpriteSheet class.
public class SpriteSheet
{
  private BitmapSource _spriteSheetSource;
  private WriteableBitmap _spriteSheetBitmap;
  private int _sheetWidth;
  private int _sheetHeight;    

  public SpriteSheet(BitmapSource spriteSheetSource)
  {
    if (spriteSheetSource == null) throw new ArgumentNullException("spriteSheetSource");

    _spriteSheetSource = spriteSheetSource;
    _spriteSheetBitmap = new WriteableBitmap(_spriteSheetSource);
    _sheetWidth = _spriteSheetBitmap.PixelWidth;
    _sheetHeight = _spriteSheetBitmap.PixelHeight;
  }

  public WriteableBitmap GetBitmap(int x, int y, int width, int height)
  {
    WriteableBitmap destination = new WriteableBitmap(width, height);      
    GetBitmap(destination, x, y, width, height);
    return destination;
  }

  public void GetBitmap(WriteableBitmap targetBitmap, int x, int y, int width, int height)
  {
    // Validate incomming data
    if (targetBitmap == null) throw new ArgumentNullException("targetBitmap");
    if (x < 0 || x >= _sheetWidth) throw new ArgumentOutOfRangeException("x");
    if (y < 0 || y >= _sheetHeight) throw new ArgumentOutOfRangeException("y");
    if (width < 0 || (x + width > _sheetWidth)) throw new ArgumentOutOfRangeException("width");
    if (height < 0 || (y + height > _sheetHeight)) throw new ArgumentOutOfRangeException("height");

    // Get pixel buffers for the sprite sheet and the target bitmap
    int[] sourcePixels = _spriteSheetBitmap.Pixels;
    int[] targetPixels = targetBitmap.Pixels;

    // Calculate starting offsets into the pixel buffers      
    int sourceOffset = x + (y * _sheetWidth);      
    int targetOffset = 0;

    // Note that the offsets and widths are multiplied by 4, this is because Buffer.BlockCopy requires
    // byte offsets into the buffers and our buffers are integer buffers. To optimize this I have 
    // premultiplied to values so that the multiplication is removed from the loop
    int sourceByteOffset = sourceOffset << 2;
    int sheetByteWidth = _sheetWidth << 2;
    int targetByteWidth = width << 2;
    for (int row = 0; row < height; ++row)
    {
      Buffer.BlockCopy(sourcePixels, sourceByteOffset, targetPixels, targetOffset, targetByteWidth);
      sourceByteOffset += sheetByteWidth;
      targetOffset += targetByteWidth;
    }      
  }    

  public int Width
  {
    get { return _sheetWidth; }
  }

  public int Height
  {
    get { return _sheetHeight; }
  }
}
The interesting function in this class is the GetBitmap(WriteableBitmap targetBitmap, int x, int y, int width, int height) overload. This function is the work horse of the class. It is responsible for transferring a region of the source image starting at x, y to the new WriteableBitmap. The GetBitmap(int x, int y, int width, int height) overload calls this function with a pre-allocated WriteableBitmap of the correct dimensions.
Using the class is quite simple. Assume you have a couple of standard Image controls declared on your page, something like the following.

<UserControl x:Class="Vulcan.SL.TestApplication.MainPage"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" 
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d" d:DesignWidth="640" d:DesignHeight="480">
  <Canvas x:Name="LayoutRoot">  
    <Image x:Name="mySprite1" Width="100" Height="100" Canvas.Left="0"/>
    <Image x:Name="mySprite2" Width="100" Height="100" Canvas.Left="110"/>
  </Canvas>
</UserControl>
You will notice that I have not set the Source of the images, we will do this in code using the GetBitmap function of the SpriteSheet class.
Next in the code behind file we need to load the image that and create an instance of the SpriteSheet class passing the image. The trick here is that you should only create the SpriteSheet instance once the image has been fully downloaded. Here is one way of doing this.
public MainPage()
{
  InitializeComponent();

  BitmapImage spriteSheetBitmap = new BitmapImage(new Uri("/Images/WindmillLeft.png", UriKind.Relative));      
  spriteSheetBitmap.CreateOptions = BitmapCreateOptions.None;
  spriteSheetBitmap.ImageOpened += new EventHandler<RoutedEventArgs>(image_ImageOpened);       
}

void image_ImageOpened(object sender, RoutedEventArgs e)
{
  BitmapImage spriteSheetBitmap = sender as BitmapImage;

  SpriteSheet spriteSheet = new SpriteSheet(spriteSheetBitmap);

  // Set the source of the mySprite1 to an image extracted from the SpriteSheet
  mySprite1.Source = spriteSheet.GetBitmap(0, 0, 224, 240);

  // Set the source of the mySprite2 to an image extracted from the SpriteSheet
  mySprite2.Source = spriteSheet.GetBitmap(2240, 0, 224, 240);
}
Here I create a BitmapImage class and set the CreateOptions to BitmapCreateOptions.None. This ensures that the image is created without delay, however the creation still takes place asynchronously so we need attach a handler to the ImageOpened event which indicates that the image has been downloaded and decoded, indicating that we can now use the image.
In the ImageOpened event handler we can construct our SpriteSheet and used the GetBitmap function to extract the regions of interest and assign them to the relevant Image instances. I have purposely not used instance members in these examples so that each piece of code is self contained, and of course I have forgone error checks etc. for the sake of clarity.
Note that the SpriteSheet does not perform any kind of caching of the regions that are extracted, this means that if you request the same region multiple times with the GetBitmap function you will receive new instances of WriteableBitmaps for each call. In my case I take care of the caching at a higher level. I have a SpriteFrameSet class which is a cache of the frames used for an animation sequence, internally this class uses the SpriteSheet to build an array of images ie. frames, and from that point on the array is referenced to retrieve each frame from the frame set.
As my code for the Sprite and AnimatedSprite classes matures I will hopefully find some time to release that on this blog.

No comments:

Post a Comment