物理介绍

在游戏开发中, 你经常需要知道游戏中的两个对象在何时相交或接触. 这被称为 碰撞检测 . 检测到碰撞时, 你通常希望某些事情发生. 这被称为 碰撞响应 .

Godot在2D和3D中提供了许多碰撞对象, 以提供碰撞检测和响应. 你可能很难决定哪个适合你的项目. 一旦了解了每种方法的工作原理以及它们的优缺点, 你就可以避免出现问题并简化开发过程.

在本指南中, 您将学会:

  • Godot的四种碰撞对象类型

  • 每个碰撞对象的工作原理

  • 何时以及为何选择这种类型而不是另一种类型

注解

本文档的示例将使用2D对象. 每个2D物理对象和碰撞形状在3D中具有直接等价物, 并且在大多数情况下它们以相同的方式工作.

碰撞物体

Godot提供了四种扩展自 CollisionObject2D 的物体:

  • Area2D

    Area2D 节点提供 检测影响 . 它们可以检测物体何时重叠, 并在物体进入或离开时发出信号. Area2D 也可用于覆盖物理属性, 例如一定区域内的重力或阻尼.

其他三个物体扩展自 PhysicsBody2D :

  • StaticBody2D

    静态主体是物理引擎不移动的主体. 它参与碰撞检测, 但不会响应碰撞而移动. 它们通常用于属于环境的对象或不需要任何动态行为的对象.

  • RigidBody2D

    这是实现模拟2D物理的节点. 您不直接控制 RigidBody2D , 而是您对它施加力(重力, 冲动等), 物理引擎计算得到的运动. 阅读更多关于使用刚体的信息.

  • KinematicBody2D

    提供碰撞检测的物体, 但没有物理特性. 所有移动和碰撞响应必须在代码中实现.

物理材质

静态体和刚性体可以被配置为使用 物理材质physics material 。这允许调整一个物体的摩擦力和反弹力,并设置它是否具有吸收性、粗糙性。

碰撞形状

物理体可以包含任意数量的 Shape2D 对象作为子对象. 这些形状用于定义对象的碰撞边界并检测与其他对象的接触.

注解

为了检测碰撞, 必须至少为对象分配一个 Shape2D .

分配形状的最常用方法是添加 CollisionShape2DCollisionPolygon2D 作为对象的子项. 这些节点允许您直接在编辑器工作区中绘制形状.

重要

注意, 不要在编辑器中缩放碰撞形状. 属性面板中的 "Scale" 属性应保持为 (1, 1) . 当改变碰撞形状的大小时, 应该使用尺寸控制柄, 而 不是 Node2D 缩放控制柄. 缩放形状可能会导致意外的碰撞行为.

../../_images/player_coll_shape1.png

物理过程回调

物理引擎可能会产生多个线程以提高性能, 所以它能使用最多一个帧来处理物理. 因此, 物体状态的变量的值, 如 positionlinear velocity , 可能在当前帧不完全准确.

为了避免这种不准确性, 任何需要访问物体属性的代码都应该在 Node._physics_process() 回调中运行, 它在每个物理步骤之前以恒定的帧速率(默认为每秒60次)被调用. 一个 delta 参数会传递给这个方法, 这个参数是一个浮点数, 等于自上一步以来经过的时间( ). 当使用默认的60Hz物理更新率时, 它通常等于 0.01666... (但并非总是如此, 见下文).

注解

建议在物理计算中使用 delta 参数, 以便当您更改物理更新速率或玩家设备跟不上时, 游戏能够正确运行.

碰撞层与遮罩

碰撞层系统是最强大但经常被误解的碰撞特征之一. 该系统允许您在各种对象之间建立复杂的交互. 关键概念是 层(layers)遮罩(masks) . 每个 CollisionObject2D 都有20个不同的物理层可以相互作用.

让我们依次看看每个属性:

  • collision_layer

    这描述了对象在 中出现的图层 . 默认情况下, 所有实体都在图层 1 上.

  • collision_mask

    这描述了物体将 扫描 以进行碰撞的层. 如果对象不在其中一个遮罩层中, 则物体将忽略它. 默认情况下, 所有实体都扫描图层是 1.

可以通过代码配置这些属性, 也可以在Inspector中对其进行编辑.

跟踪您正在使用每个图层的内容可能很困难, 因此您可能会发现为您正在使用的图层指定名称很有用. 可以在 项目设置 -> 图层名称 中指定名称.

../../_images/physics_layer_names.png

图形用户界面示例

游戏中有四种节点类型:Walls,Player,Enemy和Coin. 游戏角色和敌人都应该与沃尔斯碰撞. 游戏角色节点应该检测与敌人和硬币的碰撞, 但敌人和硬币应该互相忽略.

首先命名1-4层为 "墙" , "玩家" , "敌人" 和 "硬币 ", 并使用" 层 "属性将每个节点类型放在其各自的层中. 然后通过选择它应该与之互动的层来设置每个节点的 "Mask掩码" 属性. 例如, 玩家的设置将看起来像这样:

../../_images/player_collision_layers.png ../../_images/player_collision_mask.png

代码示例

在函数调用中, 为层指定位掩码. 当一个函数默认启用所有图层时, 图层掩码将被指定为 0x7fffffff. 根据你的喜好, 你的代码可以使用二进制, 十六进制或十进制来表示层掩码.

启用第1, 3, 4层, 并添加一下代码:

# Example: Setting mask value for enabling layers 1, 3 and 4

# Binary - set the bit corresponding to the layers you want to enable (1, 3, and 4) to 1, set all other bits to 0.
# Note: Layer 20 is the first bit, layer 1 is the last. The mask for layers 4,3 and 1 is therefore
0b00000000000000001101
# (This can be shortened to 0b1101)

# Hexadecimal equivalent (1101 binary converted to hexadecimal)
0x000d
# (This value can be shortened to 0xd)

# Decimal - Add the results of 2 to the power of (layer be enabled-1).
# (2^(1-1)) + (2^(3-1)) + (2^(4-1)) = 1 + 4 + 8 = 13
pow(2, 1) + pow(2, 3) + pow(2, 4)

Area2D

Area节点提供 检测影响 . 它们可以检测物体何时重叠, 并在物体进入或离开时发出信号. Area也可用于覆盖物理属性, 例如一定区域内的重力或阻尼.

有三个主要用途 Area2D:

  • 覆盖给定区域中的物理参数(例如重力).

  • 检测其他实体何时进入或退出某个区域或当前哪个实体位于某个区域.

  • 检查是否与其他区域重叠.

默认情况下,area还会接收鼠标和触摸屏输入.

StaticBody2D

静态主体是物理引擎不移动的主体. 它参与碰撞检测, 但不会响应碰撞而移动. 然而, 它可以使用它的 constant_linear_velocityconstant_angular_velocity 属性将运动或旋转传递给碰撞体, 好像 它正在移动一样.

StaticBody2D 节点最常用于属于环境的对象或不需要任何动态行为的对象.

StaticBody2D 的示例用法:

  • 平台(包括移动平台)

  • 输送带

  • 墙壁和其他障碍

RigidBody2D

这是实现模拟2D物理的节点. 你不能直接控制一个 RigidBody2D. 取而代之的是, 对它施加力, 物理引擎会计算由此产生的运动, 包括与其他物体的碰撞, 以及碰撞响应, 如弹跳, 旋转等.

You can modify a rigid body's behavior via properties such as "Mass", "Friction", or "Bounce", which can be set in the Inspector.

物体的行为也受到 项目设置 ->物理学 中设置的世界属性的影响, 或者通过输入覆盖全局物理属性的 Area2D .

当一个刚体处于静止状态, 有一段时间没有移动, 它就会进入睡眠状态. 睡眠的物体就像一个静态的物体, 它的力不会被物理引擎计算. 当力被施加时, 无论是通过碰撞还是通过代码, 该物体都会被唤醒.

刚体模式

刚体可以设置为以下四种模式之一:

  • Rigid - 物体表现为具有物理属性的对象. 它与其他物体发生碰撞, 并对施加于其上的力作出反应. 这是默认模式.

  • Static - 物体表现得像 StaticBody2D 并且不会移动.

  • Character - 类似于 "刚性" 模式, 但身体不能旋转.

  • Kinematic - 物体的行为类似于 KinematicBody2D 并且必须通过代码移动.

使用RigidBody2D

使用刚体的一个好处是, 可以 免费 获得许多行为而无需编写任何代码. 例如, 如果您正在制作一个带有下降块的 愤怒的小鸟 式游戏, 您只需要创建RigidBody2D并调整它们的属性. 堆叠, 下降和弹跳将由物理引擎自动计算.

然而, 如果你确实希望对物体有一些控制, 应该注意改变刚体的 position, linear_velocity 或其他物理属性可能会导致意外的行为. 如果你需要改变任何与物理相关的属性, 应该使用 _integrate_forces() 回调来代替 _physics_process() . 在这个回调中, 你可以访问body的 Physics2DDirectBodyState, 它允许安全地改变属性并与物理引擎同步.

例如, 以下是 小行星 式宇宙飞船的代码:

extends RigidBody2D

var thrust = Vector2(0, 250)
var torque = 20000

func _integrate_forces(state):
    if Input.is_action_pressed("ui_up"):
        applied_force = thrust.rotated(rotation)
    else:
        applied_force = Vector2()
    var rotation_dir = 0
    if Input.is_action_pressed("ui_right"):
        rotation_dir += 1
    if Input.is_action_pressed("ui_left"):
        rotation_dir -= 1
    applied_torque = rotation_dir * torque
class Spaceship : RigidBody2D
{
    private Vector2 _thrust = new Vector2(0, 250);
    private float _torque = 20000;

    public override void _IntegrateForces(Physics2DDirectBodyState state)
    {
        if (Input.IsActionPressed("ui_up"))
            SetAppliedForce(_thrust.Rotated(Rotation));
        else
            SetAppliedForce(new Vector2());

        var rotationDir = 0;
        if (Input.IsActionPressed("ui_right"))
            rotationDir += 1;
        if (Input.IsActionPressed("ui_left"))
            rotationDir -= 1;
        SetAppliedTorque(rotationDir * _torque);
    }
}

请注意, 我们不是直接设置 linear_velocityangular_velocity 属性, 而是将力( thrusttorque )施加到物体上并让物理引擎计算出最终的运动.

注解

当一个刚体进入睡眠状态时, _integrate_forces() 函数将不会被调用. 要重写这一行为, 您需要通过创建碰撞, 对其施加力或禁用 can_sleep 属性来保持物体的激活. 请注意, 这可能会对性能产生负面影响.

接触报告

默认情况下, 刚体不会跟踪接触点, 因为如果场景中存在许多体, 这可能需要大量的内存. 要启用接触报告, 请将 contacts_reported 属性设置为非零值. 然后可以通过 Physics2DDirectBodyState.get_contact_count() 和相关函数获得联系.

通过信号的接触监控, 启用 contact_monitor 属性. 请参阅 RigidBody2D 的可用信号列表.

KinematicBody2D

KinematicBody2D 物体检测与其他物体的碰撞, 但不受重力或摩擦等物理属性的影响. 相反, 它们必须由用户通过代码控制. 物理引擎不会移动运动体.

移动运动体时, 不应直接设置其 position . 相反, 您使用 move_and_collide()move_and_slide() 方法. 这些方法沿着给定的向量移动物体, 如果与另一个物体检测到碰撞, 它将立即停止. 在物体发生碰撞后, 必须手动编码任何碰撞响应.

运动碰撞响应

碰撞后, 您可能希望物体反弹, 沿着墙壁滑动, 或者改变它所击中的物体的属性. 处理碰撞响应的方式取决于您用于移动KinematicBody2D的方法.

move_and_collide

当使用 move_and_collide() 时, 该函数返回一个 KinematicCollision2D 对象, 其中包含有关碰撞和碰撞体的信息. 您可以使用此信息来确定响应.

例如, 如果要查找发生碰撞的空间点:

extends KinematicBody2D

var velocity = Vector2(250, 250)

func _physics_process(delta):
    var collision_info = move_and_collide(velocity * delta)
    if collision_info:
        var collision_point = collision_info.position
class Body : KinematicBody2D
{
    private Vector2 _velocity = new Vector2(250, 250);

    public override void _PhysicsProcess(float delta)
    {
        var collisionInfo = MoveAndCollide(_velocity * delta);
        if (collisionInfo != null)
        {
            var collisionPoint = collisionInfo.GetPosition();
        }
    }
}

或者从碰撞物体反弹:

extends KinematicBody2D

var velocity = Vector2(250, 250)

func _physics_process(delta):
    var collision_info = move_and_collide(velocity * delta)
    if collision_info:
        velocity = velocity.bounce(collision_info.normal)
class Body : KinematicBody2D
{
    private Vector2 _velocity = new Vector2(250, 250);

    public override void _PhysicsProcess(float delta)
    {
        var collisionInfo = MoveAndCollide(_velocity * delta);
        if (collisionInfo != null)
            _velocity = _velocity.Bounce(collisionInfo.Normal);
    }
}

move_and_slide

滑动是一种常见的碰撞响应; 想象一个游戏角色在上帝视角的游戏中沿着墙壁移动, 或者在平台游戏中上下坡. 虽然可在使用 move_and_collide() 之后自己编写这个响应, 但 move_and_slide() 提供了一种快捷方法来实现滑动且无需编写太多代码.

警告

move_and_slide() 在计算中自动包含时间步长, 因此您 应将速度向量乘以 delta .

例如, 使用以下代码制作一个可以沿着地面(包括斜坡)行走的角色, 并在站在地面时跳跃:

extends KinematicBody2D

var run_speed = 350
var jump_speed = -1000
var gravity = 2500

var velocity = Vector2()

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 is_on_floor() and jump:
        velocity.y = jump_speed
    if right:
        velocity.x += run_speed
    if left:
        velocity.x -= run_speed

func _physics_process(delta):
    velocity.y += gravity * delta
    get_input()
    velocity = move_and_slide(velocity, Vector2(0, -1))
class Body : KinematicBody2D
{
    private float _runSpeed = 350;
    private float _jumpSpeed = -1000;
    private float _gravity = 2500;

    private Vector2 _velocity = new Vector2();

    private void GetInput()
    {
        _velocity.x = 0;

        var right = Input.IsActionPressed("ui_right");
        var left = Input.IsActionPressed("ui_left");
        var jump = Input.IsActionPressed("ui_select");

        if (IsOnFloor() && jump)
            _velocity.y = _jumpSpeed;
        if (right)
            _velocity.x += _runSpeed;
        if (left)
            _velocity.x -= _runSpeed;
    }

    public override void _PhysicsProcess(float delta)
    {
        _velocity.y += _gravity * delta;
        GetInput();
        _velocity = MoveAndSlide(velocity, new Vector2(0,-1));
    }
}

有关使用 move_and_slide() 的更多详细信息, 请参阅 运动学角色(二维) , 包括带有详细代码的演示项目.