Misael Braga de Bitencourt
Posted on November 20, 2024
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.
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.
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.
Selecionamos o objeto "Background" e trocamos sua textura para a imagem de fundo da coleção de assets.
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).
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.
Agora, vamos criar mais dois objetos debaixo de "InGameShop". São do tipo "Node2D" e vamos chamá-los de "MenuLeft" e "MenuRight".
Dentro de "MenuLeft" e "MenuRight", para cada um deles, criamos um objeto filho do tipo "Sprite" chamado "Background".
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.
Se executarmos o projeto, no ícone de "play" na parte superior da tela, podemos ter a visão do usuário final.
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"
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".
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.
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.
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".
Ao executarmos o projeto novamente, podemos ver como o balão fica posicionado em tela.
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.
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.
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.
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
Um repositório com o projeto pode ser encontrado em: 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.