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

De Aulas
 
(18 revisões intermediárias pelo mesmo usuário não estão sendo mostradas)
Linha 2: Linha 2:
 
Afluentes : [[Jogos Digitais]], [[Usabilidade, desenvolvimento web, mobile e jogos]]
 
Afluentes : [[Jogos Digitais]], [[Usabilidade, desenvolvimento web, mobile e jogos]]
  
= Assets =
+
= 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''.<center>[[Image:Godot skyfire.jpg|500px]]</center>
  
<center>[[Image:Godot skyfire.jpg|500px]]</center>
+
== Assets ==
 +
* [https://saulo.arisa.com.br/aulas/unisul/games/assets/skyfire_godot.zip Assets]
 +
* [https://github.com/saulopz/skyfire Projeto SkyFire no GitHub]
  
* [https://saulo.arisa.com.br/~saulo/aulas/unisul/games/assets/skyfire/skyfire.zip Assets]
+
= Estrutura =
* [https://github.com/saulopz/skyfire_godot3 Projeto SkyFire Godot 3 no GitHub]
+
Veja que:
  
= Estrutura =
+
* 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 <code>Sky</code> vou explicar um pouco melhor porque vamos precisar fazer um ''scrolling'' na imagem de fundo e vamos usar um recurso bem legal do Godot.
  
* BackGround (Node2D)
+
Abaixo temos a estrutura de Cenas e nós.
** A (Sprite)
+
* Sky (Node2D)
** B (Sprite)
+
** TextureRect
* Ship (KinematicBody2D)
+
* Ship (CharacterBody2D) [''script'']
** Sprite
+
** Sprite2D
** CollisionPolygon2D
 
* Enemy (KinematicBody2D)
 
** Sprite
 
 
** CollisionShape2D
 
** CollisionShape2D
* Bomb (KinematicBody2D)
+
* Enemy (CharacterBody2D) [''script'']
** Sprite
+
** Sprite2D
 
** CollisionShape2D
 
** CollisionShape2D
** SoundBomb (AudioStreamPlayer)
+
* Bomb (CharacterBody2D) [''script'']
* Explosion (StaticBody2D)
+
** Sprite2D
** AnimatedSprite
 
 
** CollisionShape2D
 
** CollisionShape2D
** SoundExplosion (AudioStreamPlayer)
+
** SoundBomb (AudioStreamPlayer2D)
** Nó (sinal) - ''_on_AnimatedSprite_finished()''
+
* Explosion (CharacterBody2D) [''script'']
* World (Node2D)
+
** AnimatedSprite2D - Quando estiver no editor de SpriteFrames, desabilitar a repetição em um botãozinho ao lado de <code>5 FPS</code>, conforme imagem abaixo.
** Background (instancia)
+
*** Nó (sinal) - ''_on_animated_sprite_2d_animation_finished()''
 +
** CollisionShape2D - Inspector... CollisionShape2D... disabled Ativo.
 +
** SoundExplosion (AudioStreamPlayer2D)
 +
* World (Node2D) [''script'']
 +
** Sky (instancia)
 
** Ship (instancia)
 
** Ship (instancia)
** EnemiTimer (Timer)
+
** EnemiTimer (Timer) - com Autostart ativo.
** Music (AudioStreamPlayer)
+
*** Nó (sinal) - ''_on_enemy_timer_timeout()''
** GameOver (TextureRect)
+
** Music (AudioStreamPlayer2D) - lembre de deixar em Autoplaying e em loop (na importação).
** Label (Label)
+
** GameOver (TextureRect) - Em inspector... CanvasItem... Visibility... desabilitar Visible
** Score (Label)
+
** 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.
** Nó (sinal) - ''_on_EnemyTimer_timeout()''
+
** Score (Label) - Altere a cor também e o tamanho da fonte.
 +
[[Arquivo:Godot skyfire explosion desabilitar repeticao.png|centro|miniaturadaimagem|328x328px|Desabilitando repetição na animação da explosão.]]
  
= Scripts =
+
== Cena Sky ==
 +
Vamos iniciar criando uma cena com um nó raiz do tipo <code>Node2D</code>. Renomeie esse nó para Sky e crie um nó filho do tipo <code>TextureRect</code>.
 +
[[Arquivo:Godot skyfire cena sky.png|centro|commoldura|Cena Sky.]]
 +
Clique no nó <code>TextureRect</code> e arraste a imagem <code>background.png</code> (dos nossos recursos do game) para a aba Inspector... RextureRect... Texture...
 +
[[Arquivo:Godot skyfire arrastar textura.png|centro|miniaturadaimagem|279x279px|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'''.
 +
[[Arquivo:Godot skyfire material.png|centro|commoldura|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'''.
 +
[[Arquivo:Godot skyfire material shader.png|centro|commoldura|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 <code>sky.gdshader</code>. Salve esse ''script'' e clique no shader criado (em baixo da esfera).
 +
[[Arquivo:Godot skyfire novo shader.png|centro|miniaturadaimagem|347x347px|Novo Shader.]]
 +
Ele vai abrir uma ferramenta (imagem abaixo) para editar o ''script'' do shader. Vamos alterar o ''script''.
 +
[[Arquivo:Godot skyfire shader editor.png|centro|miniaturadaimagem|623x623px|Editor de script do shader.]]
 +
Não vamos precisar das funções ''<code>fragment</code>'' e ''<code>ligth</code>''. Então vamos excluí-las.<syntaxhighlight lang="python">
 +
shader_type canvas_item;
  
== Background ==
+
void vertex() {
 +
UV.y -= TIME * 0.1;
 +
}
 +
</syntaxhighlight>Veja que na função ''<code>vertex</code>'', usamos o atributo UV.y e diminuímos ele conforme o TIME. Só que como fica muito rápido, multiplicamos por 0.1.
  
<syntaxhighlight lang=gdscript>
+
Por fim, para que a textura fique se repetindo, precisamos ir, ainda em CanvaItem, em Texture, selecionar Repeat e colocar '''Enabled'''.
extends Node2D
+
[[Arquivo:Godot skyfire texture repeat.png|centro|commoldura|Deixando a textura em loop.]]
 +
E pronto! Temos nosso fundo que fica se em modo ''scrolling''.
  
var speed = 100
+
= Scripts =
var screensize
+
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>.
var h
 
var half_h
 
 
 
func _ready():
 
screensize = get_viewport_rect().size
 
h = $A.texture.get_height()
 
half_h = h / 2
 
 
 
func _process(delta):
 
$A.position.y += speed * delta
 
$B.position.y += speed * delta
 
if $A.position.y - half_h > screensize.y:
 
$A.position.y -= h * 2
 
if $B.position.y - half_h > screensize.y:
 
$B.position.y -= h * 2
 
</syntaxhighlight>
 
  
 
== Ship ==
 
== Ship ==
  
<syntaxhighlight lang=gdscript>
+
<syntaxhighlight lang="gdscript">
extends KinematicBody2D
+
extends CharacterBody2D
  
var speed = 500
+
const type : String = "ship"
var screensize
+
var speed : int = 500     # velocidade para os lados
var h
+
var screensize : Vector2  # tamanho da tela
var half_w
+
var h : int              # altura da imagem
var half_h
+
var half_w : int          # metade da largura da imagem
 +
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 = $Sprite.texture.get_height()
+
h = $Sprite2D.texture.get_height()
half_w = $Sprite.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 88: 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))
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:
queue_free()
+
kill()
get_parent().new_explosion(position)
+
 
get_parent().game_over()
+
func kill() -> void:
 +
queue_free()
 +
get_parent().new_explosion(position)
 +
get_parent().game_over()
 
</syntaxhighlight>
 
</syntaxhighlight>
  
 
== Enemy ==
 
== Enemy ==
  
<syntaxhighlight lang=gdscript>
+
<syntaxhighlight lang="gdscript">
extends KinematicBody2D
+
extends CharacterBody2D
  
const LEFT = 0
+
const type : String = "enemy"
const RIGHT = 1
+
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
  
var screensize
+
func _ready() -> void:
var direction
 
var w
 
var h
 
var half_w
 
var half_h
 
var speed_y = 200
 
var speed_x = 300
 
 
 
func _ready():
 
 
randomize()
 
randomize()
 
screensize = get_viewport_rect().size
 
screensize = get_viewport_rect().size
w = $Sprite.texture.get_width()
+
w = $Sprite2D.texture.get_width()
h = $Sprite.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 137: Linha 162:
 
if position.x - half_w < 0:
 
if position.x - half_w < 0:
 
direction = RIGHT
 
direction = RIGHT
var info = move_and_collide(vec * delta)
+
var collision = move_and_collide(vec * delta)
if info:
+
if collision:
var obj = info.get_collider()
+
var entity = collision.get_collider()
if 'Ship' in obj.name:
+
if 'ship' in entity.type:
obj.queue_free()
+
kill()
queue_free()
+
entity.kill()
get_parent().new_explosion(obj.position)
+
# Se sair da tela, passou da tela pra baixo,
get_parent().new_explosion(position)
+
# o jogador perde ponto
get_parent().game_over()
 
 
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()
 +
 +
# 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()
 
</syntaxhighlight>
 
</syntaxhighlight>
  
 
== Bomb ==
 
== Bomb ==
  
<syntaxhighlight lang=gdscript>
+
<syntaxhighlight lang="gdscript">
extends KinematicBody2D
+
extends CharacterBody2D
  
var speed = 200
+
const type : String = "bomb"
 +
var speed : int = 200
  
func _ready():
+
func _ready() -> void:  
 
$SoundBomb.play()
 
$SoundBomb.play()
  
func _process(delta):
+
func _physics_process(delta: float) -> void:
var info = move_and_collide(Vector2(0, -speed) * delta)
+
var collision = move_and_collide(Vector2(0, -speed) * delta)
if info:
+
if collision:
var obj = info.get_collider()
+
var entity = collision.get_collider()
if 'Enemy' in obj.name:
+
if 'enemy' in entity.type:
obj.queue_free()
+
entity.kill()  
 
queue_free()
 
queue_free()
get_parent().new_explosion(obj.position)
 
 
get_parent().change_score(1)
 
get_parent().change_score(1)
 
if position.y < 0:
 
if position.y < 0:
Linha 178: Linha 209:
 
== Explosion ==
 
== Explosion ==
  
<syntaxhighlight lang=gdscript>
+
<syntaxhighlight lang="gdscript">
extends StaticBody2D
+
extends CharacterBody2D
  
func _ready():
+
func _ready() -> void:
$AnimatedSprite.play()
+
$AnimatedSprite2D.play()
 
$SoundExplosion.play()
 
$SoundExplosion.play()
  
func _on_AnimatedSprite_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 191: Linha 223:
 
== World ==
 
== World ==
  
<syntaxhighlight lang=gdscript>
+
<syntaxhighlight lang="gdscript">
 
extends Node2D
 
extends Node2D
  
var Ship = preload("res://Ship.tscn")
+
# Vamos precisar conhecer os objetos porque vamos
var Bomb = preload("res://Bomb.tscn")
+
# cria-los dinamicamente. Para isso, fazemos um
var Enemy = preload("res://Enemy.tscn")
+
# preload deles.
var Explosion = preload("res://Explosion.tscn")
+
const Ship = preload("res://ship.tscn")
var score = 0
+
const Bomb = preload("res://bomb.tscn")
var is_game_over = false
+
const Enemy = preload("res://enemy.tscn")
var screensize
+
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():
+
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():
score = 0
+
is_game_over = false                # setamos gameover para falso
$Score.text = str(score)
+
score = 0                           # zeramos a pontuacao
var ship = Ship.instance()
+
$Score.text = str(score)           # atualizamos o label
ship.position = Vector2(screensize.y - 20, screensize.x / 2)
+
var ship = Ship.instantiate()       # instanciamos uma nova nave
add_child(ship)
+
# e colocamos ela em uma posicao espedifica
$GameOver.visible = false
+
ship.position = Vector2(screensize.y - 10, screensize.x / 2)
is_game_over = false
+
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
  
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()          # paramos o timer de criar inimigos
  
func new_explosion(pos):
+
# Funcao usada para criar uma explosao em uma posicao (pos) especifica
var explosion = Explosion.instance()
+
# 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.instance()
+
# 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_EnemyTimer_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.instance()
+
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