首页 > 其他分享 >第三人称游戏的相机控制

第三人称游戏的相机控制

时间:2024-06-10 22:55:20浏览次数:13  
标签:第三人称 游戏 Mathf 旋转 相机 OrbitAngles var 对齐

第三人称游戏的相机控制

Unity已经有了Cinemachine这一强大的插件来辅助开发者更容易地控制相机运动,但我觉得学习一下相机控制背后的原理还是挺有益的,没准哪天就你像定制某种相机控制的功能,又觉得Cinemachine难调呢!

本文学习自 Jasper Flick 大神的 运动系列教程

相机的旋转控制

  1. 相机会根据设备输入进行旋转,给人一种扭头的感觉。它可以是鼠标的移动、某些按键组合等,总之就是一种二维向量信息。再给定一个旋转的速度来调节灵敏度,此外相机也不需要太敏感,可以忽视微小量的输入

    //是否有操控相机旋转
    private bool IsManualRotation(Vector2 cameraInput)
    {
        if(Mathf.Abs(cameraInput.x) > minInputValue 
            || Mathf.Abs(cameraInput.y) > minInputValue)
        {
            OrbitAngles += rotationSpeed * Time.unscaledDeltaTime * cameraInput;
            return true;
        }
        return false;
    }
    
  2. 通常我们还会限制其俯仰角 (避免底裤被看穿,在游戏引擎中,就是限制相机绕x轴旋转的角度;至于水平面的旋转(偏航角),一般是不受限制,但为了配合其它相机运动的工作,会将这个值限定在 0 ~ 360 度。

    image image
    //约束角度
    private void ConstrainAngles ()
    {
        //限制俯仰角
        OrbitAngles.x = Mathf.Clamp(OrbitAngles.x, MinVerticalAngle, MaxVerticalAngle);
        //规范偏航角
        if(OrbitAngles.y >= 360f)
        {
            OrbitAngles.y -= 360f;
        }
        else if(OrbitAngles.y < 0f)
        {
            OrbitAngles.y += 360f;
        }
    }
    
  3. 我们通常还需要将相机的朝向与角色的移动、转向等相结合,最关键的一点就是提取相机朝向对于角色而言有用的分量:可以通过将相机的坐标投影当前角色运动的平面(需要法线)来获取。

image

相机聚焦

第三人称视角的相机要「紧盯」目标,但不建议将相机作为观测对象子物体的形式来实现这一目标。通常是让相机与角色保持一定距离,控制相机旋转时呈球面运动。在已知相机朝向的情况下,与观测点逆向计算就可以得到位置:

image

为了不让相机移动显得太僵硬,和相机旋转类似,我们对小范围内的移动并不进行跟踪。只有玩家超出那个范围时相机才会跟踪(为方便称呼就叫它「死区半径」)。

可以通过记录上一次玩家超出死区半径时的位置来做到:只有玩家当前位置与那个位置之间的距离再次超出死区半径时,相机才进行跟踪并更新那个位置的值,以便下次判断。

//更新聚焦点
private void UpdateFocusPoint()
{
    prevFocusPoint = focusPoint; //获取上次的聚焦点
    var curFocusPoint = Focus.position; //获取观察对象的位置
    if(FocusRadius > 0) //如果有设置死区半径
    {
        var curDis = Vector3.Distance(curFocusPoint, prevFocusPoint);
        if(curDis > FocusRadius)
        {
            focusPoint = curFocusPoint;
        }
    }
    else
    {
        focusPoint = curFocusPoint;
    }
}

当然,这样的处理会导致相机运动十分僵硬,画面几乎是抖动的。利用插值可以解决:

if(curDis > FocusRadius)
{
    float lerpT = FocusRadius / curDis;
    //选择「当前」->「以前」插值,是因为 focusRaduis / curDis 是从1减少到0
    focusPoint = Vector3.Lerp(curFocusPoint, prevFocusPoint, lerpT);
}

可这就不能保证相机与观察对象的距离是期望值了,毕竟插值计算只发生在超出死区半径的时候。所以我们在通常情况下,也让相机缓缓向观察对象处靠近,同样是利用插值:

float lerpT = 1.0f;
if(curDis > 0.01f && FocusCentering > 0) //缓慢将聚焦点移到观察对象位置处
{
    lerpT = Mathf.Pow(FocusCentering, Time.unscaledDeltaTime);
}
if(curDis > FocusRadius) //超出死区半径时
{
    lerpT = Mathf.Min(lerpT, FocusRadius / curDis);
}
//选择「当前」->「过去」插值,是因为 focusRaduis / curDis 是从1减少到0
focusPoint = Vector3.Lerp(curFocusPoint, prevFocusPoint, lerpT);

FocusCentering为值在0~1之间的小数,这个值越小,向观察对象处靠近就会越慢,反之越快。
为了让两种聚焦更好的融合,在超出死区半径时,我们选用二者的最小值。下面是对比,左边为取最小值;右边是不取最小值,直接用原本的方案:

image image

注意右边未采用最小值的情况下,在停止时会有明显跟随速度的变化,像是镜头被人往前推了一把。(因为gif帧率的原因,可能看不太出来)

相机碰撞

现在的相机只是个幽灵一样的摄影师,我们希望它能更聪明点。比如,在相机与玩家之间隔了一堵墙时,我们希望它能越过那堵墙来拍摄角色,而不是严格保持着设置的距离、盯着墙壁或是卡在墙里拍摄角色。

image image

这可以通过调整 相机的近裁剪面 做到,从观测点向相机的近裁剪面处进行物理碰撞检测,一旦发现碰撞点,就调整相机的位置,保证近裁剪面处于这个碰撞点的位置。

image

需要注意的就是,近裁剪面位置不等于相机位置,以Unity为例,默认近裁剪面都会在相机前方0.3单位距离处,所以调整相机本体位置时,要考虑这部分的偏差。

//更新相机碰撞检测
private void UpdateCameraCollision()
{
    //nearClipPlane可以获取近裁剪面与相机的距离
    Vector3 rectOffset = lookDirection * camera.nearClipPlane; //近裁剪面与相机的偏差向量
    Vector3 rectPosition = lookPosition + rectOffset; //相机近裁剪面位置

    Vector3 castFrom = Focus.position; //因为是反向投射检测,所以聚焦点是起始点
    Vector3 castVector = rectPosition - castFrom; //起始点指向近裁剪面的向量
    float castDistance = castVector.magnitude; //记录该向量长度
    //记录该线段方向(已知长度可以直接除,等同于归一化)
    Vector3 castDirection = castVector / castDistance; 
    
    //利用上述信息,进行盒状投影检测,判断近裁剪面与观察对象间有障碍
    if(Physics.BoxCast(castFrom, CameraHalfExtends, castDirection, out RaycastHit hitInfo, 
    lookRotation, castDistance, ObstructionMask))
    {
        //移动到该碰撞点
        rectPosition = castFrom + castDirection * hitInfo.distance; 
        //将该碰撞点位置减去近裁剪面,得到相机应该在的位置
        lookPosition = rectPosition - rectOffset; 
    }
}

自动对齐

当相机在达到一定时间没被操控时,相机会自动对齐玩家前进的方向,这也是第三人称视角游戏常有的功能。 (这似乎能提高游戏体验,但我没想过这是为什么

这个功能的重点是对齐的实现,首先,这里的对齐是指在世界坐标的XZ平面能与玩家运动保持一致,也就是说让相机世界坐标的y轴旋转实现的。这样才能保证相机的俯仰角不变。

我们可以记录相机上一时刻聚焦的点,然后让现在聚焦的点与之对比,便能求出运动向量,根据这个向量便能求出它对应的世界坐标Z轴的角度:

image image

但要注意,用反三角函数求出来的这个角度要人为加以区分(例如通过其在x轴的分量正负号)。例如上图的两种情况,它们用反三角函数求出的角度是一样的,不加以区分可能转反。

private static float GetAngle(Vector2 direction)
{
    var angle = Mathf.Acos(direction.y) * Mathf.Rad2Deg;
    return direction.x < 0 ? 360f - angle : angle;
}

这样一来就保证所有角度都是顺时针而言的,所以上述第一种情况就是这样的角度:

image

那就这样把相机绕顺时针旋过去,未免有点“舍近求远”了吧?所以在实际旋转之前,也要判断一下怎么旋角度变化比较小:

//是否需要自动对齐
private bool IsAutoRotation()
{
    if(Time.unscaledTime - lastManualRotateTime> attributes.AlignDelay)
    {
        //根据之前聚焦的位置和当前聚焦的位置,判断观察方向的变化
        var alignDelta = focusPoint - prevFocusPoint;
        var movement = new Vector2(alignDelta.x, alignDelta.z);
        //不开根号是因为很多时候不用对齐,需要对齐时再开根号,省些计算量
        var movementDeltaSqr = movement.sqrMagnitude; 
        if(movementDeltaSqr < 0.0001f) //角度变化很小就不用对齐了
        {
            return false;
        }
        //否则就算出该变化的角度
        movement /= Mathf.Sqrt(movementDeltaSqr); //归一化
        var headingAngle = GetAngle(movement); //计算新朝向的角度

        //得到从当前相机世界坐标偏航角变化到上述角度的差值绝对值
        var deltaAbs = Mathf.Abs(Mathf.DeltaAngle(OrbitAngles.y, headingAngle));
        float rotationChange = RotationSpeed * Time.unscaledDeltaTime;
        
        //以最小的旋转角度旋转过去,故顺时针方向和逆时针方向都判断一遍
        if(deltaAbs < AlignSmoothRange)
        {
            rotationChange *= deltaAbs / AlignSmoothRange; 
        }
        else if(180 - deltaAbs < AlignSmoothRange)
        {
            rotationChange *= (180 - deltaAbs) / AlignSmoothRange;
        }

        //插值变化角度,以求平滑过渡
        OrbitAngles.y = Mathf.MoveTowardsAngle(OrbitAngles.y, headingAngle, rotationChange);
        return true;
    }
    return false;
}

在原文中,作者还设计了一种特殊情况——在重力方向可变化的空间,这时相机该如何对齐?

image

很明显,要在常规对齐的基础上额外考虑重力作用下Up轴的变化。思路其实很相似,通过上一时刻Up轴与当前Up轴之间的角度,来插值变化:

//更新重力对齐
private void UpdateGravityAlignment()
{
    //gravityAlignment为四元数,fromUp = 将up旋转gravityAlignment之后的位置
    //因为gravityAlignment记录重力旋转后的结果,故在未更新前,可认为是「上一帧的Up轴」
    var fromUp = gravityAlignment * Vector3.up;
    var toUp = CustomGravity.GetUpAxis(focusPoint);//当前重力下的up轴
    
    
    var dot = Mathf.Clamp(Vector3.Dot(fromUp, toUp), -1, 1); //防止误差而得到Nan结果
    var angle = Mathf.Acos(dot) * Mathf.Rad2Deg;//获取从fromUp与toUp间的夹角
    var maxAngle = UpAlignmentSpeed * Time.deltaTime;

    //新Up轴对齐四元数 = 新重力对齐旋转 + 原本up轴
    var newAlignment = Quaternion.FromToRotation(fromUp, toUp) * gravityAlignment;
    
    if(angle <= maxAngle) //如果夹角在单帧变化的最大夹角限度内,直接应用变化
    {
        gravityAlignment = newAlignment;
    }
    else //否则插值变化
    {
        gravityAlignment = Quaternion.SlerpUnclamped(gravityAlignment, newAlignment, maxAngle / angle);
    }
}

但这样一来,在原本偏航角的对齐时,要排除掉重力翻转的影响,不然会干扰对齐结果:

var alignDelta = Quaternion.Inverse(gravityAlignment) * (focusPoint - prevFocusPoint);

最后再一并算上:

orbitRotation = Quaternion.Euler(OrbitAngles);
//相机的旋转由两部分组成:重力轴对齐产生的旋转和通常情况下对齐的旋转
lookRotation = gravityAlignment * orbitRotation;

标签:第三人称,游戏,Mathf,旋转,相机,OrbitAngles,var,对齐
From: https://www.cnblogs.com/OwlCat/p/18241186

相关文章

  • HTML CSS JS游戏网页设计作业「响应式高端游戏资讯bootstrap网站」
    ......
  • 仿饿了么的谁去拿外卖游戏源码
    源码介绍喝酒没有游戏玩?懒得下床不想出去那么好这个游戏会满足你!玩法每人都选择一个序号4个人为例张三选第①李四选第②王五选第③赵前选第④然后就按4下其中最小的数对应的序号就是他输了就去拿外卖!源码下载仿饿了么的谁去拿外卖游戏源码......
  • 仿饿了么的谁去拿外卖游戏源码
    源码介绍喝酒没有游戏玩?懒得下床不想出去那么好这个游戏会满足你!玩法每人都选择一个序号4个人为例张三选第①李四选第②王五选第③赵前选第④然后就按4下其中最小的数对应的序号就是他输了就去拿外卖!源码下载仿饿了么的谁去拿外卖游戏源码......
  • 无延迟,持续畅玩 - Wi-Fi 6 助力打造游戏厅极致体验
    1、需求背景:连锁游戏厅行业竞争激烈,顾客对高品质的游戏体验有着高要求。网络是游戏厅的核心基础设施之一,需要确保游戏过程中的网络连接稳定性和顾客满意度。长时间稳定连接为保证顾客的游戏体验感,游戏厅要确保网络连接长时间稳定,避免游戏过程中的中断或延迟。抗干扰性网络......
  • UE4动作游戏实例RPG Action解析-导语
    UE实战篇(动作游戏)概述解析官方示例游戏《动作RPG》动作角色扮演游戏示例项目(简称ARPG)是一个快节奏的第三人称砍杀类游戏本篇作用是从零开始写官方示例游戏重要部分,学完这篇再去研究官方示例,会轻松很多学完会获取以下知识:1.Unreal写一个C++项目2.学习UnrealGAS系......
  • 代码随想录算法训练营第三十二天 | 122.买卖股票的最佳时机 55.跳跃游戏 45.跳跃游戏I
    122.买卖股票的最佳时机II题目链接文章讲解视频讲解思路:每次记录当天的股票价格,如果下一天比今天的价钱高那么今天就买,这样保证每一次买股票都是赚的否则记录下一天的股票,因为下一天的股票比今天的便宜,下一天买比今天买划算classSolution{public:intmaxProfit(v......
  • 【leetcode 1510 石子游戏】【记忆化搜索】
    存在和对于一切的语言importjava.util.Arrays;classSolution{publicbooleanwinnerSquareGame(intn){dp=newBoolean[n+1];dp2=newBoolean[n+1];Arrays.fill(dp,null);Arrays.fill(dp2,null);dp[0]=fa......
  • 拇指相机 比较
    预算300-600 结论:目前我最倾向于C1xx系列,重量小,因为我对画质的要求你不是非常高,对重量要求比较高。画质我可以用手机拍,肯定比这种拇指相机好。主要是手机太沉了,我需要佩戴在身上。SJCAMC100+官网说是42克客服说是45g 60x20x26mmC110C110+客服说1系列重量都差不多......
  • P2734 [USACO3.3] 游戏 A Game
    原题链接题解首先,玩家一先选,那么玩家一该选最左边还是最右边呢?我们假设玩家一有穿越时空的能力,知晓了选择左边后的最大得分和选了右边后的最大得分,那么玩家一便能确定选哪个设\(dp[l][r]\)为当区间为\(l,r\)时先手最大分数选左边的最大得分:\(sumr-dp[2][r]+a[1]\)选右......
  • 对模拟经营游戏中好感度系统和npc角色的分析
    目录1.定位2.功能性2.1.玩法系统入口2.2.任务发布3.好感度系统3.1.好感度的获取3.2.好感度系统的奖励4.节日1.定位好感度系统和npc角色并不是模拟经营游戏的重头戏和主角。以星露谷物语为例,所有鹈鹕珍村民在游戏核心的农场种植-获取资金-升级农场的循环中并不是必须的,......