ScreenWrap 是一种游戏机制,当角色或物体从屏幕的一侧移动出去,会从对面另一侧“穿出来”,就像世界是环形的那样。
🎮 举个例子说明游戏画面: 你控制一艘飞船,在一个 2D 太空中飞行。 飞船从右边飞出屏幕 → 它会从左边重新出现。 飞船从下方飞出屏幕 → 会从上方跳出来。 像是一个穿了个洞的世界地图,角色在边界不断“转圈圈”,没有尽头。
这个机制有什么用?
- 节省资源:地图可以很小,视觉上却像是无限的。
- 增强玩法:让玩家思考更多空间策略,比如“绕过去攻击敌人”。
- 增强节奏:物体不会飞出地图消失,而是持续存在,战斗更激烈。
#[derive(Component, Reflect)]
#[reflect(Component)]
pub struct ScreenWrap;
fn apply_screen_wrap(
window_query: Query<&Window, With<PrimaryWindow>>,
mut wrap_query: Query<&mut Transform, With<ScreenWrap>>,
) {
let Ok(window) = window_query.single() else {
return;
};
let size = window.size() + 256.0;
let half_size = size / 2.0;
for mut transform in &mut wrap_query {
let position = transform.translation.xy();
let wrapped = (position + half_size).rem_euclid(size) - half_size;
transform.translation = wrapped.extend(transform.translation.z);
}
}
🧠 函数作用:实现 Screen Wrapping
这个函数的目标是:
让带有 ScreenWrap 组件的实体在超出主窗口边界后,从对侧重新出现,也就是典型的 screen wrapping / toroidal space 行为。
🔍 三个重点解释
1. + 256.0 —— ✨ 缓冲区 / 视觉延迟范围
let size = window.size() + 256.0;
这个 +256.0 代表给屏幕大小额外扩张一圈空间(每边128),有几个作用:
- 容错:允许物体稍微超出边界再 wrap,避免刚过边就被“闪现”回来,看着很突兀。
- 视觉更柔和:例如粒子或子弹在屏幕外还能飞一段,再从对侧进场。
- 边界留白:和美术资源大小、镜头边距有关。
🎮 类似《Asteroids》里飞船飞出屏幕一段才 wrap,不是刚挨边就“弹”回来。
2. .rem_euclid(size) —— 🧮 关键的“环绕数学”
(position + half_size).rem_euclid(size)
这一步是核心 —— 实现真正的“穿屏环绕”:
- 把坐标系移动到
[0, size]区间(通过加half_size) - 用
rem_euclid(size)让位置落回这个区间(类似取模,但始终是正数) - 再减回
half_size,恢复到原来的中心对称坐标系(以(0,0)为中心)
🧠 数学上就是构建一个 环面拓扑空间:让世界没有“边”,而是像圆环一样连续循环。
3. +half_size 和 -half_size —— 📦 坐标系平移
let wrapped = (position + half_size).rem_euclid(size) - half_size;
这两个操作就是:
+half_size:把原本中心在(0,0)的世界挪到左下角是(0,0),方便 rem_euclid 运算。-half_size:再把结果挪回中心坐标系,保持场景不乱。
图示解释:
原来 平移后 wrap后 再平移回来
-400~+400 → 0800 → (0800) → -400~+400
🧩 总结式解释
这段代码把世界看成一个 带边缘缓冲的环形地图,并让所有带 ScreenWrap 标记的物体:
在超出窗口边界后,从对侧无缝地出现,实现一个 Toroidal Screen Wrapping 世界。
其中:
+256.0是留给 wrap 的边界缓冲区rem_euclid是核心的“环绕算法”±half_size是为 wrap 做的坐标偏移处理
rem_euclid 进一步查看
Vec2的方法:
// 这里是针对
#[inline]
#[must_use]
pub fn rem_euclid(self, rhs: Self) -> Self {
Self::new(
math::rem_euclid(self.x, rhs.x),
math::rem_euclid(self.y, rhs.y),
)
}
#[inline]
pub fn rem_euclid(a: f32, b: f32) -> f32 {
f32::rem_euclid(a, b)
}
f32的方法,取模+abs,保证100%会落在指定范围内,至于r<0,是处理坐标系移动后xy可能不在范围内的异常情况:
#[doc(alias = "modulo", alias = "mod")]
#[rustc_allow_incoherent_impl]
#[must_use = "method returns a new number and does not mutate the original value"]
#[inline]
#[stable(feature = "euclidean_division", since = "1.38.0")]
pub fn rem_euclid(self, rhs: f32) -> f32 {
let r = self % rhs;
if r < 0.0 { r + rhs.abs() } else { r }
}