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
}
}
}
What this does is give a function to each button on our mouse.
- Left mouse button places a block
- Right mouse button deletes a block
- 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.
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)
}
}
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))
}
}
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
}
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)
}
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)
}
}
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
}
}
}
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]
}
}
}
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,
}
}
}
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"
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.