游戏开发中的物理之使用KinematicBody2D
介绍
Godot提供了多个碰撞对象以提供碰撞检测和响应。试图确定要为您的项目使用哪个选项可能会造成混淆。如果您了解每个问题的工作原理和优点和缺点,则可以避免这些问题并简化开发。在本教程中,我们将研究 KinematicBody2D节点,并显示一些使用它的示例。
注意
本文档假定您熟悉Godot的各种物理机构。请先阅读物理简介。
什么是运动机构?
KinematicBody2D用于实现通过代码控制的主体。运动物体在移动时会检测到与其他物体的碰撞,但不受重力或摩擦等发动机物理特性的影响。虽然这意味着您必须编写一些代码来创建其行为,但也意味着您可以更精确地控制它们的移动和反应方式。
提示
一个KinematicBody2D可以通过重力和其他力量的影响,但必须计算在代码运动。物理引擎不会移动KinematicBody2D。
运动与碰撞
移动时KinematicBody2D,您不应position直接设置其属性。而是使用move_and_collide()ormove_and_slide()方法。这些方法沿给定矢量移动物体,如果检测到与另一个物体的碰撞,则立即停止。KinematicBody2D发生碰撞后,必须手动编码任何碰撞响应。
警告
您只应在_physics_process()回调中进行运动身体运动。
两种移动方法具有不同的用途,在本教程的后面,您将看到有关它们如何工作的示例。
move_and_collide
此方法采用一个参数:Vector2,指示人体的相对运动。通常,这是您的速度矢量乘以帧时间步(delta)。如果引擎在沿该矢量的任何位置检测到碰撞,车身将立即停止移动。如果发生这种情况,该方法将返回KinematicCollision2D对象。
KinematicCollision2D是一个包含有关碰撞和碰撞对象的数据的对象。使用此数据,您可以计算碰撞响应。
move_and_slide
该move_and_slide()方法旨在简化在您希望一个物体沿另一个物体滑动的常见情况下的碰撞响应。例如,它在平台游戏或自上而下的游戏中特别有用。
提示
move_and_slide()使用会自动计算基于帧的移动delta。难道不是由乘你的速度矢量delta
它传递给前move_and_slide()。
除了速度矢量之外,还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 节点,而忽略其质量,但不会检测到与它们的碰撞。如果是这样,false 则身体将与刚体碰撞并停止。
move_and_slide_with_snap
此方法move_and_slide()通过添加snap参数来添加一些其他功能。只要此矢量与地面接触,物体就会保持附着在地面上。请注意,例如,这意味着您必须在跳跃时禁用捕捉。您可以通过设置snap 为Vector2.ZERO或使用move_and_slide()来实现。
检测碰撞
使用move_and_collide()该函数时KinematicCollision2D 直接返回一个,您可以在代码中使用它。
在move_and_slide()计算滑动响应时,使用时可能会发生多次碰撞。要处理这些冲突,请使用get_slide_count() 和get_slide_collision():
# 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() 更为笼统。例如,以下两个代码段导致相同的碰撞响应:
// 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具有两个子元素的aSprite和a CollisionShape2D。使用Godot“ icon.png”作为Sprite的纹理(将其从Filesystem停靠拖到的Texture属性Sprite)。在 CollisionShape2D“形状”属性中,选择“新建RectangleShape2D”并调整矩形大小以适合精灵图像。
注意
有关实施2D移动方案的示例,请参见2D移动概述。
将脚本附加到KinematicBody2D并添加以下代码:
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。为了获得可见性,可以使用sprite,Polygon2D或从“调试”菜单中打开“可见碰撞形状”。
再次运行场景,然后尝试移入障碍物。您会看到KinematicBody2D 无法穿透障碍物。但是,尝试以一定角度移入障碍物,您会发现障碍物就像胶水一样-感觉身体被卡住了。
发生这种情况是因为没有碰撞响应。move_and_collide()发生碰撞时停止身体的运动。我们需要对碰撞产生的任何响应进行编码。
尝试将功能更改为move_and_slide(velocity)并再次运行。注意我们delta从速度计算中删除了。
move_and_slide()提供沿碰撞对象滑动主体的默认碰撞响应。这对于许多游戏类型都非常有用,并且可能只是获得所需行为的全部。
弹跳/反射
如果不想滑动碰撞响应怎么办?对于此示例(示例项目中的“ BounceandCollide.tscn”),我们有一个射击子弹的角色,我们希望这些子弹从墙上弹起。
本示例使用三个场景。主要场景包含播放器和墙壁。子弹头和墙是分开的场景,因此可以被实例化。
播放器由w和s键控制前进和后退。瞄准使用鼠标指针。这是Player的代码,使用move_and_slide():
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);
}
}
}
和子弹的代码:
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创建平台形状。
这是播放器主体的代码:
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()。此功能仅会返回true一个后move_and_slide()碰撞,其中碰撞体的法线是在45度定地板载体。您可以通过设置来控制最大角度floor_max_angle。
例如,该角度还允许您使用来实现其他功能,例如墙跳 is_on_wall()。