Cookie Clicker en Godot

maxwellnewage

Maximiliano Burgos

Posted on June 8, 2023

Cookie Clicker en Godot

Antes de leer este artículo, te recomiendo pasarte por uno más introductorio donde explico las razones que me llevaron a elegir Godot como motor de juegos favorito:

Sin mas dilación, ¡comencemos!

Todo empieza con una galleta

Cookie Clicker Godot

Para entender Godot en su máxima expresión, decidí volver a replicar el clon de Cookie Clicker que ya había hecho en otras tecnologías.

Este motor funciona por escenas, pero a diferencia de Unity, cada scene es como un objeto en si mismo, con sus propiedades y métodos. Empecé por armar una escena "Cookie" que tiene como nodo principal un Button y dentro un TextureRect, el cual contendría la imagen png de la galleta:

Cookie Node

Esto puede parecer extraño, pero tendrá sentido en los próximos párrafos. Al botón le armé un script para manejar los eventos del click. Vamos a ver paso a paso qué hace cada parte.

@onready var cookie_texture_rect: TextureRect = $CookieTextureRect
Enter fullscreen mode Exit fullscreen mode

Almaceno una referencia al TextureRect para no tener que llamarlo cada vez que lo necesito.

func _ready() -> void:
    pressed.connect(_on_pressed)
    button_down.connect(_on_button_down)
    button_up.connect(_on_button_up)
Enter fullscreen mode Exit fullscreen mode

Este método es el primero que va a correr cuando se instancie el nodo. Las señales (signals) pressed, button_down y button_up son los eventos que nos permitirán controlar estados en el botón.

Documentación de señales

Con el método "connect" podremos crear un trigger con eventos creados y controlados por nosotros:

func _on_button_down():
    update_scale()

func _on_button_up():
    update_scale()

func _on_pressed():
    Globals.cookie_counter += 1
Enter fullscreen mode Exit fullscreen mode

El método _on_pressed va a dispararse en el momento que se presione el botón. En este caso utilizamos una variable global definida en un archivo llamado "globals.gd":

var cookie_counter: int = 0:
    set = set_cookie_counter

signal cookie_counter_changed(new_value: int)

func set_cookie_counter(new_value: int):
    if cookie_counter != new_value:
        cookie_counter = new_value
        cookie_counter_changed.emit(new_value)
Enter fullscreen mode Exit fullscreen mode

En este caso podemos acceder a cookie_counter, como también modificarlo. Cuando cambia de valor, se llama al método setter set_cookie_counter, el cual verifica que no se trate del mismo valor; y luego de cambiarlo, llama a una señal "cookie_counter_changed". Esto lo hacemos porque en otras partes del juego estamos esperando que esta señal cambie para reescribir su valor en pantalla. De esta manera nos ahorramos tener que escribirlo constantemente, como podría ser mediante el uso del método "_process" que se ejecuta frame a frame.

Animación del click

En el momento en que se presiona el botón, se llama a las señales "button_down" y "pressed". Cuando se suelta el click, se invoca "button_up". Para animar el botón, utilizo los métodos "_on_button_down" (disparador de button_down) y "_on_button_up" (disparador de button_up). Estos comparten la llamada a un método "update_scale", el cual posee la siguiente lógica:

func update_scale() -> void:
    var pressed_scale: Vector2 = Vector2(0.7, 0.7)
    var normal_scale: Vector2 = Vector2(1, 1)
    animation_tween = create_tween()

    if button_pressed:
        animation_tween.tween_property(cookie_texture_rect, "scale", pressed_scale, 0.2).set_trans(Tween.TRANS_SINE)
    else:
        animation_tween.tween_property(cookie_texture_rect, "scale", normal_scale, 0.2).set_trans(Tween.TRANS_SINE)
Enter fullscreen mode Exit fullscreen mode

documentación Tween

Mi intención era animar a cookie_texture_rect (contenedor del asset de la galleta) para que se reduzca un 30% de su tamaño (vector 0.7 en x) al hacer click en el botón. Utilizo el método create_tween(), el cual me permite armar una animación con un estilo TRANS_SINE, el cual me permitirá cierta suavidad en la transición de una escala de 0.7 a 1 (pressed_scale a normal_scale y viceversa).

En el caso de "button_pressed", simplemente se trata de una variable booleana (propia del botón) que devuelve el estado del mismo.

La tienda

Armé otra escena para crear la tienda (shop) que listará las mejoras y permitirá comprarlas para aumentar los CpS (cookies per second). Utilicé un todo ItemList y le asigné un script. En el mismo establecí una lista de mejoras en un array de diccionarios:

var shop_list = [
    {
        "name": "Click",
        "cost": 1,
        "give": 1,
        "method": "M"
    },
    {
        "name": "Cursor",
        "cost": 1,
        "give": 1,
        "method": "A"
    },
(...)
]
Enter fullscreen mode Exit fullscreen mode

A continuación, detallo cada uno de los atributos:

  • name: nombre de la mejora.
  • cost: costo en cookies de la compra.
  • give: cantidad de cookies por segundo que brinda.
  • method: La letra A indica "Auto" y M sería "Manual". En el primer caso, el atributo "give" aplica a CpS, mientras que en el segundo es el valor por click manual incrementable.

Luego recorrí dicha lista y cargué un único asset para los iconos de cada item:

func _ready():
    var icon = ResourceLoader.load("res://granny.svg")

    for item in shop_list:
        var name_format = "%s | Valor: %s cookies | Genera %s CpS"
        var item_name = name_format % [item["name"], item["cost"], item["give"]]
        add_item(item_name, icon)
Enter fullscreen mode Exit fullscreen mode

Finalmente armé un trigger para la señal "item_selected":

func _on_item_selected(index):
    var item = shop_list[index]

    if Globals.cookie_counter >= item["cost"]:
        Globals.cookie_counter -= item["cost"]
        Globals.cookies_per_second += item["give"]
    else:
        print("Galletas insuficientes!")
Enter fullscreen mode Exit fullscreen mode

La función se trae el índice de la lista, el cual equivale al índice de mi array; por lo cual obtengo el item y luego valido que la cantidad de cookies sea mayor o igual al costo del item en si. Luego resto su valor (cost) y también aumento otra variable llamada cookies_per_second, la cual podemos encontrar en globals.gd nuevamente:

var cookies_per_second: int = 0:
    set = set_cookies_per_second

signal cookies_per_second_changed(cookies: int)

func set_cookies_per_second(cookies: int):
    if cookies_per_second != cookies:
        cookies_per_second = cookies
        cookies_per_second_changed.emit(cookies)
Enter fullscreen mode Exit fullscreen mode

Como ven, la lógica es muy similar a la variable global cookie_counter.

La escena principal

Ahora que tengo la escena de la tienda y la galleta, las instancio en mi escena principal:

Game

Utilizo un nodo VBoxContainer para manejar un label que reflejará el estado de las galletas y la instancia de la escena cookie. También tengo otro nodo igual para el título de la tienda y la lista de mejoras. Pueden notar que se encuentra vacía porque se llena en tiempo de ejecución. Finalmente tengo un HBoxContainer que contiene a los dos VBoxContainer y genera este acomodamiento horizontal de columnas.

Luego tenemos un nodo Timer que contiene un script muy sencillo para actualizar cookie_counter por segundo:

func _on_timeout():
    Globals.cookie_counter += Globals.cookies_per_second
Enter fullscreen mode Exit fullscreen mode

El método "_on_timeout" esta conectado a una señal llamada "timeout", y se ejecuta una vez por segundo dado que lo establecimos en los parámetros del nodo (Wait Time):

Nodo Timer

Como el atributo "Autostart" esta activo, el contador inicia apenas se activa el nodo Timer, lo cual ocurre cuando nuestra escena principal arranca, y esto es en el momento en que empieza a ejecutarse nuestro juego.

Conclusiones

Si llegaron hasta aca leyendo todo el artículo, los felicito por haber soportado el nivel de tecnicismos que utilicé en cada apartado. Soy consciente de que puede ser muy pesado para mucha gente, pero esto es la naturaleza de desarrollar un videojuego.

No obstante, Godot simplifica y organiza como ninguno, por lo cual una vez entienden las bases y mantienen las buenas prácticas, todo se vuelve mucho más ágil.

Es muy probable que me dedique a sacar más artículos sobre videojuegos y este engine, así que si disfrutaron de este contenido, ¡compártanlo para que llegue a más gente!

💖 💪 🙅 🚩
maxwellnewage
Maximiliano Burgos

Posted on June 8, 2023

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related

Cookie Clicker en Godot
gamedev Cookie Clicker en Godot

June 8, 2023