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 muestra algunos ejemplos de cómo son usados.

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 físicas como 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 se mueves un KinematicBody2D no debes cambiar su propiedad position directamente sino que debes usar los métodos move_And_collide() o move_and_slide(). Estos métodos mueven el cuerpo a lo largo de un vector 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

El movimiento del cuerpo cinemático sólo debe realizarse en la llamada de retorno _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 otro objeto que participa en la misma. Utiliza estos datos para calcular la respuesta a la colisión.

move_and_slide

El método move_and_slide() está para simplificar la respuesta a colisiones en los casos donde un cuerpo se debe deslizar sobre otro. Este es particularmente útil, por ejemplo, en juegos de plataformas o de vista superior.

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 efecto de deslizar:

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

    Este parámetro permite definir qué superficies deben ser consideradas como «piso». Asignando esto permite utilizar los métodos is_on_floor() (si está en el piso), 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.

  • slope_stop_min_velocity - valor por defecto: 5

    El valor mínimo de velocidad cuando se está parado en una pendiente. Esto previene que un cuerpo se deslice hacia abajo cuando un cuerpo está quieto.

  • max_bounces - 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. Asignando un valor muy bajo puede detener el movimiento completamente.

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

    Es el ángulo máximo antes de que una superficie no sea más considerada un «suelo».

move_and_slide_with_snap

This method adds some additional functionality to move_and_slide() by adding the snap parameter. As long as this vector is in contact with the ground, the body will remain attached to the surface. Note that this means you must disable snapping when jumping, for example. You can do this either by setting snap to Vector2(0, 0) or by using move_and_slide() instead.

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

Una pregunta común de usuarios nuevos de Godot es: «¿Cómo decidir qué función utilizar para movimiento?». La respuesta usual es de 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 bloques 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.

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 collsion = MoveAndCollide(_velocity * delta);
        if (collsion != null)
        {
            _velocity = _velocity.Bounce(collsion.Normal);
            if (collsion.Collider.HasMethod("Hit"))
            {
                collsion.Collider.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ó remanente al finalizar la colisión. Asignando este valor nuevamente a velocity permite un movimiento suave hacia arriba y hacia abajo en las pendientes. Intenta quitar velocity = y observa lo que sucede.

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

USando la normal del piso nos permite que funcione el salto, utilizando is_in_floor(), esta función retorna true después de una colisión reportada luego de usar move_and_slide(), siempre que la normal de colisión esté dentro de 45 grados de diferencia del vector indicado como piso (esto puede ajustarse mediante el parámetro floor_max_angle).

Esto permite implementar otras características como salto contra paredes utilizando por ejemplo is_on_wall().