Unity基础:使用Transform控制物体缩放——localScale与lossyScale的区别

Unity基础:使用Transform控制物体缩放------localScale与lossyScale的区别

大家好,欢迎回到Unity教程系列。前两篇文章我们系统学习了Transform的移动和旋转操作,今天我们来完成Transform三部曲的最后一部分------缩放(Scale)。

缩放看起来简单------不就是让物体变大变小吗?但实际上,缩放涉及一个很容易混淆的概念:localScale和lossyScale的区别。此外,缩放在父子层级中的传递、非均匀缩放的副作用、以及负缩放的镜像效果,都是实际开发中会遇到的"坑"。这篇文章会把缩放相关的所有知识点一次性讲清楚。

一、缩放的基本操作

1.1 Inspector中的Scale

在Inspector的Transform组件中,Scale由X、Y、Z三个值组成。默认值为(1, 1, 1),表示物体的原始大小。

1.2 代码中操作缩放

csharp 复制代码
// 获取局部缩放
Vector3 scale = transform.localScale;

// 设置等比例缩放
transform.localScale = Vector3.one * 2f;  // 放大2倍

// 设置不同轴的不同缩放
transform.localScale = new Vector3(1, 2, 1);  // 只在Y轴拉长

// 增量缩放
transform.localScale += Vector3.one * 0.1f * Time.deltaTime;

// 乘以系数
transform.localScale *= 1.5f;

1.3 缩放的基本单位

缩放是乘法因子,不是绝对尺寸:

  • localScale = (1, 1, 1):物体的原始模型尺寸
  • localScale = (2, 2, 2):物体在每个轴上都变为原始尺寸的2倍
  • localScale = (0.5, 0.5, 0.5):物体缩小到原始尺寸的一半

📝 Cube默认边长是1单位(米)。将其localScale设为(3, 1, 2),它的实际尺寸就是3米宽、1米高、2米深。

二、localScale vs lossyScale

2.1 核心区别

💡 这是本文最重要的知识点。理解localScale和lossyScale的区别:

localScale(局部缩放):

  • 物体自身的缩放值
  • Inspector中显示的就是localScale
  • 可读写
  • 相对于父物体

lossyScale(世界缩放/实际缩放):

  • 物体在世界空间中的"实际"缩放
  • 只读!
  • 等于该物体和所有祖先物体localScale的乘积

2.2 计算示例

复制代码
场景结构:
GrandParent: localScale = (2, 2, 2)
└── Parent: localScale = (3, 3, 3)
    └── Child: localScale = (4, 4, 4)

Child的lossyScale = (2 × 3 × 4, 2 × 3 × 4, 2 × 3 × 4) = (24, 24, 24)
csharp 复制代码
void Start()
{
    // 子物体的localScale
    Debug.Log($"localScale: {transform.localScale}");   // (4, 4, 4)
    
    // 子物体在世界中的实际缩放(考虑了所有父物体的缩放)
    Debug.Log($"lossyScale: {transform.lossyScale}");    // (24, 24, 24)
}

2.3 为什么lossyScale是只读的

lossyScale不能直接设置,因为它是从localScale和所有祖先物体的缩放计算出来的。要改变物体的实际大小,你只能修改localScale(或者调整祖先物体的缩放)。

2.4 lossyScale的陷阱

⚠️ 当父物体有非均匀缩放(如(2, 1, 1))且子物体有旋转时,lossyScale可能包含"剪切"(Shear)变形,无法用简单的(Vector3.x, Vector3.y, Vector3.z)表示。这种情况下lossyScale的值可能不准确,一般应该避免这种组合。

三、缩放在父子层级中的传递

3.1 父物体缩放影响子物体

父物体的缩放会成倍地影响子物体的实际大小:

csharp 复制代码
// 父物体
parentTransform.localScale = new Vector3(2, 2, 2);

// 子物体的localScale保持(1, 1, 1)
// 但实际显示为2倍大小

3.2 缩放的"奇怪"效应------子物体位置也被缩放

💡 这一点很多人容易忽略:父物体的缩放不仅影响子物体的大小,还影响子物体的位置。

如果父物体缩放为(2, 2, 2),子物体的localPosition为(1, 0, 0),那么子物体到父物体的实际距离是 1 × 2 = 2单位(而不是1单位)。

csharp 复制代码
// 父物体在原点
parentTransform.position = Vector3.zero;
parentTransform.localScale = new Vector3(2, 2, 2);

// 子物体的localPosition = (1, 0, 0)
childTransform.localPosition = new Vector3(1, 0, 0);

// 子物体的实际世界位置 = 父物体位置 + 父物体旋转 × (子物体localPosition × 父物体localScale)
// = (0,0,0) + (1×2, 0, 0) = (2, 0, 0)

3.3 避免缩放传递带来的问题

在以下情况建议将物体的缩放保持为(1,1,1):

  • 有子物体的容器节点(避免子物体受到意外的影响)
  • 需要精确控制碰撞检测的场景(缩放会影响碰撞器的实际大小)
  • UI元素的父节点(避免子UI元素位置和大小计算混乱)

如果父节点需要缩放来整体调整大小,考虑:

  • 在3D建模软件中直接调整模型大小
  • 使用专门的Scale脚本控制每个需要缩放的物体

四、负缩放------镜像效果

4.1 负缩放的效果

将localScale的某个轴设为负值,物体会沿该轴镜像翻转:

csharp 复制代码
// 水平镜像(左右翻转)
transform.localScale = new Vector3(-1, 1, 1);

// 垂直镜像(上下翻转)
transform.localScale = new Vector3(1, -1, 1);

// 前后镜像
transform.localScale = new Vector3(1, 1, -1);

4.2 2D角色的左右翻转

负缩放在2D游戏中常用------让角色面向左或右:

csharp 复制代码
public class CharacterFlip : MonoBehaviour
{
    private bool facingRight = true;
    
    void Update()
    {
        float horizontal = Input.GetAxis("Horizontal");
        
        if (horizontal > 0 && !facingRight)
        {
            Flip();
        }
        else if (horizontal < 0 && facingRight)
        {
            Flip();
        }
    }
    
    void Flip()
    {
        facingRight = !facingRight;
        Vector3 scale = transform.localScale;
        scale.x *= -1;
        transform.localScale = scale;
    }
}

4.3 负缩放的注意事项

⚠️ 负缩放会影响:

  • 法线方向:镜像后物体表面的法线也会翻转,可能导致光照计算异常(物体内部被照亮,外部变暗)
  • 碰撞检测:某些物理计算在负缩放下可能不准确
  • 子物体:父物体的负缩放会传递给子物体

对于法线翻转的问题,可以在Shader中启用"双面渲染"(Cull Off),或在材质设置中调整。

五、缩放的实际应用

5.1 呼吸灯/脉冲效果

csharp 复制代码
using UnityEngine;

public class PulseEffect : MonoBehaviour
{
    public float pulseSpeed = 2f;
    public float pulseAmount = 0.2f;
    
    private Vector3 originalScale;
    
    void Start()
    {
        originalScale = transform.localScale;
    }
    
    void Update()
    {
        // 使用正弦波做缩放脉冲
        float scale = 1f + Mathf.Sin(Time.time * pulseSpeed) * pulseAmount;
        transform.localScale = originalScale * scale;
    }
}

5.2 伸缩挤压效果

csharp 复制代码
// 物体被挤压时变扁变宽
public IEnumerator SquashAndStretch(float duration)
{
    Vector3 originalScale = transform.localScale;
    Vector3 squashedScale = new Vector3(1.3f, 0.7f, 1.3f);
    Vector3 stretchedScale = new Vector3(0.7f, 1.3f, 0.7f);
    
    float halfDuration = duration / 2f;
    float elapsed = 0;
    
    // 挤压阶段
    while (elapsed < halfDuration)
    {
        elapsed += Time.deltaTime;
        float t = elapsed / halfDuration;
        transform.localScale = Vector3.Lerp(originalScale, squashedScale, t);
        yield return null;
    }
    
    elapsed = 0;
    
    // 拉伸恢复阶段
    while (elapsed < halfDuration)
    {
        elapsed += Time.deltaTime;
        float t = elapsed / halfDuration;
        transform.localScale = Vector3.Lerp(squashedScale, originalScale, t);
        yield return null;
    }
    
    transform.localScale = originalScale;
}

5.3 血条缩放

csharp 复制代码
public class HealthBar : MonoBehaviour
{
    public Transform fillBar; // 血条填充部分的Transform
    
    public void SetHealth(float currentHealth, float maxHealth)
    {
        float percentage = currentHealth / maxHealth;
        
        // 通过缩放控制血条长度
        Vector3 scale = fillBar.localScale;
        scale.x = Mathf.Clamp01(percentage);
        fillBar.localScale = scale;
    }
}

5.4 进度条动画

csharp 复制代码
public IEnumerator AnimateProgressBar(float targetProgress, float duration)
{
    Vector3 currentScale = fillBar.localScale;
    Vector3 targetScale = new Vector3(targetProgress, 1, 1);
    
    float elapsed = 0;
    while (elapsed < duration)
    {
        elapsed += Time.deltaTime;
        float t = elapsed / duration;
        fillBar.localScale = Vector3.Lerp(currentScale, targetScale, t);
        yield return null;
    }
    
    fillBar.localScale = targetScale;
}

六、缩放与碰撞器

6.1 缩放对碰撞器的影响

Collider碰撞器的实际大小受Transform缩放的影响:

csharp 复制代码
// BoxCollider的size是(1, 1, 1)
// localScale是(2, 2, 2)
// 实际碰撞体积 = size × localScale = (2, 2, 2)

6.2 避免缩放影响碰撞检测

如果不想碰撞器受缩放影响,可以:

  • 在场景中保持物体的缩放为(1,1,1)
  • 只调整Collider的size属性
  • 使用空父节点控制缩放,将碰撞器放在缩放为(1,1,1)的子节点上

七、常见问题

7.1 缩放后碰撞检测不准

过大的缩放可能导致物理计算精度下降。尽量将物体缩放在一个合理范围内(通常0.1到10倍之间)。

7.2 缩放后的渲染问题

非均匀缩放(不同轴的缩放不同)可能导致:

  • 法线贴图效果异常
  • 光照看起来"不对劲"
  • 模型的包围盒计算偏差

解决方案:在可能的情况下保持等比例缩放,或使用GPU Skinning等技术。

八、本篇总结

✅ 本文核心知识点回顾:

  • transform.localScale是物体自身的缩放(相对于父物体),可读写
  • transform.lossyScale是物体在世界中的实际缩放(考虑所有祖先),只读
  • 父物体的缩放会影响子物体的位置(距离被缩放)
  • 负缩放产生镜像效果,但可能影响法线和光照
  • 缩放影响碰撞器的实际大小
  • 使用正弦波和Lerp可以实现脉冲和动画缩放效果

📝 至此,Transform三部曲(移动、旋转、缩放)全部完成。下一篇文章将学习Instantiate实例化------在运行时动态创建游戏物体。

动手任务 :

① 创建一个多层级的父子结构,观察localScale和lossyScale的差异

② 用负缩放实现2D角色的左右翻转

③ 用缩放做一个脉冲呼吸灯效果


关于作者:拥有多年Unity开发经验的游戏开发者,专注Unity系统化教学与最佳实践分享。

相关推荐
郝学胜-神的一滴5 小时前
游戏引擎原理与实践 01:聊聊游戏引擎的前世今生
开发语言·c++·游戏引擎·产品运营·软件工程·产品经理·untiy
淡海水7 小时前
00-Unity和C#的GC剖析与对比-全篇导览
unity·c#·游戏引擎·gc
字节暗面20 小时前
Unity AssetBundle 热更新资源保护排查笔记
unity·游戏加固·资源加密
魔士于安6 天前
unity 丑陋哥布林 哥布林有humannoid 动画没 带动画
unity·游戏引擎·材质·贴图·模型
小贺儿开发7 天前
Unity 家居视频遥控(细节优化)
科技·unity·程序员·udp·视频·工具·通信
淡海水7 天前
13-04-面试-源码级深度追问链
数据结构·unity·面试·c#·游戏引擎·源码·il2cpp
小小数媒成员8 天前
UGUI 性能优化(详细,全,深入)
开发语言·unity·游戏引擎
林川~018 天前
Unity新输入系统使用笔记
unity·input system
喜欢的名字被抢了8 天前
01-Redis 基础篇:从定位到 Key 管理
redis·教程