使用 KinematicBody2D

前言

Godot提供了几种碰撞对象来提供碰撞检测和响应. 试图决定在你的项目中使用哪一个可能会让你感到困惑. 如果你了解它们中的每一个是如何工作的, 以及它们的优点和缺点是什么, 你就可以避免问题并简化开发. 在本教程中, 我们将查看 KinematicBody2D 节点, 并展示一些如何使用它的例子.

注解

本文假设您熟悉Godot中的各种物理体. 否则请先阅读 物理介绍 .

什么是运动体?

KinematicBody2D 用于实现通过代码控制的物体. 运动体在移动时可以检测到与其他物体的碰撞, 但不受引擎物理属性的影响, 比如重力或摩擦. 虽然这意味着你必须编写一些代码来创建它们的行为, 但这也意味着你可以更精确地控制它们如何移动和反应.

小技巧

KinematicBody2D 可以受到重力和其他力的影响, 但您必须在代码中计算它的运动. 物理引擎不会移动 KinematicBody2D.

运动与碰撞

当移动一个 KinematicBody2D 时, 你不应该直接设置它的 position 属性, 而是使用 move_and_collide()move_and_slide() 方法. 这些方法沿着给定的向量移动物体, 如果检测到与另一个体发生碰撞, 则立即停止. 在KinematicBody2D发生碰撞后, 任何 碰撞响应 必须手动编码.

警告

你应该只在 _physics_process() 回调中做Kinematic物体运动.

这两种运动方法有不同的作用, 在后面的教程中, 你会看到它们如何工作的例子.

move_and_collide

这个方法有一个 Vector2 参数以表示物体的相对运动. 通常, 这是您的速度向量乘以帧时间步长( delta ). 如果在沿着此向量方向的任何位置, 引擎检测到碰撞, 则物体将立即停止移动. 如果发生这种情况, 该方法将返回 KinematicCollision2D 对象.

KinematicCollision2D 是一个包含碰撞和碰撞对象数据的对象. 使用这些数据, 你可以计算出你的碰撞响应.

move_and_slide

move_and_slide() 方法旨在简化常见情况下的碰撞响应, 即你希望一个物体沿着另一个物体滑动. 例如, 在平台游戏或自上而下的游戏中, 它特别有用.

小技巧

move_and_slide() 使用 delta 自动计算基于帧的运动. 在将速度向量传递给 move_and_slide() 之前, 请 不要 将速度向量乘以 delta.

除了速度向量之外, move_and_slide() 还有许多其他参数, 允许您自定义滑动行为:

  • up_direction - 默认值: Vector2( 0, 0 )

    这个参数允许你定义哪些表面应该被引擎视为地板. 设置这个参数可以让你使用 is_on_floor() , is_on_wall()is_on_ceiling() 方法来检测物体接触的表面类型. 默认值意味着所有的表面都被认为是墙壁.

  • stop_on_slope - 默认值: false

    该参数可以防止物体站立不动时从斜坡上滑落.

  • max_slides - 默认值: 4

    这个参数是物体停止移动前的最大碰撞次数. 设置太低可能会完全阻止移动.

  • floor_max_angle - 默认值: 0.785398 (以弧度表示, 相当于 45 度)

    这是表面不再被视为 “地板” 之前的最大角度

  • infinite_inertia - 默认值: true

当这个参数为 true 时, 本体可以推动 RigidBody2D 节点, 忽略其质量, 不会检测到与它们的碰撞. 如果是 false, 本体会与刚体发生碰撞而停止.

move_and_slide_with_snap

这个方法通过添加 sap 参数, 给 move_and_slide() 增加了一些额外的功能. 只要这个向量与地面接触, 物体就会保持在表面上. 注意, 这意味着你必须在例如跳跃时禁用捕捉. 你可以将 sap 设置为 Vector2.ZERO 或者使用 move_and_slide() 代替.

检测碰撞

当使用 move_and_collide() 时, 函数直接返回一个 KinematicCollision2D , 你可以在代码中使用这个.

当使用 move_and_slide() 时, 有可能发生多次碰撞, 因为滑动响应是计算出来的. 要处理这些碰撞, 使用 get_slide_count()get_slide_collision():

GDScript

  1. # Using move_and_collide.
  2. var collision = move_and_collide(velocity * delta)
  3. if collision:
  4. print("I collided with ", collision.collider.name)
  5. # Using move_and_slide.
  6. velocity = move_and_slide(velocity)
  7. for i in get_slide_count():
  8. var collision = get_slide_collision(i)
  9. print("I collided with ", collision.collider.name)

注解

get_slide_count() 只计算物体碰撞和改变方向的次数.

关于返回哪些碰撞数据, 请参见 KinematicCollision2D .

使用哪种运动方式?

Godot新用户的一个常见问题是:”你如何决定使用哪个移动函数?” 通常, 回答是使用 move_and_slide() , 因为它 “更简单” , 但情况不一定如此. 有一种思路是, move_and_slide() 是一种特殊情况, 而 move_and_collide() 更通用. 例如, 下面两个代码片段的结果是相同的碰撞响应:

../../_images/k2d_compare.gif

GDScript

C#

  1. # using move_and_collide
  2. var collision = move_and_collide(velocity * delta)
  3. if collision:
  4. velocity = velocity.slide(collision.normal)
  5. # using move_and_slide
  6. velocity = move_and_slide(velocity)
  1. // using MoveAndCollide
  2. var collision = MoveAndCollide(velocity * delta);
  3. if (collision != null)
  4. {
  5. velocity = velocity.Slide(collision.Normal);
  6. }
  7. // using MoveAndSlide
  8. velocity = MoveAndSlide(velocity);

您用 move_and_slide() 做的任何事情都可以用 move_and_collide() 来完成, 但它可能需要更多的代码. 但是, 正如我们在下面的示例中将看到的, 有些情况下 move_and_slide() 不能提供您想要的响应.

在上面的例子中, 我们将 move_and_slide() 返回的速度赋值给 velocity 变量. 这是因为当角色与环境发生碰撞时, 函数会在内部重新计算速度, 以反映减速的情况.

例如, 如果角色倒在地上, 不希望它因为重力的影响而积累垂直速度, 而希望它的垂直速度重置为零.

move_and_slide() 还可以在循环中多次重新计算运动体的速度, 为了产生一个平滑的运动, 它默认会移动角色, 并碰撞5次, 在这个过程结束时, 函数返回角色的新速度, 可以将其存储在 velocity 变量中, 并在下一帧中使用.

示例

要查看这些示例, 请下载示例项目: using_kinematic2d.zip.

移动和墙壁

如果你已经下载了示例项目, 这个例子在 “BasicMovement.tscn” 中.

在这个例子中, 添加一个 KinematicBody2D , 有两个子级: SpriteCollisionShape2D . 使用Godot “icon.png” 作为Sprite的纹理, 将其从文件系统栏拖到 SpriteTexture 属性. 在 CollisionShape2DShape 属性中, 选择 “New RectangleShape2D” , 并将矩形的大小调整到适合sprite图像的大小.

注解

有关实现2D移动方案的示例, 请参阅 2D 运动概述 .

将脚本附加到KinematicBody2D并添加以下代码:

GDScript

C#

  1. extends KinematicBody2D
  2. var speed = 250
  3. var velocity = Vector2()
  4. func get_input():
  5. # Detect up/down/left/right keystate and only move when pressed.
  6. velocity = Vector2()
  7. if Input.is_action_pressed('ui_right'):
  8. velocity.x += 1
  9. if Input.is_action_pressed('ui_left'):
  10. velocity.x -= 1
  11. if Input.is_action_pressed('ui_down'):
  12. velocity.y += 1
  13. if Input.is_action_pressed('ui_up'):
  14. velocity.y -= 1
  15. velocity = velocity.normalized() * speed
  16. func _physics_process(delta):
  17. get_input()
  18. move_and_collide(velocity * delta)
  1. using Godot;
  2. using System;
  3. public class KBExample : KinematicBody2D
  4. {
  5. public int Speed = 250;
  6. private Vector2 _velocity = new Vector2();
  7. public void GetInput()
  8. {
  9. // Detect up/down/left/right keystate and only move when pressed
  10. _velocity = new Vector2();
  11. if (Input.IsActionPressed("ui_right"))
  12. _velocity.x += 1;
  13. if (Input.IsActionPressed("ui_left"))
  14. _velocity.x -= 1;
  15. if (Input.IsActionPressed("ui_down"))
  16. _velocity.y += 1;
  17. if (Input.IsActionPressed("ui_up"))
  18. _velocity.y -= 1;
  19. }
  20. public override void _PhysicsProcess(float delta)
  21. {
  22. GetInput();
  23. MoveAndCollide(_velocity * delta);
  24. }
  25. }

运行这个场景, 您会看到 move_and_collide() 按预期工作, 沿着速度向量方向移动物体. 现在让我们看看当您添加一些障碍时会发生什么. 添加一个具有矩形碰撞形状的 StaticBody2D . 为了可见性, 您可以使用精灵,Polygon2D, 或从 “调试” 菜单中打开 “可见碰撞形状”.

再次运行场景并尝试移动到障碍物中. 您会看到 KinematicBody2D 无法穿透障碍物. 但是, 尝试以某个角度进入障碍物, 您会发现障碍物就像胶水一样 - 感觉物体被卡住了.

发生这种情况是因为没有 碰撞响应 . move_and_collide() 在碰撞发生时停止物体的运动. 我们需要编写我们想要的碰撞响应.

尝试将函数更改为 move_and_slide(velocity) 并再次运行. 请注意, 我们从速度计算中删除了 “delta”.

move_and_slide() 提供了一个沿碰撞对象滑动物体的默认碰撞响应. 这对于许多游戏类型都很有用, 并且可能是获得所需行为所需的全部内容.

弹跳/反射

如果您不想要滑动碰撞响应怎么办? 对于这个示例(示例项目中的 “BounceandCollide.tscn”), 我们有一个角色射击子弹, 我们希望子弹从墙上反弹.

此示例使用三个场景. 主场景包含游戏角色和墙壁. 子弹和墙是单独的场景, 以便它们可以实例化.

游戏角色由 w 和 s 键控制前进和后退. 瞄准使用鼠标指针. 这是游戏角色的代码, 使用 move_and_slide() :

GDScript

C#

  1. extends KinematicBody2D
  2. var Bullet = preload("res://Bullet.tscn")
  3. var speed = 200
  4. var velocity = Vector2()
  5. func get_input():
  6. # Add these actions in Project Settings -> Input Map.
  7. velocity = Vector2()
  8. if Input.is_action_pressed('backward'):
  9. velocity = Vector2(-speed/3, 0).rotated(rotation)
  10. if Input.is_action_pressed('forward'):
  11. velocity = Vector2(speed, 0).rotated(rotation)
  12. if Input.is_action_just_pressed('mouse_click'):
  13. shoot()
  14. func shoot():
  15. # "Muzzle" is a Position2D placed at the barrel of the gun.
  16. var b = Bullet.instance()
  17. b.start($Muzzle.global_position, rotation)
  18. get_parent().add_child(b)
  19. func _physics_process(delta):
  20. get_input()
  21. var dir = get_global_mouse_position() - global_position
  22. # Don't move if too close to the mouse pointer.
  23. if dir.length() > 5:
  24. rotation = dir.angle()
  25. velocity = move_and_slide(velocity)
  1. using Godot;
  2. using System;
  3. public class KBExample : KinematicBody2D
  4. {
  5. private PackedScene _bullet = (PackedScene)GD.Load("res://Bullet.tscn");
  6. public int Speed = 200;
  7. private Vector2 _velocity = new Vector2();
  8. public void GetInput()
  9. {
  10. // add these actions in Project Settings -> Input Map
  11. _velocity = new Vector2();
  12. if (Input.IsActionPressed("backward"))
  13. {
  14. _velocity = new Vector2(-Speed/3, 0).Rotated(Rotation);
  15. }
  16. if (Input.IsActionPressed("forward"))
  17. {
  18. _velocity = new Vector2(Speed, 0).Rotated(Rotation);
  19. }
  20. if (Input.IsActionPressed("mouse_click"))
  21. {
  22. Shoot();
  23. }
  24. }
  25. public void Shoot()
  26. {
  27. // "Muzzle" is a Position2D placed at the barrel of the gun
  28. var b = (Bullet)_bullet.Instance();
  29. b.Start(GetNode<Node2D>("Muzzle").GlobalPosition, Rotation);
  30. GetParent().AddChild(b);
  31. }
  32. public override void _PhysicsProcess(float delta)
  33. {
  34. GetInput();
  35. var dir = GetGlobalMousePosition() - GlobalPosition;
  36. // Don't move if too close to the mouse pointer
  37. if (dir.Length() > 5)
  38. {
  39. Rotation = dir.Angle();
  40. _velocity = MoveAndSlide(_velocity);
  41. }
  42. }
  43. }

子弹的代码:

GDScript

C#

  1. extends KinematicBody2D
  2. var speed = 750
  3. var velocity = Vector2()
  4. func start(pos, dir):
  5. rotation = dir
  6. position = pos
  7. velocity = Vector2(speed, 0).rotated(rotation)
  8. func _physics_process(delta):
  9. var collision = move_and_collide(velocity * delta)
  10. if collision:
  11. velocity = velocity.bounce(collision.normal)
  12. if collision.collider.has_method("hit"):
  13. collision.collider.hit()
  14. func _on_VisibilityNotifier2D_screen_exited():
  15. queue_free()
  1. using Godot;
  2. using System;
  3. public class Bullet : KinematicBody2D
  4. {
  5. public int Speed = 750;
  6. private Vector2 _velocity = new Vector2();
  7. public void Start(Vector2 pos, float dir)
  8. {
  9. Rotation = dir;
  10. Position = pos;
  11. _velocity = new Vector2(speed, 0).Rotated(Rotation);
  12. }
  13. public override void _PhysicsProcess(float delta)
  14. {
  15. var collision = MoveAndCollide(_velocity * delta);
  16. if (collision != null)
  17. {
  18. _velocity = _velocity.Bounce(collision.Normal);
  19. if (collision.Collider.HasMethod("Hit"))
  20. {
  21. collision.Collider.Call("Hit");
  22. }
  23. }
  24. }
  25. public void OnVisibilityNotifier2DScreenExited()
  26. {
  27. QueueFree();
  28. }
  29. }

动作发生在 _physics_process() 中. 在使用 move_and_collide() 后, 如果发生碰撞, 将返回一个 KinematicCollision2D 对象, 否则, 返回 Nil .

如果有一个返回的碰撞, 我们使用碰撞的 normal 来反映子弹的 velocityVector2.bounce() 方法.

如果碰撞对象( collider )有一个 hit 方法, 我们也调用它. 在示例项目中, 我们为墙壁添加了一个颜色闪烁效果来演示这一点.

../../_images/k2d_bullet_bounce.gif

平台运动

让我们尝试一个更流行的示例:2D平台游戏. move_and_slide() 非常适合快速启动和运行功能字符控制器. 如果您已下载示例项目, 可以在 “Platformer.tscn” 中找到它.

对于这个示例, 我们假设您有一个由 StaticBody2D 对象构成的级别. 它们可以是任何形状和大小. 在示例项目中, 我们使用 Polygon2D 来创建平台形状.

这是游戏角色物体的代码:

GDScript

C#

  1. extends KinematicBody2D
  2. export (int) var run_speed = 100
  3. export (int) var jump_speed = -400
  4. export (int) var gravity = 1200
  5. var velocity = Vector2()
  6. var jumping = false
  7. func get_input():
  8. velocity.x = 0
  9. var right = Input.is_action_pressed('ui_right')
  10. var left = Input.is_action_pressed('ui_left')
  11. var jump = Input.is_action_just_pressed('ui_select')
  12. if jump and is_on_floor():
  13. jumping = true
  14. velocity.y = jump_speed
  15. if right:
  16. velocity.x += run_speed
  17. if left:
  18. velocity.x -= run_speed
  19. func _physics_process(delta):
  20. get_input()
  21. velocity.y += gravity * delta
  22. if jumping and is_on_floor():
  23. jumping = false
  24. velocity = move_and_slide(velocity, Vector2(0, -1))
  1. using Godot;
  2. using System;
  3. public class KBExample : KinematicBody2D
  4. {
  5. [Export] public int RunSpeed = 100;
  6. [Export] public int JumpSpeed = -400;
  7. [Export] public int Gravity = 1200;
  8. Vector2 velocity = new Vector2();
  9. bool jumping = false;
  10. public void GetInput()
  11. {
  12. velocity.x = 0;
  13. bool right = Input.IsActionPressed("ui_right");
  14. bool left = Input.IsActionPressed("ui_left");
  15. bool jump = Input.IsActionPressed("ui_select");
  16. if (jump && IsOnFloor())
  17. {
  18. jumping = true;
  19. velocity.y = JumpSpeed;
  20. }
  21. if (right)
  22. velocity.x += RunSpeed;
  23. if (left)
  24. velocity.x -= RunSpeed;
  25. }
  26. public override void _PhysicsProcess(float delta)
  27. {
  28. GetInput();
  29. velocity.y += Gravity * delta;
  30. if (jumping && IsOnFloor())
  31. jumping = false;
  32. velocity = MoveAndSlide(velocity, new Vector2(0, -1));
  33. }
  34. }

../../_images/k2d_platform.gif

当使用 move_and_slide() 时, 该函数返回一个向量, 代表滑动碰撞发生后剩余的运动. 将该值设置返回角色的 velocity , 我们就可以顺利地在斜坡上和斜坡下移动. 试着去掉 velocity = , 看看如果不这样做会发生什么.

同时注意, 我们已经添加了 Vector2(0, -1) 作为地板法线. 这是向上指向的向量. 因此, 如果角色与具有该法线的物体相撞, 它将被视为地板.

使用地板法线, 我们可以使用 is_on_floor() 来使跳跃工作. 这个函数只有在发生 move_and_slide() 碰撞后, 碰撞体的法线在给定的地板向量45度以内时才会返回 true . 你可以通过设置 floor_max_angle 来控制最大角度.

这个角度也允许你使用 is_on_wall() 实现其他功能, 比如墙面跳跃.