基于《蔚蓝(CELESTE)》实现一个简易的2D物理系统(二)

作者:阿客
2022-12-30
6 1 2

一个起点

既然已经解决了碰撞逻辑,那就可以在这个理念上进行物理系统的搭建了。那从何开始入手呢?我自己的思路是先建立一个碰撞体的基类,然后将围绕这个基类,进行整个系统的填补。

Collider 是所有碰撞体的基类。在整个游戏环境中,任何 GameObject 的碰撞交互,例如子弹和敌人之间,玩家和道具之间,都可以被抽象地理解为两个碰撞体相交,并产生一些特定的结果。所以在 Collider 中,它首先要具有一个属性,就是 geometry,即用于定义该碰撞体的几何形状,而沿用《蔚蓝》的设计理念,这个几何形状毫无疑问就是矩形。

所有有碰撞需求的*GameObject* 都是`Collider`,且都会有自己的一个矩形块

Collider 具备一个最基本也是最关键的方法,就是用 intersects()判断自己是否与其他碰撞体相交,当进行碰撞检测时,该方法会被不断地调用。至于 get_overlap()是对碰撞结果的补充,在一些特定情况下,我们需要知道两个碰撞体相交面积的数据,来进行一些位置的修正或者其他判定。具体如何应用将在下一篇文章里进行讨论。

两个子类

毫无疑问,Collider 的功能是纯粹而单一的,它奠定了碰撞体的基本要素,所以我们必然要用继承的思路去对它进行扩展,而根据需求就产生了两个子类,Kinematic 和 Trigger。

Kinematic 用于处理运动的,需要产生物理碰撞结果的 GameObject,例如玩家角色和地面上的箱子。在下面的动图中可以看到,角色小人可以在平台上自由活动,也可以跳到箱子上。如果将箱子推动,那箱子离开平台也会因为重力而向下掉落。

这一切的效果都归功于 Kinematic 的move()方法。该方法已经在上一篇文章讲解过了,这里展示一下箱子 Box 是如何应用的代码。首先它继承于 Kinematic,然后在 _physics_process(类似于 Unity 的 Update)中对箱子的速度添加重力加速度,因为通常情况下,箱子只受到的重力,至于角色对玩家的推力,就要特别处理。由于角色的代码相对来说比较复杂,里面涉及到状态机和运动逻辑,所以就不放出来了,但其核心思路是一样,任何运动的 GameObject,首先根据需求设计和实现计算速度的代码,然后在调用move()接口去处理运动。

extends Kinematic
class_name Box

var _velocity:= Vector2.ZERO
var _gravity:= 0.0
var _is_on_ground:= false

func _ready():
    _gravity = Config.get_box_gravity()

func _physics_process(delta):    
    _velocity.y += _gravity * delta
    move(_velocity * delta)

另一方面,Trigger 的特点就是,它可以产出碰撞交互,而且允许碰撞体重叠,它常常用于子弹,开关,入口这样的地方。基于这点就当然不用像 Kinematic 那样处理运动的问题了,任何需要运动的 Trigger 根据自己的需求独立实现运动效果即可。而自己只需要做的就是管理和它产生重叠的这些碰撞体,并回调相应的事件。

除了这两个子类,似乎还缺点什么。像地面这种,并不移动,但也不能穿过的 GameObject,该如何处理?在该项目里,我自己是直接用 Collider 这个基类来处理的,因为我个人的理解是,GameObject 的需求既然和这个 Class 是契合的,那就暂时不做更多的工作了。如果想做得更细致点,那也可以像 Unity 里面那样,用刚体统一管理,Static 和 Kinematic 作为两个选项,让开发者自由配置。

系统深化

最后就是碰撞系统的构造了,CollisionSystem 应当被视为一个单例,在游戏环境中持续存在,而任何 Collider 的初始化和销毁都要在 CollisionSystem 注册和注销,因为会有两个数组 colliders 和 triggers 来分别存放当前场景里的碰撞体和触发器。

当一个 Collider 在运动时,需要判断它是否与另一个 Collider 相撞,那 CollisionSystem 就提供了这样的一个接口 collision_detect(),在该接口中,会遍历 colliders 里面所有注册的 Collider,然后把它们的大小和位置与目标 Collider 做比较,如果重合将返回结果,没有重合将返回空。

func collision_detect(entity: Collider, offset = Vector2.ZERO) -> CollisionResult:     
    for other in _colliders:         
        if _check_collision_condition(entity, other):             
            if entity.intersects(other, offset):                 
                var result = CollisionResult.new(other)                 
                result.overlap = entity.get_overlap(other, offset)
                return result

    return null

需要注意的是,在这块逻辑中,collision_detect()是由系统提供,运动的 GameObject 主动去调用,处理碰撞结果。但 _trigger_detect()则不同,不是 Trigger 去主动调用,而是由该系统主动地每一帧调用,在这个过程中,即会检测 Trigger 和 Trigger 是否碰撞,也会检测 Trigger 和 Collider 是否碰撞,而无论是刚刚产生碰撞,还是脱离碰撞,都会调用 Trigger 相应的接口来做处理。比如像下面这样,遍历 colliders,如果相撞了,就将相撞 Collider 添加到该 Trigger 自己管理的数组当中,反之则从中移除。

func _physics_process(delta):
    _trigger_detect()

func _trigger_detect():
    for trigger in _triggers:
        for other in _colliders:
            if _check_trigger_condition(trigger, other):
                if trigger.intersects(other, Vector2.ZERO):
                    trigger.add_entered_collider(other)
                else:
                    trigger.remove_entered_collider(other)    

                    ......

深表歉意

该碰撞系统的框架就够就是说完了,而我想特别强调一点,出于对《Celeste》这套设计思路的喜欢,自己尝试搭建之后,发现确实达到了一定的效果,所以才想下笔成文,分享出来,希望能给有需要的人带来一些启发和帮助。但我也深知项目本身就是一个半成品,而且因为个人原因暂时停止了该项目的开发,所以搭建出来的 2D 物理系统是相当简易的,例如像穿越平台,或者运动平台等功能都还未实现。诸多不足,还请包涵,如果有更好的想法欢迎提出来,大家一起共勉。

而最后一篇文章,将分享一些小优化,同时也能进一步阐明自己再开发这套系统时萌生的一些思考和理解。

近期点赞的会员

 分享这篇文章

阿客 

想成为很棒的游戏设计师 

您可能还会对这些文章感兴趣

参与此文章的讨论

  1. egoist 2023-06-24 微信会员

    大佬可否将项目开源到github上面,求求

    • 阿客 2023-06-26

      @egoist:主要项目完成度比较低,就没有开源的打算了。如果有什么问题可以直接加微信交流。

您需要登录或者注册后才能发表评论

登录/注册