Godot Engine: SkyFire

De Aulas

Afluentes : Jogos Digitais, Usabilidade, desenvolvimento web, mobile e jogos

Screenshot

Já vimos alguns games passo a passo, agora vamos tentar um desafio. A partir dos assets, da estrutura do projeto, os scripts e algumas dicas, vamos tentar criar o game SkyFire. Abaixo temos um screenshot.

Godot skyfire.jpg

Assets

Estrutura

Veja que:

  • todas as Cenas que possuem scripts coloquei uma tag [script] no nó que ele deve ser criado.
  • Depois de criado os scripts, em alguns nós da cena devemos criar sinais.
  • Crie os sinais só depois da estrutura e do script criados.
  • A Cena Sky vou explicar um pouco melhor porque vamos precisar fazer um scrolling na imagem de fundo e vamos usar um recurso bem legal do Godot.

Abaixo temos a estrutura de Cenas e nós.

  • Sky (Node2D)
    • TextureRect
  • Ship (CharacterBody2D) [script]
    • Sprite2D
    • CollisionShape2D
  • Enemy (CharacterBody2D) [script]
    • Sprite2D
    • CollisionShape2D
  • Bomb (CharacterBody2D) [script]
    • Sprite2D
    • CollisionShape2D
    • SoundBomb (AudioStreamPlayer2D)
  • Explosion (CharacterBody2D) [script]
    • AnimatedSprite2D - Quando estiver no editor de SpriteFrames, desabilitar a repetição em um botãozinho ao lado de 5 FPS, conforme imagem abaixo.
      • Nó (sinal) - _on_animated_sprite_2d_animation_finished()
    • CollisionShape2D - Inspector... CollisionShape2D... disabled Ativo.
    • SoundExplosion (AudioStreamPlayer2D)
  • World (Node2D) [script]
    • Sky (instancia)
    • Ship (instancia)
    • EnemiTimer (Timer) - com Autostart ativo.
      • Nó (sinal) - _on_enemy_timer_timeout()
    • Music (AudioStreamPlayer2D) - lembre de deixar em Autoplaying e em loop (na importação).
    • GameOver (TextureRect) - Em inspector... CanvasItem... Visibility... desabilitar Visible
    • Label (Label) - Alterar a cor do texto em Inspector... Control... Themes Overrides... Colors... selecionar Font Color. pode deixar preto mesmo ou alterar a cor. Podemos alterar Font Sizes para 30px.
    • Score (Label) - Altere a cor também e o tamanho da fonte.
Desabilitando repetição na animação da explosão.

Cena Sky

Vamos iniciar criando uma cena com um nó raiz do tipo Node2D. Renomeie esse nó para Sky e crie um nó filho do tipo TextureRect.

Cena Sky.

Clique no nó TextureRect e arraste a imagem background.png (dos nossos recursos do game) para a aba Inspector... RextureRect... Texture...

Arrastando a textura.

Ainda na aba Inspector, vamos em CamvasItem e então clique em Material, expandindo ele. Dentro tem um item chamado Material (também), clique do lado onde está [vazio] e adicione um Novo Shader Material.

Adicionando Material.

Vai aparecer uma esfera cinza. Clique nela e vai expandir. Em baixo vai aparecer um elemento chamado Shader que está como [vazio], clique ali e adicione um Novo Shader.

Novo Shader

Veja que ele vai pedir pra criar um script especial para esse shader. Como já renomeamos nosso nó raiz como Sky, ele vai salvar como sky.gdshader. Salve esse script e clique no shader criado (em baixo da esfera).

Novo Shader.

Ele vai abrir uma ferramenta (imagem abaixo) para editar o script do shader. Vamos alterar o script.

Editor de script do shader.

Não vamos precisar das funções fragment e ligth. Então vamos excluí-las.

shader_type canvas_item;

void vertex() {
	UV.y -= TIME * 0.1;
}

Veja que na função vertex, usamos o atributo UV.y e diminuímos ele conforme o TIME. Só que como fica muito rápido, multiplicamos por 0.1.

Por fim, para que a textura fique se repetindo, precisamos ir, ainda em CanvaItem, em Texture, selecionar Repeat e colocar Enabled.

Deixando a textura em loop.

E pronto! Temos nosso fundo que fica se em modo scrolling.

Scripts

Nesse exemplo, nos scripts criamos uma constante chamada type. Isso se deve ao motivo de que estamos criando nossos inimigos em tempo de execução e eles surgem com nomes doidos que o Godot dá. Dessa forma, na hora de detectar o tipo de objeto que colidimos, não testaremos mais o name, mas sim o type.

Ship

extends CharacterBody2D

const type : String = "ship"
var speed : int = 500     # velocidade para os lados
var screensize : Vector2  # tamanho da tela
var h : int               # altura da imagem
var half_w : int          # metade da largura da imagem
var half_h : int          # metade da altura da imagem

# Quando o personagem eh criado, ele eh colocado no
# meio da tela, so que temos um deslocamento para a esquerda
# contando metade da textura da nave, senao a nave fica meio
# para a direita, por isso precisamos do half_w
func _ready() -> void:
	screensize = get_viewport_rect().size
	h = $Sprite2D.texture.get_height()
	half_w = $Sprite2D.texture.get_width() / 2
	half_h = int(h / 2.0)

func _process(delta) -> void:
	var vec = Vector2()
	if Input.is_action_pressed("ui_left"):
		vec.x -= speed
	elif Input.is_action_pressed("ui_right"):
		vec.x += speed
	# Quando pressiomamos a barra de espaco, damos um tiro. Isso
	# na verdade cria um objeto bomba em cima do personagem.
	if Input.is_action_just_pressed("ui_select"):
		get_parent().new_bomb(Vector2(position.x, position.y - half_h - 20))
	var _info = move_and_collide(vec * delta)
	# Se vai sair da tela, volta
	position.x = clamp(position.x, half_w, screensize.x - half_w)
	# Se pontuacao negativa, morre
	if get_parent().score < 0:
		kill()

func kill() -> void:
	queue_free()
	get_parent().new_explosion(position)
	get_parent().game_over()

Enemy

extends CharacterBody2D

const type : String = "enemy"
const LEFT : int = 0
const RIGHT : int = 1
var screensize : Vector2  # tamanho da tela
var direction : int       # direcao do inimigo [esquerda|direita]
var w : int               # largura da imagem
var h : int               # altura da imagem
var half_w : int          # metade da largura da imagem
var half_h : int          # metade da altura da imagem
var speed_y : int = 200   # velocidade para baixo
var speed_x : int = 300   # velocidade para os lados

func _ready() -> void:
	randomize()
	screensize = get_viewport_rect().size
	w = $Sprite2D.texture.get_width()
	h = $Sprite2D .texture.get_height()
	half_w = int(w / 2.0)
	half_h = int(h / 2.0)
	# Criamos nosso inimigo acima da area visivel do jogo
	# com posicao e direcao aleatorias
	position.x = (randi() % int(screensize.x - w)) + half_w
	position.y = -100
	direction = randi() % 2
	
func _process(delta) -> void:
	var vec = Vector2()
	vec.y += speed_y
	# Se o inimigo encosta dos lados da tela, ele muda a
	# o movimento para a direcao oposta
	if direction == RIGHT:
		vec.x += speed_x
		if position.x + half_w > screensize.x:
			direction = LEFT
	elif direction == LEFT:
		vec.x -= speed_x
		if position.x - half_w < 0:
			direction = RIGHT
	var collision = move_and_collide(vec * delta)
	if collision:
		var entity = collision.get_collider()
		if 'ship' in entity.type:
			kill()
			entity.kill()
	# Se sair da tela, passou da tela pra baixo,
	# o jogador perde ponto
	if position.y - half_h > screensize.y:
		get_parent().change_score(-1)
		queue_free()
	# Se o jogo acabou, libera o inimigo
	if get_parent().is_game_over:
		queue_free()

# Se o inimigo morrer, cria uma explosao nessa mesma posicao
# e retira o inimigo do jogo com queue_free.
func kill() -> void:
	get_parent().new_explosion(position)
	queue_free()

Bomb

extends CharacterBody2D

const type : String = "bomb"
var speed : int = 200

func _ready() -> void: 
	$SoundBomb.play()

func _physics_process(delta: float) -> void:
	var collision = move_and_collide(Vector2(0, -speed) * delta)
	if collision:
		var entity = collision.get_collider()
		if 'enemy' in entity.type:
			entity.kill() 
			queue_free()
			get_parent().change_score(1)
	if position.y < 0:
		queue_free()

Explosion

extends CharacterBody2D

func _ready() -> void:
	$AnimatedSprite2D.play()
	$SoundExplosion.play()

# Quando termina a animacao cai fora do jogo
func _on_animated_sprite_2d_animation_finished() -> void:
	queue_free()

World

extends Node2D

# Vamos precisar conhecer os objetos porque vamos
# cria-los dinamicamente. Para isso, fazemos um
# preload deles.
const Ship = preload("res://ship.tscn")
const Bomb = preload("res://bomb.tscn")
const Enemy = preload("res://enemy.tscn")
const Explosion = preload("res://explosion.tscn")
var score: int = 0              # pontuacao
var is_game_over: bool = false  # se o jogo terminou
var screensize: Vector2         # tamanho da tela

func _ready() -> void:
	screensize = get_viewport_rect().size

func _physics_process(_delta: float) -> void:
	# Se for gameover e pressionar esc, o jogo reinicia
	if is_game_over:
		if Input.is_action_just_pressed("ui_cancel"):
			_reset()

# Reinicializacao do jogo
func _reset():
	is_game_over = false                # setamos gameover para falso
	score = 0                           # zeramos a pontuacao
	$Score.text = str(score)            # atualizamos o label
	var ship = Ship.instantiate()       # instanciamos uma nova nave
	# e colocamos ela em uma posicao espedifica
	ship.position = Vector2(screensize.y - 10, screensize.x / 2)
	add_child(ship)                     # adicionamos ela ao world
	$GameOver.visible = false           # imagem de gameover invisivel
	$Music.play()                       # tocamos a musica
	$EnemyTimer.start()                 # iniciamos o timer para criar inimigos

# Alteramos a pontuacao
func change_score(pts: int):
	score += pts
	$Score.text = str(score)

func game_over() -> void:
	is_game_over = true         # gameover true
	$GameOver.visible = true    # mostramos a imagem gameover
	$Music.stop()               # paramos a musica
	$EnemyTimer.stop()          # paramos o timer de criar inimigos

# Funcao usada para criar uma explosao em uma posicao (pos) especifica
# normamente vai ser a propria posicao do elemento que morreu
func new_explosion(pos: Vector2):
	var explosion = Explosion.instantiate() # instanciamos um objeto Explosion
	explosion.position = pos                # colocamos ele na posicao
	add_child(explosion)                    # adicionamos ao jogo

# Cria uma bombinha. Normalmente feito pela nave Ship
# Entao usamos a posicao relativa a posicao x do personagem
# mais a metada da largura da imagem de Ship e menos a metade
# da largura da imagem da bomba. Esse calculo fazemos la no
# script de Ship. Isso para que a bombinha fique centralizada
# tambem colocamos, y acima da nave.
func new_bomb(pos: Vector2) -> void:
	var bomb = Bomb.instantiate()  # Instanciamos um objeto bomba
	bomb.position = pos            # colocamos ele na posicao
	add_child(bomb)                # adicinamos ela ao jogo

# Quando o timer de criacao do inimigo der timeout
func _on_enemy_timer_timeout() -> void:
	if is_game_over:                           # se gameover nao cria mais
		return
	var enemy = Enemy.instantiate()            # instancia um novo inimigo
	add_child(enemy)                           # adiciona ele ao jogo
	$EnemyTimer.wait_time = (randi() % 2) + 1  # altera o wait_time aleatoriamente
	$EnemyTimer.start()                        # reinicia o timer