使用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 <class_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
# Using move_and_collide.
var collision = move_and_collide(velocity * delta)
if collision:
print("I collided with ", collision.collider.name)
# Using move_and_slide.
velocity = move_and_slide(velocity)
for i in get_slide_count():
var collision = get_slide_collision(i)
print("I collided with ", collision.collider.name)
注解
`get_slide_count()`只计算物体碰撞和改变方向的次数.
关于返回哪些碰撞数据,请参见 KinematicCollision2D .
使用哪种运动方式?
Godot新用户的一个常见问题是:”你如何决定使用哪个移动函数?”通常,回答是使用 move_and_slide()
,因为它 “更简单” ,但情况不一定如此.有一种思路是, move_and_slide()
是一种特殊情况,而 move_and_collide()
更通用.例如,下面两个代码片段的结果是相同的碰撞响应:
GDScript
C#
# using move_and_collide
var collision = move_and_collide(velocity * delta)
if collision:
velocity = velocity.slide(collision.normal)
# using move_and_slide
velocity = move_and_slide(velocity)
// using MoveAndCollide
var collision = MoveAndCollide(velocity * delta);
if (collision != null)
{
velocity = velocity.Slide(collision.Normal);
}
// using MoveAndSlide
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
,有两个子级: Sprite
和 CollisionShape2D
.使用Godot “icon.png” 作为Sprite的纹理,将其从文件系统栏拖到 Sprite
的 Texture 属性.在 CollisionShape2D
的 Shape 属性中,选择 “New RectangleShape2D” ,并将矩形的大小调整到适合sprite图像的大小.
注解
有关实现2D移动方案的示例,请参阅 2D运动概述 .
将脚本附加到KinematicBody2D并添加以下代码:
GDScript
C#
extends KinematicBody2D
var speed = 250
var velocity = Vector2()
func get_input():
# Detect up/down/left/right keystate and only move when pressed.
velocity = Vector2()
if Input.is_action_pressed('ui_right'):
velocity.x += 1
if Input.is_action_pressed('ui_left'):
velocity.x -= 1
if Input.is_action_pressed('ui_down'):
velocity.y += 1
if Input.is_action_pressed('ui_up'):
velocity.y -= 1
velocity = velocity.normalized() * speed
func _physics_process(delta):
get_input()
move_and_collide(velocity * delta)
using Godot;
using System;
public class KBExample : KinematicBody2D
{
public int Speed = 250;
private Vector2 _velocity = new Vector2();
public void GetInput()
{
// Detect up/down/left/right keystate and only move when pressed
_velocity = new Vector2();
if (Input.IsActionPressed("ui_right"))
_velocity.x += 1;
if (Input.IsActionPressed("ui_left"))
_velocity.x -= 1;
if (Input.IsActionPressed("ui_down"))
_velocity.y += 1;
if (Input.IsActionPressed("ui_up"))
_velocity.y -= 1;
}
public override void _PhysicsProcess(float delta)
{
GetInput();
MoveAndCollide(_velocity * delta);
}
}
运行这个场景,您会看到 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#
extends KinematicBody2D
var Bullet = preload("res://Bullet.tscn")
var speed = 200
var velocity = Vector2()
func get_input():
# Add these actions in Project Settings -> Input Map.
velocity = Vector2()
if Input.is_action_pressed('backward'):
velocity = Vector2(-speed/3, 0).rotated(rotation)
if Input.is_action_pressed('forward'):
velocity = Vector2(speed, 0).rotated(rotation)
if Input.is_action_just_pressed('mouse_click'):
shoot()
func shoot():
# "Muzzle" is a Position2D placed at the barrel of the gun.
var b = Bullet.instance()
b.start($Muzzle.global_position, rotation)
get_parent().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()
velocity = move_and_slide(velocity)
using Godot;
using System;
public class KBExample : KinematicBody2D
{
private PackedScene _bullet = (PackedScene)GD.Load("res://Bullet.tscn");
public int Speed = 200;
private Vector2 _velocity = new Vector2();
public void GetInput()
{
// add these actions in Project Settings -> Input Map
_velocity = new Vector2();
if (Input.IsActionPressed("backward"))
{
_velocity = new Vector2(-Speed/3, 0).Rotated(Rotation);
}
if (Input.IsActionPressed("forward"))
{
_velocity = new Vector2(Speed, 0).Rotated(Rotation);
}
if (Input.IsActionPressed("mouse_click"))
{
Shoot();
}
}
public void Shoot()
{
// "Muzzle" is a Position2D placed at the barrel of the gun
var b = (Bullet)_bullet.Instance();
b.Start(GetNode<Node2D>("Muzzle").GlobalPosition, Rotation);
GetParent().AddChild(b);
}
public override void _PhysicsProcess(float delta)
{
GetInput();
var dir = GetGlobalMousePosition() - GlobalPosition;
// Don't move if too close to the mouse pointer
if (dir.Length() > 5)
{
Rotation = dir.Angle();
_velocity = MoveAndSlide(_velocity);
}
}
}
子弹的代码:
GDScript
C#
extends KinematicBody2D
var speed = 750
var velocity = Vector2()
func start(pos, dir):
rotation = dir
position = pos
velocity = Vector2(speed, 0).rotated(rotation)
func _physics_process(delta):
var collision = move_and_collide(velocity * delta)
if collision:
velocity = velocity.bounce(collision.normal)
if collision.collider.has_method("hit"):
collision.collider.hit()
func _on_VisibilityNotifier2D_screen_exited():
queue_free()
using Godot;
using System;
public class Bullet : KinematicBody2D
{
public int Speed = 750;
private Vector2 _velocity = new Vector2();
public void Start(Vector2 pos, float dir)
{
Rotation = dir;
Position = pos;
_velocity = new Vector2(speed, 0).Rotated(Rotation);
}
public override void _PhysicsProcess(float delta)
{
var collision = MoveAndCollide(_velocity * delta);
if (collision != null)
{
_velocity = _velocity.Bounce(collision.Normal);
if (collision.Collider.HasMethod("Hit"))
{
collision.Collider.Call("Hit");
}
}
}
public void OnVisibilityNotifier2DScreenExited()
{
QueueFree();
}
}
动作发生在 _physics_process()
中.在使用 move_and_collide()
后,如果发生碰撞,将返回一个 KinematicCollision2D
对象,否则,返回 Nil
.
如果有一个返回的碰撞,我们使用碰撞的 normal
来反映子弹的 velocity
和 Vector2.bounce()
方法.
如果碰撞对象( collider
)有一个 hit
方法,我们也调用它. 在示例项目中,我们为墙壁添加了一个颜色闪烁效果来演示这一点.
平台运动
让我们尝试一个更流行的示例:2D平台游戏. ``move_and_slide()``非常适合快速启动和运行功能字符控制器. 如果您已下载示例项目,可以在”Platformer.tscn”中找到它.
对于这个示例,我们假设您有一个由 StaticBody2D
对象构成的级别. 它们可以是任何形状和大小. 在示例项目中,我们使用 Polygon2D 来创建平台形状.
这是游戏角色物体的代码:
GDScript
C#
extends KinematicBody2D
export (int) var run_speed = 100
export (int) var jump_speed = -400
export (int) var gravity = 1200
var velocity = Vector2()
var jumping = false
func get_input():
velocity.x = 0
var right = Input.is_action_pressed('ui_right')
var left = Input.is_action_pressed('ui_left')
var jump = Input.is_action_just_pressed('ui_select')
if jump and is_on_floor():
jumping = true
velocity.y = jump_speed
if right:
velocity.x += run_speed
if left:
velocity.x -= run_speed
func _physics_process(delta):
get_input()
velocity.y += gravity * delta
if jumping and is_on_floor():
jumping = false
velocity = move_and_slide(velocity, Vector2(0, -1))
using Godot;
using System;
public class KBExample : KinematicBody2D
{
[Export] public int RunSpeed = 100;
[Export] public int JumpSpeed = -400;
[Export] public int Gravity = 1200;
Vector2 velocity = new Vector2();
bool jumping = false;
public void GetInput()
{
velocity.x = 0;
bool right = Input.IsActionPressed("ui_right");
bool left = Input.IsActionPressed("ui_left");
bool jump = Input.IsActionPressed("ui_select");
if (jump && IsOnFloor())
{
jumping = true;
velocity.y = JumpSpeed;
}
if (right)
velocity.x += RunSpeed;
if (left)
velocity.x -= RunSpeed;
}
public override void _PhysicsProcess(float delta)
{
GetInput();
velocity.y += Gravity * delta;
if (jumping && IsOnFloor())
jumping = false;
velocity = MoveAndSlide(velocity, new Vector2(0, -1));
}
}
当使用 move_and_slide()
时,该函数返回一个向量,代表滑动碰撞发生后剩余的运动.将该值设置返回角色的 velocity
,我们就可以顺利地在斜坡上和斜坡下移动.试着去掉 velocity =
,看看如果不这样做会发生什么.
同时注意,我们已经添加了 Vector2(0, -1)
作为地板法线.这是向上指向的向量.因此,如果角色与具有该法线的物体相撞,它将被视为地板.
使用地板法线,我们可以使用 is_on_floor()
来使跳跃工作.这个函数只有在发生 move_and_slide()
碰撞后,碰撞体的法线在给定的地板矢量45度以内时才会返回 true
.你可以通过设置 floor_max_angle
来控制最大角度.
这个角度也允许你使用 is_on_wall()
实现其他功能,比如墙面跳跃.