我最近发现了创建纯 CSS 游戏的乐趣。 HTML 和 CSS 能够处理整个在线游戏的逻辑总是令人着迷,所以我不得不尝试一下! 此类游戏通常依赖于 ol' Checkbox Hack,我们将 HTML 输入的选中/未选中状态与 :checked
CSS中的伪类。 我们可以用那个组合做很多魔法!
事实上,我挑战自己构建一个没有 Checkbox 的完整游戏。 我不确定这是否可能,但肯定是,我将向您展示如何。
除了我们将在本文中学习的益智游戏外,我还制作了 纯CSS游戏合集,其中大多数没有 Checkbox Hack。 (它们也可用 在 CodePen 上.)
想在我们开始之前玩吗?
我个人更喜欢全屏模式玩游戏,但你可以在下面玩或 在这里打开它.
酷吧? 我知道,它不是你见过的最好的益智游戏™,但对于只使用 CSS 和几行 HTML 的东西来说,它也一点也不差。 您可以轻松调整网格大小,更改单元格数量以控制难度级别,并使用您想要的任何图像!
我们将一起重新制作该演示,然后在最后添加一些额外的闪光以进行一些踢球。
拖放功能
虽然使用 CSS Grid 拼图的结构相当简单,但拖放拼图块的能力有点棘手。 我不得不依靠过渡、悬停效果和兄弟选择器的组合来完成它。
如果您将鼠标悬停在该演示中的空框上,即使您将光标移出框,图像也会在其中移动并停留在那里。 诀窍是增加一个大的过渡持续时间和延迟——大到图像需要很多时间才能返回到它的初始位置。
img {
transform: translate(200%);
transition: 999s 999s; /* very slow move on mouseout */
}
.box:hover img {
transform: translate(0);
transition: 0s; /* instant move on hover */
}
仅指定 transition-delay
已经足够了,但是在延迟和持续时间上使用较大的值会降低玩家看到图像向后移动的机会。 如果你等待 999s + 999s
— 大约 30 分钟 — 然后您将看到图像移动。 但你不会的,对吧? 我的意思是,除非他们离开比赛,否则没有人会在回合之间花费那么长时间。 所以,我认为这是在两种状态之间切换的好技巧。
您是否注意到悬停图像也会触发更改? 那是因为图像是框元素的一部分,这对我们不利。 我们可以通过添加来解决这个问题 pointer-events: none
到图像,但我们稍后将无法拖动它。
这意味着我们必须在 .box
:
那额外的 div
(我们正在使用一类 .a
) 将占用与图像相同的区域(感谢 CSS Grid 和 grid-area: 1 / 1
) 并且将是触发悬停效果的元素。 这就是兄弟选择器发挥作用的地方:
.a {
grid-area: 1 / 1;
}
img {
grid-area: 1 / 1;
transform: translate(200%);
transition: 999s 999s;
}
.a:hover + img {
transform: translate(0);
transition: 0s;
}
悬停在 .a
element 移动图像,并且由于它占用了盒子内的所有空间,所以就像我们将鼠标悬停在盒子上一样! 悬停图像不再是问题!
让我们将图像拖放到框中并查看结果:
你看见了吗? 您首先抓取图像并将其移动到盒子中,没什么特别的。 但是,一旦您释放图像,就会触发移动图像的悬停效果,然后我们模拟拖放功能。 如果您在框外释放鼠标,则不会发生任何事情。
嗯,您的模拟并不完美,因为我们也可以将框悬停并获得相同的效果。
是的,我们将纠正这一点。 我们需要禁用悬停效果并仅在我们释放框内的图像时才允许它。 我们将玩我们的维度 .a
元素来实现这一点。
现在,悬停在盒子上什么也没做。 但是,如果您开始拖动图像, .a
元素出现,一旦在框内释放,我们就可以触发悬停效果并移动图像。
让我们剖析一下代码:
.a {
width: 0%;
transition: 0s .2s; /* add a small delay to make sure we catch the hover effect */
}
.box:active .a { /* on :active increase the width */
width: 100%;
transition: 0s; /* instant change */
}
img {
transform: translate(200%);
transition: 999s 999s;
}
.a:hover + img {
transform: translate(0);
transition: 0s;
}
点击图片会触发 :active
使 .a
元素全宽(最初等于 0
)。 活动状态将保持 要积极。 直到我们发布图像。 如果我们释放框内的图像, .a
元素回到 width: 0
,但我们会在它发生之前触发悬停效果,并且图像将落入框内! 如果你把它释放到盒子外面,什么都不会发生。
有一个小怪癖:单击空框也会移动图像并破坏我们的功能。 目前, :active
链接到 .box
元素,因此单击它或其任何子元素将激活它; 通过这样做,我们最终展示了 .a
元素并触发悬停效果。
我们可以通过玩来解决这个问题 pointer-events
. 它允许我们禁用与 .box
同时保持与子元素的交互。
.box {
pointer-events: none;
}
.box * {
pointer-events: initial;
}
现在 我们的拖放功能非常完美。 除非您能找到破解方法,否则移动图像的唯一方法是将其拖放到框内。
构建拼图网格
与我们刚刚为拖放功能所做的相比,将拼图放在一起会感觉很容易。 我们将依靠 CSS 网格和背景技巧来创建拼图。
这是我们的网格,为方便起见,用 Pug 编写:
- let n = 4; /* number of columns/rows */
- let image = "https://picsum.photos/id/1015/800/800";
g(style=`--i:url(${image})`)
- for(let i = 0; i < n*n; i++)
z
a
b(draggable="true")
代码可能看起来很奇怪,但它会编译成纯 HTML:
<g style="--i: url(https://picsum.photos/id/1015/800/800)">
<z>
<a></a>
<b draggable="true"></b>
</z>
<z>
<a></a>
<b draggable="true"></b>
</z>
<z>
<a></a>
<b draggable="true"></b>
</z>
<!-- etc. -->
</g>
我打赌你想知道这些标签是怎么回事。 这些元素都没有任何特殊含义——我只是发现使用代码更容易编写 <z>
比一堆 <div class="z">
管他呢。
这就是我绘制它们的方式:
<g>
是我们的网格容器,包含N*N
<z>
元素。<z>
代表我们的网格项目。 它扮演着.box
我们在上一节中看到的元素。<a>
触发悬停效果。<b>
代表我们形象的一部分。 我们应用draggable
属性,因为它默认不能拖动。
好的,让我们注册我们的网格容器 <g>
. 这是在 Sass 中而不是在 CSS 中:
$n : 4; /* number of columns/rows */
g {
--s: 300px; /* size of the puzzle */
display: grid;
max-width: var(--s);
border: 1px solid;
margin: auto;
grid-template-columns: repeat($n, 1fr);
}
我们实际上要让我们的网格孩子—— <z>
元素 - 网格以及两者都有 <a>
和 <b>
在同一网格区域内:
z {
aspect-ratio: 1;
display: grid;
outline: 1px dashed;
}
a {
grid-area: 1/1;
}
b {
grid-area: 1/1;
}
如您所见,没什么特别的——我们创建了一个具有特定大小的网格。 我们需要的其余 CSS 用于拖放功能,这需要我们将棋子随机放置在棋盘周围。 我将为此求助于 Sass,再次是为了方便使用函数循环和设置所有拼图的样式:
b {
background: var(--i) 0/var(--s) var(--s);
}
@for $i from 1 to ($n * $n + 1) {
$r: (random(180));
$x: (($i - 1)%$n);
$y: floor(($i - 0.001) / $n);
z:nth-of-type(#{$i}) b{
background-position: ($x / ($n - 1)) * 100% ($y / ($n - 1)) * 100%;
transform:
translate((($n - 1) / 2 - $x) * 100%, (($n - 1)/2 - $y) * 100%)
rotate($r * 1deg)
translate((random(100)*1% + ($n - 1) * 100%))
rotate((random(20) - 10 - $r) * 1deg)
}
}
你可能已经注意到我正在使用 Sass random()
功能。 这就是我们如何获得拼图的随机位置。 请记住,我们将 关闭 将鼠标悬停在该位置 <a>
拖放相应元素后的元素 <b>
网格单元内的元素。
z a:hover ~ b {
transform: translate(0);
transition: 0s;
}
在同一个循环中,我还定义了每个拼图的背景配置。 它们都将在逻辑上与背景共享相同的图像,并且其大小应等于整个网格的大小(用 --s
多变的)。 使用相同的 background-image
和一些数学,我们更新 background-position
只显示图像的一部分。
而已! 我们的纯 CSS 益智游戏在技术上已经完成!
但我们总是可以做得更好,对吧? 我给你看了 如何制作拼图形状的网格 在另一篇文章中。 让我们把同样的想法应用在这里,好吗?
拼图形状
这是我们的新益智游戏。 功能相同,但形状更逼真!
这是网格上形状的图示:
如果您仔细观察,您会发现我们有九种不同的拼图形状: 四个角是, 四边及 一个用于其他一切.
我在我提到的另一篇文章中制作的拼图网格更简单一些:
我们可以使用结合 CSS 蒙版和渐变的相同技术来创建不同的形状。 如果你不熟悉 mask
和渐变,我强烈建议检查 那个简化的案例 在进入下一部分之前更好地理解该技术。
首先,我们需要使用特定的选择器来定位具有相同形状的每组元素。 我们有九个组,所以我们将使用八个选择器,以及一个选择所有这些选择器的默认选择器。
z /* 0 */
z:first-child /* 1 */
z:nth-child(-n + 4):not(:first-child) /* 2 */
z:nth-child(5) /* 3 */
z:nth-child(5n + 1):not(:first-child):not(:nth-last-child(5)) /* 4 */
z:nth-last-child(5) /* 5 */
z:nth-child(5n):not(:nth-child(5)):not(:last-child) /* 6 */
z:last-child /* 7 */
z:nth-last-child(-n + 4):not(:last-child) /* 8 */
下图显示了它如何映射到我们的网格:
现在让我们处理形状。 让我们专注于学习一两个形状,因为它们都使用相同的技术——这样,你就有一些功课要继续学习!
对于网格中心的拼图, 0
:
mask:
radial-gradient(var(--r) at calc(50% - var(--r) / 2) 0, #0000 98%, #000) var(--r)
0 / 100% var(--r) no-repeat,
radial-gradient(var(--r) at calc(100% - var(--r)) calc(50% - var(--r) / 2), #0000 98%, #000)
var(--r) 50% / 100% calc(100% - 2 * var(--r)) no-repeat,
radial-gradient(var(--r) at var(--r) calc(50% - var(--r) / 2), #000 98%, #0000),
radial-gradient(var(--r) at calc(50% + var(--r) / 2) calc(100% - var(--r)), #000 98%, #0000);
代码可能看起来很复杂,但让我们一次只关注一个渐变,看看发生了什么:
两个渐变创建两个圆圈(在演示中标记为绿色和紫色),另外两个渐变创建其他部分连接的槽(标记为蓝色的填充了大部分形状,而标记为红色的填充了顶部)。 一个 CSS 变量, --r
, 设置圆形的半径。
中间拼图的形状(标记 0
在插图中)是最难制作的,因为它使用四个渐变并且有四个曲率。 所有其他部分都使用较少的渐变。
例如,沿着拼图顶部边缘的拼图(标记 2
在插图中)使用三个渐变而不是四个:
mask:
radial-gradient(var(--r) at calc(100% - var(--r)) calc(50% + var(--r) / 2), #0000 98%, #000) var(--r) calc(-1 * var(--r)) no-repeat,
radial-gradient(var(--r) at var(--r) calc(50% - var(--r) / 2), #000 98%, #0000),
radial-gradient(var(--r) at calc(50% + var(--r) / 2) calc(100% - var(--r)), #000 98%, #0000);
我们移除了第一个(顶部)渐变并调整了第二个渐变的值,以便它覆盖留下的空间。 如果比较这两个示例,您不会注意到代码有很大的不同。 需要注意的是,我们可以找到不同的背景配置来创建相同的形状。 如果你开始玩渐变,你肯定会想出与我不同的东西。 你甚至可以写一些更简洁的东西——如果是这样,请在评论中分享!
除了创建形状之外,您还会发现我正在增加元素的宽度和/或高度,如下所示:
height: calc(100% + var(--r));
width: calc(100% + var(--r));
拼图的碎片需要溢出它们的网格单元才能连接。
最终演示
这是完整的演示。 如果将其与第一个版本进行比较,您将看到创建网格和拖放功能的相同代码结构,以及创建形状的代码。
可能的改进
文章到此结束,但我们可以通过更多功能继续增强我们的拼图! 一个计时器怎么样? 或者,当玩家完成谜题时,也许是某种祝贺?
我可能会在未来的版本中考虑所有这些功能,所以 留意我的 GitHub 存储库.
结束了
和 CSS 不是一种编程语言, 他们说。 哈!
我不是想以此引发一些#HotDrama。 我这么说是因为我们做了一些非常棘手的逻辑工作,并且在此过程中涵盖了很多 CSS 属性和技术。 我们使用了 CSS Grid、过渡、遮罩、渐变、选择器和背景属性。 更不用说我们用来使代码易于调整的一些 Sass 技巧了。
目标不是构建游戏,而是探索 CSS 并发现可以在其他项目中使用的新属性和技巧。 使用 CSS 创建在线游戏是一项挑战,它促使您深入探索 CSS 功能并学习如何使用它们。 另外,当一切都说完后,我们得到了一些可以玩的东西,这很有趣。
无论 CSS 是否是一种编程语言,都不会改变我们总是通过构建和创造创新的东西来学习的事实。