Menu de Game Retrô com Godot4

justaguyfrombr

Misael Braga de Bitencourt

Posted on November 20, 2024

Menu de Game Retrô com Godot4

A Godot é uma game engine das mais famosas, como a Unreal Engine e Unit. Assim como suas concorrentes, a Godot permite a criação de games 2D e 3D com editor visual. O seu principal diferencial é ser de código aberto. Além disso, esta permite que o programador escreva em uma linguagem chamada Godot Script, muito parecida com Python.

Esse pequeno artigo vai mostrar rapidamente como criar a parte de menu de um game utilizando esta engine em sua versão major mais recente, a versão 4. O menu em questão será a tela onde o jogador entra com seu personagem em uma loja, para comprar itens com o ouro coletado in-game. Todo o mundo que já jogou um JRPG é familiar com esse tipo de tela.

Demo

Obtendo assets

Os assets utilizados na cena são bem básicos, criados por I.A. Estes podem ser facilmente trocados por artes de verdade depois do desenvolvimento.

Assets

Criando árvore da cena

Depois de criar um novo projeto usando o editor da engine, criei o primeiro objeto na árvore da cena que é um Node2D o qual chamei de InGameShop. Esse é o objeto raíz da cena. A Godot permite que você exporte ele como um recurso reutilizável. A ferramenta também permite que você simplesmente copie esse nó é cole em uma cena mais complexa.

Como filho do nó raiz, adicionamos um objeto do tipo "Sprite", que será o plano de fundo do menu.

Step1

Selecionamos o objeto "Background" e trocamos sua textura para a imagem de fundo da coleção de assets.

Step2

Como a imagem possui uma escala diferente da tela (podemos ver o contorno da tela do usuário final em azul, no editor), vamos aumentar o tamanho
da imagem de fundo para que ela ocupe um espaço maior do que a tela. (isso pode ser feito na propriedade Transform >> Scale).

Step3

Ainda nas propriedades da imagem, podemos modular as cores da imagem para torná-la menos chamativa, visto que o plano de fundo não pode atrapalhar a visão dos itens do menu na tela. Caso prefira, você pode fazer isso direto na imagem com um editor como o Photoshop ou Gimp.

Step4

Agora, vamos criar mais dois objetos debaixo de "InGameShop". São do tipo "Node2D" e vamos chamá-los de "MenuLeft" e "MenuRight".

Step5

Dentro de "MenuLeft" e "MenuRight", para cada um deles, criamos um objeto filho do tipo "Sprite" chamado "Background".

Step6
Step7

Para cada um dos sprites criados anteriormente, selecionamos a imagem que é um painel azul como textura. Depois disso, definimos seu tamanho, o tamanho de cada painel, redimensionando o sprite. Isso pode ser feito pela propriedade transform. Depois, reposicionamos eles na tela, por meio da pripriedade position ou utilziando as ferramentas na parte superior do editor.

Step8
Step9
Step10
Step11

Se executarmos o projeto, no ícone de "play" na parte superior da tela, podemos ter a visão do usuário final.

Step12

Para criar o painel no meio com a quantidade de moedas, fazemos da mesma forma com que fizemos os outros paineis. Criamos um "Node2D" debaixo do objeto raíz, nomeamos ele e inserimos um objeto "Sprite" dentro desse. Chamei esse de "CoinPanel"

Step13
Step14
Step15

Adicionei um objeto do tipo "Label" dentro do "CoinPanel", como o próprio nome sugere, esse objeto insere um texto. A propriedade "text", como texto inicial, foi inserido o valor "0000". Para controlar seu tamanho e posição, altera-se position e scale, como nos objetos "Sprite".

Step16
Step17
Step18

Adicionado mais um Sprite dentro de "CoinPanel", este sprite é referente ao ícone de moeda que fica à esquerda, no pequeno contador de moedas. O Tamanho e posição foram ajustados nas mesmas propriedades citadas anteriormente.

Step19

Mais dois objetos "Labels" precisam ser criados, um dentro de "MenuLeft" e outro dentro de "MenuRight". Eles representarão um item dentro de um painel. É ajustado o tamanho destes itens e inserido no que seria a primeira linha de itens de cada painel. Estes labels perderão a visibilidade antes da cena começar. O script irá clonar esses itens com o fim de renderizar a lista real de itens da cena.

Step20
Step21

O balão de mensagens do vendedor é criado inserindo mais um "Node2D" dentro do objeto raíz. Chamei esse node de "SalesmanDialog". Este node possui um Sprite e um Label dentro dele. O Sprite é a imagem de fundo que utilizei a imagem do balão de fala. O Label é centralizado no balão e a sua cor é alterada para um tom escuro utilizando a propriedade Material >> Novo Canvas item >> Editar o canvas item e alterando o Blend mode para "Subtract".

Step22

Ao executarmos o projeto novamente, podemos ver como o balão fica posicionado em tela.

Step23

Nos assets selecionados, existe um png de um painel branco com a opacidade da imagem pela metade. Isso quer dizer que esse painel não ficará sobreposto sobre outros elementos por trás dele. Em vez disso, ele se comportará como uma luz (para cores claras) ou sombra (para cores escuras). Um objeot Sprite com a textura desse painel foi inserido no "MenuRight" sobrepondo o primeiro item.

Step24

Depois de ajustada a posição, o balão da fala do vendedor deve ter sua propriedade "visible" alterada para false. Isso pode ser feito no ícone de "olho" no menu á esquerda ou editando as propriedades do objeto mesmo.

Step25

Os Labels, que serão duplicados para cada item na esquerda ou à direita, terão suas visibilidades alteradas de modo que fiquem invisíveis. O ícone com o "olho" no painel á esquerda pode ser utilizado tanto quanto a propriedade visible no painel à direita.

Step26

Um Godot Script é adicionado no nó principal, clicando com o botão direito neste. O script que programa o comportamento da cena é este abaixo. Ele está comentado e com os nomes de variávels bem descritas. Se for utilizar este script ou algo parecido em um projeto, recomendo extrair os enums e mensagens em scripts separados.

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

Um repositório com o projeto pode ser encontrado em: https://github.com/misabitencourt/in-game-shop

💖 💪 🙅 🚩
justaguyfrombr
Misael Braga de Bitencourt

Posted on November 20, 2024

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related

Menu de Game Retrô com Godot4
godot Menu de Game Retrô com Godot4

November 20, 2024