使用 CharacterBody2D/3D

前言

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

备注

虽然本文档在其示例中使用 CharacterBody2D ,但相同的概念也适用于 3D。

什么是角色体?

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

备注

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

小技巧

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

运动与碰撞

当移动一个 CharacterBody2D 时,你不应该直接设置它的 position 属性,而应该使用 move_and_collide()move_and_slide() 方法。这些方法沿着给定的向量移动物体,并且检测碰撞。

警告

你应该在 _physics_process() 回调中处理物理体的运动。

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

move_and_collide

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

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

当你只想移动物体并检测碰撞,并且不需要任何自动碰撞响应时, move_and_collide 最有用。例如,如果你需要一颗从墙上弹开的子弹,你可以在检测到碰撞时直接更改速度角度。请参阅下面的示例。

move_and_slide

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

当调用 move_and_slide() 时,该函数使用许多节点属性来计算其滑动行为。这些属性可以在检查器中找到,或在代码中设置。

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

    此属性表示身体的速度向量(以每秒像素为单位)。 move_and_slide() 会在碰撞时自动修改此值。

  • motion_mode - 默认值: MOTION_MODE_GROUNDED

    这个属性通常用于区分 横向滚动视角俯视角 。默认情况下,你可以使用 is_on_floor()is_on_wall()is_on_ceiling() 方法来检测物体与哪种表面接触,以及物体会与这些斜坡互动。当使用 MOTION_MODE_FLOATING 时,所有碰撞都会被认为是“墙”。

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

    这个参数允许你定义哪些表面应该被引擎视为地板。设置这个参数然后使用 is_on_floor()is_on_wall()is_on_ceiling() 方法来检测物体接触的表面类型。默认值意味着所有水平表面的顶部都被认为是“地面”。

  • floor_stop_on_slope - 默认值: true

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

  • wall_min_slide_angle - 默认值: 0.261799 (以弧度表示,相当于 15 度)

    这是当身体在遇到斜坡时允许滑动的最小角度。

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

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

在特定情况下,还有许多其他属性可用于修改身体的行为。详情请参见 CharacterBody2D 文档。

检测碰撞

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

当使用 move_and_slide() 时,有可能发生多次碰撞,因为滑动响应也被计算在内。要处理这些碰撞,使用 get_slide_collision_count()get_slide_collision()

GDScriptC#

  1. # Using move_and_collide.
  2. var collision = move_and_collide(velocity * delta)
  3. if collision:
  4. print("I collided with ", collision.get_collider().name)
  5. # Using move_and_slide.
  6. move_and_slide()
  7. for i in get_slide_collision_count():
  8. var collision = get_slide_collision(i)
  9. print("I collided with ", collision.get_collider().name)
  1. // Using MoveAndCollide.
  2. var collision = MoveAndCollide(Velocity * (float)delta);
  3. if (collision != null)
  4. {
  5. GD.Print("I collided with ", ((Node)collision.GetCollider()).Name);
  6. }
  7. // Using MoveAndSlide.
  8. MoveAndSlide();
  9. for (int i = 0; i < GetSlideCollisionCount(); i++)
  10. {
  11. var collision = GetSlideCollision(i);
  12. GD.Print("I collided with ", ((Node)collision.GetCollider()).Name);
  13. }

备注

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

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

使用哪种移动方式?

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

../../_images/k2d_compare.gif

GDScriptC#

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

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

在上面的例子中, move_and_slide() 自动更改了 velocity 变量。这是因为当角色与环境发生碰撞时,函数会在内部重新计算速度,以反映减速的情况。

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

move_and_slide() 将会在循环中多次重新计算运动物体的速度,以产生平滑的运动。默认情况下,他会移动角色并最多与环境碰撞5次。在这个过程结束时,角色的的新速度将会用于下一帧。

示例

若要查看这些案例的实际效果,请下载示例项目:character_body_2d_starter.zip

移动和墙壁

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

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

备注

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

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

GDScriptC#

  1. extends CharacterBody2D
  2. var speed = 300
  3. func get_input():
  4. var input_dir = Input.get_vector("ui_left", "ui_right", "ui_up", "ui_down")
  5. velocity = input_dir * speed
  6. func _physics_process(delta):
  7. get_input()
  8. move_and_collide(velocity * delta)
  1. using Godot;
  2. public partial class MyCharacterBody2D : CharacterBody2D
  3. {
  4. private int _speed = 300;
  5. public void GetInput()
  6. {
  7. Vector2 inputDir = Input.GetVector("ui_left", "ui_right", "ui_up", "ui_down");
  8. Velocity = inputDir * _speed;
  9. }
  10. public override void _PhysicsProcess(double delta)
  11. {
  12. GetInput();
  13. MoveAndCollide(Velocity * (float)delta);
  14. }
  15. }

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

再次运行场景并尝试移动到障碍物上,你会看到 CharacterBody2D 无法穿过障碍物。 不过,当你以一个角度移动到障碍物上,你会发现障碍物就像胶水一样——感觉被卡住了。

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

尝试将函数更改为 move_and_slide() 并再次运行。

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

弹跳/反射

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

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

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

GDScriptC#

  1. extends CharacterBody2D
  2. var Bullet = preload("res://bullet.tscn")
  3. var speed = 200
  4. func get_input():
  5. # Add these actions in Project Settings -> Input Map.
  6. var input_dir = Input.get_axis("backward", "forward")
  7. velocity = transform.x * input_dir * speed
  8. if Input.is_action_just_pressed("shoot"):
  9. shoot()
  10. func shoot():
  11. # "Muzzle" is a Marker2D placed at the barrel of the gun.
  12. var b = Bullet.instantiate()
  13. b.start($Muzzle.global_position, rotation)
  14. get_tree().root.add_child(b)
  15. func _physics_process(delta):
  16. get_input()
  17. var dir = get_global_mouse_position() - global_position
  18. # Don't move if too close to the mouse pointer.
  19. if dir.length() > 5:
  20. rotation = dir.angle()
  21. move_and_slide()
  1. using Godot;
  2. public partial class MyCharacterBody2D : CharacterBody2D
  3. {
  4. private PackedScene _bullet = GD.Load<PackedScene>("res://Bullet.tscn");
  5. private int _speed = 200;
  6. public void GetInput()
  7. {
  8. // Add these actions in Project Settings -> Input Map.
  9. float inputDir = Input.GetAxis("backward", "forward");
  10. Velocity = Transform.X * inputDir * _speed;
  11. if (Input.IsActionPressed("shoot"))
  12. {
  13. Shoot();
  14. }
  15. }
  16. public void Shoot()
  17. {
  18. // "Muzzle" is a Marker2D placed at the barrel of the gun.
  19. var b = (Bullet)_bullet.Instantiate();
  20. b.Start(GetNode<Node2D>("Muzzle").GlobalPosition, Rotation);
  21. GetTree().Root.AddChild(b);
  22. }
  23. public override void _PhysicsProcess(double delta)
  24. {
  25. GetInput();
  26. var dir = GetGlobalMousePosition() - GlobalPosition;
  27. // Don't move if too close to the mouse pointer.
  28. if (dir.Length() > 5)
  29. {
  30. Rotation = dir.Angle();
  31. MoveAndSlide();
  32. }
  33. }
  34. }

子弹的代码:

GDScriptC#

  1. extends CharacterBody2D
  2. var speed = 750
  3. func start(_position, _direction):
  4. rotation = _direction
  5. position = _position
  6. velocity = Vector2(speed, 0).rotated(rotation)
  7. func _physics_process(delta):
  8. var collision = move_and_collide(velocity * delta)
  9. if collision:
  10. velocity = velocity.bounce(collision.get_normal())
  11. if collision.get_collider().has_method("hit"):
  12. collision.get_collider().hit()
  13. func _on_VisibilityNotifier2D_screen_exited():
  14. # Deletes the bullet when it exits the screen.
  15. queue_free()
  1. using Godot;
  2. public partial class Bullet : CharacterBody2D
  3. {
  4. public int _speed = 750;
  5. public void Start(Vector2 position, float direction)
  6. {
  7. Rotation = direction;
  8. Position = position;
  9. Velocity = new Vector2(speed, 0).Rotated(Rotation);
  10. }
  11. public override void _PhysicsProcess(double delta)
  12. {
  13. var collision = MoveAndCollide(Velocity * (float)delta);
  14. if (collision != null)
  15. {
  16. Velocity = Velocity.Bounce(collision.GetNormal());
  17. if (collision.GetCollider().HasMethod("Hit"))
  18. {
  19. collision.GetCollider().Call("Hit");
  20. }
  21. }
  22. }
  23. private void OnVisibilityNotifier2DScreenExited()
  24. {
  25. // Deletes the bullet when it exits the screen.
  26. QueueFree();
  27. }
  28. }

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

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

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

../../_images/k2d_bullet_bounce.gif

平台移动

让我们尝试一个更流行的示例:2D平台游戏。 move_and_slide() 非常适合快速创建一个功能性的角色控制器。如果你已下载示例项目,可以在“platformer.tscn”中找到它。

在这个示例中,我们假设你的关卡由一个或多个 StaticBody2D 组成。它们可以是任何形状和大小。在示例项目中,我们使用 Polygon2D 来创建平台的形状。

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

GDScriptC#

  1. extends CharacterBody2D
  2. var speed = 300.0
  3. var jump_speed = -400.0
  4. # Get the gravity from the project settings so you can sync with rigid body nodes.
  5. var gravity = ProjectSettings.get_setting("physics/2d/default_gravity")
  6. func _physics_process(delta):
  7. # Add the gravity.
  8. velocity.y += gravity * delta
  9. # Handle Jump.
  10. if Input.is_action_just_pressed("jump") and is_on_floor():
  11. velocity.y = jump_speed
  12. # Get the input direction.
  13. var direction = Input.get_axis("ui_left", "ui_right")
  14. velocity.x = direction * speed
  15. move_and_slide()
  1. using Godot;
  2. public partial class MyCharacterBody2D : CharacterBody2D
  3. {
  4. private float _speed = 100.0f;
  5. private float _jumpSpeed = -400.0f;
  6. // Get the gravity from the project settings so you can sync with rigid body nodes.
  7. public float Gravity = ProjectSettings.GetSetting("physics/2d/default_gravity").AsSingle();
  8. public override void _PhysicsProcess(double delta)
  9. {
  10. Vector2 velocity = Velocity;
  11. // Add the gravity.
  12. velocity.Y += Gravity * (float)delta;
  13. // Handle jump.
  14. if (Input.IsActionJustPressed("jump") && IsOnFloor())
  15. velocity.Y = _jumpSpeed;
  16. // Get the input direction.
  17. float direction = Input.GetAxis("ui_left", "ui_right");
  18. velocity.X = direction * _speed;
  19. Velocity = velocity;
  20. MoveAndSlide();
  21. }
  22. }

../../_images/k2d_platform.gif

在本段代码实现中,我们调用了 move_and_slide() 方法,该方法根据物体的速度向量对物体进行平移,并在碰撞检测到地面或平台等碰撞体时,使物体沿碰撞表面滑动。此外,我们还利用了 is_on_floor() 方法来判断角色是否处于可跳跃状态。若缺少这一逻辑判断,角色将能够在非地面状态下执行跳跃动作;这种情况在开发如 “Flappy Bird” 这类的飞行躲避游戏中可能是可取的,但在开发平台跳跃类型的游戏中则不适宜。

一个完整的平台游戏角色还有很多内容:加速度、二段跳、土狼时间,等等。上面的代码只是一个起点。你可以在此基础上扩展,以得到你的项目所需的任何运动行为。