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

前言

前边我们做好了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;
    }
}

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/124863.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

Python 框架学习 Django篇 (九) 产品发布、服务部署

我们前面编写的所有代码都是在windows上面运行的&#xff0c;因为我们还处于开发阶段 当我们完成具体任务开发后&#xff0c;就需要把我们开发的网站服务发布给真正的用户 通常来说我们会选择一台公有云服务器比如阿里云ecs&#xff0c;现在的web服务通常都是基于liunx操作系统…

虹科示波器 | 汽车免拆检测 | 2017款路虎发现车行驶中发动机抖动且加速无力

一、故障现象 一辆2017款路虎发现车&#xff0c;搭载3.0L发动机&#xff0c;累计行驶里程约为3.8万km。车主反映&#xff0c;车辆在行驶过程中突然出现发动机抖动且加速无力的现象&#xff0c;于是请求拖车救援。 二、故障诊断 拖车到店后首先试车&#xff0c;发动机怠速轻微抖…

Unity地面交互效果——5、角色足迹的制作

大家好&#xff0c;我是阿赵。   之前几篇文章&#xff0c;已经介绍了地面交互的轨迹做法。包括了法线、曲面细分还有顶点偏移。Shader方面的内容已经说完了&#xff0c;不过之前都是用一个球来模拟轨迹&#xff0c;这次来介绍一下&#xff0c;怎样和角色动作结合&#xff0c…

深入浅出理解ResNet网络模型+PyTorch实现

温故而知新&#xff0c;可以为师矣&#xff01; 一、参考资料 论文&#xff1a;Identity Mappings in Deep Residual Networks 论文&#xff1a;Deep Residual Learning for Image Recognition ResNet详解PyTorch实现 PyTorch官方实现ResNet 【pytorch】ResNet18、ResNet20、…

文件夹重命名:解决文件夹名称难题,批量将中文翻译成英文

在日常生活和工作中&#xff0c;我们经常需要处理各种各样的文件和文件夹。然而&#xff0c;有时候我们会遇到文件夹名称混乱或者无法识别的问题&#xff0c;这给我们的文件管理和查找带来了很大的不便。为了解决这些问题&#xff0c;我们可以使用文件夹批量重命名技巧&#xf…

strtok函数详解:字符串【分割】的利器

目录 一&#xff0c;strtok函数简介 二&#xff0c;strtok函数的用法 三&#xff0c;strtok函数的注意事项 一&#xff0c;strtok函数简介 strtok函数可以帮助我们将一个字符串按照指定的分隔符进行分割&#xff0c;从而得到我们想要的子字符串。 &#x1f342;函数头文件&am…

前端工程化(vue脚手架安装)

用命令行安装&#xff0c;先安装NodeJs&#xff0c;配置环境变量 1.npm config set prefix "C:\ProgramData\Microsoft\Windows\Start Menu\Programs\Node.js" //引号内路径是NodeJs安装所在路径 2.npm config get prefix 查看其是否成功 3.npm install -g vue/cli 4…

MySQL数据库入门到大牛_05_排序ORDER BY与分页LIMIT

文章目录 1. 排序数据1.1 排序规则1.2 单列排序1.3 多列排序 2. 分页2.1 背景2.2 实现规则2.3 拓展 3. 第五章练习 1. 排序数据 1.1 排序规则 如果没有使用排序操作&#xff0c;默认情况下&#xff0c;查询返回的数据是按照添加数据的顺序显示。例如&#xff1a;在淘宝中可以…

【Vue】组件封装小技巧 — 利用$attrs和v-bind接收传递未定义的属性

使用介绍 在Vue.js中&#xff0c;$attrs 和v-bind可以用于组件的二次封装&#xff0c;以在封装的组件中传递父组件的属性和事件。这对于创建高度可定制的通用组件非常有用。 下面是一些示例代码&#xff1a; 假设你有一个名为MyButton的自定义按钮组件&#xff0c;它接受一些…

14.序列化和文件的输入/输出 保存对象

14.1 保存对象状态 你已经奏出完美的乐章&#xff0c;现在会想把它储存起来。你可以抓个文房四宝把它记下来&#xff0c;但也可以按下储存按钮(或按下File菜单上的Save)。然后你帮文件命名&#xff0c;并希望这个文件不会让屏幕变成蓝色的画面。 储存状态的选择有很多种&…

域名解析DNS:如何查询txt类型的解析记录

前言 略 查询txt类型的解析记录 使用 nslookup 命令查询。 示例&#xff1a; cmd> nslookup -qttxt _acme-challenge.mydomain.com 服务器: UnKnown Address: fe80::1非权威应答: _acme-challenge.mydomain.com text "_unitrust-dcv2311071423492fmnwb1w…

07 # 手写 find 方法

find 的使用 find() 方法返回数组中满足提供的测试函数的第一个元素的值。否则返回 undefined。 ele&#xff1a;表示数组中的每一个元素index&#xff1a;表示数据中元素的索引array&#xff1a;表示数组 <script>var arr [1, 3, 5, 7, 9];var result arr.find(fun…

第七讲:利用类事件改变对象的属性(上)

《VBA中类的解读及应用》教程【10165646】是我推出的第五套教程&#xff0c;目前已经是第一版修订了。这套教程定位于最高级&#xff0c;是学完初级&#xff0c;中级后的教程。 类&#xff0c;是非常抽象的&#xff0c;更具研究的价值。随着我们学习、应用VBA的深入&#xff0…

矿泉水除溴酸盐、矿泉水除溴化物的技术

我们常饮用的各品牌的矿泉水&#xff0c;实际在生产过程当中也涉及到了相当复杂的处理工艺的&#xff0c;今天为大家分享的是关于矿泉水中溴酸盐、溴化物的知识点&#xff0c;以及矿泉水中为什么要除溴酸盐&#xff1f;原理是什么&#xff0c;那么又是什么样的技术能真正从根本…

基于情感分析+聚类分析+LDA主题分析对服装产品类的消费者评论分析(文末送书)

&#x1f935;‍♂️ 个人主页&#xff1a;艾派森的个人主页 ✍&#x1f3fb;作者简介&#xff1a;Python学习者 &#x1f40b; 希望大家多多支持&#xff0c;我们一起进步&#xff01;&#x1f604; 如果文章对你有帮助的话&#xff0c; 欢迎评论 &#x1f4ac;点赞&#x1f4…

华为防火墙基本原理工作方法总结

防火墙只会对tcp首包syn建立会话表&#xff0c;其它丢掉&#xff0c;如synack&#xff0c;ack udp直接建立会话表 icmp只对首包请求包建立会话表&#xff0c;其它包&#xff0c;如应答的不会建立直接丢掉 防火墙状态查看&#xff1a; rule name trust_untrust source-zone tru…

Qlik Sense : Fetching data with Qlik Web Connectors

目录 Connecting to data sources Opening a connector Connecting to a data source Authenticating the connector Defining table parameters Using standard mode or legacy mode Standard mode Connector overview Using multi-line input parameters to fetch da…

解析虚拟文件系统的调用

Linux 可以支持多达数十种不同的文件系统。它们的实现各不相同&#xff0c;因此 Linux 内核向用户空间提供了虚拟文件系统这个统一的接口&#xff0c;来对文件系统进行操作。它提供了常见的文件系统对象模型&#xff0c;例如 inode、directory entry、mount 等&#xff0c;以及…

【Linux】 reboot 命令使用

reboot 命令用于用来重新启动计算机。 语法 reboot [参数] 命令选项及作用 执行令 man --reboot 执行命令结果 参数 -n : 在重开机前不做将记忆体资料写回硬盘的动作-w : 并不会真的重开机&#xff0c;只是把记录写到 /var/log/wtmp 档案里-d : 不把记录写到 /var/log…