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.
Checking the stable version of the documentation...
使用 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)
// Using MoveAndCollide.
var collision = MoveAndCollide(Velocity * (float)delta);
if (collision != null)
{
GD.Print("I collided with ", ((Node)collision.GetCollider()).Name);
}
// Using MoveAndSlide.
MoveAndSlide();
for (int i = 0; i < GetSlideCollisionCount(); i++)
{
var collision = GetSlideCollision(i);
GD.Print("I collided with ", ((Node)collision.GetCollider()).Name);
}
备注
get_slide_collision_count() 只计算物体碰撞和改变方向的次数。
关于返回哪些碰撞数据, 请参见 KinematicCollision2D .
使用哪种移动方式?
Godot 新手的一个常见问题是:“你如何决定使用哪个移动函数?”通常,回答是 move_and_slide()
,因为它“更简单”,但情况不一定如此。有一种思路是,move_and_slide()
是一种特殊情况,而 move_and_collide()
更通用。例如,下面两个代码片段的结果是相同的碰撞响应:

# 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()
// using MoveAndCollide
var collision = MoveAndCollide(Velocity * (float)delta);
if (collision != null)
{
Velocity = Velocity.Slide(collision.GetNormal());
}
// using MoveAndSlide
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
,并有两个子级: Sprite2D
和 CollisionShape2D
。使用 Godot 的 “icon.svg” 作为 Sprite2D 的纹理(将其从文件系统栏拖到 Sprite2D
的 Texture 属性)。在 CollisionShape2D
的 Shape 属性中,选择“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)
using Godot;
public partial class MyCharacterBody2D : CharacterBody2D
{
private int _speed = 300;
public void GetInput()
{
Vector2 inputDir = Input.GetVector("ui_left", "ui_right", "ui_up", "ui_down");
Velocity = inputDir * _speed;
}
public override void _PhysicsProcess(double delta)
{
GetInput();
MoveAndCollide(Velocity * (float)delta);
}
}
运行这个场景,你会看到 move_and_collide()
按预期工作,沿着速度向量方向移动物体。现在让我们看看当你添加一些障碍时会发生什么。添加一个具有矩形碰撞形状的 StaticBody2D 。为了可见性,你可以使用Sprite2D,Polygon2D,或从“调试”菜单中打开“可见碰撞形状”。
再次运行场景并尝试移动到障碍物上,你会看到 CharacterBody2D
无法穿过障碍物。 不过,当你以一个角度移动到障碍物上,你会发现障碍物就像胶水一样——感觉被卡住了。
发生这种情况是因为没有 碰撞响应 . move_and_collide()
在碰撞发生时停止物体的运动. 我们需要编写我们想要的碰撞响应.
尝试将函数更改为 move_and_slide()
并再次运行。
move_and_slide()
提供了一个沿碰撞对象滑动物体的默认碰撞响应. 这对于许多游戏类型都很有用, 并且可能是获得所需行为所需的全部内容.
弹跳/反射
如果你不想要滑动碰撞响应怎么办? 对于这个示例(示例项目中的 “bounce_and_collide.tscn”), 我们有一个角色射击子弹,我们希望子弹从墙上反弹。
此示例使用三个场景. 主场景包含游戏角色和墙壁. 子弹和墙是单独的场景, 以便它们可以实例化.
游戏角色由 w
和 s
键控制前进和后退。瞄准使用鼠标指针。这是游戏角色的代码,使用 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()
using Godot;
public partial class MyCharacterBody2D : CharacterBody2D
{
private PackedScene _bullet = GD.Load<PackedScene>("res://Bullet.tscn");
private int _speed = 200;
public void GetInput()
{
// Add these actions in Project Settings -> Input Map.
float inputDir = Input.GetAxis("backward", "forward");
Velocity = Transform.X * inputDir * _speed;
if (Input.IsActionPressed("shoot"))
{
Shoot();
}
}
public void Shoot()
{
// "Muzzle" is a Marker2D placed at the barrel of the gun.
var b = (Bullet)_bullet.Instantiate();
b.Start(GetNode<Node2D>("Muzzle").GlobalPosition, Rotation);
GetTree().Root.AddChild(b);
}
public override void _PhysicsProcess(double delta)
{
GetInput();
var dir = GetGlobalMousePosition() - GlobalPosition;
// Don't move if too close to the mouse pointer.
if (dir.Length() > 5)
{
Rotation = dir.Angle();
MoveAndSlide();
}
}
}
子弹的代码:
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()
using Godot;
public partial class Bullet : CharacterBody2D
{
public int _speed = 750;
public void Start(Vector2 position, float direction)
{
Rotation = direction;
Position = position;
Velocity = new Vector2(speed, 0).Rotated(Rotation);
}
public override void _PhysicsProcess(double delta)
{
var collision = MoveAndCollide(Velocity * (float)delta);
if (collision != null)
{
Velocity = Velocity.Bounce(collision.GetNormal());
if (collision.GetCollider().HasMethod("Hit"))
{
collision.GetCollider().Call("Hit");
}
}
}
private void OnVisibilityNotifier2DScreenExited()
{
// Deletes the bullet when it exits the screen.
QueueFree();
}
}
动作发生在 _physics_process()
中。在使用 move_and_collide()
后,如果发生碰撞,将返回一个 KinematicCollision2D
对象,否则,返回 null
。
如果有一个返回的碰撞, 我们使用碰撞的 normal
来反映子弹的 velocity
和 Vector2.bounce()
方法.
如果碰撞对象( collider
)有一个 hit
方法, 我们也调用它. 在示例项目中, 我们为墙壁添加了一个颜色闪烁效果来演示这一点.

平台移动
让我们尝试一个更流行的示例: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()
using Godot;
public partial class MyCharacterBody2D : CharacterBody2D
{
private float _speed = 100.0f;
private float _jumpSpeed = -400.0f;
// Get the gravity from the project settings so you can sync with rigid body nodes.
public float Gravity = ProjectSettings.GetSetting("physics/2d/default_gravity").AsSingle();
public override void _PhysicsProcess(double delta)
{
Vector2 velocity = Velocity;
// Add the gravity.
velocity.Y += Gravity * (float)delta;
// Handle jump.
if (Input.IsActionJustPressed("jump") && IsOnFloor())
velocity.Y = _jumpSpeed;
// Get the input direction.
float direction = Input.GetAxis("ui_left", "ui_right");
velocity.X = direction * _speed;
Velocity = velocity;
MoveAndSlide();
}
}

在本段代码实现中,我们调用了 move_and_slide()
方法,该方法根据物体的速度向量对物体进行平移,并在碰撞检测到地面或平台等碰撞体时,使物体沿碰撞表面滑动。此外,我们还利用了 is_on_floor()
方法来判断角色是否处于可跳跃状态。若缺少这一逻辑判断,角色将能够在非地面状态下执行跳跃动作;这种情况在开发如 “Flappy Bird” 这类的飞行躲避游戏中可能是可取的,但在开发平台跳跃类型的游戏中则不适宜。
一个完整的平台游戏角色还有很多内容:加速度、二段跳、土狼时间,等等。上面的代码只是一个起点。你可以在此基础上扩展,以得到你的项目所需的任何运动行为。