-
Notifications
You must be signed in to change notification settings - Fork 16
At the end of this chapter, your dungeon will be filled with floors, walls and actors.
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.
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
.
(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
.
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 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.
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.