Godot

De castillowiki
Ir a la navegación Ir a la búsqueda

Godot és un motor de jocs amb IDE per a programar-los. Si vols aprendre Godot, el millor és anar al manual oficial i després anar fent exemples i tutorials. Aquest article és un tutorial més, adaptat més o menys al nivell de 2on de la ESO i pensat per a que siga didàctic per a gent que no ha programat abans.

El teu primer joc

Aquest article (en construcció), és una simplificació i traducció del manual oficial. Si ja eres experimentat i tens soltura en l'anglés, et recomane l'original
Aquest és el resultat final del joc (captura extreta del manual oficial)

Anem a fer el joc per fases i explicarem el perquè de cadascuna.

Configuració inicial

El primer que tenim que fer és crear el projecte. Ens tenim que descarregar els recursos de la web oficial en aquest enllaç. Després, creem un projecte nou i descomprimim els recursos en la carpeta del projecte.

Observa que la carpeta del projecte té dins la dels recursos.

Dins de godot, tenim que fer que la pantalla tinga unes dimensions determinades de 480x720 i crear el jugador com un node Area2D amb nom Player:

Creació del projecte
Els projectes de godot estan dins d'una carpeta que creem quan li donem nom. Podem seleccionar la ruta on estarà. El projecte té un arxiu principal project.godot i altres .tscn que són les escenes.
Una escena en Godot és qualsevol element del joc que té entitat pròpia. En el nostre cas el jugador, els enemics i la pantalla principal. La creació del player és la primera escena que anem a fer i quan es guarde tindrà un arxiu de nom Player.tscn.

L'escena del Player

Els personatges i objectes dels jocs en 2D es poden fer en qualsevol dels nodes de la secció de 2D. Cal triar bé quin utilitzem. Per exemple, el Area2D del Player és un node que detecta col·lisions i li afecten aspectes de la física del joc. El Area2D pot ser controlat per jugador amb el teclat.

Animació

A continuació cal crear l'animació del node Player. En aquest cas, triem un AnimatedSprite al que donarem varis moviments. Amb l'opció Frames.

Creació del AnimatedSprite
En aquesta captura es veu cóm afegir el moviment rigthi up

Per últim, el farem un poc més menut:

Escalar el sprite al 50%

La forma de col·lisió

Ara toca definir la forma per la qual detectarà les col·lisions. En el nostre cas, la que més es sembla és la càpsula:

Creació de la CollisionShape. Observa que sols manipulem els punts interiors de la forma i no el rectangle exterior. Això és important.
Els personatges en 2D poden tindre formes molt complexes i detectar quan toquen amb altres personatges pot ser molt costós computacionalment. Per tant, es tracta de simplificar aquestes formes en altres més simples com són les formes geomètriques. Mentre que una forma en mapa de bits necessitaria comprovar cada píxel amb cada píxel de l'altre node, una forma geomètrica pot comprovar la col·lisió amb una fórmula matemàtica a partir de les dimensions i coordenades.

És important que, una vegada estan tots els nodes fills creats, bloquegem al node Player de manera que no es puga seleccionar nodes fills. Això es fa en aquest botó:

Bloquejar el node pare per a no poder manipular els fills.

El moviment del Player

Afegir un script a un node.

El nostre jugador es mourà amb les fletxes del teclat. Per aconseguir-ho, tenim que crear un script per al node que detecte el teclat i canvie la posició del node en la pantalla. Aquest és l'script:

extends Area2D

export (int) var speed  # velocita del jugador (pixels/sec).
var screensize  # mida de la pantalla

func _ready():
    screensize = get_viewport_rect().size

func _process(delta):
    var velocity = Vector2() # Vector de velocitat (x,y)
    if Input.is_action_pressed("ui_right"):
        velocity.x += 1
    if Input.is_action_pressed("ui_left"):
        velocity.x -= 1
    if Input.is_action_pressed("ui_down"):
        velocity.y += 1
    if Input.is_action_pressed("ui_up"):
        velocity.y -= 1
    if velocity.length() > 0:
        velocity = velocity.normalized() * speed
        $AnimatedSprite.play() # Si hi ha moviment es mou
    else:
        $AnimatedSprite.stop() # Si no hi ha moviment para l'animació
    position += velocity * delta # Calcula la posició
    position.x = clamp(position.x, 0, screensize.x)
    position.y = clamp(position.y, 0, screensize.y)

    if velocity.x != 0:                                        # Si es mou de costat
     $AnimatedSprite.animation = "right"  
     $AnimatedSprite.flip_v = false
     $AnimatedSprite.flip_h = velocity.x < 0     # Dreta o esquerra
    elif velocity.y != 0:                                    # Si es mou en vertical
     $AnimatedSprite.animation = "up"
     $AnimatedSprite.flip_v = velocity.y > 0     # Dalt o baix

Els enemics

Els enemics han d'estar en una altra escena. Cal crear un RigidBody2D i crear aquest arbre:

Node Mob (enemic) amb tots les seus nodes fills

I cal afegir algunes propietats al node:

Propietats de l'enemic.

D'aquesta manera als enemics no els afecta la gravetat, i no col·lisionen entre ells.

Cal afegir els sprites corresponents a l'enemic:

Els sprites es fan igual que els del player.

Per últim, els donem una escala de 0.75 i configurem la seua CollisionShape2D per a ser una càpsula com el player, però amb una rotació de 90 graus:

La forma de col·lisió.

L'script dels enemics Fes un script per al Mob i pega aquest codi:

extends RigidBody2D

export (int) var min_speed # Minimum speed range.
export (int) var max_speed # Maximum speed range.
var mob_types = ["walk", "swim", "fly"]

func _ready():
    $AnimatedSprite.animation = mob_types[randi() % mob_types.size()]

Recorda omplir les noves propietats min_speed i max_speed amb 150 i 250 respectivament.

Ara, cal connectar l'esdeveniment de que l'enemic a abandone la pantalla en una funció que elimine a l'enemic per a que no ocupe memòria.

Quan l'enemic ix de la pantalla, s'esborra.

La pantalla principal

Fins al moment, hem creat el jugador i l'enemic i ara falta anar juntant-ho tot en la pantalla principal que implementarà les regles del joc.

Creació de l'escena principal.

Cal crear els nodes per a tindre:

El main es un node, el primer de la llista.

Cal configurar els Timers de manera que el WaitTime quede així:

  • ModTimer: 0.5
  • ScoreTimer: 1
  • StartTimer: 2 OneShot: On
  • En StartPosition cal ficar en Position 240 x 450

Ara hi ha que definir la posició inicial dels enemics. Com poden eixir per qualsevol costat de la pantalla, cal crear un Path2D i després seleccionar una posició aleatòria en el path. De moment, anem a dibuixar el path:

Creació del path i dibuix del seu camí

A continuació, cal afegir un PathFollow2D i modificar el nom:

Creació del path i dibuix del seu camí

Després, anem a afegir aquest script a la pantalla principal:

extends Node

export (PackedScene) var Mob   # Afegir l'escena dels enemics
var score              # La variable de la puntuació

func _ready():
    randomize()
Cóm afegir el codi anterior.

I ara cal afegir a la nova variable exportada Mob l'escena dels enemics:

Observa cóm arrosseguem l'escena a la variable.

Les col·lisions

Anem a preparar el joc per detectar col·lisions.

El primer que tenim que fer és afegir la capacitat de enviar una senyal de colisió "hit":

Afegir la senyal hit al Player per que puga indicar quan ha col·lisionat.

D'aquesta manera, el protagonista té un esdeveniment nou:

Ara es veu cóm té la senyal hit

Com que els enemics són Rigidbody2D, anem a detectar quan entra un 'body':

Cal enllaçat l'esdeveniment Body Entered amb una funció que emeta la senyal hit, amague al Player i deshabilite la collisionShape de manera que no detecte altres col·lisions.

Ací tens el codi que pots pegar:

    hide() # El jugador desapareix quan el toquen
    emit_signal("hit") # El jugador emet la senyal hit
    $CollisionShape2D.disabled = true  # Es deshabilita la forma de la col·lisió per no seguir detectant.

També pots pegar aquest codi que es pot cridar per a iniciar al jugador després:

func start(pos):
    position = pos     # Situar al jugador en la posició inicial
    show()               # Mostrar al jugador
    $CollisionShape2D.disabled = false   # tornar a hablitat la la forma de la col·lisió

La lògica del joc

Fins al moment, hem creat per separat als enemics, al player y la pantalla principal. En l'anterior exercici hem començat a ajuntar-ho tot. En aquest ja anem a donar-li una relació entre la pantalla i els altres elements i veure cóm interactuen en col·lisions i moviment del enemics.

Afegeix a l'script del main aquest codi:

func game_over():
    $ScoreTimer.stop() # Quan el joc acaba, es paren els temporitzadors
    $MobTimer.stop()

func new_game():
    score = 0
    $Player.start($StartPosition.position) # Crida a la funció start de player
    $StartTimer.start()

Cal connectar la senyal hit() del player amb la funció game_over() del Main:

A l'hora de seleccionar la funció, escrivim game_over() i li diguem connectar.

També anem a afegir la lògica dels temporitzadors. Però per a fer-ho, cal connectar el timeout() dels temporitzadors en la seua funció:

Observa cóm fent doble click, podem enllaçar l'esdeveniment timeout() a funcions a l'script del Main.

I aquest és el codi que han de tindre:

func _on_StartTimer_timeout():
    $MobTimer.start()  # començar els temporitzadors de l'enemic i de la puntació
    $ScoreTimer.start()

func _on_ScoreTimer_timeout():
    score += 1  # Cada segon augmenta en 1 la puntuació

A continuació, anem a fer que apareguen els enemics amb el temporitzador ModTimer:

Observa cóm fent doble click, podem enllaçar l'esdeveniment timeout() a funcions a l'script del Main.

I sols cal afegir aquest codi:

# Elegir una posició aleatòria en el Modpath per a que comencen els enemics
    $MobPath/MobSpawnLocation.set_offset(randi())
# Crear un enemic (mob) nou
    var mob = Mob.instance()
    add_child(mob)
# Fer que l'enemic tinga una direcció cap a dins de la pantalla
    var direction = $MobPath/MobSpawnLocation.rotation + PI / 2
# Posar l'enemic a una posició aleatoria del path
    mob.position = $MobPath/MobSpawnLocation.position
# Afegir certa aleatorietat a la direcció de l'enemic
    direction += rand_range(-PI / 4, PI / 4)
    mob.rotation = direction
    # Triar una velocitat aleatoria per a l'enemic
    mob.set_linear_velocity(Vector2(rand_range(mob.min_speed, mob.max_speed), 0).rotated(direction))

Per a que funcione el joc, podem afegir aquest codi al main.gb:

func _process(delta):
     if Input.is_action_pressed("ui_accept"):
        new_game()
Aquesta última funció sols la deixarem per a provar que el joc funciona fins ací. Per a la última fase de desenvolupament del joc la deguem de llevar.

La interfície d'usuari

El que queda és fer que l'usuari veja els punts o missatges d'inici del joc.

Per a fer això, cal crear una nova escena amb un CanvasLayer anomenat HUD (heads-up display). Aquesta permet afegir lletres per damunt del joc sense interferir. Aquest HUD informarà de la puntuació, de quan comença o acaba el joc i tindrà un botó per començar.

Creació del HUD

Els elements d'interfície gràfica que anem a utilitzar són el label i button. Per tant, cal crear:

  • Label anomenat ScoreLabel.
  • Label anomenat MessageLabel.
  • Button anomenat StartButton.
  • Timer anomenat MessageTimer.
Nodes del HUD.

Es poden arrossegar els elements dirèctament en la pantalla. Si volem millor ajust, es recomana aquests paràmetres:

ScoreLabel
   Layout: “Center Top”
   Margin:
       Left: -25
       Top: 0
       Right: 25
       Bottom: 100
   Text: 0
MessageLabel
   Layout: “Center”
   Margin:
       Left: -200
       Top: -150
       Right: 200
       Bottom: 0
   Text: Esquiva els Creeps
StartButton
   Layout: “Center Bottom”
   Margin:
       Left: -100
       Top: -200
       Right: 100
       Bottom: -100
   Text: Start

També cal modificar la font per a mostrar millor les lletres. Al directori descarregat trobarem la font Xolonium-Regular.ttf:

Afegir la tipografia i modificar la mida de la lletra.

Ara cal afegir aquest script al HUD:

extends CanvasLayer

signal start_game

func show_message(text):
    $MessageLabel.text = text
    $MessageLabel.show()
    $MessageTimer.start()

func show_game_over():
    show_message("Game Over")
    yield($MessageTimer, "timeout")
    $StartButton.show()
    $MessageLabel.text = "Dodge the\nCreeps!"
    $MessageLabel.show()

func update_score(score):
    $ScoreLabel.text = str(score)

func _on_StartButton_pressed():
    $StartButton.hide()
    emit_signal("start_game")

func _on_MessageTimer_timeout():
    $MessageLabel.hide()
Crear l'script del HUD.

Cal modificar certs paràmetres:

  • El messageTimer ha d'estar amb Wait Time = 2 i OneShot activat.
  • Cal connectar el timeout() de MessageTimer amb la funció _on_MessageTimer_Timeout() i l'esdeveniment pressed() del startButton a la seua funció:
Associar els esdeveniments a les funcions.

Connectar el HUD a Main

Ara cal fer que la pantalla principal (Main) puga interactuar i utilitzar el HUD. Cal instanciar-la com hem fet en el Player:

Instanciant el HUD com a fill del Main.

Ara cal connectar-ho tot. En primer lloc, connectem la senyal start_game a la funció new_game() del main:

Instanciant el HUD com a fill del Main.

Afegir aquest codi a la funció:

  $HUD.update_score(score)
  $HUD.show_message("Get Ready")

També afegim açò a game_over():

  $HUD.show_game_over()

I açò a _on_ScoreTimer_timeout():

  $HUD.update_score(score)
El resultat de pegar els codis anteriors a les funcions corresponents.

Codi final

Aquests són els codis de tots els scripts que té el joc al final:

Main:

extends Node

export (PackedScene) var Mob
var score

func _ready():
    randomize()

#func _process(delta):
	 #if Input.is_action_pressed("ui_accept"):
	  #  new_game()


func game_over():
    $ScoreTimer.stop() # Quan el joc acaba, es paren els temporitzadors
    $MobTimer.stop()
    $HUD.show_game_over()
	
func new_game():
    score = 0
    $Player.start($StartPosition.position) # Crida a la funció start de player
    $StartTimer.start()
    $HUD.update_score(score)
    $HUD.show_message("Get Ready")

func _on_StartTimer_timeout():
    $MobTimer.start()  # començar els temporitzadors de l'enemic i de la puntació
    $ScoreTimer.start()

func _on_ScoreTimer_timeout():
    score += 1  # Cada segon augmenta en 1 la puntuació
    $HUD.update_score(score)
	

func _on_MobTimer_timeout():
# Elegir una posició aleatòria en el Modpath per a que comencen els enemics
    $MobPath/MobSpawnLocation.set_offset(randi())
# Crear un enemic (mob) nou
    var mob = Mob.instance()
    add_child(mob)
# Fer que l'enemic tinga una direcció cap a dins de la pantalla
    var direction = $MobPath/MobSpawnLocation.rotation + PI / 2
# Posar l'enemic a una posició aleatoria del path
    mob.position = $MobPath/MobSpawnLocation.position
# Afegir certa aleatorietat a la direcció de l'enemic
    direction += rand_range(-PI / 4, PI / 4)
    mob.rotation = direction
    # Triar una velocitat aleatoria per a l'enemic
    mob.set_linear_velocity(Vector2(rand_range(mob.min_speed, mob.max_speed), 0).rotated(direction))

Player:

extends Area2D

signal hit

export (int) var speed  # velocita del jugador (pixels/sec).
var screensize  # mida de la pantalla

func _ready():
	screensize = get_viewport_rect().size
	

func _process(delta):
    var velocity = Vector2() # Vector de velocitat (x,y)
    if Input.is_action_pressed("ui_right"):
        velocity.x += 1
    if Input.is_action_pressed("ui_left"):
        velocity.x -= 1
    if Input.is_action_pressed("ui_down"):
        velocity.y += 1
    if Input.is_action_pressed("ui_up"):
        velocity.y -= 1
    if velocity.length() > 0:
        velocity = velocity.normalized() * speed
        $AnimatedSprite.play() # Si hi ha moviment es mou
    else:
        $AnimatedSprite.stop() # Si no hi ha moviment para l'animació
    position += velocity * delta # Calcula la posició
    position.x = clamp(position.x, 0, screensize.x)
    position.y = clamp(position.y, 0, screensize.y)
	
    if velocity.x != 0:
     $AnimatedSprite.animation = "right"
     $AnimatedSprite.flip_v = false
     $AnimatedSprite.flip_h = velocity.x < 0
    elif velocity.y != 0:
     $AnimatedSprite.animation = "up"
     $AnimatedSprite.flip_v = velocity.y > 0
	

func _on_Player_body_entered(body):
    hide() # Player disappears after being hit.
    emit_signal("hit")
    $CollisionShape2D.disabled = true


func start(pos):
    position = pos
    show()
    $CollisionShape2D.disabled = false

Mob:

extends RigidBody2D

export (int) var min_speed # Minimum speed range.
export (int) var max_speed # Maximum speed range.
var mob_types = ["walk", "swim", "fly"]

func _ready():
    $AnimatedSprite.animation = mob_types[randi() % mob_types.size()]
    $AnimatedSprite.play() 


func _on_Visibility_screen_exited():
	queue_free()

HUD:

extends CanvasLayer

signal start_game

func show_message(text):
    $MessageLabel.text = text
    $MessageLabel.show()
    $MessageTimer.start()

func show_game_over():
    show_message("Game Over")
    yield($MessageTimer, "timeout")
    $StartButton.show()
    $MessageLabel.text = "Esquiva els Creeps!"
    $MessageLabel.show()

func update_score(score):
    $ScoreLabel.text = str(score)

func _on_StartButton_pressed():
    $StartButton.hide()
    emit_signal("start_game")

func _on_MessageTimer_timeout():
    $MessageLabel.hide()