Mudanças entre as edições de "Godot Engine: SkyFire"

De Aulas
 
(5 revisões intermediárias pelo mesmo usuário não estão sendo mostradas)
Linha 1: Linha 1:
 
 
 
 
 
 
 
 
 
  
 
Afluentes : [[Jogos Digitais]], [[Usabilidade, desenvolvimento web, mobile e jogos]]
 
Afluentes : [[Jogos Digitais]], [[Usabilidade, desenvolvimento web, mobile e jogos]]
Linha 15: Linha 6:
  
 
== Assets ==
 
== Assets ==
* [https://saulo.arisa.com.br/~saulo/aulas/unisul/games/assets/skyfire/skyfire.zip Assets]
+
* [https://saulo.arisa.com.br/aulas/unisul/games/assets/skyfire_godot.zip Assets]
* [https://github.com/saulopz/skyfire_godot3 Projeto SkyFire Godot 3 no GitHub]
+
* [https://github.com/saulopz/skyfire Projeto SkyFire no GitHub]
  
 
= Estrutura =
 
= Estrutura =
Linha 81: Linha 72:
  
 
= Scripts =
 
= Scripts =
 +
Nesse exemplo, nos ''scripts'' criamos uma constante chamada <code>type</code>. 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 <code>name</code>, mas sim o <code>type</code>.
  
 
== Ship ==
 
== Ship ==
Linha 87: Linha 79:
 
extends CharacterBody2D
 
extends CharacterBody2D
  
const type = 'ship'
+
const type : String = "ship"
var speed = 500
+
var speed : int = 500     # velocidade para os lados
var screensize
+
var screensize : Vector2  # tamanho da tela
var h
+
var h : int              # altura da imagem
var half_w
+
var half_w : int          # metade da largura da imagem
var half_h
+
var half_h : int          # metade da altura da imagem
  
func _ready():
+
# 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
 
screensize = get_viewport_rect().size
 
h = $Sprite2D.texture.get_height()
 
h = $Sprite2D.texture.get_height()
 
half_w = $Sprite2D.texture.get_width() / 2
 
half_w = $Sprite2D.texture.get_width() / 2
half_h = h / 2
+
half_h = int(h / 2.0)
  
func _process(delta):
+
func _process(delta) -> void:
 
var vec = Vector2()
 
var vec = Vector2()
 
if Input.is_action_pressed("ui_left"):
 
if Input.is_action_pressed("ui_left"):
Linha 106: Linha 102:
 
elif Input.is_action_pressed("ui_right"):
 
elif Input.is_action_pressed("ui_right"):
 
vec.x += speed
 
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"):
 
if Input.is_action_just_pressed("ui_select"):
 
get_parent().new_bomb(Vector2(position.x, position.y - half_h - 20))
 
get_parent().new_bomb(Vector2(position.x, position.y - half_h - 20))
 
var _info = move_and_collide(vec * delta)
 
var _info = move_and_collide(vec * delta)
 +
# Se vai sair da tela, volta
 
position.x = clamp(position.x, half_w, screensize.x - half_w)
 
position.x = clamp(position.x, half_w, screensize.x - half_w)
 +
# Se pontuacao negativa, morre
 
if get_parent().score < 0:
 
if get_parent().score < 0:
 
kill()
 
kill()
  
func kill():
+
func kill() -> void:
 
queue_free()
 
queue_free()
 
get_parent().new_explosion(position)
 
get_parent().new_explosion(position)
Linha 124: Linha 124:
 
extends CharacterBody2D
 
extends CharacterBody2D
  
const type = 'enemy'
+
const type : String = "enemy"
const LEFT = 0
+
const LEFT : int = 0
const RIGHT = 1
+
const RIGHT : int = 1
 
+
var screensize : Vector2  # tamanho da tela
var screensize
+
var direction : int      # direcao do inimigo [esquerda|direita]
var direction
+
var w : int              # largura da imagem
var w
+
var h : int              # altura da imagem
var h
+
var half_w : int          # metade da largura da imagem
var half_w
+
var half_h : int          # metade da altura da imagem
var half_h
+
var speed_y : int = 200   # velocidade para baixo
var speed_y = 200
+
var speed_x : int = 300   # velocidade para os lados
var speed_x = 300
 
  
func _ready():
+
func _ready() -> void:
 
randomize()
 
randomize()
 
screensize = get_viewport_rect().size
 
screensize = get_viewport_rect().size
 
w = $Sprite2D.texture.get_width()
 
w = $Sprite2D.texture.get_width()
 
h = $Sprite2D .texture.get_height()
 
h = $Sprite2D .texture.get_height()
half_w = w / 2
+
half_w = int(w / 2.0)
half_h = h / 2
+
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.x = (randi() % int(screensize.x - w)) + half_w
 
position.y = -100
 
position.y = -100
 
direction = randi() % 2
 
direction = randi() % 2
 
 
func _process(delta):
+
func _process(delta) -> void:
 
var vec = Vector2()
 
var vec = Vector2()
 
vec.y += speed_y
 
vec.y += speed_y
 +
# Se o inimigo encosta dos lados da tela, ele muda a
 +
# o movimento para a direcao oposta
 
if direction == RIGHT:
 
if direction == RIGHT:
 
vec.x += speed_x
 
vec.x += speed_x
Linha 165: Linha 168:
 
kill()
 
kill()
 
entity.kill()
 
entity.kill()
 +
# Se sair da tela, passou da tela pra baixo,
 +
# o jogador perde ponto
 
if position.y - half_h > screensize.y:
 
if position.y - half_h > screensize.y:
 
get_parent().change_score(-1)
 
get_parent().change_score(-1)
 
queue_free()
 
queue_free()
 +
# Se o jogo acabou, libera o inimigo
 
if get_parent().is_game_over:
 
if get_parent().is_game_over:
 
queue_free()
 
queue_free()
  
func kill():
+
# 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)
 
get_parent().new_explosion(position)
 
queue_free()
 
queue_free()
Linha 181: Linha 189:
 
extends CharacterBody2D
 
extends CharacterBody2D
  
const type = 'bomb'
+
const type : String = "bomb"
var speed = 200
+
var speed : int = 200
  
func _ready():  
+
func _ready() -> void:  
 
$SoundBomb.play()
 
$SoundBomb.play()
  
func _process(delta):
+
func _physics_process(delta: float) -> void:
 
var collision = move_and_collide(Vector2(0, -speed) * delta)
 
var collision = move_and_collide(Vector2(0, -speed) * delta)
 
if collision:
 
if collision:
Linha 202: Linha 210:
  
 
<syntaxhighlight lang="gdscript">
 
<syntaxhighlight lang="gdscript">
extends StaticBody2D
+
extends CharacterBody2D
  
const type = 'explosion'
+
func _ready() -> void:
 
 
func _ready():
 
 
$AnimatedSprite2D.play()
 
$AnimatedSprite2D.play()
 
$SoundExplosion.play()
 
$SoundExplosion.play()
  
func _on_animated_sprite_2d_animation_finished():
+
# Quando termina a animacao cai fora do jogo
 +
func _on_animated_sprite_2d_animation_finished() -> void:
 
queue_free()
 
queue_free()
 
</syntaxhighlight>
 
</syntaxhighlight>
Linha 219: Linha 226:
 
extends Node2D
 
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 Ship = preload("res://ship.tscn")
 
const Bomb = preload("res://bomb.tscn")
 
const Bomb = preload("res://bomb.tscn")
 
const Enemy = preload("res://enemy.tscn")
 
const Enemy = preload("res://enemy.tscn")
 
const Explosion = preload("res://explosion.tscn")
 
const Explosion = preload("res://explosion.tscn")
var score = 0
+
var score: int = 0             # pontuacao
var is_game_over = false
+
var is_game_over: bool = false # se o jogo terminou
var screensize
+
var screensize: Vector2        # tamanho da tela
  
func _ready():
+
func _ready() -> void:
 
screensize = get_viewport_rect().size
 
screensize = get_viewport_rect().size
  
func _process(_delta):
+
func _physics_process(_delta: float) -> void:
 +
# Se for gameover e pressionar esc, o jogo reinicia
 
if is_game_over:
 
if is_game_over:
if Input.is_action_just_pressed("ui_accept"):
+
if Input.is_action_just_pressed("ui_cancel"):
 
_reset()
 
_reset()
  
 +
# Reinicializacao do jogo
 
func _reset():
 
func _reset():
is_game_over = false
+
is_game_over = false               # setamos gameover para falso
score = 0
+
score = 0                           # zeramos a pontuacao
$Score.text = str(score)
+
$Score.text = str(score)           # atualizamos o label
var ship = Ship.instantiate()
+
var ship = Ship.instantiate()       # instanciamos uma nova nave
 +
# e colocamos ela em uma posicao espedifica
 
ship.position = Vector2(screensize.y - 10, screensize.x / 2)
 
ship.position = Vector2(screensize.y - 10, screensize.x / 2)
add_child(ship)
+
add_child(ship)                     # adicionamos ela ao world
$GameOver.visible = false
+
$GameOver.visible = false           # imagem de gameover invisivel
$Music.play()
+
$Music.play()                       # tocamos a musica
$EnemyTimer.start()
+
$EnemyTimer.start()                 # iniciamos o timer para criar inimigos
  
func change_score(pts):
+
# Alteramos a pontuacao
 +
func change_score(pts: int):
 
score += pts
 
score += pts
 
$Score.text = str(score)
 
$Score.text = str(score)
  
func game_over():
+
func game_over() -> void:
is_game_over = true
+
is_game_over = true        # gameover true
$GameOver.visible = true
+
$GameOver.visible = true   # mostramos a imagem gameover
$Music.stop()
+
$Music.stop()               # paramos a musica
$EnemyTimer.stop()
+
$EnemyTimer.stop()         # paramos o timer de criar inimigos
  
func new_explosion(pos):
+
# Funcao usada para criar uma explosao em uma posicao (pos) especifica
var explosion = Explosion.instantiate()
+
# normamente vai ser a propria posicao do elemento que morreu
explosion.position = pos
+
func new_explosion(pos: Vector2):
add_child(explosion)
+
var explosion = Explosion.instantiate() # instanciamos um objeto Explosion
 +
explosion.position = pos               # colocamos ele na posicao
 +
add_child(explosion)                   # adicionamos ao jogo
  
func new_bomb(pos):
+
# Cria uma bombinha. Normalmente feito pela nave Ship
var bomb = Bomb.instantiate()
+
# Entao usamos a posicao relativa a posicao x do personagem
bomb.position = pos
+
# mais a metada da largura da imagem de Ship e menos a metade
add_child(bomb)
+
# 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
  
func _on_enemy_timer_timeout():
+
# Quando o timer de criacao do inimigo der timeout
if is_game_over:
+
func _on_enemy_timer_timeout() -> void:
 +
if is_game_over:                           # se gameover nao cria mais
 
return
 
return
var enemy = Enemy.instantiate()
+
var enemy = Enemy.instantiate()           # instancia um novo inimigo
add_child(enemy)
+
add_child(enemy)                           # adiciona ele ao jogo
$EnemyTimer.wait_time = (randi() % 2) + 1
+
$EnemyTimer.wait_time = (randi() % 2) + 1 # altera o wait_time aleatoriamente
$EnemyTimer.start()
+
$EnemyTimer.start()                       # reinicia o timer
 
</syntaxhighlight>
 
</syntaxhighlight>

Edição atual tal como às 18h15min de 24 de setembro de 2024

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