Sliding a Sphere 滑动一个球体
拖尾
Reading Player Input读取玩家输入
Vector2 playerInput;
playerInput.x = Input.GetAxis("Horizontal");
playerInput.y = Input.GetAxis("Vertical");
Normalizing the Input Vector 归一化输入向量
Constraining the Input Vector 约束输入向量
playerInput = Vector2.ClampMagnitude(playerInput, 1.0f);
//将输入向量限制在一个最大长度为1的圆内,保持方向不变
速度,加速度,desired Velocity 目标速度
Physics 物理
Rigid body 刚体
Rigid body Component刚体组件
将物体转化为物理对象
Controlling Rigid body Velocity 控制刚体速度
速度
velocity = body.velocity;
Friction 摩擦
物理材质
_Asset / Create / Physic Material
FixedUpdate()
FixedUpdate()在物理模拟步骤开始被调用,频率与PhysX相同
Support vertical movement via Jumping
Jump 跳跃
void Update(){
……
desiredJump |= Input.GetBottonDown("Jump");
}
void FixedUpdate() {
…
if (desiredJump) {
desiredJump = **false**;
Jump();
}
body.velocity = velocity;
}
void Jump() {
velocity.y += 5f;
}
- 是否跳跃bool desiredJump
- 跳跃高度 jumpHeight
- 在地面上(不在墙上)起跳onGround:法向量与y轴正方向余弦的判断
- 空中跳跃 maxAirJump在空中可以跳跃的最大次数
- 限制向上速度:连续跳跃速度远大于单词跳跃速度
- 空中加速度 maxAirAcceleration在空中的最大加速度
Detect the ground and its angle检测地面及其倾斜角度
-
ProBuilder创造场景
-
地面角度 maxGroundAngle(用 cos判断):大于这个角度的是墙
-
斜坡上跳跃:垂直斜坡跳跃,法线contactNormal方向
-
沿斜坡运动
- 如果加速度过大,垂直斜面的力大,小球可能在斜坡上跳跃前行
- 方法:投影平行斜面的向量
Multiple Ground Normals 多个地面法线
-
contactNormal 进行所有地面接触点法线的加和,然后归一化
-
根据地面接触次数着色
Surface Contact 表面接触
- Stick to the ground instead of launching off a ramp 贴地滑行,不从坡道跃起
- Perform a raycast 射线检测身下
- Configure multiple layers and their interaction配置多个图层及其交互关系
- Navigate stairs攀爬楼梯:碰撞器
- Make use of steep contacts利用陡峭的接触点
Orbit Camera 轨道相机
移动相机使视角进行手动或自动的改变
Create a orbiting camera 创建一个环绕摄像机
即实现第三人称视角下的物体移动
Attribute: 该物体一定有某种组件
用法:[RequireComponent(typeof(ComponentType))]
一定有Camera组件
[RequireComponent(typeof(Camera))]
public class OrbitCamera : MonoBehaviour{}
对焦对象,距离,焦距半径,居中对焦
焦距半径:物体运动幅度小于焦点半径时,摄像机不动
居中对焦:摄像机跟踪点在每一帧向目标位置靠拢的速度变化
![[Pasted image 20260628145128.png]]
Support manual and automatic camera rotation 支持手动或自动旋转
手动旋转
看 bool ManualRotation(){}
自动对齐
看 bool AutomaticRotation(){}
Make movement relative to the camera 使运动相对于摄像机进行
-
在MovingSphere中加入变量 输入空间 playerInputSpace = default;
-
然后在
if (playerInputSpace) {
desiredVelocity = playerInputSpace.TransformDirection( playerInput.x, 0f, playerInput.y) * maxSpeed;
}
- 归一化
if (playerInputSpace) {
Vector3 forward = playerInputSpace.forward;
forward.y = 0f;
forward.Normalize();
Vector3 right = playerInputSpace.right;
right.y = 0f;
right.Normalize();
desiredVelocity =(forward * playerInput.y + right * playerInput.x) * maxSpeed;
}
Prevent the camera from intersecting geometry 防止相机与集合体相交
看 属性 CameraHalfExtends
Custom Gravity 自定义重力
Walking on a Sphere
Support arbitrary gravity 支持任意重力
在单个物体实现支持任意重力
上轴
upAxis = -Physics.gravity.normalized;
然后在所有原来直接使用gravity的改成 upAxis
相对控制
添加前轴,右轴保持摄像机视角x,z轴的移动
需要在平面上进行投影 ,看ProjectDirctionOnPlane
对齐轨道相机
物体的上轴改变后,轨道相机没有改变,上轴仍然是y轴正半轴
所以现在要更改OrbitCamera类的轨道相机,实现轨道旋转
用orbitRotation追踪轨道旋转(玩家手动输入),与orbitRotation重力对齐(重力对齐)旋转彻底分开管理,让轨道旋转对重力“无感知”
Spherical Gravity 球形引力
自定义重力
-
创建一个新的 静态类
CustomGravity来根据世界坐标位置返回重力向量
脚本不挂载,负责全局管理,在多重力环境实现了所有重力源的整合 -
使用自定义重力时,确保标准重力不作用与物体的刚体
body.useGravity = false; -
向原点拉动或推离原点
e.g.
public static Vector3 GetGravity(Vector3 position,out Vector3 upAxis){
Vector3 up = position.normalized;
upAxis = Physics.gravity.y < 0f up : -up;
return up * Physics.gravity.y;
}
- 引力源列表:将所有
position所处的所有重力源加和
static List<GravitySource> sources = new List<GravitySource>();//引力源列表
public static Vector3 GetGravity(Vector3 position)
{
Vector3 g = Vector3.zero;
for (int i = 0; i < sources.Count; i++)
{
g += sources[i].GetGravity(position);
}
return g;
}
- 进行注册与注销重力源操作
每个重力源至多注册一次,注销时只在被注册后注销
同一来源可以多次注册或注销,从而实现临时停用
public static void Register(GravitySource source)
{
Debug.Assert(!sources.Contains(source), "Duplicate registration of gravity source!", source);
//Debug.Assert(条件,消息,上下文对象);
sources.Add(source);
}
public static void Unregister(GravitySource source)
{
Debug.Assert(sources.Contains(source),"Unregistration of unknown gravity source!", source);
sources.Remove(source);
}
注:Debug.Assert(条件,消息,上下文对象) 若第一个参数是false;若提供了第二个参数,则使用该消息;第三个参数将在控制台中选中该消息时在编辑器中高亮显示
专用刚体组件 CustomGravityRigidBody
- 获取重力
- 休眠:在静止或加速度足够下时休眠,以节省性能
- 浮力延迟:在空中或其他原因暂时悬停,但仍有可能下落;悬停时间足够长时,会受力。
- 漂浮入睡:在突然改变重力或其他情况时,入睡的物体会反常识的飘在空中。加上bool的判断是否允许入睡后,如果不允许,则会很快被唤醒
Complex Gravity 复杂引力
引力平面、球体与方块
Support multiple gravity sources 支持多种重力源
默认重力源 GravitySource
-
提供一个公共的
GetGravity方法
这个默认重力源可以挂载到任何场景中 -
发起注册或注销源的请求
void OnEnable(){
CustomGravity.Register(this);//this是一个GravitySource
}
void OnDisable(){
CustomGravity.Unregister(this);
}
- 有
virtual关键字的方法可以在子类重写
e.g.
public virtual Vector3 GetGravity(Vector3 position){
return Physics.gravity;
}
重力平面 GravityPlane
- 是
GravitySource的子类 - 重写
GetGravity,修改重力逻辑
Visualizing the Plane可视化平面
将重力的范围可视化 看 GravityPlane.OnDrawGizmos
Gizmos.matrix = Matrix4x4.TRS(transform.position,transform.rotation,scale);
//Matrix4x4.TRS(pos,rot,scale);利用位置旋转和缩放构建一个4x4变换矩阵
//Gizmos.matrix设置原点到赋值位置
Vector3 size=new Vector3(1f, 0f, 1f);
Gizmos.color= Color.yellow;//绘制黄色
Gizmos.DrawWireCube(Vector3.zero, size);
实现在重力源位置绘制黄色线框正方形
Limit the range of gravity 限制引力的作用范围
- 设定
range,将 法线方向 与 重力源位置到当前位置的向量 做点集,结果为距离distance。若距离大于range,则重力为0 range>0时,将作用范围绘制青色正方形
Camera Alignment Speed 相机对齐速度
- 重力变化时,相机也应调整视角
- 通过计算两个向上向量的点集,通过
Mathf.Acos转化成度数,再乘Mathf.Rad2Deg来求二者之间的夹角
注:arccos函数要进行Mathf.Clamp进行钳制,将点击进行归一化 - 旋转角度足够大时,要进行
Quaternion.Slerp的插值,得到正确的旋转
看OrbitCamera.UpdateGravityAlignment()
Make gravity decrease with distance 使引力随距离衰减
- 若距离
distance>0,则g *= 1f - distance / range; - 实现在两个平面的边缘的重力的过渡更加平滑
Inverted spherical gravity 倒置球形重力源
半径与衰减范围
- 新建
GravitySphere继承GravitySource - 设立外半径,外衰减半径,半径,内衰减半径等变量
- 外衰减半径>外半径>半径>内衰减半径
- 将这些变量染色,黄、青
- 在外衰减半径和外半径之间、半径和内衰减半径之间,重力衰减
- 看
GravitySpere.GetGravity()
Gravity Boxes 重力盒
GravityBox,重力会直接向下作用于最近的面,而不是随方向平滑进行
- 边界距离:红框
- 内部距离:黄框
- 内部衰减距离:青框
- 外侧距离:黄框
- 外侧衰减距离:青框
- 外侧距离和外侧衰减距离都是外侧矩形的外侧延申
- 绘制四个边,形成矩形
- 边界立方体
- 用线框立方体代替需要大量代码的圆角指示器
- 距离/sqrt(3)
- 重写
GetGravity()- 判断在立方体内部还是外部
outside- case 0:在内部
- case 1,2,3:在外部
- 判断在立方体内部还是外部
Moving the Ground 移动地面
Create animating platforms 创建动态平台
Animation 动画
- Window/Animation/Animation打开Animation界面
- 在Animation界面添加一个Animation controller (管理者,动画片段的切换和混合)和 Animation Clip(单个动画文件,只包含一个动作的数据)
- Controller拖到对应物体的Animator中的Controller
- 在Animator界面点击红点(Preview右侧)
- 底部切换Curves可以调节动画在过程中的速度等
动画时序
- Animate Physics 使小球运动与PhysX同步
Keep track of a connected body 跟踪连接的身体
connectedBody判断是否有另一个对象(有RigidBody组件)- 在
EvaluateCollision中,在地面则直接connectedBody=collision.rigidbody;若在斜面,在不接触地面条件下,赋值 - 在
SnapToGround中,若检测到地面,则直接赋值为地面 - 在
ClearState中重置为null
连接时
- 设
RigidBody previousConnectedBody;V3 connectionVelocity;V3 connectionWorldPosition; - 当连接时、连接体是运动学物体(
connectedBody.isKinematic即由动画控制的物体)且连接体质量大于小球质量时,更新小球的状态 - 更新小球状态:当前
connectedBody==previousConnectedBody时,算出connectionVelocity,求出位移 - 在
AdjustVelocity用球体速度velocity-连接体速度connectionVelocity得到相对速度,从而在运动体上移动
旋转时
- 设
connectionLocalPosition作为局部坐标 - 如果连接,局部坐标转化为世界坐标
connectionMovement = connectedBody.transform.TransformPoint(connectionLocalPosition);
- 算出连接体的速度(位移/单位时间)
- 世界坐标 = 小球坐标
- 世界坐标转化成局部坐标
connectionLocalPosition = connectedBody.transform.InverseTransformPoint(connectionWorldPosition);
Climbing 攀爬
Make surfaces climbable and detect them 使表面可攀爬,并检测
判断可攀爬
最大爬升角度maxClimbingAngle
检测可攀爬表面
攀爬法线climbNormal;
在EvaluateCollision中,检测是在平地上、陡坡上、墙上。
**if** (upDot >= minClimbDotProduct) {
climbContactCount += 1;
climbNormal += normal;
connectedBody = collision.rigidbody;
}
不可攀爬的墙壁
- 在Project Setting新增
Unclimbable层(layer) - 为小球添加爬升遮罩
climbMask - 爬升遮罩中不包括
Unclimable - 将不可攀爬的物品的layer设置成
Unclimbable
攀爬材质
行走和攀爬时的材质(类比人动作)不同
- 设置
Material normalMaterial = default, climbingMaterial = default; MeshRenderer meshRenderer;- 在
Awake中meshRenderer=GerComponent<MwshRenderer>(); - 在
Update里更新材质
Moving along Walls 沿墙移动
是否在攀爬
boolCheckClimbing () {
if(Climbing) {
groundContactCount = climbContactCount;
contactNormal = climbNormal;
return true;
}
return false;
}
在不攀爬时才用重力
墙面相对运动
在攀爬时,z轴为向上轴,x轴为向上轴与法线的差集
平地行走时,z为前轴,x为右轴
攀爬速度,加速度
与平地原理相同,再设两个变量,进行控制
绕外侧墙角爬升
此时已经可以内侧墙角爬升
- 外侧爬升,在小球法向量的反方向给个加速度(模拟抓握力)
- 可能在90°内角被卡住,* 0.9f 为了减速不至于完全停住
velocity -= contactNormal * (maxClimbAcceleration * Time.deltaTime*0.9f); - 从墙面跳起时,加速度可能降低,因此在刚跳跃时禁用攀爬
bool Climbing => climbContactCount > 0 && stepsSinceLasrJump > 2;
可选攀登
Setting新增Climb
小球新增变量desireClimbing
Input.GetButton("Climb");
GetButton 是持续摁着才能发挥作用
想要攀爬,但还在地面时
- 提前减速,做好攀爬准备
- 攀爬瞬间,加重力,使小球贴近地面
velocity+=(gravity- contactNormal * (maxClimbAcceleration * 0.9f)) * Time.deltaTime;
在斜坡上静止
在斜坡上,沿法线给一个向下的力,使其静止
velocity += contactNormal * (Vector3.Dot(gravity,contactNormal)*Time.deltaTime);
爬出裂缝
- 设置
lastClimbNormal - 在
EvaluateCollision更新lastClimbNormal=normal; - 多个爬升接触点时,使用最后一个爬升法线,爬出裂缝
bool CheckClimbing{
……
if (climbContactCount > 1)
{
climbNormal.Normalize();
float upDot =Vector3.Dot(upAxis, climbNormal);
if(upDot >= minGroundDotProduct)
{
climbNormal = lastClimbNormal;
}
}
……
}
Swimming 游泳
在水中移动和漂浮
Detect water volumes 检测水体体积
水景
水面用中低alpha的蓝色贴图表示
注意:材质的Surface Options/Surface Type 要选择Transparent(透明的)才能应用Alpha值
水体的体积用触发器的碰撞体实现
即Box Collider/Is Trigger为true,并将Layer设置成Water
忽略触发器碰撞体
SnapToGround和OrbitCamera.LateUpdate忽略水体碰撞体
检测水
小球设置swimmingMaterial,在水中时变化材质,指示作用
设置InWater属性,来检测是否在水中
进入和呆在水体碰撞体时进行状态检测
void OnTriggerEnter(Collider other){
if ((waterMask & (1 << other.gameObject.layer)) != 0){
……
}
}
void OnTriggerStay(Collider other){
if ((waterMask & (1 << other.gameObject.layer)) !=0){
……
}
}
Apply water drag and buoyancy 应用水的阻力和浮力
浸没度:Submergence
submergence>0时InWater为true
浸没范围:submergenceRange 和 浸没偏移:submergenceOffset
![[Pasted image 20260805184853.png]]
更新小球在水中的浸没度 void EvaluateSubmergence()
向下射线投射
if (Physics.Raycast(body.position + upAxis * submergenceOffset, -upAxis, out RaycastHit hit, submergenceRange , waterMask, QueryTriggerInteraction.Collide)){//检测触发器
submergence = 1f - hit.distance / submergenceRange;
}else{
submergence = 1f;
}
水阻:WaterDrag=1f
在FixedUpdate(),如果InWater,则有水阻(与浸没度相关,要乘Submergence)
if (InWater){
velocity *= 1f - waterDrag * submergence * Time.deltaTime;
}
注:速度变化在调用AdjustVelocity()之前
浮力:buoyancy
在FixedUpdate()里,若在水中,则根据浸没度给向上的浮力
else if(InWater){
velocity += gravity * ((1f - buoyancy * submergence) * Time.deltaTime);
}
Swim through water 在水中游动
游泳阈值:swimThreshold
在水深足够时才能游泳(不一定完全浸没)
游泳状态Swimming
浸没度>=游泳阈值时,进入游泳状态
bool Swimming => submergence >= swimTreshold;
判断是否游泳,并更改相应状态
bool CheckSwimming() {
if (Swimming){
groundContactCount = 0;
contactNormal = upAxis;
return true;
}
return false;
}
并且在UpdateState()中调用
最大游泳速度:maxSwimSpeend和 游泳加速度:maxSwimAcceleration
未完全浸没前,速度应随深度变化。进行UpdateVelocity插值
else if (InWater)
{
float swimFactor = Mathf.Min(1f, submergence / swimTreshold);
//Mathf.LerpUnclamed(a,b,t),a是起始值,b是结束值,t是插值因子,返回a和b之间的线性插值
acceleration = Mathf.LerpUnclamped(OnGround ? maxAcceleration : maxAirAcceleration, maxSwimAcceleration, swimFactor);
speed = Mathf.LerpUnclamped(maxSpeed, maxSwimSpeed, swimFactor);
xAxis = rightAxis;
zAxis = forwardAxis;
}
潜水与上浮
在Edit/Project Settings/Input Manager中设置一个叫UpDowm,如果在Swimming状态下就使用
另外,在AdjustVelocity,若Swimming更新向上向下的速度
if (Swimming)
{
float currentY = Vector3.Dot(relativeVelocity, upAxis);
float newY = Mathf.MoveTowards( currentY, playerInput.z * speed, maxSpeedChange);
velocity += upAxis * (newY-currentY);
}
水中攀爬和跳跃
游泳时,攀爬禁止 ,Update()
desiresClimbing = false;
在水中时,跳跃难度增加,Jump()
if (InWater)
{
jumpSpeed *= Mathf.Max(0f, 1f - submergence / swimTreshold);//在水中跳跃速度降低
}
在流动的水中游泳
还未学到流动水流,暂时只处理整体运动的水体
在EvaluteSubmergence()和运动的水体碰撞体一起运动
if (Swimming) {
connectedBody =collider.attachedRigidbody;
}
Make objects float 让物体漂浮
- 针对
CustomGravityRigidBody进行修改
浸没度等
添加可配置的下潜偏移、下潜范围、浮力、水阻和水遮罩
添加与MovingSphere一样的OnTriggerEnter(),OnTriggerStay(),EvaluteSubmergence()
在OnTriggerStay()中,进入睡眠状态时,不进行调用
浮动
给速度、角速度以水阻
收到的阻力和浸没度有关
在偏移点给浮力
稳定漂浮
把浮力偏移改成数组
gravity = CustomGravity.GetGravity(body.position);
float dragFactor = waterDrag * Time.deltaTime/buoyancyOffsets.Length;//水中阻力系数
float buoyancyFactor=-buoyancy / buoyancyOffsets.Length;//浮力系数
for (int i = 0; i < submergence.Length; i++)
{
if (submergence[i] > 0f)
{
float drag = Mathf.Max(0f, 1f - dragFactor * submergence[i] );//水中阻力
body.linearVelocity *= drag;
body.angularVelocity *= drag;//旋转速度(角速度)
body.AddForceAtPosition(gravity * (buoyancyFactor * submergence[i]), transform.TransformPoint(buoyancyOffsets[i]), ForceMode.Acceleration);
submergence[i] = 0f;
}
}
body.AddForce(gravity, ForceMode.Acceleration);
//AddForce(V3要施加的力向量,ForceMode力的类型)施加力
//ForceMode.Acceleration是持续加速度,忽略质量
意外悬浮
对于大物体,可能出现顶部悬浮在空中,无法落回水中
设safeFloating=false;在EvaluteSubmergence()
若某个偏移点出水面,则不算这个点为浸没,就能落回水中
else if(!safeFloating||Physics.CheckSphere( p,0.01f,waterMask,QueryTriggerInteraction.Collide))
//球心位置,球体半径,层掩码,检测触发器
{
submergence[i] = 1f;
}
Reactive Environment 反应式环境
Acceleration Zone加速区域
能进入其中的任何物体加速的区域。
创建AccelerationZone类
Zone Component 区域组件
当碰撞体进入触发器时,需要进行加速,所以创建OnTriggerEnter和OnTriggerStay,即进入触发器和呆在触发器里都进行加速 Accelerate() 操作,实现瞬时加速&&持续加速效果
void OnTriggerEnter(Collider other)
{
Rigidbody body = other.attachedRigidbody;
if (body)
{
Accelerate(body);
}
}
void OnTriggerStay(Collider other)
{
Rigidbody body = other.attachedRigidbody;
if (body)
{
Accelerate(body);
}
}
防止吸附到地面
对于有MovingSphere的小球,会进行捕捉地面,在此时应该禁用捕捉地面功能
即在MovingSphere里添加以下方法
public void PreventSnapToGround()
{
stepsSinceLastJump = -1;
}
加速方法
新建Accelerate()方法
是否吸附
那我们的小球应该什么时候不进行吸附,这时需要进行判断
if(body.TryGetComponent(out MovingSphere sphere))
{
sphere.PreventSnapToGround();
}
持续加速时配置加速度
配置加速度的代码和在平地上加速的代码 原理相近
[SerializeField, Min(0f)] float speed = 10f, acceleration = 10f;
void Accelerate(){
……
if(acceleration>0f)
velocity.y = Mathf.MoveTowards( velocity.y,speed,acceleration*Time.deltaTime);
else velocity.y = speed;
……
}
任意方向
将小球的速度转化为局部空间,然后再进行加速,这样就能实现相对于触发器的y轴的加速了。
void Accelerate(Rigidbody body)
{
Vector3 velocity = transform.InverseTransformDirection(body.linearVelocity);
……
body.linearVelocity = transform.TransformDirection(velocity);
}
Make a multi-purpose detection zone 创建一个多用途检测区域
加速区域只是检测区域的一个实例,其实对于简单的行为(如激活一个对象),只需专门为这个行为创建组件即可。更复杂的行为则由多个简单行为组而已。
以下,是检测区域 DetectionZone 的组件
检测区域
public class DetectionZone : MonoBehaviour
{
[SerializeField]
UnityEvent onEnter = default, onExit = default;
//检测碰撞体进入触发器时调用
private void OnTriggerEnter(Collider other)
{
onEnter.Invoke();
}
private void OnTriggerExit(Collider other)
{
onExit.Invoke();
}
}
此时在检视器有OnEnter和OnExit的列表,可以把其他组件加进去,实现复杂行为。
以材质选择器为例
材质选择器MaterialSelector
代码贴上,略。
using UnityEngine;
public class MaterialSelector : MonoBehaviour
{
[SerializeField] Material[] materials = default;
[SerializeField] MeshRenderer meshRenderer = default;
public void Select(int index)
{
if(meshRenderer && materials != null &&
index >= 0 && index < materials.Length)
{
meshRenderer.material = materials[index];
}
}
}
在OnEnter/+后,选择MaterialSelector.Select(int),下面写整型数字,就能实现对材质的选择。OnExit同理
多个组件,onFirstEnter&&onLastExit
加个链表就行了。
但是只有这样并不可靠,因为游戏对象或碰撞器被禁用,停用或销毁时,它不会被调用。
- 碰撞体不可用
我们应该:在每个物理步中,检查链表里的碰撞体是否有效。如下
private void Awake(){ enabled = false ;}
private void FixedUpdate()
{
//在固定更新中检查碰撞体是否仍然存在
for (int i = 0; i < colliders.Count; i++)
{
Collider collider = colliders[i];
if (!collider || !collider.gameObject.activeInHierarchy)
//如果碰撞体不存在或不在层级中
{
colliders.RemoveAt(i--);
if (colliders.Count == 0){
onLastExit.Invoke();
enabled = false;
}
}
}
}
private void Awake(){ enabled = false ;}
作用:在组件被唤醒后及最后一个碰撞器退出后禁用,节省性能
- 检测区域不可用
当物体被禁用时,清空所有记录的碰撞体,并触发“最后一个物体已离开”的事件,避免因突然禁用导致事件遗漏
private void OnDisable()
{
#if UNITY_EDITOR //只有在编辑器中才会执行,打包后不会执行
if (enabled && gameObject.activeInHierarchy)
{
return;
}
#endif
if (colliders.Count > 0)
{
colliders.Clear();
onLastExit.Invoke();
}
}
更复杂的行为
在OnFirstEnter()&&OnLastExit()中添加简单组件
Move objects via simple interpolation triggered by events 通过时间触发的简单插值来移动物体
自动滑块AutomaticSlider
能通过autoReverse的变化实现自动反转
SmoothValue使运动更加平滑
using UnityEngine;
using UnityEngine.Events;//通用事件
//自动推进的值渐变器
public class AutomaticSlider : MonoBehaviour
{
[SerializeField,Min(0.01f)]
float duration = 1f;//持续时间
[SerializeField]
bool autoReverse = false, smoothstep = false;//自动反转,平滑步进
[System.Serializable]
public class OnValueChangedEvent : UnityEvent<float> { }
[SerializeField]
OnValueChangedEvent onValueChanged = default;//值改变事件
float value;//当前值
public bool Reversed { get; set; }//是否反转
public bool AutoReverse {
get=>autoReverse;
set=>autoReverse=value;
}//是否自动反转
//平滑值
float SmoothValue => 3f * value * value - 2f * value * value * value;
private void FixedUpdate()
{
float delta=Time.deltaTime / duration;
if (Reversed)
{
value -= delta;
if (value <= 0f)
{
if (AutoReverse)
{
value = Mathf.Min(1f, -value);
Reversed = false;
}
else
{
value = 0f;
enabled = false;
}
}
}
else
{
value += delta;
if (value >= 1f)
{
if (AutoReverse)
{
value = Mathf.Max(0f, 2f - value);
Reversed = true;
}
else{
value = 1f;
enabled = false;
}
}
}
onValueChanged.Invoke(smoothstep ? SmoothValue:value);
}
}
插值器
最简单的位置插值器
using UnityEngine;
public class PiositionInterpolator : MonoBehaviour
{
[SerializeField]
Rigidbody body = default;
[SerializeField]
Vector3 from = default, to = default;
public void Interpolate(float t)
{
body.MovePosition(Vector3.LerpUnclamped(from, to, t));
}
}
局部插值
[SerializeField] Transform relativeTo= default;
public void Interpolate(float t)
{
Vector3 p;
if(relativeTo)
{
p = relativeTo.TransformPoint (Vector3.LerpUnclamped(from, to, t));
}
else
{
p = Vector3.LerpUnclamped(from, to, t);
}
body.MovePosition(p);
}
relativeTo作用:当前物体相对relativeTo指向物体进行运动插值。
Rolling 滚动
Accelerate based on current velocity 根据当前速度加速
原来的速度控制方法在x轴和z轴同时减速时,可能出现x轴速度为0,但z轴还有速度的情况
更改后,将限制在最大速度变化范围内,同时作用于x、y、z轴。
Vector3 adjustment;
adjustment.x = playerInput.x * speed - Vector3.Dot(relativeVelocity, xAxis);
adjustment.z =playerInput.z * speed - Vector3.Dot(relativeVelocity, zAxis);
adjustment.y = Swimming ? playerInput.y * speed - Vector3.Dot(relativeVelocity, upAxis) : 0f;
adjustment = Vector3.ClampMagnitude(adjustment, acceleration * Time.deltaTime);
velocity += xAxis * adjustment.x + zAxis * adjustment.z;
Make the sphere visually roll 使球体在视觉上滚动
现在球看起来光秃秃的,没有变化,看不出来球在滚动。
为了看起来在滚动,我们给球体应用一张纹理。
球体子对象
目前,移动的小球本身不旋转,为其添加一个球形子对象,赋予其纹理,并从父级移除mesh renderer和filter components。
这样纹理在旋转时,就有小球在旋转的效果
调整球体材质
现在父级的小球已经没有材质了,所以新建Transform类型的ball(记得在Awake里得到子级的mesh),将子级导入,从而外显子级的材质。
子级材质的运动
将所有相关代码都放在UpdateBall方法中,便于理解和使用。
我们需要知道球转一圈的长度,即球的周长,也就需要知道半径,因此新建float ballRadius,(要与父级小球半径相同)。
Vector3 movement = body.linearVelocity * Time.deltaTime;
float distance = movement.magnitude;
float angle = distance * (180f / Mathf.PI) / ballRadius;
ball.localRotation = Quaternion.Euler(Vector3.right * angle)* ball.localRotation;
暂以x轴为旋转轴。
旋转轴
为了适用于任意方向的运动,必须根据运动方向和接触法线来推到出旋转轴。
新建lastContactNormal变量,用于记录已经清空的上帧的法线,在ClearState()中,contactNormal清空之前。
回到UpdateBall,旋转轴由lastContactNormal与movement的叉乘决定
Vector3 rotationAxis = Vector3.Cross(lastContactNormal, movement).normalized;
在distance<0.001f时,中止
使小球对齐
由于纹理是竖型条纹,我们让小球运动也沿着条纹滚动。
这部分对齐的东西和摄像机对齐有共通。
原理过,只把对齐改为基于行进的距离而非时间,留代码
Quaternion AlignBallRotation(Vector3 rotationAxis, Quaternion rotation, float traveledDistance)
{
Vector3 ballAxis = ball.up;
float dot = Mathf.Clamp(Vector3.Dot(ballAxis, rotationAxis), -1f, 1f);
float angle = Mathf.Acos(dot) * Mathf.Rad2Deg;
float maxAngle = ballAlignSpeed * traveledDistance;
Quaternion newAlignment =
Quaternion.FromToRotation(ballAxis, rotationAxis) * rotation;
if (angle <= maxAngle)
{
return newAlignment;
}
else
{
return Quaternion.SlerpUnclamped(
rotation, newAlignment, maxAngle / angle
);
}
}
Align the sphere with its motion 使球体与其运动方向保持一致
在某些特殊情况下,小球并不能恰当的滚动。
陡坡
新增lastSteepNormal,并进行记录。
回到UpdateBall,在判断小球在什么情况(如爬行,游泳等)前,默认法线是上一帧平地法线,在if条件句里进行修改。
忽略被向上的运动
由于我们通过三个维度确定小球的旋转和朝向,重力不均匀时,垂直起跳可能出现抖动,所以现在忽略相对的垂直运动。
movement -= rotationPlaneNormal * Vector3.Dot(movement, rotationPlaneNormal);
空中与水中的旋转
新建rotationFactor,在某个状态变为某个数,可以自己设个变量调,也可以固定。
Stay aligned with the ground,even when it moves 即使地面移动,也保持与地面齐平
现在,小球脚下地面在运动,小球也会运动。
现在要小球和底下的地面保持相对静止。
新建lastConnectionVelocity,在connectionVelocity被清理前进行赋值。
回到UpdateBall
Vector3 movement = (body.linearVelocity - lastConnectionVelocity) * Time.deltaTime;
另一个问题出现了,小球不能跟着底下的地面一起旋转了
以下是解决方法:
Quaternion rotation = ball.localRotation;
if (connectedBody && connectedBody == previousConnectedBody)
{
rotation = Quaternion.Euler(connectedBody.angularVelocity * (Mathf.Rad2Deg * Time.deltaTime)) * rotation;
if (distance < 0.001f)
{
ball.localRotation = rotation;
return;
}
}
else if( distance < 0.001f)
{
return;
}
float angle = distance * rotationFactor *(180f / Mathf.PI) / ballRadius;
Vector3 rotationAxis = Vector3.Cross(rotationPlaneNormal, movement).normalized;
rotation = Quaternion.Euler(rotationAxis * angle) * rotation;
浙公网安备 33010602011771号