Lycoris Recoil再现!Unity实现Sakana~,代码思路解析,代码开源,Unity弹簧效果
Posted Dr.勿忘
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Lycoris Recoil再现!Unity实现Sakana~,代码思路解析,代码开源,Unity弹簧效果相关的知识,希望对你有一定的参考价值。
效果、源码下载
gif演示
(千束是刻意起飞的,不是bug)
前言
我之前在看到网上有人实现了网页版的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();
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] 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(代码