3D RPG Course | Core 学习日记四:鼠标控制人物移动-CSDN博客

阿里云国内75折 回扣 微信号:monov8
阿里云国际,腾讯云国际,低至75折。AWS 93折 免费开户实名账号 代冲值 优惠多多 微信号:monov8 飞机:@monov6

前言

前边我们做好了Navgation智能导航地图烘焙并且设置好了Player的NavMeshAgent现在我们可以开始实现鼠标控制人物的移动了。除了控制人物移动以外我们还需要实现鼠标指针的变换。



实现要点

要实现鼠标控制人物移动点击地图对应的位置然后玩家移动到那个位置我们需要先了解两个关键的API这两个API对于与用户界面和鼠标交互相关的场景十分关键可以说大部分的3D场景鼠标交互都是用这两个API。


第一个API是Camera.ScreenPointToRay官方文档描述
在这里插入图片描述

这个API的作用是返回一条从摄像机射向屏幕指定坐标位置的一条射线。
函数原型public Ray ScreenPointToRay (Vector3 pos);


第二个API是Physics.Raycast官方文档描述
在这里插入图片描述
这个API的作用是判断一条射线是否有和场景中任何碰撞体发声碰撞。
函数原型public static bool Raycast(Ray ray, out RaycastHit hitInfo);


MouseManager代码

using System.ComponentModel;
using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Events;
using UnityEngine.EventSystems;

public class MouseManager : MonoBehaviour {

    public static MouseManager Instance;                        // 单例

    public Texture2D point, doorway, attack, target, arrow;     // 鼠标指针
    
    
    /** 事件广播 **/
    public event Action<Vector3> OnMouseClicked;                // 鼠标点击事件
    public event Action<GameObject> OnEnemyClicked;             // 点击敌人事件

    private RaycastHit hitInfo;                                 // 射线信息

    private void Awake() {
        if (Instance != null) {
            Destroy(gameObject);
        }
        Instance = this;
    }

    private void Update() {
        SetCursorTexture();
        MouseControl();
    }

    public void SetCursorTexture() {
        // 从摄像机发射一条射线到目标点这在进行射线命中检测时非常有用特别是与用户界面和鼠标交互相关的场景中
        Ray ray = Camera.main.ScreenPointToRay(Input.mousePosition);
        
        // 判断射线是否与任何碰撞体发生碰撞并且通过out参数返回碰撞信息然后我们通过碰撞体的tag来切换对应的鼠标贴图
        if (Physics.Raycast(ray, out hitInfo)) {
            // 切换鼠标贴图
            switch (hitInfo.collider.gameObject.tag) {
                case "Ground":
                    Cursor.SetCursor(target, new Vector2(16, 16), CursorMode.Auto);
                    break;
                case "Enemy":
                    Cursor.SetCursor(attack, new Vector2(16, 16), CursorMode.Auto);
                    break;
            }
        }
    }

    public void MouseControl() {
        // 如果鼠标左键点下并且射线有和场景种的物体发生碰撞则根据碰撞的物体执行对应的操作
        if (Input.GetMouseButton(0) && hitInfo.collider != null) {
            // 如果射线碰撞的物体tag是"Ground"则执行OnMouseClicked绑定的事件
            if (hitInfo.collider.gameObject.CompareTag("Ground")) {
                OnMouseClicked?.Invoke(hitInfo.point);
            }
            // 如果射线碰撞的物体tag是"Enemy"则执行OnEnemyClicked绑定的事件
            if (hitInfo.collider.gameObject.CompareTag("Enemy")) {
                OnEnemyClicked?.Invoke(hitInfo.collider.gameObject);
            }
        }
    }

}

总结实现鼠标与场景交互功能我们一般需要进行两个步骤第一步先使用Camera.ScreenPointToRay方法获得从摄像机发射到对应屏幕坐标位置的射线第二步使用Physics.Raycast判断这个射线是否有和场景中的碰撞体发生碰撞并且通过out参数返回碰撞信息hitInfo如果发生了碰撞我们根据碰撞信息进行对应的操作。


PlayerController代码

using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.AI;

public class PlayerController : MonoBehaviour {

    private NavMeshAgent agent;
    private Animator anim;

    private GameObject attackTarget;
    private float attackCD;

    private void Awake() {
        agent = GetComponent<NavMeshAgent>();
        anim = GetComponent<Animator>();
    }

    private void Start() {
        // 绑定event事件
        MouseManager.Instance.OnMouseClicked += EventMoveToTarget;
        MouseManager.Instance.OnEnemyClicked += EventAttack;
    }

    private void Update() {
        SwitchAnimation();
        AttackCDTimeCounter();
    }

    /// <summary>
    ///     根据人物的移动速度切换相应的动画
    /// </summary>
    private void SwitchAnimation() {
        anim.SetFloat("Speed", agent.velocity.sqrMagnitude);
    }

    /// <summary>
    ///     走向目标位置
    /// </summary>
    /// <param name="target">目标位置</param>
    public void EventMoveToTarget(Vector3 target) {
        // 当我们点击了地图开始行走时我们要中断正在进行的攻击逻辑
        StopAllCoroutines();
        // 因为在走向敌人并且攻击的逻辑完成之后我们会把NavMeshAgent的isStopped设为true所以我们在这里要先将其重新置为false
        agent.isStopped = false;
        // 将NavMeshAgent的目标设置为target这样角色就会寻路移动到target位置
        agent.destination = target;
    }

    /// <summary>
    ///     走向攻击目标并开始攻击
    /// </summary>
    /// <param name="target">攻击目标</param>
    private void EventAttack(GameObject target) {
        if (target != null) {
            attackTarget = target;
            StartCoroutine("MoveAndAttackEnemy");
        }
    }

    /// <summary>
    ///     攻击CD计时器
    /// </summary>
    private void AttackCDTimeCounter() {
        if (attackCD > 0f) {
            attackCD -= Time.deltaTime;
        }
    }

    /// <summary>
    ///     通过协程实现移动到敌人面前并采取攻击
    /// </summary>
    public IEnumerator MoveAndAttackEnemy() {
        // 将NavMeshAgent的isStopped置为false开启移动
        agent.isStopped = false;


        // 如果玩家此时就在怪物旁边距离小于等于1那么玩家在攻击前需要先转身如果不写这一句的话当玩家离怪物距离小于1的时候则玩家不会转身就直接开始攻击
        transform.LookAt(attackTarget.transform);

        // 如果玩家没有走近怪物身边则将NavMeshAgent的destination设置为怪物的位置
        while (Vector3.Distance(attackTarget.transform.position, transform.position) > 1.0f) {
            agent.destination = attackTarget.transform.position;
            yield return null;  // 暂停协程等待下一帧继续执行
        }
        
        // 将NavMeshAgent的isStopped置为true停止移动
        agent.isStopped = true;

        // 攻击
        if (attackCD <= 0) {	// 攻击CD
            // 触发攻击动画
            anim.SetTrigger("Attack");
            // 重置攻击冷却
            attackCD = 0.5f;
        }
    }

}

代码要点
1通过MouseManager的单例访问event并给event绑定事件监听
2通过Animator动画控制器的状态值来触发动画改变
3通过改变NavMeshAgent的destination属性值来控制人物的移动
4因为destination属性值控制人物的移动是异步操作所以我们不能直接在设置destination属性之后启动攻击动画以下这种方式是错误的

agent.destination = attackTarget.transform.position;
anim.SetFloat("Speed", agent.velocity.sqrMagnitude);

我们要通过协程来实现移动到怪物身边之后启动攻击动画的逻辑。

/// <summary>
///     通过协程实现移动到敌人面前并采取攻击
/// </summary>
public IEnumerator MoveAndAttackEnemy() {
    // 将NavMeshAgent的isStopped置为false开启移动
    agent.isStopped = false;


    // 如果玩家此时就在怪物旁边距离小于等于1那么玩家在攻击前需要先转身如果不写这一句的话当玩家离怪物距离小于1的时候则玩家不会转身就直接开始攻击
    transform.LookAt(attackTarget.transform);

    // 如果玩家没有走近怪物身边则将NavMeshAgent的destination设置为怪物的位置
    while (Vector3.Distance(attackTarget.transform.position, transform.position) > 1.0f) {
        agent.destination = attackTarget.transform.position;
        yield return null;  // 暂停协程等待下一帧继续执行
    }
    
    // 将NavMeshAgent的isStopped置为true停止移动
    agent.isStopped = true;

    // 攻击
    if (attackCD <= 0) {	// 攻击CD
        // 触发攻击动画
        anim.SetTrigger("Attack");
        // 重置攻击冷却
        attackCD = 0.5f;
    }
}
阿里云国内75折 回扣 微信号:monov8
阿里云国际,腾讯云国际,低至75折。AWS 93折 免费开户实名账号 代冲值 优惠多多 微信号:monov8 飞机:@monov6