Unity--射线检测--RayCast

news2024/11/13 9:55:03

Unity–射线检测–RayCast

1.射线检测的含义

射线检测,根据名称而言,使用一条射线来检测是击中了某个物体/多个物体

射线检测的包含两个部分: 射线检测

2.射线检测可以用在哪些地方

  1. 射击游戏
    • 玩家的瞄准和射击:检测玩家视线是否与敌人或其他目标相交。
    • 子弹轨迹和效果:模拟子弹的飞行路径和击中效果。
  2. 交互和UI
    • 鼠标点击检测:检测玩家的鼠标点击是否与游戏对象或UI元素相交。
    • 触摸屏交互:在移动设备上检测玩家的触摸是否与特定的游戏元素相交。
  3. 角色控制器和AI
    • 视野检测:NPC或敌人在一定范围内检测玩家或其他角色。
    • 碰撞避免:AI角色在移动时使用射线检测来避免碰撞。
  4. 虚拟现实(VR)和增强现实(AR)
    • 眼睛或手部追踪:在VR中检测玩家的视线或手部位置。
    • 对象交互:在AR中检测玩家是否与虚拟对象相交。

3.Unity中的射线Ray

在日常生活中的场景射线是很多地方都可以见到的, 比如手电筒,ppt激光翻页笔,庆余年中的镭射眼

射线由一个起点,一个方向和一个距离构成, 即: origin , directiondistance

在物理上射线的距离是无限远的, 因此物理上的射线只有一个起点和一个方向. 在游戏中,射线的最大距离也是被系统限制的, 一般我们是自定义距离,例如1000.0米. 以下是关于射线Ray的说明.

在Unity中,射线Ray是一个结构体,结构体积的基本成员包括origin, directionGetPoint. 也就是起点,方向和沿射线一定距离的点的位置. 以下是Unity中有关Ray的代码

using System;

namespace UnityEngine
{
    public struct Ray : IFormattable
    {        
        public Ray(Vector3 origin, Vector3 direction);
        public Vector3 origin { get; set; } // 起点(向量)
        public Vector3 direction { get; set; }// 方向(向量)
        public Vector3 GetPoint(float distance);// 沿着射线一定距离的点(向量)
        public override string ToString();
        public string ToString(string format);
        public string ToString(string format, IFormatProvider formatProvider);
    }
}

3.1 构建射线

根据上面的代码,可以看出,可以直接Ray的构造函数来构建一条射线,例如:

Ray ray = new Ray(Vector3.zero, Vector3.forward); // 射线的起点 + 射线的方向

根据上面的代码,我们可以看出,射线的起点是Unity中世界坐标的原点(0,0,0), 射线的方向是世界坐标的向前方向.

3.2 如何显示射线

现实中我们的上激光笔,手电筒是可以看见的,而Unity中的射线是看不见,因此,如果要将射线显示出来,我们可以使用的方法有

  • 使用Debug.DrawRay()进行显示射线
  • 使用LineRenderer组件将射线绘制出来

4.Unity中的射线检测

只是将射线构建出来或者显示出来并没有什么意义, 这就相当于手上拿着一个工具,不用工具来干活一个道理.

如何使用射线来进行物体检测.

仔细思考一下: 日常中我们的激光笔或者手电筒发出激光后可以在墙上显示出来光点或者照亮某个地方.这说明墙面是可以被交互的的,换句话说就是射线碰撞到了物体/检测到了物体. 在Unity中使用以下的API来判断是否检测到了物体.

4.1Raycat 函数

// 基础版API
public static bool Raycast(Ray ray);

public static bool Raycast(Vector3 origin, Vector3 direction, float maxDistance, int layerMask);
public static bool Raycast(Ray ray, out RaycastHit hitInfo);
public static bool Raycast(Ray ray, out RaycastHit hitInfo, float maxDistance);
                           
// 常用版API
public static bool Raycast(Ray ray, out RaycastHit hitInfo, float maxDistance, int layerMask);
                           
public static bool Raycast(Ray ray, float maxDistance);

要使用射线检测, 需要使用Unity中的Physics库, 里面包含都是和物理相关的静态函数

看到上面的这么的重载函数,我们往往不知道使用哪一个. 其实, 只需要基础基本的即可, 参数多的其实就是根据需求来使用不同的重载参数.

首先看基础版本的API public static bool Raycast(Ray ray); 根据返回值我们可以获取了解到的是射线是否击中了一个物体,击中了就返回true,没有击中就返回false.

4.2HitInfo 结构体

在另外一个API中射线检测public static bool Raycast(Ray ray, out RaycastHit hitInfo);中多了一个参数hitInfo

这个hitInfo参数就是射线击中的物体的信息结构体. 该结构体比较大,指的包含的信息比较多,这和Unreal Engine中的射线检测击中物体的结果(FHitResult)是类似的. 从宏观上来讲或者从现实上来讲, 射线击中的物体, 我们可以获取物体本身的信息和击中点的信息

  • 物体的信息, 通过transfor可以获得所有信息
  • 击中点的信息

获取物体的信息很好理解: 比如物体的名称,物体的transfrom组件,该物体的Tag…

获取击中点的信息即射线击中物体的一个点(命中点)的信息:比如该点的坐标,法线(normal),法平面,该点的顶点颜色

以下是RaycastHit结构体的信息, 其中常用的已添加注释. 没有添加的注释的也有很多是常用的,比如lightmapCoord是光照贴图坐标, 用于渲染的.

namespace UnityEngine
{
    public struct RaycastHit
    {
        public Collider collider { get; }						// 碰撞器
        public int colliderInstanceID { get; }
        public Vector3 point { get; set; }						// 击中的点(命中点)
        public Vector3 normal { get; set; }						// 命中点的法线
        public Vector3 barycentricCoordinate { get; set; }		 // 重心坐标
        public float distance { get; set; }						// 命中点距离射线起点的距离
        public int triangleIndex { get; }
        public Vector2 textureCoord { get; }
        public Vector2 textureCoord2 { get; }
        public Transform transform { get; }						// Transform组件
        public Rigidbody rigidbody { get; }						// 刚体组件
        public ArticulationBody articulationBody { get; }
        public Vector2 lightmapCoord { get; }
        public Vector2 textureCoord1 { get; }
    }
}

也就是说HitInfo保存了我们击中物体的信息, 我们可以通过该信息来做更多的事情

4.3 layerMask

在Unity中,layerMask 是一个用于控制物理碰撞、光线投射、射线检测等操作的对象层选择机制。通过设置 layerMask,你可以指定哪些层应该被包括或排除在这些操作中。

继续回到常用的射线检测API public static bool Raycast(Ray ray, out RaycastHit hitInfo, float maxDistance, int layerMask);中.

在上面的API中,maxDistance这个不用多说, 就是射线的距离,系统也有一个设定最大值为Mathf.Infinity. 该APi中还有一个参数 layerMask, 也就是层级遮罩.

在Unity中区分游戏对象, 我们可以通过添加标签的方式将游戏对象区分开来, 即添加Tag. 但是Tag比较麻烦, 需要我们手动输入标签名,手敲还容易敲错. 同时, 由于是字符串, 在Unity底层计算的时候速度要慢一点. 在在Unity中即有层的概念.

在Unity中的layerMask中包含32层, 其中部分层级是系统已经使用了的,比如Player层, UI层. 还有很多层我们没有被系统使用, 我们可以添加层, 然后给游戏对象添加童工层的方式来分类.

如何添加层? 需要在任何一个物体的Insepector面板上点击Layer,然后点击AddLayer即可, 然后将需要需要修改层级的物体,手动指定层即可.

Unity_RayCast_AddLayer

手动添加了Layer后要如何使用.

public static bool Raycast(Ray ray, out RaycastHit hitInfo, float maxDistance, int layerMask);中的layereMask我们了解到它使一个int类型的整数, 但是, 我们不能直接填写数字,填写规则使用移位操作,

如何填写layerMask:

  1. 获取Layer Mask值

    • 每个层都有一个对应的整数值,从0开始。例如,默认层(Default)的值为0,UI层通常为5。
    • 要为特定层创建layerMask,可以使用 1 << LayerMask.NameToLayer("LayerName")。这将返回一个整数值,表示该层的layerMask。
  2. 组合多个层

    • 如果你想要组合多个层,可以使用位或操作符 |。例如,layerMask = LayerMask.GetMask("Layer1", "Layer2") 会创建一个layerMask,包括 “Layer1” 和 “Layer2”。
  3. 排除层

    • 要排除一个层,可以先创建一个包含所有层的mask,然后使用位异或操作符 ^ 来排除特定层。例如,layerMask = ~LayerMask.GetMask("ExcludeLayer")
  4. 检查层

    • 要检查一个对象是否在指定的layerMask中,可以使用 layerMask.value & (1 << gameObject.layer)。如果结果不为0,则表示对象在layerMask中。
    // 示例代码
    // 创建一个包含Layer1和Layer2的layerMask
    int layerMask = LayerMask.GetMask("Layer1", "Layer2");
    
    // 排除Layer3
    layerMask = ~LayerMask.GetMask("Layer3");
    
    // 使用layerMask进行射线检测
    RaycastHit hit;
    if (Physics.Raycast(ray, out hit, maxDistance, layerMask))
    {
        // 处理射线击中的对象
    }
    
    

5.射线检测代码

以下是使用不含有hitInfo和含有hitInfo参数的代码

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

public class RayCast : MonoBehaviour
{
    /
    // 构建一条射线 : 射线产生的起始点 + 射线的方向.
    private Ray ray1 = new Ray(Vector3.zero, Vector3.forward);
    // 射线距离
    private float rayDistance = 100.0f;
   
    // 击中的判定结果
    bool hitResult = false;
    // 射线击中的物体
    private RaycastHit hitInfo;

    void Start()
    {
        // 不含有hitInfo的函数
        bool result1 = Physics.Raycast(Vector3.zero + new Vector3(0,0,10), Vector3.forward, 1000.0f, 1 << LayerMask.NameToLayer("Default"), QueryTriggerInteraction.UseGlobal);
        if (result1)
        {
            Debug.Log("射线击中物体");
        }
        // 含有hitInfo的函数
		hitResult =  Physics.Raycast(ray1, out hitInfo, rayDistance, 1 << LayerMask.NameToLayer("Default"), QueryTriggerInteraction.UseGlobal);
        if (hitResult == true)
        {
            print(hitInfo.collider.name);
            print(hitInfo.transform);
            print(hitInfo.point);
        }
    }       
}

6.射线警报器

使用射线检测实现一个旋转的激光笔, 遇到物体射线长度就减少, 需要绘制出射线

需要用到的技能: 射线检测 + 旋转 + LineRender

以下是代码

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

public class RotateRay : MonoBehaviour
{
    // LinerRender组件
    private LineRenderer lineRenderer = null;

    // 构建一条射线 : 射线产生的起始点 + 射线的方向.
    private Ray ray = new Ray(Vector3.zero, Vector3.forward);
    // 射线距离
    private float rayDistance = 1000.0f;
    // 击中的判定结果
    bool hitResult = false;
    // 射线击中的物体
    private RaycastHit hitInfo;

    void Start()
    {
        // 添加线条绘制组件
        lineRenderer = this.gameObject.AddComponent<LineRenderer>();
        InitLineRenderer(lineRenderer);

        // 设置射线的起点和方向
        ray.origin = this.transform.position;
        ray.direction = this.transform.forward;
    }

    // Update is called once per frame
    void Update()
    {
        // 重新设置射线的位置
        ray.origin = this.transform.position;
        ray.direction = this.transform.forward;

        // 旋转游戏对象 -- 每秒旋转60°
        Quaternion quaternion = Quaternion.AngleAxis(60f * Time.deltaTime, this.transform.up);
        this.transform.rotation *= quaternion;        

        // 判断击中的物体
        hitResult =  Physics.Raycast(ray, out hitInfo, rayDistance, 1 << LayerMask.NameToLayer("Default"), QueryTriggerInteraction.UseGlobal);
        if (hitResult == true)
        {
            print(hitInfo.collider.name);
        }
        // 显示并更新射线
        UpdateLineRendererByRay(lineRenderer, ray, hitResult,hitInfo, rayDistance);
    }

    /// <summary>
    /// 初始化线条渲染组件
    /// </summary>
    void InitLineRenderer(LineRenderer lineRenderer)
    {
        // lineRenderer = this.gameObject.AddComponent<LineRenderer>();
        lineRenderer.positionCount = 2;
        lineRenderer.startWidth = 0.2f;
        lineRenderer.endWidth = 0.2f;
        lineRenderer.startColor = Color.red;
        lineRenderer.endColor = Color.green;
    }

    /// <summary>
    /// 击中物体的时候修改射线的长度
    /// </summary>
    /// <param name="lineRenderer">lineRenderer组件</param>
    /// <param name="ray">射线</param>
    /// <param name="hitResult">是否命中物体</param>
    /// <param name="hitInfo">命中物体信息</param>
    /// <param name="rayDistance">射线距离</param>
    void UpdateLineRendererByRay(LineRenderer lineRenderer,Ray ray, bool hitResult, RaycastHit hitInfo, float rayDistance)
    {
        if (lineRenderer == null || lineRenderer.positionCount < 2)
        {
            Debug.Log("LineRender组件不可以使用");
            return;
        }

        // 修改起点位置
        lineRenderer.SetPosition(0, ray.origin);
        // 修改终点位置
        if (hitResult == true)
        {
            lineRenderer.SetPosition(1, hitInfo.point);
        }
        else 
        {
            lineRenderer.SetPosition(1, ray.GetPoint(rayDistance));
        }
    }
}

优化后的代码

using UnityEngine;

public class RotateRay : MonoBehaviour
{
    private LineRenderer lineRenderer;
    private Ray ray;
    private float rayDistance = 1000.0f;
    private RaycastHit hitInfo;

    void Start()
    {
        lineRenderer = this.gameObject.AddComponent<LineRenderer>();
        InitLineRenderer(lineRenderer);
        ray = new Ray(Vector3.zero, Vector3.forward);
    }

    void Update()
    {
        UpdateRayPosition();
        RotateObject();
        PerformRaycast();
        UpdateLineRenderer();
    }

    void InitLineRenderer(LineRenderer lineRenderer)
    {
        lineRenderer.positionCount = 2;
        lineRenderer.startWidth = 0.2f;
        lineRenderer.endWidth = 0.2f;
        lineRenderer.startColor = Color.red;
        lineRenderer.endColor = Color.green;
    }

    void UpdateRayPosition()
    {
        ray.origin = this.transform.position;
        ray.direction = this.transform.forward;
    }

    void RotateObject()
    {
        Quaternion rotation = Quaternion.AngleAxis(60f * Time.deltaTime, this.transform.up);
        this.transform.rotation *= rotation;
    }

    void PerformRaycast()
    {
        int layerMask = 1 << LayerMask.NameToLayer("Default");
        hitInfo = new RaycastHit(); // 初始化hitInfo,避免未击中时的错误
        Physics.Raycast(ray, out hitInfo, rayDistance, layerMask, QueryTriggerInteraction.UseGlobal);
    }

    void UpdateLineRenderer()
    {
        if (lineRenderer == null || lineRenderer.positionCount < 2)
        {
            Debug.LogError("LineRenderer component is not available or not properly initialized.");
            return;
        }

        lineRenderer.SetPosition(0, ray.origin);
        lineRenderer.SetPosition(1, hitInfo.collider != null ? hitInfo.point : ray.GetPoint(rayDistance));
    }
}

效果图如下

Unity_RayCast.gif

7.鼠标点击生成一个特效

using UnityEngine;

public class  CameraRay: MonoBehaviour
{
    // 射线距离
    private float rayDistance = 1000.0f;
    // 特效Prefab--外部可以自定义特效
    public GameObject effectPrefab;

    void Update()
    {
        if (Input.GetMouseButtonDown(0))
        {
            // 从摄像机发出一条射线
            Ray ray = Camera.main.ScreenPointToRay(Input.mousePosition);
            RaycastHit hitInfo;

            // 如果射线击中了指定层级的物体
            if (Physics.Raycast(ray, out hitInfo, rayDistance, LayerMask.GetMask("Wall")))
            {
                // 生成特效 --格局法线来计算特效位置
                GameObject effectObject = Instantiate(effectPrefab, hitInfo.point, Quaternion.LookRotation(hitInfo.normal));
                // 销毁特效,参数为延迟时间
                Destroy(effectObject, EffectPrefab.GetComponent<ParticleSystem>().main.duration);
            }
        }
    }
}

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

初识c++(命名空间,缺省参数,函数重载)

一、命名空间 1、namespace的意义 在C/C中&#xff0c;变量、函数和后面要学到的类都是大量存在的&#xff0c;这些变量、函数和类的名称将都存在于全 局作用域中&#xff0c;可能会导致很多冲突。使用命名空间的目的是对标识符的名称进行本地化&#xff0c;以避免命名 冲突…

ubuntu24.04按关键字卸载不需要的apt包

使用的时候发现一个imagemagic无法正常读取文件&#xff0c;试图卸载 man apt经过尝试后&#xff0c;发现list的一个神奇关键字&#xff0c;用来显示已安装的软件包 sudo apt list --installed | grep image按image关键字过滤&#xff1a; 之后按软件名卸载即可 sudo apt pu…

数学建模论文写作文档word

目录 1. 摘要写法1.1 确定题目与方法1.2 编写开头段落1.3 填写问题一1.4 重复步骤3填写其他问题1.5 编写结尾段落1.6 编写关键词 2. 问题重述2.1 问题背景2.2 问题提出 3. 问题分析4. 问题X模型的建立与求解5. 模型的分析5.1 灵敏度分析5.2 误差分析&#xff08;主要用于预测类…

DAY2:插件学习

文章目录 插件学习ClangGoogle TestCMakeDoxygen 收获 插件学习 Clang 是什么&#xff1a;Clang 是指 LLVM 项目的编译器的前端部分&#xff0c;支持对 C 家族语言(C、C、Objective-C)的编译。Clang 的功能包括&#xff1a;词法分析、语法分析、语义分析、生成中间中间代码 L…

浅析C++引用

浅析C引用"&" ​ C中引入了一个新的语言特性——引用(&)&#xff0c;它表示某一对象的别名&#xff0c;对象与该对象的引用都是指向统一地址。那么我们就来看看关于引用的一些知识点吧&#x1f9d0; 特性 引用在定义时必须初始化一个变量可以有多个引用引…

C-10 凸包

凸包 数学定义 平面的一个子集S被称为是凸的&#xff0c;当且仅当对于任意两点A&#xff0c;B属于S&#xff0c;线段PS都完全属于S过于基础就不详细介绍了 凸包的计算 github上找到了别人的代码&#xff0c;用4种方式实现了凸包的计算&#xff0c;把他放在这里链接地址htt…

六、数据可视化—Wordcloud词云(爬虫及数据可视化)

六、数据可视化—Wordcloud词云&#xff08;爬虫及数据可视化&#xff09; 也是一个应用程序 http://amueller.github.io/word_cloud/ Wordcloud词云&#xff0c;在一些知乎&#xff0c;论坛等有这样一些东西&#xff0c;要么做封面&#xff0c;要么做讲解&#xff0c;进行分析…

Echarts 实现数据可视化

Echarts 简介 Echarts 是一个开源的、免费的、成熟的、商业级图表可视化框架&#xff0c;是 Apache 开源社区的顶级项目之一&#xff0c;也是国内使用最多和最为广泛的可视化图表框架之一。 数据可视化图表框架并没有一个统一的行业标准&#xff0c;比较常见的有 D3、Highchart…

电子设备常用的胶水有哪些?

目录 1、502胶水 2、703胶水 3、704胶水 4、AB胶 5、红胶 6、Underfill 7、导电胶 8、UV胶 9、热熔胶 10、环氧树脂胶 11、硅酮胶 12、聚氨酯胶 13、丙烯酸胶 14、丁基胶 1、502胶水 502胶水&#xff0c;也被称为瞬间胶或快干胶&#xff0c;是一种非常常见的粘合…

如何下载Github上项目中的一个目录或几个文件

目录 问题的由来 GitZip for Github插件 Edge中插件的安装 Chome中插件的安装 插件的使用 Github授权 文件下载 问题的由来 经常使用Github的可能都会有这样的需求&#xff0c;有的时候一个仓库好几个GB&#xff0c;但是感兴趣的只是某个目录下的文件。Git没有单独下载…

CTFShow的RE题(四)

真的是签到 给的是无后缀的 zip 文件&#xff0c;解压发现需要密码&#xff0c;也没有提示&#xff0c;猜测可能是 zip 伪加密 &#xff08;走错厂了吧&#xff09; zip是否加密 首先就是看开头的6 &#xff0c;7byte&#xff0c;和中间 01 02 后的 5 &#xff0c;6byte 成功解…

如何做一个透明度渐现且向上位移逐行出现的文字效果

前言 在这个夜黑风高的夜晚&#xff0c;你的眼睛已经开始有些疲惫。你的手指在键盘上轻轻地敲击着&#xff0c;仿佛在弹奏一首无声的夜曲。你的思绪在代码的海洋中飘荡&#xff0c;寻找着最后一行需要完成的代码。就在这时&#xff0c;你的老板走了过来&#xff0c;他的脸上带…

简过网:事业单位编制有哪几种类型,你都知道吗?

近几年来&#xff0c;随着考编、考公热&#xff0c;越来越多的朋友都有考编的想法&#xff0c;尤其是刚毕业的大学生&#xff0c;但是很多朋友对于事业单位编制有不清楚的地方&#xff0c;比如到底哪些属于事业单位的范围&#xff0c;事业单位编制分为哪些类型&#xff1f;今天…

解决:Android Studio 突然打不开!提示Failed to create JVM:error code -1

Android studio1.5 一直用得好好的&#xff0c;突然有一天打不开&#xff0c;并提示&#xff1a; 可是系统配置中&#xff0c;java的配置也是正常的。 解决方法&#xff1a; 修改安装目录下的studio64.exe.vmoptions 文件 直接将文件内容改成&#xff1a; -Xms128m -Xmx512m…

【活动行】参与上海两场线下活动,教育生态行业赛总决赛活动和WAIC人工智能大会活动 - 上海活动总结

目录 背景决赛最后一公里领域范围 决赛作品AI智教相机辅导老师Copilot辅导老师Copilot雅思写作竞技场 优秀作品总结 背景 决赛 百度发起的千帆杯教育生态行业赛于2024年7月4日进行线下决赛&#xff0c;博主虽然没能进入决赛&#xff0c;但也非常荣幸能够以嘉宾身份到现场给进…

213.贪心算法:跳跃游戏||(力扣)

class Solution { public:int jump(vector<int>& nums) {if (nums.size() 1) return 0; // 如果数组长度为1&#xff0c;已经在终点&#xff0c;不需要跳跃int cur 0; // 当前跳跃能到达的最远位置int flag 0; // 记录跳跃次数int next 0; // 下一次跳跃能到…

软件设计之Java入门视频(13)

软件设计之Java入门视频(13) 视频教程来自B站尚硅谷&#xff1a; 尚硅谷Java入门视频教程&#xff0c;宋红康java基础视频 相关文件资料&#xff08;百度网盘&#xff09; 提取密码&#xff1a;8op3 idea 下载可以关注 软件管家 公众号 学习内容&#xff1a; 该视频共分为1-7…

2024年【低压电工】考试题库及低压电工考试总结

题库来源&#xff1a;安全生产模拟考试一点通公众号小程序 2024年低压电工考试题库为正在备考低压电工操作证的学员准备的理论考试专题&#xff0c;每个月更新的低压电工考试总结祝您顺利通过低压电工考试。 1、【单选题】PE线或PEN线上除工作接地外其他接地点的再次接地称为(…

通过Vxlan实现数据中心互联有感

随着企业的发展&#xff0c;为满足跨地域运营、用户接入、异地灾备等场景&#xff0c;越来越多的企业通常在多地域部署多个数据中心。 数据中心互联DCl(Data Center Interconnection)是不同数据中心VM之间互相通信的一种解决方案使用VXLAN、BGP EVPN等技术&#xff0c;使数据中…

RoPE 旋转位置编码,详细解释(下)NLP 面试的女生彻底说明白了

RoPE 旋转位置编码&#xff0c;详细解释&#xff08;下&#xff09;NLP 面试的女生彻底说明白了 原创 看图学 看图学 2024年07月01日 07:55 北京 书接上文&#xff0c;上文见&#xff1a;这么解释 RoPE 旋转位置编码&#xff0c;女朋友睁大了双眼&#xff08;上&#xff09; …