Creating A Level Editor (Cosplore Pt:11)
Tags: go, gamedev, learning
Published: 2024-02-13 01:40:16

## Intro
This is a series following [Cosplore3D](https://github.com/Chig-Beef/Cosplore3D), a raycaster game to learn 3D graphics. This project is part of 12 Months 12 Projects, a challenge I set myself. Making levels using a text editor is tedious, and doesn't really make sense, so in this post we will make an actual level editor to make level editing a nicer experience.

## Creating And Editing A Default Level
To start editing a level, we first need a level, which is a default [][]uint8. To edit this, we can use our mouse.
```golang
if inpututil.IsMouseButtonJustPressed(ebiten.MouseButton0) {
        col := int(math.Floor(float64(g.curMousePos[0]) / tileSize))
        row := int(math.Floor(float64(g.curMousePos[1]-160) / tileSize))

        if row < len(g.level.data) && row >= 0 {
            if col < len(g.level.data[row]) && col >= 0 {
                g.level.data[row][col] = g.curCodeSelection
            }
        }
    }
    if inpututil.IsMouseButtonJustPressed(ebiten.MouseButton1) {
        g.curCodeSelection++
    }
    if inpututil.IsMouseButtonJustPressed(ebiten.MouseButton2) {
        col := int(math.Floor(float64(g.curMousePos[0]) / tileSize))
        row := int(math.Floor(float64(g.curMousePos[1]-160) / tileSize))

        if row < len(g.level.data) && row >= 0 {
            if col < len(g.level.data[row]) && col >= 0 {
                g.level.data[row][col] = 0
            }
        }
    }
```
What this does is give a function to each button on our mouse.

1. Left mouse button places a block
2. Right mouse button deletes a block
3. Middle mouse button moves on to the next code

The code for drawing is a big switch statement, so it's not that interesting so I won't place it here, but here is the result.

![Simple level created in the editor](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/beogoi27n7o0is1ciyb3.png)

This is already looking a lot better than just text. Now I'm going to add a save button in the top bar (which is why the top of the screen is black). This is pretty easy, as we can just save `g.level.data` almost as is, with some extra commas.
```golang
func save(d [][]uint8) {
    output := ""

    for row := 0; row < len(d); row++ {
        for col := 0; col < len(d[row]); col++ {
            output += strconv.Itoa(int(d[row][col]))
            output += ","
        }
        // Get rid of extra comma
        output = output[:len(output)-1] + "\n"
    }
    // Get rid of extra newline
    output = output[:len(output)-1]

    file, err := os.Create("temp.txt")
    if err != nil {
        log.Fatal(err)
    }

    _, err = file.Write([]byte(output))
    if err != nil {
        log.Fatal(err)
    }
}
```
That's all pretty simple, and all we do is call this with a button press.

## Editing Pre-Existing Maps
It would be annoying if we had to create every map from scratch, so let's load what's in temp.txt if it exists. A lot of the function is error checking, and ends up being a lot of code, so I'll skip it in favor of this.
```golang
data := [][]uint8{}

rows := splitData[1:]
for y := 0; y < len(rows); y++ {
    data = append(data, []uint8{})
    row := strings.Split(rows[y], ",")
    for x := 0; x < len(row); x++ {
        n, err := strconv.Atoi(row[x])
        if err != nil {
            log.Println("Need integers for data")
            return Level{name, blank_level(32, 32), floorColor, skyColor}
        }
        data[y] = append(data[y], uint8(n))
    }
}
```
And this works, we have what we made in the last program showing up in this one.

## Floor And Sky Color
Now we need to implement floor and sky color, because this is really important as to whether a map looks good or not. To implement this, we're going to need some sort or text box so that we can type the value we want.

First is creating a text box.
```golang
type TextBox struct {
    text      string
    value     uint8
    x         float64
    y         float64
    w         float64
    h         float64
    bgColor   color.Color
    textcolor color.Color
    active    bool
}
```
When someone puts types in a value, we should basically delete it if it's not valid.
```golang
func (tb *TextBox) correct() {
    n, err := strconv.Atoi(tb.text)
    if err != nil {
        n = 0
    }
    if n < 0 || n > 255 {
        n = 0
    }
    tb.value = uint8(n)
}
```
We should also be able to draw the text box on screen.
```golang
func (tb *TextBox) draw(screen *ebiten.Image, g *Game) {
    ebitenutil.DrawRect(screen, tb.x, tb.y, tb.w, tb.h, tb.bgColor)

    text.Draw(screen, tb.text, g.fonts["colors"], int(tb.x), int(tb.y), tb.textcolor)

    if tb.active {
        ebitenutil.DrawLine(screen, tb.x, tb.y+tb.h, tb.x+tb.w, tb.y+tb.h, color.White)
    }
}
```
`tb.active` tells us whether we are typing in this `TextBox`. So now we need to be able to toggle `tb.active`.
```golang
func (tb *TextBox) check_click(g *Game) {
    x := g.curMousePos[0]
    y := g.curMousePos[1]

    if int(tb.x) <= x && x <= int(tb.x+tb.w) {
        if int(tb.y) <= y && y <= int(tb.y+tb.y) {
            if tb.active {
                tb.correct()
            }
            tb.active = !tb.active
        }
    }
}
```
Lastly, we want to add text to the box when we type.
```golang
unc (tb *TextBox) update() {
    if !tb.active {
        return
    }

    for i := 43; i < 53; i++ {
        if inpututil.IsKeyJustPressed(ebiten.Key(i)) {
            tb.text += strconv.Itoa(i - 43)
            return
        }
    }

    if inpututil.IsKeyJustPressed(ebiten.KeyBackspace) {
        if len(tb.text) > 0 {
            tb.text = tb.text[:len(tb.text)-1]
        }
    }
}
```
That all looks very good, so now we need to use it.
```golang
func (g *Game) create_textboxes() {
    g.textBoxes = [6]TextBox{}
    for i := 0; i < 6; i++ {
        g.textBoxes[i] = TextBox{
            "0",
            0,
            float64(i * 62),
            5,
            60,
            20,
            color.RGBA{64, 64, 64, 255},
            color.White,
            false,
        }
    }
}
```
Now when we save, we need turn what the `TextBox` has into a string to prepend to the file.
```golang
output := ""
for i := 0; i < 2; i++ {
    for j := 0; j < 3; j++ {
        output += strconv.Itoa(int(g.textBoxes[i*3+j].value)) + ","
    }
    output = output[:len(output)-1] + "|"
}
output = output[:len(output)-1] + "\n"
```
This all saves correctly, so that's perfect, we're all done! Obviously this editor is very bare, so I'll keep updating it throughout development, but this is a great start.

## Next
We're going to go back into the actual game of Cosplore3D, and we're going to start work on The Cosplorer, which is the ship that the player came from. In the "lore" we get the Cosmium to fuel the ship (if you remember from a previous post, that blue orb thing with stars). So now that we've got the Cosmium, we should create the level that is the Cosplorer.