Creating A Level Editor (Cosplore Pt:11)

Chig Beef - Feb 13 - - Dev Community

Intro

This is a series following 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.

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
            }
        }
    }
Enter fullscreen mode Exit fullscreen mode

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

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.

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)
    }
}
Enter fullscreen mode Exit fullscreen mode

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.

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))
    }
}
Enter fullscreen mode Exit fullscreen mode

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.

type TextBox struct {
    text      string
    value     uint8
    x         float64
    y         float64
    w         float64
    h         float64
    bgColor   color.Color
    textcolor color.Color
    active    bool
}
Enter fullscreen mode Exit fullscreen mode

When someone puts types in a value, we should basically delete it if it's not valid.

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)
}
Enter fullscreen mode Exit fullscreen mode

We should also be able to draw the text box on screen.

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)
    }
}
Enter fullscreen mode Exit fullscreen mode

tb.active tells us whether we are typing in this TextBox. So now we need to be able to toggle tb.active.

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
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

Lastly, we want to add text to the box when we type.

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]
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

That all looks very good, so now we need to use it.

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,
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

Now when we save, we need turn what the TextBox has into a string to prepend to the file.

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"
Enter fullscreen mode Exit fullscreen mode

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.

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .