Bevy 0.20 迁移指南:TextFont 默认字号改为 FontSize::Rem(1.),让 RemSize 真正成为根字号
【免费下载链接】bevyA refreshingly simple>项目地址: https://gitcode.com/GitHub_Trending/be/bevy
本文基于 Bevy 仓库中的官方迁移文档 default_font_size_is_rem.md 展开,讲清 0.20 版本中TextFont::default()默认字号从FontSize::Px(20.)变为FontSize::Rem(1.)的动机与行为差异,并结合 bevy_text 与 bevy_ui 的源码说明RemSize资源如何参与字号求值与重排,最后给出"保持固定字号"与"跟随根字号缩放"两种迁移写法。读完后你能判断自己的文本代码在 0.20 下是否需要改动,以及改动点应该写在哪里。
一、变更内容:默认字号从固定 20px 变为 1rem
迁移文档给出的核心变更只有一句话:
TextFont::default()now usesFontSize::Rem(1.)instead ofFontSize::Px(20.), so that theRemSizeresource actually sets the default font size.
也就是说:
- 旧行为(0.19 及以前):没有显式设置字号的文本,字号被硬编码为 20 逻辑像素(
FontSize::Px(20.)),与RemSize资源无关; - 新行为(0.20):默认字号改为
FontSize::Rem(1.),即"1 个根字号单位",由RemSize资源决定。由于RemSize的默认值恰好是 20 逻辑像素,所以在默认配置下渲染结果完全一致,但从此以后,凡是保持默认字号的文本,都会随RemSize的修改而整体缩放。
文档给出的迁移建议是:如果你希望文本大小固定不变,就显式设置像素字号:
// 0.20 TextFont { font_size: FontSize::Px(20.), ..default() }这个改动是 Bevy 文本子系统 rem/em 单位支持的一部分,与同 PR(#25231)中的另一篇迁移文档 val_em_and_rem.md 描述的Val::Em/Val::Rem变体、EmSize组件、FontSize::eval签名变更共同构成了 0.20 的"根字号"体系。
二、源码印证:TextFont::default()现在返回 Rem(1.)
在 crates/bevy_text/src/text.rs 中,TextFont的Default实现已经落实了这一变更:
impl Default for TextFont { fn default() -> Self { Self { font: Default::default(), font_size: FontSize::Rem(1.), style: FontStyle::Normal, weight: FontWeight::NORMAL, width: FontWidth::NORMAL, font_features: FontFeatures::default(), font_variations: FontVariations::default(), font_smoothing: Default::default(), } } }需要注意一个容易混淆的细节:TextFont的默认字号变了,但FontSize枚举自身的Default实现仍然是Px(20.),见 crates/bevy_text/src/text.rs:
impl Default for FontSize { fn default() -> Self { Self::Px(20.) } }因此,直接调用TextFont::default()(绝大多数 UI/2D 文本路径都走这里)得到的是Rem(1.);而单独使用FontSize::default()或..Default::default()展开font_size字段的地方得到的仍是Px(20.)。二者不一致正是这类迁移的常见"坑点",升级时建议全局搜索FontSize::default()和TextFont::default()确认行为是否符合预期。
三、RemSize:根字号资源的定义、注册与求值
3.1 资源定义与默认值
RemSize定义在 crates/bevy_text/src/text.rs:
/// Default [`RemSize`] and [`EmSize`], in logical pixels pub const DEFAULT_REM_SIZE_PX: f32 = 20.0; /// The root font size, in logical pixels. /// /// `Rem` units always resolve against this one global resource, including /// `FontSize::Rem`, `LetterSpacing::Rem` and `Val::Rem`. #[derive(Resource, Copy, Clone, Debug, PartialEq, Deref, DerefMut, Reflect)] pub struct RemSize(pub f32); impl Default for RemSize { fn default() -> Self { Self(DEFAULT_REM_SIZE_PX) } }要点:
RemSize(pub f32)是单一全局Resource,单位为逻辑像素;默认值 20.0 对应常量DEFAULT_REM_SIZE_PX;- 文档注释明确了它的作用域:
FontSize::Rem、LetterSpacing::Rem和Val::Rem三种 rem 单位都只解析到这一个全局资源; - 该资源由
TextPlugin在插件构建时注册(init_resource::<RemSize>()),见 crates/bevy_text/src/lib.rs。也就是说,只要你的应用启用了文本功能,RemSize就可用,无需手动注册。
3.2FontSize::eval:rem 如何变成像素
FontSize是文本字号的枚举,支持Px、Vw、Vh、VMin、VMax、Rem六种单位,定义见 crates/bevy_text/src/text.rs。求值入口是eval方法(crates/bevy_text/src/text.rs):
impl FontSize { /// Evaluate the font size to a value in logical pixels pub fn eval( self, // Viewport size in logical pixels logical_viewport_size: Vec2, // Base Rem size in logical pixels rem_size: RemSize, ) -> f32 { match self { FontSize::Px(s) => s, FontSize::Vw(s) => logical_viewport_size.x * s / 100., FontSize::Vh(s) => logical_viewport_size.y * s / 100., FontSize::VMin(s) => logical_viewport_size.min_element() * s / 100., FontSize::VMax(s) => logical_viewport_size.max_element() * s / 100., FontSize::Rem(s) => rem_size.0 * s, } } }FontSize::Rem(s)的求值就是rem_size.0 * s:Rem(1.)在当前默认配置下等于20.0 * 1.0 = 20逻辑像素,与旧版的Px(20.)数值相同——这就是迁移文档所说"renders identically"(渲染结果一致)的数学依据。
另一个值得注意的签名变化:0.20 中eval的第二个参数从裸f32改成了RemSize类型。这一点在 val_em_and_rem.md 中有专门记录:
// 0.19 let size = font_size.eval(logical_viewport_size, rem_size_px); // 0.20 let size = font_size.eval(logical_viewport_size, RemSize(rem_size_px));如果你自己封装了字号计算逻辑,这是需要跟着改的调用点。
3.3 渲染管线中的重排机制
默认字号变成Rem之后,"修改RemSize能全局缩放文本"这一行为是如何生效的?从源码结构看,答案在文本管线与重排标记里:
TextPipeline::update_buffer在遍历每个文本 span 时,如果发现字号是 rem 系单位,就把该文本块的uses_rem_sizes标记置为 true,见 crates/bevy_text/src/pipeline.rs:
match text_font.font_size { crate::FontSize::Vw(_) | crate::FontSize::Vh(_) | crate::FontSize::VMin(_) | crate::FontSize::VMax(_) => computed.uses_viewport_sizes = true, crate::FontSize::Rem(_) => computed.uses_rem_sizes = true, _ => (), }随后字号通过text_font.font_size.eval(logical_viewport_size, base_rem_size)求值(crates/bevy_text/src/pipeline.rs)。
ComputedTextBlock持有uses_rem_sizes标志(crates/bevy_text/src/text.rs),其needs_rerender方法会在RemSize发生变化且该文本块使用了 rem 单位时返回 true,从而触发重新排版(crates/bevy_text/src/text.rs):
pub fn needs_rerender( &self, is_viewport_size_changed: bool, is_rem_size_changed: bool, ) -> bool { self.needs_rerender || (is_viewport_size_changed && self.uses_viewport_sizes) || (is_rem_size_changed && self.uses_rem_sizes) }这个设计的实际效果是:只有使用了 rem(或视口)单位的文本才会因RemSize变化而重排,纯Px文本不受影响,避免不必要的全量重排开销。这也再次说明:0.20 之后"默认字号"的文本天然进入了 rem 依赖集合,而显式写成Px的文本则被排除在外——这正是迁移文档推荐"固定尺寸请显式写Px"的底层原因。
四、对 UI 布局与文本输入的影响
RemSize不只影响字号本身,它已经贯穿 UI 布局链路:
4.1 UI 节点的font_size转换
在 crates/bevy_ui/src/geometry.rs 中,Val到FontSize的转换显示:未显式设置字号的 UI 节点(Val::Auto)会被转成FontSize::Rem(1.),Val::Em(em)也会被归一化为FontSize::Rem(em)(em 相对于本节点字号解析,先折算成 rem 表达):
Val::Auto => FontSize::Rem(1.), Val::Px(px) => FontSize::Px(px), Val::Percent(percent) => FontSize::Rem(percent / 100.), Val::Rem(rem) => FontSize::Rem(rem), Val::Em(em) => FontSize::Rem(em), // see comment above也就是说,没有手动设置字号的 UI 文本,在 0.20 中会统一走 rem 路径,这是本次迁移影响面最大的场景。
4.2 文本输入组件的重排条件
bevy_ui的文本输入布局系统同样感知了这次变化。crates/bevy_ui/src/widget/text_input_layout.rs 中的重排条件明确加入了"字号是 rem 且RemSize已变化"的分支:
|| matches!(text_font.font_size, FontSize::Rem(_)) && rem_size.is_changed()并在光标宽度等计算中直接用text_font.font_size.eval(target.logical_size(), *rem_size)求值(同文件 L515)。所以基于TextInput的组件在RemSize变化时也会正确跟随缩放。
4.3 同 PR 的其他连带变更
由于 rem 求值需要"根字号 + 节点字号"两个输入,同一 PR 还带来了 val_em_and_rem.md 记录的 API 变更,做 0.20 升级时应一并检查:
Val、Val2、UiPosition、CornerRadius、RadialGradientShape、UiTransform::compute_affine、BorderRadius的resolve/compute_affine方法新增em_size: EmSize与rem_size: RemSize两个参数;ComputedNode新增em_size、rem_size字段,用于记录布局时实际采用的字号,方便下游(如 box shadow)复用;Node现在要求EmSize组件(从bevy_text导出、经bevy_ui::prelude可用),节点有TextFont时会在TextFont、RemSize或渲染目标信息变化时自动重算EmSize;没有TextFont时该值由应用自行维护,默认值为DEFAULT_REM_SIZE_PX(20 逻辑像素),且不跟随RemSize的后续变化——需要跟随根字号的布局值应使用Val::Rem。
五、迁移实操:如何写代码
5.1 场景一:保持固定字号(推荐大多数游戏 UI)
如果你的文本尺寸是美术规格定的固定像素,按迁移文档显式写死即可:
use bevy::prelude::*; fn setup(mut commands: Commands) { commands.spawn(Text2d::new("Hello Bevy 0.20")).insert(TextFont { font_size: FontSize::Px(20.), ..default() }); }也可以走链式构造器TextFont::from_font_size/with_font_size(见 crates/bevy_text/src/text.rs),例如TextFont::from_font_size(20_f32)会构造出Px语义的字号。仓库示例中的既有写法本身就是这种风格,如 examples/2d/text2d.rs 里的font_size: FontSize::Px(50.0)、examples/2d/texture_atlas.rs 中的FontSize::Px(42.0),这些文本在 0.20 中行为不变。
5.2 场景二:让文本跟随根字号缩放
如果你希望做"全局字号调节"(例如设置面板里的 A/A+ 选项、高分辨率适配),现在只需修改RemSize,所有保持默认(或显式Rem)的文本会自动重排:
fn change_root_font_size(mut rem_size: ResMut<RemSize>) { rem_size.0 = 32.0; // 所有 Rem(1.) 的默认文本变为 32 逻辑像素 }生效链路是:RemSize变化 → 使用 rem 字号的文本块needs_rerender为 true(crates/bevy_text/src/text.rs)→ 文本管线按rem_size.0 * s重新求值并排版(crates/bevy_text/src/pipeline.rs)。
注意一个行为差异:在旧版本中,同样的操作对"默认文本"完全无效(因为默认就是Px(20.));升级后同一份代码里"没写字号"的文本会开始响应RemSize。如果你在旧代码里依赖过"默认文本不会缩放"这一事实,需要在 0.20 中显式补上FontSize::Px。
5.3 相关示例参考
- examples/2d/text2d.rs:2D 文本示例,演示显式
FontSize::Px的常规写法; - examples/ui/text/letter_spacing.rs:展示了
RemSize与LetterSpacing::Rem/LetterSpacing::Px之间的换算方式(LetterSpacing::Rem(v / rem_size.0)),是理解"一切 rem 单位都以RemSize为基准"的良好参考。
六、小结
| 项目 | 0.19 及以前 | 0.20 |
|---|---|---|
TextFont::default()的字号 | FontSize::Px(20.) | FontSize::Rem(1.) |
FontSize::default() | FontSize::Px(20.) | 仍为FontSize::Px(20.) |
默认RemSize | —(对默认字号无作用) | 20.0逻辑像素,且真正决定默认字号 |
修改RemSize的影响 | 不影响默认字号文本 | 所有默认/Rem字号文本重排缩放 |
| 保持固定尺寸的写法 | 不写即可 | 显式TextFont { font_size: FontSize::Px(20.), ..default() } |
本次变更本质上是把 Bevy 文本系统的"根字号"概念补齐:RemSize从"仅供显式Rem单位使用的资源"升级为"所有默认文本字号的真正来源",并在渲染管线中通过uses_rem_sizes标记实现了按需重排。升级 0.20 时的检查清单很简单:确认默认字号文本是否需要固定(需要则显式写Px);检查自定义代码中FontSize::eval、Val::resolve等 API 调用是否补上了RemSize/EmSize参数;以及留意 val_em_and_rem.md 中Node对EmSize的新要求。
【免费下载链接】bevyA refreshingly simple>项目地址: https://gitcode.com/GitHub_Trending/be/bevy
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考