Tile Editor – Del 2

Inledning

Dags för nästa steg. Vi kommer i denna del att utöka GUI:t och lägga till stöd för att ladda grafik, s.k. "Tile sheets". En anledning till att vi envisas med engelska termer så som "tile" och "tile sheets" är för att du lättare skall hitta mer information på nätet. Testa en gång att googl'a efter "grafik 2d spel" och du hittar inte mycket. Om du istället googl'ar efter "games 2d tiles" eller "game sprite sheets" så får du bättre resultat.

En "tile sheet" är helt enkelt en bild, oftast i formatet bmp eller png, där du har all grafik till banan samlad. Det är utifrån denna bild som banan sedan skall byggas. Ett exempel ser du nedan.

bild

TileSheetPictureBox

Vi börjar med att skapa en ny kontroll på liknande sätt vi gjorde MapPictureBox i del 1. Skapa en ny klass och döp den till TileSheetPictureBox.cs.

TileSheetPictureBox.cs

using System;
using System.ComponentModel;
using System.Data;
using System.Windows.Forms;
using System.Drawing;
using LevelEditor;

namespace LevelEditorControls
{
    class TileSheetPictureBox : PictureBox
    {
        int mouseX, mouseY;
        int selectedTileIndex = -1;

        public Map MapParent
        {
            get;
            set;
        }

        public int TileIndex
        {
            get
            {
                if (MapParent != null)
                    return (mouseX / MapParent.TileWidth)
                        + (mouseY / MapParent.TileHeight) * MapParent.MapWidth;
                else
                    return -1;
            }
        }

        public int SelectedTileIndex
        {
            get
            {
                if (Image == null)
                    return -1;
                else
                    return selectedTileIndex;
            }
            set { selectedTileIndex = value; }
        }

        public Rectangle SelectedTileRectangle
        {
            get
            {
                int x = (SelectedTileIndex % MapParent.MapWidth) * MapParent.TileWidth;
                int y = (SelectedTileIndex / MapParent.MapHeight) * MapParent.TileHeight;
                return new Rectangle(x, y, MapParent.TileWidth, MapParent.TileHeight);
            }
        }

        public TileSheetPictureBox()
        {
        }

        protected override void OnPaint(PaintEventArgs pe)
        {
            base.OnPaint(pe);

            if (MapParent != null)
            {
                pe.Graphics.FillRectangle(new SolidBrush(Color.FromArgb(128, 255, 255, 255)),
                    SelectedTileRectangle);
                pe.Graphics.DrawRectangle(Pens.Black, SelectedTileRectangle);
            }
        }

        protected override void OnMouseMove(MouseEventArgs e)
        {
            mouseX = e.X;
            mouseY = e.Y;
            base.OnMouseMove(e);
            this.Invalidate();
        }

        protected override void OnClick(EventArgs e)
        {
            base.OnClick(e);
            this.SelectedTileIndex = this.TileIndex;
        }
    }
}

Kontrollen liknar i mycket MapPictureBox då vi även här håller koll på muspekarens rörelser i kontrollen. Vid ett "Click" så beräknar vi och uppdaterar vilken "tile" som blev vald.

Egenskaperna TileIndex, SelectedTileIndex och SelectedTileRectangle innehåller beräkningar för att t.ex. räkna ut den valda rutans rektangel i kontrollen och vilket index rutan har. Indexet börjar på 0, vänstra hörnet längst upp i kontrollen, och fortsätter i riktning höger. I bilden som exempel så har sten/grus index 0, gräs index 1 och vägg/mur index 2.

SelectedTileIndex returnerar -1 om ingen ruta är vald.

OnPaint lägger till en markering i uppritningen så att vald ruta visas.

Modifiering av Map

Vi lägger in en TabControl i formuläret Map. Tanken är att en tab skall visa kartan och en tab skall visa vilka "tiles" vi har att välja på. Namnge den första fliken "Map" och den andra "Tiles". Flytta sedan in mapPictureBox i "Map" och lägg till en TileSheetPictureBox i "Tiles som du döper till tileSheetPictureBox. Bilden nedan visar designen:

bild

Ange Dock = Fill på din TabControl så att den tar all plats i fönstret. På varje TabPage (både "Map" och "Tiles") anger du AllowScroll = True.

Vi gör ändringar i koden för Map.cs enligt nedan:

Map.cs

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Windows.Forms;

namespace LevelEditor
{
    public partial class Map : Form, IMapSettings
    {
        Image mapImage, tileSheet;

        public Image TileSheet
        {
            get { return tileSheet; }
            set
            {
                tileSheet = value;
                tileSheetPictureBox.MapParent = this;
                tileSheetPictureBox.Image = tileSheet;
                tileSheetPictureBox.Width = mapImage.Width;
                tileSheetPictureBox.Height = mapImage.Height;
                tabControl.SelectedIndex = 1;
            }
        }

        public int SelectedTileIndex
        {
            get { return tileSheetPictureBox.SelectedTileIndex; }
        }

        public Rectangle SelectedTileRectangle
        {
            get { return tileSheetPictureBox.SelectedTileRectangle; }
        }

        public Map(IMapSettings source)
        {
            LevelEditor.CopySettings(source, this);
            InitializeComponent();

            mapImage = new Bitmap(MapWidth * TileWidth, MapHeight * TileHeight);
            Graphics g = Graphics.FromImage(mapImage);
            g.Clear(Color.Black);
            mapPictureBox.MapParent = this;
            mapPictureBox.Image = mapImage;
            mapPictureBox.Width = mapImage.Width;
            mapPictureBox.Height = mapImage.Height;
            mapPictureBox.Click += new EventHandler(pictureBoxMap_Click);
            mapPictureBox.MouseMove += new MouseEventHandler(pictureBoxMap_MouseMove);
        }

        private void DrawTile()
        {
            Graphics g = Graphics.FromImage(mapImage);

            if (TileSheet != null && SelectedTileIndex != -1)
            {
                Rectangle dest = new Rectangle(mapPictureBox.TilePixelPosition.X, mapPictureBox.TilePixelPosition.Y,
                    TileWidth, TileHeight);
                g.DrawImage(TileSheet, dest, tileSheetPictureBox.SelectedTileRectangle, GraphicsUnit.Pixel);
            }
        }

        #region mapPictureBox Events

        void pictureBoxMap_MouseMove(object sender, MouseEventArgs e)
        {
            if (e.Button == MouseButtons.Left)
            {
                DrawTile();
            }
        }

        void pictureBoxMap_Click(object sender, EventArgs e)
        {
            DrawTile();
        }

        #endregion

        #region IMapSettings Members

        public int MapWidth
        {
            get;
            set;
        }

        public int MapHeight
        {
            get;
            set;
        }

        public int TileWidth
        {
            get;
            set;
        }

        public int TileHeight
        {
            get;
            set;
        }

        #endregion
    }
}

Map behöver en ny Image för grafiken till "tiles". Vi ändrar här mapImage från att ha varit Bitmap till att vara Image vilket är en mer generell klass. Vi skapar också en property, TileSheet, så att vi kan uppdatera tileSheetPictureBox med rätt bredd, höjd och bild.

Vi inför egenskaperna SelectedTileIndex och SelectedTileRectangle som skickar vidare information från tileSheetPictureBox.

DrawTile uppdaterar nu kartan med riktig grafik från vår "tile sheet".

Det som återstår nu är GUI och kod för att ladda en "tile sheet". Vi skall även göra ändringar i MapPictureBox för att byta ut den röda pricken mot grafik från vår "tile sheet".

Ändringar i LevelEditor

Vi behöver en möjlighet att ladda "tile sheets" i programmet. Enklast är att lägga till en ToolStripMenuItem, namngiven som loadTilesheetToolStripMenuItem, i menyn under "File". Vi kan sedan fånga "Click"-eventet, visa en fildialogruta, och försöka läsa in bilden som valts.

bild

Vi passar på att sätta Enabled = False på följande grafiska element:

  • loadTilesheetToolStripMenuItem
  • saveToolStripMenuItem
  • saveAsToolStripMenuItem
  • saveToolStripButton

Anledning till detta är att dessa element skall inte gå att använda om inte vi har en bana (MDI fönster) öppnad och markerad. Hur vet vi då vilket fönster vi eventuellt har? Jo, det finns en property i Form som heter ActiveMdiChild. ActiveMdiChild är antingen null eller så refererar den till det aktiva fönster som då finns. Det finns som tur är en event också som inträffar då det aktiva fönstret ändras eller stängs. Denna event heter MdiChildActive och den skall vi binda till vår LevelEditor.

LevelEditor.cs - Tillägg

#region MDI events

private void LevelEditor_MdiChildActivate(object sender, EventArgs e)
{
	bool enableButtons = (this.ActiveMdiChild == null) ? false : true;
	saveToolStripButton.Enabled = enableButtons;
	saveToolStripMenuItem.Enabled = enableButtons;
	saveAsToolStripMenuItem.Enabled = enableButtons;
	loadTilesheetToolStripMenuItem.Enabled = enableButtons;
}

#endregion

private void loadTilesheetToolStripMenuItem_Click(object sender, EventArgs e)
{
	OpenFileDialog openFileDialog = new OpenFileDialog();
	openFileDialog.InitialDirectory = Environment.GetFolderPath(Environment.SpecialFolder.Personal);
	openFileDialog.Filter = "BMP Files (*.bmp)|*.bmp|PNG Files (*.png)|*.png|All Files (*.*)|*.*";
	if (openFileDialog.ShowDialog(this) == DialogResult.OK)
	{
		string FileName = openFileDialog.FileName;
		try
		{
			Map map = this.ActiveMdiChild as Map;
			map.TileSheet = Bitmap.FromFile(FileName);
		}
		catch (Exception ex)
		{
			MessageBox.Show(ex.Message);
		}
	}
}

I LevelEditor_MdiChildActivate aktiverar vi de delar av GUI:t som kräver att det finns ett öppnat fönster.

I loadTilesheetToolStripMenuItem_Click öppnar vi en fildialogruta för BMP och PNG filer. Väljer vi en fil och trycker OK så försöker vi att ladda en bild och sätta den som vår aktuella TileSheet.

Nu skall det gå bra att köra programmet, skapa en ny bana, ladda en valfri "tile sheet" och börja rita en bana. Kvar återstår endast några grafiska förbättringar i MapPictureBox.

Ändringar i MapPictureBox

MapPictureBox.cs - OnPaint

protected override void OnPaint(PaintEventArgs pe)
{
	base.OnPaint(pe);

	if (drawTile && MapParent != null && MapParent.SelectedTileIndex != -1)
	{
		Rectangle dest = new Rectangle(TilePixelPosition.X, TilePixelPosition.Y,
			MapParent.TileWidth, MapParent.TileHeight);
		Rectangle src = MapParent.SelectedTileRectangle;

		ColorMatrix cm = new ColorMatrix();
		cm.Matrix00 = cm.Matrix11 = cm.Matrix22 = cm.Matrix44 = 1;
		cm.Matrix33 = 0.5f;
		ImageAttributes ia = new ImageAttributes();
		ia.SetColorMatrix(cm);
		pe.Graphics.DrawImage(MapParent.TileSheet, dest, 
								src.X, src.Y, src.Width, src.Height, GraphicsUnit.Pixel, ia);
	}
}

Vi ändrar den tidigare röda pricken som visade var muspekaren var mot den "tile" som verkligen kommer att ritas ut. Vi använder oss av en ColorMatrix för att ändra alpha och på så sätt få en transparent visning av den "tile" som vi har valt.

Avslutning

Skalet till programmet börjar bli klart. Ännu har vi inte presenterat vilken data som skall sparas och hur den skall sparas. En bana kommer att bestå av "tiles", deras placering och ev. egenskaper samt kartans inställningar (storlek etc.). Nästa artikel kommer att avsluta serien och presentera en strategi för att spara kartan som XML + bilder.

Nedan kan du som vanligt ladda hem projektet i sin helhet.

bild

Lämna ett svar

Din e-postadress kommer inte publiceras.

Scroll to top