内容字号:默认大号超大号

段落设置:段首缩进取消段首缩进

字体设置:切换到微软雅黑切换到宋体

使用纯CSS制作四子连珠游戏

2018-08-29 18:38 出处:清屏网 人气: 评论(0

序言:你有没有想过单纯使用 CSS 也可以制作一款游戏?甚至可以双人对决?这是一篇非常有趣的文章,作者详细讲解了使用纯 CSS 制作四子连珠游戏的思路以及使用奇淫巧技解决困难问题的方法。因为案例本身比较复杂,而本人水平有限,翻译必有不恰当之处,望指正。

原文: How the Roman Empire Made Pure CSS Connect 4 Possible

翻译:nzbin

实验是学习新技巧、思考新想法、并突破自身极限的有趣的方式。“纯 CSS”演示很早就有了,但是随着浏览器和CSS的发展,新的挑战又出现了。CSS 和 HTML 预处理器也促进了纯 CSS 演示的发展。有时候,预处理程序用于硬编码每个可能的场景,比如 :checked 的长字符串和相邻兄弟选择器。

在本文中,我将介绍使用纯CSS 制作的四子连珠游戏的关键思想。在我的实验中,我尽量避免硬编码,并且不使用预处理器,专注于保持代码的简洁。以下是游戏的所有代码以及演示:

See the Pen Pure CSS Connect 4 by Bence Szabó ( @finnhvman ) on CodePen .

基本概念

我认为在“纯 CSS”类型中有一些概念是必不可少的。通常,表单元素用于管理状态和捕获用户操作。当我发现有人使用 <button type="reset"> 重置或者重新开始新游戏时,我非常兴奋。只需要将元素包裹在 <form> 标签中并添加按钮。在我看来,这是一个比刷新页面更方便的解决方案。

第一步就是创建表单元素,再在表单中创建一些用作圆孔(the slots)的 input,然后添加重置按钮。以下是使用 <button type="reset"> 的基本演示:

See the Pen Pure HTML Form Reset by Bence Szabó ( @finnhvman ) on CodePen .

为了让演示好看一些,我使用 radial-gradient() ,而不是在游戏板(the board)或者圆盘(the discs)上贴一张图片。我经常使用 Lea Verou 制作的 CSS3 图案库 。它是使用渐变制作的图案集,而且很容易编辑。我使用了 currentcolor ,非常适合圆盘的图案。我添加了头部,并且复用了自己制作的 纯 CSS 波纹按钮

现在,布局和圆盘已经设计好了,只是还不能游戏。

把圆盘放到游戏板上

接下来,需要让用户轮流将圆盘放到四子连珠的游戏板上。在四子连珠游戏中,玩家(一个红色,一个黄色)轮流将圆盘放置在面板的列中。游戏板有 7 列 6 行(一共有 42 个圆孔)。每一个圆孔可以为空或者被一个红色或黄色的圆盘占用。所以,一个圆孔可以有三种状态(空、红色或者黄色)。在同一列中掉落的圆盘会堆叠在一起。

首先我为每个圆孔放置了两个 checkbox 。当它们都没有被选中时,圆孔就被认为是空的,当其中一个被选中时,相应的玩家就会把他的圆盘放进去。

当其中任何一个被选中之后,应该把它隐藏起来,避免出现两者都被选中的状态。这些 checkbox 是直接的兄弟类,所以如果选中第一个之后,可以使用 :checked 伪类和相邻兄弟选择器( + )来隐藏两个元素。但是如果选中第二个呢?你可以隐藏第二个,但是怎么才能影响第一个呢?可惜没有选择前一个的兄弟选择器,这不是 CSS 选择器的工作方式。我不得不拒绝这个想法。

实际上,一个 checkbox 本身可以有三个状态,可以使用 indeterminate 状态 。问题是,仅仅使用 HTML 不能将其置于不确定状态。即使可以,当再次点击复选框时,它也会转换成选中状态。强迫第二个玩家在移动圆盘时进行双击是不现实的。

我仔细阅读了 MDN 上关于 :indeterminate 的文档后发现 radio input 通用都有 indeterminate 状态。名称相同的 radio按钮在未选中时都处于这种状态。哇,这是一个真正的初始状态!真正有用的是,选中后一个同胞元素也会对前者产生影响!于是我在游戏板上放置了 42 对 radio input。

从以往的经历来看,使用 label ,并通过合理的顺序搭配 checkbox 或 radio 可以解决问题,但我认为 label 不能使代码更简洁。

为了获得更好的用户体验,我希望交互区域可以更大一些,所以合理的做法是让玩家点击一个列来移动圆盘。通过在合适的元素上添加绝对和相对位置,我将同一列的控件相互叠加。这样,在每一列中只能选择最下面的圆孔。我仔细地设置了每一行的圆盘下降的时间,它们的时间函数近似于一个二次曲线,与现实中的自由落体相似。到目前为止,游戏的各部分都做好了,但是下图清晰地显示出只有红色的玩家才能操作。

尽管已经设置了所有的控件,但只有红色的圆盘可以落在游戏板上。

我用彩色且半透明的矩形对 Radio input 的可点击区域用进行了可视化显示。黄色和红色的 input 在每列上重叠 6 次(= 6 行),将最下面一行的红色的 input 放在顶部。红色和黄色的混合形成了橙黄色,可以在游戏板上看到。每一列中可用的圆孔越少,这种橙黄色就越不强烈,因为 radio input 只有在 :indeterminate 状态时才会显示。由于在每个圆孔上,红色 input 总是盖住黄色 input,所以只有红色的玩家能够移动。

轮流游戏

我只有一个模糊的想法,就是能不能使用普通的兄弟选择器解决玩家轮流游戏的问题。这个想法就是统计选中的 input 的数量,为偶数(0、2、4等)时红色玩家移动,为奇数时黄色玩家移动。很快我就意识到一般的兄弟选择器不能(也不应该!)按照我想要的方式工作。

还有一种方式就是使用 nth 选择器。尽管我喜欢使用 偶数奇数 这样的关键词,但我还是走进了死胡同。 :nth-child 选择器 “统计”父类中的子元素,包括所有类型,类、伪类等等。 :nth-of-type 选择器 “统计”在父类中某类型的子类,不包括类或伪类。所以问题就在于无法通过 :checked 状态去统计。

CSS counters 也可以统计,所以为什么不试试呢?计数器的一个常见用法是在文档中对标题(甚至多个级别)进行编号。它们由 CSS 规则控制,可以在任何时候被重置,其增量(或递减!)值可以是任意整数。计数器“counter()”函数显示在 content 属性中。

所以最简单的方法就是设置计数器,然后统计四子连珠游戏中 :checked 的 input 的数量。这种方法只有两个困难。首先,你不能在一个计数器上执行算术运算来检测它是偶数还是奇数。其次,你不能基于计数器的值在元素上应用 CSS 规则。

我使用二进制解决了第一个问题。计数器的初始值设为 0 。当红色玩家选中 radio 按钮时,计数器加 1。当黄色玩家选中 radio 按钮时,计数器就减 1,以此类推。因此,计数器的值始终是 0 或 1,偶数或奇数。

解决第二个问题需要更多的创造力(read: hack)。如上所述,计数器只能显示在 ::before::after 伪元素中。这是显而易见的,但它们如何影响其他元素呢?至少计数器值可以改变伪元素的宽度。不同的数有不同的宽度。字符 1 通常比 0 纤细,但这是很难控制的。如果改变的是字符的数量,而不是字符本身,那么由此产生的宽度变化就是可控的。在 CSS 计数器中使用罗马数字并不少见。用罗马数字表示的 1 和 2 与字符 1 和 2 是相同的,它们的像素宽度也是相同的。

我的想法是将一个玩家(黄色)的单选按钮连接到左边,并将另一个玩家(红色)的单选按钮连接到共享父容器的右边。最初,红色的按钮被覆盖在黄色的按钮上,然后容器的宽度变化会导致红色的按钮“消失”,显示黄色的按钮。可以将其比作现实中有两个窗格的滑动窗口,一个窗格是固定的(黄色按钮),另一个是可滑动的(红色按钮)。区别在于,在游戏中只有一半的窗口是可见的。

到目前为止,还不错,但我并不满意使用 font-size (以及其他 font 属性)间接控制宽度。更好的方式是使用 letter-spacing ,因为它只在一个维度上改变了大小。出乎意料的是,即使是一个字母也有字母间距(在字母后面呈现),两个字母就有两个字母间距。可靠性的关键就是保证宽度是可预知的。宽度为 0 的字符加上单字母和双字母间距都可以,但是将 font-size 设置为 0 是存在风险的。为了兼容所有浏览器,可以将 letter-spacing (以像素为单位)设置的大一些并且将 font-size 设置的小一点( 1px ),是的,我说的是子像素。

我需要容器的宽度在初始大小( =w )与至少两倍以上大小( >=2w )之间交替变换,以便能够完全隐藏和显示黄色按钮。假设 v 是 'i' 字符的渲染宽度(小写罗马字母表示,在不同的浏览器中不同), cletter-spacing 的渲染宽度(常量)。我需要 v + c = w 为真,但这是不可能的,因为 cw 是整数,而 v 是非整数。最后我使用了 min-widthmax-width 属性来约束可能的宽度值,因此我还将可能的计数器值更改为 'i' 和 'iii' ,以确保文本在流下变宽并溢出约束。通过方程 v + c < w3v + 3c > 2w ,, v << c ,可以得到 2/3w < c < w 。结论就是“字母间距”必须比初始宽度小一些。

我一直以为伪元素显示的计数值是 radio 按钮的父元素,可惜不是。但是,我注意到伪元素的宽度改变了其父元素的宽度,在本例中父元素是 radio 按钮的容器。

如果你在想,难道不能用阿拉伯数字来解决吗?你说得对,计数器的值在 '1' 和 '111' 之间交替变换也是可以的。尽管如此,罗马数字最先给了我启示,它们也是点击器标题的不错的方式,所以我保留了它们。

从红色玩家开始,然后轮流游戏。

应用所讨论的技术使 radio input 的父容器在选中红色 input 时宽度加倍,在选中黄色 input 宽度变为原来的宽度。在原始宽度的容器中,红色 input 位于黄色 input 之上,而在双宽度容器中,红色 input 被移开。

识别模式

在现实生活中,四子连珠游戏并不会告诉你是赢了还是输了,但是提供适当的反馈是任何软件良好用户体验的一部分。下一个目标是检测玩家是否赢得了游戏。要想赢得比赛,玩家必须在一列、一行或对角线上放四个圆盘。在许多编程语言中,这是一个非常简单的任务,但是在纯 CSS 世界中,这是一个巨大的挑战。将它分解成子任务是系统地处理这个问题的方法。

我使用一个 flex 容器作为 radio 按钮和圆盘的父类。一个黄色的 radio 按钮、一个红色的 radio 按钮和一个代表圆盘并与圆孔重叠的 div 。这样的圆孔重复了42 次,并排列成多列。因此,列中的圆孔是相邻的,这使得使用相邻选择器识别列中的四个是最容易的:

<div class="grid">
  <input type="radio" name="slot11">
  <input type="radio" name="slot11">
  <div class="disc"></div>
  <input type="radio" name="slot12">
  <input type="radio" name="slot12">
  <div class="disc"></div>
  ...
  <input type="radio" name="slot16">
  <input type="radio" name="slot16">
  <div class="disc"></div>

  <input type="radio" name="slot21">
  <input type="radio" name="slot21">
  <div class="disc"></div>
  ...
</div>
/* Red four in a column selector */
input:checked + .disc + input + input:checked + .disc + input + input:checked + .disc + input + input:checked ~ .outcome

/* Yellow four in a column selector */
input:checked + input + .disc + input:checked + input + .disc + input:checked + input + .disc + input:checked ~ .outcome

这是一个简单但丑陋的解决方案。为了检测一列中四子相连的情况,每个玩家都有 11 个类型和类选择符链接在一起。在圆孔元素后面添加一个类名为 .outcomediv 可以展示输出的信息。在被列包裹的一列中,检测四子相连存在问题,但是我们先把这个问题放到一边。

如果采用类似的方法判断一行中是否有四子相连,那将是一个可怕的想法。每个玩家将会有 56 个选择器(如果我算对了的话),更不用说他们会有类似的检测错误的情况。在将来, :nth-child(An+B [of S]) 或者 column combinators 会派得上用场.

为了更好的语义化,可以为每个列添加一个新的 div ,并在其中排列圆孔元素。这一修改也将消除上述检测错误的情况。然后,检测一行中的有四子相连可以用以下方法:选择第一个红色 radio input 被选中的一个列,然后再选择第一个红色 radio input 被选中的相邻同胞列,重复两次。这听起来很麻烦,需要 "parent"选择器

选择父节点是不可行的,但是选择子节点是可行的。如何用选择器及其组合方式检测一行中的四子相连? 选择一个列,再选择它的第一个被选中的红色 radio input,然后选择相邻的列,再选择它的第一个被选中的红色 radio input ,以此类推,再重复两次。这听起来仍然很麻烦,但却是可行的。诀窍不仅在 CSS 中,而且在 HTML 中,下一列必须是上一列中创建嵌套结构的单选按钮的同胞元素。

<div class="grid column">
  <input type="radio" name="slot11">
  <input type="radio" name="slot11">
  <div class="disc"></div>
  ...
  <input type="radio" name="slot16">
  <input type="radio" name="slot16">
  <div class="disc"></div>

  <div class="column">
    <input type="radio" name="slot21">
    <input type="radio" name="slot21">
    <div class="disc"></div>
    ...
    <input type="radio" name="slot26">
    <input type="radio" name="slot26">
    <div class="disc"></div>

    <div class="column">
      ...
    </div>
  </div>
</div>
/* Red four in a row selectors */
input:nth-of-type(2):checked ~ .column > input:nth-of-type(2):checked ~ .column > input:nth-of-type(2):checked ~ .column > input:nth-of-type(2):checked ~ .column::after,
input:nth-of-type(4):checked ~ .column > input:nth-of-type(4):checked ~ .column > input:nth-of-type(4):checked ~ .column > input:nth-of-type(4):checked ~ .column::after,
...
input:nth-of-type(12):checked ~ .column > input:nth-of-type(12):checked ~ .column > input:nth-of-type(12):checked ~ .column > input:nth-of-type(12):checked ~ .column::after

语义混乱了,这些选择器只适用于红色的玩家(黄色的玩家有另一轮),但是它确实有用。有一个好处是不会出现检测错误的列或行。结果的显示也必须进行修改,任何匹配列使用的 ::after 伪元素都应该是一致的。因此,必须在最后一个位置之后添加一个伪第八列。

如上面的代码片段所示,列的特殊的位置关系可以检测一行中的四子相连。同样的技术可以通过调整这些位置来检测对角线上的四子相连。注意对角线可以在两个方向上。

input:nth-of-type(2):checked ~ .column > input:nth-of-type(4):checked ~ .column > input:nth-of-type(6):checked ~ .column > input:nth-of-type(8):checked ~ .column::after,
input:nth-of-type(4):checked ~ .column > input:nth-of-type(6):checked ~ .column > input:nth-of-type(8):checked ~ .column > input:nth-of-type(10):checked ~ .column::after,
...
input:nth-of-type(12):checked ~ .column > input:nth-of-type(10):checked ~ .column > input:nth-of-type(8):checked ~ .column > input:nth-of-type(6):checked ~ .column::after

在最终的代码中,选择器的数量非常庞大,如果使用 CSS 预处理器则可以显著减少声明长度。尽管如此,我认为演示的代码还是比较短的。它应该是在中间的某个地方,从硬编码一个选择器到使用 4 个神奇的选择器(列,行,两个对角线)。

当有玩家获得胜利就会显示一条信息。

修复漏洞

任何软件都有边缘情况需要处理。四子相连游戏的可能结果不仅是红色或黄色的玩家获胜,而且会出现游戏板被填满的平局。从技术上讲,这种情况不会破坏游戏或产生任何错误,所缺少的是对玩家的反馈。

我们的目标是检测出黑板上有 42 个 :checked 的单选按钮。这也意味着它们没有一个处于 :indeterminate 状态。这就 要求 为每个单选按钮做一个选择。单选按钮处于 :indeterminate 时是 invalid ,否则是 valid 。因此,我为每个输入添加了 required 属性,然后在表单上使用 :valid 伪类来检测平局。

当游戏板被填满时会显示平局的信息。

检测平局结果出现了一个 bug。在极少数的情况下会出现黄色玩家最终胜利的情况,胜利和平局的消息都显示出来了。这是因为这些结果的检测和显示方法是正交的。我解决了这个问题,确保获胜消息有一个白色的背景,并在平局消息之上。我还必须延迟绘制消息的转换,这样它就不会与获胜消息转换混合。

黄方胜利的信息盖住了平局结果

虽然许多单选按钮是通过绝对定位隐藏在彼此后面的,但是所有处于不确定状态的按钮仍然可以通过控件的 tabindex 来访问。这使得玩家可以将他们的圆盘放入任意的圆孔中。处理这个问题的一种方法是简单地禁止使用 tabindex 属性进行键盘交互:将其设置为 -1 意味着不应该通过连续的键盘导航来访问它。为了解决这个问题,必须在每个单选按钮上添加这一属性。

<input type="radio" name="slot11" tabindex="-1" required>
<input type="radio" name="slot11" tabindex="-1" required>
<div class="disc"></div>
...

限制

最实质性的缺点是,由于轮流游戏的解决方案不可靠,游戏板没有响应,并且可能在小的视图窗口上出现故障。我不敢冒险重构响应式的解决方案,由于实现的本质,硬编码看起来更安全。

另一个问题是触摸设备上的 sticky hover 。在正确的位置添加一些 媒体查询 是解决这个问题最简单的方法,但是这会消除自由落体动画。

有人可能认为 :indeterminate 伪类已经得到了广泛的支持,事实的确如此。问题是它只在一些浏览器中得到部分支持。注意 兼容性表 中的注释1:MS IE 和 Edge 在单选按钮上不支持它。如果您在这些浏览器中查看演示程序,您的光标将变成 not-allowed 的光标,这是无意的,但有点优雅的降级。

不是所有浏览器都支持 radio 按钮的 :indeterminate 属性。

总结

感谢阅读到最后一部分!让我们看看这个游戏的一些数据:

  • 140 个 HTML 元素

  • 350 行 (合理地) CSS

  • 0 行 JavaScript

  • 0 个外部资源

总的来说,我对结果很满意,反馈也很好。做这个演示我确实学到了很多,我希望我可以分享更多这样的文章!

分享给小伙伴们:
本文标签: CSS

相关文章

发表评论愿您的每句评论,都能给大家的生活添色彩,带来共鸣,带来思索,带来快乐。

CopyRight © 2015-2016 QingPingShan.com , All Rights Reserved.

清屏网 版权所有 豫ICP备15026204号