Mini Tutorial Infinity Runner na Godot

🚀 Domine o Desenvolvimento de Jogos! 🎮 Descubra o Tutorial Exclusivo de Infinity Runner 2D na Godot! 💥 Torne-se um Mestre na Godot e Crie Jogos Épicos! Não perca esta Oportunidade de Aprimorar suas Habilidades! 🔥 #Godot #InfinityRunner #DesenvolvimentoDeJogos

Mini Tutorial Infinity Runner na Godot
Photo by Jenny Hill / Unsplash

Fala Game Dev, tudo em cima? Vamos falar de Infinity Runner hoje?

Um infinity runner é um gênero popular onde o jogador controla um personagem que corre infinitamente pela tela, enfrentando obstáculos e desafios. O tutorial que vou trazer hoje vai ser só uma visão bem alto nível de uma possível forma de fazer o seu Infinity Runner.

Que tal identificarmos o que precisamos ter no nosso jogo? Simbora:

  • Um jogador que ficará parado no lado esquerdo da tela, mas com sprites indicando movimento;
  • Obstáculos que se moverão da direita para a esquerda da tela, como se estivessem indo na direção do jogador;
  • Uma forma de gerar os obstáculos de forma aleatória fora da cena;
  • Colisões entre jogador e obstáculos, para que o jogador tenha que desviar dos elementos gerados;
  • Um HUD para apresentar pontuação e vidas ao jogador.
  • Um fundo de tela em paralaxe.

Já sabemos o que precisa ser feito, né? Então vamos criar o projeto lá na Godot. Lembra como se faz? Não? Vamos com passinhos curtos então.

Começamos no Project List, clicando em +New para definir onde será armazenado o nosso projeto:

Project Manager com a lista de projetos atuais

Selecione uma pasta vazia, ou crie uma nova pasta para o seu projeto:

Definindo a pasta para o projeto.

No Editor que se abriu, vamos definir nossa primeira cena, a cena do Jogador. Clique em + ou em Other Node ou use as teclas de atalho Ctrl + A para selecionar o nó da sua cena. Vamos utilizar um CharacterBody2D:

Selecionando o nó CharacterBody2D para a cena do jogador.

Renomeie este nó como Player e salve a cena em uma sub-pasta scenes no seu projeto:

Salvando a cena do Player

Crie 2 nós filhos para nosso player, sendo um deles um AnimatedSprite2D e o outro um CollisionShape2D:

Incluíndo os nós filhos ao Player

Clique com o botão direito no FileSystem e crie uma nova pasta para os assets. No meu caso estou utilizando assets do Kenney porque que acho fantástico o trabalho que ele faz, mas vc pode usar os assets que achar melhor.

Você pode usar spritesheets com todos os movimentos ou usar imagens individuais para cada frame do movimento, vamos um spritesheet como esse aqui:

Asset do Kenney utilizado para o projeto

Dependendo dos assets que vc vá usar, você vai precisar de animações diferentes, vamos definir as animações de idle, jump, fall e walk. Sabe como fazer isso? Se não sabe, é facinho assim, óh:

  • Com o nó AnimatedSprite2D selecionado, clique na opção Sprite Frames no Inspector e selecione New SpriteFrames;
  • Clique novamente nesta opção para abrir o Editor de Animações;
  • Clique com o botão direito na animação default para alterar o nome para idle, selecione para adicionar os frames à partir de arquivos ou de spritesheets conforme sua opção;
  • Clique no ícone de um documento com um + verde para incluir novas animações e fazer o mesmo processo.

Você deve ficar com algo mais ou menos desse jeito:

Configure as animações do jogador

A animação de caminhada eu configurei como em loop e auto-start, as demais não terão loop no meu caso, mas isso vai variar de acordo com os assets que vc utilizar.

Configurei também o CollisionShape2D com o formato de cápsula, cobrindo a maior parte do meu Sprite:

Configure as colisões do jogador

Nosso próximo passo é definir o script de movimentos. Se você está utilizando a Godot 4 ou superior, vc pode usar um template com as definições básicas já definidas, o script padrão é esse aqui:

extends CharacterBody2D


const SPEED = 300.0
const JUMP_VELOCITY = -400.0

# Get the gravity from the project settings to be synced with RigidBody nodes.
var gravity = ProjectSettings.get_setting("physics/2d/default_gravity")


func _physics_process(delta):
	# Add the gravity.
	if not is_on_floor():
		velocity.y += gravity * delta

	# Handle jump.
	if Input.is_action_just_pressed("ui_accept") and is_on_floor():
		velocity.y = JUMP_VELOCITY

	# Get the input direction and handle the movement/deceleration.
	# As good practice, you should replace UI actions with custom gameplay actions.
	var direction = Input.get_axis("ui_left", "ui_right")
	if direction:
		velocity.x = direction * SPEED
	else:
		velocity.x = move_toward(velocity.x, 0, SPEED)

	move_and_slide()

Vamos começar utilizando este mesmo script, mas vou alterar as ações para ações personalizadas. Para isso, vamos abrir as configurações de projeto, pelo menu Project > Project Settings... E na tela que abre, selecionar a aba Input Map.

Vamos incluir as ações move_right, move_left e jump, desse jeitinho aí:

Mapeamento das ações versus teclas

Para o Player, por enquanto é isso. Agora vamos fazer os obstáculos e na sequência a gente vê se tem ajustes à fazer na cena do jogador, ok?

Para a cena dos obstáculos, vamos utilizar como Raiz o nó Area2D, tendo como filhos um Sprite2D e um CollisionShape2D, bastante similar ao que fizemos para o Player, também vou utilizar assets do Kenney. A nova cena ficará assim:

Cena dos obstáculos

Crie um script associado ao nó raiz da sua cena e conecte o sinal body_entered à tal script. Para conectar o sinal, selecione o nó raiz e no Inspector selecione a aba Node > Signals e dê um duplo clique no sinal correspondente. A seguinte tela será apresentada para indicar a função à conectar, por hora vou deixar o nome padrão:

Conectando o sinal body_entered ao nó do obstáculo

No script agora, temos uma função conectada ao sinal, precisamos incluir as regras do que deve acontecer quando houver a colisão entre o jogador e o obstáculo. Vamos fazer o mais simples... vamos remover o Player da cena, chamando a função nativa queue_free. O código vai ficar assim:

extends Area2D


func _on_body_entered(body):
	if body.name == "Player":
		body.queue_free()

Repita o procedimento para tantos obstáculos quanto ache necessário ou interessante, quanto mais tipos de obstáculos diferentes, maior será a diversidade de dificuldades que o jogador vai enfrentar.

O próximo passo é a parte mais importante do nosso jogo, que é a definição de como serão apresentados os obstáculos ao jogador. Existem várias opções diferentes:

  • Podemos ter algumas opções de disposições dos obstáculos em uma tela criados como "segmentos" e criar tais segmentos de forma sequencial para o jogador (de forma randomica ou não).
  • Podemos ter apenas ranges de posições pré-definidas para gerar os obstáculos na tela e tornar o posicionamento de tais elementos independente dos demais elementos da tela.
  • Podemos ter uma combinação das duas abordagens acima.

No nosso caso, vamos criar segmentos, estes segmentos serão criados todos com o tamanho completo da tela somente por simplificação, mas vc pode fazer de formas diferentes caso entenda que é melhor.

Cada segmento será criado como um Node2D e terá como filhos um ou mais StaticBody2D para o chão (caso queira ter múltiplos níveis de chão ou buracos, será necessário ter mais de um deste elemento) e como filhos deste, um Sprite2D e o CollisionShape2D correspondente. Além disso tantos obstáculos quanto ache necessário.

Criei um segmento inicial com um obstáculo só no final da tela:

Segmento criado com um chão contínuo e um obstáculo ao final da tela

Associe um script ao Node2D para a movimentação automática do segmento, emissão de um sinal para criação de um novo segmento e remoção do segmento quando ele já não estiver mais na cena. Você vai precisar criar umas variáveis e sinais para que funcione corretamente. O código ficou desse jeito aqui:

extends Node2D

signal generate_new_segment

@export var speed : float = 0

var signal_emitted : bool

func _ready():
	signal_emitted = false

func _process(delta):
	position.x -= speed * delta
	
	if !signal_emitted and position.x < 0:
		generate_new_segment.emit()
		signal_emitted = true
		
	if position.x <= -1200:
		queue_free()

Crie os demais segmentos associando o mesmo script, deixei ele genérico para que independente do segmento o comportamento seja sempre o mesmo. Estou criando mais 2 segmentos para ter alguma variação, mas você pode criar tantos quanto entenda interessante.

Agora vamos criar um Spawner para gerar os segmentos continuamente com base no sinal recebido dos segmentos criados abaixo dele. Este nó será do tipo Node, não terá nenhum filho e deve ter um script associado à ele com o seguinte código:

extends Node

@export var segments : Array[PackedScene]
@export var speed : float = 100

@onready var next_segment_index = 0


func _ready():
	var segment = segments[next_segment_index]
	var segment_instance = segment.instantiate()
	segment_instance.speed = speed
	segment_instance.generate_new_segment.connect(generate_new_segment)
	add_child(segment_instance)
	

func generate_new_segment():
	next_segment_index = randi_range(0, segments.size()-1)
	var segment = segments[next_segment_index]
	var segment_instance = segment.instantiate()
	segment_instance.position.x = 1152
	segment_instance.speed = speed
	segment_instance.generate_new_segment.connect(generate_new_segment)
	add_child(segment_instance)

Note que na função _ready temos o mesmo conteúdo da função generate_new_segment mudando apenas que vou sempre iniciar com o primeiro segmento que eu criei. Caso não queira que tal segmento seja apresentado posteriormente, basta mudar o range para não considerar o indice 0 (zero).

Perceba também que deixei a velocidade como uma variável exportável, assim é possível alterar a velocidade à partir do inspetor sem mexer no código novamente.

E a posição do novo segmento estou colocando logo após o final da tela, note que este ponto pode variar de acordo com o tamanho do segmento que vc está criando. Se forem segmentos menores que a tela, você terá que ajustar a posição dele e o momento em que será gerado o sinal para sua geração.

Defina no Inspetor os segmentos associados ao Spawner:

Associando os segmentos ao nó spanwer

E associe o Spawner e o Player ao nó Main:

Associando cena do jogador e do spawner ao nó main

Neste ponto você já tem as lógicas necessárias para gerar segmentos sequenciais, movimentar o jogador e os objetos da tela. Então, para quem não é registrado no site o tutorial fica por aqui.

Se você quer ver outros ajustes que fiz, como geração da pontuação, tela de game over e alteração das animações do jogador de acordo com as ações que está realizando, se registra aí embaixo. Aproveita que é gratuito!!!