Let's Make a Map Editor Pt. 2
Contents
Checkout the previous post
Previous commit: 0b0527d
Performance issue
In the previous code session, I was creating grid lines on every render. This is causing the app to run unnecessarily slow.
We can fix this by splitting the creation of the lines from the drawing of the lines. e.g.
func initGrid() []Line {
var lines []Line
for n := 0.0; n <= ngridLine; n++ {
horizontal := NewLine(canvasWidth, 1, 0, n*tileSize)
vertical := NewLine(1, canvasHeight, n*tileSize, 0)
lines = append(lines, *horizontal)
lines = append(lines, *vertical)
}
return lines
}
func drawGrid(lines []Line) {
for _, g := range lines {
g.Draw(canvas)
}
}
We then call the initGrid function up front and the drawGrid function on every render (i.e. in the main Draw function).
There's now a slight issue that it takes a while to load and it doesn't tell the user that it's loading/initializing. I could process these grid lines more concurrently to mitigate this or just add a "loading…" message to communicate to the user that the app isn't broken. I will deal this later.
Panning issue
The function that managed user input for panning the tile map was having an issue that would make the tile map reset to origin on first mouse button press.
The fix for this was to relatively adjust the offset and to update the start position always (even if the mouse button isn't pressed).
func updateGrid() {
mouseX, mouseY := ebiten.CursorPosition()
if ebiten.IsMouseButtonPressed(ebiten.MouseButtonMiddle) {
offsetX = offsetX + (float64(mouseX) - float64(startX))
offsetY = offsetY + (float64(mouseY) - float64(startY))
}
startX, startY = ebiten.CursorPosition()
}
Select a Tile on the Grid
Get Mouse Position on Click
func canvasClick() {
if ebiten.IsMouseButtonPressed(ebiten.MouseButtonLeft) {
mouseX, mouseY := ebiten.CursorPosition()
fmt.Println(mouseX, mouseY)
}
}
Compare it to the Offset
We need to do some math here to determine if the click happens on a tile.
func getTileIndex(mouseX, mouseY int) (int, int) {
mapX := mouseX - int(offsetX)
mapY := mouseY - int(offsetY)
return mapX / tileSize, mapY / tileSize
}
func canvasClick() {
if ebiten.IsMouseButtonPressed(ebiten.MouseButtonLeft) {
tileX, tileY := getTileIndex(ebiten.CursorPosition())
fmt.Println(tileX, tileY)
}
}
I think that will work for now.
Stamp Something on the Selected Tile
Let's start by stamping the clicked tile with an empty(white) image.
func getTileIndex(mouseX, mouseY int) (int, int) {
mapX := mouseX - int(offsetX)
mapY := mouseY - int(offsetY)
return mapX / tileSize, mapY / tileSize
}
func canvasClick() {
if inpututil.IsMouseButtonJustPressed(ebiten.MouseButtonLeft) {
tileX, tileY := getTileIndex(ebiten.CursorPosition())
coords := coordinates{
x: tileX * tileSize,
y: tileY * tileSize,
}
fmt.Println(coords.x, coords.y)
if worldTiles[coords] != nil {
delete(worldTiles, coords)
return
}
worldTiles[coords] = ebiten.NewImage(tileSize, tileSize)
}
}
func drawMap(canvas *ebiten.Image) {
for coords, tile := range worldTiles {
op := &ebiten.DrawImageOptions{}
tile.Fill(color.White)
op.GeoM.Translate(float64(coords.x), float64(coords.y))
canvas.DrawImage(tile, op)
}
}
worldTiles is a map that's indexed by coordinates. Each coordinate can contain an Image.
worldTiles map[coordinates]*ebiten.Image
func init() {
...
worldTiles = make(map[coordinates]*ebiten.Image)
}
Blocking Clicks
There's an issue now that allows you click in the area that the sprite sheet renders. To block this, we will update our function to ignore these clicks.
mouseX, mouseY := ebiten.CursorPosition()
if mouseY > screenHeight-spriteSheetHeight {
return
}
tileX, tileY := getTileIndex(mouseX, mouseY)
Select a tile in the Sprite Sheet
Now let's see if we can click the sprite sheet to select which tile we want to paint on the canvas.
This is very similar to registering a click in the world map. We just need to apply an offset.
func getSpriteIndex(mouseX, mouseY int) (int, int) {
mapX := mouseX
mapY := mouseY - (screenHeight - spriteSheetHeight)
return mapX / (spriteSize), mapY / (spriteSize)
}
func spriteSheetClick() {
x, y := getSpriteIndex(ebiten.CursorPosition())
selectedSpriteCoords.x = x
selectedSpriteCoords.y = y
spriteCursorOp.GeoM.Reset()
spriteCursorOp.GeoM.Translate(float64(selectedSpriteCoords.x*spriteSize), float64(selectedSpriteCoords.y*spriteSize))
}
Stamp a tile from the Sprite Sheet on the Grid
Now we can update the white square that we were stamping on the tilemap to a subimage of the spritesheet.
worldTiles[coords] = ebiten.NewImageFromImage(loadedSpriteSheet.SubImage(image.Rectangle{
image.Point{X: selectedSpriteCoords.x * spriteSize, Y: selectedSpriteCoords.y * spriteSize},
image.Point{X: (selectedSpriteCoords.x * spriteSize) + spriteSize, Y: (selectedSpriteCoords.y * spriteSize) + spriteSize},
}))
Nice, we are now adding a subimage of the spritesheet to the worldmap. However, it's too small. We happen to know that the world map is scaled up by 2. So, we can scale our subimage up by 2 as well.
op.GeoM.Scale(2, 2)
We will need to revisit this if we decide to allows sprite sheets of different sizes.
Continuing
Sweet!
Now we can stamp subimages from the sprite sheet on the map.
Where do we go from here?
There are several features I can think to add.
- Save map as PNG
- embed map metadata in png
- add a bool to make a tile collidable
- map layers
- undo/redo
commit 47ad357d37