Skip to content
bozar42 edited this page Aug 14, 2020 · 10 revisions

03: Create A Static Dungeon

Source code.

At the end of this chapter, your dungeon will be filled with floors, walls and actors.

IMAGE: A static dungeon

Create Sprites From A Tilemap

First of all, add a font and a tilemap to resource folder. The GUI font, Fira Code, is created by Nikita Prokopov. The tileset, curses_vector, is created by DragonDePlatino for Dwarf Fortress. Create a new folder sprite which will be used to store sprite scenes.

IMAGE: Load a tilemap

Remove icon node from previous chapter. Add a Sprite node and name it as PC. Follow these steps to cut a sprite off the tilemap. (1) Select PC node. (2) In the Inspector tab, click Sprite -> Texture -> [empty]. In the dropdown menu, select New AtlasTexture. (3) Click Sprite -> Texture -> AtlasTexture. Click Atlas -> [empty]. In the dropdown menu, select Load. (4) Load res://resource/curses_vector_24x36.png.

IMAGE: Select a sprite

(5) Click TextureRegion on the Bottom Panel. (6) Click the up arrows on the lower right corner or press Shift+F12 to expand the panel. (7) Set Snap Mode to Grid Snap. The offsets and steps are 24x36, which corrsepond to the size of each tile (24px wide and 36px high). (8) Drag and drop a rectangular to select @ symbol.

There are two more post-processing steps in Godot engine: change PC's color and save it as a packed scene.

(9) Click PC sprite. In the Inspector tab, locate CanvasItem -> Visibility -> Modulate. Find it directly or use the filter textbox in the top row of the tab. Set Modulate to ABB2BF. Note that Modulate is different from Self Modulate. Modulate changes the color of PC and all sprites that inherit it. However Self Modulate only affects PC itself.

(10) Right click PC sprite, in the dropdown menu, select Save Branch as Scene to save the node as a scene (PC.tscn) in sprite folder. Refer to Instancing for more information.

Repeat these steps for other sprites until your demo looks like the image below. Note that Floor sprite has a greyish color: 495162.

IMAGE: All sprites in the demo

Create Sprites From Scripts

Remove all nodes except the root. Add a new Node2D node to MainScene and name it as InitWorld. Attach a script (InitWorld.gd) to it. Before writing the first line of code, here is a reminder. I use static typing and stick to the style guide as much as possible. However, when my mind fails me and my fingers betray me, you shall not pass the wide gate. <('o')=|~~~B

It is fairly simple to instance a PC sprite (actually it is a packed scene), place it at (100, 100) and add it to group pc. The pair of coordinates (100, 100) means 100 pixel from the left edge of the screen and 100 pixel from the top. It has no special meaning. Just a way to show that you can place a sprite anywhere you like in a script.

# InitWorld.gd

func _ready() -> void:
    var new_sprite := preload("res://sprite/PC.tscn").instance() as Sprite
    new_sprite.position = Vector2(100, 100)
    new_sprite.add_to_group("pc")

    add_child(new_sprite)

In order to create many more sprites, put them at different positions and add them to various groups, we need to satisify three requirements.

  • Add a private function to InitWorld.gd for creating sprites.
  • Find a way to convert 0, 0 (a pair of integers that represent the top left grid in the dungeon) to the Vector position used by sprites. (Note that the top left grid in the DUNGEON may or may not be the top left grid on the SCREEN. More on this below.)
  • Find a way to avoid typing or copying the string pc.

Start from the last requirement. Since GDScripts can be loaded as resources just as you load an image or a packed scene, a possible solution is to store the string pc as a constant in a script and load the script inside InitWorld.gd.

Make a new folder library to store helper scripts. Add a GDScript resource GroupName.gd to the folder.

# GroupName.gd

const PC: String = "pc"


# InitWorld.gd

const Player := preload("res://sprite/PC.tscn")

var _new_GroupName := preload("res://library/GroupName.gd").new()


func _ready() -> void:
    var new_sprite := Player.instance() as Sprite
    new_sprite.position = Vector2(100, 100)
    new_sprite.add_to_group(_new_GroupName.PC)

    add_child(new_sprite)

Group is a powerful tool in Godot engine (see Groups). Call add_to_group() to tag a sprite with a user defined name and have fun with it later: identify a sprite by its group name, or iterate every element in a group and execute a method.

In my Godot project, One More Level, a sprite belong to two groups. The main group defines its type: an actor moves around and either has an AI or is controlled by player; a building cannot move; a trap is a building which has special game mechanics; lastly, a ground is the dungeon floor. The sub group is the sprite's name: knight, sandworm, wall, etc.

As for the second requirement, add another GDScript resource ConvertCoord.gd to library.

# ConvertCoord.gd

const START_X: int = 50
const START_Y: int = 54
const STEP_X: int = 26
const STEP_Y: int = 34


func vector_to_array(vector_coord: Vector2) -> Array:
    var x: int = ((vector_coord.x - START_X) / STEP_X) as int
    var y: int = ((vector_coord.y - START_Y) / STEP_Y) as int

    return [x, y]


func index_to_vector(x: int, y: int,
        x_offset: int = 0, y_offset: int = 0) -> Vector2:
    var x_vector: int = START_X + STEP_X * x + x_offset
    var y_vector: int = START_Y + STEP_Y * y + y_offset

    return Vector2(x_vector, y_vector)

There are two types of positions in the game. Vector position defines where a sprite is displayed on the screen. A pair of integers indicates the position of a sprite on an imaginary dungeon board. Suppose that the top left sprite of the dungeon board is 50 pixels from the left edge of the screen and 54 pixels from the top, we need to convert Vector(50, 54) to [0, 0]. If a sprite is 26 pixels wide and 34 pixels high, we can convert a vector to and from an pair of integers as shown in the code above.

If we want to render a sprite slightly off the dungeon board grid, for example, a vertical health bar that hangs on the top right corner of a grid, or two arrows outside the dungeon as in the first image in this article, we use x_offset and y_offset to move a sprite by pixels. Their default values are 0 pixel.

The helper script enables us to generate pc in the corner of the dungeon like this.

# InitWorld.gd

new_sprite.position = _new_ConvertCoord.index_to_vector(0, 0)

The first requirement should be easy to meet. Implement _create_sprite() yourself.

# InitWorld.gd

const Player := preload("res://sprite/PC.tscn")

var _new_GroupName := preload("res://library/GroupName.gd").new()
var _new_ConvertCoord := preload("res://library/ConvertCoord.gd").new()


func _ready() -> void:
    _create_sprite(Player, _new_GroupName.PC, 0, 0)


func _create_sprite(prefab: PackedScene, group: String, x: int, y: int,
        x_offset: int = 0, y_offset: int = 0) -> void:

    pass

Now that you know how to instance one sprite, with the help of res://library/DungeonSize.gd, you should be able to expand InitWorld.gd to generate the full dungeon as shown in the first image.

# DungeonSize.gd

const MAX_X: int = 21
const MAX_Y: int = 15

const CENTER_X: int = 10
const CENTER_Y: int = 7

const ARROW_MARGIN: int = 32

One more thing. If you happen to create wall sprites before floor, the grey dot appears in front of the wall sign #. To solve this problem, simply set the Z Index of wall, dwarf and pc to 1. Let floor's Z Index remain to be 0.

Add Mock Up GUIs

Add a Control node MainGUI to MainScene. Save the node as a scene into scene/gui/. Open res://scene/gui/MainGUI.tscn. Set the size of MainGUI node to 800x600. Add two label nodes: Bottom and Sidebar. In the Inspector tab, type text in Label -> Text. Move these nodes to proper positions.

IMAGE: Set custom font

In order to use custom font, select Bottom node. Click Control -> Custom Fonts -> Font -> [empty]. Select New DynamicFont. Click Custom Fonts -> Font -> DynamicFont. Click Font -> Font Data -> [empty]. Load res://resource/FiraCode-Regular.ttf. Set font Size to 24 and Font Color to ABB2BF.

We will redesign GUI layout in Chapter 8. The Label controls we add here are just placeholders. The two arrows in the previous part are also placeholders. They are meant to move with PC as indicators but we shall not fulfill this function. You can think of it as an exercise to test your knowledge.

Clone this wiki locally