html 编写一个小游戏,用 JavaScript 写一个卡片小游戏

小游戏使用了HTML5,CSS3和JavaScript的基本的技术。

将讨论数据属性、定位、透视、转换、flexbox、事件处理、超时和三元组。

你不需要在编程方面有太多的知识和经验就能看懂,不过还是需要知道HTML,CSS和JS都是什么。

AAffA0nNPuCLAAAAAElFTkSuQmCC

项目结构

先在终端中创建项目文件:

mkdir memory-game

cd memory-game

touch index.html styles.css

scripts.js mkdir img

HTML

初始化页面模版并链接 css 文件 js 文件.

Memory Game

这个游戏有 12 张卡片。 每张卡片中都包含一个名为 .memory-card 的容器 div,它包含两个img元素。 一个代表卡片的正面 front-face ,另一个个代表背面 back-face。

AAffA0nNPuCLAAAAAElFTkSuQmCC

React

Memory Card

这组卡片将被包装在一个 section 容器元素中。 最终代码如下:

React

Memory Card

React

Memory Card

Angular

Memory Card

Angular

Memory Card

Ember

Memory Card

Ember

Memory Card

Vue

Memory Card

Vue

Memory Card

Backbone

Memory Card

Backbone

Memory Card

Aurelia

Memory Card

Aurelia

Memory Card

CSS

我们将使用一个简单但非常有用的配置,把它应用于所有项目:

/* styles.css */

* {

padding: 0;

margin: 0;

box-sizing: border-box;

}

box-sizing: border-box 属性能使元素充满整个边框,所以我们就可以不用做一些数学计算了。

把 display:flex 设置给 body ,并且把 margin:auto应用到到 .memory-game 容器,这样可以使它将垂直水平居中。

.memory-game 是一个弹性容器,在默认情况下,里面的元素会缩小宽度来适应这个容器。通过把 flex-wrap 的值设置为 wrap,会根据弹性元素的大小进行自适应。

/* styles.css */

body {

height: 100vh;

display: flex;

background: #060AB2;

}

.memory-game {

width: 640px;

height: 640px;

margin: auto;

display: flex;

flex-wrap: wrap;

}

每个卡片的 width 和 height 都是用 CSS 的 calc()函数进行计算的。 下面我们需要制作一个三行四列的界面,并且把 width 设置为 25%, height 设置为 33.333% ,还要再减去 10px 留足边距.

为了定位 .memory-card 子元素,还要添加属性 position: relative ,这样我们就可以相对它进行子元素的绝对定位。

把 front-face and back-face 的position属性都设置为 absolute ,这样就可以从原始位置移除元素,并使它们堆叠在一起。

这时页面模版看上去应该是这样:

AAffA0nNPuCLAAAAAElFTkSuQmCC

我们还需要添加一个点击效果。 每次元素被点击时都会触发 :active 伪类,它引发一个 0.2秒的过渡:

AAffA0nNPuCLAAAAAElFTkSuQmCC

翻转卡片

要在单击时翻转卡片,需要把一个 flip 类添加到元素。 为此,让我们用 document.querySelectorAll 选择所有 memory-card 元素,然后使用 forEach 遍历它们并附加一个事件监听器。 每当卡片被点击时,都会触发 flipCard 函数,其中 this 代表被单击的卡片。 该函数访问元素的 classList 并切换到 flip 类:

// scripts.js

const cards = document.querySelectorAll('.memory-card');

function flipCard() {

this.classList.toggle('flip');

}

cards.forEach(card => card.addEventListener('click', flipCard));

CSS 中的 flip 类会把卡片旋转 180deg:

.memory-card.flip {

transform: rotateY(180deg);

}

为了产生3D翻转效果,还需要将 perspective 属性添加到 .memory-game。 这个属性用来设置对象与用户在 z 轴上的距离。 值越小,透视效果越强。 为了能达得最佳的效果,把它设置为 1000px:

.memory-game {

width: 640px;

height: 640px;

margin: auto;

display: flex;

flex-wrap: wrap;

+ perspective: 1000px;

}

接下来对 .memory-card 元素添加 transform-style:preserve-3d属性,这样就把卡片置于在父节点中创建的3D空间中,而不是将其平铺在 z = 0 的平面上(transform-style)。

.memory-card {

width: calc(25% - 10px);

height: calc(33.333% - 10px);

margin: 5px;

position: relative;

box-shadow: 1px 1px 1px rgba(0,0,0,.3);

transform: scale(1);

+ transform-style: preserve-3d;

}

再把 transition 属性的值设置为 transform 就可以生成动态效果了

.memory-card {

width: calc(25% - 10px);

height: calc(33.333% - 10px);

margin: 5px;

position: relative;

box-shadow: 1px 1px 1px rgba(0,0,0,.3);

transform: scale(1);

transform-style: preserve-3d;

+ transition: transform .5s;

}

现在我们得到了带有 3D 翻转效果的卡片, 不过为什么卡片的另一面没有出现? 由于绝对定位的原因,现在 .front-face 和 .back-face 都堆叠在了一起。 每个元素的 back face 都是它 front face 的镜像。 属性 backface-visibility 默认为 visible,因此当我们翻转卡片时,得到的是背面的 JS 徽章。

为了显示它背面的图像,让我们在 .front-face 和 .back-face 中添加 backface-visibility:hidden

.front-face,

.back-face {

width: 100%;

height: 100%;

padding: 20px;

position: absolute;

border-radius: 5px;

background: #1C7CCC;

+ backface-visibility: hidden;

}

如果我们刷新页面并翻转一张卡片,它就消失了!

AAffA0nNPuCLAAAAAElFTkSuQmCC

由于我们将两个图像都藏在了背面,所以另一面没有任何东西。 所以接下来需要再把 .front-face 翻转180度:

.front-face {

transform: rotateY(180deg);

}

效果出来了!

AAffA0nNPuCLAAAAAElFTkSuQmCC

匹配卡片

完成翻转卡片的功能之后,接下来处理匹配的逻辑。

当点击第一张卡片时,需要等待另一张被翻转。 变量 hasFlippedCard 和 flippedCard 用来管理翻转状态。 如果没有卡片翻转,hasFlippedCard 的值为 true,flippedCard 被设置为点击的卡片。 让我们切换到 toggle 方法:

const cards = document.querySelectorAll('.memory-card');

+ let hasFlippedCard = false;

+ let firstCard, secondCard;

function flipCard() {

- this.classList.toggle('flip');

+ this.classList.add('flip');

+ if (!hasFlippedCard) {

+ hasFlippedCard = true;

+ firstCard = this;

+ }

}

cards.forEach(card => card.addEventListener('click', flipCard));

现在,当用户点击第二张牌时,代码会进入 else 块,我们将检查它们是否匹配。为了做到这一点,需要能够识别每一张卡片。

每当我们想要向HTML元素添加额外信息时,就可以使用数据属性。 通过使用以下语法: data-,这里的 可以是任何单词,它将被插入到元素的 dataset 属性中。 所以接下来为每张卡片添加一个 data-framework :

+

React

Memory Card

+

React

Memory Card

+

Angular

Memory Card

+

Angular

Memory Card

+

Ember

Memory Card

+

Ember

Memory Card

+

Vue

Memory Card

+

Vue

Memory Card

+

Backbone

Memory Card

+

Backbone

Memory Card

+

Aurelia

Memory Card

+

Aurelia

Memory Card

这下就可以通过访问两个卡片的数据集来检查匹配了。 下面将匹配逻辑提取到它自己的方法 checkForMatch(),并将 hasFlippedCard 设置为 false。 如果匹配的话,则调用 disableCards() 并分离两个卡上的事件侦听器,以防止再次翻转。 否则 unflipCards() 会将两张卡都恢复成超过 1500 毫秒的超时,从而删除 .flip 类:

把代码组合起来:

const cards = document.querySelectorAll('.memory-card');

let hasFlippedCard = false;

let firstCard, secondCard;

function flipCard() {

this.classList.add('flip');

if (!hasFlippedCard) {

hasFlippedCard = true;

firstCard = this;

+ return;

+ }

+

+ secondCard = this;

+ hasFlippedCard = false;

+

+ checkForMatch();

+ }

+

+ function checkForMatch() {

+ if (firstCard.dataset.framework === secondCard.dataset.framework) {

+ disableCards();

+ return;

+ }

+

+ unflipCards();

+ }

+

+ function disableCards() {

+ firstCard.removeEventListener('click', flipCard);

+ secondCard.removeEventListener('click', flipCard);

+ }

+

+ function unflipCards() {

+ setTimeout(() => {

+ firstCard.classList.remove('flip');

+ secondCard.classList.remove('flip');

+ }, 1500);

+ }

cards.forEach(card => card.addEventListener('click', flipCard));

更优雅的进行条件匹配的方法是用三元运算符,它由三部分组成: 第一部分是要判断的条件, 如果条件符合就执行第二部分的代码,否则执行第三部分:

- if (firstCard.dataset.name === secondCard.dataset.name) {

- disableCards();

- return;

- }

-

- unflipCards();

+ let isMatch = firstCard.dataset.name === secondCard.dataset.name;

+ isMatch ? disableCards() : unflipCards();

锁定

现在已经完成了匹配逻辑,接着为了避免同时转动两组卡片,还需要锁定它们,否则翻转将会被失败。

AAffA0nNPuCLAAAAAElFTkSuQmCC

先声明一个 lockBoard 变量。 当玩家点击第二张牌时,lockBoard将设置为true,条件 if (lockBoard) return; 在卡被隐藏或匹配之前会阻止其他卡片翻转:

const cards = document.querySelectorAll('.memory-card');

let hasFlippedCard = false;

+ let lockBoard = false;

let firstCard, secondCard;

function flipCard() {

+ if (lockBoard) return;

this.classList.add('flip');

if (!hasFlippedCard) {

hasFlippedCard = true;

firstCard = this;

return;

}

secondCard = this;

hasFlippedCard = false;

checkForMatch();

}

function checkForMatch() {

let isMatch = firstCard.dataset.name === secondCard.dataset.name;

isMatch ? disableCards() : unflipCards();

}

function disableCards() {

firstCard.removeEventListener('click', flipCard);

secondCard.removeEventListener('click', flipCard);

}

function unflipCards() {

+ lockBoard = true;

setTimeout(() => {

firstCard.classList.remove('flip');

secondCard.classList.remove('flip');

+ lockBoard = false;

}, 1500);

}

cards.forEach(card => card.addEventListener('click', flipCard));

点击同一个卡片

仍然是玩家可以在同一张卡上点击两次的情况。 如果匹配条件判断为 true,从该卡上删除事件侦听器。

AAffA0nNPuCLAAAAAElFTkSuQmCC

为了防止这种情况,需要检查当前点击的卡片是否等于firstCard,如果是肯定的则返回。

if (this === firstCard) return;

变量 firstCard 和 secondCard 需要在每一轮之后被重置,所以让我们将它提取到一个新方法 resetBoard()中, 再其中写上 hasFlippedCard = false; 和 lockBoard = false 。 es6 的解构赋值功能 [var1, var2] = [‘value1’, ‘value2’] 允许我们把代码写得超短:

function resetBoard() {

[hasFlippedCard, lockBoard] = [false, false];

[firstCard, secondCard] = [null, null];

}

接着调用新方法 disableCards() 和 unflipCards():

const cards = document.querySelectorAll('.memory-card');

let hasFlippedCard = false;

let lockBoard = false;

let firstCard, secondCard;

function flipCard() {

if (lockBoard) return;

+ if (this === firstCard) return;

this.classList.add('flip');

if (!hasFlippedCard) {

hasFlippedCard = true;

firstCard = this;

return;

}

secondCard = this;

- hasFlippedCard = false;

checkForMatch();

}

function checkForMatch() {

let isMatch = firstCard.dataset.name === secondCard.dataset.name;

isMatch ? disableCards() : unflipCards();

}

function disableCards() {

firstCard.removeEventListener('click', flipCard);

secondCard.removeEventListener('click', flipCard);

+ resetBoard();

}

function unflipCards() {

lockBoard = true;

setTimeout(() => {

firstCard.classList.remove('flip');

secondCard.classList.remove('flip');

- lockBoard = false;

+ resetBoard();

}, 1500);

}

+ function resetBoard() {

+ [hasFlippedCard, lockBoard] = [false, false];

+ [firstCard, secondCard] = [null, null];

+ }

cards.forEach(card => card.addEventListener('click', flipCard));

点击同一个卡片

仍然是玩家可以在同一张卡上点击两次的情况。 如果匹配条件判断为 true,从该卡上删除事件侦听器。

AAffA0nNPuCLAAAAAElFTkSuQmCC

为了防止这种情况,需要检查当前点击的卡片是否等于firstCard,如果是肯定的则返回。

if (this === firstCard) return;

变量 firstCard 和 secondCard 需要在每一轮之后被重置,所以让我们将它提取到一个新方法 resetBoard()中, 再其中写上 hasFlippedCard = false; 和 lockBoard = false 。 es6 的解构赋值功能 [var1, var2] = [‘value1’, ‘value2’] 允许我们把代码写得超短:

function resetBoard() {

[hasFlippedCard, lockBoard] = [false, false];

[firstCard, secondCard] = [null, null];

}

接着调用新方法 disableCards() 和 unflipCards():

const cards = document.querySelectorAll('.memory-card');

let hasFlippedCard = false;

let lockBoard = false;

let firstCard, secondCard;

function flipCard() {

if (lockBoard) return;

+ if (this === firstCard) return;

this.classList.add('flip');

if (!hasFlippedCard) {

hasFlippedCard = true;

firstCard = this;

return;

}

secondCard = this;

- hasFlippedCard = false;

checkForMatch();

}

function checkForMatch() {

let isMatch = firstCard.dataset.name === secondCard.dataset.name;

isMatch ? disableCards() : unflipCards();

}

function disableCards() {

firstCard.removeEventListener('click', flipCard);

secondCard.removeEventListener('click', flipCard);

+ resetBoard();

}

function unflipCards() {

lockBoard = true;

setTimeout(() => {

firstCard.classList.remove('flip');

secondCard.classList.remove('flip');

- lockBoard = false;

+ resetBoard();

}, 1500);

}

+ function resetBoard() {

+ [hasFlippedCard, lockBoard] = [false, false];

+ [firstCard, secondCard] = [null, null];

+ }

cards.forEach(card => card.addEventListener('click', flipCard));

洗牌

我们的游戏看起来相当不错,但是如果不能洗牌就没有乐趣,所以现在处理这个功能。

当 display: flex 在容器上被声明时,flex-items 会按照组和源的顺序进行排序。 每个组由order属性定义,该属性包含正整数或负整数。 默认情况下,每个 flex-item 都将其 order 属性设置为 0,这意味着它们都属于同一个组,并将按源的顺序排列。 如果有多个组,则首先按组升序顺序排列。

游戏中有12张牌,因此我们将迭代它们,生成 0 到 12 之间的随机数并将其分配给 flex-item order 属性:

function shuffle() {

cards.forEach(card => {

let ramdomPos = Math.floor(Math.random() * 12);

card.style.order = ramdomPos;

});

}

为了调用 shuffle 函数,让它成为一个立即调用函数表达式(IIFE),这意味着它将在声明后立即执行。 脚本应如下所示:

const cards = document.querySelectorAll('.memory-card');

let hasFlippedCard = false;

let lockBoard = false;

let firstCard, secondCard;

function flipCard() {

if (lockBoard) return;

if (this === firstCard) return;

this.classList.add('flip');

if (!hasFlippedCard) {

hasFlippedCard = true;

firstCard = this;

return;

}

secondCard = this;

lockBoard = true;

checkForMatch();

}

function checkForMatch() {

let isMatch = firstCard.dataset.name === secondCard.dataset.name;

isMatch ? disableCards() : unflipCards();

}

function disableCards() {

firstCard.removeEventListener('click', flipCard);

secondCard.removeEventListener('click', flipCard);

resetBoard();

}

function unflipCards() {

setTimeout(() => {

firstCard.classList.remove('flip');

secondCard.classList.remove('flip');

resetBoard();

}, 1500);

}

function resetBoard() {

[hasFlippedCard, lockBoard] = [false, false];

[firstCard, secondCard] = [null, null];

}

+ (function shuffle() {

+ cards.forEach(card => {

+ let ramdomPos = Math.floor(Math.random() * 12);

+ card.style.order = ramdomPos;

+ });

+ })();

cards.forEach(card => card.addEventListener('click', flipCard));

完成了!

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值