DEV Community

Misael Braga de Bitencourt
Misael Braga de Bitencourt

Posted on

2D Game Menu with Godot4

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.

Demo

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.

Assets

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.

Step1

Select the "Background" object and set its texture to the background image from the asset collection.

Step2

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.)

Step3

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.

Step4

Next, create two additional objects under InGameShop. These are of type Node2D, named MenuLeft and MenuRight.

Step5

Inside each of MenuLeft and MenuRight, create a child object of type Sprite named Background.

Step6
Step7

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.

Step8
Step9
Step10
Step11

Running the project with the "play" button at the top of the screen shows the user’s perspective.

Step12

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.

Step13
Step14
Step15

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.

Step16
Step17
Step18

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.

Step19

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.

Step20
Step21

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."

Step22

When running the project again, you can see how the bubble is positioned on the screen.

Step23

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.

Step24

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.

Step25

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.

Step26

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
Enter fullscreen mode Exit fullscreen mode

Github repository: https://github.com/misabitencourt/in-game-shop

Top comments (0)