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

Posted Dr.勿忘

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Lycoris Recoil再现!Unity实现Sakana~,代码思路解析,代码开源,Unity弹簧效果相关的知识,希望对你有一定的参考价值。

效果、源码下载

视频展示,源码及demo下载

gif演示

(千束是刻意起飞的,不是bug)

前言

我之前在看到网上有人实现了网页版的sakana,感觉超级有意思,于是动手用unity实现了一下。

整个代码就弹簧效果上实现起来稍微麻烦一点,用了点数学物理的知识。Unity原本有个弹簧关节 (Spring Joint)插件可以实现弹簧效果。但是我试了一下,用不会、不好用,还是自己查资料、动手写来的方便、自由。

虽然这个效果看起来很简单,但是做起来实在是容易踩坑。

另外,此项目的代码是对弹簧进行简单的模拟,简化版的弹簧运动,仅用到了高中物理或者说大物的知识,不是硬核地模拟真实的弹簧。

本文对代码思路进行简单地阐述,源代码已经写了很多注释了。

效果拆分


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

  1. 鼠标拖动
  2. 物体相对弹簧的径向运动
  3. 物体相对弹簧的摆动
  4. 物体朝向
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();
        
    

React 的状态管理库 —— Recoil

为什么使用 Recoil

在学一样东西之前,我们得了解它为什么会诞生,或者是它解决了什么问题。

Recoil 是由 Facebook 推出的一个全新的、实验性的 JavaScript 状态管理库,它解决了使用现有 Context API 在构建大型应用时所面临的很多问题。

使用 React 内置的状态管理能力有这样一些局限性:

  • 组件间的状态共享只能通过将 state 提升至它们的公共祖先来实现,但这样做可能导致重新渲染一颗巨大的组件树。
  • Context 只能存储单一值,无法存储多个各自拥有 Consumer 的值的集合。
  • 以上两种方式都很难将组件树的顶层(state 必须存在的地方)与叶子组件 (使用 state 的地方) 进行代码分割。

尽管像 Redux 和 MobX 这样的库能够确保应用的状态保持一致,但是对于很多应用来讲,它们所带来的开销是难以估量的。

Redux、Mobx 本身并不是 React 库,我们是借助这些库的能力来实现状态管理。像 Redux 它本身虽然提供了强大的状态管理能力,但是使用的成本非常高,你还需要编写大量冗长的代码,另外像异步处理或缓存计算也不是这些库本身的能力,甚至需要借助其他的外部库。

并且,它们并不能访问 React 内部的调度程序,而 Recoil 在后台使用 React 本身的状态,在未来还能提供并发模式这样的能力

Recoil 是什么

Facebook 的软件工程师做过这样一个演讲分享

更新 List 里面第二个节点,然后希望 Canvas 的第二个节点也跟着更新。

最古老的方式就是通过共同父子组件通信,但父组件下面的子组件都会更新,这种情况下一般使用  memo  或者  PureComponent

还可以使用 React 自带的 Context API,将状态从父组件传给子组件。

但这样带来的问题就是如果我们共享的状态越多就需要越多的 Provider,层层嵌套。

那是否有一种可以精准更新节点,同时又不需要嵌套太多层级的方案呢?它就是 Recoil。通过创建正交的 tree,将每个 state 和组件对应起来,从而实现精准更新。

Recoil 将这些 state 称之为 Atom(英文翻译为原子),顾名思义,Atom 是 Recoil 里面最小的数据单元,它支持更新和订阅。

使用

先来看看 Recoil 是怎么使用的

根组件

使用 recoil 状态的组件需要使用 RecoilRoot 包裹起来,一般是根组件直接包裹

import React from \'react\'
import ReactDOM from \'react-dom\'
import { RecoilRoot } from \'recoil\'
import App from \'./App\'

ReactDOM.render(
  <RecoilRoot>
    <App />
  </RecoilRoot>,
  document.getElementById(\'root\')
)

Atoms

Atom 是最小状态单元。它们可以被订阅和更新:当它更新时,所有订阅它的组件都会使用新数据重绘;它可以在运行时创建;它也可以在局部状态使用;同一个 Atom 可以被多个组件使用与共享。

相比 Redux 维护的全局 Store,Recoil 则是采用分散管理原子状态的设计模式,方便进行代码分割。

Atom 和传统的 state 不同,它可以被任何组件订阅,当一个 Atom 被更新时,每个被订阅的组件都会用新的值来重新渲染。

所以 Atom 相当于一组 state 的集合,改变一个 Atom 只会渲染特定的子组件,并不会让整个父组件重新渲染。

import { atom } from \'recoil\'

export const todoList = atom({
  key: \'todoList\',
  default: [],
})

要创建一个 Atom ,必须要提供一个 key ,其必须在 RecoilRoot 作用域中是唯一的,并且要提供一个默认值,默认值可以是一个静态值、函数甚至可以是一个异步函数。

API

Recoil 采用 Hooks 方式订阅和更新状态,常用的 API 如下:

useRecoilState

类似 useState 的一个 Hook,可以对 atom 进行读写

import React, { useState } from \'react\'
import { useRecoilState } from \'recoil\'
import { TodoListStore } from \'./store\'

export default function OperatePanel() {
  const [inputValue, setInputValue] = useState(\'\')
  const [todoListData, setTodoListData] = useRecoilState(TodoListStore.todoList)

  const addItem = () => {
    const newList = [...todoListData, { thing: inputValue, isComplete: false }]
    setTodoListData(newList)
    setInputValue(\'\')
  }

  return (
    <div>
      <h3>OperatePanel Page</h3>
      <input type=\'text\' value={inputValue} onChange={e => setInputValue(e.target.value)} />
      <button onClick={addItem}>添加</button>
    </div>
  )
}

useSetRecoilState

只获取 setter 函数,不会返回 state 的值,如果只使用了这个函数,状态变化不会导致组件重新渲染

import React from \'react\'
import { useSetRecoilState } from \'recoil\'
import { TodoListStore } from \'./store\'
export default function SetPanel() {
  const setTodoListData = useSetRecoilState(TodoListStore.todoList)

  const clearData = () => {
    setTodoListData([])
  }

  return (
    <div>
      <button onClick={clearData}>清空recoil的数组</button>
    </div>
  )
}

useRecoilValue

只返回 state 的值,不提供修改方法

import React from \'react\'
import { useRecoilValue } from \'recoil\'
import { TodoListStore } from \'./store\'
export default function ShowPanel() {
  const todoListData = useRecoilValue(TodoListStore.todoList)
  return (
    <div>
      <h3>ShowPanel Page</h3>
      recoil中获取结果展示:
      {todoListData.map((item, index) => {
        return <div key={index}>{item.thing}</div>
      })}
    </div>
  )
}

selector

selector 表示一段派生状态,它使我们能够建立依赖于其他 atom 的状态。它有一个强制性的 get 函数,其作用与 redux 的 reselect 或 MobX 的 computed 类似。

selector 是一个纯函数:对于给定的一组输入,它们应始终产生相同的结果(至少在应用程序的生命周期内)。这一点很重要,因为选择器可能会执行一次或多次,可能会重新启动并可能会被缓存。

export const completeCountSelector = selector({
  key: \'completeCountSelector\',
  get({ get }) {
    const completedList = get(todoList)
    return completedList.filter(item => item.isComplete).length
  },
})

selector 还支持异步函数,可以将一个 Promise 作为返回值

结语

除了 Facebook,暂时还没有看到有哪些网站已经用了 Recoil。

Recoil 的核心概念都很简单,没有 Redux 那么绕的概念,也不需要写一堆像 action、reducer 之类的模板文件,基于 Hooks 的 API 以及它的直观性。与其他一些库相比,Recoil 的 API 比大多数库更容易,让开发更加简单。

我们现在的项目使用了 Recoil,目前感受是简化版的 Context API,使用较 Redux 简单,暂时没有发现能像 Redux 生态那样方便的时间回溯功能,后续使用有待继续观察。

本文案例代码


  • 个人技术博文 Github 仓库
    觉得不错的话欢迎 star,给我一点鼓励继续写作吧~

以上是关于Lycoris Recoil再现!Unity实现Sakana~,代码思路解析,代码开源,Unity弹簧效果的主要内容,如果未能解决你的问题,请参考以下文章

React 的状态管理库 —— Recoil

[React Recoil] Use selectors to calculate derived data based on state stored within a Recoil atom(代码

[React Recoil] Use selectors to calculate derived data based on state stored within a Recoil atom(代码

HNUSTOJ-1543 字符串的运算再现

Facebook 新一代 React 状态管理库 Recoil

如何更新 Recoil.js 外部组件中的原子(状态)? (反应)