屏幕环绕ScreenWrap的实现

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);
    }
}

这个函数的目标是:
让带有 ScreenWrap 组件的实体在超出主窗口边界后,从对侧重新出现,也就是典型的 screen wrapping / toroidal space 行为。

let size = window.size() + 256.0;

这个 +256.0 代表给屏幕大小额外扩张一圈空间(每边128),有几个作用:

  • 容错:允许物体稍微超出边界再 wrap,避免刚过边就被“闪现”回来,看着很突兀。
  • 视觉更柔和:例如粒子或子弹在屏幕外还能飞一段,再从对侧进场。
  • 边界留白:和美术资源大小、镜头边距有关。

🎮 类似《Asteroids》里飞船飞出屏幕一段才 wrap,不是刚挨边就“弹”回来。

(position + half_size).rem_euclid(size)

这一步是核心 —— 实现真正的“穿屏环绕”:

  • 把坐标系移动到 [0, size] 区间(通过加 half_size
  • rem_euclid(size) 让位置落回这个区间(类似取模,但始终是正数)
  • 再减回 half_size,恢复到原来的中心对称坐标系(以 (0,0) 为中心)

🧠 数学上就是构建一个 环面拓扑空间:让世界没有“边”,而是像圆环一样连续循环。

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 做的坐标偏移处理

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 }
    }