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(&#34;ow:&#34; + ow + &#34; alpha:&#34;+alpha + &#34;r:&#34;+ r + &#34; l:&#34;+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(&#34;Bottom1&#34;).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]