EasyTrajectory
EasyTrajectory是一个针对2D场景,用于快速,轻松的创建诸如弹道,敌人移动之类的轨迹的插件,完全由GDscript编写。可以通过不同轨迹类型的组合,简单,灵活地创建出较为复杂的轨迹
将addons文件夹复制到你的godot项目即可完成安装
快速上手
要创建一个轨迹,有两种方法:
- 使用new()方法
- 使用工厂函数create()
下面以创建一条倾角30°的直线轨迹为例,物体在此轨迹上的移动速度为30
**注意:**Godot的坐标系是计算机图形学中常用的坐标系,因此,30°是从x轴正方向顺时针旋转30°而非像数学中那样逆时针旋转30°
new()
var traj = LinearTrajectory.new(30.0,30.0)
create()
var traj = BaseTrajectory.create(
"linear",
{
"speed" : 30.0,
"direction" : 30.0
}
)
在使用工厂函数时,第一个参数为轨迹类型,第二个参数时轨迹的参数,键名与轨迹的对应属性名相同
建议:总是使用工厂函数create()的方式,除非需要快速对某些轨迹形状进行测试
使用轨迹
要使用轨迹,需要在 _process 方法或是 _physics_process 方法中进行如下操作:
func _process(delta : float):
traj.update(delta) #更新轨迹状态
position += traj.evaluate(delta) #迭代节点位置
-
update(delta) 用于更新轨迹内部属性,应当在 evaluate(delta) 前调用
-
evaluate(delta) 返回一个 Vector2 类型的位移矢量,与节点的position属性相加即可让节点沿轨迹移动
注意:轨迹的坐标位置并不是绝对的,EasyTrajectory定义的轨迹只是定义一个轨迹的形状,轨迹的起点为节点开始迭代时的自身位置。从效果上来看,对哪个节点使用EasyTrajectoy的轨迹迭代,轨迹使用的就是该节点的局部坐标系
重置与重定义轨迹
- 重置轨迹至初始状态:
traj.reset()
注:如上所述,这并不能让被迭代的对象回到原来的位置
- 重定义轨迹(以LinearTrajectory为例):
traj.redefine(
{
"speed" : 50.0,
"direction" : 20.0
}
)
重定义的轨迹与原轨迹类型相同,且重定义的参数只能由字典方式指定(就像工厂函数那样)
注:在TrajectoryHolder及其子类中,子轨迹的类型在重定义时可以改变(见下文)
EasyTrajectory提供了集中默认集成的轨迹类型,通过这些轨迹类型及其组合,能满足大部分的基本功能,下面将介绍EasyTrajectory中自带的轨迹类型
默认轨迹类型
类型清单列表(括号内为工厂函数中需要使用的名字):
- LinearTrajectory (linear)
- CircleTrajectory (circle)
- VelAccelTrajectory (velaccel)
- BezierTrajectory (bezier)
- SequenceTrajectoryHolder (sequence_holder)
- BlendTrajectoryHolder (blend_holder)
**注:**标有(可选)的属性,在创建时含默认参数,可以不提供
BaseTrajectory
所有轨迹的基类,具有如下的常用属性:
_progress : float #轨迹进程,范围为 0.0 ~ 1.0 ,如果轨迹具有一个结束状态,该属性表明了轨迹当前的迭代进程
signal ended #结束信号,如果轨迹存在结束状态,则轨迹迭代至结束状态时,该信号会发出
LinearTrajectory (linear)
直线轨迹类型,具有如下属性:
speed : float #速率
acceleration : float #加速度(可选),默认值 0
direction : float: #方向角,用角度制表示
ending_phase : float #结束状态,此处由直线长度定义 (可选), 默认值 -1
在工厂函数中,按如下格式创建(后续仅列出必要属性):
BaseTrajectory.create(
"linear",
{
"speed" : 10.0,
"acceleration" : 0.0,
"direction" : 20.0,
"ending_phase" : 60.0
}
)
CircleTrajectory (circle)
圆形轨迹类型,具有如下属性(角度相关的均为角度制):
radius : float #半径
angle : float #当前相位 (可选), 默认值 0,在创建时设置此参数改变节点在轨迹上的初始位置
angular_speed : float #角速度
angular_acceleration : float #角加速度 (可选), 默认值 0
ending_phase : float #结束状态,此处由角位移定义 (可选), 默认值 -1
在工厂函数中,按如下格式创建:
BaseTrajectory.create(
"circle",
{
"radius" : 50.0,
"angular_speed" : 30.0
}
)
VelAccelTrajectory (velaccel)
由一个Vector2 类型的速度和 Vector2 类型的加速度定义,具有如下属性:
velocity : Vector2 #速度
acceleration : Vector2 #加速度 (可选), 默认值 Vector2.ZERO
ending_phase : float #结束状态,此处由轨迹长度(路程)定义 (可选),默认值 -1
在工厂函数中,按如下格式创建:
BaseTrajectory.create(
"velaccel",
{
"velocity" : Vector2(30,0),
"acceleration" : Vector2(0,50)
}
)
BezierTrajectory (bezier)
BezierUnit
贝塞尔曲线的基本单元,定义如下:
class BezierUnit:
var point : Vector2 #点位置
var ctrl_in : Vector2 #控制向量in (可选)
var ctrl_out : Vector2 #控制向量out (可选)
在构造 BezierUnit 时,ctrl_in 和 ctrl_out 可以不指定,默认为 Vector.ZERO
详细有关贝塞尔曲线的信息,请参照Godot官方文档:贝塞尔,曲线和路径
Trajectory
由一系列贝塞尔曲线的控制点定义的轨迹类型,具有如下属性:
_curve : Curve2D #贝塞尔曲线(创建时不用提供)
speed : float #速率
acceleration : float #加速度 (可选), 默认值 0
BezierTrajectory在创建时:
-
需要提供控制点列_points : Array作为第一个参数,其中的类型可以是:
-
BezierUnit
-
Vector2
- 作为 BezierUnit 的 point 属性
-
Dictionary
-
{ "point": Vector2(0,0), "in" : Vector2(-10,-10), "out" : Vector2(10,10) }
-
-
-
BezierTrajectory并没有ending_phase参数,因为贝塞尔曲线通常总是有一个确定的长度。
在工厂函数中,按如下格式创建:
var traj = BaseTrajectory.create(
"bezier",
{
"speed" : 60.0,
"points": [
{
"point": Vector2(226,388),
"out" : Vector2(284.1,-42.44)
},
{
"point" : Vector2(267,83),
"in" : Vector2(17.68,137.94),
"out" : Vector2(-17.68,-137.9)
},
{
"point" : Vector2(0,0),
"in" : Vector2(236.9,-80.17)
}
]
}
)
注:迭代对象在贝塞尔曲线上的速度完全由speed属性决定,与曲线在某点的曲率无关
TrajectoryHolder
以下是较为特殊的轨迹类型TrajectoryHolder,它们本身不直接定义轨迹,它们包含多条各种类型的轨迹,并以某种方式将这些轨迹组合,这使得创建复杂灵活地轨迹成为可能
因为TrajectoryHolder仅仅包含上述的 ”基本轨迹“,因此它们通常没有额外的属性。TrajectoryHolder中_process属性是无效的,不能通过 _process得知轨迹的迭代进程。但用户仍然可以使用ended信号来获知轨迹是否结束,当TrajectoryHolder中所有轨迹均结束时,ended信号会被发出
注:因为EasyTrajectory效果上使用的是迭代对象的局部坐标系,因此TrajectoryHolder下包含的的轨迹之间无需满足例如连续等约束条件。
SequenceTrajectoryHolder (sequence_holder)
轨迹序列,在此之中的各段轨迹,按顺序从头到尾进行拼接,具有以下额外属性:
current_traj : int #当前轨迹的下标编号
SequenceTrajectoryHolder中,可以使用如下代码间接获取轨迹的迭代进程
_holder[current_traj]._process
**注:**除了SequenceTrajectoryHolder中最后一段轨迹,其余轨迹理论上应当是具有结束状态的,否则该段轨迹后面的轨迹将会被忽略
在工厂函数中,按如下格式创建:
BaseTrajectory.create(
"sequence_holder",
{
1 : {
"type" : "linear",
"param" : {
"speed" : 30.0,
"direction" : 40.0,
"ending_phase" : 100.0
}
},
2 : {
"type" : "circle",
"param" : {
"radius" : 60.0,
"angular_speed" : 40.0
}
}
}
)
注:每一段轨迹前的 “键名”并无任何要求(包括类型要求),可以将键名作为该段轨迹的注释使用
BlendTrajectoryHolder (blend_holder)
合成轨迹,在此之中的各段轨迹,将会进行运动合成,不具有额外属性
**注:**BlendTrajectoryHolder中的各段轨迹并无任何约束限制,其中的轨迹可以具有结束状态也可以不具有,具有结束状态的轨迹相当于只影响迭代中的一段过程
在工厂函数中,按如下格式创建, 此处将给出一个较为复杂的嵌套Holder轨迹定义,描述一段直线接一段螺旋线:
BaseTrajectory.create(
"sequence_holder",
{
"line" : {
"type" : "linear",
"param" : {
"speed" : 30.0,
"direction" : 20.0,
"ending_phase" : 160.0
}
},
"spiral" : {
"type" : "blend_holder",
"param" : {
1 : {
"type": "linear",
"param": {
"speed" : 30.0,
"direction" : 30.0
}
},
2 : {
"type": "circle",
"param": {
"radius" : 40.0,
"angular_speed" : 60.0
}
}
}
}
}
)
自定义轨迹类型
以下是EasyTrajectory较为高阶的使用方法,EasyTrajectory允许用户创建自己的轨迹类型,它们使用上与默认轨迹类型完全相同,以下是自定义轨迹类型的步骤
创建类型脚本
用户可以继承自任意一个以BaseTrajectory为基类(或其自身)的类型(通常为BaseTrajectory或TrajectoryHolder),为保证行为一致性,以下方法需要被重载
-
func update(delta : float):
-
func evaluate(delta : float) -> Vector2:
在update方法中,除了描述轨迹的内部属性,还应当正确的维护_progress变量(继承自TrajectoryHolder的除外),以下是LinearTrajectory中的代码示例
func update(delta : float):
if not _ended and _valid: #轨迹没有结束且有效
speed += acceleration * delta #更新描述轨迹的内部属性
if not _ending_phase == -1: #如果定义了结束状态
_progress += (speed * delta * _vec_direction).length() / _ending_phase #维护_progress变量
SequenceTrajectoryHolder中的代码示例:
func update(delta : float):
if not _ended and _valid:
_holder[current_traj].update(delta)
在evaluate方法中,需要返回一个位移矢量,以下是LinearTrajectory中的代码示例:
func evaluate(delta : float) -> Vector2:
if not _ended and _valid: #轨迹没有结束且有效
return speed * delta * _vec_direction #返回位移矢量
else:
return Vector2.ZERO #此时轨迹不应该作用,返回Vector2.ZERO
以下是SequenceTrajectoryHolder中的代码示例:
func evaluate(delta : float) -> Vector2:
if not _ended and _valid:
return _holder[current_traj].evaluate(delta)
else:
return Vector2.ZERO
注:通常来说,用户还需要重载_init方法,以在创建轨迹时传入必要的参数
支持工厂函数
以上,我们完成了一个新轨迹类型的基本迭代功能,但此时无法通过工厂函数 BaseTrajectory.create() 来创建我们定义的轨迹类型
要支持工厂函数,需要完成以下额外工作:
- 重载 _static_init 方法
- 在插件根目录下的UserConfig/register.json中的 "custom"里,加入类型脚本的脚本路径
重载_static_init方法
在此方法中,需要调用以下方法注册我们的自定义类型
static func _register(type_name : String, _constructor : Callable, _validator : Callable = func(_p): return true):
参数解释:
- type_name
- 工厂函数中的轨迹类型,如前文提到的"linear","circle"
- _constructor
- 构造器,用以实际创建轨迹对象
- _validator
- 参数验证器,验证传来的参数字典是否合法
以下为LinearTrajectory中的代码示例:
static func _static_init() -> void:
var validator := func(_p : Dictionary): #定义参数验证器,需要返回bool值,验证成功为true
return (
_p.has("speed") and _p.speed is float and
_p.has("direction") and _p.direction is float and
((_p.has("acceleration") and _p.acceleration is float) or not _p.has("acceleration")) and
((_p.has("ending_phase") and _p.ending_phase is float) or not _p.has("ending_phase"))
)
#注册函数
BaseTrajectory._register(
"linear", #轨迹类型名
func(_p) : #构造器
return LinearTrajectory.new(
_p.speed, #必要参数
_p.direction,
0 if not _p.has("acceleration") else _p.acceleration, #处理可选参数
-1 if not _p.has("ending_phase") else _p.ending_phase
),
validator #参数验证器
)
加入脚本路径
由于_static_init方法需要加载脚本(load或preload)时才被调用,因此需要确保在使用工厂函数前,轨迹类型脚本均已被加载。
加载脚本由全局自动脚本 Trajectory/trajectory_register.gd进行,将逐一使用load方法加载配置文件中的所有脚本路径。
配置文件在插件根目录下的UserConfig/register.json中(通常为res://addons/easy_trajectory/UserConfig/register.json),其应该已经存在如下内容:
{
"default" : [
"res://addons/easy_trajectory/Trajectory/SimpleTrajectory/linear.gd",
"res://addons/easy_trajectory/Trajectory/SimpleTrajectory/circle.gd",
"res://addons/easy_trajectory/Trajectory/ComplexTrajectory/va.gd",
"res://addons/easy_trajectory/Trajectory/ComplexTrajectory/bezier.gd",
"res://addons/easy_trajectory/Trajectory/TrajectoryHolder/trajectory_holder.gd",
"res://addons/easy_trajectory/Trajectory/TrajectoryHolder/sequence_trajectory_holder.gd",
"res://addons/easy_trajectory/Trajectory/TrajectoryHolder/blend_trajectory_holder.gd"
],
"custom" : [
]
}
default一栏为插件的默认类型,为方便区分,请将自定义的脚本路径加在custom中
支持重置与重定义
要支持重置与重定义功能,需要一些额外处理
重置
重置功能函数:
#BaseTrajectory
_resetter : Callable
赋值该属性以定义重置器,重置功能需要轨迹被创建时的原始参数,此处建议使用bind()方法绑定原始参数
以LinearTrajectory为例:
func take_param(speed : float, direction : float, acceleration : float = 0, ending_phase : float = -1):
self.speed = speed
self.direction = direction
self.acceleration = acceleration
self._ending_phase = ending_phase
func _init(speed : float, direction : float, acceleration : float = 0, ending_phase : float = -1) -> void:
...
self._resetter = Callable(take_param).bind(speed, direction, acceleration, ending_phase)
...
注:基本轨迹属性,如 _progress, _last_progress, _valid, _ended 不需要在重置器中维护
重定义
重定义功能函数:
#BaseTrajectory
_redefiner := func(_p): return
赋值该属性以定义重定义器,重定义器接受一个字典,从中提取必要参数
注:
- 你无需在_redefiner中校验字典数据,校验在调用redefine() 时自动完成
- 在_redefiner中,请重新bind() _resetter中的参数
以LinearTrajectory为例:
func take_param_dict(_p : Dictionary):
self.speed = _p.speed
self.direction = _p.direction
self.acceleration = 0 if not _p.has("acceleration") else _p.acceleration
self._ending_phase = -1 if not _p.has("ending_phase") else _p.ending_phase
func _init(speed : float, direction : float, acceleration : float = 0, ending_phase : float = -1) -> void:
...
self._redefiner = func(_p) :
take_param_dict(_p)
self._resetter = Callable(take_param).bind(
_p.speed,
_p.direction,
0 if not _p.has("acceleration") else _p.acceleration,
-1 if not _p.has("ending_phase") else _p.ending_phase
)
...
针对TrajectoryHolder类型
TrajectoryHolder类型有一个额外属性,用于在子轨迹结束时执行相应操作
var _connector := func(item : BaseTrajectory):
return
以SequenceTrajectoryHolder为例:
func _init(trajectories : Array[BaseTrajectory] = []) -> void:
self._connector = func(item : BaseTrajectory):
item.ended.connect(
func():
if not ending_cnt >= _holder.size():
current_traj += 1
)
super(trajectories)
...
注:继承自TrajectoryHolder的类型,请一定调用父级(TrajectoryHolder)的 _init() 方法,否则会导致信号异常,轨迹无法结束
TrajectoryHolder 重载了reset() 和 redefine() 方法,能正确的处理ended信号连接和子轨迹的构造,因此除非特殊需求,无需重新编写_resetter 和 _redefiner。如果定义了 _redefiner, 也无需重新bind() _resetter
在redefine() 方法中,_redefiner会在信号连接处理完毕后,reset() 方法之前被调用
至此,我们自定义的轨迹类型应当可以像默认类型那样工作了,恭喜~
一些后话
感谢能读到这里,这款插件仍然处于初期开发阶段,仍有些功能不完善,也有一些实现方式比较粗糙,后续会更新以下功能:
- 参数突变(轨迹运行时更改参数值,保留目前的状态)
- 集成的轨迹对象池,用以支持诸如弹幕地狱游戏频繁,大量创建轨迹的需求
再次感谢使用,支持本插件的所有人 ღ( ´・ᴗ・` )~