找回密码
 立即注册
查看: 244|回复: 0

[笔记] Unity实现Sakana~,代码思路解析,代码开源,Unity弹簧效果

[复制链接]
发表于 2022-9-21 21:49 | 显示全部楼层 |阅读模式


https://www.zhihu.com/video/1555936053399281665
效果如上面的视频,视频也发布到了B站。 视频以及github地址
我之前在看到网上有人实现了网页版的sakana,感觉超级有意思,于是动手用unity实现了一下。
整个代码就弹簧效果上实现起来稍微麻烦一点,用了点数学物理的知识。Unity原本有个弹簧关节 (Spring Joint)插件可以实现弹簧效果。但是我试了一下,用不会、不好用,还是自己查资料、动手写来的方便、自由。
虽然这个效果看起来很简单,但是做起来实在是容易踩坑。
另外,此项目的代码是对弹簧进行简单的模拟,简化版的弹簧运动,仅用到了高中物理或者说大物的知识,不是硬核地模拟真实的弹簧。
本文对代码思路进行简单地阐述,源代码已经写了很多注释了。
效果拆分



我把最终效果拆分成一下几个部分:

  • 鼠标拖动
  • 物体相对弹簧的径向运动
  • 物体相对弹簧的摆动
  • 物体朝向
void FixedUpdate()
    {
        //鼠标拖动
        OnMouseDown();

        //物体朝向
        //3D项目用transform.LookAt就可以简单实现朝向,2D的话就得自己手动来了
        Look2D();

        //鼠标松开
        OnMouseUp();

        if (start)
        {         
            //沿弹簧方向的运动
            SpringMove();
            //弹簧的左右摆动
            SpringSwing();
        }
    }
有个小细节,我把流程代码放在FixedUpdate而不是Update里面,是因为不同平台运行代码时候帧率不一样,通过Time.deltaTime计算出来的运动效果会有差别。FixedUpdate是固定时间执行一次,就能在不同平台达到相近的效果。
鼠标拖动

使用协程OnMouseDow来检测鼠标的动作,这个方法只会检测挂了此脚本的物体。
屏幕坐标和世界坐标之间的转换
另外添加了limit限制拖动范围
//鼠标拖动
    IEnumerator OnMouseDown()    //使用协程
    {
        Vector3 targetScreenPos = Camera.main.WorldToScreenPoint(transform.position);//三维物体坐标转屏幕坐标
        //将鼠标屏幕坐标转为三维坐标,再计算物体位置与鼠标之间的距离
        var offset = transform.position - Camera.main.ScreenToWorldPoint(new Vector3(Input.mousePosition.x, Input.mousePosition.y, targetScreenPos.z));

        while (Input.GetMouseButton(0))
        {
            start = false;

            //将鼠标位置二维坐标转为三维坐标
            Vector3 mousePos = new Vector3(Input.mousePosition.x, Input.mousePosition.y, targetScreenPos.z);
            //将鼠标转换的三维坐标再转换成世界坐标+物体与鼠标位置的偏移量
            var targetPos = Camera.main.ScreenToWorldPoint(mousePos) + offset;
            //限制拖动范围
            if (Vector3.Distance(targetPos, oriPosition) <= limit)
            {
                transform.position = targetPos;
            }


            yield return new WaitForFixedUpdate();//循环执行
        }     
    }
物体相对弹簧地径向运动

物体直线运动公式
x = v * dt + 0.5f * a * dt * dt      (代码里面不要写1 / 2,因为 1 / 2 等于0)
代码中的time是Time.fixedDeltaTime,相当于公式中的dt,另外,本段代码的运算采用的是矢量,所以速度用了个向量。
在SpringMove()中先计算当前速度的位移,在AddForce()中计算力作用下的位移,力来源于弹簧,设弹簧的劲度系数为k, scalarF = k *(dAB.magnitude - L),设置了一个bottom点,表示弹簧的另外一段(没有连接运动物体的那一段)(这个bottom还会用来计算摆动)。
//x = v * dt + 0.5f * a * dt * dt
    //沿弹簧方向的运动
    void SpringMove()
    {
        transform.position += v * time;//速度产生的位移
        v *= aS;//空气阻力会使速度减小
        //AddForce(new Vector3(0, 9.8f, 0));//重力

        Vector3 dAB = transform.position - bottom;//向量
        float scalarF = k * (dAB.magnitude - L);//产生的力的大小
        AddForce(-dAB.normalized * scalarF);     
    }
    void AddForce(Vector3 force)
    {
        Vector3 a = force / rb.mass;//此力作用于当前质点上产生的加速度
        v += a * time;//加速度对质点速度的作用:用来加速度
        transform.position += 0.5f * a * Mathf.Pow(time, 2);//加速度产生的位移
    }
物体相对弹簧的摆动

摆动我是让物体绕着一个点做摆动,再用transform.RotateAround方法来计算,参数是旋转轴,旋转点,旋转位移。
向量求叉积可以判断当前物体运动到原点左边还是右边,所以求了旋转轴和一个axis,这样能计算运动方向。

//弹簧的左右摆动
    void SpringSwing()
    {
        //半径
        float r = Vector3.Distance(transform.position, targetObject.position);
        //水平l
        float l = Vector3.Distance( new Vector3(targetObject.position.x, transform.position.y, transform.position.z)
            , transform.position);
        //叉积的向量用来和旋转轴相乘,判断物体是正向还是反向运动
        Vector3 axis = Vector3.Cross( transform.position -  targetObject.position, Vector3.down);
        if( Vector3.Dot(axis, rotateAxi) < 0)
        {
            l = -l;
        }
        //角加速度
        float alpha = (-g) * l / Mathf.Pow(r, 2);
        ow += alpha * time;
        ow *= aR;//衰减
        //求角位移(乘以180/PI 是为了将弧度转换为角度)
        float thelta = ow * time * 180.0f / Mathf.PI / 2;
        //绕targetObject圆点,rotateAxi旋转轴,旋转位移thelta
        transform.RotateAround(targetObject.position, rotateAxi, thelta);
        //print("ow:" + ow + " alpha:"+alpha + "r:"+ r + " l:"+l);
    }
物体朝向

为了让物体看起来不那么生硬,更有弹簧旋转起来的效果,我让物体始终朝向bottom点。
在3d里面有LookAt直接用,但是2d要自己实现一下
用物体的坐标与bottom(被看向的点)两个点的出一个向量,让物体的旋转性保持这个向量的方向即可。
//朝向
    void Look2D()
    {
        Vector3 v = targetObject.position - transform.position;
        v.z = 0;
        Quaternion rotation = Quaternion.FromToRotation(Vector3.up, -v);
        transform.rotation = rotation;
    }
其他代码

    Rigidbody2D rb;
    Vector3 v;//速度
    float p;//速率

    Vector3 bottom;//弹簧底部坐标
    float k;//弹簧劲度系数
    float L;//弹簧原长

    float aS;//衰减
    float aR;//旋转的衰减

    bool start;//监测是否开始运动

    public Transform targetObject;//朝向
    float g = 10000.8f;//向上的加速度,仅对摆动有效
    float ow = 0;//角速度
    Vector3 rotateAxi;//旋转轴

    float time;//时间

    float limit;//拖动限制范围
    Vector3 oriPosition;//物体起始位置

    AudioSource audio;

    // Start is called before the first frame update
    void Start()
    {
        rb = gameObject.GetComponent<Rigidbody2D>();
        bottom = targetObject.transform.position;
        k = 300f;
        L = gameObject.transform.position.y - bottom.y;

        limit = 0.8f*L;
        oriPosition = transform.position;

        //PC,Update时候
        //aS = 0.9995f;
        //aR = 0.995f;

        aS = 0.9999f;
        aR = 0.97f;

        //An,因为帧率的影响,这是放在update时候
        //aS = 0.9999f;
        //aR = 0.9f;

        start = false;

        //targetObject = GameObject.Find("Bottom1").transform;
        //旋转轴
        //注意!!!,gameobjec和target在世界中需要错开一点角度,如果都在一条竖线上的出来的旋转轴是0向量,无法继续计算
        rotateAxi = Vector3.Cross((transform.position - targetObject.position), Vector3.down);

        rb.gravityScale = 0;//关闭重力

        time = Time.fixedDeltaTime;

        audio = GetComponent<AudioSource>();
    }
IEnumerator OnMouseUp()
    {
        if (Input.GetMouseButtonUp(0))
        {
            start = true;
            audio.Play();
            yield return new WaitForFixedUpdate();
        }
    }

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有账号?立即注册

×
懒得打字嘛,点击右侧快捷回复 【右侧内容,后台自定义】
您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

小黑屋|手机版|Unity开发者联盟 ( 粤ICP备20003399号 )

GMT+8, 2024-4-28 22:38 , Processed in 0.098363 second(s), 27 queries .

Powered by Discuz! X3.5 Licensed

© 2001-2024 Discuz! Team.

快速回复 返回顶部 返回列表