Logo

dev-resources.site

for different kinds of informations.

2D Game Menu with Godot4

Published at
11/20/2024
Categories
game
godot
menu
Author
justaguyfrombr
Categories
3 categories in total
game
open
godot
open
menu
open
Author
14 person written this
justaguyfrombr
open
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

godot Article's
30 articles in total
Favicon
endless runner in godot 4 3d all systems and minus like subway surfers for mobile
Favicon
How to Customize Input Bindings in Godot
Favicon
Seamless Inter-Process Communication with Godot's `execute_with_pipe`.
Favicon
How a indie game developer should follow the discipline of game development?
Favicon
2D Game Menu with Godot4
Favicon
Menu de Game Retrô com Godot4
Favicon
Unlocking the Power of Gaming with Game Vault: A Valuable Resource for the DEV Community
Favicon
Launching my first game soon!
Favicon
Strontium | The Ultimate Portfolio App for Gamers and Indie Game Developers
Favicon
The Big Refactoring - Chapter 0
Favicon
🇫🇷 Framework Heroes News : la veille sur les frameworks pour la semaine 2024/40
Favicon
5 WAYS TO ORGANIZE YOUR C# CODES IN GODOT
Favicon
Introducing Mineral Hunt Mode: A Game-Changing Experience in Narqubis
Favicon
Behind the Scenes: Designing a Beat Saber-Style Game with Godot
Favicon
Basics of Game Development Using Unity, Unreal Engine, or Godot
Favicon
Unity vs. Godot: A Game Developer's Guide
Favicon
Beach Buggy Racing Mod Apk
Favicon
Online Visual Novel in Godot: Case Study on Sentou Gakuen
Favicon
Godot4 2D: Enemy Spawn Radious - problem with spawning enemy on player, no infinity loops
Favicon
Godot 3D Multiplayer Template: A Starting Point for Creating 3D Multiplayer Games
Favicon
WordPress Co-Founder Matt Mullenweg Declares WP Engine a 'Cancer' – Is Your Hosting Provider Hurting the Community?
Favicon
aus new adn cool
Favicon
How to Press and Drag to Reposition a Window in Godot
Favicon
Learn By Example: Bash Script - Godot 4 Project Creator
Favicon
"Surf the Rails in Subway Surfers Online"
Favicon
I Made A Plugin To Update Godot From Within The Editor
Favicon
7 Key Reasons Why Price Localization is Crucial for Global Success
Favicon
If You’re Interested In Learning Game Development, Subscribe To These 3 YouTube Channels
Favicon
Godot Rust CI: Handy GDScript & Rust GitHub Actions
Favicon
How Corporate Greed Killed the Joy of Gaming for Gamers Worldwide

Featured ones: