Points of Required Attention™
Please chime in on a proposed restructuring of the ROM hacking sections.
Views: 88,442,991
Main | FAQ | Uploader | IRC chat | Radio | Memberlist | Active users | Latest posts | Calendar | Stats | Online users | Search 04-20-24 05:21 PM
Guest: Register | Login

0 users currently in ROM Hacking Archives | 1 guest

Main - ROM Hacking Archives - Kawa's Map Editor Creation Tutorial New thread | New reply

Pages: 1 2 3

Kawa
Posted on 05-19-09 07:09 PM Link | Quote | ID: 107261


CHIKKN NI A BAAZZKIT!!!
80's Cheerilee is best pony
Level: 138

Posts: 1837/5344
EXP: 30930732
Next: 732249

Since: 02-20-07
From: The Netherlands

Last post: 4492 days
Last view: 2627 days

Part 1


First of all, we'll need a form with a tile selection area and a map editing area. Both should be able to scroll freely. The easiest way to do this is by having two AutoScroll-enabled Panels containing two AutoSizing PictureBoxes. Place the two Panels on a new Form, enable AutoScroll for both and position them nicely. Then, add a PictureBox in each, in the top-left corner and set to AutoSize. The tileset panel should be as wide as the tileset (128px in this example) plus some margin for the scroll bar but frankly, we can afford to play it loose for now.

Interjection: I showed the draft for this tutorial to Cearn and he insists on using constants for tile sizes. I've decided to compromise -- this tutorial will use size constants, but I'm not updating the final source code archive. So whenever I say "128 pixels wide", that should be read as "eight times the tile size", id est "8 * 16".



Now that the UI is in place, we can go over the code. Now, I like to use backbuffers, so we'll go with that. Define two Bitmap and two Graphics objects.


public partial class Tutorial : Form
{
-> private const int TileSize = 16;
-> Bitmap TilesetBackbuffer, MapBackbuffer;
-> Graphics TilesetGraphics, MapGraphics;


Next up is loading and displaying the tileset. We'll go with a slightly cut Advance Wars tileset, but anything goes, really. In this example, as announced before, the tileset should be 128px wide, most any height. This maps to eight tiles a row. There's some math involved here that we'll get to later. Also note that you can't pull a Graphics object from indexed Bitmaps, so don't just try that.


public Tutorial()
{
InitializeComponent();
-> TilesetBackbuffer = new Bitmap("tileset.png");
-> TilesetGraphics = Graphics.FromImage(TilesetBackbuffer);
-> tilesetPictureBox.Image = TilesetBackbuffer;
}


Now, to select a tile from the tileset, we must remember which tile index was selected. Add a CurrentTile variable to do this, and add the following event handler:


Graphics TilesetGraphics, MapGraphics;
-> int CurrentTile;


private void tilesetPictureBox_MouseClick(object sender, MouseEventArgs e)
{
// Note: eight tiles per row in the tileset.
CurrentTile = ((e.Y / TileSize) * 8) + (e.X / TileSize);
this.Text = "CurrentTile: " + CurrentTile.ToString();
}


I hear you ask, "Kawa, when the hell are we getting to the mapping part?". Well, how 'bout right now?

First of all, we'll need something to store the current map in. For readability's sake, we'll go with a 2D int array.


int CurrentTile;
-> int[,] Map;
-> int MapWidth, MapHeight;

tilesetPictureBox.Image = TilesetBackbuffer;
-> MapWidth = 10;
-> MapHeight = 10;
-> Map = new int[MapWidth, MapHeight];
-> MapBackbuffer = new Bitmap(MapWidth * TileSize, MapHeight * TileSize);
-> MapGraphics = Graphics.FromImage(MapBackbuffer);
-> mapPictureBox.Image = MapBackbuffer;


Next, we'll need something to draw the full map. This only has to happen under few circumstances, such as on load. For any subsequent change, drawing single tiles will suffice. We'll tackle both these tasks in one go.


private void DrawTile(int x, int y, int tileNo)
{
Rectangle sourceRect = new Rectangle((tileNo % 8) * TileSize, (tileNo / 8) * TileSize, TileSize, TileSize);
Rectangle destRect = new Rectangle(x * TileSize, y * TileSize, TileSize, TileSize);
MapGraphics.DrawImage(TilesetBackbuffer, destRect, sourceRect, GraphicsUnit.Pixel);
}

private void DrawFullMap()
{
for (int x = 0; x < MapWidth; x++)
for (int y = 0; y < MapHeight; y++)
DrawTile(x, y, Map[x, y]);
}


mapPictureBox.Image = MapBackbuffer;
-> DrawFullMap();


You should have a black map now, since the Map array defaults to all zero, which is a black tile. So let's get to the fun part!


private void mapPictureBox_MouseMove(object sender, MouseEventArgs e)
{
int x = e.X / TileSize;
int y = e.Y / TileSize;

if (x < 0 || x >= MapWidth || y < 0 || y >= MapHeight)
return;

if (e.Button == MouseButtons.Left)
{
Map[x, y] = CurrentTile;
DrawTile(x, y, CurrentTile);
mapPictureBox.Refresh();
}
}


Hook this up to both the MouseMove and MouseDown events for mapPictureBox. Why both? Because if you only hook it to MouseMove, you have to jiggle the cursor every time you start drawing and if you only hook it to MouseDown, you can't do strokes.

That's it. You now have a simple map editing system going. Next time, we'll tackle loading and saving, probably followed by loading the assets from ROM.


Download the finished app for this part

____________________
Wife make lunch - Shampoo
Opera - give it a spin
Spare some of your free time?
<GreyMaria> I walked around the Lake so many goddamn times that my sex drive was brutally murdered
Kawa rocks — byuu

Arbe
Posted on 05-20-09 10:10 PM Link | Quote | ID: 107332

go away
Level: 86

Posts: 1631/1788
EXP: 5984604
Next: 157503

Since: 02-23-07

Last post: 4963 days
Last view: 1532 days
I can hear the voices of 20 years worth of dead and gone rom hacking souls shouting "FINALLY" in the dark. Awesome that you're putting in the time.

Xenesis
Posted on 05-21-09 03:02 AM Link | Quote | ID: 107363


Level: 46

Posts: 235/416
EXP: 671808
Next: 39966

Since: 02-20-07

Last post: 4379 days
Last view: 3087 days
:o

:o

:o

Yes.

*goes to attempt to do this himself as well*

Arbe
Posted on 05-21-09 11:53 AM Link | Quote | ID: 107381

go away
Level: 86

Posts: 1636/1788
EXP: 5984604
Next: 157503

Since: 02-23-07

Last post: 4963 days
Last view: 1532 days
Suggestion this is moved to Rom Hacking at some point and maybe stickied?

Kawa
Posted on 05-21-09 06:15 PM Link | Quote | ID: 107387


CHIKKN NI A BAAZZKIT!!!
80's Cheerilee is best pony
Level: 138

Posts: 1843/5344
EXP: 30930732
Next: 732249

Since: 02-20-07
From: The Netherlands

Last post: 4492 days
Last view: 2627 days
I had the same idea.

So what should be in Part 2, people?

____________________
Wife make lunch - Shampoo
Opera - give it a spin
Spare some of your free time?
<GreyMaria> I walked around the Lake so many goddamn times that my sex drive was brutally murdered
Kawa rocks — byuu

MathOnNapkins
Posted on 05-21-09 07:55 PM (rev. 2 of 05-21-09 07:56 PM) Link | Quote | ID: 107396


Super Koopa
Level: 62

Posts: 652/842
EXP: 1934625
Next: 50061

Since: 02-19-07
From: durff

Last post: 4482 days
Last view: 4005 days
Undo / Redo would be boss.

____________________
Zelda Hacking Forum
hobbies: delectatio morosa

Kawa
Posted on 05-21-09 08:01 PM Link | Quote | ID: 107399


CHIKKN NI A BAAZZKIT!!!
80's Cheerilee is best pony
Level: 138

Posts: 1847/5344
EXP: 30930732
Next: 732249

Since: 02-20-07
From: The Netherlands

Last post: 4492 days
Last view: 2627 days
I should do that for the OpenPoké map editor first so I know what to do. Redrawing the whole map takes some time, so I think keeping a list of changes per stroke would be a nice starting point.

____________________
Wife make lunch - Shampoo
Opera - give it a spin
Spare some of your free time?
<GreyMaria> I walked around the Lake so many goddamn times that my sex drive was brutally murdered
Kawa rocks — byuu

Arbe
Posted on 05-21-09 09:41 PM Link | Quote | ID: 107406

go away
Level: 86

Posts: 1638/1788
EXP: 5984604
Next: 157503

Since: 02-23-07

Last post: 4963 days
Last view: 1532 days
RIBBON MENUS!!

Kawa
Posted on 05-21-09 09:43 PM Link | Quote | ID: 107407


CHIKKN NI A BAAZZKIT!!!
80's Cheerilee is best pony
Level: 138

Posts: 1849/5344
EXP: 30930732
Next: 732249

Since: 02-20-07
From: The Netherlands

Last post: 4492 days
Last view: 2627 days
Screw that, that's just an interface paradigm choice. A simple tutorial map editor wouldn't ever reach the point where menus and toolbars are insufficient.

____________________
Wife make lunch - Shampoo
Opera - give it a spin
Spare some of your free time?
<GreyMaria> I walked around the Lake so many goddamn times that my sex drive was brutally murdered
Kawa rocks — byuu

Arbe
Posted on 05-21-09 09:44 PM Link | Quote | ID: 107408

go away
Level: 86

Posts: 1639/1788
EXP: 5984604
Next: 157503

Since: 02-23-07

Last post: 4963 days
Last view: 1532 days
wait hang on for some reason i thought we were suggesting shit for openpoke editor

uh, events? lol

Kawa
Posted on 05-21-09 09:59 PM Link | Quote | ID: 107410


CHIKKN NI A BAAZZKIT!!!
80's Cheerilee is best pony
Level: 138

Posts: 1850/5344
EXP: 30930732
Next: 732249

Since: 02-20-07
From: The Netherlands

Last post: 4492 days
Last view: 2627 days
Events are a good topic to cover at any rate, and I might go onto it in more detail later.


For now, here's a quickie take on it:

Intermission -- Events

  1. Define an Event class. Specific kinds of events can be subclassed from this base class.
  2. It should have at the very least a property to track the event's location on the map. Point or pair of ints, doesn't really matter here.
  3. Add to the map editor a List<Event> variable to store them in.
  4. Add an EventBackbuffer and EventGraphics object. This will be used to draw the events on, in a transparant image.
  5. Add FinalBackbuffer and FinalGraphics objects as well. The basic idea is to draw the map and events on seperate layers, then simply merge them onto the final layer and show that. Makes changing their contents faster, since you don't have to worry about redrawing the one when you change the other.
  6. Add a "currently holding" variable of the Event type.
  7. On mousedown, set "currently holding" to Null and iterate through the events list, comparing coordinates to the cursor's. If you find one, set the currently holding var to that Event and stop. I suggest using an "event mode" bool here.
  8. On mousemove, if the currently holding var is not null, set its' position to the cursor coordinates and call your Event Redraw function.

The graphics should run something like this:
On load, redraw the entire map.
After redrawing the entire map, call the event drawer.
After drawing the events, call the final merger.
The final merger simply blits the map and events to the final buffer in that order and calls picMap.Refresh() as before.
On drawing a tile, blit that single tile to the map buffer as before, then call the merger.
On moving an event, call the event drawer, which in turn calls the merger.
The event drawer just iterates and draws rectangles or whatever.

My fingers hurt now.

____________________
Wife make lunch - Shampoo
Opera - give it a spin
Spare some of your free time?
<GreyMaria> I walked around the Lake so many goddamn times that my sex drive was brutally murdered
Kawa rocks — byuu

Bukkarooo
Posted on 05-22-09 12:56 AM Link | Quote | ID: 107416


Fuzzy
Son of a bitch, I'm sick of these dolphins...
Level: 59

Posts: 406/778
EXP: 1633545
Next: 39583

Since: 10-15-08
From: Florida

Last post: 5178 days
Last view: 4945 days




Posted by Arbe
Suggestion this is moved to Rom Hacking at some point and maybe stickied?


done, as this will now be helpful to a lot more people

____________________



Layout made by Stark.





Kawa
Posted on 05-22-09 04:04 PM Link | Quote | ID: 107436


CHIKKN NI A BAAZZKIT!!!
80's Cheerilee is best pony
Level: 138

Posts: 1851/5344
EXP: 30930732
Next: 732249

Since: 02-20-07
From: The Netherlands

Last post: 4492 days
Last view: 2627 days
I've thought about undo buffers some more and decided on a hybrid approach. I'll implement it in the OpenPoké map editor, then write up Part 2 of the tutorial. Is that okay?

____________________
Wife make lunch - Shampoo
Opera - give it a spin
Spare some of your free time?
<GreyMaria> I walked around the Lake so many goddamn times that my sex drive was brutally murdered
Kawa rocks — byuu

MathOnNapkins
Posted on 05-22-09 04:34 PM Link | Quote | ID: 107442


Super Koopa
Level: 62

Posts: 655/842
EXP: 1934625
Next: 50061

Since: 02-19-07
From: durff

Last post: 4482 days
Last view: 4005 days
Fine by me :/. I'm doing some independent research of my own on the subject anyways.

____________________
Zelda Hacking Forum
hobbies: delectatio morosa

Kawa
Posted on 05-23-09 12:06 PM Link | Quote | ID: 107482


CHIKKN NI A BAAZZKIT!!!
80's Cheerilee is best pony
Level: 138

Posts: 1856/5344
EXP: 30930732
Next: 732249

Since: 02-20-07
From: The Netherlands

Last post: 4492 days
Last view: 2627 days

Part 2 - Undo Buffers


To undo changes, we'll need something to track them in. Since redrawing the entire map can be slow (depending on the map's size ofcourse), we'll basically take screenshots of each operation, before and after. These will be part of each undo state. Not to be too wasteful, we'll not save copies of the entire map array.
	//One single change of the map
class TileChange
{
public int X { get; set; }
public int Y { get; set; }
public int From { get; set; }
public int To { get; set; }
public TileChange(int x, int y, int from, int to)
{
X = x;
Y = y;
From = from;
To = to;
}
}

//One "brush stroke" worth of edits, from mousedown to mouseup.
class UndoState
{
public Bitmap Before { get; set; }
public Bitmap After { get; set; }
public List<TileChange> Changes { get; set; }
public UndoState()
{
Changes = new List<TileChange>();
}
We need "before" and "after" to implement Redo, by the way. If you don't want Redo (why?) you can save a lot of space by removing UndoState.After and TileChange.To. Now, any way, we need some functions to use these things. But first, add two stacks of UndoStates to your editor.
	Stack<UndoState> UndoBuffer = new Stack<UndoState>();
Stack<UndoState> RedoBuffer = new Stack<UndoState>();
The Undo stack will continuously fill with changes. When you undo, we pop the last one, apply it, then push it on the Redo stack, which is cleared every time we change something. Get it so far? Now for those functions...
		public void StartUndo()
{
RedoBuffer.Clear();
NewUndo = new UndoState();
NewUndo.Before = (Bitmap)MapBackbuffer.Clone();
}
It seems fairly obvious what this one does...
		public void AddTileUndo(int x, int y, int from, int to)
{
NewUndo.Changes.Add(new TileChange(x, y, from, to));
}
As does this one...
		public void CommitUndo()
{
NewUndo.After = (Bitmap)MapBackBuffer.Clone();
UndoBuffer.Push(NewUndo);
}
Take another screenshot and push the result...
		public void Undo()
{
if (UndoBuffer.Count == 0)
return;
UndoState ThisUndo = UndoBuffer.Pop();
ApplyUndo(ThisUndo, false);
RedoBuffer.Push(ThisUndo);
mapPictureBox.Refresh();
}

This one's important, it gets extra spacing:
		private void ApplyUndo(UndoState buffer, bool redo)
{
MapGraphics.DrawImageUnscaled(redo ? buffer.After : buffer.Before, 0, 0);
foreach (TileChange c in buffer.Changes)
{
int x = c.X;
int y = c.Y;
int tile = redo ? c.To : c.From;
if (x < 0 || x >= MapWidth || y < 0 || y >= MapHeight)
continue;
Map[x, y] = tile;
}
}
You can see that if redo is True, we use the "after" data instead. With this information, I guess I can leave the Redo function a challenge for the reader

Now, let's hook this system up.
  • Unhook the mousedown event handler and make a new one. It should check for pressing the left mouse button. If so, call StartUndo, then call mapPictureBox_MouseMove with the same sender and eventargs.
  • In the mousemove event handler, before making the actual changes to the map data, call AddTileUndo(x, y, Map[x, y], CurrentTile).
  • Add a mouseup event, again checking for the left mouse button. If so, call CommitUndo.
  • Add two controls (menu items preferred) to call Undo and Redo with. For added bonus, you can disable them easily with, for example, undoToolStripMenuItem.Enabled = (UndoBuffer.Count > 0);


____________________
Wife make lunch - Shampoo
Opera - give it a spin
Spare some of your free time?
<GreyMaria> I walked around the Lake so many goddamn times that my sex drive was brutally murdered
Kawa rocks — byuu

DarkPhoenix
Posted on 05-29-09 02:40 AM Link | Quote | ID: 107756


Micro-Goomba
Level: 12

Posts: 14/19
EXP: 6508
Next: 1413

Since: 05-24-07

Last post: 5294 days
Last view: 5281 days
Just a thought - rather than using separate stacks for undo and redo, you could use a single list, with a pointer to the current position. FireFox 3's Forward/Back buttons provide a good visual of this approach.

For reference, since it wasn't mentioned, the book "Design Patterns" (or alternatively, the Wikipedia Article) has a collection of the industry standard solutions for common concepts like Undo/Redo, with diagrams and all that jazz. See "Command" and "Momento".

Kawa
Posted on 05-29-09 06:29 PM Link | Quote | ID: 107777


CHIKKN NI A BAAZZKIT!!!
80's Cheerilee is best pony
Level: 138

Posts: 1871/5344
EXP: 30930732
Next: 732249

Since: 02-20-07
From: The Netherlands

Last post: 4492 days
Last view: 2627 days
I just found the two stacks approach easier to implement on short notice. At least it works as designed, right? And it made sense when I visualized it while telling my friends about it.

I may look into that book/article later, it sounds like an interesting read.


Also, not to drift off-topic but what makes Fx3's forward/back buttons different from other browsers'? If you could tell me that in a PM...

____________________
Wife make lunch - Shampoo
Opera - give it a spin
Spare some of your free time?
<GreyMaria> I walked around the Lake so many goddamn times that my sex drive was brutally murdered
Kawa rocks — byuu

Kawa
Posted on 06-12-09 08:30 PM Link | Quote | ID: 108537


CHIKKN NI A BAAZZKIT!!!
80's Cheerilee is best pony
Level: 138

Posts: 1939/5344
EXP: 30930732
Next: 732249

Since: 02-20-07
From: The Netherlands

Last post: 4492 days
Last view: 2627 days

Part 3 - File IO

For demonstration purposes, we'll go with seperate files instead of diving directly into ROMs. Just bear with me, okay?

To load and save maps, add a menu bar or toolbar or what have you, with two options at the very least: Open and Save. Also add, from the Dialogs category, open and save dialog controls. In the click event for Open, put this:
			if (openFileDialog.ShowDialog() == DialogResult.OK)
{
BinaryReader mapFile = new BinaryReader(File.Open(openFileDialog.FileName, FileMode.Open));

//Using ints for all this stuff makes all the values 32 bits.
MapWidth = mapFile.ReadInt32();
MapHeight = mapFile.ReadInt32();
Map = new int[MapWidth, MapHeight];

//We use a 2D array of something that's not bytes, so we have to take the long way home.
for (int x = 0; x < MapWidth; x++)
for (int y = 0; y < MapHeight; y++)
Map[x, y] = mapFile.ReadInt32();

MakeBackbuffers();
}

This would mean that a map file in this demonstration is a series of only 32-bit values. As the comment implies, if we used a byte array for our map data we could just use File.ReadBytes(MapWidth * MapHeight) and call it a day.

Notice the new function "MakeBackbuffers"? This is it, basically:
		private void MakeBackbuffers()
{
MapBackbuffer = new Bitmap(MapWidth * TileWidth, MapHeight * TileHeight);
MapGraphics = Graphics.FromImage(MapBackbuffer);
mapPictureBox.Image = MapBackbuffer;
DrawFullMap();
}

Replace the part below "Map = new int[MapWidth, MapHeight];" in the form's constructor with a call to MakeBackbuffers as well.

Saving is pretty much the exact opposite deal as with loading, with the added bonus that you don't need to redraw the map, since it hasn't changed. I'm leaving that as an exercise for the reader.

How to speed up the first drawing of the map

You may have noticed that when first loading a map, it may take a notable amount of time to draw it all. I sure did while working on the OpenPoké editor, and my current project.

Believe it or not, replacing the DrawImage() call in the DrawFullMap function with a simple Get/SetPixel loop makes it much faster, and that's not even the limit!
			int srcX, srcY, dstX, dstY;
for (int y = 0; y < mapHeight; y++)
{
dstY = y * 16;
for (int x = 0; x < mapWidth; x++)
{
ushort rt = br.ReadUInt16();
ushort tile = (ushort)(rt & 0x3FF);
srcX = (tile % 8) * 16;
srcY = (tile / 8) * 16;
dstX = x * 16;
for (int px = 0; px < 16; px++)
for (int py = 0; py < 16; py++)
mapBackBuffer.SetPixel(dstX + px, dstY + py, TilesetBackbuffer.GetPixel(srcX + px, srcY + py));
}
}


Sample code



http://acmlm.no-ip.org/uploader/get.php?id=1569
Does NOT include the optimization described just now, nor does it contain the undo/redo thing. I wrote it way before I came up with either of those. Consider it a challenge.

____________________
Wife make lunch - Shampoo
Opera - give it a spin
Spare some of your free time?
<GreyMaria> I walked around the Lake so many goddamn times that my sex drive was brutally murdered
Kawa rocks — byuu

Coby
Posted on 06-15-09 06:15 PM Link | Quote | ID: 108724


Red Paragoomba
Level: 19

Posts: 52/53
EXP: 30530
Next: 5247

Since: 02-28-07
From: Belgium

Last post: 5422 days
Last view: 4176 days
Hey Kawa, remember me?
I must congratulate you on this splendid tutorial (Jij gekke Hollander jij)
This finally gives me an idea on how to start making level editors after all these years :p

Anyhow, I'm trying to port your code to VB.net (because we didn't learn C# yet, I know OO-Java and it looks like the syntax is about the same but fuck it :p) because I thought it would be a nice way of flexing my vb skillz (although most of the code is the same, spare for the syntax difference).

So, everything works fine up until now, just whenever I try to call the function drawTile(x,y,map(x,y)) VB gives a System.NullreferenceException.

Note that when I just call drawTile(x,y,0) then it goes fine


Public Sub drawTile(ByVal x As Integer, ByVal y As Integer, ByVal tileno As Integer)

That's how I declared the drawTile sub, and I just call it like

For x As Integer = 0 To mapWidth
For y As Integer = 0 To mapHeight
Try
drawTile(x, y, map(x, y))
Catch ex As Exception
MessageBox.Show(ex.ToString)
End Try

Next
Next

Don't mind the try-catch.

Anyhow, do you (or anyone else) have any idea why it's going fubar on me? Am I missing something really stupid or am I doing something totally wrong? :o


P.S. Sorry for just barging in like that after all these years :p
P.P.S. For those who don't know or remember me, I used to own the ExGFX Workshop

Kawa
Posted on 06-15-09 06:24 PM Link | Quote | ID: 108727


CHIKKN NI A BAAZZKIT!!!
80's Cheerilee is best pony
Level: 138

Posts: 1958/5344
EXP: 30930732
Next: 732249

Since: 02-20-07
From: The Netherlands

Last post: 4492 days
Last view: 2627 days
You forgot to initialize map[,]. Make sure that you do Map = new int[MapWidth, MapHeight]; too.

That was from the C# code, just in case. I'll leave the VB translation to you.

____________________
Wife make lunch - Shampoo
Opera - give it a spin
Spare some of your free time?
<GreyMaria> I walked around the Lake so many goddamn times that my sex drive was brutally murdered
Kawa rocks — byuu
Pages: 1 2 3


Main - ROM Hacking Archives - Kawa's Map Editor Creation Tutorial New thread | New reply

Acmlmboard 2.1+4δ (2023-01-15)
© 2005-2023 Acmlm, blackhole89, Xkeeper et al.

Page rendered in 0.028 seconds. (332KB of memory used)
MySQL - queries: 57, rows: 85/86, time: 0.017 seconds.