Cookie Clicker en Godot
Maximiliano Burgos
Posted on June 8, 2023
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:
Viaje al centro de Godot
Maximiliano Burgos ・ Jun 5 ・ 8 min read
Sin mas dilación, ¡comencemos!
Todo empieza con una galleta
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:
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
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)
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.
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
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)
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)
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"
},
(...)
]
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)
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!")
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)
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:
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
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):
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!
Posted on June 8, 2023
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.