Controle de perspectiva do jogo de tiro em terceira pessoa Unity e mira de armas

=================== Atualize o link do projeto de origem:
link: https://pan.baidu.com/s/15bxH-MPregp2ZIN92fK7XA código de extração: e7bp
==== =============== (Em comparação com este artigo, existem modificações)

Demonstração do vídeo do efeito:
https://www.bilibili.com/video/av88249417 O
código completo está na parte inferior

Eu recentemente pratiquei Unity e queria fazer uma demonstração de um jogo de tiro em terceira pessoa. Primeiro, vamos fazer um efeito de mira da arma, ou seja, deixar a arma apontada para o alvo.

Depois de procurar por muito tempo, não sabia como percebê-lo. Mais tarde, descobri o conceito de IK. IK é a dinâmica reversa. Pelo que entendi, a camada inferior do osso faz com que a camada superior se mova, que é o oposto de FK (dinâmica para frente), como estender a mão para tocar quando o objeto leva todo o corpo a se inclinar para frente. Então a mira da arma pode ser feita através do IK, deixe a mão e a arma mirar no alvo, e faça o corpo girar, o que é muito natural.

Conforme mostrado na figura:
Insira a descrição da imagem aqui
O IK integrado do Unity não é realmente fácil de usar, mesmo esta função é difícil de implementar, então usei um plug-in: Final IK, que pode ser resolvido com um componente chamado AimIK.
Anexe o link do IK1.6 final :
link: https://pan.baidu.com/s/1zY4bluDzi8xSSWPtJBLGWQ
código de extração:
tutorial eovc consulte este blog:
https://blog.csdn.net/weixin_38239050/article/details/
O uso de 101831392 Final IK não é o foco deste artigo.

A ideia de definir a mira com código é: enviar uma certa distância dos raios da posição da câmera para a direção para frente da câmera, se o raio atingir um objeto, deixe-o ser definido como o alvo da mira, caso contrário, mire o fim do raio. Observe que o raio pode precisar ser protegido do corpo de colisão do próprio jogador .
O código para definir a posição alvo do AimIK é:

aimIK.solver.target.position = targetPos;

Por exemplo, o raio atinge o solo:
Insira a descrição da imagem aqui

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

Depois, há o controle de perspectiva.Embora o Unity tenha plug-ins correspondentes, é chato usá-lo diretamente. Achei que fosse relativamente simples, mas não esperava que depois de alguns dias (demais)
, as funções implementadas fossem as seguintes:
1. A câmera segue suavemente o jogador
2. A câmera gira em torno do jogador e o ângulo é limitado
3. Ao olhar para cima, ele puxa conforme o ângulo aumenta Feche a câmera, diminua o zoom da câmera conforme o ângulo aumenta quando você olha para baixo
4. Aumente o zoom da câmera ao mirar e retorne à posição padrão quando parar de mirar
5 . Aumente o zoom quando bater na parede para bloquear a câmera

A seguir são apresentados, por sua vez, o código completo está na parte inferior

1. Acompanhamento suave da câmera

Grave o vetor de deslocamento de posição da câmera e do personagem

playerOffset = player.position - transform.position;

Quando a câmera gira, o playerOffset precisa ser atualizado e o código acima é calculado novamente.

Depois de atualizar o playerOffset, você precisa usar a interpolação no próximo quadro para mover a câmera para a posição de player.position-playerOffset

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

2. A câmera gira em torno do jogador e limita o ângulo

Use principalmente a função transform.RotateAround para fazer a câmera girar ao redor do personagem. Como o personagem pode se mover e a posição da câmera é alterada por interpolação, quando o personagem se move, o comprimento e o ângulo de rotação do vetor playerOffset mudam. Quanto mais longe o personagem vai, maior será o desvio, o que não cumprem as exigências.

Minha solução é atualizar o playerOffset quando a câmera gira em torno do player e o vetor playerOffset gira no mesmo ângulo. O comprimento de playerOffset não muda após a rotação, o que garante que player.position-playerOffset seja sempre a posição final da câmera.

Conforme mostrado na figura abaixo:
Insira a descrição da imagem aqui
Insira a descrição da imagem aqui
O uso de RotateAround é:

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

ponto: o ponto ao redor;
eixo: o eixo ao redor, como x, y, z
anjo: o ângulo de rotação

Portanto, a câmera gira horizontal e verticalmente em torno do jogador como: ( observe que o eixo de rotação é diferente )

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

Onde axisX e axisY são a velocidade de rotação xTime.deltatimex de deslocamento do mouse correspondente:

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

Para girar o vetor playerOffset em torno de um ponto, você deve primeiro obter os quatérnios de rotação horizontal e vertical: ( também preste atenção ao eixo de rotação )

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

Em seguida, multiplique esses dois quatro elementos pelo vetor para girar o vetor sem alterar o comprimento: ( observe que a multiplicação aqui não é comutativa, não altere a ordem ou a abreviação )

playerOffset = rotX * rotY * playerOffset;

Você pode consultar o artigo se não entender a rotação do vetor aqui:
https://gameinstitute.qq.com/community/detail/127450

Para limitar o ângulo, devemos primeiro girá-lo e, em seguida, obter o ângulo de Euler na direção vertical após a rotação:

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

Mas o intervalo do ângulo de Euler é o ciclo de 0-360 graus, o que não conduz a julgamento. É necessário converter o intervalo para -180 graus -180 graus, para cima é negativo, para baixo é positivo. Em seguida, determine se ele está dentro do nosso intervalo determinado. Se exceder o intervalo, restaure a rotação vertical da câmera e deixe o playerOffset girar apenas horizontalmente; caso contrário, deixe o playerOffset girar horizontalmente e verticalmente.

//欧拉角范围为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;
}

Ângulo de visão mais alto: (de acordo com minAngle, defina conforme necessário, eu o defino em -40)
Insira a descrição da imagem aqui
Ângulo de visão mais baixo: (de acordo com maxAngle, defina conforme necessário, eu o defino em 50)
Insira a descrição da imagem aqui

3. Ao olhar para cima, a câmera será ampliada conforme o ângulo aumenta, e ao olhar para baixo, a câmera será ampliada conforme o ângulo aumenta

A distância entre a câmera e o personagem é variável quando necessário. Por exemplo, a câmera deve ser ampliada para mais perto do personagem ao mirar, mas se você alterar diretamente a posição da câmera, isso destruirá a função de seguir o jogador e liberar perspectiva.

A fim de permitir que a câmera mude conforme necessário sem afetar suavemente o acompanhamento do jogador, meu método é pendurar o código de controle acima em um objeto vazio e usar a câmera como filha do objeto vazio para que o objeto vazio siga o jogador. , A câmera fará o mesmo deslocamento. Para fazer a posição do deslocamento da câmera, só precisa alterar sua posição local, que é sua posição relativa ao objeto pai.

Conforme mostrado na figura: TPSCameraParent é um objeto vazio. O
Insira a descrição da imagem aqui
deslocamento da câmera é para alterar o localPosition.z de TPSCamera para fazer TPSCamera se mover para frente e para trás em relação a TPSCameraParent. Defina um deslocamento total:

float localOffset = 0;

Existem três fatores que influenciam este deslocamento:
1. Ângulo de visão vertical
2. Se mira
3. Se há oclusão

Vejamos o efeito do ângulo de visão vertical:

Acima, obtivemos a rotação vertical do ângulo de Euler flutuante x, podemos definir o deslocamento frontal e posterior da câmera de acordo com a proporção de x para o ângulo máximo que demos:

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

Entre eles, localOffsetAngle é o deslocamento calculado de acordo com o ângulo, e localOffsetAngleUp e localOffsetAngleDown são o valor máximo desse deslocamento ao olhar para cima e para baixo, respectivamente. Quando x = 0, ou seja, quando a câmera está olhando para frente, o deslocamento = 0.

Como a câmera pode ser movida para frente e para trás? Deixe a compensação total somar:

localOffset+=localOffsetAngleMax;

No final, faça a câmera se mover suavemente para a posição de deslocamento:

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

Quando a câmera diminui o zoom de um plano:
Insira a descrição da imagem aqui
para estreitar a câmera ao olhar para cima:
Insira a descrição da imagem aqui

4. Aumente o zoom da câmera ao mirar e retorne à posição padrão quando parar de mirar

É estipulado mirar quando o botão direito do mouse é pressionado e parar de mirar quando solto.

Defina um deslocamento localOffsetAim e um valor bool isAiming

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

Em seguida, determine o evento do mouse a cada quadro:

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

Então, de acordo com isAiming para decidir se deseja adicionar esse deslocamento a localOffset:

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

Aumente o zoom da câmera ao mirar:
Insira a descrição da imagem aqui
Distância normal:
Insira a descrição da imagem aqui

5. Aumente o zoom da câmera quando você bater na parede

Quando houver uma parede ou outros objetos próximos atrás do personagem, ele bloqueará a linha de visão:
Insira a descrição da imagem aqui
neste momento, a câmera precisa ser ampliada para uma distância adequada, então defina um deslocamento:

float localOffsetCollider = 0;

Em seguida, aumente gradualmente esse deslocamento em um quadro para testar se há oclusão. O método de teste é usar a detecção de raio para ver se ele pode atingir o corpo de colisão além do player. No meu caso, o player está pendurado no 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;//如果射线没有打到任何物体也说明没有遮挡
    }

Ajuste localOffsetCollider de acordo com a oclusão

        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);
        }

Deixe o localOffset adicionar o localOffsetCollider que foi testado.

efeito:
Insira a descrição da imagem aqui

Código completo e uso

Dividido em dois scripts:
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;//如果射线没有打到任何物体也说明没有遮挡
    }

}

Este script está pendurado em TPSCameraParent, que é o pai da câmera. Os
parâmetros relevantes são definidos conforme necessário. O meu é o seguinte para referência:
Insira a descrição da imagem aqui

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();
        }
    }
}

: Pendurado no jogador, o jogador chamado "Player", um parâmetro de referência
Insira a descrição da imagem aqui
para os jogadores que precisam montar:
Insira a descrição da imagem aqui
Parâmetros AimIK :( recomendar uma olhada no uso do AimIK, se você não usar a função de segmentação, então você não pode ShootControl e o componente AimIK)
Insira a descrição da imagem aqui
cujo alvo é um objeto vazio arbitrário, arraste-o. FirePos é o objeto vazio da arma, localizado na posição do cano.
Conforme mostrado na figura:
Insira a descrição da imagem aqui
inicialmente coloque a pessoa que você deseja seguir no canto inferior esquerdo da linha de visão da câmera, caso contrário, você precisa proteger a camada do jogador da detecção de raios em ShootControl.cs.
Insira a descrição da imagem aqui
Consulte também este blog para desenhos em cruz:
https://blog.csdn.net/xboxbin/article/details/88069638

Acho que você gosta

Origin blog.csdn.net/sun124608666/article/details/111872182
Recomendado
Clasificación