Mini Tutorial Jokenpô na Godot

Desvende os segredos do Jokenpô na Godot! Crie além das regras tradicionais, explorando inovação e personalização. Da configuração à implementação, mergulhe em uma jornada de desenvolvimento envolvente. Descubra o poder da Godot e dê vida ao seu jogo!

Mini Tutorial Jokenpô na Godot
Photo by Marcus Wallis / Unsplash

Fala Game Dev, nos encontramos novamente, né? Se você é um aspirante a desenvolvedor de jogos ou apenas curioso sobre como essas criações interativas ganham vida, este post é para você.

Antes de começarmos, é fundamental destacar a escolha da Godot como nossa ferramenta de desenvolvimento. A Godot é uma engine de código aberto, que oferece uma incrível flexibilidade e poder, além de ser amigável para desenvolvedores de todos os níveis de habilidade.

Apesar do Jokenpô ser um jogo bastante simples, é importantíssimo ter uma visão clara do jogo que queremos criar. No nosso caso vamos com as regras tradicionais de Pedra, Papel e Tesoura, mas se você achar interessante, pode incluir outros elementos como na versão "The Big Bang Theory":

Vamos definir então o que precisamos no nosso jogo, ok?

  1. Jogo em tela única, simplificado;
  2. Assets para as imagens Pedra, Papel e Tesoura;
  3. Asset para o Background do nosso jogo;
  4. Pontuação para o Jogador;
  5. Pontuação para o Oponente;
  6. Apresentação da escolha do oponente;
  7. Resultado da rodada.

Cada rodada que o jogador ganha, ele aumenta 1 ponto, quando o jogador perde o oponente ganha 1 ponto, em caso de empate nenhum dos dois ganha pontos adicionais. Vamos então ao código?

Começamos abrindo a Godot e criando um novo Projeto. Criei uma nova pasta com o nome Jokenpo, nela é que vamos armazenar o nosso projeto:

Nova pasta para armazenar os assets, scripts e cenas do nosso jogo.

Depois de entrar no editor da Godot, inclua 3 sub-pastas pela janela FileSystem:

  • Scenes: vai armazenar as cenas do nosso jogo;
  • Scripts: vai armazenar os scripts do nosso jogo;
  • Sprites: vai armazenar as imagens do nosso jogo.

Vai ficar alguma coisa mais ou menos assim:

Criação das sub-pastas para guardar os elementos do projeto.

Adicione as suas imagens de background e de cada uma das opções do jogador na pasta de sprites, vamos trabalhar com elas mais tarde. As demais pastas serão preenchidas conforme vamos passando os pontos de desenvolvimento.

Feito isso vamos começar à criar a nossa cena principal. Ela deve ser do tipo Control, você pode selecionar a opção User Interface ou pelo atalho Ctrl+A ou então clicar na opção Other Node e buscar o nó do tipo Control:

Seleção do nó Control como nó raiz da nossa cena.

Renomeie o nó para Main e inclua 3 filhos do tipo GridContainer nomeados:

  • PlayerContainer
  • OpponentContainer
  • ResultsContainer

Para cada um destes containers vamos ter 2 labels, sua árvore de cenas deve ficar como essa aqui em baixo, já com os nós Label renomeados:

Árvore de cenas com os nós e labels correspondentes renomeados.

Agora é necessário ajustar a posição dos elementos, para isso eu ancorei o nó Main expandindo ele para o tamanho da tela, o nó PlayerContainer na esquerda superior, o nó OpponentContainer na direita superior e o nó ResultContainer centralizado na tela.

Também aumentei o tamanho das letras para 48 e coloquei os textos centralizados, mas isso foi uma opção pessoal, recomendo que vc brinque um pouco com as configurações e veja como você considera legal.

Por último, dei um espaçamento para não deixar os textos muito na borda da tela, no meu caso coloquei 50 px de distância, mas você pode achar melhor deixar maior ou menor, novamente foi uma opção pessoal:

Cena com os Labels configurados e posicionados.

Vamos incluir mais um Grid Container, desta vez para armazenar os botões Pedra, Papel e Tesoura. Chamei o novo container de OptionsContainer e os botões respectivamente de RockButton, PaperButton e ScissorsButton.

Eu deixei como tamanho deste grid 760 x 110 px, mas isso vai depender do tamanho que você definiu sua tela de projeto, coloquei este container ancorado no centro inferior e espacei em 50 pixels da base para não ficar colado no chão (similar ao que fiz nas labels anteriormente em relação às laterais).

Alterei a propriedade Columns desse Grid Container para 3 para os botões serem apresentados lado à lado e incluí uma separação horizontal de 16 px para os botões não ficarem tão juntinhos.

Nos botões incluí o texto dos botões, ajustei seus tamanhos para 36 e alterei a configuração de Container sizing para expand tanto vertical quanto horizontal, a cena ficou desse jeito aqui:

Cena com os botões de seleção do jogador incluídos

Agora vamos incluir a imagem de fundo como um novo nó do tipo TextureRect, vou chamá-lo de BackgroudTextureRect, lembre de colocar a textura no fundo mudando a ordem deste nó para ser o primeiro da sua árvore.

Inclua a imagem que achar adequada, fazendo as configurações de dimensionamento, posicionamento, repetição ou escala conforme necessário. No meu caso escolhi uma imagem que ajustando a escala já tenho um resultado interessante:

Inclusão da imagem de background.

Ao incluir a imagem, percebi que os textos ficaram meio ruins, então incluí uma borda preta neles, alterando a propriedade de font outline color e outline size para dar um destaque maior, mas novamente isso é preferência pessoal e talvez com o backgroud que vc escolher não fique tão legal, eu particularmente gostei assim:

Inclusão de borda nos textos

O layout tá praticamente pronto, se você não quiser mostrar as imagens de Pedra, Papel e Tesoura, você poderia parar por aqui e começar à fazer as lógicas. Vou nessa direção agora e as imagens eu vou incluir mais tarde, ok?

Vamos associar um script à cena principal, salvar e conectar os sinais dos botões, seu script deve ficar desse jeitinho aqui:

extends Control


# Called when the node enters the scene tree for the first time.
func _ready():
	pass # Replace with function body.


# Called every frame. 'delta' is the elapsed time since the previous frame.
func _process(delta):
	pass


func _on_rock_button_pressed():
	pass # Replace with function body.


func _on_paper_button_pressed():
	pass # Replace with function body.


func _on_scissors_button_pressed():
	pass # Replace with function body.

Ainda não fizemos as lógicas, então é normal que seu script esteja só com essa estrutura básica, ok?

Mas espera um momento... O clique dos botões vão executar a mesma funcionalidade ao final, o que muda é apenas a referência de qual dos botões foi pressionado, então vamos apagar estes métodos do script, desconectar eles dos botões e conectar um novo método que receba como argumento um texto com uma referência para o botão clicado:

Desconectando os sinais dos botões com o clique do botão direito do mouse

No script apagamos as funções e vamos criar a função genérica na conexão do primeiro botão, preste atenção aos detalhes da sequencia de ações:

  1. Clique para conectar o sinal normalmente;
  2. No Receiver Method, defina a função à conectar (vou usar _on_button_pressed);
  3. Selecione a opção advanced logo abaixo do método;
  4. Adicione um novo argumento string e o valor nesta conexão.

Para o botão Rock ficou assim, para os outros dois só é necessário alterar o argumento de referência para Paper ou Scissors conforme necessário:

Conexão botão Rock à função _on_button_pressed

Se quiser testar, basta incluir um print nesta função e executar seu projeto, com isso você deve ver no terminal o texto relacionado à cada um dos botões conforme eles forem clicados. Abaixo o código com a função e o print incluído:

extends Control


# Called when the node enters the scene tree for the first time.
func _ready():
	pass # Replace with function body.


# Called every frame. 'delta' is the elapsed time since the previous frame.
func _process(delta):
	pass

func _on_button_pressed(extra_arg_0):
	print(extra_arg_0)
	pass # Replace with function body.

Veja que só com esta alteração já temos um código mais conciso, ao invés de 3 funções, uma para cada botão, temos uma função única conectada aos 3 botões, recebendo um argumento diferente à partir de cada um deles.

Vamos agora trabalhar na lógica para definir a opção do oponente e na comparação dos valores do jogador e oponente para definir se houve empate ou vitória de um dos lados.

Vamos criar no nosso script variáveis exportáveis para as informações de pontuação do jogador e oponente, seleção do oponente e resultado da partida. E para cada uma delas associar pelo Editor os nós correspondentes às mesmas:

extends Control

@export var player_points_label : Label
@export var opponent_points_label : Label
@export var opponent_selecion_label : Label
@export var result_label : Label
...

Na janela do inspetor vai ficar assim:

Seleção dos nós para cada variável exportada.

Também vamos criar 2 variáveis para armazenar a pontuação do jogador e oponente temporariamente, pois a label é um texto e precisamos fazer contas matemáticas com a pontuação do jogador e oponente em cada rodada e só depois atualizar o label de ambos.

...
var player_points : int = 0
var opponent_points : int = 0
...

Para simplificar a lógica, vamos adotar valores inteiros para as opções do oponente, caso seja 0 (zero) assumimos que o oponente selecionou pedra, caso seja 1 (um) será papel, caso seja 2 (dois) será tesoura.

E com base no número aleatório definimos uma string para comparação com a string de referência do botão e definimos quem ganhou ou se houve empate.

Vamos colocar a lógica de comparação em uma função separada para ficar mais fácil dar qualquer manutenção, lembre-se de atualizar também os valores da pontuação e da Label que apresenta tal informação.

Tirei as funções ready e process que não vamos utilizar e ajustei o nome do argumento da função on button pressed pra ficar mais claro seu objetivo. O código relacionado às lógicas ficou desse jeito:

extends Control

@export var player_points_label : Label
@export var opponent_points_label : Label
@export var opponent_selecion_label : Label
@export var result_label : Label

var player_points : int = 0
var opponent_points : int = 0

func _on_button_pressed(player_selection):
	var opponent_selection_number = randi_range(0,2)
	var opponent_selection : String = ""
	
	match opponent_selection_number:
		0:
			opponent_selection = "ROCK"
		1:
			opponent_selection = "PAPER"
		2:
			opponent_selection = "SCISSORS"
		_:
			opponent_selection = ""
	
	check_results(opponent_selection, player_selection)

func check_results(opponent_selection: String, player_selection: String):
	
	opponent_selecion_label.text = 
        str("Computador escolheu: " + opponent_selection)
	
	if opponent_selection == player_selection:
		result_label.text = "Empate"
	elif opponent_selection == "ROCK":
		if player_selection == "PAPER":
			result_label.text = "Você Ganhou!"
			player_points += 1
		else:
			result_label.text = "Você Perdeu!"
			opponent_points += 1
	elif opponent_selection == "PAPER":
		if player_selection == "SCISSORS":
			result_label.text = "Você Ganhou!"
			player_points += 1
		else:
			result_label.text = "Você Perdeu!"
			opponent_points += 1
	elif opponent_selection == "SCISSORS":
		if player_selection == "ROCK":
			result_label.text = "Você Ganhou!"
			player_points += 1
		else:
			result_label.text = "Você Perdeu!"
			opponent_points += 1
		
	player_points_label.text = str(player_points)
	opponent_points_label.text = str(opponent_points)
💡
Atenção:
É possível criar a lógica de comparação validando todas as condições de vitória ao mesmo tempo, se uma delas for verdadeira o jogador ganha, se nenhuma delas for verdadeira o jogador perde, mas visualmente esta estrutura de comparação acima é mais simples.

O seu jogo já pode ser considerado pronto aqui mesmo, todas as lógicas estão funcionando corretamente. Mas se quiser incluir os símbolos e dar uma identidade visual mais bonitinha, você pode ver os últimos passos se registrando gratuitamente no nosso site aqui embaixo.