一个起点
既然已经解决了碰撞逻辑,那就可以在这个理念上进行物理系统的搭建了。那从何开始入手呢?我自己的思路是先建立一个碰撞体的基类,然后将围绕这个基类,进行整个系统的填补。
Collider是所有碰撞体的基类。在整个游戏环境中,任何GameObject的碰撞交互,例如子弹和敌人之间,玩家和道具之间,都可以被抽象地理解为两个碰撞体相交,并产生一些特定的结果。所以在Collider中,它首先要具有一个属性,就是geometry,即用于定义该碰撞体的几何形状,而沿用《蔚蓝》的设计理念,这个几何形状毫无疑问就是矩形。
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物理系统是相当简易的,例如像穿越平台,或者运动平台等功能都还未实现。诸多不足,还请包涵,如果有更好的想法欢迎提出来,大家一起共勉。
而最后一篇文章,将分享一些小优化,同时也能进一步阐明自己在开发这套系统时萌生的一些思考和理解。
哇塞