Optimización usando Servidores

Las plataformas como Godot ofrecen una mayor facilidad de uso gracias a sus construcciones y características de alto nivel. La mayoría de ellas se acceden y utilizan a través del Sistema de Escenas. El uso de nodos y recursos simplifica la organización del proyecto y la gestión de activos en juegos complejos.

Por supuesto, siempre existen inconvenientes:

  • Existe una capa adicional de complejidad

  • El rendimiento es menor que al utilizar APIs simples directamente

  • No es posible utilizar varios hilos para controlarlos

  • Se necesita más memoria.

En muchos casos, esto no es realmente un problema (Godot está muy optimizado y la mayoría de las operaciones se manejan con señales, por lo que no se requiere sondeo). Aún así, a veces puede serlo. Por ejemplo, lidiar con decenas de miles de instancias de algo que debe procesarse en cada fotograma puede ser un cuello de botella.

Este tipo de situación hace que los programadores se arrepientan de estar usando un motor de juego y deseen poder volver a una implementación más artesanal y de bajo nivel del código del juego.

Aún así, Godot está diseñado para solucionar este problema.

Servidores

Una de las decisiones de diseño más interesantes de Godot es el hecho de que todo el sistema de escenas es opcional. Aunque actualmente no es posible compilarlo por separado, se puede evitar por completo su uso.

En esencia, Godot usa el concepto de servidores. Son APIs de muy bajo nivel para controlar el renderizado, la física, el sonido, etc. El sistema de escenas se construye sobre ellas y las usa directamente. Los servidores más comunes son:

  • VisualServer: se encarga de todo lo relacionado con los gráficos.

  • PhysicsServer: se encarga de todo lo relacionado con la física en 3D.

  • Physics2DServer: se encarga de todo lo relacionado con la física en 2D.

  • AudioServer: se encarga de todo lo relacionado con el audio.

Simplemente explore sus APIs y se dará cuenta de que todas las funciones proporcionadas son implementaciones de bajo nivel de todo lo que Godot le permite hacer.

RIDs*

La clave para utilizar los servidores es comprender los objetos Resource ID (RID). Estos son identificadores opacos para la implementación del servidor. Se asignan y liberan manualmente. Casi todas las funciones de los servidores requieren RIDs para acceder al recurso real.

La mayoría de los nodos y recursos de Godot contienen estos RIDs de los servidores de forma interna, y se pueden obtener mediante diferentes funciones. De hecho, cualquier cosa que herede de Resource se puede convertir directamente a un RID. Sin embargo, no todos los recursos contienen un RID; en esos casos, el RID estará vacío. El recurso luego se puede pasar a las APIs de los servidores como un RID.

Advertencia

Los recursos están contados por referencia (ver Reference), y las referencias al RID de un recurso no se cuentan al determinar si el recurso aún está en uso. Asegúrate de mantener una referencia al recurso fuera del servidor, de lo contrario, tanto el recurso como su RID serán borrados.

Para los nodos, hay muchas funciones disponibles:

  • Para CanvasItem, el método CanvasItem.get_canvas_item() retornará el RID del canvas item en el servidor.

  • Para CanvasLayer, el método CanvasLayer.get_canvas() devolverá el RID del lienzo en el servidor.

  • Para Viewport, el método Viewport.get_viewport_rid() devolverá el RID del viewport en el servidor.

  • Para 3D, el recurso World (obtenible en los nodos Viewport y Spatial) contiene funciones para obtener el VisualServer Scenario y el PhysicsServer Space. Esto permite crear objetos 3D directamente con la API del servidor y utilizarlos.

  • Para 2D, el recurso World2D (obtenible en los nodos Viewport y CanvasItem) contiene funciones para obtener el VisualServer Canvas y el Physics2DServer Space. Esto permite crear objetos 2D directamente con la API del servidor y utilizarlos.

  • La clase VisualInstance permite obtener la instancia y la base de la instancia del escenario mediante los métodos VisualInstance.get_instance() y VisualInstance.get_base(), respectivamente.

Intenta explorar los nodos y recursos con los que estás familiarizado y encuentra las funciones para obtener los RIDs del servidor.

No se recomienda controlar los RIDs de objetos que ya tienen un nodo asociado. En cambio, las funciones del servidor siempre deben usarse para crear y controlar nuevas e interactuar con las existentes.

Creando un sprite

Este es un ejemplo sencillo de cómo crear un sprite desde código y moverlo utilizando la API de bajo nivel CanvasItem.

extends Node2D


# VisualServer expects references to be kept around.
var texture


func _ready():
    # Create a canvas item, child of this node.
    var ci_rid = VisualServer.canvas_item_create()
    # Make this node the parent.
    VisualServer.canvas_item_set_parent(ci_rid, get_canvas_item())
    # Draw a texture on it.
    # Remember, keep this reference.
    texture = load("res://my_texture.png")
    # Add it, centered.
    VisualServer.canvas_item_add_texture_rect(ci_rid, Rect2(texture.get_size() / 2, texture.get_size()), texture)
    # Add the item, rotated 45 degrees and translated.
    var xform = Transform2D().rotated(deg2rad(45)).translated(Vector2(20, 30))
    VisualServer.canvas_item_set_transform(ci_rid, xform)

La API de Canvas Item en el servidor te permite agregar primitivas de dibujo. Una vez agregadas, no se pueden modificar. Es necesario borrar el ítem y volver a agregar las primitivas (esto no es necesario para establecer la transformación, que se puede hacer tantas veces como se desee).

Los primitivos se eliminan de esta manera:

VisualServer.canvas_item_clear(ci_rid)

Instanciando una malla en el espacio 3D

Las API 3D son diferentes de las 2D, por lo que se debe utilizar la API de instanciación.

extends Spatial


# VisualServer expects references to be kept around.
var mesh


func _ready():
    # Create a visual instance (for 3D).
    var instance = VisualServer.instance_create()
    # Set the scenario from the world, this ensures it
    # appears with the same objects as the scene.
    var scenario = get_world().scenario
    VisualServer.instance_set_scenario(instance, scenario)
    # Add a mesh to it.
    # Remember, keep the reference.
    mesh = load("res://mymesh.obj")
    VisualServer.instance_set_base(instance, mesh)
    # Move the mesh around.
    var xform = Transform(Basis(), Vector3(20, 100, 0))
    VisualServer.instance_set_transform(instance, xform)

Creando un RigidBody 2D y moviendo un sprite con este

Esto crea un RigidBody2D usando el API de Physics2DServer, y mueve un CanvasItem cuando se mueve el cuerpo.

# Physics2DServer expects references to be kept around.
var body
var shape


func _body_moved(state, index):
    # Created your own canvas item, use it here.
    VisualServer.canvas_item_set_transform(canvas_item, state.transform)


func _ready():
    # Create the body.
    body = Physics2DServer.body_create()
    Physics2DServer.body_set_mode(body, Physics2DServer.BODY_MODE_RIGID)
    # Add a shape.
    shape = Physics2DServer.rectangle_shape_create()
    # Set rectangle extents.
    Physics2DServer.shape_set_data(shape, Vector2(10, 10))
    # Make sure to keep the shape reference!
    Physics2DServer.body_add_shape(body, shape)
    # Set space, so it collides in the same space as current scene.
    Physics2DServer.body_set_space(body, get_world_2d().space)
    # Move initial position.
    Physics2DServer.body_set_state(body, Physics2DServer.BODY_STATE_TRANSFORM, Transform2D(0, Vector2(10, 20)))
    # Add the transform callback, when body moves
    # The last parameter is optional, can be used as index
    # if you have many bodies and a single callback.
    Physics2DServer.body_set_force_integration_callback(body, self, "_body_moved", 0)

La versión en 3D debería ser muy similar, ya que los servidores de física en 2D y 3D son idénticos (utilizando RigidBody y PhysicsServer respectivamente).

Obteniendo datos de servidores

Intenta nunca solicitar información de VisualServer, PhysicsServer o Physics2DServer llamando a funciones a menos que sepas lo que estás haciendo. Estos servidores suelen ejecutarse de forma asíncrona para mejorar el rendimiento, y llamar a cualquier función que devuelva un valor los detendrá y los obligará a procesar cualquier tarea pendiente hasta que la función sea llamada realmente. Esto disminuirá considerablemente el rendimiento si los llamas en cada fotograma (y no será obvio por qué).

Debido a esto, la mayoría de las API en dichos servidores están diseñadas de tal manera que ni siquiera es posible solicitar información de vuelta hasta que sea realmente un dato que pueda ser guardado.