JoshWindsor 发表于 2022-9-21 21:49

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



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();
      }
    }
页: [1]
查看完整版本: Unity实现Sakana~,代码思路解析,代码开源,Unity弹簧效果