Parte 1

Introducción al tutorial

../../../_images/FinishedTutorialPicture.png

Esta serie de tutoriales te mostrará cómo crear un juego FPS para un solo jugador.

A lo largo del curso de esta serie de tutoriales, cubriremos cómo:

  • Hacer un personaje en primera persona que pueda moverse, correr y saltar.

  • Hacer una máquina de estado de animación simple para manejar transiciones de animación.

  • Añadir tres armas al personaje en primera persona, cada una usando una forma diferente de manejar las colisiones de balas:

    • Un cuchillo (usando un Area)

    • Una pistola (Escenas de bala)

  • Añadir dos tipos diferentes de granadas al personaje en primera persona:

    • Una granada normal

    • Una granada pegajosa

  • Añadir la habilidad de agarrar y lanzar nodos RigidBody

  • Añadir una entrada de mando (joypad) para el jugador

  • Añadir munición y recargar todas las armas que consumen munición.

  • Añadir munición y recuperadores de salud

    • En dos tamaños: grande y pequeño

  • Añadir una torreta automática

    • Que puede disparar usando objetos bala o un :ref:`Raycast <class_Raycast>

  • Añadir objetivos que se rompen cuando han recibido suficiente daño

  • Añadir sonidos que suenan cuando las armas disparan.

  • Añadir un menú principal simple:

    • Con un menú de opciones para cambiar el funcionamiento del juego

    • Con una pantalla de selección de nivel

  • Añadir un menú de pausa universal que podemos acceder desde cualquier lugar

Nota

Aunque este tutorial puede ser completado por principiantes, es altamente aconsejable completar Tu primer juego, si eres nuevo en Godot y/o en el desarrollo de juegos antes de pasar por esta serie de tutoriales.

Recuerda: Hacer juegos 3D es mucho más difícil que hacer juegos 2D. Si no sabes cómo crear juegos 2D es probable que tengas dificultades para desarrollar juegos 3D.

Este tutorial asume que tienes experiencia trabajando con el editor de Godot, que tienes experiencia básica de programación en GDScript y experiencia básica en el desarrollo de juegos.

Puedes encontrar los recursos de inicio para este tutorial aquí: Godot_FPS_Starter.zip

Los recursos de inicio proporcionados contienen un modelo 3D animado, un montón de modelos 3D para hacer niveles y algunas escenas ya configuradas para este tutorial.

Todos los recursos proporcionados (a menos que se indique lo contrario) fueron creados originalmente por TwistedTwigleg, con cambios/adiciones por la comunidad de Godot. Todos los activos originales proporcionados para este tutorial están liberados bajo la licencia MIT.

¡Siéntete libre de usar estos recursos como quieras! Todos los recursos originales pertenecen a la comunidad Godot, con los otros recursos pertenecientes a los enumerados a continuación:

Nota

El skybox es creado por StumpyStrust en OpenGameArt. El skybox utilizado está bajo licencia CC0.

La fuente utilizada es Titillium-Regular, y está licenciada bajo la SIL Open Font License, Version 1.1.

Truco

Puedes encontrar el proyecto terminado para cada parte en la parte inferior de la página de cada parte

Resumen

En esta parte vamos a hacer un jugador de primera persona que puede moverse alrededor del entorno.

../../../_images/PartOneFinished.png

Al final de esta parte tendrás personaje en primera persona un funcional, que puede moverse por el entorno del juego, correr, mirar a su alrededor con una cámara en primera persona basada en el ratón, que puede saltar, encender y apagar una linterna.

Preparando todo

Inicia Godot y abre el proyecto incluido en los recursos iniciales.

Nota

Aunque estos recursos no son necesariamente requeridos para utilizar los scripts proporcionados en este tutorial, harán que el tutorial sea mucho más fácil de seguir ya que hay varias escenas preconfiguradas que utilizaremos a lo largo de la serie de tutoriales.

En primer lugar, abre los ajustes del proyecto y ve a la pestaña "Mapa de Entradas". Encontrarás que ya se han definido varias acciones. Usaremos estas acciones para nuestro jugador. Siéntete libre de cambiar las teclas vinculadas a estas acciones si lo deseas.


Tomémonos un segundo para ver qué tenemos en los recursos iniciales.

En los recursos iniciales se incluyen varias escenas. Por ejemplo, en res:// tenemos 14 escenas, la mayoría de los cuales estaremos visitando a medida que avancemos en esta serie de tutoriales.

Por ahora abramos Player.tscn.

Nota

Hay un montón de escenas y unas pocas texturas en la carpeta de Recursos. Puedes mirarlas si quieres, pero no vamos a explorar Recursos en esta serie de tutoriales. Los Recursos contienen todos los modelos utilizados para cada uno de los niveles, así como algunas texturas y materiales.

Haciendo la lógica del movimiento FPS

Una vez que tengas abierto el Player.tscn, echemos un vistazo rápido a cómo está configurado:

../../../_images/PlayerSceneTree.png

Primero, fíjate en cómo se configuran las formas de colisión del jugador. El uso de una cápsula de apunte vertical como forma de colisión para el jugador es bastante común en la mayoría de los juegos en primera persona.

Añadimos un pequeño cuadrado a los "pies" del jugador para que éste no sienta que se está equilibrando en un solo punto.

Queremos los "pies" ligeramente más altos que el fondo de la cápsula para poder rodar sobre los bordes leves. Dónde colocar los "pies" depende de tus niveles y de cómo quieras que se sienta tu jugador.

Nota

Muchas veces el jugador notará que la forma de la colisión es circular cuando camina hacia un borde y se desliza. Añadimos el pequeño cuadrado en la parte inferior de la cápsula para reducir el deslizamiento en los bordes y alrededor de ellos.

Otra cosa que hay que notar es cuántos nodos son hijos de Rotation_Helper. Esto es porque Rotation_Helper contiene todos los nodos que queremos rotar en el eje "X" (arriba y abajo). La razón de esto es para que podamos rotar Player en el eje Y, y Rotation_helper en el eje X.

Nota

Otra cosa que hay que notar es cuántos nodos son hijos de Rotation_Helper. Esto es porque Rotation_Helper contiene todos los nodos que queremos rotar en el eje "X" (arriba y abajo). La razón de esto es para que podamos rotar Player en el eje Y, y Rotation_helper en el eje X.

Ver using transforms para más información


Adjunta un nuevo script al nodo Player y llámalo Player.gd.

Programemos nuestro reproductor añadiendo la capacidad de moverse, mirar con el ratón y saltar. Añade el siguiente código a Player.gd:

extends KinematicBody

const GRAVITY = -24.8
var vel = Vector3()
const MAX_SPEED = 20
const JUMP_SPEED = 18
const ACCEL = 4.5

var dir = Vector3()

const DEACCEL= 16
const MAX_SLOPE_ANGLE = 40

var camera
var rotation_helper

var MOUSE_SENSITIVITY = 0.05

func _ready():
    camera = $Rotation_Helper/Camera
    rotation_helper = $Rotation_Helper

    Input.set_mouse_mode(Input.MOUSE_MODE_CAPTURED)

func _physics_process(delta):
    process_input(delta)
    process_movement(delta)

func process_input(delta):

    # ----------------------------------
    # Walking
    dir = Vector3()
    var cam_xform = camera.get_global_transform()

    var input_movement_vector = Vector2()

    if Input.is_action_pressed("movement_forward"):
        input_movement_vector.y += 1
    if Input.is_action_pressed("movement_backward"):
        input_movement_vector.y -= 1
    if Input.is_action_pressed("movement_left"):
        input_movement_vector.x -= 1
    if Input.is_action_pressed("movement_right"):
        input_movement_vector.x += 1

    input_movement_vector = input_movement_vector.normalized()

    # Basis vectors are already normalized.
    dir += -cam_xform.basis.z * input_movement_vector.y
    dir += cam_xform.basis.x * input_movement_vector.x
    # ----------------------------------

    # ----------------------------------
    # Jumping
    if is_on_floor():
        if Input.is_action_just_pressed("movement_jump"):
            vel.y = JUMP_SPEED
    # ----------------------------------

    # ----------------------------------
    # Capturing/Freeing the cursor
    if Input.is_action_just_pressed("ui_cancel"):
        if Input.get_mouse_mode() == Input.MOUSE_MODE_VISIBLE:
            Input.set_mouse_mode(Input.MOUSE_MODE_CAPTURED)
        else:
            Input.set_mouse_mode(Input.MOUSE_MODE_VISIBLE)
    # ----------------------------------

func process_movement(delta):
    dir.y = 0
    dir = dir.normalized()

    vel.y += delta * GRAVITY

    var hvel = vel
    hvel.y = 0

    var target = dir
    target *= MAX_SPEED

    var accel
    if dir.dot(hvel) > 0:
        accel = ACCEL
    else:
        accel = DEACCEL

    hvel = hvel.linear_interpolate(target, accel * delta)
    vel.x = hvel.x
    vel.z = hvel.z
    vel = move_and_slide(vel, Vector3(0, 1, 0), 0.05, 4, deg2rad(MAX_SLOPE_ANGLE))

func _input(event):
    if event is InputEventMouseMotion and Input.get_mouse_mode() == Input.MOUSE_MODE_CAPTURED:
        rotation_helper.rotate_x(deg2rad(event.relative.y * MOUSE_SENSITIVITY))
        self.rotate_y(deg2rad(event.relative.x * MOUSE_SENSITIVITY * -1))

        var camera_rot = rotation_helper.rotation_degrees
        camera_rot.x = clamp(camera_rot.x, -70, 70)
        rotation_helper.rotation_degrees = camera_rot
using Godot;
using System;

public class Player : KinematicBody
{
    [Export]
    public float Gravity = -24.8f;
    [Export]
    public float MaxSpeed = 20.0f;
    [Export]
    public float JumpSpeed = 18.0f;
    [Export]
    public float Accel = 4.5f;
    [Export]
    public float Deaccel = 16.0f;
    [Export]
    public float MaxSlopeAngle = 40.0f;
    [Export]
    public float MouseSensitivity = 0.05f;

    private Vector3 _vel = new Vector3();
    private Vector3 _dir = new Vector3();

    private Camera _camera;
    private Spatial _rotationHelper;

    // Called when the node enters the scene tree for the first time.
    public override void _Ready()
    {
        _camera = GetNode<Camera>("Rotation_Helper/Camera");
        _rotationHelper = GetNode<Spatial>("Rotation_Helper");

        Input.SetMouseMode(Input.MouseMode.Captured);
    }

    public override void _PhysicsProcess(float delta)
    {
        ProcessInput(delta);
        ProcessMovement(delta);
    }

    private void ProcessInput(float delta)
    {
        //  -------------------------------------------------------------------
        //  Walking
        _dir = new Vector3();
        Transform camXform = _camera.GlobalTransform;

        Vector2 inputMovementVector = new Vector2();

        if (Input.IsActionPressed("movement_forward"))
            inputMovementVector.y += 1;
        if (Input.IsActionPressed("movement_backward"))
            inputMovementVector.y -= 1;
        if (Input.IsActionPressed("movement_left"))
            inputMovementVector.x -= 1;
        if (Input.IsActionPressed("movement_right"))
            inputMovementVector.x += 1;

        inputMovementVector = inputMovementVector.Normalized();

        // Basis vectors are already normalized.
        _dir += -camXform.basis.z * inputMovementVector.y;
        _dir += camXform.basis.x * inputMovementVector.x;
        //  -------------------------------------------------------------------

        //  -------------------------------------------------------------------
        //  Jumping
        if (IsOnFloor())
        {
            if (Input.IsActionJustPressed("movement_jump"))
                _vel.y = JumpSpeed;
        }
        //  -------------------------------------------------------------------

        //  -------------------------------------------------------------------
        //  Capturing/Freeing the cursor
        if (Input.IsActionJustPressed("ui_cancel"))
        {
            if (Input.GetMouseMode() == Input.MouseMode.Visible)
                Input.SetMouseMode(Input.MouseMode.Captured);
            else
                Input.SetMouseMode(Input.MouseMode.Visible);
        }
        //  -------------------------------------------------------------------
    }

    private void ProcessMovement(float delta)
    {
        _dir.y = 0;
        _dir = _dir.Normalized();

        _vel.y += delta * Gravity;

        Vector3 hvel = _vel;
        hvel.y = 0;

        Vector3 target = _dir;

        target *= MaxSpeed;

        float accel;
        if (_dir.Dot(hvel) > 0)
            accel = Accel;
        else
            accel = Deaccel;

        hvel = hvel.LinearInterpolate(target, accel * delta);
        _vel.x = hvel.x;
        _vel.z = hvel.z;
        _vel = MoveAndSlide(_vel, new Vector3(0, 1, 0), false, 4, Mathf.Deg2Rad(MaxSlopeAngle));
    }

    public override void _Input(InputEvent @event)
    {
        if (@event is InputEventMouseMotion && Input.GetMouseMode() == Input.MouseMode.Captured)
        {
            InputEventMouseMotion mouseEvent = @event as InputEventMouseMotion;
            _rotationHelper.RotateX(Mathf.Deg2Rad(mouseEvent.Relative.y * MouseSensitivity));
            RotateY(Mathf.Deg2Rad(-mouseEvent.Relative.x * MouseSensitivity));

            Vector3 cameraRot = _rotationHelper.RotationDegrees;
            cameraRot.x = Mathf.Clamp(cameraRot.x, -70, 70);
            _rotationHelper.RotationDegrees = cameraRot;
        }
    }
}

Esto es un montón de código, así que vamos a desglosarlo función por función:

Truco

Aunque copiar y pegar código no es aconsejable, ya que se puede aprender mucho escribiendo el código manualmente, puedes copiar y pegar el código de esta página directamente en el editor de scripts.

Si haces esto, todo el código copiado estará usando espacios en lugar de tabulaciones.

Para convertir los espacios en pestañas en el editor de scripts, haz clic en el menú "Editar" y selecciona "Convertir indentados en tabuladores". Esto convertirá todos los espacios en tabuladores. Puedes seleccionar "Convertir indentados en tabuladores" para convertir los tabuladores de nuevo en espacios.


Primero, definiremos algunas variables de clase para dictar cómo nuestro jugador se moverá por el mundo.

Nota

A lo largo de este tutorial, las variables definidas fuera de las funciones serán referidas como "variables de clase ". Esto se debe a que podemos acceder a cualquiera de estas variables desde cualquier lugar del script.

Vamos a repasar cada una de las variables de clase:

  • "GRAVEDAD": Como la fuerte gravedad nos empuja hacia abajo.

  • vel: nuestra velocidad para el :ref`KinematicBody <class_KinematicBody>`.

  • "MAX_SPEED": La velocidad más rápida que podemos alcanzar. Una vez que alcancemos esta velocidad, no podremos aumentarla.

  • JUMP_SPEED: Qué tan alto podemos saltar.

  • "ACCEL": La rapidez con que aceleramos. Cuanto más alto sea el valor, más rápido llegaremos a la velocidad máxima.

  • "DEACCEL": Qué tan rápido vamos a frenarnos. Cuanto más alto sea el valor, antes nos detendremos por completo.

  • "ÁNGULO DE INCLINACIÓN MÁXIMA": El ángulo más pronunciado que nuestro :ref:``KinematicBody <class_KinematicBody>` considerará como un 'piso'.

  • camara: El nodo Camera.

  • rotation_helper: Un nodo Spatial que contiene todo lo que queremos que rote en el eje X (arriba y abajo).

  • "SENSIBILIDAD AL RATÓN": Lo sensible que es el ratón. Encuentro que un valor de "0.05" funciona bien para mi ratón, pero puede que necesites cambiarlo basado en lo sensible que es tu ratón.

Puedes ajustar muchas de estas variables para obtener resultados diferentes. Por ejemplo, bajando GRAVEDAD y/o aumentando JUMP_SPEED puedes obtener un carácter con sensación flotante. Siéntete libre de experimentar!

Nota

Habrás notado que SENSIBILIDAD DE RATON está escrita en mayúsculas como las otras constantes, pero la SENSIBILIDAD DE RATON no es una constante.

La razón de esto es que queremos tratarlo como una variable constante (una variable que no puede cambiar) a lo largo de todo nuestro script, pero queremos poder cambiar el valor más tarde cuando agreguemos ajustes personalizables. Así que, en un esfuerzo por recordarnos a nosotros mismos tratarla como una constante, se nombra en mayúsculas.


Ahora veamos la función _ready:

Primero obtenemos los nodos de la camara y de la rotation_helper y los almacenamos en sus variables.

Entonces tenemos que establecer el modo de ratón a capturado, para que el ratón no pueda salir de la ventana del juego.

Esto esconderá el ratón y lo mantendrá en el centro de la pantalla. Hacemos esto por dos razones: La primera razón es que no queremos que el jugador vea el cursor del ratón mientras juega.

La segunda razón es porque no queremos que el cursor salga de la ventana del juego. Si el cursor sale de la ventana del juego podría haber casos en los que el jugador hace clic fuera de la ventana, y entonces el juego perdería el foco. Para asegurarnos de que ninguno de estos problemas ocurra, capturamos el cursor del ratón.

Nota

Ver Input documentation para los distintos modos de ratón. Sólo usaremos "MOUSE_MODE_CAPTURED" y "MOUSE_MODE_VISIBLE" en esta serie de tutoriales.


A continuación, echemos un vistazo al -physics_process:

Todo lo que estamos haciendo en _physics_process es llamar a dos funciones: process_input and process_movement.

process_input será donde almacenaremos todo el código relacionado con la entrada del jugador. Queremos llamarlo primero, antes que nada, para tener una nueva entrada del jugador con la que trabajar.

process_movement es donde enviaremos todos los datos necesarios al KinematicBody para que pueda moverse por el mundo del juego.


Veamos el process_input a continuación:

Primero ponemos dir a un Vector3.

dir se usará para almacenar la dirección hacia la que el jugador pretende moverse. Como no queremos que la entrada previa del jugador afecte al jugador más allá de una simple llamada de process_movement, reiniciamos el dir.

Luego obtenemos la transformada global de la cámara y la almacenamos también, en la variable cam_xform.

La razón por la que necesitamos la transformada global de la cámara es para poder usar sus vectores direccionales. Muchos han encontrado los vectores direccionales confusos, así que vamos a tomarnos un momento para explicar cómo funcionan:


El espacio mundial puede definirse como: El espacio en el que todos los objetos se colocan, en relación con un punto de origen constante. Cada objeto, no importa si es 2D o 3D, tiene una posición en el espacio mundial.

Para decirlo de otra manera: el espacio mundial es el espacio en un universo donde la posición, rotación y escala de cada objeto puede ser medida por un único punto fijo conocido llamado el origen.

En Godot, el origen está en la posición (0, 0, 0) con una rotación de (0, 0, 0) y una escala de (1, 1, 1).

Nota

Cuando abres el editor de Godot y seleccionas un nodo basado en Spatial, aparece un gizmo. Cada una de las flechas usando las direcciones del espacio mundial por defecto.

Si quieres moverte usando los vectores direccionales espaciales del mundo, haz algo como esto:

if Input.is_action_pressed("movement_forward"):
    node.translate(Vector3(0, 0, 1))
if Input.is_action_pressed("movement_backward"):
    node.translate(Vector3(0, 0, -1))
if Input.is_action_pressed("movement_left"):
    node.translate(Vector3(1, 0, 0))
if Input.is_action_pressed("movement_right"):
    node.translate(Vector3(-1, 0, 0))
if (Input.IsActionPressed("movement_forward"))
    node.Translate(new Vector3(0, 0, 1));
if (Input.IsActionPressed("movement_backward"))
    node.Translate(new Vector3(0, 0, -1));
if (Input.IsActionPressed("movement_left"))
    node.Translate(new Vector3(1, 0, 0));
if (Input.IsActionPressed("movement_right"))
    node.Translate(new Vector3(-1, 0, 0));

Nota

Fíjense que no necesitamos hacer ningún cálculo para obtener los vectores direccionales del espacio mundial. Podemos definir unas pocas variables Vector3 e introducir los valores que apuntan en cada dirección.

Así es como se ve el espacio mundial en 2D:

Nota

Las siguientes imágenes son sólo ejemplos. Cada flecha/rectángulo representa un vector direccional

../../../_images/WorldSpaceExample.png

Y esto es lo que parece en 3D:

../../../_images/WorldSpaceExample_3D.png

Observa cómo en ambos ejemplos, la rotación del nodo no cambia las flechas direccionales. Esto se debe a que el espacio del mundo es una constante. No importa cómo se traslade, rote o escale un objeto, el espacio mundial siempre apuntará en la misma dirección.

El espacio local es diferente, porque tiene en cuenta la rotación del objeto.

El espacio local puede definirse de la siguiente manera: El espacio en el que la posición de un objeto es el origen del universo. Debido a que la posición del origen puede estar en N muchos lugares, los valores derivados del espacio local cambian con la posición del origen.

Nota

Esta pregunta de Game Development Stack Exchange tiene una explicación mejor del espacio mundial y del espacio local.

https://gamedev.stackexchange.com/questions/65783/what-are-world-space-and-eye-space-in-game-development (El espacio local y el espacio ocular son esencialmente lo mismo en este contexto)

Para obtener el espacio local de un nodo Spatial, necesitamos obtener su Transform, así que podemos obtener el Basis del Transform.

Cada Base tiene tres vectores: X, Y y Z. Cada uno de esos vectores apunta hacia cada uno de los vectores espaciales locales que vienen de ese objeto.

Para usar los vectores direccionales locales del nodo Spatial, usaremos este código:

if Input.is_action_pressed("movement_forward"):
    node.translate(node.global_transform.basis.z.normalized())
if Input.is_action_pressed("movement_backward"):
    node.translate(-node.global_transform.basis.z.normalized())
if Input.is_action_pressed("movement_left"):
    node.translate(node.global_transform.basis.x.normalized())
if Input.is_action_pressed("movement_right"):
    node.translate(-node.global_transform.basis.x.normalized())
if (Input.IsActionPressed("movement_forward"))
    node.Translate(node.GlobalTransform.basis.z.Normalized());
if (Input.IsActionPressed("movement_backward"))
    node.Translate(-node.GlobalTransform.basis.z.Normalized());
if (Input.IsActionPressed("movement_left"))
    node.Translate(node.GlobalTransform.basis.x.Normalized());
if (Input.IsActionPressed("movement_right"))
    node.Translate(-node.GlobalTransform.basis.x.Normalized());

Así es como se ve el espacio local en 2D:

../../../_images/LocalSpaceExample.png

Y esto es lo que parece en 3D:

../../../_images/LocalSpaceExample_3D.png

Esto es lo que el gizmo Spatial muestra cuando usas el modo de espacio local. Fíjate en cómo las flechas siguen la rotación del objeto de la izquierda, que se ve exactamente igual que el ejemplo 3D para el espacio local.

Nota

Puedes cambiar entre los modos de espacio local y mundial presionando T o el pequeño botón del cubo cuando tienes seleccionado un nodo basado en Spatial.

../../../_images/LocalSpaceExampleGizmo.png

Los vectores locales son confusos incluso para los desarrolladores de juegos más experimentados, así que no te preocupes si todo esto no tiene mucho sentido. Lo que hay que recordar sobre los vectores locales es que estamos usando coordenadas locales para obtener la dirección desde el punto de vista del objeto, en contraposición a usar vectores mundiales, que dan la dirección desde el punto de vista del mundo.


Bien, volvamos al process_input:

A continuación hacemos una nueva variable llamada input_movement_vector y la asignamos a un empty Vector2. Usaremos esto para hacer una especie de eje virtual, para mapear la entrada del jugador al movimiento.

Nota

Esto puede parecer exagerado sólo para el teclado, pero tendrá sentido más tarde cuando agreguemos la entrada del joypad.

Basándonos en la acción de movimiento direccional que se presiona, añadimos o restamos a "input_movement_vector".

Después de comprobar cada una de las acciones de movimiento direccional, normalizamos el input_movement_vector. Esto hace que los valores de input_movement_vector estén dentro de un círculo de unidad de radio de 1.

A continuación, añadimos el vector Z local de la cámara a los tiempos input_movement_vector.y a dir. Esto es así, cuando el jugador presiona hacia adelante o hacia atrás, añadimos el eje local Z de la cámara para que el jugador se mueva hacia adelante o hacia atrás en relación con la cámara.

Nota

Debido a que la cámara está rotada por -180 grados, tenemos que voltear el vector direccional de la Z. Normalmente hacia adelante sería el eje Z positivo, así que usar basis.z.normalized() funcionaría, pero estamos usando -basis.z.normalized() porque el eje Z de nuestra cámara está orientado hacia atrás en relación con el resto del jugador.

Hacemos lo mismo para el vector X local de la cámara, y en lugar de usar input_movement_vector.y usamos en su lugar input_movement_vector.x. Esto hace que el jugador se mueva a la izquierda/derecha en relación con la cámara cuando el jugador presiona izquierda/derecha.

A continuación comprobamos si el jugador está en el suelo usando la función KinematicBody's is_on_floor. Si lo está, entonces comprobamos si la acción "movement_jump" acaba de ser pulsada. Si lo ha hecho, entonces ponemos la velocidad Y del jugador en JUMP_SPEED.

Debido a que estamos ajustando la velocidad Y, el jugador saltará en el aire.

Entonces comprobamos la acción de ui_cancel. Esto es para que podamos liberar/capturar el cursor del ratón cuando el botón escape sea presionado. Hacemos esto porque de otra manera no tendríamos forma de liberar el cursor, lo que significa que estaría atascado hasta que termines el tiempo de ejecución.

Para liberar/capturar el cursor, comprobamos si el ratón está visible (liberado) o no. Si lo está, lo capturamos, y si no lo está, lo hacemos visible (lo liberamos).

Eso es todo lo que estamos haciendo ahora mismo para la process_input. Volveremos varias veces a esta función a medida que agreguemos más complejidades a nuestro jugador.


Ahora veamos el process_movement:

Primero nos aseguramos de que dir no tenga ningún movimiento en el eje Y poniendo su valor Y a cero.

Luego normalizamos dir para asegurarnos de que estamos dentro de un círculo de unidad de radio de 1. Esto hace que nos movamos a una velocidad constante sin importar si el jugador se mueve recto o en diagonal. Si no normalizamos, el jugador se moverá más rápido en la diagonal que cuando vaya en línea recta.

A continuación, añadimos la gravedad al jugador añadiendo GRAVITY * delta a la velocidad Y del jugador.

Después de eso asignamos la velocidad del jugador a una nueva variable (llamada hvel) y eliminamos cualquier movimiento en el eje Y.

A continuación establecemos una nueva variable (target) en el vector de dirección del jugador. Luego la multiplicamos por la velocidad máxima del jugador para saber cuán lejos se moverá el jugador en la dirección proporcionada por dir.

Después de eso hacemos una nueva variable para la aceleración, llamada accel.

Luego tomamos el producto escalar de hvel para ver si el jugador se mueve según hvel. Recuerda, el hvel no tiene ninguna velocidad Y, lo que significa que sólo estamos comprobando si el jugador se mueve hacia adelante, hacia atrás, a la izquierda o a la derecha.

Luego tomamos el producto escalar de hvel para ver si el jugador se mueve según hvel. Recuerda, el hvel no tiene ninguna velocidad Y, lo que significa que sólo estamos comprobando si el jugador se mueve hacia adelante, hacia atrás, a la izquierda o a la derecha.

Luego interpolamos la velocidad horizontal, ponemos la velocidad X y Z del jugador a la velocidad horizontal interpolada, y llamamos a move_and_slide para que el KinematicBody se encargue de mover al jugador por el mundo de la física.

Truco

¡Todo el código en process_movement es exactamente igual que el código de movimiento de la demo Kinematic Character!


La función final que tenemos es la función _input, y afortunadamente es bastante corta:

Primero nos aseguramos de que el evento que estamos tratando es un InputEventMouseMotion evento. También queremos comprobar si el cursor es capturado, ya que no queremos rotar si no lo es.

Nota

Ver Mouse and input coordinates para una lista de posibles eventos de entrada.

Si el evento es de hecho un evento de movimiento de ratón y el cursor es capturado, rotamos basándonos en el movimiento relativo del ratón proporcionado por InputEventMouseMotion.

Primero rotamos el nodo rotation_helper en el eje X, usando el valor Y del movimiento relativo del ratón, proporcionado por InputEventMouseMotion.

Luego rotamos todo el KinematicBody en el eje Y por el valor X del movimiento relativo del ratón.

Truco

Godot convierte el movimiento relativo del ratón en un Vector2 donde el movimiento del ratón al subir y bajar es 1 y -1 respectivamente. El movimiento de la derecha y la izquierda es 1 y -1 respectivamente.

Debido a la forma en que estamos rotando el jugador, multiplicamos el valor X del movimiento relativo del ratón por -1 para que el movimiento del ratón que va a la izquierda y a la derecha rote el jugador a la izquierda y a la derecha en la misma dirección.

Por último, fijamos la rotación X de rotation_helper entre -70 y 70 grados para que el jugador no pueda girar al revés.

Truco

Ver using transforms para más información sobre las transformaciones rotativas.


Para probar el código, abre la escena llamada Testing_Area.tscn, si no está ya abierta. Usaremos esta escena a medida que vayamos avanzando en las próximas partes del tutorial, así que asegúrate de mantenerla abierta en una de tus pestañas de escenas.

Adelante y prueba tu código ya sea presionando F6 con Testing_Area.tscn como la pestaña abierta, presionando el botón de reproducción en la esquina superior derecha, o presionando F5. Ahora deberías ser capaz de caminar, saltar en el aire, y mirar alrededor usando el ratón.

Dando al jugador una linterna y la opción de esprintar

Antes de que lleguemos a hacer funcionar las armas, hay un par de cosas más que deberíamos añadir.

Muchos juegos de FPS tienen una opción para correr y una linterna. Podemos fácilmente añadirlas a nuestro jugador, ¡así que hagámoslo!

Primero necesitamos unas cuantas variables de clase más en nuestro script de jugador:

const MAX_SPRINT_SPEED = 30
const SPRINT_ACCEL = 18
var is_sprinting = false

var flashlight
[Export]
public float MaxSprintSpeed = 30.0f;
[Export]
public float SprintAccel = 18.0f;
private bool _isSprinting = false;

private SpotLight _flashlight;

Todas las variables de sprint funcionan exactamente igual que las variables de no sprint con nombres similares.

is_sprinting es una booleana para saber si el jugador está corriendo, y "flashlight" es una variable que usaremos para mantener el nodo de la luz de la linterna del jugador.

Ahora tenemos que añadir unas cuantas líneas de código, empezando por _ready. Añade lo siguiente a _ready:

flashlight = $Rotation_Helper/Flashlight
_flashlight = GetNode<SpotLight>("Rotation_Helper/Flashlight");

Esto obtiene el nodo Flashlight (linterna) y lo asigna a la variable flashlight.


Ahora tenemos que cambiar algo del código en process_input. Añade lo siguiente en algún lugar de process_input:

# ----------------------------------
# Sprinting
if Input.is_action_pressed("movement_sprint"):
    is_sprinting = true
else:
    is_sprinting = false
# ----------------------------------

# ----------------------------------
# Turning the flashlight on/off
if Input.is_action_just_pressed("flashlight"):
    if flashlight.is_visible_in_tree():
        flashlight.hide()
    else:
        flashlight.show()
# ----------------------------------
//  -------------------------------------------------------------------
//  Sprinting
if (Input.IsActionPressed("movement_sprint"))
    _isSprinting = true;
else
    _isSprinting = false;
//  -------------------------------------------------------------------

//  -------------------------------------------------------------------
//  Turning the flashlight on/off
if (Input.IsActionJustPressed("flashlight"))
{
    if (_flashlight.IsVisibleInTree())
        _flashlight.Hide();
    else
        _flashlight.Show();
}

Repasemos los añadidos:

Ponemos is_sprinting en true cuando el jugador mantiene presionada la acción de la movement_sprint y false cuando la acción de la movement_sprint es liberada. En process_movement añadiremos el código que hace que el jugador sea más rápido cuando corra. Aquí, en process_input vamos a cambiar la variable is_sprinting.

Hacemos algo similar a liberar/capturar el cursor para manejar la linterna. Primero comprobamos si la acción "linterna" acaba de ser presionada. Si lo estaba, entonces comprobamos si la "linterna" es visible en el árbol de la escena. Si lo es, entonces la escondemos, y si no lo es, la mostramos.


Ahora tenemos que cambiar un par de cosas en process_movement. Primero, reemplace``target *= MAX_SPEED`` con lo siguiente:

if is_sprinting:
    target *= MAX_SPRINT_SPEED
else:
    target *= MAX_SPEED
if (_isSprinting)
    target *= MaxSprintSpeed;
else
    target *= MaxSpeed;

Ahora, en lugar de multiplicar siempre target por MAX_SPEED, primero comprobamos si el jugador está corriendo o no. Si el jugador está corriendo, multiplicamos target por MAX_SPRINT_SPEED.

Ahora todo lo que queda es cambiar la aceleración al correr. Cambiar acceAhora todo lo que queda es cambiar la aceleración al correr. Cambiar ``accel = ACCEL a lo siguiente:l = ACCEL`` a lo siguiente:

if is_sprinting:
    accel = SPRINT_ACCEL
else:
    accel = ACCEL
if (_isSprinting)
    accel = SprintAccel;
else
    accel = Accel;

Ahora, cuando el jugador esté corriendo, usaremos SPRINT_ACCEL en lugar de ACEL, lo que acelerará al jugador más rápido.


Ahora deberías ser capaz de correr si presionas Shift, y puedes encender y apagar la linterna presionando F!

¡Ve a probarlo! ¡Puedes cambiar las variables de clase relacionadas con el sprint para que el jugador sea más rápido o más lento al sprint!

Notas finales

../../../_images/PartOneFinished.png

¡Uf! Eso fue un montón de trabajo. ¡Ahora tienes un personaje en primera persona que funciona completamente!

En Parte 2 añadiremos algunas armas a nuestro personaje jugador.

Nota

¡En este punto hemos recreado la demo del Personaje Cinemático desde una perspectiva en primera persona con sprint y una luz de flash!

Truco

Actualmente el guión del jugador estaría en un estado ideal para hacer todo tipo de juegos en primera persona. ¡Por ejemplo: Juegos de terror, juegos de plataformas, juegos de aventuras y más!

Advertencia

¡Si alguna vez te pierdes, asegúrate de leer el código de nuevo!

Puede descargar el proyecto terminado para esta parte aquí: Godot_FPS_Part_1.zip