pixelboyz logo
Desarrollo de Videojuegos

Tutorial de Godot Engine Parte 3 — Ciclo de vida del programa y manejo de entrada

Video_2015-01-27_105150

Índice de contenido


Este tutorial está disponible en forma de video aquí o incrustado debajo.

Ahora podría ser un buen momento para hacer una pausa y mirar el ciclo de vida de un programa típico, ya que esto puede ser un poco confuso con Godot, pero es algo que realmente necesitas entender. Cada juego no trivial tiene un bucle de juego en alguna parte. Este es el código que se ejecuta después de que se inicia un programa y es básicamente el corazón de su aplicación. En pseudocódigo, podría verse así:

main() {
   setupApplication()
   scene = createScene()
   while(!quit){
      get_input()
      update_physics()
      scene.updateAllChildren()
      scene.render()
   }
}

En esencia, es un bucle glorificado que se ejecuta una y otra vez, verificando la entrada, actualizando la escena y generando los resultados hasta que se le indique que se detenga.

Godot, por supuesto, no es una excepción, aunque por defecto este comportamiento está oculto para ti, como es la norma con los motores de juegos. En cambio, el objeto que posee su escena es un EscenaÁrbol que a su vez hereda la Bucle principal, que proporciona la funcionalidad anterior. Se le proporciona uno predeterminado, pero si lo desea, puede implementar el suyo propio, más sobre eso a continuación.

Sin embargo, debe darse cuenta de que este SceneTree es el corazón palpitante de su aplicación. Cada cuadro que llama es una escena activa que pasa toda la entrada que se ha producido, así como la actualización de los nodos que solicitan la actualización. Veremos este proceso ahora. Una cosa importante a tener en cuenta… Los nodos pueden acceder a SceneTree utilizando el método .get_tree().

Actualización de un nodo en cada cuadro

Ok, eso es lo básico de cómo fluye la ejecución del programa, ahora echemos un vistazo a un ejemplo más práctico. Digamos, por ejemplo, que tenemos un Sprite que queremos actualizar cada cuadro. ¿Cómo le decimos a nuestro MainLoop que queremos que se actualice o Node? Afortunadamente es bastante simple.

Cree un nodo Sprite, agregue un gráfico, colóquelo en la pantalla y luego agregue un nuevo script. Todo esto estaba cubierto en el tutorial anterior si no está seguro de cómo proceder.

Ahora que tenemos un Script adjunto, primero debemos decirle que queremos recibir actualizaciones. Es decir, en cada iteración del ciclo principal, queremos que se llame a nuestro script. Este es un proceso de dos partes, juego de palabras no pretendido… mucho. Primero, en su _listo función, le dices a Godot que quieres recibir actualizaciones por llamada establecer_proceso(verdadero). Entonces anulas el funcion virtual _proceso().

Echemos un vistazo a un sprite simple que se mueve hacia la derecha hasta que toca el borde de la pantalla, momento en el que se envuelve.

extends Sprite
func _ready():
   self.set_process(true)
func _process(delta):
   var cur_pos = self.get_pos()
   cur_pos.x += 100 * delta   
   # wrap around screen
   if(cur_pos.x > self.get_viewport_rect().size.width + self.get_item_rect().size.width/2):
      cur_pos.x = -self.get_item_rect().size.width/2
   self.set_pos(cur_pos)

set_process le dice a Godot que llame a estos nodos _proceso() función. El valor pasado, delta, es la cantidad de tiempo transcurrido desde la última vez _proceso fue llamado Como puede ver en el ejemplo anterior, este valor se puede usar para animar a una velocidad constante. El ejemplo anterior actualizará el valor X en 100 píxeles por segundo. Su resultado final debería ser algo como esto:

Entonces, en pocas palabras, si desea manejar las actualizaciones en su objeto derivado de Node, simplemente llame establecer_proceso(verdadero) y proporcionar un _proceso(flotante) anular.

Manejo de entrada por sondeo

Eso nos lleva a manejar la entrada. Notará que el manejo de Entrada y Proceso es muy similar. Hay un par de formas en que puedes manejar la entrada en Godot. Comencemos con lo más fácil, el sondeo.

Puede sondear la entrada en cualquier momento usando el objeto global Entrada, así:

func _process(delta):
   if(Input.is_key_pressed(KEY_ESCAPE)):
      if(Input.is_key_pressed(KEY_SHIFT)):
         get_tree().quit()

Esto verifica primero si la tecla ESCAPE, luego si la tecla SHIFT (¡también!) está presionada. Si es así, le decimos a SceneTree que salga de la aplicación. Como dije antes, un nodo puede acceder a su SceneTree usando obtener_árbol().

Además de sondear el teclado, también hay métodos is_joy_button_pressed(), is_mouse_button_pressed() y is_action_pressed() que tendrá más sentido en un futuro próximo. También puede sondear el estado. Por ejemplo, para verificar el cursor del mouse o tocar la ubicación, podría:

func _process(delta):
   if(Input.is_mouse_button_pressed(BUTTON_LEFT)):
      print(str("Mouse at location:",Input.get_mouse_pos(), " moving at speed: ", Input.get_mouse_speed()));

Hay otras entradas que también puede sondear, en su mayoría basadas en dispositivos móviles, pero todas usan una interfaz muy similar. Cubriré los controles específicos para dispositivos móviles en un punto posterior de esta serie.

Manejo de la entrada como impulsada por eventos

También puede hacer que Godot entregue a su aplicación todos los eventos de entrada a medida que ocurren y elija qué procesar. Al igual que con el manejo de actualizaciones, debe registrarse para recibir eventos de entrada, así:

func _ready():
   set_process_input(true)

Entonces anulas la función _aporteque toma un Evento de entrada como parámetro.

func _input(event):
   # if user left clicks
   if(event.type == InputEvent.MOUSE_BUTTON):
      if(event.button_index == 1):
         self.set_pos(Vector2(event.x,event.y)) 
         
   # on keyboard cursor key
   if(event.type == InputEvent.KEY):
      var curPos = self.get_pos()
      
      if(event.scancode == KEY_RIGHT):
         curPos.x+= 10
         self.set_pos(curPos)

      if(event.scancode == KEY_LEFT):
         curPos.x-= 10
         self.set_pos(curPos)

El ejemplo anterior maneja un par de escenarios diferentes. Primero, si el usuario hace clic en el botón izquierdo, establecemos la posición en la ubicación x e y actual del mouse, tal como la pasa la clase InputEvent. Observe que en este caso probé el botón por índice en lugar de definir BUTTON_LEFT como antes. No debería haber ninguna diferencia funcional, aunque esto le permitiría probar los botones para los que no se ha definido una asignación, como uno de esos locos ratones de 12 botones. A continuación, verificamos si el evento es un evento CLAVE y, si lo es, verificamos qué clave. En el caso de la flecha derecha o izquierda, actualizamos nuestra posición en consecuencia.

A veces, sin embargo, cuando manejas un evento, quieres que se termine y se vaya. De forma predeterminada, todos los eventos seguirán transmitiéndose a todos los receptores de eventos. Cuando no desea este comportamiento, es bastante simple decirle a Godot que se maneja un evento. Del ejemplo anterior, traguemos el evento en el caso de que sea un InputEvent.KEY. Esto significa que solo esta clase tendrá acceso a los eventos del teclado (bueno… y a los controles de la GUI, que en realidad se aprovechan de los eventos antes).

   # on keyboard cursor key
   if(event.type == InputEvent.KEY):
      self.get_tree().set_input_as_handled()
      var curPos = self.get_pos()

Vocación set_input_handled() hará que InputEvent no se propague más.

Finalmente, es posible que quieras hacer un catch all. Tal vez desee registrar todos los eventos de entrada no controlados que ocurrieron. Esto también se puede hacer y también debe registrarse para este comportamiento, así:

func _ready():
   set_process_unhandled_input(true)

Luego simplemente anula la función correspondiente:

func _unhandled_input(event):
   print(str("An event was unhandled ",event))

En este caso, simplemente registramos el evento en la consola. Advertencia, sin embargo, habrá MUCHOS de ellos. ¡Simplemente no hay eventos de teclado, ya que ahora los estamos comiendo!

Mapas de entrada

Muy a menudo desea que varios comandos realicen la misma acción. Por ejemplo, desea presionar a la derecha en el controlador d-pad para realizar la misma acción que presionar la tecla de flecha derecha. ¿O tal vez quiere dejar que el usuario defina sus propios controles? En ambos casos, el sistema de alias de entrada es increíblemente útil… y afortunadamente Godot tiene uno incorporado… InputMaps.

Es posible que haya notado la pestaña InputMap cuando estábamos en la configuración del proyecto antes… si no abre la configuración del proyecto ahora…

imagen

Aquí puede ver que ya se han definido varias asignaciones para las acciones de la interfaz de usuario. Avancemos y creemos un mapa propio, MOVE_RIGHT.

En la parte superior, en Acción, ingrese MOVE_RIGHT

imagen

Luego haga clic en Agregar

imagen

Se agregará una nueva entrada al final de la página, así:

imagen

Haga clic en el ícono + y agregue una nueva asignación de tipo Clave

imagen

Luego se le pedirá que presione una tecla:

imagen

Repita este proceso y, en su lugar, seleccione otro dispositivo… También voy a asignar el botón derecho del mouse, así:

imagen

Su mapa de entrada ahora debería verse así:

imagen

Ahora haga clic en el botón Guardar y cierre el cuadro de diálogo.

Ahora, en el código, puede verificar fácilmente la actividad usando el mapa de entrada, así:

func _process(delta):
   if(Input.is_action_pressed("MOVE_RIGHT")):
      var cur_pos = self.get_pos()
      cur_pos.x += 1
      self.set_pos(cur_pos)

Este código se ejecutará si cualquiera de las condiciones es verdadera… se presiona la tecla derecha o el botón derecho del mouse. El ejemplo anterior está sondeado, pero es igual de fácil usar un InputMap con código controlado por eventos, así:

func _input(event):   
   if(event.is_action("MOVE_RIGHT")):
      self.set_pos(Vector2(0,0))

Sin embargo, una advertencia aquí… Las acciones son más estados (como en On o Off) que eventos, por lo que probablemente tenga mucho más sentido tratar con los primeros (sondeo) que con los últimos (impulsados ​​por eventos).

Un vistazo detrás de la cortina

Si eres como yo, probablemente no estés contento con no saber exactamente lo que sucede detrás de escena. Las cajas negras simplemente no son lo mío y esta es una de las mejores cosas de que Godot sea de código abierto, ¡no hay cajas negras! Entonces, si desea comprender exactamente cómo funciona el flujo del programa, es útil saltar al código fuente.

¡ESTO ES COMPLETAMENTE OPCIONAL!

Pensé que lo pondría en negrita. La siguiente información es solo para las personas que desean comprender un poco más lo que sucede detrás de escena… Vamos a buscar el bucle principal real en el código fuente, y esencialmente esta justo aqui en la función principal/principal.cpp. Específicamente, el método iteration() es efectivamente el bucle principal:

bool Main::iteration() {

   uint64_t ticks=OS::get_singleton()->get_ticks_usec();
   uint64_t ticks_elapsed=ticks-last_ticks;

   frame+=ticks_elapsed;

   last_ticks=ticks;
   double step=(double)ticks_elapsed / 1000000.0;

   float frame_slice=1.0/OS::get_singleton()->get_iterations_per_second();

   if (step>frame_slice*8)
      step=frame_slice*8;

   time_accum+=step;

   float time_scale = OS::get_singleton()->get_time_scale();

   bool exit=false;


   int iters = 0;

   while(time_accum>frame_slice) {

      uint64_t fixed_begin = OS::get_singleton()->get_ticks_usec();

      PhysicsServer::get_singleton()->sync();
      PhysicsServer::get_singleton()->flush_queries();

      Physics2DServer::get_singleton()->sync();
      Physics2DServer::get_singleton()->flush_queries();

      if (OS::get_singleton()->get_main_loop()->iteration( frame_slice*time_scale )) {
         exit=true;
         break;
      }

      message_queue->flush();

      PhysicsServer::get_singleton()->step(frame_slice*time_scale);
      Physics2DServer::get_singleton()->step(frame_slice*time_scale);

      time_accum-=frame_slice;
      message_queue->flush();
      //if (AudioServer::get_singleton())
      // AudioServer::get_singleton()->update();

      fixed_process_max=MAX(OS::get_singleton()->get_ticks_usec()-fixed_begin,fixed_process_max);
      iters++;
   }

   uint64_t idle_begin = OS::get_singleton()->get_ticks_usec();

   OS::get_singleton()->get_main_loop()->idle( step*time_scale );
   message_queue->flush();

   if (SpatialSoundServer::get_singleton())
      SpatialSoundServer::get_singleton()->update( step*time_scale );
   if (SpatialSound2DServer::get_singleton())
      SpatialSound2DServer::get_singleton()->update( step*time_scale );


   if (OS::get_singleton()->can_draw()) {

      if ((!force_redraw_requested) && OS::get_singleton()->is_in_low_processor_usage_mode()) {
         if (VisualServer::get_singleton()->has_changed()) {
            VisualServer::get_singleton()->draw(); // flush visual commands
            OS::get_singleton()->frames_drawn++;
         }
      } else {
         VisualServer::get_singleton()->draw(); // flush visual commands
         OS::get_singleton()->frames_drawn++;
         force_redraw_requested = false;
      }
   } else {
      VisualServer::get_singleton()->flush(); // flush visual commands
   }

   if (AudioServer::get_singleton())
      AudioServer::get_singleton()->update();

   for(int i=0;i<ScriptServer::get_language_count();i++) {
      ScriptServer::get_language(i)->frame();
   }

   idle_process_max=MAX(OS::get_singleton()->get_ticks_usec()-idle_begin,idle_process_max);

   if (script_debugger)
      script_debugger->idle_poll();


   // x11_delay_usec(10000);
   frames++;

   if (frame>1000000) {

      if (GLOBAL_DEF("debug/print_fps", OS::get_singleton()->is_stdout_verbose())) {
         print_line("FPS: "+itos(frames));
      };

      OS::get_singleton()->_fps=frames;
      performance->set_process_time(idle_process_max/1000000.0);
      performance->set_fixed_process_time(fixed_process_max/1000000.0);
      idle_process_max=0;
      fixed_process_max=0;

      if (GLOBAL_DEF("debug/print_metrics", false)) {

         //PerformanceMetrics::print();
      };

      frame%=1000000;
      frames=0;
   }

   if (OS::get_singleton()->is_in_low_processor_usage_mode() || !OS::get_singleton()->can_draw())
      OS::get_singleton()->delay_usec(25000); //apply some delay to force idle time
   else {
      uint32_t frame_delay = OS::get_singleton()->get_frame_delay();
      if (frame_delay)
         OS::get_singleton()->delay_usec( OS::get_singleton()->get_frame_delay()*1000 );
   }

   int taret_fps = OS::get_singleton()->get_target_fps();
   if (taret_fps>0) {
      uint64_t time_step = 1000000L/taret_fps;
      target_ticks += time_step;
      uint64_t current_ticks = OS::get_singleton()->get_ticks_usec();
      if (current_ticks<target_ticks) OS::get_singleton()->delay_usec(target_ticks-current_ticks);
      current_ticks = OS::get_singleton()->get_ticks_usec();
      target_ticks = MIN(MAX(target_ticks,current_ticks-time_step),current_ticks+time_step);
   }

   return exit;
}

Si observa bien ese código, notará que más allá de la complejidad, en realidad es notablemente similar al pseudocódigo con el que comencé esta publicación. Como dije, la mayoría de los bucles de juego comienzan a verse bastante iguales con el tiempo. Ahora, mirando el código, notará una serie de llamadas como esta:

OS::get_singleton()->get_main_loop()->iteration( frame_slice*time_scale ))

Estas son devoluciones de llamada al MainLoop que mencionamos anteriormente. Por defecto Godot implementa uno en C++ puedes acceder aquí en core/os/main_loop.cpp:

#include "main_loop.h"
#include "script_language.h"

void MainLoop::_bind_methods() {

   ObjectTypeDB::bind_method("input_event",&MainLoop::input_event);

   BIND_CONSTANT(NOTIFICATION_WM_FOCUS_IN);
   BIND_CONSTANT(NOTIFICATION_WM_FOCUS_OUT);
   BIND_CONSTANT(NOTIFICATION_WM_QUIT_REQUEST);
   BIND_CONSTANT(NOTIFICATION_WM_UNFOCUS_REQUEST);
   BIND_CONSTANT(NOTIFICATION_OS_MEMORY_WARNING);

};

void MainLoop::set_init_script(const Ref<Script>& p_init_script) {

   init_script=p_init_script;
}

MainLoop::MainLoop() {
}


MainLoop::~MainLoop()
{
}



void MainLoop::input_text( const String& p_text ) {


}

void MainLoop::input_event( const InputEvent& p_event ) {

   if (get_script_instance())
      get_script_instance()->call("input_event",p_event);

}

void MainLoop::init() {

   if (init_script.is_valid())
      set_script(init_script.get_ref_ptr());

   if (get_script_instance())
      get_script_instance()->call("init");

}
bool MainLoop::iteration(float p_time) {

   if (get_script_instance())
      return get_script_instance()->call("iteration",p_time);

   return false;

}
bool MainLoop::idle(float p_time) {

   if (get_script_instance())
      return get_script_instance()->call("idle",p_time);

   return false;
}
void MainLoop::finish() {

   if (get_script_instance()) {
      get_script_instance()->call("finish");
      set_script(RefPtr()); //clear script
   }


}

El bucle principal predeterminado, a su vez, es principalmente un conjunto de devoluciones de llamada en el script activo. Puede reemplazar fácilmente esta implementación de MainLoop con la suya propia, ya sea en GDScript o C++. Simplemente pase el nombre de su clase en el valor to main_loop_type en Configuración del proyecto:

imagen

Por supuesto, muy pocas personas realmente necesitarán hacer esto… principalmente las personas que quieren vivir completamente en la tierra de C++. ¡Sin embargo, creo que es extremadamente valioso comprender lo que sucede detrás de escena en un motor de juego!

El video




Source link

Tags :
Ciclo,del,Engine,entrada,Godot,manejo,parte,programa,Tutorial,vida

Comparte :

Deja un comentario

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *