© The Author(s), under exclusive license to APress Media, LLC, part of Springer Nature 2023
C. PittProcedural Generation in Godothttps://doi.org/10.1007/978-1-4842-8795-8_5

5. Designing Levels in Pixel Art

Christopher Pitt1  
(1)
Durbanville, South Africa
 

Our Sokoban implementation is pretty cool, but it has a problem that I’d like to solve in this chapter. We could keep designing our levels with Godot’s visual tools, but there’s a better way to handle procedural content.

In fact, we started down the better path while designing our levels in Sokoban. You might have wondered why we used an obscure grid system when we could draw levels by hand.

It’s because algorithms can use grid-based data like this to render our tiles and nodes for us. All we have to do is find an easier way to define the grid data.

Creating Pixel Art

Creating any kind of art has its challenges, so I won’t pretend to give you shortcuts for it. All I know is that pixel art offers a set of constraints (limit color palette and canvas size) that encourage creativity in me.

There are a bunch of applications you can use to draw pixel art. If you want something free, you can try Piskel. I can also recommend Aseprite, though it’s not free.

Try either, and create a bit of pixel art that is 16 × 16 pixels:

A square-shaped layout with ten square-shaped pixels in a gradient of colors.

Example pixel layout

What we want to do is try to represent a typical game level in pixel art so that we can parse that image file inside of Godot. In this example:
  • Green pixels represent trees.

  • Gray ones represent rocks.

  • The orange pixel represents the player.

You don’t have to use the same colors or layout. You’ll soon see how to accommodate different designs and colors.

Converting Pixel Art to a Grid

Let’s open up our experiment project and add a new one for this pixel art code. Import the pixel art image you created and set up a new inherited scene, from the Experiment scene, called PixelsExperiment:

A screenshot depicts the scene window that lists options under the pixels experiment. It has the file system with highlighted layout, and pixel experiment.

Setting the stage

Pixel art is a grid, by design. We need to use some new code to get the grid data out of the image and into a format that we can manipulate and draw.

Let’s add some methods to do this in PixelsExperiment:

This is from nodes/experiments/pixels_experiment.gd
extends GameExperiment
@export var layout_texture : Texture2D
enum types {
    none,
    tree,
    rock,
    player,
}
const type_colors := {
    types.tree: "65a30d",
    types.rock: "57534e",
    types.player: "ea580c",
}
func _ready() -> void:
    var layout = get_layout()
    # ...do something with the layout
func get_layout() -> Array[Array]:
    var layout_image := layout_texture.get_image()
    var rows := []
    for y in layout_texture.get_height():
        var row := []
        for x in layout_texture.get_width():
            var type := types.none
            var color := layout_image.get_pixel(x, y).to_html(false)
            for t in types.values():
                if not type_colors.has(t):
                    continue
                if color == type_colors[t]:
                    type = t
            row.append(type)
        rows.append(row)
    return rows

As you can probably tell, I created an enum of the possible types of pixels I have in my image file. I care about the three colors: for trees, rocks, and the player.

I use the enum values as keys for a dictionary that holds the hexadecimal colors of each type. This creates a type-safe lookup for the colors while also being quick to extend with more colors.

We export a Texture2D because it allows for any popular image format to be set through the property inspector. It has a get_image method we can use to get the underlying image data.

This Image class has a get_pixel method, which is what we can use to get the image grid’s pixel data to inspection. get_pixel returns a Color instance, which we can convert to a hexadecimal value.

We can compare the dictionary of colors to the pixel color to figure out what the type of each pixel. This gives us a similar grid to the one we built in Sokoban.

Flipping Layouts

Here’s where things get more interesting. We can take this pixel grid data and manipulate it to create variation. Let’s start by flipping the grid:

This is from nodes/experiments/pixels_experiment.gd
enum flip_axis {
    none,
    x,
    y,
}
func flip_layout(layout: Array[Array], flip := flip_axis.none) -> Array[Array]:
    var new_rows := []
    for row in layout:
        var new_row := []
        for cell in row:
            if flip == flip_axis.x:
                new_row.push_front(cell)
            else:
                new_row.push_back(cell)
        if flip == flip_axis.y:
            new_rows.push_front(new_row)
        else:
            new_rows.push_back(new_row)
    return new_rows

Since we have the pixels in a multidimensional array, flipping is a matter of reversing the direction of rows or cells in each row. We can make use of this by passing the layout through this new method:

This is from nodes/experiments/pixels-experiment.gd
func _ready() -> void:
    var layout = get_layout()
    var flipped_layout = flip_layout(layout, flip_axis.y)
    # ...do something with the flipped_layout

This is one of many different manipulations possible with this kind of grid data structure. In a couple chapters, we’ll see how useful it can be for generating varied maps.

Combining with Nodes and Tile Maps

We can combine this knowledge with what we learned in the Sokoban project. We can use this array in place of one that we built by hand:
for row in layout:
    for cell in row:
        if tiles.has(cell):
            _tiles.set_cell(
                0, Vector2i(x, y), 0, tiles[cell]
            )
        if nodes.has(cell):
            var new_node = nodes[cell].instantiate()
            _nodes.add_child(new_node)
            new_node.position = Vector2(x * 64, y * 64)

Summary

In this chapter, we took our first steps toward designing our levels with pixel art. While converting images to arrays isn't groundbreaking, we can manipulate those arrays in interesting ways.

Take a bit of time to think of other kinds of manipulations we could do to the resulting pixel art grids. Can you think of how to rotate a layout, or how we could vary the drawing of cells to inject a bit of realism and randomness?

In the next chapter, we’re going to dive even deeper into the randomness aspect of content generation, as we gear up to build our next game.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset