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

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

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

项目结构

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

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。

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

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 ,这样就可以从原始位置移除元素,并使它们堆叠在一起。

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

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

翻转卡片

要在单击时翻转卡片,需要把一个 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;

}

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

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

.front-face {

transform: rotateY(180deg);

}

效果出来了!

匹配卡片

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

当点击第一张卡片时,需要等待另一张被翻转。 变量 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 :

+

+

+

+

+

+

+

+

+

+

+

+

这下就可以通过访问两个卡片的数据集来检查匹配了。 下面将匹配逻辑提取到它自己的方法 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();

锁定

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

先声明一个 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,从该卡上删除事件侦听器。

为了防止这种情况,需要检查当前点击的卡片是否等于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,从该卡上删除事件侦听器。

为了防止这种情况,需要检查当前点击的卡片是否等于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));

完成了!

html5翻卡片游戏,用 JavaScript 写一个卡片小游戏相关推荐

  1. 弹力细胞,一个由JavaScript写的网页小游戏

    弹力细胞 (BounceCell) 一个由JavaScript写的网页小游戏 作为大一菜鸟,这是我第一次比较正式的写文章 [害臊] 游戏玩法 通过鼠标或触屏控制屏幕底部的滑动弹板将发射的小球反弹出去撞 ...

  2. ES6 手写一个“辨色”小游戏

    1. 前言 依稀记得几年前朋友圈流行的辨色小游戏,找出颜色不同的矩形.前些天突发奇想,打算自己手写一个类似的游戏,话不多说,先上 Demo . --项目源码 本实例基于 ES6 实现,并兼容 ie9及 ...

  3. 教你前端如何用js写一个跑酷小游戏

    在线体验地址:http://summer.pkec.net/ 源码地址:https://gitee.com/ihope_top/juejin-summer 前言 不知不觉夏天又到了,提到夏天你们能想到 ...

  4. ChatGPT实现用C语言写一个扫雷小游戏

    前几天我们利用 ChatGPT实现用C语言写一个学生成绩管理系统 其过程用时不到30秒,速度惊人 今天又让ChatGPT用C语言写了一个扫雷小游戏,它的回答是:抱歉,我是AI语言模型,无法编写程序. ...

  5. python弹球小游戏程序设计_Python写的弹球小游戏

    原标题:Python写的弹球小游戏 Python 的功能强大应用广泛,从爬虫到 Web 开发,从科学计算到人工智能,都能见到它的身影.当然,Python 还可以编写游戏代码,虽然不是主流,但却十分有趣 ...

  6. python经典小游戏-用Python设计一个经典小游戏:猜大小

    码农那点事儿 关注我们,一起学习进步 本文主要介绍如何用Python设计一个经典小游戏:猜大小. 游戏规则: 初始本金是1000元,默认赔率是1倍,赢了,获得一倍金额,输了,扣除1倍金额. 玩家选择下 ...

  7. 20行python代码的入门级小游戏-用Python设计一个经典小游戏

    本文主要介绍如何用Python设计一个经典小游戏:猜大小. 在这个游戏中,将用到前面我介绍过的所有内容:变量的使用.参数传递.函数设计.条件控制和循环等,做个整体的总结和复习. 游戏规则: 初始本金是 ...

  8. python经典小游戏-用Python设计一个经典小游戏

    本文主要介绍如何用Python设计一个经典小游戏:猜大小. 在这个游戏中,将用到前面我介绍过的所有内容:变量的使用.参数传递.函数设计.条件控制和循环等,做个整体的总结和复习. 游戏规则: 初始本金是 ...

  9. python简单代码制作小游戏-用Python设计一个经典小游戏

    本文主要介绍如何用Python设计一个经典小游戏:猜大小. 在这个游戏中,将用到前面我介绍过的所有内容:变量的使用.参数传递.函数设计.条件控制和循环等,做个整体的总结和复习. 游戏规则: 初始本金是 ...

最新文章

  1. mysql 关联关系
  2. Apache下如何禁止指定目录运行PHP脚本
  3. android 静态list,android studio 利用gradle和cmakelist生成c++静态库.a的方法总结
  4. Eigrp恶意插入路由和致瘫***测试(四)
  5. 读者来信(1)——项目经理,不要迷信制度!
  6. nginx 不带www到www域名的重定向
  7. php调用文章标题,zblogphp标题格式的标签调用详细说明
  8. android 应用切换滑动,Android应用中利用ViewPager实现多页面滑动切换效果示例
  9. 表单在线生成 html代码,JSP实现用于自动生成表单标签html代码的自定义表单标签...
  10. 2021-06-27Date时间
  11. linux的视频格式转换软件,工具盘点:必备的Linux视频转换工具(1)
  12. Connectify让你的本本变成无线接入点
  13. ExtJs6学习(五)【Extjs MVC开发模式详解】
  14. 移动支付新趋势:短信支付「Pay by Text」
  15. 谷歌浏览器报错-应用程序正常初始化(0xc0000005)
  16. 非IE浏览器(谷歌、火狐、Edge)使用IE打开指定链接
  17. OkHttp Events事件
  18. 库存JAVA_Java解决高并发下商品库存更新
  19. 芯片数据分析步骤4 标准化-affy
  20. 乓乓响再度冲刺港股:来自临时及应急服务客户毛利率达70%

热门文章

  1. 【算法】剑指 Offer 36. 二叉搜索树与双向链表
  2. 【clickhouse】Port 9000 is for clickhouse-client program
  3. 【Kafka】kafka 监控指标项
  4. 【Java】5 个刁钻的 String 面试题
  5. 95-190-035-源码-window-Time Window 实现
  6. chrome 控制台 base64加密解密
  7. Springboot: Failed to get nested archive for entry BOOT-INF/lib/ Zip64 archives are not supported
  8. 【java】JDK8的HashMap
  9. 手机方舟官方服务器稳定吗,方舟生存进化手游选官服还是S服好?有什么区别...
  10. typora插入代码设置_五分钟快速上手神器Typora