Usando KinematicBody2D

Introducción

Godot ofrece una variedad de objetos de colisión para proveer detección y respuesta a colisiones. Tratar de decidir cuál usar para tu proyecto puede ser confuso. Puedes evitar problemas y simplificar el desarrollo si comprendes cómo funciona cada uno de ellos y cuáles son sus pros y contras. En este tutorial, veremos el nodo KinematicBody2D y mostraremos algunos ejemplos de cómo usarlos.

Nota

Este documento asume que los varios cuerpos físicos de Godot te son familiares. Por favor lee primero Introducción a la física.

¿Qué es un cuerpo cinemático?

KinematicBody2D está para implementar cuerpos que serán controlados por código. Pueden detectar colisiones con otros cuerpos cuando se mueven, pero no son afectados por propiedades del motor de físicas como la gravedad o fricción. Esto quiere decir que deberás escribir algo de código para crear su comportamiento, pero también significa que podrás tener un control más preciso sobre cómo se mueven y reaccionan.

Truco

Un KinematicBody2D no puede ser afectado por la gravedad y otras fuerzas, deberás calcular su movimiento por código. El motor de física no moverá un KinematicBody2D.

Movimiento y Colisión

Cuando mueves un KinematicBody2D no debes cambiar su propiedad position directamente. En lugar de eso , debes usar los métodos move_And_collide() o move_and_slide(). Estos métodos mueven el cuerpo a lo largo de un vector dado y se detendrán inmediatamente si se detecta una colisión con otro cuerpo. Luego de que un KinematicBody2D ha colisionado, cualquier respuesta a colisión deberá ser manejada manualmente.

Advertencia

Sólo debes hacer el movimiento del Kinematic Body en la llamada de _physics_process().

Ambos métodos de movimiento tienen diferentes propósitos, y más adelante se mostrarán ejemplos de cómo funcionan.

move_and_collide

Este método toma un sólo parámetro: un Vector2 que indica el movimiento relativo del cuerpo. Típicamente, este es tu vector velocidad multiplicado por el timestep del frame actual (delta). Si el motor detecta una colisión en cualquier parte del trayecto indicado por el vector, el cuerpo se detendrá inmediatamente. Si esto sucede, el método retorna un objeto KinematicCollision2D.

KinematicCollision2D es un objeto que contiene datos sobre la colisión y el objeto con el cual se colisiona. Utiliza estos datos para calcular la respuesta a la colisión.

move_and_slide

El método move_and_slide() tiene como objetivo simplificar la respuesta a colisiones en el caso común donde quieres que un cuerpo se deslice sobre otro. Este es particularmente útil en juegos de plataformas o de vista superior, por ejemplo.

Truco

move_and_slide() calcula automáticamente el movimiento por fotogramas usando delta. No multipliques tu vector de velocidad por delta antes de pasarlo a move_and_slide().

Además del vector velocidad, move_and_slide() acepta otros parámetros para personalizar el comportamiento del deslizamiento:

  • up_direction - valor por defecto: Vector2( 0, 0 )

    Este parámetro permite definir qué superficies debe considerar como suelo el motor de físicas. Al establecerlo puedes utilizar los métodos is_on_floor() (si está en el suelo), is_on_wall() (si está en la pared) , y is_on_ceiling() (si está en el techo) para detectar el tipo de superficie con la que hay contacto. El valor por defecto significa que todas las superficies son consideradas paredes.

  • stop_on_slope - valor por defecto: false

    Este parámetro evita que un cuerpo se deslice por las pendientes cuando está detenido.

  • max_slides - valor por defecto: 4

    Este es el número máximo de colisiones que se detectarán antes de que el cuerpo pare de moverse. Ponerlo demasiado bajo puede impedir el movimiento por completo.

  • floor_max_angle - valor por defecto: 0.785398 (en radianes, equivalente a 45 grados)

    Es el ángulo máximo antes de que una superficie deje de ser considerada un "suelo."

  • infinite_inertia - valor por defecto: true

Cuando este parámetro es true, el cuerpo puede empujar nodos RigidBody2D, ignorando su masa, pero no detectará colisiones con ellos. Si es false el cuerpo colisionará con cuerpos rígidos y se detendrá.

move_and_slide_with_snap

Este método añade algunas funcionalidades adicionales a move_and_slide() agregando el parámetro snap. Siempre que este vector esté en contacto con el suelo, el cuerpo permanecerá unido a la superficie. Ten en cuenta que esto significa que debes deshabilitar el snap por ejemplo, al saltar. Puedes hacer esto configurando snap a Vector2.ZERO o usando move_and_slide() en su lugar.

Detectando colisiones

Al usar move_and_collide() la función retorna un KinematicCollision2D directamente, y puedes usarlo en tu código.

Al usar move_and_slide() es posible que se produzcan múltiples colisiones, ya que es calculada la respuesta al deslizamiento. Para procesar estas colisiones, utiliza get_slide_count() y get_slide_collision():

# Using move_and_collide.
var collision = move_and_collide(velocity * delta)
if collision:
    print("I collided with ", collision.collider.name)

# Using move_and_slide.
velocity = move_and_slide(velocity)
for i in get_slide_count():
    var collision = get_slide_collision(i)
    print("I collided with ", collision.collider.name)

Nota

get_slide_count() only counts times the body has collided and changed direction.

Mira KinematicCollision2D para más detalles sobre que datos de colisión son retornados.

¿Cuál método de movimiento hay que usar?

Una duda común para los usuarios nuevos de Godot es: "¿Cómo decidir qué función de movimiento usar?". Frecuentemente, la respuesta es usar move_and_slide() porque es más "simple", pero esto no quiere decir que siempre sea así. Un modo de verlo es que move_and_slide() es un caso especial, y move_and_collide() es más general. Para poner un ejemplo, los siguientes dos fragmentos de código producen la misma respuesta a colisiones:

../../_images/k2d_compare.gif
# using move_and_collide
var collision = move_and_collide(velocity * delta)
if collision:
    velocity = velocity.slide(collision.normal)

# using move_and_slide
velocity = move_and_slide(velocity)
// using MoveAndCollide
var collision = MoveAndCollide(velocity * delta);
if (collision != null)
{
    velocity = velocity.Slide(collision.Normal);
}
// using MoveAndSlide
velocity = MoveAndSlide(velocity);

Cualquier cosa hecha con move_and_slide() se puede hacer también con move_and_collide(), pero puede requerir un poco más de código. Sin embargo, como veremos en los siguientes ejemplos, hay casos en los que move_and_slide() no provee la respuesta deseada.

En el ejemplo de arriba, asignamos la velocidad que retorna move_and_slide() a la variable velocity. Esto se debe a que cuando el personaje colisiona con el entorno, la función internamente vuelve a calcular la velocidad para reflejar la desaceleración.

Por ejemplo, si tu personaje se cae al suelo, no quieres que este acumule velocidad vertical por efecto de la gravedad. En vez de eso, quieres que su velocidad vertical sea restablecida a cero.

move_and_slide() también puede recalcular la velocidad del cuerpo cinemático varias veces en un bucle ya que, para producir un movimiento suave, mueve al personaje y colisiona hasta cinco veces por defecto. Al final del proceso, la función devuelve la nueva velocidad del personaje que podemos almacenar en nuestra variable velocity, y usarla en el siguiente frame.

Ejemplos

Para ver estos ejemplos en funcionamiento, descarga el proyecto: using_kinematic2d.zip.

Movimiento y paredes

Si has descargado el proyecto, este ejemplo está en la escena "BasicMovement.tscn".

Para este ejemplo, añade un KinematicBody2D con dos hijos: un Sprite y un CollisionShape2D. Usa el "icon.png" de Godot como textura del Sprite (arrástralo desde el panel de Sistema de Archivos a la propiedad Texture del Sprite). En la propiedad Shape de CollisionShape2D, selecciona "Nuevo RectangleShape2D" y redimensiona el rectángulo para que quepa sobre la imagen del sprite.

Nota

Ver Movimiento en 2D para más ejemplos de técnicas para implementar movimiento en 2D.

Anexa un script al KinematicBody2D y agrega el siguiente código:

extends KinematicBody2D

var speed = 250
var velocity = Vector2()

func get_input():
    # Detect up/down/left/right keystate and only move when pressed.
    velocity = Vector2()
    if Input.is_action_pressed('ui_right'):
        velocity.x += 1
    if Input.is_action_pressed('ui_left'):
        velocity.x -= 1
    if Input.is_action_pressed('ui_down'):
        velocity.y += 1
    if Input.is_action_pressed('ui_up'):
        velocity.y -= 1
    velocity = velocity.normalized() * speed

func _physics_process(delta):
    get_input()
    move_and_collide(velocity * delta)
using Godot;
using System;

public class KBExample : KinematicBody2D
{
    public int Speed = 250;
    private Vector2 _velocity = new Vector2();

    public void GetInput()
    {
        // Detect up/down/left/right keystate and only move when pressed
        _velocity = new Vector2();

        if (Input.IsActionPressed("ui_right"))
            _velocity.x += 1;

        if (Input.IsActionPressed("ui_left"))
            _velocity.x -= 1;

        if (Input.IsActionPressed("ui_down"))
            _velocity.y += 1;

        if (Input.IsActionPressed("ui_up"))
            _velocity.y -= 1;
    }

    public override void _PhysicsProcess(float delta)
    {
        GetInput();
        MoveAndCollide(_velocity * delta);
    }
}

Ejecuta esta escena y verás que move_And_collide() funciona como es esperado, moviendo el cuerpo a lo largo de lo indicado por el vector velocidad. Ahora veamos qué sucede cuando agregas algunos obstáculos. Coloca un StaticBody2D con una CollisionShape2D rectangular. Para visualizarlo puedes utilizar un Sprite, un Polygon2D o activa la opción "Ver Formas de Colisión" del menú "Depurar".

Ejecuta la escena nuevamente e intenta moverte contra el obstáculo, verás que el KinematicBody2D no puede penetrarlo. Ahora intenta moverte contra un obstáculo con la superficie en ángulo y verás que se "adhiere" en el obstáculo, como si estuviese atascado.

Esto sucede porque no hay respuesta a la colisión, move_and_collide() detiene el movimiento cuando ocurre una colisión. Tenemos que programar la respuesta que queremos cuando eso sucede.

Intenta cambiar la función a move_and_slide(velocity) y ejecútala nuevamente. Notar que se ha quitado delta del cálculo del parámetro velocity.

move_and_slide() provee una respuesta a colisión por defecto haciendo que el cuerpo se deslice a lo largo del objeto impactado. Esto es útil para muchos tipos de juegos y puede que sea todo lo necesario para obtener el comportamiento deseado.

Rebote/reflejo

¿Qué sucede si no quieres una respuesta a colisión tipo deslizar? Para este ejemplo (ver "BounceandCollide.tscn" en el mismo proyecto), tenemos un personaje disparando balas y queremos que las balas reboten al golpear las paredes.

Este ejemplo utiliza tres escenas, la escena principal contiene el Player (jugador) y las Walls (paredes), las Bullet (balas) y paredes son escenas separadas que pueden ser instanciadas.

Player es controlado con las teclas w y s para moverse hacia adelante y hacia atrás, se apunta con el puntero del ratón. Este es el código de Player, usando move_and_slide():

extends KinematicBody2D

var Bullet = preload("res://Bullet.tscn")
var speed = 200
var velocity = Vector2()

func get_input():
    # Add these actions in Project Settings -> Input Map.
    velocity = Vector2()
    if Input.is_action_pressed('backward'):
        velocity = Vector2(-speed/3, 0).rotated(rotation)
    if Input.is_action_pressed('forward'):
        velocity = Vector2(speed, 0).rotated(rotation)
    if Input.is_action_just_pressed('mouse_click'):
        shoot()

func shoot():
    # "Muzzle" is a Position2D placed at the barrel of the gun.
    var b = Bullet.instance()
    b.start($Muzzle.global_position, rotation)
    get_parent().add_child(b)

func _physics_process(delta):
    get_input()
    var dir = get_global_mouse_position() - global_position
    # Don't move if too close to the mouse pointer.
    if dir.length() > 5:
        rotation = dir.angle()
        velocity = move_and_slide(velocity)
using Godot;
using System;

public class KBExample : KinematicBody2D
{
    private PackedScene _bullet = (PackedScene)GD.Load("res://Bullet.tscn");
    public int Speed = 200;
    private Vector2 _velocity = new Vector2();

    public void GetInput()
    {
        // add these actions in Project Settings -> Input Map
        _velocity = new Vector2();
        if (Input.IsActionPressed("backward"))
        {
            _velocity = new Vector2(-Speed/3, 0).Rotated(Rotation);
        }
        if (Input.IsActionPressed("forward"))
        {
            _velocity = new Vector2(Speed, 0).Rotated(Rotation);
        }
        if (Input.IsActionPressed("mouse_click"))
        {
            Shoot();
        }
    }

    public void Shoot()
    {
        // "Muzzle" is a Position2D placed at the barrel of the gun
        var b = (Bullet)_bullet.Instance();
        b.Start(GetNode<Node2D>("Muzzle").GlobalPosition, Rotation);
        GetParent().AddChild(b);
    }

    public override void _PhysicsProcess(float delta)
    {
        GetInput();
        var dir = GetGlobalMousePosition() - GlobalPosition;
        // Don't move if too close to the mouse pointer
        if (dir.Length() > 5)
        {
            Rotation = dir.Angle();
            _velocity = MoveAndSlide(_velocity);
        }
    }
}

Y el código para Bullet:

extends KinematicBody2D

var speed = 750
var velocity = Vector2()

func start(pos, dir):
    rotation = dir
    position = pos
    velocity = Vector2(speed, 0).rotated(rotation)

func _physics_process(delta):
    var collision = move_and_collide(velocity * delta)
    if collision:
        velocity = velocity.bounce(collision.normal)
        if collision.collider.has_method("hit"):
            collision.collider.hit()

func _on_VisibilityNotifier2D_screen_exited():
    queue_free()
using Godot;
using System;

public class Bullet : KinematicBody2D
{
    public int Speed = 750;
    private Vector2 _velocity = new Vector2();

    public void Start(Vector2 pos, float dir)
    {
        Rotation = dir;
        Position = pos;
        _velocity = new Vector2(speed, 0).Rotated(Rotation);
    }

    public override void _PhysicsProcess(float delta)
    {
        var collision = MoveAndCollide(_velocity * delta);
        if (collision != null)
        {
            _velocity = _velocity.Bounce(collision.Normal);
            if (collision.Collider.HasMethod("Hit"))
            {
                collision.Collider.Call("Hit");
            }
        }
    }

    public void OnVisibilityNotifier2DScreenExited()
    {
        QueueFree();
    }
}

La acción sucede en _physics_process(). Si sucede una colisión después de utilizar move_and_collide(), la función retornará un objeto KinematicCollision2D (de otro modo retornará null).

Si retorna un objeto, usaremos la normal de la colisión para reflejar la velocity de Bullet con le método Vector2.bounce().

Si el objeto que colisiona tiene un método hit también lo llamaremos. En el proyecto agregamos un efecto de destello de color a Wall para demostrar esto.

../../_images/k2d_bullet_bounce.gif

Movimiento de juego de plataformas

Intentemos hacer un ejemplo más popular: El juego de plataformas 2D. move_and_slide() es ideal para tener rápidamente un controlador de personaje funcional. Si has descargado el proyecto de ejemplo, lo podrás encontrar en "Platformer.tscn".

Para este ejemplo, asumiremos que tienes un nivel hecho de objetos StaticBody2D, estos pueden ser de cualquier forma y tamaño. En el proyecto utilizamos Polygon2D para crear las figuras de las plataformas.

Este es el código para el jugador:

extends KinematicBody2D

export (int) var run_speed = 100
export (int) var jump_speed = -400
export (int) var gravity = 1200

var velocity = Vector2()
var jumping = false

func get_input():
    velocity.x = 0
    var right = Input.is_action_pressed('ui_right')
    var left = Input.is_action_pressed('ui_left')
    var jump = Input.is_action_just_pressed('ui_select')

    if jump and is_on_floor():
        jumping = true
        velocity.y = jump_speed
    if right:
        velocity.x += run_speed
    if left:
        velocity.x -= run_speed

func _physics_process(delta):
    get_input()
    velocity.y += gravity * delta
    if jumping and is_on_floor():
        jumping = false
    velocity = move_and_slide(velocity, Vector2(0, -1))
using Godot;
using System;

public class KBExample : KinematicBody2D
{
    [Export] public int RunSpeed = 100;
    [Export] public int JumpSpeed = -400;
    [Export] public int Gravity = 1200;

    Vector2 velocity = new Vector2();
    bool jumping = false;

    public void GetInput()
    {
        velocity.x = 0;
        bool right = Input.IsActionPressed("ui_right");
        bool left = Input.IsActionPressed("ui_left");
        bool jump = Input.IsActionPressed("ui_select");

        if (jump && IsOnFloor())
        {
            jumping = true;
            velocity.y = JumpSpeed;
        }

        if (right)
            velocity.x += RunSpeed;
        if (left)
            velocity.x -= RunSpeed;
    }

    public override void _PhysicsProcess(float delta)
    {
        GetInput();
        velocity.y += Gravity * delta;
        if (jumping && IsOnFloor())
            jumping = false;
        velocity = MoveAndSlide(velocity, new Vector2(0, -1));
    }
}
../../_images/k2d_platform.gif

Cuando se utiliza la función move_and_slide(), esta retorna un vector que representa el movimiento que quedó después que la colisión con deslizamiento ocurriera. Asignando este de vuelta a la velocity del personaje nos permite un movimiento suave hacia arriba y hacia abajo en las pendientes. Prueba quitar velocity = y observa lo que sucede.

Nota también que hemos agregado Vector2(0,-1) como normal del suelo. Este es un vector que apunta hacia arriba. Esto significa que, si el personaje colisiona con un objeto que tiene esta normal, será considerado como suelo.

Usando la normal del suelo nos permite que funcione el salto, utilizando is_in_floor(). Esta función solo retornará true después de una colisión usando move_and_slide() siempre que la normal del cuerpo colisionado esté dentro de 45 grados de diferencia del vector indicado como suelo. Puedes controlar el ángulo máximo estableciendo un floor_max_angle.

Este ángulo también te permite implementar otras características como saltos en paredes usando is_on_wall(), por ejemplo.