Author Archives: dylan

Displaying image thumbnails in the level editor

To display the sprite thumbnails in my level editor application, I wrote a couple of simple custom controls.

The first control extends from the .NET PictureBox control, and just has an additional property to track whether it’s been selected. Then, in the OnPaint method, if the SelectablePictureBox is selected, then I draw a border over the top of it.

The second control extends from the .NET FlowLayoutPanel control, and has an event that is raised when any child SelectablePictureBox is clicked, and then some code to handle Ctrl-clicking and Shift-clicking to select and deselect individual SelectablePictureBox child controls.

The source code for both controls is below. Feel free to use it if you want to, but note that it’s pretty simple and inflexible, so the SelectableImageFlowLayoutPanel will only work with the SelectablePictureBox.

Let me know if you have any questions about it.

using System;
using System.Collections.Generic;
using System.Text;
using System.Windows.Forms;
using System.Drawing;

namespace Editor
{
    public class SelectablePictureBox : PictureBox
    {
        private bool _selected = false;

        public bool Selected
        {
            get { return _selected; }
            set
            {
                _selected = value;
            }
        }

        public SelectablePictureBox()
            : base()
        {
            this.SizeMode = PictureBoxSizeMode.Zoom;
            this.BorderStyle = BorderStyle.FixedSingle;
        }

        protected override void OnPaint(PaintEventArgs pe)
        {
            if (_selected)
            {
                this.BorderStyle = BorderStyle.None;
            }
            else
            {
                this.BorderStyle = BorderStyle.FixedSingle;
            }

            base.OnPaint(pe);

            if (_selected)
            {
                this.BorderStyle = BorderStyle.None;
                Pen pen = new Pen(System.Drawing.Color.CornflowerBlue, 6.0f);
                int penWidth = (int)pen.Width;
                Point[] points = new Point[5] { new Point(pe.ClipRectangle.Left, pe.ClipRectangle.Top),
                    new Point(pe.ClipRectangle.Right, pe.ClipRectangle.Top),
                    new Point(pe.ClipRectangle.Right, pe.ClipRectangle.Bottom),
                    new Point(pe.ClipRectangle.Left, pe.ClipRectangle.Bottom),
                    new Point(pe.ClipRectangle.Left, pe.ClipRectangle.Top )};
                pe.Graphics.DrawLines(pen, points);
            }
        }
    }
}
using System;
using System.Collections.Generic;
using System.Text;
using System.Windows.Forms;

namespace Editor
{
    public class SelectableImageFlowLayoutPanel : FlowLayoutPanel
    {
        public delegate void OnSelectedIndexChanged();
        public event OnSelectedIndexChanged SelectedIndexChanged;

        public SelectableImageFlowLayoutPanel()
            : base()
        {
        }

        protected override void OnControlAdded(ControlEventArgs e)
        {
            base.OnControlAdded(e);

            if (e.Control.GetType() == typeof(SelectablePictureBox))
            {
                e.Control.Click += new EventHandler(SelectablePictureBox_Click);
            }
        }

        public int SelectedIndex
        {
            get
            {
                int i = 0;
                bool found = false; ;
                foreach (Control c in Controls)
                {
                    if (((SelectablePictureBox)c).Selected)
                    {
                        found = true;
                        break;
                    }
                    i++;
                }

                if (found)
                {
                    return i;
                }
                else
                {
                    return -1;
                }
            }
            set
            {
                int i = 0;
                foreach (Control c in Controls)
                {
                    if (i == value)
                    {
                        // select controls
                        ((SelectablePictureBox)c).Selected = true;
                        // force a redraw
                        c.Invalidate();
                        break;
                    }
                    else
                    {
                        // deselect controls
                        ((SelectablePictureBox)c).Selected = false;
                        // force a redraw
                        c.Invalidate();
                    }
                    i++;
                }
            }
        }

        public List<SelectablePictureBox> SelectedItems
        {
            get
            {
                List<SelectablePictureBox> selectedItems = new List<SelectablePictureBox>();

                foreach (Control c in Controls)
                {
                    if (((SelectablePictureBox)c).Selected)
                    {
                        selectedItems.Add(((SelectablePictureBox)c));
                    }
                }
                return selectedItems;
            }
        }

        public List<SelectablePictureBox> Items
        {
            get
            {
                List<SelectablePictureBox> items = new List<SelectablePictureBox>();

                foreach (Control c in Controls)
                {
                    items.Add(((SelectablePictureBox)c));
                }
                return items;
            }
        }

        void SelectablePictureBox_Click(object sender, EventArgs e)
        {
            if (InputHelper.IsKeyDown(Keys.ControlKey))
            {
                ((SelectablePictureBox)sender).Selected = true;
                // force a redraw
                ((SelectablePictureBox)sender).Invalidate();
            }
            else if (InputHelper.IsKeyDown(Keys.ShiftKey))
            {
                int startIdx = 0, endIdx = 0;
                bool foundStart = false;

                endIdx = Controls.IndexOf(((SelectablePictureBox)sender));

                foreach (Control c in Controls)
                {
                    if (!foundStart && ((SelectablePictureBox)c).Selected)
                    {
                        foundStart = true;
                        startIdx = Controls.IndexOf(c);
                    }
                    else
                    {
                        // deselect controls
                        ((SelectablePictureBox)c).Selected = false;
                        // force a redraw
                        c.Invalidate();
                    }
                }

                if (!foundStart)
                {
                    startIdx = endIdx;
                }

                for (int i = startIdx; i <= endIdx; i++)
                {
                    // select the controls in range
                    ((SelectablePictureBox)Controls[i]).Selected = true;
                    // force a redraw
                    Controls[i].Invalidate();
                }
            }
            else
            {
                foreach (Control c in Controls)
                {
                    if (c == sender)
                    {
                        ((SelectablePictureBox)c).Selected = true;
                    }
                    else
                    {
                        ((SelectablePictureBox)c).Selected = false;
                    }
                    // force a redraw
                    c.Invalidate();
                }
            }

            if (SelectedIndexChanged != null)
            {
                SelectedIndexChanged();
            }
        }
    }
}
Share

Jolicloud

I installed the Jolicloud OS on my EEE PC 701 this weekend, and so far it’s working beautifully.

Installation was fast (around 15 minutes) and easy, and it picked up my home WiFi with no problems.

It’s also easy to install new applications. There’s an application catalog in the OS, and it’s a one-click process to install them, and you can queue up multiple installs.

I couldn’t find Comix in the catalog, so I ran apt-get, and it was downloaded quickly from a local repository, installed, and a shortcut was placed in the main screen (something I could never get the EEE OS to do properly).

So far I’m very impressed, and would recommend it over the default EEE OS without hesitation, alpha version or not.

Share

Writing the level processor

I thought it might be useful to go over how I wrote the custom level processor, because after following the example in the documentation I still had some questions.

The Level Classes

I already had the classes that make up my level, because I was using them in the level editor. This is the class diagram of the level objects:

Level Class Diagram

As you can see, it’s pretty straightforward.
The classes on the left hold the Farseer collision entity data. I need to update these to add extra information, such as making the collision entities static or dynamic.
The TextureInstance class represents a single texture drawn on the screen. Note that it doesn’t hold the actual texture, just the name of the texture. The textures themselves are stored in the Level class. This is so that I’m not duplicating textures in memory if I need to draw the same image in multiple places.
The Level class just holds collections of the other classes, the Texture2D images, and the height and width of the level. Continue reading

Share

XNA Level Editor

I’m working on a level editor for creating XNA games.

At the moment, it only supports the 2D game engine I’m working on, but I’m planning to extend both the engine and editor to support 3D graphics (but keeping it to 2D logic for the time-being).

This is what the editor looks like when it loads:
Level Editor
The grid can be turned on or off, and the grid size will be editable in the application settings.

From here, you can load your images, and place them in the level: Level Editor Images Note: game sprites are from Lost Garden.

The engine supports the Farseer Physics engine for XNA, and the editor supports drawing polygon and rectangle collision objects (visible as magenta lines):
Level Editor Collisions

I wrote a custom processor to get the level into the game engine, so I only have to save the level in the editor and add it to the Content project in XNA Game Studio, along with the images that I’ve used:
Level Content Processor

Then I just need to add some code to the LoadContent method to load the images and create the collision objects in Farseer:

foreach (PolygonCollisionEntity poly in level.PolygonCollisionEntities)
{
    Body polyBody = BodyFactory.Instance.CreatePolygonBody(new Vertices(poly.Vertices), poly.Mass);
    polyBody.IsStatic = true;
    Geom polyGeom = GeomFactory.Instance.CreatePolygonGeom(physicsSimulator, polyBody, new Vertices(poly.Vertices), 1.0f);
} 

foreach (RectangleCollisionEntity rect in level.RectangleCollisionEntities)
{
    Body rectBody = BodyFactory.Instance.CreateRectangleBody(rect.Width, rect.Height, rect.Mass);
    rectBody.IsStatic = true;
    rectBody.Position = rect.Position;
    Geom polyGeom = GeomFactory.Instance.CreateRectangleGeom(physicsSimulator, rectBody, rect.Width, rect.Height);
} 

foreach (string textureName in level.TextureResources)
{
    Texture2D texture = Content.Load<Texture2D>(textureName);
    level.Textures.Add(textureName, texture);
}

Then I can run the level:

Level In Game

The collision objects are visible as dark green lines, and the green and white boxes form a chain, implemented in Farseer, interacting with the level.

Next up I plan on implementing the following features:

  • Modify level content processor to load textures automatically, so that they don’t need to be added manually
  • Implement a camera system to be able to create larger levels and move around them
  • Add functionality to the level editor to be able to edit objects once they have been placed
  • Implement the Properties tab using the .NET Property Grid control to be able to edit the properties of selected objects

Once that’s done, it’s on to the game itself!

Share