前言
说起来,Unity的社区环境跟插件支持确实要比Godot好很多,比如我们Unity最喜欢的Cinemachine插件,只需要动动手指就能轻松实现很多高级的摄像机动效。
所以一转到Godot就有一种力不从心的感觉,于是既然动不了手指我们就动手。自己做一个想要的摄像机。
Godot版本:4.3 mono
思路
其实没什么好说的,一开始只是想做一个能跟着某个节点移动的摄像机,至于为什么不直接把摄像机作为被跟随节点的子节点呢?因为Godot中的摄像机是按照节点树层次找到并展示最近的一个父级Viewport的,我觉得如果一个场景中有多个摄像机的情况,再加上被跟随节点自带的摄像机可能就不好管理了。还有用Unity习惯了,就这样考虑了。
人都是贪得无厌的,一开始还是只想做一个跟随就好了,结果为了满足自己的奇葩需求,索性就加了一些其他功能。
源码
using System.Collections;
using Godot;
namespace GoDogKit
{
/// <summary>
/// A highly customizable camera that automatically follows a target.
/// </summary>
public partial class AutoCamera2D : Camera2D
{
/// <summary>
/// The target to follow.
/// </summary>
[Export] public Node2D FollowTarget { get; set; } = null;
/// <summary>
/// Defines the maximum distance from the target to follow. Does not effect to the predict behaviour.
/// </summary>
[Export] public float FollowClamp { get; set; } = 1.0f;
/// <summary>
/// Defines the camera's behaviour when following the target.
/// </summary>
public enum BehaviourType
{
/// <summary>
/// Follows the target normally. Results in global position copying.
/// </summary>
Normal,
/// <summary>
/// Smoothly follows the target in a given duration. Results in global position interpolation.
/// </summary>
Inching,
/// <summary>
/// Follow the target with a constant speed. It can be faster or slower than the target's speed.
/// If the follow speed equals or exceeds the target's speed, results just like the Normal behaviour.
/// If the follow speed is slower than the target's speed, the camera will
/// be clamped within a given distance from the target aka max distance.
/// </summary>
Slow,
/// <summary>
/// Follow the target with predictive behaviour.
/// It predicts the target's movement based on its last position.
/// And moves the camera towards the predicted position which
/// determined by predict distance with a constant speed.
/// </summary>
Predict
}
[Export] public BehaviourType Behaviour { get; set; } = BehaviourType.Normal;
[ExportGroup("Inching Properties")]
[Export]
public float InchingDuration
{
get => m_inchingDuration;
set => m_inchingDuration = value;
}
private float m_inchingDuration = 1.0f;
private float m_inchingTimer = 0.0f;
[ExportGroup("Slow Properties")]
[Export] public float SlowFollowSpeed { get; set; } = 100.0f;
[Export] public float SlowFollowMaxDistance { get; set; } = 100.0f;
[ExportGroup("Predict Properties")]
[Export] public float PredictFollowSpeed { get; set; } = 100.0f;
[Export] public float PredictDistance { get; set; } = 100.0f;
private Vector2 m_targetLastPos = Vector2.Zero;
public override void _Ready()
{
m_inchingTimer = m_inchingDuration;
m_targetLastPos = Vector2.Zero;
}
private void NormalFollow(double delta)
{
GlobalPosition = FollowTarget.GlobalPosition;
}
private void InchingFollow(double delta)
{
float distance = GlobalPosition.DistanceTo(FollowTarget.GlobalPosition);
// If the target is too close, stop inching.
if (distance < FollowClamp)
{
m_inchingTimer = m_inchingDuration;
return;
}
m_inchingTimer -= (float)delta;
// If the inching timer has reached 0, reset it and start inching again.
float rate = m_inchingTimer <= 0.0f ? 1.0f : 1.0f - m_inchingTimer / m_inchingDuration;
var _x = Mathf.Lerp(GlobalPosition.X, FollowTarget.GlobalPosition.X, rate);
var _y = Mathf.Lerp(GlobalPosition.Y, FollowTarget.GlobalPosition.Y, rate);
GlobalPosition = new Vector2(_x, _y);
}
private void SlowFollow(double delta)
{
float distance = GlobalPosition.DistanceTo(FollowTarget.GlobalPosition);
// If the target is too close, stop following.
if (distance < FollowClamp)
{
return;
}
// If the target is too far, move it to max distance position.
if (distance > SlowFollowMaxDistance)
{
Vector2 distanceVec = (FollowTarget.GlobalPosition - GlobalPosition).Normalized() * SlowFollowMaxDistance;
GlobalPosition = FollowTarget.GlobalPosition - distanceVec;
return;
}
var _x = Mathf.MoveToward(GlobalPosition.X, FollowTarget.GlobalPosition.X, (float)delta * SlowFollowSpeed);
var _y = Mathf.MoveToward(GlobalPosition.Y, FollowTarget.GlobalPosition.Y, (float)delta * SlowFollowSpeed);
GlobalPosition = new Vector2(_x, _y);
}
private void PredictFollow(double delta)
{
// Predict the direction of the target based on its last position.
Vector2 predictedDir = (FollowTarget.GlobalPosition - m_targetLastPos).Normalized();
Vector2 predictedPos = FollowTarget.GlobalPosition + predictedDir * PredictDistance;
var _x = Mathf.MoveToward(GlobalPosition.X, predictedPos.X, (float)delta * PredictFollowSpeed);
var _y = Mathf.MoveToward(GlobalPosition.Y, predictedPos.Y, (float)delta * PredictFollowSpeed);
GlobalPosition = new Vector2(_x, _y);
// Record the last position of the target for the next prediction.
m_targetLastPos = FollowTarget.GlobalPosition;
}
public override void _PhysicsProcess(double delta)
{
// If there is no target, do nothing.
if (FollowTarget == null) return;
switch (Behaviour)
{
case BehaviourType.Normal: NormalFollow(delta); break;
case BehaviourType.Inching: InchingFollow(delta); break;
case BehaviourType.Slow: SlowFollow(delta); break;
case BehaviourType.Predict: PredictFollow(delta); break;
}
}
}
}
其实结构还是非常简单明了的(因为我也写不出很复杂的东西)。通过预设值决定摄像机的具体行为逻辑,就是这么简单。
哦对了,Godot的2D和3D的区别跟Unity不一样,Unity的2D是伪2D,而Godot的2D是真2D,
所以2D跟3D之间的沟壑可能比Unity大。所以我先做了2D的相机。
这个的操作方式应该跟Unity的差不多,就是调整数值还有选模式。主要这些模式都是我硬编的,其实我也不知道应该怎么为这些模式命名:
1.Normal,普通行为,就一直跟着,其实就是复制位置。
2.Inching,我管它叫缓动,从代码可以看出,这玩意跟时间有关,设计之初是想实现“在规定时间结束时,镜头恰好到达物体位置”,结构因为插值插的太快了,所以只能看出一点点效果,所以之后应该会大改或者直接砍掉;
3.Slow,慢跟随。其实也可以快,通过控制跟随速度营造出“镜头和物体相对运动的效果”。
实际上镜头跟随太慢会被限制在一个距离内,从而避免物体跑太快了以至于跑出镜头外。
// If the target is too far, move it to max distance position.
if (distance > SlowFollowMaxDistance)
{
Vector2 distanceVec = (FollowTarget.GlobalPosition - GlobalPosition).Normalized() * SlowFollowMaxDistance;
GlobalPosition = FollowTarget.GlobalPosition - distanceVec;
return;
}
限制手段就是这个:当相对距离超过限定距离时,根据等式关系减去偏移量。
为什么要单独拿出来记录呢?因为我之前写那个Untiy卡牌拖拽模型的时候,就是遇到了这种“锁定偏移量”的类似问题,当时还强调了一下,结果现在做开发的时候又又又错了。
4.Predict,预测跟随。这个比较有意思,我忘了Cinemachine有没有,印象中好像就是没有的。因为感觉很多游戏都会有这么一个“根据玩家移动方向适当移动镜头”的操作,那么我也尽量用自己的手段实现:很简单,根据上下帧得出运动方向的预测值,然后朝那个方向运动预设的一段距离。
然后其实没什么了,我记得Cinemachine可以设置帧处理方式,比如Update和FixedUpdate,但是在这里我就索性扔到物理帧处理中了。
所谓的什么模式,只是打开一个DIY思路,后面有什么需求再自己修改就好了。
结语
这里不得不提一嘴,Godot开发插件的方式真的极其简单,基本上直接把源码拿进去就行,所以我索性就把学习开发过程中造的轮子搞成一堆插件扔在Github上了,这几天Unity转Godot就一直在更新:
MOWEIII/GoDogKit: A Plugin kit used by Godot which personally used and maybe continue to be update. (github.com)https://github.com/MOWEIII/GoDogKit
有需要的同志可以看看,虽然我的水平很低就是了。
摄像机震动???
我在Unity开发中曾做个一个相机震动的效果,就很简单的在一个圆形范围内随机点,赋值给摄像机位置,只要频率够快,就能模拟出震动效果。
虽然逻辑简单,但处理起来还要考虑很多东西,如果用Timer的方式(就是声明计时用的一堆变量)就需要很复杂的启动逻辑和变量管理。幸好Unity为我们提供了协程,我们可以轻松实现延迟和计时等等。
那么问题来了,Godot C# 也没有协程啊(GDScript好像有)。那没办法了,只能自己做去罢。结果这一做不得了,又发现很多好玩的东西。留到下一章单独细说吧。