0
votes

I'm trying to build a map editor like this one: http://blog.rpgmakerweb.com/wp-content/uploads/2012/04/T2EditorLabelled.png in WPF.

How should it work:
By selecting a specific tile on the "tile list" - B section - is possible to draw that tile on the canvas - section A.
In the end the final result is a full game level drawn on the canvas.

First approach:
In my first approach each tile is drawn by creating a new image control and adding it to the canvas (a WPF canvas control).

Steps:

  1. Select the tile from the tileset
  2. Catch the click event on the canvas
  3. Create a new image, cropping the tile from the tileset
  4. Adding the image in the right spot on the canvas (as a child)

This method is quite naive and it imply two big issues:

  1. All the pixels are already buffered on the tileset that contains all the tiles, but each time I draw a tile on the canvas a new image is create and by doing so I am obliged to replicate part of the tileset pixel data as a source for the new
  2. Too many controls on the canvas:
    A map game can reach the size of 1000 tiles x 1000 tiles, WPF start to have sensible decay of performance on a 100x100 tile map.
    So create a control image for each tile is an unfeasible solution.

Second approach:

My second approach contemplate the use of a single big WriteableBitmap as canvas background.

As in the previous approach the tile is selected on the tileset and the draw event is a click on the canvas.

In this case though no new image is created ex-novo but the background WriteableBitmap is modified accordingly.

So in the number of controls is reduced drastically since all the draw mechanism is performed on the WriteableBitmap.

The main problem with this approach is that if I want to create a large map with 1k x 1k tiles with a tile of 32x32, the size of the background image would be astronomical.

I wonder if there is a way to develop a good solution of this problem in WPF. How would you address this development problem?

1

1 Answers

1
votes

There a variety of different ways you could approach this problem to increase performance.

In terms of the rendering of images, WPF by default isn't amazing, so you could;

  1. Use GDI's BitBlt to quickly render images to a WinForms control which can be hosted. The benefit of this is that GDI is software and hence does not require a graphics card or anything. (WPF fast method to draw image in UI)

  2. You can use D3DImage as an image source. This would mean that you can use the D3DImage as the canvas to which to draw. Doing this would mean you would have to render all the tiles to the D3DImage image source using Direct3D, which would be much faster as it is hardware accelerated. (https://www.codeproject.com/Articles/28526/Introduction-to-D3DImage)

  3. You may be able to host XNA through a WinForms control and rendering using that, I have no experience with this, so I test to any performance. (WPF vs XNA to render thousands of sprites)

Personally, for rendering, I would use the GDI method as it is software based, relatively easy to set up and I have had experience with it, and seen it's performance.

Additionally, when rendering the tiles to a control, you can use the scrollbar positions and control size to determine the area of your map which is actually visible. From this, you can simply select those few tiles and only render them, saving a lot of time.

Furthermore, when you manage it yourself, you can simply load the different sprites into memory and then use that same memory to draw it in different locations onto a buffer image. This would decrease that memory issue you mentioned.

Below is my example code for the GDI method, I render 2500 32x32 pixel sprites (all of which are the same green color, however you'd set this memory to an actual sprite - src memory). The sprites are bitblit to a buffer image (srcb memory) which is then bitblit to the window, in your case, you'd bitblit the buffer image to a winforms canvas or something. With this, I get between 30 and 40 fps on my base model Surface Pro 3. This should be enough performance for a level editor's rendering. Please note this code is very crude and just roughly outlines the process, it can almost certainly be improved upon.

//
        // GDI DLL IMPORT
        //

        [DllImport("gdi32.dll", SetLastError = true)]
        public static extern IntPtr SelectObject(IntPtr hdc, IntPtr hgdiobj);

        [DllImport("gdi32.dll", SetLastError = true)]
        public static extern bool DeleteObject(IntPtr hObject);

        [DllImport("gdi32.dll", SetLastError = true)]
        public static extern IntPtr CreateCompatibleDC(IntPtr hDC);

        [DllImport("gdi32.dll", SetLastError = true)]
        [return: MarshalAs(UnmanagedType.Bool)]
        public static extern bool DeleteDC(IntPtr hDC);

        [DllImport("gdi32.dll", SetLastError = true)]
        public static extern bool BitBlt(IntPtr hDC, int x, int y, int width, int height, IntPtr hDCSource, int sourceX, int sourceY, uint type);

        [DllImport("gdi32.dll", ExactSpelling = true)]
        public static extern bool FillRgn(IntPtr hdc, IntPtr hrgn, IntPtr hbr);

        [DllImport("gdi32.dll", ExactSpelling = true)]
        public static extern IntPtr CreateRectRgn(int nLeftRect, int nTopRect, int nRightRect, int nBottomRect);

        [DllImport("gdi32.dll", ExactSpelling = true)]
        public static extern IntPtr CreateSolidBrush(uint crColor);

        [DllImport("gdi32.dll", ExactSpelling = true)]
        public static extern IntPtr CreateCompatibleBitmap(IntPtr hdc, int nWidth, int nHeight);

        public const uint SRCCOPY = 0x00CC0020; // dest = source                   
        public const uint SRCPAINT = 0x00EE0086;    // dest = source OR dest           
        public const uint SRCAND = 0x008800C6;  // dest = source AND dest          
        public const uint SRCINVERT = 0x00660046;   // dest = source XOR dest          
        public const uint SRCERASE = 0x00440328;    // dest = source AND (NOT dest )   
        public const uint NOTSRCCOPY = 0x00330008;  // dest = (NOT source)             
        public const uint NOTSRCERASE = 0x001100A6; // dest = (NOT src) AND (NOT dest) 
        public const uint MERGECOPY = 0x00C000CA;   // dest = (source AND pattern)     
        public const uint MERGEPAINT = 0x00BB0226;  // dest = (NOT source) OR dest     
        public const uint PATCOPY = 0x00F00021; // dest = pattern                  
        public const uint PATPAINT = 0x00FB0A09;    // dest = DPSnoo                   
        public const uint PATINVERT = 0x005A0049;   // dest = pattern XOR dest         
        public const uint DSTINVERT = 0x00550009;   // dest = (NOT dest)               
        public const uint BLACKNESS = 0x00000042;   // dest = BLACK                    
        public const uint WHITENESS = 0x00FF0062;   // dest = WHITE     

        //
        // END DLL IMPORT
        //

        //GDI Graphics
        private Graphics g;

        //Colors
        private const int BACKGROUND_COLOR = 0xffffff;
        private const int GRAPH_COLOR_ONE = 0x00FF00;

        //Pointers
        IntPtr hdc;
        IntPtr srcb;
        IntPtr dchb;
        IntPtr origb;
        IntPtr src;
        IntPtr dch;
        IntPtr orig;

        //Brushes
        IntPtr brush_one;
        IntPtr brush_back;

        public Form1()
        {
            InitializeComponent();
            //Create a graphics engine from the window
            g = Graphics.FromHwnd(this.Handle);

            //Get the handle of the Window's graphics and then create a compatible source handle
            hdc = g.GetHdc();
            srcb = CreateCompatibleDC(hdc);
            src = CreateCompatibleDC(hdc);

            //Get the handle of a new compatible bitmap object and map it using the source handle to produce a handle to the actual source
            dchb = CreateCompatibleBitmap(hdc, ClientRectangle.Width, ClientRectangle.Height);
            origb = SelectObject(srcb, dchb);

            //Get the handle of a new compatible bitmap object and map it using the source handle to produce a handle to the actual source
            dch = CreateCompatibleBitmap(hdc, 32, 32);
            orig = SelectObject(src, dch);

            //Create the burshes
            brush_one = CreateSolidBrush(GRAPH_COLOR_ONE);
            brush_back = CreateSolidBrush(BACKGROUND_COLOR);

            //Create Image
            FillRectangle(brush_one, src, 0, 0, 32, 32);

            //Fill Background
            FillRectangle(brush_back, hdc, 0, 0, ClientRectangle.Width, ClientRectangle.Height);

            this.Show();
            Render();
        }

        private void Render()
        {
            Stopwatch s = new Stopwatch();
            s.Start();

            int frames = 0;

            while(frames <= 30)
            {
                frames++;

                FillRectangle(brush_back, srcb, 0, 0, ClientRectangle.Width, ClientRectangle.Height);

                for (int i = 0; i < 50; i++)
                    for (int j = 0; j < 50; j++)
                        BlitBitmap(i * 5, j * 5, 32, 32, srcb, src);

                BlitBitmap(0, 0, ClientRectangle.Width, ClientRectangle.Height, hdc, srcb);
            }

            s.Stop();
            float fps = (float)frames / ((float)s.ElapsedMilliseconds / 1000.0f);
            MessageBox.Show(Math.Round(fps, 2).ToString(), "FPS");
        }

        private void FillRectangle(IntPtr b, IntPtr hdc, int x, int y, int w, int h)
        {
            //Create the region
            IntPtr r = CreateRectRgn(x, y, x + w, y + h);

            //Fill the region using the specified brush
            FillRgn(hdc, r, b);

            //Delete the region object
            DeleteObject(r);
        }

        private void BlitBitmap(int x, int y, int w, int h, IntPtr to, IntPtr from)
        {
            //Blit the bits of the actual source object to the window, using its handle
            BitBlt(to, x, y, w, h, from, 0, 0, SRCCOPY);
        }