Catlike-Coding教程Movement学习笔记

Posted on 2026-08-25 20:25  Latencale  阅读(18)  评论(0)    收藏  举报

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 使运动相对于摄像机进行

  1. 在MovingSphere中加入变量 输入空间 playerInputSpace = default;

  2. 然后在

	if (playerInputSpace) {
		desiredVelocity = playerInputSpace.TransformDirection(			playerInput.x, 0f, playerInput.y) * maxSpeed;
	}
  1. 归一化
	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 倒置球形重力源

半径与衰减范围

  1. 新建GravitySphere继承GravitySource
  2. 设立外半径,外衰减半径,半径,内衰减半径等变量
    • 外衰减半径>外半径>半径>内衰减半径
  3. 将这些变量染色,黄、青
  4. 在外衰减半径和外半径之间、半径和内衰减半径之间,重力衰减
  • 看 GravitySpere.GetGravity()

Gravity Boxes 重力盒

GravityBox,重力会直接向下作用于最近的面,而不是随方向平滑进行

  1. 边界距离:红框
  2. 内部距离:黄框
  3. 内部衰减距离:青框
  4. 外侧距离:黄框
  5. 外侧衰减距离:青框
    • 外侧距离和外侧衰减距离都是外侧矩形的外侧延申
    • 绘制四个边,形成矩形
  6. 边界立方体
    • 用线框立方体代替需要大量代码的圆角指示器
    • 距离/sqrt(3)
  7. 重写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 跟踪连接的身体

  1. connectedBody判断是否有另一个对象(有RigidBody组件)
  2. 在EvaluateCollision中,在地面则直接connectedBody=collision.rigidbody;若在斜面,在不接触地面条件下,赋值
  3. 在SnapToGround中,若检测到地面,则直接赋值为地面
  4. 在ClearState中重置为null

连接时

  1. 设RigidBody previousConnectedBody;V3 connectionVelocity;V3 connectionWorldPosition;
  2. 当连接时、连接体是运动学物体(connectedBody.isKinematic即由动画控制的物体)且连接体质量大于小球质量时,更新小球的状态
  3. 更新小球状态:当前connectedBody==previousConnectedBody时,算出connectionVelocity,求出位移
  4. 在AdjustVelocity用球体速度velocity-连接体速度connectionVelocity得到相对速度,从而在运动体上移动

旋转时

  1. 设connectionLocalPosition作为局部坐标
  2. 如果连接,局部坐标转化为世界坐标
connectionMovement =   connectedBody.transform.TransformPoint(connectionLocalPosition);
  1. 算出连接体的速度(位移/单位时间)
  2. 世界坐标 = 小球坐标
  3. 世界坐标转化成局部坐标
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;
				}

不可攀爬的墙壁

  1. 在Project Setting新增Unclimbable层(layer)
  2. 为小球添加爬升遮罩climbMask
  3. 爬升遮罩中不包括Unclimable
  4. 将不可攀爬的物品的layer设置成Unclimbable

攀爬材质

行走和攀爬时的材质(类比人动作)不同

  1. 设置Material normalMaterial = default, climbingMaterial = default;
  2. MeshRenderer meshRenderer;
  3. 在Awake中meshRenderer=GerComponent<MwshRenderer>();
  4. 在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 是持续摁着才能发挥作用

想要攀爬,但还在地面时

  1. 提前减速,做好攀爬准备
  2. 攀爬瞬间,加重力,使小球贴近地面
    velocity+=(gravity- contactNormal * (maxClimbAcceleration * 0.9f)) * Time.deltaTime;

在斜坡上静止

在斜坡上,沿法线给一个向下的力,使其静止
velocity += contactNormal * (Vector3.Dot(gravity,contactNormal)*Time.deltaTime);

爬出裂缝

  1. 设置lastClimbNormal
  2. 在EvaluateCollision 更新lastClimbNormal=normal;
  3. 多个爬升接触点时,使用最后一个爬升法线,爬出裂缝
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

加个链表就行了。
但是只有这样并不可靠,因为游戏对象或碰撞器被禁用,停用或销毁时,它不会被调用。

  1. 碰撞体不可用
    我们应该:在每个物理步中,检查链表里的碰撞体是否有效。如下
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 ;}
作用:在组件被唤醒后及最后一个碰撞器退出后禁用,节省性能

  1. 检测区域不可用
    当物体被禁用时,清空所有记录的碰撞体,并触发“最后一个物体已离开”的事件,避免因突然禁用导致事件遗漏
    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;

Movement部分笔记到此结束