Unityサードパーソンシューティングゲームのパースペクティブコントロールと武器の照準

===================ソースプロジェクトのリンクを更新します:
リンク:https://pan.baidu.com/s/15bxH-MPregp2ZIN92fK7XA抽出コード:e7bp
==== ===============(この記事と比較して、変更があります)

エフェクトビデオのデモンストレーション:
https//www.bilibili.com/video/av88249417
完全なコードは下部にあります

私は最近Unityを練習し、サードパーソンシューティングゲームのデモを作りたいと思いました。まず、武器の照準効果を作成します。つまり、銃をターゲットに向けます。

長い間探していたのですが、どうやって実現するのかわからなかったのですが、後でIKの概念を知りました。IKは逆ダイナミクスです。私の理解によれば、骨の下層が上層を動かし、これは、FK(フォワードダイナミクス)の反対です。たとえば、オブジェクトが全身を前傾させたときに手を伸ばして触れるなどです。したがって、武器の照準はIKを介して実行でき、手と武器をターゲットに照準を合わせ、体を回転させることができます。これは非常に自然なことです。

図に示すように、
ここに画像の説明を挿入
Unityの組み込みIKは実際には使いにくく、この関数でも実装が難しいため、AimIKと呼ばれるコンポーネントで解決できるプラグイン:FinalIKを使用しました。Final IK1.6のリンクを
添付してください:リンク:https://pan.baidu.com/s/1zY4bluDzi8xSSWPtJBLGWQ抽出コード:eovcチュートリアルはこのブログを参照してくださいhttps://blog.csdn.net/weixin_38239050/article/details/ 101831392 Final IKの使用は、この記事の焦点では​​ありません。




コードで照準を設定するアイデアは次のとおりです:カメラの位置からカメラの前方方向に一定の距離の光線を送信します。光線がオブジェクトに当たった場合は、照準ターゲットとして設定します。それ以外の場合は、照準を合わせます。光線の終わり。レイは、プレイヤー自身の衝突ボディからシールドする必要がある場合があることに注意してください
AimIKのターゲット位置を設定するコードは次のとおりです。

aimIK.solver.target.position = targetPos;

たとえば、光線は地面に当たります。
ここに画像の説明を挿入

-------------------------------------------

次に、パースペクティブコントロールがあります。Unityには対応するプラグインがありますが、直接使用するのは退屈です。比較的簡単だと思いましたが、数日後(多すぎる)
、実装される機能が次のようになるとは思っていませんでした
。1。カメラがプレーヤーをスムーズに追う
2.カメラがプレーヤーの周りを回転し、角度が制限されてい
ます3.見上げると、角度が大きくなると引っ張られますカメラを閉じ、見下ろすと角度が大きくなるとカメラをズームアウトします
4.照準を合わせるとカメラをズームインし、照準を停止するとデフォルトの位置に戻ります
5 。壁にぶつかったときにズームインしてカメラをブロックします

以下が順番に紹介され、完全なコードは下部にあります

1.スムーズなカメラフォロー

カメラとキャラクターの位置オフセットベクトルを記録します

playerOffset = player.position - transform.position;

カメラが回転したら、playerOffsetを更新する必要があり、上記のコードが再度計算されます。

playerOffsetを更新した後、次のフレームで補間を使用して、カメラをplayer.position-playerOffsetの位置に移動する必要があります。

transform.position = Vector3.Lerp(transform.position, player.position - playerOffset, moveSpeed * Time.deltaTime);

2.カメラがプレーヤーの周りを回転し、角度を制限します

主にtransform.RotateAround関数を使用して、カメラをキャラクターの周りで回転させます。キャラクターは動くことができ、補間によってカメラの位置が変わるため、キャラクターが動くと、playerOffsetベクトルの長さと回転角が変わります。キャラクターが遠くに行くほど、偏差は大きくなりますが、そうではありません。要件を満たします。

私の解決策は、カメラがプレーヤーの周りを回転し、playerOffsetベクトルが同じ角度で回転したときに、playerOffsetを更新することです。playerOffsetの長さは回転後も変化しないため、player.position-playerOffsetは常にカメラの最終位置になります。

次の図に示すように、
ここに画像の説明を挿入
ここに画像の説明を挿入
RotateAroundの使用法は次のとおりです。

RotateAround(Vector3 point, Vector3 axis, float angle);

点:周りにある点;
軸:周りにある軸(x、y、zなど)
天使:回転角

したがって、カメラはプレーヤーの周りで水平方向と垂直方向に次のように回転します:(回転軸が異なることに注意してください

transform.RotateAround(player.position, Vector3.up, axisX);
transform.RotateAround(player.position, transform.right, -axisY);

ここで、axisXとaxisYは、対応するマウスのオフセットxTime.deltatimex回転速度です。

float axisX = Input.GetAxis("Mouse X") * rotateSpeed * Time.deltaTime;
float axisY = Input.GetAxis("Mouse Y") * rotateSpeed * Time.deltaTime;

playerOffsetベクトルをポイントを中心に回転させるには、最初に水平および垂直の回転クォータニオンを取得する必要があります:(回転軸にも注意してください

Quaternion rotX = Quaternion.AngleAxis(axisX, Vector3.up);
Quaternion rotY = Quaternion.AngleAxis(-axisY, transform.right);

次に、これら2つの4つの要素にベクトルを乗算して、長さを変更せずにベクトルを回転させます(ここでの乗算は可換ではないことに注意してください。順序や省略形は変更しないでください)。

playerOffset = rotX * rotY * playerOffset;

ここでベクトル回転を理解できない場合は、記事を参照できます:https
//gameinstitute.qq.com/community/detail/127450

角度を制限するには、最初にそれを回転させ、次に回転後に垂直方向のオイラー角を取得する必要があります。

float x = (transform.rotation).eulerAngles.x;

ただし、オイラー角の範囲は0〜360度周期であり、判断に役立ちません。範囲を-180度〜180度に変換する必要があります。上向きは負、下向きは正です。次に、指定された範囲内にあるかどうかを判断します。範囲を超えている場合は、カメラの垂直回転を復元し、playerOffsetを水平方向にのみ回転させます。そうでない場合は、playerOffsetを水平方向と垂直方向に回転させます。

//欧拉角范围为0~360,这里要转为-180~180方便判断
if (x > 180) x -= 360;

if (x < minAngle || x > maxAngle)//超出角度
{
	//还原位置和旋转
	transform.position = posPre;
	transform.rotation = rotPre;

	//更新offset向量,offset与本物体同步旋转
	//我们需要通过这offset去计算本物体(包括摄像机)应该平滑移向的位置
	//如果仅仅使用RotateAround函数,当人物在移动时会出现误差
	playerOffset = rotX*playerOffset;   
}
else//垂直视角符合范围的情况
{
	//更新offset向量,offset与本物体同步旋转
	playerOffset = rotX * rotY * playerOffset;
}

最高の画角:(minAngleに従って、必要に応じて設定し、-40に設定します)
ここに画像の説明を挿入
最小の画角:( maxAngleに従って、必要に応じて設定し、50に設定します)
ここに画像の説明を挿入

3.見上げると、角度が大きくなるとカメラがズームインし、見下ろすと、角度が大きくなるとカメラがズームアウトします。

カメラとキャラクターの距離は必要に応じて可変です。たとえば、照準を合わせるときにカメラをキャラクターに近づける必要がありますが、カメラの位置を直接変更すると、プレーヤーをフォローする機能が失われ、解放されます。視点。

プレーヤーをスムーズにフォローすることなく、必要に応じてカメラをシフトできるようにするために、私の方法は、上記の制御コードを空のオブジェクトに掛け、カメラを空のオブジェクトの子として使用して、空のオブジェクトがプレーヤーをフォローするようにすることです。 、カメラは同じ変位を行います。カメラの位置をオフセットするには、親オブジェクトに対する相対位置であるlocalPositionを変更するだけで済みます。

図に示すように、TPSCameraParentは空のオブジェクトです。
ここに画像の説明を挿入
カメラオフセットは、TPSCameraのlocalPosition.zを変更して、TPSCameraをTPSCameraParentに対して前後に移動させることです。合計オフセットを定義します。

float localOffset = 0;

このオフセットには3つの影響要因があります:
1。垂直視野角
2.照準を
合わせるかどうか3.オクルージョンがあるかどうか

垂直方向の視野角の影響を見てみましょう。

上記で、垂直回転オイラー角フロートxを取得しました。指定した最大角度に対するxの比率に従って、カメラの前後のオフセットを設定できます。

//更据角度设置摄像机位置偏移
            if (x < 0)//往上角度为负
            {
                //往上看时距离拉近
                localOffsetAngle = (x / minAngle) * localOffsetAngleUp;
            }
            else
            {
                //往下看时距离拉远
                localOffsetAngle = -(x / maxAngle) * localOffsetAngleDown;
            }

その中で、localOffsetAngleは角度に従って計算されたオフセットであり、localOffsetAngleUpとlocalOffsetAngleDownは、それぞれ上向きと下向きのときのこのオフセットの最大値です。x = 0の場合、つまりカメラが真正面を向いている場合、offset = 0です。

カメラを前後に移動するにはどうすればよいですか?合計オフセットにそれを追加させます。

localOffset+=localOffsetAngleMax;

最後に、カメラをオフセット位置にスムーズに移動させます。

Vector3 offsetPos = new Vector3(0, 0, localOffset);//这是相机应该移向的位置
//使相机平滑移动到这个位置
cam.transform.localPosition = Vector3.Lerp(cam.transform.localPosition, offsetPos, localOffsetSpeed * Time.deltaTime);

カメラがプランをズームアウトする
ここに画像の説明を挿入
とき:見上げるときにカメラを絞り込むには:
ここに画像の説明を挿入

4.照準を合わせるときはカメラを拡大し、照準を停止するとデフォルトの位置に戻ります。

マウスの右ボタンを押したままにすると照準を合わせ、離すと照準を停止するように規定されています。

オフセットlocalOffsetAimとブール値isAimingを定義します

public float localOffsetAim = 2;//根据是否瞄准而产生的偏移量,表示瞄准时摄像机应该前进多远距离,根据需要设值
private bool isAiming = false;//是否正在瞄准

次に、フレームごとにマウスイベントを決定します。

if (Input.GetMouseButtonDown(1))//鼠标右键按下为瞄准
   {
   	isAiming = true;
   }
if (Input.GetMouseButtonUp(1))//鼠标右键松开停止瞄准
{
   	isAiming = false;
}

次に、isAimingに従って、このオフセットをlocalOffsetに追加するかどうかを決定します。

//根据是否瞄准而调整
if (isAiming)
{
	localOffset += localOffsetAim;
}

照準を合わせるときにカメラをズームインします。
ここに画像の説明を挿入
通常の距離:
ここに画像の説明を挿入

5.壁にぶつかったときにカメラを拡大します

キャラクターの後ろに壁やその他のオブジェクトがあると、視線が遮られます。
ここに画像の説明を挿入
このとき、カメラを適切な距離にズームインする必要があるため、オフセットを定義します。

float localOffsetCollider = 0;

次に、このオフセットを1フレームで徐々に増やして、オクルージョンがあるかどうかをテストします。テストの方法は、光線検出を使用して、プレーヤー以外の衝突ボディに当たるかどうかを確認することです。私の場合、プレーヤーはCapsuleColliderにぶら下がっています。

private bool CheckView(Vector3 checkPos)
    {
        //发出射线来检测碰撞
        RaycastHit hit;
        //射线终点为玩家物体的中间位置
        Vector3 endPos = player.position + player.up * player.GetComponent<CapsuleCollider>().height * 0.5f;

        Debug.DrawLine(checkPos,endPos, Color.blue);

        //从checkPos发射一条长度为起点到终点距离的射线
        if (Physics.Raycast(checkPos,endPos-checkPos,out hit,(endPos-checkPos).magnitude)){
            if (hit.transform == player)//如果射线打到玩家说明没有遮挡
                return true;
            else//如果射线打击到其他物体说明有遮挡
                return false;
        }
        return true;//如果射线没有打到任何物体也说明没有遮挡
    }

オクルージョンがあるかどうかに応じてlocalOffsetColliderを調整します

        Vector3 checkPos = transform.position + cam.transform.forward * localOffset;//这是没有调整前相机应该移向的位置
        for(localOffsetCollider=0; !CheckView(checkPos);localOffsetCollider+=0.2f)//让localOffset递增直至没有遮挡
        {
            //更新checkPos为我们想要移动到的位置,再去试探
            checkPos = transform.position + cam.transform.forward * (localOffset+localOffsetCollider);
        }

localOffsetに、テストされたlocalOffsetColliderを追加させます。

効果:
ここに画像の説明を挿入

完全なコードと使用

2つのスクリプトに分かれています:
1.TPSCamera.cs

using UnityEngine;

public class TPSCamera : MonoBehaviour
{
    public static TPSCamera _instance;//用作单例模式
    public Camera cam;//摄像机,是本物体下的子物体
    public Transform player;//玩家物体的Transform
    public Vector3 playerOffset;//本物体与玩家位置的偏移向量

    public float rotateSpeed;//控制旋转速度
    public float moveSpeed;//控制跟随的平滑度

    public float minAngle;//垂直视角的最小角度值
    public float maxAngle;//垂直视角的最大角度值

    public float localOffsetSpeed = 8;//控制相机与父物体偏移时的平滑度
    public float localOffsetAim = 2;//根据是否瞄准而产生的偏移量,表示瞄准时摄像机应该前进多远距离,根据需要设值
    private float localOffsetAngle = 0;//根据垂直视角角度而产生的偏移量
    public float localOffsetAngleUp = 1.5f;//根据向上的角度而产生的偏移量的最大值
    public float localOffsetAngleDown = 1.5f;//根据向下的角度而产生的偏移量的最大值
    private float localOffsetCollider = 0;//根据玩家与摄像机间是否有遮挡而产生的偏移量

    private bool isAiming = false;//是否正在瞄准

    private void Awake()
    {
        _instance = this;
        player = GameObject.Find("Player").transform;//根据名字找到玩家物体
        playerOffset = player.position - transform.position;//初始化playerOffset
        cam = transform.GetComponentInChildren<Camera>();//获取子物体的Camera组件
    }

    private void Update()
    {
        if (Input.GetMouseButtonDown(1))//鼠标右键按下为瞄准
        {
            isAiming = true;
        }
        if (Input.GetMouseButtonUp(1))//鼠标右键松开停止瞄准
        {
            isAiming = false;
        }
        SetPosAndRot();//设置视角旋转后的位置和朝向
        Cursor.visible = false;//隐藏鼠标
    }

    /// <summary>
    /// 上下移动鼠标时,相机围绕玩家旋转,并且限制旋转角度
    /// </summary>
    public void SetPosAndRot()
    {
        //更新本物体的position,相机会和本物体做相同的位移,使相机平滑跟随玩家
        transform.position = Vector3.Lerp(transform.position, player.position - playerOffset, moveSpeed * Time.deltaTime);
        
        //获取鼠标移动量
        float axisX = Input.GetAxis("Mouse X") * rotateSpeed * Time.deltaTime;
        float axisY = Input.GetAxis("Mouse Y") * rotateSpeed * Time.deltaTime;

        //计算水平和垂直的旋转角
        Quaternion rotX = Quaternion.AngleAxis(axisX, Vector3.up);
        Quaternion rotY = Quaternion.AngleAxis(-axisY, transform.right);

        //摄像机在水平方向绕玩家旋转
        transform.RotateAround(player.position, Vector3.up, axisX);
        
        //保存未旋转垂直视角前的position和rotation
        Vector3 posPre = transform.position;
        Quaternion rotPre = transform.rotation;

        //先垂直绕玩家旋转,注意这里旋转的轴为transform.right
        transform.RotateAround(player.position, transform.right, -axisY);

        //判断垂直角度是否符合范围
        float x = (transform.rotation).eulerAngles.x;
        //欧拉角范围为0~360,这里要转为-180~180方便判断
        if (x > 180) x -= 360;
        if (x < minAngle || x > maxAngle)//超出角度
        {
            //还原位置和旋转
            transform.position = posPre;
            transform.rotation = rotPre;

            //更新offset向量,offset与本物体同步旋转
            //我们需要通过这offset去计算本物体(包括摄像机)应该平滑移向的位置
            //如果仅仅使用RotateAround函数,当人物在移动时会出现误差
            playerOffset = rotX*playerOffset;   
        }
        else//垂直视角符合范围的情况
        {
            //更新offset向量,offset与本物体同步旋转
            playerOffset = rotX * rotY * playerOffset;

            //更据角度设置摄像机位置偏移
            if (x < 0)//往上角度为负
            {
                //往上看时距离拉近
                localOffsetAngle = (x / minAngle) * localOffsetAngleUp;
            }
            else
            {
                //往下看时距离拉远
                localOffsetAngle = -(x / maxAngle) * localOffsetAngleDown;
            }
        }

        //设置摄像机与父物体的偏移,三个影响因素
        SetLocalOffset(); 
    }

    /// <summary>
    /// 根据是否瞄准、垂直视角和是否有遮挡来调整摄像机与父物体的偏移
    /// </summary>
    public void SetLocalOffset()
    {
        float localOffset = 0;//摄像机与父物体(即本脚本所在的空物体)的偏移
        //根据垂直视角调整
        localOffset += localOffsetAngle;
        //根据是否瞄准而调整
        if (isAiming)
        {
            localOffset += localOffsetAim;
        }

        //根据是否有遮挡而调整
        Vector3 checkPos = transform.position + cam.transform.forward * localOffset;//这是没有调整前相机应该移向的位置
        for(localOffsetCollider=0; !CheckView(checkPos);localOffsetCollider+=0.2f)//让localOffset递增直至没有遮挡
        {
            //更新checkPos为我们想要移动到的位置,再去试探
            checkPos = transform.position + cam.transform.forward * (localOffset+localOffsetCollider);
        }
        localOffset += localOffsetCollider;//加上这个试探出的偏移量

        Vector3 offsetPos = new Vector3(0, 0, localOffset);//这是调整后相机应该移向的位置
        //使相机平滑移动到这个位置
        cam.transform.localPosition = Vector3.Lerp(cam.transform.localPosition, offsetPos, localOffsetSpeed * Time.deltaTime);
    }

    /// <summary>
    /// 检查玩家与摄像机之间是否有碰撞体遮挡
    /// </summary>
    /// <param name="checkPos">假设相机的位置</param>
    /// <returns></returns>
    private bool CheckView(Vector3 checkPos)
    {
        //发出射线来检测碰撞
        RaycastHit hit;
        //射线终点为玩家物体的中间位置
        Vector3 endPos = player.position + player.up * player.GetComponent<CapsuleCollider>().height * 0.5f;

        Debug.DrawLine(checkPos,endPos, Color.blue);

        //从checkPos发射一条长度为起点到终点距离的射线
        if (Physics.Raycast(checkPos,endPos-checkPos,out hit,(endPos-checkPos).magnitude)){
            if (hit.transform == player)//如果射线打到玩家说明没有遮挡
                return true;
            else//如果射线打击到其他物体说明有遮挡
                return false;
        }
        return true;//如果射线没有打到任何物体也说明没有遮挡
    }

}

このスクリプトは、カメラの親であるTPSCameraParentにハングアップし
ます。必要に応じて、関連するパラメーターを設定します。参考までに、次のように設定します。
ここに画像の説明を挿入

2.ShootControl.cs

using RootMotion.FinalIK;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class ShootControl : MonoBehaviour
{
    public TPSCamera tpsCamera;//控制相机视角的脚本
    public Camera cam;//摄像机
    public float range;//射线距离
    private float offsetDis;//摄像机与玩家的距离
    public Vector3 targetPos;//目标位置
    private AimIK aimIK;//对应的final ik组件
    public float speed;//移动速度
    public float rotateSpeed;//旋转速度

    private void Awake()
    {
        tpsCamera = GameObject.Find("TPSCameraParent").GetComponent<TPSCamera>();//获取相机的父物体
        cam = tpsCamera.GetComponentInChildren<Camera>();//相机为tpsCamera的子物体
        aimIK = GetComponent<AimIK>();//获取AimIk组件
        offsetDis = Vector3.Distance(transform.position, cam.transform.position);//初始化offsetDis
    }

    private void Update()
    {
        SetTarget();//设置瞄准的目标位置
        OnKeyEvent();//处理按键响应
    }

    /// <summary>
    /// 设置瞄准的目标
    /// 从摄像机位置向摄像机正方向发射射线(即从屏幕视口中心发出)
    /// 射线的长度=range,可以近似设为子弹的射程
    /// 若射线打到非玩家的物体则将该物体设为目标
    /// 若射线没有打到物体则将目标设为射线的终点
    /// </summary>
    public void SetTarget()
    {
        //从摄像机位置向摄像机正方向发射射线(即从屏幕视口中心发出)
        RaycastHit hit;
        if (Physics.Raycast(cam.transform.position, cam.transform.forward, out hit, range))
        {
            //若射线打到非玩家的物体则将该物体设为目标
            //我这里并没有进行判断该物体是否是玩家,因为我设置的玩家位于屏幕的偏左下位置,射线不会穿过玩家
            //需要的话,可以给玩家设定layer,然后让射线屏蔽这个layer
            targetPos = hit.point;
        }
        else
        {
            //若射线没有打到物体则将目标设为射线的终点
            targetPos = cam.transform.position + (cam.transform.forward * range);
        }
        //画出射线便于观察(不会显示在game中)
        Debug.DrawRay(cam.transform.position, cam.transform.forward * range, Color.green);

        //按下鼠标右键时开启AimIK,进入瞄准状态
        if (Input.GetMouseButtonDown(1))
        {
            aimIK.enabled = true;
        }

        //按住鼠标右键时为瞄准状态,人物身体始终朝向摄像机的前方
        if (Input.GetMouseButton(1))
        {
            RotateBodyToTarget();
        }
        else//松开右键时为自由视角状态,关闭AimIK,不进行瞄准
        {
            //注意这里使用Disale(),不要直接enabled=false,原因不清楚
            aimIK.Disable();
        }
    }

    /// <summary>
    /// 旋转玩家身体,使玩家朝向摄像机的水平前方
    /// </summary>
    private void RotateBodyToTarget()
    {
        Vector3 rotEulerAngles = cam.transform.eulerAngles;//获取摄像机的旋转的欧拉角
        rotEulerAngles.x = 0;//垂直方向不进行旋转
        //使用插值让玩家平滑转向
        transform.rotation = Quaternion.Lerp(transform.rotation, Quaternion.Euler(rotEulerAngles), rotateSpeed * Time.deltaTime);
        SetAimIKTarget();//更新AimIK的target的位置
    }

    /// <summary>
    /// 更新AimIK的target的位置
    /// </summary>
    private void SetAimIKTarget()
    {
        //将AimIK的target位置设为之前射线检测到的位置
        aimIK.solver.target.position = targetPos;
    }

    /// <summary>
    /// 管理键盘的响应,这里只用来控制玩家移动,不重要,可以忽略
    /// </summary>
    private void OnKeyEvent()
    {
        //Horizontal和Vertical的默认按键为ad←→和ws↑↓
        float h = Input.GetAxis("Horizontal");
        float v = Input.GetAxis("Vertical");
        if (h != 0 || v != 0)
        {
            Vector3 moveDir = new Vector3(h, 0, v);
            transform.Translate(moveDir * speed * Time.deltaTime);
            RotateBodyToTarget();
        }
    }
}

:プレーヤーにぶら下がっている、「Player」という名前のプレーヤー、
ここに画像の説明を挿入
マウントする必要のある参照パラメーターコンポーネントプレーヤー:
ここに画像の説明を挿入
AimIKパラメーター:( AimIKの使用を確認することをお勧めします。ターゲット機能を使用しない場合、ShootControlおよびAimIKコンポーネントは使用できません)
ここに画像の説明を挿入
どのターゲットが任意の空のオブジェクトであるかをドラッグします。FirePosは武器の空のオブジェクトであり、銃口の位置にあります。
図に示すように、
ここに画像の説明を挿入
最初にフォローしたい人物をカメラの視線の左下隅に配置します。そうでない場合は、ShootControl.csでの光線検出からプレーヤーのレイヤーをシールドする必要があります。
ここに画像の説明を挿入
十字線の描画については、次のブログも参照してくださいhttps
//blog.csdn.net/xboxbin/article/details/88069638

おすすめ

転載: blog.csdn.net/sun124608666/article/details/111872182