Misael Braga de Bitencourt
Posted on November 20, 2024
Godot is one of the most famous game engines, like Unreal Engine and Unity. Like its competitors, Godot allows the creation of 2D and 3D games with a visual editor. Its main advantage is being open-source. Additionally, it enables developers to write code in a language called Godot Script, which is very similar to Python.
This brief article quickly demonstrates how to create a game menu using the latest major version of the engine, version 4. The menu in question will serve as the screen where the player enters a shop with their character to buy items using gold collected in-game. Anyone who has played a JRPG will recognize this type of screen.
Obtaining Assets
The assets used in this scene are simple and were created with AI. These can easily be replaced with real artwork after development.
Creating the Scene Tree
After starting a new project in the Godot editor, create the first object in the scene tree, a Node2D called InGameShop. This is the root object of the scene. Godot allows you to export it as a reusable resource. The tool also lets you copy and paste this node into a more complex scene.
As a child of the root node, add an object of type Sprite, which will serve as the background for the menu.
Select the "Background" object and set its texture to the background image from the asset collection.
Since the image has a different scale than the screen (you can see the blue outline of the user’s screen in the editor), increase the image size to ensure it fills more than the screen space. (This can be done in the Transform >> Scale property.)
In the image properties, adjust the image's colors to make it less striking, as the background shouldn’t distract from the menu items on the screen. If you prefer, you can do this directly in an editor like Photoshop or GIMP.
Next, create two additional objects under InGameShop. These are of type Node2D, named MenuLeft and MenuRight.
Inside each of MenuLeft and MenuRight, create a child object of type Sprite named Background.
For each sprite created, set its texture to a blue panel image and adjust the size via the Transform property. Reposition them on the screen using the Position property or the editor's tools.
Running the project with the "play" button at the top of the screen shows the user’s perspective.
For the central panel showing the coin count, follow the same process used for the other panels. Create a Node2D under the root object, name it, and add a Sprite inside it. I named it CoinPanel.
Add a Label object inside CoinPanel. This object displays text. In the text property, set the initial value to "0000." Adjust its size and position using the Position and Scale properties, similar to Sprite objects.
Finally, add a Sprite inside CoinPanel to represent the coin icon, positioned on the left of the coin counter. Adjust its size and position using the same properties as before.
Two additional "Label" objects need to be created, one inside "MenuLeft" and another inside "MenuRight". These will represent items within a panel. The size of these items is adjusted, and they are placed as the first row of items in each panel. These labels will lose visibility before the scene begins. The script will clone these items to render the actual list of items for the scene.
The vendor's speech bubble is created by inserting another "Node2D" into the root object. This node is named "SalesmanDialog". It contains a Sprite and a Label. The Sprite serves as the background image, using the speech bubble image. The Label is centered within the bubble, and its color is changed to a dark tone using the property Material >> New Canvas Item >> Edit Canvas Item, and changing the Blend Mode to "Subtract."
When running the project again, you can see how the bubble is positioned on the screen.
Among the selected assets, there is a PNG of a white panel with the image opacity reduced by half. This means the panel will not overlap other elements behind it. Instead, it will behave like light (for light colors) or shadow (for dark colors). A Sprite object with this panel texture was added to "MenuRight," overlaying the first item.
After adjusting the position, the vendor's speech bubble should have its "visible" property set to false. This can be done using the "eye" icon in the left-hand menu or by editing the object's properties directly.
The Labels, which will be duplicated for each item on the left or right, will have their visibility set to invisible. You can use the "eye" icon in the left-hand panel or the "visible" property in the right-hand panel for this.
A Godot Script is added to the main node by right-clicking on it. The script that programs the behavior of the scene is shown below. It is well-commented, with descriptive variable names. If you plan to use this script or something similar in a project, it is recommended to extract enums and messages into separate scripts.
extends Node2D
# Max amount of itens on panel, without scrolling
const max_amount_vertically = 8
# Store itens available enum
enum Adquirance {
MUSHROOM,
MUSHROOM_5x,
RING,
RING_10x,
GREEN_MUSHROOM,
GREEN_MUSHROOM_5x,
GREEN_MUSHROOM_10x,
GREEN_MUSHROOM_20x,
LEAF,
LEAF_5x,
LEAF_10x,
}
var adquirances = []
var menu_left_items = []
var menu_right_items = []
var menu_left_scroll_top = 0
var menu_right_scroll_top = 0
var selectedItemIndex = 0
var focusInitialPosition = 0
var game_state = null
var coin_available = 800
var coin_transfering = 0
var salesman_talking = 0
# Store itens available on this store
var salesman_store = [
Adquirance.MUSHROOM,
Adquirance.MUSHROOM_5x,
Adquirance.RING,
Adquirance.RING_10x,
Adquirance.GREEN_MUSHROOM,
Adquirance.GREEN_MUSHROOM_5x,
Adquirance.GREEN_MUSHROOM_10x,
Adquirance.GREEN_MUSHROOM_20x,
Adquirance.LEAF,
Adquirance.LEAF_5x,
Adquirance.LEAF_10x,
]
# Internationalization messages
var i18n_messages = {
'MUSHROOM': 'Mushroom',
'MUSHROOM_5x': 'Mushroom 5x',
'RING': 'Ring',
'RING_10x': 'Ring 10x',
'GREEN_MUSHROOM': 'Green Mushroom',
'GREEN_MUSHROOM_5x': 'Green Mushroom 5x',
'GREEN_MUSHROOM_10x': 'Green Mushroom 10x',
'GREEN_MUSHROOM_20x': 'Green Mushroom 20x',
'LEAF': 'Leaf',
'LEAF_5x': 'Leaf 5x',
'LEAF_10x': 'Leaf 10x',
'THX': 'Thanks!',
'NOT_ENOUGHT_COINS': 'Not enough coins.'
}
# price mapping
var price_table = [
{
'item': Adquirance.MUSHROOM,
'price': 30
},
{
'item': Adquirance.MUSHROOM_5x,
'price': 70
},
{
'item': Adquirance.RING,
'price': 90
},
{
'item': Adquirance.RING_10x,
'price': 140
},
{
'item': Adquirance.GREEN_MUSHROOM,
'price': 60
},
{
'item': Adquirance.GREEN_MUSHROOM_5x,
'price': 90
},
{
'item': Adquirance.GREEN_MUSHROOM_10x,
'price': 120
},
{
'item': Adquirance.GREEN_MUSHROOM_20x,
'price': 120
},
{
'item': Adquirance.LEAF,
'price': 50
},
{
'item': Adquirance.LEAF_5x,
'price': 200
},
]
# Called when the node enters the scene tree for the first time.
func _ready():
rearrange_store()
focusInitialPosition = $MenuRight/Selection.position.y
pass
# Called every frame. 'delta' is the elapsed time since the previous frame.
func _physics_process(delta):
if coin_transfering > 0:
coin_available -= 1
coin_transfering -= 1
$CoinPanel/Label.text = str(coin_available)
else:
if Input.is_action_just_released("ui_down"):
selectedItemIndex += 1
# $PopAudio.play()
if Input.is_action_just_released("ui_up"):
selectedItemIndex -= 1
# $PopAudio.play()
if Input.is_action_just_released("ui_accept"):
buy_item(salesman_store[selectedItemIndex + menu_right_scroll_top])
# $PopAudio.play()
if selectedItemIndex < 0:
selectedItemIndex = 0
if menu_right_scroll_top > 0:
menu_left_scroll_top -= 1
do_menu_right_scroll_up()
if selectedItemIndex > salesman_store.size():
selectedItemIndex = salesman_store.size()
if (selectedItemIndex + menu_right_scroll_top) > (salesman_store.size()-1):
selectedItemIndex -= 1
if selectedItemIndex > (max_amount_vertically-1):
do_menu_right_scroll_down()
selectedItemIndex = (max_amount_vertically-1)
$MenuRight/Selection.position.y = focusInitialPosition + (selectedItemIndex * 35)
if salesman_talking > 0:
salesman_talking -= 1
if salesman_talking == 0:
$SalesmanDialog.visible = false
pass
func do_menu_right_scroll_down():
menu_right_scroll_top += 1
rearrange_store()
func do_menu_right_scroll_up():
menu_right_scroll_top -= 1
rearrange_store()
func get_item_name(item):
match item:
'mushroom':
return i18n_messages['MUSHROOM']
'ring':
return i18n_messages['RING']
'green_mushroom':
return i18n_messages['GREEN_MUSHROOM']
'leaf':
return i18n_messages['LEAF']
Adquirance.MUSHROOM:
return i18n_messages['MUSHROOM']
Adquirance.MUSHROOM_5x:
return i18n_messages['MUSHROOM_5x']
Adquirance.RING:
return i18n_messages['RING']
Adquirance.RING_10x:
return i18n_messages['RING_10x']
Adquirance.GREEN_MUSHROOM:
return i18n_messages['GREEN_MUSHROOM']
Adquirance.GREEN_MUSHROOM_5x:
return i18n_messages['GREEN_MUSHROOM_5x']
Adquirance.GREEN_MUSHROOM_10x:
return i18n_messages['GREEN_MUSHROOM_10x']
Adquirance.GREEN_MUSHROOM_20x:
return i18n_messages['GREEN_MUSHROOM_20x']
Adquirance.LEAF:
return i18n_messages['LEAF']
Adquirance.LEAF_5x:
return i18n_messages['LEAF_5x']
Adquirance.LEAF_10x:
return i18n_messages['LEAF_10x']
return ""
func get_product_price(item):
for price_table_item in price_table:
if price_table_item['item'] == item:
return price_table_item['price']
return 0
func rearrange_store():
$CoinPanel/Label.text = str(coin_available)
var amount = 0
for item in menu_right_items:
$MenuRight.remove_child(item['node'])
for item in menu_left_items:
$MenuLeft.remove_child(item['node'])
menu_right_items = []
var scrolling = menu_right_scroll_top
for item in salesman_store:
if scrolling > 0:
scrolling -= 1
continue
var item_node = $MenuRight/Item.duplicate()
var in_list_item = {
'kind': item,
'node': item_node
}
menu_right_items.push_back(in_list_item)
if amount < max_amount_vertically:
$MenuRight.add_child(item_node)
item_node.position.y += 35 * amount
item_node.set_visible(true)
item_node.text = get_item_name(item) + " $" + str(get_product_price(item))
amount += 1
if amount == max_amount_vertically:
var more_indicator = $MenuRight/Item.duplicate()
more_indicator.text = "..."
more_indicator.set_visible(true)
more_indicator.position.y += 35 * amount
$MenuRight.add_child(more_indicator)
amount = 0
for adquirance in adquirances:
var item_node = $MenuLeft/Item.duplicate()
item_node.visible = true
item_node.position.y += 35 * amount
item_node.text = get_item_name(adquirance.type) + " x" + str(adquirance.amount)
var in_list_item = {
'kind': adquirance.type,
'amount': adquirance.amount,
'node': item_node
}
menu_left_items.push_back(in_list_item)
$MenuLeft.add_child(item_node)
amount += 1
func buy_item(item):
var price = get_product_price(item)
if coin_available < price:
salesman_talks(i18n_messages['NOT_ENOUGHT_COINS'])
return; # TODO more coins needed
coin_transfering = price
salesman_talks(i18n_messages['THX'])
match item:
Adquirance.MUSHROOM:
add_item_to_bag('mushroom', 1)
return;
Adquirance.MUSHROOM_5x:
add_item_to_bag('mushroom', 5)
return;
Adquirance.RING:
add_item_to_bag('ring', 1)
return;
Adquirance.RING_10x:
add_item_to_bag('ring', 10)
return;
Adquirance.GREEN_MUSHROOM:
add_item_to_bag('green_mushroom', 1)
return;
Adquirance.GREEN_MUSHROOM_5x:
add_item_to_bag('green_mushroom', 5)
return;
Adquirance.GREEN_MUSHROOM_10x:
add_item_to_bag('green_mushroom', 10)
return;
Adquirance.GREEN_MUSHROOM_20x:
add_item_to_bag('green_mushroom', 20)
return;
Adquirance.LEAF:
add_item_to_bag('leaf', 1)
return;
Adquirance.LEAF_5x:
add_item_to_bag('leaf', 5)
return;
Adquirance.LEAF_10x:
add_item_to_bag('leaf', 10)
return;
func add_item_to_bag(type, amount):
for adquired in adquirances:
if adquired.type == type:
adquired.amount += amount
rearrange_store()
return;
adquirances.push_back({ 'type': type, 'amount': amount })
rearrange_store()
func salesman_talks(message):
$SalesmanDialog/Label.text = message
$SalesmanDialog.visible = true
salesman_talking = 200
Github repository: https://github.com/misabitencourt/in-game-shop
Posted on November 20, 2024
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.