专栏首页微卡智享学习|Unity3D使用协程实现减速停车效果

学习|Unity3D使用协程实现减速停车效果

学更好的别人,

做更好的自己。

——《微卡智享》

本文长度为4303,预计阅读11分钟

前言

上一篇《学习|Unity3d的导航实现循环线路移动》讲了一下定制循环行驶路线的方法,在视频中还有一个就是非会员的车辆需要人工收费,所以就要有一个减速停车等待的动画效果,本篇就来讲讲怎么用Unity3d的协程来实现的这一效果。

实现效果

上面的动图中我们可以看到无卡车辆在到达起杆前有一个慢慢减速后停止的效果(如果动图不明显可以看看历史文章里的这个视频),实现这个效果我用的是协程的方式,其实在FixUpdate的函数中进行处理应该效果会更好一些,但是也是为了掌握协程这个技巧,所以才用的协程的方式来实现的。

关于协程

微卡智享

协程本身有点像线程,但又不同于线程,协程本身还是在主程序中运行的,完全不用考虑使用线程时如线程锁或是线程同步的问题。

Update()函数中我们可以知道是每一帧都调用的,在每一帧处理时我们可能会有不少事件需要进行判断处理,如果需要判断的事件还需要有计时的处理时,在Update中看代码的可读性非常差,所有这里我们就可以用到协程了。

官方文档Monobehaviour的函数执行顺序图,就对协程再次执行的时机做了很好的描述:

相关函数

函数

参数

开启协程

StartCoroutine(string methodName)输入参数名StartCoroutine(IEnumerator method),输入方法名,此方法可以有多个参数

终止协程

StopCoroutine(string methodName);//终止指定的协程StopAllCoroutine();//终止所有协程

挂起协程

yield return 0;//程序在下一帧中从当前位置继续执行yield return null;//程序在下一帧中从当前位置继续执行yield return new WaitForSeconds(N);//程序等待N秒后从当前位置继续执行yield new WaitForEndOfFrame();//在所有的渲染以及GUI程序执行完成后从当前位置继续执行yield new WaitForFixedUpdate();//所有脚本中的FixedUpdate()函数都被执行后从当前位置继续执行yield return WWW;//等待一个网络请求完成后从当前位置继续执行yield return StartCoroutine(xxx);//等待一个xxx的协程执行完成后从当前位置继续执行yield break;//如果使用yield break语句,将会导致协程的执行条件不被满足,不会从当前的位置继续执行程序,而是直接从当前位置跳出函数体,回到函数的根部

程序实现

微卡智享

01

碰撞器与钢体的设置

上图中红色框是我们设置的一个boxCollider(盒形碰撞器),而蓝框的车里面我们也加入了一个碰撞器,还有一个钢体。

在红框里的碰撞器我们把isTrigger打上勾,代表是触发器,这样两个物体碰撞是不会产生物理效果了,只会生成触发的事件。这样基本就设置好了,下面的就是我们在代码里实现了。

实现思路

1. 当两个物体碰撞触发事件后,判断是否是会员车辆,如果不是进入停车减速的协程,设置一个停车减速的时间为参数。

2. 根据输入的减速时间参数先计算出大约多少帧,然后用当前的车速除帧数得到每一帧应该减的速度为多少,设置循环,每一帧降低刚才计算要减的车速,直到停车

3. 设置一个停止时长

4. 再按刚才的帧数每帧再增加车速,直到恢复原来的速度

核心代码

    IEnumerator StopAndStartCar(float seconds)
    {
        Debug.Log("time:" + Time.deltaTime);
        //计算输入时长大约多少帧
        float fps = seconds / Time.deltaTime;
        //计算每帧要调整的车速
        float speed = oldspeed / fps;

        Debug.Log("fps:" + fps);
        Debug.Log("speed:" + speed);
        //减速停车
        for (int i=0; i < fps; ++i)
        {
            if (nav.speed > 0)
            {
                nav.speed -= speed;
                if (nav.speed < 0) nav.speed = 0;
            }
            yield return null;
        }
        if (nav.speed != 0) nav.speed = 0;
        //等待0.5秒
        yield return new WaitForSeconds(1f);
        //启动加速
        for(int i=0; i < fps; ++i)
        {
            nav.speed += speed;
            yield return null;
        }
        nav.speed = oldspeed;
    }

上面这个就是实现减速后停止0.5秒,然后再加速的协程方法。

当我们进入触发函数时判断不是会员车辆加入了一个启动协程的方法,参数输入的是3f(即3秒)。这样我们的停车减速的效果就实现了。

完整代码

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

public class NavCar : MonoBehaviour
{
    //定义接收导航网络组件
    private NavMeshAgent nav;
    //坐标点列表
    private List<Vector3> destpoints;
    //导航下一个坐标点
    private int nextindex;
    //离导航坐标点的距离 
    private float calcdist = 5f;
    private float dist = 0f;

    private TextMesh textMesh;
    private float oldspeed;

    // Start is called before the first frame update
    void Start()
    {
        //将定义的路线加入到List列表中
        destpoints = new List<Vector3>();
        destpoints.Add(GameObject.Find("RoadPoint0").transform.position);
        destpoints.Add(GameObject.Find("RoadPoint1").transform.position);
        destpoints.Add(GameObject.Find("RoadPoint2").transform.position);
        destpoints.Add(GameObject.Find("RoadPoint3").transform.position);

        textMesh = this.transform.GetComponentInChildren<TextMesh>();

        //获取当前车辆的NavMeshAgent
        nav = this.transform.GetComponent<NavMeshAgent>();
        oldspeed = nav.speed;

        //计算最近的点,获取下一点的序号
        Vector3 navpoint = this.transform.position;
        Debug.Log("now:" + navpoint);
        for (int i = 0; i < destpoints.Count; ++i)
        {
            //首先判断点在当前位置的前方还是后方,如果是后方不做计算
            Vector3 dir = destpoints[i] - navpoint;
            float dot = Vector3.Dot(transform.forward, dir);
            Debug.Log("dot:" + dot);

            //判断点在前方时才计算最近的点的距离
            if (dot > 0)
            {
                float tmpdist = Vector3.Distance(destpoints[i], navpoint);
                if (dist == 0)
                {
                    dist = tmpdist;
                    nextindex = i;
                }
                else if (dist > tmpdist)
                {
                    dist = tmpdist;
                    nextindex = i;
                }
                Debug.Log("position:" + destpoints[i] + "  dist:" + tmpdist);
            }

        }

        Debug.Log("final:" + nextindex + " dist:" + dist);
    }

    private void OnTriggerEnter(Collider other)
    {
        if (other.gameObject.name == "GateDown")
        {
            Debug.Log(other.gameObject.name);
            if (textMesh.text != "会员车辆")
            {
                StartCoroutine(StopAndStartCar(3f));
            }
        }
    }

    IEnumerator StopAndStartCar(float seconds)
    {
        Debug.Log("time:" + Time.deltaTime);
        //计算输入时长大约多少帧
        float fps = seconds / Time.deltaTime;
        //计算每帧要调整的车速
        float speed = oldspeed / fps;

        Debug.Log("fps:" + fps);
        Debug.Log("speed:" + speed);
        //减速停车
        for (int i=0; i < fps; ++i)
        {
            if (nav.speed > 0)
            {
                nav.speed -= speed;
                if (nav.speed < 0) nav.speed = 0;
            }
            yield return null;
        }
        if (nav.speed != 0) nav.speed = 0;
        //等待0.5秒
        yield return new WaitForSeconds(1f);
        //启动加速
        for(int i=0; i < fps; ++i)
        {
            nav.speed += speed;
            yield return null;
        }
        nav.speed = oldspeed;
    }

    // Update is called once per frame
    void Update()
    {
        //判断距离是否在到达范围内,如果在走到一下个点
        if (Vector3.Distance(this.transform.position, destpoints[nextindex])< calcdist)
        {
            if (nextindex == destpoints.Count - 1)
            {
                nextindex = 0;
            }
            else
            {
                nextindex++;
            }
        }

        nav.SetDestination(destpoints[nextindex]);

    }
}

在动画中的起杆的动画也是按这个方法实现的,这里就不再进行描述了。

扫描二维码

获取更多精彩

微卡智享

「 往期文章 」

学习|Unity3d的导航实现循环线路移动

学习|C#线程中AutoResetEvent的使用

学习|C#的EventHandler的委托使用

本文分享自微信公众号 - 微卡智享(VaccaeShare),作者:Vaccae

原文出处及转载信息见文内详细说明,如有侵权,请联系 yunjia_community@tencent.com 删除。

原始发表时间:2020-08-27

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

我来说两句

0 条评论
登录 后参与评论

相关文章

  • C++ OpenCV基于距离变换与分水岭的图像分割

    图像分割,英文名image segmentation,就是把图像分成若干个特定的、具有独特性质的区域并提出感兴趣目标的技术和过程。它是由图像处理到图像分析的关键...

    Vaccae
  • Android利用SurfaceView显示Camera图像爬坑记(二)

    前一章《Android利用SurfaceView显示Camera图像爬坑记(一)》我们已经实现了利用SurfaceView将Camera中的实时帧图像显示出来了...

    Vaccae
  • C++ OpenCV直方图均衡化

    图像直方图由于其计算代价较小,且具有图像平移、旋转、缩放不变性等众多优点,广泛地应用于图像处理的各个领域,特别是灰度图像的阈值分割、基于颜色的图像检索以及图像分...

    Vaccae
  • PHP-5.3向更高版本迁移之不兼容

    luxixing
  • php安装gd扩展

    码农二狗
  • jquery 模态窗口 弹出窗 simpleModal

    <!DOCTYPE html> <html> <head> <meta http-equiv="Content-Type" content="text/h...

    用户5760343
  • 【推荐】飞林沙:商品推荐算法&推荐解释

    这是今天看到的一篇蛮有新意的讲稿,由于不是一篇完整的论文,所以理解起来稍微有些困难,就顺着写个笔记,仅供参考。 Ref: http://www.wsdm-con...

    小莹莹
  • 关于即时通信服务器架构的一些思考

    对于一个即时通信服务器来说,在用户量少的时候,一台服务器就足以提供所有的服务。而这种架构也最简单,举个例子,用户A与用户B互为好友,A向B发消息,服务器接收到消...

    范蠡
  • 即时通信服务器架构的一些思考

    对于一个即时通信服务器来说,在用户量少的时候,一台服务器就足以提供所有的服务。而这种架构也最简单,举个例子,用户A与用户B互为好友,A向B发消息,服务器接收到消...

  • 麒麟芯片成为绝唱之后,华为手机真的太难了

    “由于(特朗普政府)第二轮制裁,台积电只接受了我们芯片5月15号之前的订单,到9月16号生产就停止了,所以今年可能是全球最领先的、华为麒麟高端芯片的绝版、最后一...

    镁客网

扫码关注云+社区

领取腾讯云代金券