Godot C# 自定义摄像机

前言

        说起来,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)icon-default.png?t=O83Ahttps://github.com/MOWEIII/GoDogKit

        有需要的同志可以看看,虽然我的水平很低就是了。

摄像机震动???

        我在Unity开发中曾做个一个相机震动的效果,就很简单的在一个圆形范围内随机点,赋值给摄像机位置,只要频率够快,就能模拟出震动效果。

        虽然逻辑简单,但处理起来还要考虑很多东西,如果用Timer的方式(就是声明计时用的一堆变量)就需要很复杂的启动逻辑和变量管理。幸好Unity为我们提供了协程,我们可以轻松实现延迟和计时等等。

        那么问题来了,Godot C# 也没有协程啊(GDScript好像有)。那没办法了,只能自己做去罢。结果这一做不得了,又发现很多好玩的东西。留到下一章单独细说吧。

猜你喜欢

转载自blog.csdn.net/m0_73087695/article/details/142456698