使用 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#
# 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()
更通用。例如,下面两个代码片段的结果是相同的碰撞响应:
GDScriptC#
# 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并添加以下代码:
GDScriptC#
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()
:
GDScriptC#
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();
}
}
}
子弹的代码:
GDScriptC#
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 来创建平台的形状。
这是游戏角色物体的代码:
GDScriptC#
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” 这类的飞行躲避游戏中可能是可取的,但在开发平台跳跃类型的游戏中则不适宜。
一个完整的平台游戏角色还有很多内容:加速度、二段跳、土狼时间,等等。上面的代码只是一个起点。你可以在此基础上扩展,以得到你的项目所需的任何运动行为。