Attention: Here be dragons

This is the latest (unstable) version of this documentation, which may document features not available in or compatible with released stable versions of Godot.

使用 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()

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

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

备注

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

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

使用哪种移动方式?

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

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

# using move_and_slide
move_and_slide()

你用 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并添加以下代码:

extends CharacterBody2D

var speed = 300

func get_input():
    var input_dir = Input.get_vector("ui_left", "ui_right", "ui_up", "ui_down")
    velocity = input_dir * speed

func _physics_process(delta):
    get_input()
    move_and_collide(velocity * delta)

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

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

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

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

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

弹跳/反射

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

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

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

extends CharacterBody2D

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

func get_input():
    # Add these actions in Project Settings -> Input Map.
    var input_dir = Input.get_axis("backward", "forward")
    velocity = transform.x * input_dir * speed
    if Input.is_action_just_pressed("shoot"):
        shoot()

func shoot():
    # "Muzzle" is a Marker2D placed at the barrel of the gun.
    var b = Bullet.instantiate()
    b.start($Muzzle.global_position, rotation)
    get_tree().root.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()
        move_and_slide()

子弹的代码:

extends CharacterBody2D

var speed = 750

func start(_position, _direction):
    rotation = _direction
    position = _position
    velocity = Vector2(speed, 0).rotated(rotation)

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

func _on_VisibilityNotifier2D_screen_exited():
    # Deletes the bullet when it exits the screen.
    queue_free()

动作发生在 _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 来创建平台的形状。

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

extends CharacterBody2D

var speed = 300.0
var jump_speed = -400.0

# Get the gravity from the project settings so you can sync with rigid body nodes.
var gravity = ProjectSettings.get_setting("physics/2d/default_gravity")


func _physics_process(delta):
    # Add the gravity.
    velocity.y += gravity * delta

    # Handle Jump.
    if Input.is_action_just_pressed("jump") and is_on_floor():
        velocity.y = jump_speed

    # Get the input direction.
    var direction = Input.get_axis("ui_left", "ui_right")
    velocity.x = direction * speed

    move_and_slide()
../../_images/k2d_platform.gif

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

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