Laberinto de neón

Seguimos con los prototipos de videojuegos con mecánica de rotación de escenario. Para diferenciarlo de otros proyectos que se vayan haciendo en paralelo, lo clasificaremos en su propia categoría con el nombre «Proyecto Newton».

El siguiente video es un ejemplo de rotación del escenario en modo relativo, con rotación completa del escenario, que fue explicado con más detalle en la entrada anterior.

Me recuerda ligeramente a las fases especiales del primer juego de Sonic para Mega Drive. En ellas había que encontrar y recoger la esmeralda del caos en un escenario con rotación continua. En el caso de Sonic el control es para el protagonista, mientras que la rotación del escenario es automática y sólo se puede influir en ella tocando ciertos objetos repartidos por el escenario. En este otro ejemplo, el jugador controla la rotación y los objetos se mueven indirectamente por efecto de la gravedad, pero también se podría probar a añadir controles de saltos o impulsos.

El código fuente de este ejemplo está disponible en un repositorio de github.

De momento el objetivo es mostrar ideas, no es necesario que estén desarrolladas en detalle. Si alguna de las ideas nos gusta especialmente, la podríamos elegir para desarrollo y hacer un prototipo jugable.

La idea puede estar basada en el concepto inicial (un objeto que cae hasta la salida evitando las trampas), o puede ser una idea nueva o cualquier prueba experimental que se te pase por la cabeza y que pueda encajar con la mecánica de la gravedad. ¿Qué ideas se te ocurren?

Rotación del escenario

Vamos a continuar con la propuesta de juegos basados en la rotación de escenario para Godot. Aquí está publicado el código que se encarga de la rotación, para usarlo como plantilla y que podamos centrarnos en elaborar las ideas de mini juegos.

Por supuesto, si prefieres programar su propio código de rotación, no es necesario utilizar este ejemplo, pero recomiendo leer los siguientes párrafos igualmente, para ahorrar tiempo.

Aunque el código para el control por rotación del escenario no es muy extenso, tampoco es tan trivial como puede parecer. En una primera impresión podríamos pensar en rotar los nodos que corresponden al escenario del juego. Si el mapa tiene desplazamiento, habría que hacerlo tomando como centro de rotación la posición del actor o de la cámara, en lugar de utilizar un punto de rotación fijo.

Esta manera tiene muchas complicaciones, en especial con las áreas de colisión de los StaticBody y la física de los RigidBody. Si lo intentas hacer así, lo más probable es que acabes desesperado intentando comprender qué está pasando con la física y por qué los RigidBody se comportan erróneamente con las paredes.

Cuando nos encontramos con una situación como ésta, conviene detenerse a pensar si hay una manera alternativa más simple y directa de obtener el resultado. Y normalmente esa manera alternativa existe. En este caso, la solución más efectiva fue rotar la cámara y el vector de gravedad. Sencillo, rápido, y funciona perfectamente sin efectos secundarios.

Empezamos por configurar la cámara

El nodo de la cámara no debe ser un nodo hijo del nodo RigidBody. Eso sería lo más habitual, dejar que la cámara copie automáticamente la posición del nodo que será el foco de atención, pero en este caso debe pertenecer a la escena del mapa o del juego global porque necesitamos manipular manualmente sus propiedades.

Como la cámara no copiará la posición del nodo del jugador, debemos hacerlo manualmente desde otro lugar, algún nodo situado más arriba en la jerarquía.

func _process(_delta):
	$Camera.position = $Character.position

El script asociado a la cámara es el que procesa las acciones del jugador. Esta es otra particularidad de la mecánica de juego, el movimiento es indirecto: no controlamos al objeto o personaje, sino la inclinación del escenario, y resulta más conveniente que sea procesado en la cámara o en otro lugar global. Los ejemplos que muestro a continuación están pensados para funcionar dentro del script de la cámara.

func _process(delta):
	var wanted_rotation := 0.0
	if Input.is_action_pressed("go_left"):
		wanted_rotation = PI / 3
	if Input.is_action_pressed("go_right"):
		wanted_rotation = -PI / 3

	absolute_rotation(wanted_rotation, delta)
	rotate_gravity_vector()

Las funciones absolute_rotation y rotate_gravity_vector se puede ver a continuación. Voy a mostrar dos funciones para hacer la rotación de la cámara, puedes elegir la que prefieras, según el tipo de juego o preferencias de control.

Es conveniente recordar que el nodo Camera2D tiene una propiedad Rotating, que debe activarse para que los cambios de rotación tengan efecto.

Control absoluto de posición de rotación

func absolute_rotation(target: float, delta: float):
	var v1 := Vector2.DOWN.rotated(rotation)
	var v2 := Vector2.DOWN.rotated(target)
	var wanted_angle := v1.angle_to(v2)

	var angle := wanted_angle * rotation_speed_factor * delta
	var min_angle := rotation_speed_min * delta

	if abs(angle) < min_angle:
		angle = sign(angle) * min_angle
		if abs(angle) > abs(wanted_angle):
			angle = wanted_angle

	rotation += angle

La función absolute_rotation recibe un ángulo en radianes en la variable target. Este ángulo indica la posición final a la que el usuario quiere rotar el escenario, donde 0 significa la posición «normal» sin rotación. Con un joystick analógico se podría usar la posición del joystick para que fuese copiada por el escenario.

La rotación se hace siempre por el camino más corto entre el ángulo actual y el ángulo destino, y no se hace instantáneamente, sino que aplica interpolación durante un tiempo para suavizar el movimiento. Los parámetros de esta interpolación se pueden controlar mediante dos variables:

var rotation_speed_factor := 2.5
var rotation_speed_min := 1.2

Así se hizo el juego que salió en la fachada de Medialab Prado, el ángulo rotación se tomaba directamente de la posición de las personas en la plaza.

Control relativo de posición de rotación

# Declaramos en el script
var rotation_speed := 0.0

func relative_rotation(force: float, delta: float):
	if abs(force) > 0.001:
		rotation_speed += rotation_accel_factor * force * delta
		rotation_speed = clamp(rotation_speed, -rotation_speed_max, rotation_speed_max)
	else:
		if abs(rotation_speed) < 0.05:
			rotation_speed = 0
		else:
			var deceleration := rotation_speed * rotation_stop_factor * delta
			if sign(rotation_speed) != sign(rotation_speed - deceleration):
				rotation_speed = 0
			else:
				rotation_speed -= deceleration

	rotation += rotation_speed * delta

La función relative_rotation es más indicada para juegos en los que el escenario puede rotar 360 grados, dar vueltas sin fin, y no hay una posición que se pueda considerar como la posición «normal». Recibe un valor que, a partir de la posición actual, indica la fuerza con la que se quiere rotar el escenario y la dirección de rotación (signo positivo o negativo).

Es conveniente que el valor de fuerza se mantenga entre -1 y 1. Para un control digital como puede ser el teclado, el valor de fuerza podría ser -1 hacia un la derecha y 1 hacia la izquierda.

La rotación tiene efecto de aceleración e inercia, lo que proporciona una sensación de control más suave. Para controlar estos parámetros se utilizan tres variables:

var rotation_speed_max := 2.0
var rotation_accel_factor := 6.0
var rotation_stop_factor := 3.5

Ajuste del vector de gravedad

En este punto ya tenemos la rotación de la cámara controlada por el jugador, pero la gravedad sigue funcionando en la misma dirección: si rotamos hacia la izquierda, tendremos la sensación de que las cosas caen hacia la derecha. Para que todo caiga siempre «hacia abajo» (visto desde fuera de la pantalla), hay que rotar también el vector de gravedad.

Hay varias formas de hacerlo. Originalmente se hizo modificando las propiedades de un nodo de tipo Area2D invisible que cubría el área de juego, pero hay otra manera más directa que se aplica globalmente a la física de todo el juego al completo:

func rotate_gravity_vector():
	Physics2DServer.area_set_param(
			get_world_2d().space,
			Physics2DServer.AREA_PARAM_GRAVITY_VECTOR,
			Vector2.DOWN.rotated(rotation))

La función rotate_gravity_vector se ejecuta después de alguna de las dos funciones anteriores, y se encarga de ajustar los parámetros de Physics2DServer para que el vector de gravedad se mantenga en la posición esperada.

Regreso al pasado

Hace unas semanas estuvimos repasando proyectos antiguos y pensamos en hacer algo similar a lo que fue nuestro primer contacto con Godot, en el año 2016. Ese proyecto estaba pensado como un ejercicio de iniciación para quienes acaban de llegar: concepto de nodos y escenas, física y colisiones.

Lo preparé en una tarde con los gráficos de Kenney, tuvo buena aceptación y se terminó convirtiendo en un mini juego para la fachada digital de Medialab Prado, controlado mediante las cámaras del edificio y el movimiento de una o dos personas en la plaza.

Para transformar un ejercicio básico en algo que fuese jugable, fueron necesarios nuevos elementos; un temporizador, objetos de bonificación, muchas trampas, y un objetivo para los jugadores: encontrar al menos una llave para abrir la puerta final y pasar al siguiente mapa. Junto con Gonzalo y Carles se hicieron los mapas adicionales y la adaptación a la fachada.

El código no es aprovechable a estas alturas. Se hizo muy rápido, sin planificación previa, y sin tener experiencia con el motor. Además, ahora que no hay un plazo de tiempo concreto, podríamos crear gráficos propios.

Este proyecto se suma a los otros que hay en curso. Hemos iniciado la fase de propuestas y cacharreo para acumular ideas, prototipos y demostraciones conceptuales, la fase más divertida del desarrollo 🙂

Posteriormente decidiremos el proyecto a realizar, ya sea un mini juego, un tutorial, o ambas cosas. No es necesario seguir el mismo patrón que el juego para Medialab, se puede buscar algo nuevo. La mecánica de rotar escenario para controlar uno (o varios) objetos RigidBody ofrece muchas posibilidades.

El juego LocoRoco, cuya versión inicial fue para la consola Sony PSP, es un ejemplo de la mecánica de rotación del escenario en combinación con otra serie de características interesantes, como el control indirecto de múltiples protagonistas que pueden fusionarse y separarse.