沙漠赛车🏜️:世界上第一款仅使用 CSS 的滑动感知游戏!

2025-06-07

沙漠赛车🏜️:世界上第一款仅使用 CSS 的滑动感知游戏!

一款纯 CSS、无 JS、无复选框、支持滑动(滚动)的游戏。附带配置选项和音乐!


简介

我开发了《沙漠赛车》,旨在展示纯 CSS 滑动感知纯 CSS 碰撞检测这些独特且非同寻常的技巧。我相信这些技巧是同类中的首创。欢迎您提出质疑。本文涵盖了上述技巧,以及构建滑动感知游戏的整个过程。

今天(2024 年 3 月 27 日)再次检查,Gemini(谷歌的新人工智能)仍然认为滑动感知是不可能的。

双子座的截图,见下方标题


转录:

👨 — “我能仅使用 CSS 和 HTML 实现双向滑动检测吗?”
🤖 — “不可以,仅使用 CSS 和 HTML 实现双向滑动检测是不可能的。CSS缺乏确定滑动手势方向和意图所需的功能。”

✨ 创造力还没有过时!✨

 

CSS 是完成这项工作的合适工具吗?目前还不行。但是……CSS 一直在减轻 JavaScript 事件处理程序的负担。希望本文能对此有所贡献。无论如何,作为一名艺术家,为了揭穿某些看似不可能的事情而使用错误的工具来构建事物,是一种自然而然的冲动。我预感它会奏效,而事实也确实如此。

本文将采用互动形式,希望能给您带来启发。所以,请继续关注!(或者至少滚动查看精彩的 GIF


目录

  1. 这个想法
  2. 构建一个支持滑动的游戏
    1. 主要障碍(想法验证)
    2. 微调移动用户体验
    3. 次要障碍
  3. 经验教训
  4. 荣誉
  5. 致谢
  6. 常问问题

1. 理念

2023年10月6日,我偶然发现了一个全新的CSS特性:滚动驱动动画。这一切都要感谢Bramus那篇精彩的阐述文章。那天晚上,我通过WhatsApp向自己分享了以下想法:

WhatsApp 截图,日期为 2023 年 10 月 6 日。请参阅下方标题


👨 — “X 和 Y 滚动驱动动画,释放时自动对齐到中心,模拟滑动检测!!!使用 CSS 隐藏滚动条。”

 
你能感受到各种拼写错误、逗号放错、冠词缺失和介词缺失带来的兴奋。不用 JavaScript 能实现滑动感知吗?这个想法最终发展成了你今天看到的这个纯 HTML 和 CSS 游戏。去试试吧,然后回来看看我是如何实现的。

 

点击玩《沙漠赛车》

 

📙:原始 CSS 背景、使用 AI 生成的资产、来自Pixabay的音乐。

📙:scroll-timeline仅支持 Blink/Chromium 浏览器(Chrome 桌面版、Edge 桌面版以及 Android 版 Chrome)。对于 iPhone 用户:iOS 版 Chrome 只不过是换了皮肤的 Safari,所以请在 MacBook 上使用 Chrome(我们都知道你有)。


2. 构建一个支持滑动的游戏

这个过程需要多次原型验证、调整、重构,以及发明前所未见的 CSS 技巧(这可能会让你陷入探索的兔子洞)。

⚠️ 请记住,在我创建这个游戏的时候,我并不知道空格切换这个 hack。所以这个游戏的整个逻辑都依赖于接受数值的CSS属性。例如:我可以操作,animation-duration但不能animation-play-state

我们是在滑动还是滚动?

我们在底层使用了 CSS 滚动属性和实验性的滚动时间线 🧪。然而,对于触屏设备和触控板来说,实际操作实际上是滑动,因此得名滑动检测。我将其设计为滑动优先的游戏,并设置了鼠标滚轮检测作为后备机制。如果您的鼠标支持横向滚动,请记得在《沙漠赛车》的主屏幕上激活鼠标输入设置。
老鼠 鼠标输入配置

 

2.1.:主要障碍(验证想法)

· 2.1.a.:实现双向滚动驱动动画

我首先需要验证的是,我是否可以通过在两个轴上滚动来控制时间轴。起初,我被scroll-timeline-axis属性的定义误导了,因为它接受的值只有单向(xyblockinline)。为了解决这个问题,我嵌套了两个可滚动容器来分别控制每个轴。这仍然是移动设备上使用的解决方案,因为它可以限制运动并避免上下滑动时意外的水平运动。

项目进行到一半的时候,我偶然发现了Bramus巧妙的单元素双向解决方案:逗号分隔的滚动时间轴!一看就明白。我不会自动想到一个新的 CSS 属性会支持逗号,所以当时根本没想过。

迅速回到中心

这种技术与普通滚动驱动动画的区别有两个方面:目的性和可重复性。

  1. 目的:我们不是为了给内容添加动画而滚动,而是为了检测滑动。DOM 保持不变,只有--x--y更新。
  2. 可重复性:通过弹回中心,我们可以根据需要重复操作。我们不希望滚动框总是重新开始,但我们确实希望能够再次滑动。

滑动感知 CSS 的基本结构

💡:[ 上一作品 ] Adam Argyle还混合使用了滚动动画和滚动捕捉功能,以模拟移动设备上的“刷新页面”滑动交互。文章👏

主要技巧

我们使用两个独立的滚动时间轴来控制水平轴和垂直轴的变化。为了激活滚动时间轴,我们需要内容大于滚动包装器——正如您所期望的那样。根据游戏目标,我们可以设置任意大小的网格(建议使用 3x3)。我们还可以决定是否捕捉回中心。对于《沙漠赛车》来说,跳跃轴(y 轴)会捕捉回地面,但变换车道不会触发捕捉动作。您还可以使用 Houdini 的 @property 声明来实现 0 到 1 之间的插值,从而使滑动动作即使最小的移动也能被检测到。这样,您就可以创建诸如画圆之类的动作。

使用设置⚙️菜单来尝试不同的滑动感知配置。

📙:设置菜单也是用纯 CSS 编写的,因为“为什么不呢?”

· 2.1.b.:检测碰撞

为了检测碰撞,我检查车辆的当前单元是否也是障碍物单元。
--collision-on-cell-4: calc(var(--vehicle-on-cell-4) * var(--obstacle-on-cell-4))

分为以下步骤:

  1. 通过将坐标转换--x并滑动到其对应的当前网格单元来检测车辆所在的单元--y --cell-pattern-n代表--vehicle-on-cell-n 例如:如果我们的滑动坐标是(-1, 0)
   --vehicle-on-cell-4: 1;
Enter fullscreen mode Exit fullscreen mode
  1. 在 3x3 网格上放置障碍物。例如:道路左侧的一棵树:
   --obstacle-on-cell-1: 1;
   --obstacle-on-cell-4: 1;
   --obstacle-on-cell-7: 1;
Enter fullscreen mode Exit fullscreen mode
  1. 如果当前单元格也有障碍物,则检测碰撞。
   --collision-on-cell-4: calc(
     var(--vehicle-on-cell-4) * var(--obstacle-on-cell-4)
   );
Enter fullscreen mode Exit fullscreen mode

您可以在下面的 .gif 中看到此逻辑的动态:
碰撞

· 2.1.c.:随时间变化的碰撞图动画

对 的值进行动画处理--obstacle-on-cell-k,其中1 ≤ k ≤ 9

为了简化我的工作,我声明式地生成了动画。(使用 .SCSS)

$OBSTACLES: {
  (),
  (),
  (("tree", 1), ("tree-arch", 3)),
  (("tree-arch", 1), ("tree", 2), ("tree-arch", 3)),
  (),
  (),
  (("arch", 1), ("arch", 3)),
  (("rock", 1), ("rock", 3)),
  (("arch", 1), ("rock", 2), ("arch", 3)),
  (),
  (),
}
Enter fullscreen mode Exit fullscreen mode

每个项目代表一个关键帧。

您可以在下面的 .gif 中看到障碍物随时间变化的动画:
障碍物深度

📙:蓝色覆盖层表示障碍物,红色覆盖层表示碰撞

📙:透明的地板让你看到我如何设计地下障碍物来阻挡通道

· 2.1.d.:发生任何碰撞后立即停止游戏

我检查每个单元格是否存在可能的碰撞,并将结果存储在 中--collision-on-cell-k,其中1 ≤ k ≤ 9
如果所有可能碰撞的总和大于零,则发生了碰撞!

现在是棘手的部分。

一旦动画跳转到下一个关键帧,碰撞就会消失。那么,我该如何保持碰撞状态呢?记住,我无法控制非数字 CSS 属性,因此我不能简单地设置animation-play-state: paused;。通过将持续时间更改为 ,animation-duration: calc(var(--virtually-infinite) * 1s);我也会改变当前动画的进度。(例如:如果我处于 50% 的进度,突然将动画持续时间增加 10 倍,则动画总进度将下降到 5%)。

那么我做了什么?

我立即滑入了“游戏结束”屏幕,并将滑出过渡设置为 31.7 年!这意味着,除非你打算等待,否则“游戏结束”状态在感知上是静止的。

游戏结束

代码如下:

:root {
  --virtually-infinite: 1000000000s; // 31.7 years
}

.game-over {
  background: black;
  bottom: calc(var(--zero-collisions) * 200lvh);
  transition: bottom calc(
      var(--zero-collisions) * var(--virtually-infinite) + 1ms
    ) linear;
  z-index: 100;
}
Enter fullscreen mode Exit fullscreen mode

--zero-collisions在 为0 的瞬间,bottom被设置为,0transition-duration被设置为1ms。活板门已落下。--zero-collisions再次被设置为 1,但重置活板门需要 31.7 秒。如果--zero-collisions由于二次碰撞而在后台将 设置为 0,我们不会注意到,因为活板门已经落下。

· 2.1.e.:检测胜利

这个很简单。我--you-win在回合结束时将其设置为 true。胜利屏幕会向上滑动——位于任何可能的 Game Over 屏幕之后——并且保持向上。

@keyframes move-obstacles {
  // ...

  99.999% {
    --you-win: 0;
  }
  100% {
    --you-win: 1; // last keyframe
  }
}

.victory {
  transition: opacity 250ms ease-out;
  bottom: calc((1 - var(--you-win)) * 200lvh);
  opacity: calc(0.875 * var(--you-win));
  z-index: 99;
}
Enter fullscreen mode Exit fullscreen mode

2.2.:微调优先滑动的移动用户体验

· 2.2.a.:禁用原生滑动导航

当您开始创建优先滑动的移动网络体验时,您很快就会意识到浏览器已经使用滑动操作来实现诸如水平滑动本机浏览器导航、垂直下拉刷新手势、切换地址栏和捏合缩放等功能。

以下是我们阻止它的方法:

contain 值禁用本机浏览器导航,包括垂直下拉刷新手势和水平滑动导航。

html,
body {
  overscroll-behavior: contain;
}
Enter fullscreen mode Exit fullscreen mode

· 2.2.b.:修复垂直滑动布局偏移

垂直滑动可切换地址栏的可见性,从而调整 UI 大小。
解决方案:将布局机制与视口底部绑定。

.container {
  position: relative; // or absolute;
  height: 100lvh;
}

.game-view {
  position: absolute;
  height: 100svh;
  bottom: 0;
}
Enter fullscreen mode Exit fullscreen mode

· 2.2.c.:阻止捏合缩放

<meta
  name="viewport"
  content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no"
/>
Enter fullscreen mode Exit fullscreen mode
.view {
  touch-action: pan-x pan-y;
}
Enter fullscreen mode Exit fullscreen mode

2.3.:次要障碍

· 2.3.a.:如何处理巨大的 CSS 文件

为了加载非常大的 CSS 文件,我创建了一个老式的 MPA(多页应用程序)。这是一种更巧妙的说法,我为每个页面加载不同的 CSS 文件,这样我就可以在不增加 CSS 文件体积的情况下为游戏添加多个阶段。但是,由于我需要在多个页面之间切换,所以无法使用复选框来保持状态。因此,我决定使用 URL 和:target选择器来保持状态。

body:has(
    :is(
        #color-1:target,
        #color-1--lowres:target,
        #color-1--mouse:target,
        #color-1--lowres--mouse:target,
        #color-1--muted:target,
        #color-1--lowres--muted:target,
        #color-1--mouse--muted:target,
        #color-1--lowres--mouse--muted:target
      )
  ) {
  --car-color: 1;

  .dynamic-link:nth-of-type(1) {
    display: inline-block;
  }
}
Enter fullscreen mode Exit fullscreen mode

· 2.3.b.:如何在 3D 空间中单阶段实现多达 126 个障碍物的动画

我不知道,GPU 无法处理 3D 空间中 126 个障碍物的动画,所以这只是一种假象。所有障碍物都被隐藏起来,并固定在给定的距离,并且只在提示时向屏幕方向移动animation-delay。这样,在任何给定时间内,我们都不会有超过几十个障碍物的动画。

其他技巧

will-animate,,(这超出了本文的讨论范围contain: strictbackface-visibility: hidden

· 2.3.c.:如何自动播放声音

这就像<audio autoplay>在 HTML 文档中放置元素并使用 CSS 隐藏它们一样简单。
要使用静音选项,请使用单独的不带标签的 HTML 文档<audio>

· 2.3.a.:如何在页面切换时保持状态

通过将汽车颜色和游戏配置状态存储在 URL 中,并使用:target选择器读取。但是,对于音频的开启和关闭,我直接渲染了一个带或不带标签的页面<audio>,因为没有 JavaScript 就无法打开和关闭音频。


3. 经验教训

凭借现有的 CSS 数学和 CSS 逻辑,我们能够创造出如此多的作品,真是令人难以置信。GrahamTheDev就是个证明!

我会留下一条经验教训: CSS 变量最好不要使用单位,除非你真的需要用到它。我不是第一个这么说的人,但值得强调一下。

width: calc(var(--complex-logic) * 1vw);
Enter fullscreen mode Exit fullscreen mode

4. 荣誉

我要感谢几位通过提供如此优质的教育内容间接为该项目做出贡献的开发人员。

  1. Bramus致敬
    • 有关滚动驱动动画的所有精彩教程。
    • 用于干净的双向滚动驱动动画设置。
  2. 杰米·库尔特致敬
    • 以提高纯 CSS 游戏的质量标准。
      • 你如此巧妙地展示了复选框的强大功能,以至于我故意不去使用它们
  3. 艾米·卡佩尼克致敬
    • 传播有关 HTML 状态存储 hack 的信息:target
      • 汽车颜色和配置选项存储在:target
  4. 向凯文·鲍威尔致敬
  • 传播有关命名网格线的信息。

    • 如果没有它,主页的高度动态的 Bento Style Grid 就不可能实现!
       /* 
           I pretty much only had to reset these two properties per media query. 
           7 grid sections styled across 11 @media definitions totaled 22 style declarations.
           The traditional grid-area approach could need as many as 77 style declarations.
           e.g.:
       */
    
       .bento-box {
         grid-template-columns: [header-start display-start] 4fr [display-end share-start actions-start specs-start] 3fr [header-end share-end config-start] 1fr [config-end actions-end specs-end];
         grid-template-rows: [header-start config-start] 1fr [header-end display-start share-start] 0.75fr [config-end share-end actions-start] 1fr [actions-end specs-start] 1.5fr [display-end specs-end];
       }
    
       @media screen and (max-width: 1300px) {
         .bento-box {
           grid-template-columns: [header-start config-start display-start] 3.25fr [config-end display-end actions-start specs-start share-start] 3.875fr [header-end display-end actions-end specs-end share-end];
           grid-template-rows: [header-start] 0.875fr [header-end config-start actions-start] 0.625fr [config-end display-start] 0.125fr [actions-end specs-start] 1.5fr [specs-end share-start] 1fr [display-end share-end];
         }
       }
    
       @media screen and (max-width: 1000px) {
         .bento-box {
           grid-template-columns: [header-start config-start display-start specs-start] 2.75fr [config-end display-end specs-end actions-start share-start] 1.5fr [header-end actions-end share-end];
           grid-template-rows: [header-start] 1.125fr [header-end config-start actions-start] 0.75fr [config-end display-start] 2fr [actions-end share-start] 2fr [display-end specs-start] 2.5fr [share-end specs-end];
         }
       }
    
       /* ... 8 other queries */
    
  1. 《乐高®好朋友心湖城冲刺》致敬
    • 获得 UI 和游戏玩法灵感!

5. 致谢

资产和 UI

声音片段和配乐

  • 主页 — 加速 — 作者:warkentien2
  • 所有阶段——驱动噪音——作者:warkentien2
  • 第一阶段 — 黑暗乡村摇滚 — 作者:moodmode
  • 第二阶段 — 西部牛仔 — Music_For_Videos出品
  • 第三阶段 — Tumbleweed Tango — 作者:moodmode
  • 第四阶段 — 过电压 — 作者:moodmode
  • 第五阶段 — 道路精神 — 作者:SergePavkinMusic
  • Phase X — 牛仔救赎 — Music_Unlimited

6. 常见问题

转到主页底部的Desert Racer 常见问题解答部分。

 
 

感谢您的阅读!

欢迎您提出问题并说出您的想法。

X上关注我@warkentien2

文章来源:https://dev.to/warkentien2/desert-racer-worlds-first-css-only-swipe-aware-game-4j0h
PREV
亲爱的 JavaScript,希望您收到这封邮件后一切安好……
NEXT
如何在 Now.sh 上部署 Express 如何在 Now.sh 上部署 Express